@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,109 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { parseMarkdown } from '../markdown/parse.js';
|
|
3
|
+
import { stringifyMarkdown } from '../markdown/stringify.js';
|
|
4
|
+
import { markdownToDoc, flattenBlocks, flattenRenderableBlocks } from '../doc/markdownToDoc.js';
|
|
5
|
+
import { validateMarkdownSource } from '../doc/validate.js';
|
|
6
|
+
import { isContainerTemplate } from '../doc/templates/index.js';
|
|
7
|
+
|
|
8
|
+
const ORG_CHART = `## Org chart {[drawing]}
|
|
9
|
+
|
|
10
|
+
### CEO {#ceo} {[rectangle x=21 y=25 width=100 height=100]}
|
|
11
|
+
|
|
12
|
+
The CEO is the chief executive.
|
|
13
|
+
|
|
14
|
+
### reports to {[line from=ceo to=dev1]}
|
|
15
|
+
|
|
16
|
+
### Developer {#dev1} {[rectangle x=21 y=190 width=100 height=100]}
|
|
17
|
+
|
|
18
|
+
The lead developer.
|
|
19
|
+
`;
|
|
20
|
+
|
|
21
|
+
describe('drawing markdown → doc', () => {
|
|
22
|
+
it('turns child headings into shape blocks with geometry overrides', () => {
|
|
23
|
+
const doc = markdownToDoc(parseMarkdown(ORG_CHART));
|
|
24
|
+
const drawing = doc.blocks[0];
|
|
25
|
+
expect(drawing.template).toBe('drawing');
|
|
26
|
+
expect(drawing.children).toHaveLength(3);
|
|
27
|
+
|
|
28
|
+
const ceo = drawing.children!.find((c) => c.id === 'ceo')!;
|
|
29
|
+
expect(ceo.template).toBe('rectangle');
|
|
30
|
+
expect(ceo.title).toBe('CEO');
|
|
31
|
+
expect(ceo.templateOverrides).toEqual({
|
|
32
|
+
x: '21',
|
|
33
|
+
y: '25',
|
|
34
|
+
width: '100',
|
|
35
|
+
height: '100',
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const line = drawing.children!.find((c) => c.id === 'reports-to')!;
|
|
39
|
+
expect(line.template).toBe('line');
|
|
40
|
+
expect(line.templateOverrides).toEqual({ from: 'ceo', to: 'dev1' });
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('round-trips losslessly (shape annotations + ids survive)', () => {
|
|
44
|
+
const once = stringifyMarkdown(parseMarkdown(ORG_CHART));
|
|
45
|
+
// Canonical form is stable: a second pass is identical to the first.
|
|
46
|
+
const twice = stringifyMarkdown(parseMarkdown(once));
|
|
47
|
+
expect(twice).toBe(once);
|
|
48
|
+
expect(once).toContain('{#ceo} {[rectangle x=21 y=25 width=100 height=100]}');
|
|
49
|
+
expect(once).toContain('{[line from=ceo to=dev1]}');
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe('drawing validation', () => {
|
|
54
|
+
it('reports no diagnostics for a well-formed drawing', () => {
|
|
55
|
+
const { diagnostics, errorCount } = validateMarkdownSource(ORG_CHART);
|
|
56
|
+
expect(errorCount).toBe(0);
|
|
57
|
+
expect(diagnostics).toEqual([]);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('flags an unknown shape with a did-you-mean suggestion', () => {
|
|
61
|
+
const md = `## D {[drawing]}\n\n### A {[rectanlge x=0 y=0]}\n`;
|
|
62
|
+
const { diagnostics } = validateMarkdownSource(md);
|
|
63
|
+
const d = diagnostics.find((x) => x.code === 'unknown-shape');
|
|
64
|
+
expect(d).toBeDefined();
|
|
65
|
+
expect(d!.message).toContain('rectangle');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('flags a shape annotation used outside a drawing', () => {
|
|
69
|
+
const md = `## Not a drawing\n\n### A {[rectangle x=0 y=0]}\n`;
|
|
70
|
+
const { diagnostics } = validateMarkdownSource(md);
|
|
71
|
+
expect(diagnostics.some((x) => x.code === 'shape-outside-drawing')).toBe(true);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('flags a connector whose from/to does not resolve', () => {
|
|
75
|
+
const md = `## D {[drawing]}\n\n### A {#a} {[rect x=0 y=0]}\n\n### L {[line from=a to=ghost]}\n`;
|
|
76
|
+
const { diagnostics } = validateMarkdownSource(md);
|
|
77
|
+
const d = diagnostics.find((x) => x.code === 'unresolved-connection');
|
|
78
|
+
expect(d).toBeDefined();
|
|
79
|
+
expect(d!.message).toContain('ghost');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('flags non-numeric shape geometry', () => {
|
|
83
|
+
const md = `## D {[drawing]}\n\n### A {[rectangle x=abc y=0]}\n`;
|
|
84
|
+
const { diagnostics } = validateMarkdownSource(md);
|
|
85
|
+
expect(diagnostics.some((x) => x.code === 'invalid-attribute' && x.message.includes('x'))).toBe(
|
|
86
|
+
true,
|
|
87
|
+
);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe('container-template suppression', () => {
|
|
92
|
+
it('isContainerTemplate covers diagram and drawing only', () => {
|
|
93
|
+
expect(isContainerTemplate('drawing')).toBe(true);
|
|
94
|
+
expect(isContainerTemplate('diagram')).toBe(true);
|
|
95
|
+
expect(isContainerTemplate('sectionHeader')).toBe(false);
|
|
96
|
+
expect(isContainerTemplate(undefined)).toBe(false);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('flattenRenderableBlocks skips a drawing’s child shapes', () => {
|
|
100
|
+
const doc = markdownToDoc(parseMarkdown(ORG_CHART));
|
|
101
|
+
const all = flattenBlocks(doc.blocks);
|
|
102
|
+
const renderable = flattenRenderableBlocks(doc.blocks);
|
|
103
|
+
// Full flatten includes the drawing + its 3 shapes; renderable has only
|
|
104
|
+
// the drawing itself (its shapes are consumed by the drawing's render).
|
|
105
|
+
expect(all.length).toBe(4);
|
|
106
|
+
expect(renderable.length).toBe(1);
|
|
107
|
+
expect(renderable[0].template).toBe('drawing');
|
|
108
|
+
});
|
|
109
|
+
});
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { drawingBlock } from '../doc/templates/drawingBlock.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 { DrawingBlockInput } from '../schemas/BlockTemplates.js';
|
|
8
|
+
|
|
9
|
+
function shape(
|
|
10
|
+
id: string,
|
|
11
|
+
template: string,
|
|
12
|
+
overrides: Record<string, string> = {},
|
|
13
|
+
extra: Partial<Block> = {},
|
|
14
|
+
): Block {
|
|
15
|
+
return {
|
|
16
|
+
id,
|
|
17
|
+
startTime: 0,
|
|
18
|
+
duration: 0,
|
|
19
|
+
audioSegment: 0,
|
|
20
|
+
template,
|
|
21
|
+
templateOverrides: overrides,
|
|
22
|
+
...extra,
|
|
23
|
+
} as Block;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function makeInput(overrides: Partial<DrawingBlockInput> = {}): DrawingBlockInput {
|
|
27
|
+
return { template: 'drawing', id: 'sketch', duration: 0, audioSegment: 0, ...overrides };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function ctxWith(children: Block[]) {
|
|
31
|
+
const ctx = createTemplateContext(DEFAULT_THEME, 0, 1, VIEWPORT_PRESETS.landscape);
|
|
32
|
+
ctx.children = children;
|
|
33
|
+
return ctx;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
describe('drawingBlock template', () => {
|
|
37
|
+
it('emits a shape + label layer for each rect/circle child', () => {
|
|
38
|
+
const layers = drawingBlock(
|
|
39
|
+
makeInput(),
|
|
40
|
+
ctxWith([
|
|
41
|
+
shape(
|
|
42
|
+
'ceo',
|
|
43
|
+
'rectangle',
|
|
44
|
+
{ x: '20', y: '20', width: '120', height: '80' },
|
|
45
|
+
{ title: 'CEO' },
|
|
46
|
+
),
|
|
47
|
+
shape(
|
|
48
|
+
'dev',
|
|
49
|
+
'circle',
|
|
50
|
+
{ x: '20', y: '200', width: '100', height: '100' },
|
|
51
|
+
{ title: 'Dev' },
|
|
52
|
+
),
|
|
53
|
+
]),
|
|
54
|
+
);
|
|
55
|
+
const shapes = layers.filter((l): l is ShapeLayer => l.type === 'shape');
|
|
56
|
+
const texts = layers.filter((l): l is TextLayer => l.type === 'text');
|
|
57
|
+
expect(shapes.map((s) => s.content.shape).sort()).toEqual(['circle', 'rect']);
|
|
58
|
+
expect(texts.map((t) => t.content.text).sort()).toEqual(['CEO', 'Dev']);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('renders a from/to line as a path connector behind the shapes', () => {
|
|
62
|
+
const layers = drawingBlock(
|
|
63
|
+
makeInput(),
|
|
64
|
+
ctxWith([
|
|
65
|
+
shape('a', 'rect', { x: '0', y: '0', width: '80', height: '60' }, { title: 'A' }),
|
|
66
|
+
shape('b', 'rect', { x: '0', y: '200', width: '80', height: '60' }, { title: 'B' }),
|
|
67
|
+
shape('l', 'arrow', { from: 'a', to: 'b' }),
|
|
68
|
+
]),
|
|
69
|
+
);
|
|
70
|
+
const paths = layers.filter((l): l is PathLayer => l.type === 'path');
|
|
71
|
+
expect(paths).toHaveLength(1);
|
|
72
|
+
expect(paths[0].content.endMarker).toBe('arrow');
|
|
73
|
+
expect(paths[0].content.d.startsWith('M ')).toBe(true);
|
|
74
|
+
// Connector emitted before the shapes (so it sits behind them).
|
|
75
|
+
expect(layers.indexOf(paths[0])).toBeLessThan(layers.findIndex((l) => l.type === 'shape'));
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('emits a title layer when input.title is set', () => {
|
|
79
|
+
const layers = drawingBlock(
|
|
80
|
+
makeInput({ title: 'Org chart' }),
|
|
81
|
+
ctxWith([shape('a', 'rect', { x: '0', y: '0', width: '80', height: '60' })]),
|
|
82
|
+
);
|
|
83
|
+
expect(layers.find((l) => l.id === 'drawing-title')).toBeDefined();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('returns a single hint layer for an empty drawing', () => {
|
|
87
|
+
const layers = drawingBlock(makeInput({ title: 'Nothing yet' }), ctxWith([]));
|
|
88
|
+
expect(layers).toHaveLength(1);
|
|
89
|
+
expect(layers[0].type).toBe('text');
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe('drawingBlock integration with getLayers', () => {
|
|
94
|
+
it('renders a drawing block through the template registry', async () => {
|
|
95
|
+
const { getLayers } = await import('../doc/getLayers.js');
|
|
96
|
+
const parent: Block = {
|
|
97
|
+
id: 'sketch',
|
|
98
|
+
startTime: 0,
|
|
99
|
+
duration: 0,
|
|
100
|
+
audioSegment: 0,
|
|
101
|
+
template: 'drawing',
|
|
102
|
+
title: 'Org chart',
|
|
103
|
+
children: [
|
|
104
|
+
shape(
|
|
105
|
+
'ceo',
|
|
106
|
+
'rectangle',
|
|
107
|
+
{ x: '20', y: '20', width: '120', height: '80' },
|
|
108
|
+
{ title: 'CEO' },
|
|
109
|
+
),
|
|
110
|
+
shape(
|
|
111
|
+
'dev',
|
|
112
|
+
'rectangle',
|
|
113
|
+
{ x: '20', y: '220', width: '120', height: '80' },
|
|
114
|
+
{ title: 'Dev' },
|
|
115
|
+
),
|
|
116
|
+
shape('l', 'arrow', { from: 'ceo', to: 'dev' }, { title: 'manages' }),
|
|
117
|
+
],
|
|
118
|
+
};
|
|
119
|
+
const layers = getLayers(parent, {});
|
|
120
|
+
const types = new Set(layers.map((l: Layer) => l.type));
|
|
121
|
+
expect(types.has('shape')).toBe(true);
|
|
122
|
+
expect(types.has('path')).toBe(true);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
computeDrawingLayout,
|
|
4
|
+
normalizeShapeKind,
|
|
5
|
+
isShapeName,
|
|
6
|
+
} from '../doc/templates/drawingLayout.js';
|
|
7
|
+
import type { Block } from '../schemas/Doc.js';
|
|
8
|
+
|
|
9
|
+
/** Build a drawing child: `template` is the shape, geometry rides in overrides. */
|
|
10
|
+
function shape(
|
|
11
|
+
id: string,
|
|
12
|
+
template: string,
|
|
13
|
+
overrides: Record<string, string> = {},
|
|
14
|
+
extra: Partial<Block> = {},
|
|
15
|
+
): Block {
|
|
16
|
+
return {
|
|
17
|
+
id,
|
|
18
|
+
startTime: 0,
|
|
19
|
+
duration: 0,
|
|
20
|
+
audioSegment: 0,
|
|
21
|
+
template,
|
|
22
|
+
templateOverrides: overrides,
|
|
23
|
+
...extra,
|
|
24
|
+
} as Block;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe('normalizeShapeKind', () => {
|
|
28
|
+
it('maps aliases to canonical kinds', () => {
|
|
29
|
+
expect(normalizeShapeKind('rectangle')).toBe('rect');
|
|
30
|
+
expect(normalizeShapeKind('rect')).toBe('rect');
|
|
31
|
+
expect(normalizeShapeKind('ellipse')).toBe('circle');
|
|
32
|
+
expect(normalizeShapeKind('CIRCLE')).toBe('circle');
|
|
33
|
+
expect(normalizeShapeKind('line')).toBe('line');
|
|
34
|
+
expect(normalizeShapeKind('arrow')).toBe('arrow');
|
|
35
|
+
expect(normalizeShapeKind('path')).toBe('path');
|
|
36
|
+
expect(normalizeShapeKind('text')).toBe('text');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('returns null for non-shapes', () => {
|
|
40
|
+
expect(normalizeShapeKind('sectionHeader')).toBeNull();
|
|
41
|
+
expect(normalizeShapeKind(undefined)).toBeNull();
|
|
42
|
+
expect(isShapeName('rectangle')).toBe(true);
|
|
43
|
+
expect(isShapeName('banana')).toBe(false);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe('computeDrawingLayout', () => {
|
|
48
|
+
it('positions a rectangle from its geometry params', () => {
|
|
49
|
+
const layout = computeDrawingLayout([
|
|
50
|
+
shape('ceo', 'rectangle', { x: '21', y: '25', width: '100', height: '80' }, { title: 'CEO' }),
|
|
51
|
+
]);
|
|
52
|
+
expect(layout.shapes).toHaveLength(1);
|
|
53
|
+
expect(layout.shapes[0]).toMatchObject({
|
|
54
|
+
id: 'ceo',
|
|
55
|
+
kind: 'rect',
|
|
56
|
+
x: 21,
|
|
57
|
+
y: 25,
|
|
58
|
+
width: 100,
|
|
59
|
+
height: 80,
|
|
60
|
+
pinned: true,
|
|
61
|
+
label: 'CEO',
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('tolerates trailing commas in geometry (matches the sketch syntax)', () => {
|
|
66
|
+
const layout = computeDrawingLayout([
|
|
67
|
+
shape('a', 'rect', { x: '21,', y: '25,', width: '100,', height: '80' }),
|
|
68
|
+
]);
|
|
69
|
+
expect(layout.shapes[0]).toMatchObject({ x: 21, y: 25, width: 100, height: 80 });
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('applies default size when width/height are omitted', () => {
|
|
73
|
+
const layout = computeDrawingLayout([shape('a', 'circle', { x: '0', y: '0' })]);
|
|
74
|
+
expect(layout.shapes[0].kind).toBe('circle');
|
|
75
|
+
expect(layout.shapes[0].width).toBeGreaterThan(0);
|
|
76
|
+
expect(layout.shapes[0].height).toBeGreaterThan(0);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('auto-positions shapes missing a coordinate below pinned ones', () => {
|
|
80
|
+
const layout = computeDrawingLayout([
|
|
81
|
+
shape('pinned', 'rect', { x: '200', y: '100', width: '100', height: '60' }),
|
|
82
|
+
shape('floating', 'rect'),
|
|
83
|
+
]);
|
|
84
|
+
const pinned = layout.shapes.find((s) => s.id === 'pinned')!;
|
|
85
|
+
const floating = layout.shapes.find((s) => s.id === 'floating')!;
|
|
86
|
+
expect(pinned.pinned).toBe(true);
|
|
87
|
+
expect(floating.pinned).toBe(false);
|
|
88
|
+
expect(floating.y).toBeGreaterThan(pinned.y);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('turns a line with from/to into a resolved connector', () => {
|
|
92
|
+
const layout = computeDrawingLayout([
|
|
93
|
+
shape('ceo', 'rectangle', { x: '0', y: '0', width: '100', height: '60' }),
|
|
94
|
+
shape('dev', 'rectangle', { x: '0', y: '200', width: '100', height: '60' }),
|
|
95
|
+
shape('l1', 'line', { from: 'ceo', to: 'dev' }, { title: 'reports to' }),
|
|
96
|
+
]);
|
|
97
|
+
expect(layout.shapes).toHaveLength(2); // line is a connector, not a shape
|
|
98
|
+
expect(layout.connectors).toHaveLength(1);
|
|
99
|
+
expect(layout.connectors[0]).toMatchObject({
|
|
100
|
+
id: 'l1',
|
|
101
|
+
kind: 'line',
|
|
102
|
+
from: 'ceo',
|
|
103
|
+
to: 'dev',
|
|
104
|
+
label: 'reports to',
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('emits an arrow connector with an arrowhead for arrow shapes', () => {
|
|
109
|
+
const layout = computeDrawingLayout([
|
|
110
|
+
shape('a', 'rect', { x: '0', y: '0', width: '50', height: '50' }),
|
|
111
|
+
shape('b', 'rect', { x: '200', y: '0', width: '50', height: '50' }),
|
|
112
|
+
shape('arr', 'arrow', { from: 'a', to: 'b' }),
|
|
113
|
+
]);
|
|
114
|
+
expect(layout.connectors[0].kind).toBe('arrow');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('drops a connector whose endpoint matches no shape, with a warning', () => {
|
|
118
|
+
const layout = computeDrawingLayout([
|
|
119
|
+
shape('a', 'rect', { x: '0', y: '0', width: '50', height: '50' }),
|
|
120
|
+
shape('l', 'line', { from: 'a', to: 'ghost' }),
|
|
121
|
+
]);
|
|
122
|
+
expect(layout.connectors).toHaveLength(0);
|
|
123
|
+
expect(layout.warnings.some((w) => w.includes('ghost'))).toBe(true);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('also derives arrow connectors from a shape connectsTo', () => {
|
|
127
|
+
const layout = computeDrawingLayout([
|
|
128
|
+
shape(
|
|
129
|
+
'a',
|
|
130
|
+
'rect',
|
|
131
|
+
{ x: '0', y: '0', width: '50', height: '50' },
|
|
132
|
+
{
|
|
133
|
+
connectsTo: [{ target: 'b' }],
|
|
134
|
+
},
|
|
135
|
+
),
|
|
136
|
+
shape('b', 'rect', { x: '200', y: '0', width: '50', height: '50' }),
|
|
137
|
+
]);
|
|
138
|
+
expect(layout.connectors).toHaveLength(1);
|
|
139
|
+
expect(layout.connectors[0]).toMatchObject({ from: 'a', to: 'b', kind: 'arrow' });
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('skips non-shape children and warns', () => {
|
|
143
|
+
const layout = computeDrawingLayout([
|
|
144
|
+
shape('a', 'rect', { x: '0', y: '0', width: '50', height: '50' }),
|
|
145
|
+
shape('note', 'sectionHeader'),
|
|
146
|
+
]);
|
|
147
|
+
expect(layout.shapes).toHaveLength(1);
|
|
148
|
+
expect(layout.warnings.some((w) => w.includes('note'))).toBe(true);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('reads text content for text shapes (text= param, else title)', () => {
|
|
152
|
+
const fromParam = computeDrawingLayout([shape('t', 'text', { x: '0', y: '0', text: 'Hello' })]);
|
|
153
|
+
expect(fromParam.shapes[0].text).toBe('Hello');
|
|
154
|
+
const fromTitle = computeDrawingLayout([
|
|
155
|
+
shape('t', 'text', { x: '0', y: '0' }, { title: 'Heading text' }),
|
|
156
|
+
]);
|
|
157
|
+
expect(fromTitle.shapes[0].text).toBe('Heading text');
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('carries the raw d for path shapes', () => {
|
|
161
|
+
const layout = computeDrawingLayout([
|
|
162
|
+
shape('p', 'path', { x: '0', y: '0', d: 'M 0 0 L 10 10' }),
|
|
163
|
+
]);
|
|
164
|
+
expect(layout.shapes[0].d).toBe('M 0 0 L 10 10');
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('returns an empty layout for no children', () => {
|
|
168
|
+
const layout = computeDrawingLayout([]);
|
|
169
|
+
expect(layout).toEqual({ shapes: [], connectors: [], warnings: [] });
|
|
170
|
+
});
|
|
171
|
+
});
|
|
@@ -51,9 +51,9 @@ describe('getGeohash4Neighbors', () => {
|
|
|
51
51
|
expect(result[0]).toBe('c23n');
|
|
52
52
|
});
|
|
53
53
|
|
|
54
|
-
it('
|
|
55
|
-
expect(
|
|
56
|
-
expect(
|
|
54
|
+
it('returns empty array for non-4-char input', () => {
|
|
55
|
+
expect(getGeohash4Neighbors('c2')).toEqual([]);
|
|
56
|
+
expect(getGeohash4Neighbors('c23nn')).toEqual([]);
|
|
57
57
|
});
|
|
58
58
|
});
|
|
59
59
|
|
|
@@ -70,8 +70,9 @@ describe('geohashToHierarchicalPath', () => {
|
|
|
70
70
|
expect(geohashToHierarchicalPath('9q8y')).toBe('9/q/8/y');
|
|
71
71
|
});
|
|
72
72
|
|
|
73
|
-
it('
|
|
74
|
-
expect(
|
|
73
|
+
it('returns empty string for non-4-char input', () => {
|
|
74
|
+
expect(geohashToHierarchicalPath('c2')).toBe('');
|
|
75
|
+
expect(geohashToHierarchicalPath('c23nn')).toBe('');
|
|
75
76
|
});
|
|
76
77
|
});
|
|
77
78
|
|
|
@@ -108,7 +108,7 @@ describe('getLayers', () => {
|
|
|
108
108
|
}
|
|
109
109
|
});
|
|
110
110
|
|
|
111
|
-
it('
|
|
111
|
+
it('renders a visible fallback card for unknown templates (never blank)', () => {
|
|
112
112
|
const block = {
|
|
113
113
|
template: 'totally_nonexistent',
|
|
114
114
|
id: 'unknown-1',
|
|
@@ -117,7 +117,11 @@ describe('getLayers', () => {
|
|
|
117
117
|
} as unknown as TemplateBlock;
|
|
118
118
|
const layers = getLayers(block, defaultContext);
|
|
119
119
|
|
|
120
|
-
expect(layers).
|
|
120
|
+
expect(layers.length).toBeGreaterThan(0);
|
|
121
|
+
const noticeText = layers
|
|
122
|
+
.map((l) => (l.type === 'text' ? ((l.content as { text?: string }).text ?? '') : ''))
|
|
123
|
+
.join('\n');
|
|
124
|
+
expect(noticeText).toContain('Unknown template "totally_nonexistent"');
|
|
121
125
|
});
|
|
122
126
|
|
|
123
127
|
it('returns empty array for block with no template and no layers', () => {
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { describe, it, expect, afterEach } from 'vitest';
|
|
2
|
+
import { parseMarkdown } from '../markdown/parse.js';
|
|
3
|
+
import { markdownToDoc } from '../doc/markdownToDoc.js';
|
|
4
|
+
import { getLayers } from '../doc/getLayers.js';
|
|
5
|
+
import { templateRegistry } from '../doc/templates/index.js';
|
|
6
|
+
import type { Block, Layer } from '../schemas/Doc.js';
|
|
7
|
+
|
|
8
|
+
function textOf(layers: Layer[]): string {
|
|
9
|
+
return layers
|
|
10
|
+
.map((l) => (l.type === 'text' ? ((l.content as { text?: string }).text ?? '') : ''))
|
|
11
|
+
.join('\n');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function firstBlock(md: string): Block {
|
|
15
|
+
return markdownToDoc(parseMarkdown(md)).blocks[0];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe('getLayers — graceful degradation', () => {
|
|
19
|
+
it('renders an unknown template as a plain card with title, body, and notice', () => {
|
|
20
|
+
const block = firstBlock('## My Section {[photGrid]}\n\nSome body prose here.');
|
|
21
|
+
const layers = getLayers(block);
|
|
22
|
+
expect(layers.length).toBeGreaterThan(0);
|
|
23
|
+
const text = textOf(layers);
|
|
24
|
+
expect(text).toContain('My Section');
|
|
25
|
+
expect(text).toContain('Some body prose here.');
|
|
26
|
+
expect(text).toContain('Unknown template "photGrid"');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('renders a fallback card when a template function throws', () => {
|
|
30
|
+
(templateRegistry as Record<string, unknown>)['__boom'] = () => {
|
|
31
|
+
throw new Error('kaboom');
|
|
32
|
+
};
|
|
33
|
+
try {
|
|
34
|
+
const block: Block = {
|
|
35
|
+
id: 'b1',
|
|
36
|
+
startTime: 0,
|
|
37
|
+
duration: 5,
|
|
38
|
+
audioSegment: 0,
|
|
39
|
+
template: '__boom',
|
|
40
|
+
title: 'Exploding block',
|
|
41
|
+
};
|
|
42
|
+
const layers = getLayers(block);
|
|
43
|
+
expect(layers.length).toBeGreaterThan(0);
|
|
44
|
+
expect(textOf(layers)).toContain('Template "__boom" failed');
|
|
45
|
+
expect(textOf(layers)).toContain('Exploding block');
|
|
46
|
+
} finally {
|
|
47
|
+
delete (templateRegistry as Record<string, unknown>)['__boom'];
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('blocks with no template at all still return empty layers', () => {
|
|
52
|
+
const block: Block = { id: 'raw', startTime: 0, duration: 5, audioSegment: 0 };
|
|
53
|
+
expect(getLayers(block)).toEqual([]);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe('getLayers — structured data feeds templates', () => {
|
|
58
|
+
afterEach(() => {
|
|
59
|
+
delete (templateRegistry as Record<string, unknown>)['__capture'];
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('merges templateData and templateOverrides into the template input', () => {
|
|
63
|
+
let received: Record<string, unknown> | null = null;
|
|
64
|
+
(templateRegistry as Record<string, unknown>)['__capture'] = (
|
|
65
|
+
input: Record<string, unknown>,
|
|
66
|
+
) => {
|
|
67
|
+
received = input;
|
|
68
|
+
return [];
|
|
69
|
+
};
|
|
70
|
+
const block: Block = {
|
|
71
|
+
id: 'b1',
|
|
72
|
+
startTime: 0,
|
|
73
|
+
duration: 5,
|
|
74
|
+
audioSegment: 0,
|
|
75
|
+
template: '__capture',
|
|
76
|
+
templateData: { rows: [['a', 'b']], zoom: 12 },
|
|
77
|
+
templateOverrides: { zoom: '14' },
|
|
78
|
+
};
|
|
79
|
+
getLayers(block);
|
|
80
|
+
expect(received).not.toBeNull();
|
|
81
|
+
expect(received!['rows']).toEqual([['a', 'b']]);
|
|
82
|
+
// String overrides from {[…]} params win last.
|
|
83
|
+
expect(received!['zoom']).toBe('14');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('renders a dataTable from a GFM table in the markdown body', () => {
|
|
87
|
+
const md = [
|
|
88
|
+
'## People {[dataTable]}',
|
|
89
|
+
'',
|
|
90
|
+
'| Name | Age |',
|
|
91
|
+
'| --- | --- |',
|
|
92
|
+
'| Alice | 30 |',
|
|
93
|
+
].join('\n');
|
|
94
|
+
const block = firstBlock(md);
|
|
95
|
+
const layers = getLayers(block);
|
|
96
|
+
expect(layers.length).toBeGreaterThan(0);
|
|
97
|
+
const tableLayer = layers.find((l) => l.type === 'table');
|
|
98
|
+
expect(tableLayer).toBeDefined();
|
|
99
|
+
const content = tableLayer!.content as { headers?: string[]; rows?: string[][] };
|
|
100
|
+
expect(content.headers).toEqual(['Name', 'Age']);
|
|
101
|
+
expect(content.rows).toEqual([['Alice', '30']]);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { buildSvgString } from '../imageEdit/export';
|
|
3
|
+
import { createEmptyImageEditDoc, addLayer } from '../imageEdit/state';
|
|
4
|
+
import { MemoryContentContainer } from '../storage/ContentContainer';
|
|
5
|
+
import type { ImageEditLayer } from '../schemas/ImageEditDoc';
|
|
6
|
+
|
|
7
|
+
const container = new MemoryContentContainer();
|
|
8
|
+
|
|
9
|
+
describe('buildSvgString — path (drawing shape) layers', () => {
|
|
10
|
+
it('renders a named shape (shapeKind) as a <path>, deriving d from the box', async () => {
|
|
11
|
+
let doc = createEmptyImageEditDoc(200, 200);
|
|
12
|
+
const diamond: ImageEditLayer = {
|
|
13
|
+
id: 'p1',
|
|
14
|
+
type: 'path',
|
|
15
|
+
position: { x: 10, y: 20, width: 100, height: 80 },
|
|
16
|
+
content: {
|
|
17
|
+
d: 'M 0 0',
|
|
18
|
+
shapeKind: 'diamond',
|
|
19
|
+
fill: '#3399ff',
|
|
20
|
+
stroke: '#1a4d80',
|
|
21
|
+
strokeWidth: 2,
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
doc = addLayer(doc, diamond);
|
|
25
|
+
|
|
26
|
+
const svg = await buildSvgString(doc, container);
|
|
27
|
+
expect(svg).toContain('<path');
|
|
28
|
+
expect(svg).toContain('fill="#3399ff"');
|
|
29
|
+
expect(svg).toContain('stroke="#1a4d80"');
|
|
30
|
+
// A diamond inscribed in [10,20,100,80] starts at the top-center (60,20).
|
|
31
|
+
expect(svg).toContain('d="M 60 20');
|
|
32
|
+
// The seed `d="M 0 0"` must NOT survive — geometry is re-derived.
|
|
33
|
+
expect(svg).not.toContain('d="M 0 0"');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('renders an arrow path with an end marker def', async () => {
|
|
37
|
+
let doc = createEmptyImageEditDoc(200, 200);
|
|
38
|
+
const arrow: ImageEditLayer = {
|
|
39
|
+
id: 'a1',
|
|
40
|
+
type: 'path',
|
|
41
|
+
position: { x: 0, y: 0, width: 100, height: 100 },
|
|
42
|
+
content: {
|
|
43
|
+
d: 'M 0 0 L 100 100',
|
|
44
|
+
stroke: '#000000',
|
|
45
|
+
strokeWidth: 2,
|
|
46
|
+
fill: 'none',
|
|
47
|
+
endMarker: 'arrow',
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
doc = addLayer(doc, arrow);
|
|
51
|
+
|
|
52
|
+
const svg = await buildSvgString(doc, container);
|
|
53
|
+
expect(svg).toContain('<marker id="marker-end-a1"');
|
|
54
|
+
expect(svg).toContain('marker-end="url(#marker-end-a1)"');
|
|
55
|
+
expect(svg).toContain('fill="none"');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('still renders native rect/circle/line shape layers', async () => {
|
|
59
|
+
let doc = createEmptyImageEditDoc(100, 100);
|
|
60
|
+
doc = addLayer(doc, {
|
|
61
|
+
id: 's1',
|
|
62
|
+
type: 'shape',
|
|
63
|
+
position: { x: 5, y: 5, width: 40, height: 30 },
|
|
64
|
+
content: { shape: 'rect', fill: '#fff', borderRadius: 4 },
|
|
65
|
+
});
|
|
66
|
+
const svg = await buildSvgString(doc, container);
|
|
67
|
+
expect(svg).toContain('<rect x="5" y="5" width="40" height="30"');
|
|
68
|
+
expect(svg).toContain('rx="4"');
|
|
69
|
+
});
|
|
70
|
+
});
|