@bendyline/squisq 2.0.1 → 2.2.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 (438) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE.md +24 -0
  3. package/dist/{ContentContainer-BXUlIf18.d.ts → ContentContainer-B2w9sUoL.d.ts} +15 -0
  4. package/dist/{Doc-ylqWKDc_.d.ts → Doc-BubIpWsp.d.ts} +160 -3
  5. package/dist/{ImageEditDoc-D828IXJ1.d.ts → ImageEditDoc-CGxk5CHc.d.ts} +1 -1
  6. package/dist/{annotationCoercion-Dr_DY9Hx.d.ts → annotationCoercion-PtRQhk5V.d.ts} +1 -1
  7. package/dist/chunk-2L55443L.js +0 -0
  8. package/dist/chunk-2ZWIXGAC.js +214 -0
  9. package/dist/{chunk-2PWOO4ZE.js → chunk-32R2RZAO.js} +72 -3
  10. package/dist/chunk-3IGPAPQ7.js +645 -0
  11. package/dist/{chunk-QE7TWGVT.js → chunk-4VOD55SX.js} +0 -1
  12. package/dist/{chunk-3E5F2XMR.js → chunk-67L6WRJN.js} +0 -1
  13. package/dist/{chunk-5DKBFXPY.js → chunk-6ECMNUIP.js} +26 -18
  14. package/dist/{chunk-X6DUONGT.js → chunk-7ARJZYQY.js} +280 -128
  15. package/dist/{chunk-DBS6RIFL.js → chunk-7N4G32LG.js} +0 -1
  16. package/dist/{chunk-JOQYPRDZ.js → chunk-7YEZFHLS.js} +10 -8
  17. package/dist/{chunk-2UQOHYE2.js → chunk-7ZAAICW4.js} +0 -1
  18. package/dist/{chunk-66QO7XFB.js → chunk-ACZOVWX3.js} +117 -7
  19. package/dist/{chunk-5J4HKRJF.js → chunk-BAOV476U.js} +0 -1
  20. package/dist/chunk-BCCXTMN5.js +699 -0
  21. package/dist/{chunk-W4O2TX5I.js → chunk-C33GTPUZ.js} +579 -4
  22. package/dist/{chunk-MMPBOUK7.js → chunk-CB45YGAM.js} +7 -2
  23. package/dist/{chunk-CY2R2VPR.js → chunk-DYPMOPIW.js} +18 -2
  24. package/dist/{chunk-VR7UPJ7C.js → chunk-FCG3BIZN.js} +12 -1
  25. package/dist/{chunk-5UYI3RJB.js → chunk-FDYDYL77.js} +54 -40
  26. package/dist/{chunk-TFJVXUHH.js → chunk-I2O6W7DX.js} +1513 -79
  27. package/dist/{chunk-Q7LROV2H.js → chunk-NKNMGIIN.js} +0 -1
  28. package/dist/chunk-OPO5K4DZ.js +241 -0
  29. package/dist/{chunk-TUWMK62B.js → chunk-PUS54YU6.js} +45 -2
  30. package/dist/chunk-QKPXHDLR.js +0 -0
  31. package/dist/{chunk-GZY66SDN.js → chunk-R3KTTOMS.js} +0 -1
  32. package/dist/{chunk-PXLVXTLW.js → chunk-RS5AP3J4.js} +72 -15
  33. package/dist/chunk-WV5ARNDA.js +0 -0
  34. package/dist/{chunk-4X3JQXNM.js → chunk-XOFT65EX.js} +1 -2
  35. package/dist/{chunk-FVTQSNR7.js → chunk-XRW6P7IF.js} +13 -5
  36. package/dist/chunk-XWNNA3KE.js +0 -0
  37. package/dist/chunk-YZNGMV3G.js +0 -0
  38. package/dist/{chunk-R2YZTCLO.js → chunk-ZQKZSJAX.js} +1 -2
  39. package/dist/doc/index.d.ts +166 -12
  40. package/dist/doc/index.js +36 -17
  41. package/dist/generate/index.d.ts +2 -2
  42. package/dist/generate/index.js +2 -3
  43. package/dist/icons/index.js +3 -4
  44. package/dist/icons/inlineIconMarker.js +1 -2
  45. package/dist/imageEdit/index.d.ts +48 -17
  46. package/dist/imageEdit/index.js +3 -4
  47. package/dist/index.d.ts +13 -12
  48. package/dist/index.js +118 -33
  49. package/dist/jsonForm/index.d.ts +10 -3
  50. package/dist/jsonForm/index.js +8 -7
  51. package/dist/markdown/index.d.ts +75 -11
  52. package/dist/markdown/index.js +32 -7
  53. package/dist/materializePageSection-wSLA2A1g.d.ts +223 -0
  54. package/dist/narration/index.d.ts +16 -3
  55. package/dist/narration/index.js +13 -14
  56. package/dist/random/index.d.ts +11 -2
  57. package/dist/random/index.js +2 -3
  58. package/dist/recommend/index.d.ts +1 -1
  59. package/dist/recommend/index.js +3 -4
  60. package/dist/schemas/index.d.ts +31 -6
  61. package/dist/schemas/index.js +43 -6
  62. package/dist/spatial/index.d.ts +2 -1
  63. package/dist/spatial/index.js +1 -2
  64. package/dist/storage/index.d.ts +4 -2
  65. package/dist/storage/index.js +1 -2
  66. package/dist/{themeLibrary-RjVgLpUi.d.ts → themeLibrary-DU9mUmVl.d.ts} +1 -1
  67. package/dist/timing/index.js +3 -4
  68. package/dist/transform/index.d.ts +9 -10
  69. package/dist/transform/index.js +6 -7
  70. package/dist/{types-C4kiRn_k.d.ts → types-8QjefM9J.d.ts} +38 -2
  71. package/dist/{types-BwMPl2CB.d.ts → types-CUNc9biN.d.ts} +24 -1
  72. package/dist/versions/index.d.ts +14 -11
  73. package/dist/versions/index.js +2 -3
  74. package/package.json +4 -2
  75. package/dist/chunk-2PWOO4ZE.js.map +0 -1
  76. package/dist/chunk-2UQOHYE2.js.map +0 -1
  77. package/dist/chunk-3E5F2XMR.js.map +0 -1
  78. package/dist/chunk-4X3JQXNM.js.map +0 -1
  79. package/dist/chunk-5DKBFXPY.js.map +0 -1
  80. package/dist/chunk-5J4HKRJF.js.map +0 -1
  81. package/dist/chunk-5UYI3RJB.js.map +0 -1
  82. package/dist/chunk-66QO7XFB.js.map +0 -1
  83. package/dist/chunk-AMIR72JP.js +0 -380
  84. package/dist/chunk-AMIR72JP.js.map +0 -1
  85. package/dist/chunk-CY2R2VPR.js.map +0 -1
  86. package/dist/chunk-DBS6RIFL.js.map +0 -1
  87. package/dist/chunk-DNJKBAI7.js +0 -1
  88. package/dist/chunk-DNJKBAI7.js.map +0 -1
  89. package/dist/chunk-ET53IIEP.js +0 -1
  90. package/dist/chunk-ET53IIEP.js.map +0 -1
  91. package/dist/chunk-FVJUUTEM.js +0 -74
  92. package/dist/chunk-FVJUUTEM.js.map +0 -1
  93. package/dist/chunk-FVTQSNR7.js.map +0 -1
  94. package/dist/chunk-GZY66SDN.js.map +0 -1
  95. package/dist/chunk-JOQYPRDZ.js.map +0 -1
  96. package/dist/chunk-MMPBOUK7.js.map +0 -1
  97. package/dist/chunk-PGGGKYWG.js +0 -1
  98. package/dist/chunk-PGGGKYWG.js.map +0 -1
  99. package/dist/chunk-PXLVXTLW.js.map +0 -1
  100. package/dist/chunk-Q7LROV2H.js.map +0 -1
  101. package/dist/chunk-QE7TWGVT.js.map +0 -1
  102. package/dist/chunk-R2GC6RLD.js +0 -344
  103. package/dist/chunk-R2GC6RLD.js.map +0 -1
  104. package/dist/chunk-R2YZTCLO.js.map +0 -1
  105. package/dist/chunk-S7A5D7ZJ.js +0 -174
  106. package/dist/chunk-S7A5D7ZJ.js.map +0 -1
  107. package/dist/chunk-TFJVXUHH.js.map +0 -1
  108. package/dist/chunk-TUWMK62B.js.map +0 -1
  109. package/dist/chunk-V543AI2F.js +0 -1
  110. package/dist/chunk-V543AI2F.js.map +0 -1
  111. package/dist/chunk-VR7UPJ7C.js.map +0 -1
  112. package/dist/chunk-W4O2TX5I.js.map +0 -1
  113. package/dist/chunk-X6DUONGT.js.map +0 -1
  114. package/dist/chunk-ZLQKNOZQ.js +0 -1
  115. package/dist/chunk-ZLQKNOZQ.js.map +0 -1
  116. package/dist/doc/index.js.map +0 -1
  117. package/dist/generate/index.js.map +0 -1
  118. package/dist/icons/index.js.map +0 -1
  119. package/dist/icons/inlineIconMarker.js.map +0 -1
  120. package/dist/imageEdit/index.js.map +0 -1
  121. package/dist/index.js.map +0 -1
  122. package/dist/jsonForm/index.js.map +0 -1
  123. package/dist/markdown/index.js.map +0 -1
  124. package/dist/narration/index.js.map +0 -1
  125. package/dist/random/index.js.map +0 -1
  126. package/dist/recommend/index.js.map +0 -1
  127. package/dist/schemas/index.js.map +0 -1
  128. package/dist/spatial/index.js.map +0 -1
  129. package/dist/storage/index.js.map +0 -1
  130. package/dist/timing/index.js.map +0 -1
  131. package/dist/transform/index.js.map +0 -1
  132. package/dist/versions/index.js.map +0 -1
  133. package/src/__tests__/animationUtils.test.ts +0 -117
  134. package/src/__tests__/annotationCoercion.test.ts +0 -243
  135. package/src/__tests__/applyNarrationTiming.test.ts +0 -190
  136. package/src/__tests__/applyRenderStyle.test.ts +0 -146
  137. package/src/__tests__/asciiDiagramBattle.test.ts +0 -246
  138. package/src/__tests__/asciiDiagramDetect.test.ts +0 -162
  139. package/src/__tests__/asciiDiagramMapping.test.ts +0 -38
  140. package/src/__tests__/asciiDiagramParse.test.ts +0 -233
  141. package/src/__tests__/asciiDiagramPipeline.test.ts +0 -273
  142. package/src/__tests__/asciiDiagramRender.test.ts +0 -162
  143. package/src/__tests__/asciiDiagramRepair.test.ts +0 -84
  144. package/src/__tests__/asciiTimelineDetect.test.ts +0 -157
  145. package/src/__tests__/asciiTimelineParse.test.ts +0 -205
  146. package/src/__tests__/asciiTimelinePipeline.test.ts +0 -349
  147. package/src/__tests__/asciiTimelineRender.test.ts +0 -376
  148. package/src/__tests__/atmosphereLayers.test.ts +0 -108
  149. package/src/__tests__/attrTokens.test.ts +0 -167
  150. package/src/__tests__/colorUtils.test.ts +0 -112
  151. package/src/__tests__/contentContainer.test.ts +0 -325
  152. package/src/__tests__/contentExtractor.test.ts +0 -243
  153. package/src/__tests__/coverBlock.test.ts +0 -310
  154. package/src/__tests__/customTemplateSample.test.ts +0 -144
  155. package/src/__tests__/customTemplatesFrontmatter.test.ts +0 -221
  156. package/src/__tests__/customThemesFrontmatter.test.ts +0 -146
  157. package/src/__tests__/dataTable.test.ts +0 -145
  158. package/src/__tests__/diagramBlock.test.ts +0 -122
  159. package/src/__tests__/diagramLayout.test.ts +0 -134
  160. package/src/__tests__/diagramText.test.ts +0 -26
  161. package/src/__tests__/docToMarkdownMedia.test.ts +0 -83
  162. package/src/__tests__/drawing.test.ts +0 -109
  163. package/src/__tests__/drawingBlock.test.ts +0 -124
  164. package/src/__tests__/drawingLayout.test.ts +0 -171
  165. package/src/__tests__/fixtures/asciiDiagramBattle.ts +0 -512
  166. package/src/__tests__/fixtures/asciiDiagrams.ts +0 -287
  167. package/src/__tests__/fixtures/asciiTimelines.ts +0 -35
  168. package/src/__tests__/fixtures/treeviewFixtures.ts +0 -243
  169. package/src/__tests__/geohash.test.ts +0 -110
  170. package/src/__tests__/getLayers.test.ts +0 -243
  171. package/src/__tests__/getLayersFallback.test.ts +0 -103
  172. package/src/__tests__/haversine.test.ts +0 -63
  173. package/src/__tests__/iconResolve.test.ts +0 -122
  174. package/src/__tests__/imageEditExportSvg.test.ts +0 -70
  175. package/src/__tests__/imageEditState.test.ts +0 -74
  176. package/src/__tests__/imageEditVersions.test.ts +0 -243
  177. package/src/__tests__/inferDocumentTitle.test.ts +0 -50
  178. package/src/__tests__/inlineIconMarker.test.ts +0 -36
  179. package/src/__tests__/inputDescriptors.test.ts +0 -162
  180. package/src/__tests__/jsonForm.chooseControl.test.ts +0 -86
  181. package/src/__tests__/jsonForm.evaluateWhen.test.ts +0 -59
  182. package/src/__tests__/jsonForm.inferSchema.test.ts +0 -25
  183. package/src/__tests__/jsonForm.pointer.test.ts +0 -149
  184. package/src/__tests__/jsonForm.tokens.test.ts +0 -84
  185. package/src/__tests__/layoutBlock.test.ts +0 -83
  186. package/src/__tests__/layoutLayout.test.ts +0 -169
  187. package/src/__tests__/localForageAdapter.test.ts +0 -155
  188. package/src/__tests__/majorApiTypes.test.ts +0 -18
  189. package/src/__tests__/markdown.test.ts +0 -1009
  190. package/src/__tests__/markdownIcons.test.ts +0 -104
  191. package/src/__tests__/markdownSanitize.test.ts +0 -81
  192. package/src/__tests__/markdownToDoc.test.ts +0 -957
  193. package/src/__tests__/matchFontFamily.test.ts +0 -114
  194. package/src/__tests__/materializeBlockLayers.test.ts +0 -135
  195. package/src/__tests__/materializeTestUtils.ts +0 -17
  196. package/src/__tests__/mediaAnnotations.test.ts +0 -91
  197. package/src/__tests__/mediaSchedule.test.ts +0 -118
  198. package/src/__tests__/narrationAlign.test.ts +0 -150
  199. package/src/__tests__/narrationFeatures.test.ts +0 -71
  200. package/src/__tests__/narrationNuclei.test.ts +0 -45
  201. package/src/__tests__/narrationPacing.test.ts +0 -189
  202. package/src/__tests__/narrationScript.test.ts +0 -149
  203. package/src/__tests__/narrationSessionRepro.test.ts +0 -62
  204. package/src/__tests__/narrationSidecar.test.ts +0 -88
  205. package/src/__tests__/narrationSyllables.test.ts +0 -65
  206. package/src/__tests__/narrationTestSignals.ts +0 -167
  207. package/src/__tests__/narrationVad.test.ts +0 -66
  208. package/src/__tests__/pandocAttrParse.test.ts +0 -187
  209. package/src/__tests__/recommendTemplates.test.ts +0 -183
  210. package/src/__tests__/resolveDocTheme.test.ts +0 -69
  211. package/src/__tests__/rootExports.test.ts +0 -61
  212. package/src/__tests__/scopedContentContainer.test.ts +0 -109
  213. package/src/__tests__/seededRandom.test.ts +0 -142
  214. package/src/__tests__/shapeGeometry.test.ts +0 -138
  215. package/src/__tests__/shapeVocabulary.test.ts +0 -133
  216. package/src/__tests__/storage.test.ts +0 -60
  217. package/src/__tests__/structuredData.test.ts +0 -249
  218. package/src/__tests__/surface.test.ts +0 -68
  219. package/src/__tests__/templateAnnotationParse.test.ts +0 -160
  220. package/src/__tests__/templateInputs.test.ts +0 -69
  221. package/src/__tests__/templateMetadata.test.ts +0 -32
  222. package/src/__tests__/templates.test.ts +0 -710
  223. package/src/__tests__/themeCompile.test.ts +0 -225
  224. package/src/__tests__/themeValidator.test.ts +0 -131
  225. package/src/__tests__/timing.test.ts +0 -167
  226. package/src/__tests__/transformProvenance.test.ts +0 -160
  227. package/src/__tests__/transformV2.test.ts +0 -163
  228. package/src/__tests__/transitionNormalize.test.ts +0 -61
  229. package/src/__tests__/treeviewBattle.test.ts +0 -76
  230. package/src/__tests__/treeviewDetect.test.ts +0 -168
  231. package/src/__tests__/treeviewParse.test.ts +0 -118
  232. package/src/__tests__/treeviewPipeline.test.ts +0 -183
  233. package/src/__tests__/treeviewRender.test.ts +0 -91
  234. package/src/__tests__/validateDoc.test.ts +0 -260
  235. package/src/__tests__/versions.test.ts +0 -459
  236. package/src/doc/annotationBlocks.ts +0 -117
  237. package/src/doc/applyNarrationTiming.ts +0 -244
  238. package/src/doc/asciiDiagram/chars.ts +0 -210
  239. package/src/doc/asciiDiagram/detect.ts +0 -175
  240. package/src/doc/asciiDiagram/index.ts +0 -38
  241. package/src/doc/asciiDiagram/mapping.ts +0 -225
  242. package/src/doc/asciiDiagram/parse.ts +0 -1095
  243. package/src/doc/asciiDiagram/render.ts +0 -963
  244. package/src/doc/asciiDiagram/repair.ts +0 -400
  245. package/src/doc/asciiDiagram/types.ts +0 -65
  246. package/src/doc/asciiTimeline/detect.ts +0 -138
  247. package/src/doc/asciiTimeline/index.ts +0 -26
  248. package/src/doc/asciiTimeline/mapping.ts +0 -43
  249. package/src/doc/asciiTimeline/parse.ts +0 -870
  250. package/src/doc/asciiTimeline/render.ts +0 -427
  251. package/src/doc/asciiTimeline/types.ts +0 -73
  252. package/src/doc/asciiTimeline/wrappedFlow.ts +0 -427
  253. package/src/doc/audioMapping.ts +0 -442
  254. package/src/doc/customTemplatesFrontmatter.ts +0 -273
  255. package/src/doc/customThemesFrontmatter.ts +0 -95
  256. package/src/doc/docToMarkdown.ts +0 -284
  257. package/src/doc/index.ts +0 -127
  258. package/src/doc/markdownToDoc.ts +0 -1054
  259. package/src/doc/materializeBlockLayers.ts +0 -304
  260. package/src/doc/mediaAnnotations.ts +0 -120
  261. package/src/doc/resolveDocTheme.ts +0 -56
  262. package/src/doc/standaloneAnnotation.ts +0 -58
  263. package/src/doc/structuredData.ts +0 -301
  264. package/src/doc/templateInputs.ts +0 -423
  265. package/src/doc/templates/__tests__/customTemplate.test.ts +0 -170
  266. package/src/doc/templates/__tests__/customTemplateRegistry.test.ts +0 -124
  267. package/src/doc/templates/__tests__/listBlock.test.ts +0 -64
  268. package/src/doc/templates/accentImage.ts +0 -362
  269. package/src/doc/templates/captionUtils.ts +0 -56
  270. package/src/doc/templates/comparisonBar.ts +0 -173
  271. package/src/doc/templates/coverBlock.ts +0 -255
  272. package/src/doc/templates/customTemplate.ts +0 -51
  273. package/src/doc/templates/dataTable.ts +0 -102
  274. package/src/doc/templates/dateEvent.ts +0 -147
  275. package/src/doc/templates/definitionCard.ts +0 -140
  276. package/src/doc/templates/diagramBlock.ts +0 -403
  277. package/src/doc/templates/diagramLayout.ts +0 -159
  278. package/src/doc/templates/drawingBlock.ts +0 -325
  279. package/src/doc/templates/drawingLayout.ts +0 -539
  280. package/src/doc/templates/factCard.ts +0 -146
  281. package/src/doc/templates/fallbackBlock.ts +0 -123
  282. package/src/doc/templates/featureBlock.ts +0 -209
  283. package/src/doc/templates/fullBleedQuote.ts +0 -83
  284. package/src/doc/templates/imageWithCaption.ts +0 -186
  285. package/src/doc/templates/index.ts +0 -642
  286. package/src/doc/templates/inputDescriptors.ts +0 -451
  287. package/src/doc/templates/layoutBlock.ts +0 -43
  288. package/src/doc/templates/layoutLayout.ts +0 -243
  289. package/src/doc/templates/listBlock.ts +0 -207
  290. package/src/doc/templates/mapBlock.ts +0 -138
  291. package/src/doc/templates/metadata.ts +0 -130
  292. package/src/doc/templates/persistentLayers.ts +0 -526
  293. package/src/doc/templates/photoGrid.ts +0 -190
  294. package/src/doc/templates/pullQuote.ts +0 -141
  295. package/src/doc/templates/quoteBlock.ts +0 -141
  296. package/src/doc/templates/registry.ts +0 -88
  297. package/src/doc/templates/sectionHeader.ts +0 -120
  298. package/src/doc/templates/statHighlight.ts +0 -146
  299. package/src/doc/templates/templateNames.ts +0 -22
  300. package/src/doc/templates/timelineBlock.ts +0 -613
  301. package/src/doc/templates/titleBlock.ts +0 -150
  302. package/src/doc/templates/tokens/__tests__/resolveTokens.test.ts +0 -487
  303. package/src/doc/templates/tokens/resolveTokens.ts +0 -453
  304. package/src/doc/templates/treeBlock.ts +0 -131
  305. package/src/doc/templates/twoColumn.ts +0 -263
  306. package/src/doc/templates/videoPullQuote.ts +0 -142
  307. package/src/doc/templates/videoWithCaption.ts +0 -135
  308. package/src/doc/treeview/chars.ts +0 -72
  309. package/src/doc/treeview/detect.ts +0 -145
  310. package/src/doc/treeview/index.ts +0 -30
  311. package/src/doc/treeview/mapping.ts +0 -95
  312. package/src/doc/treeview/parse.ts +0 -201
  313. package/src/doc/treeview/render.ts +0 -41
  314. package/src/doc/treeview/types.ts +0 -43
  315. package/src/doc/utils/animationUtils.ts +0 -399
  316. package/src/doc/utils/applyRenderStyle.ts +0 -106
  317. package/src/doc/utils/diagramText.ts +0 -78
  318. package/src/doc/utils/imageTreatment.ts +0 -70
  319. package/src/doc/utils/nearest.ts +0 -59
  320. package/src/doc/utils/shapeGeometry.ts +0 -614
  321. package/src/doc/utils/themeUtils.ts +0 -243
  322. package/src/doc/validate.ts +0 -575
  323. package/src/generate/contentExtractor.ts +0 -921
  324. package/src/generate/index.ts +0 -18
  325. package/src/generate/templateMapper.ts +0 -177
  326. package/src/icons/iconData.ts +0 -31
  327. package/src/icons/index.ts +0 -26
  328. package/src/icons/inlineIconMarker.ts +0 -73
  329. package/src/icons/resolve.ts +0 -139
  330. package/src/imageEdit/ImageEditVersionManager.ts +0 -73
  331. package/src/imageEdit/export.ts +0 -410
  332. package/src/imageEdit/index.ts +0 -72
  333. package/src/imageEdit/persistence.ts +0 -113
  334. package/src/imageEdit/state.ts +0 -112
  335. package/src/imageEdit/versionPaths.ts +0 -44
  336. package/src/imageEdit/versions.ts +0 -152
  337. package/src/index.ts +0 -15
  338. package/src/internal/immutable.ts +0 -58
  339. package/src/jsonForm/chooseControl.ts +0 -88
  340. package/src/jsonForm/evaluateWhen.ts +0 -59
  341. package/src/jsonForm/index.ts +0 -32
  342. package/src/jsonForm/inferSchema.ts +0 -25
  343. package/src/jsonForm/pointer.ts +0 -154
  344. package/src/jsonForm/tokens.ts +0 -78
  345. package/src/jsonForm/types.ts +0 -167
  346. package/src/markdown/annotationCoercion.ts +0 -295
  347. package/src/markdown/attrTokens.ts +0 -243
  348. package/src/markdown/convert.ts +0 -1353
  349. package/src/markdown/htmlParse.ts +0 -212
  350. package/src/markdown/index.ts +0 -152
  351. package/src/markdown/parse.ts +0 -103
  352. package/src/markdown/sanitize.ts +0 -318
  353. package/src/markdown/stringify.ts +0 -207
  354. package/src/markdown/types.ts +0 -710
  355. package/src/markdown/utils.ts +0 -459
  356. package/src/narration/align.ts +0 -514
  357. package/src/narration/features.ts +0 -231
  358. package/src/narration/index.ts +0 -86
  359. package/src/narration/nuclei.ts +0 -118
  360. package/src/narration/pacing.ts +0 -211
  361. package/src/narration/script.ts +0 -239
  362. package/src/narration/session.ts +0 -100
  363. package/src/narration/sidecar.ts +0 -187
  364. package/src/narration/syllables.ts +0 -85
  365. package/src/narration/trace.ts +0 -50
  366. package/src/narration/types.ts +0 -299
  367. package/src/narration/vad.ts +0 -131
  368. package/src/ngeohash.d.ts +0 -11
  369. package/src/random/SeededRandom.ts +0 -127
  370. package/src/random/index.ts +0 -1
  371. package/src/recommend/index.ts +0 -16
  372. package/src/recommend/numberHighlight.ts +0 -25
  373. package/src/recommend/templates.ts +0 -393
  374. package/src/schemas/BlockTemplates.ts +0 -1046
  375. package/src/schemas/CustomTemplates.ts +0 -126
  376. package/src/schemas/Doc.ts +0 -1052
  377. package/src/schemas/ImageEditDoc.ts +0 -102
  378. package/src/schemas/LayoutStrategy.ts +0 -178
  379. package/src/schemas/Media.ts +0 -179
  380. package/src/schemas/MediaProvider.ts +0 -70
  381. package/src/schemas/Theme.ts +0 -406
  382. package/src/schemas/Transitions.ts +0 -224
  383. package/src/schemas/Types.ts +0 -15
  384. package/src/schemas/Viewport.ts +0 -91
  385. package/src/schemas/colorUtils.ts +0 -219
  386. package/src/schemas/fontStacks.ts +0 -339
  387. package/src/schemas/index.ts +0 -29
  388. package/src/schemas/themeCompile.ts +0 -237
  389. package/src/schemas/themeConstants.ts +0 -2
  390. package/src/schemas/themeLibrary.ts +0 -60
  391. package/src/schemas/themeValidator.ts +0 -426
  392. package/src/schemas/themes/bold.json +0 -48
  393. package/src/schemas/themes/cinematic.json +0 -111
  394. package/src/schemas/themes/documentary.json +0 -96
  395. package/src/schemas/themes/gezellig.json +0 -93
  396. package/src/schemas/themes/index.ts +0 -53
  397. package/src/schemas/themes/magazine.json +0 -89
  398. package/src/schemas/themes/minimalist.json +0 -45
  399. package/src/schemas/themes/morning-light.json +0 -85
  400. package/src/schemas/themes/standard-dark.json +0 -85
  401. package/src/schemas/themes/standard.json +0 -74
  402. package/src/schemas/themes/tech-dark.json +0 -101
  403. package/src/schemas/themes/warm-earth.json +0 -50
  404. package/src/spatial/Geohash.ts +0 -196
  405. package/src/spatial/Haversine.ts +0 -59
  406. package/src/spatial/index.ts +0 -2
  407. package/src/storage/ContentContainer.ts +0 -220
  408. package/src/storage/LocalForageAdapter.ts +0 -103
  409. package/src/storage/LocalStorageAdapter.ts +0 -57
  410. package/src/storage/MediaProviderFromContainer.ts +0 -92
  411. package/src/storage/MemoryStorageAdapter.ts +0 -37
  412. package/src/storage/ScopedContentContainer.ts +0 -143
  413. package/src/storage/Storage.ts +0 -28
  414. package/src/storage/index.ts +0 -9
  415. package/src/timing/index.ts +0 -17
  416. package/src/timing/narrationTiming.ts +0 -220
  417. package/src/timing/readingTime.ts +0 -94
  418. package/src/transform/applyTransform.ts +0 -122
  419. package/src/transform/blockAnalyzer.ts +0 -181
  420. package/src/transform/index.ts +0 -23
  421. package/src/transform/registry.ts +0 -198
  422. package/src/transform/styles/dataDriven.ts +0 -17
  423. package/src/transform/styles/documentary.ts +0 -17
  424. package/src/transform/styles/index.ts +0 -5
  425. package/src/transform/styles/magazine.ts +0 -18
  426. package/src/transform/styles/minimal.ts +0 -17
  427. package/src/transform/styles/narrative.ts +0 -18
  428. package/src/transform/templateSelector.ts +0 -389
  429. package/src/transform/timingAllocator.ts +0 -257
  430. package/src/transform/types.ts +0 -151
  431. package/src/versions/DocumentVersionManager.ts +0 -70
  432. package/src/versions/gitignore.ts +0 -34
  433. package/src/versions/index.ts +0 -56
  434. package/src/versions/operations.ts +0 -144
  435. package/src/versions/paths.ts +0 -51
  436. package/src/versions/snapshotStore.ts +0 -202
  437. package/src/versions/timestamp.ts +0 -48
  438. package/src/versions/types.ts +0 -93
@@ -1,1054 +0,0 @@
1
- /**
2
- * Markdown → Doc Conversion
3
- *
4
- * Converts a MarkdownDocument into a hierarchical Doc whose Block tree
5
- * mirrors the heading structure of the markdown. Every heading (H1–H6)
6
- * becomes a Block; body content between headings populates `contents`;
7
- * sub-headings nest as `children`.
8
- *
9
- * This enables using BlockTemplates to supply alternate visualizations
10
- * for each section of a markdown document.
11
- *
12
- * @example
13
- * ```ts
14
- * import { parseMarkdown } from '@bendyline/squisq/markdown';
15
- * import { markdownToDoc } from '@bendyline/squisq/doc';
16
- *
17
- * const md = parseMarkdown('# Intro\n\nHello world\n\n## Details\n\nMore text');
18
- * const doc = markdownToDoc(md);
19
- * // doc.blocks[0].sourceHeading.depth === 1 ("# Intro")
20
- * // doc.blocks[0].contents → [paragraph("Hello world")]
21
- * // doc.blocks[0].children[0].sourceHeading.depth === 2 ("## Details")
22
- * ```
23
- */
24
-
25
- import type {
26
- Doc,
27
- Block,
28
- CaptionTrack,
29
- CaptionPhrase,
30
- StartBlockConfig,
31
- DocDiagnostic,
32
- } from '../schemas/Doc.js';
33
- import { readCustomTemplatesFromFrontmatter } from './customTemplatesFrontmatter.js';
34
- import { readCustomThemesFromFrontmatter } from './customThemesFrontmatter.js';
35
- import type {
36
- MarkdownDocument,
37
- MarkdownBlockNode,
38
- MarkdownCodeBlock,
39
- MarkdownHeading,
40
- MarkdownNode,
41
- HtmlNode,
42
- } from '../markdown/types.js';
43
- import { extractPlainText, readFrontmatterThemeId } from '../markdown/utils.js';
44
- import { coerceAnnotationValues, type CoercedBlockMeta } from '../markdown/annotationCoercion.js';
45
- import { estimateReadingTime } from '../timing/readingTime.js';
46
- import { resolveTemplateName, isContainerTemplate } from './templates/index.js';
47
- import { isDataFence, parseDataFence, findFirstTable, extractTableData } from './structuredData.js';
48
- import { extractMediaFromContents } from './mediaAnnotations.js';
49
- import { extractTemplateBlocksFromContents } from './annotationBlocks.js';
50
- import type { ParsedAnnotation } from './standaloneAnnotation.js';
51
- import { profileBlockContents, pickAutoTemplate } from '../recommend/templates.js';
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';
58
- import type { MediaClip } from '../schemas/Media.js';
59
-
60
- // ============================================
61
- // Options
62
- // ============================================
63
-
64
- /**
65
- * Options for markdownToDoc().
66
- */
67
- export interface MarkdownToDocOptions {
68
- /** Article ID for the generated Doc. Default: 'markdown-doc' */
69
- articleId?: string;
70
-
71
- /** Default template name for heading blocks. Default: 'sectionHeader' */
72
- defaultTemplate?: string;
73
-
74
- /** Default duration per block in seconds. Default: 5 */
75
- defaultDuration?: number;
76
-
77
- /** Custom ID generator. Receives the heading node and its index. */
78
- generateId?: (heading: MarkdownHeading, index: number) => string;
79
-
80
- /**
81
- * Whether to auto-generate a cover startBlock from the first H1 heading.
82
- * When true (default), a StartBlockConfig is created using the first H1's
83
- * text as the title. If the document contains an image, the first image
84
- * is used as the hero. Set to false to suppress automatic cover generation.
85
- */
86
- generateCoverBlock?: boolean;
87
-
88
- /**
89
- * Timestamp recorded as `captions.generatedAt`. When omitted, the field
90
- * is left unset so that conversion is fully deterministic — the same
91
- * markdown always converts to an identical Doc (important for snapshot
92
- * tests, caching, and agents that verify their output by re-converting).
93
- */
94
- captionsGeneratedAt?: string;
95
-
96
- /**
97
- * Content-aware template auto-picking for unannotated headings
98
- * (default: true). When a heading block carries a strong content signal
99
- * — a table, images, a blockquote, a list, a stat-looking line — the
100
- * matching template is applied (with inputs derived from the body)
101
- * instead of the structural `defaultTemplate`. Explicit `{[template]}`
102
- * annotations always win. Authors can also disable per document with
103
- * frontmatter `squisq-auto-templates: false`.
104
- */
105
- autoTemplates?: boolean;
106
- }
107
-
108
- // ============================================
109
- // ID Generation
110
- // ============================================
111
-
112
- /**
113
- * Convert text to a URL-friendly slug.
114
- * "Getting Started & More" → "getting-started-more"
115
- */
116
- const SLUG_NON_WORD_RE = /[^\w\s-]/g;
117
- const SLUG_SPACES_RE = /[\s_]+/g;
118
- const SLUG_MULTI_HYPHEN_RE = /-+/g;
119
- const SLUG_TRIM_HYPHEN_RE = /^-|-$/g;
120
-
121
- function slugify(text: string): string {
122
- return (
123
- text
124
- .toLowerCase()
125
- .replace(SLUG_NON_WORD_RE, '') // Remove non-word chars (except spaces and hyphens)
126
- .replace(SLUG_SPACES_RE, '-') // Replace spaces/underscores with hyphens
127
- .replace(SLUG_MULTI_HYPHEN_RE, '-') // Collapse multiple hyphens
128
- .replace(SLUG_TRIM_HYPHEN_RE, '') || // Trim leading/trailing hyphens
129
- 'block'
130
- ); // Fallback for empty result
131
- }
132
-
133
- /**
134
- * Creates an ID generator that produces unique slugified IDs.
135
- * Appends -2, -3, etc. for duplicate headings. `reserve()` registers an
136
- * author-pinned `{#id}` so a later heading whose slug matches doesn't
137
- * collide with it.
138
- */
139
- function createIdGenerator() {
140
- const used = new Map<string, number>();
141
-
142
- const generate = (heading: MarkdownHeading, _index: number): string => {
143
- const text = extractPlainText(heading);
144
- const base = slugify(text);
145
- const count = used.get(base) ?? 0;
146
- used.set(base, count + 1);
147
-
148
- if (count === 0) return base;
149
- return `${base}-${count + 1}`;
150
- };
151
-
152
- generate.reserve = (id: string): void => {
153
- used.set(id, Math.max(used.get(id) ?? 0, 1));
154
- };
155
-
156
- // Slug + dedupe from arbitrary text (used for heading-less standalone
157
- // annotation blocks, which have no MarkdownHeading to slug from). Shares
158
- // the same `used` map so a standalone id can't collide with a heading id.
159
- generate.fromText = (text: string): string => {
160
- const base = slugify(text);
161
- const count = used.get(base) ?? 0;
162
- used.set(base, count + 1);
163
- return count === 0 ? base : `${base}-${count + 1}`;
164
- };
165
-
166
- return generate;
167
- }
168
-
169
- // ============================================
170
- // Core Conversion
171
- // ============================================
172
-
173
- /**
174
- * Convert a MarkdownDocument into a Doc with a heading-driven Block hierarchy.
175
- *
176
- * **Algorithm:**
177
- * 1. Walk the document's top-level children sequentially
178
- * 2. Each MarkdownHeading starts a new block at its depth level
179
- * 3. Non-heading nodes accumulate as `contents` on the current block
180
- * 4. Sub-headings (deeper depth) nest as `children` of the current block
181
- * 5. Same-level or shallower headings close the current block and start a sibling/ancestor
182
- * 6. Content before the first heading goes into a "preamble" block with no sourceHeading
183
- *
184
- * @param markdownDoc - A parsed MarkdownDocument
185
- * @param options - Conversion options
186
- * @returns A Doc whose blocks mirror the markdown heading structure
187
- */
188
- /**
189
- * Apply coerced block-meta values onto a block's typed fields. Only keys
190
- * actually present are written, so a later caller (the Pandoc attribute
191
- * block) can override values supplied by the squiggly `{[…]}` annotation.
192
- */
193
- function applyBlockMeta(block: Block, m: CoercedBlockMeta): void {
194
- if (m.x != null) block.x = m.x;
195
- if (m.y != null) block.y = m.y;
196
- if (m.startTime != null) block.startTime = m.startTime;
197
- if (m.duration != null) block.duration = m.duration;
198
- if (m.connectsTo) block.connectsTo = m.connectsTo;
199
- if (m.transition) block.transition = m.transition;
200
- }
201
-
202
- /**
203
- * The block-meta an author pinned on a heading, drawn from either the
204
- * Pandoc `{key=value}` block or the squiggly `{[key=value]}` annotation
205
- * (Pandoc wins on conflict — same precedence as {@link makeBlock}). Used by
206
- * the timing passes to tell "author pinned this" from "derive it".
207
- */
208
- function pinnedHeadingMeta(heading: MarkdownHeading | undefined): CoercedBlockMeta {
209
- if (!heading) return {};
210
- const fromAnnotation = heading.templateAnnotation?.params
211
- ? coerceAnnotationValues(heading.templateAnnotation.params).blockMeta
212
- : {};
213
- const fromPandoc = heading.attributes?.blockMeta ?? {};
214
- return { ...fromAnnotation, ...fromPandoc };
215
- }
216
-
217
- /**
218
- * Pinned block-meta for any block: the heading pins ({@link pinnedHeadingMeta})
219
- * for heading blocks, or the standalone-annotation params for heading-less
220
- * standalone blocks. Lets a standalone `{[quote duration=8]}` pin timing the
221
- * same way a heading annotation would.
222
- */
223
- function pinnedMeta(block: Block): CoercedBlockMeta {
224
- if (block.sourceHeading) return pinnedHeadingMeta(block.sourceHeading);
225
- if (block.sourceAnnotation?.params) {
226
- return coerceAnnotationValues(block.sourceAnnotation.params).blockMeta;
227
- }
228
- return {};
229
- }
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
-
242
- /**
243
- * Rebuild a block list, lifting standalone `{[…]}` template annotations out of
244
- * each block's body into heading-less sibling blocks (recursing into children
245
- * first). See {@link extractTemplateBlocksFromContents}.
246
- */
247
- function expandStandaloneAnnotations(
248
- blocks: Block[],
249
- makeId: (parsed: ParsedAnnotation) => string,
250
- defaultDuration: number,
251
- ): Block[] {
252
- const out: Block[] = [];
253
- for (const block of blocks) {
254
- if (block.children && block.children.length > 0) {
255
- block.children = expandStandaloneAnnotations(block.children, makeId, defaultDuration);
256
- }
257
- const { leading, blocks: produced } = extractTemplateBlocksFromContents(block.contents, {
258
- makeId,
259
- defaultDuration,
260
- });
261
- if (produced.length > 0) {
262
- block.contents = leading;
263
- }
264
- out.push(block, ...produced);
265
- }
266
- return out;
267
- }
268
-
269
- export function markdownToDoc(markdownDoc: MarkdownDocument, options?: MarkdownToDocOptions): Doc {
270
- const articleId = options?.articleId ?? 'markdown-doc';
271
- const defaultTemplate = options?.defaultTemplate ?? 'sectionHeader';
272
- const defaultDuration = options?.defaultDuration ?? 5;
273
- const idGenerator = options?.generateId ? null : createIdGenerator();
274
- const generateId = options?.generateId ?? idGenerator!;
275
-
276
- let rootBlocks: Block[] = [];
277
- const diagnostics: DocDiagnostic[] = [];
278
- let headingIndex = 0;
279
-
280
- // Stack tracks the nesting context: each entry is a block and its heading depth.
281
- // We push when we go deeper, pop when we come back up.
282
- const stack: Array<{ block: Block; depth: number }> = [];
283
-
284
- // Accumulator for content nodes that appear before any heading (preamble)
285
- // or between the current heading and the next heading/sub-heading.
286
- let pendingContents: MarkdownBlockNode[] = [];
287
- let currentBlock: Block | null = null;
288
-
289
- function flushContents() {
290
- if (currentBlock && pendingContents.length > 0) {
291
- currentBlock.contents = (currentBlock.contents ?? []).concat(pendingContents);
292
- pendingContents = [];
293
- }
294
- }
295
-
296
- function makeBlock(heading: MarkdownHeading | null): Block {
297
- // Pandoc `{#id}` overrides slug-from-heading when present. Pinned ids
298
- // are reserved in the slug generator so a later un-pinned heading with
299
- // the same slugified text doesn't collide.
300
- const pandocId = heading?.attributes?.id;
301
- const id = pandocId ? pandocId : heading ? generateId(heading, headingIndex++) : 'preamble';
302
- if (pandocId && idGenerator) {
303
- idGenerator.reserve(pandocId);
304
- }
305
-
306
- // Use template from annotation if present, otherwise fall back to default.
307
- // Legacy template ids (e.g. `titleBlock`) are normalized to their canonical
308
- // short form (`title`) so downstream comparisons can rely on the new names
309
- // without sprinkling alias resolution everywhere.
310
- const annotation = heading?.templateAnnotation;
311
- const rawTemplate = annotation?.template ?? (heading ? defaultTemplate : undefined);
312
- const template = rawTemplate != null ? resolveTemplateName(rawTemplate) : undefined;
313
-
314
- // Extract heading text so templates (e.g. sectionHeader) that expect a
315
- // `title` property receive it without having to reach into sourceHeading.
316
- const title = heading ? extractPlainText(heading) : undefined;
317
-
318
- const block: Block = {
319
- id,
320
- startTime: 0,
321
- duration: defaultDuration,
322
- audioSegment: 0,
323
- template,
324
- ...(heading ? { sourceHeading: heading } : {}),
325
- ...(title ? { title } : {}),
326
- };
327
-
328
- // {[…]} template params → templateOverrides (template-specific overrides).
329
- // Known block-meta keys (duration, startTime, x, y, connectsTo) are *also*
330
- // honored in the squiggly form, so an author — or the timeline editor —
331
- // can write `{[duration=8]}` instead of the Pandoc `{duration=8}` block.
332
- // The raw params still flow to `templateOverrides` for round-tripping; the
333
- // Pandoc attribute block applied below wins when a key appears in both.
334
- if (annotation?.params) {
335
- block.templateOverrides = annotation.params;
336
- applyBlockMeta(block, coerceAnnotationValues(annotation.params).blockMeta);
337
- }
338
-
339
- // Pandoc {#id .class key=value} attributes → block-level fields.
340
- const attrs = heading?.attributes;
341
- if (attrs) {
342
- if (attrs.blockMeta) {
343
- applyBlockMeta(block, attrs.blockMeta);
344
- }
345
- if (attrs.classes && attrs.classes.length > 0) {
346
- block.classes = attrs.classes;
347
- }
348
- if (attrs.metadata && Object.keys(attrs.metadata).length > 0) {
349
- block.metadata = attrs.metadata;
350
- }
351
- }
352
-
353
- return block;
354
- }
355
-
356
- for (const node of markdownDoc.children) {
357
- if (node.type === 'heading') {
358
- const heading = node as MarkdownHeading;
359
- const depth = heading.depth;
360
-
361
- // Flush any accumulated content to the current block
362
- flushContents();
363
-
364
- // Create the new block for this heading
365
- const newBlock = makeBlock(heading);
366
-
367
- if (stack.length === 0) {
368
- // If there's a preamble block with no heading, push it first
369
- if (currentBlock && !currentBlock.sourceHeading) {
370
- rootBlocks.push(currentBlock);
371
- }
372
-
373
- // This is a root-level block (or the first heading)
374
- rootBlocks.push(newBlock);
375
- stack.push({ block: newBlock, depth });
376
- } else {
377
- // Find where this heading fits in the hierarchy
378
- // Pop blocks from the stack until we find a parent with lower depth
379
- while (stack.length > 0 && stack[stack.length - 1].depth >= depth) {
380
- stack.pop();
381
- }
382
-
383
- if (stack.length === 0) {
384
- // Same level as root or shallower — new root block
385
- rootBlocks.push(newBlock);
386
- stack.push({ block: newBlock, depth });
387
- } else {
388
- // Deeper — child of the current stack top
389
- const parent = stack[stack.length - 1].block;
390
- if (!parent.children) parent.children = [];
391
- parent.children.push(newBlock);
392
- stack.push({ block: newBlock, depth });
393
- }
394
- }
395
-
396
- currentBlock = newBlock;
397
- } else {
398
- // Non-heading block node — accumulate as content
399
- if (!currentBlock) {
400
- // Content before any heading → create preamble block
401
- currentBlock = makeBlock(null);
402
- }
403
- pendingContents.push(node);
404
- }
405
- }
406
-
407
- // Flush remaining content
408
- flushContents();
409
-
410
- // If we only had preamble content (no headings at all), push it
411
- if (currentBlock && !currentBlock.sourceHeading && !rootBlocks.includes(currentBlock)) {
412
- rootBlocks.push(currentBlock);
413
- }
414
-
415
- // Lift standalone body-level media annotations (`{[audio …]}` / `{[video …]}`)
416
- // into typed clips: block-anchored clips ride on `block.media`; clips flagged
417
- // `anchor=document` collect into the doc-level `documentMedia`. Done before
418
- // the reading-time pass so annotation text doesn't inflate block durations.
419
- const documentMedia: MediaClip[] = [];
420
- for (const block of flattenBlocks(rootBlocks)) {
421
- const {
422
- media,
423
- documentMedia: docMedia,
424
- remaining,
425
- } = extractMediaFromContents(
426
- block.contents,
427
- (src, i) => `${block.id}-media-${i}-${src.replace(/[^a-zA-Z0-9]+/g, '-')}`,
428
- block.id,
429
- );
430
- if (media.length > 0) block.media = media;
431
- if (docMedia.length > 0) documentMedia.push(...docMedia);
432
- if (block.contents) block.contents = remaining;
433
- }
434
-
435
- // A preamble block that held only document-media annotations is now empty —
436
- // drop it so a doc-spanning narration doesn't leave a phantom 5s block at the
437
- // top of the timeline. Only the heading-less preamble (rootBlocks[0]) can be
438
- // emptied this way.
439
- if (
440
- rootBlocks.length > 0 &&
441
- !rootBlocks[0].sourceHeading &&
442
- (rootBlocks[0].contents?.length ?? 0) === 0 &&
443
- (rootBlocks[0].children?.length ?? 0) === 0 &&
444
- !rootBlocks[0].media
445
- ) {
446
- rootBlocks.shift();
447
- }
448
-
449
- // Standalone `{[templateName …]}` paragraphs (non-media) become heading-less
450
- // template blocks; the body after each one, up to the next annotation,
451
- // becomes its `contents`. Done after media extraction (so media names keep
452
- // their meaning) and before structured-data parsing (so `json data` fences
453
- // attach to the produced blocks). Produced blocks are inserted as siblings
454
- // following the block whose body held the annotation.
455
- const makeStandaloneId = (parsed: ParsedAnnotation): string => {
456
- const base = parsed.params.title ?? parsed.template ?? 'block';
457
- return idGenerator ? idGenerator.fromText(base) : slugify(base);
458
- };
459
- rootBlocks = expandStandaloneAnnotations(rootBlocks, makeStandaloneId, defaultDuration);
460
- // A leading preamble whose entire body was consumed into standalone blocks
461
- // is now empty — drop it so it doesn't leave a phantom slide.
462
- if (
463
- rootBlocks.length > 0 &&
464
- !rootBlocks[0].sourceHeading &&
465
- !rootBlocks[0].standaloneAnnotation &&
466
- (rootBlocks[0].contents?.length ?? 0) === 0 &&
467
- (rootBlocks[0].children?.length ?? 0) === 0 &&
468
- !rootBlocks[0].media
469
- ) {
470
- rootBlocks.shift();
471
- }
472
-
473
- const allBlocks = flattenBlocks(rootBlocks);
474
-
475
- // Structured template data: ```json data / ```yaml data fences in a
476
- // block's body parse into `templateData`; for dataTable blocks the first
477
- // GFM table supplies headers/rows when not explicitly provided. Parse
478
- // failures degrade gracefully (the fence stays visible as code) and are
479
- // recorded as diagnostics.
480
- for (const block of allBlocks) {
481
- applyStructuredData(block, diagnostics);
482
- }
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
-
494
- // Content-aware template auto-pick (default on): unannotated heading
495
- // blocks whose body carries a strong signal get the matching template
496
- // plus inputs derived from that body. Strict derivation — when the
497
- // essential input can't be built (e.g. feature without an image src),
498
- // the block keeps the structural default.
499
- if ((options?.autoTemplates ?? true) && !frontmatterDisablesAutoTemplates(markdownDoc)) {
500
- applyAutoTemplates(rootBlocks, resolveTemplateName(defaultTemplate), { featureIndex: 0 });
501
- }
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
-
508
- // Duplicate ids make connections and navigation ambiguous. Generated
509
- // slugs are already deduped; this catches author-pinned `{#id}` clashes.
510
- const seenIds = new Map<string, Block>();
511
- for (const block of allBlocks) {
512
- if (seenIds.has(block.id)) {
513
- diagnostics.push({
514
- severity: 'error',
515
- code: 'duplicate-id',
516
- message: `Duplicate block id "${block.id}" — later connections and links are ambiguous`,
517
- blockId: block.id,
518
- ...lineOf(block),
519
- });
520
- } else {
521
- seenIds.set(block.id, block);
522
- }
523
- }
524
-
525
- // connectsTo targets must resolve to a block id in this doc.
526
- for (const block of allBlocks) {
527
- for (const conn of block.connectsTo ?? []) {
528
- if (!seenIds.has(conn.target)) {
529
- diagnostics.push({
530
- severity: 'warning',
531
- code: 'unresolved-connection',
532
- message: `Block "${block.id}" connects to unknown target "${conn.target}"`,
533
- blockId: block.id,
534
- ...lineOf(block),
535
- });
536
- }
537
- }
538
- }
539
-
540
- // Calculate reading-time-based durations and generate captions
541
- const minDuration = 3; // seconds — minimum for blocks with little/no text
542
- const phrases: CaptionPhrase[] = [];
543
-
544
- // First pass: compute duration from body-content reading time.
545
- // Skip blocks that pinned an explicit duration via a heading attribute —
546
- // author intent wins over reading-time heuristics.
547
- for (const block of allBlocks) {
548
- if (pinnedMeta(block).duration != null) continue;
549
- const bodyText = getBlockBodyText(block);
550
- if (bodyText.length > 0) {
551
- const estimate = estimateReadingTime(bodyText);
552
- block.duration = Math.max(minDuration, estimate.seconds);
553
- } else {
554
- block.duration = defaultDuration;
555
- }
556
- }
557
-
558
- // Second pass: assign start times sequentially and build caption phrases.
559
- // Blocks with an explicit `startTime` attribute keep their pinned value;
560
- // the running cursor still advances by their duration so following blocks
561
- // sequence after them.
562
- let currentTime = 0;
563
- for (const block of allBlocks) {
564
- const explicitStart = pinnedMeta(block).startTime;
565
- if (explicitStart == null) {
566
- block.startTime = currentTime;
567
- }
568
-
569
- // Generate caption phrases from the block's body content
570
- const bodyText = getBlockBodyText(block);
571
- if (bodyText.length > 0) {
572
- const sentences = splitIntoSentences(bodyText);
573
- if (sentences.length > 0) {
574
- const timePerSentence = block.duration / sentences.length;
575
- for (let i = 0; i < sentences.length; i++) {
576
- phrases.push({
577
- text: sentences[i],
578
- startTime: currentTime + i * timePerSentence,
579
- endTime: currentTime + (i + 1) * timePerSentence,
580
- audioSegment: 0,
581
- });
582
- }
583
- }
584
- }
585
-
586
- currentTime += block.duration;
587
- }
588
-
589
- // `generatedAt` is only stamped when the caller provides a timestamp —
590
- // conversion itself never reads the clock, so identical markdown always
591
- // converts to an identical Doc.
592
- const captions: CaptionTrack | undefined =
593
- phrases.length > 0
594
- ? {
595
- phrases,
596
- ...(options?.captionsGeneratedAt ? { generatedAt: options.captionsGeneratedAt } : {}),
597
- version: 1,
598
- }
599
- : undefined;
600
-
601
- const customTemplates = readCustomTemplatesFromFrontmatter(markdownDoc.frontmatter);
602
- const customThemes = readCustomThemesFromFrontmatter(markdownDoc.frontmatter);
603
- const themeId = readFrontmatterThemeId(markdownDoc.frontmatter);
604
- const doc: Doc = {
605
- articleId,
606
- duration: currentTime,
607
- blocks: rootBlocks,
608
- audio: {
609
- segments: [],
610
- },
611
- ...(captions ? { captions } : {}),
612
- ...(markdownDoc.frontmatter ? { frontmatter: markdownDoc.frontmatter } : {}),
613
- ...(themeId ? { themeId } : {}),
614
- ...(customTemplates ? { customTemplates } : {}),
615
- ...(customThemes ? { customThemes } : {}),
616
- ...(diagnostics.length > 0 ? { diagnostics } : {}),
617
- ...(documentMedia.length > 0 ? { documentMedia } : {}),
618
- };
619
-
620
- // Auto-generate cover startBlock from the first H1 heading
621
- if (options?.generateCoverBlock ?? true) {
622
- const coverConfig = buildStartBlock(markdownDoc, rootBlocks);
623
- if (coverConfig) {
624
- doc.startBlock = coverConfig;
625
- }
626
- }
627
-
628
- return doc;
629
- }
630
-
631
- // ============================================
632
- // Utilities
633
- // ============================================
634
-
635
- /**
636
- * Flatten a nested block tree into a depth-first ordered array.
637
- * Useful for calculating sequential timing or iterating all blocks.
638
- */
639
- export function flattenBlocks(blocks: Block[]): Block[] {
640
- const result: Block[] = [];
641
- for (const block of blocks) {
642
- result.push(block);
643
- if (block.children) {
644
- result.push(...flattenBlocks(block.children));
645
- }
646
- }
647
- return result;
648
- }
649
-
650
- /**
651
- * Flatten the block tree into the list of *independently renderable*
652
- * blocks — like {@link flattenBlocks}, but it does NOT descend into the
653
- * children of a container template (`diagram`, `drawing`; see
654
- * `isContainerTemplate`). Those children are consumed by the parent's
655
- * render (as nodes / shapes), so they must not also surface as their own
656
- * slides or sections.
657
- *
658
- * Use this anywhere a doc is flattened for rendering (slideshow, video,
659
- * static pages). Use {@link flattenBlocks} when you genuinely need every
660
- * block regardless of role — validation, timing, duplicate-id checks.
661
- */
662
- export function flattenRenderableBlocks(blocks: Block[]): Block[] {
663
- const result: Block[] = [];
664
- for (const block of blocks) {
665
- result.push(block);
666
- if (block.children && !isContainerTemplate(block.template)) {
667
- result.push(...flattenRenderableBlocks(block.children));
668
- }
669
- }
670
- return result;
671
- }
672
-
673
- /**
674
- * Count the total number of blocks in a nested tree (including children at all levels).
675
- */
676
- export function countBlocks(blocks: Block[]): number {
677
- let count = 0;
678
- for (const block of blocks) {
679
- count += 1;
680
- if (block.children) {
681
- count += countBlocks(block.children);
682
- }
683
- }
684
- return count;
685
- }
686
-
687
- /**
688
- * Get the heading depth for a block. Returns 0 for preamble blocks (no heading).
689
- */
690
- export function getBlockDepth(block: Block): number {
691
- return block.sourceHeading?.depth ?? 0;
692
- }
693
-
694
- // ============================================
695
- // Internal helpers
696
- // ============================================
697
-
698
- /** Source line of a block's heading, when position info is available. */
699
- function lineOf(block: Block): { line: number } | Record<string, never> {
700
- const line = block.sourceHeading?.position?.start.line;
701
- return line != null ? { line } : {};
702
- }
703
-
704
- /**
705
- * Populate `block.templateData` from structured body content:
706
- * 1. Every ```json data / ```yaml data fence in the body merges its parsed
707
- * object in (later fences override earlier keys).
708
- * 2. For `dataTable` blocks, the first GFM table supplies headers/rows/align
709
- * unless the author already provided them via a fence or `{[…]}` params.
710
- * Parse failures are recorded on `diagnostics` and skip the fence.
711
- */
712
- function applyStructuredData(block: Block, diagnostics: DocDiagnostic[]): void {
713
- let data: Record<string, unknown> | undefined;
714
-
715
- for (const node of block.contents ?? []) {
716
- if (node.type !== 'code' || !isDataFence(node)) continue;
717
- const result = parseDataFence(node);
718
- if (result.error) {
719
- diagnostics.push({
720
- severity: 'error',
721
- code: 'data-fence-parse',
722
- message: `Data fence in block "${block.id}": ${result.error}`,
723
- blockId: block.id,
724
- ...(node.position ? { line: node.position.start.line } : lineOf(block)),
725
- });
726
- continue;
727
- }
728
- data = { ...data, ...result.data };
729
- }
730
-
731
- if (block.template === 'dataTable') {
732
- const provided = (key: string) =>
733
- (data && key in data) || (block.templateOverrides && key in block.templateOverrides);
734
- if (!provided('headers') && !provided('rows')) {
735
- const table = findFirstTable(block.contents);
736
- if (table) {
737
- data = { ...extractTableData(table), ...data };
738
- }
739
- }
740
- }
741
-
742
- if (data && Object.keys(data).length > 0) {
743
- block.templateData = data;
744
- }
745
- }
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
-
835
- /** Truthiness of the `squisq-auto-templates` frontmatter kill-switch. */
836
- function frontmatterDisablesAutoTemplates(markdownDoc: MarkdownDocument): boolean {
837
- const v = markdownDoc.frontmatter?.['squisq-auto-templates'];
838
- return v === false || v === 'false' || v === 'off' || v === 'no' || v === 0;
839
- }
840
-
841
- /**
842
- * Walk heading blocks and apply content-aware templates to the ones that
843
- * still hold the structural default. Children of container templates
844
- * (diagram/drawing/layout) are consumed by their parent and never
845
- * re-templated. `state.featureIndex` alternates left/right feature
846
- * composition across the document.
847
- */
848
- function applyAutoTemplates(
849
- blocks: Block[],
850
- resolvedDefault: string,
851
- state: { featureIndex: number },
852
- ): void {
853
- for (const block of blocks) {
854
- const annotated = !!block.sourceHeading?.templateAnnotation?.template;
855
- if (block.sourceHeading && !annotated && block.template === resolvedDefault) {
856
- const profile = profileBlockContents(block.contents ?? []);
857
- const picked = pickAutoTemplate(profile, state.featureIndex);
858
- if (picked) {
859
- const inputs = deriveTemplateInputs(picked, block.title ?? '', block.contents);
860
- if (inputs) {
861
- if (picked === 'leftFeature' || picked === 'rightFeature') state.featureIndex += 1;
862
- block.template = picked;
863
- block.autoTemplate = true;
864
- // Author-provided structured data (```json data fences) wins
865
- // over derived inputs.
866
- block.templateData = { ...inputs, ...block.templateData };
867
- }
868
- }
869
- }
870
- if (block.children && block.children.length > 0 && !isContainerTemplate(block.template)) {
871
- applyAutoTemplates(block.children, resolvedDefault, state);
872
- }
873
- }
874
- }
875
-
876
- /**
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".
882
- */
883
- export function getBlockBodyText(block: Block): string {
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);
893
- // Join with newlines to preserve paragraph/list-item boundaries.
894
- // splitIntoPhrases uses these newlines as natural split points.
895
- return block.contents
896
- .filter((node) => !(consumedFence && node.type === 'code'))
897
- .map((node) => extractPlainText(node))
898
- .join('\n')
899
- .trim();
900
- }
901
-
902
- /** Maximum words per caption phrase. Long sentences get split at this point. */
903
- const MAX_PHRASE_WORDS = 12;
904
-
905
- /**
906
- * Split text into caption-sized phrases.
907
- *
908
- * Splits on:
909
- * 1. Newlines (paragraph/list-item boundaries from markdown)
910
- * 2. Sentence endings (.!?) followed by whitespace
911
- * 3. Long fragments (> MAX_PHRASE_WORDS) at clause boundaries (commas, semicolons, dashes)
912
- *
913
- * Merges very short fragments (< 15 chars) with the previous phrase.
914
- */
915
- function splitIntoSentences(text: string): string[] {
916
- // First split on newlines (each paragraph/list item becomes separate)
917
- const lines = text
918
- .split(/\n+/)
919
- .map((s) => s.trim())
920
- .filter((s) => s.length > 0);
921
-
922
- // Then split each line on sentence boundaries
923
- const sentenceRe = /(?<=[.!?])\s+/;
924
- const fragments: string[] = [];
925
- for (const line of lines) {
926
- const sentences = line
927
- .split(sentenceRe)
928
- .map((s) => s.trim())
929
- .filter((s) => s.length > 0);
930
- fragments.push(...sentences);
931
- }
932
-
933
- if (fragments.length === 0) return [];
934
-
935
- // Split long fragments at clause boundaries
936
- const split: string[] = [];
937
- for (const frag of fragments) {
938
- const words = frag.split(/\s+/);
939
- if (words.length > MAX_PHRASE_WORDS) {
940
- // Try to split at comma, semicolon, or dash near the midpoint
941
- const mid = Math.floor(frag.length / 2);
942
- const clauseRe = /[,;]\s+|\s+—\s+|\s+-\s+/g;
943
- let bestSplit = -1;
944
- let bestDist = Infinity;
945
- let match;
946
- while ((match = clauseRe.exec(frag)) !== null) {
947
- const dist = Math.abs(match.index - mid);
948
- if (dist < bestDist) {
949
- bestDist = dist;
950
- bestSplit = match.index + match[0].length;
951
- }
952
- }
953
- if (bestSplit > 0 && bestSplit < frag.length - 5) {
954
- split.push(frag.slice(0, bestSplit).trim());
955
- split.push(frag.slice(bestSplit).trim());
956
- } else {
957
- split.push(frag);
958
- }
959
- } else {
960
- split.push(frag);
961
- }
962
- }
963
-
964
- // Merge very short fragments (< 15 chars) with the previous phrase
965
- const merged: string[] = [split[0]];
966
- for (let i = 1; i < split.length; i++) {
967
- if (split[i].length < 15 && merged.length > 0) {
968
- merged[merged.length - 1] += ' ' + split[i];
969
- } else {
970
- merged.push(split[i]);
971
- }
972
- }
973
- return merged;
974
- }
975
-
976
- interface ImageRef {
977
- url: string;
978
- alt?: string;
979
- }
980
-
981
- /**
982
- * Walk a parsed HTML sub-tree depth-first for an `<img>` with a `src`.
983
- * Used to surface images that the WYSIWYG editor serialized as raw HTML
984
- * (e.g. when an image was resized — markdown's `![alt](url)` shorthand
985
- * has no width syntax, so resized images round-trip as `<img src width>`).
986
- */
987
- function findFirstHtmlImage(nodes: HtmlNode[]): ImageRef | undefined {
988
- for (const node of nodes) {
989
- if (node.type !== 'htmlElement') continue;
990
- if (node.tagName === 'img' && node.attributes.src) {
991
- return { url: node.attributes.src, alt: node.attributes.alt };
992
- }
993
- const nested = findFirstHtmlImage(node.children);
994
- if (nested) return nested;
995
- }
996
- return undefined;
997
- }
998
-
999
- /**
1000
- * Walk a MarkdownNode tree depth-first to find the first image reference,
1001
- * whether expressed as a markdown image node or a raw HTML `<img>` tag.
1002
- */
1003
- function findFirstImage(node: MarkdownNode): ImageRef | undefined {
1004
- if (node.type === 'image') {
1005
- const img = node as { url: string; alt?: string };
1006
- return { url: img.url, alt: img.alt };
1007
- }
1008
- if (node.type === 'htmlBlock' || node.type === 'htmlInline') {
1009
- const html = node as { htmlChildren?: HtmlNode[] };
1010
- if (html.htmlChildren) {
1011
- const found = findFirstHtmlImage(html.htmlChildren);
1012
- if (found) return found;
1013
- }
1014
- }
1015
- if ('children' in node && Array.isArray((node as { children?: unknown[] }).children)) {
1016
- for (const child of (node as { children: MarkdownNode[] }).children) {
1017
- const found = findFirstImage(child);
1018
- if (found) return found;
1019
- }
1020
- }
1021
- return undefined;
1022
- }
1023
-
1024
- /**
1025
- * Build a StartBlockConfig from the document's first H1 heading and optional
1026
- * first image. Returns undefined if the document has no H1 heading.
1027
- */
1028
- function buildStartBlock(
1029
- markdownDoc: MarkdownDocument,
1030
- rootBlocks: Block[],
1031
- ): StartBlockConfig | undefined {
1032
- // Find the first H1 block — it provides the cover title
1033
- const firstH1 = rootBlocks.find((b) => b.sourceHeading?.depth === 1);
1034
- if (!firstH1) return undefined;
1035
-
1036
- const title = firstH1.title ?? extractPlainText(firstH1.sourceHeading!);
1037
- if (!title) return undefined;
1038
-
1039
- // Look for the first paragraph immediately after the H1 to use as subtitle
1040
- const subtitle =
1041
- firstH1.contents?.[0]?.type === 'paragraph' ? extractPlainText(firstH1.contents[0]) : undefined;
1042
-
1043
- // Scan the whole document for the first image to use as the hero
1044
- const firstImage = findFirstImage(markdownDoc);
1045
-
1046
- const config: StartBlockConfig = {
1047
- title,
1048
- ...(subtitle ? { subtitle } : {}),
1049
- ...(firstImage ? { heroSrc: firstImage.url, heroAlt: firstImage.alt ?? title } : {}),
1050
- ambientMotion: 'zoomIn',
1051
- };
1052
-
1053
- return config;
1054
- }