@bendyline/squisq-formats 1.2.3 → 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.
Files changed (65) hide show
  1. package/dist/__tests__/html.test.js +78 -4
  2. package/dist/__tests__/html.test.js.map +1 -1
  3. package/dist/__tests__/pdfExport.test.js +1 -1
  4. package/dist/__tests__/pdfExport.test.js.map +1 -1
  5. package/dist/__tests__/plainHtml.test.d.ts +5 -0
  6. package/dist/__tests__/plainHtml.test.d.ts.map +1 -0
  7. package/dist/__tests__/plainHtml.test.js +327 -0
  8. package/dist/__tests__/plainHtml.test.js.map +1 -0
  9. package/dist/__tests__/plainHtmlBundle.test.d.ts +11 -0
  10. package/dist/__tests__/plainHtmlBundle.test.d.ts.map +1 -0
  11. package/dist/__tests__/plainHtmlBundle.test.js +210 -0
  12. package/dist/__tests__/plainHtmlBundle.test.js.map +1 -0
  13. package/dist/chunk-33YRFXZZ.js +1187 -0
  14. package/dist/chunk-33YRFXZZ.js.map +1 -0
  15. package/dist/{chunk-EUQQYBZZ.js → chunk-ERZ627GR.js} +4 -4
  16. package/dist/chunk-ERZ627GR.js.map +1 -0
  17. package/dist/{chunk-MEZF76JA.js → chunk-UDS45KUJ.js} +68 -25
  18. package/dist/chunk-UDS45KUJ.js.map +1 -0
  19. package/dist/{chunk-NGWHV77G.js → chunk-VN2KEOYB.js} +7 -6
  20. package/dist/chunk-VN2KEOYB.js.map +1 -0
  21. package/dist/docx/export.d.ts.map +1 -1
  22. package/dist/docx/export.js +153 -22
  23. package/dist/docx/export.js.map +1 -1
  24. package/dist/epub/export.js +3 -3
  25. package/dist/epub/export.js.map +1 -1
  26. package/dist/html/docsHtmlBundle.d.ts +53 -0
  27. package/dist/html/docsHtmlBundle.d.ts.map +1 -0
  28. package/dist/html/docsHtmlBundle.js +299 -0
  29. package/dist/html/docsHtmlBundle.js.map +1 -0
  30. package/dist/html/htmlTemplate.d.ts.map +1 -1
  31. package/dist/html/htmlTemplate.js +43 -0
  32. package/dist/html/htmlTemplate.js.map +1 -1
  33. package/dist/html/index.d.ts +6 -0
  34. package/dist/html/index.d.ts.map +1 -1
  35. package/dist/html/index.js +31 -3
  36. package/dist/html/index.js.map +1 -1
  37. package/dist/html/plainHtml.d.ts +79 -0
  38. package/dist/html/plainHtml.d.ts.map +1 -0
  39. package/dist/html/plainHtml.js +614 -0
  40. package/dist/html/plainHtml.js.map +1 -0
  41. package/dist/html/plainHtmlBundle.d.ts +93 -0
  42. package/dist/html/plainHtmlBundle.d.ts.map +1 -0
  43. package/dist/html/plainHtmlBundle.js +357 -0
  44. package/dist/html/plainHtmlBundle.js.map +1 -0
  45. package/dist/pptx/export.d.ts.map +1 -1
  46. package/dist/pptx/export.js +10 -8
  47. package/dist/pptx/export.js.map +1 -1
  48. package/package.json +3 -2
  49. package/src/__tests__/html.test.ts +82 -4
  50. package/src/__tests__/pdfExport.test.ts +1 -1
  51. package/src/__tests__/plainHtml.test.ts +372 -0
  52. package/src/__tests__/plainHtmlBundle.test.ts +235 -0
  53. package/src/docx/export.ts +163 -22
  54. package/src/epub/export.ts +3 -3
  55. package/src/html/docsHtmlBundle.ts +369 -0
  56. package/src/html/htmlTemplate.ts +40 -0
  57. package/src/html/index.ts +32 -3
  58. package/src/html/plainHtml.ts +736 -0
  59. package/src/html/plainHtmlBundle.ts +419 -0
  60. package/src/pptx/export.ts +10 -9
  61. package/dist/chunk-EUQQYBZZ.js.map +0 -1
  62. package/dist/chunk-MEZF76JA.js.map +0 -1
  63. package/dist/chunk-NGWHV77G.js.map +0 -1
  64. package/dist/chunk-UM5V2XZG.js +0 -242
  65. package/dist/chunk-UM5V2XZG.js.map +0 -1
@@ -0,0 +1,1187 @@
1
+ import {
2
+ arrayBufferToBase64DataUrl,
3
+ extractFilename,
4
+ inferMimeType
5
+ } from "./chunk-A3FHLTY5.js";
6
+
7
+ // src/html/index.ts
8
+ import JSZip3 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.contents) scanMarkdownNodes(block.contents, addIfRelative);
33
+ if (block.children) block.children.forEach(scanBlock);
34
+ }
35
+ doc.blocks.forEach(scanBlock);
36
+ if (doc.startBlock?.heroSrc) addIfRelative(doc.startBlock.heroSrc);
37
+ if (doc.persistentLayers) {
38
+ for (const pl of doc.persistentLayers.bottomLayers ?? []) {
39
+ if ("src" in pl && typeof pl.src === "string") addIfRelative(pl.src);
40
+ }
41
+ for (const pl of doc.persistentLayers.topLayers ?? []) {
42
+ if ("src" in pl && typeof pl.src === "string") addIfRelative(pl.src);
43
+ }
44
+ }
45
+ for (const block of doc.blocks) {
46
+ scanTemplateImageFields(block, paths);
47
+ }
48
+ return paths;
49
+ }
50
+ function scanMarkdownNodes(nodes, visit) {
51
+ for (const node of nodes) {
52
+ if (!node || typeof node !== "object") continue;
53
+ const n = node;
54
+ if (n.type === "image" && typeof n.url === "string") {
55
+ visit(n.url);
56
+ }
57
+ if ((n.type === "htmlBlock" || n.type === "htmlInline") && Array.isArray(n.htmlChildren)) {
58
+ scanHtmlNodes(n.htmlChildren, visit);
59
+ }
60
+ if (Array.isArray(n.children)) {
61
+ scanMarkdownNodes(n.children, visit);
62
+ }
63
+ }
64
+ }
65
+ function scanHtmlNodes(nodes, visit) {
66
+ for (const node of nodes) {
67
+ if (!node || typeof node !== "object") continue;
68
+ const n = node;
69
+ if (n.type !== "htmlElement") continue;
70
+ if (n.tagName === "img") {
71
+ const attrs = n.attributes;
72
+ if (attrs && typeof attrs.src === "string") visit(attrs.src);
73
+ }
74
+ if (Array.isArray(n.children)) {
75
+ scanHtmlNodes(n.children, visit);
76
+ }
77
+ }
78
+ }
79
+ function scanTemplateImageFields(obj, paths) {
80
+ if (!obj || typeof obj !== "object") return;
81
+ const imageFieldNames = [
82
+ "imageSrc",
83
+ "src",
84
+ "heroSrc",
85
+ "backgroundImage",
86
+ "posterSrc",
87
+ "staticSrc",
88
+ "videoSrc",
89
+ "thumbnailSrc"
90
+ ];
91
+ for (const key of imageFieldNames) {
92
+ const val = obj[key];
93
+ if (typeof val === "string" && val && !val.startsWith("data:") && !val.startsWith("http")) {
94
+ paths.add(val);
95
+ }
96
+ if (val && typeof val === "object" && !Array.isArray(val)) {
97
+ scanTemplateImageFields(val, paths);
98
+ }
99
+ }
100
+ if (Array.isArray(obj)) {
101
+ for (const item of obj) {
102
+ if (item && typeof item === "object") {
103
+ scanTemplateImageFields(item, paths);
104
+ }
105
+ }
106
+ }
107
+ for (const key of ["images", "accentImage", "backgroundVideo", "children", "blocks"]) {
108
+ const val = obj[key];
109
+ if (Array.isArray(val)) {
110
+ for (const item of val) {
111
+ if (item && typeof item === "object") {
112
+ scanTemplateImageFields(item, paths);
113
+ }
114
+ }
115
+ } else if (val && typeof val === "object") {
116
+ scanTemplateImageFields(val, paths);
117
+ }
118
+ }
119
+ }
120
+ function escapeForScript(str) {
121
+ return str.replace(/<\/(script)/gi, "<\\/$1");
122
+ }
123
+ function generateInlineHtml(doc, options) {
124
+ const {
125
+ playerScript,
126
+ images,
127
+ mode = "slideshow",
128
+ title = "Squisq Document",
129
+ autoPlay = false,
130
+ themeId
131
+ } = options;
132
+ if (themeId) {
133
+ doc = { ...doc, themeId };
134
+ }
135
+ const imageMap = {};
136
+ if (images) {
137
+ for (const [path, buffer] of images.entries()) {
138
+ const mimeType = inferMimeType(path);
139
+ imageMap[path] = arrayBufferToBase64DataUrl(buffer, mimeType);
140
+ }
141
+ }
142
+ const docJson = escapeForScript(JSON.stringify(doc));
143
+ const imageMapJson = escapeForScript(JSON.stringify(imageMap));
144
+ return `<!DOCTYPE html>
145
+ <html lang="en">
146
+ <head>
147
+ <meta charset="UTF-8">
148
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
149
+ <title>${escapeHtml(title)}</title>
150
+ <style>
151
+ *,*::before,*::after{box-sizing:border-box}
152
+ html,body{margin:0;padding:0;height:100%;background:#1a1a2e;color:#e0e0e0;font-family:system-ui,-apple-system,sans-serif}
153
+ #squisq-root{width:100%;height:100%;display:flex;align-items:center;justify-content:center}
154
+ ${mode === "static" ? "#squisq-root{align-items:flex-start;overflow-y:auto;background:#fff;color:#1f2937}" : ""}
155
+ </style>
156
+ </head>
157
+ <body>
158
+ <div id="squisq-root"></div>
159
+ <script>${escapeForScript(playerScript)}</script>
160
+ <script>
161
+ (function(){
162
+ var doc = JSON.parse(${JSON.stringify(docJson)});
163
+ var images = JSON.parse(${JSON.stringify(imageMapJson)});
164
+ SquisqPlayer.mount(document.getElementById("squisq-root"), doc, {
165
+ mode: ${JSON.stringify(mode)},
166
+ images: images,
167
+ autoPlay: ${JSON.stringify(autoPlay)},
168
+ basePath: "."
169
+ });
170
+ })();
171
+ </script>
172
+ </body>
173
+ </html>`;
174
+ }
175
+ function generateExternalHtml(doc, options) {
176
+ const {
177
+ playerScriptPath,
178
+ imagePathMap,
179
+ audioPathMap,
180
+ mode = "slideshow",
181
+ title = "Squisq Document",
182
+ autoPlay = false
183
+ } = options;
184
+ const docJson = escapeForScript(JSON.stringify(doc));
185
+ const imageMapJson = imagePathMap ? escapeForScript(JSON.stringify(imagePathMap)) : "{}";
186
+ const audioMapJson = audioPathMap ? escapeForScript(JSON.stringify(audioPathMap)) : "null";
187
+ return `<!DOCTYPE html>
188
+ <html lang="en">
189
+ <head>
190
+ <meta charset="UTF-8">
191
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
192
+ <title>${escapeHtml(title)}</title>
193
+ <style>
194
+ *,*::before,*::after{box-sizing:border-box}
195
+ html,body{margin:0;padding:0;height:100%;background:#1a1a2e;color:#e0e0e0;font-family:system-ui,-apple-system,sans-serif}
196
+ #squisq-root{width:100%;height:100%;display:flex;align-items:center;justify-content:center}
197
+ ${mode === "static" ? "#squisq-root{align-items:flex-start;overflow-y:auto;background:#fff;color:#1f2937}" : ""}
198
+ </style>
199
+ </head>
200
+ <body>
201
+ <div id="squisq-root"></div>
202
+ <script src="${escapeHtml(playerScriptPath)}"></script>
203
+ <script>
204
+ (function(){
205
+ var doc = JSON.parse(${JSON.stringify(docJson)});
206
+ var images = JSON.parse(${JSON.stringify(imageMapJson)});
207
+ var audio = ${audioMapJson};
208
+ SquisqPlayer.mount(document.getElementById("squisq-root"), doc, {
209
+ mode: ${JSON.stringify(mode)},
210
+ images: images,
211
+ audio: audio,
212
+ autoPlay: ${JSON.stringify(autoPlay)},
213
+ basePath: "."
214
+ });
215
+ })();
216
+ </script>
217
+ </body>
218
+ </html>`;
219
+ }
220
+ function escapeHtml(str) {
221
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
222
+ }
223
+
224
+ // src/html/plainHtml.ts
225
+ import { resolveFontFamily, buildGoogleFontsUrl, resolveTheme } from "@bendyline/squisq/schemas";
226
+ function markdownDocToPlainHtml(doc, options = {}) {
227
+ const { title = "Document", images, links, themeId, iconsCss } = options;
228
+ const theme = options.theme ?? (themeId ? resolveTheme(themeId) : void 0) ?? (typeof doc.frontmatter?.themeId === "string" ? resolveTheme(doc.frontmatter.themeId) : void 0);
229
+ const ctx = { images, links };
230
+ const body = renderTopLevel(doc.children, ctx);
231
+ const fontsLink = theme ? renderFontsLink(theme) : "";
232
+ const usesIcons = docUsesIcons(doc);
233
+ let iconsLink = "";
234
+ if (usesIcons) {
235
+ iconsLink = iconsCss ? `<style data-fa-inline>
236
+ ${iconsCss}
237
+ </style>
238
+ ` : FONT_AWESOME_LINK;
239
+ }
240
+ const themedCss = theme ? renderThemedCss(theme) : DEFAULT_CSS;
241
+ return `<!DOCTYPE html>
242
+ <html lang="en">
243
+ <head>
244
+ <meta charset="UTF-8">
245
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
246
+ <title>${escapeHtml2(title)}</title>
247
+ ${fontsLink}${iconsLink}<style>
248
+ ${themedCss}
249
+ ${FEATURE_CSS}
250
+ </style>
251
+ </head>
252
+ <body>
253
+ ${body}
254
+ </body>
255
+ </html>`;
256
+ }
257
+ var FONT_AWESOME_LINK = `<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css" integrity="sha512-SnH5WK+bZxgPHs44uWIX+LLJAJ9/2PkPKZ5QiAj6Ta86w+fsb2TkcmfRyVX3pBnMFcV7oQPJkl9QevSCWr3W6A==" crossorigin="anonymous" referrerpolicy="no-referrer">
258
+ `;
259
+ function docUsesIcons(doc) {
260
+ function visit(node) {
261
+ if (!node || typeof node !== "object") return false;
262
+ const n = node;
263
+ if (n.type === "inlineIcon") return true;
264
+ if (Array.isArray(n.children)) {
265
+ for (const child of n.children) if (visit(child)) return true;
266
+ }
267
+ return false;
268
+ }
269
+ return visit(doc);
270
+ }
271
+ function renderTopLevel(children, ctx) {
272
+ const out = [];
273
+ for (let i = 0; i < children.length; i++) {
274
+ const node = children[i];
275
+ if (node && node.type === "heading") {
276
+ const heading = node;
277
+ const tpl = heading.templateAnnotation?.template;
278
+ if (tpl === "leftFeature" || tpl === "rightFeature") {
279
+ const end = findSectionEnd(children, i);
280
+ const sectionBody = children.slice(i + 1, end);
281
+ out.push(renderFeatureSection(heading, sectionBody, tpl, ctx));
282
+ i = end - 1;
283
+ continue;
284
+ }
285
+ }
286
+ out.push(nodeToHtml(node, ctx));
287
+ }
288
+ return out.join("\n");
289
+ }
290
+ function findSectionEnd(nodes, from) {
291
+ for (let i = from + 1; i < nodes.length; i++) {
292
+ const n = nodes[i];
293
+ if (n && n.type === "heading") return i;
294
+ }
295
+ return nodes.length;
296
+ }
297
+ function renderFeatureSection(heading, bodyNodes, side, ctx) {
298
+ const headingTag = `h${Math.min(Math.max(heading.depth ?? 2, 1), 6)}`;
299
+ const headingHtml = `<${headingTag}>${childrenToHtml({ children: heading.children }, ctx)}</${headingTag}>`;
300
+ const featured = takeFirstImage(bodyNodes);
301
+ const media = featured.image ? renderFeatureImage(featured.image, ctx) : '<div class="squisq-feature__media squisq-feature__media--empty"></div>';
302
+ const textHtml = [headingHtml, ...featured.remaining.map((n) => nodeToHtml(n, ctx))].filter((s) => s.length > 0).join("\n");
303
+ const sideClass = side === "leftFeature" ? "squisq-feature--left" : "squisq-feature--right";
304
+ return `<section class="squisq-feature ${sideClass}">
305
+ ${media}
306
+ <div class="squisq-feature__body">
307
+ ${textHtml}
308
+ </div>
309
+ </section>`;
310
+ }
311
+ function renderFeatureImage(img, ctx) {
312
+ const resolved = ctx?.images?.get(img.src) ?? img.src;
313
+ const attrs = [`src="${escapeAttr(resolved)}"`, `alt="${escapeAttr(img.alt)}"`];
314
+ if (typeof img.width === "number") attrs.push(`width="${img.width}"`);
315
+ if (typeof img.height === "number") attrs.push(`height="${img.height}"`);
316
+ const sizedClass = typeof img.width === "number" || typeof img.height === "number" ? " squisq-feature__media--sized" : "";
317
+ return `<div class="squisq-feature__media${sizedClass}"><img ${attrs.join(" ")} /></div>`;
318
+ }
319
+ function takeFirstImage(nodes) {
320
+ for (let i = 0; i < nodes.length; i++) {
321
+ const found = extractFirstImageFromBlock(nodes[i]);
322
+ if (!found) continue;
323
+ const remaining = [...nodes];
324
+ if (found.replacement === null) {
325
+ remaining.splice(i, 1);
326
+ } else {
327
+ remaining[i] = found.replacement;
328
+ }
329
+ return { image: found.image, remaining };
330
+ }
331
+ return { image: null, remaining: nodes };
332
+ }
333
+ function extractFirstImageFromBlock(block) {
334
+ if (!block || typeof block !== "object") return null;
335
+ const b = block;
336
+ if (b.type === "paragraph" && Array.isArray(b.children)) {
337
+ const kids = b.children;
338
+ const imgIdx = kids.findIndex(
339
+ (k) => k.type === "image" && typeof k.url === "string"
340
+ );
341
+ if (imgIdx >= 0) {
342
+ const img = kids[imgIdx];
343
+ const remainingKids = [...kids.slice(0, imgIdx), ...kids.slice(imgIdx + 1)].filter(
344
+ (k) => !isBlankInline(k)
345
+ );
346
+ const replacement = remainingKids.length === 0 ? null : { ...b, children: remainingKids };
347
+ return {
348
+ image: { src: img.url, alt: img.alt ?? "" },
349
+ replacement
350
+ };
351
+ }
352
+ }
353
+ if (b.type === "htmlBlock" && Array.isArray(b.htmlChildren)) {
354
+ const hit = findHtmlImg(b.htmlChildren);
355
+ if (hit) return { image: hit, replacement: null };
356
+ }
357
+ return null;
358
+ }
359
+ function isBlankInline(node) {
360
+ if (!node || typeof node !== "object") return false;
361
+ const n = node;
362
+ return n.type === "text" && typeof n.value === "string" && n.value.trim().length === 0;
363
+ }
364
+ function findHtmlImg(nodes) {
365
+ for (const n of nodes) {
366
+ if (n.type !== "htmlElement") continue;
367
+ if (n.tagName.toLowerCase() === "img") {
368
+ const src = n.attributes.src;
369
+ if (typeof src === "string" && src) {
370
+ return {
371
+ src,
372
+ alt: typeof n.attributes.alt === "string" ? n.attributes.alt : "",
373
+ width: parseHtmlDim(n.attributes.width),
374
+ height: parseHtmlDim(n.attributes.height)
375
+ };
376
+ }
377
+ }
378
+ const nested = findHtmlImg(n.children);
379
+ if (nested) return nested;
380
+ }
381
+ return null;
382
+ }
383
+ function parseHtmlDim(raw) {
384
+ if (raw === void 0) return void 0;
385
+ const n = parseFloat(raw);
386
+ return Number.isFinite(n) && n > 0 ? n : void 0;
387
+ }
388
+ var FEATURE_CSS = ` .squisq-feature {
389
+ display: flex;
390
+ flex-wrap: wrap;
391
+ gap: 1.5em;
392
+ align-items: center;
393
+ margin: 1.75em 0;
394
+ }
395
+ .squisq-feature--right { flex-direction: row-reverse; }
396
+ .squisq-feature__media {
397
+ flex: 0 0 42%;
398
+ min-width: 0;
399
+ }
400
+ .squisq-feature__media--empty { display: none; }
401
+ /* Default: image fills the media column. */
402
+ .squisq-feature__media img {
403
+ width: 100%;
404
+ height: auto;
405
+ display: block;
406
+ border-radius: 6px;
407
+ }
408
+ /* "Sized" media: the image carried explicit width/height attrs (the
409
+ WYSIWYG editor wrote them after a resize). Honor the attribute
410
+ values and center the image inside the media column with padding. */
411
+ .squisq-feature__media--sized {
412
+ display: flex;
413
+ align-items: center;
414
+ justify-content: center;
415
+ padding: 1em;
416
+ box-sizing: border-box;
417
+ }
418
+ /* Leave width and height untouched here -- the HTML attributes set
419
+ the intrinsic dimensions, and CSS overrides would silently discard
420
+ the author's sizing. max-width: 100% still keeps the image from
421
+ overflowing the column on narrow viewports; HTML5 derives the
422
+ aspect ratio from the width/height pair so the scale stays right. */
423
+ .squisq-feature__media--sized img {
424
+ max-width: 100%;
425
+ display: block;
426
+ border-radius: 6px;
427
+ }
428
+ .squisq-feature__body {
429
+ flex: 1 1 0;
430
+ min-width: 0;
431
+ }
432
+ .squisq-feature__body > :first-child { margin-top: 0; }
433
+ .squisq-feature__body > :last-child { margin-bottom: 0; }
434
+ @media (max-width: 600px) {
435
+ .squisq-feature, .squisq-feature--right { flex-direction: column; }
436
+ .squisq-feature__media { flex-basis: auto; width: 100%; }
437
+ }`;
438
+ var DEFAULT_CSS = ` body { font-family: system-ui, -apple-system, sans-serif; max-width: 800px; margin: 2em auto; padding: 0 1em; line-height: 1.6; color: #1f2937; }
439
+ h1, h2, h3, h4, h5, h6 { margin-top: 1.5em; margin-bottom: 0.5em; }
440
+ pre { background: #f3f4f6; padding: 1em; border-radius: 4px; overflow-x: auto; }
441
+ code { background: #f3f4f6; padding: 0.15em 0.3em; border-radius: 3px; font-size: 0.9em; }
442
+ pre code { background: none; padding: 0; }
443
+ blockquote { border-left: 3px solid #d1d5db; margin-left: 0; padding-left: 1em; color: #6b7280; }
444
+ /* Images: cap at the container width so nothing overflows, but only
445
+ force aspect-ratio height when the author didn't set explicit
446
+ dimensions on the <img> tag. The WYSIWYG editor writes width/height
447
+ attributes after a resize \u2014 overriding them here would silently
448
+ ignore the user's sizing. */
449
+ img { max-width: 100%; }
450
+ img:not([width]):not([height]) { height: auto; }
451
+ a { color: #3b82f6; }
452
+ table { border-collapse: collapse; width: 100%; margin: 1em 0; }
453
+ th, td { border: 1px solid #d1d5db; padding: 6px 10px; text-align: left; }
454
+ th { background: #f3f4f6; font-weight: 600; }
455
+ hr { border: none; border-top: 1px solid #d1d5db; margin: 1.5em 0; }`;
456
+ function renderThemedCss(theme) {
457
+ const bodyFamily = resolveFontFamily(theme.typography.bodyFont, "system-ui, sans-serif");
458
+ const titleFamily = resolveFontFamily(theme.typography.titleFont, "Georgia, serif");
459
+ const monoFamily = resolveFontFamily(theme.typography.monoFont, "Consolas, monospace");
460
+ const lineHeight = theme.typography.lineHeight ?? 1.6;
461
+ const titleLineHeight = theme.typography.titleLineHeight ?? 1.25;
462
+ const titleWeight = theme.typography.titleWeight === "normal" ? 400 : 700;
463
+ const c = theme.colors;
464
+ return ` :root {
465
+ --plain-bg: ${c.background};
466
+ --plain-text: ${c.text};
467
+ --plain-muted: ${c.textMuted};
468
+ --plain-primary: ${c.primary};
469
+ --plain-secondary: ${c.secondary};
470
+ --plain-accent: ${c.highlight};
471
+ --plain-bg-light: ${c.backgroundLight};
472
+ --plain-body-font: ${bodyFamily};
473
+ --plain-title-font: ${titleFamily};
474
+ --plain-mono-font: ${monoFamily};
475
+ }
476
+ body {
477
+ font-family: var(--plain-body-font);
478
+ max-width: 800px;
479
+ margin: 2em auto;
480
+ padding: 0 1em;
481
+ line-height: ${lineHeight};
482
+ color: var(--plain-text);
483
+ background: var(--plain-bg);
484
+ }
485
+ h1, h2, h3, h4, h5, h6 {
486
+ font-family: var(--plain-title-font);
487
+ color: var(--plain-text);
488
+ margin-top: 1.5em;
489
+ margin-bottom: 0.5em;
490
+ line-height: ${titleLineHeight};
491
+ font-weight: ${titleWeight};
492
+ }
493
+ p { margin: 0.75em 0; }
494
+ pre {
495
+ background: var(--plain-bg-light);
496
+ padding: 1em;
497
+ border-radius: 4px;
498
+ overflow-x: auto;
499
+ font-family: var(--plain-mono-font);
500
+ }
501
+ code {
502
+ background: var(--plain-bg-light);
503
+ padding: 0.15em 0.3em;
504
+ border-radius: 3px;
505
+ font-size: 0.9em;
506
+ font-family: var(--plain-mono-font);
507
+ }
508
+ pre code { background: none; padding: 0; }
509
+ blockquote {
510
+ border-left: 3px solid var(--plain-primary);
511
+ margin-left: 0;
512
+ padding-left: 1em;
513
+ color: var(--plain-muted);
514
+ }
515
+ /* See DEFAULT_CSS comment: only auto-scale height when the author
516
+ didn't set explicit width/height attributes. */
517
+ img { max-width: 100%; }
518
+ img:not([width]):not([height]) { height: auto; }
519
+ /* Blend the theme primary toward the body text color so links stay
520
+ theme-flavored but read clearly on every theme's background \u2014 some
521
+ themes (e.g. Gezellig) pick a mid-tone primary that's almost
522
+ invisible on a dark page when used neat. Underline makes the link
523
+ unambiguous independent of the color contrast. */
524
+ a {
525
+ color: color-mix(in srgb, var(--plain-primary) 65%, var(--plain-text));
526
+ text-decoration: underline;
527
+ text-decoration-thickness: 1px;
528
+ text-underline-offset: 2px;
529
+ }
530
+ a:hover { color: var(--plain-accent); }
531
+ table { border-collapse: collapse; width: 100%; margin: 1em 0; }
532
+ th, td {
533
+ border: 1px solid color-mix(in srgb, var(--plain-muted) 30%, transparent);
534
+ padding: 6px 10px;
535
+ text-align: left;
536
+ }
537
+ th {
538
+ background: var(--plain-bg-light);
539
+ color: var(--plain-text);
540
+ font-family: var(--plain-title-font);
541
+ font-weight: 600;
542
+ }
543
+ hr {
544
+ border: none;
545
+ border-top: 1px solid color-mix(in srgb, var(--plain-muted) 40%, transparent);
546
+ margin: 1.5em 0;
547
+ }`;
548
+ }
549
+ function renderFontsLink(theme) {
550
+ const url = buildGoogleFontsUrl([
551
+ theme.typography.bodyFont,
552
+ theme.typography.titleFont,
553
+ theme.typography.monoFont
554
+ ]);
555
+ if (!url) return "";
556
+ return `<link rel="preconnect" href="https://fonts.googleapis.com">
557
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
558
+ <link rel="stylesheet" href="${escapeAttr(url)}">
559
+ `;
560
+ }
561
+ function nodeToHtml(node, ctx) {
562
+ if (!node) return "";
563
+ switch (node.type) {
564
+ case "heading": {
565
+ const depth = Math.min(Math.max(node.depth ?? 1, 1), 6);
566
+ return `<h${depth}>${childrenToHtml(node, ctx)}</h${depth}>`;
567
+ }
568
+ case "paragraph":
569
+ return `<p>${childrenToHtml(node, ctx)}</p>`;
570
+ case "text":
571
+ return escapeHtml2(node.value ?? "");
572
+ case "strong":
573
+ return `<strong>${childrenToHtml(node, ctx)}</strong>`;
574
+ case "emphasis":
575
+ return `<em>${childrenToHtml(node, ctx)}</em>`;
576
+ case "delete":
577
+ return `<del>${childrenToHtml(node, ctx)}</del>`;
578
+ case "inlineCode":
579
+ return `<code>${escapeHtml2(node.value ?? "")}</code>`;
580
+ case "code": {
581
+ const lang = node.lang ? ` class="language-${escapeAttr(node.lang)}"` : "";
582
+ return `<pre><code${lang}>${escapeHtml2(node.value ?? "")}</code></pre>`;
583
+ }
584
+ case "blockquote":
585
+ return `<blockquote>${childrenToHtml(node, ctx)}</blockquote>`;
586
+ case "list": {
587
+ const tag = node.ordered ? "ol" : "ul";
588
+ const start = node.ordered && typeof node.start === "number" && node.start !== 1 ? ` start="${node.start}"` : "";
589
+ return `<${tag}${start}>${childrenToHtml(node, ctx)}</${tag}>`;
590
+ }
591
+ case "listItem":
592
+ return `<li>${childrenToHtml(node, ctx)}</li>`;
593
+ case "link": {
594
+ const original = node.url ?? "";
595
+ const rewritten = ctx?.links?.get(original) ?? original;
596
+ return `<a href="${escapeAttr(rewritten)}">${childrenToHtml(node, ctx)}</a>`;
597
+ }
598
+ case "image": {
599
+ const original = node.url ?? "";
600
+ const resolved = ctx?.images?.get(original) ?? original;
601
+ return `<img src="${escapeAttr(resolved)}" alt="${escapeAttr(node.alt ?? "")}" />`;
602
+ }
603
+ case "thematicBreak":
604
+ return "<hr />";
605
+ case "table":
606
+ return tableToHtml(node, ctx);
607
+ case "inlineIcon": {
608
+ const family = escapeAttr(node.family ?? "solid");
609
+ const name = escapeAttr(node.name ?? "");
610
+ const token = escapeAttr(node.token ?? `${node.family}:${node.name}`);
611
+ return `<i class="fa-${family} fa-${name}" data-icon="${token}" aria-hidden="true"></i>`;
612
+ }
613
+ case "htmlBlock":
614
+ case "htmlInline":
615
+ return htmlChildrenToHtml(node.htmlChildren, ctx);
616
+ default: {
617
+ const withChildren = node;
618
+ if (Array.isArray(withChildren.children)) {
619
+ return childrenToHtml(node, ctx);
620
+ }
621
+ if (typeof withChildren.value === "string") {
622
+ return escapeHtml2(withChildren.value);
623
+ }
624
+ return "";
625
+ }
626
+ }
627
+ }
628
+ function childrenToHtml(node, ctx) {
629
+ if (!node.children) return node.value ? escapeHtml2(node.value) : "";
630
+ return node.children.map((child) => nodeToHtml(child, ctx)).join("");
631
+ }
632
+ function tableToHtml(node, ctx) {
633
+ const [headerRow, ...bodyRows] = node.children;
634
+ const parts = ["<table>"];
635
+ if (headerRow) {
636
+ parts.push("<thead><tr>");
637
+ for (const cell of headerRow.children) {
638
+ parts.push(`<th>${childrenToHtml(cell, ctx)}</th>`);
639
+ }
640
+ parts.push("</tr></thead>");
641
+ }
642
+ if (bodyRows.length > 0) {
643
+ parts.push("<tbody>");
644
+ for (const row of bodyRows) {
645
+ parts.push("<tr>");
646
+ for (const cell of row.children) {
647
+ parts.push(`<td>${childrenToHtml(cell, ctx)}</td>`);
648
+ }
649
+ parts.push("</tr>");
650
+ }
651
+ parts.push("</tbody>");
652
+ }
653
+ parts.push("</table>");
654
+ return parts.join("");
655
+ }
656
+ function htmlChildrenToHtml(nodes, ctx) {
657
+ if (!nodes || nodes.length === 0) return "";
658
+ const out = [];
659
+ for (const node of nodes) {
660
+ if (node.type === "htmlText") {
661
+ out.push(node.value);
662
+ continue;
663
+ }
664
+ if (node.type === "htmlComment") {
665
+ out.push(`<!--${node.value}-->`);
666
+ continue;
667
+ }
668
+ const tag = node.tagName.toLowerCase();
669
+ const attrs = { ...node.attributes };
670
+ if ((tag === "img" || tag === "video" || tag === "audio" || tag === "source") && typeof attrs.src === "string") {
671
+ attrs.src = ctx?.images?.get(attrs.src) ?? attrs.src;
672
+ }
673
+ if ((tag === "video" || tag === "audio") && typeof attrs.poster === "string") {
674
+ attrs.poster = ctx?.images?.get(attrs.poster) ?? attrs.poster;
675
+ }
676
+ const attrStr = Object.entries(attrs).map(([k, v]) => ` ${k}="${escapeAttr(v)}"`).join("");
677
+ if (node.selfClosing) {
678
+ out.push(`<${tag}${attrStr} />`);
679
+ } else {
680
+ out.push(`<${tag}${attrStr}>${htmlChildrenToHtml(node.children, ctx)}</${tag}>`);
681
+ }
682
+ }
683
+ return out.join("");
684
+ }
685
+ function escapeHtml2(s) {
686
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
687
+ }
688
+ function escapeAttr(s) {
689
+ return escapeHtml2(s).replace(/"/g, "&quot;");
690
+ }
691
+
692
+ // src/html/plainHtmlBundle.ts
693
+ import JSZip from "jszip";
694
+ import { parseMarkdown, inferDocumentTitle } from "@bendyline/squisq/markdown";
695
+ import { resolveTheme as resolveTheme2 } from "@bendyline/squisq/schemas";
696
+ async function markdownDocsToPlainHtmlBundle(options) {
697
+ const {
698
+ entryPath,
699
+ readDocument,
700
+ readBinary,
701
+ title,
702
+ theme,
703
+ themeId,
704
+ maxDepth = Infinity,
705
+ entryAsIndex = false
706
+ } = options;
707
+ const resolvedTheme = theme ?? (themeId ? resolveTheme2(themeId) : void 0);
708
+ const entry = normalizePath(entryPath);
709
+ if (!entry) {
710
+ throw new Error("markdownDocsToPlainHtmlBundle: entryPath is required");
711
+ }
712
+ const scopeRoot = posixDirname(entry);
713
+ const entryHtmlPath = entryAsIndex ? scopeRoot ? `${scopeRoot}/index.html` : "index.html" : entry.slice(0, -3) + ".html";
714
+ const htmlPathFor = (mdPath) => mdPath === entry ? entryHtmlPath : mdPath.slice(0, -3) + ".html";
715
+ const zip = new JSZip();
716
+ const visited = /* @__PURE__ */ new Set();
717
+ const queue = [{ path: entry, depth: 0 }];
718
+ while (queue.length > 0) {
719
+ const { path, depth } = queue.shift();
720
+ if (visited.has(path)) continue;
721
+ visited.add(path);
722
+ const source = await readDocument(path);
723
+ if (source === null) {
724
+ throw new Error(`markdownDocsToPlainHtmlBundle: failed to read "${path}"`);
725
+ }
726
+ const mdDoc = parseMarkdown(source);
727
+ const docDir = posixDirname(path);
728
+ const linkMap = /* @__PURE__ */ new Map();
729
+ for (const raw of collectLinkRefs(mdDoc)) {
730
+ const parsed = parseLinkRef(raw);
731
+ if (!parsed) continue;
732
+ const resolved = resolveRelative(docDir, parsed.path);
733
+ if (resolved === null) continue;
734
+ if (!isInScope(resolved, scopeRoot)) continue;
735
+ if (!resolved.toLowerCase().endsWith(".md")) continue;
736
+ const htmlTarget = htmlPathFor(resolved);
737
+ const relHref = relativeFrom(docDir, htmlTarget) + parsed.fragment;
738
+ linkMap.set(raw, relHref);
739
+ if (depth + 1 <= maxDepth && !visited.has(resolved)) {
740
+ queue.push({ path: resolved, depth: depth + 1 });
741
+ }
742
+ }
743
+ const images = await readImagesForDoc(mdDoc, docDir, readBinary);
744
+ for (const [, { data, zipPath }] of images) {
745
+ const safe = sanitizeZipPath(zipPath);
746
+ if (!safe) continue;
747
+ zip.file(safe, data);
748
+ }
749
+ const imageRewriteMap = /* @__PURE__ */ new Map();
750
+ for (const [authored, { zipPath }] of images) {
751
+ imageRewriteMap.set(authored, relativeFrom(docDir, zipPath));
752
+ }
753
+ const docTitle = depth === 0 ? title : void 0;
754
+ const html = markdownDocToPlainHtml(mdDoc, {
755
+ title: docTitle ?? titleForFilename(path, mdDoc),
756
+ images: imageRewriteMap,
757
+ links: linkMap,
758
+ theme: resolvedTheme
759
+ });
760
+ const htmlPath = htmlPathFor(path);
761
+ zip.file(htmlPath, html);
762
+ }
763
+ return zip.generateAsync({
764
+ type: "blob",
765
+ compression: "DEFLATE",
766
+ compressionOptions: { level: 6 }
767
+ });
768
+ }
769
+ function collectLinkRefs(doc) {
770
+ const refs = /* @__PURE__ */ new Set();
771
+ function visitHtml(nodes) {
772
+ for (const n of nodes) {
773
+ if (n.type !== "htmlElement") continue;
774
+ if (n.tagName.toLowerCase() === "a") {
775
+ const href = n.attributes.href;
776
+ if (typeof href === "string" && href) refs.add(href);
777
+ }
778
+ visitHtml(n.children);
779
+ }
780
+ }
781
+ function visit(node) {
782
+ if (!node || typeof node !== "object") return;
783
+ const n = node;
784
+ if (n.type === "link" && typeof n.url === "string" && n.url) {
785
+ refs.add(n.url);
786
+ }
787
+ if ((n.type === "htmlBlock" || n.type === "htmlInline") && Array.isArray(n.htmlChildren)) {
788
+ visitHtml(n.htmlChildren);
789
+ }
790
+ if (Array.isArray(n.children)) {
791
+ for (const child of n.children) visit(child);
792
+ }
793
+ }
794
+ for (const child of doc.children) visit(child);
795
+ return refs;
796
+ }
797
+ function collectImageRefs(doc) {
798
+ const refs = /* @__PURE__ */ new Set();
799
+ function visitHtml(nodes) {
800
+ for (const n of nodes) {
801
+ if (n.type !== "htmlElement") continue;
802
+ const tag = n.tagName.toLowerCase();
803
+ if (tag === "img" || tag === "video" || tag === "audio" || tag === "source") {
804
+ const src = n.attributes.src;
805
+ if (typeof src === "string" && src) refs.add(src);
806
+ }
807
+ if (tag === "video" || tag === "audio") {
808
+ const poster = n.attributes.poster;
809
+ if (typeof poster === "string" && poster) refs.add(poster);
810
+ }
811
+ visitHtml(n.children);
812
+ }
813
+ }
814
+ function visit(node) {
815
+ if (!node || typeof node !== "object") return;
816
+ const n = node;
817
+ if (n.type === "image" && typeof n.url === "string" && n.url) refs.add(n.url);
818
+ if ((n.type === "htmlBlock" || n.type === "htmlInline") && Array.isArray(n.htmlChildren)) {
819
+ visitHtml(n.htmlChildren);
820
+ }
821
+ if (Array.isArray(n.children)) for (const c of n.children) visit(c);
822
+ }
823
+ for (const child of doc.children) visit(child);
824
+ return refs;
825
+ }
826
+ function parseLinkRef(url) {
827
+ if (!url) return null;
828
+ if (url.startsWith("#")) return null;
829
+ if (/^[a-z][a-z0-9+.-]*:/.test(url) || // http:, https:, mailto:, ftp:, …
830
+ url.startsWith("//") || url.startsWith("/")) {
831
+ return null;
832
+ }
833
+ const hashIdx = url.indexOf("#");
834
+ const queryIdx = url.indexOf("?");
835
+ const cut = hashIdx >= 0 && queryIdx >= 0 ? Math.min(hashIdx, queryIdx) : hashIdx >= 0 ? hashIdx : queryIdx;
836
+ const path = cut >= 0 ? url.slice(0, cut) : url;
837
+ const fragment = hashIdx >= 0 ? url.slice(hashIdx) : "";
838
+ return { path, fragment };
839
+ }
840
+ async function readImagesForDoc(mdDoc, docDir, readBinary) {
841
+ const out = /* @__PURE__ */ new Map();
842
+ for (const authored of collectImageRefs(mdDoc)) {
843
+ if (/^[a-z][a-z0-9+.-]*:/.test(authored) || authored.startsWith("//")) continue;
844
+ const cleanAuthored = authored.replace(/[#?].*$/, "");
845
+ const resolved = resolveRelative(docDir, cleanAuthored);
846
+ if (resolved === null) continue;
847
+ const data = await readBinary(resolved);
848
+ if (!data) continue;
849
+ out.set(authored, { data, zipPath: resolved });
850
+ }
851
+ return out;
852
+ }
853
+ function posixDirname(p) {
854
+ const idx = p.lastIndexOf("/");
855
+ return idx < 0 ? "" : p.slice(0, idx);
856
+ }
857
+ function normalizePath(p) {
858
+ const parts = p.split("/");
859
+ const out = [];
860
+ for (const segment of parts) {
861
+ if (segment === "" || segment === ".") continue;
862
+ if (segment === "..") {
863
+ if (out.length === 0) return null;
864
+ out.pop();
865
+ continue;
866
+ }
867
+ out.push(segment);
868
+ }
869
+ return out.join("/");
870
+ }
871
+ function resolveRelative(baseDir, rel) {
872
+ if (rel.startsWith("/")) return null;
873
+ const joined = baseDir ? `${baseDir}/${rel}` : rel;
874
+ return normalizePath(joined);
875
+ }
876
+ function isInScope(target, root) {
877
+ if (!root) return true;
878
+ return target === root || target.startsWith(root + "/");
879
+ }
880
+ function relativeFrom(fromDir, toPath) {
881
+ const fromParts = fromDir ? fromDir.split("/") : [];
882
+ const toParts = toPath.split("/");
883
+ let common = 0;
884
+ while (common < fromParts.length && common < toParts.length - 1 && fromParts[common] === toParts[common]) {
885
+ common++;
886
+ }
887
+ const up = fromParts.length - common;
888
+ const down = toParts.slice(common);
889
+ const prefix = Array(up).fill("..").join("/");
890
+ if (!prefix) return down.join("/");
891
+ return `${prefix}/${down.join("/")}`;
892
+ }
893
+ function sanitizeZipPath(path) {
894
+ const normalized = path.replace(/\\/g, "/").replace(/^\/+/, "");
895
+ if (!normalized) return null;
896
+ if (normalized.split("/").some((seg) => seg === "..")) return null;
897
+ return normalized;
898
+ }
899
+ function titleForFilename(path, mdDoc) {
900
+ const inferred = inferDocumentTitle(mdDoc);
901
+ if (inferred) return inferred;
902
+ const base = path.split("/").pop() ?? path;
903
+ return base.replace(/\.md$/i, "");
904
+ }
905
+
906
+ // src/html/docsHtmlBundle.ts
907
+ import JSZip2 from "jszip";
908
+ import { parseMarkdown as parseMarkdown2, inferDocumentTitle as inferDocumentTitle2 } from "@bendyline/squisq/markdown";
909
+ import { markdownToDoc } from "@bendyline/squisq/doc";
910
+ async function markdownDocsToHtmlBundle(options) {
911
+ const {
912
+ entryPath,
913
+ readDocument,
914
+ readBinary,
915
+ playerScript,
916
+ title,
917
+ themeId,
918
+ mode = "static",
919
+ maxDepth = Infinity,
920
+ entryAsIndex = false
921
+ } = options;
922
+ const entry = normalizePath(entryPath);
923
+ if (!entry) {
924
+ throw new Error("markdownDocsToHtmlBundle: entryPath is required");
925
+ }
926
+ const scopeRoot = posixDirname(entry);
927
+ const entryHtmlPath = entryAsIndex ? scopeRoot ? `${scopeRoot}/index.html` : "index.html" : entry.slice(0, -3) + ".html";
928
+ const htmlPathFor = (mdPath) => mdPath === entry ? entryHtmlPath : mdPath.slice(0, -3) + ".html";
929
+ const zip = new JSZip2();
930
+ zip.file("squisq-player.js", playerScript);
931
+ const visited = /* @__PURE__ */ new Set();
932
+ const queue = [{ path: entry, depth: 0 }];
933
+ while (queue.length > 0) {
934
+ const { path, depth } = queue.shift();
935
+ if (visited.has(path)) continue;
936
+ visited.add(path);
937
+ const source = await readDocument(path);
938
+ if (source === null) {
939
+ throw new Error(`markdownDocsToHtmlBundle: failed to read "${path}"`);
940
+ }
941
+ const mdDoc = parseMarkdown2(source);
942
+ const docDir = posixDirname(path);
943
+ const linkMap = /* @__PURE__ */ new Map();
944
+ for (const raw of collectLinkRefs(mdDoc)) {
945
+ const parsed = parseInternalRef(raw);
946
+ if (!parsed) continue;
947
+ const resolved = resolveRelative(docDir, parsed.path);
948
+ if (resolved === null) continue;
949
+ if (!isInScope(resolved, scopeRoot)) continue;
950
+ if (!resolved.toLowerCase().endsWith(".md")) continue;
951
+ const htmlTarget = htmlPathFor(resolved);
952
+ const relHref = relativeFrom(docDir, htmlTarget) + parsed.fragment;
953
+ linkMap.set(raw, relHref);
954
+ if (depth + 1 <= maxDepth && !visited.has(resolved)) {
955
+ queue.push({ path: resolved, depth: depth + 1 });
956
+ }
957
+ }
958
+ const imageRewriteMap = /* @__PURE__ */ new Map();
959
+ for (const authored of collectImageRefs2(mdDoc)) {
960
+ if (/^[a-z][a-z0-9+.-]*:/i.test(authored) || authored.startsWith("//")) continue;
961
+ const clean = authored.replace(/[#?].*$/, "");
962
+ const resolvedImg = resolveRelative(docDir, clean);
963
+ if (resolvedImg === null) continue;
964
+ const data = await readBinary(resolvedImg);
965
+ if (!data) continue;
966
+ const safe = sanitizeZipPath2(resolvedImg);
967
+ if (!safe) continue;
968
+ zip.file(safe, data);
969
+ imageRewriteMap.set(authored, relativeFrom(docDir, resolvedImg));
970
+ }
971
+ let doc = markdownToDoc(mdDoc);
972
+ if (themeId) doc = { ...doc, themeId };
973
+ if (linkMap.size > 0) {
974
+ doc = rewriteDocLinks(doc, linkMap);
975
+ }
976
+ const pageTitle = depth === 0 ? title ?? titleForDoc(path, mdDoc) : titleForDoc(path, mdDoc);
977
+ const htmlPath = htmlPathFor(path);
978
+ const playerScriptPath = relativeFrom(docDir, "squisq-player.js");
979
+ const html = generateExternalHtml(doc, {
980
+ playerScriptPath,
981
+ // Convert Map → plain record for the template (which expects a
982
+ // record). The renderer reads this map at runtime to swap image
983
+ // sources before rendering each block.
984
+ imagePathMap: imageRewriteMap.size > 0 ? Object.fromEntries(imageRewriteMap.entries()) : void 0,
985
+ mode,
986
+ title: pageTitle
987
+ });
988
+ zip.file(htmlPath, html);
989
+ }
990
+ return zip.generateAsync({
991
+ type: "blob",
992
+ compression: "DEFLATE",
993
+ compressionOptions: { level: 6 }
994
+ });
995
+ }
996
+ function rewriteDocLinks(doc, linkMap) {
997
+ if (linkMap.size === 0) return doc;
998
+ function rewriteHtmlNodes(nodes) {
999
+ return nodes.map((n) => {
1000
+ if (n.type !== "htmlElement") return n;
1001
+ const tag = n.tagName.toLowerCase();
1002
+ const nextAttrs = tag === "a" && typeof n.attributes.href === "string" && linkMap.has(n.attributes.href) ? { ...n.attributes, href: linkMap.get(n.attributes.href) } : n.attributes;
1003
+ return {
1004
+ ...n,
1005
+ attributes: nextAttrs,
1006
+ children: rewriteHtmlNodes(n.children)
1007
+ };
1008
+ });
1009
+ }
1010
+ function rewriteInline(node) {
1011
+ if (node.type === "link") {
1012
+ const nextUrl = linkMap.get(node.url) ?? node.url;
1013
+ return {
1014
+ ...node,
1015
+ url: nextUrl,
1016
+ children: node.children.map(rewriteInline)
1017
+ };
1018
+ }
1019
+ if (node.type === "htmlInline") {
1020
+ const withChildren = node;
1021
+ if (Array.isArray(withChildren.htmlChildren)) {
1022
+ return {
1023
+ ...node,
1024
+ htmlChildren: rewriteHtmlNodes(withChildren.htmlChildren)
1025
+ };
1026
+ }
1027
+ return node;
1028
+ }
1029
+ if ("children" in node && Array.isArray(node.children)) {
1030
+ const children = node.children.map(rewriteInline);
1031
+ return { ...node, children };
1032
+ }
1033
+ return node;
1034
+ }
1035
+ function rewriteBlockNode(node) {
1036
+ if (node.type === "htmlBlock") {
1037
+ const withChildren = node;
1038
+ if (Array.isArray(withChildren.htmlChildren)) {
1039
+ return {
1040
+ ...node,
1041
+ htmlChildren: rewriteHtmlNodes(withChildren.htmlChildren)
1042
+ };
1043
+ }
1044
+ return node;
1045
+ }
1046
+ if ("children" in node && Array.isArray(node.children)) {
1047
+ const rewritten = node.children.map((c) => {
1048
+ const child = c;
1049
+ if (child && typeof child === "object" && "type" in child) {
1050
+ if (isBlockNodeType(child.type)) {
1051
+ return rewriteBlockNode(c);
1052
+ }
1053
+ return rewriteInline(c);
1054
+ }
1055
+ return c;
1056
+ });
1057
+ return { ...node, children: rewritten };
1058
+ }
1059
+ return node;
1060
+ }
1061
+ function rewriteBlock(block) {
1062
+ const next = { ...block };
1063
+ if (Array.isArray(block.contents)) {
1064
+ next.contents = block.contents.map(rewriteBlockNode);
1065
+ }
1066
+ if (Array.isArray(block.children)) {
1067
+ next.children = block.children.map(rewriteBlock);
1068
+ }
1069
+ return next;
1070
+ }
1071
+ return {
1072
+ ...doc,
1073
+ blocks: doc.blocks.map(rewriteBlock)
1074
+ };
1075
+ }
1076
+ function isBlockNodeType(t) {
1077
+ return t === "paragraph" || t === "heading" || t === "list" || t === "listItem" || t === "blockquote" || t === "code" || t === "thematicBreak" || t === "table" || t === "tableRow" || t === "tableCell" || t === "htmlBlock";
1078
+ }
1079
+ function parseInternalRef(url) {
1080
+ if (!url) return null;
1081
+ if (url.startsWith("#")) return null;
1082
+ if (/^[a-z][a-z0-9+.-]*:/i.test(url) || url.startsWith("//") || url.startsWith("/")) {
1083
+ return null;
1084
+ }
1085
+ const hashIdx = url.indexOf("#");
1086
+ const queryIdx = url.indexOf("?");
1087
+ const cut = hashIdx >= 0 && queryIdx >= 0 ? Math.min(hashIdx, queryIdx) : hashIdx >= 0 ? hashIdx : queryIdx;
1088
+ const path = cut >= 0 ? url.slice(0, cut) : url;
1089
+ const fragment = hashIdx >= 0 ? url.slice(hashIdx) : "";
1090
+ return { path, fragment };
1091
+ }
1092
+ function collectImageRefs2(doc) {
1093
+ const refs = /* @__PURE__ */ new Set();
1094
+ function visitHtml(nodes) {
1095
+ for (const n of nodes) {
1096
+ if (n.type !== "htmlElement") continue;
1097
+ if (n.tagName.toLowerCase() === "img") {
1098
+ const src = n.attributes.src;
1099
+ if (typeof src === "string" && src) refs.add(src);
1100
+ }
1101
+ visitHtml(n.children);
1102
+ }
1103
+ }
1104
+ function visit(node) {
1105
+ if (!node || typeof node !== "object") return;
1106
+ const n = node;
1107
+ if (n.type === "image" && typeof n.url === "string" && n.url) refs.add(n.url);
1108
+ if ((n.type === "htmlBlock" || n.type === "htmlInline") && Array.isArray(n.htmlChildren)) {
1109
+ visitHtml(n.htmlChildren);
1110
+ }
1111
+ if (Array.isArray(n.children)) for (const c of n.children) visit(c);
1112
+ }
1113
+ for (const child of doc.children) visit(child);
1114
+ return refs;
1115
+ }
1116
+ function sanitizeZipPath2(path) {
1117
+ const normalized = path.replace(/\\/g, "/").replace(/^\/+/, "");
1118
+ if (!normalized) return null;
1119
+ if (normalized.split("/").some((seg) => seg === "..")) return null;
1120
+ return normalized;
1121
+ }
1122
+ function titleForDoc(path, mdDoc) {
1123
+ const inferred = inferDocumentTitle2(mdDoc);
1124
+ if (inferred) return inferred;
1125
+ const base = path.split("/").pop() ?? path;
1126
+ return base.replace(/\.md$/i, "");
1127
+ }
1128
+
1129
+ // src/html/index.ts
1130
+ function sanitizeZipPath3(path) {
1131
+ const normalized = path.replace(/\\/g, "/").replace(/^\/+/, "");
1132
+ if (!normalized) return null;
1133
+ if (normalized.split("/").some((seg) => seg === "..")) return null;
1134
+ return normalized;
1135
+ }
1136
+ function docToHtml(doc, options) {
1137
+ return generateInlineHtml(doc, options);
1138
+ }
1139
+ async function docToHtmlZip(doc, options) {
1140
+ const { playerScript, images, audio, mode = "slideshow", title, autoPlay } = options;
1141
+ const zip = new JSZip3();
1142
+ zip.file("squisq-player.js", playerScript);
1143
+ const imagePathMap = {};
1144
+ if (images) {
1145
+ for (const [originalPath, buffer] of images.entries()) {
1146
+ const zipPath = sanitizeZipPath3(originalPath);
1147
+ if (!zipPath) continue;
1148
+ zip.file(zipPath, buffer);
1149
+ imagePathMap[originalPath] = zipPath;
1150
+ }
1151
+ }
1152
+ const audioPathMap = {};
1153
+ if (audio) {
1154
+ for (const [segmentKey, buffer] of audio.entries()) {
1155
+ const filename = extractFilename(segmentKey);
1156
+ const finalName = filename.includes(".") ? filename : `${filename}.mp3`;
1157
+ const zipPath = `audio/${finalName}`;
1158
+ zip.file(zipPath, buffer);
1159
+ audioPathMap[segmentKey] = zipPath;
1160
+ }
1161
+ }
1162
+ const html = generateExternalHtml(doc, {
1163
+ playerScriptPath: "squisq-player.js",
1164
+ imagePathMap: Object.keys(imagePathMap).length > 0 ? imagePathMap : void 0,
1165
+ audioPathMap: Object.keys(audioPathMap).length > 0 ? audioPathMap : void 0,
1166
+ mode,
1167
+ title,
1168
+ autoPlay
1169
+ });
1170
+ zip.file("index.html", html);
1171
+ return zip.generateAsync({
1172
+ type: "blob",
1173
+ compression: "DEFLATE",
1174
+ compressionOptions: { level: 6 }
1175
+ });
1176
+ }
1177
+
1178
+ export {
1179
+ collectImagePaths,
1180
+ markdownDocToPlainHtml,
1181
+ markdownDocsToPlainHtmlBundle,
1182
+ collectLinkRefs,
1183
+ markdownDocsToHtmlBundle,
1184
+ docToHtml,
1185
+ docToHtmlZip
1186
+ };
1187
+ //# sourceMappingURL=chunk-33YRFXZZ.js.map