@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.
- package/dist/cjs/client/applyThemeOverrides.js +89 -0
- package/dist/cjs/client/applyThemeOverrides.js.map +1 -0
- package/dist/cjs/client/clientBundleUrl.js +43 -0
- package/dist/cjs/client/clientBundleUrl.js.map +1 -1
- package/dist/cjs/component/componentDefinitions/ComparisonSectionDefinition.js +33 -1
- package/dist/cjs/component/componentDefinitions/ComparisonSectionDefinition.js.map +1 -1
- package/dist/cjs/component/componentDefinitions/FeatureSection9PlusDefinition.js +9 -5
- package/dist/cjs/component/componentDefinitions/FeatureSection9PlusDefinition.js.map +1 -1
- package/dist/cjs/component/componentDefinitions/HeroEntitySectionDefinition.js +5 -2
- package/dist/cjs/component/componentDefinitions/HeroEntitySectionDefinition.js.map +1 -1
- package/dist/cjs/component/componentDefinitions/HeroSectionDefinition.js +4 -1
- package/dist/cjs/component/componentDefinitions/HeroSectionDefinition.js.map +1 -1
- package/dist/cjs/component/componentDefinitions/NextStepsSectionDefinition.js +35 -2
- package/dist/cjs/component/componentDefinitions/NextStepsSectionDefinition.js.map +1 -1
- package/dist/cjs/component/componentDefinitions/TextBlockSectionDefinition.js +27 -1
- package/dist/cjs/component/componentDefinitions/TextBlockSectionDefinition.js.map +1 -1
- package/dist/cjs/component/componentDefinitions/index.js +8 -1
- package/dist/cjs/component/componentDefinitions/index.js.map +1 -1
- package/dist/cjs/component/componentDefinitions/listItemsSectionDefinition.js +10 -5
- package/dist/cjs/component/componentDefinitions/listItemsSectionDefinition.js.map +1 -1
- package/dist/cjs/component/componentDefinitions/parse-utils.js +45 -8
- package/dist/cjs/component/componentDefinitions/parse-utils.js.map +1 -1
- package/dist/cjs/component/componentParser.js +15 -0
- package/dist/cjs/component/componentParser.js.map +1 -1
- package/dist/cjs/component/section-definition.js +14 -0
- package/dist/cjs/component/section-definition.js.map +1 -1
- package/dist/cjs/component/types.js.map +1 -1
- package/dist/cjs/components/ui/MarkdownText.js +41 -42
- package/dist/cjs/components/ui/MarkdownText.js.map +1 -1
- package/dist/cjs/components/ui/UnifiedLink.js +22 -10
- package/dist/cjs/components/ui/UnifiedLink.js.map +1 -1
- package/dist/cjs/hostScope.js.map +1 -1
- package/dist/cjs/index.js +13 -3
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/utils/markdownPreprocessor.js +162 -8
- package/dist/cjs/utils/markdownPreprocessor.js.map +1 -1
- package/dist/esm/client/applyThemeOverrides.js +85 -0
- package/dist/esm/client/applyThemeOverrides.js.map +1 -0
- package/dist/esm/client/clientBundleUrl.js +39 -0
- package/dist/esm/client/clientBundleUrl.js.map +1 -1
- package/dist/esm/component/componentDefinitions/ComparisonSectionDefinition.js +34 -1
- package/dist/esm/component/componentDefinitions/ComparisonSectionDefinition.js.map +1 -1
- package/dist/esm/component/componentDefinitions/FeatureSection9PlusDefinition.js +10 -6
- package/dist/esm/component/componentDefinitions/FeatureSection9PlusDefinition.js.map +1 -1
- package/dist/esm/component/componentDefinitions/HeroEntitySectionDefinition.js +6 -3
- package/dist/esm/component/componentDefinitions/HeroEntitySectionDefinition.js.map +1 -1
- package/dist/esm/component/componentDefinitions/HeroSectionDefinition.js +5 -2
- package/dist/esm/component/componentDefinitions/HeroSectionDefinition.js.map +1 -1
- package/dist/esm/component/componentDefinitions/NextStepsSectionDefinition.js +36 -3
- package/dist/esm/component/componentDefinitions/NextStepsSectionDefinition.js.map +1 -1
- package/dist/esm/component/componentDefinitions/TextBlockSectionDefinition.js +27 -1
- package/dist/esm/component/componentDefinitions/TextBlockSectionDefinition.js.map +1 -1
- package/dist/esm/component/componentDefinitions/index.js +8 -1
- package/dist/esm/component/componentDefinitions/index.js.map +1 -1
- package/dist/esm/component/componentDefinitions/listItemsSectionDefinition.js +11 -6
- package/dist/esm/component/componentDefinitions/listItemsSectionDefinition.js.map +1 -1
- package/dist/esm/component/componentDefinitions/parse-utils.js +44 -8
- package/dist/esm/component/componentDefinitions/parse-utils.js.map +1 -1
- package/dist/esm/component/componentParser.js +15 -0
- package/dist/esm/component/componentParser.js.map +1 -1
- package/dist/esm/component/section-definition.js +13 -1
- package/dist/esm/component/section-definition.js.map +1 -1
- package/dist/esm/component/types.js.map +1 -1
- package/dist/esm/components/ui/MarkdownText.js +8 -9
- package/dist/esm/components/ui/MarkdownText.js.map +1 -1
- package/dist/esm/components/ui/UnifiedLink.js +7 -1
- package/dist/esm/components/ui/UnifiedLink.js.map +1 -1
- package/dist/esm/hostScope.js.map +1 -1
- package/dist/esm/index.js +4 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/utils/markdownPreprocessor.js +160 -8
- package/dist/esm/utils/markdownPreprocessor.js.map +1 -1
- package/dist/types/client/applyThemeOverrides.d.ts +19 -0
- package/dist/types/client/applyThemeOverrides.d.ts.map +1 -0
- package/dist/types/client/clientBundleUrl.d.ts +22 -0
- package/dist/types/client/clientBundleUrl.d.ts.map +1 -1
- package/dist/types/component/componentDefinitions/ComparisonSectionDefinition.d.ts +2 -2
- package/dist/types/component/componentDefinitions/ComparisonSectionDefinition.d.ts.map +1 -1
- package/dist/types/component/componentDefinitions/FeatureSection9PlusDefinition.d.ts.map +1 -1
- package/dist/types/component/componentDefinitions/HeroEntitySectionDefinition.d.ts.map +1 -1
- package/dist/types/component/componentDefinitions/HeroSectionDefinition.d.ts.map +1 -1
- package/dist/types/component/componentDefinitions/NextStepsSectionDefinition.d.ts.map +1 -1
- package/dist/types/component/componentDefinitions/TextBlockSectionDefinition.d.ts.map +1 -1
- package/dist/types/component/componentDefinitions/index.d.ts.map +1 -1
- package/dist/types/component/componentDefinitions/listItemsSectionDefinition.d.ts.map +1 -1
- package/dist/types/component/componentDefinitions/parse-utils.d.ts +10 -0
- package/dist/types/component/componentDefinitions/parse-utils.d.ts.map +1 -1
- package/dist/types/component/componentParser.d.ts.map +1 -1
- package/dist/types/component/section-definition.d.ts +13 -1
- package/dist/types/component/section-definition.d.ts.map +1 -1
- package/dist/types/component/types.d.ts +7 -0
- package/dist/types/component/types.d.ts.map +1 -1
- package/dist/types/components/ui/MarkdownText.d.ts +13 -0
- package/dist/types/components/ui/MarkdownText.d.ts.map +1 -1
- package/dist/types/components/ui/UnifiedLink.d.ts +1 -1
- package/dist/types/components/ui/UnifiedLink.d.ts.map +1 -1
- package/dist/types/hostScope.d.ts +11 -0
- package/dist/types/hostScope.d.ts.map +1 -1
- package/dist/types/index.d.ts +4 -2
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/utils/markdownPreprocessor.d.ts +30 -5
- package/dist/types/utils/markdownPreprocessor.d.ts.map +1 -1
- 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.
|
|
235
|
-
* 4.
|
|
236
|
-
* 5.
|
|
237
|
-
* 6.
|
|
238
|
-
* 7.
|
|
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(
|
|
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
|
-
|
|
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":"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","ignoreList":[]}
|
|
1
|
+
{"version":3,"names":["escapeWeb5Links","markdown","replace","match","linkText","protocol","url","decodedUrl","decodeURIComponent","encodedUrl","encodeURI","fixMalformedLinks","recoverOrphanedEmphasisLinks","diagnostics","result","prefix","href","normalized","trim","push","type","original","ORPHANED_WEB5_TARGET","ANY_WEB5_URL","MAX_RECOVERED_LABEL_WORDS","LABEL_BOUNDARY_WORDS","Set","hasOpenLinkBracket","segment","lastIndexOf","splitRecoveredLabel","run","sentenceStart","boundary","matchAll","index","length","tokens","slice","map","token","word","start","window","i","bare","toLowerCase","has","label","repairOrphanedWeb5Links","offset","whole","lineStart","scrubVisibleWeb5Urls","trimTrailingWhitespace","lines","split","processedLines","line","trimmedLine","trimEnd","join","normalizeIconUrls","rest","spaceName","cleanName","isFenceToggle","test","splitPipeRow","trimmed","startsWith","endsWith","cell","isPipeRow","isTableDelimiterRow","cells","every","insertMissingTableSeparators","inFence","previousLine","nextLine","headerCells","nextCells","separator","decodeLinkText","decoded","preprocessMarkdown","recoveredLinks","recoveredLinkDiagnostics","processed","repairedLinks","repairedLinkDiagnostics","decodedText","decodeDiagnostics","iconDiagnostics","tableNormalized","tableDiagnostics","scrubbed","scrubDiagnostics"],"sources":["../../../src/utils/markdownPreprocessor.ts"],"sourcesContent":["/**\n * Markdown preprocessing utilities\n *\n * These functions fix common markdown formatting issues before AST parsing.\n * They are applied in a specific order to ensure proper processing.\n */\n\nexport interface PreprocessorDiagnostic {\n type: string;\n original: string;\n normalized: string;\n}\n\nexport interface PreprocessResult {\n markdown: string;\n diagnostics: PreprocessorDiagnostic[];\n}\n\n/**\n * Escape web5:// links by URL-encoding the URL part\n * e.g. [text](web5://some text with spaces) → [text](web5://some%20text%20with%20spaces)\n *\n * @param markdown - The markdown string to process\n * @returns The markdown string with web5:// URLs properly encoded\n */\nexport function escapeWeb5Links(markdown: string): string {\n // Match markdown links with web5:// protocol\n // Pattern: [text](web5://...)\n return markdown.replace(\n /\\[([^\\]]+)\\]\\((web5|entity|image):\\/\\/([^)]+)\\)/g,\n (match, linkText, protocol, url) => {\n try {\n // Decode first to avoid double-encoding (e.g. %20 → %2520)\n const decodedUrl = decodeURIComponent(url);\n // Use encodeURI (not encodeURIComponent) to preserve URL structure\n // characters like /, ?, &, = while encoding spaces and unsafe chars\n const encodedUrl = encodeURI(decodedUrl);\n return `[${linkText}](${protocol}://${encodedUrl})`;\n } catch {\n // decodeURIComponent throws on a malformed percent sequence (raw `%`\n // or truncated `%X`), encodeURI on a lone surrogate. During streaming\n // this happens when chunks are concatenated mid-escape or a multi-byte\n // char is split. Leave the link untouched rather than throw — an\n // uncaught error here aborts the whole streaming parse and surfaces as\n // a 908 conversation-streaming-error even though the page rendered.\n return match;\n }\n },\n );\n}\n\n/**\n * Fix malformed markdown links with whitespace (spaces, tabs, newlines) between ] and (\n * e.g. [text] (url) → [text](url)\n * e.g. [text]\\n(url) → [text](url)\n *\n * @param markdown - The markdown string to process\n * @returns The markdown string with fixed link formatting\n */\nexport function fixMalformedLinks(markdown: string): string {\n // \\s matches spaces, tabs, and newlines; + means one or more\n // Also handle cases with newlines explicitly\n return markdown.replace(/\\][\\s\\r\\n]+\\(/g, '](');\n}\n\n/**\n * Recover inline CTA chips where the LLM emitted emphasized text followed by a\n * dangling markdown link close, e.g. `**label →**](web5://ask/query)`.\n */\nexport function recoverOrphanedEmphasisLinks(markdown: string): {\n result: string;\n diagnostics: PreprocessorDiagnostic[];\n} {\n const diagnostics: PreprocessorDiagnostic[] = [];\n const result = markdown.replace(\n /(^|[^\\[])\\*\\*([^\\n\\]*]+?)\\*\\*\\]\\((web5:\\/\\/(?:ask|action)\\/[^)]+)\\)/g,\n (match, prefix: string, linkText: string, href: string) => {\n const normalized = `${prefix}[${linkText.trim()}](${href})`;\n diagnostics.push({\n type: 'orphaned_emphasis_link_recovered',\n original: match,\n normalized,\n });\n return normalized;\n },\n );\n return { result, diagnostics };\n}\n\n/**\n * A `web5://` link target whose opening `[` never arrived, e.g.\n * `try filtering for men](web5://ask/Which+are+for+men%3F)`.\n *\n * Dropping the opening bracket is the most common way an inline ask link is\n * mis-emitted, and it is the worst one: remark parses nothing as a link, so the\n * scheme renders as literal text and the visitor reads `web5://ask/...`.\n *\n * `run` is the text before the `]` on the same line, which is what the label\n * should have been. It may be empty, in which case there is nothing to promote\n * and the construct is dropped instead.\n */\nconst ORPHANED_WEB5_TARGET = /([^[\\]()*\\n]{0,80}?)\\]\\((web5:\\/\\/[^)\\s]*)\\)/g;\n\n/** Any `web5://` url, for the final check that none is visible as text. */\nconst ANY_WEB5_URL = /web5:\\/\\/[^\\s)\\]]*/g;\n\n/** Longest label we will promote out of surrounding prose. */\nconst MAX_RECOVERED_LABEL_WORDS = 4;\n\n/**\n * Words a label should start *after* rather than with. The label is cut at the\n * last of these inside the window, so a promoted phrase reads as a noun phrase\n * (\"sensitive skin\") instead of trailing off a verb or preposition\n * (\"carry for sensitive skin\").\n */\nconst LABEL_BOUNDARY_WORDS = new Set([\n 'a',\n 'an',\n 'and',\n 'at',\n 'but',\n 'by',\n 'for',\n 'from',\n 'in',\n 'into',\n 'of',\n 'on',\n 'or',\n 'the',\n 'to',\n 'with',\n]);\n\n/**\n * True when `segment` holds a `[` that is still waiting to be closed — meaning\n * the `](` after it closes a real link and there is nothing to repair.\n */\nfunction hasOpenLinkBracket(segment: string): boolean {\n return segment.lastIndexOf('[') > segment.lastIndexOf(']');\n}\n\n/**\n * Split `run` into the prose to leave in place and the phrase to promote into\n * the link label. Slices on token offsets rather than rejoining words, so the\n * original spacing around the label is preserved exactly.\n *\n * An empty label means there is nothing worth promoting and the caller should\n * drop the link instead.\n */\nfunction splitRecoveredLabel(run: string): { prefix: string; label: string } {\n // Never let the label cross a sentence boundary.\n let sentenceStart = 0;\n for (const boundary of run.matchAll(/(?<=[.!?;:])\\s+/g)) {\n sentenceStart = (boundary.index ?? 0) + boundary[0].length;\n }\n\n const tokens = [...run.slice(sentenceStart).matchAll(/\\S+/g)].map(\n (token) => ({\n word: token[0],\n start: sentenceStart + (token.index ?? 0),\n }),\n );\n\n let window = tokens.slice(-MAX_RECOVERED_LABEL_WORDS);\n for (let i = window.length - 1; i >= 0; i--) {\n const bare = window[i].word.toLowerCase().replace(/[^a-z]/g, '');\n if (LABEL_BOUNDARY_WORDS.has(bare)) {\n window = window.slice(i + 1);\n break;\n }\n }\n\n if (window.length === 0) {\n return { prefix: run, label: '' };\n }\n\n return {\n prefix: run.slice(0, window[0].start),\n label: run.slice(window[0].start).trim(),\n };\n}\n\n/**\n * Repair `web5://` links that lost their opening `[`.\n *\n * Recovers the link by promoting the words immediately before the `]` into its\n * label; when there are no words to promote, drops the whole construct. Either\n * way the scheme stops being visible, which is the point — a link the visitor\n * cannot use is a smaller failure than a url the visitor can read.\n */\nexport function repairOrphanedWeb5Links(markdown: string): {\n result: string;\n diagnostics: PreprocessorDiagnostic[];\n} {\n const diagnostics: PreprocessorDiagnostic[] = [];\n const result = markdown.replace(\n ORPHANED_WEB5_TARGET,\n (\n match: string,\n run: string,\n href: string,\n offset: number,\n whole: string,\n ) => {\n const lineStart = whole.lastIndexOf('\\n', offset) + 1;\n if (hasOpenLinkBracket(whole.slice(lineStart, offset))) {\n // A real `[label](url)` — the bracket is there, leave it alone.\n return match;\n }\n\n const { prefix, label } = splitRecoveredLabel(run);\n if (!label) {\n diagnostics.push({\n type: 'orphaned_web5_link_dropped',\n original: match,\n normalized: run,\n });\n return run;\n }\n\n const normalized = `${prefix}[${label}](${href})`;\n diagnostics.push({\n type: 'orphaned_web5_link_recovered',\n original: match,\n normalized,\n });\n return normalized;\n },\n );\n return { result, diagnostics };\n}\n\n/**\n * Last resort: remove any `web5://` url still sitting in visible text.\n *\n * Runs after every recovery step, so anything left here is a url no repair\n * could turn back into a link. A url in a link target (`](web5://...)`) is\n * left alone — only text the visitor would actually read is scrubbed.\n */\nexport function scrubVisibleWeb5Urls(markdown: string): {\n result: string;\n diagnostics: PreprocessorDiagnostic[];\n} {\n const diagnostics: PreprocessorDiagnostic[] = [];\n const result = markdown.replace(\n ANY_WEB5_URL,\n (match: string, offset: number, whole: string) => {\n if (whole.slice(offset - 2, offset) === '](') {\n return match;\n }\n diagnostics.push({\n type: 'visible_web5_url_scrubbed',\n original: match,\n normalized: '',\n });\n return '';\n },\n );\n return { result, diagnostics };\n}\n\n/**\n * Trim trailing whitespace from lines that contain text\n *\n * @param markdown - The markdown string to process\n * @returns The markdown string with trailing whitespace trimmed from text lines\n */\nexport function trimTrailingWhitespace(markdown: string): string {\n const lines = markdown.split('\\n');\n const processedLines: string[] = [];\n\n for (const line of lines) {\n const trimmedLine = line.trim();\n\n // If the line has content, trim trailing whitespace (keep leading spaces for indentation)\n if (trimmedLine.length > 0) {\n processedLines.push(line.trimEnd());\n } else {\n // Lines with only whitespace become empty lines\n processedLines.push('');\n }\n }\n\n return processedLines.join('\\n');\n}\n\n/**\n * Normalize malformed web5://icon URLs to the canonical web5://icon/{name} format.\n * Handles variants like web5://icon:arrow, web5://icon-arrow, web5://icon arrow → web5://icon/arrow.\n * Only alphanumeric characters and dashes are kept in the icon name.\n */\nexport function normalizeIconUrls(markdown: string): {\n result: string;\n diagnostics: PreprocessorDiagnostic[];\n} {\n const diagnostics: PreprocessorDiagnostic[] = [];\n const result = markdown.replace(\n /web5:\\/\\/icon(?:([:/-][^)\\s]+)|(?:\\s+([a-zA-Z0-9-]+)))/g,\n (match, rest: string | undefined, spaceName: string | undefined) => {\n const cleanName = (spaceName ?? rest ?? '')\n .replace(/[^a-zA-Z0-9-]/g, '')\n .replace(/^-+|-+$/g, '');\n const normalized = `web5://icon/${cleanName}`;\n if (match !== normalized) {\n diagnostics.push({\n type: 'icon_url_normalized',\n original: match,\n normalized,\n });\n }\n return normalized;\n },\n );\n return { result, diagnostics };\n}\n\nfunction isFenceToggle(line: string): boolean {\n return /^\\s*(`{3,}|~{3,})/.test(line);\n}\n\nfunction splitPipeRow(line: string): string[] {\n const trimmed = line.trim();\n if (!trimmed.startsWith('|') || !trimmed.endsWith('|')) {\n return [];\n }\n return trimmed\n .slice(1, -1)\n .split('|')\n .map((cell) => cell.trim());\n}\n\nfunction isPipeRow(line: string): boolean {\n return splitPipeRow(line).length >= 2;\n}\n\nfunction isTableDelimiterRow(line: string): boolean {\n const cells = splitPipeRow(line);\n return (\n cells.length >= 2 &&\n cells.every((cell) => /^:?-{3,}:?$/.test(cell.replace(/\\s+/g, '')))\n );\n}\n\n/**\n * Repair LLM-emitted pipe tables that omit the GFM delimiter row.\n * Without this line remark-gfm parses the rows as plain paragraphs, so\n * registry sections such as comparison never receive a `table` part.\n */\nexport function insertMissingTableSeparators(markdown: string): {\n result: string;\n diagnostics: PreprocessorDiagnostic[];\n} {\n const diagnostics: PreprocessorDiagnostic[] = [];\n const lines = markdown.split('\\n');\n const result: string[] = [];\n let inFence = false;\n\n for (let i = 0; i < lines.length; i++) {\n const line = lines[i];\n const previousLine = result[result.length - 1] ?? '';\n const nextLine = lines[i + 1] ?? '';\n\n if (isFenceToggle(line)) {\n inFence = !inFence;\n result.push(line);\n continue;\n }\n\n result.push(line);\n\n if (inFence || !isPipeRow(line) || isPipeRow(previousLine)) {\n continue;\n }\n if (!isPipeRow(nextLine) || isTableDelimiterRow(nextLine)) {\n continue;\n }\n\n const headerCells = splitPipeRow(line);\n const nextCells = splitPipeRow(nextLine);\n if (headerCells.length !== nextCells.length) {\n continue;\n }\n\n const separator = `| ${headerCells.map(() => '---').join(' | ')} |`;\n result.push(separator);\n diagnostics.push({\n type: 'table_separator_inserted',\n original: line,\n normalized: `${line}\\n${separator}`,\n });\n }\n\n return { result: result.join('\\n'), diagnostics };\n}\n\n/**\n * Decode URL-encoded text in markdown link labels.\n * e.g. [Some%20Text](https://example.com) → [Some Text](https://example.com)\n * e.g. [Which+solution%3F](web5://...) → [Which solution?](web5://...)\n *\n * Only decodes the text portion [text], never the URL portion (url).\n * Handles both `+` (form-encoded space) and `%XX` escapes; falls back to\n * the original string on malformed percent sequences.\n */\nexport function decodeLinkText(markdown: string): {\n result: string;\n diagnostics: PreprocessorDiagnostic[];\n} {\n const diagnostics: PreprocessorDiagnostic[] = [];\n const result = markdown.replace(\n /\\[([^\\]]+)\\]\\(/g,\n (match, linkText: string) => {\n try {\n const decoded = decodeURIComponent(linkText.replace(/\\+/g, '%20'));\n if (decoded !== linkText) {\n diagnostics.push({\n type: 'link_text_decoded',\n original: linkText,\n normalized: decoded,\n });\n }\n return `[${decoded}](`;\n } catch {\n // If decodeURIComponent fails (e.g. malformed percent sequence like %ZZ),\n // leave the text unchanged\n return match;\n }\n },\n );\n return { result, diagnostics };\n}\n\n/**\n * Preprocess markdown to fix common formatting issues before AST parsing.\n *\n * The preprocessing steps are applied in the following order:\n * 1. Recover orphaned emphasized web5 links\n * 2. Fix malformed links (remove whitespace between ] and () - must happen before web5 links are matched)\n * 3. Repair web5 links missing their opening `[` (after step 2, so `] (url)` is already normalized)\n * 4. Decode URL-encoded link text\n * 5. Escape web5:// links (URL-encode URLs)\n * 6. Normalize icon URLs\n * 7. Insert missing table separators for obvious pipe tables\n * 8. Scrub any web5:// url still visible as text (last, so every repair has had its chance)\n * 9. Trim trailing whitespace from lines\n *\n * @param markdown - The raw markdown string to preprocess\n * @returns The preprocessed markdown string ready for AST parsing\n */\nexport function preprocessMarkdown(markdown: string): PreprocessResult {\n const { result: recoveredLinks, diagnostics: recoveredLinkDiagnostics } =\n recoverOrphanedEmphasisLinks(markdown);\n let processed = fixMalformedLinks(recoveredLinks);\n const { result: repairedLinks, diagnostics: repairedLinkDiagnostics } =\n repairOrphanedWeb5Links(processed);\n const { result: decodedText, diagnostics: decodeDiagnostics } =\n decodeLinkText(repairedLinks);\n processed = escapeWeb5Links(decodedText);\n const { result: normalized, diagnostics: iconDiagnostics } =\n normalizeIconUrls(processed);\n const { result: tableNormalized, diagnostics: tableDiagnostics } =\n insertMissingTableSeparators(normalized);\n const { result: scrubbed, diagnostics: scrubDiagnostics } =\n scrubVisibleWeb5Urls(tableNormalized);\n processed = trimTrailingWhitespace(scrubbed);\n return {\n markdown: processed,\n diagnostics: [\n ...recoveredLinkDiagnostics,\n ...repairedLinkDiagnostics,\n ...decodeDiagnostics,\n ...iconDiagnostics,\n ...tableDiagnostics,\n ...scrubDiagnostics,\n ],\n };\n}\n"],"mappings":"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","ignoreList":[]}
|
|
@@ -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","
|
|
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
|
|
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":";AAAA,SACEA,WAAW,EACXC,QAAQ,EACRC,SAAS,EACTC,eAAe,QACV,eAAe;AACtB,SAASC,cAAc,QAAmB,mBAAmB;AAa7D,SAASC,YAAY,QAAQ,uBAAuB;AACpD,SAASC,gBAAgB,QAAQ,oBAAoB;;AAErD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,SAASC,WAAWA,CAACC,GAAuB,EAAW;EACrD,MAAMC,CAAC,GAAG,CAACD,GAAG,IAAI,EAAE,EAAEE,IAAI,CAAC,CAAC,CAACC,WAAW,CAAC,CAAC;EAC1C;EACA,OAAOF,CAAC,KAAK,EAAE,IAAIA,CAAC,KAAK,KAAK,IAAI,YAAY,CAACG,IAAI,CAACH,CAAC,CAAC;AACxD;AAEA,OAAO,MAAMI,2BAA2B,CAExC;EAAAC,YAAA;IAAAC,eAAA,sBACgB,YAAY;IAAAA,eAAA,mBACf,CACT,qGAAqG,CACtG;IAAAA,eAAA,mBAE+B;MAC9BC,KAAK,EAAE,YAAY;MACnBC,YAAY,EAAE,CAAC,UAAU,EAAE,UAAU,CAAC;MACtCC,IAAI,EAAE,CACJ;QAAEC,OAAO,EAAE,OAAO;QAAEC,MAAM,EAAE,CAAC,KAAK,EAAE,KAAK;MAAE,CAAC,EAC5C;QAAED,OAAO,EAAE,SAAS;QAAEC,MAAM,EAAE,CAAC,OAAO,EAAE,KAAK;MAAE,CAAC;IAEpD,CAAC;IAAAL,eAAA,mBAEiD,CAChD;MACEM,QAAQ,EAAE;AAChB;AACA;AACA;AACA;AACA;AACA,0BAA0B;MACpBC,QAAQ,EAAE;QACRN,KAAK,EAAE,eAAe;QACtBO,kBAAkB,EAAE,SAAS;QAC7BN,YAAY,EAAE,CAAC,OAAO,EAAE,KAAK,CAAC;QAC9BC,IAAI,EAAE,CACJ;UAAEC,OAAO,EAAE,OAAO;UAAEC,MAAM,EAAE,CAAC,GAAG,EAAE,WAAW;QAAE,CAAC,EAChD;UAAED,OAAO,EAAE,SAAS;UAAEC,MAAM,EAAE,CAAC,OAAO,EAAE,KAAK;QAAE,CAAC;MAEpD;IACF,CAAC,EACD;MACEC,QAAQ,EAAE;AAChB;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,uBAAuB;MACjBC,QAAQ,EAAE;QACRN,KAAK,EAAE,qBAAqB;QAC5BQ,WAAW,EAAE,yDAAyD;QACtED,kBAAkB,EAAE,SAAS;QAC7BN,YAAY,EAAE,CAAC,cAAc,EAAE,YAAY,CAAC;QAC5CC,IAAI,EAAE,CACJ;UAAEC,OAAO,EAAE,SAAS;UAAEC,MAAM,EAAE,CAAC,KAAK,EAAE,OAAO;QAAE,CAAC,EAChD;UAAED,OAAO,EAAE,OAAO;UAAEC,MAAM,EAAE,CAAC,WAAW,EAAE,GAAG;QAAE,CAAC,EAChD;UAAED,OAAO,EAAE,SAAS;UAAEC,MAAM,EAAE,CAAC,eAAe,EAAE,YAAY;QAAE,CAAC,CAChE;QACDK,OAAO,EAAE,aAAa;QACtBC,OAAO,EAAE;MACX;IACF,CAAC,CACF;EAAA;EAEDC,KAAKA,CACHC,KAAa,EACbC,OAAsB,EACa;IAAA,IAAAC,YAAA;IACnC,MAAMd,KAAK,GACThB,WAAW,CAAC4B,KAAK,EAAE,CAAC,CAAC,IACrB5B,WAAW,CAAC4B,KAAK,EAAE,CAAC,CAAC,IACrB5B,WAAW,CAAC4B,KAAK,EAAE,CAAC,CAAC,IACrB,YAAY;IACd,MAAMJ,WAAW,GAAGvB,QAAQ,CAAC2B,KAAK,CAAC,IAAIG,SAAS;;IAEhD;IACA,MAAMC,UAAU,GAAG5B,cAAc,CAAOwB,KAAK,EAAE,OAAO,CAAC;IACvD,MAAMK,SAAS,IAAAH,YAAA,GAAGE,UAAU,CAAC,CAAC,CAAC,qBAAbF,YAAA,CAAeI,IASpB;IAEb,IAAIjB,YAAsB;IAC1B,IAAIkB,OAAuC;IAC3C,IAAIZ,kBAAsC;IAC1C,IAAIL,IAAqB;IACzB;IACA,IAAIkB,YAAwB;IAE5B,IAAI,CAAAH,SAAS,oBAATA,SAAS,CAAEhB,YAAY,MAAKc,SAAS,EAAE;MACzC;MACAd,YAAY,GAAGgB,SAAS,CAAChB,YAAY;MACrCkB,OAAO,GAAGF,SAAS,CAACE,OAAO;MAC3BZ,kBAAkB,GAAGU,SAAS,CAACV,kBAAkB,IAAIQ,SAAS;MAC9DK,YAAY,GAAG,CAACH,SAAS,CAACf,IAAI,IAAI,EAAE,EAAEmB,GAAG,CAAEC,GAAG,IAAKA,GAAG,CAAClB,MAAM,CAAC;MAC9DF,IAAI,GAAG,CAACe,SAAS,CAACf,IAAI,IAAI,EAAE,EAAEmB,GAAG,CAAEC,GAAG,KAAM;QAC1CnB,OAAO,EAAEmB,GAAG,CAACnB,OAAO;QACpB,IAAImB,GAAG,CAACC,QAAQ,GAAG;UAAEA,QAAQ,EAAED,GAAG,CAACC;QAAS,CAAC,GAAG,CAAC,CAAC,CAAC;QACnDnB,MAAM,EAAEkB,GAAG,CAAClB,MAAM,CAACiB,GAAG,CAAClC,eAAe;MACxC,CAAC,CAAC,CAAC;IACL,CAAC,MAAM;MACL;MACA,MAAMqC,UAAU,GAAG,CAAAP,SAAS,oBAATA,SAAS,CAAEQ,OAAO,KAAI,EAAE;MAC3C,MAAMC,OAAO,GAAG,CAACT,SAAS,oBAATA,SAAS,CAAUf,IAAI,KAAI,EAAE;MAC9CK,kBAAkB,GAAGiB,UAAU,CAAC,CAAC,CAAC,IAAIT,SAAS;MAC/Cd,YAAY,GAAGuB,UAAU,CAACG,KAAK,CAAC,CAAC,CAAC;MAClCR,OAAO,GAAGlB,YAAY,CAACoB,GAAG,CAAEO,KAAK,KAAM;QAAEA;MAAM,CAAC,CAAC,CAAC;MAClDR,YAAY,GAAGM,OAAO,CAACL,GAAG,CAAEC,GAAa,IAAKA,GAAG,CAACK,KAAK,CAAC,CAAC,CAAC,CAAC;MAC3DzB,IAAI,GAAGwB,OAAO,CAACL,GAAG,CAAEC,GAAa,KAAM;QACrCnB,OAAO,EAAEmB,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE;QACrBlB,MAAM,EAAEkB,GAAG,CAACK,KAAK,CAAC,CAAC,CAAC,CAACN,GAAG,CAAClC,eAAe;MAC1C,CAAC,CAAC,CAAC;IACL;;IAEA;IACA,MAAM0C,IAAI,GAAG3C,SAAS,CAAC0B,KAAK,CAAC;IAC7B,MAAMH,OAAO,GAAG,CAAAoB,IAAI,oBAAJA,IAAI,CAAEC,IAAI,KAAIf,SAAS;IACvC,MAAML,OAAO,GAAG,CAAAmB,IAAI,oBAAJA,IAAI,CAAEE,IAAI,KAAIhB,SAAS;;IAEvC;IACA,MAAMiB,OAAO,GAAGZ,YAAY,CAACC,GAAG,CAAE7B,GAAG,IAAKA,GAAG,CAACyC,IAAI,CAAExC,CAAC,IAAK,CAACF,WAAW,CAACE,CAAC,CAAC,CAAC,CAAC;IAC3E,MAAMyC,YAAY,GAAGhC,IAAI,CAACiC,MAAM,CAAC,CAACC,CAAC,EAAEC,CAAC,KAAKL,OAAO,CAACK,CAAC,CAAC,CAAC;IACtD,MAAMC,eAAe,GAAGlB,YAAY,CAACe,MAAM,CAAC,CAACC,CAAC,EAAEC,CAAC,KAAKL,OAAO,CAACK,CAAC,CAAC,CAAC;IACjE,MAAME,YAAY,GAAGrC,IAAI,CAACsC,MAAM,GAAGN,YAAY,CAACM,MAAM;IACtD,IAAID,YAAY,GAAG,CAAC,EAAE;MACpB1B,OAAO,YAAPA,OAAO,CAAE4B,gBAAgB,YAAzB5B,OAAO,CAAE4B,gBAAgB,CACvBnD,gBAAgB,CAACoD,aAAa,EAC9B,eAAeH,YAAY,oCAC7B,CAAC;IACH;;IAEA;IACA;IACA;IACA;IACA;IACA,MAAMI,YAAY,GAAG1C,YAAY,CAC9BoB,GAAG,CAAC,CAACe,CAAC,EAAEQ,GAAG,KAAKA,GAAG,CAAC,CACpBT,MAAM,CAAES,GAAG,IACVN,eAAe,CAACO,KAAK,CAAErD,GAAG,IAAKD,WAAW,CAACC,GAAG,CAACoD,GAAG,CAAC,CAAC,CACtD,CAAC;IACH,IAAI3C,YAAY,CAACuC,MAAM,GAAG,CAAC,IAAIG,YAAY,CAACH,MAAM,GAAG,CAAC,EAAE;MACtD3B,OAAO,YAAPA,OAAO,CAAE4B,gBAAgB,YAAzB5B,OAAO,CAAE4B,gBAAgB,CACvBnD,gBAAgB,CAACwD,gBAAgB,EACjC,yBAAyBH,YAAY,CAClCtB,GAAG,CAAE0B,CAAC,IAAK9C,YAAY,CAAC8C,CAAC,CAAC,IAAI,IAAIA,CAAC,GAAG,CAAC,EAAE,CAAC,CAC1CC,IAAI,CAAC,IAAI,CAAC,qBAAqBV,eAAe,CAACE,MAAM,2BAC1D,CAAC;MACD,OAAOnD,YAAY;IACrB;IAEA,OAAO;MACLW,KAAK;MACLQ,WAAW;MACXP,YAAY;MACZkB,OAAO;MACPjB,IAAI,EAAEgC,YAAY;MAClB3B,kBAAkB;MAClBE,OAAO;MACPC;IACF,CAAC;EACH;AACF","ignoreList":[]}
|