@bendyline/squisq 1.5.2 → 2.0.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/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-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-KKNUBQ6Y.js → chunk-6PJRB2CF.js} +156 -91
- package/dist/chunk-6PJRB2CF.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-FVJUUTEM.js +74 -0
- package/dist/chunk-FVJUUTEM.js.map +1 -0
- package/dist/{chunk-5TYS5V3G.js → chunk-GF3BVNLU.js} +4127 -3538
- package/dist/chunk-GF3BVNLU.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-LYNJEOAC.js} +10 -73
- package/dist/chunk-LYNJEOAC.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-T5UK6YOB.js → chunk-QTSJOY54.js} +60 -17
- package/dist/chunk-QTSJOY54.js.map +1 -0
- package/dist/chunk-QVANWDPH.js +2358 -0
- package/dist/chunk-QVANWDPH.js.map +1 -0
- 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-T3TI7GSZ.js +884 -0
- package/dist/chunk-T3TI7GSZ.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/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__/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 +41 -0
- 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 +135 -1
- 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/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/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/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
|
@@ -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', () => {
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { parseMarkdown } from '../markdown/parse';
|
|
3
|
+
import { markdownToDoc, flattenRenderableBlocks } from '../doc/markdownToDoc';
|
|
4
|
+
import { applyTransform } from '../transform/applyTransform';
|
|
5
|
+
import { allocateTiming } from '../transform/timingAllocator';
|
|
6
|
+
import { isTemplateBlock, type TemplateBlock } from '../schemas/BlockTemplates';
|
|
7
|
+
import type { Block, Doc } from '../schemas/Doc';
|
|
8
|
+
|
|
9
|
+
const MD = `# The Numbers
|
|
10
|
+
|
|
11
|
+
Revenue grew to 4,200 units in 2019 which was a big jump for the small team.
|
|
12
|
+
"We never expected this kind of growth," said the founder about the year.
|
|
13
|
+
|
|
14
|
+
# The Turning Point
|
|
15
|
+
|
|
16
|
+
By 2021 the company had 87 employees across three offices and a plan.
|
|
17
|
+
The expansion cost 1,500,000 dollars and took fourteen months to finish.
|
|
18
|
+
|
|
19
|
+
# The Result
|
|
20
|
+
|
|
21
|
+
Today the product ships to 40 countries and the team keeps growing steadily.
|
|
22
|
+
`;
|
|
23
|
+
|
|
24
|
+
function makeDoc(): Doc {
|
|
25
|
+
return markdownToDoc(parseMarkdown(MD));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Blocks a transform produced (positional transform-* ids). */
|
|
29
|
+
function transformedBlocks(doc: Doc): Block[] {
|
|
30
|
+
return doc.blocks.filter((b) => b.id.startsWith('transform-'));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe('transform provenance', () => {
|
|
34
|
+
it('every extraction-derived slide carries a sourceBlockId from the input doc', () => {
|
|
35
|
+
const source = makeDoc();
|
|
36
|
+
const sourceIds = new Set(flattenRenderableBlocks(source.blocks).map((b) => b.id));
|
|
37
|
+
const { doc } = applyTransform(source, 'data-driven');
|
|
38
|
+
|
|
39
|
+
const produced = transformedBlocks(doc).filter(
|
|
40
|
+
(b) => b.id !== 'transform-intro' && b.id !== 'transform-outro',
|
|
41
|
+
);
|
|
42
|
+
expect(produced.length).toBeGreaterThan(0);
|
|
43
|
+
for (const block of produced) {
|
|
44
|
+
expect(block.sourceBlockId, `block ${block.id} missing provenance`).toBeTruthy();
|
|
45
|
+
expect(sourceIds.has(block.sourceBlockId!)).toBe(true);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('sourceStartTime is SECONDS on the doc timeline, not a char offset (regression)', () => {
|
|
50
|
+
const source = makeDoc();
|
|
51
|
+
const sourceEnd = Math.max(
|
|
52
|
+
...flattenRenderableBlocks(source.blocks).map((b) => b.startTime + b.duration),
|
|
53
|
+
);
|
|
54
|
+
const { doc } = applyTransform(source, 'data-driven');
|
|
55
|
+
|
|
56
|
+
for (const block of doc.blocks) {
|
|
57
|
+
const tb = block as unknown as TemplateBlock;
|
|
58
|
+
if (typeof tb.sourceStartTime !== 'number') continue;
|
|
59
|
+
// The old bug put char offsets (hundreds) here; real seconds must
|
|
60
|
+
// stay inside the source timeline.
|
|
61
|
+
expect(tb.sourceStartTime).toBeLessThanOrEqual(sourceEnd + 1);
|
|
62
|
+
expect(tb.sourceStartTime).toBeGreaterThanOrEqual(0);
|
|
63
|
+
// The raw char offset is preserved separately as provenance.
|
|
64
|
+
expect(typeof tb.sourceCharOffset).toBe('number');
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('narration-timed source blocks anchor their slides inside the source range', () => {
|
|
69
|
+
const source = makeDoc();
|
|
70
|
+
// Simulate applyNarrationTiming: give the source blocks recorded times.
|
|
71
|
+
const flat = flattenRenderableBlocks(source.blocks);
|
|
72
|
+
const bounds = [0, 20, 45, 70];
|
|
73
|
+
flat.forEach((block, i) => {
|
|
74
|
+
block.startTime = bounds[i] ?? 70;
|
|
75
|
+
block.duration = (bounds[i + 1] ?? 70) - (bounds[i] ?? 70);
|
|
76
|
+
});
|
|
77
|
+
source.duration = 70;
|
|
78
|
+
|
|
79
|
+
const { doc } = applyTransform(source, 'data-driven');
|
|
80
|
+
const bySource = new Map(flat.map((b) => [b.id, b]));
|
|
81
|
+
let anchoredChecked = 0;
|
|
82
|
+
for (const block of transformedBlocks(doc)) {
|
|
83
|
+
// Only ANCHORED slides (real narration seconds) must sit inside
|
|
84
|
+
// their source range; floats (section headers, images) are placed
|
|
85
|
+
// approximately by design.
|
|
86
|
+
const tb = block as unknown as TemplateBlock;
|
|
87
|
+
if (!block.sourceBlockId || typeof tb.sourceStartTime !== 'number') continue;
|
|
88
|
+
const origin = bySource.get(block.sourceBlockId);
|
|
89
|
+
if (!origin || origin.duration <= 0) continue;
|
|
90
|
+
expect(block.startTime).toBeGreaterThanOrEqual(origin.startTime - 1e-6);
|
|
91
|
+
expect(block.startTime).toBeLessThan(origin.startTime + origin.duration + 1e-6);
|
|
92
|
+
anchoredChecked++;
|
|
93
|
+
}
|
|
94
|
+
expect(anchoredChecked).toBeGreaterThan(0);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('transformed timelines stay contiguous (no uncovered narration seconds)', () => {
|
|
98
|
+
const source = makeDoc();
|
|
99
|
+
const { doc } = applyTransform(source, 'data-driven');
|
|
100
|
+
const sorted = [...doc.blocks].sort((a, b) => a.startTime - b.startTime);
|
|
101
|
+
for (let i = 0; i < sorted.length - 1; i++) {
|
|
102
|
+
const end = sorted[i].startTime + sorted[i].duration;
|
|
103
|
+
expect(end).toBeCloseTo(sorted[i + 1].startTime, 5);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
describe('allocateTiming fallback', () => {
|
|
109
|
+
it('keeps the historical rescale behavior when no anchors exist', () => {
|
|
110
|
+
const floats: TemplateBlock[] = [
|
|
111
|
+
{ template: 'sectionHeader', id: 'a', duration: 5, audioSegment: 0, title: 'A' },
|
|
112
|
+
{ template: 'sectionHeader', id: 'b', duration: 5, audioSegment: 0, title: 'B' },
|
|
113
|
+
];
|
|
114
|
+
const result = allocateTiming(floats, 20);
|
|
115
|
+
expect(result[0].startTime).toBe(0);
|
|
116
|
+
expect(result[0].duration).toBe(10);
|
|
117
|
+
expect(result[1].startTime).toBe(10);
|
|
118
|
+
expect(result[1].duration).toBe(10);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('anchored blocks keep their positions; floats fill the gaps', () => {
|
|
122
|
+
const blocks: TemplateBlock[] = [
|
|
123
|
+
// A floating intro before the first anchor.
|
|
124
|
+
{ template: 'title', id: 'intro', duration: 4, audioSegment: 0, title: 'T' },
|
|
125
|
+
{
|
|
126
|
+
template: 'statHighlight',
|
|
127
|
+
id: 's1',
|
|
128
|
+
duration: 6,
|
|
129
|
+
audioSegment: 0,
|
|
130
|
+
stat: '42',
|
|
131
|
+
description: 'd',
|
|
132
|
+
sourceStartTime: 10,
|
|
133
|
+
sourceDuration: 6,
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
template: 'statHighlight',
|
|
137
|
+
id: 's2',
|
|
138
|
+
duration: 6,
|
|
139
|
+
audioSegment: 0,
|
|
140
|
+
stat: '87',
|
|
141
|
+
description: 'd',
|
|
142
|
+
sourceStartTime: 30,
|
|
143
|
+
sourceDuration: 6,
|
|
144
|
+
},
|
|
145
|
+
];
|
|
146
|
+
const result = allocateTiming(blocks, 40);
|
|
147
|
+
// Intro fills [0, 10) up to the first anchor.
|
|
148
|
+
expect(result[0].startTime).toBe(0);
|
|
149
|
+
expect(result[0].duration).toBeCloseTo(10, 5);
|
|
150
|
+
// Anchors sit at their narration positions; s1 holds until s2 starts.
|
|
151
|
+
expect(result[1].startTime).toBe(10);
|
|
152
|
+
expect(result[1].duration).toBeCloseTo(20, 5);
|
|
153
|
+
expect(result[2].startTime).toBe(30);
|
|
154
|
+
// The final block covers through the total duration.
|
|
155
|
+
expect(result[2].startTime + result[2].duration).toBeCloseTo(40, 5);
|
|
156
|
+
for (const b of result) {
|
|
157
|
+
expect(isTemplateBlock(b as unknown as TemplateBlock) || true).toBe(true);
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
});
|
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import { describe, it, expect
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
2
|
import {
|
|
3
3
|
applyTransform,
|
|
4
4
|
resolveTransformStyle,
|
|
5
|
-
registerTransformStyle,
|
|
6
|
-
unregisterTransformStyle,
|
|
7
5
|
getTransformStyleIds,
|
|
6
|
+
createTransformStyleRegistry,
|
|
8
7
|
} from '../transform/index.js';
|
|
9
8
|
import type { TransformStyleConfig } from '../transform/index.js';
|
|
10
9
|
import type { Doc, Block } from '../schemas/Doc.js';
|
|
@@ -49,8 +48,6 @@ function sampleDoc(): Doc {
|
|
|
49
48
|
}
|
|
50
49
|
|
|
51
50
|
describe('transform style contract v2', () => {
|
|
52
|
-
afterEach(() => unregisterTransformStyle('custom-test'));
|
|
53
|
-
|
|
54
51
|
it('suggestedThemeId applies only when the doc declares no theme', () => {
|
|
55
52
|
const { doc } = applyTransform(sampleDoc(), 'magazine');
|
|
56
53
|
expect(doc.themeId).toBe('magazine');
|
|
@@ -88,23 +85,53 @@ describe('transform style contract v2', () => {
|
|
|
88
85
|
expect(last.template).toBe('sectionHeader');
|
|
89
86
|
});
|
|
90
87
|
|
|
91
|
-
it('
|
|
88
|
+
it('resolves custom styles only through their caller-owned registry', () => {
|
|
92
89
|
const custom: TransformStyleConfig = {
|
|
93
90
|
...resolveTransformStyle('minimal'),
|
|
94
91
|
id: 'custom-test',
|
|
95
92
|
name: 'Custom Test',
|
|
96
93
|
description: 'test style',
|
|
97
94
|
};
|
|
98
|
-
|
|
99
|
-
expect(getTransformStyleIds()).toContain('custom-test');
|
|
100
|
-
expect(resolveTransformStyle('custom-test').name).toBe('Custom Test');
|
|
101
|
-
unregisterTransformStyle('custom-test');
|
|
102
|
-
// Unknown id falls back to the default (documentary)
|
|
95
|
+
const registry = createTransformStyleRegistry([custom]);
|
|
96
|
+
expect(getTransformStyleIds(registry)).toContain('custom-test');
|
|
97
|
+
expect(resolveTransformStyle('custom-test', registry).name).toBe('Custom Test');
|
|
103
98
|
expect(resolveTransformStyle('custom-test').id).toBe('documentary');
|
|
99
|
+
expect(
|
|
100
|
+
applyTransform(sampleDoc(), 'custom-test', { registry }).doc.blocks.length,
|
|
101
|
+
).toBeGreaterThan(0);
|
|
104
102
|
});
|
|
105
103
|
|
|
106
|
-
it(
|
|
104
|
+
it('normalizes the stored legacy dataDriven id without advertising it', () => {
|
|
105
|
+
expect(resolveTransformStyle('data-driven').id).toBe('data-driven');
|
|
107
106
|
expect(resolveTransformStyle('dataDriven').id).toBe('data-driven');
|
|
107
|
+
expect(getTransformStyleIds()).not.toContain('dataDriven');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('validates and stores immutable style snapshots in isolated registries', () => {
|
|
111
|
+
const base = resolveTransformStyle('minimal');
|
|
112
|
+
const source: TransformStyleConfig = {
|
|
113
|
+
...base,
|
|
114
|
+
id: 'tenant-style',
|
|
115
|
+
name: 'Tenant Style',
|
|
116
|
+
preferredTypes: [...base.preferredTypes],
|
|
117
|
+
colorSchemes: [...base.colorSchemes],
|
|
118
|
+
blocksPerSection: { ...base.blocksPerSection },
|
|
119
|
+
};
|
|
120
|
+
const tenantA = createTransformStyleRegistry([source]);
|
|
121
|
+
const tenantB = createTransformStyleRegistry();
|
|
122
|
+
source.name = 'Changed later';
|
|
123
|
+
source.colorSchemes[0] = 'red';
|
|
124
|
+
|
|
125
|
+
const stored = tenantA.get('tenant-style')!;
|
|
126
|
+
expect(stored.name).toBe('Tenant Style');
|
|
127
|
+
expect(stored.colorSchemes[0]).toBe(base.colorSchemes[0]);
|
|
128
|
+
expect(Object.isFrozen(stored)).toBe(true);
|
|
129
|
+
expect(Object.isFrozen(stored.colorSchemes)).toBe(true);
|
|
130
|
+
expect(tenantB.get('tenant-style')).toBeUndefined();
|
|
131
|
+
|
|
132
|
+
expect(() => tenantA.register({ ...source, id: 'bad-style', transformRatio: 2 })).toThrow(
|
|
133
|
+
/transformRatio/,
|
|
134
|
+
);
|
|
108
135
|
});
|
|
109
136
|
|
|
110
137
|
it('budget.slidesPerMinute caps promotions on long docs', () => {
|
|
@@ -127,9 +154,8 @@ describe('transform style contract v2', () => {
|
|
|
127
154
|
id: 'custom-test',
|
|
128
155
|
budget: { slidesPerMinute: 2 },
|
|
129
156
|
};
|
|
130
|
-
registerTransformStyle(budgeted);
|
|
131
157
|
const loose = applyTransform(manyBlocks, 'data-driven');
|
|
132
|
-
const tight = applyTransform(manyBlocks,
|
|
158
|
+
const tight = applyTransform(manyBlocks, budgeted);
|
|
133
159
|
expect(tight.stats.transformedBlocks).toBeLessThan(loose.stats.transformedBlocks);
|
|
134
160
|
// 12 blocks x 5s = 60s → 2/minute → at most 2 promotions
|
|
135
161
|
expect(tight.stats.transformedBlocks).toBeLessThanOrEqual(2);
|