{"_id":"@carl.fyi/text","name":"@carl.fyi/text","dist-tags":{"latest":"0.1.0"},"versions":{"0.1.0":{"name":"@carl.fyi/text","version":"0.1.0","description":"Make unruly text usable, from safe filenames and slugs to Markdown cleanup, keyword analysis and content ranking.","type":"module","sideEffects":false,"license":"MIT","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./contact":{"types":"./dist/contact.d.ts","import":"./dist/contact.js"},"./files":{"types":"./dist/files.d.ts","import":"./dist/files.js"},"./git":{"types":"./dist/git.d.ts","import":"./dist/git.js"},"./keywords":{"types":"./dist/keywords.d.ts","import":"./dist/keywords.js"},"./markdown-links":{"types":"./dist/markdown-links.d.ts","import":"./dist/markdown-links.js"},"./markdown-path":{"types":"./dist/markdown-path.d.ts","import":"./dist/markdown-path.js"},"./rich-text":{"types":"./dist/rich-text.d.ts","import":"./dist/rich-text.js"},"./relevance":{"types":"./dist/relevance.d.ts","import":"./dist/relevance.js"},"./text":{"types":"./dist/text.d.ts","import":"./dist/text.js"}},"gitHead":"8395ae726af204ee2590ca681a8076b73a31a8e7","_id":"@carl.fyi/text@0.1.0","_nodeVersion":"26.7.0","_npmVersion":"11.19.0","dist":{"integrity":"sha512-S03168bvGSv+iJewTB4YNv/ztnz/htl1F75TsnCDLbF3GorcP5OUzEBgURnpLuFR+9NVcgjeRbJK0Na8GOm7qw==","shasum":"9feb2ae482a660107de74c24af809822818a5db0","tarball":"https://registry.npmjs.org/@carl.fyi/text/-/text-0.1.0.tgz","fileCount":43,"unpackedSize":124615,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIF42GaiITiNygzx+0RjGCBCOmIJ6lF4M0J7UkUb+L60/AiEAj1HUYygKLk1jmHWr+Gan1YwrBUM4gAW2XtkBFsLsTIQ="}]},"_npmUser":{"name":"carlfairclough","email":"hi@carlfairclough.me"},"directories":{},"maintainers":[{"name":"carlfairclough","email":"hi@carlfairclough.me"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/text_0.1.0_1787903740433_0.4104341972476031"},"_hasShrinkwrap":false}},"time":{"created":"2026-08-28T07:55:40.236Z","0.1.0":"2026-08-28T07:55:40.572Z","modified":"2026-08-28T07:55:40.790Z"},"maintainers":[{"name":"carlfairclough","email":"hi@carlfairclough.me"}],"description":"Make unruly text usable, from safe filenames and slugs to Markdown cleanup, keyword analysis and content ranking.","license":"MIT","readme":"<!-- Generated from package.json and src TSDoc by scripts/generate-tool-documentation.mjs. Do not edit by hand. -->\n\n# @carl.fyi/text\n\nMake unruly text usable, from safe filenames and slugs to Markdown cleanup, keyword analysis and content ranking.\n\n[Interactive documentation](https://tools.carl.fyi/packages/text)\n\n## Install\n\n```sh\nnpm install @carl.fyi/text\n```\n\n## Quick start\n\n```ts\nimport { htmlToText } from \"@carl.fyi/text\";\n\nhtmlToText(\"<h1>Hello</h1><p>One <b>thing</b>.</p>\");\n// => \"Hello\\nOne thing.\"\n```\n\n## Entry points\n\n| Import                          | Purpose                |\n| ------------------------------- | ---------------------- |\n| `@carl.fyi/text`                | All public exports     |\n| `@carl.fyi/text/contact`        | Focused subpath import |\n| `@carl.fyi/text/files`          | Focused subpath import |\n| `@carl.fyi/text/git`            | Focused subpath import |\n| `@carl.fyi/text/keywords`       | Focused subpath import |\n| `@carl.fyi/text/markdown-links` | Focused subpath import |\n| `@carl.fyi/text/markdown-path`  | Focused subpath import |\n| `@carl.fyi/text/rich-text`      | Focused subpath import |\n| `@carl.fyi/text/relevance`      | Focused subpath import |\n| `@carl.fyi/text/text`           | Focused subpath import |\n\nAll entry points are ESM, side-effect-free, and include TypeScript declarations.\n\n## Public API\n\nThis is the complete export inventory. The detailed reference below mirrors the APIs demonstrated on the documentation website; its signatures, descriptions, defaults, errors, and examples are compiled from the package source.\n\n| Export                     | Kind      | Import from                     | Description                                                                   |\n| -------------------------- | --------- | ------------------------------- | ----------------------------------------------------------------------------- |\n| `extractMentions`          | function  | `@carl.fyi/text/contact`        | Extract unique mention names in their first-seen order.                       |\n| `isLikelyEmail`            | function  | `@carl.fyi/text/contact`        | Check whether a value has a plausible email-address shape.                    |\n| `MentionOptions`           | interface | `@carl.fyi/text/contact`        | Options controlling mention recognition in {@link extractMentions}.           |\n| `isSafeRelativePath`       | function  | `@carl.fyi/text/files`          | Determine whether a string is a traversal-free relative path.                 |\n| `joinPath`                 | function  | `@carl.fyi/text/files`          | Join safe path fragments while preserving an optional URL-style protocol.     |\n| `safeRelativePath`         | function  | `@carl.fyi/text/files`          | Return a trimmed relative path when safe, or `null` otherwise.                |\n| `SafeRelativePathOptions`  | interface | `@carl.fyi/text/files`          | Options controlling relative-path validation.                                 |\n| `sanitizeFileName`         | function  | `@carl.fyi/text/files`          | Turn an untrusted display name into a portable filename.                      |\n| `SanitizeFileNameOptions`  | interface | `@carl.fyi/text/files`          | Options controlling portable filename generation.                             |\n| `branchSlug`               | function  | `@carl.fyi/text/git`            | Convert free text into one safe Git branch-path segment.                      |\n| `BranchSlugOptions`        | interface | `@carl.fyi/text/git`            | Options controlling a single Git branch segment.                              |\n| `integrationBranchName`    | function  | `@carl.fyi/text/git`            | Create a namespaced integration branch for a session.                         |\n| `ticketBranchName`         | function  | `@carl.fyi/text/git`            | Build a predictable Git branch name from a ticket and title.                  |\n| `analyzeKeywordCoverage`   | function  | `@carl.fyi/text/keywords`       | Measure how many extracted reference keywords appear in comparison text.      |\n| `extractKeywords`          | function  | `@carl.fyi/text/keywords`       | Extract weighted keywords and recurring phrases from reference text.          |\n| `Keyword`                  | interface | `@carl.fyi/text/keywords`       | Public interface exported by this package.                                    |\n| `KeywordCoverageAnalysis`  | interface | `@carl.fyi/text/keywords`       | Public interface exported by this package.                                    |\n| `KeywordCoverageOptions`   | interface | `@carl.fyi/text/keywords`       | Public interface exported by this package.                                    |\n| `tokenizeForCoverage`      | function  | `@carl.fyi/text/keywords`       | Public function exported by this package.                                     |\n| `findMarkdownNoteLinks`    | function  | `@carl.fyi/text/markdown-links` | Find bare bracket note links while ignoring code, images, and standard links. |\n| `MarkdownNoteLink`         | interface | `@carl.fyi/text/markdown-links` | Location and label of a bare bracket note link.                               |\n| `MarkdownRewriteResult`    | interface | `@carl.fyi/text/markdown-links` | Result returned by Markdown note-link rewrite operations.                     |\n| `renameMarkdownNoteLinks`  | function  | `@carl.fyi/text/markdown-links` | Rename bare Markdown note links by file or folder path.                       |\n| `rewriteMarkdownNoteLinks` | function  | `@carl.fyi/text/markdown-links` | Rewrite bare note-link targets selected by a callback.                        |\n| `ensureMarkdownExtension`  | function  | `@carl.fyi/text/markdown-path`  | Add `.md` when a path has no case-insensitive Markdown extension.             |\n| `isMarkdownNotePath`       | function  | `@carl.fyi/text/markdown-path`  | Determine whether a value is a safe relative path ending in `.md`.            |\n| `markdownNoteTitle`        | function  | `@carl.fyi/text/markdown-path`  | Return a Markdown note's filename without its extension.                      |\n| `parentMarkdownPath`       | function  | `@carl.fyi/text/markdown-path`  | Return the parent portion of a safe Markdown path.                            |\n| `stripMarkdownExtension`   | function  | `@carl.fyi/text/markdown-path`  | Remove one case-insensitive `.md` extension from a path.                      |\n| `RankedRelevanceItem`      | interface | `@carl.fyi/text/relevance`      | Public interface exported by this package.                                    |\n| `rankRelatedContent`       | function  | `@carl.fyi/text/relevance`      | Rank related content with explainable tag, term, and diversity signals.       |\n| `RelevanceItem`            | interface | `@carl.fyi/text/relevance`      | Public interface exported by this package.                                    |\n| `searchableTerms`          | function  | `@carl.fyi/text/relevance`      | Public function exported by this package.                                     |\n| `escapeHtml`               | function  | `@carl.fyi/text/rich-text`      | Escape a string for safe placement in HTML text or quoted attributes.         |\n| `RichTextBlock`            | interface | `@carl.fyi/text/rich-text`      | A paragraph or heading in the compact rich-text AST.                          |\n| `RichTextContent`          | type      | `@carl.fyi/text/rich-text`      | Readonly block collection accepted by {@link richTextToHtml}.                 |\n| `RichTextInline`           | interface | `@carl.fyi/text/rich-text`      | A text leaf and its supported inline formatting.                              |\n| `richTextToHtml`           | function  | `@carl.fyi/text/rich-text`      | Render the package's compact rich-text AST as escaped HTML.                   |\n| `htmlToText`               | function  | `@carl.fyi/text/text`           | Convert an HTML fragment or document into readable plain text.                |\n| `markdownToText`           | function  | `@carl.fyi/text/text`           | Remove common Markdown formatting while retaining readable content.           |\n| `slugify`                  | function  | `@carl.fyi/text/text`           | Convert free text into a lower-case, URL-friendly slug.                       |\n| `SlugifyOptions`           | interface | `@carl.fyi/text/text`           | Options controlling {@link slugify}.                                          |\n\n# API reference\n\n## `@carl.fyi/text/text`\n\n### `htmlToText`\n\n```ts\nhtmlToText(html: string): string\n```\n\nConvert an HTML fragment or document into readable plain text.\n\nComments and non-content elements such as `script`, `style`, `noscript`, and `head` are removed. Common block elements become line boundaries and named or numeric entities are decoded. This is a text extractor, not an HTML sanitizer and not a replacement for a browser-grade HTML parser.\n\n| Parameter | Type     | Description                           |\n| --------- | -------- | ------------------------------------- |\n| `html`    | `string` | The HTML source to extract text from. |\n\n**Returns:** Trimmed plain text with useful paragraph and list boundaries.\n\n**Example**\n\n```ts\nimport { htmlToText } from \"@carl.fyi/text\";\n\nhtmlToText(\"<h1>Hello</h1><p>One <b>thing</b>.</p>\");\n// => \"Hello\\nOne thing.\"\n```\n\n## `@carl.fyi/text/text`\n\n### `markdownToText`\n\n```ts\nmarkdownToText(markdown: string): string\n```\n\nRemove common Markdown formatting while retaining readable content.\n\nThis predictable cleanup helper handles the most common block and inline markers. It is not a standards-complete CommonMark parser and does not aim to reproduce the rendering semantics of every Markdown extension.\n\n| Parameter  | Type     | Description                     |\n| ---------- | -------- | ------------------------------- |\n| `markdown` | `string` | The Markdown source to flatten. |\n\n**Returns:** Trimmed plain text with meaningful line breaks preserved.\n\n**Example**\n\n```ts\nimport { markdownToText } from \"@carl.fyi/text\";\n\nmarkdownToText(\"# Hello\\n\\n- **One** [link](/one)\");\n// => \"Hello\\n\\nOne link\"\n```\n\n## `@carl.fyi/text/text`\n\n### `slugify`\n\n```ts\nslugify(input: string, options?: SlugifyOptions): string\n```\n\nConvert free text into a lower-case, URL-friendly slug.\n\nThe input is compatibility-normalized, combining marks are removed, and unsupported character runs are collapsed into one separator. This utility deliberately produces an ASCII slug rather than transliterating every writing system.\n\n| Parameter                      | Type             | Description                                                                                   |\n| ------------------------------ | ---------------- | --------------------------------------------------------------------------------------------- |\n| `input`                        | `string`         | The title or phrase to normalize.                                                             |\n| `options (optional)`           | `SlugifyOptions` | Separator, length bound, and empty-input fallback.                                            |\n| `options.separator (optional)` | `string`         | The single character inserted between words. Default: `\"-\"`.                                  |\n| `options.maxLength (optional)` | `number`         | Maximum returned length. Must be a positive integer when supplied.                            |\n| `options.fallback (optional)`  | `string`         | Value returned when the input contains no supported letters or digits. Default: `\"untitled\"`. |\n\n**Returns:** The normalized slug, or the configured fallback when it is empty.\n\n**Throws**\n\n- `TypeError` if `separator` is not exactly one character.\n- `RangeError` if `maxLength` is not a positive integer.\n\n**Example**\n\n```ts\nimport { slugify } from \"@carl.fyi/text\";\n\nslugify(\"Déjà Vu — Again!\", { maxLength: 20 });\n// => \"deja-vu-again\"\n```\n\n## `@carl.fyi/text/files`\n\n### `sanitizeFileName`\n\n```ts\nsanitizeFileName(input: string, options?: SanitizeFileNameOptions): string\n```\n\nTurn an untrusted display name into a portable filename.\n\nNormalizes Unicode, replaces control and Windows-reserved characters, protects reserved device names, and removes trailing path separators, dots, and spaces. The function returns a name only: it never creates directories or writes files.\n\n| Parameter                        | Type                      | Description                                                             |\n| -------------------------------- | ------------------------- | ----------------------------------------------------------------------- |\n| `input`                          | `string`                  | The proposed filename without trusted path segments.                    |\n| `options (optional)`             | `SanitizeFileNameOptions` | Replacement, length, and fallback behaviour.                            |\n| `options.replacement (optional)` | `string`                  | Text used for invalid characters and whitespace. Default: `\"-\"`.        |\n| `options.maxLength (optional)`   | `number`                  | Maximum number of UTF-16 code units retained. Default: `255`.           |\n| `options.fallback (optional)`    | `string`                  | Name used when sanitization removes all content. Default: `\"untitled\"`. |\n\n**Returns:** A non-empty portable filename unless an empty fallback is explicit.\n\n**Throws**\n\n- `TypeError` if `replacement` contains a slash, backslash, or null.\n\n**Example**\n\n```ts\nimport { sanitizeFileName } from \"@carl.fyi/text\";\n\nsanitizeFileName(\"Q3/Q4: final?.pdf\");\n// => \"Q3-Q4-final-.pdf\"\n```\n\n### `safeRelativePath`\n\n```ts\nsafeRelativePath(input: string, options?: SafeRelativePathOptions): string | null\n```\n\nReturn a trimmed relative path when safe, or `null` otherwise.\n\nThis applies the same shape checks as `isSafeRelativePath` after trimming surrounding whitespace. It does not resolve the path or prove containment inside a filesystem root.\n\n| Parameter                        | Type                      | Description                                                        |\n| -------------------------------- | ------------------------- | ------------------------------------------------------------------ |\n| `input`                          | `string`                  | The candidate relative path.                                       |\n| `options (optional)`             | `SafeRelativePathOptions` | Set `allowHidden` to accept dot-prefixed segments.                 |\n| `options.allowHidden (optional)` | `boolean`                 | Whether dot-prefixed path segments are accepted. Default: `false`. |\n\n**Returns:** The trimmed path when `isSafeRelativePath` accepts it; otherwise `null`.\n\n**Example**\n\n```ts\nimport { safeRelativePath } from \"@carl.fyi/text\";\n\nsafeRelativePath(\" notes/plan.md \");\n// => \"notes/plan.md\"\n```\n\n## `@carl.fyi/text/git`\n\n### `ticketBranchName`\n\n```ts\nticketBranchName(ticketIdentifier: string, ticketTitle: string, prefix?: string): string\n```\n\nBuild a predictable Git branch name from a ticket and title.\n\nThe identifier is sanitized, the title is processed with `branchSlug`, and boundary slashes are removed from the prefix. Existence is not checked.\n\n| Parameter           | Type     | Description                                     |\n| ------------------- | -------- | ----------------------------------------------- |\n| `ticketIdentifier`  | `string` | Ticket key such as `CF-123`.                    |\n| `ticketTitle`       | `string` | Free text appended as a normalized branch slug. |\n| `prefix (optional)` | `string` | Branch namespace. Default: `\"agent\"`.           |\n\n**Returns:** A branch such as `agent/CF-123-add-search`.\n\n**Example**\n\n```ts\nimport { ticketBranchName } from \"@carl.fyi/text\";\n\nticketBranchName(\"CF 123\", \"Add URL parser\");\n// => \"agent/CF-123-add-url-parser\"\n```\n\n### `branchSlug`\n\n```ts\nbranchSlug(text: string, options?: BranchSlugOptions | number): string\n```\n\nConvert free text into one safe Git branch-path segment.\n\nAccents are folded, unsupported runs become hyphens, and boundary hyphens are removed. Passing a number as `options` is shorthand for `maxLength`.\n\n| Parameter                      | Type                          | Description                                               |\n| ------------------------------ | ----------------------------- | --------------------------------------------------------- |\n| `text`                         | `string`                      | The text to normalize.                                    |\n| `options (optional)`           | `BranchSlugOptions \\| number` | Length and fallback behaviour, or a numeric length limit. |\n| `options.maxLength (optional)` | `number`                      | Positive maximum segment length. Default: `40`.           |\n| `options.fallback (optional)`  | `string`                      | Text to normalize if the primary input becomes empty.     |\n\n**Returns:** A lower-case ASCII segment, or an empty string if no value remains.\n\n**Throws**\n\n- `RangeError` if `maxLength` is not a positive integer.\n\n**Example**\n\n```ts\nimport { branchSlug } from \"@carl.fyi/text\";\n\nbranchSlug(\"Déjà vu / shipped!\", { maxLength: 24 });\n// => \"deja-vu-shipped\"\n```\n\n## `@carl.fyi/text/files`\n\n### `joinPath`\n\n```ts\njoinPath(parentPath: string, name: string): string\n```\n\nJoin safe path fragments while preserving an optional URL-style protocol.\n\nBoundary slashes are removed before joining. After an optional `scheme://` prefix is removed, the result must pass `isSafeRelativePath`. Dot segments are rejected rather than resolved.\n\n| Parameter    | Type     | Description                                |\n| ------------ | -------- | ------------------------------------------ |\n| `parentPath` | `string` | The parent path or protocol-prefixed base. |\n| `name`       | `string` | The child name or path fragment.           |\n\n**Returns:** The normalized joined path with its protocol prefix preserved.\n\n**Throws**\n\n- `TypeError` if the joined value is not a safe relative path.\n\n**Example**\n\n```ts\nimport { joinPath } from \"@carl.fyi/text\";\n\njoinPath(\"https://example.com/projects/\", \"/tools.md/\");\n// => \"https://example.com/projects/tools.md\"\n```\n\n## `@carl.fyi/text/markdown-links`\n\n### `renameMarkdownNoteLinks`\n\n```ts\nrenameMarkdownNoteLinks(content: string, fromPath: string, toPath: string, isFolder?: boolean): MarkdownRewriteResult\n```\n\nRename bare Markdown note links by file or folder path.\n\n`.md` extensions are ignored for matching. File mode rewrites exact targets; folder mode rewrites descendants beginning with the source folder.\n\n| Parameter             | Type      | Description                                                  |\n| --------------------- | --------- | ------------------------------------------------------------ |\n| `content`             | `string`  | Markdown containing bracket-style note links.                |\n| `fromPath`            | `string`  | Existing note or folder path, optionally ending in `.md`.    |\n| `toPath`              | `string`  | Replacement note or folder path, optionally ending in `.md`. |\n| `isFolder (optional)` | `boolean` | Whether to rewrite descendant paths. Default: `false`.       |\n\n**Returns:** Rewritten content and change metadata.\n\n**Example**\n\n```ts\nimport { renameMarkdownNoteLinks } from \"@carl.fyi/text\";\n\nrenameMarkdownNoteLinks(\"See [notes/old]\", \"notes/old.md\", \"notes/new.md\");\n// => { value: \"See [notes/new]\", changed: true, replacements: 1 }\n```\n\n## `@carl.fyi/text/rich-text`\n\n### `richTextToHtml`\n\n```ts\nrichTextToHtml(content: RichTextContent): string\n```\n\nRender the package's compact rich-text AST as escaped HTML.\n\nText and link attributes are escaped. Only absolute HTTP and HTTPS links are emitted; invalid or unsafe destinations leave the formatted text unlinked. The returned HTML is safe for this constrained AST, not arbitrary HTML input.\n\n| Parameter                                | Type                         | Description                                                       |\n| ---------------------------------------- | ---------------------------- | ----------------------------------------------------------------- |\n| `content`                                | `RichTextContent`            | Paragraph and heading blocks containing inline text nodes.        |\n| `content[].type`                         | `\"paragraph\" \\| \"heading\"`   | Element family emitted for the block.                             |\n| `content[].level (optional)`             | `1 \\| 2 \\| 3 \\| 4 \\| 5 \\| 6` | Heading rank. Used only when `type` is `\"heading\"`. Default: `2`. |\n| `content[].children`                     | `readonly RichTextInline[]`  | Inline nodes rendered in source order.                            |\n| `content[].children[].text`              | `string`                     | Literal text content. It is always HTML-escaped while rendering.  |\n| `content[].children[].bold (optional)`   | `boolean`                    | Wrap the text in `strong`.                                        |\n| `content[].children[].italic (optional)` | `boolean`                    | Wrap the text in `em`.                                            |\n| `content[].children[].code (optional)`   | `boolean`                    | Wrap the text in `code`.                                          |\n| `content[].children[].link (optional)`   | `string`                     | Optional absolute HTTP or HTTPS destination.                      |\n\n**Returns:** An HTML string containing the requested safe structure and styles.\n\n**Example**\n\n```ts\nimport { richTextToHtml } from \"@carl.fyi/text\";\n\nrichTextToHtml([{ type: \"paragraph\", children: [{ text: \"Hello\", bold: true }] }]);\n// => \"<p><strong>Hello</strong></p>\"\n```\n\n## `@carl.fyi/text/contact`\n\n### `isLikelyEmail`\n\n```ts\nisLikelyEmail(value: unknown): value is string\n```\n\nCheck whether a value has a plausible email-address shape.\n\nThis is intentionally a lightweight UI validation check. It does not prove that the mailbox exists and should not replace an address-confirmation flow.\n\n| Parameter | Type      | Description                   |\n| --------- | --------- | ----------------------------- |\n| `value`   | `unknown` | The unknown value to inspect. |\n\n**Returns:** `true` when the value is a string with a plausible local part, `@`, and dotted domain.\n\n**Example**\n\n```ts\nimport { isLikelyEmail } from \"@carl.fyi/text\";\n\nisLikelyEmail(\"carl@example.com\"); // true\n```\n\n### `extractMentions`\n\n```ts\nextractMentions(text: string, options?: MentionOptions): string[]\n```\n\nExtract unique mention names in their first-seen order.\n\nA mention must begin at the start of the input or immediately after whitespace. Names are lower-cased and deduplicated by default. Returned values do not include the prefix.\n\n| Parameter                          | Type             | Description                                                       |\n| ---------------------------------- | ---------------- | ----------------------------------------------------------------- |\n| `text`                             | `string`         | The text to scan.                                                 |\n| `options (optional)`               | `MentionOptions` | Prefix and name-matching behaviour.                               |\n| `options.prefix (optional)`        | `string`         | Single character placed before each mention. Default: `\"@\"`.      |\n| `options.allowDots (optional)`     | `boolean`        | Allow dots inside mention names. Default: `false`.                |\n| `options.caseSensitive (optional)` | `boolean`        | Preserve case and deduplicate case-sensitively. Default: `false`. |\n\n**Returns:** Mention names in their first-seen order.\n\n**Throws**\n\n- `TypeError` if `prefix` is not exactly one character.\n\n**Example**\n\n```ts\nimport { extractMentions } from \"@carl.fyi/text\";\n\nextractMentions(\"Hello @Ada and @grace_hopper, then @ada\");\n// => [\"ada\", \"grace_hopper\"]\n```\n\n## `@carl.fyi/text/keywords`\n\n### `extractKeywords`\n\n```ts\nextractKeywords(referenceText: string, options?: KeywordCoverageOptions): Keyword[]\n```\n\nExtract weighted keywords and recurring phrases from reference text.\n\nComparison is case-insensitive, common stopwords and standalone numbers are removed, and recurring phrases replace their component words.\n\n| Parameter                           | Type                     | Description                                                     |\n| ----------------------------------- | ------------------------ | --------------------------------------------------------------- |\n| `referenceText`                     | `string`                 | Source text from which keywords are derived.                    |\n| `options (optional)`                | `KeywordCoverageOptions` | Result limit, phrase weighting, and extra stopwords.            |\n| `options.maxKeywords (optional)`    | `number`                 | Maximum number of terms and phrases to retain. Default: `25`.   |\n| `options.phraseWeight (optional)`   | `number`                 | Multiplier applied to recurring two-word phrases. Default: `2`. |\n| `options.extraStopwords (optional)` | `Iterable<string>`       | Additional case-insensitive words to exclude. Default: `[]`.    |\n\n**Returns:** Keywords sorted by descending weight and then alphabetically.\n\n**Example**\n\n```ts\nimport { extractKeywords } from \"@carl.fyi/text\";\n\nconst keywords = extractKeywords(\"Design tokens make design systems consistent.\", { maxKeywords: 10 });\n```\n\n### `analyzeKeywordCoverage`\n\n```ts\nanalyzeKeywordCoverage(referenceText: string, comparisonText: string, options?: KeywordCoverageOptions): KeywordCoverageAnalysis\n```\n\nMeasure how many extracted reference keywords appear in comparison text.\n\nPhrases require adjacent tokens while single words require exact token coverage. An empty keyword set reports 100% coverage.\n\n| Parameter                           | Type                     | Description                                                     |\n| ----------------------------------- | ------------------------ | --------------------------------------------------------------- |\n| `referenceText`                     | `string`                 | Text that defines the important terms.                          |\n| `comparisonText`                    | `string`                 | Text checked for keyword coverage.                              |\n| `options (optional)`                | `KeywordCoverageOptions` | Extraction limit, phrase weighting, and extra stopwords.        |\n| `options.maxKeywords (optional)`    | `number`                 | Maximum number of terms and phrases to retain. Default: `25`.   |\n| `options.phraseWeight (optional)`   | `number`                 | Multiplier applied to recurring two-word phrases. Default: `2`. |\n| `options.extraStopwords (optional)` | `Iterable<string>`       | Additional case-insensitive words to exclude. Default: `[]`.    |\n\n**Returns:** Coverage percentage, covered and missing keywords, the full keyword set, and a readable summary.\n\n**Example**\n\n```ts\nimport { analyzeKeywordCoverage } from \"@carl.fyi/text\";\n\nconst analysis = analyzeKeywordCoverage(\n  \"Design tokens support consistent interfaces.\",\n  \"Use design tokens across the interface.\",\n);\n```\n\n## `@carl.fyi/text/relevance`\n\n### `rankRelatedContent`\n\n```ts\nrankRelatedContent<T extends RelevanceItem>(current: RelevanceItem, candidates: readonly T[], options?: { limit?: number; tagWeight?: number; termWeight?: number; differentKindBoost?: number; }): RankedRelevanceItem<T>[]\n```\n\nRank related content with explainable tag, term, and diversity signals.\n\nThe current item is excluded, ties preserve candidate order, and every result reports the exact tags and terms that contributed.\n\n| Parameter                               | Type                                                                                                                                                                                                                                                                                                                                                                                       | Description                                                       |\n| --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------- |\n| `current`                               | `RelevanceItem`                                                                                                                                                                                                                                                                                                                                                                            | Content item that establishes the matching context.               |\n| `current.id`                            | `string`                                                                                                                                                                                                                                                                                                                                                                                   | Stable identifier used to exclude the current item.               |\n| `current.title`                         | `string`                                                                                                                                                                                                                                                                                                                                                                                   | Human-readable content title.                                     |\n| `current.description (optional)`        | `string`                                                                                                                                                                                                                                                                                                                                                                                   | Optional summary included in term matching.                       |\n| `current.tags (optional)`               | `readonly string[]`                                                                                                                                                                                                                                                                                                                                                                        | Optional explicit tags, matched case-insensitively.               |\n| `current.kind (optional)`               | `string`                                                                                                                                                                                                                                                                                                                                                                                   | Optional content kind used for diversity boosting.                |\n| `candidates`                            | `readonly T[]`                                                                                                                                                                                                                                                                                                                                                                             | Potential related items in stable tie-break order.                |\n| `candidates[].id`                       | `string`                                                                                                                                                                                                                                                                                                                                                                                   | Stable identifier used to exclude the current item.               |\n| `candidates[].title`                    | `string`                                                                                                                                                                                                                                                                                                                                                                                   | Human-readable content title.                                     |\n| `candidates[].description (optional)`   | `string`                                                                                                                                                                                                                                                                                                                                                                                   | Optional summary included in term matching.                       |\n| `candidates[].tags (optional)`          | `readonly string[]`                                                                                                                                                                                                                                                                                                                                                                        | Optional explicit tags, matched case-insensitively.               |\n| `candidates[].kind (optional)`          | `string`                                                                                                                                                                                                                                                                                                                                                                                   | Optional content kind used for diversity boosting.                |\n| `options (optional)`                    | ``{     /** Maximum results returned. @defaultValue `3` */     limit?: number;     /** Points for each matching tag. @defaultValue `6` */     tagWeight?: number;     /** Points for each matching significant term. @defaultValue `2` */     termWeight?: number;     /** Points for a candidate of a different content kind. @defaultValue `1` */     differentKindBoost?: number;   }`` | Result limit and scoring weights.                                 |\n| `options.limit (optional)`              | `number`                                                                                                                                                                                                                                                                                                                                                                                   | Maximum results returned. Default: `3`.                           |\n| `options.tagWeight (optional)`          | `number`                                                                                                                                                                                                                                                                                                                                                                                   | Points for each matching tag. Default: `6`.                       |\n| `options.termWeight (optional)`         | `number`                                                                                                                                                                                                                                                                                                                                                                                   | Points for each matching significant term. Default: `2`.          |\n| `options.differentKindBoost (optional)` | `number`                                                                                                                                                                                                                                                                                                                                                                                   | Points for a candidate of a different content kind. Default: `1`. |\n\n**Returns:** Ranked candidates with integer scores and matching evidence.\n\n**Example**\n\n```ts\nimport { rankRelatedContent } from \"@carl.fyi/text\";\n\nconst related = rankRelatedContent(\n  { id: \"current\", title: \"Design tokens\", tags: [\"design\"] },\n  [{ id: \"system\", title: \"Design systems\", tags: [\"design\"] }],\n  { limit: 3 },\n);\n```\n\n## License\n\n[MIT](./LICENSE)\n","readmeFilename":"README.md","_rev":"1-87f1cc4698dbb62a863d65dee4e8a127"}