@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.
- package/LICENSE +21 -0
- package/NOTICE.md +24 -0
- package/dist/{ContentContainer-BXUlIf18.d.ts → ContentContainer-B2w9sUoL.d.ts} +15 -0
- package/dist/{Doc-DynJleo-.d.ts → Doc-BubIpWsp.d.ts} +1 -1
- package/dist/{ImageEditDoc-Ca4Tr9uN.d.ts → ImageEditDoc-CGxk5CHc.d.ts} +1 -1
- package/dist/{annotationCoercion-Dr_DY9Hx.d.ts → annotationCoercion-PtRQhk5V.d.ts} +1 -1
- package/dist/chunk-2L55443L.js +0 -0
- package/dist/{chunk-VWCJ2WXC.js → chunk-2ZWIXGAC.js} +0 -1
- package/dist/{chunk-VYFRI7R6.js → chunk-32R2RZAO.js} +72 -3
- package/dist/chunk-3IGPAPQ7.js +645 -0
- package/dist/{chunk-QE7TWGVT.js → chunk-4VOD55SX.js} +0 -1
- package/dist/{chunk-3E5F2XMR.js → chunk-67L6WRJN.js} +0 -1
- package/dist/{chunk-5DKBFXPY.js → chunk-6ECMNUIP.js} +26 -18
- package/dist/{chunk-JI77MGAK.js → chunk-7ARJZYQY.js} +159 -120
- package/dist/{chunk-DBS6RIFL.js → chunk-7N4G32LG.js} +0 -1
- package/dist/{chunk-JOQYPRDZ.js → chunk-7YEZFHLS.js} +10 -8
- package/dist/{chunk-2UQOHYE2.js → chunk-7ZAAICW4.js} +0 -1
- package/dist/{chunk-66QO7XFB.js → chunk-ACZOVWX3.js} +117 -7
- package/dist/{chunk-5J4HKRJF.js → chunk-BAOV476U.js} +0 -1
- package/dist/chunk-BCCXTMN5.js +699 -0
- package/dist/{chunk-JHZWYRSW.js → chunk-C33GTPUZ.js} +11 -4
- package/dist/{chunk-QUUPYOSP.js → chunk-CB45YGAM.js} +2 -3
- package/dist/{chunk-XNLB57UD.js → chunk-DYPMOPIW.js} +18 -2
- package/dist/{chunk-VR7UPJ7C.js → chunk-FCG3BIZN.js} +12 -1
- package/dist/{chunk-5UYI3RJB.js → chunk-FDYDYL77.js} +54 -40
- package/dist/{chunk-OTKVFVL6.js → chunk-I2O6W7DX.js} +116 -44
- package/dist/{chunk-Q7LROV2H.js → chunk-NKNMGIIN.js} +0 -1
- package/dist/chunk-OPO5K4DZ.js +241 -0
- package/dist/{chunk-TUWMK62B.js → chunk-PUS54YU6.js} +45 -2
- package/dist/chunk-QKPXHDLR.js +0 -0
- package/dist/{chunk-GZY66SDN.js → chunk-R3KTTOMS.js} +0 -1
- package/dist/{chunk-ILWDXLUK.js → chunk-RS5AP3J4.js} +49 -10
- package/dist/chunk-WV5ARNDA.js +0 -0
- package/dist/{chunk-4X3JQXNM.js → chunk-XOFT65EX.js} +1 -2
- package/dist/{chunk-FVTQSNR7.js → chunk-XRW6P7IF.js} +13 -5
- package/dist/chunk-XWNNA3KE.js +0 -0
- package/dist/chunk-YZNGMV3G.js +0 -0
- package/dist/{chunk-R2YZTCLO.js → chunk-ZQKZSJAX.js} +1 -2
- package/dist/doc/index.d.ts +27 -12
- package/dist/doc/index.js +16 -17
- package/dist/generate/index.d.ts +2 -2
- package/dist/generate/index.js +2 -3
- package/dist/icons/index.js +3 -4
- package/dist/icons/inlineIconMarker.js +1 -2
- package/dist/imageEdit/index.d.ts +48 -17
- package/dist/imageEdit/index.js +3 -4
- package/dist/index.d.ts +11 -11
- package/dist/index.js +60 -33
- package/dist/jsonForm/index.d.ts +10 -3
- package/dist/jsonForm/index.js +8 -7
- package/dist/markdown/index.d.ts +75 -11
- package/dist/markdown/index.js +32 -7
- package/dist/{materializePageSection-DDzubQXo.d.ts → materializePageSection-wSLA2A1g.d.ts} +2 -2
- package/dist/narration/index.d.ts +16 -3
- package/dist/narration/index.js +13 -14
- package/dist/random/index.d.ts +11 -2
- package/dist/random/index.js +2 -3
- package/dist/recommend/index.d.ts +1 -1
- package/dist/recommend/index.js +3 -4
- package/dist/schemas/index.d.ts +5 -5
- package/dist/schemas/index.js +5 -6
- package/dist/spatial/index.d.ts +2 -1
- package/dist/spatial/index.js +1 -2
- package/dist/storage/index.d.ts +4 -2
- package/dist/storage/index.js +1 -2
- package/dist/{themeLibrary-B72m0W1y.d.ts → themeLibrary-DU9mUmVl.d.ts} +1 -1
- package/dist/timing/index.js +3 -4
- package/dist/transform/index.d.ts +3 -3
- package/dist/transform/index.js +6 -7
- package/dist/{types-C4kiRn_k.d.ts → types-8QjefM9J.d.ts} +38 -2
- package/dist/{types-BwMPl2CB.d.ts → types-CUNc9biN.d.ts} +24 -1
- package/dist/versions/index.d.ts +14 -11
- package/dist/versions/index.js +2 -3
- package/package.json +4 -2
- package/dist/chunk-2UQOHYE2.js.map +0 -1
- package/dist/chunk-3E5F2XMR.js.map +0 -1
- package/dist/chunk-4X3JQXNM.js.map +0 -1
- package/dist/chunk-5DKBFXPY.js.map +0 -1
- package/dist/chunk-5J4HKRJF.js.map +0 -1
- package/dist/chunk-5UYI3RJB.js.map +0 -1
- package/dist/chunk-66QO7XFB.js.map +0 -1
- package/dist/chunk-AMIR72JP.js +0 -380
- package/dist/chunk-AMIR72JP.js.map +0 -1
- package/dist/chunk-DBS6RIFL.js.map +0 -1
- package/dist/chunk-DNJKBAI7.js +0 -1
- package/dist/chunk-DNJKBAI7.js.map +0 -1
- package/dist/chunk-ET53IIEP.js +0 -1
- package/dist/chunk-ET53IIEP.js.map +0 -1
- package/dist/chunk-FVTQSNR7.js.map +0 -1
- package/dist/chunk-GZY66SDN.js.map +0 -1
- package/dist/chunk-ILWDXLUK.js.map +0 -1
- package/dist/chunk-JHZWYRSW.js.map +0 -1
- package/dist/chunk-JI77MGAK.js.map +0 -1
- package/dist/chunk-JOQYPRDZ.js.map +0 -1
- package/dist/chunk-OTKVFVL6.js.map +0 -1
- package/dist/chunk-PGGGKYWG.js +0 -1
- package/dist/chunk-PGGGKYWG.js.map +0 -1
- package/dist/chunk-Q7LROV2H.js.map +0 -1
- package/dist/chunk-QE7TWGVT.js.map +0 -1
- package/dist/chunk-QUUPYOSP.js.map +0 -1
- package/dist/chunk-R2GC6RLD.js +0 -344
- package/dist/chunk-R2GC6RLD.js.map +0 -1
- package/dist/chunk-R2YZTCLO.js.map +0 -1
- package/dist/chunk-S7A5D7ZJ.js +0 -174
- package/dist/chunk-S7A5D7ZJ.js.map +0 -1
- package/dist/chunk-TUWMK62B.js.map +0 -1
- package/dist/chunk-V543AI2F.js +0 -1
- package/dist/chunk-V543AI2F.js.map +0 -1
- package/dist/chunk-VR7UPJ7C.js.map +0 -1
- package/dist/chunk-VWCJ2WXC.js.map +0 -1
- package/dist/chunk-VYFRI7R6.js.map +0 -1
- package/dist/chunk-XNLB57UD.js.map +0 -1
- package/dist/chunk-ZLQKNOZQ.js +0 -1
- package/dist/chunk-ZLQKNOZQ.js.map +0 -1
- package/dist/doc/index.js.map +0 -1
- package/dist/generate/index.js.map +0 -1
- package/dist/icons/index.js.map +0 -1
- package/dist/icons/inlineIconMarker.js.map +0 -1
- package/dist/imageEdit/index.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/jsonForm/index.js.map +0 -1
- package/dist/markdown/index.js.map +0 -1
- package/dist/narration/index.js.map +0 -1
- package/dist/random/index.js.map +0 -1
- package/dist/recommend/index.js.map +0 -1
- package/dist/schemas/index.js.map +0 -1
- package/dist/spatial/index.js.map +0 -1
- package/dist/storage/index.js.map +0 -1
- package/dist/timing/index.js.map +0 -1
- package/dist/transform/index.js.map +0 -1
- package/dist/versions/index.js.map +0 -1
- package/src/__tests__/animationUtils.test.ts +0 -117
- package/src/__tests__/annotationCoercion.test.ts +0 -243
- package/src/__tests__/applyNarrationTiming.test.ts +0 -190
- package/src/__tests__/applyRenderStyle.test.ts +0 -146
- package/src/__tests__/asciiDiagramBattle.test.ts +0 -246
- package/src/__tests__/asciiDiagramDetect.test.ts +0 -162
- package/src/__tests__/asciiDiagramMapping.test.ts +0 -38
- package/src/__tests__/asciiDiagramParse.test.ts +0 -233
- package/src/__tests__/asciiDiagramPipeline.test.ts +0 -273
- package/src/__tests__/asciiDiagramRender.test.ts +0 -162
- package/src/__tests__/asciiDiagramRepair.test.ts +0 -84
- package/src/__tests__/asciiTimelineDetect.test.ts +0 -157
- package/src/__tests__/asciiTimelineParse.test.ts +0 -205
- package/src/__tests__/asciiTimelinePipeline.test.ts +0 -349
- package/src/__tests__/asciiTimelineRender.test.ts +0 -376
- package/src/__tests__/atmosphereLayers.test.ts +0 -108
- package/src/__tests__/attrTokens.test.ts +0 -167
- package/src/__tests__/colorUtils.test.ts +0 -112
- package/src/__tests__/contentContainer.test.ts +0 -325
- package/src/__tests__/contentExtractor.test.ts +0 -243
- package/src/__tests__/coverBlock.test.ts +0 -310
- package/src/__tests__/customTemplateSample.test.ts +0 -144
- package/src/__tests__/customTemplatesFrontmatter.test.ts +0 -221
- package/src/__tests__/customThemesFrontmatter.test.ts +0 -146
- package/src/__tests__/dataTable.test.ts +0 -145
- package/src/__tests__/diagramBlock.test.ts +0 -122
- package/src/__tests__/diagramLayout.test.ts +0 -134
- package/src/__tests__/diagramText.test.ts +0 -26
- package/src/__tests__/docToMarkdownMedia.test.ts +0 -83
- package/src/__tests__/drawing.test.ts +0 -109
- package/src/__tests__/drawingBlock.test.ts +0 -124
- package/src/__tests__/drawingLayout.test.ts +0 -171
- package/src/__tests__/fixtures/asciiDiagramBattle.ts +0 -512
- package/src/__tests__/fixtures/asciiDiagrams.ts +0 -287
- package/src/__tests__/fixtures/asciiTimelines.ts +0 -35
- package/src/__tests__/fixtures/treeviewFixtures.ts +0 -243
- package/src/__tests__/geohash.test.ts +0 -110
- package/src/__tests__/getLayers.test.ts +0 -243
- package/src/__tests__/getLayersFallback.test.ts +0 -103
- package/src/__tests__/haversine.test.ts +0 -63
- package/src/__tests__/iconResolve.test.ts +0 -122
- package/src/__tests__/imageEditExportSvg.test.ts +0 -70
- package/src/__tests__/imageEditState.test.ts +0 -74
- package/src/__tests__/imageEditVersions.test.ts +0 -243
- package/src/__tests__/inferDocumentTitle.test.ts +0 -50
- package/src/__tests__/inlineIconMarker.test.ts +0 -36
- package/src/__tests__/inputDescriptors.test.ts +0 -162
- package/src/__tests__/jsonForm.chooseControl.test.ts +0 -86
- package/src/__tests__/jsonForm.evaluateWhen.test.ts +0 -59
- package/src/__tests__/jsonForm.inferSchema.test.ts +0 -25
- package/src/__tests__/jsonForm.pointer.test.ts +0 -149
- package/src/__tests__/jsonForm.tokens.test.ts +0 -84
- package/src/__tests__/layoutBlock.test.ts +0 -83
- package/src/__tests__/layoutLayout.test.ts +0 -169
- package/src/__tests__/localForageAdapter.test.ts +0 -155
- package/src/__tests__/majorApiTypes.test.ts +0 -18
- package/src/__tests__/markdown.test.ts +0 -1009
- package/src/__tests__/markdownIcons.test.ts +0 -104
- package/src/__tests__/markdownSanitize.test.ts +0 -81
- package/src/__tests__/markdownToDoc.test.ts +0 -957
- package/src/__tests__/matchFontFamily.test.ts +0 -114
- package/src/__tests__/materializeBlockLayers.test.ts +0 -167
- package/src/__tests__/materializePageSections.test.ts +0 -404
- package/src/__tests__/materializeTestUtils.ts +0 -17
- package/src/__tests__/mediaAnnotations.test.ts +0 -91
- package/src/__tests__/mediaSchedule.test.ts +0 -118
- package/src/__tests__/narrationAlign.test.ts +0 -150
- package/src/__tests__/narrationFeatures.test.ts +0 -71
- package/src/__tests__/narrationNuclei.test.ts +0 -45
- package/src/__tests__/narrationPacing.test.ts +0 -189
- package/src/__tests__/narrationScript.test.ts +0 -149
- package/src/__tests__/narrationSessionRepro.test.ts +0 -62
- package/src/__tests__/narrationSidecar.test.ts +0 -88
- package/src/__tests__/narrationSyllables.test.ts +0 -65
- package/src/__tests__/narrationTestSignals.ts +0 -167
- package/src/__tests__/narrationVad.test.ts +0 -66
- package/src/__tests__/pageStyle.test.ts +0 -258
- package/src/__tests__/pandocAttrParse.test.ts +0 -187
- package/src/__tests__/recommendTemplates.test.ts +0 -183
- package/src/__tests__/resolveDocTheme.test.ts +0 -69
- package/src/__tests__/rootExports.test.ts +0 -61
- package/src/__tests__/scopedContentContainer.test.ts +0 -109
- package/src/__tests__/seededRandom.test.ts +0 -142
- package/src/__tests__/shapeGeometry.test.ts +0 -138
- package/src/__tests__/shapeVocabulary.test.ts +0 -133
- package/src/__tests__/storage.test.ts +0 -60
- package/src/__tests__/structuredData.test.ts +0 -249
- package/src/__tests__/surface.test.ts +0 -68
- package/src/__tests__/templateAnnotationParse.test.ts +0 -173
- package/src/__tests__/templateInputs.test.ts +0 -69
- package/src/__tests__/templateMetadata.test.ts +0 -32
- package/src/__tests__/templates.test.ts +0 -710
- package/src/__tests__/themeCompile.test.ts +0 -225
- package/src/__tests__/themeValidator.test.ts +0 -131
- package/src/__tests__/timing.test.ts +0 -167
- package/src/__tests__/transformProvenance.test.ts +0 -160
- package/src/__tests__/transformV2.test.ts +0 -163
- package/src/__tests__/transitionNormalize.test.ts +0 -61
- package/src/__tests__/treeviewBattle.test.ts +0 -76
- package/src/__tests__/treeviewDetect.test.ts +0 -168
- package/src/__tests__/treeviewParse.test.ts +0 -118
- package/src/__tests__/treeviewPipeline.test.ts +0 -183
- package/src/__tests__/treeviewRender.test.ts +0 -91
- package/src/__tests__/validateDoc.test.ts +0 -260
- package/src/__tests__/versions.test.ts +0 -459
- package/src/doc/annotationBlocks.ts +0 -117
- package/src/doc/applyNarrationTiming.ts +0 -244
- package/src/doc/asciiDiagram/chars.ts +0 -210
- package/src/doc/asciiDiagram/detect.ts +0 -175
- package/src/doc/asciiDiagram/index.ts +0 -38
- package/src/doc/asciiDiagram/mapping.ts +0 -225
- package/src/doc/asciiDiagram/parse.ts +0 -1095
- package/src/doc/asciiDiagram/render.ts +0 -963
- package/src/doc/asciiDiagram/repair.ts +0 -400
- package/src/doc/asciiDiagram/types.ts +0 -65
- package/src/doc/asciiTimeline/detect.ts +0 -138
- package/src/doc/asciiTimeline/index.ts +0 -26
- package/src/doc/asciiTimeline/mapping.ts +0 -43
- package/src/doc/asciiTimeline/parse.ts +0 -870
- package/src/doc/asciiTimeline/render.ts +0 -427
- package/src/doc/asciiTimeline/types.ts +0 -73
- package/src/doc/asciiTimeline/wrappedFlow.ts +0 -427
- package/src/doc/audioMapping.ts +0 -442
- package/src/doc/customTemplatesFrontmatter.ts +0 -273
- package/src/doc/customThemesFrontmatter.ts +0 -95
- package/src/doc/docToMarkdown.ts +0 -284
- package/src/doc/index.ts +0 -161
- package/src/doc/markdownToDoc.ts +0 -1054
- package/src/doc/materializeBlockLayers.ts +0 -465
- package/src/doc/mediaAnnotations.ts +0 -120
- package/src/doc/page/PageSection.ts +0 -174
- package/src/doc/page/materializePageSection.ts +0 -616
- package/src/doc/page/resolvePageBlock.ts +0 -108
- package/src/doc/page/sectionExtractors.ts +0 -466
- package/src/doc/pageCss.ts +0 -628
- package/src/doc/resolveDocTheme.ts +0 -56
- package/src/doc/standaloneAnnotation.ts +0 -58
- package/src/doc/structuredData.ts +0 -301
- package/src/doc/templateInputs.ts +0 -424
- package/src/doc/templates/__tests__/customTemplate.test.ts +0 -170
- package/src/doc/templates/__tests__/customTemplateRegistry.test.ts +0 -124
- package/src/doc/templates/__tests__/listBlock.test.ts +0 -64
- package/src/doc/templates/accentImage.ts +0 -362
- package/src/doc/templates/captionUtils.ts +0 -56
- package/src/doc/templates/comparisonBar.ts +0 -173
- package/src/doc/templates/coverBlock.ts +0 -255
- package/src/doc/templates/customTemplate.ts +0 -51
- package/src/doc/templates/dataTable.ts +0 -102
- package/src/doc/templates/dateEvent.ts +0 -147
- package/src/doc/templates/definitionCard.ts +0 -140
- package/src/doc/templates/diagramBlock.ts +0 -403
- package/src/doc/templates/diagramLayout.ts +0 -159
- package/src/doc/templates/drawingBlock.ts +0 -325
- package/src/doc/templates/drawingLayout.ts +0 -539
- package/src/doc/templates/factCard.ts +0 -146
- package/src/doc/templates/fallbackBlock.ts +0 -123
- package/src/doc/templates/featureBlock.ts +0 -209
- package/src/doc/templates/fullBleedQuote.ts +0 -83
- package/src/doc/templates/imageWithCaption.ts +0 -186
- package/src/doc/templates/index.ts +0 -642
- package/src/doc/templates/inputDescriptors.ts +0 -451
- package/src/doc/templates/layoutBlock.ts +0 -43
- package/src/doc/templates/layoutLayout.ts +0 -243
- package/src/doc/templates/listBlock.ts +0 -207
- package/src/doc/templates/mapBlock.ts +0 -138
- package/src/doc/templates/metadata.ts +0 -130
- package/src/doc/templates/persistentLayers.ts +0 -526
- package/src/doc/templates/photoGrid.ts +0 -190
- package/src/doc/templates/pullQuote.ts +0 -141
- package/src/doc/templates/quoteBlock.ts +0 -141
- package/src/doc/templates/registry.ts +0 -88
- package/src/doc/templates/sectionHeader.ts +0 -120
- package/src/doc/templates/statHighlight.ts +0 -146
- package/src/doc/templates/templateNames.ts +0 -22
- package/src/doc/templates/timelineBlock.ts +0 -613
- package/src/doc/templates/titleBlock.ts +0 -150
- package/src/doc/templates/tokens/__tests__/resolveTokens.test.ts +0 -487
- package/src/doc/templates/tokens/resolveTokens.ts +0 -453
- package/src/doc/templates/treeBlock.ts +0 -131
- package/src/doc/templates/twoColumn.ts +0 -263
- package/src/doc/templates/videoPullQuote.ts +0 -142
- package/src/doc/templates/videoWithCaption.ts +0 -135
- package/src/doc/treeview/chars.ts +0 -72
- package/src/doc/treeview/detect.ts +0 -145
- package/src/doc/treeview/index.ts +0 -30
- package/src/doc/treeview/mapping.ts +0 -95
- package/src/doc/treeview/parse.ts +0 -201
- package/src/doc/treeview/render.ts +0 -41
- package/src/doc/treeview/types.ts +0 -43
- package/src/doc/utils/animationUtils.ts +0 -399
- package/src/doc/utils/applyRenderStyle.ts +0 -106
- package/src/doc/utils/diagramText.ts +0 -78
- package/src/doc/utils/imageTreatment.ts +0 -70
- package/src/doc/utils/nearest.ts +0 -59
- package/src/doc/utils/shapeGeometry.ts +0 -614
- package/src/doc/utils/themeUtils.ts +0 -243
- package/src/doc/validate.ts +0 -575
- package/src/generate/contentExtractor.ts +0 -921
- package/src/generate/index.ts +0 -18
- package/src/generate/templateMapper.ts +0 -177
- package/src/icons/iconData.ts +0 -31
- package/src/icons/index.ts +0 -26
- package/src/icons/inlineIconMarker.ts +0 -73
- package/src/icons/resolve.ts +0 -139
- package/src/imageEdit/ImageEditVersionManager.ts +0 -73
- package/src/imageEdit/export.ts +0 -410
- package/src/imageEdit/index.ts +0 -72
- package/src/imageEdit/persistence.ts +0 -113
- package/src/imageEdit/state.ts +0 -112
- package/src/imageEdit/versionPaths.ts +0 -44
- package/src/imageEdit/versions.ts +0 -152
- package/src/index.ts +0 -15
- package/src/internal/immutable.ts +0 -58
- package/src/jsonForm/chooseControl.ts +0 -88
- package/src/jsonForm/evaluateWhen.ts +0 -59
- package/src/jsonForm/index.ts +0 -32
- package/src/jsonForm/inferSchema.ts +0 -25
- package/src/jsonForm/pointer.ts +0 -154
- package/src/jsonForm/tokens.ts +0 -78
- package/src/jsonForm/types.ts +0 -167
- package/src/markdown/annotationCoercion.ts +0 -295
- package/src/markdown/attrTokens.ts +0 -243
- package/src/markdown/convert.ts +0 -1353
- package/src/markdown/htmlParse.ts +0 -212
- package/src/markdown/index.ts +0 -152
- package/src/markdown/parse.ts +0 -103
- package/src/markdown/sanitize.ts +0 -318
- package/src/markdown/stringify.ts +0 -207
- package/src/markdown/types.ts +0 -710
- package/src/markdown/utils.ts +0 -459
- package/src/narration/align.ts +0 -514
- package/src/narration/features.ts +0 -231
- package/src/narration/index.ts +0 -86
- package/src/narration/nuclei.ts +0 -118
- package/src/narration/pacing.ts +0 -211
- package/src/narration/script.ts +0 -239
- package/src/narration/session.ts +0 -100
- package/src/narration/sidecar.ts +0 -187
- package/src/narration/syllables.ts +0 -85
- package/src/narration/trace.ts +0 -50
- package/src/narration/types.ts +0 -299
- package/src/narration/vad.ts +0 -131
- package/src/ngeohash.d.ts +0 -11
- package/src/random/SeededRandom.ts +0 -127
- package/src/random/index.ts +0 -1
- package/src/recommend/index.ts +0 -16
- package/src/recommend/numberHighlight.ts +0 -25
- package/src/recommend/templates.ts +0 -393
- package/src/schemas/BlockTemplates.ts +0 -1046
- package/src/schemas/CustomTemplates.ts +0 -126
- package/src/schemas/Doc.ts +0 -1073
- package/src/schemas/ImageEditDoc.ts +0 -102
- package/src/schemas/LayoutStrategy.ts +0 -178
- package/src/schemas/Media.ts +0 -179
- package/src/schemas/MediaProvider.ts +0 -70
- package/src/schemas/PageStyle.ts +0 -213
- package/src/schemas/Theme.ts +0 -413
- package/src/schemas/Transitions.ts +0 -224
- package/src/schemas/Types.ts +0 -15
- package/src/schemas/Viewport.ts +0 -91
- package/src/schemas/colorUtils.ts +0 -219
- package/src/schemas/fontStacks.ts +0 -339
- package/src/schemas/index.ts +0 -31
- package/src/schemas/pageStyleDefaults.ts +0 -204
- package/src/schemas/themeCompile.ts +0 -244
- package/src/schemas/themeConstants.ts +0 -2
- package/src/schemas/themeLibrary.ts +0 -60
- package/src/schemas/themeValidator.ts +0 -609
- package/src/schemas/themes/bold.json +0 -77
- package/src/schemas/themes/cinematic.json +0 -143
- package/src/schemas/themes/documentary.json +0 -125
- package/src/schemas/themes/gezellig.json +0 -123
- package/src/schemas/themes/index.ts +0 -53
- package/src/schemas/themes/magazine.json +0 -126
- package/src/schemas/themes/minimalist.json +0 -74
- package/src/schemas/themes/morning-light.json +0 -117
- package/src/schemas/themes/standard-dark.json +0 -109
- package/src/schemas/themes/standard.json +0 -98
- package/src/schemas/themes/tech-dark.json +0 -143
- package/src/schemas/themes/warm-earth.json +0 -74
- package/src/spatial/Geohash.ts +0 -196
- package/src/spatial/Haversine.ts +0 -59
- package/src/spatial/index.ts +0 -2
- package/src/storage/ContentContainer.ts +0 -220
- package/src/storage/LocalForageAdapter.ts +0 -103
- package/src/storage/LocalStorageAdapter.ts +0 -57
- package/src/storage/MediaProviderFromContainer.ts +0 -92
- package/src/storage/MemoryStorageAdapter.ts +0 -37
- package/src/storage/ScopedContentContainer.ts +0 -143
- package/src/storage/Storage.ts +0 -28
- package/src/storage/index.ts +0 -9
- package/src/timing/index.ts +0 -17
- package/src/timing/narrationTiming.ts +0 -220
- package/src/timing/readingTime.ts +0 -94
- package/src/transform/applyTransform.ts +0 -122
- package/src/transform/blockAnalyzer.ts +0 -181
- package/src/transform/index.ts +0 -23
- package/src/transform/registry.ts +0 -217
- package/src/transform/styles/dataDriven.ts +0 -18
- package/src/transform/styles/documentary.ts +0 -18
- package/src/transform/styles/index.ts +0 -5
- package/src/transform/styles/magazine.ts +0 -19
- package/src/transform/styles/minimal.ts +0 -18
- package/src/transform/styles/narrative.ts +0 -19
- package/src/transform/templateSelector.ts +0 -389
- package/src/transform/timingAllocator.ts +0 -257
- package/src/transform/types.ts +0 -158
- package/src/versions/DocumentVersionManager.ts +0 -70
- package/src/versions/gitignore.ts +0 -34
- package/src/versions/index.ts +0 -56
- package/src/versions/operations.ts +0 -144
- package/src/versions/paths.ts +0 -51
- package/src/versions/snapshotStore.ts +0 -202
- package/src/versions/timestamp.ts +0 -48
- package/src/versions/types.ts +0 -93
package/src/doc/markdownToDoc.ts
DELETED
|
@@ -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 `` 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
|
-
}
|