@bendyline/squisq 1.5.2 → 2.0.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 (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-T5UK6YOB.js → chunk-2PWOO4ZE.js} +60 -17
  6. package/dist/chunk-2PWOO4ZE.js.map +1 -0
  7. package/dist/{chunk-VWUFZ6ZG.js → chunk-3E5F2XMR.js} +2 -29
  8. package/dist/chunk-3E5F2XMR.js.map +1 -0
  9. package/dist/chunk-4X3JQXNM.js +35 -0
  10. package/dist/chunk-4X3JQXNM.js.map +1 -0
  11. package/dist/{chunk-P3K7NLHF.js → chunk-5DKBFXPY.js} +31 -121
  12. package/dist/chunk-5DKBFXPY.js.map +1 -0
  13. package/dist/{chunk-DLXZMS5K.js → chunk-5J4HKRJF.js} +51 -3
  14. package/dist/chunk-5J4HKRJF.js.map +1 -0
  15. package/dist/chunk-5UYI3RJB.js +2503 -0
  16. package/dist/chunk-5UYI3RJB.js.map +1 -0
  17. package/dist/{chunk-BEQODJWV.js → chunk-66QO7XFB.js} +11 -291
  18. package/dist/chunk-66QO7XFB.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-CY2R2VPR.js +884 -0
  22. package/dist/chunk-CY2R2VPR.js.map +1 -0
  23. package/dist/chunk-FVJUUTEM.js +74 -0
  24. package/dist/chunk-FVJUUTEM.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-MMPBOUK7.js} +10 -73
  30. package/dist/chunk-MMPBOUK7.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-GPLTCSXO.js → chunk-R2GC6RLD.js} +7 -7
  36. package/dist/chunk-R2GC6RLD.js.map +1 -0
  37. package/dist/chunk-R2YZTCLO.js +292 -0
  38. package/dist/chunk-R2YZTCLO.js.map +1 -0
  39. package/dist/chunk-S7A5D7ZJ.js +174 -0
  40. package/dist/chunk-S7A5D7ZJ.js.map +1 -0
  41. package/dist/chunk-TFJVXUHH.js +2358 -0
  42. package/dist/chunk-TFJVXUHH.js.map +1 -0
  43. package/dist/{chunk-LTN6I7QW.js → chunk-TUWMK62B.js} +55 -4
  44. package/dist/chunk-TUWMK62B.js.map +1 -0
  45. package/dist/{chunk-IMSCRLLV.js → chunk-VR7UPJ7C.js} +35 -25
  46. package/dist/chunk-VR7UPJ7C.js.map +1 -0
  47. package/dist/{chunk-KKNUBQ6Y.js → chunk-W4O2TX5I.js} +157 -92
  48. package/dist/chunk-W4O2TX5I.js.map +1 -0
  49. package/dist/{chunk-5TYS5V3G.js → chunk-X6DUONGT.js} +4204 -3560
  50. package/dist/chunk-X6DUONGT.js.map +1 -0
  51. package/dist/doc/index.d.ts +2334 -5
  52. package/dist/doc/index.js +127 -33
  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 +246 -80
  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__/customTemplatesFrontmatter.test.ts +0 -15
  97. package/src/__tests__/customThemesFrontmatter.test.ts +1 -0
  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 +54 -4
  114. package/src/__tests__/markdownSanitize.test.ts +7 -5
  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 +12 -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 +70 -2
  164. package/src/doc/index.ts +76 -4
  165. package/src/doc/markdownToDoc.ts +138 -2
  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/__tests__/listBlock.test.ts +64 -0
  172. package/src/doc/templates/diagramBlock.ts +237 -38
  173. package/src/doc/templates/fallbackBlock.ts +1 -1
  174. package/src/doc/templates/index.ts +134 -279
  175. package/src/doc/templates/inputDescriptors.ts +20 -1
  176. package/src/doc/templates/listBlock.ts +88 -30
  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 +67 -5
  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/index.ts +1 -0
  204. package/src/markdown/stringify.ts +4 -9
  205. package/src/markdown/utils.ts +36 -14
  206. package/src/narration/align.ts +514 -0
  207. package/src/narration/features.ts +231 -0
  208. package/src/narration/index.ts +86 -0
  209. package/src/narration/nuclei.ts +118 -0
  210. package/src/narration/pacing.ts +211 -0
  211. package/src/narration/script.ts +239 -0
  212. package/src/narration/session.ts +100 -0
  213. package/src/narration/sidecar.ts +187 -0
  214. package/src/narration/syllables.ts +85 -0
  215. package/src/narration/trace.ts +50 -0
  216. package/src/narration/types.ts +299 -0
  217. package/src/narration/vad.ts +131 -0
  218. package/src/recommend/numberHighlight.ts +25 -0
  219. package/src/recommend/templates.ts +108 -12
  220. package/src/schemas/BlockTemplates.ts +156 -7
  221. package/src/schemas/Doc.ts +72 -9
  222. package/src/schemas/Media.ts +12 -2
  223. package/src/schemas/Theme.ts +45 -25
  224. package/src/schemas/Viewport.ts +2 -2
  225. package/src/schemas/fontStacks.ts +79 -0
  226. package/src/schemas/themeCompile.ts +15 -0
  227. package/src/schemas/themeConstants.ts +2 -0
  228. package/src/schemas/themeLibrary.ts +25 -19
  229. package/src/schemas/themeValidator.ts +1 -1
  230. package/src/schemas/themes/gezellig.json +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-5TYS5V3G.js.map +0 -1
  247. package/dist/chunk-A4O7GIWE.js.map +0 -1
  248. package/dist/chunk-BEQODJWV.js.map +0 -1
  249. package/dist/chunk-DLXZMS5K.js.map +0 -1
  250. package/dist/chunk-GPLTCSXO.js.map +0 -1
  251. package/dist/chunk-HWVFJAAH.js.map +0 -1
  252. package/dist/chunk-IMSCRLLV.js.map +0 -1
  253. package/dist/chunk-JELIRVDP.js +0 -24
  254. package/dist/chunk-JELIRVDP.js.map +0 -1
  255. package/dist/chunk-KKNUBQ6Y.js.map +0 -1
  256. package/dist/chunk-LDQ2HJIX.js.map +0 -1
  257. package/dist/chunk-LTN6I7QW.js.map +0 -1
  258. package/dist/chunk-P3K7NLHF.js.map +0 -1
  259. package/dist/chunk-T5UK6YOB.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 -1801
  266. package/dist/story/index.js +0 -279
  267. package/src/doc/getLayers.ts +0 -195
  268. /package/dist/{story → narration}/index.js.map +0 -0
@@ -0,0 +1,376 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import {
3
+ asciiTimelineToTemplateData,
4
+ detectAsciiTimeline,
5
+ parseAsciiTimeline,
6
+ renderAsciiTimeline,
7
+ type AsciiTimeline,
8
+ } from '../doc/asciiTimeline/index.js';
9
+ import {
10
+ TICK_INTERPOLATION_TIMELINE,
11
+ WRAPPED_CLIENT_KERNEL_TIMELINE,
12
+ } from './fixtures/asciiTimelines.js';
13
+
14
+ function renderFixpoint(timeline: AsciiTimeline, style?: 'unicode' | 'ascii'): string {
15
+ const first = renderAsciiTimeline(timeline, style ? { style } : {});
16
+ const second = renderAsciiTimeline(parseAsciiTimeline(first));
17
+ const third = renderAsciiTimeline(parseAsciiTimeline(second));
18
+ expect(second).toBe(first);
19
+ expect(third).toBe(first);
20
+ return first;
21
+ }
22
+
23
+ describe('renderAsciiTimeline', () => {
24
+ const timeline: AsciiTimeline = {
25
+ tracks: [
26
+ {
27
+ id: 'kernel',
28
+ label: 'kernel ticks (30 Hz)',
29
+ row: 4,
30
+ startColumn: 20,
31
+ endColumn: 65,
32
+ events: [
33
+ {
34
+ id: 'tick-28',
35
+ label: 'T28',
36
+ description: 'delta-28',
37
+ column: 22,
38
+ side: 'above',
39
+ marker: 'filled',
40
+ },
41
+ {
42
+ id: 'tick-29',
43
+ label: 'T29',
44
+ column: 38,
45
+ side: 'below',
46
+ marker: 'hollow',
47
+ },
48
+ ],
49
+ },
50
+ {
51
+ id: 'branch',
52
+ label: 'client frames',
53
+ row: 9,
54
+ startColumn: 30,
55
+ endColumn: 55,
56
+ events: [
57
+ {
58
+ id: 'render-point',
59
+ label: 'render position',
60
+ description: 'interpolate T28 to T29',
61
+ column: 42,
62
+ marker: 'diamond',
63
+ },
64
+ ],
65
+ },
66
+ ],
67
+ links: [{ source: 'tick-28', target: 'render-point', label: 'feeds renderer' }],
68
+ width: 80,
69
+ height: 12,
70
+ style: 'unicode',
71
+ warnings: [],
72
+ };
73
+
74
+ it('emits a continuous rail with pointer callouts and branch declarations', () => {
75
+ const output = renderFixpoint(timeline);
76
+ const lines = output.split('\n');
77
+ const kernelRail = lines.find((line) => line.includes('kernel ticks (30 Hz)'));
78
+
79
+ expect(kernelRail).toMatch(/kernel ticks \(30 Hz\) \{#kernel start=20 end=65\}: ─*●─*○─*►/u);
80
+ expect(output).toContain('▲ T28 :: delta-28 {#tick-28 column=22}');
81
+ expect(output).toContain('▼ T29 {#tick-29 column=38}');
82
+ expect(lines[lines.length - 1]).toBe('branch: tick-28 → render-point : feeds renderer');
83
+ });
84
+
85
+ it('uses parser-recognizable ASCII markers and rails', () => {
86
+ const output = renderFixpoint(timeline, 'ascii');
87
+
88
+ expect(output).toMatch(/^kernel ticks \(30 Hz\) \{#kernel start=20 end=65\}: -*\*-*o-*>/m);
89
+ expect(output).toContain('^ T28 :: delta-28 {#tick-28 column=22}');
90
+ expect(output).toContain('v T29 {#tick-29 column=38}');
91
+ expect(output).toContain('branch: tick-28 -> render-point : feeds renderer');
92
+ expect(output).not.toMatch(/[─●○◆►→]/u);
93
+ });
94
+
95
+ it('sorts tracks/events and drops unresolved links before the first render', () => {
96
+ const shuffled: AsciiTimeline = {
97
+ ...timeline,
98
+ tracks: [...timeline.tracks].reverse().map((track) => ({
99
+ ...track,
100
+ events: [...track.events].reverse(),
101
+ })),
102
+ links: [...timeline.links, { source: 'missing', target: 'tick-28' }],
103
+ };
104
+ const output = renderFixpoint(shuffled);
105
+
106
+ expect(output.indexOf('kernel ticks')).toBeLessThan(output.indexOf('client frames'));
107
+ expect(output.indexOf('T28')).toBeLessThan(output.indexOf('T29'));
108
+ expect(output).not.toContain('missing');
109
+ });
110
+
111
+ it('normalizes colliding ids and prose-heavy Unicode without fixpoint churn', () => {
112
+ const adversarial: AsciiTimeline = {
113
+ tracks: [
114
+ {
115
+ id: 'one',
116
+ row: 0,
117
+ startColumn: 0,
118
+ endColumn: 20,
119
+ events: [
120
+ { id: 'same', label: 'oooooooooooo', column: 0 },
121
+ { id: 'same', label: 'second', column: 10 },
122
+ { id: 'same_2', label: 'third', column: 20 },
123
+ ],
124
+ },
125
+ ],
126
+ links: [],
127
+ width: 21,
128
+ height: 1,
129
+ style: 'unicode',
130
+ warnings: [],
131
+ };
132
+ const output = renderFixpoint(adversarial);
133
+
134
+ expect(output).toContain('{#same column=0}');
135
+ expect(output).toContain('{#same_2 column=10}');
136
+ expect(output).toContain('{#same_2_2 column=20}');
137
+ expect(parseAsciiTimeline(output).style).toBe('unicode');
138
+ });
139
+
140
+ it('preserves shared geometry when track labels have different lengths', () => {
141
+ const geometry: AsciiTimeline = {
142
+ tracks: [
143
+ {
144
+ id: 'short',
145
+ label: 'A',
146
+ row: 0,
147
+ startColumn: 10,
148
+ endColumn: 110,
149
+ events: [
150
+ { id: 'a20', label: '20', column: 20 },
151
+ { id: 'a90', label: '90', column: 90 },
152
+ ],
153
+ },
154
+ {
155
+ id: 'long',
156
+ label: 'A much longer parallel track label',
157
+ row: 3,
158
+ startColumn: 10,
159
+ endColumn: 110,
160
+ events: [
161
+ { id: 'b20', label: '20', column: 20 },
162
+ { id: 'b90', label: '90', column: 90 },
163
+ ],
164
+ },
165
+ ],
166
+ links: [],
167
+ width: 111,
168
+ height: 4,
169
+ style: 'unicode',
170
+ warnings: [],
171
+ };
172
+ const before = asciiTimelineToTemplateData(geometry).tracks.map((track) =>
173
+ track.events.map((event) => event.position),
174
+ );
175
+ const rendered = renderFixpoint(geometry);
176
+ const parsed = parseAsciiTimeline(rendered);
177
+ const after = asciiTimelineToTemplateData(parsed).tracks.map((track) =>
178
+ track.events.map((event) => event.position),
179
+ );
180
+
181
+ expect(parsed.tracks.map((track) => [track.startColumn, track.endColumn])).toEqual([
182
+ [10, 110],
183
+ [10, 110],
184
+ ]);
185
+ expect(after).toEqual(before);
186
+ });
187
+
188
+ it('preserves internal arrows and double hyphens in event prose', () => {
189
+ const prose: AsciiTimeline = {
190
+ tracks: [
191
+ {
192
+ id: 'flow',
193
+ label: 'Flow',
194
+ row: 0,
195
+ startColumn: 0,
196
+ endColumn: 20,
197
+ events: [
198
+ {
199
+ id: 'compare',
200
+ label: 'Compare',
201
+ description: 'A > B; release--candidate',
202
+ column: 10,
203
+ },
204
+ ],
205
+ },
206
+ ],
207
+ links: [],
208
+ width: 21,
209
+ height: 1,
210
+ style: 'unicode',
211
+ warnings: [],
212
+ };
213
+ const parsed = parseAsciiTimeline(renderFixpoint(prose));
214
+
215
+ expect(parsed.tracks[0].events[0].description).toBe('A > B; release--candidate');
216
+ });
217
+
218
+ it('canonicalizes the exact sample without losing its cadence or callout semantics', () => {
219
+ const parsed = parseAsciiTimeline(TICK_INTERPOLATION_TIMELINE);
220
+ const before = asciiTimelineToTemplateData(parsed).tracks.map((track) =>
221
+ track.events.map((event) => event.position),
222
+ );
223
+ const output = renderFixpoint(parsed);
224
+ expect(detectAsciiTimeline(output).isTimeline).toBe(true);
225
+ expect(detectAsciiTimeline(output, { explicit: true }).isTimeline).toBe(true);
226
+ const reparsed = parseAsciiTimeline(output);
227
+ const [kernel, client] = reparsed.tracks;
228
+
229
+ expect(reparsed.tracks).toHaveLength(2);
230
+ expect(kernel.endLabel).toBe('sim time');
231
+ expect(kernel.events[0]).toMatchObject({
232
+ label: 'T28',
233
+ description: 'deltas sent: Δ28',
234
+ side: 'above',
235
+ descriptionSide: 'below',
236
+ });
237
+ expect(client.events.filter((event) => event.callout === false)).toHaveLength(16);
238
+ expect(client.events.map((event) => event.label)).toEqual(
239
+ expect.arrayContaining(['estimated kernel clock', 'render position']),
240
+ );
241
+ expect(
242
+ asciiTimelineToTemplateData(reparsed).tracks.map((track) =>
243
+ track.events.map((event) => event.position),
244
+ ),
245
+ ).toEqual(before);
246
+ });
247
+
248
+ it('canonicalizes wrapped flow art into a byte-stable point timeline', () => {
249
+ const wrapped = parseAsciiTimeline(WRAPPED_CLIENT_KERNEL_TIMELINE);
250
+ const output = renderFixpoint(wrapped);
251
+ const reparsed = parseAsciiTimeline(output);
252
+
253
+ expect(output).toContain('CLIENT (main thread)');
254
+ expect(output).toContain('KERNEL (Web Worker / Node)');
255
+ expect(output).toContain('branch: command-rejected-event → error-surfaced-to-user-agent');
256
+ expect(reparsed.tracks.map((track) => track.events.map((event) => event.id))).toEqual(
257
+ wrapped.tracks.map((track) => track.events.map((event) => event.id)),
258
+ );
259
+ expect(reparsed.links).toEqual(wrapped.links);
260
+ });
261
+
262
+ it('round-trips a hyphenated label ending in an ASCII-marker letter', () => {
263
+ const hyphenated: AsciiTimeline = {
264
+ tracks: [
265
+ {
266
+ id: 'flow',
267
+ row: 0,
268
+ startColumn: 0,
269
+ endColumn: 20,
270
+ events: [
271
+ { id: 'go', label: 'Go-live', column: 0 },
272
+ { id: 'done', label: 'Done', column: 20 },
273
+ ],
274
+ },
275
+ ],
276
+ links: [],
277
+ width: 21,
278
+ height: 1,
279
+ style: 'unicode',
280
+ warnings: [],
281
+ };
282
+
283
+ const parsed = parseAsciiTimeline(renderFixpoint(hyphenated));
284
+ expect(parsed.tracks[0].events.map((event) => [event.id, event.label])).toEqual([
285
+ ['go', 'Go-live'],
286
+ ['done', 'Done'],
287
+ ]);
288
+ });
289
+
290
+ it('does not treat marker letters beside double hyphens as ASCII events', () => {
291
+ const doubleHyphen: AsciiTimeline = {
292
+ tracks: [
293
+ {
294
+ id: 'ascii-flow',
295
+ row: 0,
296
+ startColumn: 0,
297
+ endColumn: 10,
298
+ events: [{ id: 'foo', label: 'foo--bar', column: 5 }],
299
+ },
300
+ ],
301
+ links: [],
302
+ width: 11,
303
+ height: 1,
304
+ style: 'ascii',
305
+ warnings: [],
306
+ };
307
+
308
+ const parsed = parseAsciiTimeline(renderFixpoint(doubleHyphen, 'ascii'));
309
+ expect(parsed.tracks[0].events).toMatchObject([{ id: 'foo', label: 'foo--bar' }]);
310
+ });
311
+
312
+ it('round-trips brace-delimited prose in a track end label', () => {
313
+ const braceSuffix: AsciiTimeline = {
314
+ tracks: [
315
+ {
316
+ id: 'release',
317
+ row: 0,
318
+ startColumn: 0,
319
+ endColumn: 10,
320
+ endLabel: 'status {beta}',
321
+ events: [
322
+ { id: 'a', label: 'A', column: 0 },
323
+ { id: 'b', label: 'B', column: 10 },
324
+ ],
325
+ },
326
+ ],
327
+ links: [],
328
+ width: 11,
329
+ height: 1,
330
+ style: 'unicode',
331
+ warnings: [],
332
+ };
333
+
334
+ const parsed = parseAsciiTimeline(renderFixpoint(braceSuffix));
335
+ expect(parsed.tracks[0].endLabel).toBe('status {beta}');
336
+ expect(parsed.tracks[0].events.map((event) => [event.id, event.label])).toEqual([
337
+ ['a', 'A'],
338
+ ['b', 'B'],
339
+ ]);
340
+ });
341
+
342
+ it.each(['unicode', 'ascii'] as const)(
343
+ 'normalizes leading rail prose without %s fixpoint churn',
344
+ (style) => {
345
+ const leadingRails: AsciiTimeline = {
346
+ tracks: [
347
+ {
348
+ id: 'flags',
349
+ endLabel: '─-done',
350
+ row: 0,
351
+ startColumn: 0,
352
+ endColumn: 20,
353
+ events: [
354
+ { id: 'watch', label: '--watch', column: 0 },
355
+ { id: 'negative', label: '-1.5', column: 10 },
356
+ { id: 'phase', label: '─phase', column: 20 },
357
+ ],
358
+ },
359
+ ],
360
+ links: [],
361
+ width: 21,
362
+ height: 1,
363
+ style,
364
+ warnings: [],
365
+ };
366
+ const parsed = parseAsciiTimeline(renderFixpoint(leadingRails, style));
367
+
368
+ expect(parsed.tracks[0].events.map((event) => event.label)).toEqual([
369
+ '−−watch',
370
+ '−1.5',
371
+ '–phase',
372
+ ]);
373
+ expect(parsed.tracks[0].endLabel).toBe('–−done');
374
+ },
375
+ );
376
+ });
@@ -120,6 +120,11 @@ describe('needsQuoting / quoteAttrValue', () => {
120
120
  });
121
121
 
122
122
  describe('matchTrailingTemplateAnnotation', () => {
123
+ it('matches an empty trailing annotation', () => {
124
+ const m = matchTrailingTemplateAnnotation('Heading {[]}');
125
+ expect(m?.inner).toBe('');
126
+ });
127
+
123
128
  it('matches a simple trailing annotation', () => {
124
129
  const m = matchTrailingTemplateAnnotation('Getting Started {[comparisonBar]}');
125
130
  expect(m?.inner).toBe('comparisonBar');
@@ -34,6 +34,38 @@ describe('MemoryContentContainer', () => {
34
34
  expect(new Uint8Array(result!)).toEqual(new Uint8Array([1, 2, 3, 4]));
35
35
  });
36
36
 
37
+ it('accepts ArrayBuffers created in another browser realm', async () => {
38
+ const iframe = document.createElement('iframe');
39
+ document.body.appendChild(iframe);
40
+ try {
41
+ const foreignWindow = iframe.contentWindow as unknown as {
42
+ ArrayBuffer: ArrayBufferConstructor;
43
+ Uint8Array: Uint8ArrayConstructor;
44
+ };
45
+ const data = new foreignWindow.ArrayBuffer(3);
46
+ new foreignWindow.Uint8Array(data).set([7, 8, 9]);
47
+ await container.writeFile('foreign.bin', data);
48
+ expect(new Uint8Array((await container.readFile('foreign.bin'))!)).toEqual(
49
+ new Uint8Array([7, 8, 9]),
50
+ );
51
+ } finally {
52
+ iframe.remove();
53
+ }
54
+ });
55
+
56
+ it('owns copies on both write and read', async () => {
57
+ const input = new ArrayBuffer(2);
58
+ new Uint8Array(input).set([1, 2]);
59
+ await container.writeFile('owned.bin', input);
60
+
61
+ new Uint8Array(input)[0] = 9;
62
+ const firstRead = (await container.readFile('owned.bin'))!;
63
+ expect(Array.from(new Uint8Array(firstRead))).toEqual([1, 2]);
64
+
65
+ new Uint8Array(firstRead)[1] = 9;
66
+ expect(Array.from(new Uint8Array((await container.readFile('owned.bin'))!))).toEqual([1, 2]);
67
+ });
68
+
37
69
  it('writeFile overwrites existing file', async () => {
38
70
  await container.writeFile('f.txt', new TextEncoder().encode('first'));
39
71
  await container.writeFile('f.txt', new TextEncoder().encode('second'));
@@ -120,6 +152,12 @@ describe('getDocumentPath', () => {
120
152
  expect(await container.getDocumentPath()).toBe('index.md');
121
153
  });
122
154
 
155
+ it('preserves the actual case of a priority filename', async () => {
156
+ await container.writeFile('Index.MD', new TextEncoder().encode('# Hello'));
157
+ expect(await container.getDocumentPath()).toBe('Index.MD');
158
+ expect(await container.readDocument()).toBe('# Hello');
159
+ });
160
+
123
161
  it('prefers index.md over doc.md', async () => {
124
162
  await container.writeFile('doc.md', new TextEncoder().encode('doc'));
125
163
  await container.writeFile('index.md', new TextEncoder().encode('index'));
@@ -196,6 +234,11 @@ describe('findDocumentPath', () => {
196
234
  expect(findDocumentPath(entries)).toBe('index.md');
197
235
  });
198
236
 
237
+ it('returns the matched entry path rather than a lower-cased synthetic path', () => {
238
+ const entries: ContentEntry[] = [{ path: 'INDEX.md', mimeType: 'text/markdown', size: 50 }];
239
+ expect(findDocumentPath(entries)).toBe('INDEX.md');
240
+ });
241
+
199
242
  it('skips subdirectory .md files', () => {
200
243
  const entries: ContentEntry[] = [
201
244
  { path: 'docs/guide.md', mimeType: 'text/markdown', size: 50 },
@@ -10,8 +10,6 @@ import { describe, it, expect, vi } from 'vitest';
10
10
  import { markdownToDoc } from '../doc/markdownToDoc';
11
11
  import { docToMarkdown } from '../doc/docToMarkdown';
12
12
  import {
13
- encodeLayersForFrontmatter,
14
- decodeLayersFromFrontmatter,
15
13
  readCustomTemplatesFromFrontmatter,
16
14
  writeCustomTemplatesToFrontmatter,
17
15
  FRONTMATTER_CUSTOM_TEMPLATES_KEY,
@@ -43,19 +41,6 @@ const heroDef: CustomTemplateDefinition = {
43
41
  layers: sampleLayers,
44
42
  };
45
43
 
46
- describe('encodeLayersForFrontmatter ↔ decodeLayersFromFrontmatter', () => {
47
- it('round-trips a single Layer array verbatim', () => {
48
- const encoded = encodeLayersForFrontmatter(sampleLayers);
49
- const decoded = decodeLayersFromFrontmatter(encoded);
50
- expect(decoded).toEqual(sampleLayers);
51
- });
52
-
53
- it("returns [] when the base64 isn't valid JSON", () => {
54
- const garbage = encodeLayersForFrontmatter(sampleLayers).slice(0, 4);
55
- expect(decodeLayersFromFrontmatter(garbage)).toEqual([]);
56
- });
57
- });
58
-
59
44
  describe('writeCustomTemplatesToFrontmatter → readCustomTemplatesFromFrontmatter', () => {
60
45
  it('round-trips a single definition through the compact JSON wire format', () => {
61
46
  const payload = writeCustomTemplatesToFrontmatter([heroDef]);
@@ -115,6 +115,7 @@ squisq-custom-themes: ${encoded}
115
115
  A small editor for big ideas.
116
116
  `;
117
117
  const doc = markdownToDoc(parseMarkdown(sourceMd));
118
+ expect(doc.themeId).toBe('my-brand');
118
119
  expect(doc.customThemes).toHaveLength(1);
119
120
  expect(doc.customThemes![0]).toEqual(brand);
120
121
 
@@ -1,5 +1,6 @@
1
1
  import { describe, it, expect } from 'vitest';
2
- import { getLayers, RenderContext } from '../doc/getLayers.js';
2
+ import type { MaterializeBlockLayersOptions } from '../doc/materializeBlockLayers.js';
3
+ import { materializeLayers } from './materializeTestUtils.js';
3
4
  import { DEFAULT_THEME } from '../schemas/themeLibrary.js';
4
5
  import { VIEWPORT_PRESETS } from '../schemas/Viewport.js';
5
6
  import type { TemplateBlock } from '../schemas/BlockTemplates.js';
@@ -24,7 +25,7 @@ function makeDataTableBlock(overrides: Record<string, unknown> = {}): TemplateBl
24
25
  } as TemplateBlock;
25
26
  }
26
27
 
27
- const defaultContext: RenderContext = {
28
+ const defaultContext: MaterializeBlockLayersOptions = {
28
29
  theme: DEFAULT_THEME,
29
30
  viewport: VIEWPORT_PRESETS.landscape,
30
31
  blockIndex: 0,
@@ -37,7 +38,7 @@ const defaultContext: RenderContext = {
37
38
 
38
39
  describe('dataTable template', () => {
39
40
  it('produces layers including a table layer', () => {
40
- const layers = getLayers(makeDataTableBlock(), defaultContext);
41
+ const layers = materializeLayers(makeDataTableBlock(), defaultContext);
41
42
  expect(layers.length).toBeGreaterThan(0);
42
43
 
43
44
  const tableLayer = layers.find((l) => l.type === 'table');
@@ -45,14 +46,14 @@ describe('dataTable template', () => {
45
46
  });
46
47
 
47
48
  it('table layer contains the correct headers', () => {
48
- const layers = getLayers(makeDataTableBlock(), defaultContext);
49
+ const layers = materializeLayers(makeDataTableBlock(), defaultContext);
49
50
  const tableLayer = layers.find((l) => l.type === 'table') as TableLayer;
50
51
 
51
52
  expect(tableLayer.content.headers).toEqual(['Name', 'Value']);
52
53
  });
53
54
 
54
55
  it('table layer contains the correct rows', () => {
55
- const layers = getLayers(makeDataTableBlock(), defaultContext);
56
+ const layers = materializeLayers(makeDataTableBlock(), defaultContext);
56
57
  const tableLayer = layers.find((l) => l.type === 'table') as TableLayer;
57
58
 
58
59
  expect(tableLayer.content.rows).toEqual([
@@ -62,7 +63,7 @@ describe('dataTable template', () => {
62
63
  });
63
64
 
64
65
  it('includes a title text layer when title is provided', () => {
65
- const layers = getLayers(makeDataTableBlock({ title: 'My Table' }), defaultContext);
66
+ const layers = materializeLayers(makeDataTableBlock({ title: 'My Table' }), defaultContext);
66
67
  const textLayers = layers.filter((l) => l.type === 'text');
67
68
 
68
69
  expect(textLayers.length).toBeGreaterThanOrEqual(1);
@@ -71,7 +72,7 @@ describe('dataTable template', () => {
71
72
  });
72
73
 
73
74
  it('omits title layer when no title is given', () => {
74
- const layers = getLayers(makeDataTableBlock({ title: undefined }), defaultContext);
75
+ const layers = materializeLayers(makeDataTableBlock({ title: undefined }), defaultContext);
75
76
  const textLayers = layers.filter((l) => l.type === 'text');
76
77
 
77
78
  // Should have no text layers (only bg + table)
@@ -79,20 +80,23 @@ describe('dataTable template', () => {
79
80
  });
80
81
 
81
82
  it('includes a background layer', () => {
82
- const layers = getLayers(makeDataTableBlock(), defaultContext);
83
+ const layers = materializeLayers(makeDataTableBlock(), defaultContext);
83
84
  // Background is the first layer (shape with gradient fill)
84
85
  expect(['shape', 'image']).toContain(layers[0].type);
85
86
  });
86
87
 
87
88
  it('passes column alignment to the table layer', () => {
88
- const layers = getLayers(makeDataTableBlock({ align: ['left', 'right'] }), defaultContext);
89
+ const layers = materializeLayers(
90
+ makeDataTableBlock({ align: ['left', 'right'] }),
91
+ defaultContext,
92
+ );
89
93
  const tableLayer = layers.find((l) => l.type === 'table') as TableLayer;
90
94
 
91
95
  expect(tableLayer.content.align).toEqual(['left', 'right']);
92
96
  });
93
97
 
94
98
  it('applies theme colors to table styling', () => {
95
- const layers = getLayers(makeDataTableBlock(), defaultContext);
99
+ const layers = materializeLayers(makeDataTableBlock(), defaultContext);
96
100
  const tableLayer = layers.find((l) => l.type === 'table') as TableLayer;
97
101
  const style = tableLayer.content.style;
98
102
 
@@ -106,8 +110,11 @@ describe('dataTable template', () => {
106
110
  });
107
111
 
108
112
  it('adjusts table position when title is present vs absent', () => {
109
- const withTitle = getLayers(makeDataTableBlock({ title: 'Title' }), defaultContext);
110
- const withoutTitle = getLayers(makeDataTableBlock({ title: undefined }), defaultContext);
113
+ const withTitle = materializeLayers(makeDataTableBlock({ title: 'Title' }), defaultContext);
114
+ const withoutTitle = materializeLayers(
115
+ makeDataTableBlock({ title: undefined }),
116
+ defaultContext,
117
+ );
111
118
 
112
119
  const tableWithTitle = withTitle.find((l) => l.type === 'table') as TableLayer;
113
120
  const tableWithoutTitle = withoutTitle.find((l) => l.type === 'table') as TableLayer;
@@ -117,18 +124,21 @@ describe('dataTable template', () => {
117
124
  });
118
125
 
119
126
  it('works with portrait viewport', () => {
120
- const portraitContext: RenderContext = {
127
+ const portraitContext: MaterializeBlockLayersOptions = {
121
128
  ...defaultContext,
122
129
  viewport: VIEWPORT_PRESETS.portrait,
123
130
  };
124
- const layers = getLayers(makeDataTableBlock(), portraitContext);
131
+ const layers = materializeLayers(makeDataTableBlock(), portraitContext);
125
132
  expect(layers.length).toBeGreaterThan(0);
126
133
  const tableLayer = layers.find((l) => l.type === 'table');
127
134
  expect(tableLayer).toBeDefined();
128
135
  });
129
136
 
130
137
  it('handles empty rows gracefully', () => {
131
- const layers = getLayers(makeDataTableBlock({ headers: ['A', 'B'], rows: [] }), defaultContext);
138
+ const layers = materializeLayers(
139
+ makeDataTableBlock({ headers: ['A', 'B'], rows: [] }),
140
+ defaultContext,
141
+ );
132
142
  const tableLayer = layers.find((l) => l.type === 'table') as TableLayer;
133
143
  expect(tableLayer.content.rows).toEqual([]);
134
144
  });
@@ -98,9 +98,9 @@ describe('diagramBlock template', () => {
98
98
  });
99
99
  });
100
100
 
101
- describe('diagramBlock integration with getLayers', () => {
101
+ describe('diagramBlock integration with materializeBlockLayers', () => {
102
102
  it('renders through the full template registry', async () => {
103
- const { getLayers } = await import('../doc/getLayers.js');
103
+ const { materializeBlockLayers } = await import('../doc/materializeBlockLayers.js');
104
104
  const parentBlock: Block = {
105
105
  id: 'family',
106
106
  startTime: 0,
@@ -113,7 +113,7 @@ describe('diagramBlock integration with getLayers', () => {
113
113
  makeBlock({ id: 'b', title: 'B', x: 400, y: 100, connectsTo: [{ target: 'a' }] }),
114
114
  ],
115
115
  };
116
- const layers = getLayers(parentBlock, {});
116
+ const layers = materializeBlockLayers(parentBlock, { persistentLayers: false }).layers;
117
117
  // Should contain at least one shape (node card) and one path (edge).
118
118
  const types = new Set(layers.map((l: Layer) => l.type));
119
119
  expect(types.has('shape')).toBe(true);
@@ -0,0 +1,26 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { DIAGRAM_LABEL_LINE_HEIGHT, fitDiagramLabel } from '../doc/utils/diagramText';
3
+
4
+ describe('fitDiagramLabel', () => {
5
+ it('keeps a short single-line label at its preferred size', () => {
6
+ expect(fitDiagramLabel('Web1', 180, 64, 22)).toMatchObject({
7
+ fontSize: 22,
8
+ lineCount: 1,
9
+ firstLineOffset: 0,
10
+ });
11
+ });
12
+
13
+ it('shrinks and centers a dense multi-line package label inside its card', () => {
14
+ const fit = fitDiagramLabel(
15
+ '@bendyline/molen-kernel\nheadless sim — no DOM, no thread\nWorker + Node',
16
+ 252,
17
+ 112,
18
+ 38,
19
+ );
20
+
21
+ expect(fit.fontSize).toBeLessThan(38);
22
+ expect(fit.lineCount).toBeGreaterThanOrEqual(3);
23
+ expect(fit.lineCount * fit.fontSize * DIAGRAM_LABEL_LINE_HEIGHT).toBeLessThanOrEqual(96);
24
+ expect(fit.firstLineOffset).toBeLessThan(0);
25
+ });
26
+ });