@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,5 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
-
import {
|
|
2
|
+
import type { MaterializeBlockLayersOptions } from '../doc/materializeBlockLayers.js';
|
|
3
|
+
import { materializeLayers } from './materializeTestUtils.js';
|
|
3
4
|
import { DEFAULT_THEME } from '../schemas/themeLibrary.js';
|
|
4
5
|
import { TemplateBlock, PersistentLayerConfig } from '../schemas/BlockTemplates.js';
|
|
5
6
|
import { VIEWPORT_PRESETS } from '../schemas/Viewport.js';
|
|
@@ -30,7 +31,7 @@ function makeRawBlock(layers: Layer[]): Block {
|
|
|
30
31
|
};
|
|
31
32
|
}
|
|
32
33
|
|
|
33
|
-
const defaultContext:
|
|
34
|
+
const defaultContext: MaterializeBlockLayersOptions = {
|
|
34
35
|
theme: DEFAULT_THEME,
|
|
35
36
|
viewport: VIEWPORT_PRESETS.landscape,
|
|
36
37
|
blockIndex: 0,
|
|
@@ -41,10 +42,10 @@ const defaultContext: RenderContext = {
|
|
|
41
42
|
// Tests
|
|
42
43
|
// ---------------------------------------------------------------------------
|
|
43
44
|
|
|
44
|
-
describe('
|
|
45
|
+
describe('materializeBlockLayers', () => {
|
|
45
46
|
it('generates layers for a known template block', () => {
|
|
46
47
|
const block = makeTemplateBlock({ template: 'title', title: 'Test Title' });
|
|
47
|
-
const layers =
|
|
48
|
+
const layers = materializeLayers(block, defaultContext);
|
|
48
49
|
|
|
49
50
|
expect(Array.isArray(layers)).toBe(true);
|
|
50
51
|
expect(layers.length).toBeGreaterThan(0);
|
|
@@ -61,7 +62,7 @@ describe('getLayers', () => {
|
|
|
61
62
|
stat: '42%',
|
|
62
63
|
description: 'of users',
|
|
63
64
|
});
|
|
64
|
-
const layers =
|
|
65
|
+
const layers = materializeLayers(block, defaultContext);
|
|
65
66
|
|
|
66
67
|
expect(layers.length).toBeGreaterThan(0);
|
|
67
68
|
const texts = layers.filter((l) => l.type === 'text');
|
|
@@ -78,13 +79,17 @@ describe('getLayers', () => {
|
|
|
78
79
|
} as ShapeLayer,
|
|
79
80
|
];
|
|
80
81
|
const block = makeRawBlock(existingLayers);
|
|
81
|
-
const layers =
|
|
82
|
+
const layers = materializeLayers(block, defaultContext);
|
|
82
83
|
|
|
83
84
|
expect(layers).toEqual(existingLayers);
|
|
85
|
+
expect(layers).not.toBe(existingLayers);
|
|
86
|
+
expect(layers[0]).not.toBe(existingLayers[0]);
|
|
87
|
+
(layers[0] as ShapeLayer).content.fill = '#000000';
|
|
88
|
+
expect((existingLayers[0] as ShapeLayer).content.fill).toBe('#ff0000');
|
|
84
89
|
});
|
|
85
90
|
|
|
86
91
|
it('resolves legacy template aliases through TEMPLATE_ALIASES', () => {
|
|
87
|
-
// Regression:
|
|
92
|
+
// Regression: the old resolver used `block.template in templateRegistry`
|
|
88
93
|
// without alias resolution, so docs authored with the legacy long ids
|
|
89
94
|
// (`titleBlock`, `quoteBlock`, `mapBlock`, `listBlock`) rendered as
|
|
90
95
|
// empty SVG cards in the linear/document view — `hasTemplate()` accepts
|
|
@@ -103,7 +108,7 @@ describe('getLayers', () => {
|
|
|
103
108
|
items: ['a', 'b', 'c'],
|
|
104
109
|
quote: 'q',
|
|
105
110
|
} as Partial<TemplateBlock>);
|
|
106
|
-
const layers =
|
|
111
|
+
const layers = materializeLayers(block, defaultContext);
|
|
107
112
|
expect(layers.length).toBeGreaterThan(0);
|
|
108
113
|
}
|
|
109
114
|
});
|
|
@@ -115,7 +120,7 @@ describe('getLayers', () => {
|
|
|
115
120
|
duration: 5,
|
|
116
121
|
audioSegment: 0,
|
|
117
122
|
} as unknown as TemplateBlock;
|
|
118
|
-
const layers =
|
|
123
|
+
const layers = materializeLayers(block, defaultContext);
|
|
119
124
|
|
|
120
125
|
expect(layers.length).toBeGreaterThan(0);
|
|
121
126
|
const noticeText = layers
|
|
@@ -126,14 +131,14 @@ describe('getLayers', () => {
|
|
|
126
131
|
|
|
127
132
|
it('returns empty array for block with no template and no layers', () => {
|
|
128
133
|
const block: Block = { id: 'empty-1', duration: 5, startTime: 0, audioSegment: 0 };
|
|
129
|
-
const layers =
|
|
134
|
+
const layers = materializeLayers(block, defaultContext);
|
|
130
135
|
|
|
131
136
|
expect(layers).toEqual([]);
|
|
132
137
|
});
|
|
133
138
|
|
|
134
139
|
it('uses default context values when context is omitted', () => {
|
|
135
140
|
const block = makeTemplateBlock({ template: 'title', title: 'Defaults' });
|
|
136
|
-
const layers =
|
|
141
|
+
const layers = materializeLayers(block);
|
|
137
142
|
|
|
138
143
|
expect(Array.isArray(layers)).toBe(true);
|
|
139
144
|
expect(layers.length).toBeGreaterThan(0);
|
|
@@ -144,12 +149,12 @@ describe('getLayers', () => {
|
|
|
144
149
|
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
145
150
|
|
|
146
151
|
// comparisonBar requires numeric values; passing invalid data may throw
|
|
147
|
-
// but
|
|
152
|
+
// but materialization should degrade without throwing
|
|
148
153
|
const block = makeTemplateBlock({
|
|
149
154
|
template: 'comparisonBar',
|
|
150
155
|
// Missing required fields
|
|
151
156
|
});
|
|
152
|
-
const layers =
|
|
157
|
+
const layers = materializeLayers(block, defaultContext);
|
|
153
158
|
|
|
154
159
|
expect(Array.isArray(layers)).toBe(true);
|
|
155
160
|
consoleSpy.mockRestore();
|
|
@@ -177,7 +182,10 @@ describe('getLayers', () => {
|
|
|
177
182
|
|
|
178
183
|
it('injects persistent bottom and top layers around block layers', () => {
|
|
179
184
|
const block = makeTemplateBlock({ template: 'title', title: 'With Persistent' });
|
|
180
|
-
const layers =
|
|
185
|
+
const layers = materializeLayers(block, {
|
|
186
|
+
...defaultContext,
|
|
187
|
+
persistentLayers: persistentConfig,
|
|
188
|
+
});
|
|
181
189
|
|
|
182
190
|
// First layer should be the bottom persistent layer
|
|
183
191
|
expect(layers[0]).toEqual(bottomLayer);
|
|
@@ -193,7 +201,10 @@ describe('getLayers', () => {
|
|
|
193
201
|
title: 'No Bottom',
|
|
194
202
|
useBottomLayer: false,
|
|
195
203
|
});
|
|
196
|
-
const layers =
|
|
204
|
+
const layers = materializeLayers(block, {
|
|
205
|
+
...defaultContext,
|
|
206
|
+
persistentLayers: persistentConfig,
|
|
207
|
+
});
|
|
197
208
|
|
|
198
209
|
// Should NOT start with the bottom persistent layer
|
|
199
210
|
expect(layers[0]).not.toEqual(bottomLayer);
|
|
@@ -207,7 +218,10 @@ describe('getLayers', () => {
|
|
|
207
218
|
title: 'No Top',
|
|
208
219
|
useTopLayer: false,
|
|
209
220
|
});
|
|
210
|
-
const layers =
|
|
221
|
+
const layers = materializeLayers(block, {
|
|
222
|
+
...defaultContext,
|
|
223
|
+
persistentLayers: persistentConfig,
|
|
224
|
+
});
|
|
211
225
|
|
|
212
226
|
// Should start with the bottom persistent layer
|
|
213
227
|
expect(layers[0]).toEqual(bottomLayer);
|
|
@@ -217,8 +231,8 @@ describe('getLayers', () => {
|
|
|
217
231
|
|
|
218
232
|
it('skips injection when no persistent layers are configured', () => {
|
|
219
233
|
const block = makeTemplateBlock({ template: 'title', title: 'No Persistent' });
|
|
220
|
-
const withoutPersistent =
|
|
221
|
-
const withEmptyPersistent =
|
|
234
|
+
const withoutPersistent = materializeLayers(block, defaultContext);
|
|
235
|
+
const withEmptyPersistent = materializeLayers(block, {
|
|
222
236
|
...defaultContext,
|
|
223
237
|
persistentLayers: { bottomLayers: [], topLayers: [] },
|
|
224
238
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe, it, expect, afterEach } from 'vitest';
|
|
2
2
|
import { parseMarkdown } from '../markdown/parse.js';
|
|
3
3
|
import { markdownToDoc } from '../doc/markdownToDoc.js';
|
|
4
|
-
import {
|
|
4
|
+
import { materializeLayers } from './materializeTestUtils.js';
|
|
5
5
|
import { templateRegistry } from '../doc/templates/index.js';
|
|
6
6
|
import type { Block, Layer } from '../schemas/Doc.js';
|
|
7
7
|
|
|
@@ -15,10 +15,10 @@ function firstBlock(md: string): Block {
|
|
|
15
15
|
return markdownToDoc(parseMarkdown(md)).blocks[0];
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
describe('
|
|
18
|
+
describe('materializeBlockLayers — graceful degradation', () => {
|
|
19
19
|
it('renders an unknown template as a plain card with title, body, and notice', () => {
|
|
20
20
|
const block = firstBlock('## My Section {[photGrid]}\n\nSome body prose here.');
|
|
21
|
-
const layers =
|
|
21
|
+
const layers = materializeLayers(block);
|
|
22
22
|
expect(layers.length).toBeGreaterThan(0);
|
|
23
23
|
const text = textOf(layers);
|
|
24
24
|
expect(text).toContain('My Section');
|
|
@@ -39,7 +39,7 @@ describe('getLayers — graceful degradation', () => {
|
|
|
39
39
|
template: '__boom',
|
|
40
40
|
title: 'Exploding block',
|
|
41
41
|
};
|
|
42
|
-
const layers =
|
|
42
|
+
const layers = materializeLayers(block);
|
|
43
43
|
expect(layers.length).toBeGreaterThan(0);
|
|
44
44
|
expect(textOf(layers)).toContain('Template "__boom" failed');
|
|
45
45
|
expect(textOf(layers)).toContain('Exploding block');
|
|
@@ -50,11 +50,11 @@ describe('getLayers — graceful degradation', () => {
|
|
|
50
50
|
|
|
51
51
|
it('blocks with no template at all still return empty layers', () => {
|
|
52
52
|
const block: Block = { id: 'raw', startTime: 0, duration: 5, audioSegment: 0 };
|
|
53
|
-
expect(
|
|
53
|
+
expect(materializeLayers(block)).toEqual([]);
|
|
54
54
|
});
|
|
55
55
|
});
|
|
56
56
|
|
|
57
|
-
describe('
|
|
57
|
+
describe('materializeBlockLayers — structured data feeds templates', () => {
|
|
58
58
|
afterEach(() => {
|
|
59
59
|
delete (templateRegistry as Record<string, unknown>)['__capture'];
|
|
60
60
|
});
|
|
@@ -76,7 +76,7 @@ describe('getLayers — structured data feeds templates', () => {
|
|
|
76
76
|
templateData: { rows: [['a', 'b']], zoom: 12 },
|
|
77
77
|
templateOverrides: { zoom: '14' },
|
|
78
78
|
};
|
|
79
|
-
|
|
79
|
+
materializeLayers(block);
|
|
80
80
|
expect(received).not.toBeNull();
|
|
81
81
|
expect(received!['rows']).toEqual([['a', 'b']]);
|
|
82
82
|
// String overrides from {[…]} params win last.
|
|
@@ -92,7 +92,7 @@ describe('getLayers — structured data feeds templates', () => {
|
|
|
92
92
|
'| Alice | 30 |',
|
|
93
93
|
].join('\n');
|
|
94
94
|
const block = firstBlock(md);
|
|
95
|
-
const layers =
|
|
95
|
+
const layers = materializeLayers(block);
|
|
96
96
|
expect(layers.length).toBeGreaterThan(0);
|
|
97
97
|
const tableLayer = layers.find((l) => l.type === 'table');
|
|
98
98
|
expect(tableLayer).toBeDefined();
|
|
@@ -137,6 +137,8 @@ describe('imageEdit/versions', () => {
|
|
|
137
137
|
expect(result.version?.path).toMatch(/^\.versions\/state\.\d{8}T\d{6}Z\.json$/);
|
|
138
138
|
// snapshot is inside the sidecar, visible to the parent under the prefix
|
|
139
139
|
expect(await parent.exists(`image_files/${result.version!.path}`)).toBe(true);
|
|
140
|
+
const gitignore = await parent.readFile('image_files/.gitignore');
|
|
141
|
+
expect(gitignore && new TextDecoder().decode(gitignore)).toBe('.versions/\n');
|
|
140
142
|
});
|
|
141
143
|
|
|
142
144
|
it('returns no-state when sidecar is empty', async () => {
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
2
|
import {
|
|
3
|
+
escapePointerSegment,
|
|
4
|
+
appendPointer,
|
|
3
5
|
toPointer,
|
|
4
6
|
pointerSegments,
|
|
5
7
|
getByPointer,
|
|
@@ -7,6 +9,19 @@ import {
|
|
|
7
9
|
resolveRef,
|
|
8
10
|
} from '../jsonForm/index.js';
|
|
9
11
|
|
|
12
|
+
describe('pointer composition', () => {
|
|
13
|
+
it('escapes arbitrary member names and appends them safely', () => {
|
|
14
|
+
expect(escapePointerSegment('a~/b')).toBe('a~0~1b');
|
|
15
|
+
expect(appendPointer('/properties', 'a~/b')).toBe('/properties/a~0~1b');
|
|
16
|
+
expect(appendPointer('', '')).toBe('/');
|
|
17
|
+
expect(appendPointer('/', 'child')).toBe('//child');
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('validates an explicit base pointer before appending', () => {
|
|
21
|
+
expect(() => appendPointer('/bad~2base', 'child')).toThrow(/Invalid JSON Pointer/);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
10
25
|
describe('toPointer', () => {
|
|
11
26
|
it('keeps pointer-form paths intact', () => {
|
|
12
27
|
expect(toPointer('/a/b')).toBe('/a/b');
|
|
@@ -18,13 +33,18 @@ describe('toPointer', () => {
|
|
|
18
33
|
});
|
|
19
34
|
it('returns empty for root', () => {
|
|
20
35
|
expect(toPointer('')).toBe('');
|
|
21
|
-
expect(toPointer('/')).toBe('');
|
|
36
|
+
expect(toPointer('/')).toBe('/');
|
|
22
37
|
});
|
|
23
38
|
it('escapes ~ and / per RFC 6901', () => {
|
|
24
39
|
// dotted form does not allow / inside a segment; we test pointer-form decoding instead.
|
|
25
40
|
expect(pointerSegments('/a~1b')).toEqual(['a/b']);
|
|
26
41
|
expect(pointerSegments('/a~0b')).toEqual(['a~b']);
|
|
27
42
|
});
|
|
43
|
+
it('rejects malformed pointer syntax and escape sequences', () => {
|
|
44
|
+
expect(() => pointerSegments('not/a/pointer')).toThrow(/Invalid JSON Pointer/);
|
|
45
|
+
expect(() => pointerSegments('/bad~2escape')).toThrow(/Invalid JSON Pointer/);
|
|
46
|
+
expect(() => pointerSegments('/dangling~')).toThrow(/Invalid JSON Pointer/);
|
|
47
|
+
});
|
|
28
48
|
});
|
|
29
49
|
|
|
30
50
|
describe('getByPointer', () => {
|
|
@@ -43,6 +63,13 @@ describe('getByPointer', () => {
|
|
|
43
63
|
it('returns the data itself for empty pointer', () => {
|
|
44
64
|
expect(getByPointer(data, '')).toBe(data);
|
|
45
65
|
});
|
|
66
|
+
it('treats "/" as the empty-string member per RFC 6901', () => {
|
|
67
|
+
expect(getByPointer({ '': 'empty key' }, '/')).toBe('empty key');
|
|
68
|
+
});
|
|
69
|
+
it('requires canonical array indices', () => {
|
|
70
|
+
expect(getByPointer({ values: ['a'] }, '/values/00')).toBeUndefined();
|
|
71
|
+
expect(getByPointer({ values: ['a'] }, '/values/-')).toBeUndefined();
|
|
72
|
+
});
|
|
46
73
|
});
|
|
47
74
|
|
|
48
75
|
describe('setByPointer', () => {
|
|
@@ -75,6 +102,25 @@ describe('setByPointer', () => {
|
|
|
75
102
|
expect(getByPointer(after, '/a/b/1/c')).toBe(42);
|
|
76
103
|
expect(getByPointer(after, '/a/b/0/c')).toBe(1);
|
|
77
104
|
});
|
|
105
|
+
it('writes the empty-string member for "/"', () => {
|
|
106
|
+
expect(setByPointer({}, '/', 'value')).toEqual({ '': 'value' });
|
|
107
|
+
});
|
|
108
|
+
it('rejects non-canonical array indices instead of creating NaN properties', () => {
|
|
109
|
+
expect(() => setByPointer({ items: [] }, '/items/nope', 'x')).toThrow(/array index/i);
|
|
110
|
+
expect(() => setByPointer({ items: [] }, '/items/01', 'x')).toThrow(/array index/i);
|
|
111
|
+
});
|
|
112
|
+
it('writes prototype-shaped keys as safe own data properties', () => {
|
|
113
|
+
const before = {};
|
|
114
|
+
const after = setByPointer(before, '/__proto__/polluted', true) as Record<string, unknown>;
|
|
115
|
+
expect(Object.getPrototypeOf(after)).toBe(Object.prototype);
|
|
116
|
+
expect(Object.prototype).not.toHaveProperty('polluted');
|
|
117
|
+
expect(Object.prototype.hasOwnProperty.call(after, '__proto__')).toBe(true);
|
|
118
|
+
expect(getByPointer(after, '/__proto__/polluted')).toBe(true);
|
|
119
|
+
|
|
120
|
+
const constructorPath = setByPointer({}, '/constructor/prototype/polluted', true);
|
|
121
|
+
expect(getByPointer(constructorPath, '/constructor/prototype/polluted')).toBe(true);
|
|
122
|
+
expect(Object.prototype).not.toHaveProperty('polluted');
|
|
123
|
+
});
|
|
78
124
|
});
|
|
79
125
|
|
|
80
126
|
describe('resolveRef', () => {
|
|
@@ -62,8 +62,8 @@ describe('layout container behavior', () => {
|
|
|
62
62
|
expect(isContainerTemplate('layout')).toBe(true);
|
|
63
63
|
});
|
|
64
64
|
|
|
65
|
-
it('renders a layout block through the template registry
|
|
66
|
-
const {
|
|
65
|
+
it('renders a layout block through the template registry', async () => {
|
|
66
|
+
const { materializeBlockLayers } = await import('../doc/materializeBlockLayers.js');
|
|
67
67
|
const parent: Block = {
|
|
68
68
|
id: 'lay',
|
|
69
69
|
startTime: 0,
|
|
@@ -75,7 +75,7 @@ describe('layout container behavior', () => {
|
|
|
75
75
|
child('box-1', 'rectangle', { x: '760', y: '390', width: '400', height: '300' }),
|
|
76
76
|
],
|
|
77
77
|
};
|
|
78
|
-
const layers =
|
|
78
|
+
const layers = materializeBlockLayers(parent, { persistentLayers: false }).layers;
|
|
79
79
|
const types = new Set(layers.map((l: Layer) => l.type));
|
|
80
80
|
expect(types.has('text')).toBe(true);
|
|
81
81
|
expect(types.has('shape')).toBe(true);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import type { PathLayer } from '../schemas/Doc';
|
|
3
|
+
|
|
4
|
+
describe('major-version core API types', () => {
|
|
5
|
+
it('requires endpoint markers instead of the removed arrow field', () => {
|
|
6
|
+
const layer: PathLayer = {
|
|
7
|
+
id: 'path',
|
|
8
|
+
type: 'path',
|
|
9
|
+
position: { x: 0, y: 0, width: 10, height: 10 },
|
|
10
|
+
content: {
|
|
11
|
+
d: 'M 0 0 L 10 10',
|
|
12
|
+
// @ts-expect-error Old documents are read tolerantly, but new code uses endMarker.
|
|
13
|
+
arrow: 'end',
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
expect(layer.content).toHaveProperty('arrow', 'end');
|
|
17
|
+
});
|
|
18
|
+
});
|
|
@@ -831,6 +831,19 @@ describe('parseFrontmatter', () => {
|
|
|
831
831
|
expect(result).toEqual({ title: 'Hello World', name: 'Jane' });
|
|
832
832
|
});
|
|
833
833
|
|
|
834
|
+
it('keeps quoted booleans and numbers as strings and decodes escapes', () => {
|
|
835
|
+
const result = parseFrontmatter(String.raw`boolean: "true"
|
|
836
|
+
number: "1e3"
|
|
837
|
+
path: "C:\\temp\\\"quoted\""
|
|
838
|
+
single: 'it''s'`);
|
|
839
|
+
expect(result).toEqual({
|
|
840
|
+
boolean: 'true',
|
|
841
|
+
number: '1e3',
|
|
842
|
+
path: 'C:\\temp\\"quoted"',
|
|
843
|
+
single: "it's",
|
|
844
|
+
});
|
|
845
|
+
});
|
|
846
|
+
|
|
834
847
|
it('skips comment lines and blank lines', () => {
|
|
835
848
|
const result = parseFrontmatter('# comment\n\ntitle: Test');
|
|
836
849
|
expect(result).toEqual({ title: 'Test' });
|
|
@@ -903,6 +916,34 @@ describe('multi-line frontmatter round-trips', () => {
|
|
|
903
916
|
expect(md).toContain('document-render-as: portrait');
|
|
904
917
|
expect(md).not.toContain('|-');
|
|
905
918
|
});
|
|
919
|
+
|
|
920
|
+
it('losslessly round-trips ambiguous and escaped single-line strings', () => {
|
|
921
|
+
const doc = createDocument();
|
|
922
|
+
doc.frontmatter = {
|
|
923
|
+
booleanLike: 'true',
|
|
924
|
+
numericLike: '1e3',
|
|
925
|
+
padded: ' keep me ',
|
|
926
|
+
quoted: '"literal quotes"',
|
|
927
|
+
path: 'C:\\temp\\"quoted"',
|
|
928
|
+
};
|
|
929
|
+
const md = stringifyMarkdown(doc);
|
|
930
|
+
expect(md).toContain('booleanLike: "true"');
|
|
931
|
+
expect(md).toContain('numericLike: "1e3"');
|
|
932
|
+
expect(parseMarkdown(md).frontmatter).toEqual(doc.frontmatter);
|
|
933
|
+
});
|
|
934
|
+
|
|
935
|
+
it('uses the same escaping rules when updating source frontmatter', () => {
|
|
936
|
+
const out = setFrontmatterValues('# Body\n', {
|
|
937
|
+
booleanLike: 'false',
|
|
938
|
+
padded: ' value ',
|
|
939
|
+
path: 'C:\\temp\\"quoted"',
|
|
940
|
+
});
|
|
941
|
+
expect(parseMarkdown(out).frontmatter).toEqual({
|
|
942
|
+
booleanLike: 'false',
|
|
943
|
+
padded: ' value ',
|
|
944
|
+
path: 'C:\\temp\\"quoted"',
|
|
945
|
+
});
|
|
946
|
+
});
|
|
906
947
|
});
|
|
907
948
|
|
|
908
949
|
describe('frontmatter in parseMarkdown', () => {
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { matchFontFamily, guessFontFallback } from '../schemas/fontStacks.js';
|
|
3
|
+
import { accentToColorScheme } from '../schemas/themeCompile.js';
|
|
4
|
+
import { deriveScale, isHex } from '../schemas/colorUtils.js';
|
|
5
|
+
|
|
6
|
+
describe('matchFontFamily', () => {
|
|
7
|
+
it('matches curated stacks by label', () => {
|
|
8
|
+
expect(matchFontFamily('Playfair Display')).toEqual({ stackId: 'playfair' });
|
|
9
|
+
expect(matchFontFamily('JetBrains Mono')).toEqual({ stackId: 'jetbrains-mono' });
|
|
10
|
+
expect(matchFontFamily('IBM Plex Sans')).toEqual({ stackId: 'ibm-plex-sans' });
|
|
11
|
+
expect(matchFontFamily('Inter')).toEqual({ stackId: 'inter' });
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('matches case-insensitively and ignores surrounding quotes/whitespace', () => {
|
|
15
|
+
expect(matchFontFamily(' playfair display ')).toEqual({ stackId: 'playfair' });
|
|
16
|
+
expect(matchFontFamily('"Merriweather"')).toEqual({ stackId: 'merriweather' });
|
|
17
|
+
expect(matchFontFamily("'LORA'")).toEqual({ stackId: 'lora' });
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('matches explicit aliases and Google family names', () => {
|
|
21
|
+
expect(matchFontFamily('Playfair')).toEqual({ stackId: 'playfair' });
|
|
22
|
+
expect(matchFontFamily('Source Serif Pro')).toEqual({ stackId: 'source-serif' });
|
|
23
|
+
expect(matchFontFamily('Source Serif 4')).toEqual({ stackId: 'source-serif' });
|
|
24
|
+
expect(matchFontFamily('Crimson Pro')).toEqual({ stackId: 'crimson' });
|
|
25
|
+
expect(matchFontFamily('Cormorant')).toEqual({ stackId: 'cormorant' });
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('maps web-safe faces onto the system stacks that already anchor them', () => {
|
|
29
|
+
expect(matchFontFamily('Georgia')).toEqual({ stackId: 'system-serif' });
|
|
30
|
+
expect(matchFontFamily('Times New Roman')).toEqual({ stackId: 'system-serif' });
|
|
31
|
+
expect(matchFontFamily('Consolas')).toEqual({ stackId: 'system-mono' });
|
|
32
|
+
expect(matchFontFamily('Courier New')).toEqual({ stackId: 'system-mono' });
|
|
33
|
+
expect(matchFontFamily('Segoe UI')).toEqual({ stackId: 'system-sans' });
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('preserves unknown faces as custom fonts with a heuristic fallback', () => {
|
|
37
|
+
expect(matchFontFamily('Calibri')).toEqual({
|
|
38
|
+
custom: { name: 'Calibri', fallback: 'sans-serif' },
|
|
39
|
+
});
|
|
40
|
+
expect(matchFontFamily('Calibri Light')).toEqual({
|
|
41
|
+
custom: { name: 'Calibri Light', fallback: 'sans-serif' },
|
|
42
|
+
});
|
|
43
|
+
expect(matchFontFamily('Cambria')).toEqual({
|
|
44
|
+
custom: { name: 'Cambria', fallback: 'serif' },
|
|
45
|
+
});
|
|
46
|
+
expect(matchFontFamily('Aptos')).toEqual({
|
|
47
|
+
custom: { name: 'Aptos', fallback: 'sans-serif' },
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('keeps original casing but strips quotes on custom names', () => {
|
|
52
|
+
expect(matchFontFamily('"Neue Haas Grotesk"')).toEqual({
|
|
53
|
+
custom: { name: 'Neue Haas Grotesk', fallback: 'sans-serif' },
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('resolves empty/blank names to system-sans', () => {
|
|
58
|
+
expect(matchFontFamily('')).toEqual({ stackId: 'system-sans' });
|
|
59
|
+
expect(matchFontFamily(' ')).toEqual({ stackId: 'system-sans' });
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe('guessFontFallback', () => {
|
|
64
|
+
it('detects monospace faces', () => {
|
|
65
|
+
expect(guessFontFallback('Cascadia Code')).toBe('monospace');
|
|
66
|
+
expect(guessFontFallback('Roboto Mono')).toBe('monospace');
|
|
67
|
+
expect(guessFontFallback('Courier Prime')).toBe('monospace');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('detects serif faces by keyword and by known serif names', () => {
|
|
71
|
+
expect(guessFontFallback('Adobe Caslon Pro')).toBe('serif');
|
|
72
|
+
expect(guessFontFallback('Bodoni Moda')).toBe('serif');
|
|
73
|
+
expect(guessFontFallback('Palatino Linotype')).toBe('serif');
|
|
74
|
+
expect(guessFontFallback('Awesome Serif')).toBe('serif');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('does not treat sans-serif names as serif', () => {
|
|
78
|
+
expect(guessFontFallback('PT Sans Serif Whatever')).toBe('sans-serif');
|
|
79
|
+
expect(guessFontFallback('Open Sans')).toBe('sans-serif');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('defaults to sans-serif and never guesses system-ui', () => {
|
|
83
|
+
expect(guessFontFallback('Neue Haas Grotesk')).toBe('sans-serif');
|
|
84
|
+
expect(guessFontFallback('Futura')).toBe('sans-serif');
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe('accentToColorScheme', () => {
|
|
89
|
+
it('derives the same scheme the editor previously derived inline', () => {
|
|
90
|
+
// Parity with the original editor-react schemeFromAccent implementation.
|
|
91
|
+
const accent = '#63b3ed';
|
|
92
|
+
const scale = deriveScale(accent, 0.3);
|
|
93
|
+
expect(accentToColorScheme(accent)).toEqual({
|
|
94
|
+
bg: scale.darker2,
|
|
95
|
+
text: scale.lighter2,
|
|
96
|
+
accent: scale.base,
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('returns valid hex colors for every slot', () => {
|
|
101
|
+
const scheme = accentToColorScheme('#c05621');
|
|
102
|
+
expect(isHex(scheme.bg)).toBe(true);
|
|
103
|
+
expect(isHex(scheme.text)).toBe(true);
|
|
104
|
+
expect(isHex(scheme.accent)).toBe(true);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('falls back to a neutral scheme on invalid input', () => {
|
|
108
|
+
expect(accentToColorScheme('not-a-color')).toEqual({
|
|
109
|
+
bg: '#1a202c',
|
|
110
|
+
text: '#e2e8f0',
|
|
111
|
+
accent: '#63b3ed',
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
});
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { DEFAULT_THEME, expandDocBlocks } from '../doc/templates/index.js';
|
|
3
|
+
import { materializeBlockLayers } from '../doc/materializeBlockLayers.js';
|
|
4
|
+
import type { CustomTemplateDefinition } from '../schemas/CustomTemplates.js';
|
|
5
|
+
import type { Block, Layer, TextLayer } from '../schemas/Doc.js';
|
|
6
|
+
import type { TemplateBlock } from '../schemas/BlockTemplates.js';
|
|
7
|
+
|
|
8
|
+
const titleBlock: TemplateBlock = {
|
|
9
|
+
template: 'title',
|
|
10
|
+
id: 'title-1',
|
|
11
|
+
duration: 5,
|
|
12
|
+
audioSegment: 0,
|
|
13
|
+
title: 'A canonical materializer',
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
describe('materializeBlockLayers', () => {
|
|
17
|
+
it('returns owned layers and metadata for built-in and authored blocks', () => {
|
|
18
|
+
const templated = materializeBlockLayers(titleBlock, { persistentLayers: false });
|
|
19
|
+
expect(templated.source).toBe('template');
|
|
20
|
+
expect(templated.diagnostic).toBeUndefined();
|
|
21
|
+
expect(templated.layers.length).toBeGreaterThan(0);
|
|
22
|
+
|
|
23
|
+
const authoredLayer: Layer = {
|
|
24
|
+
id: 'authored',
|
|
25
|
+
type: 'shape',
|
|
26
|
+
content: { shape: 'rect', fill: '#123456' },
|
|
27
|
+
position: { x: 0, y: 0, width: 10, height: 10 },
|
|
28
|
+
};
|
|
29
|
+
const authored: Block = {
|
|
30
|
+
id: 'raw',
|
|
31
|
+
startTime: 0,
|
|
32
|
+
duration: 5,
|
|
33
|
+
audioSegment: 0,
|
|
34
|
+
layers: [authoredLayer],
|
|
35
|
+
};
|
|
36
|
+
const materialized = materializeBlockLayers(authored, { persistentLayers: false });
|
|
37
|
+
expect(materialized.source).toBe('authored');
|
|
38
|
+
expect(materialized.layers).toEqual([authoredLayer]);
|
|
39
|
+
expect(materialized.layers[0]).not.toBe(authoredLayer);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('resolves document-scoped custom templates in the on-demand API', () => {
|
|
43
|
+
const customTemplate: CustomTemplateDefinition = {
|
|
44
|
+
name: 'hero',
|
|
45
|
+
label: 'Hero',
|
|
46
|
+
viewport: { width: 1920, height: 1080 },
|
|
47
|
+
layers: [
|
|
48
|
+
{
|
|
49
|
+
id: 'hero-title',
|
|
50
|
+
type: 'text',
|
|
51
|
+
position: { x: '5%', y: '10%', width: '90%' },
|
|
52
|
+
content: { text: '{title}', style: { fontSize: 64, color: '#000000' } },
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
};
|
|
56
|
+
const block = {
|
|
57
|
+
...titleBlock,
|
|
58
|
+
template: 'hero',
|
|
59
|
+
title: 'Doc-scoped',
|
|
60
|
+
} as unknown as TemplateBlock;
|
|
61
|
+
|
|
62
|
+
const result = materializeBlockLayers(block, {
|
|
63
|
+
customTemplates: [customTemplate],
|
|
64
|
+
persistentLayers: false,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
expect(result.source).toBe('template');
|
|
68
|
+
expect((result.layers[0] as TextLayer).content.text).toBe('Doc-scoped');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('returns structured diagnostics and a visible fallback without logging', () => {
|
|
72
|
+
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
73
|
+
const error = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
74
|
+
const unknown = { ...titleBlock, template: 'not-registered' } as unknown as TemplateBlock;
|
|
75
|
+
|
|
76
|
+
const result = materializeBlockLayers(unknown, { persistentLayers: false });
|
|
77
|
+
|
|
78
|
+
expect(result.source).toBe('fallback');
|
|
79
|
+
expect(result.diagnostic).toEqual({
|
|
80
|
+
code: 'unknown-template',
|
|
81
|
+
template: 'not-registered',
|
|
82
|
+
message: 'Unknown template "not-registered"',
|
|
83
|
+
});
|
|
84
|
+
expect(result.layers.some((layer) => layer.id === 'fallback-notice')).toBe(true);
|
|
85
|
+
expect(warn).not.toHaveBeenCalled();
|
|
86
|
+
expect(error).not.toHaveBeenCalled();
|
|
87
|
+
warn.mockRestore();
|
|
88
|
+
error.mockRestore();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('supports an explicit empty failure policy', () => {
|
|
92
|
+
const unknown = { ...titleBlock, template: 'not-registered' } as unknown as TemplateBlock;
|
|
93
|
+
const result = materializeBlockLayers(unknown, {
|
|
94
|
+
failureMode: 'empty',
|
|
95
|
+
persistentLayers: false,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
expect(result.source).toBe('empty');
|
|
99
|
+
expect(result.layers).toEqual([]);
|
|
100
|
+
expect(result.diagnostic?.code).toBe('unknown-template');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('inherits theme persistent layers by default and has an explicit opt-out', () => {
|
|
104
|
+
const theme = {
|
|
105
|
+
...DEFAULT_THEME,
|
|
106
|
+
persistentLayers: {
|
|
107
|
+
bottomLayers: [
|
|
108
|
+
{
|
|
109
|
+
template: 'solidBackground' as const,
|
|
110
|
+
config: { type: 'solidBackground' as const, color: '#123456' },
|
|
111
|
+
},
|
|
112
|
+
],
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const inherited = materializeBlockLayers(titleBlock, { theme });
|
|
117
|
+
const disabled = materializeBlockLayers(titleBlock, { theme, persistentLayers: false });
|
|
118
|
+
|
|
119
|
+
expect(inherited.layers[0]?.id).toContain('solid');
|
|
120
|
+
expect(disabled.layers[0]?.id).not.toContain('solid');
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('is the only materialization path used by timed expansion', () => {
|
|
124
|
+
const diagnostics: string[] = [];
|
|
125
|
+
const unknown = { ...titleBlock, template: 'not-registered' } as unknown as TemplateBlock;
|
|
126
|
+
const direct = materializeBlockLayers(unknown, { persistentLayers: false });
|
|
127
|
+
const [scheduled] = expandDocBlocks([unknown], {
|
|
128
|
+
persistentLayers: false,
|
|
129
|
+
onDiagnostic: (diagnostic) => diagnostics.push(diagnostic.code),
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
expect(scheduled.layers).toEqual(direct.layers);
|
|
133
|
+
expect(diagnostics).toEqual(['unknown-template']);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Layer } from '../schemas/Doc.js';
|
|
2
|
+
import type { DocBlock } from '../schemas/BlockTemplates.js';
|
|
3
|
+
import {
|
|
4
|
+
materializeBlockLayers,
|
|
5
|
+
type MaterializeBlockLayersOptions,
|
|
6
|
+
} from '../doc/materializeBlockLayers.js';
|
|
7
|
+
|
|
8
|
+
/** Materialize isolated test fixtures without inheriting theme atmosphere. */
|
|
9
|
+
export function materializeLayers(
|
|
10
|
+
block: DocBlock,
|
|
11
|
+
options: MaterializeBlockLayersOptions = {},
|
|
12
|
+
): Layer[] {
|
|
13
|
+
return materializeBlockLayers(block, {
|
|
14
|
+
persistentLayers: false,
|
|
15
|
+
...options,
|
|
16
|
+
}).layers;
|
|
17
|
+
}
|