@bendyline/squisq 1.5.1 → 2.0.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.
Files changed (268) hide show
  1. package/README.md +63 -10
  2. package/dist/{Doc-BBVGq1_9.d.ts → Doc-ylqWKDc_.d.ts} +248 -37
  3. package/dist/{ImageEditDoc-FuyGtt6o.d.ts → ImageEditDoc-D828IXJ1.d.ts} +1 -1
  4. package/dist/annotationCoercion-Dr_DY9Hx.d.ts +86 -0
  5. package/dist/{chunk-VWUFZ6ZG.js → chunk-3E5F2XMR.js} +2 -29
  6. package/dist/chunk-3E5F2XMR.js.map +1 -0
  7. package/dist/chunk-4X3JQXNM.js +35 -0
  8. package/dist/chunk-4X3JQXNM.js.map +1 -0
  9. package/dist/{chunk-2OIBZYKP.js → chunk-5DKBFXPY.js} +31 -121
  10. package/dist/chunk-5DKBFXPY.js.map +1 -0
  11. package/dist/{chunk-DLXZMS5K.js → chunk-5J4HKRJF.js} +51 -3
  12. package/dist/chunk-5J4HKRJF.js.map +1 -0
  13. package/dist/chunk-5UYI3RJB.js +2503 -0
  14. package/dist/chunk-5UYI3RJB.js.map +1 -0
  15. package/dist/{chunk-TEEEILMP.js → chunk-66QO7XFB.js} +19 -292
  16. package/dist/chunk-66QO7XFB.js.map +1 -0
  17. package/dist/{chunk-KKNUBQ6Y.js → chunk-6PJRB2CF.js} +156 -91
  18. package/dist/chunk-6PJRB2CF.js.map +1 -0
  19. package/dist/{chunk-HWVFJAAH.js → chunk-AMIR72JP.js} +28 -10
  20. package/dist/chunk-AMIR72JP.js.map +1 -0
  21. package/dist/chunk-FVJUUTEM.js +74 -0
  22. package/dist/chunk-FVJUUTEM.js.map +1 -0
  23. package/dist/{chunk-LH7I6SH7.js → chunk-GF3BVNLU.js} +6360 -5723
  24. package/dist/chunk-GF3BVNLU.js.map +1 -0
  25. package/dist/{chunk-Q3ROPT5H.js → chunk-GZY66SDN.js} +1 -1
  26. package/dist/{chunk-Q3ROPT5H.js.map → chunk-GZY66SDN.js.map} +1 -1
  27. package/dist/chunk-JOQYPRDZ.js +127 -0
  28. package/dist/chunk-JOQYPRDZ.js.map +1 -0
  29. package/dist/{chunk-A4O7GIWE.js → chunk-LYNJEOAC.js} +10 -73
  30. package/dist/chunk-LYNJEOAC.js.map +1 -0
  31. package/dist/{chunk-LDQ2HJIX.js → chunk-PXLVXTLW.js} +252 -54
  32. package/dist/chunk-PXLVXTLW.js.map +1 -0
  33. package/dist/{chunk-TQWLI6S2.js → chunk-Q7LROV2H.js} +4 -2
  34. package/dist/{chunk-TQWLI6S2.js.map → chunk-Q7LROV2H.js.map} +1 -1
  35. package/dist/{chunk-T5UK6YOB.js → chunk-QTSJOY54.js} +60 -17
  36. package/dist/chunk-QTSJOY54.js.map +1 -0
  37. package/dist/chunk-QVANWDPH.js +2358 -0
  38. package/dist/chunk-QVANWDPH.js.map +1 -0
  39. package/dist/{chunk-6ATE2PSM.js → chunk-R2GC6RLD.js} +7 -7
  40. package/dist/chunk-R2GC6RLD.js.map +1 -0
  41. package/dist/chunk-R2YZTCLO.js +292 -0
  42. package/dist/chunk-R2YZTCLO.js.map +1 -0
  43. package/dist/chunk-S7A5D7ZJ.js +174 -0
  44. package/dist/chunk-S7A5D7ZJ.js.map +1 -0
  45. package/dist/chunk-T3TI7GSZ.js +884 -0
  46. package/dist/chunk-T3TI7GSZ.js.map +1 -0
  47. package/dist/{chunk-RUDYOTA6.js → chunk-TUWMK62B.js} +110 -4
  48. package/dist/chunk-TUWMK62B.js.map +1 -0
  49. package/dist/{chunk-IMSCRLLV.js → chunk-VR7UPJ7C.js} +35 -25
  50. package/dist/chunk-VR7UPJ7C.js.map +1 -0
  51. package/dist/doc/index.d.ts +2334 -5
  52. package/dist/doc/index.js +134 -34
  53. package/dist/generate/index.d.ts +7 -2
  54. package/dist/generate/index.js +1 -1
  55. package/dist/imageEdit/index.d.ts +9 -6
  56. package/dist/imageEdit/index.js +3 -3
  57. package/dist/index.d.ts +12 -10
  58. package/dist/index.js +253 -81
  59. package/dist/jsonForm/index.d.ts +7 -3
  60. package/dist/jsonForm/index.js +9 -4
  61. package/dist/markdown/index.d.ts +6 -70
  62. package/dist/markdown/index.js +13 -9
  63. package/dist/narration/index.d.ts +618 -0
  64. package/dist/narration/index.js +82 -0
  65. package/dist/recommend/index.d.ts +15 -1
  66. package/dist/recommend/index.js +2 -2
  67. package/dist/schemas/index.d.ts +28 -6
  68. package/dist/schemas/index.js +15 -13
  69. package/dist/spatial/index.d.ts +0 -1
  70. package/dist/spatial/index.js +1 -1
  71. package/dist/storage/index.js +1 -1
  72. package/dist/{themeLibrary-CehcJhzz.d.ts → themeLibrary-RjVgLpUi.d.ts} +10 -11
  73. package/dist/timing/index.js +5 -3
  74. package/dist/transform/index.d.ts +35 -24
  75. package/dist/transform/index.js +9 -10
  76. package/dist/{types-DlAZ7MW4.d.ts → types-BwMPl2CB.d.ts} +1 -1
  77. package/dist/{types-D98li1gV.d.ts → types-C4kiRn_k.d.ts} +3 -1
  78. package/dist/versions/index.d.ts +2 -1
  79. package/dist/versions/index.js +2 -2
  80. package/package.json +6 -6
  81. package/src/__tests__/applyNarrationTiming.test.ts +190 -0
  82. package/src/__tests__/applyRenderStyle.test.ts +3 -3
  83. package/src/__tests__/asciiDiagramBattle.test.ts +246 -0
  84. package/src/__tests__/asciiDiagramDetect.test.ts +162 -0
  85. package/src/__tests__/asciiDiagramMapping.test.ts +38 -0
  86. package/src/__tests__/asciiDiagramParse.test.ts +233 -0
  87. package/src/__tests__/asciiDiagramPipeline.test.ts +273 -0
  88. package/src/__tests__/asciiDiagramRender.test.ts +162 -0
  89. package/src/__tests__/asciiDiagramRepair.test.ts +84 -0
  90. package/src/__tests__/asciiTimelineDetect.test.ts +157 -0
  91. package/src/__tests__/asciiTimelineParse.test.ts +205 -0
  92. package/src/__tests__/asciiTimelinePipeline.test.ts +349 -0
  93. package/src/__tests__/asciiTimelineRender.test.ts +376 -0
  94. package/src/__tests__/attrTokens.test.ts +5 -0
  95. package/src/__tests__/contentContainer.test.ts +43 -0
  96. package/src/__tests__/coverBlock.test.ts +33 -2
  97. package/src/__tests__/customTemplatesFrontmatter.test.ts +0 -15
  98. package/src/__tests__/dataTable.test.ts +25 -15
  99. package/src/__tests__/diagramBlock.test.ts +3 -3
  100. package/src/__tests__/diagramText.test.ts +26 -0
  101. package/src/__tests__/docToMarkdownMedia.test.ts +83 -0
  102. package/src/__tests__/drawingBlock.test.ts +3 -3
  103. package/src/__tests__/fixtures/asciiDiagramBattle.ts +512 -0
  104. package/src/__tests__/fixtures/asciiDiagrams.ts +287 -0
  105. package/src/__tests__/fixtures/asciiTimelines.ts +35 -0
  106. package/src/__tests__/fixtures/treeviewFixtures.ts +243 -0
  107. package/src/__tests__/getLayers.test.ts +32 -18
  108. package/src/__tests__/getLayersFallback.test.ts +8 -8
  109. package/src/__tests__/imageEditVersions.test.ts +2 -0
  110. package/src/__tests__/jsonForm.pointer.test.ts +47 -1
  111. package/src/__tests__/layoutBlock.test.ts +3 -3
  112. package/src/__tests__/majorApiTypes.test.ts +18 -0
  113. package/src/__tests__/markdown.test.ts +41 -0
  114. package/src/__tests__/markdownToDoc.test.ts +5 -2
  115. package/src/__tests__/matchFontFamily.test.ts +114 -0
  116. package/src/__tests__/materializeBlockLayers.test.ts +135 -0
  117. package/src/__tests__/materializeTestUtils.ts +17 -0
  118. package/src/__tests__/narrationAlign.test.ts +150 -0
  119. package/src/__tests__/narrationFeatures.test.ts +71 -0
  120. package/src/__tests__/narrationNuclei.test.ts +45 -0
  121. package/src/__tests__/narrationPacing.test.ts +189 -0
  122. package/src/__tests__/narrationScript.test.ts +149 -0
  123. package/src/__tests__/narrationSessionRepro.test.ts +62 -0
  124. package/src/__tests__/narrationSidecar.test.ts +88 -0
  125. package/src/__tests__/narrationSyllables.test.ts +65 -0
  126. package/src/__tests__/narrationTestSignals.ts +167 -0
  127. package/src/__tests__/narrationVad.test.ts +66 -0
  128. package/src/__tests__/resolveDocTheme.test.ts +16 -2
  129. package/src/__tests__/rootExports.test.ts +18 -0
  130. package/src/__tests__/scopedContentContainer.test.ts +20 -0
  131. package/src/__tests__/shapeGeometry.test.ts +49 -0
  132. package/src/__tests__/templateAnnotationParse.test.ts +10 -0
  133. package/src/__tests__/templateInputs.test.ts +69 -0
  134. package/src/__tests__/templateMetadata.test.ts +2 -1
  135. package/src/__tests__/templates.test.ts +143 -17
  136. package/src/__tests__/themeCompile.test.ts +49 -22
  137. package/src/__tests__/transformProvenance.test.ts +160 -0
  138. package/src/__tests__/transformV2.test.ts +40 -14
  139. package/src/__tests__/treeviewBattle.test.ts +76 -0
  140. package/src/__tests__/treeviewDetect.test.ts +168 -0
  141. package/src/__tests__/treeviewParse.test.ts +118 -0
  142. package/src/__tests__/treeviewPipeline.test.ts +183 -0
  143. package/src/__tests__/treeviewRender.test.ts +91 -0
  144. package/src/__tests__/versions.test.ts +44 -0
  145. package/src/doc/applyNarrationTiming.ts +244 -0
  146. package/src/doc/asciiDiagram/chars.ts +210 -0
  147. package/src/doc/asciiDiagram/detect.ts +175 -0
  148. package/src/doc/asciiDiagram/index.ts +38 -0
  149. package/src/doc/asciiDiagram/mapping.ts +225 -0
  150. package/src/doc/asciiDiagram/parse.ts +1095 -0
  151. package/src/doc/asciiDiagram/render.ts +963 -0
  152. package/src/doc/asciiDiagram/repair.ts +400 -0
  153. package/src/doc/asciiDiagram/types.ts +65 -0
  154. package/src/doc/asciiTimeline/detect.ts +138 -0
  155. package/src/doc/asciiTimeline/index.ts +26 -0
  156. package/src/doc/asciiTimeline/mapping.ts +43 -0
  157. package/src/doc/asciiTimeline/parse.ts +870 -0
  158. package/src/doc/asciiTimeline/render.ts +427 -0
  159. package/src/doc/asciiTimeline/types.ts +73 -0
  160. package/src/doc/asciiTimeline/wrappedFlow.ts +427 -0
  161. package/src/doc/audioMapping.ts +60 -46
  162. package/src/doc/customTemplatesFrontmatter.ts +0 -41
  163. package/src/doc/docToMarkdown.ts +126 -39
  164. package/src/doc/index.ts +76 -4
  165. package/src/doc/markdownToDoc.ts +135 -1
  166. package/src/doc/materializeBlockLayers.ts +304 -0
  167. package/src/doc/mediaAnnotations.ts +20 -0
  168. package/src/doc/resolveDocTheme.ts +8 -7
  169. package/src/doc/templateInputs.ts +185 -2
  170. package/src/doc/templates/__tests__/customTemplateRegistry.test.ts +22 -8
  171. package/src/doc/templates/coverBlock.ts +49 -13
  172. package/src/doc/templates/diagramBlock.ts +237 -38
  173. package/src/doc/templates/drawingBlock.ts +2 -2
  174. package/src/doc/templates/fallbackBlock.ts +1 -1
  175. package/src/doc/templates/index.ts +134 -279
  176. package/src/doc/templates/inputDescriptors.ts +20 -1
  177. package/src/doc/templates/metadata.ts +10 -0
  178. package/src/doc/templates/persistentLayers.ts +12 -126
  179. package/src/doc/templates/registry.ts +88 -0
  180. package/src/doc/templates/templateNames.ts +22 -0
  181. package/src/doc/templates/timelineBlock.ts +613 -0
  182. package/src/doc/templates/treeBlock.ts +131 -0
  183. package/src/doc/treeview/chars.ts +72 -0
  184. package/src/doc/treeview/detect.ts +145 -0
  185. package/src/doc/treeview/index.ts +30 -0
  186. package/src/doc/treeview/mapping.ts +95 -0
  187. package/src/doc/treeview/parse.ts +201 -0
  188. package/src/doc/treeview/render.ts +41 -0
  189. package/src/doc/treeview/types.ts +43 -0
  190. package/src/doc/utils/applyRenderStyle.ts +2 -9
  191. package/src/doc/utils/diagramText.ts +78 -0
  192. package/src/doc/utils/shapeGeometry.ts +157 -7
  193. package/src/doc/validate.ts +1 -1
  194. package/src/generate/templateMapper.ts +7 -0
  195. package/src/imageEdit/export.ts +6 -5
  196. package/src/imageEdit/versionPaths.ts +13 -26
  197. package/src/imageEdit/versions.ts +21 -100
  198. package/src/index.ts +1 -0
  199. package/src/internal/immutable.ts +58 -0
  200. package/src/jsonForm/index.ts +9 -1
  201. package/src/jsonForm/pointer.ts +68 -23
  202. package/src/markdown/attrTokens.ts +7 -5
  203. package/src/markdown/convert.ts +11 -1
  204. package/src/markdown/index.ts +1 -0
  205. package/src/markdown/stringify.ts +4 -9
  206. package/src/markdown/utils.ts +36 -14
  207. package/src/narration/align.ts +514 -0
  208. package/src/narration/features.ts +231 -0
  209. package/src/narration/index.ts +86 -0
  210. package/src/narration/nuclei.ts +118 -0
  211. package/src/narration/pacing.ts +211 -0
  212. package/src/narration/script.ts +239 -0
  213. package/src/narration/session.ts +100 -0
  214. package/src/narration/sidecar.ts +187 -0
  215. package/src/narration/syllables.ts +85 -0
  216. package/src/narration/trace.ts +50 -0
  217. package/src/narration/types.ts +299 -0
  218. package/src/narration/vad.ts +131 -0
  219. package/src/recommend/numberHighlight.ts +25 -0
  220. package/src/recommend/templates.ts +108 -12
  221. package/src/schemas/BlockTemplates.ts +156 -7
  222. package/src/schemas/Doc.ts +72 -9
  223. package/src/schemas/Media.ts +12 -2
  224. package/src/schemas/Theme.ts +45 -25
  225. package/src/schemas/Viewport.ts +2 -2
  226. package/src/schemas/fontStacks.ts +79 -0
  227. package/src/schemas/themeCompile.ts +15 -0
  228. package/src/schemas/themeConstants.ts +2 -0
  229. package/src/schemas/themeLibrary.ts +25 -19
  230. package/src/schemas/themeValidator.ts +1 -1
  231. package/src/spatial/Geohash.ts +0 -3
  232. package/src/storage/ContentContainer.ts +17 -9
  233. package/src/storage/ScopedContentContainer.ts +45 -15
  234. package/src/transform/applyTransform.ts +5 -5
  235. package/src/transform/index.ts +3 -1
  236. package/src/transform/registry.ts +169 -49
  237. package/src/transform/templateSelector.ts +21 -2
  238. package/src/transform/timingAllocator.ts +152 -21
  239. package/src/transform/types.ts +14 -1
  240. package/src/versions/gitignore.ts +34 -0
  241. package/src/versions/index.ts +2 -0
  242. package/src/versions/operations.ts +18 -104
  243. package/src/versions/paths.ts +13 -28
  244. package/src/versions/snapshotStore.ts +202 -0
  245. package/src/versions/types.ts +3 -1
  246. package/dist/chunk-2OIBZYKP.js.map +0 -1
  247. package/dist/chunk-6ATE2PSM.js.map +0 -1
  248. package/dist/chunk-A4O7GIWE.js.map +0 -1
  249. package/dist/chunk-DLXZMS5K.js.map +0 -1
  250. package/dist/chunk-HWVFJAAH.js.map +0 -1
  251. package/dist/chunk-IMSCRLLV.js.map +0 -1
  252. package/dist/chunk-JELIRVDP.js +0 -24
  253. package/dist/chunk-JELIRVDP.js.map +0 -1
  254. package/dist/chunk-KKNUBQ6Y.js.map +0 -1
  255. package/dist/chunk-LDQ2HJIX.js.map +0 -1
  256. package/dist/chunk-LH7I6SH7.js.map +0 -1
  257. package/dist/chunk-RUDYOTA6.js.map +0 -1
  258. package/dist/chunk-T5UK6YOB.js.map +0 -1
  259. package/dist/chunk-TEEEILMP.js.map +0 -1
  260. package/dist/chunk-UY7KGQ5R.js +0 -192
  261. package/dist/chunk-UY7KGQ5R.js.map +0 -1
  262. package/dist/chunk-VLJJHDAC.js +0 -218
  263. package/dist/chunk-VLJJHDAC.js.map +0 -1
  264. package/dist/chunk-VWUFZ6ZG.js.map +0 -1
  265. package/dist/story/index.d.ts +0 -1777
  266. package/dist/story/index.js +0 -273
  267. package/src/doc/getLayers.ts +0 -195
  268. /package/dist/{story → narration}/index.js.map +0 -0
@@ -0,0 +1,160 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { parseMarkdown } from '../markdown/parse';
3
+ import { markdownToDoc, flattenRenderableBlocks } from '../doc/markdownToDoc';
4
+ import { applyTransform } from '../transform/applyTransform';
5
+ import { allocateTiming } from '../transform/timingAllocator';
6
+ import { isTemplateBlock, type TemplateBlock } from '../schemas/BlockTemplates';
7
+ import type { Block, Doc } from '../schemas/Doc';
8
+
9
+ const MD = `# The Numbers
10
+
11
+ Revenue grew to 4,200 units in 2019 which was a big jump for the small team.
12
+ "We never expected this kind of growth," said the founder about the year.
13
+
14
+ # The Turning Point
15
+
16
+ By 2021 the company had 87 employees across three offices and a plan.
17
+ The expansion cost 1,500,000 dollars and took fourteen months to finish.
18
+
19
+ # The Result
20
+
21
+ Today the product ships to 40 countries and the team keeps growing steadily.
22
+ `;
23
+
24
+ function makeDoc(): Doc {
25
+ return markdownToDoc(parseMarkdown(MD));
26
+ }
27
+
28
+ /** Blocks a transform produced (positional transform-* ids). */
29
+ function transformedBlocks(doc: Doc): Block[] {
30
+ return doc.blocks.filter((b) => b.id.startsWith('transform-'));
31
+ }
32
+
33
+ describe('transform provenance', () => {
34
+ it('every extraction-derived slide carries a sourceBlockId from the input doc', () => {
35
+ const source = makeDoc();
36
+ const sourceIds = new Set(flattenRenderableBlocks(source.blocks).map((b) => b.id));
37
+ const { doc } = applyTransform(source, 'data-driven');
38
+
39
+ const produced = transformedBlocks(doc).filter(
40
+ (b) => b.id !== 'transform-intro' && b.id !== 'transform-outro',
41
+ );
42
+ expect(produced.length).toBeGreaterThan(0);
43
+ for (const block of produced) {
44
+ expect(block.sourceBlockId, `block ${block.id} missing provenance`).toBeTruthy();
45
+ expect(sourceIds.has(block.sourceBlockId!)).toBe(true);
46
+ }
47
+ });
48
+
49
+ it('sourceStartTime is SECONDS on the doc timeline, not a char offset (regression)', () => {
50
+ const source = makeDoc();
51
+ const sourceEnd = Math.max(
52
+ ...flattenRenderableBlocks(source.blocks).map((b) => b.startTime + b.duration),
53
+ );
54
+ const { doc } = applyTransform(source, 'data-driven');
55
+
56
+ for (const block of doc.blocks) {
57
+ const tb = block as unknown as TemplateBlock;
58
+ if (typeof tb.sourceStartTime !== 'number') continue;
59
+ // The old bug put char offsets (hundreds) here; real seconds must
60
+ // stay inside the source timeline.
61
+ expect(tb.sourceStartTime).toBeLessThanOrEqual(sourceEnd + 1);
62
+ expect(tb.sourceStartTime).toBeGreaterThanOrEqual(0);
63
+ // The raw char offset is preserved separately as provenance.
64
+ expect(typeof tb.sourceCharOffset).toBe('number');
65
+ }
66
+ });
67
+
68
+ it('narration-timed source blocks anchor their slides inside the source range', () => {
69
+ const source = makeDoc();
70
+ // Simulate applyNarrationTiming: give the source blocks recorded times.
71
+ const flat = flattenRenderableBlocks(source.blocks);
72
+ const bounds = [0, 20, 45, 70];
73
+ flat.forEach((block, i) => {
74
+ block.startTime = bounds[i] ?? 70;
75
+ block.duration = (bounds[i + 1] ?? 70) - (bounds[i] ?? 70);
76
+ });
77
+ source.duration = 70;
78
+
79
+ const { doc } = applyTransform(source, 'data-driven');
80
+ const bySource = new Map(flat.map((b) => [b.id, b]));
81
+ let anchoredChecked = 0;
82
+ for (const block of transformedBlocks(doc)) {
83
+ // Only ANCHORED slides (real narration seconds) must sit inside
84
+ // their source range; floats (section headers, images) are placed
85
+ // approximately by design.
86
+ const tb = block as unknown as TemplateBlock;
87
+ if (!block.sourceBlockId || typeof tb.sourceStartTime !== 'number') continue;
88
+ const origin = bySource.get(block.sourceBlockId);
89
+ if (!origin || origin.duration <= 0) continue;
90
+ expect(block.startTime).toBeGreaterThanOrEqual(origin.startTime - 1e-6);
91
+ expect(block.startTime).toBeLessThan(origin.startTime + origin.duration + 1e-6);
92
+ anchoredChecked++;
93
+ }
94
+ expect(anchoredChecked).toBeGreaterThan(0);
95
+ });
96
+
97
+ it('transformed timelines stay contiguous (no uncovered narration seconds)', () => {
98
+ const source = makeDoc();
99
+ const { doc } = applyTransform(source, 'data-driven');
100
+ const sorted = [...doc.blocks].sort((a, b) => a.startTime - b.startTime);
101
+ for (let i = 0; i < sorted.length - 1; i++) {
102
+ const end = sorted[i].startTime + sorted[i].duration;
103
+ expect(end).toBeCloseTo(sorted[i + 1].startTime, 5);
104
+ }
105
+ });
106
+ });
107
+
108
+ describe('allocateTiming fallback', () => {
109
+ it('keeps the historical rescale behavior when no anchors exist', () => {
110
+ const floats: TemplateBlock[] = [
111
+ { template: 'sectionHeader', id: 'a', duration: 5, audioSegment: 0, title: 'A' },
112
+ { template: 'sectionHeader', id: 'b', duration: 5, audioSegment: 0, title: 'B' },
113
+ ];
114
+ const result = allocateTiming(floats, 20);
115
+ expect(result[0].startTime).toBe(0);
116
+ expect(result[0].duration).toBe(10);
117
+ expect(result[1].startTime).toBe(10);
118
+ expect(result[1].duration).toBe(10);
119
+ });
120
+
121
+ it('anchored blocks keep their positions; floats fill the gaps', () => {
122
+ const blocks: TemplateBlock[] = [
123
+ // A floating intro before the first anchor.
124
+ { template: 'title', id: 'intro', duration: 4, audioSegment: 0, title: 'T' },
125
+ {
126
+ template: 'statHighlight',
127
+ id: 's1',
128
+ duration: 6,
129
+ audioSegment: 0,
130
+ stat: '42',
131
+ description: 'd',
132
+ sourceStartTime: 10,
133
+ sourceDuration: 6,
134
+ },
135
+ {
136
+ template: 'statHighlight',
137
+ id: 's2',
138
+ duration: 6,
139
+ audioSegment: 0,
140
+ stat: '87',
141
+ description: 'd',
142
+ sourceStartTime: 30,
143
+ sourceDuration: 6,
144
+ },
145
+ ];
146
+ const result = allocateTiming(blocks, 40);
147
+ // Intro fills [0, 10) up to the first anchor.
148
+ expect(result[0].startTime).toBe(0);
149
+ expect(result[0].duration).toBeCloseTo(10, 5);
150
+ // Anchors sit at their narration positions; s1 holds until s2 starts.
151
+ expect(result[1].startTime).toBe(10);
152
+ expect(result[1].duration).toBeCloseTo(20, 5);
153
+ expect(result[2].startTime).toBe(30);
154
+ // The final block covers through the total duration.
155
+ expect(result[2].startTime + result[2].duration).toBeCloseTo(40, 5);
156
+ for (const b of result) {
157
+ expect(isTemplateBlock(b as unknown as TemplateBlock) || true).toBe(true);
158
+ }
159
+ });
160
+ });
@@ -1,10 +1,9 @@
1
- import { describe, it, expect, afterEach } from 'vitest';
1
+ import { describe, it, expect } from 'vitest';
2
2
  import {
3
3
  applyTransform,
4
4
  resolveTransformStyle,
5
- registerTransformStyle,
6
- unregisterTransformStyle,
7
5
  getTransformStyleIds,
6
+ createTransformStyleRegistry,
8
7
  } from '../transform/index.js';
9
8
  import type { TransformStyleConfig } from '../transform/index.js';
10
9
  import type { Doc, Block } from '../schemas/Doc.js';
@@ -49,8 +48,6 @@ function sampleDoc(): Doc {
49
48
  }
50
49
 
51
50
  describe('transform style contract v2', () => {
52
- afterEach(() => unregisterTransformStyle('custom-test'));
53
-
54
51
  it('suggestedThemeId applies only when the doc declares no theme', () => {
55
52
  const { doc } = applyTransform(sampleDoc(), 'magazine');
56
53
  expect(doc.themeId).toBe('magazine');
@@ -88,23 +85,53 @@ describe('transform style contract v2', () => {
88
85
  expect(last.template).toBe('sectionHeader');
89
86
  });
90
87
 
91
- it('registerTransformStyle adds a resolvable style that wins over unknown fallback', () => {
88
+ it('resolves custom styles only through their caller-owned registry', () => {
92
89
  const custom: TransformStyleConfig = {
93
90
  ...resolveTransformStyle('minimal'),
94
91
  id: 'custom-test',
95
92
  name: 'Custom Test',
96
93
  description: 'test style',
97
94
  };
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)
95
+ const registry = createTransformStyleRegistry([custom]);
96
+ expect(getTransformStyleIds(registry)).toContain('custom-test');
97
+ expect(resolveTransformStyle('custom-test', registry).name).toBe('Custom Test');
103
98
  expect(resolveTransformStyle('custom-test').id).toBe('documentary');
99
+ expect(
100
+ applyTransform(sampleDoc(), 'custom-test', { registry }).doc.blocks.length,
101
+ ).toBeGreaterThan(0);
104
102
  });
105
103
 
106
- it("accepts 'dataDriven' as an alias for the hyphenated registry id", () => {
104
+ it('normalizes the stored legacy dataDriven id without advertising it', () => {
105
+ expect(resolveTransformStyle('data-driven').id).toBe('data-driven');
107
106
  expect(resolveTransformStyle('dataDriven').id).toBe('data-driven');
107
+ expect(getTransformStyleIds()).not.toContain('dataDriven');
108
+ });
109
+
110
+ it('validates and stores immutable style snapshots in isolated registries', () => {
111
+ const base = resolveTransformStyle('minimal');
112
+ const source: TransformStyleConfig = {
113
+ ...base,
114
+ id: 'tenant-style',
115
+ name: 'Tenant Style',
116
+ preferredTypes: [...base.preferredTypes],
117
+ colorSchemes: [...base.colorSchemes],
118
+ blocksPerSection: { ...base.blocksPerSection },
119
+ };
120
+ const tenantA = createTransformStyleRegistry([source]);
121
+ const tenantB = createTransformStyleRegistry();
122
+ source.name = 'Changed later';
123
+ source.colorSchemes[0] = 'red';
124
+
125
+ const stored = tenantA.get('tenant-style')!;
126
+ expect(stored.name).toBe('Tenant Style');
127
+ expect(stored.colorSchemes[0]).toBe(base.colorSchemes[0]);
128
+ expect(Object.isFrozen(stored)).toBe(true);
129
+ expect(Object.isFrozen(stored.colorSchemes)).toBe(true);
130
+ expect(tenantB.get('tenant-style')).toBeUndefined();
131
+
132
+ expect(() => tenantA.register({ ...source, id: 'bad-style', transformRatio: 2 })).toThrow(
133
+ /transformRatio/,
134
+ );
108
135
  });
109
136
 
110
137
  it('budget.slidesPerMinute caps promotions on long docs', () => {
@@ -127,9 +154,8 @@ describe('transform style contract v2', () => {
127
154
  id: 'custom-test',
128
155
  budget: { slidesPerMinute: 2 },
129
156
  };
130
- registerTransformStyle(budgeted);
131
157
  const loose = applyTransform(manyBlocks, 'data-driven');
132
- const tight = applyTransform(manyBlocks, 'custom-test');
158
+ const tight = applyTransform(manyBlocks, budgeted);
133
159
  expect(tight.stats.transformedBlocks).toBeLessThan(loose.stats.transformedBlocks);
134
160
  // 12 blocks x 5s = 60s → 2/minute → at most 2 promotions
135
161
  expect(tight.stats.transformedBlocks).toBeLessThanOrEqual(2);
@@ -0,0 +1,76 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import {
3
+ detectTree,
4
+ parseTree,
5
+ renderTree,
6
+ treeFromTemplateData,
7
+ treeToTemplateData,
8
+ type Tree,
9
+ } from '../doc/treeview/index.js';
10
+ import { TREE_FIXTURES } from './fixtures/treeviewFixtures.js';
11
+
12
+ /**
13
+ * Battle test: a broad corpus of AI-style trees (project scaffolds,
14
+ * monorepos, dependency trees, category/decision outlines, forests) each
15
+ * asserting detect + parse-accuracy + fixpoint + byte-stability + mapping
16
+ * round-trip. Peer to asciiDiagramBattle.test.ts.
17
+ */
18
+
19
+ function flat(t: Tree): string[] {
20
+ const out: string[] = [];
21
+ const walk = (nodes: Tree['roots'], depth: number): void => {
22
+ for (const n of nodes) {
23
+ out.push(`${depth}:${n.label}${n.comment ? ` #${n.comment}` : ''}`);
24
+ walk(n.children, depth + 1);
25
+ }
26
+ };
27
+ walk(t.roots, 0);
28
+ return out;
29
+ }
30
+
31
+ describe('tree battle — every fixture', () => {
32
+ for (const fx of TREE_FIXTURES) {
33
+ describe(`${fx.category}: ${fx.name}`, () => {
34
+ const t = parseTree(fx.art);
35
+
36
+ it('detects as a tree', () => {
37
+ expect(detectTree(fx.art).isTree).toBe(true);
38
+ });
39
+
40
+ it('parses the intended hierarchy', () => {
41
+ expect(flat(t)).toEqual(fx.expect);
42
+ });
43
+
44
+ it('render→parse preserves the semantic (fixpoint)', () => {
45
+ expect(flat(parseTree(renderTree(t)))).toEqual(flat(t));
46
+ });
47
+
48
+ it('render→parse→render is byte-stable across three cycles', () => {
49
+ const t1 = renderTree(t);
50
+ const t2 = renderTree(parseTree(t1));
51
+ const t3 = renderTree(parseTree(t2));
52
+ expect(t2).toBe(t1);
53
+ expect(t3).toBe(t1);
54
+ });
55
+
56
+ it('templateData round-trips', () => {
57
+ const back = treeFromTemplateData(treeToTemplateData(t).items, { style: t.style });
58
+ expect(flat(back)).toEqual(flat(t));
59
+ });
60
+ });
61
+ }
62
+ });
63
+
64
+ describe('tree battle — coverage sanity', () => {
65
+ it('covers every intended category', () => {
66
+ const categories = new Set(TREE_FIXTURES.map((f) => f.category));
67
+ for (const c of ['scaffold', 'style', 'comments', 'outline', 'shape']) {
68
+ expect(categories.has(c)).toBe(true);
69
+ }
70
+ });
71
+
72
+ it('fixture names are unique', () => {
73
+ const names = TREE_FIXTURES.map((f) => f.name);
74
+ expect(new Set(names).size).toBe(names.length);
75
+ });
76
+ });
@@ -0,0 +1,168 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import {
3
+ detectTree,
4
+ isTreeFence,
5
+ isEligibleTreeFenceLang,
6
+ isExplicitTreeLang,
7
+ } from '../doc/treeview/index.js';
8
+ import { detectAsciiDiagram, isAsciiDiagramFence } from '../doc/asciiDiagram/index.js';
9
+ import type { MarkdownCodeBlock } from '../markdown/types.js';
10
+ import { TREE_FIXTURES } from './fixtures/treeviewFixtures.js';
11
+
12
+ const NEGATIVES: Record<string, string> = {
13
+ boxDiagram: [
14
+ '┌────────┐',
15
+ '│ Alpha │',
16
+ '└───┬────┘',
17
+ ' │',
18
+ ' ▼',
19
+ '┌────────┐',
20
+ '│ Beta │',
21
+ '└────────┘',
22
+ ].join('\n'),
23
+ // A hand-drawn box diagram whose right borders are misaligned (e.g. a
24
+ // rename widened the labels), so NO box traces as a closed rectangle. It
25
+ // must still be rejected as a tree — the top corners (┌ ┐) give it away.
26
+ misalignedBoxDiagram: [
27
+ '┌─────────────────┐',
28
+ '│ @scope/tooling │',
29
+ '│ CLI + MCP server │',
30
+ '└───────┬─────────┘',
31
+ ' │',
32
+ ' ▼',
33
+ '┌───────┴──┐ ┌──────────┐',
34
+ '│ kernel │ │ client │',
35
+ '└──────────┘ └──────────┘',
36
+ ].join('\n'),
37
+ markdownTable: ['| Name | Role |', '|-------|----------|', '| Ada | Engineer |'].join(
38
+ '\n',
39
+ ),
40
+ prose: [
41
+ 'This is a paragraph of ordinary text.',
42
+ 'It has multiple lines but no tree structure at all.',
43
+ 'Nothing here should look like a hierarchy.',
44
+ ].join('\n'),
45
+ indentedCode: [
46
+ 'function foo() {',
47
+ ' const x = 1;',
48
+ ' if (x) {',
49
+ ' return x;',
50
+ ' }',
51
+ '}',
52
+ ].join('\n'),
53
+ yaml: ['services:', ' web:', ' image: nginx', ' ports:', ' - "80:80"'].join('\n'),
54
+ singleLine: 'just one line',
55
+ };
56
+
57
+ describe('detectTree — negative corpus (must all reject)', () => {
58
+ for (const [name, art] of Object.entries(NEGATIVES)) {
59
+ it(`rejects ${name}`, () => {
60
+ const d = detectTree(art);
61
+ expect(d.isTree).toBe(false);
62
+ expect(d.reasons.length).toBeGreaterThan(0);
63
+ });
64
+ }
65
+
66
+ it('rejects a misaligned box diagram via the corner signal, not the box count', () => {
67
+ // No box traces closed (misaligned), so the ≥2-box rule can't catch it —
68
+ // the corner rejector is what keeps it out of the tree widget.
69
+ const d = detectTree(NEGATIVES.misalignedBoxDiagram);
70
+ expect(d.isTree).toBe(false);
71
+ expect(d.reasons).toContain('has-box-corners');
72
+ });
73
+
74
+ it('rejects box-corner art even when explicitly `tree`-tagged', () => {
75
+ expect(detectTree(NEGATIVES.misalignedBoxDiagram, { explicit: true }).isTree).toBe(false);
76
+ expect(detectTree(NEGATIVES.boxDiagram, { explicit: true }).isTree).toBe(false);
77
+ });
78
+ });
79
+
80
+ describe('detectTree — positive fixtures (must all accept)', () => {
81
+ for (const fx of TREE_FIXTURES) {
82
+ it(`accepts ${fx.name}`, () => {
83
+ const d = detectTree(fx.art);
84
+ expect(d.isTree).toBe(true);
85
+ expect(d.tree).toBeDefined();
86
+ });
87
+ }
88
+ });
89
+
90
+ describe('mutual exclusion with the diagram codec', () => {
91
+ it('a box diagram is a diagram, not a tree', () => {
92
+ expect(detectAsciiDiagram(NEGATIVES.boxDiagram).isDiagram).toBe(true);
93
+ expect(detectTree(NEGATIVES.boxDiagram).isTree).toBe(false);
94
+ });
95
+
96
+ it('every tree fixture is a tree, not a diagram', () => {
97
+ for (const fx of TREE_FIXTURES) {
98
+ expect(detectTree(fx.art).isTree).toBe(true);
99
+ expect(detectAsciiDiagram(fx.art).isDiagram).toBe(false);
100
+ }
101
+ });
102
+ });
103
+
104
+ describe('explicit `tree` tag (lenient detection)', () => {
105
+ const FLAT = ['README.md', 'index.ts', 'package.json'].join('\n');
106
+ const SINGLE = 'src/';
107
+
108
+ it('isExplicitTreeLang matches only the `tree` tag', () => {
109
+ for (const lang of ['tree', ' Tree ', 'TREE']) expect(isExplicitTreeLang(lang)).toBe(true);
110
+ for (const lang of [undefined, null, '', 'text', 'ascii', 'diagram', 'js'])
111
+ expect(isExplicitTreeLang(lang)).toBe(false);
112
+ });
113
+
114
+ it('a flat connector-less list is rejected untagged but accepted when explicit', () => {
115
+ // The exact bug the fence tag fixes: flattening a tree drops its
116
+ // connectors, so auto-detection rejects it on round-trip. The explicit
117
+ // `tree` tag trusts the author and keeps it a tree.
118
+ expect(detectTree(FLAT).isTree).toBe(false);
119
+ const explicit = detectTree(FLAT, { explicit: true });
120
+ expect(explicit.isTree).toBe(true);
121
+ expect(explicit.tree?.roots).toHaveLength(3);
122
+ });
123
+
124
+ it('a single-node tree is accepted when explicit', () => {
125
+ expect(detectTree(SINGLE).isTree).toBe(false);
126
+ expect(detectTree(SINGLE, { explicit: true }).isTree).toBe(true);
127
+ });
128
+
129
+ it('still rejects a box diagram even when explicitly tagged', () => {
130
+ expect(detectTree(NEGATIVES.boxDiagram, { explicit: true }).isTree).toBe(false);
131
+ });
132
+
133
+ it('still rejects a markdown table even when explicitly tagged', () => {
134
+ expect(detectTree(NEGATIVES.markdownTable, { explicit: true }).isTree).toBe(false);
135
+ });
136
+
137
+ it('isTreeFence accepts a flat `tree`-tagged fence via the lang gate', () => {
138
+ const fence: MarkdownCodeBlock = { type: 'code', lang: 'tree', value: FLAT };
139
+ expect(isTreeFence(fence)).toBe(true);
140
+ // The same content in a bare fence is not auto-detected.
141
+ expect(isTreeFence({ type: 'code', value: FLAT })).toBe(false);
142
+ });
143
+ });
144
+
145
+ describe('fence language gate', () => {
146
+ it('allows inert langs incl. tree; rejects real languages', () => {
147
+ for (const lang of [undefined, null, '', 'text', 'txt', 'plain', 'tree', 'ascii']) {
148
+ expect(isEligibleTreeFenceLang(lang)).toBe(true);
149
+ }
150
+ for (const lang of ['js', 'python', 'yaml', 'json', 'bash']) {
151
+ expect(isEligibleTreeFenceLang(lang)).toBe(false);
152
+ }
153
+ });
154
+
155
+ it('isTreeFence combines the lang gate with content detection', () => {
156
+ const art = TREE_FIXTURES[0].art;
157
+ const fence = (lang?: string): MarkdownCodeBlock => ({
158
+ type: 'code',
159
+ ...(lang !== undefined ? { lang } : {}),
160
+ value: art,
161
+ });
162
+ expect(isTreeFence(fence())).toBe(true);
163
+ expect(isTreeFence(fence('tree'))).toBe(true);
164
+ expect(isTreeFence(fence('js'))).toBe(false);
165
+ // And it is NOT an ascii-diagram fence.
166
+ expect(isAsciiDiagramFence(fence())).toBe(false);
167
+ });
168
+ });
@@ -0,0 +1,118 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { parseTree, type TreeNode } from '../doc/treeview/index.js';
3
+
4
+ /** Flatten to "depth:label[ #comment]" lines, depth-first. */
5
+ function flat(nodes: readonly TreeNode[], depth = 0, out: string[] = []): string[] {
6
+ for (const n of nodes) {
7
+ out.push(`${depth}:${n.label}${n.comment ? ` #${n.comment}` : ''}`);
8
+ flat(n.children, depth + 1, out);
9
+ }
10
+ return out;
11
+ }
12
+
13
+ describe('parseTree — renditions', () => {
14
+ it('parses a Unicode file tree', () => {
15
+ const art = [
16
+ 'src/',
17
+ '├── index.ts',
18
+ '├── components/',
19
+ '│ ├── App.tsx',
20
+ '│ └── Button.tsx',
21
+ '└── utils/',
22
+ ' └── math.ts',
23
+ ].join('\n');
24
+ expect(flat(parseTree(art).roots)).toEqual([
25
+ '0:src/',
26
+ '1:index.ts',
27
+ '1:components/',
28
+ '2:App.tsx',
29
+ '2:Button.tsx',
30
+ '1:utils/',
31
+ '2:math.ts',
32
+ ]);
33
+ });
34
+
35
+ it('parses ASCII `|-- ` / `` `-- `` style with identical semantics', () => {
36
+ const art = ['src/', '|-- a.ts', '|-- sub/', '| `-- b.ts', '`-- c.ts'].join('\n');
37
+ expect(flat(parseTree(art).roots)).toEqual(['0:src/', '1:a.ts', '1:sub/', '2:b.ts', '1:c.ts']);
38
+ expect(parseTree(art).style).toBe('ascii');
39
+ });
40
+
41
+ it('parses `+-- ` style', () => {
42
+ const art = ['root', '+-- a', '| +-- a1', '+-- b'].join('\n');
43
+ expect(flat(parseTree(art).roots)).toEqual(['0:root', '1:a', '2:a1', '1:b']);
44
+ });
45
+
46
+ it('parses pure 2-space indentation', () => {
47
+ const art = ['src/', ' a.ts', ' sub/', ' b.ts'].join('\n');
48
+ expect(flat(parseTree(art).roots)).toEqual(['0:src/', '1:a.ts', '1:sub/', '2:b.ts']);
49
+ });
50
+
51
+ it('parses 4-space indentation', () => {
52
+ const art = ['src/', ' a.ts', ' sub/', ' b.ts'].join('\n');
53
+ expect(flat(parseTree(art).roots)).toEqual(['0:src/', '1:a.ts', '1:sub/', '2:b.ts']);
54
+ });
55
+
56
+ it('parses bullet-indented lists', () => {
57
+ const art = ['- src/', ' - a.ts', ' - sub/', ' - b.ts'].join('\n');
58
+ expect(flat(parseTree(art).roots)).toEqual(['0:src/', '1:a.ts', '1:sub/', '2:b.ts']);
59
+ });
60
+
61
+ it('captures trailing comments (# and <--)', () => {
62
+ const art = ['p/', '├── a.ts # first', '└── b.ts <-- second'].join('\n');
63
+ const roots = parseTree(art).roots;
64
+ expect(flat(roots)).toEqual(['0:p/', '1:a.ts #first', '1:b.ts #second']);
65
+ });
66
+
67
+ it('marks directories (trailing slash or has children)', () => {
68
+ const art = ['top', '├── leaf.txt', '└── branch', ' └── deep.txt'].join('\n');
69
+ const roots = parseTree(art).roots;
70
+ expect(roots[0].isDir).toBe(true); // has children
71
+ const leaf = roots[0].children.find((n) => n.id === 'leaf-txt');
72
+ expect(leaf?.isDir).toBe(false);
73
+ const branch = roots[0].children.find((n) => n.id === 'branch');
74
+ expect(branch?.isDir).toBe(true); // has children
75
+ });
76
+
77
+ it('parses a forest (multiple roots)', () => {
78
+ const art = ['a/', '└── a1', 'b/', '└── b1'].join('\n');
79
+ expect(flat(parseTree(art).roots)).toEqual(['0:a/', '1:a1', '0:b/', '1:b1']);
80
+ });
81
+
82
+ it('assigns stable, deduplicated ids', () => {
83
+ const art = ['root/', '├── index.ts', '└── sub/', ' └── index.ts'].join('\n');
84
+ const roots = parseTree(art).roots;
85
+ const ids: string[] = [];
86
+ const walk = (ns: TreeNode[]) => ns.forEach((n) => (ids.push(n.id), walk(n.children)));
87
+ walk(roots);
88
+ expect(ids).toEqual(['root', 'index-ts', 'sub', 'index-ts-2']);
89
+ // Stable across re-parses.
90
+ const ids2: string[] = [];
91
+ const walk2 = (ns: TreeNode[]) => ns.forEach((n) => (ids2.push(n.id), walk2(n.children)));
92
+ walk2(parseTree(art).roots);
93
+ expect(ids2).toEqual(ids);
94
+ });
95
+
96
+ it('tolerates ragged dedents (clamp to nearest shallower level)', () => {
97
+ const art = ['a/', ' b/', ' c', ' d'].join('\n'); // d dedents to an in-between column
98
+ const roots = parseTree(art).roots;
99
+ // d clamps under a/ (the nearest shallower node).
100
+ expect(flat(roots)).toEqual(['0:a/', '1:b/', '2:c', '1:d']);
101
+ });
102
+
103
+ it('skips rails-only spacer lines', () => {
104
+ const art = ['a/', '├── b', '│', '└── c'].join('\n');
105
+ expect(flat(parseTree(art).roots)).toEqual(['0:a/', '1:b', '1:c']);
106
+ });
107
+
108
+ it('never throws on garbage / empty', () => {
109
+ expect(parseTree('').roots).toEqual([]);
110
+ expect(() => parseTree('|||\n---')).not.toThrow();
111
+ expect(parseTree('single line').roots.length).toBe(1);
112
+ });
113
+
114
+ it('handles CRLF input', () => {
115
+ const art = 'a/\r\n├── b\r\n└── c';
116
+ expect(flat(parseTree(art).roots)).toEqual(['0:a/', '1:b', '1:c']);
117
+ });
118
+ });