@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
@@ -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,
@@ -58,6 +60,29 @@ const TRANSITION_PARAM_KEYS = ['transition', 'transitionDuration', 'transitionDi
58
60
  export function docToMarkdown(doc: Doc): MarkdownDocument {
59
61
  const children: MarkdownBlockNode[] = [];
60
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
+
61
86
  function emitBlock(block: Block): void {
62
87
  // Emit the heading node if present
63
88
  if (block.sourceHeading) {
@@ -71,8 +96,29 @@ export function docToMarkdown(doc: Doc): MarkdownDocument {
71
96
  children.push(synthesizeAnnotationParagraph(block));
72
97
  }
73
98
 
74
- // Emit body content
75
- 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) {
76
122
  children.push(...block.contents);
77
123
  }
78
124
 
@@ -131,6 +177,28 @@ function synthesizeAnnotationParagraph(block: Block): MarkdownParagraph {
131
177
  return { type: 'paragraph', children: [{ type: 'text', value: text }] };
132
178
  }
133
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
+
134
202
  /**
135
203
  * Ensure the heading's `templateAnnotation` reflects the block's
136
204
  * template and templateOverrides. Returns a (possibly cloned) heading.
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,11 +35,12 @@ 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,
41
42
  } from '../markdown/types.js';
42
- import { extractPlainText } from '../markdown/utils.js';
43
+ import { extractPlainText, readFrontmatterThemeId } from '../markdown/utils.js';
43
44
  import { coerceAnnotationValues, type CoercedBlockMeta } from '../markdown/annotationCoercion.js';
44
45
  import { estimateReadingTime } from '../timing/readingTime.js';
45
46
  import { resolveTemplateName, isContainerTemplate } from './templates/index.js';
@@ -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>();
@@ -567,6 +600,7 @@ export function markdownToDoc(markdownDoc: MarkdownDocument, options?: MarkdownT
567
600
 
568
601
  const customTemplates = readCustomTemplatesFromFrontmatter(markdownDoc.frontmatter);
569
602
  const customThemes = readCustomThemesFromFrontmatter(markdownDoc.frontmatter);
603
+ const themeId = readFrontmatterThemeId(markdownDoc.frontmatter);
570
604
  const doc: Doc = {
571
605
  articleId,
572
606
  duration: currentTime,
@@ -576,6 +610,7 @@ export function markdownToDoc(markdownDoc: MarkdownDocument, options?: MarkdownT
576
610
  },
577
611
  ...(captions ? { captions } : {}),
578
612
  ...(markdownDoc.frontmatter ? { frontmatter: markdownDoc.frontmatter } : {}),
613
+ ...(themeId ? { themeId } : {}),
579
614
  ...(customTemplates ? { customTemplates } : {}),
580
615
  ...(customThemes ? { customThemes } : {}),
581
616
  ...(diagnostics.length > 0 ? { diagnostics } : {}),
@@ -709,6 +744,94 @@ function applyStructuredData(block: Block, diagnostics: DocDiagnostic[]): void {
709
744
  }
710
745
  }
711
746
 
747
+ /**
748
+ * Fill `templateData.nodes`/`edges` for an explicitly `{[diagram]}`-annotated
749
+ * block whose body is exactly one eligible ASCII-art fence. Runs without the
750
+ * detection threshold (explicit annotation = author intent), unconditionally
751
+ * of the autoTemplates switches. The fence stays in `contents`, untouched.
752
+ */
753
+ function applyAsciiDiagramData(block: Block, diagnostics: DocDiagnostic[]): void {
754
+ if (resolveTemplateName(block.template ?? '') !== 'diagram') return;
755
+ if (block.children && block.children.length > 0) return; // native children win
756
+ if (block.templateData && 'nodes' in block.templateData) return; // author data wins
757
+ const fences = (block.contents ?? []).filter((n): n is MarkdownCodeBlock => n.type === 'code');
758
+ if (fences.length !== 1 || !isEligibleAsciiFenceLang(fences[0].lang)) return;
759
+ const parsed = parseAsciiDiagram(fences[0].value);
760
+ if (parsed.nodes.length === 0) {
761
+ diagnostics.push({
762
+ severity: 'warning',
763
+ code: 'ascii-diagram-parse',
764
+ message: `Diagram block "${block.id}": code fence did not parse as an ASCII diagram`,
765
+ blockId: block.id,
766
+ });
767
+ return;
768
+ }
769
+ const { nodes, edges } = asciiDiagramToTemplateData(parsed);
770
+ block.templateData = { nodes, edges, ...block.templateData };
771
+ }
772
+
773
+ /**
774
+ * Fill `templateData.items` for an explicitly `{[tree]}`-annotated block
775
+ * whose body is one eligible ASCII tree fence (or a nested list). Runs
776
+ * without the detection threshold (explicit annotation = author intent).
777
+ */
778
+ function applyTreeData(block: Block, diagnostics: DocDiagnostic[]): void {
779
+ if (resolveTemplateName(block.template ?? '') !== 'tree') return;
780
+ if (block.templateData && 'items' in block.templateData) return; // author data wins
781
+ const derived = deriveTemplateInputs('tree', block.title ?? '', block.contents);
782
+ if (!derived || !Array.isArray(derived.items) || derived.items.length === 0) {
783
+ diagnostics.push({
784
+ severity: 'warning',
785
+ code: 'tree-parse',
786
+ message: `Tree block "${block.id}": body did not parse as a tree`,
787
+ blockId: block.id,
788
+ });
789
+ return;
790
+ }
791
+ block.templateData = { ...derived, ...block.templateData };
792
+ }
793
+
794
+ /**
795
+ * Fill `templateData.tracks`/`links` for an explicitly `{[timeline]}`-
796
+ * annotated block whose body is one eligible ASCII timeline fence.
797
+ */
798
+ function applyTimelineData(block: Block, diagnostics: DocDiagnostic[]): void {
799
+ if (resolveTemplateName(block.template ?? '') !== 'timeline') return;
800
+ if (block.templateData && 'tracks' in block.templateData) return; // author data wins
801
+ const derived = deriveTemplateInputs('timeline', block.title ?? '', block.contents);
802
+ if (!derived || !Array.isArray(derived.tracks) || derived.tracks.length === 0) {
803
+ diagnostics.push({
804
+ severity: 'warning',
805
+ code: 'timeline-parse',
806
+ message: `Timeline block "${block.id}": body did not parse as an ASCII timeline`,
807
+ blockId: block.id,
808
+ });
809
+ return;
810
+ }
811
+ block.templateData = { ...derived, ...block.templateData };
812
+ }
813
+
814
+ function collectTimelineWarnings(block: Block, diagnostics: DocDiagnostic[]): void {
815
+ if (resolveTemplateName(block.template ?? '') !== 'timeline') return;
816
+ if (!block.templateData || !('tracks' in block.templateData)) return;
817
+ const fences = (block.contents ?? []).filter(
818
+ (node): node is MarkdownCodeBlock =>
819
+ node.type === 'code' && isEligibleAsciiTimelineFenceLang(node.lang),
820
+ );
821
+ if (fences.length !== 1) return;
822
+ for (const warning of parseAsciiTimeline(fences[0].value).warnings) {
823
+ const unresolved = /^unresolved-link\((.+)\)$/.exec(warning);
824
+ diagnostics.push({
825
+ severity: 'warning',
826
+ code: unresolved ? 'timeline-unresolved-link' : 'timeline-parse-warning',
827
+ message: unresolved
828
+ ? `Timeline block "${block.id}": unresolved branch ${unresolved[1]}`
829
+ : `Timeline block "${block.id}": ${warning}`,
830
+ blockId: block.id,
831
+ });
832
+ }
833
+ }
834
+
712
835
  /** Truthiness of the `squisq-auto-templates` frontmatter kill-switch. */
713
836
  function frontmatterDisablesAutoTemplates(markdownDoc: MarkdownDocument): boolean {
714
837
  const v = markdownDoc.frontmatter?.['squisq-auto-templates'];
@@ -752,12 +875,25 @@ function applyAutoTemplates(
752
875
 
753
876
  /**
754
877
  * Extract the plain text from a block's body contents (excluding heading text).
878
+ *
879
+ * Exported because the narration script builder and the narration timing
880
+ * resolver must derive the *same* spoken text per block that captions and
881
+ * reading-time use — one source of truth for "what does this block say".
755
882
  */
756
- function getBlockBodyText(block: Block): string {
883
+ export function getBlockBodyText(block: Block): string {
757
884
  if (!block.contents || block.contents.length === 0) return '';
885
+ // A fence consumed as diagram data must not feed reading-time/captions —
886
+ // 20 lines of box-drawing art would add ~30s of duration and captions
887
+ // that "read" border runs aloud.
888
+ const tmpl = resolveTemplateName(block.template ?? '');
889
+ const consumedFence =
890
+ (tmpl === 'diagram' && block.templateData !== undefined && 'nodes' in block.templateData) ||
891
+ (tmpl === 'timeline' && block.templateData !== undefined && 'tracks' in block.templateData) ||
892
+ (tmpl === 'tree' && block.templateData !== undefined && 'items' in block.templateData);
758
893
  // Join with newlines to preserve paragraph/list-item boundaries.
759
894
  // splitIntoPhrases uses these newlines as natural split points.
760
895
  return block.contents
896
+ .filter((node) => !(consumedFence && node.type === 'code'))
761
897
  .map((node) => extractPlainText(node))
762
898
  .join('\n')
763
899
  .trim();