@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,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 },
@@ -85,6 +85,7 @@ describe('coverBlock with hero image', () => {
85
85
  it('does not produce theme background layers', () => {
86
86
  const layers = coverBlock(input, landscapeContext);
87
87
  expect(findLayer(layers, 'cover-bg')).toBeUndefined();
88
+ expect(findLayer(layers, 'cover-bg-tint')).toBeUndefined();
88
89
  expect(findLayer(layers, 'cover-accent')).toBeUndefined();
89
90
  });
90
91
 
@@ -110,12 +111,16 @@ describe('coverBlock without hero image', () => {
110
111
  subtitle: 'Subtitle text',
111
112
  };
112
113
 
113
- it('produces theme background instead of hero', () => {
114
+ it('produces theme background and soft tint instead of hero', () => {
114
115
  const layers = coverBlock(input, landscapeContext);
115
116
  const bg = findShape(layers, 'cover-bg');
117
+ const tint = findShape(layers, 'cover-bg-tint');
116
118
  expect(bg).toBeDefined();
117
119
  expect(bg!.type).toBe('shape');
118
- expect(bg!.content.fill).toContain('radial-gradient');
120
+ expect(bg!.content.fill).toBe(DEFAULT_THEME.colors.background);
121
+ expect(tint).toBeDefined();
122
+ expect(tint!.content.fill).toContain('radial-gradient');
123
+ expect(tint!.content.fill).toContain('rgba(37, 99, 235, 0.16)');
119
124
  });
120
125
 
121
126
  it('produces decorative accent line', () => {
@@ -144,6 +149,31 @@ describe('coverBlock without hero image', () => {
144
149
  const subtitle = findLayer(layers, 'cover-subtitle');
145
150
  expect(subtitle!.position.y).not.toBe('82%');
146
151
  });
152
+
153
+ it('uses high-contrast Standard Light cover text', () => {
154
+ const layers = coverBlock(input, landscapeContext);
155
+ const title = findText(layers, 'cover-title');
156
+ const subtitle = findText(layers, 'cover-subtitle');
157
+ expect(title!.content.style.shadow).toBe(false);
158
+ expect(subtitle!.content.style.fontWeight).toBe('bold');
159
+ expect(subtitle!.content.style.color).toBe('rgba(15, 23, 42, 0.78)');
160
+ });
161
+
162
+ it('keeps non-Standard themes on the existing dramatic cover background', () => {
163
+ const nonStandardContext = createTemplateContext(
164
+ { ...DEFAULT_THEME, id: 'custom-light' },
165
+ 0,
166
+ 1,
167
+ VIEWPORT_PRESETS.landscape,
168
+ );
169
+ const layers = coverBlock(input, nonStandardContext);
170
+ const bg = findShape(layers, 'cover-bg');
171
+ const title = findText(layers, 'cover-title');
172
+ expect(findShape(layers, 'cover-bg-tint')).toBeUndefined();
173
+ expect(bg!.content.fill).toContain('radial-gradient');
174
+ expect(bg!.content.fill).toContain(`${DEFAULT_THEME.colors.primary} 0%`);
175
+ expect(title!.content.style.shadow).toBe(true);
176
+ });
147
177
  });
148
178
 
149
179
  // ── coverBlock title-only (no subtitle) ─────────────────────────────
@@ -273,6 +303,7 @@ describe('expandCoverBlock', () => {
273
303
  const config: StartBlockConfig = { title: 'No Hero' };
274
304
  const layers = expandCoverBlock(config, landscapeContext);
275
305
  expect(layers.some((l) => l.id === 'cover-bg')).toBe(true);
306
+ expect(layers.some((l) => l.id === 'cover-bg-tint')).toBe(true);
276
307
  expect(layers.some((l) => l.id === 'cover-title')).toBe(true);
277
308
  expect(layers.some((l) => l.id === 'cover-hero')).toBe(false);
278
309
  });
@@ -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]);
@@ -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
  });