@bendyline/squisq 1.4.1 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{Doc-BeSeH2iX.d.ts → Doc-BsMkmzH8.d.ts} +838 -268
- package/dist/{ImageEditDoc-BTIvw0oq.d.ts → ImageEditDoc-DvoeHMP9.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-B4EEON3N.js → chunk-4AKUQPEE.js} +536 -103
- package/dist/chunk-4AKUQPEE.js.map +1 -0
- package/dist/chunk-4ZFRZBU2.js +6582 -0
- package/dist/chunk-4ZFRZBU2.js.map +1 -0
- package/dist/{chunk-57CVQCFX.js → chunk-7MQGNR76.js} +79 -3
- package/dist/chunk-7MQGNR76.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-MYIH7FWD.js → chunk-FBKP5CAO.js} +1 -1
- package/dist/chunk-FBKP5CAO.js.map +1 -0
- package/dist/chunk-KZNXZ5SM.js +333 -0
- package/dist/chunk-KZNXZ5SM.js.map +1 -0
- package/dist/{chunk-WMOI7FN3.js → chunk-LRCV7N7F.js} +12 -2
- package/dist/{chunk-WMOI7FN3.js.map → chunk-LRCV7N7F.js.map} +1 -1
- package/dist/{chunk-3K5OG6KI.js → chunk-OH43RPTP.js} +403 -187
- package/dist/chunk-OH43RPTP.js.map +1 -0
- package/dist/{chunk-H3AXU4MJ.js → chunk-OKJOQXK4.js} +149 -2
- package/dist/chunk-OKJOQXK4.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-TVSGLUID.js +596 -0
- package/dist/chunk-TVSGLUID.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 +4 -4
- package/dist/doc/index.js +124 -6
- package/dist/generate/index.d.ts +9 -2
- package/dist/generate/index.js +1 -1
- 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 +11 -5
- package/dist/imageEdit/index.js +4 -1
- package/dist/index.d.ts +11 -8
- package/dist/index.js +243 -24
- package/dist/inlineIconMarker-CiE8Hlvz.d.ts +59 -0
- package/dist/markdown/index.d.ts +212 -3
- package/dist/markdown/index.js +40 -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 +35 -5
- package/dist/spatial/index.d.ts +9 -4
- package/dist/spatial/index.js +1 -1
- package/dist/story/index.d.ts +906 -40
- package/dist/story/index.js +124 -6
- package/dist/{themeLibrary-BHjJ0Qx6.d.ts → themeLibrary-DQQKuRMx.d.ts} +1 -1
- 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/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__/customTemplateSample.test.ts +83 -0
- package/src/__tests__/customTemplatesFrontmatter.test.ts +205 -0
- package/src/__tests__/customThemesFrontmatter.test.ts +118 -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__/layoutBlock.test.ts +83 -0
- package/src/__tests__/layoutLayout.test.ts +169 -0
- package/src/__tests__/markdown.test.ts +23 -0
- package/src/__tests__/markdownSanitize.test.ts +64 -0
- package/src/__tests__/markdownToDoc.test.ts +223 -2
- package/src/__tests__/mediaAnnotations.test.ts +64 -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 +179 -0
- package/src/__tests__/templateAnnotationParse.test.ts +108 -0
- package/src/__tests__/templateMetadata.test.ts +21 -0
- package/src/__tests__/templates.test.ts +145 -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 +100 -0
- package/src/doc/customTemplatesFrontmatter.ts +308 -0
- package/src/doc/customThemesFrontmatter.ts +90 -0
- package/src/doc/docToMarkdown.ts +73 -4
- package/src/doc/getLayers.ts +40 -17
- package/src/doc/index.ts +35 -1
- package/src/doc/markdownToDoc.ts +333 -12
- package/src/doc/mediaAnnotations.ts +118 -0
- package/src/doc/resolveDocTheme.ts +55 -0
- package/src/doc/structuredData.ts +230 -0
- package/src/doc/templateInputs.ts +240 -0
- package/src/doc/templates/__tests__/customTemplate.test.ts +151 -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 +26 -18
- package/src/doc/templates/coverBlock.ts +7 -4
- package/src/doc/templates/customTemplate.ts +45 -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 +51 -25
- 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 +22 -9
- package/src/doc/templates/index.ts +215 -46
- 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 +23 -12
- 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 +221 -0
- package/src/doc/templates/tokens/resolveTokens.ts +0 -0
- package/src/doc/templates/twoColumn.ts +48 -30
- package/src/doc/templates/videoPullQuote.ts +22 -12
- package/src/doc/templates/videoWithCaption.ts +13 -8
- 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/shapeGeometry.ts +464 -0
- package/src/doc/utils/themeUtils.ts +75 -1
- package/src/doc/validate.ts +476 -0
- package/src/generate/slideshowGenerator.ts +7 -0
- 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/markdown/annotationCoercion.ts +295 -0
- package/src/markdown/attrTokens.ts +222 -0
- package/src/markdown/convert.ts +229 -26
- package/src/markdown/index.ts +36 -0
- package/src/markdown/sanitize.ts +298 -0
- package/src/markdown/stringify.ts +84 -3
- package/src/markdown/types.ts +66 -0
- package/src/markdown/utils.ts +49 -1
- package/src/recommend/templates.ts +24 -0
- package/src/schemas/BlockTemplates.ts +133 -4
- package/src/schemas/CustomTemplates.ts +74 -0
- package/src/schemas/Doc.ts +353 -29
- 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 +23 -4
- 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/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-3K5OG6KI.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-H3AXU4MJ.js.map +0 -1
- package/dist/chunk-KSWJR66U.js +0 -3706
- package/dist/chunk-KSWJR66U.js.map +0 -1
- package/dist/chunk-MYIH7FWD.js.map +0 -1
- package/dist/chunk-ZTX4PFFT.js.map +0 -1
|
@@ -137,7 +137,7 @@ describe('expandTemplateBlock', () => {
|
|
|
137
137
|
expect(imageLayer!.position.height).toBe('90%');
|
|
138
138
|
});
|
|
139
139
|
|
|
140
|
-
it('expands rightFeature template with image on the right half and
|
|
140
|
+
it('expands rightFeature template with image on the right half and left-aligned text on the left', () => {
|
|
141
141
|
const block: TemplateBlock = {
|
|
142
142
|
template: 'rightFeature',
|
|
143
143
|
id: 'rf-1',
|
|
@@ -157,9 +157,11 @@ describe('expandTemplateBlock', () => {
|
|
|
157
157
|
expect(imageLayer!.position.width).toBe('50%');
|
|
158
158
|
const textLayers = layers.filter((l) => l.type === 'text');
|
|
159
159
|
for (const t of textLayers) {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
expect(
|
|
160
|
+
// The mirror lives in the layout only: the text column occupies the
|
|
161
|
+
// left half, but the running text stays left-aligned for readability.
|
|
162
|
+
expect(t.position.x).toBe('6%');
|
|
163
|
+
expect(t.position.anchor).toBe('top-left');
|
|
164
|
+
expect((t.content as { style: { textAlign?: string } }).style.textAlign).toBe('left');
|
|
163
165
|
}
|
|
164
166
|
});
|
|
165
167
|
|
|
@@ -309,6 +311,145 @@ describe('expandDocBlocks', () => {
|
|
|
309
311
|
expect((landscape[0].layers ?? []).length).toBeGreaterThan(0);
|
|
310
312
|
expect((portrait[0].layers ?? []).length).toBeGreaterThan(0);
|
|
311
313
|
});
|
|
314
|
+
|
|
315
|
+
describe('theme renderStyle wiring', () => {
|
|
316
|
+
const blocks = (): TemplateBlock[] => [
|
|
317
|
+
{ template: 'title', id: 't-1', duration: 5, audioSegment: 0, title: 'One' },
|
|
318
|
+
{
|
|
319
|
+
template: 'imageWithCaption',
|
|
320
|
+
id: 't-2',
|
|
321
|
+
duration: 6,
|
|
322
|
+
audioSegment: 0,
|
|
323
|
+
imageSrc: 'photo.jpg',
|
|
324
|
+
imageAlt: 'photo',
|
|
325
|
+
caption: 'A caption',
|
|
326
|
+
},
|
|
327
|
+
];
|
|
328
|
+
|
|
329
|
+
it('fills the theme default transition on blocks after the first, never block 0', () => {
|
|
330
|
+
const theme = {
|
|
331
|
+
...DEFAULT_THEME,
|
|
332
|
+
renderStyle: {
|
|
333
|
+
...DEFAULT_THEME.renderStyle,
|
|
334
|
+
defaultTransition: { type: 'dissolve' as const, duration: 1.1 },
|
|
335
|
+
},
|
|
336
|
+
};
|
|
337
|
+
const result = expandDocBlocks(blocks(), { theme });
|
|
338
|
+
expect(result[0].transition).toBeUndefined();
|
|
339
|
+
expect(result[1].transition).toEqual({ type: 'dissolve', duration: 1.1 });
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it('never overrides an authored block transition', () => {
|
|
343
|
+
const theme = {
|
|
344
|
+
...DEFAULT_THEME,
|
|
345
|
+
renderStyle: {
|
|
346
|
+
...DEFAULT_THEME.renderStyle,
|
|
347
|
+
defaultTransition: { type: 'dissolve' as const },
|
|
348
|
+
},
|
|
349
|
+
};
|
|
350
|
+
const authored = blocks();
|
|
351
|
+
authored[1].transition = { type: 'cut' };
|
|
352
|
+
const result = expandDocBlocks(authored, { theme });
|
|
353
|
+
expect(result[1].transition).toEqual({ type: 'cut' });
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
it('scales template animation durations by theme animationSpeed', () => {
|
|
357
|
+
const theme = {
|
|
358
|
+
...DEFAULT_THEME,
|
|
359
|
+
style: { ...DEFAULT_THEME.style, animationSpeed: 2.0 },
|
|
360
|
+
};
|
|
361
|
+
const base = expandDocBlocks(blocks());
|
|
362
|
+
const slowed = expandDocBlocks(blocks(), { theme });
|
|
363
|
+
const baseTitle = base[0].layers?.find((l) => l.id === 'title');
|
|
364
|
+
const slowedTitle = slowed[0].layers?.find((l) => l.id === 'title');
|
|
365
|
+
expect(slowedTitle?.animation?.duration).toBe((baseTitle?.animation?.duration ?? 0) * 2);
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
it('gives full-bleed imagery ambient Ken Burns when the theme opts in', () => {
|
|
369
|
+
const theme = {
|
|
370
|
+
...DEFAULT_THEME,
|
|
371
|
+
renderStyle: { ...DEFAULT_THEME.renderStyle, ambientMotion: true },
|
|
372
|
+
};
|
|
373
|
+
const result = expandDocBlocks(blocks(), { theme });
|
|
374
|
+
const bg = result[1].layers?.find((l) => l.id === 'bg-image');
|
|
375
|
+
expect(bg?.animation?.type).toBe('slowZoom');
|
|
376
|
+
// Standard theme (ambientMotion: false) leaves the image static.
|
|
377
|
+
const plain = expandDocBlocks(blocks());
|
|
378
|
+
const plainBg = plain[1].layers?.find((l) => l.id === 'bg-image');
|
|
379
|
+
expect(plainBg?.animation).toBeUndefined();
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
it('themedEntrance: theme default text animation overrides entrance type, keeps timing', () => {
|
|
383
|
+
const theme = {
|
|
384
|
+
...DEFAULT_THEME,
|
|
385
|
+
renderStyle: { ...DEFAULT_THEME.renderStyle, defaultTextAnimation: 'zoomIn' as const },
|
|
386
|
+
};
|
|
387
|
+
const result = expandDocBlocks(blocks(), { theme });
|
|
388
|
+
const title = result[0].layers?.find((l) => l.id === 'title');
|
|
389
|
+
expect(title?.animation?.type).toBe('zoomIn');
|
|
390
|
+
// Duration authored by the template is preserved (title uses 2s)
|
|
391
|
+
expect(title?.animation?.duration).toBe(2);
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
it('templateHints: dramatic entrance switches statHighlight timing', () => {
|
|
395
|
+
const statBlock: TemplateBlock[] = [
|
|
396
|
+
{
|
|
397
|
+
template: 'statHighlight',
|
|
398
|
+
id: 's-1',
|
|
399
|
+
duration: 5,
|
|
400
|
+
audioSegment: 0,
|
|
401
|
+
stat: '42%',
|
|
402
|
+
description: 'described',
|
|
403
|
+
},
|
|
404
|
+
];
|
|
405
|
+
const theme = {
|
|
406
|
+
...DEFAULT_THEME,
|
|
407
|
+
renderStyle: {
|
|
408
|
+
...DEFAULT_THEME.renderStyle,
|
|
409
|
+
templateHints: { statHighlight: { entrance: 'dramatic' } },
|
|
410
|
+
},
|
|
411
|
+
};
|
|
412
|
+
const hinted = expandDocBlocks(statBlock, { theme });
|
|
413
|
+
const plain = expandDocBlocks(statBlock);
|
|
414
|
+
const hintedStat = hinted[0].layers?.find((l) => l.id === 'stat');
|
|
415
|
+
const plainStat = plain[0].layers?.find((l) => l.id === 'stat');
|
|
416
|
+
expect(hintedStat?.animation?.duration).toBe(0.4);
|
|
417
|
+
expect(plainStat?.animation?.duration).toBe(0.6);
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
it('theme persistentLayers render for docs without their own (wholesale precedence)', () => {
|
|
421
|
+
const theme = {
|
|
422
|
+
...DEFAULT_THEME,
|
|
423
|
+
persistentLayers: {
|
|
424
|
+
bottomLayers: [
|
|
425
|
+
{
|
|
426
|
+
template: 'solidBackground' as const,
|
|
427
|
+
config: { type: 'solidBackground' as const, color: '#112233' },
|
|
428
|
+
},
|
|
429
|
+
],
|
|
430
|
+
},
|
|
431
|
+
};
|
|
432
|
+
const inherited = expandDocBlocks(blocks(), { theme });
|
|
433
|
+
expect(inherited[0].layers?.[0]?.id).toContain('solid');
|
|
434
|
+
|
|
435
|
+
// Doc's own persistent layers win wholesale
|
|
436
|
+
const docOwn = expandDocBlocks(blocks(), {
|
|
437
|
+
theme,
|
|
438
|
+
persistentLayers: {
|
|
439
|
+
bottomLayers: [
|
|
440
|
+
{
|
|
441
|
+
template: 'solidBackground' as const,
|
|
442
|
+
config: { type: 'solidBackground' as const, color: '#445566' },
|
|
443
|
+
},
|
|
444
|
+
],
|
|
445
|
+
},
|
|
446
|
+
});
|
|
447
|
+
const first = docOwn[0].layers?.[0];
|
|
448
|
+
expect(first && 'content' in first && (first.content as { fill?: string }).fill).toBe(
|
|
449
|
+
'#445566',
|
|
450
|
+
);
|
|
451
|
+
});
|
|
452
|
+
});
|
|
312
453
|
});
|
|
313
454
|
|
|
314
455
|
describe('DEFAULT_THEME', () => {
|
|
@@ -71,6 +71,48 @@ describe('compileTheme', () => {
|
|
|
71
71
|
});
|
|
72
72
|
});
|
|
73
73
|
|
|
74
|
+
describe('compileTheme base inheritance', () => {
|
|
75
|
+
it('inherits renderStyle and colorSchemes from a chosen base', () => {
|
|
76
|
+
const base = resolveTheme('cinematic');
|
|
77
|
+
const theme = compileTheme(
|
|
78
|
+
{ id: 'brand', name: 'Brand', seedColors: { primary: '#3182ce' } },
|
|
79
|
+
{ base },
|
|
80
|
+
);
|
|
81
|
+
expect(theme.basedOn).toBe('cinematic');
|
|
82
|
+
// renderStyle inherited from the base (not the starter's 'standard')
|
|
83
|
+
expect(theme.renderStyle.name).toBe(base.renderStyle.name);
|
|
84
|
+
// colorSchemes inherited wholesale from the base
|
|
85
|
+
expect(theme.colorSchemes).toEqual(base.colorSchemes);
|
|
86
|
+
// ...but colors are still derived from the user's seed
|
|
87
|
+
expect(theme.colors.primary).toBe('#3182ce');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('replaces colorSchemes wholesale when the partial supplies them (removal sticks)', () => {
|
|
91
|
+
const base = resolveTheme('cinematic');
|
|
92
|
+
const theme = compileTheme(
|
|
93
|
+
{
|
|
94
|
+
id: 'brand',
|
|
95
|
+
name: 'Brand',
|
|
96
|
+
seedColors: { primary: '#3182ce' },
|
|
97
|
+
colorSchemes: { onlyOne: { bg: '#111111', text: '#eeeeee', accent: '#3182ce' } },
|
|
98
|
+
},
|
|
99
|
+
{ base },
|
|
100
|
+
);
|
|
101
|
+
// Only the scheme the user kept — the base's blue/green/… are gone.
|
|
102
|
+
expect(Object.keys(theme.colorSchemes)).toEqual(['onlyOne']);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('defaults to the neutral starter when no base is given (basedOn unset)', () => {
|
|
106
|
+
const theme = compileTheme({
|
|
107
|
+
id: 'brand',
|
|
108
|
+
name: 'Brand',
|
|
109
|
+
seedColors: { primary: '#3182ce' },
|
|
110
|
+
});
|
|
111
|
+
expect(theme.basedOn).toBeUndefined();
|
|
112
|
+
expect(theme.renderStyle.name).toBe('standard');
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
74
116
|
describe('parseTheme / serializeTheme round-trip', () => {
|
|
75
117
|
it('serialize → parse is lossless for built-ins', () => {
|
|
76
118
|
for (const [id, theme] of Object.entries(THEMES)) {
|
|
@@ -105,6 +105,12 @@ describe('validateTheme', () => {
|
|
|
105
105
|
expect(validateTheme(t).valid).toBe(true);
|
|
106
106
|
});
|
|
107
107
|
|
|
108
|
+
it('accepts PowerPoint-style default transitions', () => {
|
|
109
|
+
const t = clone(DEFAULT_THEME);
|
|
110
|
+
t.renderStyle.defaultTransition = { type: 'checkerboard', duration: 1 };
|
|
111
|
+
expect(validateTheme(t).valid).toBe(true);
|
|
112
|
+
});
|
|
113
|
+
|
|
108
114
|
it('rejects seedColors without primary', () => {
|
|
109
115
|
const bad = clone(DEFAULT_THEME);
|
|
110
116
|
bad.seedColors = { accent: '#63b3ed' } as never;
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { describe, it, expect, afterEach } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
applyTransform,
|
|
4
|
+
resolveTransformStyle,
|
|
5
|
+
registerTransformStyle,
|
|
6
|
+
unregisterTransformStyle,
|
|
7
|
+
getTransformStyleIds,
|
|
8
|
+
} from '../transform/index.js';
|
|
9
|
+
import type { TransformStyleConfig } from '../transform/index.js';
|
|
10
|
+
import type { Doc, Block } from '../schemas/Doc.js';
|
|
11
|
+
import { isTemplateBlock } from '../schemas/BlockTemplates.js';
|
|
12
|
+
|
|
13
|
+
function paragraphBlock(id: string, title: string, text: string, duration = 20): Block {
|
|
14
|
+
return {
|
|
15
|
+
id,
|
|
16
|
+
startTime: 0,
|
|
17
|
+
duration,
|
|
18
|
+
audioSegment: 0,
|
|
19
|
+
title,
|
|
20
|
+
contents: [
|
|
21
|
+
{
|
|
22
|
+
type: 'paragraph',
|
|
23
|
+
children: [{ type: 'text', value: text }],
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Body with a strong quote extraction. */
|
|
30
|
+
const QUOTED =
|
|
31
|
+
'The ranger paused at the overlook. "The mountain decides who climbs it," she said quietly. ' +
|
|
32
|
+
'Every year thousands of visitors make the same journey up the winding road to Paradise.';
|
|
33
|
+
|
|
34
|
+
function sampleDoc(): Doc {
|
|
35
|
+
return {
|
|
36
|
+
articleId: 'transform-v2-test',
|
|
37
|
+
duration: 60,
|
|
38
|
+
blocks: [
|
|
39
|
+
paragraphBlock('b1', 'The Mountain', QUOTED),
|
|
40
|
+
paragraphBlock(
|
|
41
|
+
'b2',
|
|
42
|
+
'Visitors',
|
|
43
|
+
'In 2024 the park recorded 2.5 million visitors, a 40% increase over the prior decade. ' +
|
|
44
|
+
'Rangers attribute the growth to social media and improved road access.',
|
|
45
|
+
),
|
|
46
|
+
],
|
|
47
|
+
audio: { segments: [] },
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
describe('transform style contract v2', () => {
|
|
52
|
+
afterEach(() => unregisterTransformStyle('custom-test'));
|
|
53
|
+
|
|
54
|
+
it('suggestedThemeId applies only when the doc declares no theme', () => {
|
|
55
|
+
const { doc } = applyTransform(sampleDoc(), 'magazine');
|
|
56
|
+
expect(doc.themeId).toBe('magazine');
|
|
57
|
+
|
|
58
|
+
const themed = { ...sampleDoc(), themeId: 'gezellig' };
|
|
59
|
+
expect(applyTransform(themed, 'magazine').doc.themeId).toBe('gezellig');
|
|
60
|
+
|
|
61
|
+
const viaOption = applyTransform(sampleDoc(), 'magazine', { themeId: 'bold' });
|
|
62
|
+
expect(viaOption.doc.themeId).toBe('bold');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('templateMap remaps quote extractions (magazine → pullQuote with accent image as background)', () => {
|
|
66
|
+
const images = [{ src: 'photo.jpg', alt: 'a photo' }];
|
|
67
|
+
const { doc } = applyTransform(sampleDoc(), 'magazine', { images });
|
|
68
|
+
const templates = doc.blocks.filter(isTemplateBlock).map((b) => b.template);
|
|
69
|
+
// The quote extraction maps to pullQuote (magazine's templateMap)
|
|
70
|
+
// because an image was available to serve as the background.
|
|
71
|
+
expect(templates).toContain('pullQuote');
|
|
72
|
+
expect(templates).not.toContain('quote');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('templateMap remap falls back to the default when the target needs an unavailable image', () => {
|
|
76
|
+
const { doc } = applyTransform(sampleDoc(), 'magazine', { images: [] });
|
|
77
|
+
const templates = doc.blocks.filter(isTemplateBlock).map((b) => b.template);
|
|
78
|
+
expect(templates).toContain('quote');
|
|
79
|
+
expect(templates).not.toContain('pullQuote');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('pacing adds intro/outro bookends (narrative)', () => {
|
|
83
|
+
const { doc } = applyTransform(sampleDoc(), 'narrative');
|
|
84
|
+
const first = doc.blocks[0];
|
|
85
|
+
const last = doc.blocks[doc.blocks.length - 1];
|
|
86
|
+
expect(first.template).toBe('title');
|
|
87
|
+
expect(first.title ?? (first as { title?: string }).title).toBeDefined();
|
|
88
|
+
expect(last.template).toBe('sectionHeader');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('registerTransformStyle adds a resolvable style that wins over unknown fallback', () => {
|
|
92
|
+
const custom: TransformStyleConfig = {
|
|
93
|
+
...resolveTransformStyle('minimal'),
|
|
94
|
+
id: 'custom-test',
|
|
95
|
+
name: 'Custom Test',
|
|
96
|
+
description: 'test style',
|
|
97
|
+
};
|
|
98
|
+
registerTransformStyle(custom);
|
|
99
|
+
expect(getTransformStyleIds()).toContain('custom-test');
|
|
100
|
+
expect(resolveTransformStyle('custom-test').name).toBe('Custom Test');
|
|
101
|
+
unregisterTransformStyle('custom-test');
|
|
102
|
+
// Unknown id falls back to the default (documentary)
|
|
103
|
+
expect(resolveTransformStyle('custom-test').id).toBe('documentary');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("accepts 'dataDriven' as an alias for the hyphenated registry id", () => {
|
|
107
|
+
expect(resolveTransformStyle('dataDriven').id).toBe('data-driven');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('budget.slidesPerMinute caps promotions on long docs', () => {
|
|
111
|
+
const manyBlocks: Doc = {
|
|
112
|
+
articleId: 'budget-test',
|
|
113
|
+
duration: 60,
|
|
114
|
+
blocks: Array.from({ length: 12 }, (_, i) =>
|
|
115
|
+
paragraphBlock(
|
|
116
|
+
`b${i}`,
|
|
117
|
+
`Section ${i}`,
|
|
118
|
+
`In 2024 the park recorded ${(i + 2) * 7}% growth across all visitor categories nationwide.`,
|
|
119
|
+
5,
|
|
120
|
+
),
|
|
121
|
+
),
|
|
122
|
+
audio: { segments: [] },
|
|
123
|
+
};
|
|
124
|
+
const unbudgeted = resolveTransformStyle('data-driven');
|
|
125
|
+
const budgeted: TransformStyleConfig = {
|
|
126
|
+
...unbudgeted,
|
|
127
|
+
id: 'custom-test',
|
|
128
|
+
budget: { slidesPerMinute: 2 },
|
|
129
|
+
};
|
|
130
|
+
registerTransformStyle(budgeted);
|
|
131
|
+
const loose = applyTransform(manyBlocks, 'data-driven');
|
|
132
|
+
const tight = applyTransform(manyBlocks, 'custom-test');
|
|
133
|
+
expect(tight.stats.transformedBlocks).toBeLessThan(loose.stats.transformedBlocks);
|
|
134
|
+
// 12 blocks x 5s = 60s → 2/minute → at most 2 promotions
|
|
135
|
+
expect(tight.stats.transformedBlocks).toBeLessThanOrEqual(2);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
normalizeTransitionType,
|
|
4
|
+
normalizeTransitionDirection,
|
|
5
|
+
isTransitionType,
|
|
6
|
+
TRANSITION_TYPES,
|
|
7
|
+
} from '../schemas/Transitions';
|
|
8
|
+
|
|
9
|
+
describe('normalizeTransitionType', () => {
|
|
10
|
+
it('returns exact canonical types unchanged', () => {
|
|
11
|
+
expect(normalizeTransitionType('fade')).toBe('fade');
|
|
12
|
+
expect(normalizeTransitionType('checkerboard')).toBe('checkerboard');
|
|
13
|
+
expect(normalizeTransitionType('slideLeft')).toBe('slideLeft');
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('resolves case / punctuation variants of canonical types', () => {
|
|
17
|
+
// These have NO explicit alias entry — they round-trip through the
|
|
18
|
+
// case-insensitive `NORMALIZED_TRANSITION_TYPES` fallback. This guards the
|
|
19
|
+
// removal of the redundant self-aliases (checkerboard/shape/uncover).
|
|
20
|
+
expect(normalizeTransitionType('Checkerboard')).toBe('checkerboard');
|
|
21
|
+
expect(normalizeTransitionType('CHECKERBOARD')).toBe('checkerboard');
|
|
22
|
+
expect(normalizeTransitionType('Shape')).toBe('shape');
|
|
23
|
+
expect(normalizeTransitionType('Uncover')).toBe('uncover');
|
|
24
|
+
expect(normalizeTransitionType('fly-through')).toBe('flyThrough');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('resolves explicit aliases (including plurals)', () => {
|
|
28
|
+
// Aliases only fire for spellings that are NOT themselves canonical types
|
|
29
|
+
// (an exact type returns early). `ferris` IS a canonical type, so its
|
|
30
|
+
// sameness with `ferrisWheel` comes from the visual map, not here.
|
|
31
|
+
expect(normalizeTransitionType('none')).toBe('cut');
|
|
32
|
+
expect(normalizeTransitionType('checkerboards')).toBe('checkerboard');
|
|
33
|
+
expect(normalizeTransitionType('shapes')).toBe('shape');
|
|
34
|
+
expect(normalizeTransitionType('flythru')).toBe('flyThrough');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('returns null for unknown values', () => {
|
|
38
|
+
expect(normalizeTransitionType('definitely-not-a-transition')).toBeNull();
|
|
39
|
+
expect(normalizeTransitionType('')).toBeNull();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('every canonical type normalizes to itself', () => {
|
|
43
|
+
for (const type of TRANSITION_TYPES) {
|
|
44
|
+
expect(normalizeTransitionType(type), type).toBe(type);
|
|
45
|
+
expect(isTransitionType(type), type).toBe(true);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe('normalizeTransitionDirection', () => {
|
|
51
|
+
it('resolves cardinal directions and shorthands', () => {
|
|
52
|
+
expect(normalizeTransitionDirection('left')).toBe('left');
|
|
53
|
+
expect(normalizeTransitionDirection('R')).toBe('right');
|
|
54
|
+
expect(normalizeTransitionDirection('horizontal')).toBe('horizontal');
|
|
55
|
+
expect(normalizeTransitionDirection('v')).toBe('vertical');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('returns null for unknown directions', () => {
|
|
59
|
+
expect(normalizeTransitionDirection('sideways')).toBeNull();
|
|
60
|
+
});
|
|
61
|
+
});
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { validateMarkdownSource } from '../doc/validate.js';
|
|
3
|
+
|
|
4
|
+
function codes(md: string, options?: Parameters<typeof validateMarkdownSource>[1]): string[] {
|
|
5
|
+
return validateMarkdownSource(md, options).diagnostics.map((d) => d.code);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
describe('validateMarkdownSource — templates', () => {
|
|
9
|
+
it('passes a clean document', () => {
|
|
10
|
+
const result = validateMarkdownSource('# Title\n\nBody.\n\n## Section {[quote]}\n\nA quote.\n');
|
|
11
|
+
expect(result.diagnostics).toEqual([]);
|
|
12
|
+
expect(result.errorCount).toBe(0);
|
|
13
|
+
expect(result.warningCount).toBe(0);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('flags unknown templates with a did-you-mean suggestion', () => {
|
|
17
|
+
const result = validateMarkdownSource('## Gallery {[photGrid columns=3]}');
|
|
18
|
+
expect(result.warningCount).toBe(1);
|
|
19
|
+
const d = result.diagnostics[0];
|
|
20
|
+
expect(d.code).toBe('unknown-template');
|
|
21
|
+
expect(d.message).toContain('photGrid');
|
|
22
|
+
expect(d.message).toContain('Did you mean "photoGrid"?');
|
|
23
|
+
expect(d.line).toBe(1);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('accepts legacy template aliases', () => {
|
|
27
|
+
expect(codes('## X {[titleBlock]}')).toEqual([]);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('accepts extra templates supplied by the host app', () => {
|
|
31
|
+
expect(codes('## X {[hero]}', { extraTemplates: ['hero'] })).toEqual([]);
|
|
32
|
+
expect(codes('## X {[hero]}')).toEqual(['unknown-template']);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe('validateMarkdownSource — unparsed annotations', () => {
|
|
37
|
+
it('flags {[…]} text in body content (annotations are heading-only)', () => {
|
|
38
|
+
const result = validateMarkdownSource('## X\n\n- {[imageWithCaption src=photo.jpg]}\n');
|
|
39
|
+
const d = result.diagnostics.find((x) => x.code === 'unparsed-annotation');
|
|
40
|
+
expect(d).toBeDefined();
|
|
41
|
+
expect(d!.message).toContain('headings');
|
|
42
|
+
expect(d!.line).toBe(3);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('flags non-trailing {[…]} text left in a heading', () => {
|
|
46
|
+
const result = validateMarkdownSource('## The {[chart]} section\n');
|
|
47
|
+
expect(result.diagnostics.map((d) => d.code)).toContain('unparsed-annotation');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('does not flag resolved inline icons or code blocks', () => {
|
|
51
|
+
const md = '## X\n\nUse {[github]} for code.\n\n```\nliteral {[notathing]}\n```\n';
|
|
52
|
+
expect(codes(md)).toEqual([]);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe('validateMarkdownSource — attributes and structure', () => {
|
|
57
|
+
it('surfaces malformed heading-attribute values', () => {
|
|
58
|
+
const result = validateMarkdownSource('## X {#a x=abc}');
|
|
59
|
+
const d = result.diagnostics.find((x) => x.code === 'invalid-attribute');
|
|
60
|
+
expect(d).toBeDefined();
|
|
61
|
+
expect(d!.message).toContain('x');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('includes conversion diagnostics (duplicate ids, bad fences)', () => {
|
|
65
|
+
const md = '## A {#dup}\n\n## B {#dup}\n\n```json data\nnot json\n```\n';
|
|
66
|
+
const result = validateMarkdownSource(md);
|
|
67
|
+
const found = result.diagnostics.map((d) => d.code);
|
|
68
|
+
expect(found).toContain('duplicate-id');
|
|
69
|
+
expect(found).toContain('data-fence-parse');
|
|
70
|
+
expect(result.errorCount).toBe(2);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe('validateMarkdownSource — assets', () => {
|
|
75
|
+
it('flags relative image references missing from the asset set', () => {
|
|
76
|
+
const result = validateMarkdownSource('## X\n\n\n', {
|
|
77
|
+
assets: new Set<string>(),
|
|
78
|
+
});
|
|
79
|
+
const d = result.diagnostics.find((x) => x.code === 'missing-asset');
|
|
80
|
+
expect(d).toBeDefined();
|
|
81
|
+
expect(d!.message).toContain('images/hero.jpg');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('passes when the asset exists, and skips absolute/external URLs', () => {
|
|
85
|
+
const md =
|
|
86
|
+
'## X\n\n\n\n\n\n\n';
|
|
87
|
+
expect(codes(md, { assets: new Set(['images/hero.jpg']) })).toEqual([]);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('checks media params from {[…]} annotations', () => {
|
|
91
|
+
const result = validateMarkdownSource('## X {[imageWithCaption src=missing.png]}', {
|
|
92
|
+
assets: new Set<string>(),
|
|
93
|
+
});
|
|
94
|
+
expect(result.diagnostics.map((d) => d.code)).toContain('missing-asset');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('skips asset checks entirely when no asset set is provided', () => {
|
|
98
|
+
expect(codes('## X\n\n\n')).toEqual([]);
|
|
99
|
+
});
|
|
100
|
+
});
|