@bendyline/squisq 0.1.1 → 1.0.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/README.md +85 -0
- package/dist/{themeLibrary-DySHPcgj.d.ts → Doc-1hmT1LZl.d.ts} +1 -42
- package/dist/__tests__/animationUtils.test.d.ts +2 -0
- package/dist/__tests__/animationUtils.test.d.ts.map +1 -0
- package/dist/__tests__/animationUtils.test.js +60 -0
- package/dist/__tests__/animationUtils.test.js.map +1 -0
- package/dist/__tests__/contentExtractor.test.d.ts +2 -0
- package/dist/__tests__/contentExtractor.test.d.ts.map +1 -0
- package/dist/__tests__/contentExtractor.test.js +151 -0
- package/dist/__tests__/contentExtractor.test.js.map +1 -0
- package/dist/__tests__/geohash.test.d.ts +2 -0
- package/dist/__tests__/geohash.test.d.ts.map +1 -0
- package/dist/__tests__/geohash.test.js +87 -0
- package/dist/__tests__/geohash.test.js.map +1 -0
- package/dist/__tests__/getLayers.test.d.ts +2 -0
- package/dist/__tests__/getLayers.test.d.ts.map +1 -0
- package/dist/__tests__/getLayers.test.js +167 -0
- package/dist/__tests__/getLayers.test.js.map +1 -0
- package/dist/__tests__/haversine.test.d.ts +2 -0
- package/dist/__tests__/haversine.test.d.ts.map +1 -0
- package/dist/__tests__/haversine.test.js +56 -0
- package/dist/__tests__/haversine.test.js.map +1 -0
- package/dist/__tests__/localForageAdapter.test.d.ts +8 -0
- package/dist/__tests__/localForageAdapter.test.d.ts.map +1 -0
- package/dist/__tests__/localForageAdapter.test.js +130 -0
- package/dist/__tests__/localForageAdapter.test.js.map +1 -0
- package/dist/__tests__/markdown.test.d.ts +2 -0
- package/dist/__tests__/markdown.test.d.ts.map +1 -0
- package/dist/__tests__/markdown.test.js +654 -0
- package/dist/__tests__/markdown.test.js.map +1 -0
- package/dist/__tests__/markdownToDoc.test.d.ts +2 -0
- package/dist/__tests__/markdownToDoc.test.d.ts.map +1 -0
- package/dist/__tests__/markdownToDoc.test.js +313 -0
- package/dist/__tests__/markdownToDoc.test.js.map +1 -0
- package/dist/__tests__/seededRandom.test.d.ts +2 -0
- package/dist/__tests__/seededRandom.test.d.ts.map +1 -0
- package/dist/__tests__/seededRandom.test.js +125 -0
- package/dist/__tests__/seededRandom.test.js.map +1 -0
- package/dist/__tests__/slideshowGenerator.test.d.ts +2 -0
- package/dist/__tests__/slideshowGenerator.test.d.ts.map +1 -0
- package/dist/__tests__/slideshowGenerator.test.js +114 -0
- package/dist/__tests__/slideshowGenerator.test.js.map +1 -0
- package/dist/__tests__/storage.test.d.ts +2 -0
- package/dist/__tests__/storage.test.d.ts.map +1 -0
- package/dist/__tests__/storage.test.js +51 -0
- package/dist/__tests__/storage.test.js.map +1 -0
- package/dist/__tests__/templates.test.d.ts +2 -0
- package/dist/__tests__/templates.test.d.ts.map +1 -0
- package/dist/__tests__/templates.test.js +217 -0
- package/dist/__tests__/templates.test.js.map +1 -0
- package/dist/__tests__/timing.test.d.ts +2 -0
- package/dist/__tests__/timing.test.d.ts.map +1 -0
- package/dist/__tests__/timing.test.js +131 -0
- package/dist/__tests__/timing.test.js.map +1 -0
- package/dist/chunk-3E5F2XMR.js +129 -0
- package/dist/chunk-3E5F2XMR.js.map +1 -0
- package/dist/chunk-6YJPI6VI.js +35 -0
- package/dist/chunk-6YJPI6VI.js.map +1 -0
- package/dist/chunk-FVTQSNR7.js +100 -0
- package/dist/chunk-FVTQSNR7.js.map +1 -0
- package/dist/chunk-V543AI2F.js +1 -0
- package/dist/chunk-V543AI2F.js.map +1 -0
- package/dist/chunk-VP5SZTZ2.js +792 -0
- package/dist/chunk-VP5SZTZ2.js.map +1 -0
- package/dist/doc/docToMarkdown.d.ts +41 -0
- package/dist/doc/docToMarkdown.d.ts.map +1 -0
- package/dist/doc/docToMarkdown.js +88 -0
- package/dist/doc/docToMarkdown.js.map +1 -0
- package/dist/doc/getLayers.d.ts +65 -0
- package/dist/doc/getLayers.d.ts.map +1 -0
- package/dist/doc/getLayers.js +103 -0
- package/dist/doc/getLayers.js.map +1 -0
- package/dist/doc/index.d.ts +9 -3
- package/dist/doc/index.d.ts.map +1 -0
- package/dist/doc/index.js +6 -125
- package/dist/doc/index.js.map +1 -1
- package/dist/doc/markdownToDoc.d.ts +68 -0
- package/dist/doc/markdownToDoc.d.ts.map +1 -0
- package/dist/doc/markdownToDoc.js +225 -0
- package/dist/doc/markdownToDoc.js.map +1 -0
- package/dist/doc/templates/accentImage.d.ts +59 -0
- package/dist/doc/templates/accentImage.d.ts.map +1 -0
- package/dist/doc/templates/accentImage.js +275 -0
- package/dist/doc/templates/accentImage.js.map +1 -0
- package/dist/doc/templates/captionUtils.d.ts +20 -0
- package/dist/doc/templates/captionUtils.d.ts.map +1 -0
- package/dist/doc/templates/captionUtils.js +29 -0
- package/dist/doc/templates/captionUtils.js.map +1 -0
- package/dist/doc/templates/comparisonBar.d.ts +13 -0
- package/dist/doc/templates/comparisonBar.d.ts.map +1 -0
- package/dist/doc/templates/comparisonBar.js +127 -0
- package/dist/doc/templates/comparisonBar.js.map +1 -0
- package/dist/doc/templates/coverBlock.d.ts +51 -0
- package/dist/doc/templates/coverBlock.d.ts.map +1 -0
- package/dist/doc/templates/coverBlock.js +125 -0
- package/dist/doc/templates/coverBlock.js.map +1 -0
- package/dist/doc/templates/dateEvent.d.ts +15 -0
- package/dist/doc/templates/dateEvent.d.ts.map +1 -0
- package/dist/doc/templates/dateEvent.js +110 -0
- package/dist/doc/templates/dateEvent.js.map +1 -0
- package/dist/doc/templates/definitionCard.d.ts +13 -0
- package/dist/doc/templates/definitionCard.d.ts.map +1 -0
- package/dist/doc/templates/definitionCard.js +114 -0
- package/dist/doc/templates/definitionCard.js.map +1 -0
- package/dist/doc/templates/factCard.d.ts +15 -0
- package/dist/doc/templates/factCard.d.ts.map +1 -0
- package/dist/doc/templates/factCard.js +100 -0
- package/dist/doc/templates/factCard.js.map +1 -0
- package/dist/doc/templates/fullBleedQuote.d.ts +13 -0
- package/dist/doc/templates/fullBleedQuote.d.ts.map +1 -0
- package/dist/doc/templates/fullBleedQuote.js +54 -0
- package/dist/doc/templates/fullBleedQuote.js.map +1 -0
- package/dist/doc/templates/imageWithCaption.d.ts +13 -0
- package/dist/doc/templates/imageWithCaption.d.ts.map +1 -0
- package/dist/doc/templates/imageWithCaption.js +142 -0
- package/dist/doc/templates/imageWithCaption.js.map +1 -0
- package/dist/doc/templates/index.d.ts +104 -0
- package/dist/doc/templates/index.d.ts.map +1 -0
- package/dist/doc/templates/index.js +442 -0
- package/dist/doc/templates/index.js.map +1 -0
- package/dist/doc/templates/listBlock.d.ts +13 -0
- package/dist/doc/templates/listBlock.d.ts.map +1 -0
- package/dist/doc/templates/listBlock.js +89 -0
- package/dist/doc/templates/listBlock.js.map +1 -0
- package/dist/doc/templates/mapBlock.d.ts +17 -0
- package/dist/doc/templates/mapBlock.d.ts.map +1 -0
- package/dist/doc/templates/mapBlock.js +112 -0
- package/dist/doc/templates/mapBlock.js.map +1 -0
- package/dist/doc/templates/persistentLayers.d.ts +39 -0
- package/dist/doc/templates/persistentLayers.d.ts.map +1 -0
- package/dist/doc/templates/persistentLayers.js +447 -0
- package/dist/doc/templates/persistentLayers.js.map +1 -0
- package/dist/doc/templates/photoGrid.d.ts +20 -0
- package/dist/doc/templates/photoGrid.d.ts.map +1 -0
- package/dist/doc/templates/photoGrid.js +172 -0
- package/dist/doc/templates/photoGrid.js.map +1 -0
- package/dist/doc/templates/pullQuote.d.ts +13 -0
- package/dist/doc/templates/pullQuote.d.ts.map +1 -0
- package/dist/doc/templates/pullQuote.js +109 -0
- package/dist/doc/templates/pullQuote.js.map +1 -0
- package/dist/doc/templates/quoteBlock.d.ts +15 -0
- package/dist/doc/templates/quoteBlock.d.ts.map +1 -0
- package/dist/doc/templates/quoteBlock.js +97 -0
- package/dist/doc/templates/quoteBlock.js.map +1 -0
- package/dist/doc/templates/sectionHeader.d.ts +14 -0
- package/dist/doc/templates/sectionHeader.d.ts.map +1 -0
- package/dist/doc/templates/sectionHeader.js +96 -0
- package/dist/doc/templates/sectionHeader.js.map +1 -0
- package/dist/doc/templates/statHighlight.d.ts +15 -0
- package/dist/doc/templates/statHighlight.d.ts.map +1 -0
- package/dist/doc/templates/statHighlight.js +99 -0
- package/dist/doc/templates/statHighlight.js.map +1 -0
- package/dist/doc/templates/titleBlock.d.ts +13 -0
- package/dist/doc/templates/titleBlock.d.ts.map +1 -0
- package/dist/doc/templates/titleBlock.js +92 -0
- package/dist/doc/templates/titleBlock.js.map +1 -0
- package/dist/doc/templates/twoColumn.d.ts +15 -0
- package/dist/doc/templates/twoColumn.d.ts.map +1 -0
- package/dist/doc/templates/twoColumn.js +219 -0
- package/dist/doc/templates/twoColumn.js.map +1 -0
- package/dist/doc/templates/videoPullQuote.d.ts +20 -0
- package/dist/doc/templates/videoPullQuote.d.ts.map +1 -0
- package/dist/doc/templates/videoPullQuote.js +118 -0
- package/dist/doc/templates/videoPullQuote.js.map +1 -0
- package/dist/doc/templates/videoWithCaption.d.ts +20 -0
- package/dist/doc/templates/videoWithCaption.d.ts.map +1 -0
- package/dist/doc/templates/videoWithCaption.js +104 -0
- package/dist/doc/templates/videoWithCaption.js.map +1 -0
- package/dist/doc/utils/animationUtils.d.ts +34 -0
- package/dist/doc/utils/animationUtils.d.ts.map +1 -0
- package/dist/doc/utils/animationUtils.js +110 -0
- package/dist/doc/utils/animationUtils.js.map +1 -0
- package/dist/doc/utils/themeUtils.d.ts +55 -0
- package/dist/doc/utils/themeUtils.d.ts.map +1 -0
- package/dist/doc/utils/themeUtils.js +109 -0
- package/dist/doc/utils/themeUtils.js.map +1 -0
- package/dist/generate/contentExtractor.d.ts +112 -0
- package/dist/generate/contentExtractor.d.ts.map +1 -0
- package/dist/generate/contentExtractor.js +638 -0
- package/dist/generate/contentExtractor.js.map +1 -0
- package/dist/generate/index.d.ts +7 -0
- package/dist/generate/index.d.ts.map +1 -0
- package/dist/generate/index.js +4 -0
- package/dist/generate/index.js.map +1 -0
- package/dist/generate/slideshowGenerator.d.ts +72 -0
- package/dist/generate/slideshowGenerator.d.ts.map +1 -0
- package/dist/generate/slideshowGenerator.js +159 -0
- package/dist/generate/slideshowGenerator.js.map +1 -0
- package/dist/generate/templateMapper.d.ts +23 -0
- package/dist/generate/templateMapper.d.ts.map +1 -0
- package/dist/generate/templateMapper.js +114 -0
- package/dist/generate/templateMapper.js.map +1 -0
- package/dist/index.d.ts +9 -8
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -216
- package/dist/index.js.map +1 -1
- package/dist/markdown/convert.d.ts +71 -0
- package/dist/markdown/convert.d.ts.map +1 -0
- package/dist/markdown/convert.js +746 -0
- package/dist/markdown/convert.js.map +1 -0
- package/dist/markdown/htmlParse.d.ts +26 -0
- package/dist/markdown/htmlParse.d.ts.map +1 -0
- package/dist/markdown/htmlParse.js +195 -0
- package/dist/markdown/htmlParse.js.map +1 -0
- package/dist/markdown/index.d.ts +19 -225
- package/dist/markdown/index.d.ts.map +1 -0
- package/dist/markdown/index.js +33 -32
- package/dist/markdown/index.js.map +1 -1
- package/dist/markdown/parse.d.ts +27 -0
- package/dist/markdown/parse.d.ts.map +1 -0
- package/dist/markdown/parse.js +90 -0
- package/dist/markdown/parse.js.map +1 -0
- package/dist/markdown/stringify.d.ts +32 -0
- package/dist/markdown/stringify.d.ts.map +1 -0
- package/dist/markdown/stringify.js +108 -0
- package/dist/markdown/stringify.js.map +1 -0
- package/dist/markdown/types.d.ts +496 -0
- package/dist/markdown/types.d.ts.map +1 -0
- package/dist/markdown/types.js +17 -0
- package/dist/markdown/types.js.map +1 -0
- package/dist/markdown/utils.d.ts +78 -0
- package/dist/markdown/utils.d.ts.map +1 -0
- package/dist/markdown/utils.js +149 -0
- package/dist/markdown/utils.js.map +1 -0
- package/dist/random/SeededRandom.d.ts +50 -0
- package/dist/random/SeededRandom.d.ts.map +1 -0
- package/dist/random/SeededRandom.js +115 -0
- package/dist/random/SeededRandom.js.map +1 -0
- package/dist/random/index.d.ts +2 -0
- package/dist/random/index.d.ts.map +1 -0
- package/dist/random/index.js +2 -0
- package/dist/random/index.js.map +1 -0
- package/dist/schemas/BlockTemplates.d.ts +583 -0
- package/dist/schemas/BlockTemplates.d.ts.map +1 -0
- package/dist/schemas/BlockTemplates.js +72 -0
- package/dist/schemas/BlockTemplates.js.map +1 -0
- package/dist/schemas/Doc.d.ts +411 -0
- package/dist/schemas/Doc.d.ts.map +1 -0
- package/dist/schemas/Doc.js +90 -0
- package/dist/schemas/Doc.js.map +1 -0
- package/dist/schemas/LayoutStrategy.d.ts +84 -0
- package/dist/schemas/LayoutStrategy.d.ts.map +1 -0
- package/dist/schemas/LayoutStrategy.js +123 -0
- package/dist/schemas/LayoutStrategy.js.map +1 -0
- package/dist/schemas/MediaProvider.d.ts +65 -0
- package/dist/schemas/MediaProvider.d.ts.map +1 -0
- package/dist/schemas/MediaProvider.js +13 -0
- package/dist/schemas/MediaProvider.js.map +1 -0
- package/dist/schemas/Theme.d.ts +163 -0
- package/dist/schemas/Theme.d.ts.map +1 -0
- package/dist/schemas/Theme.js +62 -0
- package/dist/schemas/Theme.js.map +1 -0
- package/dist/schemas/Types.d.ts +14 -0
- package/dist/schemas/Types.d.ts.map +1 -0
- package/dist/schemas/Types.js +5 -0
- package/dist/schemas/Types.js.map +1 -0
- package/dist/schemas/Viewport.d.ts +74 -0
- package/dist/schemas/Viewport.d.ts.map +1 -0
- package/dist/schemas/Viewport.js +63 -0
- package/dist/schemas/Viewport.js.map +1 -0
- package/dist/schemas/index.d.ts +10 -70
- package/dist/schemas/index.d.ts.map +1 -0
- package/dist/schemas/index.js +7 -60
- package/dist/schemas/index.js.map +1 -1
- package/dist/schemas/themeLibrary.d.ts +41 -0
- package/dist/schemas/themeLibrary.d.ts.map +1 -0
- package/dist/schemas/themeLibrary.js +466 -0
- package/dist/schemas/themeLibrary.js.map +1 -0
- package/dist/spatial/Geohash.d.ts +85 -0
- package/dist/spatial/Geohash.d.ts.map +1 -0
- package/dist/spatial/Geohash.js +142 -0
- package/dist/spatial/Geohash.js.map +1 -0
- package/dist/spatial/Haversine.d.ts +24 -0
- package/dist/spatial/Haversine.d.ts.map +1 -0
- package/dist/spatial/Haversine.js +48 -0
- package/dist/spatial/Haversine.js.map +1 -0
- package/dist/spatial/index.d.ts +3 -111
- package/dist/spatial/index.d.ts.map +1 -0
- package/dist/spatial/index.js +2 -24
- package/dist/spatial/index.js.map +1 -1
- package/dist/storage/LocalForageAdapter.d.ts +36 -0
- package/dist/storage/LocalForageAdapter.d.ts.map +1 -0
- package/dist/storage/LocalForageAdapter.js +82 -0
- package/dist/storage/LocalForageAdapter.js.map +1 -0
- package/dist/storage/LocalStorageAdapter.d.ts +16 -0
- package/dist/storage/LocalStorageAdapter.d.ts.map +1 -0
- package/dist/storage/LocalStorageAdapter.js +50 -0
- package/dist/storage/LocalStorageAdapter.js.map +1 -0
- package/dist/storage/MemoryStorageAdapter.d.ts +15 -0
- package/dist/storage/MemoryStorageAdapter.d.ts.map +1 -0
- package/dist/storage/MemoryStorageAdapter.js +34 -0
- package/dist/storage/MemoryStorageAdapter.js.map +1 -0
- package/dist/storage/Storage.d.ts +23 -0
- package/dist/storage/Storage.d.ts.map +1 -0
- package/dist/storage/Storage.js +9 -0
- package/dist/storage/Storage.js.map +1 -0
- package/dist/storage/index.d.ts +6 -91
- package/dist/storage/index.d.ts.map +1 -0
- package/dist/storage/index.js +3 -10
- package/dist/storage/index.js.map +1 -1
- package/dist/story/index.d.ts +3 -2
- package/dist/themeLibrary-DR1VRo-8.d.ts +44 -0
- package/dist/timing/index.d.ts +4 -0
- package/dist/timing/index.d.ts.map +1 -0
- package/dist/timing/index.js +3 -0
- package/dist/timing/index.js.map +1 -0
- package/dist/timing/narrationTiming.d.ts +54 -0
- package/dist/timing/narrationTiming.d.ts.map +1 -0
- package/dist/timing/narrationTiming.js +190 -0
- package/dist/timing/narrationTiming.js.map +1 -0
- package/dist/timing/readingTime.d.ts +54 -0
- package/dist/timing/readingTime.d.ts.map +1 -0
- package/dist/timing/readingTime.js +49 -0
- package/dist/timing/readingTime.js.map +1 -0
- package/package.json +19 -5
- package/src/__tests__/contentExtractor.test.ts +183 -0
- package/src/__tests__/seededRandom.test.ts +142 -0
- package/src/__tests__/slideshowGenerator.test.ts +128 -0
- package/src/__tests__/timing.test.ts +167 -0
- package/src/generate/contentExtractor.ts +808 -0
- package/src/generate/index.ts +21 -0
- package/src/generate/slideshowGenerator.ts +273 -0
- package/src/generate/templateMapper.ts +170 -0
- package/src/index.ts +3 -0
- package/src/random/SeededRandom.ts +127 -0
- package/src/random/index.ts +1 -0
- package/src/timing/index.ts +17 -0
- package/src/timing/narrationTiming.ts +220 -0
- package/src/timing/readingTime.ts +94 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading time estimation.
|
|
3
|
+
*
|
|
4
|
+
* Estimates how long it takes to read text silently vs. narrate it aloud.
|
|
5
|
+
* Uses configurable words-per-minute rates.
|
|
6
|
+
*/
|
|
7
|
+
/** Result of a reading-time estimate. */
|
|
8
|
+
export interface ReadingTimeEstimate {
|
|
9
|
+
/** Number of words (plain word count, not spoken equivalents). */
|
|
10
|
+
words: number;
|
|
11
|
+
/** Estimated minutes (decimal). */
|
|
12
|
+
minutes: number;
|
|
13
|
+
/** Estimated seconds (rounded). */
|
|
14
|
+
seconds: number;
|
|
15
|
+
}
|
|
16
|
+
/** Options for reading-time estimation. */
|
|
17
|
+
export interface ReadingTimeOptions {
|
|
18
|
+
/** Words per minute for silent reading (default: 200). */
|
|
19
|
+
wordsPerMinute?: number;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Estimate silent reading time for a block of text.
|
|
23
|
+
*
|
|
24
|
+
* @param text - The text to estimate
|
|
25
|
+
* @param options - Optional configuration
|
|
26
|
+
* @returns Word count + time estimate
|
|
27
|
+
*/
|
|
28
|
+
export declare function estimateReadingTime(text: string, options?: ReadingTimeOptions): ReadingTimeEstimate;
|
|
29
|
+
/** Result of a narration-time estimate. */
|
|
30
|
+
export interface NarrationTimeEstimate {
|
|
31
|
+
/** Spoken word equivalents (numbers expanded, comma pauses added). */
|
|
32
|
+
spokenWords: number;
|
|
33
|
+
/** Estimated minutes (decimal). */
|
|
34
|
+
minutes: number;
|
|
35
|
+
/** Estimated seconds (rounded). */
|
|
36
|
+
seconds: number;
|
|
37
|
+
}
|
|
38
|
+
/** Options for narration-time estimation. */
|
|
39
|
+
export interface NarrationTimeOptions {
|
|
40
|
+
/** Words per second for narration (default: 2.5 ≈ 150 WPM). */
|
|
41
|
+
wordsPerSecond?: number;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Estimate narration (spoken) time for a block of text.
|
|
45
|
+
*
|
|
46
|
+
* Unlike silent reading, this accounts for numbers being spoken as
|
|
47
|
+
* multiple words and pauses at commas.
|
|
48
|
+
*
|
|
49
|
+
* @param text - The text to estimate
|
|
50
|
+
* @param options - Optional configuration
|
|
51
|
+
* @returns Spoken word equivalents + time estimate
|
|
52
|
+
*/
|
|
53
|
+
export declare function estimateNarrationTime(text: string, options?: NarrationTimeOptions): NarrationTimeEstimate;
|
|
54
|
+
//# sourceMappingURL=readingTime.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"readingTime.d.ts","sourceRoot":"","sources":["../../src/timing/readingTime.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAOH,yCAAyC;AACzC,MAAM,WAAW,mBAAmB;IAClC,kEAAkE;IAClE,KAAK,EAAE,MAAM,CAAC;IACd,mCAAmC;IACnC,OAAO,EAAE,MAAM,CAAC;IAChB,mCAAmC;IACnC,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,2CAA2C;AAC3C,MAAM,WAAW,kBAAkB;IACjC,0DAA0D;IAC1D,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE,kBAAkB,GAC3B,mBAAmB,CAYrB;AAED,2CAA2C;AAC3C,MAAM,WAAW,qBAAqB;IACpC,sEAAsE;IACtE,WAAW,EAAE,MAAM,CAAC;IACpB,mCAAmC;IACnC,OAAO,EAAE,MAAM,CAAC;IAChB,mCAAmC;IACnC,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,6CAA6C;AAC7C,MAAM,WAAW,oBAAoB;IACnC,+DAA+D;IAC/D,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;;;;;;;;GASG;AACH,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE,oBAAoB,GAC7B,qBAAqB,CAYvB"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading time estimation.
|
|
3
|
+
*
|
|
4
|
+
* Estimates how long it takes to read text silently vs. narrate it aloud.
|
|
5
|
+
* Uses configurable words-per-minute rates.
|
|
6
|
+
*/
|
|
7
|
+
import { countSpokenWords, DEFAULT_WORDS_PER_SECOND } from './narrationTiming.js';
|
|
8
|
+
/** Default silent reading rate (adults average ~200–250 WPM). */
|
|
9
|
+
const DEFAULT_READING_WPM = 200;
|
|
10
|
+
/**
|
|
11
|
+
* Estimate silent reading time for a block of text.
|
|
12
|
+
*
|
|
13
|
+
* @param text - The text to estimate
|
|
14
|
+
* @param options - Optional configuration
|
|
15
|
+
* @returns Word count + time estimate
|
|
16
|
+
*/
|
|
17
|
+
export function estimateReadingTime(text, options) {
|
|
18
|
+
const configuredWpm = options?.wordsPerMinute;
|
|
19
|
+
const wpm = typeof configuredWpm === 'number' && configuredWpm > 0 ? configuredWpm : DEFAULT_READING_WPM;
|
|
20
|
+
const words = text.split(/\s+/).filter((w) => w.length > 0).length;
|
|
21
|
+
const minutes = words / wpm;
|
|
22
|
+
return {
|
|
23
|
+
words,
|
|
24
|
+
minutes,
|
|
25
|
+
seconds: Math.round(minutes * 60),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Estimate narration (spoken) time for a block of text.
|
|
30
|
+
*
|
|
31
|
+
* Unlike silent reading, this accounts for numbers being spoken as
|
|
32
|
+
* multiple words and pauses at commas.
|
|
33
|
+
*
|
|
34
|
+
* @param text - The text to estimate
|
|
35
|
+
* @param options - Optional configuration
|
|
36
|
+
* @returns Spoken word equivalents + time estimate
|
|
37
|
+
*/
|
|
38
|
+
export function estimateNarrationTime(text, options) {
|
|
39
|
+
const providedWps = options?.wordsPerSecond;
|
|
40
|
+
const wps = typeof providedWps === 'number' && providedWps > 0 ? providedWps : DEFAULT_WORDS_PER_SECOND;
|
|
41
|
+
const spokenWords = countSpokenWords(text);
|
|
42
|
+
const totalSeconds = spokenWords / wps;
|
|
43
|
+
return {
|
|
44
|
+
spokenWords,
|
|
45
|
+
minutes: totalSeconds / 60,
|
|
46
|
+
seconds: Math.round(totalSeconds),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=readingTime.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"readingTime.js","sourceRoot":"","sources":["../../src/timing/readingTime.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,gBAAgB,EAAE,wBAAwB,EAAE,MAAM,sBAAsB,CAAC;AAElF,iEAAiE;AACjE,MAAM,mBAAmB,GAAG,GAAG,CAAC;AAkBhC;;;;;;GAMG;AACH,MAAM,UAAU,mBAAmB,CACjC,IAAY,EACZ,OAA4B;IAE5B,MAAM,aAAa,GAAG,OAAO,EAAE,cAAc,CAAC;IAC9C,MAAM,GAAG,GACP,OAAO,aAAa,KAAK,QAAQ,IAAI,aAAa,GAAG,CAAC,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,mBAAmB,CAAC;IAC/F,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC;IACnE,MAAM,OAAO,GAAG,KAAK,GAAG,GAAG,CAAC;IAE5B,OAAO;QACL,KAAK;QACL,OAAO;QACP,OAAO,EAAE,IAAI,CAAC,KAAK,CAAC,OAAO,GAAG,EAAE,CAAC;KAClC,CAAC;AACJ,CAAC;AAkBD;;;;;;;;;GASG;AACH,MAAM,UAAU,qBAAqB,CACnC,IAAY,EACZ,OAA8B;IAE9B,MAAM,WAAW,GAAG,OAAO,EAAE,cAAc,CAAC;IAC5C,MAAM,GAAG,GACP,OAAO,WAAW,KAAK,QAAQ,IAAI,WAAW,GAAG,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,wBAAwB,CAAC;IAC9F,MAAM,WAAW,GAAG,gBAAgB,CAAC,IAAI,CAAC,CAAC;IAC3C,MAAM,YAAY,GAAG,WAAW,GAAG,GAAG,CAAC;IAEvC,OAAO;QACL,WAAW;QACX,OAAO,EAAE,YAAY,GAAG,EAAE;QAC1B,OAAO,EAAE,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC;KAClC,CAAC;AACJ,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bendyline/squisq",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Headless utilities for doc/block rendering, spatial math, Markdown, and storage",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Bendyline",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
9
|
-
"url": "https://github.com/
|
|
9
|
+
"url": "https://github.com/bendyline/squisq.git",
|
|
10
10
|
"directory": "packages/core"
|
|
11
11
|
},
|
|
12
|
-
"homepage": "https://github.com/
|
|
12
|
+
"homepage": "https://github.com/bendyline/squisq",
|
|
13
13
|
"keywords": [
|
|
14
14
|
"doc",
|
|
15
15
|
"block",
|
|
@@ -65,12 +65,26 @@
|
|
|
65
65
|
"types": "./dist/markdown/index.d.ts",
|
|
66
66
|
"import": "./dist/markdown/index.js",
|
|
67
67
|
"default": "./dist/markdown/index.js"
|
|
68
|
+
},
|
|
69
|
+
"./timing": {
|
|
70
|
+
"types": "./dist/timing/index.d.ts",
|
|
71
|
+
"import": "./dist/timing/index.js",
|
|
72
|
+
"default": "./dist/timing/index.js"
|
|
73
|
+
},
|
|
74
|
+
"./random": {
|
|
75
|
+
"types": "./dist/random/index.d.ts",
|
|
76
|
+
"import": "./dist/random/index.js",
|
|
77
|
+
"default": "./dist/random/index.js"
|
|
78
|
+
},
|
|
79
|
+
"./generate": {
|
|
80
|
+
"types": "./dist/generate/index.d.ts",
|
|
81
|
+
"import": "./dist/generate/index.js",
|
|
82
|
+
"default": "./dist/generate/index.js"
|
|
68
83
|
}
|
|
69
84
|
},
|
|
70
85
|
"scripts": {
|
|
71
86
|
"build": "tsup",
|
|
72
|
-
"typecheck": "tsc --noEmit"
|
|
73
|
-
"prepublishOnly": "npm run build"
|
|
87
|
+
"typecheck": "tsc --noEmit"
|
|
74
88
|
},
|
|
75
89
|
"dependencies": {
|
|
76
90
|
"hast-util-from-html": "^2.0.0",
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
extractContent,
|
|
4
|
+
stripMarkdown,
|
|
5
|
+
type ExtractionResult,
|
|
6
|
+
} from '../generate/contentExtractor.js';
|
|
7
|
+
|
|
8
|
+
const SAMPLE_TEXT = [
|
|
9
|
+
'Mount Rainier is the largest volcano in the Cascades, standing at 14,411 feet.',
|
|
10
|
+
'In July 1899, the mountain was designated a national park.',
|
|
11
|
+
'The park sees roughly 2 million visitors each year.',
|
|
12
|
+
'"The mountain was calling me," wrote John Muir after his 1888 expedition.',
|
|
13
|
+
'Temperatures range from 20 degrees to 70 degrees depending on elevation.',
|
|
14
|
+
'The park features hiking, camping, fishing, and climbing.',
|
|
15
|
+
'Imagine standing above the clouds at sunrise!',
|
|
16
|
+
'Paradise is a popular visitor area known for its wildflower meadows.',
|
|
17
|
+
'Rainier is the most glaciated peak in the lower 48 states.',
|
|
18
|
+
].join(' ');
|
|
19
|
+
|
|
20
|
+
describe('extractContent', () => {
|
|
21
|
+
let result: ExtractionResult;
|
|
22
|
+
|
|
23
|
+
// Run extraction once for all tests
|
|
24
|
+
it('extracts elements from sample text', () => {
|
|
25
|
+
result = extractContent(SAMPLE_TEXT);
|
|
26
|
+
expect(result.elements.length).toBeGreaterThan(0);
|
|
27
|
+
expect(result.sourceLength).toBe(SAMPLE_TEXT.length);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('finds stats', () => {
|
|
31
|
+
const stats = result.elements.filter((e) => e.type === 'stat');
|
|
32
|
+
expect(stats.length).toBeGreaterThan(0);
|
|
33
|
+
// Should find 14,411 feet and/or 2 million
|
|
34
|
+
const values = stats.map((s) => (s.data.type === 'stat' ? s.data.value : ''));
|
|
35
|
+
expect(
|
|
36
|
+
values.some((v) => v.includes('14,411') || v.includes('2 million') || v.includes('feet')),
|
|
37
|
+
).toBe(true);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('finds dates', () => {
|
|
41
|
+
const dates = result.elements.filter((e) => e.type === 'date');
|
|
42
|
+
expect(dates.length).toBeGreaterThan(0);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('finds quotes', () => {
|
|
46
|
+
const quotes = result.elements.filter((e) => e.type === 'quote');
|
|
47
|
+
expect(quotes.length).toBeGreaterThan(0);
|
|
48
|
+
const quoteData = quotes[0].data;
|
|
49
|
+
if (quoteData.type === 'quote') {
|
|
50
|
+
expect(quoteData.quote).toContain('mountain was calling me');
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('respects minConfidence filter', () => {
|
|
55
|
+
const strict = extractContent(SAMPLE_TEXT, { minConfidence: 0.9 });
|
|
56
|
+
const loose = extractContent(SAMPLE_TEXT, { minConfidence: 0.1 });
|
|
57
|
+
expect(loose.elements.length).toBeGreaterThanOrEqual(strict.elements.length);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('respects types filter', () => {
|
|
61
|
+
const onlyStats = extractContent(SAMPLE_TEXT, { types: ['stat'] });
|
|
62
|
+
for (const elem of onlyStats.elements) {
|
|
63
|
+
expect(elem.type).toBe('stat');
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('respects maxPerType', () => {
|
|
68
|
+
const limited = extractContent(SAMPLE_TEXT, { maxPerType: 1 });
|
|
69
|
+
const typeCounts = new Map<string, number>();
|
|
70
|
+
for (const elem of limited.elements) {
|
|
71
|
+
typeCounts.set(elem.type, (typeCounts.get(elem.type) ?? 0) + 1);
|
|
72
|
+
}
|
|
73
|
+
for (const count of typeCounts.values()) {
|
|
74
|
+
expect(count).toBeLessThanOrEqual(1);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('deduplicates overlapping extractions', () => {
|
|
79
|
+
// Every element should have a unique source position range
|
|
80
|
+
for (let i = 0; i < result.elements.length; i++) {
|
|
81
|
+
for (let j = i + 1; j < result.elements.length; j++) {
|
|
82
|
+
const a = result.elements[i];
|
|
83
|
+
const b = result.elements[j];
|
|
84
|
+
// They shouldn't share the exact same start position
|
|
85
|
+
if (a.sourcePosition === b.sourcePosition) {
|
|
86
|
+
// If they overlap, one should have been removed by dedup
|
|
87
|
+
// (this is checking that the dedup is working at all)
|
|
88
|
+
expect(a.type).not.toBe(b.type);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('returns correct stats counts', () => {
|
|
95
|
+
expect(result.stats.totalCount).toBe(result.elements.length);
|
|
96
|
+
expect(result.stats.statCount).toBe(result.elements.filter((e) => e.type === 'stat').length);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('sorts elements by source position', () => {
|
|
100
|
+
for (let i = 1; i < result.elements.length; i++) {
|
|
101
|
+
expect(result.elements[i].sourcePosition).toBeGreaterThanOrEqual(
|
|
102
|
+
result.elements[i - 1].sourcePosition,
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
describe('extractContent — comparisons', () => {
|
|
109
|
+
it('detects "from X to Y" comparisons', () => {
|
|
110
|
+
const text = 'Temperatures range from 20 degrees to 70 degrees depending on altitude.';
|
|
111
|
+
const result = extractContent(text, { types: ['comparison'] });
|
|
112
|
+
expect(result.elements.length).toBeGreaterThan(0);
|
|
113
|
+
const comp = result.elements[0].data;
|
|
114
|
+
if (comp.type === 'comparison') {
|
|
115
|
+
expect(comp.left.label).toBeTruthy();
|
|
116
|
+
expect(comp.right.label).toBeTruthy();
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
describe('extractContent — impact lines', () => {
|
|
122
|
+
it('detects short punchy sentences', () => {
|
|
123
|
+
const text =
|
|
124
|
+
'Imagine standing above the clouds! Discover the hidden valley. The weather is variable.';
|
|
125
|
+
const result = extractContent(text, { types: ['impactLine'] });
|
|
126
|
+
expect(result.elements.length).toBeGreaterThan(0);
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
describe('extractContent — lists', () => {
|
|
131
|
+
it('detects "including X, Y, and Z" lists', () => {
|
|
132
|
+
const text = 'Activities including hiking, camping, fishing, and climbing are popular.';
|
|
133
|
+
const result = extractContent(text, { types: ['list'] });
|
|
134
|
+
expect(result.elements.length).toBeGreaterThan(0);
|
|
135
|
+
const listData = result.elements[0].data;
|
|
136
|
+
if (listData.type === 'list') {
|
|
137
|
+
expect(listData.items.length).toBeGreaterThanOrEqual(3);
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
describe('extractContent — definitions', () => {
|
|
143
|
+
it('detects "Term is a definition" patterns', () => {
|
|
144
|
+
const text =
|
|
145
|
+
'Paradise is a popular visitor area known for its wildflower meadows and scenic viewpoints.';
|
|
146
|
+
const result = extractContent(text, { types: ['definition'] });
|
|
147
|
+
expect(result.elements.length).toBeGreaterThan(0);
|
|
148
|
+
const defData = result.elements[0].data;
|
|
149
|
+
if (defData.type === 'definition') {
|
|
150
|
+
expect(defData.term).toBe('Paradise');
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
describe('stripMarkdown', () => {
|
|
156
|
+
it('removes headers', () => {
|
|
157
|
+
expect(stripMarkdown('# Hello\n## World')).toBe('Hello\nWorld');
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('removes bold and italic', () => {
|
|
161
|
+
expect(stripMarkdown('**bold** and *italic*')).toBe('bold and italic');
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('removes links, keeping text', () => {
|
|
165
|
+
expect(stripMarkdown('[Click here](https://example.com)')).toBe('Click here');
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('removes images entirely', () => {
|
|
169
|
+
expect(stripMarkdown('')).toBe('');
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('removes code blocks', () => {
|
|
173
|
+
expect(stripMarkdown('before\n```js\ncode\n```\nafter')).toBe('before\n\nafter');
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('removes blockquote markers', () => {
|
|
177
|
+
expect(stripMarkdown('> quoted text')).toBe('quoted text');
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('collapses excess newlines', () => {
|
|
181
|
+
expect(stripMarkdown('a\n\n\n\nb')).toBe('a\n\nb');
|
|
182
|
+
});
|
|
183
|
+
});
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { SeededRandom, hashString } from '../random/index.js';
|
|
3
|
+
|
|
4
|
+
describe('SeededRandom', () => {
|
|
5
|
+
it('produces deterministic output from the same seed', () => {
|
|
6
|
+
const a = new SeededRandom(42);
|
|
7
|
+
const b = new SeededRandom(42);
|
|
8
|
+
expect(a.next()).toBe(b.next());
|
|
9
|
+
expect(a.next()).toBe(b.next());
|
|
10
|
+
expect(a.next()).toBe(b.next());
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('produces different output from different seeds', () => {
|
|
14
|
+
const a = new SeededRandom(1);
|
|
15
|
+
const b = new SeededRandom(2);
|
|
16
|
+
expect(a.next()).not.toBe(b.next());
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('next() returns values in [0, 1)', () => {
|
|
20
|
+
const rng = new SeededRandom(123);
|
|
21
|
+
for (let i = 0; i < 100; i++) {
|
|
22
|
+
const v = rng.next();
|
|
23
|
+
expect(v).toBeGreaterThanOrEqual(0);
|
|
24
|
+
expect(v).toBeLessThan(1);
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('nextInt(max) returns values in [0, max)', () => {
|
|
29
|
+
const rng = new SeededRandom(99);
|
|
30
|
+
for (let i = 0; i < 100; i++) {
|
|
31
|
+
const v = rng.nextInt(10);
|
|
32
|
+
expect(v).toBeGreaterThanOrEqual(0);
|
|
33
|
+
expect(v).toBeLessThan(10);
|
|
34
|
+
expect(Number.isInteger(v)).toBe(true);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('nextIntRange(min, max) stays in range', () => {
|
|
39
|
+
const rng = new SeededRandom(77);
|
|
40
|
+
for (let i = 0; i < 100; i++) {
|
|
41
|
+
const v = rng.nextIntRange(5, 15);
|
|
42
|
+
expect(v).toBeGreaterThanOrEqual(5);
|
|
43
|
+
expect(v).toBeLessThan(15);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('pick returns undefined for empty array', () => {
|
|
48
|
+
const rng = new SeededRandom(1);
|
|
49
|
+
expect(rng.pick([])).toBeUndefined();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('pick returns an element from the array', () => {
|
|
53
|
+
const rng = new SeededRandom(1);
|
|
54
|
+
const items = ['a', 'b', 'c'];
|
|
55
|
+
const picked = rng.pick(items);
|
|
56
|
+
expect(items).toContain(picked);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('pickRequired throws on empty array', () => {
|
|
60
|
+
const rng = new SeededRandom(1);
|
|
61
|
+
expect(() => rng.pickRequired([])).toThrow('Cannot pick from empty array');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('pickMultiple returns correct count', () => {
|
|
65
|
+
const rng = new SeededRandom(42);
|
|
66
|
+
const result = rng.pickMultiple([1, 2, 3, 4, 5], 3);
|
|
67
|
+
expect(result).toHaveLength(3);
|
|
68
|
+
// All elements should be unique
|
|
69
|
+
expect(new Set(result).size).toBe(3);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('pickMultiple returns all if count >= length', () => {
|
|
73
|
+
const rng = new SeededRandom(42);
|
|
74
|
+
const result = rng.pickMultiple([1, 2, 3], 5);
|
|
75
|
+
expect(result).toHaveLength(3);
|
|
76
|
+
expect(new Set(result).size).toBe(3);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('shuffle is deterministic', () => {
|
|
80
|
+
const a = new SeededRandom(42);
|
|
81
|
+
const b = new SeededRandom(42);
|
|
82
|
+
expect(a.shuffle([1, 2, 3, 4, 5])).toEqual(b.shuffle([1, 2, 3, 4, 5]));
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('shuffled does not modify original', () => {
|
|
86
|
+
const rng = new SeededRandom(42);
|
|
87
|
+
const original = [1, 2, 3, 4, 5];
|
|
88
|
+
const copy = [...original];
|
|
89
|
+
rng.shuffled(original);
|
|
90
|
+
expect(original).toEqual(copy);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('pickWeighted respects weights', () => {
|
|
94
|
+
// Heavily weight 'a' — over many picks it should dominate
|
|
95
|
+
const rng = new SeededRandom(42);
|
|
96
|
+
const items = [
|
|
97
|
+
{ item: 'a', weight: 100 },
|
|
98
|
+
{ item: 'b', weight: 1 },
|
|
99
|
+
];
|
|
100
|
+
let aCount = 0;
|
|
101
|
+
for (let i = 0; i < 100; i++) {
|
|
102
|
+
if (rng.pickWeighted(items) === 'a') aCount++;
|
|
103
|
+
}
|
|
104
|
+
expect(aCount).toBeGreaterThan(80);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('pickWeighted returns undefined for empty', () => {
|
|
108
|
+
const rng = new SeededRandom(1);
|
|
109
|
+
expect(rng.pickWeighted([])).toBeUndefined();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('derive creates independent stream', () => {
|
|
113
|
+
const rng = new SeededRandom(42);
|
|
114
|
+
const d1 = rng.derive('stream-a');
|
|
115
|
+
const d2 = rng.derive('stream-b');
|
|
116
|
+
expect(d1.next()).not.toBe(d2.next());
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('handles seed of 0 gracefully', () => {
|
|
120
|
+
const rng = new SeededRandom(0);
|
|
121
|
+
const v = rng.next();
|
|
122
|
+
expect(v).toBeGreaterThanOrEqual(0);
|
|
123
|
+
expect(v).toBeLessThan(1);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
describe('hashString', () => {
|
|
128
|
+
it('returns a positive 32-bit integer', () => {
|
|
129
|
+
const h = hashString('hello world');
|
|
130
|
+
expect(h).toBeGreaterThanOrEqual(0);
|
|
131
|
+
expect(h).toBeLessThanOrEqual(0xffffffff);
|
|
132
|
+
expect(Number.isInteger(h)).toBe(true);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('is deterministic', () => {
|
|
136
|
+
expect(hashString('test')).toBe(hashString('test'));
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('differs for different strings', () => {
|
|
140
|
+
expect(hashString('abc')).not.toBe(hashString('xyz'));
|
|
141
|
+
});
|
|
142
|
+
});
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { generateSlideshow, type SlideshowImage } from '../generate/index.js';
|
|
3
|
+
import { isTemplateBlock } from '../schemas/BlockTemplates.js';
|
|
4
|
+
|
|
5
|
+
const ARTICLE_TEXT = [
|
|
6
|
+
'Mount Rainier is the largest volcano in the Cascades, standing at 14,411 feet.',
|
|
7
|
+
'In July 1899, President McKinley signed the act establishing Mount Rainier National Park.',
|
|
8
|
+
'The park encompasses roughly 236,381 acres of old-growth forest and alpine meadows.',
|
|
9
|
+
'"The mountain was calling me," wrote John Muir after his 1888 expedition to explore the glaciers.',
|
|
10
|
+
'Temperatures range from 20 degrees to 70 degrees depending on elevation and season.',
|
|
11
|
+
'Popular activities including hiking, camping, fishing, and climbing draw visitors year-round.',
|
|
12
|
+
'Paradise is a popular visitor area known for its wildflower meadows and spectacular views.',
|
|
13
|
+
'The mountain has 25 named glaciers covering about 35 square miles of ice.',
|
|
14
|
+
'Rainier is the most glaciated peak in the lower 48 states, a remarkable natural wonder.',
|
|
15
|
+
'Imagine standing above the clouds at sunrise!',
|
|
16
|
+
].join('\n\n');
|
|
17
|
+
|
|
18
|
+
const SAMPLE_IMAGES: SlideshowImage[] = [
|
|
19
|
+
{ src: '/img/rainier-1.jpg', alt: 'Mount Rainier from Paradise' },
|
|
20
|
+
{ src: '/img/rainier-2.jpg', alt: 'Wildflower meadow', credit: 'NPS Photo' },
|
|
21
|
+
{ src: '/img/rainier-3.jpg', alt: 'Nisqually Glacier' },
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
describe('generateSlideshow', () => {
|
|
25
|
+
it('produces a valid Doc with blocks', () => {
|
|
26
|
+
const doc = generateSlideshow(ARTICLE_TEXT);
|
|
27
|
+
expect(doc.blocks.length).toBeGreaterThan(0);
|
|
28
|
+
expect(doc.duration).toBeGreaterThan(0);
|
|
29
|
+
expect(doc.articleId).toBe('slideshow');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('includes a title slide when title is provided', () => {
|
|
33
|
+
const doc = generateSlideshow(ARTICLE_TEXT, [], { title: 'Mount Rainier' });
|
|
34
|
+
expect(doc.articleId).toBe('Mount Rainier');
|
|
35
|
+
const first = doc.blocks[0];
|
|
36
|
+
if (isTemplateBlock(first)) {
|
|
37
|
+
expect(first.template).toBe('titleBlock');
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('embeds themeId in the Doc', () => {
|
|
42
|
+
const doc = generateSlideshow(ARTICLE_TEXT, [], { themeId: 'cinematic' });
|
|
43
|
+
expect(doc.themeId).toBe('cinematic');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('interleaves image slides when images are provided', () => {
|
|
47
|
+
const doc = generateSlideshow(ARTICLE_TEXT, SAMPLE_IMAGES);
|
|
48
|
+
const imageSlides = doc.blocks.filter(
|
|
49
|
+
(b) => isTemplateBlock(b) && b.template === 'imageWithCaption',
|
|
50
|
+
);
|
|
51
|
+
expect(imageSlides.length).toBeGreaterThan(0);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('produces deterministic output with the same seed', () => {
|
|
55
|
+
const a = generateSlideshow(ARTICLE_TEXT, SAMPLE_IMAGES, { seed: 42 });
|
|
56
|
+
const b = generateSlideshow(ARTICLE_TEXT, SAMPLE_IMAGES, { seed: 42 });
|
|
57
|
+
expect(a.blocks.length).toBe(b.blocks.length);
|
|
58
|
+
for (let i = 0; i < a.blocks.length; i++) {
|
|
59
|
+
if (isTemplateBlock(a.blocks[i]) && isTemplateBlock(b.blocks[i])) {
|
|
60
|
+
const aBlock = a.blocks[i] as { template: string };
|
|
61
|
+
const bBlock = b.blocks[i] as { template: string };
|
|
62
|
+
expect(aBlock.template).toBe(bBlock.template);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('produces different output with different seeds', () => {
|
|
68
|
+
const a = generateSlideshow(ARTICLE_TEXT, SAMPLE_IMAGES, { seed: 1 });
|
|
69
|
+
const b = generateSlideshow(ARTICLE_TEXT, SAMPLE_IMAGES, { seed: 999 });
|
|
70
|
+
// At least some image accents or motions should differ
|
|
71
|
+
const aSerialized = JSON.stringify(a.blocks);
|
|
72
|
+
const bSerialized = JSON.stringify(b.blocks);
|
|
73
|
+
// They should differ in at least some detail (ambient motions, accent positions)
|
|
74
|
+
expect(aSerialized).not.toBe(bSerialized);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('handles empty text gracefully', () => {
|
|
78
|
+
const doc = generateSlideshow('', []);
|
|
79
|
+
expect(doc.blocks.length).toBe(0);
|
|
80
|
+
expect(doc.duration).toBe(0);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('handles markdown input when isMarkdown is set', () => {
|
|
84
|
+
const md = '# Title\n\nThe park covers **236,381 acres** of wilderness.';
|
|
85
|
+
const doc = generateSlideshow(md, [], { isMarkdown: true });
|
|
86
|
+
expect(doc.blocks.length).toBeGreaterThan(0);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('uses explicit duration when provided', () => {
|
|
90
|
+
const doc = generateSlideshow(ARTICLE_TEXT, [], { duration: 60 });
|
|
91
|
+
expect(doc.duration).toBe(60);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('respects targetSlides option', () => {
|
|
95
|
+
const doc = generateSlideshow(ARTICLE_TEXT, [], { targetSlides: 3 });
|
|
96
|
+
// Should have ~3 slides (may be slightly more due to title, images)
|
|
97
|
+
expect(doc.blocks.length).toBeLessThanOrEqual(5);
|
|
98
|
+
expect(doc.blocks.length).toBeGreaterThan(0);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('every block has an id and duration', () => {
|
|
102
|
+
const doc = generateSlideshow(ARTICLE_TEXT, SAMPLE_IMAGES, { title: 'Test' });
|
|
103
|
+
for (const block of doc.blocks) {
|
|
104
|
+
if (isTemplateBlock(block)) {
|
|
105
|
+
expect(block.id).toBeTruthy();
|
|
106
|
+
expect(block.duration).toBeGreaterThan(0);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('rotates color schemes across content slides', () => {
|
|
112
|
+
const doc = generateSlideshow(ARTICLE_TEXT, [], {
|
|
113
|
+
colorSchemes: ['blue', 'green'],
|
|
114
|
+
targetSlides: 6,
|
|
115
|
+
});
|
|
116
|
+
const colors: string[] = [];
|
|
117
|
+
for (const block of doc.blocks) {
|
|
118
|
+
if (isTemplateBlock(block) && 'colorScheme' in block) {
|
|
119
|
+
const cs = (block as { colorScheme?: string }).colorScheme;
|
|
120
|
+
if (cs) colors.push(cs);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
// With 2 schemes, we should see alternation
|
|
124
|
+
if (colors.length >= 2) {
|
|
125
|
+
expect(colors[0]).not.toBe(colors[1]);
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
});
|