@bendyline/squisq-formats 2.1.0 → 2.2.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.
Files changed (195) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE.md +20 -0
  3. package/README.md +1 -1
  4. package/dist/{chunk-NNHKUXKA.js → chunk-26ISNJ7Y.js} +85 -65
  5. package/dist/{chunk-NKAJPJ4G.js → chunk-2JJ5RFDZ.js} +0 -1
  6. package/dist/{chunk-WQSHGBLN.js → chunk-3NKXBZSR.js} +193 -42
  7. package/dist/{chunk-KURGXM4I.js → chunk-4V3KCHAP.js} +3 -4
  8. package/dist/{chunk-MLX2BOJC.js → chunk-6RQOV3B3.js} +1 -2
  9. package/dist/{chunk-EW54IRRS.js → chunk-6S6GU3ZG.js} +5 -6
  10. package/dist/{chunk-FE6OJV6O.js → chunk-7AWFHP5U.js} +1 -1
  11. package/dist/{chunk-RFAPOKHJ.js → chunk-AD2WT564.js} +59 -9
  12. package/dist/{chunk-O3GVVND4.js → chunk-AONELFLA.js} +0 -1
  13. package/dist/{chunk-SC67HYQJ.js → chunk-EJTNGKEA.js} +5 -8
  14. package/dist/chunk-GX7RAUME.js +121 -0
  15. package/dist/{chunk-SSUPBUF5.js → chunk-IIQYS2YH.js} +0 -1
  16. package/dist/{chunk-RLU7UFYU.js → chunk-IPN56VLW.js} +83 -58
  17. package/dist/{chunk-DTDF6QDP.js → chunk-JE6LSIHE.js} +81 -20
  18. package/dist/{chunk-U4MRIFKL.js → chunk-JU2RHXUB.js} +0 -1
  19. package/dist/{chunk-4VUWTSGM.js → chunk-K6XRMVPW.js} +64 -31
  20. package/dist/{chunk-ODL3SSPT.js → chunk-KXOZMWBS.js} +0 -1
  21. package/dist/chunk-OGS5VCGJ.js +446 -0
  22. package/dist/{chunk-GVS2XXV6.js → chunk-PJXJI2LY.js} +449 -57
  23. package/dist/{chunk-74GO3FVS.js → chunk-PU7REGWV.js} +5 -8
  24. package/dist/{chunk-PN52A5AA.js → chunk-SBUW7NHR.js} +0 -1
  25. package/dist/{chunk-QFLDYKCR.js → chunk-TAAENIRB.js} +5 -8
  26. package/dist/{chunk-7ARKUCQT.js → chunk-X2DEAXNK.js} +54 -2
  27. package/dist/container/index.js +1 -2
  28. package/dist/csv/index.d.ts +27 -2
  29. package/dist/csv/index.js +1 -2
  30. package/dist/docx/index.d.ts +5 -1
  31. package/dist/docx/index.js +9 -11
  32. package/dist/epub/index.d.ts +2 -0
  33. package/dist/epub/index.js +5 -6
  34. package/dist/{export-D2NkylDT.d.ts → export-D9msROJS.d.ts} +18 -6
  35. package/dist/extract-MN7LA3NL.js +13 -0
  36. package/dist/html/index.d.ts +11 -4
  37. package/dist/html/index.js +3 -4
  38. package/dist/images-ESPQKVTW.js +6 -0
  39. package/dist/{import-K8mfc0fz.d.ts → import-C3htUTss.d.ts} +5 -1
  40. package/dist/{import-DTkDxHmZ.d.ts → import-C8whCC7_.d.ts} +6 -0
  41. package/dist/index.d.ts +7 -7
  42. package/dist/index.js +28 -26
  43. package/dist/infer/index.d.ts +3 -3
  44. package/dist/infer/index.js +7 -9
  45. package/dist/{layouts-BHrgZ5FS.d.ts → layouts-CTdPlB-u.d.ts} +1 -1
  46. package/dist/layouts-DRWZGSPD.js +10 -0
  47. package/dist/{mapTheme-IR27S6IV.js → mapTheme-4TWH25FT.js} +1 -2
  48. package/dist/ooxml/index.d.ts +3 -3
  49. package/dist/ooxml/index.js +14 -13
  50. package/dist/pdf/index.d.ts +18 -0
  51. package/dist/pdf/index.js +2 -3
  52. package/dist/pptx/index.d.ts +4 -4
  53. package/dist/pptx/index.js +11 -13
  54. package/dist/{reader-B9L8Ucbj.d.ts → reader-B_m1aKZC.d.ts} +30 -1
  55. package/dist/registry/index.d.ts +21 -5
  56. package/dist/registry/index.js +9 -6
  57. package/dist/{themeReader-DJKErl_j.d.ts → themeReader-DCtwC83Q.d.ts} +1 -1
  58. package/dist/xlsx/index.d.ts +3 -3
  59. package/dist/xlsx/index.js +6 -7
  60. package/package.json +6 -3
  61. package/dist/chunk-4VUWTSGM.js.map +0 -1
  62. package/dist/chunk-6M7Z25LA.js +0 -46
  63. package/dist/chunk-6M7Z25LA.js.map +0 -1
  64. package/dist/chunk-74GO3FVS.js.map +0 -1
  65. package/dist/chunk-7ARKUCQT.js.map +0 -1
  66. package/dist/chunk-DTDF6QDP.js.map +0 -1
  67. package/dist/chunk-EW54IRRS.js.map +0 -1
  68. package/dist/chunk-FE6OJV6O.js.map +0 -1
  69. package/dist/chunk-GVS2XXV6.js.map +0 -1
  70. package/dist/chunk-KURGXM4I.js.map +0 -1
  71. package/dist/chunk-MLX2BOJC.js.map +0 -1
  72. package/dist/chunk-NKAJPJ4G.js.map +0 -1
  73. package/dist/chunk-NNHKUXKA.js.map +0 -1
  74. package/dist/chunk-O3GVVND4.js.map +0 -1
  75. package/dist/chunk-ODL3SSPT.js.map +0 -1
  76. package/dist/chunk-PN52A5AA.js.map +0 -1
  77. package/dist/chunk-QFLDYKCR.js.map +0 -1
  78. package/dist/chunk-RFAPOKHJ.js.map +0 -1
  79. package/dist/chunk-RLU7UFYU.js.map +0 -1
  80. package/dist/chunk-SC67HYQJ.js.map +0 -1
  81. package/dist/chunk-SSUPBUF5.js.map +0 -1
  82. package/dist/chunk-U4MRIFKL.js.map +0 -1
  83. package/dist/chunk-UGYF5AZE.js +0 -275
  84. package/dist/chunk-UGYF5AZE.js.map +0 -1
  85. package/dist/chunk-WQSHGBLN.js.map +0 -1
  86. package/dist/chunk-YRT7GQ5Y.js +0 -28
  87. package/dist/chunk-YRT7GQ5Y.js.map +0 -1
  88. package/dist/container/index.js.map +0 -1
  89. package/dist/csv/index.js.map +0 -1
  90. package/dist/docx/index.js.map +0 -1
  91. package/dist/epub/index.js.map +0 -1
  92. package/dist/extract-OJ7ZQV6P.js +0 -15
  93. package/dist/extract-OJ7ZQV6P.js.map +0 -1
  94. package/dist/html/index.js.map +0 -1
  95. package/dist/images-7FBWPKE3.js +0 -7
  96. package/dist/images-7FBWPKE3.js.map +0 -1
  97. package/dist/index.js.map +0 -1
  98. package/dist/infer/index.js.map +0 -1
  99. package/dist/layouts-5VDIRPIJ.js +0 -12
  100. package/dist/layouts-5VDIRPIJ.js.map +0 -1
  101. package/dist/mapTheme-IR27S6IV.js.map +0 -1
  102. package/dist/ooxml/index.js.map +0 -1
  103. package/dist/pdf/index.js.map +0 -1
  104. package/dist/pptx/index.js.map +0 -1
  105. package/dist/registry/index.js.map +0 -1
  106. package/dist/xlsx/index.js.map +0 -1
  107. package/src/__tests__/container.test.ts +0 -230
  108. package/src/__tests__/convert.test.ts +0 -495
  109. package/src/__tests__/csvImport.test.ts +0 -84
  110. package/src/__tests__/docxExport.test.ts +0 -491
  111. package/src/__tests__/docxImport.test.ts +0 -531
  112. package/src/__tests__/epub.test.ts +0 -649
  113. package/src/__tests__/exportThemeReconciliation.test.ts +0 -87
  114. package/src/__tests__/formatRegistry.test.ts +0 -174
  115. package/src/__tests__/html.test.ts +0 -439
  116. package/src/__tests__/htmlImport.test.ts +0 -57
  117. package/src/__tests__/inferTheme.test.ts +0 -135
  118. package/src/__tests__/lossyWarnings.test.ts +0 -146
  119. package/src/__tests__/ooxml.test.ts +0 -271
  120. package/src/__tests__/ooxmlCancellation.test.ts +0 -113
  121. package/src/__tests__/ooxmlThemeReader.test.ts +0 -92
  122. package/src/__tests__/pdfExport.test.ts +0 -322
  123. package/src/__tests__/pdfImport.test.ts +0 -384
  124. package/src/__tests__/plainHtml.test.ts +0 -417
  125. package/src/__tests__/plainHtmlBundle.test.ts +0 -253
  126. package/src/__tests__/pptxExport.test.ts +0 -138
  127. package/src/__tests__/pptxImport.test.ts +0 -145
  128. package/src/__tests__/pptxInferFixtures.ts +0 -314
  129. package/src/__tests__/pptxLayoutInfer.test.ts +0 -395
  130. package/src/__tests__/roundTrip.test.ts +0 -201
  131. package/src/__tests__/roundTripAssets.test.ts +0 -50
  132. package/src/__tests__/roundTripMatrix.fixtures.ts +0 -86
  133. package/src/__tests__/roundTripMatrix.helpers.ts +0 -154
  134. package/src/__tests__/roundTripMatrix.test.ts +0 -142
  135. package/src/__tests__/sharedContainer.test.ts +0 -41
  136. package/src/__tests__/sharedImages.test.ts +0 -61
  137. package/src/__tests__/xlsxExport.test.ts +0 -164
  138. package/src/__tests__/xlsxImport.test.ts +0 -80
  139. package/src/__tests__/zipSafety.test.ts +0 -317
  140. package/src/container/index.ts +0 -94
  141. package/src/csv/index.ts +0 -188
  142. package/src/docx/export.ts +0 -1375
  143. package/src/docx/import.ts +0 -1250
  144. package/src/docx/index.ts +0 -26
  145. package/src/docx/styles.ts +0 -145
  146. package/src/epub/export.ts +0 -968
  147. package/src/epub/index.ts +0 -20
  148. package/src/html/docsHtmlBundle.ts +0 -373
  149. package/src/html/htmlTemplate.ts +0 -385
  150. package/src/html/imageUtils.ts +0 -61
  151. package/src/html/import.ts +0 -297
  152. package/src/html/index.ts +0 -212
  153. package/src/html/plainHtml.ts +0 -790
  154. package/src/html/plainHtmlBundle.ts +0 -421
  155. package/src/index.ts +0 -109
  156. package/src/infer/extract.ts +0 -127
  157. package/src/infer/index.ts +0 -199
  158. package/src/infer/mapTheme.ts +0 -176
  159. package/src/infer/types.ts +0 -27
  160. package/src/ooxml/index.ts +0 -111
  161. package/src/ooxml/namespaces.ts +0 -217
  162. package/src/ooxml/readUtils.ts +0 -44
  163. package/src/ooxml/reader.ts +0 -318
  164. package/src/ooxml/themeReader.ts +0 -197
  165. package/src/ooxml/types.ts +0 -103
  166. package/src/ooxml/writer.ts +0 -339
  167. package/src/ooxml/xmlUtils.ts +0 -123
  168. package/src/pdf/export.ts +0 -1084
  169. package/src/pdf/import.ts +0 -1164
  170. package/src/pdf/index.ts +0 -29
  171. package/src/pdf/styles.ts +0 -180
  172. package/src/pptx/export.ts +0 -1184
  173. package/src/pptx/import.ts +0 -455
  174. package/src/pptx/index.ts +0 -52
  175. package/src/pptx/layouts.ts +0 -1222
  176. package/src/pptx/styles.ts +0 -96
  177. package/src/pptx/templates.ts +0 -187
  178. package/src/registry/convert.ts +0 -433
  179. package/src/registry/defaultFormats.ts +0 -413
  180. package/src/registry/errors.ts +0 -46
  181. package/src/registry/index.ts +0 -43
  182. package/src/registry/registry.ts +0 -48
  183. package/src/registry/types.ts +0 -170
  184. package/src/shared/boundedZipArchive.ts +0 -383
  185. package/src/shared/container.ts +0 -28
  186. package/src/shared/fidelity.ts +0 -130
  187. package/src/shared/images.ts +0 -44
  188. package/src/shared/inlineRuns.ts +0 -99
  189. package/src/shared/text.ts +0 -41
  190. package/src/shared/zipEntryCount.ts +0 -151
  191. package/src/shared/zipLimits.ts +0 -296
  192. package/src/shared/zipSafety.ts +0 -19
  193. package/src/xlsx/export.ts +0 -253
  194. package/src/xlsx/import.ts +0 -160
  195. package/src/xlsx/index.ts +0 -35
@@ -1,385 +0,0 @@
1
- /**
2
- * HTML Template Generation for SquisqPlayer Exports
3
- *
4
- * Generates complete, self-contained HTML documents that load the SquisqPlayer
5
- * IIFE bundle and render a Doc as either an interactive slideshow or a static
6
- * scrollable document.
7
- *
8
- * Two variants:
9
- * 1. **Inline** — All JS, CSS, and images are embedded in the HTML (single file).
10
- * 2. **External** — JS is referenced via `<script src>`, images via relative paths.
11
- */
12
-
13
- import type { Doc, Layer, Block, ThemeRegistry } from '@bendyline/squisq/schemas';
14
- import { readFrontmatterThemeId } from '@bendyline/squisq/markdown';
15
- import { arrayBufferToBase64DataUrl, inferMimeType } from './imageUtils.js';
16
-
17
- // ── Types ──────────────────────────────────────────────────────────
18
-
19
- export interface HtmlExportOptions {
20
- /** The IIFE player bundle source code (from @bendyline/squisq-react/standalone-source) */
21
- playerScript: string;
22
-
23
- /**
24
- * Map of relative image paths (as they appear in the Doc) to binary image data.
25
- * For inline HTML export, these are converted to base64 data URIs.
26
- * For ZIP export, these are written as separate files.
27
- */
28
- images?: Map<string, ArrayBuffer>;
29
-
30
- /**
31
- * Map of audio segment identifiers to binary audio data.
32
- * Keys should match the audio segment `name` or `url` fields in the Doc.
33
- * Only used in ZIP exports — single HTML uses timer-based playback.
34
- */
35
- audio?: Map<string, ArrayBuffer>;
36
-
37
- /** Rendering mode: 'slideshow' (interactive, default) or 'static' (scrollable) */
38
- mode?: 'slideshow' | 'static';
39
-
40
- /** HTML page title (default: 'Squisq Document') */
41
- title?: string;
42
-
43
- /** Auto-play slideshow on load (default: false) */
44
- autoPlay?: boolean;
45
-
46
- /**
47
- * Squisq theme ID to apply (e.g., 'documentary', 'cinematic').
48
- * When set, the theme is assigned to the Doc before rendering,
49
- * so the SquisqPlayer renders with that theme's colors and typography.
50
- */
51
- themeId?: string;
52
- /** Explicit caller-owned registry for non-document custom themes. */
53
- themeRegistry?: ThemeRegistry;
54
- }
55
-
56
- // ── Image Path Collection ──────────────────────────────────────────
57
-
58
- /**
59
- * Collect all relative image paths referenced in a Doc's layers and template blocks.
60
- * Returns a Set of unique relative paths that need to be resolved.
61
- */
62
- export function collectImagePaths(doc: Doc): Set<string> {
63
- const paths = new Set<string>();
64
-
65
- function addIfRelative(src: string | undefined) {
66
- if (
67
- !src ||
68
- src.startsWith('data:') ||
69
- src.startsWith('blob:') ||
70
- src.startsWith('http://') ||
71
- src.startsWith('https://')
72
- ) {
73
- return;
74
- }
75
- paths.add(src);
76
- }
77
-
78
- function scanLayers(layers: Layer[] | undefined) {
79
- if (!layers) return;
80
- for (const layer of layers) {
81
- if (layer.type === 'image') addIfRelative(layer.content.src);
82
- if (layer.type === 'video') {
83
- addIfRelative(layer.content.src);
84
- addIfRelative(layer.content.posterSrc);
85
- }
86
- if (layer.type === 'map') addIfRelative(layer.content.staticSrc);
87
- }
88
- }
89
-
90
- function scanBlock(block: Block) {
91
- scanLayers(block.layers);
92
- if (block.contents) scanMarkdownNodes(block.contents, addIfRelative);
93
- if (block.children) block.children.forEach(scanBlock);
94
- }
95
-
96
- // Scan all blocks
97
- doc.blocks.forEach(scanBlock);
98
-
99
- // Scan start block hero
100
- if (doc.startBlock?.heroSrc) addIfRelative(doc.startBlock.heroSrc);
101
-
102
- // Scan persistent layers
103
- if (doc.persistentLayers) {
104
- for (const pl of doc.persistentLayers.bottomLayers ?? []) {
105
- if ('src' in pl && typeof pl.src === 'string') addIfRelative(pl.src);
106
- }
107
- for (const pl of doc.persistentLayers.topLayers ?? []) {
108
- if ('src' in pl && typeof pl.src === 'string') addIfRelative(pl.src);
109
- }
110
- }
111
-
112
- // Scan template block fields that reference images
113
- for (const block of doc.blocks) {
114
- scanTemplateImageFields(block as unknown as Record<string, unknown>, paths);
115
- }
116
-
117
- return paths;
118
- }
119
-
120
- /**
121
- * Walk markdown body nodes for image references. Catches both shorthand
122
- * `![alt](url)` (type `image`, `url` field) and raw HTML `<img src>` tags
123
- * (type `htmlBlock`/`htmlInline`, `htmlChildren` sub-tree) — the latter is
124
- * how the WYSIWYG editor round-trips resized images, since markdown
125
- * shorthand has no width syntax.
126
- */
127
- function scanMarkdownNodes(nodes: unknown[], visit: (src: string | undefined) => void): void {
128
- for (const node of nodes) {
129
- if (!node || typeof node !== 'object') continue;
130
- const n = node as Record<string, unknown>;
131
- if (n.type === 'image' && typeof n.url === 'string') {
132
- visit(n.url);
133
- }
134
- if ((n.type === 'htmlBlock' || n.type === 'htmlInline') && Array.isArray(n.htmlChildren)) {
135
- scanHtmlNodes(n.htmlChildren, visit);
136
- }
137
- if (Array.isArray(n.children)) {
138
- scanMarkdownNodes(n.children, visit);
139
- }
140
- }
141
- }
142
-
143
- /** Walk a parsed HTML sub-tree for `<img>` src attributes. */
144
- function scanHtmlNodes(nodes: unknown[], visit: (src: string | undefined) => void): void {
145
- for (const node of nodes) {
146
- if (!node || typeof node !== 'object') continue;
147
- const n = node as Record<string, unknown>;
148
- if (n.type !== 'htmlElement') continue;
149
- if (n.tagName === 'img') {
150
- const attrs = n.attributes as Record<string, string> | undefined;
151
- if (attrs && typeof attrs.src === 'string') visit(attrs.src);
152
- }
153
- if (Array.isArray(n.children)) {
154
- scanHtmlNodes(n.children, visit);
155
- }
156
- }
157
- }
158
-
159
- /**
160
- * Recursively scan any object for known image-bearing field names.
161
- */
162
- function scanTemplateImageFields(obj: Record<string, unknown>, paths: Set<string>): void {
163
- if (!obj || typeof obj !== 'object') return;
164
-
165
- const imageFieldNames = [
166
- 'imageSrc',
167
- 'src',
168
- 'heroSrc',
169
- 'backgroundImage',
170
- 'posterSrc',
171
- 'staticSrc',
172
- 'videoSrc',
173
- 'thumbnailSrc',
174
- ];
175
-
176
- for (const key of imageFieldNames) {
177
- const val = obj[key];
178
- if (typeof val === 'string' && val && !val.startsWith('data:') && !val.startsWith('http')) {
179
- paths.add(val);
180
- }
181
- // Handle nested objects like { src: '...' }
182
- if (val && typeof val === 'object' && !Array.isArray(val)) {
183
- scanTemplateImageFields(val as Record<string, unknown>, paths);
184
- }
185
- }
186
-
187
- // Handle arrays (e.g., images: [{ src: '...' }])
188
- if (Array.isArray(obj)) {
189
- for (const item of obj) {
190
- if (item && typeof item === 'object') {
191
- scanTemplateImageFields(item as Record<string, unknown>, paths);
192
- }
193
- }
194
- }
195
-
196
- // Recurse into known array/object fields
197
- for (const key of ['images', 'accentImage', 'backgroundVideo', 'children', 'blocks']) {
198
- const val = obj[key];
199
- if (Array.isArray(val)) {
200
- for (const item of val) {
201
- if (item && typeof item === 'object') {
202
- scanTemplateImageFields(item as Record<string, unknown>, paths);
203
- }
204
- }
205
- } else if (val && typeof val === 'object') {
206
- scanTemplateImageFields(val as Record<string, unknown>, paths);
207
- }
208
- }
209
- }
210
-
211
- // ── HTML Generation ────────────────────────────────────────────────
212
-
213
- /**
214
- * Escape a string for safe embedding in a `<script>` tag.
215
- * Prevents `</script>` from closing the tag prematurely.
216
- */
217
- function escapeForScript(str: string): string {
218
- return str.replace(/<\/(script)/gi, '<\\/$1');
219
- }
220
-
221
- /**
222
- * Generate a complete inline HTML document with all JS and images embedded.
223
- *
224
- * @param doc - The Doc to render
225
- * @param options - Export options
226
- * @returns Complete HTML string
227
- */
228
- export function generateInlineHtml(doc: Doc, options: HtmlExportOptions): string {
229
- const {
230
- playerScript,
231
- images,
232
- mode = 'slideshow',
233
- title = 'Squisq Document',
234
- autoPlay = false,
235
- themeId,
236
- themeRegistry,
237
- } = options;
238
-
239
- doc = applyThemeSelection(doc, themeId, themeRegistry);
240
-
241
- // Build base64 image map
242
- const imageMap: Record<string, string> = {};
243
- if (images) {
244
- for (const [path, buffer] of images.entries()) {
245
- const mimeType = inferMimeType(path);
246
- imageMap[path] = arrayBufferToBase64DataUrl(buffer, mimeType);
247
- }
248
- }
249
-
250
- const docJson = escapeForScript(JSON.stringify(doc));
251
- const imageMapJson = escapeForScript(JSON.stringify(imageMap));
252
-
253
- return `<!DOCTYPE html>
254
- <html lang="en">
255
- <head>
256
- <meta charset="UTF-8">
257
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
258
- <title>${escapeHtml(title)}</title>
259
- <style>
260
- *,*::before,*::after{box-sizing:border-box}
261
- html,body{margin:0;padding:0;height:100%;background:#1a1a2e;color:#e0e0e0;font-family:system-ui,-apple-system,sans-serif}
262
- #squisq-root{width:100%;height:100%;display:flex;align-items:center;justify-content:center}
263
- ${mode === 'static' ? '#squisq-root{display:block}' : ''}
264
- </style>
265
- </head>
266
- <body>
267
- <div id="squisq-root"></div>
268
- <script>${escapeForScript(playerScript)}</script>
269
- <script>
270
- (function(){
271
- var doc = JSON.parse(${JSON.stringify(docJson)});
272
- var images = JSON.parse(${JSON.stringify(imageMapJson)});
273
- SquisqPlayer.mount(document.getElementById("squisq-root"), doc, {
274
- mode: ${JSON.stringify(mode)},
275
- images: images,
276
- autoPlay: ${JSON.stringify(autoPlay)},
277
- basePath: "."
278
- });
279
- })();
280
- </script>
281
- </body>
282
- </html>`;
283
- }
284
-
285
- /**
286
- * Generate an HTML document that references external JS and image files.
287
- * Used for ZIP exports where files sit alongside the HTML.
288
- *
289
- * @param doc - The Doc to render (image/audio paths should already be rewritten to relative)
290
- * @param options - Export options (playerScript is not embedded, referenced via src)
291
- * @returns Complete HTML string
292
- */
293
- export function generateExternalHtml(
294
- doc: Doc,
295
- options: Pick<HtmlExportOptions, 'mode' | 'title' | 'autoPlay' | 'themeId' | 'themeRegistry'> & {
296
- /** Relative path to the player JS file (e.g., 'squisq-player.js') */
297
- playerScriptPath: string;
298
- /** Map of original image paths to their rewritten relative paths in the ZIP */
299
- imagePathMap?: Record<string, string>;
300
- /** Map of audio segment IDs to their rewritten relative paths in the ZIP */
301
- audioPathMap?: Record<string, string>;
302
- },
303
- ): string {
304
- const {
305
- playerScriptPath,
306
- imagePathMap,
307
- audioPathMap,
308
- mode = 'slideshow',
309
- title = 'Squisq Document',
310
- autoPlay = false,
311
- themeId,
312
- themeRegistry,
313
- } = options;
314
-
315
- doc = applyThemeSelection(doc, themeId, themeRegistry);
316
-
317
- const docJson = escapeForScript(JSON.stringify(doc));
318
- const imageMapJson = imagePathMap ? escapeForScript(JSON.stringify(imagePathMap)) : '{}';
319
- const audioMapJson = audioPathMap ? escapeForScript(JSON.stringify(audioPathMap)) : 'null';
320
-
321
- return `<!DOCTYPE html>
322
- <html lang="en">
323
- <head>
324
- <meta charset="UTF-8">
325
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
326
- <title>${escapeHtml(title)}</title>
327
- <style>
328
- *,*::before,*::after{box-sizing:border-box}
329
- html,body{margin:0;padding:0;height:100%;background:#1a1a2e;color:#e0e0e0;font-family:system-ui,-apple-system,sans-serif}
330
- #squisq-root{width:100%;height:100%;display:flex;align-items:center;justify-content:center}
331
- ${mode === 'static' ? '#squisq-root{display:block}' : ''}
332
- </style>
333
- </head>
334
- <body>
335
- <div id="squisq-root"></div>
336
- <script src="${escapeHtml(playerScriptPath)}"></script>
337
- <script>
338
- (function(){
339
- var doc = JSON.parse(${JSON.stringify(docJson)});
340
- var images = JSON.parse(${JSON.stringify(imageMapJson)});
341
- var audio = ${audioMapJson};
342
- SquisqPlayer.mount(document.getElementById("squisq-root"), doc, {
343
- mode: ${JSON.stringify(mode)},
344
- images: images,
345
- audio: audio,
346
- autoPlay: ${JSON.stringify(autoPlay)},
347
- basePath: "."
348
- });
349
- })();
350
- </script>
351
- </body>
352
- </html>`;
353
- }
354
-
355
- function applyThemeSelection(
356
- doc: Doc,
357
- themeId: string | undefined,
358
- registry: ThemeRegistry | undefined,
359
- ): Doc {
360
- const selectedId = themeId ?? doc.themeId ?? readFrontmatterThemeId(doc.frontmatter);
361
- if (!selectedId) return doc;
362
- const selectedDocTheme = doc.customThemes?.some((theme) => theme.id === selectedId);
363
- if (selectedDocTheme || !registry) {
364
- return selectedId === doc.themeId ? doc : { ...doc, themeId: selectedId };
365
- }
366
- const registered = registry.get(selectedId);
367
- if (!registered) return selectedId === doc.themeId ? doc : { ...doc, themeId: selectedId };
368
- return {
369
- ...doc,
370
- themeId: selectedId,
371
- customThemes: [...(doc.customThemes ?? []), registered],
372
- };
373
- }
374
-
375
- /**
376
- * Escape HTML special characters to prevent injection.
377
- */
378
- function escapeHtml(str: string): string {
379
- return str
380
- .replace(/&/g, '&amp;')
381
- .replace(/</g, '&lt;')
382
- .replace(/>/g, '&gt;')
383
- .replace(/"/g, '&quot;')
384
- .replace(/'/g, '&#39;');
385
- }
@@ -1,61 +0,0 @@
1
- /**
2
- * Image Utilities for HTML Export
3
- *
4
- * Browser-compatible helpers for converting image data to base64 data URIs
5
- * and inferring MIME types from filenames.
6
- */
7
-
8
- import { extToMime } from '../shared/images.js';
9
-
10
- /**
11
- * Infer a MIME type from a filename's extension.
12
- * Returns 'application/octet-stream' for unknown types.
13
- *
14
- * Thin wrapper over the shared {@link extToMime} map so html export, docx
15
- * import, and pptx import all agree on extension → MIME.
16
- */
17
- export function inferMimeType(filename: string): string {
18
- return extToMime(filename.split('.').pop() ?? '');
19
- }
20
-
21
- /**
22
- * Convert an ArrayBuffer to a base64-encoded data URI string.
23
- *
24
- * @param buffer - The binary image data
25
- * @param mimeType - MIME type (e.g., 'image/jpeg'). If not provided, defaults to
26
- * 'application/octet-stream'.
27
- * @returns A `data:` URI string
28
- */
29
- export function arrayBufferToBase64DataUrl(buffer: ArrayBuffer, mimeType: string): string {
30
- const bytes = new Uint8Array(buffer);
31
- // Keep chunks divisible by three so concatenating their independently
32
- // encoded results cannot introduce padding in the middle of the stream.
33
- // This avoids both quadratic string concatenation and one image-sized
34
- // intermediate binary string for large embedded assets.
35
- const chunkSize = 3 * 8192;
36
- const chunks: string[] = [];
37
- for (let offset = 0; offset < bytes.length; offset += chunkSize) {
38
- const end = Math.min(offset + chunkSize, bytes.length);
39
- let binary = '';
40
- for (let i = offset; i < end; i++) {
41
- binary += String.fromCharCode(bytes[i]);
42
- }
43
- chunks.push(btoa(binary));
44
- }
45
- return `data:${mimeType};base64,${chunks.join('')}`;
46
- }
47
-
48
- /**
49
- * Extract the filename from a path or URL (strips directory and query).
50
- *
51
- * @example
52
- * extractFilename('images/hero.jpg') // 'hero.jpg'
53
- * extractFilename('https://example.com/photo.png?v=2') // 'photo.png'
54
- */
55
- export function extractFilename(path: string): string {
56
- // Strip query/hash
57
- const clean = path.split('?')[0].split('#')[0];
58
- // Get last segment
59
- const parts = clean.split('/');
60
- return parts[parts.length - 1] || path;
61
- }