@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,292 @@
|
|
|
1
|
+
// src/storage/MemoryStorageAdapter.ts
|
|
2
|
+
var MemoryStorageAdapter = class {
|
|
3
|
+
constructor() {
|
|
4
|
+
this.supportsEnumeration = true;
|
|
5
|
+
this.store = /* @__PURE__ */ new Map();
|
|
6
|
+
}
|
|
7
|
+
async get(key) {
|
|
8
|
+
const val = this.store.get(key);
|
|
9
|
+
if (val === void 0) return null;
|
|
10
|
+
try {
|
|
11
|
+
return JSON.parse(val);
|
|
12
|
+
} catch {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
async set(key, value) {
|
|
17
|
+
this.store.set(key, JSON.stringify(value));
|
|
18
|
+
}
|
|
19
|
+
async remove(key) {
|
|
20
|
+
this.store.delete(key);
|
|
21
|
+
}
|
|
22
|
+
async clear() {
|
|
23
|
+
this.store.clear();
|
|
24
|
+
}
|
|
25
|
+
async keys() {
|
|
26
|
+
return Array.from(this.store.keys());
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// src/storage/LocalStorageAdapter.ts
|
|
31
|
+
var LocalStorageAdapter = class {
|
|
32
|
+
constructor(prefix = "") {
|
|
33
|
+
this.supportsEnumeration = true;
|
|
34
|
+
this.prefix = prefix;
|
|
35
|
+
}
|
|
36
|
+
async get(key) {
|
|
37
|
+
try {
|
|
38
|
+
const raw = localStorage.getItem(this.prefix + key);
|
|
39
|
+
if (raw === null) return null;
|
|
40
|
+
return JSON.parse(raw);
|
|
41
|
+
} catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
async set(key, value) {
|
|
46
|
+
localStorage.setItem(this.prefix + key, JSON.stringify(value));
|
|
47
|
+
}
|
|
48
|
+
async remove(key) {
|
|
49
|
+
localStorage.removeItem(this.prefix + key);
|
|
50
|
+
}
|
|
51
|
+
async clear() {
|
|
52
|
+
const keysToRemove = [];
|
|
53
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
54
|
+
const k = localStorage.key(i);
|
|
55
|
+
if (k && k.startsWith(this.prefix)) {
|
|
56
|
+
keysToRemove.push(k);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
for (const k of keysToRemove) {
|
|
60
|
+
localStorage.removeItem(k);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
async keys() {
|
|
64
|
+
const result = [];
|
|
65
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
66
|
+
const k = localStorage.key(i);
|
|
67
|
+
if (k && k.startsWith(this.prefix)) {
|
|
68
|
+
result.push(k.slice(this.prefix.length));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return result;
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
// src/storage/LocalForageAdapter.ts
|
|
76
|
+
import localforage from "localforage";
|
|
77
|
+
var LocalForageAdapter = class {
|
|
78
|
+
constructor(options = {}) {
|
|
79
|
+
this.supportsEnumeration = true;
|
|
80
|
+
const {
|
|
81
|
+
name = "squisq",
|
|
82
|
+
storeName = "data",
|
|
83
|
+
prefix = "",
|
|
84
|
+
description = "Squisq storage"
|
|
85
|
+
} = options;
|
|
86
|
+
this.prefix = prefix;
|
|
87
|
+
this.store = localforage.createInstance({ name, storeName, description });
|
|
88
|
+
}
|
|
89
|
+
async get(key) {
|
|
90
|
+
try {
|
|
91
|
+
const value = await this.store.getItem(this.prefix + key);
|
|
92
|
+
return value;
|
|
93
|
+
} catch (e) {
|
|
94
|
+
console.warn("[Storage] get (IndexedDB) failed:", key, e);
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
async set(key, value) {
|
|
99
|
+
try {
|
|
100
|
+
await this.store.setItem(this.prefix + key, value);
|
|
101
|
+
} catch (e) {
|
|
102
|
+
console.error("[Storage] set (IndexedDB) failed:", key, e);
|
|
103
|
+
throw e;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
async remove(key) {
|
|
107
|
+
try {
|
|
108
|
+
await this.store.removeItem(this.prefix + key);
|
|
109
|
+
} catch (e) {
|
|
110
|
+
console.warn("[Storage] remove (IndexedDB) failed:", key, e);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
async clear() {
|
|
114
|
+
try {
|
|
115
|
+
if (this.prefix) {
|
|
116
|
+
const keys = await this.store.keys();
|
|
117
|
+
const keysToRemove = keys.filter((k) => k.startsWith(this.prefix));
|
|
118
|
+
await Promise.all(keysToRemove.map((k) => this.store.removeItem(k)));
|
|
119
|
+
} else {
|
|
120
|
+
await this.store.clear();
|
|
121
|
+
}
|
|
122
|
+
} catch (e) {
|
|
123
|
+
console.error("[Storage] clear (IndexedDB) failed:", e);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
async keys() {
|
|
127
|
+
try {
|
|
128
|
+
const allKeys = await this.store.keys();
|
|
129
|
+
if (this.prefix) {
|
|
130
|
+
return allKeys.filter((k) => k.startsWith(this.prefix)).map((k) => k.slice(this.prefix.length));
|
|
131
|
+
}
|
|
132
|
+
return allKeys;
|
|
133
|
+
} catch (e) {
|
|
134
|
+
console.warn("[Storage] keys (IndexedDB) failed:", e);
|
|
135
|
+
return [];
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
// src/storage/ContentContainer.ts
|
|
141
|
+
var MARKDOWN_PRIORITY = ["index.md", "doc.md", "document.md"];
|
|
142
|
+
function findDocumentPath(entries) {
|
|
143
|
+
const rootFiles = entries.filter((e) => !e.path.includes("/"));
|
|
144
|
+
for (const name of MARKDOWN_PRIORITY) {
|
|
145
|
+
if (rootFiles.some((e) => e.path.toLowerCase() === name)) {
|
|
146
|
+
return name;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
const firstMd = rootFiles.find((e) => e.path.toLowerCase().endsWith(".md"));
|
|
150
|
+
return firstMd?.path ?? null;
|
|
151
|
+
}
|
|
152
|
+
var MemoryContentContainer = class {
|
|
153
|
+
constructor() {
|
|
154
|
+
this.files = /* @__PURE__ */ new Map();
|
|
155
|
+
}
|
|
156
|
+
async readFile(path) {
|
|
157
|
+
return this.files.get(path)?.data ?? null;
|
|
158
|
+
}
|
|
159
|
+
async writeFile(path, data, mimeType) {
|
|
160
|
+
const buffer = data instanceof ArrayBuffer ? data : data.slice().buffer;
|
|
161
|
+
this.files.set(path, {
|
|
162
|
+
data: buffer,
|
|
163
|
+
mimeType: mimeType ?? guessMimeType(path)
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
async removeFile(path) {
|
|
167
|
+
this.files.delete(path);
|
|
168
|
+
}
|
|
169
|
+
async listFiles(prefix) {
|
|
170
|
+
const entries = [];
|
|
171
|
+
for (const [path, file] of this.files) {
|
|
172
|
+
if (prefix && !path.startsWith(prefix)) continue;
|
|
173
|
+
entries.push({
|
|
174
|
+
path,
|
|
175
|
+
mimeType: file.mimeType,
|
|
176
|
+
size: file.data.byteLength
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
return entries;
|
|
180
|
+
}
|
|
181
|
+
async exists(path) {
|
|
182
|
+
return this.files.has(path);
|
|
183
|
+
}
|
|
184
|
+
async getDocumentPath() {
|
|
185
|
+
return findDocumentPath(await this.listFiles());
|
|
186
|
+
}
|
|
187
|
+
async readDocument() {
|
|
188
|
+
const docPath = await this.getDocumentPath();
|
|
189
|
+
if (!docPath) return null;
|
|
190
|
+
const data = await this.readFile(docPath);
|
|
191
|
+
if (!data) return null;
|
|
192
|
+
return new TextDecoder().decode(data);
|
|
193
|
+
}
|
|
194
|
+
async writeDocument(markdown, filename) {
|
|
195
|
+
const name = filename ?? "index.md";
|
|
196
|
+
const data = new TextEncoder().encode(markdown);
|
|
197
|
+
await this.writeFile(name, data, "text/markdown");
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
var EXTENSION_MIME_MAP = {
|
|
201
|
+
".md": "text/markdown",
|
|
202
|
+
".txt": "text/plain",
|
|
203
|
+
".json": "application/json",
|
|
204
|
+
".jpg": "image/jpeg",
|
|
205
|
+
".jpeg": "image/jpeg",
|
|
206
|
+
".png": "image/png",
|
|
207
|
+
".gif": "image/gif",
|
|
208
|
+
".svg": "image/svg+xml",
|
|
209
|
+
".webp": "image/webp",
|
|
210
|
+
".avif": "image/avif",
|
|
211
|
+
".mp4": "video/mp4",
|
|
212
|
+
".webm": "video/webm",
|
|
213
|
+
".mp3": "audio/mpeg",
|
|
214
|
+
".wav": "audio/wav",
|
|
215
|
+
".ogg": "audio/ogg",
|
|
216
|
+
".css": "text/css",
|
|
217
|
+
".html": "text/html",
|
|
218
|
+
".js": "application/javascript"
|
|
219
|
+
};
|
|
220
|
+
function guessMimeType(path) {
|
|
221
|
+
const dot = path.lastIndexOf(".");
|
|
222
|
+
if (dot === -1) return "application/octet-stream";
|
|
223
|
+
const ext = path.slice(dot).toLowerCase();
|
|
224
|
+
return EXTENSION_MIME_MAP[ext] ?? "application/octet-stream";
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// src/storage/MediaProviderFromContainer.ts
|
|
228
|
+
function createMediaProviderFromContainer(container) {
|
|
229
|
+
const blobUrlCache = /* @__PURE__ */ new Map();
|
|
230
|
+
return {
|
|
231
|
+
async resolveUrl(relativePath) {
|
|
232
|
+
const cached = blobUrlCache.get(relativePath);
|
|
233
|
+
if (cached) return cached;
|
|
234
|
+
const data = await container.readFile(relativePath);
|
|
235
|
+
if (!data) return relativePath;
|
|
236
|
+
const entries = await container.listFiles();
|
|
237
|
+
const entry = entries.find((e) => e.path === relativePath);
|
|
238
|
+
const mimeType = entry?.mimeType ?? "application/octet-stream";
|
|
239
|
+
const blob = new Blob([data], { type: mimeType });
|
|
240
|
+
const url = URL.createObjectURL(blob);
|
|
241
|
+
blobUrlCache.set(relativePath, url);
|
|
242
|
+
return url;
|
|
243
|
+
},
|
|
244
|
+
async listMedia() {
|
|
245
|
+
const entries = await container.listFiles();
|
|
246
|
+
return entries.filter((e) => !e.path.toLowerCase().endsWith(".md")).map((e) => ({
|
|
247
|
+
name: e.path,
|
|
248
|
+
mimeType: e.mimeType,
|
|
249
|
+
size: e.size
|
|
250
|
+
}));
|
|
251
|
+
},
|
|
252
|
+
async addMedia(name, data, mimeType) {
|
|
253
|
+
const cached = blobUrlCache.get(name);
|
|
254
|
+
if (cached) {
|
|
255
|
+
URL.revokeObjectURL(cached);
|
|
256
|
+
blobUrlCache.delete(name);
|
|
257
|
+
}
|
|
258
|
+
let buffer;
|
|
259
|
+
if (data instanceof Blob) {
|
|
260
|
+
buffer = await data.arrayBuffer();
|
|
261
|
+
} else {
|
|
262
|
+
buffer = data;
|
|
263
|
+
}
|
|
264
|
+
await container.writeFile(name, buffer, mimeType);
|
|
265
|
+
return name;
|
|
266
|
+
},
|
|
267
|
+
async removeMedia(relativePath) {
|
|
268
|
+
const cached = blobUrlCache.get(relativePath);
|
|
269
|
+
if (cached) {
|
|
270
|
+
URL.revokeObjectURL(cached);
|
|
271
|
+
blobUrlCache.delete(relativePath);
|
|
272
|
+
}
|
|
273
|
+
await container.removeFile(relativePath);
|
|
274
|
+
},
|
|
275
|
+
dispose() {
|
|
276
|
+
for (const url of blobUrlCache.values()) {
|
|
277
|
+
URL.revokeObjectURL(url);
|
|
278
|
+
}
|
|
279
|
+
blobUrlCache.clear();
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export {
|
|
285
|
+
MemoryStorageAdapter,
|
|
286
|
+
LocalStorageAdapter,
|
|
287
|
+
LocalForageAdapter,
|
|
288
|
+
findDocumentPath,
|
|
289
|
+
MemoryContentContainer,
|
|
290
|
+
createMediaProviderFromContainer
|
|
291
|
+
};
|
|
292
|
+
//# sourceMappingURL=chunk-4CRBS35L.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/storage/MemoryStorageAdapter.ts","../src/storage/LocalStorageAdapter.ts","../src/storage/LocalForageAdapter.ts","../src/storage/ContentContainer.ts","../src/storage/MediaProviderFromContainer.ts"],"sourcesContent":["/**\n * In-memory storage adapter for testing and SSR environments.\n * Data does not persist across page reloads.\n */\n\nimport type { StorageAdapter } from './Storage.js';\n\nexport class MemoryStorageAdapter implements StorageAdapter {\n readonly supportsEnumeration = true;\n private store = new Map<string, string>();\n\n async get<T>(key: string): Promise<T | null> {\n const val = this.store.get(key);\n if (val === undefined) return null;\n try {\n return JSON.parse(val) as T;\n } catch {\n return null;\n }\n }\n\n async set<T>(key: string, value: T): Promise<void> {\n this.store.set(key, JSON.stringify(value));\n }\n\n async remove(key: string): Promise<void> {\n this.store.delete(key);\n }\n\n async clear(): Promise<void> {\n this.store.clear();\n }\n\n async keys(): Promise<string[]> {\n return Array.from(this.store.keys());\n }\n}\n","/**\n * localStorage adapter for web browsers.\n * Supports a configurable key prefix to namespace stored data.\n */\n\nimport type { StorageAdapter } from './Storage.js';\n\nexport class LocalStorageAdapter implements StorageAdapter {\n readonly supportsEnumeration = true;\n private prefix: string;\n\n constructor(prefix = '') {\n this.prefix = prefix;\n }\n\n async get<T>(key: string): Promise<T | null> {\n try {\n const raw = localStorage.getItem(this.prefix + key);\n if (raw === null) return null;\n return JSON.parse(raw) as T;\n } catch {\n return null;\n }\n }\n\n async set<T>(key: string, value: T): Promise<void> {\n localStorage.setItem(this.prefix + key, JSON.stringify(value));\n }\n\n async remove(key: string): Promise<void> {\n localStorage.removeItem(this.prefix + key);\n }\n\n async clear(): Promise<void> {\n const keysToRemove: string[] = [];\n for (let i = 0; i < localStorage.length; i++) {\n const k = localStorage.key(i);\n if (k && k.startsWith(this.prefix)) {\n keysToRemove.push(k);\n }\n }\n for (const k of keysToRemove) {\n localStorage.removeItem(k);\n }\n }\n\n async keys(): Promise<string[]> {\n const result: string[] = [];\n for (let i = 0; i < localStorage.length; i++) {\n const k = localStorage.key(i);\n if (k && k.startsWith(this.prefix)) {\n result.push(k.slice(this.prefix.length));\n }\n }\n return result;\n }\n}\n","/**\n * LocalForage adapter - IndexedDB storage with automatic fallbacks.\n *\n * Uses localforage which provides IndexedDB storage with automatic\n * fallbacks to WebSQL and localStorage. This allows storing much more\n * data than raw localStorage (which is limited to ~5MB).\n *\n * Unlike LocalStorageAdapter, this adapter can store binary data\n * (ArrayBuffer, Blob, Uint8Array) natively without JSON serialization.\n *\n * Ported from qualla-internal/shared/services/StorageAdapters.ts with\n * configurable store name and key prefix.\n */\n\nimport localforage from 'localforage';\nimport type { StorageAdapter } from './Storage.js';\n\nexport interface LocalForageAdapterOptions {\n /** IndexedDB database name (default: 'squisq') */\n name?: string;\n /** IndexedDB object store name (default: 'data') */\n storeName?: string;\n /** Key prefix for namespacing (default: '') */\n prefix?: string;\n /** Human-readable description (default: 'Squisq storage') */\n description?: string;\n}\n\nexport class LocalForageAdapter implements StorageAdapter {\n readonly supportsEnumeration = true;\n private store: LocalForage;\n private prefix: string;\n\n constructor(options: LocalForageAdapterOptions = {}) {\n const {\n name = 'squisq',\n storeName = 'data',\n prefix = '',\n description = 'Squisq storage',\n } = options;\n\n this.prefix = prefix;\n this.store = localforage.createInstance({ name, storeName, description });\n }\n\n async get<T>(key: string): Promise<T | null> {\n try {\n const value = await this.store.getItem<T>(this.prefix + key);\n return value;\n } catch (e: unknown) {\n console.warn('[Storage] get (IndexedDB) failed:', key, e);\n return null;\n }\n }\n\n async set<T>(key: string, value: T): Promise<void> {\n try {\n await this.store.setItem(this.prefix + key, value);\n } catch (e: unknown) {\n console.error('[Storage] set (IndexedDB) failed:', key, e);\n throw e;\n }\n }\n\n async remove(key: string): Promise<void> {\n try {\n await this.store.removeItem(this.prefix + key);\n } catch (e: unknown) {\n console.warn('[Storage] remove (IndexedDB) failed:', key, e);\n }\n }\n\n async clear(): Promise<void> {\n try {\n if (this.prefix) {\n // Only remove keys with our prefix\n const keys = await this.store.keys();\n const keysToRemove = keys.filter((k) => k.startsWith(this.prefix));\n await Promise.all(keysToRemove.map((k) => this.store.removeItem(k)));\n } else {\n // No prefix — clear the entire store\n await this.store.clear();\n }\n } catch (e: unknown) {\n console.error('[Storage] clear (IndexedDB) failed:', e);\n }\n }\n\n async keys(): Promise<string[]> {\n try {\n const allKeys = await this.store.keys();\n if (this.prefix) {\n return allKeys\n .filter((k) => k.startsWith(this.prefix))\n .map((k) => k.slice(this.prefix.length));\n }\n return allKeys;\n } catch (e: unknown) {\n console.warn('[Storage] keys (IndexedDB) failed:', e);\n return [];\n }\n }\n}\n","/**\n * ContentContainer — virtual file system for document containers.\n *\n * A Squisq document is more than just markdown: it's a container of files\n * including the primary markdown document, images, audio, timing data, and\n * other media. ContentContainer provides an abstract async file system\n * interface for reading, writing, and listing these files.\n *\n * Paths are forward-slash separated strings relative to the container root\n * (e.g., 'images/hero.jpg', 'index.md'). No leading slash.\n *\n * Implementations:\n * - MemoryContentContainer — in-memory Map (for zip import, tests, transient use)\n * - SlotContentContainer — backed by IndexedDB slot storage (in the site package)\n */\n\n/**\n * Metadata about a file in a ContentContainer.\n */\nexport interface ContentEntry {\n /** Relative path within the container (e.g., 'images/hero.jpg') */\n path: string;\n /** MIME type (e.g., 'image/jpeg') */\n mimeType: string;\n /** File size in bytes */\n size: number;\n}\n\n/**\n * Abstract async file system for document containers.\n *\n * All paths are forward-slash separated, relative to the container root,\n * with no leading slash. Example: 'images/hero.jpg', 'index.md'.\n */\nexport interface ContentContainer {\n /** Read a file's binary content. Returns null if the file does not exist. */\n readFile(path: string): Promise<ArrayBuffer | null>;\n\n /** Write a file. Creates or overwrites. */\n writeFile(path: string, data: ArrayBuffer | Uint8Array, mimeType?: string): Promise<void>;\n\n /** Remove a file. No-op if the file does not exist. */\n removeFile(path: string): Promise<void>;\n\n /**\n * List files in the container.\n * @param prefix — Optional path prefix to filter by (e.g., 'images/')\n */\n listFiles(prefix?: string): Promise<ContentEntry[]>;\n\n /** Check whether a file exists. */\n exists(path: string): Promise<boolean>;\n\n /**\n * Find the primary markdown document path.\n *\n * Discovery order: index.md → doc.md → document.md → first *.md at root.\n * Returns null if no markdown file is found at the root level.\n */\n getDocumentPath(): Promise<string | null>;\n\n /**\n * Convenience: read the primary markdown document as a UTF-8 string.\n * Returns null if no markdown file is found.\n */\n readDocument(): Promise<string | null>;\n\n /**\n * Convenience: write a markdown document.\n * @param markdown — The markdown content\n * @param filename — Filename to use (defaults to 'index.md')\n */\n writeDocument(markdown: string, filename?: string): Promise<void>;\n}\n\n// ============================================\n// Well-known markdown filenames in priority order\n// ============================================\n\nconst MARKDOWN_PRIORITY = ['index.md', 'doc.md', 'document.md'];\n\n/**\n * Find the primary markdown path from a list of file entries.\n * Exported for reuse by other ContentContainer implementations.\n */\nexport function findDocumentPath(entries: ContentEntry[]): string | null {\n // Only consider root-level files (no '/' in path)\n const rootFiles = entries.filter((e) => !e.path.includes('/'));\n\n for (const name of MARKDOWN_PRIORITY) {\n if (rootFiles.some((e) => e.path.toLowerCase() === name)) {\n return name;\n }\n }\n\n // Fallback: first .md file at root\n const firstMd = rootFiles.find((e) => e.path.toLowerCase().endsWith('.md'));\n return firstMd?.path ?? null;\n}\n\n// ============================================\n// MemoryContentContainer\n// ============================================\n\ninterface MemoryFile {\n data: ArrayBuffer;\n mimeType: string;\n}\n\n/**\n * In-memory ContentContainer backed by a Map.\n *\n * Used for zip import (deserialize into memory), tests, and transient operations.\n */\nexport class MemoryContentContainer implements ContentContainer {\n private files = new Map<string, MemoryFile>();\n\n async readFile(path: string): Promise<ArrayBuffer | null> {\n return this.files.get(path)?.data ?? null;\n }\n\n async writeFile(path: string, data: ArrayBuffer | Uint8Array, mimeType?: string): Promise<void> {\n const buffer = data instanceof ArrayBuffer ? data : data.slice().buffer;\n this.files.set(path, {\n data: buffer,\n mimeType: mimeType ?? guessMimeType(path),\n });\n }\n\n async removeFile(path: string): Promise<void> {\n this.files.delete(path);\n }\n\n async listFiles(prefix?: string): Promise<ContentEntry[]> {\n const entries: ContentEntry[] = [];\n for (const [path, file] of this.files) {\n if (prefix && !path.startsWith(prefix)) continue;\n entries.push({\n path,\n mimeType: file.mimeType,\n size: file.data.byteLength,\n });\n }\n return entries;\n }\n\n async exists(path: string): Promise<boolean> {\n return this.files.has(path);\n }\n\n async getDocumentPath(): Promise<string | null> {\n return findDocumentPath(await this.listFiles());\n }\n\n async readDocument(): Promise<string | null> {\n const docPath = await this.getDocumentPath();\n if (!docPath) return null;\n const data = await this.readFile(docPath);\n if (!data) return null;\n return new TextDecoder().decode(data);\n }\n\n async writeDocument(markdown: string, filename?: string): Promise<void> {\n const name = filename ?? 'index.md';\n const data = new TextEncoder().encode(markdown);\n await this.writeFile(name, data, 'text/markdown');\n }\n}\n\n// ============================================\n// MIME type guessing\n// ============================================\n\nconst EXTENSION_MIME_MAP: Record<string, string> = {\n '.md': 'text/markdown',\n '.txt': 'text/plain',\n '.json': 'application/json',\n '.jpg': 'image/jpeg',\n '.jpeg': 'image/jpeg',\n '.png': 'image/png',\n '.gif': 'image/gif',\n '.svg': 'image/svg+xml',\n '.webp': 'image/webp',\n '.avif': 'image/avif',\n '.mp4': 'video/mp4',\n '.webm': 'video/webm',\n '.mp3': 'audio/mpeg',\n '.wav': 'audio/wav',\n '.ogg': 'audio/ogg',\n '.css': 'text/css',\n '.html': 'text/html',\n '.js': 'application/javascript',\n};\n\nfunction guessMimeType(path: string): string {\n const dot = path.lastIndexOf('.');\n if (dot === -1) return 'application/octet-stream';\n const ext = path.slice(dot).toLowerCase();\n return EXTENSION_MIME_MAP[ext] ?? 'application/octet-stream';\n}\n","/**\n * MediaProviderFromContainer — bridges ContentContainer to MediaProvider.\n *\n * Creates a MediaProvider that resolves relative paths by reading binary data\n * from a ContentContainer and generating blob URLs. Blob URLs are cached and\n * revoked on dispose().\n *\n * This allows any ContentContainer (memory, slot-backed, zip-loaded) to be\n * used with existing rendering components (DocPlayer, ImageLayer, VideoLayer)\n * that consume MediaProvider.\n */\n\nimport type { MediaProvider, MediaEntry } from '../schemas/MediaProvider.js';\nimport type { ContentContainer } from './ContentContainer.js';\n\n/**\n * Create a MediaProvider backed by a ContentContainer.\n *\n * @param container — The ContentContainer to read/write media from\n * @returns A MediaProvider that resolves paths to blob URLs\n */\nexport function createMediaProviderFromContainer(container: ContentContainer): MediaProvider {\n const blobUrlCache = new Map<string, string>();\n\n return {\n async resolveUrl(relativePath: string): Promise<string> {\n const cached = blobUrlCache.get(relativePath);\n if (cached) return cached;\n\n const data = await container.readFile(relativePath);\n if (!data) return relativePath;\n\n const entries = await container.listFiles();\n const entry = entries.find((e) => e.path === relativePath);\n const mimeType = entry?.mimeType ?? 'application/octet-stream';\n\n const blob = new Blob([data], { type: mimeType });\n const url = URL.createObjectURL(blob);\n blobUrlCache.set(relativePath, url);\n return url;\n },\n\n async listMedia(): Promise<MediaEntry[]> {\n const entries = await container.listFiles();\n return entries\n .filter((e) => !e.path.toLowerCase().endsWith('.md'))\n .map((e) => ({\n name: e.path,\n mimeType: e.mimeType,\n size: e.size,\n }));\n },\n\n async addMedia(\n name: string,\n data: ArrayBuffer | Blob | Uint8Array,\n mimeType: string,\n ): Promise<string> {\n // Invalidate any cached blob URL for this path before overwriting\n const cached = blobUrlCache.get(name);\n if (cached) {\n URL.revokeObjectURL(cached);\n blobUrlCache.delete(name);\n }\n\n let buffer: ArrayBuffer | Uint8Array;\n if (data instanceof Blob) {\n buffer = await data.arrayBuffer();\n } else {\n buffer = data;\n }\n await container.writeFile(name, buffer, mimeType);\n return name;\n },\n\n async removeMedia(relativePath: string): Promise<void> {\n const cached = blobUrlCache.get(relativePath);\n if (cached) {\n URL.revokeObjectURL(cached);\n blobUrlCache.delete(relativePath);\n }\n await container.removeFile(relativePath);\n },\n\n dispose(): void {\n for (const url of blobUrlCache.values()) {\n URL.revokeObjectURL(url);\n }\n blobUrlCache.clear();\n },\n };\n}\n"],"mappings":";AAOO,IAAM,uBAAN,MAAqD;AAAA,EAArD;AACL,SAAS,sBAAsB;AAC/B,SAAQ,QAAQ,oBAAI,IAAoB;AAAA;AAAA,EAExC,MAAM,IAAO,KAAgC;AAC3C,UAAM,MAAM,KAAK,MAAM,IAAI,GAAG;AAC9B,QAAI,QAAQ,OAAW,QAAO;AAC9B,QAAI;AACF,aAAO,KAAK,MAAM,GAAG;AAAA,IACvB,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AAAA,EAEA,MAAM,IAAO,KAAa,OAAyB;AACjD,SAAK,MAAM,IAAI,KAAK,KAAK,UAAU,KAAK,CAAC;AAAA,EAC3C;AAAA,EAEA,MAAM,OAAO,KAA4B;AACvC,SAAK,MAAM,OAAO,GAAG;AAAA,EACvB;AAAA,EAEA,MAAM,QAAuB;AAC3B,SAAK,MAAM,MAAM;AAAA,EACnB;AAAA,EAEA,MAAM,OAA0B;AAC9B,WAAO,MAAM,KAAK,KAAK,MAAM,KAAK,CAAC;AAAA,EACrC;AACF;;;AC7BO,IAAM,sBAAN,MAAoD;AAAA,EAIzD,YAAY,SAAS,IAAI;AAHzB,SAAS,sBAAsB;AAI7B,SAAK,SAAS;AAAA,EAChB;AAAA,EAEA,MAAM,IAAO,KAAgC;AAC3C,QAAI;AACF,YAAM,MAAM,aAAa,QAAQ,KAAK,SAAS,GAAG;AAClD,UAAI,QAAQ,KAAM,QAAO;AACzB,aAAO,KAAK,MAAM,GAAG;AAAA,IACvB,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AAAA,EAEA,MAAM,IAAO,KAAa,OAAyB;AACjD,iBAAa,QAAQ,KAAK,SAAS,KAAK,KAAK,UAAU,KAAK,CAAC;AAAA,EAC/D;AAAA,EAEA,MAAM,OAAO,KAA4B;AACvC,iBAAa,WAAW,KAAK,SAAS,GAAG;AAAA,EAC3C;AAAA,EAEA,MAAM,QAAuB;AAC3B,UAAM,eAAyB,CAAC;AAChC,aAAS,IAAI,GAAG,IAAI,aAAa,QAAQ,KAAK;AAC5C,YAAM,IAAI,aAAa,IAAI,CAAC;AAC5B,UAAI,KAAK,EAAE,WAAW,KAAK,MAAM,GAAG;AAClC,qBAAa,KAAK,CAAC;AAAA,MACrB;AAAA,IACF;AACA,eAAW,KAAK,cAAc;AAC5B,mBAAa,WAAW,CAAC;AAAA,IAC3B;AAAA,EACF;AAAA,EAEA,MAAM,OAA0B;AAC9B,UAAM,SAAmB,CAAC;AAC1B,aAAS,IAAI,GAAG,IAAI,aAAa,QAAQ,KAAK;AAC5C,YAAM,IAAI,aAAa,IAAI,CAAC;AAC5B,UAAI,KAAK,EAAE,WAAW,KAAK,MAAM,GAAG;AAClC,eAAO,KAAK,EAAE,MAAM,KAAK,OAAO,MAAM,CAAC;AAAA,MACzC;AAAA,IACF;AACA,WAAO;AAAA,EACT;AACF;;;AC1CA,OAAO,iBAAiB;AAcjB,IAAM,qBAAN,MAAmD;AAAA,EAKxD,YAAY,UAAqC,CAAC,GAAG;AAJrD,SAAS,sBAAsB;AAK7B,UAAM;AAAA,MACJ,OAAO;AAAA,MACP,YAAY;AAAA,MACZ,SAAS;AAAA,MACT,cAAc;AAAA,IAChB,IAAI;AAEJ,SAAK,SAAS;AACd,SAAK,QAAQ,YAAY,eAAe,EAAE,MAAM,WAAW,YAAY,CAAC;AAAA,EAC1E;AAAA,EAEA,MAAM,IAAO,KAAgC;AAC3C,QAAI;AACF,YAAM,QAAQ,MAAM,KAAK,MAAM,QAAW,KAAK,SAAS,GAAG;AAC3D,aAAO;AAAA,IACT,SAAS,GAAY;AACnB,cAAQ,KAAK,qCAAqC,KAAK,CAAC;AACxD,aAAO;AAAA,IACT;AAAA,EACF;AAAA,EAEA,MAAM,IAAO,KAAa,OAAyB;AACjD,QAAI;AACF,YAAM,KAAK,MAAM,QAAQ,KAAK,SAAS,KAAK,KAAK;AAAA,IACnD,SAAS,GAAY;AACnB,cAAQ,MAAM,qCAAqC,KAAK,CAAC;AACzD,YAAM;AAAA,IACR;AAAA,EACF;AAAA,EAEA,MAAM,OAAO,KAA4B;AACvC,QAAI;AACF,YAAM,KAAK,MAAM,WAAW,KAAK,SAAS,GAAG;AAAA,IAC/C,SAAS,GAAY;AACnB,cAAQ,KAAK,wCAAwC,KAAK,CAAC;AAAA,IAC7D;AAAA,EACF;AAAA,EAEA,MAAM,QAAuB;AAC3B,QAAI;AACF,UAAI,KAAK,QAAQ;AAEf,cAAM,OAAO,MAAM,KAAK,MAAM,KAAK;AACnC,cAAM,eAAe,KAAK,OAAO,CAAC,MAAM,EAAE,WAAW,KAAK,MAAM,CAAC;AACjE,cAAM,QAAQ,IAAI,aAAa,IAAI,CAAC,MAAM,KAAK,MAAM,WAAW,CAAC,CAAC,CAAC;AAAA,MACrE,OAAO;AAEL,cAAM,KAAK,MAAM,MAAM;AAAA,MACzB;AAAA,IACF,SAAS,GAAY;AACnB,cAAQ,MAAM,uCAAuC,CAAC;AAAA,IACxD;AAAA,EACF;AAAA,EAEA,MAAM,OAA0B;AAC9B,QAAI;AACF,YAAM,UAAU,MAAM,KAAK,MAAM,KAAK;AACtC,UAAI,KAAK,QAAQ;AACf,eAAO,QACJ,OAAO,CAAC,MAAM,EAAE,WAAW,KAAK,MAAM,CAAC,EACvC,IAAI,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,MAAM,CAAC;AAAA,MAC3C;AACA,aAAO;AAAA,IACT,SAAS,GAAY;AACnB,cAAQ,KAAK,sCAAsC,CAAC;AACpD,aAAO,CAAC;AAAA,IACV;AAAA,EACF;AACF;;;ACvBA,IAAM,oBAAoB,CAAC,YAAY,UAAU,aAAa;AAMvD,SAAS,iBAAiB,SAAwC;AAEvE,QAAM,YAAY,QAAQ,OAAO,CAAC,MAAM,CAAC,EAAE,KAAK,SAAS,GAAG,CAAC;AAE7D,aAAW,QAAQ,mBAAmB;AACpC,QAAI,UAAU,KAAK,CAAC,MAAM,EAAE,KAAK,YAAY,MAAM,IAAI,GAAG;AACxD,aAAO;AAAA,IACT;AAAA,EACF;AAGA,QAAM,UAAU,UAAU,KAAK,CAAC,MAAM,EAAE,KAAK,YAAY,EAAE,SAAS,KAAK,CAAC;AAC1E,SAAO,SAAS,QAAQ;AAC1B;AAgBO,IAAM,yBAAN,MAAyD;AAAA,EAAzD;AACL,SAAQ,QAAQ,oBAAI,IAAwB;AAAA;AAAA,EAE5C,MAAM,SAAS,MAA2C;AACxD,WAAO,KAAK,MAAM,IAAI,IAAI,GAAG,QAAQ;AAAA,EACvC;AAAA,EAEA,MAAM,UAAU,MAAc,MAAgC,UAAkC;AAC9F,UAAM,SAAS,gBAAgB,cAAc,OAAO,KAAK,MAAM,EAAE;AACjE,SAAK,MAAM,IAAI,MAAM;AAAA,MACnB,MAAM;AAAA,MACN,UAAU,YAAY,cAAc,IAAI;AAAA,IAC1C,CAAC;AAAA,EACH;AAAA,EAEA,MAAM,WAAW,MAA6B;AAC5C,SAAK,MAAM,OAAO,IAAI;AAAA,EACxB;AAAA,EAEA,MAAM,UAAU,QAA0C;AACxD,UAAM,UAA0B,CAAC;AACjC,eAAW,CAAC,MAAM,IAAI,KAAK,KAAK,OAAO;AACrC,UAAI,UAAU,CAAC,KAAK,WAAW,MAAM,EAAG;AACxC,cAAQ,KAAK;AAAA,QACX;AAAA,QACA,UAAU,KAAK;AAAA,QACf,MAAM,KAAK,KAAK;AAAA,MAClB,CAAC;AAAA,IACH;AACA,WAAO;AAAA,EACT;AAAA,EAEA,MAAM,OAAO,MAAgC;AAC3C,WAAO,KAAK,MAAM,IAAI,IAAI;AAAA,EAC5B;AAAA,EAEA,MAAM,kBAA0C;AAC9C,WAAO,iBAAiB,MAAM,KAAK,UAAU,CAAC;AAAA,EAChD;AAAA,EAEA,MAAM,eAAuC;AAC3C,UAAM,UAAU,MAAM,KAAK,gBAAgB;AAC3C,QAAI,CAAC,QAAS,QAAO;AACrB,UAAM,OAAO,MAAM,KAAK,SAAS,OAAO;AACxC,QAAI,CAAC,KAAM,QAAO;AAClB,WAAO,IAAI,YAAY,EAAE,OAAO,IAAI;AAAA,EACtC;AAAA,EAEA,MAAM,cAAc,UAAkB,UAAkC;AACtE,UAAM,OAAO,YAAY;AACzB,UAAM,OAAO,IAAI,YAAY,EAAE,OAAO,QAAQ;AAC9C,UAAM,KAAK,UAAU,MAAM,MAAM,eAAe;AAAA,EAClD;AACF;AAMA,IAAM,qBAA6C;AAAA,EACjD,OAAO;AAAA,EACP,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,SAAS;AAAA,EACT,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,OAAO;AACT;AAEA,SAAS,cAAc,MAAsB;AAC3C,QAAM,MAAM,KAAK,YAAY,GAAG;AAChC,MAAI,QAAQ,GAAI,QAAO;AACvB,QAAM,MAAM,KAAK,MAAM,GAAG,EAAE,YAAY;AACxC,SAAO,mBAAmB,GAAG,KAAK;AACpC;;;AClLO,SAAS,iCAAiC,WAA4C;AAC3F,QAAM,eAAe,oBAAI,IAAoB;AAE7C,SAAO;AAAA,IACL,MAAM,WAAW,cAAuC;AACtD,YAAM,SAAS,aAAa,IAAI,YAAY;AAC5C,UAAI,OAAQ,QAAO;AAEnB,YAAM,OAAO,MAAM,UAAU,SAAS,YAAY;AAClD,UAAI,CAAC,KAAM,QAAO;AAElB,YAAM,UAAU,MAAM,UAAU,UAAU;AAC1C,YAAM,QAAQ,QAAQ,KAAK,CAAC,MAAM,EAAE,SAAS,YAAY;AACzD,YAAM,WAAW,OAAO,YAAY;AAEpC,YAAM,OAAO,IAAI,KAAK,CAAC,IAAI,GAAG,EAAE,MAAM,SAAS,CAAC;AAChD,YAAM,MAAM,IAAI,gBAAgB,IAAI;AACpC,mBAAa,IAAI,cAAc,GAAG;AAClC,aAAO;AAAA,IACT;AAAA,IAEA,MAAM,YAAmC;AACvC,YAAM,UAAU,MAAM,UAAU,UAAU;AAC1C,aAAO,QACJ,OAAO,CAAC,MAAM,CAAC,EAAE,KAAK,YAAY,EAAE,SAAS,KAAK,CAAC,EACnD,IAAI,CAAC,OAAO;AAAA,QACX,MAAM,EAAE;AAAA,QACR,UAAU,EAAE;AAAA,QACZ,MAAM,EAAE;AAAA,MACV,EAAE;AAAA,IACN;AAAA,IAEA,MAAM,SACJ,MACA,MACA,UACiB;AAEjB,YAAM,SAAS,aAAa,IAAI,IAAI;AACpC,UAAI,QAAQ;AACV,YAAI,gBAAgB,MAAM;AAC1B,qBAAa,OAAO,IAAI;AAAA,MAC1B;AAEA,UAAI;AACJ,UAAI,gBAAgB,MAAM;AACxB,iBAAS,MAAM,KAAK,YAAY;AAAA,MAClC,OAAO;AACL,iBAAS;AAAA,MACX;AACA,YAAM,UAAU,UAAU,MAAM,QAAQ,QAAQ;AAChD,aAAO;AAAA,IACT;AAAA,IAEA,MAAM,YAAY,cAAqC;AACrD,YAAM,SAAS,aAAa,IAAI,YAAY;AAC5C,UAAI,QAAQ;AACV,YAAI,gBAAgB,MAAM;AAC1B,qBAAa,OAAO,YAAY;AAAA,MAClC;AACA,YAAM,UAAU,WAAW,YAAY;AAAA,IACzC;AAAA,IAEA,UAAgB;AACd,iBAAW,OAAO,aAAa,OAAO,GAAG;AACvC,YAAI,gBAAgB,GAAG;AAAA,MACzB;AACA,mBAAa,MAAM;AAAA,IACrB;AAAA,EACF;AACF;","names":[]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/schemas/Doc.ts","../src/schemas/Theme.ts"],"sourcesContent":["/**\n * Doc Schema\n *\n * Defines the JSON format for AI-scriptable visual docs that accompany\n * audio narration. A Doc describes a sequence of animated blocks\n * with text overlays, images, and transitions synchronized to audio.\n *\n * The format is designed to be:\n * 1. AI-generatable - Claude can create scripts from article content\n * 2. Browser-renderable - SVG + CSS animations (ES2015 compatible)\n * 3. Video-exportable - Frame-by-frame capture via Playwright\n *\n * Timing: Blocks are timed to match narration audio segments. Each article\n * has 3-6 MP3 files (intro + sections), and blocks are grouped by segment.\n */\n\n// ============================================\n// Core Types\n// ============================================\n\n/**\n * Configuration for the Start/resting block shown before playback begins.\n */\nexport interface StartBlockConfig {\n /** Hero image path (relative to article media) */\n heroSrc: string;\n /** Alt text for the hero image */\n heroAlt?: string;\n /** Title to display over the hero */\n title: string;\n /** Optional subtitle */\n subtitle?: string;\n /** Ambient motion for the hero image */\n ambientMotion?: 'zoomIn' | 'zoomOut' | 'panLeft' | 'panRight';\n /** Photo credit / artist name for the hero image */\n heroCredit?: string;\n /** License identifier for the hero image */\n heroLicense?: string;\n}\n\n/**\n * A complete visual doc for an article.\n */\nexport interface Doc {\n /** Article ID this doc belongs to */\n articleId: string;\n\n /** Total duration in seconds (sum of all audio segments) */\n duration: number;\n\n /** Ordered list of blocks */\n blocks: Block[];\n\n /** Audio track configuration */\n audio: AudioTrack;\n\n /** Caption track for closed captions (generated by postprocess-doc) */\n captions?: CaptionTrack;\n\n /**\n * Start/resting block shown before playback begins.\n * Displays hero image with title - acts as a \"poster\" for the doc.\n * The player shows this until the user starts playback.\n */\n startBlock?: StartBlockConfig;\n\n /**\n * Persistent layers rendered on every block (behind and/or on top of content).\n * Used for consistent branding overlays, background gradients, etc.\n */\n persistentLayers?: import('./BlockTemplates.js').PersistentLayerConfig;\n\n /**\n * Optional theme identifier. Resolved at render time via `resolveTheme()`.\n * When omitted, the renderer uses the default theme ('documentary').\n */\n themeId?: string;\n\n /** Optional metadata */\n meta?: {\n generatedAt?: string;\n generatedBy?: string;\n version?: number;\n };\n\n /**\n * YAML frontmatter from the source markdown.\n * Carries rendering hints like `document-render-as` and custom metadata.\n */\n frontmatter?: Record<string, unknown>;\n}\n\n/**\n * A single block in the doc.\n *\n * Blocks can be flat (audio-synced timeline) or nested (markdown-driven hierarchy).\n * When derived from markdown, a block corresponds to a heading section:\n * - `sourceHeading` references the original MarkdownHeading node\n * - `contents` holds the body markdown between this heading and the next\n * - `children` holds sub-heading blocks (e.g., H2s under an H1)\n */\nexport interface Block {\n /** Unique identifier for this block */\n id: string;\n\n /** When this block appears (seconds from start) */\n startTime: number;\n\n /** How long this block is visible (seconds) */\n duration: number;\n\n /** Which audio segment this block belongs to (0-indexed) */\n audioSegment: number;\n\n /**\n * Pre-computed visual layers, rendered back-to-front.\n *\n * Optional — template-derived blocks typically omit this and compute\n * layers on demand via `getLayers()` from `@bendyline/squisq/doc`.\n * Raw blocks (e.g., hand-crafted by AI) may provide layers directly.\n */\n layers?: Layer[];\n\n /** Entry transition from previous block */\n transition?: Transition;\n\n /** Template name that generated this block (for debugging) */\n template?: string;\n\n /**\n * Display title for template rendering.\n * Extracted from the sourceHeading when the block is created by markdownToDoc().\n * Templates like sectionHeader and titleBlock read this for their title layer.\n */\n title?: string;\n\n // ── Markdown-driven hierarchy (optional) ──\n\n /**\n * Nested sub-blocks (heading hierarchy).\n * When a doc is derived from markdown, sub-headings within this\n * heading's section become child blocks. For example, H2 blocks\n * under an H1 block appear here.\n */\n children?: Block[];\n\n /**\n * Markdown body content for this block's section.\n * Contains the block-level markdown nodes (paragraphs, lists, code,\n * blockquotes, etc.) that appear between this heading and the next\n * heading or sub-heading. Empty for pure structural blocks.\n */\n contents?: import('../markdown/types.js').MarkdownBlockNode[];\n\n /**\n * Reference to the MarkdownHeading node that created this block.\n * Present when the block was derived from markdown via markdownToDoc().\n * Enables round-tripping back to markdown and provides heading depth.\n * Absent for the preamble block (content before the first heading).\n */\n sourceHeading?: import('../markdown/types.js').MarkdownHeading;\n\n /**\n * Template overrides extracted from markdown directives.\n * For example, `### Data Section {template=chart colorScheme=blue}`\n * would produce `{ template: 'chart', colorScheme: 'blue' }`.\n * Allows per-block customization of template selection and parameters.\n */\n templateOverrides?: Record<string, string>;\n}\n\n// ============================================\n// Layer Types\n// ============================================\n\n/**\n * A visual element within a block.\n * Layers are composited back-to-front (first layer is background).\n */\nexport type Layer = ImageLayer | TextLayer | ShapeLayer | MapLayer | VideoLayer;\n\ninterface BaseLayer {\n /** Unique identifier for this layer */\n id: string;\n\n /** Position and size */\n position: Position;\n\n /** Animation to apply */\n animation?: Animation;\n}\n\n/**\n * Image layer - displays an image with optional Ken Burns effect.\n */\nexport interface ImageLayer extends BaseLayer {\n type: 'image';\n content: {\n /** Path to image file (relative to article media dir) */\n src: string;\n /** Alt text for accessibility */\n alt: string;\n /** How to fit image in bounds */\n fit?: 'cover' | 'contain' | 'fill';\n /** Photo credit / artist name */\n credit?: string;\n /** License identifier (e.g., 'CC BY-SA 4.0') */\n license?: string;\n };\n}\n\n/**\n * Text layer - displays text with styling.\n */\nexport interface TextLayer extends BaseLayer {\n type: 'text';\n content: {\n /** Text to display (supports \\n for line breaks) */\n text: string;\n /** Text styling */\n style: TextStyle;\n };\n}\n\n/**\n * Shape layer - simple geometric shapes for visual accents.\n */\nexport interface ShapeLayer extends BaseLayer {\n type: 'shape';\n content: {\n /** Shape type */\n shape: 'rect' | 'circle' | 'line';\n /** Fill color (CSS color or 'none') */\n fill?: string;\n /** Stroke color */\n stroke?: string;\n /** Stroke width in pixels */\n strokeWidth?: number;\n /** Corner radius for rect */\n borderRadius?: number;\n };\n}\n\n/**\n * Map layer - displays a geographic map with optional markers.\n *\n * Maps are rendered as composite tile images from free tile providers.\n * Supported styles use open-source/free tiles requiring attribution.\n */\nexport interface MapLayer extends BaseLayer {\n type: 'map';\n content: {\n /** Center coordinates for the map */\n center: {\n lat: number;\n lng: number;\n };\n /** Zoom level (0-18, typically 8-14 for doc blocks) */\n zoom: number;\n /** Map tile style */\n style: MapTileStyle;\n /** Optional markers to display on the map */\n markers?: MapMarker[];\n /** Pre-rendered static image path (for video export reliability) */\n staticSrc?: string;\n /** Show attribution text (default: true) */\n showAttribution?: boolean;\n };\n}\n\n/**\n * Video layer - displays a short video clip, always muted (narration provides audio).\n *\n * Rendered via an HTML5 <video> element inside <foreignObject>, similar to how\n * ImageLayer uses <img> for Ken Burns effects. clipStart/clipEnd define the window\n * within the source video to play. During Playwright frame capture (seekTo mode),\n * the video is seeked programmatically rather than playing in real time.\n */\nexport interface VideoLayer extends BaseLayer {\n type: 'video';\n content: {\n /** Path to video file (relative to article media dir) */\n src: string;\n /** Path to poster frame (shown before video loads / during seek) */\n posterSrc?: string;\n /** Alt text for accessibility */\n alt: string;\n /** How to fit video in bounds */\n fit?: 'cover' | 'contain' | 'fill';\n /** Start time within the source video (seconds) */\n clipStart: number;\n /** End time within the source video (seconds) */\n clipEnd: number;\n /** Total source video duration (for validation) */\n sourceDuration?: number;\n /** Video credit / artist name */\n credit?: string;\n /** License identifier (e.g., 'CC BY-SA 4.0') */\n license?: string;\n };\n}\n\n/**\n * Available map tile styles from free/open-source providers.\n */\nexport type MapTileStyle =\n | 'terrain' // OpenTopoMap - topographic/terrain (default)\n | 'satellite' // ESRI World Imagery\n | 'road' // OpenStreetMap standard\n | 'toner' // Stadia Toner (high contrast B&W)\n | 'watercolor'; // Stadia Watercolor (artistic)\n\n/**\n * A marker to display on the map.\n */\nexport interface MapMarker {\n /** Marker latitude */\n lat: number;\n /** Marker longitude */\n lng: number;\n /** Optional label text */\n label?: string;\n /** Marker color (CSS color, default: #ef4444) */\n color?: string;\n /** Marker icon type */\n icon?: 'pin' | 'circle' | 'star';\n}\n\n// ============================================\n// Position & Styling\n// ============================================\n\n/**\n * Position and dimensions for a layer.\n * Values can be pixels (number) or percentages (string like \"50%\").\n */\nexport interface Position {\n /** X position (pixels or percentage) */\n x: number | string;\n /** Y position (pixels or percentage) */\n y: number | string;\n /** Width (pixels or percentage) */\n width?: number | string;\n /** Height (pixels or percentage) */\n height?: number | string;\n /** Anchor point for positioning */\n anchor?: 'center' | 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';\n}\n\n/**\n * Text styling options.\n */\nexport interface TextStyle {\n /** Font size in pixels */\n fontSize: number;\n /** Font family (defaults to system sans-serif) */\n fontFamily?: string;\n /** Font weight */\n fontWeight?: 'normal' | 'bold';\n /** Text color (CSS color) */\n color: string;\n /** Text alignment */\n textAlign?: 'left' | 'center' | 'right';\n /** Line height multiplier */\n lineHeight?: number;\n /** Add drop shadow for readability over images */\n shadow?: boolean;\n /** Background color for text box */\n background?: string;\n /** Padding around text (pixels) */\n padding?: number;\n /** Maximum number of lines before truncation (adds \"...\" to last line) */\n maxLines?: number;\n}\n\n// ============================================\n// Animation & Transitions\n// ============================================\n\n/**\n * Animation to apply to a layer.\n */\nexport interface Animation {\n /** Animation type */\n type: AnimationType;\n /** Duration in seconds (defaults to layer's block duration) */\n duration?: number;\n /** Delay before animation starts (seconds) */\n delay?: number;\n /** CSS easing function */\n easing?: string;\n /** For Ken Burns: zoom direction */\n direction?: 'in' | 'out';\n /** For pan animations: pan direction */\n panDirection?: 'left' | 'right' | 'up' | 'down';\n}\n\n/**\n * Available animation types.\n */\nexport type AnimationType =\n | 'none'\n | 'fadeIn'\n | 'fadeOut'\n | 'slowZoom' // Slow zoom with optional pan\n | 'zoomIn'\n | 'zoomOut'\n | 'panLeft'\n | 'panRight'\n | 'typewriter'; // Text appears letter by letter\n\n/**\n * Transition between blocks.\n */\nexport interface Transition {\n /** Transition type */\n type: TransitionType;\n /** Duration in seconds */\n duration: number;\n}\n\nexport type TransitionType =\n | 'cut' // Instant switch\n | 'fade' // Cross-fade\n | 'dissolve' // Soft dissolve\n | 'slideLeft' // New block enters from right\n | 'slideRight'; // New block enters from left\n\n// ============================================\n// Audio Configuration\n// ============================================\n\n/**\n * Audio track configuration.\n * Articles have multiple MP3 segments (intro + sections).\n */\nexport interface AudioTrack {\n /** Audio segments in playback order */\n segments: AudioSegment[];\n}\n\n// ============================================\n// Captions\n// ============================================\n\n/**\n * A single word with precise timing, used for word-level highlighting\n * in social-style captions. Populated from TTS timing data when available.\n */\nexport interface CaptionWord {\n /** The word text */\n text: string;\n /** Start time in seconds (relative to doc start) */\n startTime: number;\n /** End time in seconds */\n endTime: number;\n}\n\n/**\n * A single caption phrase to display during playback.\n */\nexport interface CaptionPhrase {\n /** The text to display */\n text: string;\n /** Start time in seconds (relative to doc start) */\n startTime: number;\n /** End time in seconds */\n endTime: number;\n /** Which audio segment this caption belongs to (0-indexed) */\n audioSegment: number;\n /**\n * Optional per-word timing from TTS timing data.\n * When present, enables precise word-level highlighting in social\n * caption style. When absent, word timing is interpolated evenly\n * across the phrase duration.\n */\n words?: CaptionWord[];\n}\n\n/**\n * Caption track for a doc.\n */\nexport interface CaptionTrack {\n /** Caption phrases in chronological order */\n phrases: CaptionPhrase[];\n /** When the captions were generated */\n generatedAt: string;\n /** Algorithm version for regeneration detection */\n version: number;\n}\n\n/**\n * A single audio segment (one MP3 file).\n */\nexport interface AudioSegment {\n /** Path to MP3 file (relative to article media dir) */\n src: string;\n /** Segment name (e.g., \"intro\", \"history\") */\n name: string;\n /** Duration in seconds */\n duration: number;\n /** Start time in overall timeline (calculated) */\n startTime: number;\n}\n\n// ============================================\n// Audio Timing Data (from TTS)\n// ============================================\n\n/**\n * A single word-level timing bookmark from TTS synthesis.\n * Used for precise audio-to-content mapping and word-level caption sync.\n */\nexport interface AudioBookmark {\n /** Unique identifier (e.g., \"word-0\", \"word-1\") */\n id: string;\n /** Timestamp in seconds from audio start */\n time: number;\n /** Character offset in the source text */\n charOffset: number;\n /** The word text at this position */\n textFragment?: string;\n}\n\n/**\n * Timing data for an audio segment, typically from a `.timing.json` file\n * generated alongside TTS audio. Contains the source text and per-word\n * timing bookmarks for precise content matching and caption sync.\n */\nexport interface AudioTimingData {\n /** The normalized plain text that was synthesized */\n sourceText: string;\n /** Word-level timing bookmarks */\n bookmarks: AudioBookmark[];\n /** Total audio duration in seconds */\n duration: number;\n}\n\n// ============================================\n// Helper Functions\n// ============================================\n\n/**\n * Calculate total duration from audio segments.\n */\nexport function calculateDuration(audio: AudioTrack): number {\n return audio.segments.reduce((sum, seg) => sum + seg.duration, 0);\n}\n\n/**\n * Find which audio segment is playing at a given time.\n */\nexport function getSegmentAtTime(audio: AudioTrack, time: number): number {\n let elapsed = 0;\n for (let i = 0; i < audio.segments.length; i++) {\n elapsed += audio.segments[i].duration;\n if (time < elapsed) return i;\n }\n return audio.segments.length - 1;\n}\n\n/**\n * Find which block should be visible at a given time.\n */\nexport function getBlockAtTime(blocks: Block[], time: number): Block | null {\n for (let i = blocks.length - 1; i >= 0; i--) {\n const block = blocks[i];\n if (time >= block.startTime && time < block.startTime + block.duration) {\n return block;\n }\n }\n return blocks[0] || null;\n}\n\n/**\n * Find the caption phrase that should be displayed at a given time.\n */\nexport function getCaptionAtTime(\n captions: CaptionTrack | undefined,\n time: number,\n): CaptionPhrase | null {\n if (!captions || !captions.phrases.length) return null;\n\n for (const phrase of captions.phrases) {\n if (time >= phrase.startTime && time < phrase.endTime) {\n return phrase;\n }\n }\n return null;\n}\n\n/**\n * Validate a Doc for completeness.\n */\nexport function validateDoc(script: Doc): string[] {\n const errors: string[] = [];\n\n if (!script.articleId) {\n errors.push('Missing articleId');\n }\n\n if (!script.blocks || script.blocks.length === 0) {\n errors.push('No blocks defined');\n }\n\n if (!script.audio || !script.audio.segments || script.audio.segments.length === 0) {\n errors.push('No audio segments defined');\n }\n\n // Check for gaps in block coverage\n const totalDuration = script.duration;\n let covered = 0;\n for (const block of script.blocks) {\n if (block.startTime > covered + 0.1) {\n errors.push(`Gap in blocks: ${covered}s to ${block.startTime}s`);\n }\n covered = Math.max(covered, block.startTime + block.duration);\n }\n\n if (covered < totalDuration - 0.1) {\n errors.push(`Blocks end at ${covered}s but audio is ${totalDuration}s`);\n }\n\n return errors;\n}\n","/**\n * Theme System\n *\n * A Theme bundles color palette, typography, visual style, render-style\n * algorithm, and per-block color schemes into one JSON-serializable object.\n * Builders choose a theme from the built-in library or create a custom one\n * via `createTheme(base, overrides)`.\n *\n * Design principles:\n * - Fully JSON-serializable (no functions) — storable in config / APIs.\n * - Doc carries an optional `themeId` pointer; resolution happens at render time.\n * - `createTheme` deep-merges a base theme with partial overrides.\n */\n\nimport type { LayoutHints } from './LayoutStrategy.js';\nimport type { AnimationType, TransitionType } from './Doc.js';\nimport type { PersistentLayerConfig } from './BlockTemplates.js';\n\n// ============================================\n// Color Palette\n// ============================================\n\n/**\n * Core color palette for a theme. Every color is a CSS color string.\n */\nexport interface ThemeColorPalette {\n /** Primary accent color */\n primary: string;\n /** Secondary accent color */\n secondary: string;\n /** Background color (typically dark) */\n background: string;\n /** Lighter background for contrast panels */\n backgroundLight: string;\n /** Main text color */\n text: string;\n /** Muted/secondary text color */\n textMuted: string;\n /** Highlight/emphasis color */\n highlight: string;\n /** Warning/alert color */\n warning: string;\n}\n\n/**\n * A named color scheme used by templates for per-block color variation\n * (e.g., statHighlight or sectionHeader).\n */\nexport interface ThemeColorScheme {\n /** Background fill */\n bg: string;\n /** Primary text tint */\n text: string;\n /** Accent / secondary tint */\n accent: string;\n}\n\n// ============================================\n// Typography\n// ============================================\n\n/**\n * Typography settings for a theme.\n */\nexport interface ThemeTypography {\n /** Font family for body / description text */\n bodyFontFamily: string;\n /** Font family for titles and headings */\n titleFontFamily: string;\n /** Font family for code / monospaced text (optional) */\n monoFontFamily?: string;\n /** Multiplier applied to LayoutHints.titleScale (default 1.0) */\n titleScale?: number;\n /** Multiplier applied to LayoutHints.bodyScale (default 1.0) */\n bodyScale?: number;\n /** Default body line height (default 1.4) */\n lineHeight?: number;\n /** Default title line height */\n titleLineHeight?: number;\n /** Default title font weight */\n titleWeight?: 'normal' | 'bold';\n}\n\n// ============================================\n// Visual Style\n// ============================================\n\n/**\n * Global visual-style knobs that templates consult.\n */\nexport interface ThemeStyle {\n /** Default border radius for cards / shapes (px) */\n borderRadius?: number;\n /** Whether templates should default to text shadows */\n textShadow?: boolean;\n /** Darkness of overlay on image-backed blocks (0–1) */\n overlayOpacity?: number;\n /** Multiplier on all animation durations (1.0 = normal, <1 faster, >1 slower) */\n animationSpeed?: number;\n /** Default horizontal padding for text (percentage string, e.g. \"5%\") */\n blockPadding?: string;\n}\n\n// ============================================\n// Render Style\n// ============================================\n\n/**\n * Render-style algorithm preset. Controls layout tweaks, default animations,\n * transitions, and per-template behavioral hints.\n */\nexport interface RenderStyle {\n /** Identifier for this algorithmic approach (e.g. \"documentary\", \"magazine\") */\n name: string;\n /** Partial overrides merged onto the orientation-based LayoutHints */\n layoutOverrides?: Partial<LayoutHints>;\n /** Default entrance animation for text layers */\n defaultTextAnimation?: AnimationType;\n /** Default animation for background / image layers */\n defaultImageAnimation?: AnimationType;\n /** Whether to apply Ken Burns ambient motion to images by default */\n ambientMotion?: boolean;\n /** Default block-to-block transition */\n defaultTransition?: { type: TransitionType; duration?: number };\n /**\n * Per-template behavioral hints. Keys are template names, values are\n * string/number/boolean maps that templates can read to vary their output.\n *\n * @example\n * ```\n * { statHighlight: { entrance: 'dramatic' }, titleBlock: { showAccentLine: false } }\n * ```\n */\n templateHints?: Record<string, Record<string, string | number | boolean>>;\n}\n\n// ============================================\n// Theme\n// ============================================\n\n/**\n * A complete, JSON-serializable theme definition.\n */\nexport interface Theme {\n /** Unique identifier (e.g. \"documentary\") */\n id: string;\n /** Human-readable display name */\n name: string;\n /** Short description for theme pickers */\n description?: string;\n /** Color palette */\n colors: ThemeColorPalette;\n /** Typography settings */\n typography: ThemeTypography;\n /** Global visual-style knobs */\n style: ThemeStyle;\n /** Algorithmic render-style preset */\n renderStyle: RenderStyle;\n /**\n * Named color schemes for per-block color variation.\n * Templates reference these by name (e.g. \"blue\", \"warm\").\n */\n colorSchemes: Record<string, ThemeColorScheme>;\n /** Optional persistent layers baked into the theme */\n persistentLayers?: PersistentLayerConfig;\n}\n\n// ============================================\n// Deep-Partial helper type\n// ============================================\n\n/**\n * Recursively makes every property optional.\n */\nexport type DeepPartial<T> = {\n [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];\n};\n\n// ============================================\n// Helpers\n// ============================================\n\n/**\n * Deep-merge `source` into `target`, returning a new object.\n * Arrays are replaced wholesale (not concatenated).\n */\nfunction deepMerge<T extends Record<string, unknown>>(target: T, source: DeepPartial<T>): T {\n const result = { ...target } as Record<string, unknown>;\n for (const key of Object.keys(source)) {\n const srcVal = (source as Record<string, unknown>)[key];\n const tgtVal = (target as Record<string, unknown>)[key];\n if (\n srcVal !== null &&\n srcVal !== undefined &&\n typeof srcVal === 'object' &&\n !Array.isArray(srcVal) &&\n tgtVal !== null &&\n tgtVal !== undefined &&\n typeof tgtVal === 'object' &&\n !Array.isArray(tgtVal)\n ) {\n result[key] = deepMerge(\n tgtVal as Record<string, unknown>,\n srcVal as DeepPartial<Record<string, unknown>>,\n );\n } else if (srcVal !== undefined) {\n result[key] = srcVal;\n }\n }\n return result as T;\n}\n\n/**\n * Create a new theme by deep-merging overrides onto a base theme.\n * The returned theme gets its own `id` if one is provided in overrides,\n * otherwise keeps the base theme's `id` suffixed with \"-custom\".\n *\n * @example\n * ```ts\n * const myTheme = createTheme(THEMES.documentary, {\n * colors: { primary: '#ff0000' },\n * typography: { bodyFontFamily: '\"Inter\", sans-serif' },\n * });\n * ```\n */\nexport function createTheme(base: Theme, overrides: DeepPartial<Theme>): Theme {\n const merged = deepMerge(\n base as unknown as Record<string, unknown>,\n overrides as DeepPartial<Record<string, unknown>>,\n ) as unknown as Theme;\n if (!overrides.id && merged.id === base.id) {\n merged.id = `${base.id}-custom`;\n }\n return merged;\n}\n"],"mappings":";AAiiBO,SAAS,kBAAkB,OAA2B;AAC3D,SAAO,MAAM,SAAS,OAAO,CAAC,KAAK,QAAQ,MAAM,IAAI,UAAU,CAAC;AAClE;AAKO,SAAS,iBAAiB,OAAmB,MAAsB;AACxE,MAAI,UAAU;AACd,WAAS,IAAI,GAAG,IAAI,MAAM,SAAS,QAAQ,KAAK;AAC9C,eAAW,MAAM,SAAS,CAAC,EAAE;AAC7B,QAAI,OAAO,QAAS,QAAO;AAAA,EAC7B;AACA,SAAO,MAAM,SAAS,SAAS;AACjC;AAKO,SAAS,eAAe,QAAiB,MAA4B;AAC1E,WAAS,IAAI,OAAO,SAAS,GAAG,KAAK,GAAG,KAAK;AAC3C,UAAM,QAAQ,OAAO,CAAC;AACtB,QAAI,QAAQ,MAAM,aAAa,OAAO,MAAM,YAAY,MAAM,UAAU;AACtE,aAAO;AAAA,IACT;AAAA,EACF;AACA,SAAO,OAAO,CAAC,KAAK;AACtB;AAKO,SAAS,iBACd,UACA,MACsB;AACtB,MAAI,CAAC,YAAY,CAAC,SAAS,QAAQ,OAAQ,QAAO;AAElD,aAAW,UAAU,SAAS,SAAS;AACrC,QAAI,QAAQ,OAAO,aAAa,OAAO,OAAO,SAAS;AACrD,aAAO;AAAA,IACT;AAAA,EACF;AACA,SAAO;AACT;AAKO,SAAS,YAAY,QAAuB;AACjD,QAAM,SAAmB,CAAC;AAE1B,MAAI,CAAC,OAAO,WAAW;AACrB,WAAO,KAAK,mBAAmB;AAAA,EACjC;AAEA,MAAI,CAAC,OAAO,UAAU,OAAO,OAAO,WAAW,GAAG;AAChD,WAAO,KAAK,mBAAmB;AAAA,EACjC;AAEA,MAAI,CAAC,OAAO,SAAS,CAAC,OAAO,MAAM,YAAY,OAAO,MAAM,SAAS,WAAW,GAAG;AACjF,WAAO,KAAK,2BAA2B;AAAA,EACzC;AAGA,QAAM,gBAAgB,OAAO;AAC7B,MAAI,UAAU;AACd,aAAW,SAAS,OAAO,QAAQ;AACjC,QAAI,MAAM,YAAY,UAAU,KAAK;AACnC,aAAO,KAAK,kBAAkB,OAAO,QAAQ,MAAM,SAAS,GAAG;AAAA,IACjE;AACA,cAAU,KAAK,IAAI,SAAS,MAAM,YAAY,MAAM,QAAQ;AAAA,EAC9D;AAEA,MAAI,UAAU,gBAAgB,KAAK;AACjC,WAAO,KAAK,iBAAiB,OAAO,kBAAkB,aAAa,GAAG;AAAA,EACxE;AAEA,SAAO;AACT;;;ACtbA,SAAS,UAA6C,QAAW,QAA2B;AAC1F,QAAM,SAAS,EAAE,GAAG,OAAO;AAC3B,aAAW,OAAO,OAAO,KAAK,MAAM,GAAG;AACrC,UAAM,SAAU,OAAmC,GAAG;AACtD,UAAM,SAAU,OAAmC,GAAG;AACtD,QACE,WAAW,QACX,WAAW,UACX,OAAO,WAAW,YAClB,CAAC,MAAM,QAAQ,MAAM,KACrB,WAAW,QACX,WAAW,UACX,OAAO,WAAW,YAClB,CAAC,MAAM,QAAQ,MAAM,GACrB;AACA,aAAO,GAAG,IAAI;AAAA,QACZ;AAAA,QACA;AAAA,MACF;AAAA,IACF,WAAW,WAAW,QAAW;AAC/B,aAAO,GAAG,IAAI;AAAA,IAChB;AAAA,EACF;AACA,SAAO;AACT;AAeO,SAAS,YAAY,MAAa,WAAsC;AAC7E,QAAM,SAAS;AAAA,IACb;AAAA,IACA;AAAA,EACF;AACA,MAAI,CAAC,UAAU,MAAM,OAAO,OAAO,KAAK,IAAI;AAC1C,WAAO,KAAK,GAAG,KAAK,EAAE;AAAA,EACxB;AACA,SAAO;AACT;","names":[]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
//# sourceMappingURL=chunk-DNJKBAI7.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
|
@@ -1,11 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
SeededRandom,
|
|
3
|
-
hashString
|
|
4
|
-
} from "./chunk-FVTQSNR7.js";
|
|
5
|
-
import {
|
|
6
|
-
estimateNarrationDuration
|
|
7
|
-
} from "./chunk-3E5F2XMR.js";
|
|
8
|
-
|
|
9
1
|
// src/generate/contentExtractor.ts
|
|
10
2
|
var STAT_PATTERNS = [
|
|
11
3
|
/\b(roughly |about |approximately |nearly |over |under |more than |less than )?([\d,]+(?:\.\d+)?)\s*%/gi,
|
|
@@ -655,138 +647,9 @@ function mapDefinition(data, base, opts) {
|
|
|
655
647
|
};
|
|
656
648
|
}
|
|
657
649
|
|
|
658
|
-
// src/generate/slideshowGenerator.ts
|
|
659
|
-
var DEFAULT_COLOR_SCHEMES = ["blue", "green", "purple", "orange", "red"];
|
|
660
|
-
var DEFAULT_AMBIENT_MOTIONS = [
|
|
661
|
-
"zoomIn",
|
|
662
|
-
"zoomOut",
|
|
663
|
-
"panLeft",
|
|
664
|
-
"panRight"
|
|
665
|
-
];
|
|
666
|
-
var DEFAULT_ACCENT_POSITIONS = ["left-strip", "right-strip", "bottom-strip"];
|
|
667
|
-
function generateSlideshow(text, images = [], options = {}) {
|
|
668
|
-
const {
|
|
669
|
-
seed,
|
|
670
|
-
title,
|
|
671
|
-
subtitle,
|
|
672
|
-
duration: explicitDuration,
|
|
673
|
-
targetSlides,
|
|
674
|
-
colorSchemes = DEFAULT_COLOR_SCHEMES,
|
|
675
|
-
ambientMotions = DEFAULT_AMBIENT_MOTIONS,
|
|
676
|
-
accentPositions = DEFAULT_ACCENT_POSITIONS,
|
|
677
|
-
minSlideDuration = 4,
|
|
678
|
-
maxSlideDuration = 15,
|
|
679
|
-
themeId,
|
|
680
|
-
isMarkdown = false,
|
|
681
|
-
minConfidence = 0.3
|
|
682
|
-
} = options;
|
|
683
|
-
const plainText = isMarkdown ? stripMarkdown(text) : text;
|
|
684
|
-
const rng = new SeededRandom(seed ?? hashString(plainText));
|
|
685
|
-
const totalDuration = explicitDuration ?? estimateNarrationDuration(plainText);
|
|
686
|
-
const extraction = extractContent(plainText, { minConfidence });
|
|
687
|
-
const allElements = extraction.elements;
|
|
688
|
-
const slideBudget = targetSlides ?? computeSlideBudget(totalDuration, allElements.length);
|
|
689
|
-
const imageSlotCount = images.length > 0 ? Math.max(1, Math.round(slideBudget * 0.4)) : 0;
|
|
690
|
-
const contentSlotCount = Math.max(0, slideBudget - imageSlotCount - (title ? 1 : 0));
|
|
691
|
-
const sorted = [...allElements].sort((a, b) => b.confidence - a.confidence);
|
|
692
|
-
const selected = sorted.slice(0, contentSlotCount);
|
|
693
|
-
selected.sort((a, b) => a.sourcePosition - b.sourcePosition);
|
|
694
|
-
const blocks = [];
|
|
695
|
-
let slideIndex = 0;
|
|
696
|
-
let colorIdx = 0;
|
|
697
|
-
let accentIdx = 0;
|
|
698
|
-
const totalSlideCount = (title ? 1 : 0) + selected.length + Math.min(imageSlotCount, images.length);
|
|
699
|
-
const perSlide = totalSlideCount > 0 ? Math.min(maxSlideDuration, Math.max(minSlideDuration, totalDuration / totalSlideCount)) : minSlideDuration;
|
|
700
|
-
if (title) {
|
|
701
|
-
const titleBlock = {
|
|
702
|
-
id: `slide-${slideIndex++}`,
|
|
703
|
-
template: "titleBlock",
|
|
704
|
-
title,
|
|
705
|
-
subtitle,
|
|
706
|
-
duration: Math.min(perSlide, 6),
|
|
707
|
-
audioSegment: 0
|
|
708
|
-
};
|
|
709
|
-
blocks.push(titleBlock);
|
|
710
|
-
}
|
|
711
|
-
const standaloneImages = images.slice(0, Math.min(imageSlotCount, images.length));
|
|
712
|
-
const accentPool = images.length > standaloneImages.length ? images.slice(standaloneImages.length) : [...images];
|
|
713
|
-
const imageInterval = standaloneImages.length > 0 ? Math.max(1, Math.floor(selected.length / (standaloneImages.length + 1))) : Infinity;
|
|
714
|
-
let imgIdx = 0;
|
|
715
|
-
let sinceImage = 0;
|
|
716
|
-
for (let i = 0; i < selected.length; i++) {
|
|
717
|
-
if (sinceImage >= imageInterval && imgIdx < standaloneImages.length) {
|
|
718
|
-
blocks.push(
|
|
719
|
-
createImageSlide(standaloneImages[imgIdx], slideIndex++, perSlide, rng, ambientMotions)
|
|
720
|
-
);
|
|
721
|
-
imgIdx++;
|
|
722
|
-
sinceImage = 0;
|
|
723
|
-
}
|
|
724
|
-
const element = selected[i];
|
|
725
|
-
let accentImage;
|
|
726
|
-
if (accentPool.length > 0) {
|
|
727
|
-
const img = accentPool[i % accentPool.length];
|
|
728
|
-
accentImage = {
|
|
729
|
-
src: img.src,
|
|
730
|
-
alt: img.alt ?? "",
|
|
731
|
-
position: accentPositions[accentIdx % accentPositions.length],
|
|
732
|
-
ambientMotion: rng.pickRequired(ambientMotions),
|
|
733
|
-
credit: img.credit,
|
|
734
|
-
license: img.license
|
|
735
|
-
};
|
|
736
|
-
accentIdx++;
|
|
737
|
-
}
|
|
738
|
-
const block = mapElementToBlock(element, {
|
|
739
|
-
id: `slide-${slideIndex++}`,
|
|
740
|
-
duration: perSlide,
|
|
741
|
-
audioSegment: 0,
|
|
742
|
-
colorScheme: colorSchemes[colorIdx % colorSchemes.length],
|
|
743
|
-
accentImage
|
|
744
|
-
});
|
|
745
|
-
blocks.push(block);
|
|
746
|
-
colorIdx++;
|
|
747
|
-
sinceImage++;
|
|
748
|
-
}
|
|
749
|
-
while (imgIdx < standaloneImages.length) {
|
|
750
|
-
blocks.push(
|
|
751
|
-
createImageSlide(standaloneImages[imgIdx], slideIndex++, perSlide, rng, ambientMotions)
|
|
752
|
-
);
|
|
753
|
-
imgIdx++;
|
|
754
|
-
}
|
|
755
|
-
const doc = {
|
|
756
|
-
articleId: title ?? "slideshow",
|
|
757
|
-
duration: totalDuration,
|
|
758
|
-
blocks,
|
|
759
|
-
audio: {
|
|
760
|
-
segments: [{ src: "", name: "main", duration: totalDuration, startTime: 0 }]
|
|
761
|
-
},
|
|
762
|
-
themeId
|
|
763
|
-
};
|
|
764
|
-
return doc;
|
|
765
|
-
}
|
|
766
|
-
function computeSlideBudget(duration, elementCount) {
|
|
767
|
-
const byDuration = Math.round(duration / 60 * 5);
|
|
768
|
-
const budget = Math.min(byDuration, elementCount + 4);
|
|
769
|
-
return Math.max(3, Math.min(12, budget));
|
|
770
|
-
}
|
|
771
|
-
function createImageSlide(img, index, duration, rng, motions) {
|
|
772
|
-
return {
|
|
773
|
-
id: `slide-${index}`,
|
|
774
|
-
template: "imageWithCaption",
|
|
775
|
-
imageSrc: img.src,
|
|
776
|
-
imageAlt: img.alt ?? "",
|
|
777
|
-
caption: img.alt,
|
|
778
|
-
duration,
|
|
779
|
-
audioSegment: 0,
|
|
780
|
-
ambientMotion: rng.pickRequired(motions),
|
|
781
|
-
imageCredit: img.credit,
|
|
782
|
-
imageLicense: img.license
|
|
783
|
-
};
|
|
784
|
-
}
|
|
785
|
-
|
|
786
650
|
export {
|
|
787
651
|
extractContent,
|
|
788
652
|
stripMarkdown,
|
|
789
|
-
mapElementToBlock
|
|
790
|
-
generateSlideshow
|
|
653
|
+
mapElementToBlock
|
|
791
654
|
};
|
|
792
|
-
//# sourceMappingURL=chunk-
|
|
655
|
+
//# sourceMappingURL=chunk-MIBRIRGJ.js.map
|