@bendyline/squisq 1.5.2 → 2.0.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 +63 -10
- package/dist/{Doc-BBVGq1_9.d.ts → Doc-ylqWKDc_.d.ts} +248 -37
- package/dist/{ImageEditDoc-FuyGtt6o.d.ts → ImageEditDoc-D828IXJ1.d.ts} +1 -1
- package/dist/annotationCoercion-Dr_DY9Hx.d.ts +86 -0
- package/dist/{chunk-T5UK6YOB.js → chunk-2PWOO4ZE.js} +60 -17
- package/dist/chunk-2PWOO4ZE.js.map +1 -0
- package/dist/{chunk-VWUFZ6ZG.js → chunk-3E5F2XMR.js} +2 -29
- package/dist/chunk-3E5F2XMR.js.map +1 -0
- package/dist/chunk-4X3JQXNM.js +35 -0
- package/dist/chunk-4X3JQXNM.js.map +1 -0
- package/dist/{chunk-P3K7NLHF.js → chunk-5DKBFXPY.js} +31 -121
- package/dist/chunk-5DKBFXPY.js.map +1 -0
- package/dist/{chunk-DLXZMS5K.js → chunk-5J4HKRJF.js} +51 -3
- package/dist/chunk-5J4HKRJF.js.map +1 -0
- package/dist/chunk-5UYI3RJB.js +2503 -0
- package/dist/chunk-5UYI3RJB.js.map +1 -0
- package/dist/{chunk-BEQODJWV.js → chunk-66QO7XFB.js} +11 -291
- package/dist/chunk-66QO7XFB.js.map +1 -0
- package/dist/{chunk-HWVFJAAH.js → chunk-AMIR72JP.js} +28 -10
- package/dist/chunk-AMIR72JP.js.map +1 -0
- package/dist/chunk-CY2R2VPR.js +884 -0
- package/dist/chunk-CY2R2VPR.js.map +1 -0
- package/dist/chunk-FVJUUTEM.js +74 -0
- package/dist/chunk-FVJUUTEM.js.map +1 -0
- package/dist/{chunk-Q3ROPT5H.js → chunk-GZY66SDN.js} +1 -1
- package/dist/{chunk-Q3ROPT5H.js.map → chunk-GZY66SDN.js.map} +1 -1
- package/dist/chunk-JOQYPRDZ.js +127 -0
- package/dist/chunk-JOQYPRDZ.js.map +1 -0
- package/dist/{chunk-A4O7GIWE.js → chunk-MMPBOUK7.js} +10 -73
- package/dist/chunk-MMPBOUK7.js.map +1 -0
- package/dist/{chunk-LDQ2HJIX.js → chunk-PXLVXTLW.js} +252 -54
- package/dist/chunk-PXLVXTLW.js.map +1 -0
- package/dist/{chunk-TQWLI6S2.js → chunk-Q7LROV2H.js} +4 -2
- package/dist/{chunk-TQWLI6S2.js.map → chunk-Q7LROV2H.js.map} +1 -1
- package/dist/{chunk-GPLTCSXO.js → chunk-R2GC6RLD.js} +7 -7
- package/dist/chunk-R2GC6RLD.js.map +1 -0
- package/dist/chunk-R2YZTCLO.js +292 -0
- package/dist/chunk-R2YZTCLO.js.map +1 -0
- package/dist/chunk-S7A5D7ZJ.js +174 -0
- package/dist/chunk-S7A5D7ZJ.js.map +1 -0
- package/dist/chunk-TFJVXUHH.js +2358 -0
- package/dist/chunk-TFJVXUHH.js.map +1 -0
- package/dist/{chunk-LTN6I7QW.js → chunk-TUWMK62B.js} +55 -4
- package/dist/chunk-TUWMK62B.js.map +1 -0
- package/dist/{chunk-IMSCRLLV.js → chunk-VR7UPJ7C.js} +35 -25
- package/dist/chunk-VR7UPJ7C.js.map +1 -0
- package/dist/{chunk-KKNUBQ6Y.js → chunk-W4O2TX5I.js} +157 -92
- package/dist/chunk-W4O2TX5I.js.map +1 -0
- package/dist/{chunk-5TYS5V3G.js → chunk-X6DUONGT.js} +4204 -3560
- package/dist/chunk-X6DUONGT.js.map +1 -0
- package/dist/doc/index.d.ts +2334 -5
- package/dist/doc/index.js +127 -33
- package/dist/generate/index.d.ts +7 -2
- package/dist/generate/index.js +1 -1
- package/dist/imageEdit/index.d.ts +9 -6
- package/dist/imageEdit/index.js +3 -3
- package/dist/index.d.ts +12 -10
- package/dist/index.js +246 -80
- package/dist/jsonForm/index.d.ts +7 -3
- package/dist/jsonForm/index.js +9 -4
- package/dist/markdown/index.d.ts +6 -70
- package/dist/markdown/index.js +13 -9
- package/dist/narration/index.d.ts +618 -0
- package/dist/narration/index.js +82 -0
- package/dist/recommend/index.d.ts +15 -1
- package/dist/recommend/index.js +2 -2
- package/dist/schemas/index.d.ts +28 -6
- package/dist/schemas/index.js +15 -13
- package/dist/spatial/index.d.ts +0 -1
- package/dist/spatial/index.js +1 -1
- package/dist/storage/index.js +1 -1
- package/dist/{themeLibrary-CehcJhzz.d.ts → themeLibrary-RjVgLpUi.d.ts} +10 -11
- package/dist/timing/index.js +5 -3
- package/dist/transform/index.d.ts +35 -24
- package/dist/transform/index.js +9 -10
- package/dist/{types-DlAZ7MW4.d.ts → types-BwMPl2CB.d.ts} +1 -1
- package/dist/{types-D98li1gV.d.ts → types-C4kiRn_k.d.ts} +3 -1
- package/dist/versions/index.d.ts +2 -1
- package/dist/versions/index.js +2 -2
- package/package.json +6 -6
- package/src/__tests__/applyNarrationTiming.test.ts +190 -0
- package/src/__tests__/applyRenderStyle.test.ts +3 -3
- package/src/__tests__/asciiDiagramBattle.test.ts +246 -0
- package/src/__tests__/asciiDiagramDetect.test.ts +162 -0
- package/src/__tests__/asciiDiagramMapping.test.ts +38 -0
- package/src/__tests__/asciiDiagramParse.test.ts +233 -0
- package/src/__tests__/asciiDiagramPipeline.test.ts +273 -0
- package/src/__tests__/asciiDiagramRender.test.ts +162 -0
- package/src/__tests__/asciiDiagramRepair.test.ts +84 -0
- package/src/__tests__/asciiTimelineDetect.test.ts +157 -0
- package/src/__tests__/asciiTimelineParse.test.ts +205 -0
- package/src/__tests__/asciiTimelinePipeline.test.ts +349 -0
- package/src/__tests__/asciiTimelineRender.test.ts +376 -0
- package/src/__tests__/attrTokens.test.ts +5 -0
- package/src/__tests__/contentContainer.test.ts +43 -0
- package/src/__tests__/customTemplatesFrontmatter.test.ts +0 -15
- package/src/__tests__/customThemesFrontmatter.test.ts +1 -0
- package/src/__tests__/dataTable.test.ts +25 -15
- package/src/__tests__/diagramBlock.test.ts +3 -3
- package/src/__tests__/diagramText.test.ts +26 -0
- package/src/__tests__/docToMarkdownMedia.test.ts +83 -0
- package/src/__tests__/drawingBlock.test.ts +3 -3
- package/src/__tests__/fixtures/asciiDiagramBattle.ts +512 -0
- package/src/__tests__/fixtures/asciiDiagrams.ts +287 -0
- package/src/__tests__/fixtures/asciiTimelines.ts +35 -0
- package/src/__tests__/fixtures/treeviewFixtures.ts +243 -0
- package/src/__tests__/getLayers.test.ts +32 -18
- package/src/__tests__/getLayersFallback.test.ts +8 -8
- package/src/__tests__/imageEditVersions.test.ts +2 -0
- package/src/__tests__/jsonForm.pointer.test.ts +47 -1
- package/src/__tests__/layoutBlock.test.ts +3 -3
- package/src/__tests__/majorApiTypes.test.ts +18 -0
- package/src/__tests__/markdown.test.ts +54 -4
- package/src/__tests__/markdownSanitize.test.ts +7 -5
- package/src/__tests__/matchFontFamily.test.ts +114 -0
- package/src/__tests__/materializeBlockLayers.test.ts +135 -0
- package/src/__tests__/materializeTestUtils.ts +17 -0
- package/src/__tests__/narrationAlign.test.ts +150 -0
- package/src/__tests__/narrationFeatures.test.ts +71 -0
- package/src/__tests__/narrationNuclei.test.ts +45 -0
- package/src/__tests__/narrationPacing.test.ts +189 -0
- package/src/__tests__/narrationScript.test.ts +149 -0
- package/src/__tests__/narrationSessionRepro.test.ts +62 -0
- package/src/__tests__/narrationSidecar.test.ts +88 -0
- package/src/__tests__/narrationSyllables.test.ts +65 -0
- package/src/__tests__/narrationTestSignals.ts +167 -0
- package/src/__tests__/narrationVad.test.ts +66 -0
- package/src/__tests__/resolveDocTheme.test.ts +16 -2
- package/src/__tests__/rootExports.test.ts +18 -0
- package/src/__tests__/scopedContentContainer.test.ts +20 -0
- package/src/__tests__/shapeGeometry.test.ts +12 -0
- package/src/__tests__/templateAnnotationParse.test.ts +10 -0
- package/src/__tests__/templateInputs.test.ts +69 -0
- package/src/__tests__/templateMetadata.test.ts +2 -1
- package/src/__tests__/templates.test.ts +143 -17
- package/src/__tests__/themeCompile.test.ts +49 -22
- package/src/__tests__/transformProvenance.test.ts +160 -0
- package/src/__tests__/transformV2.test.ts +40 -14
- package/src/__tests__/treeviewBattle.test.ts +76 -0
- package/src/__tests__/treeviewDetect.test.ts +168 -0
- package/src/__tests__/treeviewParse.test.ts +118 -0
- package/src/__tests__/treeviewPipeline.test.ts +183 -0
- package/src/__tests__/treeviewRender.test.ts +91 -0
- package/src/__tests__/versions.test.ts +44 -0
- package/src/doc/applyNarrationTiming.ts +244 -0
- package/src/doc/asciiDiagram/chars.ts +210 -0
- package/src/doc/asciiDiagram/detect.ts +175 -0
- package/src/doc/asciiDiagram/index.ts +38 -0
- package/src/doc/asciiDiagram/mapping.ts +225 -0
- package/src/doc/asciiDiagram/parse.ts +1095 -0
- package/src/doc/asciiDiagram/render.ts +963 -0
- package/src/doc/asciiDiagram/repair.ts +400 -0
- package/src/doc/asciiDiagram/types.ts +65 -0
- package/src/doc/asciiTimeline/detect.ts +138 -0
- package/src/doc/asciiTimeline/index.ts +26 -0
- package/src/doc/asciiTimeline/mapping.ts +43 -0
- package/src/doc/asciiTimeline/parse.ts +870 -0
- package/src/doc/asciiTimeline/render.ts +427 -0
- package/src/doc/asciiTimeline/types.ts +73 -0
- package/src/doc/asciiTimeline/wrappedFlow.ts +427 -0
- package/src/doc/audioMapping.ts +60 -46
- package/src/doc/customTemplatesFrontmatter.ts +0 -41
- package/src/doc/docToMarkdown.ts +70 -2
- package/src/doc/index.ts +76 -4
- package/src/doc/markdownToDoc.ts +138 -2
- package/src/doc/materializeBlockLayers.ts +304 -0
- package/src/doc/mediaAnnotations.ts +20 -0
- package/src/doc/resolveDocTheme.ts +8 -7
- package/src/doc/templateInputs.ts +185 -2
- package/src/doc/templates/__tests__/customTemplateRegistry.test.ts +22 -8
- package/src/doc/templates/__tests__/listBlock.test.ts +64 -0
- package/src/doc/templates/diagramBlock.ts +237 -38
- package/src/doc/templates/fallbackBlock.ts +1 -1
- package/src/doc/templates/index.ts +134 -279
- package/src/doc/templates/inputDescriptors.ts +20 -1
- package/src/doc/templates/listBlock.ts +88 -30
- package/src/doc/templates/metadata.ts +10 -0
- package/src/doc/templates/persistentLayers.ts +12 -126
- package/src/doc/templates/registry.ts +88 -0
- package/src/doc/templates/templateNames.ts +22 -0
- package/src/doc/templates/timelineBlock.ts +613 -0
- package/src/doc/templates/treeBlock.ts +131 -0
- package/src/doc/treeview/chars.ts +72 -0
- package/src/doc/treeview/detect.ts +145 -0
- package/src/doc/treeview/index.ts +30 -0
- package/src/doc/treeview/mapping.ts +95 -0
- package/src/doc/treeview/parse.ts +201 -0
- package/src/doc/treeview/render.ts +41 -0
- package/src/doc/treeview/types.ts +43 -0
- package/src/doc/utils/applyRenderStyle.ts +2 -9
- package/src/doc/utils/diagramText.ts +78 -0
- package/src/doc/utils/shapeGeometry.ts +67 -5
- package/src/doc/validate.ts +1 -1
- package/src/generate/templateMapper.ts +7 -0
- package/src/imageEdit/export.ts +6 -5
- package/src/imageEdit/versionPaths.ts +13 -26
- package/src/imageEdit/versions.ts +21 -100
- package/src/index.ts +1 -0
- package/src/internal/immutable.ts +58 -0
- package/src/jsonForm/index.ts +9 -1
- package/src/jsonForm/pointer.ts +68 -23
- package/src/markdown/attrTokens.ts +7 -5
- package/src/markdown/index.ts +1 -0
- package/src/markdown/stringify.ts +4 -9
- package/src/markdown/utils.ts +36 -14
- package/src/narration/align.ts +514 -0
- package/src/narration/features.ts +231 -0
- package/src/narration/index.ts +86 -0
- package/src/narration/nuclei.ts +118 -0
- package/src/narration/pacing.ts +211 -0
- package/src/narration/script.ts +239 -0
- package/src/narration/session.ts +100 -0
- package/src/narration/sidecar.ts +187 -0
- package/src/narration/syllables.ts +85 -0
- package/src/narration/trace.ts +50 -0
- package/src/narration/types.ts +299 -0
- package/src/narration/vad.ts +131 -0
- package/src/recommend/numberHighlight.ts +25 -0
- package/src/recommend/templates.ts +108 -12
- package/src/schemas/BlockTemplates.ts +156 -7
- package/src/schemas/Doc.ts +72 -9
- package/src/schemas/Media.ts +12 -2
- package/src/schemas/Theme.ts +45 -25
- package/src/schemas/Viewport.ts +2 -2
- package/src/schemas/fontStacks.ts +79 -0
- package/src/schemas/themeCompile.ts +15 -0
- package/src/schemas/themeConstants.ts +2 -0
- package/src/schemas/themeLibrary.ts +25 -19
- package/src/schemas/themeValidator.ts +1 -1
- package/src/schemas/themes/gezellig.json +1 -1
- package/src/spatial/Geohash.ts +0 -3
- package/src/storage/ContentContainer.ts +17 -9
- package/src/storage/ScopedContentContainer.ts +45 -15
- package/src/transform/applyTransform.ts +5 -5
- package/src/transform/index.ts +3 -1
- package/src/transform/registry.ts +169 -49
- package/src/transform/templateSelector.ts +21 -2
- package/src/transform/timingAllocator.ts +152 -21
- package/src/transform/types.ts +14 -1
- package/src/versions/gitignore.ts +34 -0
- package/src/versions/index.ts +2 -0
- package/src/versions/operations.ts +18 -104
- package/src/versions/paths.ts +13 -28
- package/src/versions/snapshotStore.ts +202 -0
- package/src/versions/types.ts +3 -1
- package/dist/chunk-5TYS5V3G.js.map +0 -1
- package/dist/chunk-A4O7GIWE.js.map +0 -1
- package/dist/chunk-BEQODJWV.js.map +0 -1
- package/dist/chunk-DLXZMS5K.js.map +0 -1
- package/dist/chunk-GPLTCSXO.js.map +0 -1
- package/dist/chunk-HWVFJAAH.js.map +0 -1
- package/dist/chunk-IMSCRLLV.js.map +0 -1
- package/dist/chunk-JELIRVDP.js +0 -24
- package/dist/chunk-JELIRVDP.js.map +0 -1
- package/dist/chunk-KKNUBQ6Y.js.map +0 -1
- package/dist/chunk-LDQ2HJIX.js.map +0 -1
- package/dist/chunk-LTN6I7QW.js.map +0 -1
- package/dist/chunk-P3K7NLHF.js.map +0 -1
- package/dist/chunk-T5UK6YOB.js.map +0 -1
- package/dist/chunk-UY7KGQ5R.js +0 -192
- package/dist/chunk-UY7KGQ5R.js.map +0 -1
- package/dist/chunk-VLJJHDAC.js +0 -218
- package/dist/chunk-VLJJHDAC.js.map +0 -1
- package/dist/chunk-VWUFZ6ZG.js.map +0 -1
- package/dist/story/index.d.ts +0 -1801
- package/dist/story/index.js +0 -279
- package/src/doc/getLayers.ts +0 -195
- /package/dist/{story → narration}/index.js.map +0 -0
|
@@ -73,6 +73,26 @@ describe('ScopedContentContainer', () => {
|
|
|
73
73
|
expect(() => scopeContainer(parent, '/')).toThrow();
|
|
74
74
|
expect(() => scopeContainer(parent, 'a/../b')).toThrow();
|
|
75
75
|
expect(() => scopeContainer(parent, './a')).toThrow();
|
|
76
|
+
expect(() => scopeContainer(parent, 'a\\b')).toThrow(/forward slashes/);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('rejects traversal, dot segments, and backslashes on every file operation', async () => {
|
|
80
|
+
const scoped = scopeContainer(parent, 'sidecar');
|
|
81
|
+
for (const badPath of ['../escape.txt', 'a/../../escape.txt', './same.txt', 'a\\b.txt']) {
|
|
82
|
+
expect(() => scoped.readFile(badPath)).toThrow();
|
|
83
|
+
expect(() => scoped.writeFile(badPath, enc('x'))).toThrow();
|
|
84
|
+
expect(() => scoped.removeFile(badPath)).toThrow();
|
|
85
|
+
expect(() => scoped.exists(badPath)).toThrow();
|
|
86
|
+
await expect(scoped.listFiles(badPath)).rejects.toThrow();
|
|
87
|
+
}
|
|
88
|
+
expect(await parent.listFiles()).toEqual([]);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('normalizes repeated and leading slashes without changing scope', async () => {
|
|
92
|
+
const scoped = scopeContainer(parent, '//sidecar//nested//');
|
|
93
|
+
expect(scoped.prefix).toBe('sidecar/nested');
|
|
94
|
+
await scoped.writeFile('//assets//x.txt', enc('x'));
|
|
95
|
+
expect(dec(await parent.readFile('sidecar/nested/assets/x.txt'))).toBe('x');
|
|
76
96
|
});
|
|
77
97
|
|
|
78
98
|
it('getDocumentPath / readDocument always return null', async () => {
|
|
@@ -69,9 +69,21 @@ describe('connectorPath', () => {
|
|
|
69
69
|
expect(connectorPath('orthogonal', a, b)).toBe('M 0 0 L 50 0 L 50 50 L 100 50');
|
|
70
70
|
});
|
|
71
71
|
|
|
72
|
+
it('orthogonal routing approaches top/bottom ports vertically', () => {
|
|
73
|
+
expect(
|
|
74
|
+
connectorPath('orthogonal', { port: 'bottom', x: 80, y: 60 }, { port: 'top', x: 20, y: 140 }),
|
|
75
|
+
).toBe('M 80 60 L 80 100 L 20 100 L 20 140');
|
|
76
|
+
});
|
|
77
|
+
|
|
72
78
|
it('curved is a cubic bezier', () => {
|
|
73
79
|
expect(connectorPath('curved', a, b).startsWith('M 0 0 C')).toBe(true);
|
|
74
80
|
});
|
|
81
|
+
|
|
82
|
+
it('curved routing follows the endpoint port directions', () => {
|
|
83
|
+
expect(
|
|
84
|
+
connectorPath('curved', { port: 'bottom', x: 80, y: 60 }, { port: 'top', x: 20, y: 140 }),
|
|
85
|
+
).toBe('M 80 60 C 80 110, 20 90, 20 140');
|
|
86
|
+
});
|
|
75
87
|
});
|
|
76
88
|
|
|
77
89
|
describe('clipEndpoints', () => {
|
|
@@ -11,6 +11,16 @@ function getHeading(md: string) {
|
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
describe('Template annotation in Heading parse path', () => {
|
|
14
|
+
it('extracts and round-trips an empty annotation without leaving literal heading text', () => {
|
|
15
|
+
const source = '## Getting Started {[]}';
|
|
16
|
+
const heading = getHeading(source);
|
|
17
|
+
|
|
18
|
+
expect(heading.templateAnnotation).toEqual({});
|
|
19
|
+
expect(heading.children).toHaveLength(1);
|
|
20
|
+
expect(heading.children[0]).toMatchObject({ type: 'text', value: 'Getting Started' });
|
|
21
|
+
expect(stringifyMarkdown(parseMarkdown(source)).trim()).toBe(source);
|
|
22
|
+
});
|
|
23
|
+
|
|
14
24
|
it('parseMarkdown extracts {[template]} from a heading', () => {
|
|
15
25
|
const md = '## Getting Started {[comparisonBar]}';
|
|
16
26
|
const doc = parseMarkdown(md);
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { parseMarkdown } from '../markdown/parse.js';
|
|
3
|
+
import { deriveTemplateInputs } from '../doc/templateInputs.js';
|
|
4
|
+
|
|
5
|
+
function statInputs(heading: string, body: string): Record<string, unknown> | null {
|
|
6
|
+
return deriveTemplateInputs('statHighlight', heading, parseMarkdown(body).children);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
describe('statHighlight template input derivation', () => {
|
|
10
|
+
it('uses a leading bold metric as the hero stat', () => {
|
|
11
|
+
expect(
|
|
12
|
+
statInputs('The Big Number', '**42%** of teams prefer visual blocks over raw slides.'),
|
|
13
|
+
).toEqual({
|
|
14
|
+
stat: '42%',
|
|
15
|
+
description: 'of teams prefer visual blocks over raw slides.',
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('accepts an explicitly bold small integer', () => {
|
|
20
|
+
expect(statInputs('Answer Count', '**42** answers were submitted')).toEqual({
|
|
21
|
+
stat: '42',
|
|
22
|
+
description: 'answers were submitted',
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('promotes only the metric when the bold run contains a longer phrase', () => {
|
|
27
|
+
expect(statInputs('Revenue', '**Revenue grew 42%** year over year')).toEqual({
|
|
28
|
+
stat: '42%',
|
|
29
|
+
description: 'Revenue grew year over year',
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('drops an inline separator between the metric and description', () => {
|
|
34
|
+
expect(
|
|
35
|
+
statInputs('The Big Number', '**42%** — The percentage of developers who prefer it.'),
|
|
36
|
+
).toEqual({
|
|
37
|
+
stat: '42%',
|
|
38
|
+
description: 'The percentage of developers who prefer it.',
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('preserves a stat-looking heading for existing documents', () => {
|
|
43
|
+
expect(statInputs('89%', 'of customers recommend it')).toEqual({
|
|
44
|
+
stat: '89%',
|
|
45
|
+
description: 'of customers recommend it',
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('extracts an unformatted body metric for automatic templates', () => {
|
|
50
|
+
expect(statInputs('Funding', '$2.3M raised')).toEqual({
|
|
51
|
+
stat: '$2.3M',
|
|
52
|
+
description: 'raised',
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('does not let a year in a structural heading eclipse the body metric', () => {
|
|
57
|
+
expect(statInputs('2024 Results', '42% of teams reached their goal')).toEqual({
|
|
58
|
+
stat: '42%',
|
|
59
|
+
description: 'of teams reached their goal',
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('retains the historical fallback when no metric can be found', () => {
|
|
64
|
+
expect(statInputs('A Result', 'Supporting context')).toEqual({
|
|
65
|
+
stat: 'A Result',
|
|
66
|
+
description: 'Supporting context',
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { templateRegistry, TEMPLATE_METADATA
|
|
2
|
+
import { templateRegistry, TEMPLATE_METADATA } from '../doc/templates/index';
|
|
3
|
+
import { TEMPLATE_ALIASES } from '../doc/templates/templateNames';
|
|
3
4
|
|
|
4
5
|
describe('TEMPLATE_METADATA', () => {
|
|
5
6
|
it('stays exactly 1:1 with templateRegistry (same ids)', () => {
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
2
|
import {
|
|
3
|
-
expandTemplateBlock,
|
|
4
3
|
expandDocBlocks,
|
|
5
4
|
getAvailableTemplates,
|
|
6
5
|
hasTemplate,
|
|
@@ -10,11 +9,41 @@ import {
|
|
|
10
9
|
VIEWPORT_PRESETS,
|
|
11
10
|
} from '../doc/templates/index';
|
|
12
11
|
import type { TemplateBlock } from '../schemas/BlockTemplates';
|
|
13
|
-
import type { TextLayer } from '../schemas/Doc';
|
|
14
|
-
import {
|
|
12
|
+
import type { Block, Layer, TextLayer } from '../schemas/Doc';
|
|
13
|
+
import type { RuntimeTemplateRegistry } from '../doc/templates/index';
|
|
14
|
+
import { materializeBlockLayersWithRuntime } from '../doc/materializeBlockLayers';
|
|
15
|
+
import { materializeLayers } from './materializeTestUtils';
|
|
15
16
|
import { markdownToDoc } from '../doc/markdownToDoc';
|
|
16
17
|
import { parseMarkdown } from '../markdown/parse';
|
|
17
18
|
|
|
19
|
+
function materializeTemplateForTest(
|
|
20
|
+
block: TemplateBlock,
|
|
21
|
+
context: ReturnType<typeof createTemplateContext>,
|
|
22
|
+
registry: RuntimeTemplateRegistry = templateRegistry as unknown as RuntimeTemplateRegistry,
|
|
23
|
+
): Block {
|
|
24
|
+
const { layers } = materializeBlockLayersWithRuntime(
|
|
25
|
+
block,
|
|
26
|
+
{
|
|
27
|
+
theme: context.theme,
|
|
28
|
+
viewport: context.viewport,
|
|
29
|
+
persistentLayers: false,
|
|
30
|
+
blockIndex: context.blockIndex,
|
|
31
|
+
totalBlocks: context.totalBlocks,
|
|
32
|
+
failureMode: 'empty',
|
|
33
|
+
},
|
|
34
|
+
{ registry, templateContext: context, applyRenderStyle: false },
|
|
35
|
+
);
|
|
36
|
+
return {
|
|
37
|
+
id: block.id,
|
|
38
|
+
startTime: 0,
|
|
39
|
+
duration: block.duration,
|
|
40
|
+
audioSegment: block.audioSegment,
|
|
41
|
+
...(layers.length > 0 ? { layers } : {}),
|
|
42
|
+
transition: block.transition,
|
|
43
|
+
template: block.template,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
18
47
|
describe('templateRegistry', () => {
|
|
19
48
|
it('contains all expected templates', () => {
|
|
20
49
|
const expected = [
|
|
@@ -66,7 +95,7 @@ describe('hasTemplate', () => {
|
|
|
66
95
|
});
|
|
67
96
|
});
|
|
68
97
|
|
|
69
|
-
describe('
|
|
98
|
+
describe('canonical template materialization', () => {
|
|
70
99
|
it('expands titleSlide template into layers', () => {
|
|
71
100
|
const block: TemplateBlock = {
|
|
72
101
|
template: 'title',
|
|
@@ -77,7 +106,7 @@ describe('expandTemplateBlock', () => {
|
|
|
77
106
|
subtitle: 'Test Subtitle',
|
|
78
107
|
};
|
|
79
108
|
const context = createTemplateContext(DEFAULT_THEME, 0, 5, VIEWPORT_PRESETS.landscape);
|
|
80
|
-
const result =
|
|
109
|
+
const result = materializeTemplateForTest(block, context);
|
|
81
110
|
|
|
82
111
|
expect(result.id).toBe('title-1');
|
|
83
112
|
expect(result.duration).toBe(10);
|
|
@@ -97,7 +126,7 @@ describe('expandTemplateBlock', () => {
|
|
|
97
126
|
body: 'I love building software platforms.',
|
|
98
127
|
};
|
|
99
128
|
const context = createTemplateContext(DEFAULT_THEME, 0, 1, VIEWPORT_PRESETS.landscape);
|
|
100
|
-
const result =
|
|
129
|
+
const result = materializeTemplateForTest(block, context);
|
|
101
130
|
const layers = result.layers ?? [];
|
|
102
131
|
const imageLayer = layers.find((l) => l.type === 'image');
|
|
103
132
|
expect(imageLayer).toBeDefined();
|
|
@@ -130,7 +159,7 @@ describe('expandTemplateBlock', () => {
|
|
|
130
159
|
title: 'Sized',
|
|
131
160
|
};
|
|
132
161
|
const context = createTemplateContext(DEFAULT_THEME, 0, 1, VIEWPORT_PRESETS.landscape);
|
|
133
|
-
const result =
|
|
162
|
+
const result = materializeTemplateForTest(block, context);
|
|
134
163
|
const imageLayer = (result.layers ?? []).find((l) => l.type === 'image');
|
|
135
164
|
expect(imageLayer).toBeDefined();
|
|
136
165
|
expect((imageLayer!.content as { fit?: string }).fit).toBe('contain');
|
|
@@ -153,7 +182,7 @@ describe('expandTemplateBlock', () => {
|
|
|
153
182
|
body: 'A list of things.',
|
|
154
183
|
};
|
|
155
184
|
const context = createTemplateContext(DEFAULT_THEME, 0, 1, VIEWPORT_PRESETS.landscape);
|
|
156
|
-
const result =
|
|
185
|
+
const result = materializeTemplateForTest(block, context);
|
|
157
186
|
const layers = result.layers ?? [];
|
|
158
187
|
const imageLayer = layers.find((l) => l.type === 'image');
|
|
159
188
|
expect(imageLayer).toBeDefined();
|
|
@@ -179,7 +208,7 @@ describe('expandTemplateBlock', () => {
|
|
|
179
208
|
description: 'drop in salmon',
|
|
180
209
|
};
|
|
181
210
|
const context = createTemplateContext(DEFAULT_THEME, 0, 5, VIEWPORT_PRESETS.landscape);
|
|
182
|
-
const result =
|
|
211
|
+
const result = materializeTemplateForTest(block, context);
|
|
183
212
|
|
|
184
213
|
expect((result.layers ?? []).length).toBeGreaterThan(0);
|
|
185
214
|
// Should have at least a shape background + text layers
|
|
@@ -195,7 +224,7 @@ describe('expandTemplateBlock', () => {
|
|
|
195
224
|
audioSegment: 0,
|
|
196
225
|
} as unknown as TemplateBlock;
|
|
197
226
|
const context = createTemplateContext(DEFAULT_THEME, 0, 1, VIEWPORT_PRESETS.landscape);
|
|
198
|
-
const result =
|
|
227
|
+
const result = materializeTemplateForTest(block, context);
|
|
199
228
|
expect(result.layers ?? []).toEqual([]);
|
|
200
229
|
});
|
|
201
230
|
|
|
@@ -241,7 +270,7 @@ describe('expandTemplateBlock', () => {
|
|
|
241
270
|
leftValue: 50,
|
|
242
271
|
rightValue: 50,
|
|
243
272
|
} as unknown as TemplateBlock;
|
|
244
|
-
const result =
|
|
273
|
+
const result = materializeTemplateForTest(block, context);
|
|
245
274
|
expect(result.id).toBe(`test-${name}`);
|
|
246
275
|
expect(result.layers ?? []).toBeInstanceOf(Array);
|
|
247
276
|
// Each template should produce at least 1 layer
|
|
@@ -265,7 +294,7 @@ describe('expandTemplateBlock', () => {
|
|
|
265
294
|
};
|
|
266
295
|
|
|
267
296
|
const context = createTemplateContext(DEFAULT_THEME, 0, 10, VIEWPORT_PRESETS.landscape);
|
|
268
|
-
const result =
|
|
297
|
+
const result = materializeTemplateForTest(block, context);
|
|
269
298
|
const layers = result.layers ?? [];
|
|
270
299
|
const captionLayer = layers.find(
|
|
271
300
|
(layer): layer is TextLayer => layer.type === 'text' && layer.id === 'caption',
|
|
@@ -301,10 +330,10 @@ describe('expandTemplateBlock', () => {
|
|
|
301
330
|
};
|
|
302
331
|
|
|
303
332
|
const context = createTemplateContext(DEFAULT_THEME, 0, 10, VIEWPORT_PRESETS.landscape);
|
|
304
|
-
const imageCaption = (
|
|
333
|
+
const imageCaption = (materializeTemplateForTest(imageBlock, context).layers ?? []).find(
|
|
305
334
|
(layer): layer is TextLayer => layer.type === 'text' && layer.id === 'caption',
|
|
306
335
|
);
|
|
307
|
-
const videoCaption = (
|
|
336
|
+
const videoCaption = (materializeTemplateForTest(videoBlock, context).layers ?? []).find(
|
|
308
337
|
(layer): layer is TextLayer => layer.type === 'text' && layer.id === 'caption',
|
|
309
338
|
);
|
|
310
339
|
|
|
@@ -370,6 +399,85 @@ describe('expandDocBlocks', () => {
|
|
|
370
399
|
expect(result[0].startTime).toBe(0);
|
|
371
400
|
});
|
|
372
401
|
|
|
402
|
+
it('never mutates or aliases raw blocks and persistent layers', () => {
|
|
403
|
+
const rawLayer: Layer = {
|
|
404
|
+
type: 'shape',
|
|
405
|
+
id: 'raw-shape',
|
|
406
|
+
content: { shape: 'rect', fill: '#112233' },
|
|
407
|
+
position: { x: 0, y: 0, width: '100%', height: '100%' },
|
|
408
|
+
};
|
|
409
|
+
const rawBlock: Block = {
|
|
410
|
+
id: 'raw',
|
|
411
|
+
startTime: 99,
|
|
412
|
+
duration: 6,
|
|
413
|
+
audioSegment: 0,
|
|
414
|
+
layers: [rawLayer],
|
|
415
|
+
};
|
|
416
|
+
const persistentLayer: Layer = {
|
|
417
|
+
type: 'shape',
|
|
418
|
+
id: 'persistent',
|
|
419
|
+
content: { shape: 'rect', fill: '#445566' },
|
|
420
|
+
position: { x: 0, y: 0, width: '100%', height: '100%' },
|
|
421
|
+
};
|
|
422
|
+
const beforeBlock = JSON.stringify(rawBlock);
|
|
423
|
+
const beforePersistent = JSON.stringify(persistentLayer);
|
|
424
|
+
|
|
425
|
+
const [expanded] = expandDocBlocks([rawBlock], {
|
|
426
|
+
persistentLayers: { bottomLayers: [persistentLayer] },
|
|
427
|
+
audioSegments: [{ startTime: 10, duration: 6 }],
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
expect(JSON.stringify(rawBlock)).toBe(beforeBlock);
|
|
431
|
+
expect(JSON.stringify(persistentLayer)).toBe(beforePersistent);
|
|
432
|
+
expect(expanded).not.toBe(rawBlock);
|
|
433
|
+
expect(expanded.layers?.[0]).not.toBe(persistentLayer);
|
|
434
|
+
expect(expanded.layers?.[1]).not.toBe(rawLayer);
|
|
435
|
+
|
|
436
|
+
(expanded.layers?.[0] as Layer & { content: { fill: string } }).content.fill = '#ffffff';
|
|
437
|
+
expect(JSON.stringify(persistentLayer)).toBe(beforePersistent);
|
|
438
|
+
expect(expandDocBlocks([rawBlock])[0].layers).toHaveLength(1);
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
it('protects template inputs from mutating registry implementations', () => {
|
|
442
|
+
const source: TemplateBlock = {
|
|
443
|
+
template: 'title',
|
|
444
|
+
id: 'title-safe',
|
|
445
|
+
duration: 5,
|
|
446
|
+
audioSegment: 0,
|
|
447
|
+
title: 'Original',
|
|
448
|
+
};
|
|
449
|
+
const sharedLayer: Layer = {
|
|
450
|
+
type: 'shape',
|
|
451
|
+
id: 'shared-layer',
|
|
452
|
+
content: { shape: 'rect', fill: '#112233' },
|
|
453
|
+
position: { x: 0, y: 0, width: '100%', height: '100%' },
|
|
454
|
+
};
|
|
455
|
+
const registry: RuntimeTemplateRegistry = {
|
|
456
|
+
title: (input) => {
|
|
457
|
+
(input as { title: string }).title = 'Mutated';
|
|
458
|
+
return [sharedLayer];
|
|
459
|
+
},
|
|
460
|
+
};
|
|
461
|
+
const first = materializeTemplateForTest(
|
|
462
|
+
source,
|
|
463
|
+
createTemplateContext(DEFAULT_THEME, 0, 1),
|
|
464
|
+
registry,
|
|
465
|
+
);
|
|
466
|
+
const second = materializeTemplateForTest(
|
|
467
|
+
source,
|
|
468
|
+
createTemplateContext(DEFAULT_THEME, 0, 1),
|
|
469
|
+
registry,
|
|
470
|
+
);
|
|
471
|
+
expect(source.title).toBe('Original');
|
|
472
|
+
expect(first.layers?.[0]).not.toBe(sharedLayer);
|
|
473
|
+
expect(first.layers?.[0]).not.toBe(second.layers?.[0]);
|
|
474
|
+
(first.layers?.[0] as Layer & { content: { fill: string } }).content.fill = '#ffffff';
|
|
475
|
+
expect((sharedLayer as Layer & { content: { fill: string } }).content.fill).toBe('#112233');
|
|
476
|
+
expect((second.layers?.[0] as Layer & { content: { fill: string } }).content.fill).toBe(
|
|
477
|
+
'#112233',
|
|
478
|
+
);
|
|
479
|
+
});
|
|
480
|
+
|
|
373
481
|
it('supports landscape and portrait viewports', () => {
|
|
374
482
|
const blocks: TemplateBlock[] = [
|
|
375
483
|
{ template: 'title', id: 'title-1', duration: 5, audioSegment: 0, title: 'Test' },
|
|
@@ -533,7 +641,7 @@ describe('DEFAULT_THEME', () => {
|
|
|
533
641
|
});
|
|
534
642
|
});
|
|
535
643
|
|
|
536
|
-
describe('inline-param coercion →
|
|
644
|
+
describe('inline-param coercion → materializeBlockLayers', () => {
|
|
537
645
|
function firstBlock(md: string) {
|
|
538
646
|
return markdownToDoc(parseMarkdown(md), { generateCoverBlock: false }).blocks[0];
|
|
539
647
|
}
|
|
@@ -547,7 +655,7 @@ describe('inline-param coercion → getLayers', () => {
|
|
|
547
655
|
mapStyle: 'road',
|
|
548
656
|
});
|
|
549
657
|
|
|
550
|
-
const layers =
|
|
658
|
+
const layers = materializeLayers(block, {});
|
|
551
659
|
const mapLayer = layers.find((l) => l.type === 'map');
|
|
552
660
|
expect(mapLayer).toBeDefined();
|
|
553
661
|
// Coerced center/zoom flow into the map layer's content — not empty strings.
|
|
@@ -558,7 +666,7 @@ describe('inline-param coercion → getLayers', () => {
|
|
|
558
666
|
|
|
559
667
|
it('renders a twoColumn from pure-inline labeled-pair annotation', () => {
|
|
560
668
|
const block = firstBlock('# Coffee {[twoColumn left="Espresso|Bold" right="Filter|Smooth"]}\n');
|
|
561
|
-
const layers =
|
|
669
|
+
const layers = materializeLayers(block, {});
|
|
562
670
|
// Guard requires both labels — a non-empty layer set proves coercion worked.
|
|
563
671
|
expect(layers.length).toBeGreaterThan(0);
|
|
564
672
|
const texts = layers
|
|
@@ -572,6 +680,24 @@ describe('inline-param coercion → getLayers', () => {
|
|
|
572
680
|
});
|
|
573
681
|
});
|
|
574
682
|
|
|
683
|
+
describe('shared template materialization', () => {
|
|
684
|
+
it('keeps on-demand and timed expansion layer output in lockstep', () => {
|
|
685
|
+
const block: TemplateBlock = {
|
|
686
|
+
template: 'title',
|
|
687
|
+
id: 'shared-title',
|
|
688
|
+
duration: 7,
|
|
689
|
+
audioSegment: 0,
|
|
690
|
+
title: 'One materializer',
|
|
691
|
+
subtitle: 'Two orchestration modes',
|
|
692
|
+
};
|
|
693
|
+
|
|
694
|
+
const onDemand = materializeLayers(block, { theme: DEFAULT_THEME });
|
|
695
|
+
const [expanded] = expandDocBlocks([block], { theme: DEFAULT_THEME });
|
|
696
|
+
|
|
697
|
+
expect(expanded.layers).toEqual(onDemand);
|
|
698
|
+
});
|
|
699
|
+
});
|
|
700
|
+
|
|
575
701
|
describe('VIEWPORT_PRESETS', () => {
|
|
576
702
|
it('has landscape and portrait presets', () => {
|
|
577
703
|
expect(VIEWPORT_PRESETS).toHaveProperty('landscape');
|
|
@@ -6,7 +6,8 @@ import {
|
|
|
6
6
|
STARTER_THEME,
|
|
7
7
|
deriveColorPalette,
|
|
8
8
|
} from '../schemas/themeCompile.js';
|
|
9
|
-
import {
|
|
9
|
+
import { createThemeRegistry } from '../schemas/Theme.js';
|
|
10
|
+
import type { Theme } from '../schemas/Theme.js';
|
|
10
11
|
import { resolveTheme, DEFAULT_THEME, THEMES } from '../schemas/themeLibrary.js';
|
|
11
12
|
import { resolveFontFamily } from '../schemas/fontStacks.js';
|
|
12
13
|
|
|
@@ -132,46 +133,72 @@ describe('parseTheme / serializeTheme round-trip', () => {
|
|
|
132
133
|
});
|
|
133
134
|
});
|
|
134
135
|
|
|
135
|
-
describe('
|
|
136
|
-
it('
|
|
136
|
+
describe('caller-owned theme registries', () => {
|
|
137
|
+
it('an explicit registry can override a built-in without process-global leakage', () => {
|
|
137
138
|
const overlay = compileTheme({
|
|
138
139
|
id: 'standard',
|
|
139
140
|
name: 'Standard Override',
|
|
140
141
|
seedColors: { primary: '#ff0000' },
|
|
141
142
|
});
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
expect(resolved.colors.primary).toBe('#ff0000');
|
|
147
|
-
} finally {
|
|
148
|
-
unregisterTheme('standard');
|
|
149
|
-
}
|
|
150
|
-
// After cleanup, built-in is restored
|
|
143
|
+
const registry = createThemeRegistry([overlay]);
|
|
144
|
+
const resolved = resolveTheme('standard', registry);
|
|
145
|
+
expect(resolved.name).toBe('Standard Override');
|
|
146
|
+
expect(resolved.colors.primary).toBe('#ff0000');
|
|
151
147
|
expect(resolveTheme('standard').name).toBe('Standard Light');
|
|
152
148
|
});
|
|
153
149
|
|
|
154
|
-
it('
|
|
150
|
+
it('resolves a custom id only through the registry that owns it', () => {
|
|
155
151
|
const custom = compileTheme({
|
|
156
152
|
id: 'custom-test-1',
|
|
157
153
|
name: 'Custom Test One',
|
|
158
154
|
seedColors: { primary: '#00aabb' },
|
|
159
155
|
});
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
expect(resolved.name).toBe('Custom Test One');
|
|
165
|
-
expect(getRegisteredThemes().some((t) => t.id === 'custom-test-1')).toBe(true);
|
|
166
|
-
} finally {
|
|
167
|
-
unregisterTheme('custom-test-1');
|
|
168
|
-
}
|
|
156
|
+
const registry = createThemeRegistry([custom]);
|
|
157
|
+
expect(resolveTheme('custom-test-1', registry).name).toBe('Custom Test One');
|
|
158
|
+
expect(resolveTheme('custom-test-1')).toBe(DEFAULT_THEME);
|
|
159
|
+
expect(registry.list().map((theme) => theme.id)).toContain('custom-test-1');
|
|
169
160
|
});
|
|
170
161
|
|
|
171
162
|
it('resolveTheme falls back to default for unknown ids', () => {
|
|
172
163
|
expect(resolveTheme('nonexistent-theme')).toBe(DEFAULT_THEME);
|
|
173
164
|
expect(resolveTheme(undefined)).toBe(DEFAULT_THEME);
|
|
174
165
|
});
|
|
166
|
+
|
|
167
|
+
it('validates and stores an immutable snapshot', () => {
|
|
168
|
+
const registry = createThemeRegistry();
|
|
169
|
+
const source = compileTheme({
|
|
170
|
+
id: 'isolated-theme',
|
|
171
|
+
name: 'Isolated',
|
|
172
|
+
seedColors: { primary: '#336699' },
|
|
173
|
+
});
|
|
174
|
+
registry.register(source);
|
|
175
|
+
source.name = 'Changed later';
|
|
176
|
+
source.colors.primary = '#ffffff';
|
|
177
|
+
|
|
178
|
+
const stored = registry.get('isolated-theme')!;
|
|
179
|
+
expect(stored.name).toBe('Isolated');
|
|
180
|
+
expect(stored.colors.primary).toBe('#336699');
|
|
181
|
+
expect(Object.isFrozen(stored)).toBe(true);
|
|
182
|
+
expect(Object.isFrozen(stored.colors)).toBe(true);
|
|
183
|
+
expect(() => registry.register({ id: 'invalid' } as Theme)).toThrow(/invalid theme/i);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('supports caller-owned registries with isolated state', () => {
|
|
187
|
+
const source = compileTheme({
|
|
188
|
+
id: 'tenant-theme',
|
|
189
|
+
name: 'Tenant',
|
|
190
|
+
seedColors: { primary: '#123456' },
|
|
191
|
+
});
|
|
192
|
+
const tenantA = createThemeRegistry([source]);
|
|
193
|
+
const tenantB = createThemeRegistry();
|
|
194
|
+
expect(tenantA.get('tenant-theme')?.name).toBe('Tenant');
|
|
195
|
+
expect(tenantB.get('tenant-theme')).toBeUndefined();
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('freezes built-ins so lookup ergonomics do not expose mutable global state', () => {
|
|
199
|
+
expect(Object.isFrozen(THEMES)).toBe(true);
|
|
200
|
+
expect(Object.isFrozen(resolveTheme('cinematic'))).toBe(true);
|
|
201
|
+
});
|
|
175
202
|
});
|
|
176
203
|
|
|
177
204
|
describe('FontFamily resolution', () => {
|