@bendyline/squisq 1.5.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (268) hide show
  1. package/README.md +63 -10
  2. package/dist/{Doc-BBVGq1_9.d.ts → Doc-ylqWKDc_.d.ts} +248 -37
  3. package/dist/{ImageEditDoc-FuyGtt6o.d.ts → ImageEditDoc-D828IXJ1.d.ts} +1 -1
  4. package/dist/annotationCoercion-Dr_DY9Hx.d.ts +86 -0
  5. package/dist/{chunk-VWUFZ6ZG.js → chunk-3E5F2XMR.js} +2 -29
  6. package/dist/chunk-3E5F2XMR.js.map +1 -0
  7. package/dist/chunk-4X3JQXNM.js +35 -0
  8. package/dist/chunk-4X3JQXNM.js.map +1 -0
  9. package/dist/{chunk-2OIBZYKP.js → chunk-5DKBFXPY.js} +31 -121
  10. package/dist/chunk-5DKBFXPY.js.map +1 -0
  11. package/dist/{chunk-DLXZMS5K.js → chunk-5J4HKRJF.js} +51 -3
  12. package/dist/chunk-5J4HKRJF.js.map +1 -0
  13. package/dist/chunk-5UYI3RJB.js +2503 -0
  14. package/dist/chunk-5UYI3RJB.js.map +1 -0
  15. package/dist/{chunk-TEEEILMP.js → chunk-66QO7XFB.js} +19 -292
  16. package/dist/chunk-66QO7XFB.js.map +1 -0
  17. package/dist/{chunk-KKNUBQ6Y.js → chunk-6PJRB2CF.js} +156 -91
  18. package/dist/chunk-6PJRB2CF.js.map +1 -0
  19. package/dist/{chunk-HWVFJAAH.js → chunk-AMIR72JP.js} +28 -10
  20. package/dist/chunk-AMIR72JP.js.map +1 -0
  21. package/dist/chunk-FVJUUTEM.js +74 -0
  22. package/dist/chunk-FVJUUTEM.js.map +1 -0
  23. package/dist/{chunk-LH7I6SH7.js → chunk-GF3BVNLU.js} +6360 -5723
  24. package/dist/chunk-GF3BVNLU.js.map +1 -0
  25. package/dist/{chunk-Q3ROPT5H.js → chunk-GZY66SDN.js} +1 -1
  26. package/dist/{chunk-Q3ROPT5H.js.map → chunk-GZY66SDN.js.map} +1 -1
  27. package/dist/chunk-JOQYPRDZ.js +127 -0
  28. package/dist/chunk-JOQYPRDZ.js.map +1 -0
  29. package/dist/{chunk-A4O7GIWE.js → chunk-LYNJEOAC.js} +10 -73
  30. package/dist/chunk-LYNJEOAC.js.map +1 -0
  31. package/dist/{chunk-LDQ2HJIX.js → chunk-PXLVXTLW.js} +252 -54
  32. package/dist/chunk-PXLVXTLW.js.map +1 -0
  33. package/dist/{chunk-TQWLI6S2.js → chunk-Q7LROV2H.js} +4 -2
  34. package/dist/{chunk-TQWLI6S2.js.map → chunk-Q7LROV2H.js.map} +1 -1
  35. package/dist/{chunk-T5UK6YOB.js → chunk-QTSJOY54.js} +60 -17
  36. package/dist/chunk-QTSJOY54.js.map +1 -0
  37. package/dist/chunk-QVANWDPH.js +2358 -0
  38. package/dist/chunk-QVANWDPH.js.map +1 -0
  39. package/dist/{chunk-6ATE2PSM.js → chunk-R2GC6RLD.js} +7 -7
  40. package/dist/chunk-R2GC6RLD.js.map +1 -0
  41. package/dist/chunk-R2YZTCLO.js +292 -0
  42. package/dist/chunk-R2YZTCLO.js.map +1 -0
  43. package/dist/chunk-S7A5D7ZJ.js +174 -0
  44. package/dist/chunk-S7A5D7ZJ.js.map +1 -0
  45. package/dist/chunk-T3TI7GSZ.js +884 -0
  46. package/dist/chunk-T3TI7GSZ.js.map +1 -0
  47. package/dist/{chunk-RUDYOTA6.js → chunk-TUWMK62B.js} +110 -4
  48. package/dist/chunk-TUWMK62B.js.map +1 -0
  49. package/dist/{chunk-IMSCRLLV.js → chunk-VR7UPJ7C.js} +35 -25
  50. package/dist/chunk-VR7UPJ7C.js.map +1 -0
  51. package/dist/doc/index.d.ts +2334 -5
  52. package/dist/doc/index.js +134 -34
  53. package/dist/generate/index.d.ts +7 -2
  54. package/dist/generate/index.js +1 -1
  55. package/dist/imageEdit/index.d.ts +9 -6
  56. package/dist/imageEdit/index.js +3 -3
  57. package/dist/index.d.ts +12 -10
  58. package/dist/index.js +253 -81
  59. package/dist/jsonForm/index.d.ts +7 -3
  60. package/dist/jsonForm/index.js +9 -4
  61. package/dist/markdown/index.d.ts +6 -70
  62. package/dist/markdown/index.js +13 -9
  63. package/dist/narration/index.d.ts +618 -0
  64. package/dist/narration/index.js +82 -0
  65. package/dist/recommend/index.d.ts +15 -1
  66. package/dist/recommend/index.js +2 -2
  67. package/dist/schemas/index.d.ts +28 -6
  68. package/dist/schemas/index.js +15 -13
  69. package/dist/spatial/index.d.ts +0 -1
  70. package/dist/spatial/index.js +1 -1
  71. package/dist/storage/index.js +1 -1
  72. package/dist/{themeLibrary-CehcJhzz.d.ts → themeLibrary-RjVgLpUi.d.ts} +10 -11
  73. package/dist/timing/index.js +5 -3
  74. package/dist/transform/index.d.ts +35 -24
  75. package/dist/transform/index.js +9 -10
  76. package/dist/{types-DlAZ7MW4.d.ts → types-BwMPl2CB.d.ts} +1 -1
  77. package/dist/{types-D98li1gV.d.ts → types-C4kiRn_k.d.ts} +3 -1
  78. package/dist/versions/index.d.ts +2 -1
  79. package/dist/versions/index.js +2 -2
  80. package/package.json +6 -6
  81. package/src/__tests__/applyNarrationTiming.test.ts +190 -0
  82. package/src/__tests__/applyRenderStyle.test.ts +3 -3
  83. package/src/__tests__/asciiDiagramBattle.test.ts +246 -0
  84. package/src/__tests__/asciiDiagramDetect.test.ts +162 -0
  85. package/src/__tests__/asciiDiagramMapping.test.ts +38 -0
  86. package/src/__tests__/asciiDiagramParse.test.ts +233 -0
  87. package/src/__tests__/asciiDiagramPipeline.test.ts +273 -0
  88. package/src/__tests__/asciiDiagramRender.test.ts +162 -0
  89. package/src/__tests__/asciiDiagramRepair.test.ts +84 -0
  90. package/src/__tests__/asciiTimelineDetect.test.ts +157 -0
  91. package/src/__tests__/asciiTimelineParse.test.ts +205 -0
  92. package/src/__tests__/asciiTimelinePipeline.test.ts +349 -0
  93. package/src/__tests__/asciiTimelineRender.test.ts +376 -0
  94. package/src/__tests__/attrTokens.test.ts +5 -0
  95. package/src/__tests__/contentContainer.test.ts +43 -0
  96. package/src/__tests__/coverBlock.test.ts +33 -2
  97. package/src/__tests__/customTemplatesFrontmatter.test.ts +0 -15
  98. package/src/__tests__/dataTable.test.ts +25 -15
  99. package/src/__tests__/diagramBlock.test.ts +3 -3
  100. package/src/__tests__/diagramText.test.ts +26 -0
  101. package/src/__tests__/docToMarkdownMedia.test.ts +83 -0
  102. package/src/__tests__/drawingBlock.test.ts +3 -3
  103. package/src/__tests__/fixtures/asciiDiagramBattle.ts +512 -0
  104. package/src/__tests__/fixtures/asciiDiagrams.ts +287 -0
  105. package/src/__tests__/fixtures/asciiTimelines.ts +35 -0
  106. package/src/__tests__/fixtures/treeviewFixtures.ts +243 -0
  107. package/src/__tests__/getLayers.test.ts +32 -18
  108. package/src/__tests__/getLayersFallback.test.ts +8 -8
  109. package/src/__tests__/imageEditVersions.test.ts +2 -0
  110. package/src/__tests__/jsonForm.pointer.test.ts +47 -1
  111. package/src/__tests__/layoutBlock.test.ts +3 -3
  112. package/src/__tests__/majorApiTypes.test.ts +18 -0
  113. package/src/__tests__/markdown.test.ts +41 -0
  114. package/src/__tests__/markdownToDoc.test.ts +5 -2
  115. package/src/__tests__/matchFontFamily.test.ts +114 -0
  116. package/src/__tests__/materializeBlockLayers.test.ts +135 -0
  117. package/src/__tests__/materializeTestUtils.ts +17 -0
  118. package/src/__tests__/narrationAlign.test.ts +150 -0
  119. package/src/__tests__/narrationFeatures.test.ts +71 -0
  120. package/src/__tests__/narrationNuclei.test.ts +45 -0
  121. package/src/__tests__/narrationPacing.test.ts +189 -0
  122. package/src/__tests__/narrationScript.test.ts +149 -0
  123. package/src/__tests__/narrationSessionRepro.test.ts +62 -0
  124. package/src/__tests__/narrationSidecar.test.ts +88 -0
  125. package/src/__tests__/narrationSyllables.test.ts +65 -0
  126. package/src/__tests__/narrationTestSignals.ts +167 -0
  127. package/src/__tests__/narrationVad.test.ts +66 -0
  128. package/src/__tests__/resolveDocTheme.test.ts +16 -2
  129. package/src/__tests__/rootExports.test.ts +18 -0
  130. package/src/__tests__/scopedContentContainer.test.ts +20 -0
  131. package/src/__tests__/shapeGeometry.test.ts +49 -0
  132. package/src/__tests__/templateAnnotationParse.test.ts +10 -0
  133. package/src/__tests__/templateInputs.test.ts +69 -0
  134. package/src/__tests__/templateMetadata.test.ts +2 -1
  135. package/src/__tests__/templates.test.ts +143 -17
  136. package/src/__tests__/themeCompile.test.ts +49 -22
  137. package/src/__tests__/transformProvenance.test.ts +160 -0
  138. package/src/__tests__/transformV2.test.ts +40 -14
  139. package/src/__tests__/treeviewBattle.test.ts +76 -0
  140. package/src/__tests__/treeviewDetect.test.ts +168 -0
  141. package/src/__tests__/treeviewParse.test.ts +118 -0
  142. package/src/__tests__/treeviewPipeline.test.ts +183 -0
  143. package/src/__tests__/treeviewRender.test.ts +91 -0
  144. package/src/__tests__/versions.test.ts +44 -0
  145. package/src/doc/applyNarrationTiming.ts +244 -0
  146. package/src/doc/asciiDiagram/chars.ts +210 -0
  147. package/src/doc/asciiDiagram/detect.ts +175 -0
  148. package/src/doc/asciiDiagram/index.ts +38 -0
  149. package/src/doc/asciiDiagram/mapping.ts +225 -0
  150. package/src/doc/asciiDiagram/parse.ts +1095 -0
  151. package/src/doc/asciiDiagram/render.ts +963 -0
  152. package/src/doc/asciiDiagram/repair.ts +400 -0
  153. package/src/doc/asciiDiagram/types.ts +65 -0
  154. package/src/doc/asciiTimeline/detect.ts +138 -0
  155. package/src/doc/asciiTimeline/index.ts +26 -0
  156. package/src/doc/asciiTimeline/mapping.ts +43 -0
  157. package/src/doc/asciiTimeline/parse.ts +870 -0
  158. package/src/doc/asciiTimeline/render.ts +427 -0
  159. package/src/doc/asciiTimeline/types.ts +73 -0
  160. package/src/doc/asciiTimeline/wrappedFlow.ts +427 -0
  161. package/src/doc/audioMapping.ts +60 -46
  162. package/src/doc/customTemplatesFrontmatter.ts +0 -41
  163. package/src/doc/docToMarkdown.ts +126 -39
  164. package/src/doc/index.ts +76 -4
  165. package/src/doc/markdownToDoc.ts +135 -1
  166. package/src/doc/materializeBlockLayers.ts +304 -0
  167. package/src/doc/mediaAnnotations.ts +20 -0
  168. package/src/doc/resolveDocTheme.ts +8 -7
  169. package/src/doc/templateInputs.ts +185 -2
  170. package/src/doc/templates/__tests__/customTemplateRegistry.test.ts +22 -8
  171. package/src/doc/templates/coverBlock.ts +49 -13
  172. package/src/doc/templates/diagramBlock.ts +237 -38
  173. package/src/doc/templates/drawingBlock.ts +2 -2
  174. package/src/doc/templates/fallbackBlock.ts +1 -1
  175. package/src/doc/templates/index.ts +134 -279
  176. package/src/doc/templates/inputDescriptors.ts +20 -1
  177. package/src/doc/templates/metadata.ts +10 -0
  178. package/src/doc/templates/persistentLayers.ts +12 -126
  179. package/src/doc/templates/registry.ts +88 -0
  180. package/src/doc/templates/templateNames.ts +22 -0
  181. package/src/doc/templates/timelineBlock.ts +613 -0
  182. package/src/doc/templates/treeBlock.ts +131 -0
  183. package/src/doc/treeview/chars.ts +72 -0
  184. package/src/doc/treeview/detect.ts +145 -0
  185. package/src/doc/treeview/index.ts +30 -0
  186. package/src/doc/treeview/mapping.ts +95 -0
  187. package/src/doc/treeview/parse.ts +201 -0
  188. package/src/doc/treeview/render.ts +41 -0
  189. package/src/doc/treeview/types.ts +43 -0
  190. package/src/doc/utils/applyRenderStyle.ts +2 -9
  191. package/src/doc/utils/diagramText.ts +78 -0
  192. package/src/doc/utils/shapeGeometry.ts +157 -7
  193. package/src/doc/validate.ts +1 -1
  194. package/src/generate/templateMapper.ts +7 -0
  195. package/src/imageEdit/export.ts +6 -5
  196. package/src/imageEdit/versionPaths.ts +13 -26
  197. package/src/imageEdit/versions.ts +21 -100
  198. package/src/index.ts +1 -0
  199. package/src/internal/immutable.ts +58 -0
  200. package/src/jsonForm/index.ts +9 -1
  201. package/src/jsonForm/pointer.ts +68 -23
  202. package/src/markdown/attrTokens.ts +7 -5
  203. package/src/markdown/convert.ts +11 -1
  204. package/src/markdown/index.ts +1 -0
  205. package/src/markdown/stringify.ts +4 -9
  206. package/src/markdown/utils.ts +36 -14
  207. package/src/narration/align.ts +514 -0
  208. package/src/narration/features.ts +231 -0
  209. package/src/narration/index.ts +86 -0
  210. package/src/narration/nuclei.ts +118 -0
  211. package/src/narration/pacing.ts +211 -0
  212. package/src/narration/script.ts +239 -0
  213. package/src/narration/session.ts +100 -0
  214. package/src/narration/sidecar.ts +187 -0
  215. package/src/narration/syllables.ts +85 -0
  216. package/src/narration/trace.ts +50 -0
  217. package/src/narration/types.ts +299 -0
  218. package/src/narration/vad.ts +131 -0
  219. package/src/recommend/numberHighlight.ts +25 -0
  220. package/src/recommend/templates.ts +108 -12
  221. package/src/schemas/BlockTemplates.ts +156 -7
  222. package/src/schemas/Doc.ts +72 -9
  223. package/src/schemas/Media.ts +12 -2
  224. package/src/schemas/Theme.ts +45 -25
  225. package/src/schemas/Viewport.ts +2 -2
  226. package/src/schemas/fontStacks.ts +79 -0
  227. package/src/schemas/themeCompile.ts +15 -0
  228. package/src/schemas/themeConstants.ts +2 -0
  229. package/src/schemas/themeLibrary.ts +25 -19
  230. package/src/schemas/themeValidator.ts +1 -1
  231. package/src/spatial/Geohash.ts +0 -3
  232. package/src/storage/ContentContainer.ts +17 -9
  233. package/src/storage/ScopedContentContainer.ts +45 -15
  234. package/src/transform/applyTransform.ts +5 -5
  235. package/src/transform/index.ts +3 -1
  236. package/src/transform/registry.ts +169 -49
  237. package/src/transform/templateSelector.ts +21 -2
  238. package/src/transform/timingAllocator.ts +152 -21
  239. package/src/transform/types.ts +14 -1
  240. package/src/versions/gitignore.ts +34 -0
  241. package/src/versions/index.ts +2 -0
  242. package/src/versions/operations.ts +18 -104
  243. package/src/versions/paths.ts +13 -28
  244. package/src/versions/snapshotStore.ts +202 -0
  245. package/src/versions/types.ts +3 -1
  246. package/dist/chunk-2OIBZYKP.js.map +0 -1
  247. package/dist/chunk-6ATE2PSM.js.map +0 -1
  248. package/dist/chunk-A4O7GIWE.js.map +0 -1
  249. package/dist/chunk-DLXZMS5K.js.map +0 -1
  250. package/dist/chunk-HWVFJAAH.js.map +0 -1
  251. package/dist/chunk-IMSCRLLV.js.map +0 -1
  252. package/dist/chunk-JELIRVDP.js +0 -24
  253. package/dist/chunk-JELIRVDP.js.map +0 -1
  254. package/dist/chunk-KKNUBQ6Y.js.map +0 -1
  255. package/dist/chunk-LDQ2HJIX.js.map +0 -1
  256. package/dist/chunk-LH7I6SH7.js.map +0 -1
  257. package/dist/chunk-RUDYOTA6.js.map +0 -1
  258. package/dist/chunk-T5UK6YOB.js.map +0 -1
  259. package/dist/chunk-TEEEILMP.js.map +0 -1
  260. package/dist/chunk-UY7KGQ5R.js +0 -192
  261. package/dist/chunk-UY7KGQ5R.js.map +0 -1
  262. package/dist/chunk-VLJJHDAC.js +0 -218
  263. package/dist/chunk-VLJJHDAC.js.map +0 -1
  264. package/dist/chunk-VWUFZ6ZG.js.map +0 -1
  265. package/dist/story/index.d.ts +0 -1777
  266. package/dist/story/index.js +0 -273
  267. package/src/doc/getLayers.ts +0 -195
  268. /package/dist/{story → narration}/index.js.map +0 -0
@@ -0,0 +1,304 @@
1
+ /**
2
+ * Canonical block-to-layer materialization contract.
3
+ *
4
+ * Timeline scheduling and UI components both delegate here so template
5
+ * lookup, custom templates, ownership, render style, persistent layers,
6
+ * failure policy, and diagnostics cannot drift by render mode.
7
+ */
8
+
9
+ import type { Block, Layer } from '../schemas/Doc.js';
10
+ import type {
11
+ DocBlock,
12
+ PersistentLayerConfig,
13
+ TemplateBlock,
14
+ TemplateContext,
15
+ } from '../schemas/BlockTemplates.js';
16
+ import type { CustomTemplateDefinition } from '../schemas/CustomTemplates.js';
17
+ import type { Theme } from '../schemas/Theme.js';
18
+ import type { ViewportConfig } from '../schemas/Viewport.js';
19
+ import { createTemplateContext, isTemplateBlock } from '../schemas/BlockTemplates.js';
20
+ import { DEFAULT_THEME } from '../schemas/themeLibrary.js';
21
+ import { VIEWPORT_PRESETS } from '../schemas/Viewport.js';
22
+ import { cloneData } from '../internal/immutable.js';
23
+ import { applyRenderStyleToLayers } from './utils/applyRenderStyle.js';
24
+ import { coerceTemplateParams } from './templates/inputDescriptors.js';
25
+ import { fallbackBlockLayers } from './templates/fallbackBlock.js';
26
+ import { expandPersistentLayers, wrapWithPersistentLayers } from './templates/persistentLayers.js';
27
+ import { resolveTemplateName } from './templates/templateNames.js';
28
+ import {
29
+ buildRegistry,
30
+ templateRegistry,
31
+ type RuntimeTemplateRegistry,
32
+ } from './templates/registry.js';
33
+
34
+ /** Internal result from executing one template function. */
35
+ type TemplateLayerMaterialization =
36
+ | { status: 'ok'; layers: Layer[] }
37
+ | { status: 'unknown-template'; layers: [] }
38
+ | { status: 'invalid-result'; layers: []; receivedType: string }
39
+ | { status: 'error'; layers: []; error: unknown };
40
+
41
+ /** How a failed template render is represented in the returned layer graph. */
42
+ export type LayerMaterializationFailureMode = 'fallback' | 'empty';
43
+
44
+ /** A structured, inspectable explanation for a template render failure. */
45
+ export type LayerMaterializationDiagnostic =
46
+ | {
47
+ code: 'unknown-template';
48
+ template: string;
49
+ message: string;
50
+ }
51
+ | {
52
+ code: 'invalid-template-result';
53
+ template: string;
54
+ message: string;
55
+ receivedType: string;
56
+ }
57
+ | {
58
+ code: 'template-error';
59
+ template: string;
60
+ message: string;
61
+ cause: unknown;
62
+ };
63
+
64
+ /** Identifies which path produced a materialized layer graph. */
65
+ export type LayerMaterializationSource = 'authored' | 'template' | 'fallback' | 'empty';
66
+
67
+ /**
68
+ * Complete result from {@link materializeBlockLayers}. Failures are data, not
69
+ * console side effects: callers can display, report, or ignore the diagnostic.
70
+ */
71
+ export interface BlockLayerMaterialization {
72
+ layers: Layer[];
73
+ source: LayerMaterializationSource;
74
+ diagnostic?: LayerMaterializationDiagnostic;
75
+ }
76
+
77
+ /**
78
+ * The single public context for turning a block into renderable layers.
79
+ *
80
+ * Theme persistent layers are inherited by default. Pass
81
+ * `persistentLayers: false` to opt out, or a config to replace the theme's
82
+ * persistent layers wholesale.
83
+ */
84
+ export interface MaterializeBlockLayersOptions {
85
+ /** Theme used by templates and render-style post-processing. */
86
+ theme?: Theme;
87
+ /** Target viewport. Defaults to the landscape preset. */
88
+ viewport?: ViewportConfig;
89
+ /** Theme inheritance override for persistent layers. */
90
+ persistentLayers?: PersistentLayerConfig | false;
91
+ /** Zero-based position in the flattened render sequence. */
92
+ blockIndex?: number;
93
+ /** Number of blocks in the flattened render sequence. */
94
+ totalBlocks?: number;
95
+ /** Document-scoped custom templates; built-ins win name collisions. */
96
+ customTemplates?: readonly CustomTemplateDefinition[];
97
+ /** Failed templates render a visible fallback by default. */
98
+ failureMode?: LayerMaterializationFailureMode;
99
+ }
100
+
101
+ /** Pre-expanded scheduling cache; not part of the public materialization API. */
102
+ export interface ExpandedPersistentLayerSet {
103
+ bottomLayers: Layer[];
104
+ topLayers: Layer[];
105
+ }
106
+
107
+ /** Internal dependencies and compatibility switches supplied by orchestrators. */
108
+ export interface MaterializationRuntime {
109
+ registry: RuntimeTemplateRegistry;
110
+ templateContext?: TemplateContext;
111
+ applyRenderStyle?: boolean;
112
+ expandedPersistentLayers?: ExpandedPersistentLayerSet;
113
+ }
114
+
115
+ /**
116
+ * Turn any doc block into an owned, render-ready layer graph.
117
+ *
118
+ * This is the only recommended public materialization entry point.
119
+ */
120
+ export function materializeBlockLayers(
121
+ block: DocBlock,
122
+ options: MaterializeBlockLayersOptions = {},
123
+ ): BlockLayerMaterialization {
124
+ const registry =
125
+ options.customTemplates && options.customTemplates.length > 0
126
+ ? buildRegistry(options.customTemplates)
127
+ : (templateRegistry as unknown as RuntimeTemplateRegistry);
128
+ return materializeBlockLayersWithRuntime(block, options, { registry });
129
+ }
130
+
131
+ /** Internal entry point for schedulers that cache registry and persistent expansion. */
132
+ export function materializeBlockLayersWithRuntime(
133
+ block: DocBlock,
134
+ options: MaterializeBlockLayersOptions,
135
+ runtime: MaterializationRuntime,
136
+ ): BlockLayerMaterialization {
137
+ const theme = options.theme ?? DEFAULT_THEME;
138
+ const viewport = options.viewport ?? VIEWPORT_PRESETS.landscape;
139
+ const blockIndex = options.blockIndex ?? 0;
140
+ const totalBlocks = options.totalBlocks ?? 1;
141
+ const effectivePersistentLayers =
142
+ options.persistentLayers === false
143
+ ? undefined
144
+ : (options.persistentLayers ?? theme.persistentLayers);
145
+
146
+ const existingLayers = (block as Block).layers;
147
+ if (existingLayers && existingLayers.length > 0 && !isTemplateBlock(block)) {
148
+ return {
149
+ layers: injectPersistentLayers(
150
+ cloneData(existingLayers),
151
+ block,
152
+ theme,
153
+ effectivePersistentLayers,
154
+ runtime.expandedPersistentLayers,
155
+ ),
156
+ source: 'authored',
157
+ };
158
+ }
159
+
160
+ if (!isTemplateBlock(block)) {
161
+ return {
162
+ layers: injectPersistentLayers(
163
+ [],
164
+ block,
165
+ theme,
166
+ effectivePersistentLayers,
167
+ runtime.expandedPersistentLayers,
168
+ ),
169
+ source: 'empty',
170
+ };
171
+ }
172
+
173
+ const context = runtime.templateContext
174
+ ? cloneData(runtime.templateContext)
175
+ : createTemplateContext(theme, blockIndex, totalBlocks, viewport);
176
+ context.block = cloneData(block as Block);
177
+
178
+ const execution = executeTemplateMaterialization(block, context, runtime.registry);
179
+ if (execution.status === 'ok') {
180
+ const styledLayers =
181
+ runtime.applyRenderStyle !== false
182
+ ? applyRenderStyleToLayers(execution.layers, block as Block, theme)
183
+ : execution.layers;
184
+ return {
185
+ layers: injectPersistentLayers(
186
+ styledLayers,
187
+ block,
188
+ theme,
189
+ effectivePersistentLayers,
190
+ runtime.expandedPersistentLayers,
191
+ ),
192
+ source: 'template',
193
+ };
194
+ }
195
+
196
+ const diagnostic = toLayerMaterializationDiagnostic(block.template, execution);
197
+ if ((options.failureMode ?? 'fallback') === 'empty') {
198
+ return {
199
+ layers: injectPersistentLayers(
200
+ [],
201
+ block,
202
+ theme,
203
+ effectivePersistentLayers,
204
+ runtime.expandedPersistentLayers,
205
+ ),
206
+ source: 'empty',
207
+ diagnostic,
208
+ };
209
+ }
210
+
211
+ return {
212
+ layers: injectPersistentLayers(
213
+ fallbackBlockLayers(block, context, diagnostic.message),
214
+ block,
215
+ theme,
216
+ effectivePersistentLayers,
217
+ runtime.expandedPersistentLayers,
218
+ ),
219
+ source: 'fallback',
220
+ diagnostic,
221
+ };
222
+ }
223
+
224
+ function executeTemplateMaterialization(
225
+ templateBlock: TemplateBlock,
226
+ context: TemplateContext,
227
+ registry: RuntimeTemplateRegistry,
228
+ ): TemplateLayerMaterialization {
229
+ const safeTemplateBlock = cloneData(templateBlock);
230
+ const safeContext = cloneData(context);
231
+ const maybeChildren = (safeTemplateBlock as Block).children;
232
+ if ((!safeContext.children || safeContext.children.length === 0) && maybeChildren?.length) {
233
+ safeContext.children = maybeChildren;
234
+ }
235
+
236
+ const templateFn = registry[resolveTemplateName(safeTemplateBlock.template)];
237
+ if (!templateFn) return { status: 'unknown-template', layers: [] };
238
+
239
+ const { templateData, templateOverrides } = safeTemplateBlock as Block;
240
+ const input =
241
+ templateData || templateOverrides
242
+ ? ({
243
+ ...safeTemplateBlock,
244
+ ...templateData,
245
+ ...coerceTemplateParams(safeTemplateBlock.template, templateOverrides ?? {}).input,
246
+ } as TemplateBlock)
247
+ : safeTemplateBlock;
248
+
249
+ try {
250
+ const renderedLayers = templateFn(input, safeContext);
251
+ if (!Array.isArray(renderedLayers)) {
252
+ return {
253
+ status: 'invalid-result',
254
+ layers: [],
255
+ receivedType: typeof renderedLayers,
256
+ };
257
+ }
258
+ return { status: 'ok', layers: cloneData(renderedLayers) };
259
+ } catch (error: unknown) {
260
+ return { status: 'error', layers: [], error };
261
+ }
262
+ }
263
+
264
+ function toLayerMaterializationDiagnostic(
265
+ template: string,
266
+ execution: Exclude<TemplateLayerMaterialization, { status: 'ok' }>,
267
+ ): LayerMaterializationDiagnostic {
268
+ if (execution.status === 'unknown-template') {
269
+ return {
270
+ code: 'unknown-template',
271
+ template,
272
+ message: `Unknown template "${template}"`,
273
+ };
274
+ }
275
+ if (execution.status === 'invalid-result') {
276
+ return {
277
+ code: 'invalid-template-result',
278
+ template,
279
+ receivedType: execution.receivedType,
280
+ message: `Template "${template}" returned ${execution.receivedType}, not a layer array`,
281
+ };
282
+ }
283
+ return {
284
+ code: 'template-error',
285
+ template,
286
+ cause: execution.error,
287
+ message: `Template "${template}" failed`,
288
+ };
289
+ }
290
+
291
+ function injectPersistentLayers(
292
+ layers: Layer[],
293
+ block: DocBlock,
294
+ theme: Theme,
295
+ persistentLayers?: PersistentLayerConfig,
296
+ expanded?: ExpandedPersistentLayerSet,
297
+ ): Layer[] {
298
+ if (!persistentLayers) return layers;
299
+ const bottomLayers =
300
+ expanded?.bottomLayers ?? expandPersistentLayers(persistentLayers.bottomLayers, theme);
301
+ const topLayers =
302
+ expanded?.topLayers ?? expandPersistentLayers(persistentLayers.topLayers, theme);
303
+ return wrapWithPersistentLayers(layers, block as TemplateBlock, bottomLayers, topLayers);
304
+ }
@@ -73,15 +73,21 @@ export interface MediaExtraction {
73
73
  /**
74
74
  * Walk `contents`, lifting every standalone media annotation into a
75
75
  * {@link MediaClip}. `makeId(src, index)` produces stable clip ids.
76
+ * When `blockId` is given, each clip records its round-trip `origin` —
77
+ * the owning block, its position in the ORIGINAL contents array, and
78
+ * the annotation paragraph's exact text — so `docToMarkdown` can
79
+ * re-insert it byte-stably.
76
80
  */
77
81
  export function extractMediaFromContents(
78
82
  contents: MarkdownBlockNode[] | undefined,
79
83
  makeId: (src: string, index: number) => string,
84
+ blockId?: string,
80
85
  ): MediaExtraction {
81
86
  const media: MediaClip[] = [];
82
87
  const documentMedia: MediaClip[] = [];
83
88
  const remaining: MarkdownBlockNode[] = [];
84
89
  let index = 0;
90
+ let originalIndex = 0;
85
91
 
86
92
  for (const node of contents ?? []) {
87
93
  const parsed = parseStandaloneAnnotation(node);
@@ -90,10 +96,24 @@ export function extractMediaFromContents(
90
96
  clip.id = makeId(clip.src, index++);
91
97
  const line = node.position?.start.line;
92
98
  if (line != null) clip.sourceLine = line;
99
+ if (blockId !== undefined) {
100
+ // parseStandaloneAnnotation only matches a paragraph with exactly
101
+ // one text child, so this read is safe when `parsed` is non-null.
102
+ const rawText =
103
+ node.type === 'paragraph' && node.children?.[0]?.type === 'text'
104
+ ? node.children[0].value.trim()
105
+ : undefined;
106
+ clip.origin = {
107
+ blockId,
108
+ index: originalIndex,
109
+ ...(rawText !== undefined ? { raw: rawText } : {}),
110
+ };
111
+ }
93
112
  (clip.anchor === 'document' ? documentMedia : media).push(clip);
94
113
  } else {
95
114
  remaining.push(node);
96
115
  }
116
+ originalIndex++;
97
117
  }
98
118
 
99
119
  return { media, documentMedia, remaining };
@@ -3,18 +3,18 @@
3
3
  *
4
4
  * The theme analog of how `buildRegistry` resolves custom templates: pure and
5
5
  * doc-scoped, with no global state. `resolveThemeForDoc` looks up the active
6
- * theme id in the doc's own custom themes first, then falls back to the
7
- * built-in / globally-registered themes via `resolveTheme`.
6
+ * theme id in the doc's own custom themes first, then falls back to an
7
+ * optional caller-owned registry, then built-ins via `resolveTheme`.
8
8
  *
9
9
  * It accepts either a squisq `Doc` (which carries a typed `customThemes`
10
10
  * array) or anything with raw `frontmatter` — notably a `MarkdownDocument`,
11
11
  * which is the shape every export pipeline pivots through. In the latter case
12
12
  * the inline themes are decoded from the `squisq-custom-themes` frontmatter
13
- * payload. Either way, resolution needs no `registerTheme` call an inline
14
- * custom theme ships with the doc, exactly like custom templates.
13
+ * payload. Inline custom themes ship with the doc, exactly like custom
14
+ * templates; host-level themes can be supplied through an explicit registry.
15
15
  */
16
16
 
17
- import type { Theme } from '../schemas/Theme.js';
17
+ import type { Theme, ThemeRegistry } from '../schemas/Theme.js';
18
18
  import { resolveTheme } from '../schemas/themeLibrary.js';
19
19
  import { readFrontmatterThemeId } from '../markdown/utils.js';
20
20
  import { readCustomThemesFromFrontmatter } from './customThemesFrontmatter.js';
@@ -39,11 +39,12 @@ export interface ThemeResolvable {
39
39
  * editor's theme dropdown, or an export `--theme` option). When omitted, the
40
40
  * doc's `themeId` / frontmatter theme id is used.
41
41
  * @returns the matching inline custom theme (doc-scoped) when present, else
42
- * the built-in / registered theme for that id, else the default theme.
42
+ * the caller-registry or built-in theme for that id, else the default theme.
43
43
  */
44
44
  export function resolveThemeForDoc(
45
45
  doc: ThemeResolvable | null | undefined,
46
46
  explicitId?: string,
47
+ registry?: ThemeRegistry,
47
48
  ): Theme {
48
49
  const id = explicitId ?? doc?.themeId ?? readFrontmatterThemeId(doc?.frontmatter);
49
50
  // Prefer the already-parsed list on a squisq Doc; otherwise decode the
@@ -51,5 +52,5 @@ export function resolveThemeForDoc(
51
52
  const inline = doc?.customThemes ?? readCustomThemesFromFrontmatter(doc?.frontmatter);
52
53
  const custom = id ? inline?.find((t) => t.id === id) : undefined;
53
54
  if (custom) return custom;
54
- return resolveTheme(id);
55
+ return resolveTheme(id, registry);
55
56
  }
@@ -18,8 +18,24 @@
18
18
  * preview surfaces use this so a half-authored block still renders.
19
19
  */
20
20
 
21
- import type { MarkdownBlockNode, MarkdownList, MarkdownTable } from '../markdown/types.js';
21
+ import type {
22
+ MarkdownBlockNode,
23
+ MarkdownCodeBlock,
24
+ MarkdownList,
25
+ MarkdownTable,
26
+ } from '../markdown/types.js';
22
27
  import { extractPlainText } from '../markdown/utils.js';
28
+ import { matchNumberHighlight } from '../recommend/numberHighlight.js';
29
+ import {
30
+ detectAsciiDiagram,
31
+ isEligibleAsciiFenceLang,
32
+ isExplicitDiagramLang,
33
+ } from './asciiDiagram/detect.js';
34
+ import { asciiDiagramToTemplateData } from './asciiDiagram/mapping.js';
35
+ import { detectAsciiTimeline, isEligibleAsciiTimelineFenceLang } from './asciiTimeline/detect.js';
36
+ import { asciiTimelineToTemplateData } from './asciiTimeline/mapping.js';
37
+ import { detectTree, isEligibleTreeFenceLang, isExplicitTreeLang } from './treeview/detect.js';
38
+ import { treeToTemplateData, treeFromMarkdownList, findFirstList } from './treeview/mapping.js';
23
39
 
24
40
  /** First image discovered in a block's body, with explicit dimensions when present. */
25
41
  export interface FirstImage {
@@ -153,6 +169,125 @@ export function extractBlockquoteText(contents?: MarkdownBlockNode[]): string {
153
169
  return '';
154
170
  }
155
171
 
172
+ function firstParagraph(
173
+ contents: MarkdownBlockNode[] | undefined,
174
+ ): { node: Extract<MarkdownBlockNode, { type: 'paragraph' }>; index: number } | null {
175
+ if (!contents) return null;
176
+ const index = contents.findIndex((node) => node.type === 'paragraph');
177
+ if (index < 0) return null;
178
+ return {
179
+ node: contents[index] as Extract<MarkdownBlockNode, { type: 'paragraph' }>,
180
+ index,
181
+ };
182
+ }
183
+
184
+ /** A leading bold number is an explicit authoring signal for the hero stat. */
185
+ function leadingStrongStat(
186
+ paragraph: Extract<MarkdownBlockNode, { type: 'paragraph' }>,
187
+ ): string | null {
188
+ for (const child of paragraph.children) {
189
+ if (child.type === 'text' && child.value.trim() === '') continue;
190
+ if (child.type !== 'strong') return null;
191
+ const text = extractPlainText(child).trim();
192
+ // Explicit bold authoring is intentionally broader than automatic stat
193
+ // recommendation: a small integer such as `**42**` is a valid hero when
194
+ // the author chose statHighlight. Anchor at the beginning so a bold phrase
195
+ // like `**Revenue grew 42%**` does not become oversized wholesale.
196
+ const match = text.match(/^(?:[$€£¥]\s*)?[+-]?\d+(?:[.,]\d+)*(?:\s?(?:[%‰x×]|[A-Za-z]+))?/);
197
+ return match?.[0].trim() || null;
198
+ }
199
+ return null;
200
+ }
201
+
202
+ function removeStatPhrase(text: string, start: number, end: number): string {
203
+ const before = text.slice(0, start).trim();
204
+ let after = text.slice(end);
205
+ // A dash/colon visually separates inline prose, but is redundant once the
206
+ // stat and description render on separate lines.
207
+ if (!before) after = after.replace(/^\s*(?:[-–—:]\s*)?/, '');
208
+ else after = after.trim();
209
+ return [before, after].filter(Boolean).join(' ').trim();
210
+ }
211
+
212
+ function bodyWithoutParagraphStat(
213
+ contents: MarkdownBlockNode[] | undefined,
214
+ paragraphIndex: number,
215
+ paragraphText: string,
216
+ start: number,
217
+ end: number,
218
+ ): string {
219
+ if (!contents) return '';
220
+ return contents
221
+ .map((node, index) =>
222
+ index === paragraphIndex
223
+ ? removeStatPhrase(paragraphText, start, end)
224
+ : extractPlainText(node).trim(),
225
+ )
226
+ .filter(Boolean)
227
+ .join('\n')
228
+ .trim();
229
+ }
230
+
231
+ /**
232
+ * Map Markdown-authored stat content onto the template's visual roles.
233
+ *
234
+ * A leading bold metric in the body is the strongest signal (`**42%** of
235
+ * teams`), followed by a stat-looking heading for backward compatibility,
236
+ * then an unformatted metric in the first body paragraph (used by automatic
237
+ * templates). Generic content retains the historical heading/body mapping.
238
+ */
239
+ function deriveStatHighlightInputs(
240
+ headingText: string,
241
+ contents: MarkdownBlockNode[] | undefined,
242
+ bodyText: string,
243
+ ): Record<string, unknown> {
244
+ const paragraph = firstParagraph(contents);
245
+ if (paragraph) {
246
+ const paragraphText = extractPlainText(paragraph.node).trim();
247
+ const strongStat = leadingStrongStat(paragraph.node);
248
+ if (strongStat) {
249
+ const start = paragraphText.indexOf(strongStat);
250
+ const description = bodyWithoutParagraphStat(
251
+ contents,
252
+ paragraph.index,
253
+ paragraphText,
254
+ start,
255
+ start + strongStat.length,
256
+ );
257
+ return {
258
+ stat: strongStat,
259
+ description: description || headingText || strongStat,
260
+ };
261
+ }
262
+ }
263
+
264
+ const trimmedHeading = headingText.trim();
265
+ const headingStat = matchNumberHighlight(trimmedHeading);
266
+ if (headingStat && headingStat.index === 0 && headingStat.end === trimmedHeading.length) {
267
+ return { stat: headingText, description: bodyText || headingText };
268
+ }
269
+
270
+ if (paragraph) {
271
+ const paragraphText = extractPlainText(paragraph.node).trim();
272
+ const bodyStat = matchNumberHighlight(paragraphText);
273
+ if (bodyStat) {
274
+ const description = bodyWithoutParagraphStat(
275
+ contents,
276
+ paragraph.index,
277
+ paragraphText,
278
+ bodyStat.index,
279
+ bodyStat.end,
280
+ );
281
+ return {
282
+ stat: bodyStat.value,
283
+ description: description || headingText || bodyStat.value,
284
+ };
285
+ }
286
+ }
287
+
288
+ return { stat: headingText, description: bodyText || headingText };
289
+ }
290
+
156
291
  export interface DeriveTemplateInputsOptions {
157
292
  /**
158
293
  * Return visible placeholder values instead of `null` when an essential
@@ -177,7 +312,7 @@ export function deriveTemplateInputs(
177
312
 
178
313
  switch (templateName) {
179
314
  case 'statHighlight':
180
- return { stat: headingText, description: bodyText || headingText };
315
+ return deriveStatHighlightInputs(headingText, contents, bodyText);
181
316
  case 'quote': {
182
317
  const quote = extractBlockquoteText(contents) || bodyText || headingText;
183
318
  return { quote };
@@ -234,6 +369,54 @@ export function deriveTemplateInputs(
234
369
  body: bodyText,
235
370
  };
236
371
  }
372
+ case 'diagram': {
373
+ // Nodes/edges from an ASCII-art fence in the body (the fence itself
374
+ // stays in `contents`, untouched, so round-trips are lossless).
375
+ const fences = (contents ?? []).filter((n): n is MarkdownCodeBlock => n.type === 'code');
376
+ const fence =
377
+ fences.length === 1 && isEligibleAsciiFenceLang(fences[0].lang) ? fences[0] : undefined;
378
+ const detection = fence
379
+ ? detectAsciiDiagram(fence.value, { explicit: isExplicitDiagramLang(fence.lang) })
380
+ : undefined;
381
+ if (!detection?.isDiagram || !detection.diagram) return placeholders ? {} : null;
382
+ const { nodes, edges } = asciiDiagramToTemplateData(detection.diagram);
383
+ return { nodes, edges, ...(headingText ? { title: headingText } : {}) };
384
+ }
385
+ case 'timeline': {
386
+ // Tracks/events from a spatial ASCII timeline fence. Calling this case
387
+ // already implies author/template intent, so accept the detector's
388
+ // explicit one-event form even when the fence itself is untagged.
389
+ const fences = (contents ?? []).filter((n): n is MarkdownCodeBlock => n.type === 'code');
390
+ const fence =
391
+ fences.length === 1 && isEligibleAsciiTimelineFenceLang(fences[0].lang)
392
+ ? fences[0]
393
+ : undefined;
394
+ const detection = fence ? detectAsciiTimeline(fence.value, { explicit: true }) : undefined;
395
+ if (!detection?.isTimeline || !detection.timeline) return placeholders ? {} : null;
396
+ const { tracks, links } = asciiTimelineToTemplateData(detection.timeline);
397
+ return { tracks, links, ...(headingText ? { title: headingText } : {}) };
398
+ }
399
+ case 'tree': {
400
+ // Items from an ASCII tree fence, or a nested markdown bullet list when
401
+ // the author explicitly annotates a listed body. The fence/list stays
402
+ // in `contents`, so round-trips are lossless.
403
+ const fences = (contents ?? []).filter((n): n is MarkdownCodeBlock => n.type === 'code');
404
+ const fence =
405
+ fences.length === 1 && isEligibleTreeFenceLang(fences[0].lang) ? fences[0] : undefined;
406
+ if (fence) {
407
+ const detection = detectTree(fence.value, { explicit: isExplicitTreeLang(fence.lang) });
408
+ if (detection.isTree && detection.tree) {
409
+ const { items } = treeToTemplateData(detection.tree);
410
+ return { items, ...(headingText ? { title: headingText } : {}) };
411
+ }
412
+ }
413
+ const list = findFirstList(contents);
414
+ if (list) {
415
+ const { items } = treeToTemplateData(treeFromMarkdownList(list));
416
+ if (items.length > 0) return { items, ...(headingText ? { title: headingText } : {}) };
417
+ }
418
+ return placeholders ? {} : null;
419
+ }
237
420
  default:
238
421
  return placeholders ? {} : null;
239
422
  }
@@ -89,9 +89,7 @@ describe('expandDocBlocks with customTemplates', () => {
89
89
  expect(expanded[0].layers![0].position.width).toBe('90%');
90
90
  });
91
91
 
92
- it('falls back to a no-layer block (with a warning) when the template is unknown', () => {
93
- // Suppress the expected console.warn from `expandTemplateBlock`.
94
- const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
92
+ it('returns a visible fallback and structured diagnostic when the template is unknown', () => {
95
93
  const block: Block = {
96
94
  id: 'b1',
97
95
  startTime: 0,
@@ -100,11 +98,27 @@ describe('expandDocBlocks with customTemplates', () => {
100
98
  title: 'Y',
101
99
  template: 'nonexistent',
102
100
  };
103
- const expanded = expandDocBlocks([block], { customTemplates: [heroDef] });
101
+ const diagnostics: string[] = [];
102
+ const expanded = expandDocBlocks([block], {
103
+ customTemplates: [heroDef],
104
+ onDiagnostic: (diagnostic) => diagnostics.push(diagnostic.code),
105
+ });
106
+ expect(expanded[0].layers?.some((layer) => layer.id === 'fallback-notice')).toBe(true);
107
+ expect(diagnostics).toEqual(['unknown-template']);
108
+ });
109
+
110
+ it('retains an explicit empty policy for hosts that do not want fallback UI', () => {
111
+ const block: Block = {
112
+ id: 'b1',
113
+ startTime: 0,
114
+ duration: 1,
115
+ audioSegment: 0,
116
+ template: 'nonexistent',
117
+ };
118
+ const expanded = expandDocBlocks([block], {
119
+ customTemplates: [heroDef],
120
+ failureMode: 'empty',
121
+ });
104
122
  expect(expanded[0].layers).toBeUndefined();
105
- expect(warnSpy).toHaveBeenCalled();
106
- warnSpy.mockRestore();
107
123
  });
108
124
  });
109
-
110
- import { vi } from 'vitest';