@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.
Files changed (254) hide show
  1. package/README.md +14 -1
  2. package/dist/{ContentContainer-DNx460y8.d.ts → ContentContainer-BXUlIf18.d.ts} +6 -2
  3. package/dist/{Doc-BeSeH2iX.d.ts → Doc-BBVGq1_9.d.ts} +922 -273
  4. package/dist/{ImageEditDoc-BTIvw0oq.d.ts → ImageEditDoc-FuyGtt6o.d.ts} +11 -4
  5. package/dist/{chunk-ZTX4PFFT.js → chunk-2OIBZYKP.js} +75 -2
  6. package/dist/chunk-2OIBZYKP.js.map +1 -0
  7. package/dist/chunk-2UQOHYE2.js +45 -0
  8. package/dist/chunk-2UQOHYE2.js.map +1 -0
  9. package/dist/chunk-6ATE2PSM.js +344 -0
  10. package/dist/chunk-6ATE2PSM.js.map +1 -0
  11. package/dist/{chunk-57CVQCFX.js → chunk-A4O7GIWE.js} +74 -23
  12. package/dist/chunk-A4O7GIWE.js.map +1 -0
  13. package/dist/{chunk-5WFRKURX.js → chunk-DLXZMS5K.js} +4 -2
  14. package/dist/chunk-DLXZMS5K.js.map +1 -0
  15. package/dist/chunk-ET53IIEP.js +1 -0
  16. package/dist/chunk-ET53IIEP.js.map +1 -0
  17. package/dist/chunk-HWVFJAAH.js +362 -0
  18. package/dist/chunk-HWVFJAAH.js.map +1 -0
  19. package/dist/{chunk-2TMAWB4Y.js → chunk-IMSCRLLV.js} +1 -1
  20. package/dist/chunk-IMSCRLLV.js.map +1 -0
  21. package/dist/{chunk-B4EEON3N.js → chunk-KKNUBQ6Y.js} +536 -103
  22. package/dist/chunk-KKNUBQ6Y.js.map +1 -0
  23. package/dist/chunk-LDQ2HJIX.js +596 -0
  24. package/dist/chunk-LDQ2HJIX.js.map +1 -0
  25. package/dist/chunk-LH7I6SH7.js +7208 -0
  26. package/dist/chunk-LH7I6SH7.js.map +1 -0
  27. package/dist/chunk-PGGGKYWG.js +1 -0
  28. package/dist/chunk-PGGGKYWG.js.map +1 -0
  29. package/dist/{chunk-7UDSRZKG.js → chunk-Q3ROPT5H.js} +35 -14
  30. package/dist/chunk-Q3ROPT5H.js.map +1 -0
  31. package/dist/chunk-QE7TWGVT.js +178 -0
  32. package/dist/chunk-QE7TWGVT.js.map +1 -0
  33. package/dist/chunk-RUDYOTA6.js +345 -0
  34. package/dist/chunk-RUDYOTA6.js.map +1 -0
  35. package/dist/{chunk-K32VJONL.js → chunk-T5UK6YOB.js} +41 -2
  36. package/dist/chunk-T5UK6YOB.js.map +1 -0
  37. package/dist/{chunk-3K5OG6KI.js → chunk-TEEEILMP.js} +412 -186
  38. package/dist/chunk-TEEEILMP.js.map +1 -0
  39. package/dist/{chunk-FR2RBTKO.js → chunk-TQWLI6S2.js} +93 -27
  40. package/dist/chunk-TQWLI6S2.js.map +1 -0
  41. package/dist/{chunk-WMOI7FN3.js → chunk-UY7KGQ5R.js} +12 -2
  42. package/dist/{chunk-WMOI7FN3.js.map → chunk-UY7KGQ5R.js.map} +1 -1
  43. package/dist/{chunk-3E5F2XMR.js → chunk-VWUFZ6ZG.js} +29 -2
  44. package/dist/chunk-VWUFZ6ZG.js.map +1 -0
  45. package/dist/chunk-ZLQKNOZQ.js +1 -0
  46. package/dist/chunk-ZLQKNOZQ.js.map +1 -0
  47. package/dist/doc/index.d.ts +5 -5
  48. package/dist/doc/index.js +135 -8
  49. package/dist/generate/index.d.ts +3 -74
  50. package/dist/generate/index.js +2 -7
  51. package/dist/icons/index.d.ts +3 -26
  52. package/dist/icons/index.js +13 -0
  53. package/dist/icons/inlineIconMarker.d.ts +1 -0
  54. package/dist/icons/inlineIconMarker.js +15 -0
  55. package/dist/icons/inlineIconMarker.js.map +1 -0
  56. package/dist/imageEdit/index.d.ts +12 -6
  57. package/dist/imageEdit/index.js +4 -1
  58. package/dist/index.d.ts +14 -11
  59. package/dist/index.js +265 -37
  60. package/dist/inlineIconMarker-CiE8Hlvz.d.ts +59 -0
  61. package/dist/jsonForm/index.d.ts +42 -1
  62. package/dist/jsonForm/index.js +9 -1
  63. package/dist/markdown/index.d.ts +228 -3
  64. package/dist/markdown/index.js +42 -7
  65. package/dist/recommend/index.d.ts +1 -1
  66. package/dist/recommend/index.js +3 -2
  67. package/dist/schemas/index.d.ts +28 -8
  68. package/dist/schemas/index.js +36 -8
  69. package/dist/spatial/index.d.ts +9 -4
  70. package/dist/spatial/index.js +1 -1
  71. package/dist/storage/index.d.ts +2 -2
  72. package/dist/storage/index.js +1 -1
  73. package/dist/story/index.d.ts +1021 -41
  74. package/dist/story/index.js +135 -8
  75. package/dist/{themeLibrary-BHjJ0Qx6.d.ts → themeLibrary-CehcJhzz.d.ts} +1 -1
  76. package/dist/timing/index.js +3 -5
  77. package/dist/transform/index.d.ts +48 -10
  78. package/dist/transform/index.js +17 -481
  79. package/dist/transform/index.js.map +1 -1
  80. package/dist/{types-BdZkdJ3z.d.ts → types-DlAZ7MW4.d.ts} +115 -1
  81. package/dist/versions/index.d.ts +1 -1
  82. package/package.json +6 -1
  83. package/src/__tests__/animationUtils.test.ts +40 -0
  84. package/src/__tests__/annotationCoercion.test.ts +243 -0
  85. package/src/__tests__/applyRenderStyle.test.ts +146 -0
  86. package/src/__tests__/atmosphereLayers.test.ts +108 -0
  87. package/src/__tests__/attrTokens.test.ts +162 -0
  88. package/src/__tests__/contentExtractor.test.ts +60 -0
  89. package/src/__tests__/coverBlock.test.ts +46 -1
  90. package/src/__tests__/customTemplateSample.test.ts +144 -0
  91. package/src/__tests__/customTemplatesFrontmatter.test.ts +236 -0
  92. package/src/__tests__/customThemesFrontmatter.test.ts +145 -0
  93. package/src/__tests__/diagramBlock.test.ts +122 -0
  94. package/src/__tests__/diagramLayout.test.ts +134 -0
  95. package/src/__tests__/drawing.test.ts +109 -0
  96. package/src/__tests__/drawingBlock.test.ts +124 -0
  97. package/src/__tests__/drawingLayout.test.ts +171 -0
  98. package/src/__tests__/geohash.test.ts +6 -5
  99. package/src/__tests__/getLayers.test.ts +6 -2
  100. package/src/__tests__/getLayersFallback.test.ts +103 -0
  101. package/src/__tests__/imageEditExportSvg.test.ts +70 -0
  102. package/src/__tests__/imageEditVersions.test.ts +72 -0
  103. package/src/__tests__/inlineIconMarker.test.ts +36 -0
  104. package/src/__tests__/inputDescriptors.test.ts +162 -0
  105. package/src/__tests__/jsonForm.tokens.test.ts +84 -0
  106. package/src/__tests__/layoutBlock.test.ts +83 -0
  107. package/src/__tests__/layoutLayout.test.ts +169 -0
  108. package/src/__tests__/markdown.test.ts +87 -0
  109. package/src/__tests__/markdownSanitize.test.ts +79 -0
  110. package/src/__tests__/markdownToDoc.test.ts +391 -2
  111. package/src/__tests__/mediaAnnotations.test.ts +91 -0
  112. package/src/__tests__/mediaSchedule.test.ts +118 -0
  113. package/src/__tests__/pandocAttrParse.test.ts +187 -0
  114. package/src/__tests__/resolveDocTheme.test.ts +55 -0
  115. package/src/__tests__/rootExports.test.ts +43 -0
  116. package/src/__tests__/shapeGeometry.test.ts +89 -0
  117. package/src/__tests__/shapeVocabulary.test.ts +133 -0
  118. package/src/__tests__/structuredData.test.ts +249 -0
  119. package/src/__tests__/templateAnnotationParse.test.ts +108 -0
  120. package/src/__tests__/templateMetadata.test.ts +31 -0
  121. package/src/__tests__/templates.test.ts +257 -4
  122. package/src/__tests__/themeCompile.test.ts +42 -0
  123. package/src/__tests__/themeValidator.test.ts +6 -0
  124. package/src/__tests__/transformV2.test.ts +137 -0
  125. package/src/__tests__/transitionNormalize.test.ts +61 -0
  126. package/src/__tests__/validateDoc.test.ts +260 -0
  127. package/src/doc/annotationBlocks.ts +117 -0
  128. package/src/doc/customTemplatesFrontmatter.ts +314 -0
  129. package/src/doc/customThemesFrontmatter.ts +95 -0
  130. package/src/doc/docToMarkdown.ts +104 -5
  131. package/src/doc/getLayers.ts +49 -17
  132. package/src/doc/index.ts +47 -1
  133. package/src/doc/markdownToDoc.ts +411 -13
  134. package/src/doc/mediaAnnotations.ts +100 -0
  135. package/src/doc/resolveDocTheme.ts +55 -0
  136. package/src/doc/standaloneAnnotation.ts +58 -0
  137. package/src/doc/structuredData.ts +301 -0
  138. package/src/doc/templateInputs.ts +240 -0
  139. package/src/doc/templates/__tests__/customTemplate.test.ts +170 -0
  140. package/src/doc/templates/__tests__/customTemplateRegistry.test.ts +110 -0
  141. package/src/doc/templates/accentImage.ts +30 -6
  142. package/src/doc/templates/captionUtils.ts +23 -0
  143. package/src/doc/templates/comparisonBar.ts +33 -18
  144. package/src/doc/templates/coverBlock.ts +35 -5
  145. package/src/doc/templates/customTemplate.ts +51 -0
  146. package/src/doc/templates/dataTable.ts +37 -19
  147. package/src/doc/templates/dateEvent.ts +41 -20
  148. package/src/doc/templates/definitionCard.ts +26 -18
  149. package/src/doc/templates/diagramBlock.ts +204 -0
  150. package/src/doc/templates/diagramLayout.ts +159 -0
  151. package/src/doc/templates/drawingBlock.ts +325 -0
  152. package/src/doc/templates/drawingLayout.ts +539 -0
  153. package/src/doc/templates/factCard.ts +54 -26
  154. package/src/doc/templates/fallbackBlock.ts +123 -0
  155. package/src/doc/templates/featureBlock.ts +20 -13
  156. package/src/doc/templates/fullBleedQuote.ts +18 -8
  157. package/src/doc/templates/imageWithCaption.ts +30 -15
  158. package/src/doc/templates/index.ts +225 -46
  159. package/src/doc/templates/inputDescriptors.ts +432 -0
  160. package/src/doc/templates/layoutBlock.ts +43 -0
  161. package/src/doc/templates/layoutLayout.ts +243 -0
  162. package/src/doc/templates/listBlock.ts +56 -30
  163. package/src/doc/templates/mapBlock.ts +15 -12
  164. package/src/doc/templates/metadata.ts +120 -0
  165. package/src/doc/templates/persistentLayers.ts +129 -11
  166. package/src/doc/templates/photoGrid.ts +22 -34
  167. package/src/doc/templates/pullQuote.ts +30 -12
  168. package/src/doc/templates/quoteBlock.ts +54 -29
  169. package/src/doc/templates/sectionHeader.ts +25 -11
  170. package/src/doc/templates/statHighlight.ts +39 -22
  171. package/src/doc/templates/titleBlock.ts +59 -21
  172. package/src/doc/templates/tokens/__tests__/resolveTokens.test.ts +487 -0
  173. package/src/doc/templates/tokens/resolveTokens.ts +453 -0
  174. package/src/doc/templates/twoColumn.ts +48 -30
  175. package/src/doc/templates/videoPullQuote.ts +22 -12
  176. package/src/doc/templates/videoWithCaption.ts +21 -15
  177. package/src/doc/utils/animationUtils.ts +267 -3
  178. package/src/doc/utils/applyRenderStyle.ts +113 -0
  179. package/src/doc/utils/imageTreatment.ts +70 -0
  180. package/src/doc/utils/nearest.ts +59 -0
  181. package/src/doc/utils/shapeGeometry.ts +464 -0
  182. package/src/doc/utils/themeUtils.ts +75 -1
  183. package/src/doc/validate.ts +575 -0
  184. package/src/generate/contentExtractor.ts +148 -35
  185. package/src/generate/index.ts +0 -3
  186. package/src/icons/index.ts +8 -0
  187. package/src/icons/inlineIconMarker.ts +73 -0
  188. package/src/imageEdit/export.ts +85 -2
  189. package/src/imageEdit/index.ts +1 -0
  190. package/src/imageEdit/persistence.ts +51 -0
  191. package/src/index.ts +2 -0
  192. package/src/jsonForm/index.ts +2 -0
  193. package/src/jsonForm/tokens.ts +78 -0
  194. package/src/markdown/annotationCoercion.ts +295 -0
  195. package/src/markdown/attrTokens.ts +241 -0
  196. package/src/markdown/convert.ts +229 -26
  197. package/src/markdown/index.ts +37 -0
  198. package/src/markdown/sanitize.ts +318 -0
  199. package/src/markdown/stringify.ts +96 -6
  200. package/src/markdown/types.ts +66 -0
  201. package/src/markdown/utils.ts +111 -2
  202. package/src/recommend/templates.ts +24 -0
  203. package/src/schemas/BlockTemplates.ts +133 -4
  204. package/src/schemas/CustomTemplates.ts +126 -0
  205. package/src/schemas/Doc.ts +387 -65
  206. package/src/schemas/ImageEditDoc.ts +11 -4
  207. package/src/schemas/Media.ts +169 -0
  208. package/src/schemas/Theme.ts +23 -1
  209. package/src/schemas/Transitions.ts +224 -0
  210. package/src/schemas/colorUtils.ts +33 -0
  211. package/src/schemas/fontStacks.ts +5 -2
  212. package/src/schemas/index.ts +3 -0
  213. package/src/schemas/themeCompile.ts +24 -5
  214. package/src/schemas/themeValidator.ts +49 -11
  215. package/src/schemas/themes/cinematic.json +69 -12
  216. package/src/schemas/themes/documentary.json +58 -11
  217. package/src/schemas/themes/gezellig.json +60 -11
  218. package/src/schemas/themes/magazine.json +54 -9
  219. package/src/schemas/themes/morning-light.json +52 -9
  220. package/src/schemas/themes/standard-dark.json +48 -13
  221. package/src/schemas/themes/standard.json +44 -12
  222. package/src/schemas/themes/tech-dark.json +65 -12
  223. package/src/spatial/Geohash.ts +52 -21
  224. package/src/storage/ContentContainer.ts +6 -2
  225. package/src/transform/applyTransform.ts +43 -4
  226. package/src/transform/index.ts +1 -0
  227. package/src/transform/registry.ts +44 -13
  228. package/src/transform/styles/dataDriven.ts +2 -1
  229. package/src/transform/styles/documentary.ts +2 -1
  230. package/src/transform/styles/magazine.ts +3 -1
  231. package/src/transform/styles/minimal.ts +2 -1
  232. package/src/transform/styles/narrative.ts +3 -1
  233. package/src/transform/templateSelector.ts +87 -11
  234. package/src/transform/types.ts +35 -2
  235. package/dist/chunk-2TMAWB4Y.js.map +0 -1
  236. package/dist/chunk-3E5F2XMR.js.map +0 -1
  237. package/dist/chunk-3K5OG6KI.js.map +0 -1
  238. package/dist/chunk-4X3JQXNM.js +0 -35
  239. package/dist/chunk-4X3JQXNM.js.map +0 -1
  240. package/dist/chunk-57CVQCFX.js.map +0 -1
  241. package/dist/chunk-5WFRKURX.js.map +0 -1
  242. package/dist/chunk-7UDSRZKG.js.map +0 -1
  243. package/dist/chunk-B4EEON3N.js.map +0 -1
  244. package/dist/chunk-FR2RBTKO.js.map +0 -1
  245. package/dist/chunk-H3AXU4MJ.js +0 -179
  246. package/dist/chunk-H3AXU4MJ.js.map +0 -1
  247. package/dist/chunk-K32VJONL.js.map +0 -1
  248. package/dist/chunk-KSWJR66U.js +0 -3706
  249. package/dist/chunk-KSWJR66U.js.map +0 -1
  250. package/dist/chunk-MYIH7FWD.js +0 -145
  251. package/dist/chunk-MYIH7FWD.js.map +0 -1
  252. package/dist/chunk-ZTX4PFFT.js.map +0 -1
  253. package/src/__tests__/slideshowGenerator.test.ts +0 -128
  254. package/src/generate/slideshowGenerator.ts +0 -273
@@ -0,0 +1,91 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { parseMarkdown } from '../markdown/parse.js';
3
+ import { markdownToDoc } from '../doc/markdownToDoc.js';
4
+ import { resolveMediaSchedule } from '../schemas/Media.js';
5
+
6
+ function toDoc(md: string) {
7
+ return markdownToDoc(parseMarkdown(md), { articleId: 't' });
8
+ }
9
+
10
+ describe('markdownToDoc media annotations', () => {
11
+ it('lifts a block-level audio annotation into block.media', () => {
12
+ const doc = toDoc(
13
+ '# One {duration=10}\n\nIntro.\n\n{[audio src=narration.mp3 startAt=5 clipEnd=8 spillover=true]}\n',
14
+ );
15
+ const block = doc.blocks[0];
16
+ expect(block.media).toHaveLength(1);
17
+ expect(block.media![0]).toMatchObject({
18
+ src: 'narration.mp3',
19
+ kind: 'audio',
20
+ startAt: 5,
21
+ clipEnd: 8,
22
+ spillover: true,
23
+ anchor: 'block',
24
+ });
25
+ // The annotation paragraph is removed from the visible contents.
26
+ expect(JSON.stringify(block.contents)).not.toContain('narration.mp3');
27
+ });
28
+
29
+ it('routes anchor=document annotations to doc.documentMedia', () => {
30
+ const doc = toDoc('{[audio src=voice.mp3 anchor=document]}\n\n# First\n\nBody.\n');
31
+ expect(doc.documentMedia).toHaveLength(1);
32
+ expect(doc.documentMedia![0]).toMatchObject({ src: 'voice.mp3', anchor: 'document' });
33
+ // It is not also attached to the preamble block's media.
34
+ expect(doc.blocks[0].media).toBeUndefined();
35
+ });
36
+
37
+ it('recognizes a video annotation as kind video', () => {
38
+ const doc = toDoc('# B {duration=20}\n\n{[video src=clip.mp4 clipStart=2 clipEnd=8]}\n');
39
+ expect(doc.blocks[0].media![0]).toMatchObject({
40
+ src: 'clip.mp4',
41
+ kind: 'video',
42
+ clipStart: 2,
43
+ clipEnd: 8,
44
+ });
45
+ });
46
+
47
+ it('accepts startTime as an alias of startAt (startAt wins when both given)', () => {
48
+ const alias = toDoc('# B {duration=20}\n\n{[audio src=a.mp3 startTime=5]}\n');
49
+ expect(alias.blocks[0].media![0].startAt).toBe(5);
50
+
51
+ const both = toDoc('# B {duration=20}\n\n{[audio src=a.mp3 startAt=3 startTime=5]}\n');
52
+ expect(both.blocks[0].media![0].startAt).toBe(3);
53
+ });
54
+
55
+ it('media annotation text does not inflate block duration', () => {
56
+ // No explicit duration; body is only the annotation → default (5s), not reading-time of the annotation text.
57
+ const doc = toDoc('# B\n\n{[audio src=a.mp3]}\n');
58
+ expect(doc.blocks[0].duration).toBe(5);
59
+ });
60
+
61
+ it('feeds resolveMediaSchedule end to end', () => {
62
+ const doc = toDoc(
63
+ '{[audio src=voice.mp3 anchor=document]}\n\n# One {duration=30}\n\n# Two {duration=20}\n\n{[audio src=a.mp3 startAt=5 clipEnd=8]}\n',
64
+ );
65
+ const sched = resolveMediaSchedule(doc);
66
+ const doc1 = sched.find((c) => c.anchor === 'document')!;
67
+ const block = sched.find((c) => c.anchor === 'block')!;
68
+ expect(doc1.absoluteStart).toBe(0);
69
+ expect(doc1.absoluteEnd).toBe(50); // spans both blocks
70
+ expect(block.absoluteStart).toBe(35); // block Two at 30 + startAt 5
71
+ });
72
+ });
73
+
74
+ describe('media annotations vs standalone template blocks', () => {
75
+ it('extracts media annotations as clips, not standalone blocks (media names win)', () => {
76
+ const doc = toDoc('# One\n\nIntro.\n\n{[audio src=narration.mp3]}\n\n{[video src=clip.mp4]}\n');
77
+ const block = doc.blocks[0];
78
+ expect(block.media?.map((m) => m.kind)).toEqual(['audio', 'video']);
79
+ // No sibling standalone blocks were produced for the media names.
80
+ expect(doc.blocks).toHaveLength(1);
81
+ expect(doc.blocks.some((b) => b.standaloneAnnotation)).toBe(false);
82
+ });
83
+
84
+ it('mixes a media clip and a standalone template block in one section', () => {
85
+ const doc = toDoc('# One\n\n{[audio src=n.mp3]}\n\n{[quote]}\n\nquoted\n');
86
+ expect(doc.blocks[0].media?.map((m) => m.kind)).toEqual(['audio']);
87
+ const q = doc.blocks[1];
88
+ expect(q.template).toBe('quote');
89
+ expect(q.standaloneAnnotation).toBe(true);
90
+ });
91
+ });
@@ -0,0 +1,118 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { resolveMediaSchedule, getDocPlaybackDuration } from '../schemas/Media.js';
3
+ import type { Doc, Block } from '../schemas/Doc.js';
4
+
5
+ function block(partial: Partial<Block>): Block {
6
+ return {
7
+ id: partial.id ?? 'b',
8
+ startTime: partial.startTime ?? 0,
9
+ duration: partial.duration ?? 10,
10
+ audioSegment: 0,
11
+ ...partial,
12
+ };
13
+ }
14
+
15
+ function doc(blocks: Block[], extra?: Partial<Doc>): Doc {
16
+ const lastEnd = blocks.reduce((m, b) => Math.max(m, b.startTime + b.duration), 0);
17
+ return { articleId: 't', duration: lastEnd, blocks, audio: { segments: [] }, ...extra };
18
+ }
19
+
20
+ describe('resolveMediaSchedule', () => {
21
+ it('offsets a block clip by startAt relative to the block start', () => {
22
+ // Block #2 starts at 30s; a clip with startAt=5 plays at 35s.
23
+ const d = doc([
24
+ block({ id: 'one', startTime: 0, duration: 30 }),
25
+ block({
26
+ id: 'two',
27
+ startTime: 30,
28
+ duration: 20,
29
+ media: [{ id: 'm1', src: 'a.mp3', kind: 'audio', startAt: 5, clipEnd: 8, anchor: 'block' }],
30
+ }),
31
+ ]);
32
+ const [clip] = resolveMediaSchedule(d);
33
+ expect(clip.absoluteStart).toBe(35);
34
+ expect(clip.absoluteEnd).toBe(43); // 35 + (clipEnd 8 - clipStart 0)
35
+ expect(clip.sourceIn).toBe(0);
36
+ expect(clip.blockId).toBe('two');
37
+ });
38
+
39
+ it('clamps a non-spillover clip to the block end', () => {
40
+ const d = doc([
41
+ block({
42
+ id: 'b',
43
+ startTime: 0,
44
+ duration: 10,
45
+ media: [
46
+ { id: 'm', src: 'a.mp3', kind: 'audio', startAt: 2, clipEnd: 100, anchor: 'block' },
47
+ ],
48
+ }),
49
+ ]);
50
+ const [clip] = resolveMediaSchedule(d);
51
+ expect(clip.absoluteEnd).toBe(10); // clamped to block end, not 2 + 100
52
+ });
53
+
54
+ it('lets a spillover clip run past the block end', () => {
55
+ const d = doc([
56
+ block({
57
+ id: 'b',
58
+ startTime: 0,
59
+ duration: 10,
60
+ media: [
61
+ {
62
+ id: 'm',
63
+ src: 'a.mp3',
64
+ kind: 'audio',
65
+ startAt: 2,
66
+ clipEnd: 100,
67
+ spillover: true,
68
+ anchor: 'block',
69
+ },
70
+ ],
71
+ }),
72
+ block({ id: 'b2', startTime: 10, duration: 10 }),
73
+ ]);
74
+ const [clip] = resolveMediaSchedule(d);
75
+ expect(clip.absoluteEnd).toBe(102); // 2 + 100, past the block end
76
+ });
77
+
78
+ it('spans the whole document for an anchor=document clip', () => {
79
+ const d = doc([block({ id: 'b', startTime: 0, duration: 40 })], {
80
+ documentMedia: [
81
+ { id: 'narr', src: 'voice.mp3', kind: 'audio', startAt: 0, anchor: 'document' },
82
+ ],
83
+ });
84
+ const [clip] = resolveMediaSchedule(d);
85
+ expect(clip.absoluteStart).toBe(0);
86
+ expect(clip.absoluteEnd).toBe(40);
87
+ expect(clip.anchor).toBe('document');
88
+ });
89
+ });
90
+
91
+ describe('getDocPlaybackDuration', () => {
92
+ it('extends total duration to include media spillover', () => {
93
+ const d = doc([
94
+ block({
95
+ id: 'b',
96
+ startTime: 0,
97
+ duration: 10,
98
+ media: [
99
+ {
100
+ id: 'm',
101
+ src: 'a.mp3',
102
+ kind: 'audio',
103
+ startAt: 0,
104
+ clipEnd: 25,
105
+ spillover: true,
106
+ anchor: 'block',
107
+ },
108
+ ],
109
+ }),
110
+ ]);
111
+ expect(getDocPlaybackDuration(d)).toBe(25);
112
+ });
113
+
114
+ it('falls back to the block timeline when there is no media', () => {
115
+ const d = doc([block({ id: 'b', startTime: 0, duration: 12 })]);
116
+ expect(getDocPlaybackDuration(d)).toBe(12);
117
+ });
118
+ });
@@ -0,0 +1,187 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { parseMarkdown, stringifyMarkdown } from '../markdown/index.js';
3
+
4
+ function getHeading(md: string) {
5
+ const doc = parseMarkdown(md);
6
+ const heading = doc.children[0];
7
+ if (heading.type !== 'heading') throw new Error('expected heading');
8
+ return heading;
9
+ }
10
+
11
+ describe('Pandoc heading attribute parse', () => {
12
+ it('extracts an id-only block', () => {
13
+ const h = getHeading('## Section title {#intro}');
14
+ expect(h.attributes).toEqual({ id: 'intro' });
15
+ expect(h.templateAnnotation).toBeUndefined();
16
+ });
17
+
18
+ it('extracts classes', () => {
19
+ const h = getHeading('## X {.alpha .beta}');
20
+ expect(h.attributes?.classes).toEqual(['alpha', 'beta']);
21
+ });
22
+
23
+ it('extracts id + classes + params', () => {
24
+ const h = getHeading('## X {#abc .v1 .v2 x=400 y=200}');
25
+ expect(h.attributes?.id).toBe('abc');
26
+ expect(h.attributes?.classes).toEqual(['v1', 'v2']);
27
+ expect(h.attributes?.params).toEqual({ x: '400', y: '200' });
28
+ expect(h.attributes?.blockMeta).toEqual({ x: 400, y: 200 });
29
+ });
30
+
31
+ it('coerces connectsTo to a typed list', () => {
32
+ const h = getHeading('## X {connectsTo=foo:veryImportant,bar:requires,baz:requires}');
33
+ expect(h.attributes?.blockMeta?.connectsTo).toEqual([
34
+ { target: 'foo', type: 'veryImportant' },
35
+ { target: 'bar', type: 'requires' },
36
+ { target: 'baz', type: 'requires' },
37
+ ]);
38
+ });
39
+
40
+ it('coerces startTime/duration to seconds', () => {
41
+ const h = getHeading('## X {startTime=01:30 duration=00:45}');
42
+ expect(h.attributes?.blockMeta?.startTime).toBe(90);
43
+ expect(h.attributes?.blockMeta?.duration).toBe(45);
44
+ });
45
+
46
+ it('routes unknown keys to metadata', () => {
47
+ const h = getHeading('## X {#a priority=high level=info}');
48
+ expect(h.attributes?.metadata).toEqual({ priority: 'high', level: 'info' });
49
+ expect(h.attributes?.blockMeta).toBeUndefined();
50
+ });
51
+
52
+ it('handles quoted values with spaces', () => {
53
+ const h = getHeading('## X {caption="hello world"}');
54
+ expect(h.attributes?.params).toEqual({ caption: 'hello world' });
55
+ expect(h.attributes?.metadata).toEqual({ caption: 'hello world' });
56
+ });
57
+
58
+ it('preserves commas inside quoted values', () => {
59
+ const h = getHeading('## X {caption="hello, friend"}');
60
+ expect(h.attributes?.metadata).toEqual({ caption: 'hello, friend' });
61
+ });
62
+
63
+ it('escaped quotes inside quoted values', () => {
64
+ const h = getHeading('## X {label="he said \\"hi\\""}');
65
+ expect(h.attributes?.metadata?.label).toBe('he said "hi"');
66
+ });
67
+
68
+ it('handles single-quoted values with spaces', () => {
69
+ const h = getHeading("## X {caption='hello world'}");
70
+ expect(h.attributes?.params).toEqual({ caption: 'hello world' });
71
+ });
72
+
73
+ it('allows } inside a quoted value', () => {
74
+ const h = getHeading('## X {caption="a } b"}');
75
+ expect(h.attributes?.params).toEqual({ caption: 'a } b' });
76
+ });
77
+
78
+ it('treats an apostrophe inside an unquoted value as literal', () => {
79
+ const h = getHeading("## X {name=O'Brien x=4}");
80
+ expect(h.attributes?.metadata).toEqual({ name: "O'Brien" });
81
+ expect(h.attributes?.blockMeta?.x).toBe(4);
82
+ });
83
+
84
+ it('duplicate id: last wins', () => {
85
+ const h = getHeading('## X {#a #b}');
86
+ expect(h.attributes?.id).toBe('b');
87
+ });
88
+
89
+ it('duplicate key: last wins', () => {
90
+ const h = getHeading('## X {x=1 x=2}');
91
+ expect(h.attributes?.blockMeta?.x).toBe(2);
92
+ });
93
+
94
+ it('does NOT match a non-trailing brace block', () => {
95
+ const h = getHeading('## The {#wrong} section');
96
+ expect(h.attributes).toBeUndefined();
97
+ });
98
+
99
+ it('does NOT collide with {[…]} template annotation', () => {
100
+ const h = getHeading('## X {[chart]}');
101
+ expect(h.templateAnnotation).toEqual({ template: 'chart' });
102
+ expect(h.attributes).toBeUndefined();
103
+ });
104
+ });
105
+
106
+ describe('Coexistence of {#…} and {[…]}', () => {
107
+ it('Pandoc first, then template', () => {
108
+ const h = getHeading('## X {#abc} {[chart]}');
109
+ expect(h.attributes?.id).toBe('abc');
110
+ expect(h.templateAnnotation?.template).toBe('chart');
111
+ });
112
+
113
+ it('Template first, then Pandoc (order-agnostic)', () => {
114
+ const h = getHeading('## X {[chart]} {#abc}');
115
+ expect(h.attributes?.id).toBe('abc');
116
+ expect(h.templateAnnotation?.template).toBe('chart');
117
+ });
118
+
119
+ it('both with params on each side', () => {
120
+ const h = getHeading('## X {#a x=400 y=200} {[chart colorScheme=blue]}');
121
+ expect(h.attributes?.id).toBe('a');
122
+ expect(h.attributes?.blockMeta?.x).toBe(400);
123
+ expect(h.attributes?.blockMeta?.y).toBe(200);
124
+ expect(h.templateAnnotation?.template).toBe('chart');
125
+ expect(h.templateAnnotation?.params).toEqual({ colorScheme: 'blue' });
126
+ });
127
+
128
+ it('Pandoc block-meta does not leak into templateOverrides', () => {
129
+ const h = getHeading('## X {#a x=400} {[chart colorScheme=blue]}');
130
+ expect(h.templateAnnotation?.params).toEqual({ colorScheme: 'blue' });
131
+ expect(h.attributes?.params).toEqual({ x: '400' });
132
+ });
133
+ });
134
+
135
+ describe('Markdown round-trip with attributes', () => {
136
+ it('Pandoc-only id round-trips', () => {
137
+ const src = '## Section title {#intro}\n';
138
+ const md = parseMarkdown(src);
139
+ expect(stringifyMarkdown(md).trim()).toBe(src.trim());
140
+ });
141
+
142
+ it('canonicalizes order: Pandoc first, then template', () => {
143
+ const input = '## X {[chart]} {#abc}\n';
144
+ const md = parseMarkdown(input);
145
+ const out = stringifyMarkdown(md).trim();
146
+ expect(out).toBe('## X {#abc} {[chart]}');
147
+ });
148
+
149
+ it('full attribute set round-trips', () => {
150
+ const src =
151
+ '## Architecture {#arch x=600 y=300 connectsTo=foo:veryImportant,bar:requires,baz:requires} {[diagramNode]}\n';
152
+ const md = parseMarkdown(src);
153
+ const out = stringifyMarkdown(md).trim();
154
+ expect(out).toBe(src.trim());
155
+ });
156
+
157
+ it('quoted value round-trips with quoting preserved', () => {
158
+ const src = '## X {caption="hello world"}\n';
159
+ const md = parseMarkdown(src);
160
+ const out = stringifyMarkdown(md).trim();
161
+ expect(out).toBe(src.trim());
162
+ });
163
+
164
+ it('existing {[…]}-only docs are unchanged', () => {
165
+ const src = '## Getting Started {[chart colorScheme=blue]}\n';
166
+ const md = parseMarkdown(src);
167
+ const out = stringifyMarkdown(md).trim();
168
+ expect(out).toBe(src.trim());
169
+ });
170
+
171
+ it('single-quoted values canonicalize to double quotes', () => {
172
+ const md = parseMarkdown("## X {caption='hello world'}");
173
+ expect(stringifyMarkdown(md).trim()).toBe('## X {caption="hello world"}');
174
+ });
175
+
176
+ it('a value containing a double quote round-trips via single quotes', () => {
177
+ const src = '## X {label=\'say "hi"\'}';
178
+ const md = parseMarkdown(src);
179
+ expect(stringifyMarkdown(md).trim()).toBe(src);
180
+ });
181
+
182
+ it('a quoted } round-trips', () => {
183
+ const src = '## X {caption="a } b"}';
184
+ const md = parseMarkdown(src);
185
+ expect(stringifyMarkdown(md).trim()).toBe(src);
186
+ });
187
+ });
@@ -0,0 +1,55 @@
1
+ /**
2
+ * resolveThemeForDoc — pure, doc-scoped theme resolution.
3
+ *
4
+ * The theme analog of buildRegistry: an inline custom theme resolves from the
5
+ * doc's own `customThemes` list, with no global `registerTheme` call. Falls
6
+ * back to built-ins for non-inline ids.
7
+ */
8
+
9
+ import { describe, it, expect } from 'vitest';
10
+ import { resolveThemeForDoc } from '../doc/resolveDocTheme';
11
+ import { compileTheme } from '../schemas/themeCompile';
12
+ import { DEFAULT_THEME } from '../schemas/themeLibrary';
13
+ import type { Doc } from '../schemas/Doc.js';
14
+
15
+ const brand = compileTheme({
16
+ id: 'my-brand',
17
+ name: 'My Brand',
18
+ seedColors: { primary: '#3182ce' },
19
+ });
20
+
21
+ function docWith(partial: Partial<Doc>): Doc {
22
+ return {
23
+ articleId: 'd',
24
+ duration: 0,
25
+ blocks: [],
26
+ audio: { segments: [] },
27
+ ...partial,
28
+ } as Doc;
29
+ }
30
+
31
+ describe('resolveThemeForDoc', () => {
32
+ it('resolves an inline custom theme doc-scoped (no global registration)', () => {
33
+ const doc = docWith({ customThemes: [brand], frontmatter: { 'squisq-theme': 'my-brand' } });
34
+ expect(resolveThemeForDoc(doc)).toEqual(brand);
35
+ });
36
+
37
+ it('honors an explicit id over the frontmatter selection', () => {
38
+ const doc = docWith({ customThemes: [brand], frontmatter: { 'squisq-theme': 'cinematic' } });
39
+ expect(resolveThemeForDoc(doc, 'my-brand')).toEqual(brand);
40
+ });
41
+
42
+ it('falls back to a built-in when the id is not an inline theme', () => {
43
+ const doc = docWith({ customThemes: [brand], frontmatter: { 'squisq-theme': 'cinematic' } });
44
+ expect(resolveThemeForDoc(doc).id).toBe('cinematic');
45
+ });
46
+
47
+ it('uses Doc.themeId when there is no explicit or frontmatter id', () => {
48
+ const doc = docWith({ customThemes: [brand], themeId: 'my-brand' });
49
+ expect(resolveThemeForDoc(doc)).toEqual(brand);
50
+ });
51
+
52
+ it('returns the default theme for a null doc', () => {
53
+ expect(resolveThemeForDoc(null).id).toBe(DEFAULT_THEME.id);
54
+ });
55
+ });
@@ -0,0 +1,43 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import * as root from '../index';
3
+ import * as icons from '../icons/index';
4
+ import * as transform from '../transform/index';
5
+
6
+ const transformRuntimeExports = [
7
+ 'applyTransform',
8
+ 'resolveTransformStyle',
9
+ 'getTransformStyleIds',
10
+ 'getTransformStyleSummaries',
11
+ 'DEFAULT_TRANSFORM_STYLE_ID',
12
+ 'analyzeBlocks',
13
+ 'extractDocImages',
14
+ ] as const;
15
+
16
+ const iconRuntimeExports = [
17
+ 'ICONS',
18
+ 'resolveIcon',
19
+ 'canonicalIconToken',
20
+ 'looksLikeIconToken',
21
+ 'suggestIcons',
22
+ 'iconGlyph',
23
+ ] as const;
24
+
25
+ function expectReExported(
26
+ rootExports: Record<string, unknown>,
27
+ subpathExports: Record<string, unknown>,
28
+ names: readonly string[],
29
+ ): void {
30
+ for (const name of names) {
31
+ expect(rootExports[name], `root barrel should re-export ${name}`).toBe(subpathExports[name]);
32
+ }
33
+ }
34
+
35
+ describe('root barrel exports', () => {
36
+ it('re-exports transform runtime APIs', () => {
37
+ expectReExported(root, transform, transformRuntimeExports);
38
+ });
39
+
40
+ it('re-exports icon runtime APIs', () => {
41
+ expectReExported(root, icons, iconRuntimeExports);
42
+ });
43
+ });
@@ -0,0 +1,89 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import {
3
+ shapePath,
4
+ markerPath,
5
+ connectorPath,
6
+ clipEndpoints,
7
+ lineStyleDasharray,
8
+ PATH_SHAPE_KINDS,
9
+ } from '../doc/utils/shapeGeometry.js';
10
+
11
+ describe('shapePath', () => {
12
+ it('returns a non-empty path beginning with M for every path-shape kind', () => {
13
+ for (const kind of PATH_SHAPE_KINDS) {
14
+ const d = shapePath(kind, 0, 0, 100, 80);
15
+ expect(d, kind).toBeTruthy();
16
+ expect(d?.startsWith('M'), kind).toBe(true);
17
+ }
18
+ });
19
+
20
+ it('returns null for natively-rendered primitives', () => {
21
+ for (const kind of ['rect', 'circle', 'line', 'text', 'path', 'arrow']) {
22
+ expect(shapePath(kind, 0, 0, 10, 10), kind).toBeNull();
23
+ }
24
+ });
25
+
26
+ it('triangle is a closed 3-point polygon inscribed in the box', () => {
27
+ expect(shapePath('triangle', 0, 0, 100, 100)).toBe('M 50 0 L 100 100 L 0 100 Z');
28
+ });
29
+
30
+ it('diamond hits the four edge midpoints', () => {
31
+ expect(shapePath('diamond', 0, 0, 100, 100)).toBe('M 50 0 L 100 50 L 50 100 L 0 50 Z');
32
+ });
33
+
34
+ it('honors the bounding-box offset', () => {
35
+ expect(shapePath('triangle', 10, 20, 100, 100)).toBe('M 60 20 L 110 120 L 10 120 Z');
36
+ });
37
+ });
38
+
39
+ describe('markerPath', () => {
40
+ it('arrow end is a filled triangle; none is null', () => {
41
+ expect(markerPath('arrow', 'end')).toEqual({ d: 'M 0 0 L 10 5 L 0 10 z', filled: true });
42
+ expect(markerPath('none', 'end')).toBeNull();
43
+ });
44
+
45
+ it('open is unfilled; diamond/circle/square are filled', () => {
46
+ expect(markerPath('open', 'end')?.filled).toBe(false);
47
+ expect(markerPath('diamond', 'end')?.filled).toBe(true);
48
+ expect(markerPath('circle', 'end')?.filled).toBe(true);
49
+ expect(markerPath('square', 'end')?.filled).toBe(true);
50
+ });
51
+
52
+ it('start mirrors the arrow horizontally', () => {
53
+ expect(markerPath('arrow', 'start')).toEqual({ d: 'M 10 0 L 0 5 L 10 10 z', filled: true });
54
+ });
55
+ });
56
+
57
+ describe('connectorPath', () => {
58
+ const a = { x: 0, y: 0 };
59
+ const b = { x: 100, y: 50 };
60
+
61
+ it('straight is a single line segment', () => {
62
+ expect(connectorPath('straight', a, b)).toBe('M 0 0 L 100 50');
63
+ });
64
+
65
+ it('orthogonal has a mid elbow', () => {
66
+ expect(connectorPath('orthogonal', a, b)).toBe('M 0 0 L 50 0 L 50 50 L 100 50');
67
+ });
68
+
69
+ it('curved is a cubic bezier', () => {
70
+ expect(connectorPath('curved', a, b).startsWith('M 0 0 C')).toBe(true);
71
+ });
72
+ });
73
+
74
+ describe('clipEndpoints', () => {
75
+ it('clips both endpoints to the box edges', () => {
76
+ const r = clipEndpoints({ cx: 0, cy: 0, rx: 10, ry: 10 }, { cx: 100, cy: 0, rx: 10, ry: 10 });
77
+ expect(r.start).toEqual({ x: 10, y: 0 });
78
+ expect(r.end).toEqual({ x: 90, y: 0 });
79
+ });
80
+ });
81
+
82
+ describe('lineStyleDasharray', () => {
83
+ it('maps line styles to dash patterns', () => {
84
+ expect(lineStyleDasharray('dashed')).toBe('8 6');
85
+ expect(lineStyleDasharray('dotted')).toBe('2 6');
86
+ expect(lineStyleDasharray('solid')).toBeUndefined();
87
+ expect(lineStyleDasharray(undefined)).toBeUndefined();
88
+ });
89
+ });