@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,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom templates round-trip through frontmatter.
|
|
3
|
+
*
|
|
4
|
+
* Verifies that a CustomTemplateDefinition stored in markdown
|
|
5
|
+
* frontmatter is faithfully reconstructed by `markdownToDoc` and
|
|
6
|
+
* serialized back by `docToMarkdown` without loss.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
10
|
+
import { markdownToDoc } from '../doc/markdownToDoc';
|
|
11
|
+
import { docToMarkdown } from '../doc/docToMarkdown';
|
|
12
|
+
import {
|
|
13
|
+
encodeLayersForFrontmatter,
|
|
14
|
+
decodeLayersFromFrontmatter,
|
|
15
|
+
readCustomTemplatesFromFrontmatter,
|
|
16
|
+
writeCustomTemplatesToFrontmatter,
|
|
17
|
+
FRONTMATTER_CUSTOM_TEMPLATES_KEY,
|
|
18
|
+
} from '../doc/customTemplatesFrontmatter';
|
|
19
|
+
import { parseMarkdown } from '../markdown/parse';
|
|
20
|
+
import type { Layer } from '../schemas/Doc.js';
|
|
21
|
+
import type { CustomTemplateDefinition } from '../schemas/CustomTemplates.js';
|
|
22
|
+
|
|
23
|
+
const sampleLayers: Layer[] = [
|
|
24
|
+
{
|
|
25
|
+
id: 'title',
|
|
26
|
+
type: 'text',
|
|
27
|
+
position: { x: '5%', y: '10%', width: '90%' },
|
|
28
|
+
content: { text: '{title}', style: { fontSize: 72, color: '#000' } },
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
id: 'body',
|
|
32
|
+
type: 'text',
|
|
33
|
+
position: { x: '5%', y: '40%', width: '90%' },
|
|
34
|
+
content: { text: '{content}', style: { fontSize: 32, color: '#333' } },
|
|
35
|
+
},
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
const heroDef: CustomTemplateDefinition = {
|
|
39
|
+
name: 'hero',
|
|
40
|
+
label: 'Hero',
|
|
41
|
+
description: 'Large title with body below.',
|
|
42
|
+
viewport: { width: 1920, height: 1080 },
|
|
43
|
+
layers: sampleLayers,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
describe('encodeLayersForFrontmatter ↔ decodeLayersFromFrontmatter', () => {
|
|
47
|
+
it('round-trips a single Layer array verbatim', () => {
|
|
48
|
+
const encoded = encodeLayersForFrontmatter(sampleLayers);
|
|
49
|
+
const decoded = decodeLayersFromFrontmatter(encoded);
|
|
50
|
+
expect(decoded).toEqual(sampleLayers);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("returns [] when the base64 isn't valid JSON", () => {
|
|
54
|
+
const garbage = encodeLayersForFrontmatter(sampleLayers).slice(0, 4);
|
|
55
|
+
expect(decodeLayersFromFrontmatter(garbage)).toEqual([]);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe('writeCustomTemplatesToFrontmatter → readCustomTemplatesFromFrontmatter', () => {
|
|
60
|
+
it('round-trips a single definition through the compact JSON wire format', () => {
|
|
61
|
+
const payload = writeCustomTemplatesToFrontmatter([heroDef]);
|
|
62
|
+
expect(typeof payload).toBe('string');
|
|
63
|
+
// Compact form: a readable JSON object keyed by name, short keys, no
|
|
64
|
+
// base64, and the default 1920×1080 viewport omitted.
|
|
65
|
+
expect(payload!.startsWith('{"hero":')).toBe(true);
|
|
66
|
+
expect(payload).toContain('"lb":"Hero"');
|
|
67
|
+
expect(payload).not.toContain('"viewport"');
|
|
68
|
+
const fm = { [FRONTMATTER_CUSTOM_TEMPLATES_KEY]: payload };
|
|
69
|
+
const out = readCustomTemplatesFromFrontmatter(fm);
|
|
70
|
+
expect(out).toHaveLength(1);
|
|
71
|
+
expect(out![0]).toEqual(heroDef);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('default output is compact (single line) and byte-unchanged', () => {
|
|
75
|
+
const payload = writeCustomTemplatesToFrontmatter([heroDef])!;
|
|
76
|
+
expect(payload).not.toContain('\n');
|
|
77
|
+
// Explicit pretty:false must match the historical default exactly.
|
|
78
|
+
expect(writeCustomTemplatesToFrontmatter([heroDef], { pretty: false })).toBe(payload);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('pretty:true emits multi-line JSON that still reads back', () => {
|
|
82
|
+
const payload = writeCustomTemplatesToFrontmatter([heroDef], { pretty: true })!;
|
|
83
|
+
expect(payload).toContain('\n');
|
|
84
|
+
// Same compact key codec, just pretty-printed.
|
|
85
|
+
expect(payload).toContain('"lb": "Hero"');
|
|
86
|
+
const fm = { [FRONTMATTER_CUSTOM_TEMPLATES_KEY]: payload };
|
|
87
|
+
const out = readCustomTemplatesFromFrontmatter(fm);
|
|
88
|
+
expect(out).toHaveLength(1);
|
|
89
|
+
expect(out![0]).toEqual(heroDef);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('a hand-written block-scalar frontmatter parses back into the definition', () => {
|
|
93
|
+
const payload = writeCustomTemplatesToFrontmatter([heroDef], { pretty: true })!;
|
|
94
|
+
const body = payload
|
|
95
|
+
.split('\n')
|
|
96
|
+
.map((l) => ` ${l}`)
|
|
97
|
+
.join('\n');
|
|
98
|
+
const md = `---\n${FRONTMATTER_CUSTOM_TEMPLATES_KEY}: |-\n${body}\n---\n\n# Hi\n`;
|
|
99
|
+
const parsed = parseMarkdown(md);
|
|
100
|
+
const out = readCustomTemplatesFromFrontmatter(parsed.frontmatter);
|
|
101
|
+
expect(out).toHaveLength(1);
|
|
102
|
+
expect(out![0]).toEqual(heroDef);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('is markedly smaller than the legacy base64 form', () => {
|
|
106
|
+
const compact = writeCustomTemplatesToFrontmatter([heroDef])!;
|
|
107
|
+
const legacyBase64 = btoa(unescape(encodeURIComponent(JSON.stringify([{ ...heroDef }]))));
|
|
108
|
+
expect(compact.length).toBeLessThan(legacyBase64.length);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('still reads the legacy base64-JSON payload (back-compat)', () => {
|
|
112
|
+
// What older documents have on disk: base64 of the full-keyed array.
|
|
113
|
+
const legacy = btoa(unescape(encodeURIComponent(JSON.stringify([heroDef]))));
|
|
114
|
+
const out = readCustomTemplatesFromFrontmatter({
|
|
115
|
+
[FRONTMATTER_CUSTOM_TEMPLATES_KEY]: legacy,
|
|
116
|
+
});
|
|
117
|
+
expect(out).toHaveLength(1);
|
|
118
|
+
expect(out![0]).toEqual(heroDef);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('round-trips layers carrying the full styling field set without loss', () => {
|
|
122
|
+
const rich: CustomTemplateDefinition = {
|
|
123
|
+
name: 'rich',
|
|
124
|
+
label: 'Rich',
|
|
125
|
+
viewport: { width: 1080, height: 1080 },
|
|
126
|
+
layers: [
|
|
127
|
+
{
|
|
128
|
+
id: 'p',
|
|
129
|
+
type: 'path',
|
|
130
|
+
position: { x: '1%', y: '2%', width: '3%', height: '4%', anchor: 'center' },
|
|
131
|
+
content: {
|
|
132
|
+
d: 'M0 0',
|
|
133
|
+
shapeKind: 'star',
|
|
134
|
+
fill: '#fff',
|
|
135
|
+
fillOpacity: 0.5,
|
|
136
|
+
gradient: { from: '#000', to: '#fff', angle: 90 },
|
|
137
|
+
stroke: '#111',
|
|
138
|
+
strokeWidth: 3,
|
|
139
|
+
borderStyle: 'dashed',
|
|
140
|
+
dasharray: '4 2',
|
|
141
|
+
},
|
|
142
|
+
} as Layer,
|
|
143
|
+
],
|
|
144
|
+
};
|
|
145
|
+
const payload = writeCustomTemplatesToFrontmatter([rich])!;
|
|
146
|
+
const out = readCustomTemplatesFromFrontmatter({ [FRONTMATTER_CUSTOM_TEMPLATES_KEY]: payload });
|
|
147
|
+
expect(out![0]).toEqual(rich);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('round-trips multiple definitions in declaration order', () => {
|
|
151
|
+
const second: CustomTemplateDefinition = { ...heroDef, name: 'second', label: 'Second' };
|
|
152
|
+
const payload = writeCustomTemplatesToFrontmatter([heroDef, second]);
|
|
153
|
+
const out = readCustomTemplatesFromFrontmatter({
|
|
154
|
+
[FRONTMATTER_CUSTOM_TEMPLATES_KEY]: payload,
|
|
155
|
+
});
|
|
156
|
+
expect(out!.map((d) => d.name)).toEqual(['hero', 'second']);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('also accepts an already-decoded array shape (forward-compatible)', () => {
|
|
160
|
+
const out = readCustomTemplatesFromFrontmatter({
|
|
161
|
+
[FRONTMATTER_CUSTOM_TEMPLATES_KEY]: [
|
|
162
|
+
{
|
|
163
|
+
name: 'hero',
|
|
164
|
+
label: 'Hero',
|
|
165
|
+
viewport: { width: 1920, height: 1080 },
|
|
166
|
+
layers: sampleLayers,
|
|
167
|
+
},
|
|
168
|
+
],
|
|
169
|
+
});
|
|
170
|
+
expect(out).toHaveLength(1);
|
|
171
|
+
expect(out![0].name).toBe('hero');
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('returns undefined when the key is absent', () => {
|
|
175
|
+
expect(readCustomTemplatesFromFrontmatter({})).toBeUndefined();
|
|
176
|
+
expect(readCustomTemplatesFromFrontmatter(undefined)).toBeUndefined();
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("returns undefined when the encoded string isn't a valid array", () => {
|
|
180
|
+
const fm = { [FRONTMATTER_CUSTOM_TEMPLATES_KEY]: 'not-base64-of-an-array' };
|
|
181
|
+
expect(readCustomTemplatesFromFrontmatter(fm)).toBeUndefined();
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
describe('Doc.customTemplates round-trip via markdownToDoc + docToMarkdown', () => {
|
|
186
|
+
it('survives a markdownToDoc → docToMarkdown cycle', () => {
|
|
187
|
+
const encoded = writeCustomTemplatesToFrontmatter([heroDef])!;
|
|
188
|
+
// Compact JSON is stored unquoted — it round-trips verbatim through
|
|
189
|
+
// the line-based frontmatter parser.
|
|
190
|
+
const sourceMd = `---
|
|
191
|
+
title: My Doc
|
|
192
|
+
squisq-custom-templates: ${encoded}
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
# Welcome {[hero]}
|
|
196
|
+
|
|
197
|
+
A small editor for big ideas.
|
|
198
|
+
`;
|
|
199
|
+
const doc = markdownToDoc(parseMarkdown(sourceMd));
|
|
200
|
+
expect(doc.customTemplates).toBeDefined();
|
|
201
|
+
expect(doc.customTemplates).toHaveLength(1);
|
|
202
|
+
expect(doc.customTemplates![0].name).toBe('hero');
|
|
203
|
+
expect(doc.customTemplates![0].layers).toEqual(sampleLayers);
|
|
204
|
+
|
|
205
|
+
// Round-trip back — docToMarkdown must restore the frontmatter
|
|
206
|
+
// entry with an equivalent encoded payload.
|
|
207
|
+
const mdOut = docToMarkdown(doc);
|
|
208
|
+
expect(mdOut.frontmatter).toBeDefined();
|
|
209
|
+
const payload = mdOut.frontmatter![FRONTMATTER_CUSTOM_TEMPLATES_KEY];
|
|
210
|
+
expect(typeof payload).toBe('string');
|
|
211
|
+
const reread = readCustomTemplatesFromFrontmatter({
|
|
212
|
+
[FRONTMATTER_CUSTOM_TEMPLATES_KEY]: payload,
|
|
213
|
+
});
|
|
214
|
+
expect(reread).toEqual(doc.customTemplates);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('drops the frontmatter key when the doc has no custom templates anymore', () => {
|
|
218
|
+
void vi; // imported for parity; not used here
|
|
219
|
+
const doc = {
|
|
220
|
+
articleId: 'd',
|
|
221
|
+
duration: 0,
|
|
222
|
+
blocks: [],
|
|
223
|
+
audio: { segments: [] },
|
|
224
|
+
// Stale frontmatter from an earlier save when the doc DID have
|
|
225
|
+
// custom templates — but doc.customTemplates is now empty.
|
|
226
|
+
frontmatter: {
|
|
227
|
+
title: 'Doc',
|
|
228
|
+
[FRONTMATTER_CUSTOM_TEMPLATES_KEY]: 'stale-base64-blob',
|
|
229
|
+
},
|
|
230
|
+
};
|
|
231
|
+
const md = docToMarkdown(doc);
|
|
232
|
+
expect(md.frontmatter).toBeDefined();
|
|
233
|
+
expect(md.frontmatter).not.toHaveProperty(FRONTMATTER_CUSTOM_TEMPLATES_KEY);
|
|
234
|
+
expect(md.frontmatter!.title).toBe('Doc');
|
|
235
|
+
});
|
|
236
|
+
});
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom themes round-trip through frontmatter.
|
|
3
|
+
*
|
|
4
|
+
* The theme analog of customTemplatesFrontmatter.test.ts: verifies that a
|
|
5
|
+
* Theme stored in markdown frontmatter under `squisq-custom-themes` is
|
|
6
|
+
* faithfully reconstructed by `markdownToDoc` and serialized back by
|
|
7
|
+
* `docToMarkdown` without loss, and that the reader is tolerant of malformed
|
|
8
|
+
* entries.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, it, expect } from 'vitest';
|
|
12
|
+
import { markdownToDoc } from '../doc/markdownToDoc';
|
|
13
|
+
import { docToMarkdown } from '../doc/docToMarkdown';
|
|
14
|
+
import {
|
|
15
|
+
readCustomThemesFromFrontmatter,
|
|
16
|
+
writeCustomThemesToFrontmatter,
|
|
17
|
+
FRONTMATTER_CUSTOM_THEMES_KEY,
|
|
18
|
+
} from '../doc/customThemesFrontmatter';
|
|
19
|
+
import { parseMarkdown } from '../markdown/parse';
|
|
20
|
+
import { compileTheme } from '../schemas/themeCompile';
|
|
21
|
+
import type { Doc } from '../schemas/Doc.js';
|
|
22
|
+
import type { Theme } from '../schemas/Theme.js';
|
|
23
|
+
|
|
24
|
+
const brand: Theme = compileTheme({
|
|
25
|
+
id: 'my-brand',
|
|
26
|
+
name: 'My Brand',
|
|
27
|
+
seedColors: { primary: '#3182ce', secondary: '#805ad5' },
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe('writeCustomThemesToFrontmatter → readCustomThemesFromFrontmatter', () => {
|
|
31
|
+
it('round-trips a single theme through the compact JSON wire format', () => {
|
|
32
|
+
const payload = writeCustomThemesToFrontmatter([brand]);
|
|
33
|
+
expect(typeof payload).toBe('string');
|
|
34
|
+
expect(payload!.startsWith('{"my-brand":')).toBe(true);
|
|
35
|
+
const out = readCustomThemesFromFrontmatter({ [FRONTMATTER_CUSTOM_THEMES_KEY]: payload });
|
|
36
|
+
expect(out).toHaveLength(1);
|
|
37
|
+
expect(out![0]).toEqual(brand);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('default output is compact (single line) and byte-unchanged', () => {
|
|
41
|
+
const payload = writeCustomThemesToFrontmatter([brand])!;
|
|
42
|
+
expect(payload).not.toContain('\n');
|
|
43
|
+
expect(writeCustomThemesToFrontmatter([brand], { pretty: false })).toBe(payload);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('pretty:true emits multi-line JSON that still reads back', () => {
|
|
47
|
+
const payload = writeCustomThemesToFrontmatter([brand], { pretty: true })!;
|
|
48
|
+
expect(payload).toContain('\n');
|
|
49
|
+
const out = readCustomThemesFromFrontmatter({ [FRONTMATTER_CUSTOM_THEMES_KEY]: payload });
|
|
50
|
+
expect(out).toHaveLength(1);
|
|
51
|
+
expect(out![0]).toEqual(brand);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('a hand-written block-scalar frontmatter parses back into the theme', () => {
|
|
55
|
+
const payload = writeCustomThemesToFrontmatter([brand], { pretty: true })!;
|
|
56
|
+
const body = payload
|
|
57
|
+
.split('\n')
|
|
58
|
+
.map((l) => ` ${l}`)
|
|
59
|
+
.join('\n');
|
|
60
|
+
const md = `---\n${FRONTMATTER_CUSTOM_THEMES_KEY}: |-\n${body}\n---\n\n# Hi\n`;
|
|
61
|
+
const parsed = parseMarkdown(md);
|
|
62
|
+
const out = readCustomThemesFromFrontmatter(parsed.frontmatter);
|
|
63
|
+
expect(out).toHaveLength(1);
|
|
64
|
+
expect(out![0]).toEqual(brand);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('round-trips multiple themes keyed by id', () => {
|
|
68
|
+
const second = compileTheme({
|
|
69
|
+
id: 'second',
|
|
70
|
+
name: 'Second',
|
|
71
|
+
seedColors: { primary: '#e53e3e' },
|
|
72
|
+
});
|
|
73
|
+
const payload = writeCustomThemesToFrontmatter([brand, second]);
|
|
74
|
+
const out = readCustomThemesFromFrontmatter({ [FRONTMATTER_CUSTOM_THEMES_KEY]: payload });
|
|
75
|
+
expect(out!.map((t) => t.id).sort()).toEqual(['my-brand', 'second']);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('also accepts an already-decoded object map (forward-compatible)', () => {
|
|
79
|
+
const out = readCustomThemesFromFrontmatter({
|
|
80
|
+
[FRONTMATTER_CUSTOM_THEMES_KEY]: { 'my-brand': brand },
|
|
81
|
+
});
|
|
82
|
+
expect(out).toHaveLength(1);
|
|
83
|
+
expect(out![0].id).toBe('my-brand');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('drops invalid themes rather than failing the whole load', () => {
|
|
87
|
+
const payload = JSON.stringify({ 'my-brand': brand, junk: { id: 'junk' } });
|
|
88
|
+
const out = readCustomThemesFromFrontmatter({ [FRONTMATTER_CUSTOM_THEMES_KEY]: payload });
|
|
89
|
+
expect(out).toHaveLength(1);
|
|
90
|
+
expect(out![0].id).toBe('my-brand');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('returns undefined when the key is absent or unparseable', () => {
|
|
94
|
+
expect(readCustomThemesFromFrontmatter({})).toBeUndefined();
|
|
95
|
+
expect(readCustomThemesFromFrontmatter(undefined)).toBeUndefined();
|
|
96
|
+
expect(
|
|
97
|
+
readCustomThemesFromFrontmatter({ [FRONTMATTER_CUSTOM_THEMES_KEY]: 'not json' }),
|
|
98
|
+
).toBeUndefined();
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe('Doc.customThemes round-trip via markdownToDoc + docToMarkdown', () => {
|
|
103
|
+
it('survives a markdownToDoc → docToMarkdown cycle and keeps the selection', () => {
|
|
104
|
+
const encoded = writeCustomThemesToFrontmatter([brand])!;
|
|
105
|
+
// Compact JSON is stored unquoted — it round-trips verbatim through the
|
|
106
|
+
// line-based frontmatter parser, exactly like custom templates.
|
|
107
|
+
const sourceMd = `---
|
|
108
|
+
title: My Doc
|
|
109
|
+
squisq-theme: my-brand
|
|
110
|
+
squisq-custom-themes: ${encoded}
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
# Welcome
|
|
114
|
+
|
|
115
|
+
A small editor for big ideas.
|
|
116
|
+
`;
|
|
117
|
+
const doc = markdownToDoc(parseMarkdown(sourceMd));
|
|
118
|
+
expect(doc.customThemes).toHaveLength(1);
|
|
119
|
+
expect(doc.customThemes![0]).toEqual(brand);
|
|
120
|
+
|
|
121
|
+
const mdOut = docToMarkdown(doc);
|
|
122
|
+
const payload = mdOut.frontmatter![FRONTMATTER_CUSTOM_THEMES_KEY];
|
|
123
|
+
expect(typeof payload).toBe('string');
|
|
124
|
+
const reread = readCustomThemesFromFrontmatter({ [FRONTMATTER_CUSTOM_THEMES_KEY]: payload });
|
|
125
|
+
expect(reread).toEqual(doc.customThemes);
|
|
126
|
+
// The active selection is preserved untouched.
|
|
127
|
+
expect(mdOut.frontmatter!['squisq-theme']).toBe('my-brand');
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('drops the frontmatter key when the doc has no custom themes anymore', () => {
|
|
131
|
+
const doc: Doc = {
|
|
132
|
+
articleId: 'd',
|
|
133
|
+
duration: 0,
|
|
134
|
+
blocks: [],
|
|
135
|
+
audio: { segments: [] },
|
|
136
|
+
frontmatter: {
|
|
137
|
+
title: 'Doc',
|
|
138
|
+
[FRONTMATTER_CUSTOM_THEMES_KEY]: 'stale-blob',
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
const md = docToMarkdown(doc);
|
|
142
|
+
expect(md.frontmatter).not.toHaveProperty(FRONTMATTER_CUSTOM_THEMES_KEY);
|
|
143
|
+
expect(md.frontmatter!.title).toBe('Doc');
|
|
144
|
+
});
|
|
145
|
+
});
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { diagramBlock } from '../doc/templates/diagramBlock.js';
|
|
3
|
+
import { createTemplateContext } from '../schemas/BlockTemplates.js';
|
|
4
|
+
import { DEFAULT_THEME } from '../schemas/themeLibrary.js';
|
|
5
|
+
import { VIEWPORT_PRESETS } from '../schemas/Viewport.js';
|
|
6
|
+
import type { Block, Layer, ShapeLayer, PathLayer, TextLayer } from '../schemas/Doc.js';
|
|
7
|
+
import type { DiagramBlockInput } from '../schemas/BlockTemplates.js';
|
|
8
|
+
|
|
9
|
+
function makeBlock(overrides: Partial<Block> & { id: string }): Block {
|
|
10
|
+
return {
|
|
11
|
+
startTime: 0,
|
|
12
|
+
duration: 0,
|
|
13
|
+
audioSegment: 0,
|
|
14
|
+
...overrides,
|
|
15
|
+
} as Block;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function makeInput(overrides: Partial<DiagramBlockInput> = {}): DiagramBlockInput {
|
|
19
|
+
return {
|
|
20
|
+
template: 'diagram',
|
|
21
|
+
id: 'family-tree',
|
|
22
|
+
duration: 0,
|
|
23
|
+
audioSegment: 0,
|
|
24
|
+
...overrides,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe('diagramBlock template', () => {
|
|
29
|
+
it('emits node card + label layers for each child', () => {
|
|
30
|
+
const ctx = createTemplateContext(DEFAULT_THEME, 0, 1, VIEWPORT_PRESETS.landscape);
|
|
31
|
+
ctx.children = [
|
|
32
|
+
makeBlock({ id: 'a', title: 'Alpha', x: 100, y: 100 }),
|
|
33
|
+
makeBlock({ id: 'b', title: 'Beta', x: 400, y: 100 }),
|
|
34
|
+
];
|
|
35
|
+
const layers = diagramBlock(makeInput(), ctx);
|
|
36
|
+
const cards = layers.filter((l) => l.type === 'shape') as ShapeLayer[];
|
|
37
|
+
const texts = layers.filter((l) => l.type === 'text') as TextLayer[];
|
|
38
|
+
expect(cards.length).toBe(2);
|
|
39
|
+
expect(texts.length).toBe(2);
|
|
40
|
+
expect(texts[0].content.text).toBe('Alpha');
|
|
41
|
+
expect(texts[1].content.text).toBe('Beta');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('emits a path layer with arrow for each connectsTo entry', () => {
|
|
45
|
+
const ctx = createTemplateContext(DEFAULT_THEME, 0, 1, VIEWPORT_PRESETS.landscape);
|
|
46
|
+
ctx.children = [
|
|
47
|
+
makeBlock({ id: 'a', x: 100, y: 100, connectsTo: [{ target: 'b' }] }),
|
|
48
|
+
makeBlock({ id: 'b', x: 400, y: 100 }),
|
|
49
|
+
];
|
|
50
|
+
const layers = diagramBlock(makeInput(), ctx);
|
|
51
|
+
const paths = layers.filter((l) => l.type === 'path') as PathLayer[];
|
|
52
|
+
expect(paths.length).toBe(1);
|
|
53
|
+
expect(paths[0].content.endMarker).toBe('arrow');
|
|
54
|
+
expect(paths[0].content.d.startsWith('M ')).toBe(true);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('emits a typed edge label when connection has a type', () => {
|
|
58
|
+
const ctx = createTemplateContext(DEFAULT_THEME, 0, 1, VIEWPORT_PRESETS.landscape);
|
|
59
|
+
ctx.children = [
|
|
60
|
+
makeBlock({ id: 'a', x: 100, y: 100, connectsTo: [{ target: 'b', type: 'requires' }] }),
|
|
61
|
+
makeBlock({ id: 'b', x: 400, y: 100 }),
|
|
62
|
+
];
|
|
63
|
+
const layers = diagramBlock(makeInput(), ctx);
|
|
64
|
+
const labels = layers.filter(
|
|
65
|
+
(l): l is TextLayer => l.type === 'text' && l.id.startsWith('edge-label-'),
|
|
66
|
+
);
|
|
67
|
+
expect(labels.length).toBe(1);
|
|
68
|
+
expect(labels[0].content.text).toBe('requires');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('returns a single hint text layer when there are no children', () => {
|
|
72
|
+
const ctx = createTemplateContext(DEFAULT_THEME, 0, 1, VIEWPORT_PRESETS.landscape);
|
|
73
|
+
ctx.children = [];
|
|
74
|
+
const layers = diagramBlock(makeInput({ title: 'Empty' }), ctx);
|
|
75
|
+
expect(layers.length).toBe(1);
|
|
76
|
+
expect(layers[0].type).toBe('text');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('emits a title layer when input.title is set', () => {
|
|
80
|
+
const ctx = createTemplateContext(DEFAULT_THEME, 0, 1, VIEWPORT_PRESETS.landscape);
|
|
81
|
+
ctx.children = [makeBlock({ id: 'a', title: 'A', x: 100, y: 100 })];
|
|
82
|
+
const layers = diagramBlock(makeInput({ title: 'My Diagram' }), ctx);
|
|
83
|
+
const titleLayer = layers.find((l): l is TextLayer => l.id === 'diagram-title');
|
|
84
|
+
expect(titleLayer).toBeDefined();
|
|
85
|
+
expect(titleLayer?.content.text).toBe('My Diagram');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('handles children without explicit positions via auto-layout', () => {
|
|
89
|
+
const ctx = createTemplateContext(DEFAULT_THEME, 0, 1, VIEWPORT_PRESETS.landscape);
|
|
90
|
+
ctx.children = [
|
|
91
|
+
makeBlock({ id: 'a', title: 'A' }),
|
|
92
|
+
makeBlock({ id: 'b', title: 'B' }),
|
|
93
|
+
makeBlock({ id: 'c', title: 'C' }),
|
|
94
|
+
];
|
|
95
|
+
const layers = diagramBlock(makeInput(), ctx);
|
|
96
|
+
const cards = layers.filter((l) => l.type === 'shape');
|
|
97
|
+
expect(cards.length).toBe(3);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe('diagramBlock integration with getLayers', () => {
|
|
102
|
+
it('renders through the full template registry', async () => {
|
|
103
|
+
const { getLayers } = await import('../doc/getLayers.js');
|
|
104
|
+
const parentBlock: Block = {
|
|
105
|
+
id: 'family',
|
|
106
|
+
startTime: 0,
|
|
107
|
+
duration: 0,
|
|
108
|
+
audioSegment: 0,
|
|
109
|
+
template: 'diagram',
|
|
110
|
+
title: 'Family Tree',
|
|
111
|
+
children: [
|
|
112
|
+
makeBlock({ id: 'a', title: 'A', x: 100, y: 100 }),
|
|
113
|
+
makeBlock({ id: 'b', title: 'B', x: 400, y: 100, connectsTo: [{ target: 'a' }] }),
|
|
114
|
+
],
|
|
115
|
+
};
|
|
116
|
+
const layers = getLayers(parentBlock, {});
|
|
117
|
+
// Should contain at least one shape (node card) and one path (edge).
|
|
118
|
+
const types = new Set(layers.map((l: Layer) => l.type));
|
|
119
|
+
expect(types.has('shape')).toBe(true);
|
|
120
|
+
expect(types.has('path')).toBe(true);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { computeDiagramLayout } from '../doc/templates/diagramLayout.js';
|
|
3
|
+
import type { Block } from '../schemas/Doc.js';
|
|
4
|
+
|
|
5
|
+
function makeBlock(overrides: Partial<Block> & { id: string }): Block {
|
|
6
|
+
return {
|
|
7
|
+
startTime: 0,
|
|
8
|
+
duration: 0,
|
|
9
|
+
audioSegment: 0,
|
|
10
|
+
...overrides,
|
|
11
|
+
} as Block;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
describe('computeDiagramLayout', () => {
|
|
15
|
+
it('keeps explicit x/y positions intact', () => {
|
|
16
|
+
const layout = computeDiagramLayout([
|
|
17
|
+
makeBlock({ id: 'a', x: 100, y: 200 }),
|
|
18
|
+
makeBlock({ id: 'b', x: 400, y: 300 }),
|
|
19
|
+
]);
|
|
20
|
+
expect(layout.nodes).toHaveLength(2);
|
|
21
|
+
expect(layout.nodes[0]).toMatchObject({ id: 'a', x: 100, y: 200, pinned: true });
|
|
22
|
+
expect(layout.nodes[1]).toMatchObject({ id: 'b', x: 400, y: 300, pinned: true });
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('auto-positions children missing x or y in a grid below pinned nodes', () => {
|
|
26
|
+
const layout = computeDiagramLayout([
|
|
27
|
+
makeBlock({ id: 'pinned', x: 200, y: 100 }),
|
|
28
|
+
makeBlock({ id: 'a' }),
|
|
29
|
+
makeBlock({ id: 'b' }),
|
|
30
|
+
makeBlock({ id: 'c' }),
|
|
31
|
+
makeBlock({ id: 'd' }),
|
|
32
|
+
]);
|
|
33
|
+
expect(layout.nodes.find((n) => n.id === 'pinned')?.pinned).toBe(true);
|
|
34
|
+
const auto = layout.nodes.filter((n) => n.id !== 'pinned');
|
|
35
|
+
for (const node of auto) {
|
|
36
|
+
expect(node.pinned).toBe(false);
|
|
37
|
+
// Should sit below the pinned node's y.
|
|
38
|
+
expect(node.y).toBeGreaterThan(100);
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('places fully-unpositioned children starting at the default origin', () => {
|
|
43
|
+
const layout = computeDiagramLayout([
|
|
44
|
+
makeBlock({ id: 'a', title: 'A' }),
|
|
45
|
+
makeBlock({ id: 'b', title: 'B' }),
|
|
46
|
+
]);
|
|
47
|
+
expect(layout.nodes[0].pinned).toBe(false);
|
|
48
|
+
expect(layout.nodes[1].pinned).toBe(false);
|
|
49
|
+
expect(layout.nodes[0].x).toBeGreaterThanOrEqual(0);
|
|
50
|
+
expect(layout.nodes[0].y).toBeGreaterThanOrEqual(0);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('extracts edges from connectsTo, including connection types', () => {
|
|
54
|
+
const layout = computeDiagramLayout([
|
|
55
|
+
makeBlock({
|
|
56
|
+
id: 'a',
|
|
57
|
+
x: 0,
|
|
58
|
+
y: 0,
|
|
59
|
+
connectsTo: [{ target: 'b', type: 'flow' }, { target: 'c' }],
|
|
60
|
+
}),
|
|
61
|
+
makeBlock({ id: 'b', x: 100, y: 0 }),
|
|
62
|
+
makeBlock({ id: 'c', x: 200, y: 0 }),
|
|
63
|
+
]);
|
|
64
|
+
expect(layout.edges).toHaveLength(2);
|
|
65
|
+
expect(layout.edges[0]).toMatchObject({ source: 'a', target: 'b', type: 'flow' });
|
|
66
|
+
expect(layout.edges[1]).toMatchObject({ source: 'a', target: 'c' });
|
|
67
|
+
expect(layout.edges[1].type).toBeUndefined();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('drops edges with unknown targets and reports a warning', () => {
|
|
71
|
+
const layout = computeDiagramLayout([
|
|
72
|
+
makeBlock({
|
|
73
|
+
id: 'a',
|
|
74
|
+
x: 0,
|
|
75
|
+
y: 0,
|
|
76
|
+
connectsTo: [{ target: 'ghost' }, { target: 'b' }],
|
|
77
|
+
}),
|
|
78
|
+
makeBlock({ id: 'b', x: 100, y: 0 }),
|
|
79
|
+
]);
|
|
80
|
+
expect(layout.edges).toHaveLength(1);
|
|
81
|
+
expect(layout.edges[0].target).toBe('b');
|
|
82
|
+
expect(layout.warnings.length).toBe(1);
|
|
83
|
+
expect(layout.warnings[0]).toContain('ghost');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('dedupes edges with identical source/target/type', () => {
|
|
87
|
+
const layout = computeDiagramLayout([
|
|
88
|
+
makeBlock({
|
|
89
|
+
id: 'a',
|
|
90
|
+
x: 0,
|
|
91
|
+
y: 0,
|
|
92
|
+
connectsTo: [
|
|
93
|
+
{ target: 'b', type: 'flow' },
|
|
94
|
+
{ target: 'b', type: 'flow' },
|
|
95
|
+
],
|
|
96
|
+
}),
|
|
97
|
+
makeBlock({ id: 'b', x: 100, y: 0 }),
|
|
98
|
+
]);
|
|
99
|
+
expect(layout.edges).toHaveLength(1);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('distinguishes edges with different types', () => {
|
|
103
|
+
const layout = computeDiagramLayout([
|
|
104
|
+
makeBlock({
|
|
105
|
+
id: 'a',
|
|
106
|
+
x: 0,
|
|
107
|
+
y: 0,
|
|
108
|
+
connectsTo: [
|
|
109
|
+
{ target: 'b', type: 'flow' },
|
|
110
|
+
{ target: 'b', type: 'requires' },
|
|
111
|
+
],
|
|
112
|
+
}),
|
|
113
|
+
makeBlock({ id: 'b', x: 100, y: 0 }),
|
|
114
|
+
]);
|
|
115
|
+
expect(layout.edges).toHaveLength(2);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('returns an empty layout for no children', () => {
|
|
119
|
+
const layout = computeDiagramLayout([]);
|
|
120
|
+
expect(layout.nodes).toEqual([]);
|
|
121
|
+
expect(layout.edges).toEqual([]);
|
|
122
|
+
expect(layout.warnings).toEqual([]);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('falls back to the block id for the node label when title is missing', () => {
|
|
126
|
+
const layout = computeDiagramLayout([makeBlock({ id: 'lonely', x: 0, y: 0 })]);
|
|
127
|
+
expect(layout.nodes[0].label).toBe('lonely');
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('uses block.title for the node label when present', () => {
|
|
131
|
+
const layout = computeDiagramLayout([makeBlock({ id: 'a', x: 0, y: 0, title: 'Alpha' })]);
|
|
132
|
+
expect(layout.nodes[0].label).toBe('Alpha');
|
|
133
|
+
});
|
|
134
|
+
});
|