@bendyline/squisq 1.0.1 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ContentContainer-DNx460y8.d.ts +88 -0
- package/dist/{Doc-1hmT1LZl.d.ts → Doc-Cvt2lyzn.d.ts} +56 -4
- package/dist/MediaProvider-wpSe21B3.d.ts +66 -0
- package/dist/__tests__/contentContainer.test.d.ts +2 -0
- package/dist/__tests__/contentContainer.test.d.ts.map +1 -0
- package/dist/__tests__/contentContainer.test.js +234 -0
- package/dist/__tests__/contentContainer.test.js.map +1 -0
- package/dist/__tests__/markdownToDoc.test.js +75 -0
- package/dist/__tests__/markdownToDoc.test.js.map +1 -1
- package/dist/{chunk-PJ7AODIM.js → chunk-3WBJAZZ5.js} +394 -52
- package/dist/chunk-3WBJAZZ5.js.map +1 -0
- package/dist/chunk-4CRBS35L.js +292 -0
- package/dist/chunk-4CRBS35L.js.map +1 -0
- package/dist/{chunk-6YJPI6VI.js → chunk-4X3JQXNM.js} +1 -1
- package/dist/{chunk-S4F2RY37.js → chunk-CORGFIFU.js} +1 -1
- package/dist/chunk-CORGFIFU.js.map +1 -0
- package/dist/chunk-DNJKBAI7.js +1 -0
- package/dist/chunk-DNJKBAI7.js.map +1 -0
- package/dist/{chunk-VP5SZTZ2.js → chunk-MIBRIRGJ.js} +2 -139
- package/dist/chunk-MIBRIRGJ.js.map +1 -0
- package/dist/{chunk-URU6QMLY.js → chunk-NJLL5XYC.js} +15 -8
- package/dist/chunk-NJLL5XYC.js.map +1 -0
- package/dist/chunk-OFU53HI7.js +165 -0
- package/dist/chunk-OFU53HI7.js.map +1 -0
- package/dist/chunk-TSG2OSE6.js +145 -0
- package/dist/chunk-TSG2OSE6.js.map +1 -0
- package/dist/{chunk-O5HCIUAD.js → chunk-TVPKD76V.js} +81 -187
- package/dist/chunk-TVPKD76V.js.map +1 -0
- package/dist/contentExtractor-BNfVJV2U.d.ts +113 -0
- package/dist/doc/audioMapping.d.ts +36 -0
- package/dist/doc/audioMapping.d.ts.map +1 -0
- package/dist/doc/audioMapping.js +279 -0
- package/dist/doc/audioMapping.js.map +1 -0
- package/dist/doc/index.d.ts +1 -0
- package/dist/doc/index.d.ts.map +1 -1
- package/dist/doc/index.js +1 -0
- package/dist/doc/index.js.map +1 -1
- package/dist/doc/markdownToDoc.d.ts.map +1 -1
- package/dist/doc/markdownToDoc.js +126 -1
- package/dist/doc/markdownToDoc.js.map +1 -1
- package/dist/doc/templates/comparisonBar.d.ts.map +1 -1
- package/dist/doc/templates/comparisonBar.js +23 -5
- package/dist/doc/templates/comparisonBar.js.map +1 -1
- package/dist/doc/templates/coverBlock.d.ts.map +1 -1
- package/dist/doc/templates/coverBlock.js +3 -0
- package/dist/doc/templates/coverBlock.js.map +1 -1
- package/dist/doc/templates/dateEvent.d.ts.map +1 -1
- package/dist/doc/templates/dateEvent.js +4 -0
- package/dist/doc/templates/dateEvent.js.map +1 -1
- package/dist/doc/templates/definitionCard.d.ts.map +1 -1
- package/dist/doc/templates/definitionCard.js +4 -1
- package/dist/doc/templates/definitionCard.js.map +1 -1
- package/dist/doc/templates/factCard.d.ts.map +1 -1
- package/dist/doc/templates/factCard.js +4 -0
- package/dist/doc/templates/factCard.js.map +1 -1
- package/dist/doc/templates/fullBleedQuote.d.ts.map +1 -1
- package/dist/doc/templates/fullBleedQuote.js +2 -1
- package/dist/doc/templates/fullBleedQuote.js.map +1 -1
- package/dist/doc/templates/imageWithCaption.d.ts.map +1 -1
- package/dist/doc/templates/imageWithCaption.js +4 -0
- package/dist/doc/templates/imageWithCaption.js.map +1 -1
- package/dist/doc/templates/listBlock.d.ts.map +1 -1
- package/dist/doc/templates/listBlock.js +3 -0
- package/dist/doc/templates/listBlock.js.map +1 -1
- package/dist/doc/templates/mapBlock.d.ts.map +1 -1
- package/dist/doc/templates/mapBlock.js +3 -0
- package/dist/doc/templates/mapBlock.js.map +1 -1
- package/dist/doc/templates/photoGrid.d.ts.map +1 -1
- package/dist/doc/templates/photoGrid.js +3 -0
- package/dist/doc/templates/photoGrid.js.map +1 -1
- package/dist/doc/templates/pullQuote.d.ts.map +1 -1
- package/dist/doc/templates/pullQuote.js +4 -0
- package/dist/doc/templates/pullQuote.js.map +1 -1
- package/dist/doc/templates/quoteBlock.d.ts.map +1 -1
- package/dist/doc/templates/quoteBlock.js +4 -0
- package/dist/doc/templates/quoteBlock.js.map +1 -1
- package/dist/doc/templates/sectionHeader.d.ts.map +1 -1
- package/dist/doc/templates/sectionHeader.js +3 -2
- package/dist/doc/templates/sectionHeader.js.map +1 -1
- package/dist/doc/templates/statHighlight.d.ts.map +1 -1
- package/dist/doc/templates/statHighlight.js +4 -1
- package/dist/doc/templates/statHighlight.js.map +1 -1
- package/dist/doc/templates/titleBlock.d.ts.map +1 -1
- package/dist/doc/templates/titleBlock.js +3 -0
- package/dist/doc/templates/titleBlock.js.map +1 -1
- package/dist/doc/templates/twoColumn.d.ts.map +1 -1
- package/dist/doc/templates/twoColumn.js +7 -1
- package/dist/doc/templates/twoColumn.js.map +1 -1
- package/dist/doc/templates/videoPullQuote.d.ts.map +1 -1
- package/dist/doc/templates/videoPullQuote.js +4 -0
- package/dist/doc/templates/videoPullQuote.js.map +1 -1
- package/dist/doc/templates/videoWithCaption.d.ts.map +1 -1
- package/dist/doc/templates/videoWithCaption.js +3 -0
- package/dist/doc/templates/videoWithCaption.js.map +1 -1
- package/dist/doc/utils/themeUtils.d.ts +9 -0
- package/dist/doc/utils/themeUtils.d.ts.map +1 -1
- package/dist/doc/utils/themeUtils.js +26 -0
- package/dist/doc/utils/themeUtils.js.map +1 -1
- package/dist/markdown/convert.d.ts.map +1 -1
- package/dist/markdown/convert.js +16 -7
- package/dist/markdown/convert.js.map +1 -1
- package/dist/markdown/types.d.ts +4 -2
- package/dist/markdown/types.d.ts.map +1 -1
- package/dist/schemas/BlockTemplates.d.ts +2 -2
- package/dist/schemas/BlockTemplates.d.ts.map +1 -1
- package/dist/schemas/BlockTemplates.js +2 -2
- package/dist/schemas/BlockTemplates.js.map +1 -1
- package/dist/schemas/Doc.d.ts +52 -0
- package/dist/schemas/Doc.d.ts.map +1 -1
- package/dist/schemas/Doc.js.map +1 -1
- package/dist/schemas/themeLibrary.d.ts +2 -2
- package/dist/schemas/themeLibrary.d.ts.map +1 -1
- package/dist/schemas/themeLibrary.js +91 -26
- package/dist/schemas/themeLibrary.js.map +1 -1
- package/dist/storage/ContentContainer.d.ts +87 -0
- package/dist/storage/ContentContainer.d.ts.map +1 -0
- package/dist/storage/ContentContainer.js +122 -0
- package/dist/storage/ContentContainer.js.map +1 -0
- package/dist/storage/MediaProviderFromContainer.d.ts +21 -0
- package/dist/storage/MediaProviderFromContainer.d.ts.map +1 -0
- package/dist/storage/MediaProviderFromContainer.js +79 -0
- package/dist/storage/MediaProviderFromContainer.js.map +1 -0
- package/dist/storage/index.d.ts +3 -0
- package/dist/storage/index.d.ts.map +1 -1
- package/dist/storage/index.js +2 -0
- package/dist/storage/index.js.map +1 -1
- package/dist/story/index.d.ts +50 -5
- package/dist/story/index.js +15 -5
- package/dist/{themeLibrary-DR1VRo-8.d.ts → themeLibrary-VOHfPzRB.d.ts} +3 -3
- package/dist/transform/applyTransform.d.ts +22 -0
- package/dist/transform/applyTransform.d.ts.map +1 -0
- package/dist/transform/applyTransform.js +68 -0
- package/dist/transform/applyTransform.js.map +1 -0
- package/dist/transform/blockAnalyzer.d.ts +48 -0
- package/dist/transform/blockAnalyzer.d.ts.map +1 -0
- package/dist/transform/blockAnalyzer.js +135 -0
- package/dist/transform/blockAnalyzer.js.map +1 -0
- package/dist/transform/index.d.ts +6 -0
- package/dist/transform/index.d.ts.map +1 -0
- package/dist/transform/index.js +4 -0
- package/dist/transform/index.js.map +1 -0
- package/dist/transform/registry.d.ts +19 -0
- package/dist/transform/registry.d.ts.map +1 -0
- package/dist/transform/registry.js +41 -0
- package/dist/transform/registry.js.map +1 -0
- package/dist/transform/styles/dataDriven.d.ts +4 -0
- package/dist/transform/styles/dataDriven.d.ts.map +1 -0
- package/dist/transform/styles/dataDriven.js +15 -0
- package/dist/transform/styles/dataDriven.js.map +1 -0
- package/dist/transform/styles/documentary.d.ts +4 -0
- package/dist/transform/styles/documentary.d.ts.map +1 -0
- package/dist/transform/styles/documentary.js +15 -0
- package/dist/transform/styles/documentary.js.map +1 -0
- package/dist/transform/styles/index.d.ts +6 -0
- package/dist/transform/styles/index.d.ts.map +1 -0
- package/dist/transform/styles/index.js +6 -0
- package/dist/transform/styles/index.js.map +1 -0
- package/dist/transform/styles/magazine.d.ts +4 -0
- package/dist/transform/styles/magazine.d.ts.map +1 -0
- package/dist/transform/styles/magazine.js +15 -0
- package/dist/transform/styles/magazine.js.map +1 -0
- package/dist/transform/styles/minimal.d.ts +4 -0
- package/dist/transform/styles/minimal.d.ts.map +1 -0
- package/dist/transform/styles/minimal.js +15 -0
- package/dist/transform/styles/minimal.js.map +1 -0
- package/dist/transform/styles/narrative.d.ts +4 -0
- package/dist/transform/styles/narrative.d.ts.map +1 -0
- package/dist/transform/styles/narrative.js +15 -0
- package/dist/transform/styles/narrative.js.map +1 -0
- package/dist/transform/templateSelector.d.ts +30 -0
- package/dist/transform/templateSelector.d.ts.map +1 -0
- package/dist/transform/templateSelector.js +218 -0
- package/dist/transform/templateSelector.js.map +1 -0
- package/dist/transform/timingAllocator.d.ts +21 -0
- package/dist/transform/timingAllocator.d.ts.map +1 -0
- package/dist/transform/timingAllocator.js +111 -0
- package/dist/transform/timingAllocator.js.map +1 -0
- package/dist/transform/types.d.ts +86 -0
- package/dist/transform/types.d.ts.map +1 -0
- package/dist/transform/types.js +9 -0
- package/dist/transform/types.js.map +1 -0
- package/dist/{types-DHiv_Pnm.d.ts → types-DYAzisMG.d.ts} +4 -2
- package/package.json +6 -1
- package/src/__tests__/contentContainer.test.ts +282 -0
- package/src/__tests__/markdownToDoc.test.ts +96 -0
- package/src/doc/audioMapping.ts +346 -0
- package/src/doc/index.ts +1 -0
- package/src/doc/markdownToDoc.ts +136 -2
- package/src/doc/templates/comparisonBar.ts +23 -5
- package/src/doc/templates/coverBlock.ts +3 -0
- package/src/doc/templates/dateEvent.ts +4 -0
- package/src/doc/templates/definitionCard.ts +4 -1
- package/src/doc/templates/factCard.ts +4 -0
- package/src/doc/templates/fullBleedQuote.ts +2 -1
- package/src/doc/templates/imageWithCaption.ts +4 -0
- package/src/doc/templates/listBlock.ts +3 -0
- package/src/doc/templates/mapBlock.ts +3 -0
- package/src/doc/templates/photoGrid.ts +3 -0
- package/src/doc/templates/pullQuote.ts +4 -0
- package/src/doc/templates/quoteBlock.ts +4 -0
- package/src/doc/templates/sectionHeader.ts +3 -2
- package/src/doc/templates/statHighlight.ts +4 -1
- package/src/doc/templates/titleBlock.ts +3 -0
- package/src/doc/templates/twoColumn.ts +7 -1
- package/src/doc/templates/videoPullQuote.ts +4 -0
- package/src/doc/templates/videoWithCaption.ts +3 -0
- package/src/doc/utils/themeUtils.ts +29 -0
- package/src/markdown/convert.ts +17 -7
- package/src/markdown/types.ts +4 -2
- package/src/schemas/BlockTemplates.ts +2 -2
- package/src/schemas/Doc.ts +60 -0
- package/src/schemas/themeLibrary.ts +95 -26
- package/src/storage/ContentContainer.ts +200 -0
- package/src/storage/MediaProviderFromContainer.ts +92 -0
- package/src/storage/index.ts +3 -0
- package/src/transform/applyTransform.ts +83 -0
- package/src/transform/blockAnalyzer.ts +181 -0
- package/src/transform/index.ts +20 -0
- package/src/transform/registry.ts +47 -0
- package/src/transform/styles/dataDriven.ts +16 -0
- package/src/transform/styles/documentary.ts +16 -0
- package/src/transform/styles/index.ts +5 -0
- package/src/transform/styles/magazine.ts +16 -0
- package/src/transform/styles/minimal.ts +16 -0
- package/src/transform/styles/narrative.ts +16 -0
- package/src/transform/templateSelector.ts +294 -0
- package/src/transform/timingAllocator.ts +126 -0
- package/src/transform/types.ts +105 -0
- package/dist/chunk-O5HCIUAD.js.map +0 -1
- package/dist/chunk-PJ7AODIM.js.map +0 -1
- package/dist/chunk-S4F2RY37.js.map +0 -1
- package/dist/chunk-URU6QMLY.js.map +0 -1
- package/dist/chunk-VJN7UB2Z.js +0 -145
- package/dist/chunk-VJN7UB2Z.js.map +0 -1
- package/dist/chunk-VP5SZTZ2.js.map +0 -1
- /package/dist/{chunk-6YJPI6VI.js.map → chunk-4X3JQXNM.js.map} +0 -0
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
|
|
2
|
+
import { MemoryContentContainer, findDocumentPath } from '../storage/ContentContainer';
|
|
3
|
+
import { createMediaProviderFromContainer } from '../storage/MediaProviderFromContainer';
|
|
4
|
+
import type { ContentEntry } from '../storage/ContentContainer';
|
|
5
|
+
|
|
6
|
+
// ============================================
|
|
7
|
+
// MemoryContentContainer
|
|
8
|
+
// ============================================
|
|
9
|
+
|
|
10
|
+
describe('MemoryContentContainer', () => {
|
|
11
|
+
let container: MemoryContentContainer;
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
container = new MemoryContentContainer();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('readFile returns null for missing file', async () => {
|
|
18
|
+
expect(await container.readFile('missing.txt')).toBeNull();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('writeFile and readFile round-trip', async () => {
|
|
22
|
+
const data = new TextEncoder().encode('hello world');
|
|
23
|
+
await container.writeFile('test.txt', data, 'text/plain');
|
|
24
|
+
const result = await container.readFile('test.txt');
|
|
25
|
+
expect(result).not.toBeNull();
|
|
26
|
+
expect(new TextDecoder().decode(result!)).toBe('hello world');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('writeFile with ArrayBuffer', async () => {
|
|
30
|
+
const buf = new ArrayBuffer(4);
|
|
31
|
+
new Uint8Array(buf).set([1, 2, 3, 4]);
|
|
32
|
+
await container.writeFile('data.bin', buf);
|
|
33
|
+
const result = await container.readFile('data.bin');
|
|
34
|
+
expect(new Uint8Array(result!)).toEqual(new Uint8Array([1, 2, 3, 4]));
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('writeFile overwrites existing file', async () => {
|
|
38
|
+
await container.writeFile('f.txt', new TextEncoder().encode('first'));
|
|
39
|
+
await container.writeFile('f.txt', new TextEncoder().encode('second'));
|
|
40
|
+
const result = await container.readFile('f.txt');
|
|
41
|
+
expect(new TextDecoder().decode(result!)).toBe('second');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('removeFile deletes a file', async () => {
|
|
45
|
+
await container.writeFile('f.txt', new TextEncoder().encode('data'));
|
|
46
|
+
await container.removeFile('f.txt');
|
|
47
|
+
expect(await container.readFile('f.txt')).toBeNull();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('removeFile is a no-op for missing file', async () => {
|
|
51
|
+
// Should not throw
|
|
52
|
+
await container.removeFile('nonexistent.txt');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('exists returns true for existing file', async () => {
|
|
56
|
+
await container.writeFile('f.txt', new TextEncoder().encode('data'));
|
|
57
|
+
expect(await container.exists('f.txt')).toBe(true);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('exists returns false for missing file', async () => {
|
|
61
|
+
expect(await container.exists('missing.txt')).toBe(false);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('listFiles returns all files', async () => {
|
|
65
|
+
await container.writeFile('a.txt', new TextEncoder().encode('a'), 'text/plain');
|
|
66
|
+
await container.writeFile('b.jpg', new Uint8Array([0xff, 0xd8]), 'image/jpeg');
|
|
67
|
+
const entries = await container.listFiles();
|
|
68
|
+
expect(entries).toHaveLength(2);
|
|
69
|
+
expect(entries.map((e) => e.path).sort()).toEqual(['a.txt', 'b.jpg']);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('listFiles with prefix filters results', async () => {
|
|
73
|
+
await container.writeFile('index.md', new TextEncoder().encode('# Hi'));
|
|
74
|
+
await container.writeFile('images/hero.jpg', new Uint8Array([0xff]));
|
|
75
|
+
await container.writeFile('images/bg.png', new Uint8Array([0x89]));
|
|
76
|
+
await container.writeFile('audio/clip.mp3', new Uint8Array([0x49]));
|
|
77
|
+
|
|
78
|
+
const imageEntries = await container.listFiles('images/');
|
|
79
|
+
expect(imageEntries).toHaveLength(2);
|
|
80
|
+
expect(imageEntries.map((e) => e.path).sort()).toEqual(['images/bg.png', 'images/hero.jpg']);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('listFiles reports correct size', async () => {
|
|
84
|
+
const data = new Uint8Array([1, 2, 3, 4, 5]);
|
|
85
|
+
await container.writeFile('data.bin', data);
|
|
86
|
+
const entries = await container.listFiles();
|
|
87
|
+
expect(entries[0].size).toBe(5);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('listFiles guesses MIME type from extension', async () => {
|
|
91
|
+
await container.writeFile('photo.jpg', new Uint8Array([0xff]));
|
|
92
|
+
const entries = await container.listFiles();
|
|
93
|
+
expect(entries[0].mimeType).toBe('image/jpeg');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('listFiles uses provided MIME type over guess', async () => {
|
|
97
|
+
await container.writeFile('data.bin', new Uint8Array([0]), 'application/custom');
|
|
98
|
+
const entries = await container.listFiles();
|
|
99
|
+
expect(entries[0].mimeType).toBe('application/custom');
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// ============================================
|
|
104
|
+
// Document discovery
|
|
105
|
+
// ============================================
|
|
106
|
+
|
|
107
|
+
describe('getDocumentPath', () => {
|
|
108
|
+
let container: MemoryContentContainer;
|
|
109
|
+
|
|
110
|
+
beforeEach(() => {
|
|
111
|
+
container = new MemoryContentContainer();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('returns null for empty container', async () => {
|
|
115
|
+
expect(await container.getDocumentPath()).toBeNull();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('finds index.md', async () => {
|
|
119
|
+
await container.writeFile('index.md', new TextEncoder().encode('# Hello'));
|
|
120
|
+
expect(await container.getDocumentPath()).toBe('index.md');
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('prefers index.md over doc.md', async () => {
|
|
124
|
+
await container.writeFile('doc.md', new TextEncoder().encode('doc'));
|
|
125
|
+
await container.writeFile('index.md', new TextEncoder().encode('index'));
|
|
126
|
+
expect(await container.getDocumentPath()).toBe('index.md');
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('prefers doc.md over document.md', async () => {
|
|
130
|
+
await container.writeFile('document.md', new TextEncoder().encode('document'));
|
|
131
|
+
await container.writeFile('doc.md', new TextEncoder().encode('doc'));
|
|
132
|
+
expect(await container.getDocumentPath()).toBe('doc.md');
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('prefers document.md over arbitrary .md', async () => {
|
|
136
|
+
await container.writeFile('readme.md', new TextEncoder().encode('readme'));
|
|
137
|
+
await container.writeFile('document.md', new TextEncoder().encode('document'));
|
|
138
|
+
expect(await container.getDocumentPath()).toBe('document.md');
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('falls back to first .md at root', async () => {
|
|
142
|
+
await container.writeFile('my-story.md', new TextEncoder().encode('story'));
|
|
143
|
+
expect(await container.getDocumentPath()).toBe('my-story.md');
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('ignores .md files in subdirectories', async () => {
|
|
147
|
+
await container.writeFile('docs/notes.md', new TextEncoder().encode('notes'));
|
|
148
|
+
expect(await container.getDocumentPath()).toBeNull();
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('ignores non-md files', async () => {
|
|
152
|
+
await container.writeFile('data.json', new TextEncoder().encode('{}'));
|
|
153
|
+
await container.writeFile('hero.jpg', new Uint8Array([0xff]));
|
|
154
|
+
expect(await container.getDocumentPath()).toBeNull();
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
describe('readDocument / writeDocument', () => {
|
|
159
|
+
let container: MemoryContentContainer;
|
|
160
|
+
|
|
161
|
+
beforeEach(() => {
|
|
162
|
+
container = new MemoryContentContainer();
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('writeDocument creates index.md by default', async () => {
|
|
166
|
+
await container.writeDocument('# Hello');
|
|
167
|
+
expect(await container.getDocumentPath()).toBe('index.md');
|
|
168
|
+
expect(await container.readDocument()).toBe('# Hello');
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('writeDocument uses custom filename', async () => {
|
|
172
|
+
await container.writeDocument('# Custom', 'story.md');
|
|
173
|
+
expect(await container.getDocumentPath()).toBe('story.md');
|
|
174
|
+
expect(await container.readDocument()).toBe('# Custom');
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('readDocument returns null for empty container', async () => {
|
|
178
|
+
expect(await container.readDocument()).toBeNull();
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// ============================================
|
|
183
|
+
// findDocumentPath (standalone)
|
|
184
|
+
// ============================================
|
|
185
|
+
|
|
186
|
+
describe('findDocumentPath', () => {
|
|
187
|
+
it('returns null for empty list', () => {
|
|
188
|
+
expect(findDocumentPath([])).toBeNull();
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('finds index.md from entry list', () => {
|
|
192
|
+
const entries: ContentEntry[] = [
|
|
193
|
+
{ path: 'images/hero.jpg', mimeType: 'image/jpeg', size: 100 },
|
|
194
|
+
{ path: 'index.md', mimeType: 'text/markdown', size: 50 },
|
|
195
|
+
];
|
|
196
|
+
expect(findDocumentPath(entries)).toBe('index.md');
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('skips subdirectory .md files', () => {
|
|
200
|
+
const entries: ContentEntry[] = [
|
|
201
|
+
{ path: 'docs/guide.md', mimeType: 'text/markdown', size: 50 },
|
|
202
|
+
];
|
|
203
|
+
expect(findDocumentPath(entries)).toBeNull();
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// ============================================
|
|
208
|
+
// createMediaProviderFromContainer
|
|
209
|
+
// ============================================
|
|
210
|
+
|
|
211
|
+
describe('createMediaProviderFromContainer', () => {
|
|
212
|
+
let container: MemoryContentContainer;
|
|
213
|
+
let blobCounter = 0;
|
|
214
|
+
|
|
215
|
+
beforeEach(() => {
|
|
216
|
+
container = new MemoryContentContainer();
|
|
217
|
+
blobCounter = 0;
|
|
218
|
+
// Mock URL.createObjectURL / revokeObjectURL (not available in Node)
|
|
219
|
+
vi.stubGlobal('URL', {
|
|
220
|
+
...URL,
|
|
221
|
+
createObjectURL: () => `blob:mock-${++blobCounter}`,
|
|
222
|
+
revokeObjectURL: vi.fn(),
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
afterEach(() => {
|
|
227
|
+
vi.unstubAllGlobals();
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('resolveUrl returns path as-is for missing file', async () => {
|
|
231
|
+
const provider = createMediaProviderFromContainer(container);
|
|
232
|
+
expect(await provider.resolveUrl('missing.jpg')).toBe('missing.jpg');
|
|
233
|
+
provider.dispose();
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('resolveUrl returns blob URL for existing file', async () => {
|
|
237
|
+
await container.writeFile('hero.jpg', new Uint8Array([0xff, 0xd8]), 'image/jpeg');
|
|
238
|
+
const provider = createMediaProviderFromContainer(container);
|
|
239
|
+
const url = await provider.resolveUrl('hero.jpg');
|
|
240
|
+
expect(url).toMatch(/^blob:/);
|
|
241
|
+
provider.dispose();
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('resolveUrl caches blob URLs', async () => {
|
|
245
|
+
await container.writeFile('hero.jpg', new Uint8Array([0xff, 0xd8]), 'image/jpeg');
|
|
246
|
+
const provider = createMediaProviderFromContainer(container);
|
|
247
|
+
const url1 = await provider.resolveUrl('hero.jpg');
|
|
248
|
+
const url2 = await provider.resolveUrl('hero.jpg');
|
|
249
|
+
expect(url1).toBe(url2);
|
|
250
|
+
provider.dispose();
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it('listMedia excludes markdown files', async () => {
|
|
254
|
+
await container.writeFile('index.md', new TextEncoder().encode('# Doc'));
|
|
255
|
+
await container.writeFile('hero.jpg', new Uint8Array([0xff]), 'image/jpeg');
|
|
256
|
+
await container.writeFile('clip.mp4', new Uint8Array([0x00]), 'video/mp4');
|
|
257
|
+
|
|
258
|
+
const provider = createMediaProviderFromContainer(container);
|
|
259
|
+
const media = await provider.listMedia();
|
|
260
|
+
expect(media).toHaveLength(2);
|
|
261
|
+
expect(media.map((m) => m.name).sort()).toEqual(['clip.mp4', 'hero.jpg']);
|
|
262
|
+
provider.dispose();
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it('addMedia writes to container and returns path', async () => {
|
|
266
|
+
const provider = createMediaProviderFromContainer(container);
|
|
267
|
+
const path = await provider.addMedia('photo.png', new Uint8Array([0x89]), 'image/png');
|
|
268
|
+
expect(path).toBe('photo.png');
|
|
269
|
+
expect(await container.exists('photo.png')).toBe(true);
|
|
270
|
+
provider.dispose();
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it('removeMedia deletes from container', async () => {
|
|
274
|
+
await container.writeFile('hero.jpg', new Uint8Array([0xff]), 'image/jpeg');
|
|
275
|
+
const provider = createMediaProviderFromContainer(container);
|
|
276
|
+
// Resolve first to populate cache
|
|
277
|
+
await provider.resolveUrl('hero.jpg');
|
|
278
|
+
await provider.removeMedia('hero.jpg');
|
|
279
|
+
expect(await container.exists('hero.jpg')).toBe(false);
|
|
280
|
+
provider.dispose();
|
|
281
|
+
});
|
|
282
|
+
});
|
|
@@ -372,3 +372,99 @@ describe('template annotation in markdownToDoc', () => {
|
|
|
372
372
|
expect(doc.blocks[0].children![0].template).toBe('chart');
|
|
373
373
|
});
|
|
374
374
|
});
|
|
375
|
+
|
|
376
|
+
// ============================================
|
|
377
|
+
// Reading-time durations & caption generation
|
|
378
|
+
// ============================================
|
|
379
|
+
|
|
380
|
+
describe('reading-time durations', () => {
|
|
381
|
+
it('sets block duration based on body content reading time', () => {
|
|
382
|
+
// ~50 words at 200 WPM = 15 seconds
|
|
383
|
+
const body = Array.from({ length: 50 }, (_, i) => `word${i}`).join(' ');
|
|
384
|
+
const md = parseMarkdown(`# Title\n\n${body}`);
|
|
385
|
+
const doc = markdownToDoc(md);
|
|
386
|
+
|
|
387
|
+
const block = doc.blocks[0];
|
|
388
|
+
expect(block.duration).toBeGreaterThan(5); // more than default
|
|
389
|
+
expect(block.duration).toBe(15); // 50 words / 200 WPM * 60 = 15s
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
it('applies minimum 3s duration for very short content', () => {
|
|
393
|
+
const md = parseMarkdown('# Title\n\nHi.');
|
|
394
|
+
const doc = markdownToDoc(md);
|
|
395
|
+
|
|
396
|
+
expect(doc.blocks[0].duration).toBe(3);
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
it('uses defaultDuration for blocks with no body content', () => {
|
|
400
|
+
const md = parseMarkdown('# Title');
|
|
401
|
+
const doc = markdownToDoc(md);
|
|
402
|
+
|
|
403
|
+
expect(doc.blocks[0].duration).toBe(5); // default
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
it('doc.duration sums all reading-time block durations', () => {
|
|
407
|
+
// Block A: no body → 5s default. Block B: ~20 words → 6s
|
|
408
|
+
const body = Array.from({ length: 20 }, (_, i) => `word${i}`).join(' ');
|
|
409
|
+
const md = parseMarkdown(`# A\n\n# B\n\n${body}`);
|
|
410
|
+
const doc = markdownToDoc(md);
|
|
411
|
+
|
|
412
|
+
const flat = flattenBlocks(doc.blocks);
|
|
413
|
+
expect(flat[0].duration).toBe(5); // A: no body
|
|
414
|
+
expect(flat[1].duration).toBe(6); // B: 20 words / 200 WPM * 60 = 6s
|
|
415
|
+
expect(doc.duration).toBe(11);
|
|
416
|
+
});
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
describe('caption generation', () => {
|
|
420
|
+
it('generates captions from body content', () => {
|
|
421
|
+
const md = parseMarkdown('# Title\n\nThis is the first sentence. This is the second sentence.');
|
|
422
|
+
const doc = markdownToDoc(md);
|
|
423
|
+
|
|
424
|
+
expect(doc.captions).toBeDefined();
|
|
425
|
+
expect(doc.captions!.phrases.length).toBeGreaterThanOrEqual(2);
|
|
426
|
+
expect(doc.captions!.phrases[0].text).toContain('first sentence');
|
|
427
|
+
expect(doc.captions!.phrases[1].text).toContain('second sentence');
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
it('caption times span the block duration', () => {
|
|
431
|
+
const md = parseMarkdown('# Section\n\nSome text here. More text follows.');
|
|
432
|
+
const doc = markdownToDoc(md);
|
|
433
|
+
|
|
434
|
+
expect(doc.captions).toBeDefined();
|
|
435
|
+
const phrases = doc.captions!.phrases;
|
|
436
|
+
// First phrase starts at block start
|
|
437
|
+
expect(phrases[0].startTime).toBe(doc.blocks[0].startTime);
|
|
438
|
+
// Last phrase ends at block end
|
|
439
|
+
const last = phrases[phrases.length - 1];
|
|
440
|
+
expect(last.endTime).toBeCloseTo(doc.blocks[0].startTime + doc.blocks[0].duration, 1);
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
it('does not generate captions for blocks without body content', () => {
|
|
444
|
+
const md = parseMarkdown('# A\n\n# B\n\n# C');
|
|
445
|
+
const doc = markdownToDoc(md);
|
|
446
|
+
|
|
447
|
+
// No body content anywhere → no captions
|
|
448
|
+
expect(doc.captions).toBeUndefined();
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
it('generates captions version and timestamp', () => {
|
|
452
|
+
const md = parseMarkdown('# Title\n\nHello world and more text.');
|
|
453
|
+
const doc = markdownToDoc(md);
|
|
454
|
+
|
|
455
|
+
expect(doc.captions!.version).toBe(1);
|
|
456
|
+
expect(doc.captions!.generatedAt).toBeTruthy();
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
it('caption phrases have sequential non-overlapping times', () => {
|
|
460
|
+
const md = parseMarkdown(
|
|
461
|
+
'# A\n\nFirst sentence here. Second sentence here.\n\n# B\n\nThird sentence. Fourth sentence.',
|
|
462
|
+
);
|
|
463
|
+
const doc = markdownToDoc(md);
|
|
464
|
+
|
|
465
|
+
const phrases = doc.captions!.phrases;
|
|
466
|
+
for (let i = 1; i < phrases.length; i++) {
|
|
467
|
+
expect(phrases[i].startTime).toBeGreaterThanOrEqual(phrases[i - 1].endTime - 0.001);
|
|
468
|
+
}
|
|
469
|
+
});
|
|
470
|
+
});
|