@wix/web5-core 1.59.0 → 1.60.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/dist/cjs/component/componentDefinitions/ComparisonSectionDefinition.js +33 -1
  2. package/dist/cjs/component/componentDefinitions/ComparisonSectionDefinition.js.map +1 -1
  3. package/dist/cjs/component/componentDefinitions/FeatureSection9PlusDefinition.js +9 -5
  4. package/dist/cjs/component/componentDefinitions/FeatureSection9PlusDefinition.js.map +1 -1
  5. package/dist/cjs/component/componentDefinitions/listItemsSectionDefinition.js +10 -5
  6. package/dist/cjs/component/componentDefinitions/listItemsSectionDefinition.js.map +1 -1
  7. package/dist/cjs/component/componentDefinitions/parse-utils.js +45 -8
  8. package/dist/cjs/component/componentDefinitions/parse-utils.js.map +1 -1
  9. package/dist/cjs/component/componentParser.js +15 -0
  10. package/dist/cjs/component/componentParser.js.map +1 -1
  11. package/dist/cjs/component/section-definition.js +14 -0
  12. package/dist/cjs/component/section-definition.js.map +1 -1
  13. package/dist/cjs/components/ui/MarkdownText.js +41 -42
  14. package/dist/cjs/components/ui/MarkdownText.js.map +1 -1
  15. package/dist/cjs/components/ui/UnifiedLink.js +22 -10
  16. package/dist/cjs/components/ui/UnifiedLink.js.map +1 -1
  17. package/dist/cjs/index.js +5 -3
  18. package/dist/cjs/index.js.map +1 -1
  19. package/dist/cjs/utils/markdownPreprocessor.js +162 -8
  20. package/dist/cjs/utils/markdownPreprocessor.js.map +1 -1
  21. package/dist/esm/component/componentDefinitions/ComparisonSectionDefinition.js +34 -1
  22. package/dist/esm/component/componentDefinitions/ComparisonSectionDefinition.js.map +1 -1
  23. package/dist/esm/component/componentDefinitions/FeatureSection9PlusDefinition.js +10 -6
  24. package/dist/esm/component/componentDefinitions/FeatureSection9PlusDefinition.js.map +1 -1
  25. package/dist/esm/component/componentDefinitions/listItemsSectionDefinition.js +11 -6
  26. package/dist/esm/component/componentDefinitions/listItemsSectionDefinition.js.map +1 -1
  27. package/dist/esm/component/componentDefinitions/parse-utils.js +44 -8
  28. package/dist/esm/component/componentDefinitions/parse-utils.js.map +1 -1
  29. package/dist/esm/component/componentParser.js +15 -0
  30. package/dist/esm/component/componentParser.js.map +1 -1
  31. package/dist/esm/component/section-definition.js +13 -1
  32. package/dist/esm/component/section-definition.js.map +1 -1
  33. package/dist/esm/components/ui/MarkdownText.js +8 -9
  34. package/dist/esm/components/ui/MarkdownText.js.map +1 -1
  35. package/dist/esm/components/ui/UnifiedLink.js +7 -1
  36. package/dist/esm/components/ui/UnifiedLink.js.map +1 -1
  37. package/dist/esm/index.js +2 -0
  38. package/dist/esm/index.js.map +1 -1
  39. package/dist/esm/utils/markdownPreprocessor.js +160 -8
  40. package/dist/esm/utils/markdownPreprocessor.js.map +1 -1
  41. package/dist/types/component/componentDefinitions/ComparisonSectionDefinition.d.ts +2 -2
  42. package/dist/types/component/componentDefinitions/ComparisonSectionDefinition.d.ts.map +1 -1
  43. package/dist/types/component/componentDefinitions/FeatureSection9PlusDefinition.d.ts.map +1 -1
  44. package/dist/types/component/componentDefinitions/listItemsSectionDefinition.d.ts.map +1 -1
  45. package/dist/types/component/componentDefinitions/parse-utils.d.ts +10 -0
  46. package/dist/types/component/componentDefinitions/parse-utils.d.ts.map +1 -1
  47. package/dist/types/component/componentParser.d.ts.map +1 -1
  48. package/dist/types/component/section-definition.d.ts +13 -1
  49. package/dist/types/component/section-definition.d.ts.map +1 -1
  50. package/dist/types/components/ui/MarkdownText.d.ts +13 -0
  51. package/dist/types/components/ui/MarkdownText.d.ts.map +1 -1
  52. package/dist/types/components/ui/UnifiedLink.d.ts +1 -1
  53. package/dist/types/components/ui/UnifiedLink.d.ts.map +1 -1
  54. package/dist/types/index.d.ts +2 -1
  55. package/dist/types/index.d.ts.map +1 -1
  56. package/dist/types/utils/markdownPreprocessor.d.ts +30 -5
  57. package/dist/types/utils/markdownPreprocessor.d.ts.map +1 -1
  58. package/package.json +2 -2
@@ -1 +1 @@
1
- {"version":3,"names":["escapeWeb5Links","markdown","replace","match","linkText","protocol","url","decodedUrl","decodeURIComponent","encodedUrl","encodeURI","fixMalformedLinks","recoverOrphanedEmphasisLinks","diagnostics","result","prefix","href","normalized","trim","push","type","original","trimTrailingWhitespace","lines","split","processedLines","line","trimmedLine","length","trimEnd","join","normalizeIconUrls","rest","spaceName","cleanName","isFenceToggle","test","splitPipeRow","trimmed","startsWith","endsWith","slice","map","cell","isPipeRow","isTableDelimiterRow","cells","every","insertMissingTableSeparators","inFence","i","previousLine","nextLine","headerCells","nextCells","separator","decodeLinkText","decoded","preprocessMarkdown","recoveredLinks","recoveredLinkDiagnostics","processed","decodedText","decodeDiagnostics","iconDiagnostics","tableNormalized","tableDiagnostics"],"sources":["../../../src/utils/markdownPreprocessor.ts"],"sourcesContent":["/**\n * Markdown preprocessing utilities\n *\n * These functions fix common markdown formatting issues before AST parsing.\n * They are applied in a specific order to ensure proper processing.\n */\n\nexport interface PreprocessorDiagnostic {\n type: string;\n original: string;\n normalized: string;\n}\n\nexport interface PreprocessResult {\n markdown: string;\n diagnostics: PreprocessorDiagnostic[];\n}\n\n/**\n * Escape web5:// links by URL-encoding the URL part\n * e.g. [text](web5://some text with spaces) → [text](web5://some%20text%20with%20spaces)\n *\n * @param markdown - The markdown string to process\n * @returns The markdown string with web5:// URLs properly encoded\n */\nexport function escapeWeb5Links(markdown: string): string {\n // Match markdown links with web5:// protocol\n // Pattern: [text](web5://...)\n return markdown.replace(\n /\\[([^\\]]+)\\]\\((web5|entity|image):\\/\\/([^)]+)\\)/g,\n (match, linkText, protocol, url) => {\n try {\n // Decode first to avoid double-encoding (e.g. %20 → %2520)\n const decodedUrl = decodeURIComponent(url);\n // Use encodeURI (not encodeURIComponent) to preserve URL structure\n // characters like /, ?, &, = while encoding spaces and unsafe chars\n const encodedUrl = encodeURI(decodedUrl);\n return `[${linkText}](${protocol}://${encodedUrl})`;\n } catch {\n // decodeURIComponent throws on a malformed percent sequence (raw `%`\n // or truncated `%X`), encodeURI on a lone surrogate. During streaming\n // this happens when chunks are concatenated mid-escape or a multi-byte\n // char is split. Leave the link untouched rather than throw — an\n // uncaught error here aborts the whole streaming parse and surfaces as\n // a 908 conversation-streaming-error even though the page rendered.\n return match;\n }\n },\n );\n}\n\n/**\n * Fix malformed markdown links with whitespace (spaces, tabs, newlines) between ] and (\n * e.g. [text] (url) → [text](url)\n * e.g. [text]\\n(url) → [text](url)\n *\n * @param markdown - The markdown string to process\n * @returns The markdown string with fixed link formatting\n */\nexport function fixMalformedLinks(markdown: string): string {\n // \\s matches spaces, tabs, and newlines; + means one or more\n // Also handle cases with newlines explicitly\n return markdown.replace(/\\][\\s\\r\\n]+\\(/g, '](');\n}\n\n/**\n * Recover inline CTA chips where the LLM emitted emphasized text followed by a\n * dangling markdown link close, e.g. `**label →**](web5://ask/query)`.\n */\nexport function recoverOrphanedEmphasisLinks(markdown: string): {\n result: string;\n diagnostics: PreprocessorDiagnostic[];\n} {\n const diagnostics: PreprocessorDiagnostic[] = [];\n const result = markdown.replace(\n /(^|[^\\[])\\*\\*([^\\n\\]*]+?)\\*\\*\\]\\((web5:\\/\\/(?:ask|action)\\/[^)]+)\\)/g,\n (match, prefix: string, linkText: string, href: string) => {\n const normalized = `${prefix}[${linkText.trim()}](${href})`;\n diagnostics.push({\n type: 'orphaned_emphasis_link_recovered',\n original: match,\n normalized,\n });\n return normalized;\n },\n );\n return { result, diagnostics };\n}\n\n/**\n * Trim trailing whitespace from lines that contain text\n *\n * @param markdown - The markdown string to process\n * @returns The markdown string with trailing whitespace trimmed from text lines\n */\nexport function trimTrailingWhitespace(markdown: string): string {\n const lines = markdown.split('\\n');\n const processedLines: string[] = [];\n\n for (const line of lines) {\n const trimmedLine = line.trim();\n\n // If the line has content, trim trailing whitespace (keep leading spaces for indentation)\n if (trimmedLine.length > 0) {\n processedLines.push(line.trimEnd());\n } else {\n // Lines with only whitespace become empty lines\n processedLines.push('');\n }\n }\n\n return processedLines.join('\\n');\n}\n\n/**\n * Normalize malformed web5://icon URLs to the canonical web5://icon/{name} format.\n * Handles variants like web5://icon:arrow, web5://icon-arrow, web5://icon arrow → web5://icon/arrow.\n * Only alphanumeric characters and dashes are kept in the icon name.\n */\nexport function normalizeIconUrls(markdown: string): {\n result: string;\n diagnostics: PreprocessorDiagnostic[];\n} {\n const diagnostics: PreprocessorDiagnostic[] = [];\n const result = markdown.replace(\n /web5:\\/\\/icon(?:([:/-][^)\\s]+)|(?:\\s+([a-zA-Z0-9-]+)))/g,\n (match, rest: string | undefined, spaceName: string | undefined) => {\n const cleanName = (spaceName ?? rest ?? '')\n .replace(/[^a-zA-Z0-9-]/g, '')\n .replace(/^-+|-+$/g, '');\n const normalized = `web5://icon/${cleanName}`;\n if (match !== normalized) {\n diagnostics.push({\n type: 'icon_url_normalized',\n original: match,\n normalized,\n });\n }\n return normalized;\n },\n );\n return { result, diagnostics };\n}\n\nfunction isFenceToggle(line: string): boolean {\n return /^\\s*(`{3,}|~{3,})/.test(line);\n}\n\nfunction splitPipeRow(line: string): string[] {\n const trimmed = line.trim();\n if (!trimmed.startsWith('|') || !trimmed.endsWith('|')) {\n return [];\n }\n return trimmed\n .slice(1, -1)\n .split('|')\n .map((cell) => cell.trim());\n}\n\nfunction isPipeRow(line: string): boolean {\n return splitPipeRow(line).length >= 2;\n}\n\nfunction isTableDelimiterRow(line: string): boolean {\n const cells = splitPipeRow(line);\n return (\n cells.length >= 2 &&\n cells.every((cell) => /^:?-{3,}:?$/.test(cell.replace(/\\s+/g, '')))\n );\n}\n\n/**\n * Repair LLM-emitted pipe tables that omit the GFM delimiter row.\n * Without this line remark-gfm parses the rows as plain paragraphs, so\n * registry sections such as comparison never receive a `table` part.\n */\nexport function insertMissingTableSeparators(markdown: string): {\n result: string;\n diagnostics: PreprocessorDiagnostic[];\n} {\n const diagnostics: PreprocessorDiagnostic[] = [];\n const lines = markdown.split('\\n');\n const result: string[] = [];\n let inFence = false;\n\n for (let i = 0; i < lines.length; i++) {\n const line = lines[i];\n const previousLine = result[result.length - 1] ?? '';\n const nextLine = lines[i + 1] ?? '';\n\n if (isFenceToggle(line)) {\n inFence = !inFence;\n result.push(line);\n continue;\n }\n\n result.push(line);\n\n if (inFence || !isPipeRow(line) || isPipeRow(previousLine)) {\n continue;\n }\n if (!isPipeRow(nextLine) || isTableDelimiterRow(nextLine)) {\n continue;\n }\n\n const headerCells = splitPipeRow(line);\n const nextCells = splitPipeRow(nextLine);\n if (headerCells.length !== nextCells.length) {\n continue;\n }\n\n const separator = `| ${headerCells.map(() => '---').join(' | ')} |`;\n result.push(separator);\n diagnostics.push({\n type: 'table_separator_inserted',\n original: line,\n normalized: `${line}\\n${separator}`,\n });\n }\n\n return { result: result.join('\\n'), diagnostics };\n}\n\n/**\n * Decode URL-encoded text in markdown link labels.\n * e.g. [Some%20Text](https://example.com) → [Some Text](https://example.com)\n * e.g. [Which+solution%3F](web5://...) → [Which solution?](web5://...)\n *\n * Only decodes the text portion [text], never the URL portion (url).\n * Handles both `+` (form-encoded space) and `%XX` escapes; falls back to\n * the original string on malformed percent sequences.\n */\nexport function decodeLinkText(markdown: string): {\n result: string;\n diagnostics: PreprocessorDiagnostic[];\n} {\n const diagnostics: PreprocessorDiagnostic[] = [];\n const result = markdown.replace(\n /\\[([^\\]]+)\\]\\(/g,\n (match, linkText: string) => {\n try {\n const decoded = decodeURIComponent(linkText.replace(/\\+/g, '%20'));\n if (decoded !== linkText) {\n diagnostics.push({\n type: 'link_text_decoded',\n original: linkText,\n normalized: decoded,\n });\n }\n return `[${decoded}](`;\n } catch {\n // If decodeURIComponent fails (e.g. malformed percent sequence like %ZZ),\n // leave the text unchanged\n return match;\n }\n },\n );\n return { result, diagnostics };\n}\n\n/**\n * Preprocess markdown to fix common formatting issues before AST parsing.\n *\n * The preprocessing steps are applied in the following order:\n * 1. Recover orphaned emphasized web5 links\n * 2. Fix malformed links (remove whitespace between ] and () - must happen before web5 links are matched)\n * 3. Decode URL-encoded link text\n * 4. Escape web5:// links (URL-encode URLs)\n * 5. Normalize icon URLs\n * 6. Insert missing table separators for obvious pipe tables\n * 7. Trim trailing whitespace from lines\n *\n * @param markdown - The raw markdown string to preprocess\n * @returns The preprocessed markdown string ready for AST parsing\n */\nexport function preprocessMarkdown(markdown: string): PreprocessResult {\n const { result: recoveredLinks, diagnostics: recoveredLinkDiagnostics } =\n recoverOrphanedEmphasisLinks(markdown);\n let processed = fixMalformedLinks(recoveredLinks);\n const { result: decodedText, diagnostics: decodeDiagnostics } =\n decodeLinkText(processed);\n processed = escapeWeb5Links(decodedText);\n const { result: normalized, diagnostics: iconDiagnostics } =\n normalizeIconUrls(processed);\n const { result: tableNormalized, diagnostics: tableDiagnostics } =\n insertMissingTableSeparators(normalized);\n processed = trimTrailingWhitespace(tableNormalized);\n return {\n markdown: processed,\n diagnostics: [\n ...recoveredLinkDiagnostics,\n ...decodeDiagnostics,\n ...iconDiagnostics,\n ...tableDiagnostics,\n ],\n };\n}\n"],"mappings":"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","ignoreList":[]}
1
+ {"version":3,"names":["escapeWeb5Links","markdown","replace","match","linkText","protocol","url","decodedUrl","decodeURIComponent","encodedUrl","encodeURI","fixMalformedLinks","recoverOrphanedEmphasisLinks","diagnostics","result","prefix","href","normalized","trim","push","type","original","ORPHANED_WEB5_TARGET","ANY_WEB5_URL","MAX_RECOVERED_LABEL_WORDS","LABEL_BOUNDARY_WORDS","Set","hasOpenLinkBracket","segment","lastIndexOf","splitRecoveredLabel","run","sentenceStart","boundary","matchAll","index","length","tokens","slice","map","token","word","start","window","i","bare","toLowerCase","has","label","repairOrphanedWeb5Links","offset","whole","lineStart","scrubVisibleWeb5Urls","trimTrailingWhitespace","lines","split","processedLines","line","trimmedLine","trimEnd","join","normalizeIconUrls","rest","spaceName","cleanName","isFenceToggle","test","splitPipeRow","trimmed","startsWith","endsWith","cell","isPipeRow","isTableDelimiterRow","cells","every","insertMissingTableSeparators","inFence","previousLine","nextLine","headerCells","nextCells","separator","decodeLinkText","decoded","preprocessMarkdown","recoveredLinks","recoveredLinkDiagnostics","processed","repairedLinks","repairedLinkDiagnostics","decodedText","decodeDiagnostics","iconDiagnostics","tableNormalized","tableDiagnostics","scrubbed","scrubDiagnostics"],"sources":["../../../src/utils/markdownPreprocessor.ts"],"sourcesContent":["/**\n * Markdown preprocessing utilities\n *\n * These functions fix common markdown formatting issues before AST parsing.\n * They are applied in a specific order to ensure proper processing.\n */\n\nexport interface PreprocessorDiagnostic {\n type: string;\n original: string;\n normalized: string;\n}\n\nexport interface PreprocessResult {\n markdown: string;\n diagnostics: PreprocessorDiagnostic[];\n}\n\n/**\n * Escape web5:// links by URL-encoding the URL part\n * e.g. [text](web5://some text with spaces) → [text](web5://some%20text%20with%20spaces)\n *\n * @param markdown - The markdown string to process\n * @returns The markdown string with web5:// URLs properly encoded\n */\nexport function escapeWeb5Links(markdown: string): string {\n // Match markdown links with web5:// protocol\n // Pattern: [text](web5://...)\n return markdown.replace(\n /\\[([^\\]]+)\\]\\((web5|entity|image):\\/\\/([^)]+)\\)/g,\n (match, linkText, protocol, url) => {\n try {\n // Decode first to avoid double-encoding (e.g. %20 → %2520)\n const decodedUrl = decodeURIComponent(url);\n // Use encodeURI (not encodeURIComponent) to preserve URL structure\n // characters like /, ?, &, = while encoding spaces and unsafe chars\n const encodedUrl = encodeURI(decodedUrl);\n return `[${linkText}](${protocol}://${encodedUrl})`;\n } catch {\n // decodeURIComponent throws on a malformed percent sequence (raw `%`\n // or truncated `%X`), encodeURI on a lone surrogate. During streaming\n // this happens when chunks are concatenated mid-escape or a multi-byte\n // char is split. Leave the link untouched rather than throw — an\n // uncaught error here aborts the whole streaming parse and surfaces as\n // a 908 conversation-streaming-error even though the page rendered.\n return match;\n }\n },\n );\n}\n\n/**\n * Fix malformed markdown links with whitespace (spaces, tabs, newlines) between ] and (\n * e.g. [text] (url) → [text](url)\n * e.g. [text]\\n(url) → [text](url)\n *\n * @param markdown - The markdown string to process\n * @returns The markdown string with fixed link formatting\n */\nexport function fixMalformedLinks(markdown: string): string {\n // \\s matches spaces, tabs, and newlines; + means one or more\n // Also handle cases with newlines explicitly\n return markdown.replace(/\\][\\s\\r\\n]+\\(/g, '](');\n}\n\n/**\n * Recover inline CTA chips where the LLM emitted emphasized text followed by a\n * dangling markdown link close, e.g. `**label →**](web5://ask/query)`.\n */\nexport function recoverOrphanedEmphasisLinks(markdown: string): {\n result: string;\n diagnostics: PreprocessorDiagnostic[];\n} {\n const diagnostics: PreprocessorDiagnostic[] = [];\n const result = markdown.replace(\n /(^|[^\\[])\\*\\*([^\\n\\]*]+?)\\*\\*\\]\\((web5:\\/\\/(?:ask|action)\\/[^)]+)\\)/g,\n (match, prefix: string, linkText: string, href: string) => {\n const normalized = `${prefix}[${linkText.trim()}](${href})`;\n diagnostics.push({\n type: 'orphaned_emphasis_link_recovered',\n original: match,\n normalized,\n });\n return normalized;\n },\n );\n return { result, diagnostics };\n}\n\n/**\n * A `web5://` link target whose opening `[` never arrived, e.g.\n * `try filtering for men](web5://ask/Which+are+for+men%3F)`.\n *\n * Dropping the opening bracket is the most common way an inline ask link is\n * mis-emitted, and it is the worst one: remark parses nothing as a link, so the\n * scheme renders as literal text and the visitor reads `web5://ask/...`.\n *\n * `run` is the text before the `]` on the same line, which is what the label\n * should have been. It may be empty, in which case there is nothing to promote\n * and the construct is dropped instead.\n */\nconst ORPHANED_WEB5_TARGET = /([^[\\]()*\\n]{0,80}?)\\]\\((web5:\\/\\/[^)\\s]*)\\)/g;\n\n/** Any `web5://` url, for the final check that none is visible as text. */\nconst ANY_WEB5_URL = /web5:\\/\\/[^\\s)\\]]*/g;\n\n/** Longest label we will promote out of surrounding prose. */\nconst MAX_RECOVERED_LABEL_WORDS = 4;\n\n/**\n * Words a label should start *after* rather than with. The label is cut at the\n * last of these inside the window, so a promoted phrase reads as a noun phrase\n * (\"sensitive skin\") instead of trailing off a verb or preposition\n * (\"carry for sensitive skin\").\n */\nconst LABEL_BOUNDARY_WORDS = new Set([\n 'a',\n 'an',\n 'and',\n 'at',\n 'but',\n 'by',\n 'for',\n 'from',\n 'in',\n 'into',\n 'of',\n 'on',\n 'or',\n 'the',\n 'to',\n 'with',\n]);\n\n/**\n * True when `segment` holds a `[` that is still waiting to be closed — meaning\n * the `](` after it closes a real link and there is nothing to repair.\n */\nfunction hasOpenLinkBracket(segment: string): boolean {\n return segment.lastIndexOf('[') > segment.lastIndexOf(']');\n}\n\n/**\n * Split `run` into the prose to leave in place and the phrase to promote into\n * the link label. Slices on token offsets rather than rejoining words, so the\n * original spacing around the label is preserved exactly.\n *\n * An empty label means there is nothing worth promoting and the caller should\n * drop the link instead.\n */\nfunction splitRecoveredLabel(run: string): { prefix: string; label: string } {\n // Never let the label cross a sentence boundary.\n let sentenceStart = 0;\n for (const boundary of run.matchAll(/(?<=[.!?;:])\\s+/g)) {\n sentenceStart = (boundary.index ?? 0) + boundary[0].length;\n }\n\n const tokens = [...run.slice(sentenceStart).matchAll(/\\S+/g)].map(\n (token) => ({\n word: token[0],\n start: sentenceStart + (token.index ?? 0),\n }),\n );\n\n let window = tokens.slice(-MAX_RECOVERED_LABEL_WORDS);\n for (let i = window.length - 1; i >= 0; i--) {\n const bare = window[i].word.toLowerCase().replace(/[^a-z]/g, '');\n if (LABEL_BOUNDARY_WORDS.has(bare)) {\n window = window.slice(i + 1);\n break;\n }\n }\n\n if (window.length === 0) {\n return { prefix: run, label: '' };\n }\n\n return {\n prefix: run.slice(0, window[0].start),\n label: run.slice(window[0].start).trim(),\n };\n}\n\n/**\n * Repair `web5://` links that lost their opening `[`.\n *\n * Recovers the link by promoting the words immediately before the `]` into its\n * label; when there are no words to promote, drops the whole construct. Either\n * way the scheme stops being visible, which is the point — a link the visitor\n * cannot use is a smaller failure than a url the visitor can read.\n */\nexport function repairOrphanedWeb5Links(markdown: string): {\n result: string;\n diagnostics: PreprocessorDiagnostic[];\n} {\n const diagnostics: PreprocessorDiagnostic[] = [];\n const result = markdown.replace(\n ORPHANED_WEB5_TARGET,\n (\n match: string,\n run: string,\n href: string,\n offset: number,\n whole: string,\n ) => {\n const lineStart = whole.lastIndexOf('\\n', offset) + 1;\n if (hasOpenLinkBracket(whole.slice(lineStart, offset))) {\n // A real `[label](url)` — the bracket is there, leave it alone.\n return match;\n }\n\n const { prefix, label } = splitRecoveredLabel(run);\n if (!label) {\n diagnostics.push({\n type: 'orphaned_web5_link_dropped',\n original: match,\n normalized: run,\n });\n return run;\n }\n\n const normalized = `${prefix}[${label}](${href})`;\n diagnostics.push({\n type: 'orphaned_web5_link_recovered',\n original: match,\n normalized,\n });\n return normalized;\n },\n );\n return { result, diagnostics };\n}\n\n/**\n * Last resort: remove any `web5://` url still sitting in visible text.\n *\n * Runs after every recovery step, so anything left here is a url no repair\n * could turn back into a link. A url in a link target (`](web5://...)`) is\n * left alone — only text the visitor would actually read is scrubbed.\n */\nexport function scrubVisibleWeb5Urls(markdown: string): {\n result: string;\n diagnostics: PreprocessorDiagnostic[];\n} {\n const diagnostics: PreprocessorDiagnostic[] = [];\n const result = markdown.replace(\n ANY_WEB5_URL,\n (match: string, offset: number, whole: string) => {\n if (whole.slice(offset - 2, offset) === '](') {\n return match;\n }\n diagnostics.push({\n type: 'visible_web5_url_scrubbed',\n original: match,\n normalized: '',\n });\n return '';\n },\n );\n return { result, diagnostics };\n}\n\n/**\n * Trim trailing whitespace from lines that contain text\n *\n * @param markdown - The markdown string to process\n * @returns The markdown string with trailing whitespace trimmed from text lines\n */\nexport function trimTrailingWhitespace(markdown: string): string {\n const lines = markdown.split('\\n');\n const processedLines: string[] = [];\n\n for (const line of lines) {\n const trimmedLine = line.trim();\n\n // If the line has content, trim trailing whitespace (keep leading spaces for indentation)\n if (trimmedLine.length > 0) {\n processedLines.push(line.trimEnd());\n } else {\n // Lines with only whitespace become empty lines\n processedLines.push('');\n }\n }\n\n return processedLines.join('\\n');\n}\n\n/**\n * Normalize malformed web5://icon URLs to the canonical web5://icon/{name} format.\n * Handles variants like web5://icon:arrow, web5://icon-arrow, web5://icon arrow → web5://icon/arrow.\n * Only alphanumeric characters and dashes are kept in the icon name.\n */\nexport function normalizeIconUrls(markdown: string): {\n result: string;\n diagnostics: PreprocessorDiagnostic[];\n} {\n const diagnostics: PreprocessorDiagnostic[] = [];\n const result = markdown.replace(\n /web5:\\/\\/icon(?:([:/-][^)\\s]+)|(?:\\s+([a-zA-Z0-9-]+)))/g,\n (match, rest: string | undefined, spaceName: string | undefined) => {\n const cleanName = (spaceName ?? rest ?? '')\n .replace(/[^a-zA-Z0-9-]/g, '')\n .replace(/^-+|-+$/g, '');\n const normalized = `web5://icon/${cleanName}`;\n if (match !== normalized) {\n diagnostics.push({\n type: 'icon_url_normalized',\n original: match,\n normalized,\n });\n }\n return normalized;\n },\n );\n return { result, diagnostics };\n}\n\nfunction isFenceToggle(line: string): boolean {\n return /^\\s*(`{3,}|~{3,})/.test(line);\n}\n\nfunction splitPipeRow(line: string): string[] {\n const trimmed = line.trim();\n if (!trimmed.startsWith('|') || !trimmed.endsWith('|')) {\n return [];\n }\n return trimmed\n .slice(1, -1)\n .split('|')\n .map((cell) => cell.trim());\n}\n\nfunction isPipeRow(line: string): boolean {\n return splitPipeRow(line).length >= 2;\n}\n\nfunction isTableDelimiterRow(line: string): boolean {\n const cells = splitPipeRow(line);\n return (\n cells.length >= 2 &&\n cells.every((cell) => /^:?-{3,}:?$/.test(cell.replace(/\\s+/g, '')))\n );\n}\n\n/**\n * Repair LLM-emitted pipe tables that omit the GFM delimiter row.\n * Without this line remark-gfm parses the rows as plain paragraphs, so\n * registry sections such as comparison never receive a `table` part.\n */\nexport function insertMissingTableSeparators(markdown: string): {\n result: string;\n diagnostics: PreprocessorDiagnostic[];\n} {\n const diagnostics: PreprocessorDiagnostic[] = [];\n const lines = markdown.split('\\n');\n const result: string[] = [];\n let inFence = false;\n\n for (let i = 0; i < lines.length; i++) {\n const line = lines[i];\n const previousLine = result[result.length - 1] ?? '';\n const nextLine = lines[i + 1] ?? '';\n\n if (isFenceToggle(line)) {\n inFence = !inFence;\n result.push(line);\n continue;\n }\n\n result.push(line);\n\n if (inFence || !isPipeRow(line) || isPipeRow(previousLine)) {\n continue;\n }\n if (!isPipeRow(nextLine) || isTableDelimiterRow(nextLine)) {\n continue;\n }\n\n const headerCells = splitPipeRow(line);\n const nextCells = splitPipeRow(nextLine);\n if (headerCells.length !== nextCells.length) {\n continue;\n }\n\n const separator = `| ${headerCells.map(() => '---').join(' | ')} |`;\n result.push(separator);\n diagnostics.push({\n type: 'table_separator_inserted',\n original: line,\n normalized: `${line}\\n${separator}`,\n });\n }\n\n return { result: result.join('\\n'), diagnostics };\n}\n\n/**\n * Decode URL-encoded text in markdown link labels.\n * e.g. [Some%20Text](https://example.com) → [Some Text](https://example.com)\n * e.g. [Which+solution%3F](web5://...) → [Which solution?](web5://...)\n *\n * Only decodes the text portion [text], never the URL portion (url).\n * Handles both `+` (form-encoded space) and `%XX` escapes; falls back to\n * the original string on malformed percent sequences.\n */\nexport function decodeLinkText(markdown: string): {\n result: string;\n diagnostics: PreprocessorDiagnostic[];\n} {\n const diagnostics: PreprocessorDiagnostic[] = [];\n const result = markdown.replace(\n /\\[([^\\]]+)\\]\\(/g,\n (match, linkText: string) => {\n try {\n const decoded = decodeURIComponent(linkText.replace(/\\+/g, '%20'));\n if (decoded !== linkText) {\n diagnostics.push({\n type: 'link_text_decoded',\n original: linkText,\n normalized: decoded,\n });\n }\n return `[${decoded}](`;\n } catch {\n // If decodeURIComponent fails (e.g. malformed percent sequence like %ZZ),\n // leave the text unchanged\n return match;\n }\n },\n );\n return { result, diagnostics };\n}\n\n/**\n * Preprocess markdown to fix common formatting issues before AST parsing.\n *\n * The preprocessing steps are applied in the following order:\n * 1. Recover orphaned emphasized web5 links\n * 2. Fix malformed links (remove whitespace between ] and () - must happen before web5 links are matched)\n * 3. Repair web5 links missing their opening `[` (after step 2, so `] (url)` is already normalized)\n * 4. Decode URL-encoded link text\n * 5. Escape web5:// links (URL-encode URLs)\n * 6. Normalize icon URLs\n * 7. Insert missing table separators for obvious pipe tables\n * 8. Scrub any web5:// url still visible as text (last, so every repair has had its chance)\n * 9. Trim trailing whitespace from lines\n *\n * @param markdown - The raw markdown string to preprocess\n * @returns The preprocessed markdown string ready for AST parsing\n */\nexport function preprocessMarkdown(markdown: string): PreprocessResult {\n const { result: recoveredLinks, diagnostics: recoveredLinkDiagnostics } =\n recoverOrphanedEmphasisLinks(markdown);\n let processed = fixMalformedLinks(recoveredLinks);\n const { result: repairedLinks, diagnostics: repairedLinkDiagnostics } =\n repairOrphanedWeb5Links(processed);\n const { result: decodedText, diagnostics: decodeDiagnostics } =\n decodeLinkText(repairedLinks);\n processed = escapeWeb5Links(decodedText);\n const { result: normalized, diagnostics: iconDiagnostics } =\n normalizeIconUrls(processed);\n const { result: tableNormalized, diagnostics: tableDiagnostics } =\n insertMissingTableSeparators(normalized);\n const { result: scrubbed, diagnostics: scrubDiagnostics } =\n scrubVisibleWeb5Urls(tableNormalized);\n processed = trimTrailingWhitespace(scrubbed);\n return {\n markdown: processed,\n diagnostics: [\n ...recoveredLinkDiagnostics,\n ...repairedLinkDiagnostics,\n ...decodeDiagnostics,\n ...iconDiagnostics,\n ...tableDiagnostics,\n ...scrubDiagnostics,\n ],\n };\n}\n"],"mappings":"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","ignoreList":[]}
@@ -1,7 +1,23 @@
1
1
  import _defineProperty from "@babel/runtime/helpers/defineProperty";
2
2
  import { headingText, bodyText, firstLink, parseTableValue } from './parse-utils.js';
3
3
  import { getPartsByType } from '../../parts/parts.js';
4
+ import { DROP_SECTION } from '../section-definition.js';
4
5
  import { DIAGNOSTIC_TYPES } from '../diagnosticTypes.js';
6
+
7
+ /**
8
+ * A cell the writer left blank: empty, or a dash / "n/a" placeholder standing in
9
+ * for "we have no fact here".
10
+ *
11
+ * This has to be judged on the RAW cell text, before `parseTableValue`, because
12
+ * that maps an empty cell to `false` — the very same value a deliberate "No"
13
+ * produces. After parsing, "no data" and "no, it does not have that" are
14
+ * indistinguishable.
15
+ */
16
+ function isBlankCell(raw) {
17
+ const v = (raw ?? '').trim().toLowerCase();
18
+ // U+2010–U+2015 are the hyphen/dash family; U+2212 is minus.
19
+ return v === '' || v === 'n/a' || /^[-‐-―−]+$/.test(v);
20
+ }
5
21
  export class ComparisonSectionDefinition {
6
22
  constructor() {
7
23
  _defineProperty(this, "sectionType", 'comparison');
@@ -82,11 +98,14 @@ See how our platform stacks up against the competition.
82
98
  let columns;
83
99
  let featureColumnLabel;
84
100
  let rows;
101
+ /** The cells exactly as written, index-aligned with `rows`. */
102
+ let rawValueRows;
85
103
  if ((tableMeta == null ? void 0 : tableMeta.columnLabels) !== undefined) {
86
104
  // SDK nodesToParts format
87
105
  columnLabels = tableMeta.columnLabels;
88
106
  columns = tableMeta.columns;
89
107
  featureColumnLabel = tableMeta.featureColumnLabel || undefined;
108
+ rawValueRows = (tableMeta.rows ?? []).map(row => row.values);
90
109
  rows = (tableMeta.rows ?? []).map(row => ({
91
110
  feature: row.feature,
92
111
  ...(row.iconHint ? {
@@ -103,6 +122,7 @@ See how our platform stacks up against the competition.
103
122
  columns = columnLabels.map(label => ({
104
123
  label
105
124
  }));
125
+ rawValueRows = rawRows.map(row => row.slice(1));
106
126
  rows = rawRows.map(row => ({
107
127
  feature: row[0] ?? '',
108
128
  values: row.slice(1).map(parseTableValue)
@@ -115,11 +135,24 @@ See how our platform stacks up against the competition.
115
135
  const ctaHref = (link == null ? void 0 : link.href) || undefined;
116
136
 
117
137
  // Filter out rows where all values are empty (e.g. trailing non-table text parsed as a row)
118
- const filteredRows = rows.filter(row => row.values.some(v => v !== ''));
138
+ const keptRow = rawValueRows.map(raw => raw.some(v => !isBlankCell(v)));
139
+ const filteredRows = rows.filter((_, i) => keptRow[i]);
140
+ const filteredRawRows = rawValueRows.filter((_, i) => keptRow[i]);
119
141
  const droppedCount = rows.length - filteredRows.length;
120
142
  if (droppedCount > 0) {
121
143
  context == null || context.reportDiagnostic == null || context.reportDiagnostic(DIAGNOSTIC_TYPES.ITEMS_DROPPED, `comparison: ${droppedCount} row(s) dropped (all values empty)`);
122
144
  }
145
+
146
+ // A column blank in every row is a side of the comparison the writer had no
147
+ // facts for — it renders as a full column of dashes next to a populated one,
148
+ // which reads as a real answer and is not one. Half a comparison is worse
149
+ // than none, so drop the whole section. (No rows left at all lands here too:
150
+ // every column is vacuously blank.)
151
+ const blankColumns = columnLabels.map((_, col) => col).filter(col => filteredRawRows.every(raw => isBlankCell(raw[col])));
152
+ if (columnLabels.length > 0 && blankColumns.length > 0) {
153
+ context == null || context.reportDiagnostic == null || context.reportDiagnostic(DIAGNOSTIC_TYPES.SECTION_REJECTED, `comparison: column(s) ${blankColumns.map(c => columnLabels[c] || `#${c + 1}`).join(', ')} blank across all ${filteredRawRows.length} row(s) — section dropped`);
154
+ return DROP_SECTION;
155
+ }
123
156
  return {
124
157
  title,
125
158
  description,
@@ -1 +1 @@
1
- {"version":3,"names":["headingText","bodyText","firstLink","parseTableValue","getPartsByType","DIAGNOSTIC_TYPES","ComparisonSectionDefinition","constructor","_defineProperty","title","columnLabels","rows","feature","values","markdown","expected","featureColumnLabel","description","ctaText","ctaHref","parse","parts","context","_tableParts$","undefined","tableParts","tableMeta","meta","columns","map","row","iconHint","allHeaders","headers","rawRows","slice","label","link","text","href","filteredRows","filter","some","v","droppedCount","length","reportDiagnostic","ITEMS_DROPPED"],"sources":["../../../../src/component/componentDefinitions/ComparisonSectionDefinition.ts"],"sourcesContent":["import {\n headingText,\n bodyText,\n firstLink,\n parseTableValue,\n} from './parse-utils';\nimport { getPartsByType, type Part } from '../../parts/parts';\nimport type {\n ComparisonColumn,\n ComparisonCompProps,\n ComparisonRow,\n ComparisonSectionComponent,\n SectionFixture,\n} from '../types';\nimport type { SectionDefinition, ParseContext } from '../section-definition';\nimport { DIAGNOSTIC_TYPES } from '../diagnosticTypes';\n\nexport class ComparisonSectionDefinition\n implements SectionDefinition<ComparisonSectionComponent>\n{\n sectionType = 'comparison' as const;\n patterns = [\n 'html_comment[section^=\"comparison\"] > (h2 | h3 | h4 | t) > html_comment? > t* > table > (t | link)?',\n ];\n\n defaults: ComparisonCompProps = {\n title: 'Comparison',\n columnLabels: ['Option A', 'Option B'],\n rows: [\n { feature: 'Price', values: ['$10', '$20'] },\n { feature: 'Storage', values: ['100GB', '1TB'] },\n ],\n };\n\n fixtures: SectionFixture<ComparisonCompProps>[] = [\n {\n markdown: `<!-- section: comparison -->\n## Compare Plans\n\n| Feature | Basic | Pro |\n|---------|-------|-----|\n| Users | 5 | Unlimited |\n| Storage | 100GB | 1TB |`,\n expected: {\n title: 'Compare Plans',\n featureColumnLabel: 'Feature',\n columnLabels: ['Basic', 'Pro'],\n rows: [\n { feature: 'Users', values: ['5', 'Unlimited'] },\n { feature: 'Storage', values: ['100GB', '1TB'] },\n ],\n },\n },\n {\n markdown: `<!-- section: comparison -->\n## Platform Comparison\n\nSee how our platform stacks up against the competition.\n\n| Feature | Our Platform | Competitor |\n|---------|--------------|------------|\n| Storage | 1TB | 100GB |\n| Users | Unlimited | 5 |\n| Support | 24/7 Priority | Email Only |\n\n[Get started](#signup)`,\n expected: {\n title: 'Platform Comparison',\n description: 'See how our platform stacks up against the competition.',\n featureColumnLabel: 'Feature',\n columnLabels: ['Our Platform', 'Competitor'],\n rows: [\n { feature: 'Storage', values: ['1TB', '100GB'] },\n { feature: 'Users', values: ['Unlimited', '5'] },\n { feature: 'Support', values: ['24/7 Priority', 'Email Only'] },\n ],\n ctaText: 'Get started',\n ctaHref: '#signup',\n },\n },\n ];\n\n parse(parts: Part[], context?: ParseContext): ComparisonCompProps {\n const title =\n headingText(parts, 2) ||\n headingText(parts, 3) ||\n headingText(parts, 4) ||\n 'Comparison';\n const description = bodyText(parts) || undefined;\n\n // Table meta from nodesToParts: { columnLabels, featureColumnLabel, rows: { feature, values }[] }\n const tableParts = getPartsByType<Part>(parts, 'table');\n const tableMeta = tableParts[0]?.meta as\n | {\n columnLabels?: string[];\n columns?: ComparisonColumn[];\n featureColumnLabel?: string;\n rows?: { feature: string; iconHint?: string; values: string[] }[];\n // Legacy format from partsConverter\n headers?: string[];\n }\n | undefined;\n\n let columnLabels: string[];\n let columns: ComparisonColumn[] | undefined;\n let featureColumnLabel: string | undefined;\n let rows: ComparisonRow[];\n\n if (tableMeta?.columnLabels !== undefined) {\n // SDK nodesToParts format\n columnLabels = tableMeta.columnLabels;\n columns = tableMeta.columns;\n featureColumnLabel = tableMeta.featureColumnLabel || undefined;\n rows = (tableMeta.rows ?? []).map((row) => ({\n feature: row.feature,\n ...(row.iconHint ? { iconHint: row.iconHint } : {}),\n values: row.values.map(parseTableValue),\n }));\n } else {\n // Legacy partsConverter format: { headers: string[], rows: string[][] }\n const allHeaders = tableMeta?.headers ?? [];\n const rawRows = (tableMeta as any)?.rows ?? [];\n featureColumnLabel = allHeaders[0] || undefined;\n columnLabels = allHeaders.slice(1);\n columns = columnLabels.map((label) => ({ label }));\n rows = rawRows.map((row: string[]) => ({\n feature: row[0] ?? '',\n values: row.slice(1).map(parseTableValue),\n }));\n }\n\n // Extract CTA from link parts\n const link = firstLink(parts);\n const ctaText = link?.text || undefined;\n const ctaHref = link?.href || undefined;\n\n // Filter out rows where all values are empty (e.g. trailing non-table text parsed as a row)\n const filteredRows = rows.filter((row) => row.values.some((v) => v !== ''));\n const droppedCount = rows.length - filteredRows.length;\n if (droppedCount > 0) {\n context?.reportDiagnostic?.(\n DIAGNOSTIC_TYPES.ITEMS_DROPPED,\n `comparison: ${droppedCount} row(s) dropped (all values empty)`,\n );\n }\n\n return {\n title,\n description,\n columnLabels,\n columns,\n rows: filteredRows,\n featureColumnLabel,\n ctaText,\n ctaHref,\n };\n }\n}\n"],"mappings":"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","ignoreList":[]}
1
+ {"version":3,"names":["headingText","bodyText","firstLink","parseTableValue","getPartsByType","DROP_SECTION","DIAGNOSTIC_TYPES","isBlankCell","raw","v","trim","toLowerCase","test","ComparisonSectionDefinition","constructor","_defineProperty","title","columnLabels","rows","feature","values","markdown","expected","featureColumnLabel","description","ctaText","ctaHref","parse","parts","context","_tableParts$","undefined","tableParts","tableMeta","meta","columns","rawValueRows","map","row","iconHint","allHeaders","headers","rawRows","slice","label","link","text","href","keptRow","some","filteredRows","filter","_","i","filteredRawRows","droppedCount","length","reportDiagnostic","ITEMS_DROPPED","blankColumns","col","every","SECTION_REJECTED","c","join"],"sources":["../../../../src/component/componentDefinitions/ComparisonSectionDefinition.ts"],"sourcesContent":["import {\n headingText,\n bodyText,\n firstLink,\n parseTableValue,\n} from './parse-utils';\nimport { getPartsByType, type Part } from '../../parts/parts';\nimport type {\n ComparisonColumn,\n ComparisonCompProps,\n ComparisonRow,\n ComparisonSectionComponent,\n SectionFixture,\n} from '../types';\nimport type {\n SectionDefinition,\n ParseContext,\n DropSection,\n} from '../section-definition';\nimport { DROP_SECTION } from '../section-definition';\nimport { DIAGNOSTIC_TYPES } from '../diagnosticTypes';\n\n/**\n * A cell the writer left blank: empty, or a dash / \"n/a\" placeholder standing in\n * for \"we have no fact here\".\n *\n * This has to be judged on the RAW cell text, before `parseTableValue`, because\n * that maps an empty cell to `false` — the very same value a deliberate \"No\"\n * produces. After parsing, \"no data\" and \"no, it does not have that\" are\n * indistinguishable.\n */\nfunction isBlankCell(raw: string | undefined): boolean {\n const v = (raw ?? '').trim().toLowerCase();\n // U+2010–U+2015 are the hyphen/dash family; U+2212 is minus.\n return v === '' || v === 'n/a' || /^[-‐-―−]+$/.test(v);\n}\n\nexport class ComparisonSectionDefinition\n implements SectionDefinition<ComparisonSectionComponent>\n{\n sectionType = 'comparison' as const;\n patterns = [\n 'html_comment[section^=\"comparison\"] > (h2 | h3 | h4 | t) > html_comment? > t* > table > (t | link)?',\n ];\n\n defaults: ComparisonCompProps = {\n title: 'Comparison',\n columnLabels: ['Option A', 'Option B'],\n rows: [\n { feature: 'Price', values: ['$10', '$20'] },\n { feature: 'Storage', values: ['100GB', '1TB'] },\n ],\n };\n\n fixtures: SectionFixture<ComparisonCompProps>[] = [\n {\n markdown: `<!-- section: comparison -->\n## Compare Plans\n\n| Feature | Basic | Pro |\n|---------|-------|-----|\n| Users | 5 | Unlimited |\n| Storage | 100GB | 1TB |`,\n expected: {\n title: 'Compare Plans',\n featureColumnLabel: 'Feature',\n columnLabels: ['Basic', 'Pro'],\n rows: [\n { feature: 'Users', values: ['5', 'Unlimited'] },\n { feature: 'Storage', values: ['100GB', '1TB'] },\n ],\n },\n },\n {\n markdown: `<!-- section: comparison -->\n## Platform Comparison\n\nSee how our platform stacks up against the competition.\n\n| Feature | Our Platform | Competitor |\n|---------|--------------|------------|\n| Storage | 1TB | 100GB |\n| Users | Unlimited | 5 |\n| Support | 24/7 Priority | Email Only |\n\n[Get started](#signup)`,\n expected: {\n title: 'Platform Comparison',\n description: 'See how our platform stacks up against the competition.',\n featureColumnLabel: 'Feature',\n columnLabels: ['Our Platform', 'Competitor'],\n rows: [\n { feature: 'Storage', values: ['1TB', '100GB'] },\n { feature: 'Users', values: ['Unlimited', '5'] },\n { feature: 'Support', values: ['24/7 Priority', 'Email Only'] },\n ],\n ctaText: 'Get started',\n ctaHref: '#signup',\n },\n },\n ];\n\n parse(\n parts: Part[],\n context?: ParseContext,\n ): ComparisonCompProps | DropSection {\n const title =\n headingText(parts, 2) ||\n headingText(parts, 3) ||\n headingText(parts, 4) ||\n 'Comparison';\n const description = bodyText(parts) || undefined;\n\n // Table meta from nodesToParts: { columnLabels, featureColumnLabel, rows: { feature, values }[] }\n const tableParts = getPartsByType<Part>(parts, 'table');\n const tableMeta = tableParts[0]?.meta as\n | {\n columnLabels?: string[];\n columns?: ComparisonColumn[];\n featureColumnLabel?: string;\n rows?: { feature: string; iconHint?: string; values: string[] }[];\n // Legacy format from partsConverter\n headers?: string[];\n }\n | undefined;\n\n let columnLabels: string[];\n let columns: ComparisonColumn[] | undefined;\n let featureColumnLabel: string | undefined;\n let rows: ComparisonRow[];\n /** The cells exactly as written, index-aligned with `rows`. */\n let rawValueRows: string[][];\n\n if (tableMeta?.columnLabels !== undefined) {\n // SDK nodesToParts format\n columnLabels = tableMeta.columnLabels;\n columns = tableMeta.columns;\n featureColumnLabel = tableMeta.featureColumnLabel || undefined;\n rawValueRows = (tableMeta.rows ?? []).map((row) => row.values);\n rows = (tableMeta.rows ?? []).map((row) => ({\n feature: row.feature,\n ...(row.iconHint ? { iconHint: row.iconHint } : {}),\n values: row.values.map(parseTableValue),\n }));\n } else {\n // Legacy partsConverter format: { headers: string[], rows: string[][] }\n const allHeaders = tableMeta?.headers ?? [];\n const rawRows = (tableMeta as any)?.rows ?? [];\n featureColumnLabel = allHeaders[0] || undefined;\n columnLabels = allHeaders.slice(1);\n columns = columnLabels.map((label) => ({ label }));\n rawValueRows = rawRows.map((row: string[]) => row.slice(1));\n rows = rawRows.map((row: string[]) => ({\n feature: row[0] ?? '',\n values: row.slice(1).map(parseTableValue),\n }));\n }\n\n // Extract CTA from link parts\n const link = firstLink(parts);\n const ctaText = link?.text || undefined;\n const ctaHref = link?.href || undefined;\n\n // Filter out rows where all values are empty (e.g. trailing non-table text parsed as a row)\n const keptRow = rawValueRows.map((raw) => raw.some((v) => !isBlankCell(v)));\n const filteredRows = rows.filter((_, i) => keptRow[i]);\n const filteredRawRows = rawValueRows.filter((_, i) => keptRow[i]);\n const droppedCount = rows.length - filteredRows.length;\n if (droppedCount > 0) {\n context?.reportDiagnostic?.(\n DIAGNOSTIC_TYPES.ITEMS_DROPPED,\n `comparison: ${droppedCount} row(s) dropped (all values empty)`,\n );\n }\n\n // A column blank in every row is a side of the comparison the writer had no\n // facts for — it renders as a full column of dashes next to a populated one,\n // which reads as a real answer and is not one. Half a comparison is worse\n // than none, so drop the whole section. (No rows left at all lands here too:\n // every column is vacuously blank.)\n const blankColumns = columnLabels\n .map((_, col) => col)\n .filter((col) =>\n filteredRawRows.every((raw) => isBlankCell(raw[col])),\n );\n if (columnLabels.length > 0 && blankColumns.length > 0) {\n context?.reportDiagnostic?.(\n DIAGNOSTIC_TYPES.SECTION_REJECTED,\n `comparison: column(s) ${blankColumns\n .map((c) => columnLabels[c] || `#${c + 1}`)\n .join(', ')} blank across all ${filteredRawRows.length} row(s) — section dropped`,\n );\n return DROP_SECTION;\n }\n\n return {\n title,\n description,\n columnLabels,\n columns,\n rows: filteredRows,\n featureColumnLabel,\n ctaText,\n ctaHref,\n };\n }\n}\n"],"mappings":"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","ignoreList":[]}
@@ -1,5 +1,5 @@
1
1
  import _defineProperty from "@babel/runtime/helpers/defineProperty";
2
- import { headingText, bodyText, extractListItemText } from './parse-utils.js';
2
+ import { headingText, bodyText, extractListItemText, inlinePartsToMarkdown } from './parse-utils.js';
3
3
  import { getPartsByType } from '../../parts/parts.js';
4
4
  export class FeatureSection9PlusDefinition {
5
5
  constructor() {
@@ -21,14 +21,18 @@ export class FeatureSection9PlusDefinition {
21
21
  var _list$meta, _item$meta, _iconPart$meta, _item$meta2;
22
22
  const itemParts = ((_item$meta = item.meta) == null ? void 0 : _item$meta.parts) ?? [];
23
23
  const heading = itemParts.find(p => p.type === 'heading');
24
- const textParts = itemParts.filter(p => p.type === 'text' || p.type === 'richText');
25
- const featureDescription = textParts.map(p => {
26
- var _p$content;
27
- return (_p$content = p.content) == null ? void 0 : _p$content.trim();
28
- }).filter(Boolean).join('\n');
29
24
  const iconPart = itemParts.find(p => p.type === 'icon');
30
25
  const iconHint = iconPart == null || (_iconPart$meta = iconPart.meta) == null ? void 0 : _iconPart$meta.iconHint;
31
26
 
27
+ // Everything except the heading and the leading icon is the body.
28
+ // Serialize it rather than concatenating `content`: filtering to text
29
+ // parts used to drop `link` parts wholesale, deleting an inline link's
30
+ // url and label together. Same defect as listItemsSectionDefinition —
31
+ // this definition sits below that one in every registry, but the seed's
32
+ // FeatureIconsSection is registered against the other definition while
33
+ // typed on this one's props, so both have to behave the same.
34
+ const featureDescription = inlinePartsToMarkdown(itemParts.filter(p => p !== heading && p !== iconPart));
35
+
32
36
  // Try to parse bold title from markdown (preserves **bold**) or plain content
33
37
  // Strip leading icon markdown (e.g., "![](web5://icon/hint) ") before bold matching
34
38
  const rawMarkdown = (_item$meta2 = item.meta) == null || (_item$meta2 = _item$meta2.markdown) == null ? void 0 : _item$meta2.trim();
@@ -1 +1 @@
1
- {"version":3,"names":["headingText","bodyText","extractListItemText","getPartsByType","FeatureSection9PlusDefinition","constructor","_defineProperty","title","features","parse","parts","description","undefined","lists","list","item","_list$meta","meta","items","_item$meta","_iconPart$meta","_item$meta2","itemParts","heading","find","p","type","textParts","filter","featureDescription","map","_p$content","content","trim","Boolean","join","iconPart","iconHint","rawMarkdown","markdown","replace","boldMatch","match","iconField","_heading$content","push"],"sources":["../../../../src/component/componentDefinitions/FeatureSection9PlusDefinition.ts"],"sourcesContent":["import {\n headingText,\n bodyText,\n extractListItemText,\n} from './parse-utils';\nimport {\n getPartsByType,\n type Part,\n type ListPart,\n type IconPart,\n} from '../../parts/parts';\nimport type {\n FeatureSection9PlusCompProps,\n FeatureSection9PlusSectionComponent,\n SectionFixture,\n} from '../types';\nimport type { SectionDefinition } from '../section-definition';\n\nexport class FeatureSection9PlusDefinition\n implements SectionDefinition<FeatureSection9PlusSectionComponent>\n{\n sectionType = 'featureSection9Plus' as const;\n patterns = [\n 'html_comment? > (h2 | h3) > t? > list{item[2-]:(icon | h3 | t | link)* > t+}',\n ];\n\n defaults: FeatureSection9PlusCompProps = {\n title: 'Features',\n features: [],\n };\n\n fixtures: SectionFixture<FeatureSection9PlusCompProps>[] = [];\n\n parse(parts: Part[]): FeatureSection9PlusCompProps {\n const title = headingText(parts, 2) || headingText(parts, 3) || 'Features';\n const description = bodyText(parts) || undefined;\n\n const features: {\n title?: string;\n description: string;\n iconHint?: string;\n }[] = [];\n const lists = getPartsByType<ListPart>(parts, 'list');\n\n for (const list of lists) {\n for (const item of list.meta?.items ?? []) {\n const itemParts = (item.meta?.parts ?? []) as Part[];\n const heading = itemParts.find((p) => p.type === 'heading');\n const textParts = itemParts.filter(\n (p) => p.type === 'text' || p.type === 'richText',\n );\n const featureDescription = textParts\n .map((p) => p.content?.trim())\n .filter(Boolean)\n .join('\\n');\n\n const iconPart = itemParts.find((p) => p.type === 'icon') as\n | IconPart\n | undefined;\n const iconHint = (iconPart?.meta as { iconHint?: string })?.iconHint;\n\n // Try to parse bold title from markdown (preserves **bold**) or plain content\n // Strip leading icon markdown (e.g., \"![](web5://icon/hint) \") before bold matching\n const rawMarkdown = (item.meta?.markdown as string)?.trim();\n const markdown = rawMarkdown\n ?.replace(/^!\\[[^\\]]*\\]\\([^)]*\\)\\s*/, '')\n .trim();\n const content = markdown || extractListItemText(item);\n const boldMatch = content.match(/^\\*\\*([^*]+)\\*\\*:?\\s*(.*)/s);\n\n const iconField = iconHint ? { iconHint } : {};\n\n if (heading) {\n features.push({\n title: heading.content?.trim(),\n description: featureDescription || content,\n ...iconField,\n });\n } else if (boldMatch) {\n features.push({\n title: boldMatch[1].trim(),\n description: boldMatch[2].trim() || featureDescription,\n ...iconField,\n });\n } else {\n features.push({\n description: featureDescription || content,\n ...iconField,\n });\n }\n }\n }\n\n return { title, description, features };\n }\n}\n"],"mappings":"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","ignoreList":[]}
1
+ {"version":3,"names":["headingText","bodyText","extractListItemText","inlinePartsToMarkdown","getPartsByType","FeatureSection9PlusDefinition","constructor","_defineProperty","title","features","parse","parts","description","undefined","lists","list","item","_list$meta","meta","items","_item$meta","_iconPart$meta","_item$meta2","itemParts","heading","find","p","type","iconPart","iconHint","featureDescription","filter","rawMarkdown","markdown","trim","replace","content","boldMatch","match","iconField","_heading$content","push"],"sources":["../../../../src/component/componentDefinitions/FeatureSection9PlusDefinition.ts"],"sourcesContent":["import {\n headingText,\n bodyText,\n extractListItemText,\n inlinePartsToMarkdown,\n} from './parse-utils';\nimport {\n getPartsByType,\n type Part,\n type ListPart,\n type IconPart,\n} from '../../parts/parts';\nimport type {\n FeatureSection9PlusCompProps,\n FeatureSection9PlusSectionComponent,\n SectionFixture,\n} from '../types';\nimport type { SectionDefinition } from '../section-definition';\n\nexport class FeatureSection9PlusDefinition\n implements SectionDefinition<FeatureSection9PlusSectionComponent>\n{\n sectionType = 'featureSection9Plus' as const;\n patterns = [\n 'html_comment? > (h2 | h3) > t? > list{item[2-]:(icon | h3 | t | link)* > t+}',\n ];\n\n defaults: FeatureSection9PlusCompProps = {\n title: 'Features',\n features: [],\n };\n\n fixtures: SectionFixture<FeatureSection9PlusCompProps>[] = [];\n\n parse(parts: Part[]): FeatureSection9PlusCompProps {\n const title = headingText(parts, 2) || headingText(parts, 3) || 'Features';\n const description = bodyText(parts) || undefined;\n\n const features: {\n title?: string;\n description: string;\n iconHint?: string;\n }[] = [];\n const lists = getPartsByType<ListPart>(parts, 'list');\n\n for (const list of lists) {\n for (const item of list.meta?.items ?? []) {\n const itemParts = (item.meta?.parts ?? []) as Part[];\n const heading = itemParts.find((p) => p.type === 'heading');\n\n const iconPart = itemParts.find((p) => p.type === 'icon') as\n | IconPart\n | undefined;\n const iconHint = (iconPart?.meta as { iconHint?: string })?.iconHint;\n\n // Everything except the heading and the leading icon is the body.\n // Serialize it rather than concatenating `content`: filtering to text\n // parts used to drop `link` parts wholesale, deleting an inline link's\n // url and label together. Same defect as listItemsSectionDefinition —\n // this definition sits below that one in every registry, but the seed's\n // FeatureIconsSection is registered against the other definition while\n // typed on this one's props, so both have to behave the same.\n const featureDescription = inlinePartsToMarkdown(\n itemParts.filter((p) => p !== heading && p !== iconPart),\n );\n\n // Try to parse bold title from markdown (preserves **bold**) or plain content\n // Strip leading icon markdown (e.g., \"![](web5://icon/hint) \") before bold matching\n const rawMarkdown = (item.meta?.markdown as string)?.trim();\n const markdown = rawMarkdown\n ?.replace(/^!\\[[^\\]]*\\]\\([^)]*\\)\\s*/, '')\n .trim();\n const content = markdown || extractListItemText(item);\n const boldMatch = content.match(/^\\*\\*([^*]+)\\*\\*:?\\s*(.*)/s);\n\n const iconField = iconHint ? { iconHint } : {};\n\n if (heading) {\n features.push({\n title: heading.content?.trim(),\n description: featureDescription || content,\n ...iconField,\n });\n } else if (boldMatch) {\n features.push({\n title: boldMatch[1].trim(),\n description: boldMatch[2].trim() || featureDescription,\n ...iconField,\n });\n } else {\n features.push({\n description: featureDescription || content,\n ...iconField,\n });\n }\n }\n }\n\n return { title, description, features };\n }\n}\n"],"mappings":"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","ignoreList":[]}
@@ -1,5 +1,5 @@
1
1
  import _defineProperty from "@babel/runtime/helpers/defineProperty";
2
- import { headingText, extractListItemText } from './parse-utils.js';
2
+ import { headingText, extractListItemText, inlinePartsToMarkdown } from './parse-utils.js';
3
3
  import { getPartsByType } from '../../parts/parts.js';
4
4
  /** A valid list section needs at least this many feature items. */
5
5
  const MIN_FEATURES = 2;
@@ -34,11 +34,16 @@ export class ListItemsSectionDefinition {
34
34
  var _list$meta, _item$meta, _item$meta2;
35
35
  const itemParts = ((_item$meta = item.meta) == null ? void 0 : _item$meta.parts) ?? [];
36
36
  const heading = itemParts.find(p => p.type === 'heading');
37
- const textParts = itemParts.filter(p => p.type === 'text' || p.type === 'richText');
38
- const featureDescription = textParts.map(p => {
39
- var _p$content;
40
- return (_p$content = p.content) == null ? void 0 : _p$content.trim();
41
- }).filter(Boolean).join('\n');
37
+ // Everything except the heading is the body. Serialize it rather than
38
+ // concatenating `content`, so an inline link survives: filtering to
39
+ // text parts used to drop `link` parts wholesale, deleting both the
40
+ // url and its label from the rendered item.
41
+ //
42
+ // The item's own `meta.markdown` is not usable here — for a
43
+ // `- ### Title` item it runs the heading straight into the body with no
44
+ // separator ("Gentle daily washRead the ..."), so it would duplicate
45
+ // and mangle the title.
46
+ const featureDescription = inlinePartsToMarkdown(itemParts.filter(p => p !== heading));
42
47
  const markdown = (_item$meta2 = item.meta) == null || (_item$meta2 = _item$meta2.markdown) == null ? void 0 : _item$meta2.trim();
43
48
  const content = markdown || extractListItemText(item);
44
49
  const boldMatch = content.match(/^\*\*([^*]+)\*\*:?\s*(.*)/s);
@@ -1 +1 @@
1
- {"version":3,"names":["headingText","extractListItemText","getPartsByType","MIN_FEATURES","ListItemsSectionDefinition","constructor","_defineProperty","title","features","parse","parts","lists","list","item","_list$meta","meta","items","_item$meta","_item$meta2","itemParts","heading","find","p","type","textParts","filter","featureDescription","map","_p$content","content","trim","Boolean","join","markdown","boldMatch","match","_heading$content","push","description","length"],"sources":["../../../../src/component/componentDefinitions/listItemsSectionDefinition.ts"],"sourcesContent":["import { headingText, extractListItemText } from './parse-utils';\nimport { getPartsByType, type Part, type ListPart } from '../../parts/parts';\nimport type {\n ListItemsSectionCompProps,\n ListItemsSectionComponent,\n SectionFixture,\n} from '../types';\nimport type { SectionDefinition } from '../section-definition';\n\n/** A valid list section needs at least this many feature items. */\nconst MIN_FEATURES = 2;\n\nexport class ListItemsSectionDefinition\n implements SectionDefinition<ListItemsSectionComponent>\n{\n sectionType = 'listItemsSection' as const;\n\n // Comment-delimited: the section spans from a `<!-- section:list -->` marker\n // and greedily captures the tokens that follow. The repetition deliberately\n // excludes `html_comment`, so the match naturally ends just before the next\n // section marker (or end of input) without consuming it — no explicit `~`\n // stop clause needed, and it still matches when this is the final section.\n // The structural contract is enforced in `parse()` instead of the pattern:\n // `parse()` returns null unless the content is an h2 followed by a 2+ item\n // feature list, so a non-conforming block falls through to the next\n // matching definition.\n patterns = [\n 'html_comment[section=\"list\"] > (h2 | h3 | t | callout | list | image | link | table | blockquote)*',\n ];\n\n defaults: ListItemsSectionCompProps = {\n title: 'Features',\n features: [],\n };\n\n fixtures: SectionFixture<ListItemsSectionCompProps>[] = [];\n\n parse(parts: Part[]): ListItemsSectionCompProps | null {\n const title = headingText(parts, 2);\n if (!title) {\n return null;\n }\n\n const features: { title?: string; description: string }[] = [];\n const lists = getPartsByType<ListPart>(parts, 'list');\n\n for (const list of lists) {\n for (const item of list.meta?.items ?? []) {\n const itemParts = (item.meta?.parts ?? []) as Part[];\n const heading = itemParts.find((p) => p.type === 'heading');\n const textParts = itemParts.filter(\n (p) => p.type === 'text' || p.type === 'richText',\n );\n const featureDescription = textParts\n .map((p) => p.content?.trim())\n .filter(Boolean)\n .join('\\n');\n\n const markdown = (item.meta?.markdown as string)?.trim();\n const content = markdown || extractListItemText(item);\n const boldMatch = content.match(/^\\*\\*([^*]+)\\*\\*:?\\s*(.*)/s);\n\n if (heading) {\n features.push({\n title: heading.content?.trim(),\n description: featureDescription || content,\n });\n } else if (boldMatch) {\n features.push({\n title: boldMatch[1].trim(),\n description: boldMatch[2].trim() || featureDescription,\n });\n } else {\n features.push({\n description: featureDescription || content,\n });\n }\n }\n }\n\n // Valid structure is an h2 title followed by a 2+ item feature list.\n // Anything else is not this section — let the next definition try.\n if (features.length < MIN_FEATURES) {\n return null;\n }\n\n return { title, features };\n }\n}\n"],"mappings":"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","ignoreList":[]}
1
+ {"version":3,"names":["headingText","extractListItemText","inlinePartsToMarkdown","getPartsByType","MIN_FEATURES","ListItemsSectionDefinition","constructor","_defineProperty","title","features","parse","parts","lists","list","item","_list$meta","meta","items","_item$meta","_item$meta2","itemParts","heading","find","p","type","featureDescription","filter","markdown","trim","content","boldMatch","match","_heading$content","push","description","length"],"sources":["../../../../src/component/componentDefinitions/listItemsSectionDefinition.ts"],"sourcesContent":["import {\n headingText,\n extractListItemText,\n inlinePartsToMarkdown,\n} from './parse-utils';\nimport { getPartsByType, type Part, type ListPart } from '../../parts/parts';\nimport type {\n ListItemsSectionCompProps,\n ListItemsSectionComponent,\n SectionFixture,\n} from '../types';\nimport type { SectionDefinition } from '../section-definition';\n\n/** A valid list section needs at least this many feature items. */\nconst MIN_FEATURES = 2;\n\nexport class ListItemsSectionDefinition\n implements SectionDefinition<ListItemsSectionComponent>\n{\n sectionType = 'listItemsSection' as const;\n\n // Comment-delimited: the section spans from a `<!-- section:list -->` marker\n // and greedily captures the tokens that follow. The repetition deliberately\n // excludes `html_comment`, so the match naturally ends just before the next\n // section marker (or end of input) without consuming it — no explicit `~`\n // stop clause needed, and it still matches when this is the final section.\n // The structural contract is enforced in `parse()` instead of the pattern:\n // `parse()` returns null unless the content is an h2 followed by a 2+ item\n // feature list, so a non-conforming block falls through to the next\n // matching definition.\n patterns = [\n 'html_comment[section=\"list\"] > (h2 | h3 | t | callout | list | image | link | table | blockquote)*',\n ];\n\n defaults: ListItemsSectionCompProps = {\n title: 'Features',\n features: [],\n };\n\n fixtures: SectionFixture<ListItemsSectionCompProps>[] = [];\n\n parse(parts: Part[]): ListItemsSectionCompProps | null {\n const title = headingText(parts, 2);\n if (!title) {\n return null;\n }\n\n const features: { title?: string; description: string }[] = [];\n const lists = getPartsByType<ListPart>(parts, 'list');\n\n for (const list of lists) {\n for (const item of list.meta?.items ?? []) {\n const itemParts = (item.meta?.parts ?? []) as Part[];\n const heading = itemParts.find((p) => p.type === 'heading');\n // Everything except the heading is the body. Serialize it rather than\n // concatenating `content`, so an inline link survives: filtering to\n // text parts used to drop `link` parts wholesale, deleting both the\n // url and its label from the rendered item.\n //\n // The item's own `meta.markdown` is not usable here — for a\n // `- ### Title` item it runs the heading straight into the body with no\n // separator (\"Gentle daily washRead the ...\"), so it would duplicate\n // and mangle the title.\n const featureDescription = inlinePartsToMarkdown(\n itemParts.filter((p) => p !== heading),\n );\n\n const markdown = (item.meta?.markdown as string)?.trim();\n const content = markdown || extractListItemText(item);\n const boldMatch = content.match(/^\\*\\*([^*]+)\\*\\*:?\\s*(.*)/s);\n\n if (heading) {\n features.push({\n title: heading.content?.trim(),\n description: featureDescription || content,\n });\n } else if (boldMatch) {\n features.push({\n title: boldMatch[1].trim(),\n description: boldMatch[2].trim() || featureDescription,\n });\n } else {\n features.push({\n description: featureDescription || content,\n });\n }\n }\n }\n\n // Valid structure is an h2 title followed by a 2+ item feature list.\n // Anything else is not this section — let the next definition try.\n if (features.length < MIN_FEATURES) {\n return null;\n }\n\n return { title, features };\n }\n}\n"],"mappings":";AAAA,SACEA,WAAW,EACXC,mBAAmB,EACnBC,qBAAqB,QAChB,eAAe;AACtB,SAASC,cAAc,QAAkC,mBAAmB;AAQ5E;AACA,MAAMC,YAAY,GAAG,CAAC;AAEtB,OAAO,MAAMC,0BAA0B,CAEvC;EAAAC,YAAA;IAAAC,eAAA,sBACgB,kBAAkB;IAEhC;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IAAAA,eAAA,mBACW,CACT,oGAAoG,CACrG;IAAAA,eAAA,mBAEqC;MACpCC,KAAK,EAAE,UAAU;MACjBC,QAAQ,EAAE;IACZ,CAAC;IAAAF,eAAA,mBAEuD,EAAE;EAAA;EAE1DG,KAAKA,CAACC,KAAa,EAAoC;IACrD,MAAMH,KAAK,GAAGR,WAAW,CAACW,KAAK,EAAE,CAAC,CAAC;IACnC,IAAI,CAACH,KAAK,EAAE;MACV,OAAO,IAAI;IACb;IAEA,MAAMC,QAAmD,GAAG,EAAE;IAC9D,MAAMG,KAAK,GAAGT,cAAc,CAAWQ,KAAK,EAAE,MAAM,CAAC;IAErD,KAAK,MAAME,IAAI,IAAID,KAAK,EAAE;MACxB,KAAK,MAAME,IAAI,IAAI,EAAAC,UAAA,GAAAF,IAAI,CAACG,IAAI,qBAATD,UAAA,CAAWE,KAAK,KAAI,EAAE,EAAE;QAAA,IAAAF,UAAA,EAAAG,UAAA,EAAAC,WAAA;QACzC,MAAMC,SAAS,GAAI,EAAAF,UAAA,GAAAJ,IAAI,CAACE,IAAI,qBAATE,UAAA,CAAWP,KAAK,KAAI,EAAa;QACpD,MAAMU,OAAO,GAAGD,SAAS,CAACE,IAAI,CAAEC,CAAC,IAAKA,CAAC,CAACC,IAAI,KAAK,SAAS,CAAC;QAC3D;QACA;QACA;QACA;QACA;QACA;QACA;QACA;QACA;QACA,MAAMC,kBAAkB,GAAGvB,qBAAqB,CAC9CkB,SAAS,CAACM,MAAM,CAAEH,CAAC,IAAKA,CAAC,KAAKF,OAAO,CACvC,CAAC;QAED,MAAMM,QAAQ,IAAAR,WAAA,GAAIL,IAAI,CAACE,IAAI,cAAAG,WAAA,GAATA,WAAA,CAAWQ,QAAQ,qBAApBR,WAAA,CAAiCS,IAAI,CAAC,CAAC;QACxD,MAAMC,OAAO,GAAGF,QAAQ,IAAI1B,mBAAmB,CAACa,IAAI,CAAC;QACrD,MAAMgB,SAAS,GAAGD,OAAO,CAACE,KAAK,CAAC,4BAA4B,CAAC;QAE7D,IAAIV,OAAO,EAAE;UAAA,IAAAW,gBAAA;UACXvB,QAAQ,CAACwB,IAAI,CAAC;YACZzB,KAAK,GAAAwB,gBAAA,GAAEX,OAAO,CAACQ,OAAO,qBAAfG,gBAAA,CAAiBJ,IAAI,CAAC,CAAC;YAC9BM,WAAW,EAAET,kBAAkB,IAAII;UACrC,CAAC,CAAC;QACJ,CAAC,MAAM,IAAIC,SAAS,EAAE;UACpBrB,QAAQ,CAACwB,IAAI,CAAC;YACZzB,KAAK,EAAEsB,SAAS,CAAC,CAAC,CAAC,CAACF,IAAI,CAAC,CAAC;YAC1BM,WAAW,EAAEJ,SAAS,CAAC,CAAC,CAAC,CAACF,IAAI,CAAC,CAAC,IAAIH;UACtC,CAAC,CAAC;QACJ,CAAC,MAAM;UACLhB,QAAQ,CAACwB,IAAI,CAAC;YACZC,WAAW,EAAET,kBAAkB,IAAII;UACrC,CAAC,CAAC;QACJ;MACF;IACF;;IAEA;IACA;IACA,IAAIpB,QAAQ,CAAC0B,MAAM,GAAG/B,YAAY,EAAE;MAClC,OAAO,IAAI;IACb;IAEA,OAAO;MAAEI,KAAK;MAAEC;IAAS,CAAC;EAC5B;AACF","ignoreList":[]}
@@ -203,6 +203,42 @@ export function parseKpiItem(markdown) {
203
203
  description
204
204
  };
205
205
  }
206
+
207
+ /** Join inline tokens the way prose reads: single spaces, none before punctuation. */
208
+ function appendInlineToken(current, token) {
209
+ const trimmed = token.trim();
210
+ if (!trimmed) {
211
+ return current;
212
+ }
213
+ if (!current) {
214
+ return trimmed;
215
+ }
216
+ if (/^[,.;:!?)]/.test(trimmed)) {
217
+ return `${current}${trimmed}`;
218
+ }
219
+ return `${current} ${trimmed}`;
220
+ }
221
+
222
+ /**
223
+ * Serialize a run of inline parts back to markdown, preserving links.
224
+ *
225
+ * Concatenating `part.content` alone silently deletes links: a `link` part
226
+ * keeps its label in `content` and its url in `meta.href`, so a filter that
227
+ * reads only text parts turns `Read the [pricing page](/pricing) first` into
228
+ * `Read the first` — url and label both vanish, with nothing in the output to
229
+ * show anything was lost.
230
+ */
231
+ export function inlinePartsToMarkdown(parts) {
232
+ return parts.reduce((text, part) => {
233
+ if (part.type === 'link') {
234
+ var _meta2, _part$content;
235
+ const href = ((_meta2 = part.meta) == null ? void 0 : _meta2.href) ?? '';
236
+ const label = ((_part$content = part.content) == null ? void 0 : _part$content.trim()) ?? '';
237
+ return appendInlineToken(text, href ? `[${label}](${href})` : label);
238
+ }
239
+ return appendInlineToken(text, part.content ?? '');
240
+ }, '');
241
+ }
206
242
  export function extractListItemText(item) {
207
243
  var _item$meta2;
208
244
  if (item.content) {
@@ -302,8 +338,8 @@ export function extractEntityRefs(parts) {
302
338
  var _list$meta4, _item$meta4;
303
339
  const itemParts = ((_item$meta4 = item.meta) == null ? void 0 : _item$meta4.parts) ?? [];
304
340
  const entityLinks = itemParts.filter(p => {
305
- var _meta2;
306
- return p.type === 'link' && isEntityHref((_meta2 = p.meta) == null ? void 0 : _meta2.href);
341
+ var _meta3;
342
+ return p.type === 'link' && isEntityHref((_meta3 = p.meta) == null ? void 0 : _meta3.href);
307
343
  });
308
344
  for (const link of entityLinks) {
309
345
  const parsed = parseEntityUrl(link.meta.href);
@@ -362,18 +398,18 @@ export function extractEntityRefs(parts) {
362
398
  if (linkIndex >= 0) {
363
399
  // Scan following parts for description, image, callout, and list items
364
400
  for (let j = linkIndex + 1; j < parts.length; j++) {
365
- var _meta3, _p$content3;
401
+ var _meta4, _p$content3;
366
402
  const p = parts[j];
367
403
  // Stop at next entity link
368
- if (p.type === 'link' && isEntityHref((_meta3 = p.meta) == null ? void 0 : _meta3.href)) {
404
+ if (p.type === 'link' && isEntityHref((_meta4 = p.meta) == null ? void 0 : _meta4.href)) {
369
405
  break;
370
406
  }
371
407
  if (p.type === 'text' && (_p$content3 = p.content) != null && _p$content3.trim()) {
372
408
  const text = markdownOrText(p);
373
409
  description = description ? `${description} ${text}` : text;
374
410
  } else if (p.type === 'image' && !markdownImageUrl) {
375
- var _meta4;
376
- markdownImageUrl = (_meta4 = p.meta) == null ? void 0 : _meta4.src;
411
+ var _meta5;
412
+ markdownImageUrl = (_meta5 = p.meta) == null ? void 0 : _meta5.src;
377
413
  } else if ((p.type === 'callout' || p.type === 'blockquote') && !callout) {
378
414
  var _p$meta4;
379
415
  const raw = ((_p$meta4 = p.meta) == null || (_p$meta4 = _p$meta4.markdown) == null ? void 0 : _p$meta4.trim()) || str(p);
@@ -385,8 +421,8 @@ export function extractEntityRefs(parts) {
385
421
  };
386
422
  }
387
423
  } else if (p.type === 'list') {
388
- for (const item of ((_meta5 = p.meta) == null ? void 0 : _meta5.items) ?? []) {
389
- var _meta5;
424
+ for (const item of ((_meta6 = p.meta) == null ? void 0 : _meta6.items) ?? []) {
425
+ var _meta6;
390
426
  const text = extractListItemText(item);
391
427
  if (text) {
392
428
  features.push(text);