@wix/web5-core 1.58.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 (103) hide show
  1. package/dist/cjs/client/applyThemeOverrides.js +89 -0
  2. package/dist/cjs/client/applyThemeOverrides.js.map +1 -0
  3. package/dist/cjs/client/clientBundleUrl.js +43 -0
  4. package/dist/cjs/client/clientBundleUrl.js.map +1 -1
  5. package/dist/cjs/component/componentDefinitions/ComparisonSectionDefinition.js +33 -1
  6. package/dist/cjs/component/componentDefinitions/ComparisonSectionDefinition.js.map +1 -1
  7. package/dist/cjs/component/componentDefinitions/FeatureSection9PlusDefinition.js +9 -5
  8. package/dist/cjs/component/componentDefinitions/FeatureSection9PlusDefinition.js.map +1 -1
  9. package/dist/cjs/component/componentDefinitions/HeroEntitySectionDefinition.js +5 -2
  10. package/dist/cjs/component/componentDefinitions/HeroEntitySectionDefinition.js.map +1 -1
  11. package/dist/cjs/component/componentDefinitions/HeroSectionDefinition.js +4 -1
  12. package/dist/cjs/component/componentDefinitions/HeroSectionDefinition.js.map +1 -1
  13. package/dist/cjs/component/componentDefinitions/NextStepsSectionDefinition.js +35 -2
  14. package/dist/cjs/component/componentDefinitions/NextStepsSectionDefinition.js.map +1 -1
  15. package/dist/cjs/component/componentDefinitions/TextBlockSectionDefinition.js +27 -1
  16. package/dist/cjs/component/componentDefinitions/TextBlockSectionDefinition.js.map +1 -1
  17. package/dist/cjs/component/componentDefinitions/index.js +8 -1
  18. package/dist/cjs/component/componentDefinitions/index.js.map +1 -1
  19. package/dist/cjs/component/componentDefinitions/listItemsSectionDefinition.js +10 -5
  20. package/dist/cjs/component/componentDefinitions/listItemsSectionDefinition.js.map +1 -1
  21. package/dist/cjs/component/componentDefinitions/parse-utils.js +45 -8
  22. package/dist/cjs/component/componentDefinitions/parse-utils.js.map +1 -1
  23. package/dist/cjs/component/componentParser.js +15 -0
  24. package/dist/cjs/component/componentParser.js.map +1 -1
  25. package/dist/cjs/component/section-definition.js +14 -0
  26. package/dist/cjs/component/section-definition.js.map +1 -1
  27. package/dist/cjs/component/types.js.map +1 -1
  28. package/dist/cjs/components/ui/MarkdownText.js +41 -42
  29. package/dist/cjs/components/ui/MarkdownText.js.map +1 -1
  30. package/dist/cjs/components/ui/UnifiedLink.js +22 -10
  31. package/dist/cjs/components/ui/UnifiedLink.js.map +1 -1
  32. package/dist/cjs/hostScope.js.map +1 -1
  33. package/dist/cjs/index.js +13 -3
  34. package/dist/cjs/index.js.map +1 -1
  35. package/dist/cjs/utils/markdownPreprocessor.js +162 -8
  36. package/dist/cjs/utils/markdownPreprocessor.js.map +1 -1
  37. package/dist/esm/client/applyThemeOverrides.js +85 -0
  38. package/dist/esm/client/applyThemeOverrides.js.map +1 -0
  39. package/dist/esm/client/clientBundleUrl.js +39 -0
  40. package/dist/esm/client/clientBundleUrl.js.map +1 -1
  41. package/dist/esm/component/componentDefinitions/ComparisonSectionDefinition.js +34 -1
  42. package/dist/esm/component/componentDefinitions/ComparisonSectionDefinition.js.map +1 -1
  43. package/dist/esm/component/componentDefinitions/FeatureSection9PlusDefinition.js +10 -6
  44. package/dist/esm/component/componentDefinitions/FeatureSection9PlusDefinition.js.map +1 -1
  45. package/dist/esm/component/componentDefinitions/HeroEntitySectionDefinition.js +6 -3
  46. package/dist/esm/component/componentDefinitions/HeroEntitySectionDefinition.js.map +1 -1
  47. package/dist/esm/component/componentDefinitions/HeroSectionDefinition.js +5 -2
  48. package/dist/esm/component/componentDefinitions/HeroSectionDefinition.js.map +1 -1
  49. package/dist/esm/component/componentDefinitions/NextStepsSectionDefinition.js +36 -3
  50. package/dist/esm/component/componentDefinitions/NextStepsSectionDefinition.js.map +1 -1
  51. package/dist/esm/component/componentDefinitions/TextBlockSectionDefinition.js +27 -1
  52. package/dist/esm/component/componentDefinitions/TextBlockSectionDefinition.js.map +1 -1
  53. package/dist/esm/component/componentDefinitions/index.js +8 -1
  54. package/dist/esm/component/componentDefinitions/index.js.map +1 -1
  55. package/dist/esm/component/componentDefinitions/listItemsSectionDefinition.js +11 -6
  56. package/dist/esm/component/componentDefinitions/listItemsSectionDefinition.js.map +1 -1
  57. package/dist/esm/component/componentDefinitions/parse-utils.js +44 -8
  58. package/dist/esm/component/componentDefinitions/parse-utils.js.map +1 -1
  59. package/dist/esm/component/componentParser.js +15 -0
  60. package/dist/esm/component/componentParser.js.map +1 -1
  61. package/dist/esm/component/section-definition.js +13 -1
  62. package/dist/esm/component/section-definition.js.map +1 -1
  63. package/dist/esm/component/types.js.map +1 -1
  64. package/dist/esm/components/ui/MarkdownText.js +8 -9
  65. package/dist/esm/components/ui/MarkdownText.js.map +1 -1
  66. package/dist/esm/components/ui/UnifiedLink.js +7 -1
  67. package/dist/esm/components/ui/UnifiedLink.js.map +1 -1
  68. package/dist/esm/hostScope.js.map +1 -1
  69. package/dist/esm/index.js +4 -1
  70. package/dist/esm/index.js.map +1 -1
  71. package/dist/esm/utils/markdownPreprocessor.js +160 -8
  72. package/dist/esm/utils/markdownPreprocessor.js.map +1 -1
  73. package/dist/types/client/applyThemeOverrides.d.ts +19 -0
  74. package/dist/types/client/applyThemeOverrides.d.ts.map +1 -0
  75. package/dist/types/client/clientBundleUrl.d.ts +22 -0
  76. package/dist/types/client/clientBundleUrl.d.ts.map +1 -1
  77. package/dist/types/component/componentDefinitions/ComparisonSectionDefinition.d.ts +2 -2
  78. package/dist/types/component/componentDefinitions/ComparisonSectionDefinition.d.ts.map +1 -1
  79. package/dist/types/component/componentDefinitions/FeatureSection9PlusDefinition.d.ts.map +1 -1
  80. package/dist/types/component/componentDefinitions/HeroEntitySectionDefinition.d.ts.map +1 -1
  81. package/dist/types/component/componentDefinitions/HeroSectionDefinition.d.ts.map +1 -1
  82. package/dist/types/component/componentDefinitions/NextStepsSectionDefinition.d.ts.map +1 -1
  83. package/dist/types/component/componentDefinitions/TextBlockSectionDefinition.d.ts.map +1 -1
  84. package/dist/types/component/componentDefinitions/index.d.ts.map +1 -1
  85. package/dist/types/component/componentDefinitions/listItemsSectionDefinition.d.ts.map +1 -1
  86. package/dist/types/component/componentDefinitions/parse-utils.d.ts +10 -0
  87. package/dist/types/component/componentDefinitions/parse-utils.d.ts.map +1 -1
  88. package/dist/types/component/componentParser.d.ts.map +1 -1
  89. package/dist/types/component/section-definition.d.ts +13 -1
  90. package/dist/types/component/section-definition.d.ts.map +1 -1
  91. package/dist/types/component/types.d.ts +7 -0
  92. package/dist/types/component/types.d.ts.map +1 -1
  93. package/dist/types/components/ui/MarkdownText.d.ts +13 -0
  94. package/dist/types/components/ui/MarkdownText.d.ts.map +1 -1
  95. package/dist/types/components/ui/UnifiedLink.d.ts +1 -1
  96. package/dist/types/components/ui/UnifiedLink.d.ts.map +1 -1
  97. package/dist/types/hostScope.d.ts +11 -0
  98. package/dist/types/hostScope.d.ts.map +1 -1
  99. package/dist/types/index.d.ts +4 -2
  100. package/dist/types/index.d.ts.map +1 -1
  101. package/dist/types/utils/markdownPreprocessor.d.ts +30 -5
  102. package/dist/types/utils/markdownPreprocessor.d.ts.map +1 -1
  103. package/package.json +2 -2
@@ -8,6 +8,8 @@ exports.insertMissingTableSeparators = insertMissingTableSeparators;
8
8
  exports.normalizeIconUrls = normalizeIconUrls;
9
9
  exports.preprocessMarkdown = preprocessMarkdown;
10
10
  exports.recoverOrphanedEmphasisLinks = recoverOrphanedEmphasisLinks;
11
+ exports.repairOrphanedWeb5Links = repairOrphanedWeb5Links;
12
+ exports.scrubVisibleWeb5Urls = scrubVisibleWeb5Urls;
11
13
  exports.trimTrailingWhitespace = trimTrailingWhitespace;
12
14
  /**
13
15
  * Markdown preprocessing utilities
@@ -81,6 +83,148 @@ function recoverOrphanedEmphasisLinks(markdown) {
81
83
  };
82
84
  }
83
85
 
86
+ /**
87
+ * A `web5://` link target whose opening `[` never arrived, e.g.
88
+ * `try filtering for men](web5://ask/Which+are+for+men%3F)`.
89
+ *
90
+ * Dropping the opening bracket is the most common way an inline ask link is
91
+ * mis-emitted, and it is the worst one: remark parses nothing as a link, so the
92
+ * scheme renders as literal text and the visitor reads `web5://ask/...`.
93
+ *
94
+ * `run` is the text before the `]` on the same line, which is what the label
95
+ * should have been. It may be empty, in which case there is nothing to promote
96
+ * and the construct is dropped instead.
97
+ */
98
+ const ORPHANED_WEB5_TARGET = /([^[\]()*\n]{0,80}?)\]\((web5:\/\/[^)\s]*)\)/g;
99
+
100
+ /** Any `web5://` url, for the final check that none is visible as text. */
101
+ const ANY_WEB5_URL = /web5:\/\/[^\s)\]]*/g;
102
+
103
+ /** Longest label we will promote out of surrounding prose. */
104
+ const MAX_RECOVERED_LABEL_WORDS = 4;
105
+
106
+ /**
107
+ * Words a label should start *after* rather than with. The label is cut at the
108
+ * last of these inside the window, so a promoted phrase reads as a noun phrase
109
+ * ("sensitive skin") instead of trailing off a verb or preposition
110
+ * ("carry for sensitive skin").
111
+ */
112
+ const LABEL_BOUNDARY_WORDS = new Set(['a', 'an', 'and', 'at', 'but', 'by', 'for', 'from', 'in', 'into', 'of', 'on', 'or', 'the', 'to', 'with']);
113
+
114
+ /**
115
+ * True when `segment` holds a `[` that is still waiting to be closed — meaning
116
+ * the `](` after it closes a real link and there is nothing to repair.
117
+ */
118
+ function hasOpenLinkBracket(segment) {
119
+ return segment.lastIndexOf('[') > segment.lastIndexOf(']');
120
+ }
121
+
122
+ /**
123
+ * Split `run` into the prose to leave in place and the phrase to promote into
124
+ * the link label. Slices on token offsets rather than rejoining words, so the
125
+ * original spacing around the label is preserved exactly.
126
+ *
127
+ * An empty label means there is nothing worth promoting and the caller should
128
+ * drop the link instead.
129
+ */
130
+ function splitRecoveredLabel(run) {
131
+ // Never let the label cross a sentence boundary.
132
+ let sentenceStart = 0;
133
+ for (const boundary of run.matchAll(/(?<=[.!?;:])\s+/g)) {
134
+ sentenceStart = (boundary.index ?? 0) + boundary[0].length;
135
+ }
136
+ const tokens = [...run.slice(sentenceStart).matchAll(/\S+/g)].map(token => ({
137
+ word: token[0],
138
+ start: sentenceStart + (token.index ?? 0)
139
+ }));
140
+ let window = tokens.slice(-MAX_RECOVERED_LABEL_WORDS);
141
+ for (let i = window.length - 1; i >= 0; i--) {
142
+ const bare = window[i].word.toLowerCase().replace(/[^a-z]/g, '');
143
+ if (LABEL_BOUNDARY_WORDS.has(bare)) {
144
+ window = window.slice(i + 1);
145
+ break;
146
+ }
147
+ }
148
+ if (window.length === 0) {
149
+ return {
150
+ prefix: run,
151
+ label: ''
152
+ };
153
+ }
154
+ return {
155
+ prefix: run.slice(0, window[0].start),
156
+ label: run.slice(window[0].start).trim()
157
+ };
158
+ }
159
+
160
+ /**
161
+ * Repair `web5://` links that lost their opening `[`.
162
+ *
163
+ * Recovers the link by promoting the words immediately before the `]` into its
164
+ * label; when there are no words to promote, drops the whole construct. Either
165
+ * way the scheme stops being visible, which is the point — a link the visitor
166
+ * cannot use is a smaller failure than a url the visitor can read.
167
+ */
168
+ function repairOrphanedWeb5Links(markdown) {
169
+ const diagnostics = [];
170
+ const result = markdown.replace(ORPHANED_WEB5_TARGET, (match, run, href, offset, whole) => {
171
+ const lineStart = whole.lastIndexOf('\n', offset) + 1;
172
+ if (hasOpenLinkBracket(whole.slice(lineStart, offset))) {
173
+ // A real `[label](url)` — the bracket is there, leave it alone.
174
+ return match;
175
+ }
176
+ const {
177
+ prefix,
178
+ label
179
+ } = splitRecoveredLabel(run);
180
+ if (!label) {
181
+ diagnostics.push({
182
+ type: 'orphaned_web5_link_dropped',
183
+ original: match,
184
+ normalized: run
185
+ });
186
+ return run;
187
+ }
188
+ const normalized = `${prefix}[${label}](${href})`;
189
+ diagnostics.push({
190
+ type: 'orphaned_web5_link_recovered',
191
+ original: match,
192
+ normalized
193
+ });
194
+ return normalized;
195
+ });
196
+ return {
197
+ result,
198
+ diagnostics
199
+ };
200
+ }
201
+
202
+ /**
203
+ * Last resort: remove any `web5://` url still sitting in visible text.
204
+ *
205
+ * Runs after every recovery step, so anything left here is a url no repair
206
+ * could turn back into a link. A url in a link target (`](web5://...)`) is
207
+ * left alone — only text the visitor would actually read is scrubbed.
208
+ */
209
+ function scrubVisibleWeb5Urls(markdown) {
210
+ const diagnostics = [];
211
+ const result = markdown.replace(ANY_WEB5_URL, (match, offset, whole) => {
212
+ if (whole.slice(offset - 2, offset) === '](') {
213
+ return match;
214
+ }
215
+ diagnostics.push({
216
+ type: 'visible_web5_url_scrubbed',
217
+ original: match,
218
+ normalized: ''
219
+ });
220
+ return '';
221
+ });
222
+ return {
223
+ result,
224
+ diagnostics
225
+ };
226
+ }
227
+
84
228
  /**
85
229
  * Trim trailing whitespace from lines that contain text
86
230
  *
@@ -231,11 +375,13 @@ function decodeLinkText(markdown) {
231
375
  * The preprocessing steps are applied in the following order:
232
376
  * 1. Recover orphaned emphasized web5 links
233
377
  * 2. Fix malformed links (remove whitespace between ] and () - must happen before web5 links are matched)
234
- * 3. Decode URL-encoded link text
235
- * 4. Escape web5:// links (URL-encode URLs)
236
- * 5. Normalize icon URLs
237
- * 6. Insert missing table separators for obvious pipe tables
238
- * 7. Trim trailing whitespace from lines
378
+ * 3. Repair web5 links missing their opening `[` (after step 2, so `] (url)` is already normalized)
379
+ * 4. Decode URL-encoded link text
380
+ * 5. Escape web5:// links (URL-encode URLs)
381
+ * 6. Normalize icon URLs
382
+ * 7. Insert missing table separators for obvious pipe tables
383
+ * 8. Scrub any web5:// url still visible as text (last, so every repair has had its chance)
384
+ * 9. Trim trailing whitespace from lines
239
385
  *
240
386
  * @param markdown - The raw markdown string to preprocess
241
387
  * @returns The preprocessed markdown string ready for AST parsing
@@ -246,10 +392,14 @@ function preprocessMarkdown(markdown) {
246
392
  diagnostics: recoveredLinkDiagnostics
247
393
  } = recoverOrphanedEmphasisLinks(markdown);
248
394
  let processed = fixMalformedLinks(recoveredLinks);
395
+ const {
396
+ result: repairedLinks,
397
+ diagnostics: repairedLinkDiagnostics
398
+ } = repairOrphanedWeb5Links(processed);
249
399
  const {
250
400
  result: decodedText,
251
401
  diagnostics: decodeDiagnostics
252
- } = decodeLinkText(processed);
402
+ } = decodeLinkText(repairedLinks);
253
403
  processed = escapeWeb5Links(decodedText);
254
404
  const {
255
405
  result: normalized,
@@ -259,10 +409,14 @@ function preprocessMarkdown(markdown) {
259
409
  result: tableNormalized,
260
410
  diagnostics: tableDiagnostics
261
411
  } = insertMissingTableSeparators(normalized);
262
- processed = trimTrailingWhitespace(tableNormalized);
412
+ const {
413
+ result: scrubbed,
414
+ diagnostics: scrubDiagnostics
415
+ } = scrubVisibleWeb5Urls(tableNormalized);
416
+ processed = trimTrailingWhitespace(scrubbed);
263
417
  return {
264
418
  markdown: processed,
265
- diagnostics: [...recoveredLinkDiagnostics, ...decodeDiagnostics, ...iconDiagnostics, ...tableDiagnostics]
419
+ diagnostics: [...recoveredLinkDiagnostics, ...repairedLinkDiagnostics, ...decodeDiagnostics, ...iconDiagnostics, ...tableDiagnostics, ...scrubDiagnostics]
266
420
  };
267
421
  }
268
422
  //# sourceMappingURL=markdownPreprocessor.js.map
@@ -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":";;;;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;;AAaA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAASA,eAAeA,CAACC,QAAgB,EAAU;EACxD;EACA;EACA,OAAOA,QAAQ,CAACC,OAAO,CACrB,kDAAkD,EAClD,CAACC,KAAK,EAAEC,QAAQ,EAAEC,QAAQ,EAAEC,GAAG,KAAK;IAClC,IAAI;MACF;MACA,MAAMC,UAAU,GAAGC,kBAAkB,CAACF,GAAG,CAAC;MAC1C;MACA;MACA,MAAMG,UAAU,GAAGC,SAAS,CAACH,UAAU,CAAC;MACxC,OAAO,IAAIH,QAAQ,KAAKC,QAAQ,MAAMI,UAAU,GAAG;IACrD,CAAC,CAAC,MAAM;MACN;MACA;MACA;MACA;MACA;MACA;MACA,OAAON,KAAK;IACd;EACF,CACF,CAAC;AACH;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAASQ,iBAAiBA,CAACV,QAAgB,EAAU;EAC1D;EACA;EACA,OAAOA,QAAQ,CAACC,OAAO,CAAC,gBAAgB,EAAE,IAAI,CAAC;AACjD;;AAEA;AACA;AACA;AACA;AACO,SAASU,4BAA4BA,CAACX,QAAgB,EAG3D;EACA,MAAMY,WAAqC,GAAG,EAAE;EAChD,MAAMC,MAAM,GAAGb,QAAQ,CAACC,OAAO,CAC7B,sEAAsE,EACtE,CAACC,KAAK,EAAEY,MAAc,EAAEX,QAAgB,EAAEY,IAAY,KAAK;IACzD,MAAMC,UAAU,GAAG,GAAGF,MAAM,IAAIX,QAAQ,CAACc,IAAI,CAAC,CAAC,KAAKF,IAAI,GAAG;IAC3DH,WAAW,CAACM,IAAI,CAAC;MACfC,IAAI,EAAE,kCAAkC;MACxCC,QAAQ,EAAElB,KAAK;MACfc;IACF,CAAC,CAAC;IACF,OAAOA,UAAU;EACnB,CACF,CAAC;EACD,OAAO;IAAEH,MAAM;IAAED;EAAY,CAAC;AAChC;;AAEA;AACA;AACA;AACA;AACA;AACA;AACO,SAASS,sBAAsBA,CAACrB,QAAgB,EAAU;EAC/D,MAAMsB,KAAK,GAAGtB,QAAQ,CAACuB,KAAK,CAAC,IAAI,CAAC;EAClC,MAAMC,cAAwB,GAAG,EAAE;EAEnC,KAAK,MAAMC,IAAI,IAAIH,KAAK,EAAE;IACxB,MAAMI,WAAW,GAAGD,IAAI,CAACR,IAAI,CAAC,CAAC;;IAE/B;IACA,IAAIS,WAAW,CAACC,MAAM,GAAG,CAAC,EAAE;MAC1BH,cAAc,CAACN,IAAI,CAACO,IAAI,CAACG,OAAO,CAAC,CAAC,CAAC;IACrC,CAAC,MAAM;MACL;MACAJ,cAAc,CAACN,IAAI,CAAC,EAAE,CAAC;IACzB;EACF;EAEA,OAAOM,cAAc,CAACK,IAAI,CAAC,IAAI,CAAC;AAClC;;AAEA;AACA;AACA;AACA;AACA;AACO,SAASC,iBAAiBA,CAAC9B,QAAgB,EAGhD;EACA,MAAMY,WAAqC,GAAG,EAAE;EAChD,MAAMC,MAAM,GAAGb,QAAQ,CAACC,OAAO,CAC7B,yDAAyD,EACzD,CAACC,KAAK,EAAE6B,IAAwB,EAAEC,SAA6B,KAAK;IAClE,MAAMC,SAAS,GAAG,CAACD,SAAS,IAAID,IAAI,IAAI,EAAE,EACvC9B,OAAO,CAAC,gBAAgB,EAAE,EAAE,CAAC,CAC7BA,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC;IAC1B,MAAMe,UAAU,GAAG,eAAeiB,SAAS,EAAE;IAC7C,IAAI/B,KAAK,KAAKc,UAAU,EAAE;MACxBJ,WAAW,CAACM,IAAI,CAAC;QACfC,IAAI,EAAE,qBAAqB;QAC3BC,QAAQ,EAAElB,KAAK;QACfc;MACF,CAAC,CAAC;IACJ;IACA,OAAOA,UAAU;EACnB,CACF,CAAC;EACD,OAAO;IAAEH,MAAM;IAAED;EAAY,CAAC;AAChC;AAEA,SAASsB,aAAaA,CAACT,IAAY,EAAW;EAC5C,OAAO,mBAAmB,CAACU,IAAI,CAACV,IAAI,CAAC;AACvC;AAEA,SAASW,YAAYA,CAACX,IAAY,EAAY;EAC5C,MAAMY,OAAO,GAAGZ,IAAI,CAACR,IAAI,CAAC,CAAC;EAC3B,IAAI,CAACoB,OAAO,CAACC,UAAU,CAAC,GAAG,CAAC,IAAI,CAACD,OAAO,CAACE,QAAQ,CAAC,GAAG,CAAC,EAAE;IACtD,OAAO,EAAE;EACX;EACA,OAAOF,OAAO,CACXG,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CACZjB,KAAK,CAAC,GAAG,CAAC,CACVkB,GAAG,CAAEC,IAAI,IAAKA,IAAI,CAACzB,IAAI,CAAC,CAAC,CAAC;AAC/B;AAEA,SAAS0B,SAASA,CAAClB,IAAY,EAAW;EACxC,OAAOW,YAAY,CAACX,IAAI,CAAC,CAACE,MAAM,IAAI,CAAC;AACvC;AAEA,SAASiB,mBAAmBA,CAACnB,IAAY,EAAW;EAClD,MAAMoB,KAAK,GAAGT,YAAY,CAACX,IAAI,CAAC;EAChC,OACEoB,KAAK,CAAClB,MAAM,IAAI,CAAC,IACjBkB,KAAK,CAACC,KAAK,CAAEJ,IAAI,IAAK,aAAa,CAACP,IAAI,CAACO,IAAI,CAACzC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,CAAC;AAEvE;;AAEA;AACA;AACA;AACA;AACA;AACO,SAAS8C,4BAA4BA,CAAC/C,QAAgB,EAG3D;EACA,MAAMY,WAAqC,GAAG,EAAE;EAChD,MAAMU,KAAK,GAAGtB,QAAQ,CAACuB,KAAK,CAAC,IAAI,CAAC;EAClC,MAAMV,MAAgB,GAAG,EAAE;EAC3B,IAAImC,OAAO,GAAG,KAAK;EAEnB,KAAK,IAAIC,CAAC,GAAG,CAAC,EAAEA,CAAC,GAAG3B,KAAK,CAACK,MAAM,EAAEsB,CAAC,EAAE,EAAE;IACrC,MAAMxB,IAAI,GAAGH,KAAK,CAAC2B,CAAC,CAAC;IACrB,MAAMC,YAAY,GAAGrC,MAAM,CAACA,MAAM,CAACc,MAAM,GAAG,CAAC,CAAC,IAAI,EAAE;IACpD,MAAMwB,QAAQ,GAAG7B,KAAK,CAAC2B,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE;IAEnC,IAAIf,aAAa,CAACT,IAAI,CAAC,EAAE;MACvBuB,OAAO,GAAG,CAACA,OAAO;MAClBnC,MAAM,CAACK,IAAI,CAACO,IAAI,CAAC;MACjB;IACF;IAEAZ,MAAM,CAACK,IAAI,CAACO,IAAI,CAAC;IAEjB,IAAIuB,OAAO,IAAI,CAACL,SAAS,CAAClB,IAAI,CAAC,IAAIkB,SAAS,CAACO,YAAY,CAAC,EAAE;MAC1D;IACF;IACA,IAAI,CAACP,SAAS,CAACQ,QAAQ,CAAC,IAAIP,mBAAmB,CAACO,QAAQ,CAAC,EAAE;MACzD;IACF;IAEA,MAAMC,WAAW,GAAGhB,YAAY,CAACX,IAAI,CAAC;IACtC,MAAM4B,SAAS,GAAGjB,YAAY,CAACe,QAAQ,CAAC;IACxC,IAAIC,WAAW,CAACzB,MAAM,KAAK0B,SAAS,CAAC1B,MAAM,EAAE;MAC3C;IACF;IAEA,MAAM2B,SAAS,GAAG,KAAKF,WAAW,CAACX,GAAG,CAAC,MAAM,KAAK,CAAC,CAACZ,IAAI,CAAC,KAAK,CAAC,IAAI;IACnEhB,MAAM,CAACK,IAAI,CAACoC,SAAS,CAAC;IACtB1C,WAAW,CAACM,IAAI,CAAC;MACfC,IAAI,EAAE,0BAA0B;MAChCC,QAAQ,EAAEK,IAAI;MACdT,UAAU,EAAE,GAAGS,IAAI,KAAK6B,SAAS;IACnC,CAAC,CAAC;EACJ;EAEA,OAAO;IAAEzC,MAAM,EAAEA,MAAM,CAACgB,IAAI,CAAC,IAAI,CAAC;IAAEjB;EAAY,CAAC;AACnD;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAAS2C,cAAcA,CAACvD,QAAgB,EAG7C;EACA,MAAMY,WAAqC,GAAG,EAAE;EAChD,MAAMC,MAAM,GAAGb,QAAQ,CAACC,OAAO,CAC7B,iBAAiB,EACjB,CAACC,KAAK,EAAEC,QAAgB,KAAK;IAC3B,IAAI;MACF,MAAMqD,OAAO,GAAGjD,kBAAkB,CAACJ,QAAQ,CAACF,OAAO,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;MAClE,IAAIuD,OAAO,KAAKrD,QAAQ,EAAE;QACxBS,WAAW,CAACM,IAAI,CAAC;UACfC,IAAI,EAAE,mBAAmB;UACzBC,QAAQ,EAAEjB,QAAQ;UAClBa,UAAU,EAAEwC;QACd,CAAC,CAAC;MACJ;MACA,OAAO,IAAIA,OAAO,IAAI;IACxB,CAAC,CAAC,MAAM;MACN;MACA;MACA,OAAOtD,KAAK;IACd;EACF,CACF,CAAC;EACD,OAAO;IAAEW,MAAM;IAAED;EAAY,CAAC;AAChC;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAAS6C,kBAAkBA,CAACzD,QAAgB,EAAoB;EACrE,MAAM;IAAEa,MAAM,EAAE6C,cAAc;IAAE9C,WAAW,EAAE+C;EAAyB,CAAC,GACrEhD,4BAA4B,CAACX,QAAQ,CAAC;EACxC,IAAI4D,SAAS,GAAGlD,iBAAiB,CAACgD,cAAc,CAAC;EACjD,MAAM;IAAE7C,MAAM,EAAEgD,WAAW;IAAEjD,WAAW,EAAEkD;EAAkB,CAAC,GAC3DP,cAAc,CAACK,SAAS,CAAC;EAC3BA,SAAS,GAAG7D,eAAe,CAAC8D,WAAW,CAAC;EACxC,MAAM;IAAEhD,MAAM,EAAEG,UAAU;IAAEJ,WAAW,EAAEmD;EAAgB,CAAC,GACxDjC,iBAAiB,CAAC8B,SAAS,CAAC;EAC9B,MAAM;IAAE/C,MAAM,EAAEmD,eAAe;IAAEpD,WAAW,EAAEqD;EAAiB,CAAC,GAC9DlB,4BAA4B,CAAC/B,UAAU,CAAC;EAC1C4C,SAAS,GAAGvC,sBAAsB,CAAC2C,eAAe,CAAC;EACnD,OAAO;IACLhE,QAAQ,EAAE4D,SAAS;IACnBhD,WAAW,EAAE,CACX,GAAG+C,wBAAwB,EAC3B,GAAGG,iBAAiB,EACpB,GAAGC,eAAe,EAClB,GAAGE,gBAAgB;EAEvB,CAAC;AACH","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":";;;;;;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;;AAaA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAASA,eAAeA,CAACC,QAAgB,EAAU;EACxD;EACA;EACA,OAAOA,QAAQ,CAACC,OAAO,CACrB,kDAAkD,EAClD,CAACC,KAAK,EAAEC,QAAQ,EAAEC,QAAQ,EAAEC,GAAG,KAAK;IAClC,IAAI;MACF;MACA,MAAMC,UAAU,GAAGC,kBAAkB,CAACF,GAAG,CAAC;MAC1C;MACA;MACA,MAAMG,UAAU,GAAGC,SAAS,CAACH,UAAU,CAAC;MACxC,OAAO,IAAIH,QAAQ,KAAKC,QAAQ,MAAMI,UAAU,GAAG;IACrD,CAAC,CAAC,MAAM;MACN;MACA;MACA;MACA;MACA;MACA;MACA,OAAON,KAAK;IACd;EACF,CACF,CAAC;AACH;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAASQ,iBAAiBA,CAACV,QAAgB,EAAU;EAC1D;EACA;EACA,OAAOA,QAAQ,CAACC,OAAO,CAAC,gBAAgB,EAAE,IAAI,CAAC;AACjD;;AAEA;AACA;AACA;AACA;AACO,SAASU,4BAA4BA,CAACX,QAAgB,EAG3D;EACA,MAAMY,WAAqC,GAAG,EAAE;EAChD,MAAMC,MAAM,GAAGb,QAAQ,CAACC,OAAO,CAC7B,sEAAsE,EACtE,CAACC,KAAK,EAAEY,MAAc,EAAEX,QAAgB,EAAEY,IAAY,KAAK;IACzD,MAAMC,UAAU,GAAG,GAAGF,MAAM,IAAIX,QAAQ,CAACc,IAAI,CAAC,CAAC,KAAKF,IAAI,GAAG;IAC3DH,WAAW,CAACM,IAAI,CAAC;MACfC,IAAI,EAAE,kCAAkC;MACxCC,QAAQ,EAAElB,KAAK;MACfc;IACF,CAAC,CAAC;IACF,OAAOA,UAAU;EACnB,CACF,CAAC;EACD,OAAO;IAAEH,MAAM;IAAED;EAAY,CAAC;AAChC;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAMS,oBAAoB,GAAG,+CAA+C;;AAE5E;AACA,MAAMC,YAAY,GAAG,qBAAqB;;AAE1C;AACA,MAAMC,yBAAyB,GAAG,CAAC;;AAEnC;AACA;AACA;AACA;AACA;AACA;AACA,MAAMC,oBAAoB,GAAG,IAAIC,GAAG,CAAC,CACnC,GAAG,EACH,IAAI,EACJ,KAAK,EACL,IAAI,EACJ,KAAK,EACL,IAAI,EACJ,KAAK,EACL,MAAM,EACN,IAAI,EACJ,MAAM,EACN,IAAI,EACJ,IAAI,EACJ,IAAI,EACJ,KAAK,EACL,IAAI,EACJ,MAAM,CACP,CAAC;;AAEF;AACA;AACA;AACA;AACA,SAASC,kBAAkBA,CAACC,OAAe,EAAW;EACpD,OAAOA,OAAO,CAACC,WAAW,CAAC,GAAG,CAAC,GAAGD,OAAO,CAACC,WAAW,CAAC,GAAG,CAAC;AAC5D;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,SAASC,mBAAmBA,CAACC,GAAW,EAAqC;EAC3E;EACA,IAAIC,aAAa,GAAG,CAAC;EACrB,KAAK,MAAMC,QAAQ,IAAIF,GAAG,CAACG,QAAQ,CAAC,kBAAkB,CAAC,EAAE;IACvDF,aAAa,GAAG,CAACC,QAAQ,CAACE,KAAK,IAAI,CAAC,IAAIF,QAAQ,CAAC,CAAC,CAAC,CAACG,MAAM;EAC5D;EAEA,MAAMC,MAAM,GAAG,CAAC,GAAGN,GAAG,CAACO,KAAK,CAACN,aAAa,CAAC,CAACE,QAAQ,CAAC,MAAM,CAAC,CAAC,CAACK,GAAG,CAC9DC,KAAK,KAAM;IACVC,IAAI,EAAED,KAAK,CAAC,CAAC,CAAC;IACdE,KAAK,EAAEV,aAAa,IAAIQ,KAAK,CAACL,KAAK,IAAI,CAAC;EAC1C,CAAC,CACH,CAAC;EAED,IAAIQ,MAAM,GAAGN,MAAM,CAACC,KAAK,CAAC,CAACd,yBAAyB,CAAC;EACrD,KAAK,IAAIoB,CAAC,GAAGD,MAAM,CAACP,MAAM,GAAG,CAAC,EAAEQ,CAAC,IAAI,CAAC,EAAEA,CAAC,EAAE,EAAE;IAC3C,MAAMC,IAAI,GAAGF,MAAM,CAACC,CAAC,CAAC,CAACH,IAAI,CAACK,WAAW,CAAC,CAAC,CAAC5C,OAAO,CAAC,SAAS,EAAE,EAAE,CAAC;IAChE,IAAIuB,oBAAoB,CAACsB,GAAG,CAACF,IAAI,CAAC,EAAE;MAClCF,MAAM,GAAGA,MAAM,CAACL,KAAK,CAACM,CAAC,GAAG,CAAC,CAAC;MAC5B;IACF;EACF;EAEA,IAAID,MAAM,CAACP,MAAM,KAAK,CAAC,EAAE;IACvB,OAAO;MAAErB,MAAM,EAAEgB,GAAG;MAAEiB,KAAK,EAAE;IAAG,CAAC;EACnC;EAEA,OAAO;IACLjC,MAAM,EAAEgB,GAAG,CAACO,KAAK,CAAC,CAAC,EAAEK,MAAM,CAAC,CAAC,CAAC,CAACD,KAAK,CAAC;IACrCM,KAAK,EAAEjB,GAAG,CAACO,KAAK,CAACK,MAAM,CAAC,CAAC,CAAC,CAACD,KAAK,CAAC,CAACxB,IAAI,CAAC;EACzC,CAAC;AACH;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAAS+B,uBAAuBA,CAAChD,QAAgB,EAGtD;EACA,MAAMY,WAAqC,GAAG,EAAE;EAChD,MAAMC,MAAM,GAAGb,QAAQ,CAACC,OAAO,CAC7BoB,oBAAoB,EACpB,CACEnB,KAAa,EACb4B,GAAW,EACXf,IAAY,EACZkC,MAAc,EACdC,KAAa,KACV;IACH,MAAMC,SAAS,GAAGD,KAAK,CAACtB,WAAW,CAAC,IAAI,EAAEqB,MAAM,CAAC,GAAG,CAAC;IACrD,IAAIvB,kBAAkB,CAACwB,KAAK,CAACb,KAAK,CAACc,SAAS,EAAEF,MAAM,CAAC,CAAC,EAAE;MACtD;MACA,OAAO/C,KAAK;IACd;IAEA,MAAM;MAAEY,MAAM;MAAEiC;IAAM,CAAC,GAAGlB,mBAAmB,CAACC,GAAG,CAAC;IAClD,IAAI,CAACiB,KAAK,EAAE;MACVnC,WAAW,CAACM,IAAI,CAAC;QACfC,IAAI,EAAE,4BAA4B;QAClCC,QAAQ,EAAElB,KAAK;QACfc,UAAU,EAAEc;MACd,CAAC,CAAC;MACF,OAAOA,GAAG;IACZ;IAEA,MAAMd,UAAU,GAAG,GAAGF,MAAM,IAAIiC,KAAK,KAAKhC,IAAI,GAAG;IACjDH,WAAW,CAACM,IAAI,CAAC;MACfC,IAAI,EAAE,8BAA8B;MACpCC,QAAQ,EAAElB,KAAK;MACfc;IACF,CAAC,CAAC;IACF,OAAOA,UAAU;EACnB,CACF,CAAC;EACD,OAAO;IAAEH,MAAM;IAAED;EAAY,CAAC;AAChC;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAASwC,oBAAoBA,CAACpD,QAAgB,EAGnD;EACA,MAAMY,WAAqC,GAAG,EAAE;EAChD,MAAMC,MAAM,GAAGb,QAAQ,CAACC,OAAO,CAC7BqB,YAAY,EACZ,CAACpB,KAAa,EAAE+C,MAAc,EAAEC,KAAa,KAAK;IAChD,IAAIA,KAAK,CAACb,KAAK,CAACY,MAAM,GAAG,CAAC,EAAEA,MAAM,CAAC,KAAK,IAAI,EAAE;MAC5C,OAAO/C,KAAK;IACd;IACAU,WAAW,CAACM,IAAI,CAAC;MACfC,IAAI,EAAE,2BAA2B;MACjCC,QAAQ,EAAElB,KAAK;MACfc,UAAU,EAAE;IACd,CAAC,CAAC;IACF,OAAO,EAAE;EACX,CACF,CAAC;EACD,OAAO;IAAEH,MAAM;IAAED;EAAY,CAAC;AAChC;;AAEA;AACA;AACA;AACA;AACA;AACA;AACO,SAASyC,sBAAsBA,CAACrD,QAAgB,EAAU;EAC/D,MAAMsD,KAAK,GAAGtD,QAAQ,CAACuD,KAAK,CAAC,IAAI,CAAC;EAClC,MAAMC,cAAwB,GAAG,EAAE;EAEnC,KAAK,MAAMC,IAAI,IAAIH,KAAK,EAAE;IACxB,MAAMI,WAAW,GAAGD,IAAI,CAACxC,IAAI,CAAC,CAAC;;IAE/B;IACA,IAAIyC,WAAW,CAACvB,MAAM,GAAG,CAAC,EAAE;MAC1BqB,cAAc,CAACtC,IAAI,CAACuC,IAAI,CAACE,OAAO,CAAC,CAAC,CAAC;IACrC,CAAC,MAAM;MACL;MACAH,cAAc,CAACtC,IAAI,CAAC,EAAE,CAAC;IACzB;EACF;EAEA,OAAOsC,cAAc,CAACI,IAAI,CAAC,IAAI,CAAC;AAClC;;AAEA;AACA;AACA;AACA;AACA;AACO,SAASC,iBAAiBA,CAAC7D,QAAgB,EAGhD;EACA,MAAMY,WAAqC,GAAG,EAAE;EAChD,MAAMC,MAAM,GAAGb,QAAQ,CAACC,OAAO,CAC7B,yDAAyD,EACzD,CAACC,KAAK,EAAE4D,IAAwB,EAAEC,SAA6B,KAAK;IAClE,MAAMC,SAAS,GAAG,CAACD,SAAS,IAAID,IAAI,IAAI,EAAE,EACvC7D,OAAO,CAAC,gBAAgB,EAAE,EAAE,CAAC,CAC7BA,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC;IAC1B,MAAMe,UAAU,GAAG,eAAegD,SAAS,EAAE;IAC7C,IAAI9D,KAAK,KAAKc,UAAU,EAAE;MACxBJ,WAAW,CAACM,IAAI,CAAC;QACfC,IAAI,EAAE,qBAAqB;QAC3BC,QAAQ,EAAElB,KAAK;QACfc;MACF,CAAC,CAAC;IACJ;IACA,OAAOA,UAAU;EACnB,CACF,CAAC;EACD,OAAO;IAAEH,MAAM;IAAED;EAAY,CAAC;AAChC;AAEA,SAASqD,aAAaA,CAACR,IAAY,EAAW;EAC5C,OAAO,mBAAmB,CAACS,IAAI,CAACT,IAAI,CAAC;AACvC;AAEA,SAASU,YAAYA,CAACV,IAAY,EAAY;EAC5C,MAAMW,OAAO,GAAGX,IAAI,CAACxC,IAAI,CAAC,CAAC;EAC3B,IAAI,CAACmD,OAAO,CAACC,UAAU,CAAC,GAAG,CAAC,IAAI,CAACD,OAAO,CAACE,QAAQ,CAAC,GAAG,CAAC,EAAE;IACtD,OAAO,EAAE;EACX;EACA,OAAOF,OAAO,CACX/B,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CACZkB,KAAK,CAAC,GAAG,CAAC,CACVjB,GAAG,CAAEiC,IAAI,IAAKA,IAAI,CAACtD,IAAI,CAAC,CAAC,CAAC;AAC/B;AAEA,SAASuD,SAASA,CAACf,IAAY,EAAW;EACxC,OAAOU,YAAY,CAACV,IAAI,CAAC,CAACtB,MAAM,IAAI,CAAC;AACvC;AAEA,SAASsC,mBAAmBA,CAAChB,IAAY,EAAW;EAClD,MAAMiB,KAAK,GAAGP,YAAY,CAACV,IAAI,CAAC;EAChC,OACEiB,KAAK,CAACvC,MAAM,IAAI,CAAC,IACjBuC,KAAK,CAACC,KAAK,CAAEJ,IAAI,IAAK,aAAa,CAACL,IAAI,CAACK,IAAI,CAACtE,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,CAAC;AAEvE;;AAEA;AACA;AACA;AACA;AACA;AACO,SAAS2E,4BAA4BA,CAAC5E,QAAgB,EAG3D;EACA,MAAMY,WAAqC,GAAG,EAAE;EAChD,MAAM0C,KAAK,GAAGtD,QAAQ,CAACuD,KAAK,CAAC,IAAI,CAAC;EAClC,MAAM1C,MAAgB,GAAG,EAAE;EAC3B,IAAIgE,OAAO,GAAG,KAAK;EAEnB,KAAK,IAAIlC,CAAC,GAAG,CAAC,EAAEA,CAAC,GAAGW,KAAK,CAACnB,MAAM,EAAEQ,CAAC,EAAE,EAAE;IACrC,MAAMc,IAAI,GAAGH,KAAK,CAACX,CAAC,CAAC;IACrB,MAAMmC,YAAY,GAAGjE,MAAM,CAACA,MAAM,CAACsB,MAAM,GAAG,CAAC,CAAC,IAAI,EAAE;IACpD,MAAM4C,QAAQ,GAAGzB,KAAK,CAACX,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE;IAEnC,IAAIsB,aAAa,CAACR,IAAI,CAAC,EAAE;MACvBoB,OAAO,GAAG,CAACA,OAAO;MAClBhE,MAAM,CAACK,IAAI,CAACuC,IAAI,CAAC;MACjB;IACF;IAEA5C,MAAM,CAACK,IAAI,CAACuC,IAAI,CAAC;IAEjB,IAAIoB,OAAO,IAAI,CAACL,SAAS,CAACf,IAAI,CAAC,IAAIe,SAAS,CAACM,YAAY,CAAC,EAAE;MAC1D;IACF;IACA,IAAI,CAACN,SAAS,CAACO,QAAQ,CAAC,IAAIN,mBAAmB,CAACM,QAAQ,CAAC,EAAE;MACzD;IACF;IAEA,MAAMC,WAAW,GAAGb,YAAY,CAACV,IAAI,CAAC;IACtC,MAAMwB,SAAS,GAAGd,YAAY,CAACY,QAAQ,CAAC;IACxC,IAAIC,WAAW,CAAC7C,MAAM,KAAK8C,SAAS,CAAC9C,MAAM,EAAE;MAC3C;IACF;IAEA,MAAM+C,SAAS,GAAG,KAAKF,WAAW,CAAC1C,GAAG,CAAC,MAAM,KAAK,CAAC,CAACsB,IAAI,CAAC,KAAK,CAAC,IAAI;IACnE/C,MAAM,CAACK,IAAI,CAACgE,SAAS,CAAC;IACtBtE,WAAW,CAACM,IAAI,CAAC;MACfC,IAAI,EAAE,0BAA0B;MAChCC,QAAQ,EAAEqC,IAAI;MACdzC,UAAU,EAAE,GAAGyC,IAAI,KAAKyB,SAAS;IACnC,CAAC,CAAC;EACJ;EAEA,OAAO;IAAErE,MAAM,EAAEA,MAAM,CAAC+C,IAAI,CAAC,IAAI,CAAC;IAAEhD;EAAY,CAAC;AACnD;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAASuE,cAAcA,CAACnF,QAAgB,EAG7C;EACA,MAAMY,WAAqC,GAAG,EAAE;EAChD,MAAMC,MAAM,GAAGb,QAAQ,CAACC,OAAO,CAC7B,iBAAiB,EACjB,CAACC,KAAK,EAAEC,QAAgB,KAAK;IAC3B,IAAI;MACF,MAAMiF,OAAO,GAAG7E,kBAAkB,CAACJ,QAAQ,CAACF,OAAO,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;MAClE,IAAImF,OAAO,KAAKjF,QAAQ,EAAE;QACxBS,WAAW,CAACM,IAAI,CAAC;UACfC,IAAI,EAAE,mBAAmB;UACzBC,QAAQ,EAAEjB,QAAQ;UAClBa,UAAU,EAAEoE;QACd,CAAC,CAAC;MACJ;MACA,OAAO,IAAIA,OAAO,IAAI;IACxB,CAAC,CAAC,MAAM;MACN;MACA;MACA,OAAOlF,KAAK;IACd;EACF,CACF,CAAC;EACD,OAAO;IAAEW,MAAM;IAAED;EAAY,CAAC;AAChC;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAASyE,kBAAkBA,CAACrF,QAAgB,EAAoB;EACrE,MAAM;IAAEa,MAAM,EAAEyE,cAAc;IAAE1E,WAAW,EAAE2E;EAAyB,CAAC,GACrE5E,4BAA4B,CAACX,QAAQ,CAAC;EACxC,IAAIwF,SAAS,GAAG9E,iBAAiB,CAAC4E,cAAc,CAAC;EACjD,MAAM;IAAEzE,MAAM,EAAE4E,aAAa;IAAE7E,WAAW,EAAE8E;EAAwB,CAAC,GACnE1C,uBAAuB,CAACwC,SAAS,CAAC;EACpC,MAAM;IAAE3E,MAAM,EAAE8E,WAAW;IAAE/E,WAAW,EAAEgF;EAAkB,CAAC,GAC3DT,cAAc,CAACM,aAAa,CAAC;EAC/BD,SAAS,GAAGzF,eAAe,CAAC4F,WAAW,CAAC;EACxC,MAAM;IAAE9E,MAAM,EAAEG,UAAU;IAAEJ,WAAW,EAAEiF;EAAgB,CAAC,GACxDhC,iBAAiB,CAAC2B,SAAS,CAAC;EAC9B,MAAM;IAAE3E,MAAM,EAAEiF,eAAe;IAAElF,WAAW,EAAEmF;EAAiB,CAAC,GAC9DnB,4BAA4B,CAAC5D,UAAU,CAAC;EAC1C,MAAM;IAAEH,MAAM,EAAEmF,QAAQ;IAAEpF,WAAW,EAAEqF;EAAiB,CAAC,GACvD7C,oBAAoB,CAAC0C,eAAe,CAAC;EACvCN,SAAS,GAAGnC,sBAAsB,CAAC2C,QAAQ,CAAC;EAC5C,OAAO;IACLhG,QAAQ,EAAEwF,SAAS;IACnB5E,WAAW,EAAE,CACX,GAAG2E,wBAAwB,EAC3B,GAAGG,uBAAuB,EAC1B,GAAGE,iBAAiB,EACpB,GAAGC,eAAe,EAClB,GAAGE,gBAAgB,EACnB,GAAGE,gBAAgB;EAEvB,CAAC;AACH","ignoreList":[]}
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Runtime theme token injection (DL #131).
3
+ *
4
+ * `Configuration.themeOverrides` carries per-store CSS custom-property
5
+ * overrides (`--primary`, `--radius`, `--heading`, ...) computed from the
6
+ * store's own theme (e.g. the Shopify `settings_data.json` extractor). This
7
+ * applies them to the page so they win over the bundle's baked `theme.css`:
8
+ *
9
+ * - **Scoped, not `:root`**: the rule targets `WEB5_SCOPE`, the same
10
+ * `:is(#root, .w5-root, [data-web5-placement], .debug-view)` selector every
11
+ * compiled bundle stylesheet uses — host-page styling is never touched, and
12
+ * equal specificity + later document order makes the override win. Callers
13
+ * must therefore apply AFTER the client bundle's CSS is in the document.
14
+ * - **Open token set**: any sanely-shaped custom property is applied, so a
15
+ * template can grow new tokens without an FE or BE release. The mandatory
16
+ * `--` prefix means an override can only define custom properties — it can
17
+ * never set a real CSS property inside the scope. The server enforces the
18
+ * same shape (plus value sanitation) at write time.
19
+ * - **Values are inert**: entries are written with
20
+ * `CSSStyleDeclaration.setProperty`, never string-concatenated into CSS, so
21
+ * a hostile value cannot terminate the declaration or open a new rule.
22
+ * - **Idempotent**: one marker element per document, replaced wholesale on
23
+ * re-apply; empty/absent input removes it.
24
+ */
25
+ import { WEB5_SCOPE } from '../hostScope.js';
26
+
27
+ /** A themeOverrides key: always a CSS custom property. */
28
+
29
+ /**
30
+ * Writer-side shape for a theme override map. The catalog below documents
31
+ * the tokens the seed consumes today; the type stays open so new tokens
32
+ * flow end-to-end with no release anywhere but the seed.
33
+ */
34
+
35
+ /**
36
+ * The known token catalog — the CSS custom properties the seed's Tailwind
37
+ * bridge consumes today. GUIDANCE, NOT A GATE: writers (the Shopify theme
38
+ * extractor, admin tooling) should stick to these to produce visible effect,
39
+ * but `applyThemeOverrides` deliberately applies any well-formed custom
40
+ * property so the vocabulary can grow from the seed alone (matching the
41
+ * open-set validation on the server).
42
+ */
43
+ export const THEME_OVERRIDE_TOKENS = new Set(['--background', '--foreground', '--card', '--card-foreground', '--popover', '--popover-foreground', '--primary', '--primary-foreground', '--secondary', '--secondary-foreground', '--muted', '--muted-foreground', '--accent', '--accent-foreground', '--border', '--input', '--ring', '--radius', '--font-sans', '--font-display', '--heading']);
44
+
45
+ /** Mirrors the server's Consts.ThemeOverrideKeyPattern. */
46
+ const KEY_PATTERN = /^--[a-z][a-z0-9-]{0,62}$/;
47
+ const MARKER_ATTR = 'data-web5-theme-overrides';
48
+ export function applyThemeOverrides(overrides) {
49
+ if (typeof document === 'undefined') return;
50
+ const existing = document.head.querySelector(`style[${MARKER_ATTR}]`);
51
+ const entries = Object.entries(overrides ?? {}).filter(_ref => {
52
+ let [key] = _ref;
53
+ const wellFormed = KEY_PATTERN.test(key);
54
+ if (!wellFormed) {
55
+ console.warn(`[web5-theme] Skipping malformed theme override key: ${key}`);
56
+ }
57
+ return wellFormed;
58
+ });
59
+ if (entries.length === 0) {
60
+ existing == null || existing.remove();
61
+ return;
62
+ }
63
+ const style = document.createElement('style');
64
+ style.setAttribute(MARKER_ATTR, '');
65
+ document.head.appendChild(style);
66
+ const sheet = style.sheet;
67
+ if (!sheet) {
68
+ style.remove();
69
+ return;
70
+ }
71
+ sheet.insertRule(`${WEB5_SCOPE} {}`, 0);
72
+ const rule = sheet.cssRules[0];
73
+ for (const [key, value] of entries) {
74
+ try {
75
+ rule.style.setProperty(key, value);
76
+ } catch {
77
+ // An engine that rejects the value leaves the token at its baked
78
+ // default — degraded theming, never broken CSS.
79
+ }
80
+ }
81
+ // Replace-on-reapply: the fresh element is appended (so it stays last in
82
+ // document order) before the stale one is dropped.
83
+ existing == null || existing.remove();
84
+ }
85
+ //# sourceMappingURL=applyThemeOverrides.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["WEB5_SCOPE","THEME_OVERRIDE_TOKENS","Set","KEY_PATTERN","MARKER_ATTR","applyThemeOverrides","overrides","document","existing","head","querySelector","entries","Object","filter","_ref","key","wellFormed","test","console","warn","length","remove","style","createElement","setAttribute","appendChild","sheet","insertRule","rule","cssRules","value","setProperty"],"sources":["../../../src/client/applyThemeOverrides.ts"],"sourcesContent":["/**\n * Runtime theme token injection (DL #131).\n *\n * `Configuration.themeOverrides` carries per-store CSS custom-property\n * overrides (`--primary`, `--radius`, `--heading`, ...) computed from the\n * store's own theme (e.g. the Shopify `settings_data.json` extractor). This\n * applies them to the page so they win over the bundle's baked `theme.css`:\n *\n * - **Scoped, not `:root`**: the rule targets `WEB5_SCOPE`, the same\n * `:is(#root, .w5-root, [data-web5-placement], .debug-view)` selector every\n * compiled bundle stylesheet uses — host-page styling is never touched, and\n * equal specificity + later document order makes the override win. Callers\n * must therefore apply AFTER the client bundle's CSS is in the document.\n * - **Open token set**: any sanely-shaped custom property is applied, so a\n * template can grow new tokens without an FE or BE release. The mandatory\n * `--` prefix means an override can only define custom properties — it can\n * never set a real CSS property inside the scope. The server enforces the\n * same shape (plus value sanitation) at write time.\n * - **Values are inert**: entries are written with\n * `CSSStyleDeclaration.setProperty`, never string-concatenated into CSS, so\n * a hostile value cannot terminate the declaration or open a new rule.\n * - **Idempotent**: one marker element per document, replaced wholesale on\n * re-apply; empty/absent input removes it.\n */\nimport { WEB5_SCOPE } from '../hostScope';\n\n/** A themeOverrides key: always a CSS custom property. */\nexport type ThemeOverrideKey = `--${string}`;\n\n/**\n * Writer-side shape for a theme override map. The catalog below documents\n * the tokens the seed consumes today; the type stays open so new tokens\n * flow end-to-end with no release anywhere but the seed.\n */\nexport type ThemeOverrides = Record<ThemeOverrideKey, string>;\n\n/**\n * The known token catalog — the CSS custom properties the seed's Tailwind\n * bridge consumes today. GUIDANCE, NOT A GATE: writers (the Shopify theme\n * extractor, admin tooling) should stick to these to produce visible effect,\n * but `applyThemeOverrides` deliberately applies any well-formed custom\n * property so the vocabulary can grow from the seed alone (matching the\n * open-set validation on the server).\n */\nexport const THEME_OVERRIDE_TOKENS: ReadonlySet<string> = new Set([\n '--background', '--foreground',\n '--card', '--card-foreground',\n '--popover', '--popover-foreground',\n '--primary', '--primary-foreground',\n '--secondary', '--secondary-foreground',\n '--muted', '--muted-foreground',\n '--accent', '--accent-foreground',\n '--border', '--input', '--ring',\n '--radius',\n '--font-sans', '--font-display',\n '--heading',\n]);\n\n/** Mirrors the server's Consts.ThemeOverrideKeyPattern. */\nconst KEY_PATTERN = /^--[a-z][a-z0-9-]{0,62}$/;\n\nconst MARKER_ATTR = 'data-web5-theme-overrides';\n\nexport function applyThemeOverrides(\n overrides?: Record<string, string> | null,\n): void {\n if (typeof document === 'undefined') return;\n\n const existing = document.head.querySelector(`style[${MARKER_ATTR}]`);\n const entries = Object.entries(overrides ?? {}).filter(([key]) => {\n const wellFormed = KEY_PATTERN.test(key);\n if (!wellFormed) {\n console.warn(`[web5-theme] Skipping malformed theme override key: ${key}`);\n }\n return wellFormed;\n });\n\n if (entries.length === 0) {\n existing?.remove();\n return;\n }\n\n const style = document.createElement('style');\n style.setAttribute(MARKER_ATTR, '');\n document.head.appendChild(style);\n const sheet = style.sheet;\n if (!sheet) {\n style.remove();\n return;\n }\n sheet.insertRule(`${WEB5_SCOPE} {}`, 0);\n const rule = sheet.cssRules[0] as CSSStyleRule;\n for (const [key, value] of entries) {\n try {\n rule.style.setProperty(key, value);\n } catch {\n // An engine that rejects the value leaves the token at its baked\n // default — degraded theming, never broken CSS.\n }\n }\n // Replace-on-reapply: the fresh element is appended (so it stays last in\n // document order) before the stale one is dropped.\n existing?.remove();\n}\n"],"mappings":"AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,SAASA,UAAU,QAAQ,cAAc;;AAEzC;;AAGA;AACA;AACA;AACA;AACA;;AAGA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,MAAMC,qBAA0C,GAAG,IAAIC,GAAG,CAAC,CAChE,cAAc,EAAE,cAAc,EAC9B,QAAQ,EAAE,mBAAmB,EAC7B,WAAW,EAAE,sBAAsB,EACnC,WAAW,EAAE,sBAAsB,EACnC,aAAa,EAAE,wBAAwB,EACvC,SAAS,EAAE,oBAAoB,EAC/B,UAAU,EAAE,qBAAqB,EACjC,UAAU,EAAE,SAAS,EAAE,QAAQ,EAC/B,UAAU,EACV,aAAa,EAAE,gBAAgB,EAC/B,WAAW,CACZ,CAAC;;AAEF;AACA,MAAMC,WAAW,GAAG,0BAA0B;AAE9C,MAAMC,WAAW,GAAG,2BAA2B;AAE/C,OAAO,SAASC,mBAAmBA,CACjCC,SAAyC,EACnC;EACN,IAAI,OAAOC,QAAQ,KAAK,WAAW,EAAE;EAErC,MAAMC,QAAQ,GAAGD,QAAQ,CAACE,IAAI,CAACC,aAAa,CAAC,SAASN,WAAW,GAAG,CAAC;EACrE,MAAMO,OAAO,GAAGC,MAAM,CAACD,OAAO,CAACL,SAAS,IAAI,CAAC,CAAC,CAAC,CAACO,MAAM,CAACC,IAAA,IAAW;IAAA,IAAV,CAACC,GAAG,CAAC,GAAAD,IAAA;IAC3D,MAAME,UAAU,GAAGb,WAAW,CAACc,IAAI,CAACF,GAAG,CAAC;IACxC,IAAI,CAACC,UAAU,EAAE;MACfE,OAAO,CAACC,IAAI,CAAC,uDAAuDJ,GAAG,EAAE,CAAC;IAC5E;IACA,OAAOC,UAAU;EACnB,CAAC,CAAC;EAEF,IAAIL,OAAO,CAACS,MAAM,KAAK,CAAC,EAAE;IACxBZ,QAAQ,YAARA,QAAQ,CAAEa,MAAM,CAAC,CAAC;IAClB;EACF;EAEA,MAAMC,KAAK,GAAGf,QAAQ,CAACgB,aAAa,CAAC,OAAO,CAAC;EAC7CD,KAAK,CAACE,YAAY,CAACpB,WAAW,EAAE,EAAE,CAAC;EACnCG,QAAQ,CAACE,IAAI,CAACgB,WAAW,CAACH,KAAK,CAAC;EAChC,MAAMI,KAAK,GAAGJ,KAAK,CAACI,KAAK;EACzB,IAAI,CAACA,KAAK,EAAE;IACVJ,KAAK,CAACD,MAAM,CAAC,CAAC;IACd;EACF;EACAK,KAAK,CAACC,UAAU,CAAC,GAAG3B,UAAU,KAAK,EAAE,CAAC,CAAC;EACvC,MAAM4B,IAAI,GAAGF,KAAK,CAACG,QAAQ,CAAC,CAAC,CAAiB;EAC9C,KAAK,MAAM,CAACd,GAAG,EAAEe,KAAK,CAAC,IAAInB,OAAO,EAAE;IAClC,IAAI;MACFiB,IAAI,CAACN,KAAK,CAACS,WAAW,CAAChB,GAAG,EAAEe,KAAK,CAAC;IACpC,CAAC,CAAC,MAAM;MACN;MACA;IAAA;EAEJ;EACA;EACA;EACAtB,QAAQ,YAARA,QAAQ,CAAEa,MAAM,CAAC,CAAC;AACpB","ignoreList":[]}
@@ -17,6 +17,14 @@
17
17
  const CLIENT_BUNDLE_CDN = 'https://d13cktzsgh0qls.cloudfront.net';
18
18
  const CLIENT_BUNDLE_FILENAME = 'w5-components.umd.js';
19
19
 
20
+ /**
21
+ * The template catalog the onboarding gallery and the preview picker render —
22
+ * published by web5-seed-1's CI (`templates/manifest.json` + previews),
23
+ * CORS-open. Thumbnail paths inside are relative to `TEMPLATES_CDN_BASE`.
24
+ */
25
+ export const TEMPLATES_CDN_BASE = `${CLIENT_BUNDLE_CDN}/templates`;
26
+ export const TEMPLATES_MANIFEST_URL = `${TEMPLATES_CDN_BASE}/manifest.json`;
27
+
20
28
  /**
21
29
  * Template ids are kebab-case slugs from `templates/manifest.json`
22
30
  * (`web5-seed-1`). The id becomes a CDN path segment, so anything else is a
@@ -24,6 +32,37 @@ const CLIENT_BUNDLE_FILENAME = 'w5-components.umd.js';
24
32
  * store keeps rendering) rather than fetching a malformed URL.
25
33
  */
26
34
  const TEMPLATE_ID_PATTERN = /^[a-z0-9]+(-[a-z0-9]+)*$/;
35
+ export function isValidTemplateId(id) {
36
+ return id.length <= 64 && TEMPLATE_ID_PATTERN.test(id);
37
+ }
38
+
39
+ /**
40
+ * `?web5Template=<id>` — client-side template override (DL #129 Q10), the
41
+ * mechanism behind the live-preview picker: reload with the param and the page
42
+ * renders under that template's shared bundle, no Configuration write.
43
+ *
44
+ * Strictly narrower than `?clientBundleUrl=`: the value is a kebab-validated
45
+ * id interpolated into the fixed CDN template prefix, so it can only ever
46
+ * select one of our own template bundles — purely presentational, harmless if
47
+ * a visitor sets it by hand. Precedence at the call sites:
48
+ * `clientBundleUrl` override > this > `Configuration.templateId`.
49
+ */
50
+ export function getTemplateOverride(search) {
51
+ const value = new URLSearchParams(search).get('web5Template');
52
+ if (!value) {
53
+ return null;
54
+ }
55
+ if (!isValidTemplateId(value)) {
56
+ console.warn(`[web5-core] Ignoring malformed ?web5Template= override "${value}".`);
57
+ return null;
58
+ }
59
+ return value;
60
+ }
61
+
62
+ /** `?web5TemplatePicker=1` — should the preview picker overlay render? */
63
+ export function isTemplatePickerRequested(search) {
64
+ return new URLSearchParams(search).get('web5TemplatePicker') === '1';
65
+ }
27
66
  export function resolveClientBundleUrl(msid, templateId) {
28
67
  if (templateId) {
29
68
  if (TEMPLATE_ID_PATTERN.test(templateId)) {
@@ -1 +1 @@
1
- {"version":3,"names":["CLIENT_BUNDLE_CDN","CLIENT_BUNDLE_FILENAME","TEMPLATE_ID_PATTERN","resolveClientBundleUrl","msid","templateId","test","console","warn"],"sources":["../../../src/client/clientBundleUrl.ts"],"sourcesContent":["/**\n * Client-UMD URL resolution, shared between `web50-server-ui` (the main app)\n * and `embed-placement` (the storefront widget) — DL #094 (per-msid) + DL #129\n * (per-template). Centralized here for the same reason as\n * `clientBundleOverride.ts`: both callers must derive identical URLs, and\n * divergence on which bundle a tenant executes would be a real bug.\n *\n * Two tiers (DL #129):\n * - `templateId` present (DIY store) → the shared per-template bundle,\n * `<cdn>/templates/<id>/production/…`. Every store on a template runs the\n * same artifact; `production` is the promoted channel.\n * - `templateId` absent (customized tier: feature-com, circana, smallflower)\n * → the per-msid bundle at `<cdn>/<msid>/latest/…`, byte-identical to the\n * pre-template behavior.\n */\n\nconst CLIENT_BUNDLE_CDN = 'https://d13cktzsgh0qls.cloudfront.net';\nconst CLIENT_BUNDLE_FILENAME = 'w5-components.umd.js';\n\n/**\n * Template ids are kebab-case slugs from `templates/manifest.json`\n * (`web5-seed-1`). The id becomes a CDN path segment, so anything else is a\n * config error — resolve falls back to the per-msid path (fail-soft, and the\n * store keeps rendering) rather than fetching a malformed URL.\n */\nconst TEMPLATE_ID_PATTERN = /^[a-z0-9]+(-[a-z0-9]+)*$/;\n\nexport function resolveClientBundleUrl(\n msid: string,\n templateId?: string | null,\n): string {\n if (templateId) {\n if (TEMPLATE_ID_PATTERN.test(templateId)) {\n return `${CLIENT_BUNDLE_CDN}/templates/${templateId}/production/${CLIENT_BUNDLE_FILENAME}`;\n }\n console.warn(\n `[web5-core] Ignoring malformed templateId \"${templateId}\" — falling back to the per-msid bundle.`,\n );\n }\n return `${CLIENT_BUNDLE_CDN}/${msid}/latest/${CLIENT_BUNDLE_FILENAME}`;\n}\n"],"mappings":"AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA,MAAMA,iBAAiB,GAAG,uCAAuC;AACjE,MAAMC,sBAAsB,GAAG,sBAAsB;;AAErD;AACA;AACA;AACA;AACA;AACA;AACA,MAAMC,mBAAmB,GAAG,0BAA0B;AAEtD,OAAO,SAASC,sBAAsBA,CACpCC,IAAY,EACZC,UAA0B,EAClB;EACR,IAAIA,UAAU,EAAE;IACd,IAAIH,mBAAmB,CAACI,IAAI,CAACD,UAAU,CAAC,EAAE;MACxC,OAAO,GAAGL,iBAAiB,cAAcK,UAAU,eAAeJ,sBAAsB,EAAE;IAC5F;IACAM,OAAO,CAACC,IAAI,CACV,8CAA8CH,UAAU,0CAC1D,CAAC;EACH;EACA,OAAO,GAAGL,iBAAiB,IAAII,IAAI,WAAWH,sBAAsB,EAAE;AACxE","ignoreList":[]}
1
+ {"version":3,"names":["CLIENT_BUNDLE_CDN","CLIENT_BUNDLE_FILENAME","TEMPLATES_CDN_BASE","TEMPLATES_MANIFEST_URL","TEMPLATE_ID_PATTERN","isValidTemplateId","id","length","test","getTemplateOverride","search","value","URLSearchParams","get","console","warn","isTemplatePickerRequested","resolveClientBundleUrl","msid","templateId"],"sources":["../../../src/client/clientBundleUrl.ts"],"sourcesContent":["/**\n * Client-UMD URL resolution, shared between `web50-server-ui` (the main app)\n * and `embed-placement` (the storefront widget) — DL #094 (per-msid) + DL #129\n * (per-template). Centralized here for the same reason as\n * `clientBundleOverride.ts`: both callers must derive identical URLs, and\n * divergence on which bundle a tenant executes would be a real bug.\n *\n * Two tiers (DL #129):\n * - `templateId` present (DIY store) → the shared per-template bundle,\n * `<cdn>/templates/<id>/production/…`. Every store on a template runs the\n * same artifact; `production` is the promoted channel.\n * - `templateId` absent (customized tier: feature-com, circana, smallflower)\n * → the per-msid bundle at `<cdn>/<msid>/latest/…`, byte-identical to the\n * pre-template behavior.\n */\n\nconst CLIENT_BUNDLE_CDN = 'https://d13cktzsgh0qls.cloudfront.net';\nconst CLIENT_BUNDLE_FILENAME = 'w5-components.umd.js';\n\n/**\n * The template catalog the onboarding gallery and the preview picker render —\n * published by web5-seed-1's CI (`templates/manifest.json` + previews),\n * CORS-open. Thumbnail paths inside are relative to `TEMPLATES_CDN_BASE`.\n */\nexport const TEMPLATES_CDN_BASE = `${CLIENT_BUNDLE_CDN}/templates`;\nexport const TEMPLATES_MANIFEST_URL = `${TEMPLATES_CDN_BASE}/manifest.json`;\n\n/**\n * Template ids are kebab-case slugs from `templates/manifest.json`\n * (`web5-seed-1`). The id becomes a CDN path segment, so anything else is a\n * config error — resolve falls back to the per-msid path (fail-soft, and the\n * store keeps rendering) rather than fetching a malformed URL.\n */\nconst TEMPLATE_ID_PATTERN = /^[a-z0-9]+(-[a-z0-9]+)*$/;\n\nexport function isValidTemplateId(id: string): boolean {\n return id.length <= 64 && TEMPLATE_ID_PATTERN.test(id);\n}\n\n/**\n * `?web5Template=<id>` — client-side template override (DL #129 Q10), the\n * mechanism behind the live-preview picker: reload with the param and the page\n * renders under that template's shared bundle, no Configuration write.\n *\n * Strictly narrower than `?clientBundleUrl=`: the value is a kebab-validated\n * id interpolated into the fixed CDN template prefix, so it can only ever\n * select one of our own template bundles — purely presentational, harmless if\n * a visitor sets it by hand. Precedence at the call sites:\n * `clientBundleUrl` override > this > `Configuration.templateId`.\n */\nexport function getTemplateOverride(search: string): string | null {\n const value = new URLSearchParams(search).get('web5Template');\n if (!value) {\n return null;\n }\n if (!isValidTemplateId(value)) {\n console.warn(\n `[web5-core] Ignoring malformed ?web5Template= override \"${value}\".`,\n );\n return null;\n }\n return value;\n}\n\n/** `?web5TemplatePicker=1` — should the preview picker overlay render? */\nexport function isTemplatePickerRequested(search: string): boolean {\n return new URLSearchParams(search).get('web5TemplatePicker') === '1';\n}\n\nexport function resolveClientBundleUrl(\n msid: string,\n templateId?: string | null,\n): string {\n if (templateId) {\n if (TEMPLATE_ID_PATTERN.test(templateId)) {\n return `${CLIENT_BUNDLE_CDN}/templates/${templateId}/production/${CLIENT_BUNDLE_FILENAME}`;\n }\n console.warn(\n `[web5-core] Ignoring malformed templateId \"${templateId}\" — falling back to the per-msid bundle.`,\n );\n }\n return `${CLIENT_BUNDLE_CDN}/${msid}/latest/${CLIENT_BUNDLE_FILENAME}`;\n}\n"],"mappings":"AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA,MAAMA,iBAAiB,GAAG,uCAAuC;AACjE,MAAMC,sBAAsB,GAAG,sBAAsB;;AAErD;AACA;AACA;AACA;AACA;AACA,OAAO,MAAMC,kBAAkB,GAAG,GAAGF,iBAAiB,YAAY;AAClE,OAAO,MAAMG,sBAAsB,GAAG,GAAGD,kBAAkB,gBAAgB;;AAE3E;AACA;AACA;AACA;AACA;AACA;AACA,MAAME,mBAAmB,GAAG,0BAA0B;AAEtD,OAAO,SAASC,iBAAiBA,CAACC,EAAU,EAAW;EACrD,OAAOA,EAAE,CAACC,MAAM,IAAI,EAAE,IAAIH,mBAAmB,CAACI,IAAI,CAACF,EAAE,CAAC;AACxD;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASG,mBAAmBA,CAACC,MAAc,EAAiB;EACjE,MAAMC,KAAK,GAAG,IAAIC,eAAe,CAACF,MAAM,CAAC,CAACG,GAAG,CAAC,cAAc,CAAC;EAC7D,IAAI,CAACF,KAAK,EAAE;IACV,OAAO,IAAI;EACb;EACA,IAAI,CAACN,iBAAiB,CAACM,KAAK,CAAC,EAAE;IAC7BG,OAAO,CAACC,IAAI,CACV,2DAA2DJ,KAAK,IAClE,CAAC;IACD,OAAO,IAAI;EACb;EACA,OAAOA,KAAK;AACd;;AAEA;AACA,OAAO,SAASK,yBAAyBA,CAACN,MAAc,EAAW;EACjE,OAAO,IAAIE,eAAe,CAACF,MAAM,CAAC,CAACG,GAAG,CAAC,oBAAoB,CAAC,KAAK,GAAG;AACtE;AAEA,OAAO,SAASI,sBAAsBA,CACpCC,IAAY,EACZC,UAA0B,EAClB;EACR,IAAIA,UAAU,EAAE;IACd,IAAIf,mBAAmB,CAACI,IAAI,CAACW,UAAU,CAAC,EAAE;MACxC,OAAO,GAAGnB,iBAAiB,cAAcmB,UAAU,eAAelB,sBAAsB,EAAE;IAC5F;IACAa,OAAO,CAACC,IAAI,CACV,8CAA8CI,UAAU,0CAC1D,CAAC;EACH;EACA,OAAO,GAAGnB,iBAAiB,IAAIkB,IAAI,WAAWjB,sBAAsB,EAAE;AACxE","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":[]}