@bendyline/squisq 1.4.1 → 1.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -1
- package/dist/{ContentContainer-DNx460y8.d.ts → ContentContainer-BXUlIf18.d.ts} +6 -2
- package/dist/{Doc-BeSeH2iX.d.ts → Doc-BBVGq1_9.d.ts} +922 -273
- package/dist/{ImageEditDoc-BTIvw0oq.d.ts → ImageEditDoc-FuyGtt6o.d.ts} +11 -4
- package/dist/{chunk-ZTX4PFFT.js → chunk-2OIBZYKP.js} +75 -2
- package/dist/chunk-2OIBZYKP.js.map +1 -0
- package/dist/chunk-2UQOHYE2.js +45 -0
- package/dist/chunk-2UQOHYE2.js.map +1 -0
- package/dist/chunk-6ATE2PSM.js +344 -0
- package/dist/chunk-6ATE2PSM.js.map +1 -0
- package/dist/{chunk-57CVQCFX.js → chunk-A4O7GIWE.js} +74 -23
- package/dist/chunk-A4O7GIWE.js.map +1 -0
- package/dist/{chunk-5WFRKURX.js → chunk-DLXZMS5K.js} +4 -2
- package/dist/chunk-DLXZMS5K.js.map +1 -0
- package/dist/chunk-ET53IIEP.js +1 -0
- package/dist/chunk-ET53IIEP.js.map +1 -0
- package/dist/chunk-HWVFJAAH.js +362 -0
- package/dist/chunk-HWVFJAAH.js.map +1 -0
- package/dist/{chunk-2TMAWB4Y.js → chunk-IMSCRLLV.js} +1 -1
- package/dist/chunk-IMSCRLLV.js.map +1 -0
- package/dist/{chunk-B4EEON3N.js → chunk-KKNUBQ6Y.js} +536 -103
- package/dist/chunk-KKNUBQ6Y.js.map +1 -0
- package/dist/chunk-LDQ2HJIX.js +596 -0
- package/dist/chunk-LDQ2HJIX.js.map +1 -0
- package/dist/chunk-LH7I6SH7.js +7208 -0
- package/dist/chunk-LH7I6SH7.js.map +1 -0
- package/dist/chunk-PGGGKYWG.js +1 -0
- package/dist/chunk-PGGGKYWG.js.map +1 -0
- package/dist/{chunk-7UDSRZKG.js → chunk-Q3ROPT5H.js} +35 -14
- package/dist/chunk-Q3ROPT5H.js.map +1 -0
- package/dist/chunk-QE7TWGVT.js +178 -0
- package/dist/chunk-QE7TWGVT.js.map +1 -0
- package/dist/chunk-RUDYOTA6.js +345 -0
- package/dist/chunk-RUDYOTA6.js.map +1 -0
- package/dist/{chunk-K32VJONL.js → chunk-T5UK6YOB.js} +41 -2
- package/dist/chunk-T5UK6YOB.js.map +1 -0
- package/dist/{chunk-3K5OG6KI.js → chunk-TEEEILMP.js} +412 -186
- package/dist/chunk-TEEEILMP.js.map +1 -0
- package/dist/{chunk-FR2RBTKO.js → chunk-TQWLI6S2.js} +93 -27
- package/dist/chunk-TQWLI6S2.js.map +1 -0
- package/dist/{chunk-WMOI7FN3.js → chunk-UY7KGQ5R.js} +12 -2
- package/dist/{chunk-WMOI7FN3.js.map → chunk-UY7KGQ5R.js.map} +1 -1
- package/dist/{chunk-3E5F2XMR.js → chunk-VWUFZ6ZG.js} +29 -2
- package/dist/chunk-VWUFZ6ZG.js.map +1 -0
- package/dist/chunk-ZLQKNOZQ.js +1 -0
- package/dist/chunk-ZLQKNOZQ.js.map +1 -0
- package/dist/doc/index.d.ts +5 -5
- package/dist/doc/index.js +135 -8
- package/dist/generate/index.d.ts +3 -74
- package/dist/generate/index.js +2 -7
- package/dist/icons/index.d.ts +3 -26
- package/dist/icons/index.js +13 -0
- package/dist/icons/inlineIconMarker.d.ts +1 -0
- package/dist/icons/inlineIconMarker.js +15 -0
- package/dist/icons/inlineIconMarker.js.map +1 -0
- package/dist/imageEdit/index.d.ts +12 -6
- package/dist/imageEdit/index.js +4 -1
- package/dist/index.d.ts +14 -11
- package/dist/index.js +265 -37
- package/dist/inlineIconMarker-CiE8Hlvz.d.ts +59 -0
- package/dist/jsonForm/index.d.ts +42 -1
- package/dist/jsonForm/index.js +9 -1
- package/dist/markdown/index.d.ts +228 -3
- package/dist/markdown/index.js +42 -7
- package/dist/recommend/index.d.ts +1 -1
- package/dist/recommend/index.js +3 -2
- package/dist/schemas/index.d.ts +28 -8
- package/dist/schemas/index.js +36 -8
- package/dist/spatial/index.d.ts +9 -4
- package/dist/spatial/index.js +1 -1
- package/dist/storage/index.d.ts +2 -2
- package/dist/storage/index.js +1 -1
- package/dist/story/index.d.ts +1021 -41
- package/dist/story/index.js +135 -8
- package/dist/{themeLibrary-BHjJ0Qx6.d.ts → themeLibrary-CehcJhzz.d.ts} +1 -1
- package/dist/timing/index.js +3 -5
- package/dist/transform/index.d.ts +48 -10
- package/dist/transform/index.js +17 -481
- package/dist/transform/index.js.map +1 -1
- package/dist/{types-BdZkdJ3z.d.ts → types-DlAZ7MW4.d.ts} +115 -1
- package/dist/versions/index.d.ts +1 -1
- package/package.json +6 -1
- package/src/__tests__/animationUtils.test.ts +40 -0
- package/src/__tests__/annotationCoercion.test.ts +243 -0
- package/src/__tests__/applyRenderStyle.test.ts +146 -0
- package/src/__tests__/atmosphereLayers.test.ts +108 -0
- package/src/__tests__/attrTokens.test.ts +162 -0
- package/src/__tests__/contentExtractor.test.ts +60 -0
- package/src/__tests__/coverBlock.test.ts +46 -1
- package/src/__tests__/customTemplateSample.test.ts +144 -0
- package/src/__tests__/customTemplatesFrontmatter.test.ts +236 -0
- package/src/__tests__/customThemesFrontmatter.test.ts +145 -0
- package/src/__tests__/diagramBlock.test.ts +122 -0
- package/src/__tests__/diagramLayout.test.ts +134 -0
- package/src/__tests__/drawing.test.ts +109 -0
- package/src/__tests__/drawingBlock.test.ts +124 -0
- package/src/__tests__/drawingLayout.test.ts +171 -0
- package/src/__tests__/geohash.test.ts +6 -5
- package/src/__tests__/getLayers.test.ts +6 -2
- package/src/__tests__/getLayersFallback.test.ts +103 -0
- package/src/__tests__/imageEditExportSvg.test.ts +70 -0
- package/src/__tests__/imageEditVersions.test.ts +72 -0
- package/src/__tests__/inlineIconMarker.test.ts +36 -0
- package/src/__tests__/inputDescriptors.test.ts +162 -0
- package/src/__tests__/jsonForm.tokens.test.ts +84 -0
- package/src/__tests__/layoutBlock.test.ts +83 -0
- package/src/__tests__/layoutLayout.test.ts +169 -0
- package/src/__tests__/markdown.test.ts +87 -0
- package/src/__tests__/markdownSanitize.test.ts +79 -0
- package/src/__tests__/markdownToDoc.test.ts +391 -2
- package/src/__tests__/mediaAnnotations.test.ts +91 -0
- package/src/__tests__/mediaSchedule.test.ts +118 -0
- package/src/__tests__/pandocAttrParse.test.ts +187 -0
- package/src/__tests__/resolveDocTheme.test.ts +55 -0
- package/src/__tests__/rootExports.test.ts +43 -0
- package/src/__tests__/shapeGeometry.test.ts +89 -0
- package/src/__tests__/shapeVocabulary.test.ts +133 -0
- package/src/__tests__/structuredData.test.ts +249 -0
- package/src/__tests__/templateAnnotationParse.test.ts +108 -0
- package/src/__tests__/templateMetadata.test.ts +31 -0
- package/src/__tests__/templates.test.ts +257 -4
- package/src/__tests__/themeCompile.test.ts +42 -0
- package/src/__tests__/themeValidator.test.ts +6 -0
- package/src/__tests__/transformV2.test.ts +137 -0
- package/src/__tests__/transitionNormalize.test.ts +61 -0
- package/src/__tests__/validateDoc.test.ts +260 -0
- package/src/doc/annotationBlocks.ts +117 -0
- package/src/doc/customTemplatesFrontmatter.ts +314 -0
- package/src/doc/customThemesFrontmatter.ts +95 -0
- package/src/doc/docToMarkdown.ts +104 -5
- package/src/doc/getLayers.ts +49 -17
- package/src/doc/index.ts +47 -1
- package/src/doc/markdownToDoc.ts +411 -13
- package/src/doc/mediaAnnotations.ts +100 -0
- package/src/doc/resolveDocTheme.ts +55 -0
- package/src/doc/standaloneAnnotation.ts +58 -0
- package/src/doc/structuredData.ts +301 -0
- package/src/doc/templateInputs.ts +240 -0
- package/src/doc/templates/__tests__/customTemplate.test.ts +170 -0
- package/src/doc/templates/__tests__/customTemplateRegistry.test.ts +110 -0
- package/src/doc/templates/accentImage.ts +30 -6
- package/src/doc/templates/captionUtils.ts +23 -0
- package/src/doc/templates/comparisonBar.ts +33 -18
- package/src/doc/templates/coverBlock.ts +35 -5
- package/src/doc/templates/customTemplate.ts +51 -0
- package/src/doc/templates/dataTable.ts +37 -19
- package/src/doc/templates/dateEvent.ts +41 -20
- package/src/doc/templates/definitionCard.ts +26 -18
- package/src/doc/templates/diagramBlock.ts +204 -0
- package/src/doc/templates/diagramLayout.ts +159 -0
- package/src/doc/templates/drawingBlock.ts +325 -0
- package/src/doc/templates/drawingLayout.ts +539 -0
- package/src/doc/templates/factCard.ts +54 -26
- package/src/doc/templates/fallbackBlock.ts +123 -0
- package/src/doc/templates/featureBlock.ts +20 -13
- package/src/doc/templates/fullBleedQuote.ts +18 -8
- package/src/doc/templates/imageWithCaption.ts +30 -15
- package/src/doc/templates/index.ts +225 -46
- package/src/doc/templates/inputDescriptors.ts +432 -0
- package/src/doc/templates/layoutBlock.ts +43 -0
- package/src/doc/templates/layoutLayout.ts +243 -0
- package/src/doc/templates/listBlock.ts +56 -30
- package/src/doc/templates/mapBlock.ts +15 -12
- package/src/doc/templates/metadata.ts +120 -0
- package/src/doc/templates/persistentLayers.ts +129 -11
- package/src/doc/templates/photoGrid.ts +22 -34
- package/src/doc/templates/pullQuote.ts +30 -12
- package/src/doc/templates/quoteBlock.ts +54 -29
- package/src/doc/templates/sectionHeader.ts +25 -11
- package/src/doc/templates/statHighlight.ts +39 -22
- package/src/doc/templates/titleBlock.ts +59 -21
- package/src/doc/templates/tokens/__tests__/resolveTokens.test.ts +487 -0
- package/src/doc/templates/tokens/resolveTokens.ts +453 -0
- package/src/doc/templates/twoColumn.ts +48 -30
- package/src/doc/templates/videoPullQuote.ts +22 -12
- package/src/doc/templates/videoWithCaption.ts +21 -15
- package/src/doc/utils/animationUtils.ts +267 -3
- package/src/doc/utils/applyRenderStyle.ts +113 -0
- package/src/doc/utils/imageTreatment.ts +70 -0
- package/src/doc/utils/nearest.ts +59 -0
- package/src/doc/utils/shapeGeometry.ts +464 -0
- package/src/doc/utils/themeUtils.ts +75 -1
- package/src/doc/validate.ts +575 -0
- package/src/generate/contentExtractor.ts +148 -35
- package/src/generate/index.ts +0 -3
- package/src/icons/index.ts +8 -0
- package/src/icons/inlineIconMarker.ts +73 -0
- package/src/imageEdit/export.ts +85 -2
- package/src/imageEdit/index.ts +1 -0
- package/src/imageEdit/persistence.ts +51 -0
- package/src/index.ts +2 -0
- package/src/jsonForm/index.ts +2 -0
- package/src/jsonForm/tokens.ts +78 -0
- package/src/markdown/annotationCoercion.ts +295 -0
- package/src/markdown/attrTokens.ts +241 -0
- package/src/markdown/convert.ts +229 -26
- package/src/markdown/index.ts +37 -0
- package/src/markdown/sanitize.ts +318 -0
- package/src/markdown/stringify.ts +96 -6
- package/src/markdown/types.ts +66 -0
- package/src/markdown/utils.ts +111 -2
- package/src/recommend/templates.ts +24 -0
- package/src/schemas/BlockTemplates.ts +133 -4
- package/src/schemas/CustomTemplates.ts +126 -0
- package/src/schemas/Doc.ts +387 -65
- package/src/schemas/ImageEditDoc.ts +11 -4
- package/src/schemas/Media.ts +169 -0
- package/src/schemas/Theme.ts +23 -1
- package/src/schemas/Transitions.ts +224 -0
- package/src/schemas/colorUtils.ts +33 -0
- package/src/schemas/fontStacks.ts +5 -2
- package/src/schemas/index.ts +3 -0
- package/src/schemas/themeCompile.ts +24 -5
- package/src/schemas/themeValidator.ts +49 -11
- package/src/schemas/themes/cinematic.json +69 -12
- package/src/schemas/themes/documentary.json +58 -11
- package/src/schemas/themes/gezellig.json +60 -11
- package/src/schemas/themes/magazine.json +54 -9
- package/src/schemas/themes/morning-light.json +52 -9
- package/src/schemas/themes/standard-dark.json +48 -13
- package/src/schemas/themes/standard.json +44 -12
- package/src/schemas/themes/tech-dark.json +65 -12
- package/src/spatial/Geohash.ts +52 -21
- package/src/storage/ContentContainer.ts +6 -2
- package/src/transform/applyTransform.ts +43 -4
- package/src/transform/index.ts +1 -0
- package/src/transform/registry.ts +44 -13
- package/src/transform/styles/dataDriven.ts +2 -1
- package/src/transform/styles/documentary.ts +2 -1
- package/src/transform/styles/magazine.ts +3 -1
- package/src/transform/styles/minimal.ts +2 -1
- package/src/transform/styles/narrative.ts +3 -1
- package/src/transform/templateSelector.ts +87 -11
- package/src/transform/types.ts +35 -2
- package/dist/chunk-2TMAWB4Y.js.map +0 -1
- package/dist/chunk-3E5F2XMR.js.map +0 -1
- package/dist/chunk-3K5OG6KI.js.map +0 -1
- package/dist/chunk-4X3JQXNM.js +0 -35
- package/dist/chunk-4X3JQXNM.js.map +0 -1
- package/dist/chunk-57CVQCFX.js.map +0 -1
- package/dist/chunk-5WFRKURX.js.map +0 -1
- package/dist/chunk-7UDSRZKG.js.map +0 -1
- package/dist/chunk-B4EEON3N.js.map +0 -1
- package/dist/chunk-FR2RBTKO.js.map +0 -1
- package/dist/chunk-H3AXU4MJ.js +0 -179
- package/dist/chunk-H3AXU4MJ.js.map +0 -1
- package/dist/chunk-K32VJONL.js.map +0 -1
- package/dist/chunk-KSWJR66U.js +0 -3706
- package/dist/chunk-KSWJR66U.js.map +0 -1
- package/dist/chunk-MYIH7FWD.js +0 -145
- package/dist/chunk-MYIH7FWD.js.map +0 -1
- package/dist/chunk-ZTX4PFFT.js.map +0 -1
- package/src/__tests__/slideshowGenerator.test.ts +0 -128
- package/src/generate/slideshowGenerator.ts +0 -273
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* makeCustomTemplateFn — verifies a CustomTemplateDefinition expands
|
|
3
|
+
* correctly via the standard template function signature and that
|
|
4
|
+
* `%`-based positions resolve correctly across viewport presets.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, it, expect } from 'vitest';
|
|
8
|
+
import { makeCustomTemplateFn } from '../customTemplate';
|
|
9
|
+
import type { CustomTemplateDefinition } from '../../../schemas/CustomTemplates.js';
|
|
10
|
+
import type { Block, TextLayer } from '../../../schemas/Doc.js';
|
|
11
|
+
import type { RawLayersInput, TemplateContext } from '../../../schemas/BlockTemplates.js';
|
|
12
|
+
import { createTemplateContext } from '../../../schemas/BlockTemplates.js';
|
|
13
|
+
import { DEFAULT_THEME } from '../../../schemas/themeLibrary.js';
|
|
14
|
+
import { VIEWPORT_PRESETS, type ViewportConfig } from '../../../schemas/Viewport.js';
|
|
15
|
+
|
|
16
|
+
function defn(layers: TextLayer[]): CustomTemplateDefinition {
|
|
17
|
+
return {
|
|
18
|
+
name: 'hero',
|
|
19
|
+
label: 'Hero',
|
|
20
|
+
viewport: { width: 1920, height: 1080 },
|
|
21
|
+
layers,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function makeContext(
|
|
26
|
+
block: Block,
|
|
27
|
+
viewport: ViewportConfig = VIEWPORT_PRESETS.landscape,
|
|
28
|
+
): TemplateContext {
|
|
29
|
+
const ctx = createTemplateContext(DEFAULT_THEME, 0, 1, viewport);
|
|
30
|
+
ctx.block = block;
|
|
31
|
+
return ctx;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function makeBlock(overrides: Partial<Block> = {}): Block {
|
|
35
|
+
return {
|
|
36
|
+
id: 'b',
|
|
37
|
+
startTime: 0,
|
|
38
|
+
duration: 1,
|
|
39
|
+
audioSegment: 0,
|
|
40
|
+
title: 'Welcome',
|
|
41
|
+
template: 'hero',
|
|
42
|
+
...overrides,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function makeInput(): RawLayersInput {
|
|
47
|
+
return {
|
|
48
|
+
id: 'b',
|
|
49
|
+
template: 'layout',
|
|
50
|
+
duration: 1,
|
|
51
|
+
audioSegment: 0,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
describe('makeCustomTemplateFn', () => {
|
|
56
|
+
it('substitutes the block title into a layer carrying {title}', () => {
|
|
57
|
+
const fn = makeCustomTemplateFn(
|
|
58
|
+
defn([
|
|
59
|
+
{
|
|
60
|
+
id: 't',
|
|
61
|
+
type: 'text',
|
|
62
|
+
position: { x: '10%', y: '20%', width: '80%', height: '15%' },
|
|
63
|
+
content: { text: 'Hello, {title}!', style: { fontSize: 48, color: '#000' } },
|
|
64
|
+
},
|
|
65
|
+
]),
|
|
66
|
+
);
|
|
67
|
+
const ctx = makeContext(makeBlock({ title: 'World' }));
|
|
68
|
+
const layers = fn(makeInput(), ctx);
|
|
69
|
+
expect(layers).toHaveLength(1);
|
|
70
|
+
expect((layers[0] as TextLayer).content.text).toBe('Hello, World!');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('falls back to the input block when context.block is missing', () => {
|
|
74
|
+
const fn = makeCustomTemplateFn(
|
|
75
|
+
defn([
|
|
76
|
+
{
|
|
77
|
+
id: 't',
|
|
78
|
+
type: 'text',
|
|
79
|
+
position: { x: '0%', y: '0%', width: '100%', height: '10%' },
|
|
80
|
+
content: { text: '{title}', style: { fontSize: 24, color: '#000' } },
|
|
81
|
+
},
|
|
82
|
+
]),
|
|
83
|
+
);
|
|
84
|
+
const ctx = createTemplateContext(DEFAULT_THEME, 0, 1, VIEWPORT_PRESETS.landscape);
|
|
85
|
+
// No ctx.block set; the fn resolves against `input` instead. The
|
|
86
|
+
// default input carries no title, so {title} collapses to empty.
|
|
87
|
+
const layers = fn(makeInput(), ctx);
|
|
88
|
+
expect((layers[0] as TextLayer).content.text).toBe('');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('resolves {attr:key} from the input block when context.block is missing', () => {
|
|
92
|
+
const fn = makeCustomTemplateFn(
|
|
93
|
+
defn([
|
|
94
|
+
{
|
|
95
|
+
id: 't',
|
|
96
|
+
type: 'text',
|
|
97
|
+
position: { x: '0%', y: '0%', width: '100%', height: '10%' },
|
|
98
|
+
content: { text: '{attr:role}', style: { fontSize: 24, color: '#000' } },
|
|
99
|
+
},
|
|
100
|
+
]),
|
|
101
|
+
);
|
|
102
|
+
const ctx = createTemplateContext(DEFAULT_THEME, 0, 1, VIEWPORT_PRESETS.landscape);
|
|
103
|
+
const input = { ...makeInput(), templateOverrides: { role: 'lead' } } as RawLayersInput;
|
|
104
|
+
const layers = fn(input, ctx);
|
|
105
|
+
expect((layers[0] as TextLayer).content.text).toBe('lead');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('preserves %-based positions verbatim — they resolve at SSR render time', () => {
|
|
109
|
+
const fn = makeCustomTemplateFn(
|
|
110
|
+
defn([
|
|
111
|
+
{
|
|
112
|
+
id: 't',
|
|
113
|
+
type: 'text',
|
|
114
|
+
position: { x: '10%', y: '20%', width: '80%' },
|
|
115
|
+
content: { text: 'static', style: { fontSize: 24, color: '#000' } },
|
|
116
|
+
},
|
|
117
|
+
]),
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
// Landscape: 1920×1080
|
|
121
|
+
const landscapeLayers = fn(makeInput(), makeContext(makeBlock(), VIEWPORT_PRESETS.landscape));
|
|
122
|
+
expect(landscapeLayers[0].position.x).toBe('10%');
|
|
123
|
+
|
|
124
|
+
// Portrait: 1080×1920
|
|
125
|
+
const portraitLayers = fn(makeInput(), makeContext(makeBlock(), VIEWPORT_PRESETS.portrait));
|
|
126
|
+
expect(portraitLayers[0].position.x).toBe('10%');
|
|
127
|
+
|
|
128
|
+
// Square: 1080×1080
|
|
129
|
+
const squareLayers = fn(makeInput(), makeContext(makeBlock(), VIEWPORT_PRESETS.square));
|
|
130
|
+
expect(squareLayers[0].position.x).toBe('10%');
|
|
131
|
+
|
|
132
|
+
// The positions stay as `%`-strings — the SSR layer renderer (not
|
|
133
|
+
// this function) is responsible for resolving them into pixels
|
|
134
|
+
// against the target viewport. We verify the function doesn't
|
|
135
|
+
// perturb authoring intent.
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('round-trips multiple tokens in one definition', () => {
|
|
139
|
+
const fn = makeCustomTemplateFn(
|
|
140
|
+
defn([
|
|
141
|
+
{
|
|
142
|
+
id: 'title',
|
|
143
|
+
type: 'text',
|
|
144
|
+
position: { x: '5%', y: '5%', width: '90%', height: '20%' },
|
|
145
|
+
content: { text: '{title}', style: { fontSize: 72, color: '#000' } },
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
id: 'body',
|
|
149
|
+
type: 'text',
|
|
150
|
+
position: { x: '5%', y: '30%', width: '90%', height: '60%' },
|
|
151
|
+
content: { text: '{content}', style: { fontSize: 32, color: '#333' } },
|
|
152
|
+
},
|
|
153
|
+
]),
|
|
154
|
+
);
|
|
155
|
+
const ctx = makeContext(
|
|
156
|
+
makeBlock({
|
|
157
|
+
title: 'Welcome',
|
|
158
|
+
contents: [
|
|
159
|
+
{
|
|
160
|
+
type: 'paragraph',
|
|
161
|
+
children: [{ type: 'text', value: 'A small editor for big ideas.' }],
|
|
162
|
+
},
|
|
163
|
+
],
|
|
164
|
+
}),
|
|
165
|
+
);
|
|
166
|
+
const layers = fn(makeInput(), ctx);
|
|
167
|
+
expect((layers[0] as TextLayer).content.text).toBe('Welcome');
|
|
168
|
+
expect((layers[1] as TextLayer).content.text).toBe('A small editor for big ideas.');
|
|
169
|
+
});
|
|
170
|
+
});
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registry merge — end-to-end: a doc that uses a custom template name
|
|
3
|
+
* resolves through `expandDocBlocks(opts.customTemplates)` and produces
|
|
4
|
+
* layers with tokens substituted from the source block.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, it, expect } from 'vitest';
|
|
8
|
+
import { buildRegistry, expandDocBlocks } from '../index';
|
|
9
|
+
import type { CustomTemplateDefinition } from '../../../schemas/CustomTemplates.js';
|
|
10
|
+
import type { Block, TextLayer } from '../../../schemas/Doc.js';
|
|
11
|
+
|
|
12
|
+
const heroDef: CustomTemplateDefinition = {
|
|
13
|
+
name: 'hero',
|
|
14
|
+
label: 'Hero',
|
|
15
|
+
viewport: { width: 1920, height: 1080 },
|
|
16
|
+
layers: [
|
|
17
|
+
{
|
|
18
|
+
id: 'title',
|
|
19
|
+
type: 'text',
|
|
20
|
+
position: { x: '5%', y: '10%', width: '90%' },
|
|
21
|
+
content: { text: '{title}', style: { fontSize: 72, color: '#000' } },
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
id: 'body',
|
|
25
|
+
type: 'text',
|
|
26
|
+
position: { x: '5%', y: '40%', width: '90%' },
|
|
27
|
+
content: { text: '{content}', style: { fontSize: 32, color: '#333' } },
|
|
28
|
+
},
|
|
29
|
+
],
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
describe('buildRegistry', () => {
|
|
33
|
+
it('merges custom templates onto the built-in registry', () => {
|
|
34
|
+
const reg = buildRegistry([heroDef]);
|
|
35
|
+
expect(reg.hero).toBeDefined();
|
|
36
|
+
expect(reg.title).toBeDefined(); // built-in survives
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('does not let custom templates shadow built-in names', () => {
|
|
40
|
+
const collide: CustomTemplateDefinition = {
|
|
41
|
+
...heroDef,
|
|
42
|
+
name: 'title', // collides with built-in
|
|
43
|
+
};
|
|
44
|
+
const reg = buildRegistry([collide]);
|
|
45
|
+
// Built-in `title` is preserved — the collide entry is silently
|
|
46
|
+
// dropped (callers shouldn't have created it). We verify by
|
|
47
|
+
// checking the function reference is the original built-in's.
|
|
48
|
+
const builtIn = buildRegistry();
|
|
49
|
+
expect(reg.title).toBe(builtIn.title);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe('expandDocBlocks with customTemplates', () => {
|
|
54
|
+
it('expands a doc-defined template name into resolved layers', () => {
|
|
55
|
+
const block: Block = {
|
|
56
|
+
id: 'b1',
|
|
57
|
+
startTime: 0,
|
|
58
|
+
duration: 1,
|
|
59
|
+
audioSegment: 0,
|
|
60
|
+
title: 'Welcome',
|
|
61
|
+
template: 'hero',
|
|
62
|
+
contents: [
|
|
63
|
+
{
|
|
64
|
+
type: 'paragraph',
|
|
65
|
+
children: [{ type: 'text', value: 'A small editor for big ideas.' }],
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const expanded = expandDocBlocks([block], { customTemplates: [heroDef] });
|
|
71
|
+
expect(expanded).toHaveLength(1);
|
|
72
|
+
const layers = expanded[0].layers ?? [];
|
|
73
|
+
expect(layers).toHaveLength(2);
|
|
74
|
+
expect((layers[0] as TextLayer).content.text).toBe('Welcome');
|
|
75
|
+
expect((layers[1] as TextLayer).content.text).toBe('A small editor for big ideas.');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('preserves %-based positions so the SSR renderer can resolve them per viewport', () => {
|
|
79
|
+
const block: Block = {
|
|
80
|
+
id: 'b1',
|
|
81
|
+
startTime: 0,
|
|
82
|
+
duration: 1,
|
|
83
|
+
audioSegment: 0,
|
|
84
|
+
title: 'X',
|
|
85
|
+
template: 'hero',
|
|
86
|
+
};
|
|
87
|
+
const expanded = expandDocBlocks([block], { customTemplates: [heroDef] });
|
|
88
|
+
expect(expanded[0].layers![0].position.x).toBe('5%');
|
|
89
|
+
expect(expanded[0].layers![0].position.width).toBe('90%');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('falls back to a no-layer block (with a warning) when the template is unknown', () => {
|
|
93
|
+
// Suppress the expected console.warn from `expandTemplateBlock`.
|
|
94
|
+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
95
|
+
const block: Block = {
|
|
96
|
+
id: 'b1',
|
|
97
|
+
startTime: 0,
|
|
98
|
+
duration: 1,
|
|
99
|
+
audioSegment: 0,
|
|
100
|
+
title: 'Y',
|
|
101
|
+
template: 'nonexistent',
|
|
102
|
+
};
|
|
103
|
+
const expanded = expandDocBlocks([block], { customTemplates: [heroDef] });
|
|
104
|
+
expect(expanded[0].layers).toBeUndefined();
|
|
105
|
+
expect(warnSpy).toHaveBeenCalled();
|
|
106
|
+
warnSpy.mockRestore();
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
import { vi } from 'vitest';
|
|
@@ -13,7 +13,13 @@
|
|
|
13
13
|
* This is shared code used by both site and efb-app doc renderers.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
import type {
|
|
16
|
+
import type {
|
|
17
|
+
Layer,
|
|
18
|
+
ImageLayer,
|
|
19
|
+
ImageTreatment,
|
|
20
|
+
ShapeLayer,
|
|
21
|
+
Animation,
|
|
22
|
+
} from '../../schemas/Doc.js';
|
|
17
23
|
import type { AccentImage, AccentPosition } from '../../schemas/BlockTemplates.js';
|
|
18
24
|
|
|
19
25
|
const PERCENTAGE_RE = /^(\d+(?:\.\d+)?)\s*%?$/;
|
|
@@ -125,28 +131,40 @@ export function getAccentLayout(position: AccentPosition): AccentLayout {
|
|
|
125
131
|
* Create layers for an accent image.
|
|
126
132
|
* Returns the image layer and any overlay/gradient layers needed.
|
|
127
133
|
*/
|
|
128
|
-
export function createAccentLayers(
|
|
134
|
+
export function createAccentLayers(
|
|
135
|
+
accent: AccentImage,
|
|
136
|
+
slideId: string,
|
|
137
|
+
treatment?: ImageTreatment,
|
|
138
|
+
): Layer[] {
|
|
129
139
|
const layers: Layer[] = [];
|
|
130
140
|
const { src, alt, position, ambientMotion, credit, license } = accent;
|
|
131
141
|
|
|
132
142
|
switch (position) {
|
|
133
143
|
case 'left-strip':
|
|
134
|
-
layers.push(
|
|
144
|
+
layers.push(
|
|
145
|
+
createStripImage(src, alt, 'left', slideId, ambientMotion, credit, license, treatment),
|
|
146
|
+
);
|
|
135
147
|
layers.push(createStripGradient('left', slideId));
|
|
136
148
|
break;
|
|
137
149
|
|
|
138
150
|
case 'right-strip':
|
|
139
|
-
layers.push(
|
|
151
|
+
layers.push(
|
|
152
|
+
createStripImage(src, alt, 'right', slideId, ambientMotion, credit, license, treatment),
|
|
153
|
+
);
|
|
140
154
|
layers.push(createStripGradient('right', slideId));
|
|
141
155
|
break;
|
|
142
156
|
|
|
143
157
|
case 'bottom-strip':
|
|
144
|
-
layers.push(
|
|
158
|
+
layers.push(
|
|
159
|
+
createBottomStripImage(src, alt, slideId, ambientMotion, credit, license, treatment),
|
|
160
|
+
);
|
|
145
161
|
layers.push(createBottomStripGradient(slideId));
|
|
146
162
|
break;
|
|
147
163
|
|
|
148
164
|
case 'corner-inset':
|
|
149
|
-
layers.push(
|
|
165
|
+
layers.push(
|
|
166
|
+
createCornerInsetImage(src, alt, slideId, ambientMotion, credit, license, treatment),
|
|
167
|
+
);
|
|
150
168
|
layers.push(createCornerVignette(slideId));
|
|
151
169
|
break;
|
|
152
170
|
}
|
|
@@ -165,6 +183,7 @@ function createStripImage(
|
|
|
165
183
|
ambientMotion?: AccentImage['ambientMotion'],
|
|
166
184
|
credit?: string,
|
|
167
185
|
license?: string,
|
|
186
|
+
treatment?: ImageTreatment,
|
|
168
187
|
): ImageLayer {
|
|
169
188
|
return {
|
|
170
189
|
type: 'image',
|
|
@@ -175,6 +194,7 @@ function createStripImage(
|
|
|
175
194
|
fit: 'cover',
|
|
176
195
|
credit,
|
|
177
196
|
license,
|
|
197
|
+
...(treatment ? { treatment } : {}),
|
|
178
198
|
},
|
|
179
199
|
position: {
|
|
180
200
|
x: side === 'left' ? 0 : `${100 - STRIP_SIZE}%`,
|
|
@@ -221,6 +241,7 @@ function createBottomStripImage(
|
|
|
221
241
|
ambientMotion?: AccentImage['ambientMotion'],
|
|
222
242
|
credit?: string,
|
|
223
243
|
license?: string,
|
|
244
|
+
treatment?: ImageTreatment,
|
|
224
245
|
): ImageLayer {
|
|
225
246
|
return {
|
|
226
247
|
type: 'image',
|
|
@@ -231,6 +252,7 @@ function createBottomStripImage(
|
|
|
231
252
|
fit: 'cover',
|
|
232
253
|
credit,
|
|
233
254
|
license,
|
|
255
|
+
...(treatment ? { treatment } : {}),
|
|
234
256
|
},
|
|
235
257
|
position: {
|
|
236
258
|
x: 0,
|
|
@@ -272,6 +294,7 @@ function createCornerInsetImage(
|
|
|
272
294
|
ambientMotion?: AccentImage['ambientMotion'],
|
|
273
295
|
credit?: string,
|
|
274
296
|
license?: string,
|
|
297
|
+
treatment?: ImageTreatment,
|
|
275
298
|
): ImageLayer {
|
|
276
299
|
return {
|
|
277
300
|
type: 'image',
|
|
@@ -282,6 +305,7 @@ function createCornerInsetImage(
|
|
|
282
305
|
fit: 'cover',
|
|
283
306
|
credit,
|
|
284
307
|
license,
|
|
308
|
+
...(treatment ? { treatment } : {}),
|
|
285
309
|
},
|
|
286
310
|
position: {
|
|
287
311
|
x: '70%',
|
|
@@ -31,3 +31,26 @@ export function createBackgroundLayer(id: string, fill: string): Layer {
|
|
|
31
31
|
position: { x: 0, y: 0, width: '100%', height: '100%' },
|
|
32
32
|
};
|
|
33
33
|
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Estimate the rendered height (px) of a wrapped text block.
|
|
37
|
+
*
|
|
38
|
+
* Templates are pure functions with no access to real text metrics, but
|
|
39
|
+
* stacked lockups (quote + attribution, fact + explanation + source) need
|
|
40
|
+
* to place each element relative to the previous one instead of at fixed
|
|
41
|
+
* slots — fixed slots leave 200px voids for short content and collide for
|
|
42
|
+
* long content. The 0.52 average-glyph-width factor is tuned for the
|
|
43
|
+
* sans/serif faces the built-in themes use; it only needs to be right
|
|
44
|
+
* within ~20% for spacing purposes.
|
|
45
|
+
*/
|
|
46
|
+
export function estimateTextHeight(
|
|
47
|
+
text: string,
|
|
48
|
+
fontSizePx: number,
|
|
49
|
+
maxWidthPx: number,
|
|
50
|
+
lineHeight: number,
|
|
51
|
+
): number {
|
|
52
|
+
const avgCharWidth = fontSizePx * 0.52;
|
|
53
|
+
const charsPerLine = Math.max(8, Math.floor(maxWidthPx / avgCharWidth));
|
|
54
|
+
const lines = Math.max(1, Math.ceil(text.length / charsPerLine));
|
|
55
|
+
return lines * fontSizePx * lineHeight;
|
|
56
|
+
}
|
|
@@ -10,31 +10,30 @@
|
|
|
10
10
|
|
|
11
11
|
import type { Layer } from '../../schemas/Doc.js';
|
|
12
12
|
import type { ComparisonBarInput, TemplateContext } from '../../schemas/BlockTemplates.js';
|
|
13
|
-
import {
|
|
14
|
-
|
|
13
|
+
import {
|
|
14
|
+
resolveColorScheme,
|
|
15
|
+
getThemeFont,
|
|
16
|
+
themedFontSize,
|
|
17
|
+
themedSurfaceGradient,
|
|
18
|
+
} from '../utils/themeUtils.js';
|
|
15
19
|
|
|
16
20
|
export function comparisonBar(input: ComparisonBarInput, context: TemplateContext): Layer[] {
|
|
17
|
-
const { leftLabel,
|
|
18
|
-
const { theme } = context;
|
|
21
|
+
const { leftLabel, rightLabel, unit, colorScheme = 'blue' } = input;
|
|
22
|
+
const { theme, viewport } = context;
|
|
19
23
|
const colors = resolveColorScheme(context, colorScheme);
|
|
20
24
|
|
|
21
|
-
|
|
22
|
-
|
|
25
|
+
// Defensive: coerce non-finite values to 0. Callers should pass real numbers,
|
|
26
|
+
// but a NaN would otherwise render as literal "NaN" text and collapse every
|
|
27
|
+
// bar/label to a `NaN%` position, cramming the block against its left edge.
|
|
28
|
+
const leftValue = Number.isFinite(input.leftValue) ? input.leftValue : 0;
|
|
29
|
+
const rightValue = Number.isFinite(input.rightValue) ? input.rightValue : 0;
|
|
23
30
|
|
|
24
|
-
|
|
25
|
-
const
|
|
26
|
-
const maxBarWidth = 65;
|
|
27
|
-
const leftBarWidth = (leftValue / maxValue) * maxBarWidth;
|
|
28
|
-
const rightBarWidth = (rightValue / maxValue) * maxBarWidth;
|
|
29
|
-
|
|
30
|
-
// Bar positioning
|
|
31
|
-
const barStartX = 15;
|
|
32
|
-
const barHeight = 6; // % of viewport height
|
|
33
|
-
const topBarY = 36;
|
|
34
|
-
const bottomBarY = 58;
|
|
31
|
+
const labelFontSize = themedFontSize(28, context, false);
|
|
32
|
+
const valueFontSize = themedFontSize(48, context, true);
|
|
35
33
|
|
|
36
34
|
// Format values for display
|
|
37
35
|
const formatValue = (v: number): string => {
|
|
36
|
+
if (!Number.isFinite(v)) return '—';
|
|
38
37
|
if (v >= 1_000_000) return `${(v / 1_000_000).toFixed(1)}M`;
|
|
39
38
|
if (v >= 1_000) return `${(v / 1_000).toFixed(v >= 10_000 ? 0 : 1)}K`;
|
|
40
39
|
return v.toLocaleString();
|
|
@@ -43,6 +42,22 @@ export function comparisonBar(input: ComparisonBarInput, context: TemplateContex
|
|
|
43
42
|
const leftDisplay = unit ? `${formatValue(leftValue)} ${unit}` : formatValue(leftValue);
|
|
44
43
|
const rightDisplay = unit ? `${formatValue(rightValue)} ${unit}` : formatValue(rightValue);
|
|
45
44
|
|
|
45
|
+
// Bar positioning
|
|
46
|
+
const barStartX = 15;
|
|
47
|
+
const barHeight = 6; // % of viewport height
|
|
48
|
+
const topBarY = 36;
|
|
49
|
+
const bottomBarY = 58;
|
|
50
|
+
|
|
51
|
+
// Calculate proportional bar widths. The widest bar must leave room
|
|
52
|
+
// for its trailing value label inside a 96% safe area — otherwise a
|
|
53
|
+
// long label ("84 clarity score") runs off the right edge of the block.
|
|
54
|
+
const maxValue = Math.max(leftValue, rightValue, 1);
|
|
55
|
+
const longestLabelPx = Math.max(leftDisplay.length, rightDisplay.length) * valueFontSize * 0.58;
|
|
56
|
+
const labelWidthPct = (longestLabelPx / viewport.width) * 100;
|
|
57
|
+
const maxBarWidth = Math.max(20, Math.min(65, 96 - barStartX - labelWidthPct - 2));
|
|
58
|
+
const leftBarWidth = (leftValue / maxValue) * maxBarWidth;
|
|
59
|
+
const rightBarWidth = (rightValue / maxValue) * maxBarWidth;
|
|
60
|
+
|
|
46
61
|
return [
|
|
47
62
|
// Background
|
|
48
63
|
{
|
|
@@ -50,7 +65,7 @@ export function comparisonBar(input: ComparisonBarInput, context: TemplateContex
|
|
|
50
65
|
id: 'bg',
|
|
51
66
|
content: {
|
|
52
67
|
shape: 'rect',
|
|
53
|
-
fill:
|
|
68
|
+
fill: themedSurfaceGradient(context, 180),
|
|
54
69
|
},
|
|
55
70
|
position: { x: 0, y: 0, width: '100%', height: '100%' },
|
|
56
71
|
},
|
|
@@ -16,9 +16,8 @@
|
|
|
16
16
|
|
|
17
17
|
import type { Layer } from '../../schemas/Doc.js';
|
|
18
18
|
import type { TemplateContext } from '../../schemas/BlockTemplates.js';
|
|
19
|
-
import { scaledFontSize } from '../../schemas/BlockTemplates.js';
|
|
20
19
|
import type { StartBlockConfig } from '../../schemas/Doc.js';
|
|
21
|
-
import { getThemeFont } from '../utils/themeUtils.js';
|
|
20
|
+
import { getThemeFont, themedFontSize, themedImageTreatment } from '../utils/themeUtils.js';
|
|
22
21
|
import { mapAmbientMotion } from './accentImage.js';
|
|
23
22
|
|
|
24
23
|
/**
|
|
@@ -39,18 +38,48 @@ export interface CoverBlockInput {
|
|
|
39
38
|
heroCredit?: string;
|
|
40
39
|
/** License identifier */
|
|
41
40
|
heroLicense?: string;
|
|
41
|
+
/** Per-block override for the theme's photographic image grade. */
|
|
42
|
+
imageTreatment?: 'none' | 'mono' | 'duotone' | 'warm' | 'cool';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Base (pre-viewport-scale) font size for a full-size cover title. */
|
|
46
|
+
const COVER_TITLE_BASE_PX = 120;
|
|
47
|
+
/** Title length (chars) that still reads well at the full base size. */
|
|
48
|
+
const COVER_TITLE_COMFORTABLE_CHARS = 26;
|
|
49
|
+
/** Readability floor so very long titles never shrink to nothing. */
|
|
50
|
+
const COVER_TITLE_MIN_BASE_PX = 62;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Compute the base cover-title font size for a given title, shrinking long
|
|
54
|
+
* titles so their wrapped block fits the frame. Returns the pre-viewport-scale
|
|
55
|
+
* value expected by {@link themedFontSize}; short titles get the full base.
|
|
56
|
+
*/
|
|
57
|
+
export function fitCoverTitleSize(title: string): number {
|
|
58
|
+
const len = title.trim().length;
|
|
59
|
+
if (len <= COVER_TITLE_COMFORTABLE_CHARS) return COVER_TITLE_BASE_PX;
|
|
60
|
+
const scaled = COVER_TITLE_BASE_PX * Math.sqrt(COVER_TITLE_COMFORTABLE_CHARS / len);
|
|
61
|
+
return Math.max(COVER_TITLE_MIN_BASE_PX, Math.round(scaled));
|
|
42
62
|
}
|
|
43
63
|
|
|
44
64
|
/**
|
|
45
65
|
* Generate cover block layers from StartBlockConfig.
|
|
46
66
|
*/
|
|
47
67
|
export function coverBlock(input: CoverBlockInput, context: TemplateContext): Layer[] {
|
|
68
|
+
const treatment = themedImageTreatment(context, input.imageTreatment);
|
|
48
69
|
const { heroSrc, heroAlt, title, subtitle, ambientMotion, heroCredit, heroLicense } = input;
|
|
49
70
|
const { theme, layout } = context;
|
|
50
71
|
|
|
51
|
-
// Scale font sizes for viewport - cover titles are larger than regular title blocks
|
|
52
|
-
|
|
53
|
-
|
|
72
|
+
// Scale font sizes for viewport - cover titles are larger than regular title blocks.
|
|
73
|
+
// The cover title wraps within maxTextWidth but does not otherwise shrink, so a long
|
|
74
|
+
// title (e.g. "Seattle: The Emerald City That Rebuilt Itself on Top of Itself") wrapped
|
|
75
|
+
// to three 120px lines overflows the frame and is clipped by the player controls.
|
|
76
|
+
// Auto-fit the base size to title length so long titles stay within the frame. The
|
|
77
|
+
// wrapped title's total height grows with fontSize² × length (line count ∝ length ×
|
|
78
|
+
// fontSize / width, line height ∝ fontSize), so scaling the base by √(threshold/length)
|
|
79
|
+
// keeps the wrapped block's area roughly constant past the threshold. Short titles are
|
|
80
|
+
// unaffected (scale clamps at 1).
|
|
81
|
+
const titleFontSize = themedFontSize(fitCoverTitleSize(title), context, true);
|
|
82
|
+
const subtitleFontSize = themedFontSize(40, context, false);
|
|
54
83
|
|
|
55
84
|
const layers: Layer[] = [];
|
|
56
85
|
|
|
@@ -68,6 +97,7 @@ export function coverBlock(input: CoverBlockInput, context: TemplateContext): La
|
|
|
68
97
|
fit: 'cover',
|
|
69
98
|
credit: heroCredit,
|
|
70
99
|
license: heroLicense,
|
|
100
|
+
...(treatment ? { treatment } : {}),
|
|
71
101
|
},
|
|
72
102
|
position: { x: 0, y: 0, width: '100%', height: '100%' },
|
|
73
103
|
animation: imageAnimation,
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom template factory.
|
|
3
|
+
*
|
|
4
|
+
* Turns a `CustomTemplateDefinition` (user-authored visual design with
|
|
5
|
+
* placeholder tokens) into a runtime `TemplateFunction` that can be
|
|
6
|
+
* dropped into the template registry alongside built-ins.
|
|
7
|
+
*
|
|
8
|
+
* The generated function's job is small: read the source `Block` from
|
|
9
|
+
* `context.block` and hand the definition's layers off to
|
|
10
|
+
* `resolveTokens` for placeholder substitution. The hard work (drag-
|
|
11
|
+
* to-position, token grammar, image lookup) happens in the designer
|
|
12
|
+
* UI and the resolver, not here.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { Block, Layer } from '../../schemas/Doc.js';
|
|
16
|
+
import type {
|
|
17
|
+
RawLayersInput,
|
|
18
|
+
TemplateContext,
|
|
19
|
+
TemplateFunction,
|
|
20
|
+
} from '../../schemas/BlockTemplates.js';
|
|
21
|
+
import type { CustomTemplateDefinition } from '../../schemas/CustomTemplates.js';
|
|
22
|
+
import { resolveTokens } from './tokens/resolveTokens.js';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Build a template function for a user-authored custom template.
|
|
26
|
+
*
|
|
27
|
+
* The returned function is registered under `def.name` in the merged
|
|
28
|
+
* registry (see `buildRegistry` in `templates/index.ts`). At expansion
|
|
29
|
+
* time it receives the standard `TemplateBlock` + `TemplateContext`
|
|
30
|
+
* pair; the context's `block` field carries the source markdown-derived
|
|
31
|
+
* block whose data drives token resolution.
|
|
32
|
+
*
|
|
33
|
+
* When `context.block` is missing (shouldn't happen in the normal
|
|
34
|
+
* pipeline but defensively handled), the layers are returned with
|
|
35
|
+
* tokens left unresolved — better visible placeholders than a thrown
|
|
36
|
+
* error.
|
|
37
|
+
*/
|
|
38
|
+
export function makeCustomTemplateFn(
|
|
39
|
+
def: CustomTemplateDefinition,
|
|
40
|
+
): TemplateFunction<RawLayersInput> {
|
|
41
|
+
return (input: RawLayersInput, context: TemplateContext): Layer[] => {
|
|
42
|
+
// `{attr:…}` reads the source block's `templateOverrides` /
|
|
43
|
+
// `templateData` / `metadata`. Prefer `context.block` (the raw
|
|
44
|
+
// block the expander attaches); fall back to `input` (the merged
|
|
45
|
+
// TemplateBlock, itself a Block superset) so attributes still
|
|
46
|
+
// resolve if `context.block` is ever absent.
|
|
47
|
+
const block = context.block ?? (input as unknown as Block | undefined);
|
|
48
|
+
if (!block) return def.layers.slice() as Layer[];
|
|
49
|
+
return resolveTokens(def.layers, block);
|
|
50
|
+
};
|
|
51
|
+
}
|