@bendyline/squisq-formats 1.2.2 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/__tests__/html.test.js +78 -4
- package/dist/__tests__/html.test.js.map +1 -1
- package/dist/__tests__/pdfExport.test.js +1 -1
- package/dist/__tests__/pdfExport.test.js.map +1 -1
- package/dist/__tests__/plainHtml.test.d.ts +5 -0
- package/dist/__tests__/plainHtml.test.d.ts.map +1 -0
- package/dist/__tests__/plainHtml.test.js +327 -0
- package/dist/__tests__/plainHtml.test.js.map +1 -0
- package/dist/__tests__/plainHtmlBundle.test.d.ts +11 -0
- package/dist/__tests__/plainHtmlBundle.test.d.ts.map +1 -0
- package/dist/__tests__/plainHtmlBundle.test.js +210 -0
- package/dist/__tests__/plainHtmlBundle.test.js.map +1 -0
- package/dist/chunk-33YRFXZZ.js +1187 -0
- package/dist/chunk-33YRFXZZ.js.map +1 -0
- package/dist/{chunk-EUQQYBZZ.js → chunk-ERZ627GR.js} +4 -4
- package/dist/chunk-ERZ627GR.js.map +1 -0
- package/dist/{chunk-MEZF76JA.js → chunk-UDS45KUJ.js} +68 -25
- package/dist/chunk-UDS45KUJ.js.map +1 -0
- package/dist/{chunk-NGWHV77G.js → chunk-VN2KEOYB.js} +7 -6
- package/dist/chunk-VN2KEOYB.js.map +1 -0
- package/dist/docx/export.d.ts.map +1 -1
- package/dist/docx/export.js +153 -22
- package/dist/docx/export.js.map +1 -1
- package/dist/epub/export.js +3 -3
- package/dist/epub/export.js.map +1 -1
- package/dist/html/docsHtmlBundle.d.ts +53 -0
- package/dist/html/docsHtmlBundle.d.ts.map +1 -0
- package/dist/html/docsHtmlBundle.js +299 -0
- package/dist/html/docsHtmlBundle.js.map +1 -0
- package/dist/html/htmlTemplate.d.ts.map +1 -1
- package/dist/html/htmlTemplate.js +43 -0
- package/dist/html/htmlTemplate.js.map +1 -1
- package/dist/html/index.d.ts +6 -0
- package/dist/html/index.d.ts.map +1 -1
- package/dist/html/index.js +31 -3
- package/dist/html/index.js.map +1 -1
- package/dist/html/plainHtml.d.ts +79 -0
- package/dist/html/plainHtml.d.ts.map +1 -0
- package/dist/html/plainHtml.js +614 -0
- package/dist/html/plainHtml.js.map +1 -0
- package/dist/html/plainHtmlBundle.d.ts +93 -0
- package/dist/html/plainHtmlBundle.d.ts.map +1 -0
- package/dist/html/plainHtmlBundle.js +357 -0
- package/dist/html/plainHtmlBundle.js.map +1 -0
- package/dist/pptx/export.d.ts.map +1 -1
- package/dist/pptx/export.js +10 -8
- package/dist/pptx/export.js.map +1 -1
- package/package.json +3 -2
- package/src/__tests__/html.test.ts +82 -4
- package/src/__tests__/pdfExport.test.ts +1 -1
- package/src/__tests__/plainHtml.test.ts +372 -0
- package/src/__tests__/plainHtmlBundle.test.ts +235 -0
- package/src/docx/export.ts +163 -22
- package/src/epub/export.ts +3 -3
- package/src/html/docsHtmlBundle.ts +369 -0
- package/src/html/htmlTemplate.ts +40 -0
- package/src/html/index.ts +32 -3
- package/src/html/plainHtml.ts +736 -0
- package/src/html/plainHtmlBundle.ts +419 -0
- package/src/pptx/export.ts +10 -9
- package/dist/chunk-EUQQYBZZ.js.map +0 -1
- package/dist/chunk-MEZF76JA.js.map +0 -1
- package/dist/chunk-NGWHV77G.js.map +0 -1
- package/dist/chunk-UM5V2XZG.js +0 -242
- package/dist/chunk-UM5V2XZG.js.map +0 -1
package/dist/chunk-UM5V2XZG.js
DELETED
|
@@ -1,242 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
arrayBufferToBase64DataUrl,
|
|
3
|
-
extractFilename,
|
|
4
|
-
inferMimeType
|
|
5
|
-
} from "./chunk-A3FHLTY5.js";
|
|
6
|
-
|
|
7
|
-
// src/html/index.ts
|
|
8
|
-
import JSZip from "jszip";
|
|
9
|
-
|
|
10
|
-
// src/html/htmlTemplate.ts
|
|
11
|
-
function collectImagePaths(doc) {
|
|
12
|
-
const paths = /* @__PURE__ */ new Set();
|
|
13
|
-
function addIfRelative(src) {
|
|
14
|
-
if (!src || src.startsWith("data:") || src.startsWith("blob:") || src.startsWith("http://") || src.startsWith("https://")) {
|
|
15
|
-
return;
|
|
16
|
-
}
|
|
17
|
-
paths.add(src);
|
|
18
|
-
}
|
|
19
|
-
function scanLayers(layers) {
|
|
20
|
-
if (!layers) return;
|
|
21
|
-
for (const layer of layers) {
|
|
22
|
-
if (layer.type === "image") addIfRelative(layer.content.src);
|
|
23
|
-
if (layer.type === "video") {
|
|
24
|
-
addIfRelative(layer.content.src);
|
|
25
|
-
addIfRelative(layer.content.posterSrc);
|
|
26
|
-
}
|
|
27
|
-
if (layer.type === "map") addIfRelative(layer.content.staticSrc);
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
function scanBlock(block) {
|
|
31
|
-
scanLayers(block.layers);
|
|
32
|
-
if (block.children) block.children.forEach(scanBlock);
|
|
33
|
-
}
|
|
34
|
-
doc.blocks.forEach(scanBlock);
|
|
35
|
-
if (doc.startBlock?.heroSrc) addIfRelative(doc.startBlock.heroSrc);
|
|
36
|
-
if (doc.persistentLayers) {
|
|
37
|
-
for (const pl of doc.persistentLayers.bottomLayers ?? []) {
|
|
38
|
-
if ("src" in pl && typeof pl.src === "string") addIfRelative(pl.src);
|
|
39
|
-
}
|
|
40
|
-
for (const pl of doc.persistentLayers.topLayers ?? []) {
|
|
41
|
-
if ("src" in pl && typeof pl.src === "string") addIfRelative(pl.src);
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
for (const block of doc.blocks) {
|
|
45
|
-
scanTemplateImageFields(block, paths);
|
|
46
|
-
}
|
|
47
|
-
return paths;
|
|
48
|
-
}
|
|
49
|
-
function scanTemplateImageFields(obj, paths) {
|
|
50
|
-
if (!obj || typeof obj !== "object") return;
|
|
51
|
-
const imageFieldNames = [
|
|
52
|
-
"imageSrc",
|
|
53
|
-
"src",
|
|
54
|
-
"heroSrc",
|
|
55
|
-
"backgroundImage",
|
|
56
|
-
"posterSrc",
|
|
57
|
-
"staticSrc",
|
|
58
|
-
"videoSrc",
|
|
59
|
-
"thumbnailSrc"
|
|
60
|
-
];
|
|
61
|
-
for (const key of imageFieldNames) {
|
|
62
|
-
const val = obj[key];
|
|
63
|
-
if (typeof val === "string" && val && !val.startsWith("data:") && !val.startsWith("http")) {
|
|
64
|
-
paths.add(val);
|
|
65
|
-
}
|
|
66
|
-
if (val && typeof val === "object" && !Array.isArray(val)) {
|
|
67
|
-
scanTemplateImageFields(val, paths);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
if (Array.isArray(obj)) {
|
|
71
|
-
for (const item of obj) {
|
|
72
|
-
if (item && typeof item === "object") {
|
|
73
|
-
scanTemplateImageFields(item, paths);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
for (const key of ["images", "accentImage", "backgroundVideo", "children", "blocks"]) {
|
|
78
|
-
const val = obj[key];
|
|
79
|
-
if (Array.isArray(val)) {
|
|
80
|
-
for (const item of val) {
|
|
81
|
-
if (item && typeof item === "object") {
|
|
82
|
-
scanTemplateImageFields(item, paths);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
} else if (val && typeof val === "object") {
|
|
86
|
-
scanTemplateImageFields(val, paths);
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
function escapeForScript(str) {
|
|
91
|
-
return str.replace(/<\/(script)/gi, "<\\/$1");
|
|
92
|
-
}
|
|
93
|
-
function generateInlineHtml(doc, options) {
|
|
94
|
-
const {
|
|
95
|
-
playerScript,
|
|
96
|
-
images,
|
|
97
|
-
mode = "slideshow",
|
|
98
|
-
title = "Squisq Document",
|
|
99
|
-
autoPlay = false,
|
|
100
|
-
themeId
|
|
101
|
-
} = options;
|
|
102
|
-
if (themeId) {
|
|
103
|
-
doc = { ...doc, themeId };
|
|
104
|
-
}
|
|
105
|
-
const imageMap = {};
|
|
106
|
-
if (images) {
|
|
107
|
-
for (const [path, buffer] of images.entries()) {
|
|
108
|
-
const mimeType = inferMimeType(path);
|
|
109
|
-
imageMap[path] = arrayBufferToBase64DataUrl(buffer, mimeType);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
const docJson = escapeForScript(JSON.stringify(doc));
|
|
113
|
-
const imageMapJson = escapeForScript(JSON.stringify(imageMap));
|
|
114
|
-
return `<!DOCTYPE html>
|
|
115
|
-
<html lang="en">
|
|
116
|
-
<head>
|
|
117
|
-
<meta charset="UTF-8">
|
|
118
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
119
|
-
<title>${escapeHtml(title)}</title>
|
|
120
|
-
<style>
|
|
121
|
-
*,*::before,*::after{box-sizing:border-box}
|
|
122
|
-
html,body{margin:0;padding:0;height:100%;background:#1a1a2e;color:#e0e0e0;font-family:system-ui,-apple-system,sans-serif}
|
|
123
|
-
#squisq-root{width:100%;height:100%;display:flex;align-items:center;justify-content:center}
|
|
124
|
-
${mode === "static" ? "#squisq-root{align-items:flex-start;overflow-y:auto;background:#fff;color:#1f2937}" : ""}
|
|
125
|
-
</style>
|
|
126
|
-
</head>
|
|
127
|
-
<body>
|
|
128
|
-
<div id="squisq-root"></div>
|
|
129
|
-
<script>${escapeForScript(playerScript)}</script>
|
|
130
|
-
<script>
|
|
131
|
-
(function(){
|
|
132
|
-
var doc = JSON.parse(${JSON.stringify(docJson)});
|
|
133
|
-
var images = JSON.parse(${JSON.stringify(imageMapJson)});
|
|
134
|
-
SquisqPlayer.mount(document.getElementById("squisq-root"), doc, {
|
|
135
|
-
mode: ${JSON.stringify(mode)},
|
|
136
|
-
images: images,
|
|
137
|
-
autoPlay: ${JSON.stringify(autoPlay)},
|
|
138
|
-
basePath: "."
|
|
139
|
-
});
|
|
140
|
-
})();
|
|
141
|
-
</script>
|
|
142
|
-
</body>
|
|
143
|
-
</html>`;
|
|
144
|
-
}
|
|
145
|
-
function generateExternalHtml(doc, options) {
|
|
146
|
-
const {
|
|
147
|
-
playerScriptPath,
|
|
148
|
-
imagePathMap,
|
|
149
|
-
audioPathMap,
|
|
150
|
-
mode = "slideshow",
|
|
151
|
-
title = "Squisq Document",
|
|
152
|
-
autoPlay = false
|
|
153
|
-
} = options;
|
|
154
|
-
const docJson = escapeForScript(JSON.stringify(doc));
|
|
155
|
-
const imageMapJson = imagePathMap ? escapeForScript(JSON.stringify(imagePathMap)) : "{}";
|
|
156
|
-
const audioMapJson = audioPathMap ? escapeForScript(JSON.stringify(audioPathMap)) : "null";
|
|
157
|
-
return `<!DOCTYPE html>
|
|
158
|
-
<html lang="en">
|
|
159
|
-
<head>
|
|
160
|
-
<meta charset="UTF-8">
|
|
161
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
162
|
-
<title>${escapeHtml(title)}</title>
|
|
163
|
-
<style>
|
|
164
|
-
*,*::before,*::after{box-sizing:border-box}
|
|
165
|
-
html,body{margin:0;padding:0;height:100%;background:#1a1a2e;color:#e0e0e0;font-family:system-ui,-apple-system,sans-serif}
|
|
166
|
-
#squisq-root{width:100%;height:100%;display:flex;align-items:center;justify-content:center}
|
|
167
|
-
${mode === "static" ? "#squisq-root{align-items:flex-start;overflow-y:auto;background:#fff;color:#1f2937}" : ""}
|
|
168
|
-
</style>
|
|
169
|
-
</head>
|
|
170
|
-
<body>
|
|
171
|
-
<div id="squisq-root"></div>
|
|
172
|
-
<script src="${escapeHtml(playerScriptPath)}"></script>
|
|
173
|
-
<script>
|
|
174
|
-
(function(){
|
|
175
|
-
var doc = JSON.parse(${JSON.stringify(docJson)});
|
|
176
|
-
var images = JSON.parse(${JSON.stringify(imageMapJson)});
|
|
177
|
-
var audio = ${audioMapJson};
|
|
178
|
-
SquisqPlayer.mount(document.getElementById("squisq-root"), doc, {
|
|
179
|
-
mode: ${JSON.stringify(mode)},
|
|
180
|
-
images: images,
|
|
181
|
-
audio: audio,
|
|
182
|
-
autoPlay: ${JSON.stringify(autoPlay)},
|
|
183
|
-
basePath: "."
|
|
184
|
-
});
|
|
185
|
-
})();
|
|
186
|
-
</script>
|
|
187
|
-
</body>
|
|
188
|
-
</html>`;
|
|
189
|
-
}
|
|
190
|
-
function escapeHtml(str) {
|
|
191
|
-
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// src/html/index.ts
|
|
195
|
-
function docToHtml(doc, options) {
|
|
196
|
-
return generateInlineHtml(doc, options);
|
|
197
|
-
}
|
|
198
|
-
async function docToHtmlZip(doc, options) {
|
|
199
|
-
const { playerScript, images, audio, mode = "slideshow", title, autoPlay } = options;
|
|
200
|
-
const zip = new JSZip();
|
|
201
|
-
zip.file("squisq-player.js", playerScript);
|
|
202
|
-
const imagePathMap = {};
|
|
203
|
-
if (images) {
|
|
204
|
-
for (const [originalPath, buffer] of images.entries()) {
|
|
205
|
-
const filename = extractFilename(originalPath);
|
|
206
|
-
const zipPath = `images/${filename}`;
|
|
207
|
-
zip.file(zipPath, buffer);
|
|
208
|
-
imagePathMap[originalPath] = zipPath;
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
const audioPathMap = {};
|
|
212
|
-
if (audio) {
|
|
213
|
-
for (const [segmentKey, buffer] of audio.entries()) {
|
|
214
|
-
const filename = extractFilename(segmentKey);
|
|
215
|
-
const finalName = filename.includes(".") ? filename : `${filename}.mp3`;
|
|
216
|
-
const zipPath = `audio/${finalName}`;
|
|
217
|
-
zip.file(zipPath, buffer);
|
|
218
|
-
audioPathMap[segmentKey] = zipPath;
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
const html = generateExternalHtml(doc, {
|
|
222
|
-
playerScriptPath: "squisq-player.js",
|
|
223
|
-
imagePathMap: Object.keys(imagePathMap).length > 0 ? imagePathMap : void 0,
|
|
224
|
-
audioPathMap: Object.keys(audioPathMap).length > 0 ? audioPathMap : void 0,
|
|
225
|
-
mode,
|
|
226
|
-
title,
|
|
227
|
-
autoPlay
|
|
228
|
-
});
|
|
229
|
-
zip.file("index.html", html);
|
|
230
|
-
return zip.generateAsync({
|
|
231
|
-
type: "blob",
|
|
232
|
-
compression: "DEFLATE",
|
|
233
|
-
compressionOptions: { level: 6 }
|
|
234
|
-
});
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
export {
|
|
238
|
-
collectImagePaths,
|
|
239
|
-
docToHtml,
|
|
240
|
-
docToHtmlZip
|
|
241
|
-
};
|
|
242
|
-
//# sourceMappingURL=chunk-UM5V2XZG.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/html/index.ts","../src/html/htmlTemplate.ts"],"sourcesContent":["/**\n * HTML Export Module — @bendyline/squisq-formats/html\n *\n * Exports squisq documents as self-contained HTML files or ZIP archives.\n *\n * Two export modes:\n *\n * 1. **Single HTML** (`docToHtml`) — Everything embedded in one file:\n * - SquisqPlayer IIFE bundle inlined in `<script>`\n * - Images base64-encoded as data URIs\n * - Timer-based playback (no audio — too large for inline)\n *\n * 2. **ZIP Archive** (`docToHtmlZip`) — Multi-file package:\n * - `index.html` referencing external `squisq-player.js`\n * - `squisq-player.js` — the IIFE bundle\n * - `images/` — extracted image files\n * - `audio/` — optional audio segment files (enables full playback)\n *\n * @example\n * ```ts\n * import { PLAYER_BUNDLE } from '@bendyline/squisq-react/standalone-source';\n * import { docToHtml, docToHtmlZip } from '@bendyline/squisq-formats/html';\n *\n * // Single HTML file\n * const html = docToHtml(doc, { playerScript: PLAYER_BUNDLE, images });\n *\n * // ZIP archive\n * const zipBlob = await docToHtmlZip(doc, { playerScript: PLAYER_BUNDLE, images, audio });\n * ```\n */\n\nimport type { Doc } from '@bendyline/squisq/schemas';\nimport JSZip from 'jszip';\nimport {\n generateInlineHtml,\n generateExternalHtml,\n type HtmlExportOptions,\n} from './htmlTemplate.js';\nimport { extractFilename } from './imageUtils.js';\n\n// ── Public Types ───────────────────────────────────────────────────\n\nexport type { HtmlExportOptions };\n\nexport interface HtmlZipExportOptions extends HtmlExportOptions {\n /**\n * Map of audio segment identifiers to binary audio data.\n * Keys should match the audio segment `name` or `url` fields in the Doc.\n * When provided, audio files are included in the ZIP and full playback is enabled.\n */\n audio?: Map<string, ArrayBuffer>;\n}\n\n// ── Single HTML Export ─────────────────────────────────────────────\n\n/**\n * Export a Doc as a single, self-contained HTML file.\n *\n * The player JS is embedded inline, images are base64-encoded as data URIs,\n * and playback uses a timer (no audio) for compact file size.\n *\n * @param doc - The Doc to export\n * @param options - Export options (must include `playerScript`)\n * @returns Complete HTML document as a string\n *\n * @example\n * ```ts\n * const html = docToHtml(myDoc, {\n * playerScript: PLAYER_BUNDLE,\n * images: imageMap,\n * title: 'My Document',\n * mode: 'slideshow',\n * });\n * const blob = new Blob([html], { type: 'text/html' });\n * ```\n */\nexport function docToHtml(doc: Doc, options: HtmlExportOptions): string {\n return generateInlineHtml(doc, options);\n}\n\n// ── ZIP Archive Export ─────────────────────────────────────────────\n\n/**\n * Export a Doc as a ZIP archive containing HTML, JS, images, and optionally audio.\n *\n * The archive structure:\n * ```\n * document.zip\n * ├── index.html # HTML page referencing squisq-player.js\n * ├── squisq-player.js # Standalone IIFE bundle\n * ├── images/ # Extracted image files\n * │ ├── hero.jpg\n * │ └── ...\n * └── audio/ # Optional audio segment files\n * ├── intro.mp3\n * └── ...\n * ```\n *\n * @param doc - The Doc to export\n * @param options - Export options (must include `playerScript`)\n * @returns A Promise resolving to a ZIP Blob\n *\n * @example\n * ```ts\n * const blob = await docToHtmlZip(myDoc, {\n * playerScript: PLAYER_BUNDLE,\n * images: imageMap,\n * audio: audioMap,\n * });\n * // Trigger browser download\n * const url = URL.createObjectURL(blob);\n * ```\n */\nexport async function docToHtmlZip(doc: Doc, options: HtmlZipExportOptions): Promise<Blob> {\n const { playerScript, images, audio, mode = 'slideshow', title, autoPlay } = options;\n\n const zip = new JSZip();\n\n // 1. Add player JS as a separate file\n zip.file('squisq-player.js', playerScript);\n\n // 2. Add images to images/ folder and build path mapping\n const imagePathMap: Record<string, string> = {};\n if (images) {\n for (const [originalPath, buffer] of images.entries()) {\n const filename = extractFilename(originalPath);\n const zipPath = `images/${filename}`;\n zip.file(zipPath, buffer);\n imagePathMap[originalPath] = zipPath;\n }\n }\n\n // 3. Add audio to audio/ folder and build path mapping\n const audioPathMap: Record<string, string> = {};\n if (audio) {\n for (const [segmentKey, buffer] of audio.entries()) {\n const filename = extractFilename(segmentKey);\n // Ensure .mp3 extension\n const finalName = filename.includes('.') ? filename : `${filename}.mp3`;\n const zipPath = `audio/${finalName}`;\n zip.file(zipPath, buffer);\n audioPathMap[segmentKey] = zipPath;\n }\n }\n\n // 4. Generate HTML that references external files\n const html = generateExternalHtml(doc, {\n playerScriptPath: 'squisq-player.js',\n imagePathMap: Object.keys(imagePathMap).length > 0 ? imagePathMap : undefined,\n audioPathMap: Object.keys(audioPathMap).length > 0 ? audioPathMap : undefined,\n mode,\n title,\n autoPlay,\n });\n zip.file('index.html', html);\n\n // 5. Generate ZIP blob\n return zip.generateAsync({\n type: 'blob',\n compression: 'DEFLATE',\n compressionOptions: { level: 6 },\n });\n}\n\n// ── Re-exports ─────────────────────────────────────────────────────\n\nexport { collectImagePaths } from './htmlTemplate.js';\nexport { inferMimeType, arrayBufferToBase64DataUrl, extractFilename } from './imageUtils.js';\n","/**\n * HTML Template Generation for SquisqPlayer Exports\n *\n * Generates complete, self-contained HTML documents that load the SquisqPlayer\n * IIFE bundle and render a Doc as either an interactive slideshow or a static\n * scrollable document.\n *\n * Two variants:\n * 1. **Inline** — All JS, CSS, and images are embedded in the HTML (single file).\n * 2. **External** — JS is referenced via `<script src>`, images via relative paths.\n */\n\nimport type { Doc, Layer, Block } from '@bendyline/squisq/schemas';\nimport { arrayBufferToBase64DataUrl, inferMimeType } from './imageUtils.js';\n\n// ── Types ──────────────────────────────────────────────────────────\n\nexport interface HtmlExportOptions {\n /** The IIFE player bundle source code (from @bendyline/squisq-react/standalone-source) */\n playerScript: string;\n\n /**\n * Map of relative image paths (as they appear in the Doc) to binary image data.\n * For inline HTML export, these are converted to base64 data URIs.\n * For ZIP export, these are written as separate files.\n */\n images?: Map<string, ArrayBuffer>;\n\n /**\n * Map of audio segment identifiers to binary audio data.\n * Keys should match the audio segment `name` or `url` fields in the Doc.\n * Only used in ZIP exports — single HTML uses timer-based playback.\n */\n audio?: Map<string, ArrayBuffer>;\n\n /** Rendering mode: 'slideshow' (interactive, default) or 'static' (scrollable) */\n mode?: 'slideshow' | 'static';\n\n /** HTML page title (default: 'Squisq Document') */\n title?: string;\n\n /** Auto-play slideshow on load (default: false) */\n autoPlay?: boolean;\n\n /**\n * Squisq theme ID to apply (e.g., 'documentary', 'cinematic').\n * When set, the theme is assigned to the Doc before rendering,\n * so the SquisqPlayer renders with that theme's colors and typography.\n */\n themeId?: string;\n}\n\n// ── Image Path Collection ──────────────────────────────────────────\n\n/**\n * Collect all relative image paths referenced in a Doc's layers and template blocks.\n * Returns a Set of unique relative paths that need to be resolved.\n */\nexport function collectImagePaths(doc: Doc): Set<string> {\n const paths = new Set<string>();\n\n function addIfRelative(src: string | undefined) {\n if (\n !src ||\n src.startsWith('data:') ||\n src.startsWith('blob:') ||\n src.startsWith('http://') ||\n src.startsWith('https://')\n ) {\n return;\n }\n paths.add(src);\n }\n\n function scanLayers(layers: Layer[] | undefined) {\n if (!layers) return;\n for (const layer of layers) {\n if (layer.type === 'image') addIfRelative(layer.content.src);\n if (layer.type === 'video') {\n addIfRelative(layer.content.src);\n addIfRelative(layer.content.posterSrc);\n }\n if (layer.type === 'map') addIfRelative(layer.content.staticSrc);\n }\n }\n\n function scanBlock(block: Block) {\n scanLayers(block.layers);\n if (block.children) block.children.forEach(scanBlock);\n }\n\n // Scan all blocks\n doc.blocks.forEach(scanBlock);\n\n // Scan start block hero\n if (doc.startBlock?.heroSrc) addIfRelative(doc.startBlock.heroSrc);\n\n // Scan persistent layers\n if (doc.persistentLayers) {\n for (const pl of doc.persistentLayers.bottomLayers ?? []) {\n if ('src' in pl && typeof pl.src === 'string') addIfRelative(pl.src);\n }\n for (const pl of doc.persistentLayers.topLayers ?? []) {\n if ('src' in pl && typeof pl.src === 'string') addIfRelative(pl.src);\n }\n }\n\n // Scan template block fields that reference images\n for (const block of doc.blocks) {\n scanTemplateImageFields(block as unknown as Record<string, unknown>, paths);\n }\n\n return paths;\n}\n\n/**\n * Recursively scan any object for known image-bearing field names.\n */\nfunction scanTemplateImageFields(obj: Record<string, unknown>, paths: Set<string>): void {\n if (!obj || typeof obj !== 'object') return;\n\n const imageFieldNames = [\n 'imageSrc',\n 'src',\n 'heroSrc',\n 'backgroundImage',\n 'posterSrc',\n 'staticSrc',\n 'videoSrc',\n 'thumbnailSrc',\n ];\n\n for (const key of imageFieldNames) {\n const val = obj[key];\n if (typeof val === 'string' && val && !val.startsWith('data:') && !val.startsWith('http')) {\n paths.add(val);\n }\n // Handle nested objects like { src: '...' }\n if (val && typeof val === 'object' && !Array.isArray(val)) {\n scanTemplateImageFields(val as Record<string, unknown>, paths);\n }\n }\n\n // Handle arrays (e.g., images: [{ src: '...' }])\n if (Array.isArray(obj)) {\n for (const item of obj) {\n if (item && typeof item === 'object') {\n scanTemplateImageFields(item as Record<string, unknown>, paths);\n }\n }\n }\n\n // Recurse into known array/object fields\n for (const key of ['images', 'accentImage', 'backgroundVideo', 'children', 'blocks']) {\n const val = obj[key];\n if (Array.isArray(val)) {\n for (const item of val) {\n if (item && typeof item === 'object') {\n scanTemplateImageFields(item as Record<string, unknown>, paths);\n }\n }\n } else if (val && typeof val === 'object') {\n scanTemplateImageFields(val as Record<string, unknown>, paths);\n }\n }\n}\n\n// ── HTML Generation ────────────────────────────────────────────────\n\n/**\n * Escape a string for safe embedding in a `<script>` tag.\n * Prevents `</script>` from closing the tag prematurely.\n */\nfunction escapeForScript(str: string): string {\n return str.replace(/<\\/(script)/gi, '<\\\\/$1');\n}\n\n/**\n * Generate a complete inline HTML document with all JS and images embedded.\n *\n * @param doc - The Doc to render\n * @param options - Export options\n * @returns Complete HTML string\n */\nexport function generateInlineHtml(doc: Doc, options: HtmlExportOptions): string {\n const {\n playerScript,\n images,\n mode = 'slideshow',\n title = 'Squisq Document',\n autoPlay = false,\n themeId,\n } = options;\n\n // Apply theme to doc if specified (the SquisqPlayer respects doc.themeId at render time)\n if (themeId) {\n doc = { ...doc, themeId };\n }\n\n // Build base64 image map\n const imageMap: Record<string, string> = {};\n if (images) {\n for (const [path, buffer] of images.entries()) {\n const mimeType = inferMimeType(path);\n imageMap[path] = arrayBufferToBase64DataUrl(buffer, mimeType);\n }\n }\n\n const docJson = escapeForScript(JSON.stringify(doc));\n const imageMapJson = escapeForScript(JSON.stringify(imageMap));\n\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"UTF-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n<title>${escapeHtml(title)}</title>\n<style>\n*,*::before,*::after{box-sizing:border-box}\nhtml,body{margin:0;padding:0;height:100%;background:#1a1a2e;color:#e0e0e0;font-family:system-ui,-apple-system,sans-serif}\n#squisq-root{width:100%;height:100%;display:flex;align-items:center;justify-content:center}\n${mode === 'static' ? '#squisq-root{align-items:flex-start;overflow-y:auto;background:#fff;color:#1f2937}' : ''}\n</style>\n</head>\n<body>\n<div id=\"squisq-root\"></div>\n<script>${escapeForScript(playerScript)}</script>\n<script>\n(function(){\n var doc = JSON.parse(${JSON.stringify(docJson)});\n var images = JSON.parse(${JSON.stringify(imageMapJson)});\n SquisqPlayer.mount(document.getElementById(\"squisq-root\"), doc, {\n mode: ${JSON.stringify(mode)},\n images: images,\n autoPlay: ${JSON.stringify(autoPlay)},\n basePath: \".\"\n });\n})();\n</script>\n</body>\n</html>`;\n}\n\n/**\n * Generate an HTML document that references external JS and image files.\n * Used for ZIP exports where files sit alongside the HTML.\n *\n * @param doc - The Doc to render (image/audio paths should already be rewritten to relative)\n * @param options - Export options (playerScript is not embedded, referenced via src)\n * @returns Complete HTML string\n */\nexport function generateExternalHtml(\n doc: Doc,\n options: Pick<HtmlExportOptions, 'mode' | 'title' | 'autoPlay'> & {\n /** Relative path to the player JS file (e.g., 'squisq-player.js') */\n playerScriptPath: string;\n /** Map of original image paths to their rewritten relative paths in the ZIP */\n imagePathMap?: Record<string, string>;\n /** Map of audio segment IDs to their rewritten relative paths in the ZIP */\n audioPathMap?: Record<string, string>;\n },\n): string {\n const {\n playerScriptPath,\n imagePathMap,\n audioPathMap,\n mode = 'slideshow',\n title = 'Squisq Document',\n autoPlay = false,\n } = options;\n\n const docJson = escapeForScript(JSON.stringify(doc));\n const imageMapJson = imagePathMap ? escapeForScript(JSON.stringify(imagePathMap)) : '{}';\n const audioMapJson = audioPathMap ? escapeForScript(JSON.stringify(audioPathMap)) : 'null';\n\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"UTF-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n<title>${escapeHtml(title)}</title>\n<style>\n*,*::before,*::after{box-sizing:border-box}\nhtml,body{margin:0;padding:0;height:100%;background:#1a1a2e;color:#e0e0e0;font-family:system-ui,-apple-system,sans-serif}\n#squisq-root{width:100%;height:100%;display:flex;align-items:center;justify-content:center}\n${mode === 'static' ? '#squisq-root{align-items:flex-start;overflow-y:auto;background:#fff;color:#1f2937}' : ''}\n</style>\n</head>\n<body>\n<div id=\"squisq-root\"></div>\n<script src=\"${escapeHtml(playerScriptPath)}\"></script>\n<script>\n(function(){\n var doc = JSON.parse(${JSON.stringify(docJson)});\n var images = JSON.parse(${JSON.stringify(imageMapJson)});\n var audio = ${audioMapJson};\n SquisqPlayer.mount(document.getElementById(\"squisq-root\"), doc, {\n mode: ${JSON.stringify(mode)},\n images: images,\n audio: audio,\n autoPlay: ${JSON.stringify(autoPlay)},\n basePath: \".\"\n });\n})();\n</script>\n</body>\n</html>`;\n}\n\n/**\n * Escape HTML special characters to prevent injection.\n */\nfunction escapeHtml(str: string): string {\n return str\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n}\n"],"mappings":"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","names":[]}
|