@bendyline/squisq 2.1.0 → 2.3.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 (442) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE.md +24 -0
  3. package/README.md +8 -8
  4. package/dist/{ContentContainer-BXUlIf18.d.ts → ContentContainer-B2w9sUoL.d.ts} +15 -0
  5. package/dist/{Doc-DynJleo-.d.ts → Doc-COe-rPQB.d.ts} +7 -5
  6. package/dist/{ImageEditDoc-Ca4Tr9uN.d.ts → ImageEditDoc-dtKahVyD.d.ts} +1 -1
  7. package/dist/{annotationCoercion-Dr_DY9Hx.d.ts → annotationCoercion-PtRQhk5V.d.ts} +1 -1
  8. package/dist/{chunk-VWCJ2WXC.js → chunk-2ZWIXGAC.js} +0 -1
  9. package/dist/{chunk-VYFRI7R6.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-DBS6RIFL.js → chunk-7N4G32LG.js} +0 -1
  15. package/dist/{chunk-JOQYPRDZ.js → chunk-7YEZFHLS.js} +10 -8
  16. package/dist/{chunk-2UQOHYE2.js → chunk-7ZAAICW4.js} +0 -1
  17. package/dist/{chunk-66QO7XFB.js → chunk-ACZOVWX3.js} +117 -7
  18. package/dist/{chunk-5J4HKRJF.js → chunk-BAOV476U.js} +0 -1
  19. package/dist/chunk-BCCXTMN5.js +699 -0
  20. package/dist/{chunk-JHZWYRSW.js → chunk-C33GTPUZ.js} +11 -4
  21. package/dist/{chunk-XNLB57UD.js → chunk-EK446BUE.js} +18 -2
  22. package/dist/{chunk-VR7UPJ7C.js → chunk-FCG3BIZN.js} +12 -1
  23. package/dist/{chunk-JI77MGAK.js → chunk-GIIOY6XR.js} +2178 -1291
  24. package/dist/{chunk-Q7LROV2H.js → chunk-NKNMGIIN.js} +0 -1
  25. package/dist/chunk-OPO5K4DZ.js +241 -0
  26. package/dist/{chunk-TUWMK62B.js → chunk-PUS54YU6.js} +45 -2
  27. package/dist/{chunk-OTKVFVL6.js → chunk-QNTQIEIV.js} +130 -47
  28. package/dist/{chunk-5UYI3RJB.js → chunk-QWCFK5FN.js} +55 -40
  29. package/dist/{chunk-GZY66SDN.js → chunk-R3KTTOMS.js} +0 -1
  30. package/dist/{chunk-ILWDXLUK.js → chunk-RS5AP3J4.js} +49 -10
  31. package/dist/{chunk-QUUPYOSP.js → chunk-SPTY4C6F.js} +136 -5
  32. package/dist/{chunk-4X3JQXNM.js → chunk-XOFT65EX.js} +1 -2
  33. package/dist/{chunk-FVTQSNR7.js → chunk-XRW6P7IF.js} +13 -5
  34. package/dist/{chunk-R2YZTCLO.js → chunk-ZQKZSJAX.js} +1 -2
  35. package/dist/doc/index.d.ts +311 -13
  36. package/dist/doc/index.js +25 -17
  37. package/dist/generate/index.d.ts +2 -2
  38. package/dist/generate/index.js +1 -3
  39. package/dist/icons/index.js +2 -4
  40. package/dist/icons/inlineIconMarker.js +1 -2
  41. package/dist/imageEdit/index.d.ts +48 -17
  42. package/dist/imageEdit/index.js +3 -4
  43. package/dist/index.d.ts +13 -13
  44. package/dist/index.js +67 -33
  45. package/dist/jsonForm/index.d.ts +10 -3
  46. package/dist/jsonForm/index.js +8 -7
  47. package/dist/markdown/index.d.ts +75 -11
  48. package/dist/markdown/index.js +32 -7
  49. package/dist/{materializePageSection-DDzubQXo.d.ts → materializePageSection-BSMOocve.d.ts} +2 -2
  50. package/dist/narration/index.d.ts +16 -3
  51. package/dist/narration/index.js +12 -14
  52. package/dist/random/index.d.ts +11 -2
  53. package/dist/random/index.js +1 -3
  54. package/dist/recommend/index.d.ts +1 -1
  55. package/dist/recommend/index.js +2 -4
  56. package/dist/schemas/index.d.ts +18 -6
  57. package/dist/schemas/index.js +7 -6
  58. package/dist/spatial/index.d.ts +2 -1
  59. package/dist/spatial/index.js +1 -2
  60. package/dist/storage/index.d.ts +4 -2
  61. package/dist/storage/index.js +1 -2
  62. package/dist/{themeLibrary-B72m0W1y.d.ts → themeLibrary-BUTWjufL.d.ts} +1 -1
  63. package/dist/timing/index.js +2 -4
  64. package/dist/transform/index.d.ts +3 -3
  65. package/dist/transform/index.js +6 -7
  66. package/dist/{types-C4kiRn_k.d.ts → types-8QjefM9J.d.ts} +38 -2
  67. package/dist/{types-BwMPl2CB.d.ts → types-CUNc9biN.d.ts} +24 -1
  68. package/dist/versions/index.d.ts +14 -11
  69. package/dist/versions/index.js +2 -3
  70. package/package.json +8 -3
  71. package/dist/chunk-2UQOHYE2.js.map +0 -1
  72. package/dist/chunk-3E5F2XMR.js.map +0 -1
  73. package/dist/chunk-4X3JQXNM.js.map +0 -1
  74. package/dist/chunk-5DKBFXPY.js.map +0 -1
  75. package/dist/chunk-5J4HKRJF.js.map +0 -1
  76. package/dist/chunk-5UYI3RJB.js.map +0 -1
  77. package/dist/chunk-66QO7XFB.js.map +0 -1
  78. package/dist/chunk-AMIR72JP.js +0 -380
  79. package/dist/chunk-AMIR72JP.js.map +0 -1
  80. package/dist/chunk-DBS6RIFL.js.map +0 -1
  81. package/dist/chunk-DNJKBAI7.js +0 -1
  82. package/dist/chunk-DNJKBAI7.js.map +0 -1
  83. package/dist/chunk-ET53IIEP.js +0 -1
  84. package/dist/chunk-ET53IIEP.js.map +0 -1
  85. package/dist/chunk-FVTQSNR7.js.map +0 -1
  86. package/dist/chunk-GZY66SDN.js.map +0 -1
  87. package/dist/chunk-ILWDXLUK.js.map +0 -1
  88. package/dist/chunk-JHZWYRSW.js.map +0 -1
  89. package/dist/chunk-JI77MGAK.js.map +0 -1
  90. package/dist/chunk-JOQYPRDZ.js.map +0 -1
  91. package/dist/chunk-OTKVFVL6.js.map +0 -1
  92. package/dist/chunk-PGGGKYWG.js +0 -1
  93. package/dist/chunk-PGGGKYWG.js.map +0 -1
  94. package/dist/chunk-Q7LROV2H.js.map +0 -1
  95. package/dist/chunk-QE7TWGVT.js.map +0 -1
  96. package/dist/chunk-QUUPYOSP.js.map +0 -1
  97. package/dist/chunk-R2GC6RLD.js +0 -344
  98. package/dist/chunk-R2GC6RLD.js.map +0 -1
  99. package/dist/chunk-R2YZTCLO.js.map +0 -1
  100. package/dist/chunk-S7A5D7ZJ.js +0 -174
  101. package/dist/chunk-S7A5D7ZJ.js.map +0 -1
  102. package/dist/chunk-TUWMK62B.js.map +0 -1
  103. package/dist/chunk-V543AI2F.js +0 -1
  104. package/dist/chunk-V543AI2F.js.map +0 -1
  105. package/dist/chunk-VR7UPJ7C.js.map +0 -1
  106. package/dist/chunk-VWCJ2WXC.js.map +0 -1
  107. package/dist/chunk-VYFRI7R6.js.map +0 -1
  108. package/dist/chunk-XNLB57UD.js.map +0 -1
  109. package/dist/chunk-ZLQKNOZQ.js +0 -1
  110. package/dist/chunk-ZLQKNOZQ.js.map +0 -1
  111. package/dist/doc/index.js.map +0 -1
  112. package/dist/generate/index.js.map +0 -1
  113. package/dist/icons/index.js.map +0 -1
  114. package/dist/icons/inlineIconMarker.js.map +0 -1
  115. package/dist/imageEdit/index.js.map +0 -1
  116. package/dist/index.js.map +0 -1
  117. package/dist/jsonForm/index.js.map +0 -1
  118. package/dist/markdown/index.js.map +0 -1
  119. package/dist/narration/index.js.map +0 -1
  120. package/dist/random/index.js.map +0 -1
  121. package/dist/recommend/index.js.map +0 -1
  122. package/dist/schemas/index.js.map +0 -1
  123. package/dist/spatial/index.js.map +0 -1
  124. package/dist/storage/index.js.map +0 -1
  125. package/dist/timing/index.js.map +0 -1
  126. package/dist/transform/index.js.map +0 -1
  127. package/dist/versions/index.js.map +0 -1
  128. package/src/__tests__/animationUtils.test.ts +0 -117
  129. package/src/__tests__/annotationCoercion.test.ts +0 -243
  130. package/src/__tests__/applyNarrationTiming.test.ts +0 -190
  131. package/src/__tests__/applyRenderStyle.test.ts +0 -146
  132. package/src/__tests__/asciiDiagramBattle.test.ts +0 -246
  133. package/src/__tests__/asciiDiagramDetect.test.ts +0 -162
  134. package/src/__tests__/asciiDiagramMapping.test.ts +0 -38
  135. package/src/__tests__/asciiDiagramParse.test.ts +0 -233
  136. package/src/__tests__/asciiDiagramPipeline.test.ts +0 -273
  137. package/src/__tests__/asciiDiagramRender.test.ts +0 -162
  138. package/src/__tests__/asciiDiagramRepair.test.ts +0 -84
  139. package/src/__tests__/asciiTimelineDetect.test.ts +0 -157
  140. package/src/__tests__/asciiTimelineParse.test.ts +0 -205
  141. package/src/__tests__/asciiTimelinePipeline.test.ts +0 -349
  142. package/src/__tests__/asciiTimelineRender.test.ts +0 -376
  143. package/src/__tests__/atmosphereLayers.test.ts +0 -108
  144. package/src/__tests__/attrTokens.test.ts +0 -167
  145. package/src/__tests__/colorUtils.test.ts +0 -112
  146. package/src/__tests__/contentContainer.test.ts +0 -325
  147. package/src/__tests__/contentExtractor.test.ts +0 -243
  148. package/src/__tests__/coverBlock.test.ts +0 -310
  149. package/src/__tests__/customTemplateSample.test.ts +0 -144
  150. package/src/__tests__/customTemplatesFrontmatter.test.ts +0 -221
  151. package/src/__tests__/customThemesFrontmatter.test.ts +0 -146
  152. package/src/__tests__/dataTable.test.ts +0 -145
  153. package/src/__tests__/diagramBlock.test.ts +0 -122
  154. package/src/__tests__/diagramLayout.test.ts +0 -134
  155. package/src/__tests__/diagramText.test.ts +0 -26
  156. package/src/__tests__/docToMarkdownMedia.test.ts +0 -83
  157. package/src/__tests__/drawing.test.ts +0 -109
  158. package/src/__tests__/drawingBlock.test.ts +0 -124
  159. package/src/__tests__/drawingLayout.test.ts +0 -171
  160. package/src/__tests__/fixtures/asciiDiagramBattle.ts +0 -512
  161. package/src/__tests__/fixtures/asciiDiagrams.ts +0 -287
  162. package/src/__tests__/fixtures/asciiTimelines.ts +0 -35
  163. package/src/__tests__/fixtures/treeviewFixtures.ts +0 -243
  164. package/src/__tests__/geohash.test.ts +0 -110
  165. package/src/__tests__/getLayers.test.ts +0 -243
  166. package/src/__tests__/getLayersFallback.test.ts +0 -103
  167. package/src/__tests__/haversine.test.ts +0 -63
  168. package/src/__tests__/iconResolve.test.ts +0 -122
  169. package/src/__tests__/imageEditExportSvg.test.ts +0 -70
  170. package/src/__tests__/imageEditState.test.ts +0 -74
  171. package/src/__tests__/imageEditVersions.test.ts +0 -243
  172. package/src/__tests__/inferDocumentTitle.test.ts +0 -50
  173. package/src/__tests__/inlineIconMarker.test.ts +0 -36
  174. package/src/__tests__/inputDescriptors.test.ts +0 -162
  175. package/src/__tests__/jsonForm.chooseControl.test.ts +0 -86
  176. package/src/__tests__/jsonForm.evaluateWhen.test.ts +0 -59
  177. package/src/__tests__/jsonForm.inferSchema.test.ts +0 -25
  178. package/src/__tests__/jsonForm.pointer.test.ts +0 -149
  179. package/src/__tests__/jsonForm.tokens.test.ts +0 -84
  180. package/src/__tests__/layoutBlock.test.ts +0 -83
  181. package/src/__tests__/layoutLayout.test.ts +0 -169
  182. package/src/__tests__/localForageAdapter.test.ts +0 -155
  183. package/src/__tests__/majorApiTypes.test.ts +0 -18
  184. package/src/__tests__/markdown.test.ts +0 -1009
  185. package/src/__tests__/markdownIcons.test.ts +0 -104
  186. package/src/__tests__/markdownSanitize.test.ts +0 -81
  187. package/src/__tests__/markdownToDoc.test.ts +0 -957
  188. package/src/__tests__/matchFontFamily.test.ts +0 -114
  189. package/src/__tests__/materializeBlockLayers.test.ts +0 -167
  190. package/src/__tests__/materializePageSections.test.ts +0 -404
  191. package/src/__tests__/materializeTestUtils.ts +0 -17
  192. package/src/__tests__/mediaAnnotations.test.ts +0 -91
  193. package/src/__tests__/mediaSchedule.test.ts +0 -118
  194. package/src/__tests__/narrationAlign.test.ts +0 -150
  195. package/src/__tests__/narrationFeatures.test.ts +0 -71
  196. package/src/__tests__/narrationNuclei.test.ts +0 -45
  197. package/src/__tests__/narrationPacing.test.ts +0 -189
  198. package/src/__tests__/narrationScript.test.ts +0 -149
  199. package/src/__tests__/narrationSessionRepro.test.ts +0 -62
  200. package/src/__tests__/narrationSidecar.test.ts +0 -88
  201. package/src/__tests__/narrationSyllables.test.ts +0 -65
  202. package/src/__tests__/narrationTestSignals.ts +0 -167
  203. package/src/__tests__/narrationVad.test.ts +0 -66
  204. package/src/__tests__/pageStyle.test.ts +0 -258
  205. package/src/__tests__/pandocAttrParse.test.ts +0 -187
  206. package/src/__tests__/recommendTemplates.test.ts +0 -183
  207. package/src/__tests__/resolveDocTheme.test.ts +0 -69
  208. package/src/__tests__/rootExports.test.ts +0 -61
  209. package/src/__tests__/scopedContentContainer.test.ts +0 -109
  210. package/src/__tests__/seededRandom.test.ts +0 -142
  211. package/src/__tests__/shapeGeometry.test.ts +0 -138
  212. package/src/__tests__/shapeVocabulary.test.ts +0 -133
  213. package/src/__tests__/storage.test.ts +0 -60
  214. package/src/__tests__/structuredData.test.ts +0 -249
  215. package/src/__tests__/surface.test.ts +0 -68
  216. package/src/__tests__/templateAnnotationParse.test.ts +0 -173
  217. package/src/__tests__/templateInputs.test.ts +0 -69
  218. package/src/__tests__/templateMetadata.test.ts +0 -32
  219. package/src/__tests__/templates.test.ts +0 -710
  220. package/src/__tests__/themeCompile.test.ts +0 -225
  221. package/src/__tests__/themeValidator.test.ts +0 -131
  222. package/src/__tests__/timing.test.ts +0 -167
  223. package/src/__tests__/transformProvenance.test.ts +0 -160
  224. package/src/__tests__/transformV2.test.ts +0 -163
  225. package/src/__tests__/transitionNormalize.test.ts +0 -61
  226. package/src/__tests__/treeviewBattle.test.ts +0 -76
  227. package/src/__tests__/treeviewDetect.test.ts +0 -168
  228. package/src/__tests__/treeviewParse.test.ts +0 -118
  229. package/src/__tests__/treeviewPipeline.test.ts +0 -183
  230. package/src/__tests__/treeviewRender.test.ts +0 -91
  231. package/src/__tests__/validateDoc.test.ts +0 -260
  232. package/src/__tests__/versions.test.ts +0 -459
  233. package/src/doc/annotationBlocks.ts +0 -117
  234. package/src/doc/applyNarrationTiming.ts +0 -244
  235. package/src/doc/asciiDiagram/chars.ts +0 -210
  236. package/src/doc/asciiDiagram/detect.ts +0 -175
  237. package/src/doc/asciiDiagram/index.ts +0 -38
  238. package/src/doc/asciiDiagram/mapping.ts +0 -225
  239. package/src/doc/asciiDiagram/parse.ts +0 -1095
  240. package/src/doc/asciiDiagram/render.ts +0 -963
  241. package/src/doc/asciiDiagram/repair.ts +0 -400
  242. package/src/doc/asciiDiagram/types.ts +0 -65
  243. package/src/doc/asciiTimeline/detect.ts +0 -138
  244. package/src/doc/asciiTimeline/index.ts +0 -26
  245. package/src/doc/asciiTimeline/mapping.ts +0 -43
  246. package/src/doc/asciiTimeline/parse.ts +0 -870
  247. package/src/doc/asciiTimeline/render.ts +0 -427
  248. package/src/doc/asciiTimeline/types.ts +0 -73
  249. package/src/doc/asciiTimeline/wrappedFlow.ts +0 -427
  250. package/src/doc/audioMapping.ts +0 -442
  251. package/src/doc/customTemplatesFrontmatter.ts +0 -273
  252. package/src/doc/customThemesFrontmatter.ts +0 -95
  253. package/src/doc/docToMarkdown.ts +0 -284
  254. package/src/doc/index.ts +0 -161
  255. package/src/doc/markdownToDoc.ts +0 -1054
  256. package/src/doc/materializeBlockLayers.ts +0 -465
  257. package/src/doc/mediaAnnotations.ts +0 -120
  258. package/src/doc/page/PageSection.ts +0 -174
  259. package/src/doc/page/materializePageSection.ts +0 -616
  260. package/src/doc/page/resolvePageBlock.ts +0 -108
  261. package/src/doc/page/sectionExtractors.ts +0 -466
  262. package/src/doc/pageCss.ts +0 -628
  263. package/src/doc/resolveDocTheme.ts +0 -56
  264. package/src/doc/standaloneAnnotation.ts +0 -58
  265. package/src/doc/structuredData.ts +0 -301
  266. package/src/doc/templateInputs.ts +0 -424
  267. package/src/doc/templates/__tests__/customTemplate.test.ts +0 -170
  268. package/src/doc/templates/__tests__/customTemplateRegistry.test.ts +0 -124
  269. package/src/doc/templates/__tests__/listBlock.test.ts +0 -64
  270. package/src/doc/templates/accentImage.ts +0 -362
  271. package/src/doc/templates/captionUtils.ts +0 -56
  272. package/src/doc/templates/comparisonBar.ts +0 -173
  273. package/src/doc/templates/coverBlock.ts +0 -255
  274. package/src/doc/templates/customTemplate.ts +0 -51
  275. package/src/doc/templates/dataTable.ts +0 -102
  276. package/src/doc/templates/dateEvent.ts +0 -147
  277. package/src/doc/templates/definitionCard.ts +0 -140
  278. package/src/doc/templates/diagramBlock.ts +0 -403
  279. package/src/doc/templates/diagramLayout.ts +0 -159
  280. package/src/doc/templates/drawingBlock.ts +0 -325
  281. package/src/doc/templates/drawingLayout.ts +0 -539
  282. package/src/doc/templates/factCard.ts +0 -146
  283. package/src/doc/templates/fallbackBlock.ts +0 -123
  284. package/src/doc/templates/featureBlock.ts +0 -209
  285. package/src/doc/templates/fullBleedQuote.ts +0 -83
  286. package/src/doc/templates/imageWithCaption.ts +0 -186
  287. package/src/doc/templates/index.ts +0 -642
  288. package/src/doc/templates/inputDescriptors.ts +0 -451
  289. package/src/doc/templates/layoutBlock.ts +0 -43
  290. package/src/doc/templates/layoutLayout.ts +0 -243
  291. package/src/doc/templates/listBlock.ts +0 -207
  292. package/src/doc/templates/mapBlock.ts +0 -138
  293. package/src/doc/templates/metadata.ts +0 -130
  294. package/src/doc/templates/persistentLayers.ts +0 -526
  295. package/src/doc/templates/photoGrid.ts +0 -190
  296. package/src/doc/templates/pullQuote.ts +0 -141
  297. package/src/doc/templates/quoteBlock.ts +0 -141
  298. package/src/doc/templates/registry.ts +0 -88
  299. package/src/doc/templates/sectionHeader.ts +0 -120
  300. package/src/doc/templates/statHighlight.ts +0 -146
  301. package/src/doc/templates/templateNames.ts +0 -22
  302. package/src/doc/templates/timelineBlock.ts +0 -613
  303. package/src/doc/templates/titleBlock.ts +0 -150
  304. package/src/doc/templates/tokens/__tests__/resolveTokens.test.ts +0 -487
  305. package/src/doc/templates/tokens/resolveTokens.ts +0 -453
  306. package/src/doc/templates/treeBlock.ts +0 -131
  307. package/src/doc/templates/twoColumn.ts +0 -263
  308. package/src/doc/templates/videoPullQuote.ts +0 -142
  309. package/src/doc/templates/videoWithCaption.ts +0 -135
  310. package/src/doc/treeview/chars.ts +0 -72
  311. package/src/doc/treeview/detect.ts +0 -145
  312. package/src/doc/treeview/index.ts +0 -30
  313. package/src/doc/treeview/mapping.ts +0 -95
  314. package/src/doc/treeview/parse.ts +0 -201
  315. package/src/doc/treeview/render.ts +0 -41
  316. package/src/doc/treeview/types.ts +0 -43
  317. package/src/doc/utils/animationUtils.ts +0 -399
  318. package/src/doc/utils/applyRenderStyle.ts +0 -106
  319. package/src/doc/utils/diagramText.ts +0 -78
  320. package/src/doc/utils/imageTreatment.ts +0 -70
  321. package/src/doc/utils/nearest.ts +0 -59
  322. package/src/doc/utils/shapeGeometry.ts +0 -614
  323. package/src/doc/utils/themeUtils.ts +0 -243
  324. package/src/doc/validate.ts +0 -575
  325. package/src/generate/contentExtractor.ts +0 -921
  326. package/src/generate/index.ts +0 -18
  327. package/src/generate/templateMapper.ts +0 -177
  328. package/src/icons/iconData.ts +0 -31
  329. package/src/icons/index.ts +0 -26
  330. package/src/icons/inlineIconMarker.ts +0 -73
  331. package/src/icons/resolve.ts +0 -139
  332. package/src/imageEdit/ImageEditVersionManager.ts +0 -73
  333. package/src/imageEdit/export.ts +0 -410
  334. package/src/imageEdit/index.ts +0 -72
  335. package/src/imageEdit/persistence.ts +0 -113
  336. package/src/imageEdit/state.ts +0 -112
  337. package/src/imageEdit/versionPaths.ts +0 -44
  338. package/src/imageEdit/versions.ts +0 -152
  339. package/src/index.ts +0 -15
  340. package/src/internal/immutable.ts +0 -58
  341. package/src/jsonForm/chooseControl.ts +0 -88
  342. package/src/jsonForm/evaluateWhen.ts +0 -59
  343. package/src/jsonForm/index.ts +0 -32
  344. package/src/jsonForm/inferSchema.ts +0 -25
  345. package/src/jsonForm/pointer.ts +0 -154
  346. package/src/jsonForm/tokens.ts +0 -78
  347. package/src/jsonForm/types.ts +0 -167
  348. package/src/markdown/annotationCoercion.ts +0 -295
  349. package/src/markdown/attrTokens.ts +0 -243
  350. package/src/markdown/convert.ts +0 -1353
  351. package/src/markdown/htmlParse.ts +0 -212
  352. package/src/markdown/index.ts +0 -152
  353. package/src/markdown/parse.ts +0 -103
  354. package/src/markdown/sanitize.ts +0 -318
  355. package/src/markdown/stringify.ts +0 -207
  356. package/src/markdown/types.ts +0 -710
  357. package/src/markdown/utils.ts +0 -459
  358. package/src/narration/align.ts +0 -514
  359. package/src/narration/features.ts +0 -231
  360. package/src/narration/index.ts +0 -86
  361. package/src/narration/nuclei.ts +0 -118
  362. package/src/narration/pacing.ts +0 -211
  363. package/src/narration/script.ts +0 -239
  364. package/src/narration/session.ts +0 -100
  365. package/src/narration/sidecar.ts +0 -187
  366. package/src/narration/syllables.ts +0 -85
  367. package/src/narration/trace.ts +0 -50
  368. package/src/narration/types.ts +0 -299
  369. package/src/narration/vad.ts +0 -131
  370. package/src/ngeohash.d.ts +0 -11
  371. package/src/random/SeededRandom.ts +0 -127
  372. package/src/random/index.ts +0 -1
  373. package/src/recommend/index.ts +0 -16
  374. package/src/recommend/numberHighlight.ts +0 -25
  375. package/src/recommend/templates.ts +0 -393
  376. package/src/schemas/BlockTemplates.ts +0 -1046
  377. package/src/schemas/CustomTemplates.ts +0 -126
  378. package/src/schemas/Doc.ts +0 -1073
  379. package/src/schemas/ImageEditDoc.ts +0 -102
  380. package/src/schemas/LayoutStrategy.ts +0 -178
  381. package/src/schemas/Media.ts +0 -179
  382. package/src/schemas/MediaProvider.ts +0 -70
  383. package/src/schemas/PageStyle.ts +0 -213
  384. package/src/schemas/Theme.ts +0 -413
  385. package/src/schemas/Transitions.ts +0 -224
  386. package/src/schemas/Types.ts +0 -15
  387. package/src/schemas/Viewport.ts +0 -91
  388. package/src/schemas/colorUtils.ts +0 -219
  389. package/src/schemas/fontStacks.ts +0 -339
  390. package/src/schemas/index.ts +0 -31
  391. package/src/schemas/pageStyleDefaults.ts +0 -204
  392. package/src/schemas/themeCompile.ts +0 -244
  393. package/src/schemas/themeConstants.ts +0 -2
  394. package/src/schemas/themeLibrary.ts +0 -60
  395. package/src/schemas/themeValidator.ts +0 -609
  396. package/src/schemas/themes/bold.json +0 -77
  397. package/src/schemas/themes/cinematic.json +0 -143
  398. package/src/schemas/themes/documentary.json +0 -125
  399. package/src/schemas/themes/gezellig.json +0 -123
  400. package/src/schemas/themes/index.ts +0 -53
  401. package/src/schemas/themes/magazine.json +0 -126
  402. package/src/schemas/themes/minimalist.json +0 -74
  403. package/src/schemas/themes/morning-light.json +0 -117
  404. package/src/schemas/themes/standard-dark.json +0 -109
  405. package/src/schemas/themes/standard.json +0 -98
  406. package/src/schemas/themes/tech-dark.json +0 -143
  407. package/src/schemas/themes/warm-earth.json +0 -74
  408. package/src/spatial/Geohash.ts +0 -196
  409. package/src/spatial/Haversine.ts +0 -59
  410. package/src/spatial/index.ts +0 -2
  411. package/src/storage/ContentContainer.ts +0 -220
  412. package/src/storage/LocalForageAdapter.ts +0 -103
  413. package/src/storage/LocalStorageAdapter.ts +0 -57
  414. package/src/storage/MediaProviderFromContainer.ts +0 -92
  415. package/src/storage/MemoryStorageAdapter.ts +0 -37
  416. package/src/storage/ScopedContentContainer.ts +0 -143
  417. package/src/storage/Storage.ts +0 -28
  418. package/src/storage/index.ts +0 -9
  419. package/src/timing/index.ts +0 -17
  420. package/src/timing/narrationTiming.ts +0 -220
  421. package/src/timing/readingTime.ts +0 -94
  422. package/src/transform/applyTransform.ts +0 -122
  423. package/src/transform/blockAnalyzer.ts +0 -181
  424. package/src/transform/index.ts +0 -23
  425. package/src/transform/registry.ts +0 -217
  426. package/src/transform/styles/dataDriven.ts +0 -18
  427. package/src/transform/styles/documentary.ts +0 -18
  428. package/src/transform/styles/index.ts +0 -5
  429. package/src/transform/styles/magazine.ts +0 -19
  430. package/src/transform/styles/minimal.ts +0 -18
  431. package/src/transform/styles/narrative.ts +0 -19
  432. package/src/transform/templateSelector.ts +0 -389
  433. package/src/transform/timingAllocator.ts +0 -257
  434. package/src/transform/types.ts +0 -158
  435. package/src/versions/DocumentVersionManager.ts +0 -70
  436. package/src/versions/gitignore.ts +0 -34
  437. package/src/versions/index.ts +0 -56
  438. package/src/versions/operations.ts +0 -144
  439. package/src/versions/paths.ts +0 -51
  440. package/src/versions/snapshotStore.ts +0 -202
  441. package/src/versions/timestamp.ts +0 -48
  442. 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
- }