@bendyline/squisq 1.3.0 → 1.4.1
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/dist/{Doc-Ch4tc3vv.d.ts → Doc-BeSeH2iX.d.ts} +118 -41
- package/dist/ImageEditDoc-BTIvw0oq.d.ts +84 -0
- package/dist/{chunk-CYQHG27J.js → chunk-2TMAWB4Y.js} +74 -1
- package/dist/chunk-2TMAWB4Y.js.map +1 -0
- package/dist/{chunk-OASJKWO5.js → chunk-3K5OG6KI.js} +92 -7
- package/dist/chunk-3K5OG6KI.js.map +1 -0
- package/dist/chunk-57CVQCFX.js +193 -0
- package/dist/chunk-57CVQCFX.js.map +1 -0
- package/dist/{chunk-QM5PKNPW.js → chunk-5WFRKURX.js} +1 -1
- package/dist/chunk-5WFRKURX.js.map +1 -0
- package/dist/chunk-B4EEON3N.js +1317 -0
- package/dist/chunk-B4EEON3N.js.map +1 -0
- package/dist/chunk-DBS6RIFL.js +81 -0
- package/dist/chunk-DBS6RIFL.js.map +1 -0
- package/dist/{chunk-MIBRIRGJ.js → chunk-FR2RBTKO.js} +3 -3
- package/dist/{chunk-MIBRIRGJ.js.map → chunk-FR2RBTKO.js.map} +1 -1
- package/dist/chunk-H3AXU4MJ.js +179 -0
- package/dist/chunk-H3AXU4MJ.js.map +1 -0
- package/dist/chunk-JELIRVDP.js +24 -0
- package/dist/chunk-JELIRVDP.js.map +1 -0
- package/dist/chunk-K32VJONL.js +193 -0
- package/dist/chunk-K32VJONL.js.map +1 -0
- package/dist/{chunk-6BAVXFYC.js → chunk-KSWJR66U.js} +218 -42
- package/dist/chunk-KSWJR66U.js.map +1 -0
- package/dist/{chunk-TSG2OSE6.js → chunk-MYIH7FWD.js} +3 -3
- package/dist/chunk-MYIH7FWD.js.map +1 -0
- package/dist/chunk-VLJJHDAC.js +218 -0
- package/dist/chunk-VLJJHDAC.js.map +1 -0
- package/dist/chunk-WMOI7FN3.js +182 -0
- package/dist/chunk-WMOI7FN3.js.map +1 -0
- package/dist/chunk-ZTX4PFFT.js +520 -0
- package/dist/chunk-ZTX4PFFT.js.map +1 -0
- package/dist/doc/index.d.ts +5 -10
- package/dist/doc/index.js +145 -7
- package/dist/doc/index.js.map +1 -1
- package/dist/generate/index.d.ts +99 -7
- package/dist/generate/index.js +16 -3
- package/dist/generate/index.js.map +1 -1
- package/dist/icons/index.d.ts +87 -0
- package/dist/icons/index.js +17 -0
- package/dist/icons/index.js.map +1 -0
- package/dist/imageEdit/index.d.ts +216 -0
- package/dist/imageEdit/index.js +54 -0
- package/dist/imageEdit/index.js.map +1 -0
- package/dist/index.d.ts +21 -9
- package/dist/index.js +461 -8
- package/dist/index.js.map +1 -1
- package/dist/jsonForm/index.d.ts +191 -0
- package/dist/jsonForm/index.js +27 -0
- package/dist/jsonForm/index.js.map +1 -0
- package/dist/markdown/index.d.ts +265 -19
- package/dist/markdown/index.js +39 -33
- package/dist/markdown/index.js.map +1 -1
- package/dist/random/index.d.ts +51 -2
- package/dist/random/index.js +9 -1
- package/dist/random/index.js.map +1 -1
- package/dist/recommend/index.d.ts +42 -0
- package/dist/recommend/index.js +10 -0
- package/dist/recommend/index.js.map +1 -0
- package/dist/schemas/index.d.ts +188 -10
- package/dist/schemas/index.js +120 -7
- package/dist/schemas/index.js.map +1 -1
- package/dist/spatial/index.d.ts +111 -3
- package/dist/spatial/index.js +24 -2
- package/dist/spatial/index.js.map +1 -1
- package/dist/storage/index.d.ts +159 -9
- package/dist/storage/index.js +20 -5
- package/dist/storage/index.js.map +1 -1
- package/dist/story/index.d.ts +53 -13
- package/dist/story/index.js +12 -4
- package/dist/themeLibrary-BHjJ0Qx6.d.ts +40 -0
- package/dist/timing/index.d.ts +109 -4
- package/dist/timing/index.js +23 -2
- package/dist/timing/index.js.map +1 -1
- package/dist/transform/index.d.ts +175 -6
- package/dist/transform/index.js +490 -3
- package/dist/transform/index.js.map +1 -1
- package/dist/{types-CkNttVSF.d.ts → types-BdZkdJ3z.d.ts} +17 -2
- package/dist/types-D98li1gV.d.ts +93 -0
- package/dist/versions/index.d.ts +128 -0
- package/dist/versions/index.js +35 -0
- package/dist/versions/index.js.map +1 -0
- package/package.json +28 -1
- package/src/__tests__/colorUtils.test.ts +112 -0
- package/src/__tests__/getLayers.test.ts +32 -7
- package/src/__tests__/iconResolve.test.ts +122 -0
- package/src/__tests__/imageEditState.test.ts +74 -0
- package/src/__tests__/imageEditVersions.test.ts +169 -0
- package/src/__tests__/inferDocumentTitle.test.ts +50 -0
- package/src/__tests__/jsonForm.chooseControl.test.ts +86 -0
- package/src/__tests__/jsonForm.evaluateWhen.test.ts +59 -0
- package/src/__tests__/jsonForm.inferSchema.test.ts +25 -0
- package/src/__tests__/jsonForm.pointer.test.ts +103 -0
- package/src/__tests__/markdownIcons.test.ts +104 -0
- package/src/__tests__/markdownToDoc.test.ts +19 -4
- package/src/__tests__/recommendTemplates.test.ts +183 -0
- package/src/__tests__/scopedContentContainer.test.ts +89 -0
- package/src/__tests__/slideshowGenerator.test.ts +1 -1
- package/src/__tests__/templateAnnotationParse.test.ts +42 -0
- package/src/__tests__/templates.test.ts +93 -9
- package/src/__tests__/themeCompile.test.ts +156 -0
- package/src/__tests__/themeValidator.test.ts +125 -0
- package/src/__tests__/versions.test.ts +415 -0
- package/src/doc/audioMapping.ts +55 -11
- package/src/doc/getLayers.ts +32 -21
- package/src/doc/markdownToDoc.ts +45 -7
- package/src/doc/templates/featureBlock.ts +202 -0
- package/src/doc/templates/fullBleedQuote.ts +13 -0
- package/src/doc/templates/index.ts +50 -16
- package/src/doc/templates/listBlock.ts +24 -5
- package/src/doc/templates/statHighlight.ts +13 -0
- package/src/doc/templates/titleBlock.ts +9 -0
- package/src/doc/utils/themeUtils.ts +8 -7
- package/src/generate/slideshowGenerator.ts +1 -1
- package/src/generate/templateMapper.ts +2 -2
- package/src/icons/iconData.ts +31 -0
- package/src/icons/index.ts +18 -0
- package/src/icons/resolve.ts +139 -0
- package/src/imageEdit/ImageEditVersionManager.ts +73 -0
- package/src/imageEdit/export.ts +326 -0
- package/src/imageEdit/index.ts +71 -0
- package/src/imageEdit/persistence.ts +62 -0
- package/src/imageEdit/state.ts +112 -0
- package/src/imageEdit/versionPaths.ts +57 -0
- package/src/imageEdit/versions.ts +231 -0
- package/src/index.ts +4 -0
- package/src/jsonForm/chooseControl.ts +88 -0
- package/src/jsonForm/evaluateWhen.ts +59 -0
- package/src/jsonForm/index.ts +22 -0
- package/src/jsonForm/inferSchema.ts +25 -0
- package/src/jsonForm/pointer.ts +109 -0
- package/src/jsonForm/types.ts +167 -0
- package/src/markdown/convert.ts +145 -5
- package/src/markdown/index.ts +3 -0
- package/src/markdown/types.ts +19 -2
- package/src/markdown/utils.ts +159 -0
- package/src/recommend/index.ts +16 -0
- package/src/recommend/templates.ts +273 -0
- package/src/schemas/BlockTemplates.ts +50 -7
- package/src/schemas/ImageEditDoc.ts +95 -0
- package/src/schemas/Theme.ts +98 -31
- package/src/schemas/colorUtils.ts +186 -0
- package/src/schemas/fontStacks.ts +257 -0
- package/src/schemas/index.ts +12 -0
- package/src/schemas/themeCompile.ts +203 -0
- package/src/schemas/themeLibrary.ts +18 -597
- package/src/schemas/themeValidator.ts +388 -0
- package/src/schemas/themes/bold.json +48 -0
- package/src/schemas/themes/cinematic.json +54 -0
- package/src/schemas/themes/documentary.json +49 -0
- package/src/schemas/themes/gezellig.json +44 -0
- package/src/schemas/themes/index.ts +53 -0
- package/src/schemas/themes/magazine.json +44 -0
- package/src/schemas/themes/minimalist.json +45 -0
- package/src/schemas/themes/morning-light.json +42 -0
- package/src/schemas/themes/standard-dark.json +50 -0
- package/src/schemas/themes/standard.json +42 -0
- package/src/schemas/themes/tech-dark.json +48 -0
- package/src/schemas/themes/warm-earth.json +50 -0
- package/src/storage/ScopedContentContainer.ts +113 -0
- package/src/storage/index.ts +1 -0
- package/src/versions/DocumentVersionManager.ts +70 -0
- package/src/versions/index.ts +54 -0
- package/src/versions/operations.ts +230 -0
- package/src/versions/paths.ts +66 -0
- package/src/versions/timestamp.ts +48 -0
- package/src/versions/types.ts +91 -0
- package/dist/__tests__/animationUtils.test.d.ts +0 -2
- package/dist/__tests__/animationUtils.test.d.ts.map +0 -1
- package/dist/__tests__/animationUtils.test.js +0 -60
- package/dist/__tests__/animationUtils.test.js.map +0 -1
- package/dist/__tests__/contentContainer.test.d.ts +0 -2
- package/dist/__tests__/contentContainer.test.d.ts.map +0 -1
- package/dist/__tests__/contentContainer.test.js +0 -234
- package/dist/__tests__/contentContainer.test.js.map +0 -1
- package/dist/__tests__/contentExtractor.test.d.ts +0 -2
- package/dist/__tests__/contentExtractor.test.d.ts.map +0 -1
- package/dist/__tests__/contentExtractor.test.js +0 -151
- package/dist/__tests__/contentExtractor.test.js.map +0 -1
- package/dist/__tests__/coverBlock.test.d.ts +0 -2
- package/dist/__tests__/coverBlock.test.d.ts.map +0 -1
- package/dist/__tests__/coverBlock.test.js +0 -197
- package/dist/__tests__/coverBlock.test.js.map +0 -1
- package/dist/__tests__/dataTable.test.d.ts +0 -2
- package/dist/__tests__/dataTable.test.d.ts.map +0 -1
- package/dist/__tests__/dataTable.test.js +0 -110
- package/dist/__tests__/dataTable.test.js.map +0 -1
- package/dist/__tests__/geohash.test.d.ts +0 -2
- package/dist/__tests__/geohash.test.d.ts.map +0 -1
- package/dist/__tests__/geohash.test.js +0 -87
- package/dist/__tests__/geohash.test.js.map +0 -1
- package/dist/__tests__/getLayers.test.d.ts +0 -2
- package/dist/__tests__/getLayers.test.d.ts.map +0 -1
- package/dist/__tests__/getLayers.test.js +0 -167
- package/dist/__tests__/getLayers.test.js.map +0 -1
- package/dist/__tests__/haversine.test.d.ts +0 -2
- package/dist/__tests__/haversine.test.d.ts.map +0 -1
- package/dist/__tests__/haversine.test.js +0 -56
- package/dist/__tests__/haversine.test.js.map +0 -1
- package/dist/__tests__/localForageAdapter.test.d.ts +0 -8
- package/dist/__tests__/localForageAdapter.test.d.ts.map +0 -1
- package/dist/__tests__/localForageAdapter.test.js +0 -130
- package/dist/__tests__/localForageAdapter.test.js.map +0 -1
- package/dist/__tests__/markdown.test.d.ts +0 -2
- package/dist/__tests__/markdown.test.d.ts.map +0 -1
- package/dist/__tests__/markdown.test.js +0 -715
- package/dist/__tests__/markdown.test.js.map +0 -1
- package/dist/__tests__/markdownToDoc.test.d.ts +0 -2
- package/dist/__tests__/markdownToDoc.test.d.ts.map +0 -1
- package/dist/__tests__/markdownToDoc.test.js +0 -447
- package/dist/__tests__/markdownToDoc.test.js.map +0 -1
- package/dist/__tests__/seededRandom.test.d.ts +0 -2
- package/dist/__tests__/seededRandom.test.d.ts.map +0 -1
- package/dist/__tests__/seededRandom.test.js +0 -125
- package/dist/__tests__/seededRandom.test.js.map +0 -1
- package/dist/__tests__/slideshowGenerator.test.d.ts +0 -2
- package/dist/__tests__/slideshowGenerator.test.d.ts.map +0 -1
- package/dist/__tests__/slideshowGenerator.test.js +0 -114
- package/dist/__tests__/slideshowGenerator.test.js.map +0 -1
- package/dist/__tests__/storage.test.d.ts +0 -2
- package/dist/__tests__/storage.test.d.ts.map +0 -1
- package/dist/__tests__/storage.test.js +0 -51
- package/dist/__tests__/storage.test.js.map +0 -1
- package/dist/__tests__/surface.test.d.ts +0 -2
- package/dist/__tests__/surface.test.d.ts.map +0 -1
- package/dist/__tests__/surface.test.js +0 -57
- package/dist/__tests__/surface.test.js.map +0 -1
- package/dist/__tests__/templates.test.d.ts +0 -2
- package/dist/__tests__/templates.test.d.ts.map +0 -1
- package/dist/__tests__/templates.test.js +0 -217
- package/dist/__tests__/templates.test.js.map +0 -1
- package/dist/__tests__/timing.test.d.ts +0 -2
- package/dist/__tests__/timing.test.d.ts.map +0 -1
- package/dist/__tests__/timing.test.js +0 -131
- package/dist/__tests__/timing.test.js.map +0 -1
- package/dist/chunk-6BAVXFYC.js.map +0 -1
- package/dist/chunk-7GF6RDED.js +0 -82
- package/dist/chunk-7GF6RDED.js.map +0 -1
- package/dist/chunk-CYQHG27J.js.map +0 -1
- package/dist/chunk-F22AE6BY.js +0 -121
- package/dist/chunk-F22AE6BY.js.map +0 -1
- package/dist/chunk-FIQR4AJG.js +0 -517
- package/dist/chunk-FIQR4AJG.js.map +0 -1
- package/dist/chunk-OASJKWO5.js.map +0 -1
- package/dist/chunk-QM5PKNPW.js.map +0 -1
- package/dist/chunk-TSG2OSE6.js.map +0 -1
- package/dist/doc/audioMapping.d.ts +0 -36
- package/dist/doc/audioMapping.d.ts.map +0 -1
- package/dist/doc/audioMapping.js +0 -315
- package/dist/doc/audioMapping.js.map +0 -1
- package/dist/doc/docToMarkdown.d.ts +0 -41
- package/dist/doc/docToMarkdown.d.ts.map +0 -1
- package/dist/doc/docToMarkdown.js +0 -88
- package/dist/doc/docToMarkdown.js.map +0 -1
- package/dist/doc/getLayers.d.ts +0 -65
- package/dist/doc/getLayers.d.ts.map +0 -1
- package/dist/doc/getLayers.js +0 -103
- package/dist/doc/getLayers.js.map +0 -1
- package/dist/doc/index.d.ts.map +0 -1
- package/dist/doc/markdownToDoc.d.ts +0 -75
- package/dist/doc/markdownToDoc.d.ts.map +0 -1
- package/dist/doc/markdownToDoc.js +0 -398
- package/dist/doc/markdownToDoc.js.map +0 -1
- package/dist/doc/templates/accentImage.d.ts +0 -59
- package/dist/doc/templates/accentImage.d.ts.map +0 -1
- package/dist/doc/templates/accentImage.js +0 -275
- package/dist/doc/templates/accentImage.js.map +0 -1
- package/dist/doc/templates/captionUtils.d.ts +0 -20
- package/dist/doc/templates/captionUtils.d.ts.map +0 -1
- package/dist/doc/templates/captionUtils.js +0 -29
- package/dist/doc/templates/captionUtils.js.map +0 -1
- package/dist/doc/templates/comparisonBar.d.ts +0 -13
- package/dist/doc/templates/comparisonBar.d.ts.map +0 -1
- package/dist/doc/templates/comparisonBar.js +0 -145
- package/dist/doc/templates/comparisonBar.js.map +0 -1
- package/dist/doc/templates/coverBlock.d.ts +0 -51
- package/dist/doc/templates/coverBlock.d.ts.map +0 -1
- package/dist/doc/templates/coverBlock.js +0 -153
- package/dist/doc/templates/coverBlock.js.map +0 -1
- package/dist/doc/templates/dataTable.d.ts +0 -14
- package/dist/doc/templates/dataTable.d.ts.map +0 -1
- package/dist/doc/templates/dataTable.js +0 -73
- package/dist/doc/templates/dataTable.js.map +0 -1
- package/dist/doc/templates/dateEvent.d.ts +0 -15
- package/dist/doc/templates/dateEvent.d.ts.map +0 -1
- package/dist/doc/templates/dateEvent.js +0 -114
- package/dist/doc/templates/dateEvent.js.map +0 -1
- package/dist/doc/templates/definitionCard.d.ts +0 -13
- package/dist/doc/templates/definitionCard.d.ts.map +0 -1
- package/dist/doc/templates/definitionCard.js +0 -117
- package/dist/doc/templates/definitionCard.js.map +0 -1
- package/dist/doc/templates/factCard.d.ts +0 -15
- package/dist/doc/templates/factCard.d.ts.map +0 -1
- package/dist/doc/templates/factCard.js +0 -104
- package/dist/doc/templates/factCard.js.map +0 -1
- package/dist/doc/templates/fullBleedQuote.d.ts +0 -13
- package/dist/doc/templates/fullBleedQuote.d.ts.map +0 -1
- package/dist/doc/templates/fullBleedQuote.js +0 -55
- package/dist/doc/templates/fullBleedQuote.js.map +0 -1
- package/dist/doc/templates/imageWithCaption.d.ts +0 -13
- package/dist/doc/templates/imageWithCaption.d.ts.map +0 -1
- package/dist/doc/templates/imageWithCaption.js +0 -146
- package/dist/doc/templates/imageWithCaption.js.map +0 -1
- package/dist/doc/templates/index.d.ts +0 -105
- package/dist/doc/templates/index.d.ts.map +0 -1
- package/dist/doc/templates/index.js +0 -445
- package/dist/doc/templates/index.js.map +0 -1
- package/dist/doc/templates/listBlock.d.ts +0 -13
- package/dist/doc/templates/listBlock.d.ts.map +0 -1
- package/dist/doc/templates/listBlock.js +0 -92
- package/dist/doc/templates/listBlock.js.map +0 -1
- package/dist/doc/templates/mapBlock.d.ts +0 -17
- package/dist/doc/templates/mapBlock.d.ts.map +0 -1
- package/dist/doc/templates/mapBlock.js +0 -115
- package/dist/doc/templates/mapBlock.js.map +0 -1
- package/dist/doc/templates/persistentLayers.d.ts +0 -39
- package/dist/doc/templates/persistentLayers.d.ts.map +0 -1
- package/dist/doc/templates/persistentLayers.js +0 -447
- package/dist/doc/templates/persistentLayers.js.map +0 -1
- package/dist/doc/templates/photoGrid.d.ts +0 -20
- package/dist/doc/templates/photoGrid.d.ts.map +0 -1
- package/dist/doc/templates/photoGrid.js +0 -175
- package/dist/doc/templates/photoGrid.js.map +0 -1
- package/dist/doc/templates/pullQuote.d.ts +0 -13
- package/dist/doc/templates/pullQuote.d.ts.map +0 -1
- package/dist/doc/templates/pullQuote.js +0 -113
- package/dist/doc/templates/pullQuote.js.map +0 -1
- package/dist/doc/templates/quoteBlock.d.ts +0 -15
- package/dist/doc/templates/quoteBlock.d.ts.map +0 -1
- package/dist/doc/templates/quoteBlock.js +0 -101
- package/dist/doc/templates/quoteBlock.js.map +0 -1
- package/dist/doc/templates/sectionHeader.d.ts +0 -14
- package/dist/doc/templates/sectionHeader.d.ts.map +0 -1
- package/dist/doc/templates/sectionHeader.js +0 -97
- package/dist/doc/templates/sectionHeader.js.map +0 -1
- package/dist/doc/templates/statHighlight.d.ts +0 -15
- package/dist/doc/templates/statHighlight.d.ts.map +0 -1
- package/dist/doc/templates/statHighlight.js +0 -102
- package/dist/doc/templates/statHighlight.js.map +0 -1
- package/dist/doc/templates/titleBlock.d.ts +0 -13
- package/dist/doc/templates/titleBlock.d.ts.map +0 -1
- package/dist/doc/templates/titleBlock.js +0 -95
- package/dist/doc/templates/titleBlock.js.map +0 -1
- package/dist/doc/templates/twoColumn.d.ts +0 -15
- package/dist/doc/templates/twoColumn.d.ts.map +0 -1
- package/dist/doc/templates/twoColumn.js +0 -225
- package/dist/doc/templates/twoColumn.js.map +0 -1
- package/dist/doc/templates/videoPullQuote.d.ts +0 -20
- package/dist/doc/templates/videoPullQuote.d.ts.map +0 -1
- package/dist/doc/templates/videoPullQuote.js +0 -122
- package/dist/doc/templates/videoPullQuote.js.map +0 -1
- package/dist/doc/templates/videoWithCaption.d.ts +0 -20
- package/dist/doc/templates/videoWithCaption.d.ts.map +0 -1
- package/dist/doc/templates/videoWithCaption.js +0 -107
- package/dist/doc/templates/videoWithCaption.js.map +0 -1
- package/dist/doc/utils/animationUtils.d.ts +0 -34
- package/dist/doc/utils/animationUtils.d.ts.map +0 -1
- package/dist/doc/utils/animationUtils.js +0 -110
- package/dist/doc/utils/animationUtils.js.map +0 -1
- package/dist/doc/utils/themeUtils.d.ts +0 -64
- package/dist/doc/utils/themeUtils.d.ts.map +0 -1
- package/dist/doc/utils/themeUtils.js +0 -135
- package/dist/doc/utils/themeUtils.js.map +0 -1
- package/dist/generate/contentExtractor.d.ts +0 -112
- package/dist/generate/contentExtractor.d.ts.map +0 -1
- package/dist/generate/contentExtractor.js +0 -638
- package/dist/generate/contentExtractor.js.map +0 -1
- package/dist/generate/index.d.ts.map +0 -1
- package/dist/generate/slideshowGenerator.d.ts +0 -72
- package/dist/generate/slideshowGenerator.d.ts.map +0 -1
- package/dist/generate/slideshowGenerator.js +0 -159
- package/dist/generate/slideshowGenerator.js.map +0 -1
- package/dist/generate/templateMapper.d.ts +0 -23
- package/dist/generate/templateMapper.d.ts.map +0 -1
- package/dist/generate/templateMapper.js +0 -114
- package/dist/generate/templateMapper.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/markdown/convert.d.ts +0 -71
- package/dist/markdown/convert.d.ts.map +0 -1
- package/dist/markdown/convert.js +0 -845
- package/dist/markdown/convert.js.map +0 -1
- package/dist/markdown/htmlParse.d.ts +0 -26
- package/dist/markdown/htmlParse.d.ts.map +0 -1
- package/dist/markdown/htmlParse.js +0 -195
- package/dist/markdown/htmlParse.js.map +0 -1
- package/dist/markdown/index.d.ts.map +0 -1
- package/dist/markdown/parse.d.ts +0 -27
- package/dist/markdown/parse.d.ts.map +0 -1
- package/dist/markdown/parse.js +0 -90
- package/dist/markdown/parse.js.map +0 -1
- package/dist/markdown/stringify.d.ts +0 -32
- package/dist/markdown/stringify.d.ts.map +0 -1
- package/dist/markdown/stringify.js +0 -108
- package/dist/markdown/stringify.js.map +0 -1
- package/dist/markdown/types.d.ts +0 -516
- package/dist/markdown/types.d.ts.map +0 -1
- package/dist/markdown/types.js +0 -17
- package/dist/markdown/types.js.map +0 -1
- package/dist/markdown/utils.d.ts +0 -78
- package/dist/markdown/utils.d.ts.map +0 -1
- package/dist/markdown/utils.js +0 -153
- package/dist/markdown/utils.js.map +0 -1
- package/dist/random/SeededRandom.d.ts +0 -50
- package/dist/random/SeededRandom.d.ts.map +0 -1
- package/dist/random/SeededRandom.js +0 -115
- package/dist/random/SeededRandom.js.map +0 -1
- package/dist/random/index.d.ts.map +0 -1
- package/dist/schemas/BlockTemplates.d.ts +0 -600
- package/dist/schemas/BlockTemplates.d.ts.map +0 -1
- package/dist/schemas/BlockTemplates.js +0 -72
- package/dist/schemas/BlockTemplates.js.map +0 -1
- package/dist/schemas/Doc.d.ts +0 -503
- package/dist/schemas/Doc.d.ts.map +0 -1
- package/dist/schemas/Doc.js +0 -90
- package/dist/schemas/Doc.js.map +0 -1
- package/dist/schemas/LayoutStrategy.d.ts +0 -84
- package/dist/schemas/LayoutStrategy.d.ts.map +0 -1
- package/dist/schemas/LayoutStrategy.js +0 -123
- package/dist/schemas/LayoutStrategy.js.map +0 -1
- package/dist/schemas/MediaProvider.d.ts +0 -65
- package/dist/schemas/MediaProvider.d.ts.map +0 -1
- package/dist/schemas/MediaProvider.js +0 -13
- package/dist/schemas/MediaProvider.js.map +0 -1
- package/dist/schemas/Theme.d.ts +0 -194
- package/dist/schemas/Theme.d.ts.map +0 -1
- package/dist/schemas/Theme.js +0 -97
- package/dist/schemas/Theme.js.map +0 -1
- package/dist/schemas/Types.d.ts +0 -14
- package/dist/schemas/Types.d.ts.map +0 -1
- package/dist/schemas/Types.js +0 -5
- package/dist/schemas/Types.js.map +0 -1
- package/dist/schemas/Viewport.d.ts +0 -74
- package/dist/schemas/Viewport.d.ts.map +0 -1
- package/dist/schemas/Viewport.js +0 -63
- package/dist/schemas/Viewport.js.map +0 -1
- package/dist/schemas/index.d.ts.map +0 -1
- package/dist/schemas/themeLibrary.d.ts +0 -41
- package/dist/schemas/themeLibrary.d.ts.map +0 -1
- package/dist/schemas/themeLibrary.js +0 -597
- package/dist/schemas/themeLibrary.js.map +0 -1
- package/dist/spatial/Geohash.d.ts +0 -85
- package/dist/spatial/Geohash.d.ts.map +0 -1
- package/dist/spatial/Geohash.js +0 -142
- package/dist/spatial/Geohash.js.map +0 -1
- package/dist/spatial/Haversine.d.ts +0 -24
- package/dist/spatial/Haversine.d.ts.map +0 -1
- package/dist/spatial/Haversine.js +0 -48
- package/dist/spatial/Haversine.js.map +0 -1
- package/dist/spatial/index.d.ts.map +0 -1
- package/dist/storage/ContentContainer.d.ts +0 -87
- package/dist/storage/ContentContainer.d.ts.map +0 -1
- package/dist/storage/ContentContainer.js +0 -131
- package/dist/storage/ContentContainer.js.map +0 -1
- package/dist/storage/LocalForageAdapter.d.ts +0 -36
- package/dist/storage/LocalForageAdapter.d.ts.map +0 -1
- package/dist/storage/LocalForageAdapter.js +0 -82
- package/dist/storage/LocalForageAdapter.js.map +0 -1
- package/dist/storage/LocalStorageAdapter.d.ts +0 -16
- package/dist/storage/LocalStorageAdapter.d.ts.map +0 -1
- package/dist/storage/LocalStorageAdapter.js +0 -50
- package/dist/storage/LocalStorageAdapter.js.map +0 -1
- package/dist/storage/MediaProviderFromContainer.d.ts +0 -21
- package/dist/storage/MediaProviderFromContainer.d.ts.map +0 -1
- package/dist/storage/MediaProviderFromContainer.js +0 -79
- package/dist/storage/MediaProviderFromContainer.js.map +0 -1
- package/dist/storage/MemoryStorageAdapter.d.ts +0 -15
- package/dist/storage/MemoryStorageAdapter.d.ts.map +0 -1
- package/dist/storage/MemoryStorageAdapter.js +0 -34
- package/dist/storage/MemoryStorageAdapter.js.map +0 -1
- package/dist/storage/Storage.d.ts +0 -23
- package/dist/storage/Storage.d.ts.map +0 -1
- package/dist/storage/Storage.js +0 -9
- package/dist/storage/Storage.js.map +0 -1
- package/dist/storage/index.d.ts.map +0 -1
- package/dist/themeLibrary-CEo2D4b6.d.ts +0 -44
- package/dist/timing/index.d.ts.map +0 -1
- package/dist/timing/narrationTiming.d.ts +0 -54
- package/dist/timing/narrationTiming.d.ts.map +0 -1
- package/dist/timing/narrationTiming.js +0 -190
- package/dist/timing/narrationTiming.js.map +0 -1
- package/dist/timing/readingTime.d.ts +0 -54
- package/dist/timing/readingTime.d.ts.map +0 -1
- package/dist/timing/readingTime.js +0 -49
- package/dist/timing/readingTime.js.map +0 -1
- package/dist/transform/applyTransform.d.ts +0 -22
- package/dist/transform/applyTransform.d.ts.map +0 -1
- package/dist/transform/applyTransform.js +0 -68
- package/dist/transform/applyTransform.js.map +0 -1
- package/dist/transform/blockAnalyzer.d.ts +0 -48
- package/dist/transform/blockAnalyzer.d.ts.map +0 -1
- package/dist/transform/blockAnalyzer.js +0 -135
- package/dist/transform/blockAnalyzer.js.map +0 -1
- package/dist/transform/index.d.ts.map +0 -1
- package/dist/transform/registry.d.ts +0 -19
- package/dist/transform/registry.d.ts.map +0 -1
- package/dist/transform/registry.js +0 -41
- package/dist/transform/registry.js.map +0 -1
- package/dist/transform/styles/dataDriven.d.ts +0 -4
- package/dist/transform/styles/dataDriven.d.ts.map +0 -1
- package/dist/transform/styles/dataDriven.js +0 -15
- package/dist/transform/styles/dataDriven.js.map +0 -1
- package/dist/transform/styles/documentary.d.ts +0 -4
- package/dist/transform/styles/documentary.d.ts.map +0 -1
- package/dist/transform/styles/documentary.js +0 -15
- package/dist/transform/styles/documentary.js.map +0 -1
- package/dist/transform/styles/index.d.ts +0 -6
- package/dist/transform/styles/index.d.ts.map +0 -1
- package/dist/transform/styles/index.js +0 -6
- package/dist/transform/styles/index.js.map +0 -1
- package/dist/transform/styles/magazine.d.ts +0 -4
- package/dist/transform/styles/magazine.d.ts.map +0 -1
- package/dist/transform/styles/magazine.js +0 -15
- package/dist/transform/styles/magazine.js.map +0 -1
- package/dist/transform/styles/minimal.d.ts +0 -4
- package/dist/transform/styles/minimal.d.ts.map +0 -1
- package/dist/transform/styles/minimal.js +0 -15
- package/dist/transform/styles/minimal.js.map +0 -1
- package/dist/transform/styles/narrative.d.ts +0 -4
- package/dist/transform/styles/narrative.d.ts.map +0 -1
- package/dist/transform/styles/narrative.js +0 -15
- package/dist/transform/styles/narrative.js.map +0 -1
- package/dist/transform/templateSelector.d.ts +0 -30
- package/dist/transform/templateSelector.d.ts.map +0 -1
- package/dist/transform/templateSelector.js +0 -218
- package/dist/transform/templateSelector.js.map +0 -1
- package/dist/transform/timingAllocator.d.ts +0 -21
- package/dist/transform/timingAllocator.d.ts.map +0 -1
- package/dist/transform/timingAllocator.js +0 -111
- package/dist/transform/timingAllocator.js.map +0 -1
- package/dist/transform/types.d.ts +0 -86
- package/dist/transform/types.d.ts.map +0 -1
- package/dist/transform/types.js +0 -9
- package/dist/transform/types.js.map +0 -1
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
import { describe, expect, it, beforeEach } from 'vitest';
|
|
2
|
+
import { MemoryContentContainer } from '../storage/ContentContainer';
|
|
3
|
+
import {
|
|
4
|
+
DocumentVersionManager,
|
|
5
|
+
VERSIONS_PREFIX,
|
|
6
|
+
buildVersionPath,
|
|
7
|
+
coalesceVersions,
|
|
8
|
+
formatVersionTimestamp,
|
|
9
|
+
getDocBasename,
|
|
10
|
+
listVersions,
|
|
11
|
+
parseVersionPath,
|
|
12
|
+
parseVersionTimestamp,
|
|
13
|
+
pruneVersions,
|
|
14
|
+
readVersion,
|
|
15
|
+
revertToVersion,
|
|
16
|
+
saveVersion,
|
|
17
|
+
} from '../versions';
|
|
18
|
+
|
|
19
|
+
const decoder = new TextDecoder();
|
|
20
|
+
|
|
21
|
+
async function readUtf8(c: MemoryContentContainer, path: string): Promise<string | null> {
|
|
22
|
+
const data = await c.readFile(path);
|
|
23
|
+
return data ? decoder.decode(data) : null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe('formatVersionTimestamp / parseVersionTimestamp', () => {
|
|
27
|
+
it('round-trips to second precision', () => {
|
|
28
|
+
const d = new Date(Date.UTC(2026, 3, 30, 15, 20, 30));
|
|
29
|
+
const stamp = formatVersionTimestamp(d);
|
|
30
|
+
expect(stamp).toBe('20260430T152030Z');
|
|
31
|
+
const parsed = parseVersionTimestamp(stamp);
|
|
32
|
+
expect(parsed?.getTime()).toBe(d.getTime());
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('returns null for invalid input', () => {
|
|
36
|
+
expect(parseVersionTimestamp('not-a-stamp')).toBeNull();
|
|
37
|
+
expect(parseVersionTimestamp('20260430T152030')).toBeNull(); // missing Z
|
|
38
|
+
expect(parseVersionTimestamp('2026-04-30T15:20:30Z')).toBeNull(); // wrong format
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('orders chronologically when sorted lexicographically', () => {
|
|
42
|
+
const a = formatVersionTimestamp(new Date(Date.UTC(2026, 0, 1, 0, 0, 0)));
|
|
43
|
+
const b = formatVersionTimestamp(new Date(Date.UTC(2026, 0, 1, 0, 0, 1)));
|
|
44
|
+
const c = formatVersionTimestamp(new Date(Date.UTC(2026, 5, 15, 12, 0, 0)));
|
|
45
|
+
expect([c, a, b].sort()).toEqual([a, b, c]);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe('paths', () => {
|
|
50
|
+
it('getDocBasename strips folder + extension', () => {
|
|
51
|
+
expect(getDocBasename('index.md')).toBe('index');
|
|
52
|
+
expect(getDocBasename('subdir/foo.md')).toBe('foo');
|
|
53
|
+
expect(getDocBasename('a/b/c/longname.md')).toBe('longname');
|
|
54
|
+
expect(getDocBasename('noext')).toBe('noext');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('buildVersionPath round-trips through parseVersionPath', () => {
|
|
58
|
+
const d = new Date(Date.UTC(2026, 3, 30, 15, 20, 30));
|
|
59
|
+
const p = buildVersionPath('index', d);
|
|
60
|
+
expect(p).toBe(`${VERSIONS_PREFIX}index.20260430T152030Z.md`);
|
|
61
|
+
const parsed = parseVersionPath(p);
|
|
62
|
+
expect(parsed).not.toBeNull();
|
|
63
|
+
expect(parsed!.basename).toBe('index');
|
|
64
|
+
expect(parsed!.timestamp.getTime()).toBe(d.getTime());
|
|
65
|
+
expect(parsed!.collision).toBe(0);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('buildVersionPath / parseVersionPath handle collision suffix', () => {
|
|
69
|
+
const d = new Date(Date.UTC(2026, 3, 30, 15, 20, 30));
|
|
70
|
+
const p1 = buildVersionPath('index', d, 1);
|
|
71
|
+
expect(p1).toBe(`${VERSIONS_PREFIX}index.20260430T152030Z-2.md`);
|
|
72
|
+
const parsed = parseVersionPath(p1);
|
|
73
|
+
expect(parsed?.collision).toBe(1);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('parseVersionPath returns null for non-version files', () => {
|
|
77
|
+
expect(parseVersionPath('index.md')).toBeNull();
|
|
78
|
+
expect(parseVersionPath('.versions/notes.txt')).toBeNull();
|
|
79
|
+
expect(parseVersionPath('.versions/index.invalidstamp.md')).toBeNull();
|
|
80
|
+
expect(parseVersionPath('.versions/index.20260430T152030Z-1.md')).toBeNull(); // suffix must be >= 2
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe('saveVersion', () => {
|
|
85
|
+
let container: MemoryContentContainer;
|
|
86
|
+
|
|
87
|
+
beforeEach(() => {
|
|
88
|
+
container = new MemoryContentContainer();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('returns no-document when container has no markdown', async () => {
|
|
92
|
+
const result = await saveVersion(container);
|
|
93
|
+
expect(result.saved).toBe(false);
|
|
94
|
+
expect(result.reason).toBe('no-document');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('returns empty when document content is empty', async () => {
|
|
98
|
+
await container.writeDocument('', 'index.md');
|
|
99
|
+
const result = await saveVersion(container);
|
|
100
|
+
expect(result.saved).toBe(false);
|
|
101
|
+
expect(result.reason).toBe('empty');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('writes a snapshot to .versions/ on first save', async () => {
|
|
105
|
+
await container.writeDocument('# hello', 'index.md');
|
|
106
|
+
const now = new Date(Date.UTC(2026, 3, 30, 15, 20, 30));
|
|
107
|
+
const result = await saveVersion(container, { now });
|
|
108
|
+
|
|
109
|
+
expect(result.saved).toBe(true);
|
|
110
|
+
expect(result.reason).toBe('saved');
|
|
111
|
+
expect(result.version?.path).toBe(`${VERSIONS_PREFIX}index.20260430T152030Z.md`);
|
|
112
|
+
expect(result.version?.basename).toBe('index');
|
|
113
|
+
expect(await readUtf8(container, result.version!.path)).toBe('# hello');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('returns unchanged on identical second save', async () => {
|
|
117
|
+
await container.writeDocument('# hello', 'index.md');
|
|
118
|
+
await saveVersion(container, { now: new Date(Date.UTC(2026, 3, 30, 15, 20, 30)) });
|
|
119
|
+
const result = await saveVersion(container, {
|
|
120
|
+
now: new Date(Date.UTC(2026, 3, 30, 15, 20, 31)),
|
|
121
|
+
});
|
|
122
|
+
expect(result.saved).toBe(false);
|
|
123
|
+
expect(result.reason).toBe('unchanged');
|
|
124
|
+
const all = await listVersions(container);
|
|
125
|
+
expect(all).toHaveLength(1);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('writes again when content changes', async () => {
|
|
129
|
+
await container.writeDocument('# hello', 'index.md');
|
|
130
|
+
const r1 = await saveVersion(container, { now: new Date(Date.UTC(2026, 3, 30, 15, 20, 30)) });
|
|
131
|
+
await container.writeDocument('# hello world', 'index.md');
|
|
132
|
+
const r2 = await saveVersion(container, { now: new Date(Date.UTC(2026, 3, 30, 15, 20, 31)) });
|
|
133
|
+
|
|
134
|
+
expect(r1.saved).toBe(true);
|
|
135
|
+
expect(r2.saved).toBe(true);
|
|
136
|
+
expect(r1.version?.path).not.toBe(r2.version?.path);
|
|
137
|
+
expect(await listVersions(container)).toHaveLength(2);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('force: true writes despite equality', async () => {
|
|
141
|
+
await container.writeDocument('# hello', 'index.md');
|
|
142
|
+
await saveVersion(container, { now: new Date(Date.UTC(2026, 3, 30, 15, 20, 30)) });
|
|
143
|
+
const r = await saveVersion(container, {
|
|
144
|
+
force: true,
|
|
145
|
+
now: new Date(Date.UTC(2026, 3, 30, 15, 20, 31)),
|
|
146
|
+
});
|
|
147
|
+
expect(r.saved).toBe(true);
|
|
148
|
+
expect(await listVersions(container)).toHaveLength(2);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('appends a collision suffix when two saves land on the same UTC second', async () => {
|
|
152
|
+
await container.writeDocument('# hello', 'index.md');
|
|
153
|
+
const now = new Date(Date.UTC(2026, 3, 30, 15, 20, 30));
|
|
154
|
+
const r1 = await saveVersion(container, { now });
|
|
155
|
+
// Mutate doc and force-save in the same second.
|
|
156
|
+
await container.writeDocument('# hello v2', 'index.md');
|
|
157
|
+
const r2 = await saveVersion(container, { now });
|
|
158
|
+
|
|
159
|
+
expect(r1.version?.path).toBe(`${VERSIONS_PREFIX}index.20260430T152030Z.md`);
|
|
160
|
+
expect(r2.version?.path).toBe(`${VERSIONS_PREFIX}index.20260430T152030Z-2.md`);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('content option overrides container.readDocument()', async () => {
|
|
164
|
+
await container.writeDocument('# stored', 'index.md');
|
|
165
|
+
const r = await saveVersion(container, {
|
|
166
|
+
content: '# override',
|
|
167
|
+
now: new Date(Date.UTC(2026, 3, 30, 15, 20, 30)),
|
|
168
|
+
});
|
|
169
|
+
expect(r.saved).toBe(true);
|
|
170
|
+
expect(await readUtf8(container, r.version!.path)).toBe('# override');
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('basename option overrides the derived basename', async () => {
|
|
174
|
+
await container.writeDocument('# hi', 'index.md');
|
|
175
|
+
const r = await saveVersion(container, {
|
|
176
|
+
basename: 'custom',
|
|
177
|
+
now: new Date(Date.UTC(2026, 3, 30, 15, 20, 30)),
|
|
178
|
+
});
|
|
179
|
+
expect(r.version?.path).toBe(`${VERSIONS_PREFIX}custom.20260430T152030Z.md`);
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
describe('listVersions', () => {
|
|
184
|
+
let container: MemoryContentContainer;
|
|
185
|
+
beforeEach(() => {
|
|
186
|
+
container = new MemoryContentContainer();
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('ignores non-matching filenames inside .versions/', async () => {
|
|
190
|
+
await container.writeFile(`${VERSIONS_PREFIX}README.txt`, new TextEncoder().encode('skip me'));
|
|
191
|
+
await container.writeFile(
|
|
192
|
+
`${VERSIONS_PREFIX}index.invalidstamp.md`,
|
|
193
|
+
new TextEncoder().encode('also skip'),
|
|
194
|
+
);
|
|
195
|
+
expect(await listVersions(container)).toEqual([]);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('returns newest-first', async () => {
|
|
199
|
+
await container.writeDocument('a', 'index.md');
|
|
200
|
+
await saveVersion(container, { now: new Date(Date.UTC(2026, 3, 30, 10, 0, 0)) });
|
|
201
|
+
await container.writeDocument('b', 'index.md');
|
|
202
|
+
await saveVersion(container, { now: new Date(Date.UTC(2026, 3, 30, 11, 0, 0)) });
|
|
203
|
+
await container.writeDocument('c', 'index.md');
|
|
204
|
+
await saveVersion(container, { now: new Date(Date.UTC(2026, 3, 30, 12, 0, 0)) });
|
|
205
|
+
|
|
206
|
+
const versions = await listVersions(container);
|
|
207
|
+
expect(versions.map((v) => v.timestamp.getUTCHours())).toEqual([12, 11, 10]);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('basename filter excludes other docs', async () => {
|
|
211
|
+
await container.writeFile(
|
|
212
|
+
`${VERSIONS_PREFIX}foo.20260430T100000Z.md`,
|
|
213
|
+
new TextEncoder().encode('foo'),
|
|
214
|
+
);
|
|
215
|
+
await container.writeFile(
|
|
216
|
+
`${VERSIONS_PREFIX}bar.20260430T100000Z.md`,
|
|
217
|
+
new TextEncoder().encode('bar'),
|
|
218
|
+
);
|
|
219
|
+
expect((await listVersions(container, 'foo')).map((v) => v.basename)).toEqual(['foo']);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('orders collision-suffix snapshots newest-first within the same UTC second', async () => {
|
|
223
|
+
// Three saves at the same UTC second produce paths with collisions 0, 1, 2.
|
|
224
|
+
// The lex order of the suffix bytes (`-` < `.`) does not match the write
|
|
225
|
+
// order, so the comparator must use the parsed collision number.
|
|
226
|
+
await container.writeDocument('a', 'index.md');
|
|
227
|
+
const now = new Date(Date.UTC(2026, 3, 30, 15, 20, 30));
|
|
228
|
+
await saveVersion(container, { now });
|
|
229
|
+
await container.writeDocument('b', 'index.md');
|
|
230
|
+
await saveVersion(container, { now });
|
|
231
|
+
await container.writeDocument('c', 'index.md');
|
|
232
|
+
await saveVersion(container, { now });
|
|
233
|
+
|
|
234
|
+
const versions = await listVersions(container);
|
|
235
|
+
expect(versions.map((v) => v.collision)).toEqual([2, 1, 0]);
|
|
236
|
+
expect(versions[0]!.path).toBe(`${VERSIONS_PREFIX}index.20260430T152030Z-3.md`);
|
|
237
|
+
expect(versions[2]!.path).toBe(`${VERSIONS_PREFIX}index.20260430T152030Z.md`);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it('saveVersion dedup compares against the actual newest snapshot after a collision', async () => {
|
|
241
|
+
// Regression: when a collision suffix exists, dedup must read the
|
|
242
|
+
// collision file (the real "latest"), not the earlier no-suffix file.
|
|
243
|
+
await container.writeDocument('first', 'index.md');
|
|
244
|
+
const now = new Date(Date.UTC(2026, 3, 30, 15, 20, 30));
|
|
245
|
+
await saveVersion(container, { now });
|
|
246
|
+
await container.writeDocument('second', 'index.md');
|
|
247
|
+
await saveVersion(container, { now });
|
|
248
|
+
|
|
249
|
+
// Latest content on disk is "second"; saving it again should be a no-op.
|
|
250
|
+
const r = await saveVersion(container, { now: new Date(Date.UTC(2026, 3, 30, 15, 20, 31)) });
|
|
251
|
+
expect(r.saved).toBe(false);
|
|
252
|
+
expect(r.reason).toBe('unchanged');
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
describe('readVersion', () => {
|
|
257
|
+
it('accepts both Version object and string path', async () => {
|
|
258
|
+
const container = new MemoryContentContainer();
|
|
259
|
+
await container.writeDocument('# content', 'index.md');
|
|
260
|
+
const r = await saveVersion(container, { now: new Date(Date.UTC(2026, 3, 30, 15, 20, 30)) });
|
|
261
|
+
expect(await readVersion(container, r.version!)).toBe('# content');
|
|
262
|
+
expect(await readVersion(container, r.version!.path)).toBe('# content');
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it('returns null for missing snapshot', async () => {
|
|
266
|
+
const container = new MemoryContentContainer();
|
|
267
|
+
expect(await readVersion(container, '.versions/missing.20260430T100000Z.md')).toBeNull();
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
describe('revertToVersion', () => {
|
|
272
|
+
let container: MemoryContentContainer;
|
|
273
|
+
beforeEach(() => {
|
|
274
|
+
container = new MemoryContentContainer();
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it('writes a snapshot of the current doc before reverting (default)', async () => {
|
|
278
|
+
await container.writeDocument('# v1', 'index.md');
|
|
279
|
+
const r1 = await saveVersion(container, { now: new Date(Date.UTC(2026, 3, 30, 10, 0, 0)) });
|
|
280
|
+
await container.writeDocument('# v2 in flight', 'index.md');
|
|
281
|
+
|
|
282
|
+
const result = await revertToVersion(container, r1.version!);
|
|
283
|
+
expect(result.reverted).toBe(true);
|
|
284
|
+
expect(result.snapshotted).not.toBeNull();
|
|
285
|
+
expect(await container.readDocument()).toBe('# v1');
|
|
286
|
+
// We should now have two snapshots: the original + the just-snapshotted v2.
|
|
287
|
+
expect(await listVersions(container)).toHaveLength(2);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it('snapshotCurrent: false skips the safety snapshot', async () => {
|
|
291
|
+
await container.writeDocument('# v1', 'index.md');
|
|
292
|
+
const r1 = await saveVersion(container, { now: new Date(Date.UTC(2026, 3, 30, 10, 0, 0)) });
|
|
293
|
+
await container.writeDocument('# v2 in flight', 'index.md');
|
|
294
|
+
|
|
295
|
+
const result = await revertToVersion(container, r1.version!, { snapshotCurrent: false });
|
|
296
|
+
expect(result.snapshotted).toBeNull();
|
|
297
|
+
expect(await container.readDocument()).toBe('# v1');
|
|
298
|
+
expect(await listVersions(container)).toHaveLength(1);
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it('returns reverted: false for missing snapshot', async () => {
|
|
302
|
+
const result = await revertToVersion(container, '.versions/missing.20260430T100000Z.md');
|
|
303
|
+
expect(result.reverted).toBe(false);
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
describe('pruneVersions', () => {
|
|
308
|
+
let container: MemoryContentContainer;
|
|
309
|
+
|
|
310
|
+
async function seed(times: number): Promise<void> {
|
|
311
|
+
for (let i = 0; i < times; i++) {
|
|
312
|
+
await container.writeDocument(`# v${i}`, 'index.md');
|
|
313
|
+
await saveVersion(container, { now: new Date(Date.UTC(2026, 3, 30, 10 + i, 0, 0)) });
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
beforeEach(() => {
|
|
318
|
+
container = new MemoryContentContainer();
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it('keep-last-n keeps the n newest', async () => {
|
|
322
|
+
await seed(5);
|
|
323
|
+
const deleted = await pruneVersions(container, { type: 'keep-last-n', n: 3 });
|
|
324
|
+
expect(deleted).toHaveLength(2);
|
|
325
|
+
const remaining = await listVersions(container);
|
|
326
|
+
expect(remaining).toHaveLength(3);
|
|
327
|
+
expect(remaining.map((v) => v.timestamp.getUTCHours())).toEqual([14, 13, 12]);
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it('keep-last-n with n: 0 deletes everything', async () => {
|
|
331
|
+
await seed(3);
|
|
332
|
+
await pruneVersions(container, { type: 'keep-last-n', n: 0 });
|
|
333
|
+
expect(await listVersions(container)).toHaveLength(0);
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
it('older-than deletes only items older than the cutoff', async () => {
|
|
337
|
+
await seed(5);
|
|
338
|
+
const cutoff = new Date(Date.UTC(2026, 3, 30, 12, 0, 0));
|
|
339
|
+
const deleted = await pruneVersions(container, { type: 'older-than', date: cutoff });
|
|
340
|
+
// hours 10 and 11 are older than 12 → deleted.
|
|
341
|
+
expect(deleted).toHaveLength(2);
|
|
342
|
+
const remaining = await listVersions(container);
|
|
343
|
+
expect(remaining.map((v) => v.timestamp.getUTCHours())).toEqual([14, 13, 12]);
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
it('predicate keeps items the predicate accepts', async () => {
|
|
347
|
+
await seed(4);
|
|
348
|
+
const deleted = await pruneVersions(container, {
|
|
349
|
+
type: 'predicate',
|
|
350
|
+
keep: (v) => v.timestamp.getUTCHours() % 2 === 0,
|
|
351
|
+
});
|
|
352
|
+
// 10 and 12 are even (kept); 11 and 13 are odd (deleted).
|
|
353
|
+
expect(deleted).toHaveLength(2);
|
|
354
|
+
expect((await listVersions(container)).map((v) => v.timestamp.getUTCHours())).toEqual([12, 10]);
|
|
355
|
+
});
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
describe('coalesceVersions', () => {
|
|
359
|
+
it('collapses snapshots within the window', async () => {
|
|
360
|
+
const container = new MemoryContentContainer();
|
|
361
|
+
// Three saves: t=0, t=30s (within window), t=2min (outside window).
|
|
362
|
+
await container.writeDocument('a', 'index.md');
|
|
363
|
+
await saveVersion(container, { now: new Date(Date.UTC(2026, 3, 30, 10, 0, 0)) });
|
|
364
|
+
await container.writeDocument('b', 'index.md');
|
|
365
|
+
await saveVersion(container, { now: new Date(Date.UTC(2026, 3, 30, 10, 0, 30)) });
|
|
366
|
+
await container.writeDocument('c', 'index.md');
|
|
367
|
+
await saveVersion(container, { now: new Date(Date.UTC(2026, 3, 30, 10, 2, 0)) });
|
|
368
|
+
|
|
369
|
+
const deleted = await coalesceVersions(container, { windowMs: 60_000 });
|
|
370
|
+
// Walking newest-first: the (10:00:30, 10:00:00) pair is 30s apart
|
|
371
|
+
// (within the window), so 10:00:00 — the older of the two — is dropped.
|
|
372
|
+
// The (10:02:00, 10:00:30) pair is 90s apart, so 10:00:30 survives.
|
|
373
|
+
expect(deleted).toHaveLength(1);
|
|
374
|
+
expect(deleted[0]!.timestamp.getTime()).toBe(Date.UTC(2026, 3, 30, 10, 0, 0));
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
it('anchors to the last kept snapshot instead of chaining adjacent pairs', async () => {
|
|
378
|
+
// Regression: four snapshots 30s apart with windowMs: 60_000 should keep
|
|
379
|
+
// the newest plus anything beyond 60s of the anchor (here, t=0). Chaining
|
|
380
|
+
// through deleted entries would erase everything but the newest.
|
|
381
|
+
const container = new MemoryContentContainer();
|
|
382
|
+
const base = Date.UTC(2026, 3, 30, 10, 0, 0);
|
|
383
|
+
await container.writeDocument('a', 'index.md');
|
|
384
|
+
await saveVersion(container, { now: new Date(base) });
|
|
385
|
+
await container.writeDocument('b', 'index.md');
|
|
386
|
+
await saveVersion(container, { now: new Date(base + 30_000) });
|
|
387
|
+
await container.writeDocument('c', 'index.md');
|
|
388
|
+
await saveVersion(container, { now: new Date(base + 60_000) });
|
|
389
|
+
await container.writeDocument('d', 'index.md');
|
|
390
|
+
await saveVersion(container, { now: new Date(base + 90_000) });
|
|
391
|
+
|
|
392
|
+
const deleted = await coalesceVersions(container, { windowMs: 60_000 });
|
|
393
|
+
// Anchor=t+90 keeps t+30 dropped (60s ≤ window), keeps t=0 (90s > window
|
|
394
|
+
// from anchor → new anchor). t+60 is also within 60s of t+90 → dropped.
|
|
395
|
+
expect(deleted).toHaveLength(2);
|
|
396
|
+
const remaining = await listVersions(container);
|
|
397
|
+
expect(remaining.map((v) => v.timestamp.getTime() - base)).toEqual([90_000, 0]);
|
|
398
|
+
});
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
describe('DocumentVersionManager', () => {
|
|
402
|
+
it('forwards calls and applies the basename override', async () => {
|
|
403
|
+
const container = new MemoryContentContainer();
|
|
404
|
+
await container.writeDocument('# hi', 'index.md');
|
|
405
|
+
const manager = new DocumentVersionManager(container, { basename: 'custom' });
|
|
406
|
+
const r = await manager.saveVersion({ now: new Date(Date.UTC(2026, 3, 30, 15, 20, 30)) });
|
|
407
|
+
expect(r.version?.path).toBe(`${VERSIONS_PREFIX}custom.20260430T152030Z.md`);
|
|
408
|
+
|
|
409
|
+
const list = await manager.listVersions();
|
|
410
|
+
expect(list).toHaveLength(1);
|
|
411
|
+
|
|
412
|
+
const content = await manager.readVersion(list[0]!);
|
|
413
|
+
expect(content).toBe('# hi');
|
|
414
|
+
});
|
|
415
|
+
});
|
package/src/doc/audioMapping.ts
CHANGED
|
@@ -112,16 +112,55 @@ function flattenForAudio(blocks: Block[]): Block[] {
|
|
|
112
112
|
return result;
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
-
/**
|
|
115
|
+
/**
|
|
116
|
+
* Info about a narration audio file found in the container.
|
|
117
|
+
*
|
|
118
|
+
* Originally limited to MP3, this now also covers WebM/MP4/Ogg/M4A
|
|
119
|
+
* audio files that the browser recorder writes into `audio/`. The type
|
|
120
|
+
* name is retained for backwards compatibility with the surrounding
|
|
121
|
+
* matching logic; the data shape is identical regardless of codec.
|
|
122
|
+
*/
|
|
116
123
|
interface Mp3Info {
|
|
117
|
-
/** Path in the container (e.g., "
|
|
124
|
+
/** Path in the container (e.g., "audio/narration-2026.webm"). */
|
|
118
125
|
path: string;
|
|
119
|
-
/** Filename without path (e.g., "
|
|
126
|
+
/** Filename without path (e.g., "narration-2026.webm"). */
|
|
120
127
|
filename: string;
|
|
121
|
-
/** Parsed timing data (if .timing.json exists) */
|
|
128
|
+
/** Parsed timing data (if .timing.json exists). */
|
|
122
129
|
timing: AudioTimingData | null;
|
|
123
130
|
}
|
|
124
131
|
|
|
132
|
+
/**
|
|
133
|
+
* File extensions that unambiguously denote audio anywhere in the
|
|
134
|
+
* container. These predate the recorder feature.
|
|
135
|
+
*/
|
|
136
|
+
const AUDIO_ONLY_EXTENSIONS = ['.mp3', '.wav', '.ogg', '.m4a'];
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Extensions that could be audio or video — `.webm` and `.mp4` are the
|
|
140
|
+
* containers `MediaRecorder` produces for browser-side narration, but
|
|
141
|
+
* they're also used for video clips. Only treat them as narration when
|
|
142
|
+
* they live under the `audio/` directory the recorder writes to.
|
|
143
|
+
*/
|
|
144
|
+
const AMBIGUOUS_EXTENSIONS = ['.webm', '.mp4'];
|
|
145
|
+
|
|
146
|
+
function isNarrationAudioPath(path: string): boolean {
|
|
147
|
+
const lower = path.toLowerCase();
|
|
148
|
+
if (AUDIO_ONLY_EXTENSIONS.some((ext) => lower.endsWith(ext))) return true;
|
|
149
|
+
if (AMBIGUOUS_EXTENSIONS.some((ext) => lower.endsWith(ext))) {
|
|
150
|
+
return lower.startsWith('audio/');
|
|
151
|
+
}
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Strip a known narration audio extension from a filename. */
|
|
156
|
+
function stripAudioExtension(filename: string): string {
|
|
157
|
+
const lower = filename.toLowerCase();
|
|
158
|
+
for (const ext of [...AUDIO_ONLY_EXTENSIONS, ...AMBIGUOUS_EXTENSIONS]) {
|
|
159
|
+
if (lower.endsWith(ext)) return filename.slice(0, -ext.length);
|
|
160
|
+
}
|
|
161
|
+
return filename;
|
|
162
|
+
}
|
|
163
|
+
|
|
125
164
|
// ── Core Functions ───────────────────────────────────────────────────
|
|
126
165
|
|
|
127
166
|
/**
|
|
@@ -142,12 +181,17 @@ function parseConsolidatedTiming(data: ArrayBuffer): Record<string, AudioTimingD
|
|
|
142
181
|
}
|
|
143
182
|
|
|
144
183
|
/**
|
|
145
|
-
* Discover all
|
|
146
|
-
*
|
|
184
|
+
* Discover all narration audio files in a container and load their
|
|
185
|
+
* timing data. Covers historical `.mp3`/`.wav`/`.ogg`/`.m4a` files
|
|
186
|
+
* anywhere in the container plus `.webm`/`.mp4` files under `audio/`
|
|
187
|
+
* (the destination the browser recorder writes to).
|
|
188
|
+
*
|
|
189
|
+
* Checks for a consolidated timing.json first, then falls back to
|
|
190
|
+
* per-file `.timing.json` sidecars.
|
|
147
191
|
*/
|
|
148
192
|
async function discoverMp3s(container: ContentContainer): Promise<Mp3Info[]> {
|
|
149
193
|
const files = await container.listFiles();
|
|
150
|
-
const
|
|
194
|
+
const audioFiles = files.filter((f) => isNarrationAudioPath(f.path));
|
|
151
195
|
const results: Mp3Info[] = [];
|
|
152
196
|
|
|
153
197
|
// Try to load consolidated timing.json
|
|
@@ -157,15 +201,15 @@ async function discoverMp3s(container: ContentContainer): Promise<Mp3Info[]> {
|
|
|
157
201
|
consolidatedSections = parseConsolidatedTiming(consolidatedData);
|
|
158
202
|
}
|
|
159
203
|
|
|
160
|
-
for (const file of
|
|
204
|
+
for (const file of audioFiles) {
|
|
161
205
|
const filename = file.path.split('/').pop() ?? file.path;
|
|
162
206
|
let timing: AudioTimingData | null = null;
|
|
163
207
|
|
|
164
208
|
// Try consolidated timing first: match section name from filename
|
|
165
209
|
if (consolidatedSections) {
|
|
166
|
-
const
|
|
210
|
+
const audioBase = stripAudioExtension(filename);
|
|
167
211
|
for (const [sectionName, sectionTiming] of Object.entries(consolidatedSections)) {
|
|
168
|
-
if (
|
|
212
|
+
if (audioBase.endsWith(`-${sectionName}`)) {
|
|
169
213
|
timing = sectionTiming;
|
|
170
214
|
break;
|
|
171
215
|
}
|
|
@@ -259,7 +303,7 @@ function autoMatchBlocks(
|
|
|
259
303
|
// Fallback: filename-based matching
|
|
260
304
|
if (score < MIN_CONTENT_SIMILARITY && block.title) {
|
|
261
305
|
const titleSlug = slugify(block.title);
|
|
262
|
-
const fileSlug = slugify(mp3.filename
|
|
306
|
+
const fileSlug = slugify(stripAudioExtension(mp3.filename));
|
|
263
307
|
// Check if the title slug appears in the filename slug
|
|
264
308
|
if (fileSlug.includes(titleSlug) && titleSlug.length >= 3) {
|
|
265
309
|
score = Math.max(score, MIN_FILENAME_SIMILARITY + 0.1);
|
package/src/doc/getLayers.ts
CHANGED
|
@@ -34,7 +34,7 @@ import type { ViewportConfig } from '../schemas/Viewport.js';
|
|
|
34
34
|
import { VIEWPORT_PRESETS } from '../schemas/Viewport.js';
|
|
35
35
|
import { createTemplateContext, isTemplateBlock } from '../schemas/BlockTemplates.js';
|
|
36
36
|
import { DEFAULT_THEME } from '../schemas/themeLibrary.js';
|
|
37
|
-
import { templateRegistry } from './templates/index.js';
|
|
37
|
+
import { templateRegistry, resolveTemplateName } from './templates/index.js';
|
|
38
38
|
import { expandPersistentLayers } from './templates/persistentLayers.js';
|
|
39
39
|
|
|
40
40
|
// ============================================
|
|
@@ -98,29 +98,40 @@ export function getLayers(block: DocBlock, context: RenderContext = {}): Layer[]
|
|
|
98
98
|
return injectPersistentLayers(existingLayers, block, context);
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
-
// 2. Template path: look up and call the template function
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
101
|
+
// 2. Template path: look up and call the template function.
|
|
102
|
+
//
|
|
103
|
+
// Resolve through TEMPLATE_ALIASES so legacy ids (`titleBlock`,
|
|
104
|
+
// `quoteBlock`, `mapBlock`, `listBlock`) hit the registry by their
|
|
105
|
+
// canonical short names. Without this the block-section path renders
|
|
106
|
+
// an empty SVG card: `hasTemplate()` accepts the alias (so
|
|
107
|
+
// `isAnnotatedBlock` returns true and the card wrapper renders), but
|
|
108
|
+
// a raw `block.template in templateRegistry` check below misses it
|
|
109
|
+
// and the layer list comes back empty.
|
|
110
|
+
if (isTemplateBlock(block)) {
|
|
111
|
+
const resolved = resolveTemplateName(block.template);
|
|
112
|
+
if (resolved in templateRegistry) {
|
|
113
|
+
const templateName = resolved as keyof typeof templateRegistry;
|
|
114
|
+
const templateCtx = createTemplateContext(theme, blockIndex, totalBlocks, viewport);
|
|
115
|
+
let layers: Layer[];
|
|
116
|
+
try {
|
|
117
|
+
// Each registry entry accepts its specific TemplateBlock variant; the
|
|
118
|
+
// discriminated union guarantees the shapes match at runtime.
|
|
119
|
+
const templateFn = templateRegistry[templateName] as (
|
|
120
|
+
input: TemplateBlock,
|
|
121
|
+
ctx: TemplateContext,
|
|
122
|
+
) => Layer[];
|
|
123
|
+
layers = templateFn(block, templateCtx);
|
|
124
|
+
if (!Array.isArray(layers)) {
|
|
125
|
+
console.error(`Template ${templateName} did not return an array, got:`, typeof layers);
|
|
126
|
+
layers = [];
|
|
127
|
+
}
|
|
128
|
+
} catch (err: unknown) {
|
|
129
|
+
console.error(`Error expanding template ${templateName}:`, err);
|
|
116
130
|
layers = [];
|
|
117
131
|
}
|
|
118
|
-
} catch (err: unknown) {
|
|
119
|
-
console.error(`Error expanding template ${templateName}:`, err);
|
|
120
|
-
layers = [];
|
|
121
|
-
}
|
|
122
132
|
|
|
123
|
-
|
|
133
|
+
return injectPersistentLayers(layers, block, context);
|
|
134
|
+
}
|
|
124
135
|
}
|
|
125
136
|
|
|
126
137
|
// 3. Fallback: no layers and no known template
|
package/src/doc/markdownToDoc.ts
CHANGED
|
@@ -28,10 +28,11 @@ import type {
|
|
|
28
28
|
MarkdownBlockNode,
|
|
29
29
|
MarkdownHeading,
|
|
30
30
|
MarkdownNode,
|
|
31
|
-
|
|
31
|
+
HtmlNode,
|
|
32
32
|
} from '../markdown/types.js';
|
|
33
33
|
import { extractPlainText } from '../markdown/utils.js';
|
|
34
34
|
import { estimateReadingTime } from '../timing/readingTime.js';
|
|
35
|
+
import { resolveTemplateName } from './templates/index.js';
|
|
35
36
|
|
|
36
37
|
// ============================================
|
|
37
38
|
// Options
|
|
@@ -152,9 +153,13 @@ export function markdownToDoc(markdownDoc: MarkdownDocument, options?: MarkdownT
|
|
|
152
153
|
function makeBlock(heading: MarkdownHeading | null): Block {
|
|
153
154
|
const id = heading ? generateId(heading, headingIndex++) : 'preamble';
|
|
154
155
|
|
|
155
|
-
// Use template from annotation if present, otherwise fall back to default
|
|
156
|
+
// Use template from annotation if present, otherwise fall back to default.
|
|
157
|
+
// Legacy template ids (e.g. `titleBlock`) are normalized to their canonical
|
|
158
|
+
// short form (`title`) so downstream comparisons can rely on the new names
|
|
159
|
+
// without sprinkling alias resolution everywhere.
|
|
156
160
|
const annotation = heading?.templateAnnotation;
|
|
157
|
-
const
|
|
161
|
+
const rawTemplate = annotation?.template ?? (heading ? defaultTemplate : undefined);
|
|
162
|
+
const template = rawTemplate != null ? resolveTemplateName(rawTemplate) : undefined;
|
|
158
163
|
|
|
159
164
|
// Extract heading text so templates (e.g. sectionHeader) that expect a
|
|
160
165
|
// `title` property receive it without having to reach into sourceHeading.
|
|
@@ -434,12 +439,45 @@ function splitIntoSentences(text: string): string[] {
|
|
|
434
439
|
return merged;
|
|
435
440
|
}
|
|
436
441
|
|
|
442
|
+
interface ImageRef {
|
|
443
|
+
url: string;
|
|
444
|
+
alt?: string;
|
|
445
|
+
}
|
|
446
|
+
|
|
437
447
|
/**
|
|
438
|
-
* Walk a
|
|
439
|
-
*
|
|
448
|
+
* Walk a parsed HTML sub-tree depth-first for an `<img>` with a `src`.
|
|
449
|
+
* Used to surface images that the WYSIWYG editor serialized as raw HTML
|
|
450
|
+
* (e.g. when an image was resized — markdown's `` shorthand
|
|
451
|
+
* has no width syntax, so resized images round-trip as `<img src width>`).
|
|
440
452
|
*/
|
|
441
|
-
function
|
|
442
|
-
|
|
453
|
+
function findFirstHtmlImage(nodes: HtmlNode[]): ImageRef | undefined {
|
|
454
|
+
for (const node of nodes) {
|
|
455
|
+
if (node.type !== 'htmlElement') continue;
|
|
456
|
+
if (node.tagName === 'img' && node.attributes.src) {
|
|
457
|
+
return { url: node.attributes.src, alt: node.attributes.alt };
|
|
458
|
+
}
|
|
459
|
+
const nested = findFirstHtmlImage(node.children);
|
|
460
|
+
if (nested) return nested;
|
|
461
|
+
}
|
|
462
|
+
return undefined;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Walk a MarkdownNode tree depth-first to find the first image reference,
|
|
467
|
+
* whether expressed as a markdown image node or a raw HTML `<img>` tag.
|
|
468
|
+
*/
|
|
469
|
+
function findFirstImage(node: MarkdownNode): ImageRef | undefined {
|
|
470
|
+
if (node.type === 'image') {
|
|
471
|
+
const img = node as { url: string; alt?: string };
|
|
472
|
+
return { url: img.url, alt: img.alt };
|
|
473
|
+
}
|
|
474
|
+
if (node.type === 'htmlBlock' || node.type === 'htmlInline') {
|
|
475
|
+
const html = node as { htmlChildren?: HtmlNode[] };
|
|
476
|
+
if (html.htmlChildren) {
|
|
477
|
+
const found = findFirstHtmlImage(html.htmlChildren);
|
|
478
|
+
if (found) return found;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
443
481
|
if ('children' in node && Array.isArray((node as { children?: unknown[] }).children)) {
|
|
444
482
|
for (const child of (node as { children: MarkdownNode[] }).children) {
|
|
445
483
|
const found = findFirstImage(child);
|