@bendyline/squisq 1.5.2 → 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 (263) 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-P3K7NLHF.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-BEQODJWV.js → chunk-66QO7XFB.js} +11 -291
  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-5TYS5V3G.js → chunk-GF3BVNLU.js} +4127 -3538
  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-GPLTCSXO.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-LTN6I7QW.js → chunk-TUWMK62B.js} +55 -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 +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__/dataTable.test.ts +25 -15
  98. package/src/__tests__/diagramBlock.test.ts +3 -3
  99. package/src/__tests__/diagramText.test.ts +26 -0
  100. package/src/__tests__/docToMarkdownMedia.test.ts +83 -0
  101. package/src/__tests__/drawingBlock.test.ts +3 -3
  102. package/src/__tests__/fixtures/asciiDiagramBattle.ts +512 -0
  103. package/src/__tests__/fixtures/asciiDiagrams.ts +287 -0
  104. package/src/__tests__/fixtures/asciiTimelines.ts +35 -0
  105. package/src/__tests__/fixtures/treeviewFixtures.ts +243 -0
  106. package/src/__tests__/getLayers.test.ts +32 -18
  107. package/src/__tests__/getLayersFallback.test.ts +8 -8
  108. package/src/__tests__/imageEditVersions.test.ts +2 -0
  109. package/src/__tests__/jsonForm.pointer.test.ts +47 -1
  110. package/src/__tests__/layoutBlock.test.ts +3 -3
  111. package/src/__tests__/majorApiTypes.test.ts +18 -0
  112. package/src/__tests__/markdown.test.ts +41 -0
  113. package/src/__tests__/matchFontFamily.test.ts +114 -0
  114. package/src/__tests__/materializeBlockLayers.test.ts +135 -0
  115. package/src/__tests__/materializeTestUtils.ts +17 -0
  116. package/src/__tests__/narrationAlign.test.ts +150 -0
  117. package/src/__tests__/narrationFeatures.test.ts +71 -0
  118. package/src/__tests__/narrationNuclei.test.ts +45 -0
  119. package/src/__tests__/narrationPacing.test.ts +189 -0
  120. package/src/__tests__/narrationScript.test.ts +149 -0
  121. package/src/__tests__/narrationSessionRepro.test.ts +62 -0
  122. package/src/__tests__/narrationSidecar.test.ts +88 -0
  123. package/src/__tests__/narrationSyllables.test.ts +65 -0
  124. package/src/__tests__/narrationTestSignals.ts +167 -0
  125. package/src/__tests__/narrationVad.test.ts +66 -0
  126. package/src/__tests__/resolveDocTheme.test.ts +16 -2
  127. package/src/__tests__/rootExports.test.ts +18 -0
  128. package/src/__tests__/scopedContentContainer.test.ts +20 -0
  129. package/src/__tests__/shapeGeometry.test.ts +12 -0
  130. package/src/__tests__/templateAnnotationParse.test.ts +10 -0
  131. package/src/__tests__/templateInputs.test.ts +69 -0
  132. package/src/__tests__/templateMetadata.test.ts +2 -1
  133. package/src/__tests__/templates.test.ts +143 -17
  134. package/src/__tests__/themeCompile.test.ts +49 -22
  135. package/src/__tests__/transformProvenance.test.ts +160 -0
  136. package/src/__tests__/transformV2.test.ts +40 -14
  137. package/src/__tests__/treeviewBattle.test.ts +76 -0
  138. package/src/__tests__/treeviewDetect.test.ts +168 -0
  139. package/src/__tests__/treeviewParse.test.ts +118 -0
  140. package/src/__tests__/treeviewPipeline.test.ts +183 -0
  141. package/src/__tests__/treeviewRender.test.ts +91 -0
  142. package/src/__tests__/versions.test.ts +44 -0
  143. package/src/doc/applyNarrationTiming.ts +244 -0
  144. package/src/doc/asciiDiagram/chars.ts +210 -0
  145. package/src/doc/asciiDiagram/detect.ts +175 -0
  146. package/src/doc/asciiDiagram/index.ts +38 -0
  147. package/src/doc/asciiDiagram/mapping.ts +225 -0
  148. package/src/doc/asciiDiagram/parse.ts +1095 -0
  149. package/src/doc/asciiDiagram/render.ts +963 -0
  150. package/src/doc/asciiDiagram/repair.ts +400 -0
  151. package/src/doc/asciiDiagram/types.ts +65 -0
  152. package/src/doc/asciiTimeline/detect.ts +138 -0
  153. package/src/doc/asciiTimeline/index.ts +26 -0
  154. package/src/doc/asciiTimeline/mapping.ts +43 -0
  155. package/src/doc/asciiTimeline/parse.ts +870 -0
  156. package/src/doc/asciiTimeline/render.ts +427 -0
  157. package/src/doc/asciiTimeline/types.ts +73 -0
  158. package/src/doc/asciiTimeline/wrappedFlow.ts +427 -0
  159. package/src/doc/audioMapping.ts +60 -46
  160. package/src/doc/customTemplatesFrontmatter.ts +0 -41
  161. package/src/doc/docToMarkdown.ts +70 -2
  162. package/src/doc/index.ts +76 -4
  163. package/src/doc/markdownToDoc.ts +135 -1
  164. package/src/doc/materializeBlockLayers.ts +304 -0
  165. package/src/doc/mediaAnnotations.ts +20 -0
  166. package/src/doc/resolveDocTheme.ts +8 -7
  167. package/src/doc/templateInputs.ts +185 -2
  168. package/src/doc/templates/__tests__/customTemplateRegistry.test.ts +22 -8
  169. package/src/doc/templates/diagramBlock.ts +237 -38
  170. package/src/doc/templates/fallbackBlock.ts +1 -1
  171. package/src/doc/templates/index.ts +134 -279
  172. package/src/doc/templates/inputDescriptors.ts +20 -1
  173. package/src/doc/templates/metadata.ts +10 -0
  174. package/src/doc/templates/persistentLayers.ts +12 -126
  175. package/src/doc/templates/registry.ts +88 -0
  176. package/src/doc/templates/templateNames.ts +22 -0
  177. package/src/doc/templates/timelineBlock.ts +613 -0
  178. package/src/doc/templates/treeBlock.ts +131 -0
  179. package/src/doc/treeview/chars.ts +72 -0
  180. package/src/doc/treeview/detect.ts +145 -0
  181. package/src/doc/treeview/index.ts +30 -0
  182. package/src/doc/treeview/mapping.ts +95 -0
  183. package/src/doc/treeview/parse.ts +201 -0
  184. package/src/doc/treeview/render.ts +41 -0
  185. package/src/doc/treeview/types.ts +43 -0
  186. package/src/doc/utils/applyRenderStyle.ts +2 -9
  187. package/src/doc/utils/diagramText.ts +78 -0
  188. package/src/doc/utils/shapeGeometry.ts +67 -5
  189. package/src/doc/validate.ts +1 -1
  190. package/src/generate/templateMapper.ts +7 -0
  191. package/src/imageEdit/export.ts +6 -5
  192. package/src/imageEdit/versionPaths.ts +13 -26
  193. package/src/imageEdit/versions.ts +21 -100
  194. package/src/index.ts +1 -0
  195. package/src/internal/immutable.ts +58 -0
  196. package/src/jsonForm/index.ts +9 -1
  197. package/src/jsonForm/pointer.ts +68 -23
  198. package/src/markdown/attrTokens.ts +7 -5
  199. package/src/markdown/index.ts +1 -0
  200. package/src/markdown/stringify.ts +4 -9
  201. package/src/markdown/utils.ts +36 -14
  202. package/src/narration/align.ts +514 -0
  203. package/src/narration/features.ts +231 -0
  204. package/src/narration/index.ts +86 -0
  205. package/src/narration/nuclei.ts +118 -0
  206. package/src/narration/pacing.ts +211 -0
  207. package/src/narration/script.ts +239 -0
  208. package/src/narration/session.ts +100 -0
  209. package/src/narration/sidecar.ts +187 -0
  210. package/src/narration/syllables.ts +85 -0
  211. package/src/narration/trace.ts +50 -0
  212. package/src/narration/types.ts +299 -0
  213. package/src/narration/vad.ts +131 -0
  214. package/src/recommend/numberHighlight.ts +25 -0
  215. package/src/recommend/templates.ts +108 -12
  216. package/src/schemas/BlockTemplates.ts +156 -7
  217. package/src/schemas/Doc.ts +72 -9
  218. package/src/schemas/Media.ts +12 -2
  219. package/src/schemas/Theme.ts +45 -25
  220. package/src/schemas/Viewport.ts +2 -2
  221. package/src/schemas/fontStacks.ts +79 -0
  222. package/src/schemas/themeCompile.ts +15 -0
  223. package/src/schemas/themeConstants.ts +2 -0
  224. package/src/schemas/themeLibrary.ts +25 -19
  225. package/src/schemas/themeValidator.ts +1 -1
  226. package/src/spatial/Geohash.ts +0 -3
  227. package/src/storage/ContentContainer.ts +17 -9
  228. package/src/storage/ScopedContentContainer.ts +45 -15
  229. package/src/transform/applyTransform.ts +5 -5
  230. package/src/transform/index.ts +3 -1
  231. package/src/transform/registry.ts +169 -49
  232. package/src/transform/templateSelector.ts +21 -2
  233. package/src/transform/timingAllocator.ts +152 -21
  234. package/src/transform/types.ts +14 -1
  235. package/src/versions/gitignore.ts +34 -0
  236. package/src/versions/index.ts +2 -0
  237. package/src/versions/operations.ts +18 -104
  238. package/src/versions/paths.ts +13 -28
  239. package/src/versions/snapshotStore.ts +202 -0
  240. package/src/versions/types.ts +3 -1
  241. package/dist/chunk-5TYS5V3G.js.map +0 -1
  242. package/dist/chunk-A4O7GIWE.js.map +0 -1
  243. package/dist/chunk-BEQODJWV.js.map +0 -1
  244. package/dist/chunk-DLXZMS5K.js.map +0 -1
  245. package/dist/chunk-GPLTCSXO.js.map +0 -1
  246. package/dist/chunk-HWVFJAAH.js.map +0 -1
  247. package/dist/chunk-IMSCRLLV.js.map +0 -1
  248. package/dist/chunk-JELIRVDP.js +0 -24
  249. package/dist/chunk-JELIRVDP.js.map +0 -1
  250. package/dist/chunk-KKNUBQ6Y.js.map +0 -1
  251. package/dist/chunk-LDQ2HJIX.js.map +0 -1
  252. package/dist/chunk-LTN6I7QW.js.map +0 -1
  253. package/dist/chunk-P3K7NLHF.js.map +0 -1
  254. package/dist/chunk-T5UK6YOB.js.map +0 -1
  255. package/dist/chunk-UY7KGQ5R.js +0 -192
  256. package/dist/chunk-UY7KGQ5R.js.map +0 -1
  257. package/dist/chunk-VLJJHDAC.js +0 -218
  258. package/dist/chunk-VLJJHDAC.js.map +0 -1
  259. package/dist/chunk-VWUFZ6ZG.js.map +0 -1
  260. package/dist/story/index.d.ts +0 -1801
  261. package/dist/story/index.js +0 -279
  262. package/src/doc/getLayers.ts +0 -195
  263. /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,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();