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