@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
@@ -29,10 +29,6 @@
29
29
  * payload and a structured array, so older documents load unchanged.
30
30
  * The first save migrates them to the compact form.
31
31
  *
32
- * The `encodeLayersForFrontmatter` / `decodeLayersFromFrontmatter` pair
33
- * handles a single Layer array for the editor's `dataLayers="…"` Pandoc
34
- * param path; that path stays base64 (it's a different, per-block
35
- * mechanism) and is re-exported here for locality.
36
32
  */
37
33
 
38
34
  import type { Layer } from '../schemas/Doc.js';
@@ -134,43 +130,6 @@ function isDefaultViewport(v: { width: number; height: number } | undefined): bo
134
130
  return !v || (v.width === DEFAULT_VIEWPORT.width && v.height === DEFAULT_VIEWPORT.height);
135
131
  }
136
132
 
137
- // ─── Single-Layer base64 codec (legacy `dataLayers` path) ───────────
138
-
139
- /**
140
- * Base64-encode a Layer array as JSON, UTF-8 safe. Used by the editor's
141
- * per-block `dataLayers="…"` Pandoc param (not the doc-level template
142
- * list, which uses the compact JSON above).
143
- *
144
- * `btoa` only accepts Latin1 strings — any character outside that range
145
- * (e.g. an em-dash in a description) throws — so we round through
146
- * `TextEncoder`. Works in Node ≥18 and browsers.
147
- */
148
- export function encodeLayersForFrontmatter(layers: readonly Layer[]): string {
149
- return utf8ToBase64(JSON.stringify(layers));
150
- }
151
-
152
- /** Inverse of {@link encodeLayersForFrontmatter}. Returns [] on parse failure. */
153
- export function decodeLayersFromFrontmatter(encoded: string): Layer[] {
154
- try {
155
- const json = base64ToUtf8(encoded);
156
- const parsed = JSON.parse(json);
157
- return Array.isArray(parsed) ? (parsed as Layer[]) : [];
158
- } catch {
159
- return [];
160
- }
161
- }
162
-
163
- /** UTF-8 safe `btoa` — uses TextEncoder so arbitrary Unicode round-trips. */
164
- function utf8ToBase64(str: string): string {
165
- const bytes = new TextEncoder().encode(str);
166
- if (typeof globalThis.btoa === 'function') {
167
- let binary = '';
168
- for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
169
- return globalThis.btoa(binary);
170
- }
171
- return Buffer.from(bytes).toString('base64');
172
- }
173
-
174
133
  /** UTF-8 safe `atob`. */
175
134
  function base64ToUtf8(b64: string): string {
176
135
  if (typeof globalThis.atob === 'function') {
@@ -23,6 +23,7 @@
23
23
  */
24
24
 
25
25
  import type { Doc, Block } from '../schemas/Doc.js';
26
+ import type { MediaClip } from '../schemas/Media.js';
26
27
  import type {
27
28
  MarkdownDocument,
28
29
  MarkdownBlockNode,
@@ -30,6 +31,7 @@ import type {
30
31
  MarkdownParagraph,
31
32
  } from '../markdown/types.js';
32
33
  import { serializeAnnotation } from '../markdown/attrTokens.js';
34
+ import { flattenBlocks } from './markdownToDoc.js';
33
35
  import {
34
36
  FRONTMATTER_CUSTOM_TEMPLATES_KEY,
35
37
  writeCustomTemplatesToFrontmatter,
@@ -39,6 +41,8 @@ import {
39
41
  writeCustomThemesToFrontmatter,
40
42
  } from './customThemesFrontmatter.js';
41
43
 
44
+ const TRANSITION_PARAM_KEYS = ['transition', 'transitionDuration', 'transitionDirection'] as const;
45
+
42
46
  /**
43
47
  * Convert a Doc with heading-driven blocks back to a MarkdownDocument.
44
48
  *
@@ -56,6 +60,29 @@ import {
56
60
  export function docToMarkdown(doc: Doc): MarkdownDocument {
57
61
  const children: MarkdownBlockNode[] = [];
58
62
 
63
+ // Media clips were LIFTED out of block contents at parse time
64
+ // (`extractMediaFromContents`); re-insert their authoring annotations so
65
+ // the round-trip doesn't silently drop narration/clip references.
66
+ // Doc-anchored clips whose home block still exists ride back into that
67
+ // block's body; homeless ones (programmatic clips, or a preamble block
68
+ // that was dropped once emptied) emit at the document top — the
69
+ // canonical authored position.
70
+ const blockIds = new Set(flattenBlocks(doc.blocks).map((b) => b.id));
71
+ const docMediaByBlock = new Map<string, MediaClip[]>();
72
+ const homelessDocMedia: MediaClip[] = [];
73
+ for (const clip of doc.documentMedia ?? []) {
74
+ if (clip.origin && blockIds.has(clip.origin.blockId)) {
75
+ const list = docMediaByBlock.get(clip.origin.blockId) ?? [];
76
+ list.push(clip);
77
+ docMediaByBlock.set(clip.origin.blockId, list);
78
+ } else {
79
+ homelessDocMedia.push(clip);
80
+ }
81
+ }
82
+ for (const clip of homelessDocMedia) {
83
+ children.push(synthesizeMediaParagraph(clip));
84
+ }
85
+
59
86
  function emitBlock(block: Block): void {
60
87
  // Emit the heading node if present
61
88
  if (block.sourceHeading) {
@@ -69,8 +96,29 @@ export function docToMarkdown(doc: Doc): MarkdownDocument {
69
96
  children.push(synthesizeAnnotationParagraph(block));
70
97
  }
71
98
 
72
- // Emit body content
73
- if (block.contents) {
99
+ // Emit body content, with this block's media annotations re-inserted
100
+ // at their original positions. `origin.index` refers to the ORIGINAL
101
+ // contents array (which included every annotation), so ascending
102
+ // insertion into the stripped contents reproduces the source order.
103
+ const clips = [...(block.media ?? []), ...(docMediaByBlock.get(block.id) ?? [])];
104
+ if (clips.length > 0) {
105
+ const contents = [...(block.contents ?? [])];
106
+ const anchored = clips
107
+ .filter((clip) => clip.origin)
108
+ .sort((a, b) => a.origin!.index - b.origin!.index);
109
+ for (const clip of anchored) {
110
+ contents.splice(
111
+ Math.min(clip.origin!.index, contents.length),
112
+ 0,
113
+ synthesizeMediaParagraph(clip),
114
+ );
115
+ }
116
+ // Origin-less block clips (programmatic) sit right under the heading.
117
+ for (const clip of clips.filter((c) => !c.origin).reverse()) {
118
+ contents.unshift(synthesizeMediaParagraph(clip));
119
+ }
120
+ children.push(...contents);
121
+ } else if (block.contents) {
74
122
  children.push(...block.contents);
75
123
  }
76
124
 
@@ -129,69 +177,108 @@ function synthesizeAnnotationParagraph(block: Block): MarkdownParagraph {
129
177
  return { type: 'paragraph', children: [{ type: 'text', value: text }] };
130
178
  }
131
179
 
180
+ /**
181
+ * Re-synthesize the authoring paragraph for a media clip. The extraction
182
+ * kept the exact source text (`origin.raw`) — emit it verbatim so quoting,
183
+ * param order, and `mm:ss` time forms stay byte-stable. Programmatic clips
184
+ * (no raw) serialize canonically: src, anchor, then timing params.
185
+ */
186
+ function synthesizeMediaParagraph(clip: MediaClip): MarkdownParagraph {
187
+ if (clip.origin?.raw) {
188
+ return { type: 'paragraph', children: [{ type: 'text', value: clip.origin.raw }] };
189
+ }
190
+ const params: Record<string, string> = { src: clip.src };
191
+ if (clip.anchor === 'document') params.anchor = 'document';
192
+ if (clip.startAt) params.startAt = String(clip.startAt);
193
+ if (clip.clipStart != null) params.clipStart = String(clip.clipStart);
194
+ if (clip.clipEnd != null) params.clipEnd = String(clip.clipEnd);
195
+ if (clip.spillover) params.spillover = 'true';
196
+ return {
197
+ type: 'paragraph',
198
+ children: [{ type: 'text', value: serializeAnnotation(clip.kind, params) }],
199
+ };
200
+ }
201
+
132
202
  /**
133
203
  * Ensure the heading's `templateAnnotation` reflects the block's
134
204
  * template and templateOverrides. Returns a (possibly cloned) heading.
135
205
  */
136
206
  function ensureAnnotation(block: Block, heading: MarkdownHeading): MarkdownHeading {
137
- const attrs = ensureTransitionAttributes(block, heading);
207
+ const { attributes, transitionParams } = ensureTransitionMetadata(block, heading);
138
208
 
139
209
  // If the block has a non-default template or overrides, inject an annotation
140
210
  const hasExplicitTemplate =
141
211
  block.template && block.template !== 'sectionHeader' && block.autoTemplate !== true;
142
212
  const hasOverrides = block.templateOverrides && Object.keys(block.templateOverrides).length > 0;
143
213
 
144
- // If the heading already has an annotation, trust it (it came from parsing).
145
- // Transition attrs may still have been injected above if the block changed.
146
- if (heading.templateAnnotation) {
147
- return attrs === heading.attributes ? heading : { ...heading, attributes: attrs };
214
+ let templateAnnotation = heading.templateAnnotation;
215
+ if (!templateAnnotation && (hasExplicitTemplate || hasOverrides || transitionParams)) {
216
+ templateAnnotation = {
217
+ ...(hasExplicitTemplate ? { template: block.template ?? 'sectionHeader' } : {}),
218
+ ...(hasOverrides ? { params: block.templateOverrides } : {}),
219
+ };
148
220
  }
149
221
 
150
- if (!hasExplicitTemplate && !hasOverrides) {
151
- return attrs === heading.attributes ? heading : { ...heading, attributes: attrs };
222
+ if (transitionParams) {
223
+ templateAnnotation = {
224
+ ...(templateAnnotation ?? {}),
225
+ params: {
226
+ ...(templateAnnotation?.params ?? {}),
227
+ ...transitionParams,
228
+ },
229
+ };
152
230
  }
153
231
 
154
- // Clone to avoid mutating the original
155
- return {
156
- ...heading,
157
- children: [...heading.children],
158
- attributes: attrs,
159
- templateAnnotation: {
160
- template: block.template ?? 'sectionHeader',
161
- ...(hasOverrides ? { params: block.templateOverrides } : {}),
162
- },
163
- };
232
+ if (attributes === heading.attributes && templateAnnotation === heading.templateAnnotation) {
233
+ return heading;
234
+ }
235
+
236
+ return { ...heading, children: [...heading.children], attributes, templateAnnotation };
164
237
  }
165
238
 
166
- function ensureTransitionAttributes(
239
+ function ensureTransitionMetadata(
167
240
  block: Block,
168
241
  heading: MarkdownHeading,
169
- ): MarkdownHeading['attributes'] {
242
+ ): {
243
+ attributes: MarkdownHeading['attributes'];
244
+ transitionParams: Record<string, string> | undefined;
245
+ } {
170
246
  const transition = block.transition;
171
- if (!transition) return heading.attributes;
172
-
173
- // The transition is deliberately written to BOTH channels, which serve
174
- // different consumers:
175
- // - `params` is what `stringifyMarkdown` serializes into the Pandoc `{…}`
176
- // attribute text (serializePandocAttributes reads only id/classes/params).
177
- // - `blockMeta` is what `markdownToDoc` reads back directly (makeBlock,
178
- // pinnedHeadingMeta) an in-memory Doc → MarkdownDocument → Doc round
179
- // trip never re-parses params, so a stale/missing `blockMeta.transition`
180
- // would lose or resurrect the wrong transition.
181
- const attrs = heading.attributes ?? {};
182
- return {
247
+ if (!transition) {
248
+ return { attributes: heading.attributes, transitionParams: undefined };
249
+ }
250
+
251
+ // The transition is written to the squisq-native `{[]}` params for
252
+ // author-facing markdown, and to `attributes.blockMeta` for the in-memory
253
+ // Doc MarkdownDocument Doc path (which does not re-parse string params).
254
+ const attrs = removeTransitionParams(heading.attributes) ?? {};
255
+ const attributes = {
183
256
  ...attrs,
184
- params: {
185
- ...(attrs.params ?? {}),
257
+ blockMeta: {
258
+ ...(attrs.blockMeta ?? {}),
259
+ transition,
260
+ },
261
+ };
262
+ return {
263
+ attributes,
264
+ transitionParams: {
186
265
  transition: transition.type,
187
266
  ...(transition.duration !== undefined
188
267
  ? { transitionDuration: String(transition.duration) }
189
268
  : {}),
190
269
  ...(transition.direction ? { transitionDirection: transition.direction } : {}),
191
270
  },
192
- blockMeta: {
193
- ...(attrs.blockMeta ?? {}),
194
- transition,
195
- },
271
+ };
272
+ }
273
+
274
+ function removeTransitionParams(
275
+ attrs: MarkdownHeading['attributes'],
276
+ ): MarkdownHeading['attributes'] {
277
+ if (!attrs?.params) return attrs;
278
+ const params = { ...attrs.params };
279
+ for (const key of TRANSITION_PARAM_KEYS) delete params[key];
280
+ return {
281
+ ...attrs,
282
+ ...(Object.keys(params).length > 0 ? { params } : { params: undefined }),
196
283
  };
197
284
  }
package/src/doc/index.ts CHANGED
@@ -3,6 +3,7 @@ export * from './utils/animationUtils.js';
3
3
  export * from './utils/themeUtils.js';
4
4
  export * from './utils/applyRenderStyle.js';
5
5
  export * from './utils/imageTreatment.js';
6
+ export * from './utils/diagramText.js';
6
7
  export * from './templateInputs.js';
7
8
  export * from './utils/shapeGeometry.js';
8
9
  export {
@@ -11,12 +12,12 @@ export {
11
12
  flattenRenderableBlocks,
12
13
  countBlocks,
13
14
  getBlockDepth,
15
+ getBlockBodyText,
16
+ getPinnedBlockMeta,
14
17
  } from './markdownToDoc.js';
15
18
  export type { MarkdownToDocOptions } from './markdownToDoc.js';
16
19
  export { docToMarkdown } from './docToMarkdown.js';
17
20
  export {
18
- encodeLayersForFrontmatter,
19
- decodeLayersFromFrontmatter,
20
21
  readCustomTemplatesFromFrontmatter,
21
22
  writeCustomTemplatesToFrontmatter,
22
23
  FRONTMATTER_CUSTOM_TEMPLATES_KEY,
@@ -27,9 +28,8 @@ export {
27
28
  FRONTMATTER_CUSTOM_THEMES_KEY,
28
29
  } from './customThemesFrontmatter.js';
29
30
  export { resolveThemeForDoc } from './resolveDocTheme.js';
30
- export { getLayers } from './getLayers.js';
31
- export type { RenderContext } from './getLayers.js';
32
31
  export { resolveAudioMapping, scoreTextSimilarity } from './audioMapping.js';
32
+ export { applyNarrationTiming, type NarrationResolution } from './applyNarrationTiming.js';
33
33
  export {
34
34
  isDataFence,
35
35
  parseDataFence,
@@ -53,3 +53,75 @@ export type {
53
53
  TemplateInputDescriptor,
54
54
  TemplateParamFinding,
55
55
  } from './templates/inputDescriptors.js';
56
+ export {
57
+ ASCII_CHAR_H,
58
+ ASCII_CHAR_W,
59
+ ASCII_DIAGRAM_FENCE_LANGS,
60
+ asciiCellToCanvas,
61
+ asciiDiagramFromBlocks,
62
+ asciiDiagramFromTemplateData,
63
+ asciiDiagramToTemplateData,
64
+ canvasToAsciiCell,
65
+ detectAsciiDiagram,
66
+ isAsciiDiagramFence,
67
+ isEligibleAsciiFenceLang,
68
+ isExplicitDiagramLang,
69
+ isRepairableDiagram,
70
+ parseAsciiDiagram,
71
+ renderAsciiDiagram,
72
+ repairAsciiDiagram,
73
+ } from './asciiDiagram/index.js';
74
+ export type {
75
+ AsciiDiagram,
76
+ AsciiDiagramDetection,
77
+ AsciiDiagramEdge,
78
+ AsciiDiagramNode,
79
+ RenderAsciiDiagramOptions,
80
+ RepairResult,
81
+ } from './asciiDiagram/index.js';
82
+ export {
83
+ ASCII_TIMELINE_FENCE_LANGS,
84
+ asciiTimelineToTemplateData,
85
+ detectAsciiTimeline,
86
+ isAsciiTimelineFence,
87
+ isEligibleAsciiTimelineFenceLang,
88
+ isExplicitTimelineLang,
89
+ isWrappedFlowTimelineSource,
90
+ parseAsciiTimeline,
91
+ parseAsciiTimelineWithStats,
92
+ parseWrappedFlowTimeline,
93
+ renderAsciiTimeline,
94
+ } from './asciiTimeline/index.js';
95
+ export type {
96
+ AsciiTimeline,
97
+ AsciiTimelineDetection,
98
+ AsciiTimelineEvent,
99
+ AsciiTimelineLink,
100
+ AsciiTimelineMarker,
101
+ AsciiTimelineSide,
102
+ AsciiTimelineStats,
103
+ AsciiTimelineStyle,
104
+ AsciiTimelineTrack,
105
+ DetectAsciiTimelineOptions,
106
+ RenderAsciiTimelineOptions,
107
+ } from './asciiTimeline/index.js';
108
+ export {
109
+ TREE_FENCE_LANGS,
110
+ detectTree,
111
+ findFirstList,
112
+ isEligibleTreeFenceLang,
113
+ isExplicitTreeLang,
114
+ isTreeFence,
115
+ parseTree,
116
+ renderTree,
117
+ treeFromMarkdownList,
118
+ treeFromTemplateData,
119
+ treeToTemplateData,
120
+ } from './treeview/index.js';
121
+ export type {
122
+ RenderTreeOptions,
123
+ Tree,
124
+ TreeDetection,
125
+ TreeItem,
126
+ TreeNode,
127
+ } from './treeview/index.js';
@@ -35,6 +35,7 @@ import { readCustomThemesFromFrontmatter } from './customThemesFrontmatter.js';
35
35
  import type {
36
36
  MarkdownDocument,
37
37
  MarkdownBlockNode,
38
+ MarkdownCodeBlock,
38
39
  MarkdownHeading,
39
40
  MarkdownNode,
40
41
  HtmlNode,
@@ -49,6 +50,11 @@ import { extractTemplateBlocksFromContents } from './annotationBlocks.js';
49
50
  import type { ParsedAnnotation } from './standaloneAnnotation.js';
50
51
  import { profileBlockContents, pickAutoTemplate } from '../recommend/templates.js';
51
52
  import { deriveTemplateInputs } from './templateInputs.js';
53
+ import { isEligibleAsciiFenceLang } from './asciiDiagram/detect.js';
54
+ import { parseAsciiDiagram } from './asciiDiagram/parse.js';
55
+ import { asciiDiagramToTemplateData } from './asciiDiagram/mapping.js';
56
+ import { isEligibleAsciiTimelineFenceLang } from './asciiTimeline/detect.js';
57
+ import { parseAsciiTimeline } from './asciiTimeline/parse.js';
52
58
  import type { MediaClip } from '../schemas/Media.js';
53
59
 
54
60
  // ============================================
@@ -222,6 +228,17 @@ function pinnedMeta(block: Block): CoercedBlockMeta {
222
228
  return {};
223
229
  }
224
230
 
231
+ /**
232
+ * Public accessor for {@link pinnedMeta}: the timing/meta values an author
233
+ * pinned on a block (heading `{key=value}` attrs or `{[key=value]}`
234
+ * annotation params). Consumers use this to tell "author pinned this"
235
+ * from "derive it" — e.g. narration timing never overwrites a pinned
236
+ * `duration=`/`startTime=`.
237
+ */
238
+ export function getPinnedBlockMeta(block: Block): CoercedBlockMeta {
239
+ return pinnedMeta(block);
240
+ }
241
+
225
242
  /**
226
243
  * Rebuild a block list, lifting standalone `{[…]}` template annotations out of
227
244
  * each block's body into heading-less sibling blocks (recursing into children
@@ -408,6 +425,7 @@ export function markdownToDoc(markdownDoc: MarkdownDocument, options?: MarkdownT
408
425
  } = extractMediaFromContents(
409
426
  block.contents,
410
427
  (src, i) => `${block.id}-media-${i}-${src.replace(/[^a-zA-Z0-9]+/g, '-')}`,
428
+ block.id,
411
429
  );
412
430
  if (media.length > 0) block.media = media;
413
431
  if (docMedia.length > 0) documentMedia.push(...docMedia);
@@ -463,6 +481,16 @@ export function markdownToDoc(markdownDoc: MarkdownDocument, options?: MarkdownT
463
481
  applyStructuredData(block, diagnostics);
464
482
  }
465
483
 
484
+ // Explicitly diagram-annotated blocks whose body is a single eligible
485
+ // ASCII-art fence get nodes/edges derived from it — no detection
486
+ // threshold, the author asked for a diagram. Children-driven diagrams
487
+ // and author `json data` node lists win over the fence.
488
+ for (const block of allBlocks) {
489
+ applyAsciiDiagramData(block, diagnostics);
490
+ applyTimelineData(block, diagnostics);
491
+ applyTreeData(block, diagnostics);
492
+ }
493
+
466
494
  // Content-aware template auto-pick (default on): unannotated heading
467
495
  // blocks whose body carries a strong signal get the matching template
468
496
  // plus inputs derived from that body. Strict derivation — when the
@@ -472,6 +500,11 @@ export function markdownToDoc(markdownDoc: MarkdownDocument, options?: MarkdownT
472
500
  applyAutoTemplates(rootBlocks, resolveTemplateName(defaultTemplate), { featureIndex: 0 });
473
501
  }
474
502
 
503
+ // Timeline parsing is intentionally tolerant and can retain unresolved
504
+ // branch declarations as warnings. Surface those after auto-template
505
+ // selection so both explicit and auto-derived timeline blocks participate.
506
+ for (const block of allBlocks) collectTimelineWarnings(block, diagnostics);
507
+
475
508
  // Duplicate ids make connections and navigation ambiguous. Generated
476
509
  // slugs are already deduped; this catches author-pinned `{#id}` clashes.
477
510
  const seenIds = new Map<string, Block>();
@@ -709,6 +742,94 @@ function applyStructuredData(block: Block, diagnostics: DocDiagnostic[]): void {
709
742
  }
710
743
  }
711
744
 
745
+ /**
746
+ * Fill `templateData.nodes`/`edges` for an explicitly `{[diagram]}`-annotated
747
+ * block whose body is exactly one eligible ASCII-art fence. Runs without the
748
+ * detection threshold (explicit annotation = author intent), unconditionally
749
+ * of the autoTemplates switches. The fence stays in `contents`, untouched.
750
+ */
751
+ function applyAsciiDiagramData(block: Block, diagnostics: DocDiagnostic[]): void {
752
+ if (resolveTemplateName(block.template ?? '') !== 'diagram') return;
753
+ if (block.children && block.children.length > 0) return; // native children win
754
+ if (block.templateData && 'nodes' in block.templateData) return; // author data wins
755
+ const fences = (block.contents ?? []).filter((n): n is MarkdownCodeBlock => n.type === 'code');
756
+ if (fences.length !== 1 || !isEligibleAsciiFenceLang(fences[0].lang)) return;
757
+ const parsed = parseAsciiDiagram(fences[0].value);
758
+ if (parsed.nodes.length === 0) {
759
+ diagnostics.push({
760
+ severity: 'warning',
761
+ code: 'ascii-diagram-parse',
762
+ message: `Diagram block "${block.id}": code fence did not parse as an ASCII diagram`,
763
+ blockId: block.id,
764
+ });
765
+ return;
766
+ }
767
+ const { nodes, edges } = asciiDiagramToTemplateData(parsed);
768
+ block.templateData = { nodes, edges, ...block.templateData };
769
+ }
770
+
771
+ /**
772
+ * Fill `templateData.items` for an explicitly `{[tree]}`-annotated block
773
+ * whose body is one eligible ASCII tree fence (or a nested list). Runs
774
+ * without the detection threshold (explicit annotation = author intent).
775
+ */
776
+ function applyTreeData(block: Block, diagnostics: DocDiagnostic[]): void {
777
+ if (resolveTemplateName(block.template ?? '') !== 'tree') return;
778
+ if (block.templateData && 'items' in block.templateData) return; // author data wins
779
+ const derived = deriveTemplateInputs('tree', block.title ?? '', block.contents);
780
+ if (!derived || !Array.isArray(derived.items) || derived.items.length === 0) {
781
+ diagnostics.push({
782
+ severity: 'warning',
783
+ code: 'tree-parse',
784
+ message: `Tree block "${block.id}": body did not parse as a tree`,
785
+ blockId: block.id,
786
+ });
787
+ return;
788
+ }
789
+ block.templateData = { ...derived, ...block.templateData };
790
+ }
791
+
792
+ /**
793
+ * Fill `templateData.tracks`/`links` for an explicitly `{[timeline]}`-
794
+ * annotated block whose body is one eligible ASCII timeline fence.
795
+ */
796
+ function applyTimelineData(block: Block, diagnostics: DocDiagnostic[]): void {
797
+ if (resolveTemplateName(block.template ?? '') !== 'timeline') return;
798
+ if (block.templateData && 'tracks' in block.templateData) return; // author data wins
799
+ const derived = deriveTemplateInputs('timeline', block.title ?? '', block.contents);
800
+ if (!derived || !Array.isArray(derived.tracks) || derived.tracks.length === 0) {
801
+ diagnostics.push({
802
+ severity: 'warning',
803
+ code: 'timeline-parse',
804
+ message: `Timeline block "${block.id}": body did not parse as an ASCII timeline`,
805
+ blockId: block.id,
806
+ });
807
+ return;
808
+ }
809
+ block.templateData = { ...derived, ...block.templateData };
810
+ }
811
+
812
+ function collectTimelineWarnings(block: Block, diagnostics: DocDiagnostic[]): void {
813
+ if (resolveTemplateName(block.template ?? '') !== 'timeline') return;
814
+ if (!block.templateData || !('tracks' in block.templateData)) return;
815
+ const fences = (block.contents ?? []).filter(
816
+ (node): node is MarkdownCodeBlock =>
817
+ node.type === 'code' && isEligibleAsciiTimelineFenceLang(node.lang),
818
+ );
819
+ if (fences.length !== 1) return;
820
+ for (const warning of parseAsciiTimeline(fences[0].value).warnings) {
821
+ const unresolved = /^unresolved-link\((.+)\)$/.exec(warning);
822
+ diagnostics.push({
823
+ severity: 'warning',
824
+ code: unresolved ? 'timeline-unresolved-link' : 'timeline-parse-warning',
825
+ message: unresolved
826
+ ? `Timeline block "${block.id}": unresolved branch ${unresolved[1]}`
827
+ : `Timeline block "${block.id}": ${warning}`,
828
+ blockId: block.id,
829
+ });
830
+ }
831
+ }
832
+
712
833
  /** Truthiness of the `squisq-auto-templates` frontmatter kill-switch. */
713
834
  function frontmatterDisablesAutoTemplates(markdownDoc: MarkdownDocument): boolean {
714
835
  const v = markdownDoc.frontmatter?.['squisq-auto-templates'];
@@ -752,12 +873,25 @@ function applyAutoTemplates(
752
873
 
753
874
  /**
754
875
  * Extract the plain text from a block's body contents (excluding heading text).
876
+ *
877
+ * Exported because the narration script builder and the narration timing
878
+ * resolver must derive the *same* spoken text per block that captions and
879
+ * reading-time use — one source of truth for "what does this block say".
755
880
  */
756
- function getBlockBodyText(block: Block): string {
881
+ export function getBlockBodyText(block: Block): string {
757
882
  if (!block.contents || block.contents.length === 0) return '';
883
+ // A fence consumed as diagram data must not feed reading-time/captions —
884
+ // 20 lines of box-drawing art would add ~30s of duration and captions
885
+ // that "read" border runs aloud.
886
+ const tmpl = resolveTemplateName(block.template ?? '');
887
+ const consumedFence =
888
+ (tmpl === 'diagram' && block.templateData !== undefined && 'nodes' in block.templateData) ||
889
+ (tmpl === 'timeline' && block.templateData !== undefined && 'tracks' in block.templateData) ||
890
+ (tmpl === 'tree' && block.templateData !== undefined && 'items' in block.templateData);
758
891
  // Join with newlines to preserve paragraph/list-item boundaries.
759
892
  // splitIntoPhrases uses these newlines as natural split points.
760
893
  return block.contents
894
+ .filter((node) => !(consumedFence && node.type === 'code'))
761
895
  .map((node) => extractPlainText(node))
762
896
  .join('\n')
763
897
  .trim();