@bendyline/squisq 1.4.1 → 1.5.1
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/README.md +14 -1
- package/dist/{ContentContainer-DNx460y8.d.ts → ContentContainer-BXUlIf18.d.ts} +6 -2
- package/dist/{Doc-BeSeH2iX.d.ts → Doc-BBVGq1_9.d.ts} +922 -273
- package/dist/{ImageEditDoc-BTIvw0oq.d.ts → ImageEditDoc-FuyGtt6o.d.ts} +11 -4
- package/dist/{chunk-ZTX4PFFT.js → chunk-2OIBZYKP.js} +75 -2
- package/dist/chunk-2OIBZYKP.js.map +1 -0
- package/dist/chunk-2UQOHYE2.js +45 -0
- package/dist/chunk-2UQOHYE2.js.map +1 -0
- package/dist/chunk-6ATE2PSM.js +344 -0
- package/dist/chunk-6ATE2PSM.js.map +1 -0
- package/dist/{chunk-57CVQCFX.js → chunk-A4O7GIWE.js} +74 -23
- package/dist/chunk-A4O7GIWE.js.map +1 -0
- package/dist/{chunk-5WFRKURX.js → chunk-DLXZMS5K.js} +4 -2
- package/dist/chunk-DLXZMS5K.js.map +1 -0
- package/dist/chunk-ET53IIEP.js +1 -0
- package/dist/chunk-ET53IIEP.js.map +1 -0
- package/dist/chunk-HWVFJAAH.js +362 -0
- package/dist/chunk-HWVFJAAH.js.map +1 -0
- package/dist/{chunk-2TMAWB4Y.js → chunk-IMSCRLLV.js} +1 -1
- package/dist/chunk-IMSCRLLV.js.map +1 -0
- package/dist/{chunk-B4EEON3N.js → chunk-KKNUBQ6Y.js} +536 -103
- package/dist/chunk-KKNUBQ6Y.js.map +1 -0
- package/dist/chunk-LDQ2HJIX.js +596 -0
- package/dist/chunk-LDQ2HJIX.js.map +1 -0
- package/dist/chunk-LH7I6SH7.js +7208 -0
- package/dist/chunk-LH7I6SH7.js.map +1 -0
- package/dist/chunk-PGGGKYWG.js +1 -0
- package/dist/chunk-PGGGKYWG.js.map +1 -0
- package/dist/{chunk-7UDSRZKG.js → chunk-Q3ROPT5H.js} +35 -14
- package/dist/chunk-Q3ROPT5H.js.map +1 -0
- package/dist/chunk-QE7TWGVT.js +178 -0
- package/dist/chunk-QE7TWGVT.js.map +1 -0
- package/dist/chunk-RUDYOTA6.js +345 -0
- package/dist/chunk-RUDYOTA6.js.map +1 -0
- package/dist/{chunk-K32VJONL.js → chunk-T5UK6YOB.js} +41 -2
- package/dist/chunk-T5UK6YOB.js.map +1 -0
- package/dist/{chunk-3K5OG6KI.js → chunk-TEEEILMP.js} +412 -186
- package/dist/chunk-TEEEILMP.js.map +1 -0
- package/dist/{chunk-FR2RBTKO.js → chunk-TQWLI6S2.js} +93 -27
- package/dist/chunk-TQWLI6S2.js.map +1 -0
- package/dist/{chunk-WMOI7FN3.js → chunk-UY7KGQ5R.js} +12 -2
- package/dist/{chunk-WMOI7FN3.js.map → chunk-UY7KGQ5R.js.map} +1 -1
- package/dist/{chunk-3E5F2XMR.js → chunk-VWUFZ6ZG.js} +29 -2
- package/dist/chunk-VWUFZ6ZG.js.map +1 -0
- package/dist/chunk-ZLQKNOZQ.js +1 -0
- package/dist/chunk-ZLQKNOZQ.js.map +1 -0
- package/dist/doc/index.d.ts +5 -5
- package/dist/doc/index.js +135 -8
- package/dist/generate/index.d.ts +3 -74
- package/dist/generate/index.js +2 -7
- package/dist/icons/index.d.ts +3 -26
- package/dist/icons/index.js +13 -0
- package/dist/icons/inlineIconMarker.d.ts +1 -0
- package/dist/icons/inlineIconMarker.js +15 -0
- package/dist/icons/inlineIconMarker.js.map +1 -0
- package/dist/imageEdit/index.d.ts +12 -6
- package/dist/imageEdit/index.js +4 -1
- package/dist/index.d.ts +14 -11
- package/dist/index.js +265 -37
- package/dist/inlineIconMarker-CiE8Hlvz.d.ts +59 -0
- package/dist/jsonForm/index.d.ts +42 -1
- package/dist/jsonForm/index.js +9 -1
- package/dist/markdown/index.d.ts +228 -3
- package/dist/markdown/index.js +42 -7
- package/dist/recommend/index.d.ts +1 -1
- package/dist/recommend/index.js +3 -2
- package/dist/schemas/index.d.ts +28 -8
- package/dist/schemas/index.js +36 -8
- package/dist/spatial/index.d.ts +9 -4
- package/dist/spatial/index.js +1 -1
- package/dist/storage/index.d.ts +2 -2
- package/dist/storage/index.js +1 -1
- package/dist/story/index.d.ts +1021 -41
- package/dist/story/index.js +135 -8
- package/dist/{themeLibrary-BHjJ0Qx6.d.ts → themeLibrary-CehcJhzz.d.ts} +1 -1
- package/dist/timing/index.js +3 -5
- package/dist/transform/index.d.ts +48 -10
- package/dist/transform/index.js +17 -481
- package/dist/transform/index.js.map +1 -1
- package/dist/{types-BdZkdJ3z.d.ts → types-DlAZ7MW4.d.ts} +115 -1
- package/dist/versions/index.d.ts +1 -1
- package/package.json +6 -1
- package/src/__tests__/animationUtils.test.ts +40 -0
- package/src/__tests__/annotationCoercion.test.ts +243 -0
- package/src/__tests__/applyRenderStyle.test.ts +146 -0
- package/src/__tests__/atmosphereLayers.test.ts +108 -0
- package/src/__tests__/attrTokens.test.ts +162 -0
- package/src/__tests__/contentExtractor.test.ts +60 -0
- package/src/__tests__/coverBlock.test.ts +46 -1
- package/src/__tests__/customTemplateSample.test.ts +144 -0
- package/src/__tests__/customTemplatesFrontmatter.test.ts +236 -0
- package/src/__tests__/customThemesFrontmatter.test.ts +145 -0
- package/src/__tests__/diagramBlock.test.ts +122 -0
- package/src/__tests__/diagramLayout.test.ts +134 -0
- package/src/__tests__/drawing.test.ts +109 -0
- package/src/__tests__/drawingBlock.test.ts +124 -0
- package/src/__tests__/drawingLayout.test.ts +171 -0
- package/src/__tests__/geohash.test.ts +6 -5
- package/src/__tests__/getLayers.test.ts +6 -2
- package/src/__tests__/getLayersFallback.test.ts +103 -0
- package/src/__tests__/imageEditExportSvg.test.ts +70 -0
- package/src/__tests__/imageEditVersions.test.ts +72 -0
- package/src/__tests__/inlineIconMarker.test.ts +36 -0
- package/src/__tests__/inputDescriptors.test.ts +162 -0
- package/src/__tests__/jsonForm.tokens.test.ts +84 -0
- package/src/__tests__/layoutBlock.test.ts +83 -0
- package/src/__tests__/layoutLayout.test.ts +169 -0
- package/src/__tests__/markdown.test.ts +87 -0
- package/src/__tests__/markdownSanitize.test.ts +79 -0
- package/src/__tests__/markdownToDoc.test.ts +391 -2
- package/src/__tests__/mediaAnnotations.test.ts +91 -0
- package/src/__tests__/mediaSchedule.test.ts +118 -0
- package/src/__tests__/pandocAttrParse.test.ts +187 -0
- package/src/__tests__/resolveDocTheme.test.ts +55 -0
- package/src/__tests__/rootExports.test.ts +43 -0
- package/src/__tests__/shapeGeometry.test.ts +89 -0
- package/src/__tests__/shapeVocabulary.test.ts +133 -0
- package/src/__tests__/structuredData.test.ts +249 -0
- package/src/__tests__/templateAnnotationParse.test.ts +108 -0
- package/src/__tests__/templateMetadata.test.ts +31 -0
- package/src/__tests__/templates.test.ts +257 -4
- package/src/__tests__/themeCompile.test.ts +42 -0
- package/src/__tests__/themeValidator.test.ts +6 -0
- package/src/__tests__/transformV2.test.ts +137 -0
- package/src/__tests__/transitionNormalize.test.ts +61 -0
- package/src/__tests__/validateDoc.test.ts +260 -0
- package/src/doc/annotationBlocks.ts +117 -0
- package/src/doc/customTemplatesFrontmatter.ts +314 -0
- package/src/doc/customThemesFrontmatter.ts +95 -0
- package/src/doc/docToMarkdown.ts +104 -5
- package/src/doc/getLayers.ts +49 -17
- package/src/doc/index.ts +47 -1
- package/src/doc/markdownToDoc.ts +411 -13
- package/src/doc/mediaAnnotations.ts +100 -0
- package/src/doc/resolveDocTheme.ts +55 -0
- package/src/doc/standaloneAnnotation.ts +58 -0
- package/src/doc/structuredData.ts +301 -0
- package/src/doc/templateInputs.ts +240 -0
- package/src/doc/templates/__tests__/customTemplate.test.ts +170 -0
- package/src/doc/templates/__tests__/customTemplateRegistry.test.ts +110 -0
- package/src/doc/templates/accentImage.ts +30 -6
- package/src/doc/templates/captionUtils.ts +23 -0
- package/src/doc/templates/comparisonBar.ts +33 -18
- package/src/doc/templates/coverBlock.ts +35 -5
- package/src/doc/templates/customTemplate.ts +51 -0
- package/src/doc/templates/dataTable.ts +37 -19
- package/src/doc/templates/dateEvent.ts +41 -20
- package/src/doc/templates/definitionCard.ts +26 -18
- package/src/doc/templates/diagramBlock.ts +204 -0
- package/src/doc/templates/diagramLayout.ts +159 -0
- package/src/doc/templates/drawingBlock.ts +325 -0
- package/src/doc/templates/drawingLayout.ts +539 -0
- package/src/doc/templates/factCard.ts +54 -26
- package/src/doc/templates/fallbackBlock.ts +123 -0
- package/src/doc/templates/featureBlock.ts +20 -13
- package/src/doc/templates/fullBleedQuote.ts +18 -8
- package/src/doc/templates/imageWithCaption.ts +30 -15
- package/src/doc/templates/index.ts +225 -46
- package/src/doc/templates/inputDescriptors.ts +432 -0
- package/src/doc/templates/layoutBlock.ts +43 -0
- package/src/doc/templates/layoutLayout.ts +243 -0
- package/src/doc/templates/listBlock.ts +56 -30
- package/src/doc/templates/mapBlock.ts +15 -12
- package/src/doc/templates/metadata.ts +120 -0
- package/src/doc/templates/persistentLayers.ts +129 -11
- package/src/doc/templates/photoGrid.ts +22 -34
- package/src/doc/templates/pullQuote.ts +30 -12
- package/src/doc/templates/quoteBlock.ts +54 -29
- package/src/doc/templates/sectionHeader.ts +25 -11
- package/src/doc/templates/statHighlight.ts +39 -22
- package/src/doc/templates/titleBlock.ts +59 -21
- package/src/doc/templates/tokens/__tests__/resolveTokens.test.ts +487 -0
- package/src/doc/templates/tokens/resolveTokens.ts +453 -0
- package/src/doc/templates/twoColumn.ts +48 -30
- package/src/doc/templates/videoPullQuote.ts +22 -12
- package/src/doc/templates/videoWithCaption.ts +21 -15
- package/src/doc/utils/animationUtils.ts +267 -3
- package/src/doc/utils/applyRenderStyle.ts +113 -0
- package/src/doc/utils/imageTreatment.ts +70 -0
- package/src/doc/utils/nearest.ts +59 -0
- package/src/doc/utils/shapeGeometry.ts +464 -0
- package/src/doc/utils/themeUtils.ts +75 -1
- package/src/doc/validate.ts +575 -0
- package/src/generate/contentExtractor.ts +148 -35
- package/src/generate/index.ts +0 -3
- package/src/icons/index.ts +8 -0
- package/src/icons/inlineIconMarker.ts +73 -0
- package/src/imageEdit/export.ts +85 -2
- package/src/imageEdit/index.ts +1 -0
- package/src/imageEdit/persistence.ts +51 -0
- package/src/index.ts +2 -0
- package/src/jsonForm/index.ts +2 -0
- package/src/jsonForm/tokens.ts +78 -0
- package/src/markdown/annotationCoercion.ts +295 -0
- package/src/markdown/attrTokens.ts +241 -0
- package/src/markdown/convert.ts +229 -26
- package/src/markdown/index.ts +37 -0
- package/src/markdown/sanitize.ts +318 -0
- package/src/markdown/stringify.ts +96 -6
- package/src/markdown/types.ts +66 -0
- package/src/markdown/utils.ts +111 -2
- package/src/recommend/templates.ts +24 -0
- package/src/schemas/BlockTemplates.ts +133 -4
- package/src/schemas/CustomTemplates.ts +126 -0
- package/src/schemas/Doc.ts +387 -65
- package/src/schemas/ImageEditDoc.ts +11 -4
- package/src/schemas/Media.ts +169 -0
- package/src/schemas/Theme.ts +23 -1
- package/src/schemas/Transitions.ts +224 -0
- package/src/schemas/colorUtils.ts +33 -0
- package/src/schemas/fontStacks.ts +5 -2
- package/src/schemas/index.ts +3 -0
- package/src/schemas/themeCompile.ts +24 -5
- package/src/schemas/themeValidator.ts +49 -11
- package/src/schemas/themes/cinematic.json +69 -12
- package/src/schemas/themes/documentary.json +58 -11
- package/src/schemas/themes/gezellig.json +60 -11
- package/src/schemas/themes/magazine.json +54 -9
- package/src/schemas/themes/morning-light.json +52 -9
- package/src/schemas/themes/standard-dark.json +48 -13
- package/src/schemas/themes/standard.json +44 -12
- package/src/schemas/themes/tech-dark.json +65 -12
- package/src/spatial/Geohash.ts +52 -21
- package/src/storage/ContentContainer.ts +6 -2
- package/src/transform/applyTransform.ts +43 -4
- package/src/transform/index.ts +1 -0
- package/src/transform/registry.ts +44 -13
- package/src/transform/styles/dataDriven.ts +2 -1
- package/src/transform/styles/documentary.ts +2 -1
- package/src/transform/styles/magazine.ts +3 -1
- package/src/transform/styles/minimal.ts +2 -1
- package/src/transform/styles/narrative.ts +3 -1
- package/src/transform/templateSelector.ts +87 -11
- package/src/transform/types.ts +35 -2
- package/dist/chunk-2TMAWB4Y.js.map +0 -1
- package/dist/chunk-3E5F2XMR.js.map +0 -1
- package/dist/chunk-3K5OG6KI.js.map +0 -1
- package/dist/chunk-4X3JQXNM.js +0 -35
- package/dist/chunk-4X3JQXNM.js.map +0 -1
- package/dist/chunk-57CVQCFX.js.map +0 -1
- package/dist/chunk-5WFRKURX.js.map +0 -1
- package/dist/chunk-7UDSRZKG.js.map +0 -1
- package/dist/chunk-B4EEON3N.js.map +0 -1
- package/dist/chunk-FR2RBTKO.js.map +0 -1
- package/dist/chunk-H3AXU4MJ.js +0 -179
- package/dist/chunk-H3AXU4MJ.js.map +0 -1
- package/dist/chunk-K32VJONL.js.map +0 -1
- package/dist/chunk-KSWJR66U.js +0 -3706
- package/dist/chunk-KSWJR66U.js.map +0 -1
- package/dist/chunk-MYIH7FWD.js +0 -145
- package/dist/chunk-MYIH7FWD.js.map +0 -1
- package/dist/chunk-ZTX4PFFT.js.map +0 -1
- package/src/__tests__/slideshowGenerator.test.ts +0 -128
- package/src/generate/slideshowGenerator.ts +0 -273
|
@@ -147,12 +147,66 @@ const QUOTE_PATTERNS = [
|
|
|
147
147
|
/(\w+(?:\s+\w+)?)\s+(?:said|stated|wrote|noted|observed|commented),?\s*"([^"]+)"/gi,
|
|
148
148
|
];
|
|
149
149
|
|
|
150
|
+
// A single side of a comparison ("X" in "from X to Y"). Deliberately excludes
|
|
151
|
+
// sentence/clause terminators (. ! ? ;) and newlines so a capture group can
|
|
152
|
+
// never swallow text from an adjacent sentence. Without this, greedy `.{2,30}`
|
|
153
|
+
// matches across a period — e.g. "…contradicted from witness to witness.
|
|
154
|
+
// Robertson said…" yields right="witness. Robertson", producing a nonsensical
|
|
155
|
+
// "witness → witness. Robertson" slide. Commas and colons are allowed (ranges
|
|
156
|
+
// like "9:00 to 17:00", ratios); downstream label truncation trims commas.
|
|
157
|
+
const COMPARISON_SIDE = '[^.!?;\\r\\n]{2,30}';
|
|
158
|
+
|
|
150
159
|
const COMPARISON_PATTERNS = [
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
160
|
+
new RegExp(`from\\s+(${COMPARISON_SIDE})\\s+to\\s+(${COMPARISON_SIDE})`, 'gi'),
|
|
161
|
+
new RegExp(`(${COMPARISON_SIDE})\\s+(?:vs\\.?|versus)\\s+(${COMPARISON_SIDE})`, 'gi'),
|
|
162
|
+
new RegExp(`(${COMPARISON_SIDE})\\s+compared\\s+to\\s+(${COMPARISON_SIDE})`, 'gi'),
|
|
154
163
|
];
|
|
155
164
|
|
|
165
|
+
// Words whose trailing period is an abbreviation, not a sentence end. Used by
|
|
166
|
+
// isSentenceBoundary() so names like "George W. Tibbetts" or "Quong Chong & Co."
|
|
167
|
+
// don't get split into fragments. Matched only when the entire preceding word
|
|
168
|
+
// equals one of these (case-insensitive), so "canal." / "taco." are unaffected.
|
|
169
|
+
const SENTENCE_ABBREVIATIONS = new Set([
|
|
170
|
+
'mr',
|
|
171
|
+
'mrs',
|
|
172
|
+
'ms',
|
|
173
|
+
'dr',
|
|
174
|
+
'prof',
|
|
175
|
+
'st',
|
|
176
|
+
'mt',
|
|
177
|
+
'jr',
|
|
178
|
+
'sr',
|
|
179
|
+
'gen',
|
|
180
|
+
'col',
|
|
181
|
+
'capt',
|
|
182
|
+
'lt',
|
|
183
|
+
'sgt',
|
|
184
|
+
'rev',
|
|
185
|
+
'hon',
|
|
186
|
+
'sen',
|
|
187
|
+
'rep',
|
|
188
|
+
'gov',
|
|
189
|
+
'pres',
|
|
190
|
+
'vs',
|
|
191
|
+
'etc',
|
|
192
|
+
'inc',
|
|
193
|
+
'co',
|
|
194
|
+
'ltd',
|
|
195
|
+
'corp',
|
|
196
|
+
'al',
|
|
197
|
+
'fig',
|
|
198
|
+
'eg',
|
|
199
|
+
'ie',
|
|
200
|
+
'approx',
|
|
201
|
+
'dept',
|
|
202
|
+
'univ',
|
|
203
|
+
'assn',
|
|
204
|
+
'bros',
|
|
205
|
+
'ave',
|
|
206
|
+
'blvd',
|
|
207
|
+
'rd',
|
|
208
|
+
]);
|
|
209
|
+
|
|
156
210
|
const FACT_INDICATORS = [
|
|
157
211
|
'largest',
|
|
158
212
|
'smallest',
|
|
@@ -427,10 +481,38 @@ function extractComparisons(text: string): ExtractedElement[] {
|
|
|
427
481
|
if (seen.has(position)) continue;
|
|
428
482
|
seen.add(position);
|
|
429
483
|
|
|
430
|
-
|
|
431
|
-
|
|
484
|
+
// Trim a trailing clause introduced by a comma + space ("retired
|
|
485
|
+
// airliners, arranged in rows" → "retired airliners"). A greedy side
|
|
486
|
+
// match often swallows the words after the compared thing; the comma is a
|
|
487
|
+
// clause boundary, not part of the value. Numeric grouping commas
|
|
488
|
+
// ("4,000") survive because those are comma+digit, not comma+space.
|
|
489
|
+
const trimClause = (s: string) => s.replace(/,\s+.*$/, '').trim();
|
|
490
|
+
const left = trimClause(match[1].trim());
|
|
491
|
+
const right = trimClause(match[2].trim());
|
|
432
492
|
if (left.length < 2 || left.length > 30) continue;
|
|
433
493
|
if (right.length < 2 || right.length > 30) continue;
|
|
494
|
+
// Reject "from X to X"-style idioms — "from witness to witness",
|
|
495
|
+
// "from day to day", "from generation to generation". These mean "it
|
|
496
|
+
// varied", not "A versus B", and render as a pointless "X → X" card.
|
|
497
|
+
// The signature is a word repeated across the connector: the last word
|
|
498
|
+
// of the left side equals the first word of the right side. (Greedy
|
|
499
|
+
// matching may append trailing context to the right side, so a whole-
|
|
500
|
+
// string equality check is not enough.)
|
|
501
|
+
const wordsOf = (s: string) =>
|
|
502
|
+
s
|
|
503
|
+
.toLowerCase()
|
|
504
|
+
.split(/\s+/)
|
|
505
|
+
.map((w) => w.replace(/[^a-z0-9]+/g, ''))
|
|
506
|
+
.filter(Boolean);
|
|
507
|
+
const leftWords = wordsOf(left);
|
|
508
|
+
const rightWords = wordsOf(right);
|
|
509
|
+
if (
|
|
510
|
+
leftWords.length > 0 &&
|
|
511
|
+
rightWords.length > 0 &&
|
|
512
|
+
leftWords[leftWords.length - 1] === rightWords[0]
|
|
513
|
+
) {
|
|
514
|
+
continue;
|
|
515
|
+
}
|
|
434
516
|
|
|
435
517
|
const context = getSentenceContext(text, position, match[0].length);
|
|
436
518
|
|
|
@@ -447,46 +529,77 @@ function extractComparisons(text: string): ExtractedElement[] {
|
|
|
447
529
|
return elements;
|
|
448
530
|
}
|
|
449
531
|
|
|
532
|
+
/**
|
|
533
|
+
* Whether the `. ! ?` at `index` is a real sentence terminator rather than a
|
|
534
|
+
* period inside an abbreviation ("George W. Tibbetts", "M. DeWitt", "Mr.",
|
|
535
|
+
* "U.S."), a single-letter initial, a decimal ("3.5"), or an ellipsis. Naive
|
|
536
|
+
* splitting on every period produces broken fragments — e.g. the standalone
|
|
537
|
+
* slide "They were met at George W" from "…at George W. Tibbetts' store…".
|
|
538
|
+
* Returns false for any non-terminator character so callers can scan freely.
|
|
539
|
+
*/
|
|
540
|
+
function isSentenceBoundary(text: string, index: number): boolean {
|
|
541
|
+
const ch = text[index];
|
|
542
|
+
if (ch === '!' || ch === '?') return true;
|
|
543
|
+
if (ch !== '.') return false;
|
|
544
|
+
|
|
545
|
+
// Ellipsis: defer the boundary to the final dot in a run of dots.
|
|
546
|
+
if (text[index + 1] === '.') return false;
|
|
547
|
+
// Decimal number: digit "." digit → "3.5".
|
|
548
|
+
if (/[0-9]/.test(text[index - 1] ?? '') && /[0-9]/.test(text[index + 1] ?? '')) return false;
|
|
549
|
+
|
|
550
|
+
// The run of letters immediately preceding the period.
|
|
551
|
+
let w = index - 1;
|
|
552
|
+
while (w >= 0 && /[A-Za-z]/.test(text[w])) w--;
|
|
553
|
+
const word = text.slice(w + 1, index);
|
|
554
|
+
if (word.length === 1) return false; // single-letter initial: "George W."
|
|
555
|
+
if (word && SENTENCE_ABBREVIATIONS.has(word.toLowerCase())) return false;
|
|
556
|
+
|
|
557
|
+
// A genuine sentence starts with an uppercase letter, digit, or opening
|
|
558
|
+
// quote/bracket. A lowercase continuation means the period was mid-sentence
|
|
559
|
+
// (an abbreviation we didn't enumerate).
|
|
560
|
+
let j = index + 1;
|
|
561
|
+
while (j < text.length && /\s/.test(text[j])) j++;
|
|
562
|
+
if (j >= text.length) return true; // end of text
|
|
563
|
+
return !/[a-z]/.test(text[j]);
|
|
564
|
+
}
|
|
565
|
+
|
|
450
566
|
/**
|
|
451
567
|
* Iterate over sentence-like spans in the text, yielding trimmed text and
|
|
452
568
|
* accurate start/end indices within the original string.
|
|
453
569
|
*
|
|
454
|
-
*
|
|
455
|
-
*
|
|
570
|
+
* Boundaries are detected by isSentenceBoundary() so abbreviations, initials,
|
|
571
|
+
* decimals, and ellipses do not split a sentence. Leading whitespace and
|
|
572
|
+
* trailing whitespace/terminators are excluded from each span.
|
|
456
573
|
*/
|
|
457
574
|
function forEachSentenceSpan(
|
|
458
575
|
text: string,
|
|
459
576
|
callback: (trimmed: string, start: number, end: number) => void,
|
|
460
577
|
): void {
|
|
461
|
-
const
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
const rawEnd = rawStart + raw.length;
|
|
468
|
-
|
|
469
|
-
// Compute leading whitespace to exclude
|
|
470
|
-
let leading = 0;
|
|
471
|
-
while (leading < raw.length && /\s/.test(raw[leading])) {
|
|
472
|
-
leading++;
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
// Compute trailing whitespace or sentence-ending punctuation to exclude
|
|
476
|
-
let trailing = 0;
|
|
477
|
-
while (trailing < raw.length - leading && /[\s.!?]/.test(raw[raw.length - 1 - trailing])) {
|
|
478
|
-
trailing++;
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
const start = rawStart + leading;
|
|
482
|
-
const end = rawEnd - trailing;
|
|
483
|
-
if (end <= start) continue;
|
|
484
|
-
|
|
578
|
+
const emit = (rawStart: number, rawEnd: number) => {
|
|
579
|
+
let start = rawStart;
|
|
580
|
+
while (start < rawEnd && /\s/.test(text[start])) start++;
|
|
581
|
+
let end = rawEnd;
|
|
582
|
+
while (end > start && /[\s.!?]/.test(text[end - 1])) end--;
|
|
583
|
+
if (end <= start) return;
|
|
485
584
|
const trimmed = text.slice(start, end);
|
|
486
|
-
if (
|
|
585
|
+
if (trimmed) callback(trimmed, start, end);
|
|
586
|
+
};
|
|
487
587
|
|
|
488
|
-
|
|
588
|
+
let spanStart = 0;
|
|
589
|
+
let i = 0;
|
|
590
|
+
while (i < text.length) {
|
|
591
|
+
if (isSentenceBoundary(text, i)) {
|
|
592
|
+
// Consume any consecutive terminators ("?!", "…") as one boundary.
|
|
593
|
+
let end = i + 1;
|
|
594
|
+
while (end < text.length && /[.!?]/.test(text[end])) end++;
|
|
595
|
+
emit(spanStart, end);
|
|
596
|
+
spanStart = end;
|
|
597
|
+
i = end;
|
|
598
|
+
} else {
|
|
599
|
+
i++;
|
|
600
|
+
}
|
|
489
601
|
}
|
|
602
|
+
if (spanStart < text.length) emit(spanStart, text.length);
|
|
490
603
|
}
|
|
491
604
|
|
|
492
605
|
function extractFacts(text: string): ExtractedElement[] {
|
|
@@ -621,11 +734,11 @@ function getSentenceContext(
|
|
|
621
734
|
matchLength: number,
|
|
622
735
|
): { sentence: string; start: number; end: number } {
|
|
623
736
|
let start = matchStart;
|
|
624
|
-
while (start > 0 &&
|
|
737
|
+
while (start > 0 && !isSentenceBoundary(text, start - 1)) start--;
|
|
625
738
|
while (start < matchStart && /\s/.test(text[start])) start++;
|
|
626
739
|
|
|
627
740
|
let end = matchStart + matchLength;
|
|
628
|
-
while (end < text.length &&
|
|
741
|
+
while (end < text.length && !isSentenceBoundary(text, end)) end++;
|
|
629
742
|
if (end < text.length) end++;
|
|
630
743
|
|
|
631
744
|
return { sentence: text.slice(start, end).trim(), start, end };
|
package/src/generate/index.ts
CHANGED
|
@@ -16,6 +16,3 @@ export type {
|
|
|
16
16
|
|
|
17
17
|
export { mapElementToBlock } from './templateMapper.js';
|
|
18
18
|
export type { MapOptions } from './templateMapper.js';
|
|
19
|
-
|
|
20
|
-
export { generateSlideshow } from './slideshowGenerator.js';
|
|
21
|
-
export type { SlideshowDoc, SlideshowImage, SlideshowOptions } from './slideshowGenerator.js';
|
package/src/icons/index.ts
CHANGED
|
@@ -16,3 +16,11 @@ export {
|
|
|
16
16
|
iconGlyph,
|
|
17
17
|
} from './resolve.js';
|
|
18
18
|
export type { IconSuggestion } from './resolve.js';
|
|
19
|
+
export {
|
|
20
|
+
iconMarker,
|
|
21
|
+
hasIconMarker,
|
|
22
|
+
stripIconMarkers,
|
|
23
|
+
splitIconMarkers,
|
|
24
|
+
iconClass,
|
|
25
|
+
} from './inlineIconMarker.js';
|
|
26
|
+
export type { IconTextRun } from './inlineIconMarker.js';
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inline-icon markers for template text.
|
|
3
|
+
*
|
|
4
|
+
* Slide templates flatten rich markdown body content into strings before
|
|
5
|
+
* rendering. Inline icons need to survive that flattening step, so we encode
|
|
6
|
+
* them as sentinel-delimited text markers and let `TextLayer` turn them back
|
|
7
|
+
* into FontAwesome glyphs when rendering.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { IconFamily } from './iconData.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Private-Use-Area sentinel that brackets an encoded icon.
|
|
14
|
+
* Kept as an escape sequence so the source remains valid TypeScript text.
|
|
15
|
+
*/
|
|
16
|
+
const SENTINEL = '\uE000';
|
|
17
|
+
|
|
18
|
+
/** Matches one encoded marker: `<sentinel><family>:<name><sentinel>`. */
|
|
19
|
+
const MARKER_RE = new RegExp(`${SENTINEL}([a-z]+):([a-zA-Z0-9-]+)${SENTINEL}`, 'g');
|
|
20
|
+
|
|
21
|
+
/** Encode a resolved icon (family + name) as an embeddable text marker. */
|
|
22
|
+
export function iconMarker(family: IconFamily, name: string): string {
|
|
23
|
+
return `${SENTINEL}${family}:${name}${SENTINEL}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** True when a string contains at least one encoded icon marker. */
|
|
27
|
+
export function hasIconMarker(value: string): boolean {
|
|
28
|
+
return value.includes(SENTINEL);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Remove every icon marker, leaving a clean plain-text projection. */
|
|
32
|
+
export function stripIconMarkers(value: string): string {
|
|
33
|
+
return value.replace(MARKER_RE, '');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** A run of a marker-bearing string: literal text or a resolved icon. */
|
|
37
|
+
export type IconTextRun =
|
|
38
|
+
| { type: 'text'; text: string }
|
|
39
|
+
| { type: 'icon'; family: IconFamily; name: string };
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Split a string into ordered text/icon runs. Text between markers is
|
|
43
|
+
* preserved verbatim, including whitespace; each marker becomes an icon run.
|
|
44
|
+
*/
|
|
45
|
+
export function splitIconMarkers(value: string): IconTextRun[] {
|
|
46
|
+
const runs: IconTextRun[] = [];
|
|
47
|
+
let lastIndex = 0;
|
|
48
|
+
MARKER_RE.lastIndex = 0;
|
|
49
|
+
|
|
50
|
+
let match: RegExpExecArray | null;
|
|
51
|
+
while ((match = MARKER_RE.exec(value)) !== null) {
|
|
52
|
+
if (match.index > lastIndex) {
|
|
53
|
+
runs.push({ type: 'text', text: value.slice(lastIndex, match.index) });
|
|
54
|
+
}
|
|
55
|
+
runs.push({
|
|
56
|
+
type: 'icon',
|
|
57
|
+
family: match[1] as IconFamily,
|
|
58
|
+
name: match[2],
|
|
59
|
+
});
|
|
60
|
+
lastIndex = match.index + match[0].length;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (lastIndex < value.length) {
|
|
64
|
+
runs.push({ type: 'text', text: value.slice(lastIndex) });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return runs;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** FontAwesome CSS class for a resolved icon, e.g. `fa-solid fa-rocket`. */
|
|
71
|
+
export function iconClass(family: IconFamily, name: string): string {
|
|
72
|
+
return `fa-${family} fa-${name}`;
|
|
73
|
+
}
|
package/src/imageEdit/export.ts
CHANGED
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
|
|
16
16
|
import type { ContentContainer } from '../storage/ContentContainer.js';
|
|
17
17
|
import type { ImageEditCanvas, ImageEditDoc, ImageEditLayer } from '../schemas/ImageEditDoc.js';
|
|
18
|
-
import type { Position } from '../schemas/Doc.js';
|
|
18
|
+
import type { MarkerStyle, Position } from '../schemas/Doc.js';
|
|
19
|
+
import { markerPath, shapePath } from '../doc/utils/shapeGeometry.js';
|
|
19
20
|
|
|
20
21
|
/** Supported export formats. */
|
|
21
22
|
export type ImageEditExportFormat = 'png' | 'jpeg' | 'webp';
|
|
@@ -78,7 +79,15 @@ export async function exportImageEditDoc(
|
|
|
78
79
|
// SVG construction
|
|
79
80
|
// ============================================
|
|
80
81
|
|
|
81
|
-
|
|
82
|
+
/**
|
|
83
|
+
* Build the standalone SVG string that mirrors the canvas + layers. Exposed
|
|
84
|
+
* for tests (the raster step needs a browser); shape/text/path layers don't
|
|
85
|
+
* touch `container`, only image layers do (to inline asset bytes).
|
|
86
|
+
*/
|
|
87
|
+
export async function buildSvgString(
|
|
88
|
+
doc: ImageEditDoc,
|
|
89
|
+
container: ContentContainer,
|
|
90
|
+
): Promise<string> {
|
|
82
91
|
const { width, height, background } = doc.canvas;
|
|
83
92
|
const bgRect =
|
|
84
93
|
background && background !== 'transparent'
|
|
@@ -145,6 +154,10 @@ async function renderLayerInner(
|
|
|
145
154
|
return `<line x1="${x}" y1="${y}" x2="${x + w}" y2="${y + h}"${stroke}${strokeWidth}/>`;
|
|
146
155
|
}
|
|
147
156
|
|
|
157
|
+
if (layer.type === 'path') {
|
|
158
|
+
return renderPathToSvg(layer, x, y, w, h);
|
|
159
|
+
}
|
|
160
|
+
|
|
148
161
|
// text
|
|
149
162
|
const c = layer.content;
|
|
150
163
|
const style = c.style;
|
|
@@ -169,6 +182,76 @@ async function renderLayerInner(
|
|
|
169
182
|
);
|
|
170
183
|
}
|
|
171
184
|
|
|
185
|
+
/**
|
|
186
|
+
* Serialize a `path` layer to SVG. Mirrors the runtime
|
|
187
|
+
* `react/src/layers/PathLayer.tsx`: when `shapeKind` is set, `d` is
|
|
188
|
+
* re-derived from the position box via `shapePath` (so named shapes scale);
|
|
189
|
+
* otherwise the stored absolute `d` is used. Start/end markers are emitted
|
|
190
|
+
* as `<marker>` defs scoped to the layer id, matching `markerPath` geometry.
|
|
191
|
+
*/
|
|
192
|
+
function renderPathToSvg(
|
|
193
|
+
layer: ImageEditLayer & { type: 'path' },
|
|
194
|
+
x: number,
|
|
195
|
+
y: number,
|
|
196
|
+
w: number,
|
|
197
|
+
h: number,
|
|
198
|
+
): string {
|
|
199
|
+
const c = layer.content;
|
|
200
|
+
const d = c.shapeKind ? (shapePath(c.shapeKind, x, y, w, h) ?? c.d) : c.d;
|
|
201
|
+
const stroke = c.stroke ?? '#1e293b';
|
|
202
|
+
const strokeWidth = c.strokeWidth ?? 2;
|
|
203
|
+
const fill = c.fill ?? 'none';
|
|
204
|
+
const dash = c.dasharray ? ` stroke-dasharray="${escapeAttr(c.dasharray)}"` : '';
|
|
205
|
+
|
|
206
|
+
// Markers: explicit start/end win; otherwise derive from the legacy `arrow` flag.
|
|
207
|
+
const startStyle = effectiveExportMarker(c.startMarker, c.arrow, 'start');
|
|
208
|
+
const endStyle = effectiveExportMarker(c.endMarker, c.arrow, 'end');
|
|
209
|
+
const start = markerPath(startStyle, 'start');
|
|
210
|
+
const end = markerPath(endStyle, 'end');
|
|
211
|
+
const startId = `marker-start-${layer.id}`;
|
|
212
|
+
const endId = `marker-end-${layer.id}`;
|
|
213
|
+
const defs =
|
|
214
|
+
start || end
|
|
215
|
+
? `<defs>${start ? markerDefSvg(startId, 'start', start, stroke) : ''}${
|
|
216
|
+
end ? markerDefSvg(endId, 'end', end, stroke) : ''
|
|
217
|
+
}</defs>`
|
|
218
|
+
: '';
|
|
219
|
+
const markerStart = start ? ` marker-start="url(#${startId})"` : '';
|
|
220
|
+
const markerEnd = end ? ` marker-end="url(#${endId})"` : '';
|
|
221
|
+
|
|
222
|
+
return (
|
|
223
|
+
defs +
|
|
224
|
+
`<path d="${escapeAttr(d)}" fill="${escapeAttr(fill)}"` +
|
|
225
|
+
` stroke="${escapeAttr(stroke)}" stroke-width="${strokeWidth}"${dash}${markerStart}${markerEnd}/>`
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function effectiveExportMarker(
|
|
230
|
+
explicit: MarkerStyle | undefined,
|
|
231
|
+
arrow: (ImageEditLayer & { type: 'path' })['content']['arrow'],
|
|
232
|
+
end: 'start' | 'end',
|
|
233
|
+
): MarkerStyle {
|
|
234
|
+
if (explicit) return explicit;
|
|
235
|
+
return arrow === 'both' || arrow === end ? 'arrow' : 'none';
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function markerDefSvg(
|
|
239
|
+
id: string,
|
|
240
|
+
dir: 'start' | 'end',
|
|
241
|
+
marker: { d: string; filled: boolean },
|
|
242
|
+
stroke: string,
|
|
243
|
+
): string {
|
|
244
|
+
const refX = dir === 'end' ? 9 : 1;
|
|
245
|
+
const paint = marker.filled
|
|
246
|
+
? `fill="${escapeAttr(stroke)}"`
|
|
247
|
+
: `fill="none" stroke="${escapeAttr(stroke)}" stroke-width="1.5"`;
|
|
248
|
+
return (
|
|
249
|
+
`<marker id="${escapeAttr(id)}" viewBox="0 0 10 10" refX="${refX}" refY="5"` +
|
|
250
|
+
` markerWidth="4" markerHeight="4" orient="auto-start-reverse" markerUnits="strokeWidth">` +
|
|
251
|
+
`<path d="${escapeAttr(marker.d)}" ${paint}/></marker>`
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
172
255
|
function preserveAspectRatioFor(fit?: 'cover' | 'contain' | 'fill'): string {
|
|
173
256
|
if (fit === 'cover') return 'xMidYMid slice';
|
|
174
257
|
if (fit === 'fill') return 'none';
|
package/src/imageEdit/index.ts
CHANGED
|
@@ -59,4 +59,55 @@ function assertImageEditDoc(value: unknown, filename: string): asserts value is
|
|
|
59
59
|
if (!Array.isArray(v.layers)) {
|
|
60
60
|
throw new Error(`readImageEditDoc: ${filename} layers must be an array`);
|
|
61
61
|
}
|
|
62
|
+
v.layers.forEach((layer, i) => assertLayer(layer, i, filename));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const VALID_LAYER_TYPES = new Set(['image', 'text', 'shape', 'path']);
|
|
66
|
+
|
|
67
|
+
function assertLayer(value: unknown, index: number, filename: string): void {
|
|
68
|
+
const ctx = `${filename} layers[${index}]`;
|
|
69
|
+
if (!value || typeof value !== 'object') {
|
|
70
|
+
throw new Error(`readImageEditDoc: ${ctx} must be an object`);
|
|
71
|
+
}
|
|
72
|
+
const layer = value as Record<string, unknown>;
|
|
73
|
+
|
|
74
|
+
if (typeof layer['id'] !== 'string') {
|
|
75
|
+
throw new Error(`readImageEditDoc: ${ctx} must have a string id`);
|
|
76
|
+
}
|
|
77
|
+
if (typeof layer['type'] !== 'string' || !VALID_LAYER_TYPES.has(layer['type'])) {
|
|
78
|
+
throw new Error(
|
|
79
|
+
`readImageEditDoc: ${ctx} type must be one of image|text|shape, got ${String(layer['type'])}`,
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const pos = layer['position'];
|
|
84
|
+
if (
|
|
85
|
+
!pos ||
|
|
86
|
+
typeof pos !== 'object' ||
|
|
87
|
+
(typeof (pos as Record<string, unknown>)['x'] !== 'number' &&
|
|
88
|
+
typeof (pos as Record<string, unknown>)['x'] !== 'string') ||
|
|
89
|
+
(typeof (pos as Record<string, unknown>)['y'] !== 'number' &&
|
|
90
|
+
typeof (pos as Record<string, unknown>)['y'] !== 'string')
|
|
91
|
+
) {
|
|
92
|
+
throw new Error(`readImageEditDoc: ${ctx} position must have numeric or string x and y`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const content = layer['content'];
|
|
96
|
+
if (!content || typeof content !== 'object') {
|
|
97
|
+
throw new Error(`readImageEditDoc: ${ctx} content must be an object`);
|
|
98
|
+
}
|
|
99
|
+
const c = content as Record<string, unknown>;
|
|
100
|
+
|
|
101
|
+
if (layer['type'] === 'image' && typeof c['src'] !== 'string') {
|
|
102
|
+
throw new Error(`readImageEditDoc: ${ctx} (image) content.src must be a string`);
|
|
103
|
+
}
|
|
104
|
+
if (layer['type'] === 'text' && typeof c['text'] !== 'string') {
|
|
105
|
+
throw new Error(`readImageEditDoc: ${ctx} (text) content.text must be a string`);
|
|
106
|
+
}
|
|
107
|
+
if (layer['type'] === 'shape' && typeof c['shape'] !== 'string') {
|
|
108
|
+
throw new Error(`readImageEditDoc: ${ctx} (shape) content.shape must be a string`);
|
|
109
|
+
}
|
|
110
|
+
if (layer['type'] === 'path' && typeof c['d'] !== 'string') {
|
|
111
|
+
throw new Error(`readImageEditDoc: ${ctx} (path) content.d must be a string`);
|
|
112
|
+
}
|
|
62
113
|
}
|
package/src/index.ts
CHANGED
|
@@ -6,7 +6,9 @@ export * from './markdown/index.js';
|
|
|
6
6
|
export * from './timing/index.js';
|
|
7
7
|
export * from './random/index.js';
|
|
8
8
|
export * from './generate/index.js';
|
|
9
|
+
export * from './transform/index.js';
|
|
9
10
|
export * from './versions/index.js';
|
|
10
11
|
export * from './jsonForm/index.js';
|
|
11
12
|
export * from './imageEdit/index.js';
|
|
13
|
+
export * from './icons/index.js';
|
|
12
14
|
export * from './recommend/index.js';
|
package/src/jsonForm/index.ts
CHANGED
|
@@ -20,3 +20,5 @@ export { evaluateWhen, resolveFlag } from './evaluateWhen.js';
|
|
|
20
20
|
export { toPointer, pointerSegments, getByPointer, setByPointer, resolveRef } from './pointer.js';
|
|
21
21
|
export { inferSchema } from './inferSchema.js';
|
|
22
22
|
export type { InferSchemaOptions } from './inferSchema.js';
|
|
23
|
+
export { buildJsonFormTokens, resolveJsonFormTheme } from './tokens.js';
|
|
24
|
+
export type { BuildJsonFormTokensOptions } from './tokens.js';
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure CSS-custom-property builder shared by `<JsonView>` (read-only, in
|
|
3
|
+
* `@bendyline/squisq-react`) and `<JsonEditor>` (editable, in
|
|
4
|
+
* `@bendyline/squisq-editor-react`).
|
|
5
|
+
*
|
|
6
|
+
* Both components derive an identical token bag from a Theme + resolved
|
|
7
|
+
* Surface; they differ only in the CSS-var prefix (`--squisq-json` vs
|
|
8
|
+
* `--squisq-jsonform`) so the viewer and editor can coexist on one page.
|
|
9
|
+
* This function is the single source of that derivation. It is pure and
|
|
10
|
+
* framework-free: it performs no `window` / `matchMedia` access — the
|
|
11
|
+
* surface must already be resolved to a concrete `SurfaceScheme` (or left
|
|
12
|
+
* `undefined` to skip surface application) by the calling React hook.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { applySurface, DEFAULT_THEME, resolveFontFamily } from '../schemas/index.js';
|
|
16
|
+
import type { SurfaceScheme, Theme } from '../schemas/index.js';
|
|
17
|
+
|
|
18
|
+
export interface BuildJsonFormTokensOptions {
|
|
19
|
+
/**
|
|
20
|
+
* CSS-var prefix WITHOUT the trailing dash, e.g. `--squisq-json` or
|
|
21
|
+
* `--squisq-jsonform`. Each token key is `${prefix}-${name}`.
|
|
22
|
+
*/
|
|
23
|
+
prefix: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Build the CSS custom-property map for a JSON form/view surface.
|
|
28
|
+
*
|
|
29
|
+
* @param theme The theme to render with (falls back to `DEFAULT_THEME`).
|
|
30
|
+
* @param surface Already-resolved concrete surface, or `undefined` to leave
|
|
31
|
+
* the theme un-surfaced. (The `'auto'` → concrete resolution
|
|
32
|
+
* is the React hook's job, so this stays pure.)
|
|
33
|
+
* @param options `{ prefix }` — the CSS-var namespace.
|
|
34
|
+
*/
|
|
35
|
+
export function buildJsonFormTokens(
|
|
36
|
+
theme: Theme | undefined,
|
|
37
|
+
surface: SurfaceScheme | undefined,
|
|
38
|
+
options: BuildJsonFormTokensOptions,
|
|
39
|
+
): Record<string, string> {
|
|
40
|
+
const { prefix } = options;
|
|
41
|
+
const baseTheme = theme ?? DEFAULT_THEME;
|
|
42
|
+
const finalTheme = surface ? applySurface(baseTheme, surface) : baseTheme;
|
|
43
|
+
|
|
44
|
+
const titleFont = resolveFontFamily(finalTheme.typography.titleFont, 'system-ui, sans-serif');
|
|
45
|
+
const bodyFont = resolveFontFamily(finalTheme.typography.bodyFont, 'system-ui, sans-serif');
|
|
46
|
+
const monoFont = resolveFontFamily(
|
|
47
|
+
finalTheme.typography.monoFont,
|
|
48
|
+
'ui-monospace, Consolas, monospace',
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
[`${prefix}-bg`]: finalTheme.colors.background,
|
|
53
|
+
[`${prefix}-text`]: finalTheme.colors.text,
|
|
54
|
+
[`${prefix}-muted`]: finalTheme.colors.textMuted,
|
|
55
|
+
[`${prefix}-primary`]: finalTheme.colors.primary,
|
|
56
|
+
[`${prefix}-accent`]: finalTheme.colors.secondary,
|
|
57
|
+
[`${prefix}-warning`]: finalTheme.colors.warning,
|
|
58
|
+
[`${prefix}-border`]: `color-mix(in srgb, ${finalTheme.colors.textMuted} 35%, transparent)`,
|
|
59
|
+
[`${prefix}-input-bg`]: finalTheme.colors.backgroundLight,
|
|
60
|
+
[`${prefix}-title-font`]: titleFont,
|
|
61
|
+
[`${prefix}-body-font`]: bodyFont,
|
|
62
|
+
[`${prefix}-mono-font`]: monoFont,
|
|
63
|
+
[`${prefix}-radius`]: `${finalTheme.style.borderRadius ?? 8}px`,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Resolve the effective theme after surface application — the companion to
|
|
69
|
+
* `buildJsonFormTokens` for callers that also need the `Theme` object (both
|
|
70
|
+
* hooks return it alongside the style bag).
|
|
71
|
+
*/
|
|
72
|
+
export function resolveJsonFormTheme(
|
|
73
|
+
theme: Theme | undefined,
|
|
74
|
+
surface: SurfaceScheme | undefined,
|
|
75
|
+
): Theme {
|
|
76
|
+
const baseTheme = theme ?? DEFAULT_THEME;
|
|
77
|
+
return surface ? applySurface(baseTheme, surface) : baseTheme;
|
|
78
|
+
}
|