@bendyline/squisq 1.2.2 → 1.4.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/dist/{Doc-BfBhiCNr.d.ts → Doc-BeSeH2iX.d.ts} +141 -33
- package/dist/ImageEditDoc-BTIvw0oq.d.ts +84 -0
- package/dist/__tests__/colorUtils.test.d.ts +2 -0
- package/dist/__tests__/colorUtils.test.d.ts.map +1 -0
- package/dist/__tests__/colorUtils.test.js +94 -0
- package/dist/__tests__/colorUtils.test.js.map +1 -0
- package/dist/__tests__/getLayers.test.js +31 -7
- package/dist/__tests__/getLayers.test.js.map +1 -1
- package/dist/__tests__/iconResolve.test.d.ts +2 -0
- package/dist/__tests__/iconResolve.test.d.ts.map +1 -0
- package/dist/__tests__/iconResolve.test.js +99 -0
- package/dist/__tests__/iconResolve.test.js.map +1 -0
- package/dist/__tests__/imageEditState.test.d.ts +2 -0
- package/dist/__tests__/imageEditState.test.d.ts.map +1 -0
- package/dist/__tests__/imageEditState.test.js +61 -0
- package/dist/__tests__/imageEditState.test.js.map +1 -0
- package/dist/__tests__/imageEditVersions.test.d.ts +2 -0
- package/dist/__tests__/imageEditVersions.test.d.ts.map +1 -0
- package/dist/__tests__/imageEditVersions.test.js +141 -0
- package/dist/__tests__/imageEditVersions.test.js.map +1 -0
- package/dist/__tests__/inferDocumentTitle.test.d.ts +2 -0
- package/dist/__tests__/inferDocumentTitle.test.d.ts.map +1 -0
- package/dist/__tests__/inferDocumentTitle.test.js +42 -0
- package/dist/__tests__/inferDocumentTitle.test.js.map +1 -0
- package/dist/__tests__/jsonForm.chooseControl.test.d.ts +2 -0
- package/dist/__tests__/jsonForm.chooseControl.test.d.ts.map +1 -0
- package/dist/__tests__/jsonForm.chooseControl.test.js +74 -0
- package/dist/__tests__/jsonForm.chooseControl.test.js.map +1 -0
- package/dist/__tests__/jsonForm.evaluateWhen.test.d.ts +2 -0
- package/dist/__tests__/jsonForm.evaluateWhen.test.d.ts.map +1 -0
- package/dist/__tests__/jsonForm.evaluateWhen.test.js +51 -0
- package/dist/__tests__/jsonForm.evaluateWhen.test.js.map +1 -0
- package/dist/__tests__/jsonForm.inferSchema.test.d.ts +2 -0
- package/dist/__tests__/jsonForm.inferSchema.test.d.ts.map +1 -0
- package/dist/__tests__/jsonForm.inferSchema.test.js +23 -0
- package/dist/__tests__/jsonForm.inferSchema.test.js.map +1 -0
- package/dist/__tests__/jsonForm.pointer.test.d.ts +2 -0
- package/dist/__tests__/jsonForm.pointer.test.d.ts.map +1 -0
- package/dist/__tests__/jsonForm.pointer.test.js +94 -0
- package/dist/__tests__/jsonForm.pointer.test.js.map +1 -0
- package/dist/__tests__/markdown.test.js +40 -0
- package/dist/__tests__/markdown.test.js.map +1 -1
- package/dist/__tests__/markdownIcons.test.d.ts +2 -0
- package/dist/__tests__/markdownIcons.test.d.ts.map +1 -0
- package/dist/__tests__/markdownIcons.test.js +93 -0
- package/dist/__tests__/markdownIcons.test.js.map +1 -0
- package/dist/__tests__/markdownToDoc.test.js +15 -4
- package/dist/__tests__/markdownToDoc.test.js.map +1 -1
- package/dist/__tests__/recommendTemplates.test.d.ts +2 -0
- package/dist/__tests__/recommendTemplates.test.d.ts.map +1 -0
- package/dist/__tests__/recommendTemplates.test.js +158 -0
- package/dist/__tests__/recommendTemplates.test.js.map +1 -0
- package/dist/__tests__/scopedContentContainer.test.d.ts +2 -0
- package/dist/__tests__/scopedContentContainer.test.d.ts.map +1 -0
- package/dist/__tests__/scopedContentContainer.test.js +74 -0
- package/dist/__tests__/scopedContentContainer.test.js.map +1 -0
- package/dist/__tests__/slideshowGenerator.test.js +1 -1
- package/dist/__tests__/slideshowGenerator.test.js.map +1 -1
- package/dist/__tests__/surface.test.d.ts +2 -0
- package/dist/__tests__/surface.test.d.ts.map +1 -0
- package/dist/__tests__/surface.test.js +57 -0
- package/dist/__tests__/surface.test.js.map +1 -0
- package/dist/__tests__/templateAnnotationParse.test.d.ts +2 -0
- package/dist/__tests__/templateAnnotationParse.test.d.ts.map +1 -0
- package/dist/__tests__/templateAnnotationParse.test.js +42 -0
- package/dist/__tests__/templateAnnotationParse.test.js.map +1 -0
- package/dist/__tests__/templates.test.js +90 -9
- package/dist/__tests__/templates.test.js.map +1 -1
- package/dist/__tests__/themeCompile.test.d.ts +2 -0
- package/dist/__tests__/themeCompile.test.d.ts.map +1 -0
- package/dist/__tests__/themeCompile.test.js +134 -0
- package/dist/__tests__/themeCompile.test.js.map +1 -0
- package/dist/__tests__/themeValidator.test.d.ts +2 -0
- package/dist/__tests__/themeValidator.test.d.ts.map +1 -0
- package/dist/__tests__/themeValidator.test.js +107 -0
- package/dist/__tests__/themeValidator.test.js.map +1 -0
- package/dist/__tests__/versions.test.d.ts +2 -0
- package/dist/__tests__/versions.test.d.ts.map +1 -0
- package/dist/__tests__/versions.test.js +339 -0
- package/dist/__tests__/versions.test.js.map +1 -0
- package/dist/{chunk-CYQHG27J.js → chunk-2TMAWB4Y.js} +74 -1
- package/dist/chunk-2TMAWB4Y.js.map +1 -0
- package/dist/{chunk-LAZELBON.js → chunk-3K5OG6KI.js} +165 -17
- 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-GAQLMORN.js → chunk-KSWJR66U.js} +248 -44
- 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/audioMapping.d.ts.map +1 -1
- package/dist/doc/audioMapping.js +80 -9
- package/dist/doc/audioMapping.js.map +1 -1
- package/dist/doc/getLayers.d.ts.map +1 -1
- package/dist/doc/getLayers.js +29 -18
- package/dist/doc/getLayers.js.map +1 -1
- package/dist/doc/markdownToDoc.d.ts.map +1 -1
- package/dist/doc/markdownToDoc.js +40 -6
- package/dist/doc/markdownToDoc.js.map +1 -1
- package/dist/doc/templates/featureBlock.d.ts +18 -0
- package/dist/doc/templates/featureBlock.d.ts.map +1 -0
- package/dist/doc/templates/featureBlock.js +180 -0
- package/dist/doc/templates/featureBlock.js.map +1 -0
- package/dist/doc/templates/fullBleedQuote.d.ts +12 -0
- package/dist/doc/templates/fullBleedQuote.d.ts.map +1 -1
- package/dist/doc/templates/fullBleedQuote.js +12 -0
- package/dist/doc/templates/fullBleedQuote.js.map +1 -1
- package/dist/doc/templates/index.d.ts +28 -4
- package/dist/doc/templates/index.d.ts.map +1 -1
- package/dist/doc/templates/index.js +45 -12
- package/dist/doc/templates/index.js.map +1 -1
- package/dist/doc/templates/listBlock.d.ts.map +1 -1
- package/dist/doc/templates/listBlock.js +22 -5
- package/dist/doc/templates/listBlock.js.map +1 -1
- package/dist/doc/templates/statHighlight.d.ts +12 -0
- package/dist/doc/templates/statHighlight.d.ts.map +1 -1
- package/dist/doc/templates/statHighlight.js +12 -0
- package/dist/doc/templates/statHighlight.js.map +1 -1
- package/dist/doc/templates/titleBlock.d.ts +11 -0
- package/dist/doc/templates/titleBlock.d.ts.map +1 -1
- package/dist/doc/templates/titleBlock.js +8 -0
- package/dist/doc/templates/titleBlock.js.map +1 -1
- package/dist/doc/utils/themeUtils.d.ts +4 -4
- package/dist/doc/utils/themeUtils.d.ts.map +1 -1
- package/dist/doc/utils/themeUtils.js +8 -7
- package/dist/doc/utils/themeUtils.js.map +1 -1
- package/dist/generate/slideshowGenerator.js +1 -1
- package/dist/generate/slideshowGenerator.js.map +1 -1
- package/dist/generate/templateMapper.js +2 -2
- package/dist/generate/templateMapper.js.map +1 -1
- package/dist/icons/iconData.d.ts +26 -0
- package/dist/icons/iconData.d.ts.map +1 -0
- package/dist/icons/iconData.js +12 -0
- package/dist/icons/iconData.js.map +1 -0
- package/dist/icons/index.d.ts +12 -0
- package/dist/icons/index.d.ts.map +1 -0
- package/dist/icons/index.js +10 -0
- package/dist/icons/index.js.map +1 -0
- package/dist/icons/resolve.d.ts +61 -0
- package/dist/icons/resolve.d.ts.map +1 -0
- package/dist/icons/resolve.js +130 -0
- package/dist/icons/resolve.js.map +1 -0
- package/dist/imageEdit/ImageEditVersionManager.d.ts +31 -0
- package/dist/imageEdit/ImageEditVersionManager.d.ts.map +1 -0
- package/dist/imageEdit/ImageEditVersionManager.js +39 -0
- package/dist/imageEdit/ImageEditVersionManager.js.map +1 -0
- package/dist/imageEdit/export.d.ts +33 -0
- package/dist/imageEdit/export.d.ts.map +1 -0
- package/dist/imageEdit/export.js +253 -0
- package/dist/imageEdit/export.js.map +1 -0
- package/dist/imageEdit/index.d.ts +14 -0
- package/dist/imageEdit/index.d.ts.map +1 -0
- package/dist/imageEdit/index.js +22 -0
- package/dist/imageEdit/index.js.map +1 -0
- package/dist/imageEdit/persistence.d.ts +15 -0
- package/dist/imageEdit/persistence.d.ts.map +1 -0
- package/dist/imageEdit/persistence.js +49 -0
- package/dist/imageEdit/persistence.js.map +1 -0
- package/dist/imageEdit/state.d.ts +42 -0
- package/dist/imageEdit/state.d.ts.map +1 -0
- package/dist/imageEdit/state.js +98 -0
- package/dist/imageEdit/state.js.map +1 -0
- package/dist/imageEdit/versionPaths.d.ts +27 -0
- package/dist/imageEdit/versionPaths.d.ts.map +1 -0
- package/dist/imageEdit/versionPaths.js +57 -0
- package/dist/imageEdit/versionPaths.js.map +1 -0
- package/dist/imageEdit/versions.d.ts +67 -0
- package/dist/imageEdit/versions.d.ts.map +1 -0
- package/dist/imageEdit/versions.js +161 -0
- package/dist/imageEdit/versions.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/jsonForm/chooseControl.d.ts +20 -0
- package/dist/jsonForm/chooseControl.d.ts.map +1 -0
- package/dist/jsonForm/chooseControl.js +82 -0
- package/dist/jsonForm/chooseControl.js.map +1 -0
- package/dist/jsonForm/evaluateWhen.d.ts +12 -0
- package/dist/jsonForm/evaluateWhen.d.ts.map +1 -0
- package/dist/jsonForm/evaluateWhen.js +65 -0
- package/dist/jsonForm/evaluateWhen.js.map +1 -0
- package/dist/jsonForm/index.d.ts +14 -0
- package/dist/jsonForm/index.d.ts.map +1 -0
- package/dist/jsonForm/index.js +12 -0
- package/dist/jsonForm/index.js.map +1 -0
- package/dist/jsonForm/inferSchema.d.ts +12 -0
- package/dist/jsonForm/inferSchema.d.ts.map +1 -0
- package/dist/jsonForm/inferSchema.js +15 -0
- package/dist/jsonForm/inferSchema.js.map +1 -0
- package/dist/jsonForm/pointer.d.ts +26 -0
- package/dist/jsonForm/pointer.d.ts.map +1 -0
- package/dist/jsonForm/pointer.js +102 -0
- package/dist/jsonForm/pointer.js.map +1 -0
- package/dist/jsonForm/types.d.ts +120 -0
- package/dist/jsonForm/types.d.ts.map +1 -0
- package/dist/jsonForm/types.js +10 -0
- package/dist/jsonForm/types.js.map +1 -0
- package/dist/markdown/convert.d.ts +11 -1
- package/dist/markdown/convert.d.ts.map +1 -1
- package/dist/markdown/convert.js +235 -15
- package/dist/markdown/convert.js.map +1 -1
- package/dist/markdown/index.d.ts +2 -2
- package/dist/markdown/index.d.ts.map +1 -1
- package/dist/markdown/index.js +1 -1
- package/dist/markdown/index.js.map +1 -1
- package/dist/markdown/types.d.ts +34 -1
- package/dist/markdown/types.d.ts.map +1 -1
- package/dist/markdown/utils.d.ts +40 -0
- package/dist/markdown/utils.d.ts.map +1 -1
- package/dist/markdown/utils.js +155 -0
- package/dist/markdown/utils.js.map +1 -1
- package/dist/recommend/index.d.ts +16 -0
- package/dist/recommend/index.d.ts.map +1 -0
- package/dist/recommend/index.js +15 -0
- package/dist/recommend/index.js.map +1 -0
- package/dist/recommend/templates.d.ts +39 -0
- package/dist/recommend/templates.d.ts.map +1 -0
- package/dist/recommend/templates.js +235 -0
- package/dist/recommend/templates.js.map +1 -0
- package/dist/schemas/BlockTemplates.d.ts +46 -8
- package/dist/schemas/BlockTemplates.d.ts.map +1 -1
- package/dist/schemas/BlockTemplates.js +3 -3
- package/dist/schemas/BlockTemplates.js.map +1 -1
- package/dist/schemas/ImageEditDoc.d.ts +81 -0
- package/dist/schemas/ImageEditDoc.d.ts.map +1 -0
- package/dist/schemas/ImageEditDoc.js +20 -0
- package/dist/schemas/ImageEditDoc.js.map +1 -0
- package/dist/schemas/Theme.d.ts +93 -23
- package/dist/schemas/Theme.d.ts.map +1 -1
- package/dist/schemas/Theme.js +80 -13
- package/dist/schemas/Theme.js.map +1 -1
- package/dist/schemas/colorUtils.d.ts +51 -0
- package/dist/schemas/colorUtils.d.ts.map +1 -0
- package/dist/schemas/colorUtils.js +154 -0
- package/dist/schemas/colorUtils.js.map +1 -0
- package/dist/schemas/fontStacks.d.ts +57 -0
- package/dist/schemas/fontStacks.d.ts.map +1 -0
- package/dist/schemas/fontStacks.js +227 -0
- package/dist/schemas/fontStacks.js.map +1 -0
- package/dist/schemas/index.d.ts +5 -0
- package/dist/schemas/index.d.ts.map +1 -1
- package/dist/schemas/index.js +4 -0
- package/dist/schemas/index.js.map +1 -1
- package/dist/schemas/themeCompile.d.ts +45 -0
- package/dist/schemas/themeCompile.d.ts.map +1 -0
- package/dist/schemas/themeCompile.js +160 -0
- package/dist/schemas/themeCompile.js.map +1 -0
- package/dist/schemas/themeLibrary.d.ts +11 -15
- package/dist/schemas/themeLibrary.d.ts.map +1 -1
- package/dist/schemas/themeLibrary.js +18 -499
- package/dist/schemas/themeLibrary.js.map +1 -1
- package/dist/schemas/themeValidator.d.ts +26 -0
- package/dist/schemas/themeValidator.d.ts.map +1 -0
- package/dist/schemas/themeValidator.js +352 -0
- package/dist/schemas/themeValidator.js.map +1 -0
- package/dist/schemas/themes/bold.json +48 -0
- package/dist/schemas/themes/cinematic.json +54 -0
- package/dist/schemas/themes/documentary.json +49 -0
- package/dist/schemas/themes/gezellig.json +44 -0
- package/dist/schemas/themes/index.d.ts +26 -0
- package/dist/schemas/themes/index.d.ts.map +1 -0
- package/dist/schemas/themes/index.js +49 -0
- package/dist/schemas/themes/index.js.map +1 -0
- package/dist/schemas/themes/magazine.json +44 -0
- package/dist/schemas/themes/minimalist.json +45 -0
- package/dist/schemas/themes/morning-light.json +42 -0
- package/dist/schemas/themes/standard-dark.json +50 -0
- package/dist/schemas/themes/standard.json +42 -0
- package/dist/schemas/themes/tech-dark.json +48 -0
- package/dist/schemas/themes/warm-earth.json +50 -0
- package/dist/storage/ScopedContentContainer.d.ts +44 -0
- package/dist/storage/ScopedContentContainer.d.ts.map +1 -0
- package/dist/storage/ScopedContentContainer.js +94 -0
- package/dist/storage/ScopedContentContainer.js.map +1 -0
- package/dist/storage/index.d.ts +1 -0
- package/dist/storage/index.d.ts.map +1 -1
- package/dist/storage/index.js +1 -0
- 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/{types-DYAzisMG.d.ts → types-BdZkdJ3z.d.ts} +35 -2
- package/dist/types-D98li1gV.d.ts +93 -0
- package/dist/versions/DocumentVersionManager.d.ts +31 -0
- package/dist/versions/DocumentVersionManager.d.ts.map +1 -0
- package/dist/versions/DocumentVersionManager.js +33 -0
- package/dist/versions/DocumentVersionManager.js.map +1 -0
- package/dist/versions/index.d.ts +29 -0
- package/dist/versions/index.d.ts.map +1 -0
- package/dist/versions/index.js +28 -0
- package/dist/versions/index.js.map +1 -0
- package/dist/versions/operations.d.ts +44 -0
- package/dist/versions/operations.d.ts.map +1 -0
- package/dist/versions/operations.js +190 -0
- package/dist/versions/operations.js.map +1 -0
- package/dist/versions/paths.d.ts +29 -0
- package/dist/versions/paths.d.ts.map +1 -0
- package/dist/versions/paths.js +66 -0
- package/dist/versions/paths.js.map +1 -0
- package/dist/versions/timestamp.d.ts +22 -0
- package/dist/versions/timestamp.d.ts.map +1 -0
- package/dist/versions/timestamp.js +44 -0
- package/dist/versions/timestamp.js.map +1 -0
- package/dist/versions/types.d.ts +92 -0
- package/dist/versions/types.d.ts.map +1 -0
- package/dist/versions/types.js +11 -0
- package/dist/versions/types.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__/markdown.test.ts +44 -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__/surface.test.ts +68 -0
- 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 +95 -13
- 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 +248 -15
- package/src/markdown/index.ts +4 -0
- package/src/markdown/types.ts +38 -1
- 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 +152 -23
- 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 -524
- 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/chunk-53TZ2UTL.js +0 -470
- package/dist/chunk-53TZ2UTL.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-GAQLMORN.js.map +0 -1
- package/dist/chunk-LAZELBON.js.map +0 -1
- package/dist/chunk-QM5PKNPW.js.map +0 -1
- package/dist/chunk-TSG2OSE6.js.map +0 -1
- package/dist/chunk-XM4WSOHB.js +0 -92
- package/dist/chunk-XM4WSOHB.js.map +0 -1
- package/dist/themeLibrary-IP7EdyRq.d.ts +0 -44
|
@@ -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,35 +112,117 @@ 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
|
/**
|
|
128
|
-
*
|
|
167
|
+
* Parse a consolidated timing.json (version 2) which contains all section
|
|
168
|
+
* timing data in a single file.
|
|
169
|
+
*/
|
|
170
|
+
function parseConsolidatedTiming(data: ArrayBuffer): Record<string, AudioTimingData> | null {
|
|
171
|
+
try {
|
|
172
|
+
const text = new TextDecoder().decode(data);
|
|
173
|
+
const parsed = JSON.parse(text);
|
|
174
|
+
if (parsed && parsed.version === 2 && parsed.sections) {
|
|
175
|
+
return parsed.sections as Record<string, AudioTimingData>;
|
|
176
|
+
}
|
|
177
|
+
return null;
|
|
178
|
+
} catch {
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
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.
|
|
129
191
|
*/
|
|
130
192
|
async function discoverMp3s(container: ContentContainer): Promise<Mp3Info[]> {
|
|
131
193
|
const files = await container.listFiles();
|
|
132
|
-
const
|
|
194
|
+
const audioFiles = files.filter((f) => isNarrationAudioPath(f.path));
|
|
133
195
|
const results: Mp3Info[] = [];
|
|
134
196
|
|
|
135
|
-
|
|
197
|
+
// Try to load consolidated timing.json
|
|
198
|
+
let consolidatedSections: Record<string, AudioTimingData> | null = null;
|
|
199
|
+
const consolidatedData = await container.readFile('timing.json');
|
|
200
|
+
if (consolidatedData) {
|
|
201
|
+
consolidatedSections = parseConsolidatedTiming(consolidatedData);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
for (const file of audioFiles) {
|
|
136
205
|
const filename = file.path.split('/').pop() ?? file.path;
|
|
137
206
|
let timing: AudioTimingData | null = null;
|
|
138
207
|
|
|
139
|
-
// Try
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
208
|
+
// Try consolidated timing first: match section name from filename
|
|
209
|
+
if (consolidatedSections) {
|
|
210
|
+
const audioBase = stripAudioExtension(filename);
|
|
211
|
+
for (const [sectionName, sectionTiming] of Object.entries(consolidatedSections)) {
|
|
212
|
+
if (audioBase.endsWith(`-${sectionName}`)) {
|
|
213
|
+
timing = sectionTiming;
|
|
214
|
+
break;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Fall back to per-file .timing.json
|
|
220
|
+
if (!timing) {
|
|
221
|
+
const timingPath = `${file.path}.timing.json`;
|
|
222
|
+
const timingData = await container.readFile(timingPath);
|
|
223
|
+
if (timingData) {
|
|
224
|
+
timing = parseTimingJson(timingData);
|
|
225
|
+
}
|
|
144
226
|
}
|
|
145
227
|
|
|
146
228
|
results.push({ path: file.path, filename, timing });
|
|
@@ -221,7 +303,7 @@ function autoMatchBlocks(
|
|
|
221
303
|
// Fallback: filename-based matching
|
|
222
304
|
if (score < MIN_CONTENT_SIMILARITY && block.title) {
|
|
223
305
|
const titleSlug = slugify(block.title);
|
|
224
|
-
const fileSlug = slugify(mp3.filename
|
|
306
|
+
const fileSlug = slugify(stripAudioExtension(mp3.filename));
|
|
225
307
|
// Check if the title slug appears in the filename slug
|
|
226
308
|
if (fileSlug.includes(titleSlug) && titleSlug.length >= 3) {
|
|
227
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
|