@bendyline/squisq-formats 1.4.1 → 2.0.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 (139) hide show
  1. package/README.md +48 -2
  2. package/dist/{chunk-WWBIKRNP.js → chunk-2CSZJEAL.js} +13 -11
  3. package/dist/chunk-2CSZJEAL.js.map +1 -0
  4. package/dist/{chunk-RTS5XBZ7.js → chunk-2EFDWKC6.js} +262 -84
  5. package/dist/chunk-2EFDWKC6.js.map +1 -0
  6. package/dist/chunk-2PRVLAU3.js +740 -0
  7. package/dist/chunk-2PRVLAU3.js.map +1 -0
  8. package/dist/{chunk-SHLPSADL.js → chunk-4HJ6AK2Q.js} +54 -193
  9. package/dist/chunk-4HJ6AK2Q.js.map +1 -0
  10. package/dist/{chunk-5LSSWZPU.js → chunk-4VUWTSGM.js} +72 -25
  11. package/dist/chunk-4VUWTSGM.js.map +1 -0
  12. package/dist/{chunk-YESS7VY3.js → chunk-CVNVD7VH.js} +154 -39
  13. package/dist/chunk-CVNVD7VH.js.map +1 -0
  14. package/dist/chunk-CX3LH47J.js +99 -0
  15. package/dist/chunk-CX3LH47J.js.map +1 -0
  16. package/dist/{chunk-D7IFPWNZ.js → chunk-GVS2XXV6.js} +27 -26
  17. package/dist/chunk-GVS2XXV6.js.map +1 -0
  18. package/dist/chunk-H7T5DXUU.js +479 -0
  19. package/dist/chunk-H7T5DXUU.js.map +1 -0
  20. package/dist/{chunk-EHLGMSTE.js → chunk-HBSCRS5P.js} +10 -8
  21. package/dist/chunk-HBSCRS5P.js.map +1 -0
  22. package/dist/{chunk-46FMDOWZ.js → chunk-LXYLOOST.js} +49 -17
  23. package/dist/chunk-LXYLOOST.js.map +1 -0
  24. package/dist/{chunk-MJGRI6XR.js → chunk-MLX2BOJC.js} +12 -7
  25. package/dist/chunk-MLX2BOJC.js.map +1 -0
  26. package/dist/chunk-NKAJPJ4G.js +106 -0
  27. package/dist/chunk-NKAJPJ4G.js.map +1 -0
  28. package/dist/{chunk-DQAZR57U.js → chunk-O3GVVND4.js} +1 -1
  29. package/dist/chunk-ODL3SSPT.js +18 -0
  30. package/dist/chunk-ODL3SSPT.js.map +1 -0
  31. package/dist/chunk-X4USWOQ2.js +131 -0
  32. package/dist/chunk-X4USWOQ2.js.map +1 -0
  33. package/dist/chunk-XTLQXR4U.js +195 -0
  34. package/dist/chunk-XTLQXR4U.js.map +1 -0
  35. package/dist/chunk-YRT7GQ5Y.js +28 -0
  36. package/dist/chunk-YRT7GQ5Y.js.map +1 -0
  37. package/dist/chunk-ZZ7D4DN6.js +97 -0
  38. package/dist/chunk-ZZ7D4DN6.js.map +1 -0
  39. package/dist/container/index.d.ts +6 -2
  40. package/dist/container/index.js +15 -17
  41. package/dist/container/index.js.map +1 -1
  42. package/dist/docx/index.d.ts +6 -2
  43. package/dist/docx/index.js +5 -3
  44. package/dist/epub/index.d.ts +3 -1
  45. package/dist/epub/index.js +3 -3
  46. package/dist/export-Djev9BLU.d.ts +69 -0
  47. package/dist/extract-LNOA7DBK.js +15 -0
  48. package/dist/extract-LNOA7DBK.js.map +1 -0
  49. package/dist/html/index.d.ts +15 -85
  50. package/dist/html/index.js +3 -3
  51. package/dist/images-7FBWPKE3.js +7 -0
  52. package/dist/images-7FBWPKE3.js.map +1 -0
  53. package/dist/import-DTkDxHmZ.d.ts +84 -0
  54. package/dist/import-Yivc3kZM.d.ts +136 -0
  55. package/dist/index.d.ts +13 -4
  56. package/dist/index.js +30 -11
  57. package/dist/infer/index.d.ts +138 -0
  58. package/dist/infer/index.js +28 -0
  59. package/dist/infer/index.js.map +1 -0
  60. package/dist/layouts-7OUUVAGU.js +12 -0
  61. package/dist/layouts-7OUUVAGU.js.map +1 -0
  62. package/dist/layouts-CZ0bdsNL.d.ts +150 -0
  63. package/dist/mapTheme-IR27S6IV.js +11 -0
  64. package/dist/mapTheme-IR27S6IV.js.map +1 -0
  65. package/dist/ooxml/index.d.ts +21 -127
  66. package/dist/ooxml/index.js +31 -9
  67. package/dist/pdf/index.d.ts +3 -1
  68. package/dist/pdf/index.js +1 -1
  69. package/dist/pptx/index.d.ts +8 -108
  70. package/dist/pptx/index.js +13 -4
  71. package/dist/reader-Gof6OrJX.d.ts +140 -0
  72. package/dist/registry/index.d.ts +46 -8
  73. package/dist/registry/index.js +5 -2
  74. package/dist/themeReader-DPw4J5Ss.d.ts +56 -0
  75. package/dist/xlsx/index.d.ts +6 -66
  76. package/dist/xlsx/index.js +4 -2
  77. package/dist/zipLimits-bGYMS9cI.d.ts +29 -0
  78. package/package.json +8 -2
  79. package/src/__tests__/container.test.ts +41 -0
  80. package/src/__tests__/convert.test.ts +167 -0
  81. package/src/__tests__/epub.test.ts +102 -1
  82. package/src/__tests__/html.test.ts +62 -0
  83. package/src/__tests__/inferTheme.test.ts +135 -0
  84. package/src/__tests__/lossyWarnings.test.ts +41 -0
  85. package/src/__tests__/ooxml.test.ts +59 -2
  86. package/src/__tests__/ooxmlThemeReader.test.ts +92 -0
  87. package/src/__tests__/plainHtml.test.ts +15 -1
  88. package/src/__tests__/plainHtmlBundle.test.ts +18 -0
  89. package/src/__tests__/pptxImport.test.ts +30 -0
  90. package/src/__tests__/pptxInferFixtures.ts +314 -0
  91. package/src/__tests__/pptxLayoutInfer.test.ts +395 -0
  92. package/src/__tests__/zipSafety.test.ts +317 -0
  93. package/src/container/index.ts +20 -29
  94. package/src/docx/export.ts +4 -2
  95. package/src/docx/import.ts +4 -3
  96. package/src/epub/export.ts +94 -25
  97. package/src/html/docsHtmlBundle.ts +6 -2
  98. package/src/html/htmlTemplate.ts +31 -6
  99. package/src/html/imageUtils.ts +14 -5
  100. package/src/html/index.ts +12 -1
  101. package/src/html/plainHtml.ts +7 -3
  102. package/src/html/plainHtmlBundle.ts +11 -9
  103. package/src/index.ts +20 -0
  104. package/src/infer/extract.ts +127 -0
  105. package/src/infer/index.ts +168 -0
  106. package/src/infer/mapTheme.ts +176 -0
  107. package/src/infer/types.ts +27 -0
  108. package/src/ooxml/index.ts +14 -0
  109. package/src/ooxml/readUtils.ts +44 -0
  110. package/src/ooxml/reader.ts +87 -28
  111. package/src/ooxml/themeReader.ts +197 -0
  112. package/src/ooxml/types.ts +8 -9
  113. package/src/ooxml/writer.ts +28 -10
  114. package/src/pdf/export.ts +4 -2
  115. package/src/pdf/import.ts +40 -34
  116. package/src/pptx/export.ts +4 -2
  117. package/src/pptx/import.ts +214 -32
  118. package/src/pptx/index.ts +14 -0
  119. package/src/pptx/layouts.ts +1168 -0
  120. package/src/registry/convert.ts +54 -23
  121. package/src/registry/defaultFormats.ts +131 -44
  122. package/src/registry/index.ts +3 -0
  123. package/src/registry/types.ts +52 -6
  124. package/src/shared/boundedZipArchive.ts +342 -0
  125. package/src/shared/fidelity.ts +130 -0
  126. package/src/shared/zipEntryCount.ts +97 -0
  127. package/src/shared/zipLimits.ts +286 -0
  128. package/src/shared/zipSafety.ts +19 -0
  129. package/src/xlsx/import.ts +3 -2
  130. package/dist/chunk-46FMDOWZ.js.map +0 -1
  131. package/dist/chunk-5LSSWZPU.js.map +0 -1
  132. package/dist/chunk-D7IFPWNZ.js.map +0 -1
  133. package/dist/chunk-EHLGMSTE.js.map +0 -1
  134. package/dist/chunk-MJGRI6XR.js.map +0 -1
  135. package/dist/chunk-RTS5XBZ7.js.map +0 -1
  136. package/dist/chunk-SHLPSADL.js.map +0 -1
  137. package/dist/chunk-WWBIKRNP.js.map +0 -1
  138. package/dist/chunk-YESS7VY3.js.map +0 -1
  139. /package/dist/{chunk-DQAZR57U.js.map → chunk-O3GVVND4.js.map} +0 -0
@@ -7,13 +7,13 @@ import {
7
7
  import {
8
8
  extractFilename,
9
9
  inferMimeType
10
- } from "./chunk-MJGRI6XR.js";
10
+ } from "./chunk-MLX2BOJC.js";
11
11
 
12
12
  // src/epub/export.ts
13
13
  import JSZip from "jszip";
14
14
  import { resolveFontFamily } from "@bendyline/squisq/schemas";
15
15
  import { resolveThemeForDoc } from "@bendyline/squisq/doc";
16
- import { readFrontmatterThemeId } from "@bendyline/squisq/markdown";
16
+ import { readFrontmatterThemeId, sanitizeUrl } from "@bendyline/squisq/markdown";
17
17
  async function markdownDocToEpub(doc, options = {}) {
18
18
  const fmTitle = doc.frontmatter?.title;
19
19
  const fmAuthor = doc.frontmatter?.author;
@@ -22,30 +22,26 @@ async function markdownDocToEpub(doc, options = {}) {
22
22
  const language = options.language ?? "en";
23
23
  const description = options.description ?? "";
24
24
  const publisher = options.publisher ?? "";
25
- const uuid = crypto.randomUUID();
25
+ const uuid = createEpubUuid();
26
26
  const chapters = splitIntoChapters(doc.children);
27
27
  const imageEntries = collectDocImages(doc.children);
28
28
  const resolvedImages = /* @__PURE__ */ new Map();
29
+ const usedImageNames = /* @__PURE__ */ new Set();
29
30
  if (options.images) {
30
- const usedNames = /* @__PURE__ */ new Set();
31
31
  for (const src of imageEntries) {
32
32
  const data = options.images.get(src);
33
33
  if (data) {
34
- let filename = extractFilename(src);
35
- if (usedNames.has(filename)) {
36
- const dot = filename.lastIndexOf(".");
37
- const base = dot > 0 ? filename.slice(0, dot) : filename;
38
- const ext = dot > 0 ? filename.slice(dot) : "";
39
- let counter = 2;
40
- while (usedNames.has(`${base}-${counter}${ext}`)) counter++;
41
- filename = `${base}-${counter}${ext}`;
42
- }
43
- usedNames.add(filename);
34
+ const filename = uniqueFilename(safeArchiveBasename(src, "image"), usedImageNames);
35
+ usedImageNames.add(filename);
44
36
  resolvedImages.set(src, { data, mime: inferMimeType(filename), filename });
45
37
  }
46
38
  }
47
39
  }
48
- const css = generateStylesheet(options.themeId ?? readFrontmatterThemeId(doc.frontmatter), doc);
40
+ const css = generateStylesheet(
41
+ options.themeId ?? readFrontmatterThemeId(doc.frontmatter),
42
+ doc,
43
+ options.themeRegistry
44
+ );
49
45
  const zip = new JSZip();
50
46
  zip.file("mimetype", "application/epub+zip", { compression: "STORE" });
51
47
  zip.file("META-INF/container.xml", CONTAINER_XML);
@@ -57,7 +53,8 @@ async function markdownDocToEpub(doc, options = {}) {
57
53
  if (options.coverImage) {
58
54
  const bytes = new Uint8Array(options.coverImage);
59
55
  const isPng = bytes.length >= 4 && bytes[0] === 137 && bytes[1] === 80 && bytes[2] === 78 && bytes[3] === 71;
60
- coverFilename = isPng ? "cover.png" : "cover.jpg";
56
+ coverFilename = uniqueFilename(isPng ? "cover.png" : "cover.jpg", usedImageNames);
57
+ usedImageNames.add(coverFilename);
61
58
  zip.file(`OEBPS/images/${coverFilename}`, options.coverImage);
62
59
  const coverXhtml = `<?xml version="1.0" encoding="UTF-8"?>
63
60
  <!DOCTYPE html>
@@ -81,16 +78,27 @@ async function markdownDocToEpub(doc, options = {}) {
81
78
  const hasAudio = audioMap && audioSegments && audioSegments.length > 0;
82
79
  const segmentAudioFiles = [];
83
80
  const allAudioFiles = [];
81
+ const audioFilenameBySource = /* @__PURE__ */ new Map();
82
+ const usedAudioFilenames = /* @__PURE__ */ new Set();
84
83
  if (hasAudio) {
85
84
  for (const seg of audioSegments) {
86
85
  const data = audioMap.get(seg.src) ?? audioMap.get(seg.name);
87
86
  if (data) {
88
- const filename = extractFilename(seg.src);
89
- const finalName = filename.includes(".") ? filename : `${filename}.mp3`;
90
- zip.file(`OEBPS/audio/${finalName}`, data);
87
+ const sourceKey = seg.src || seg.name;
88
+ let finalName = audioFilenameBySource.get(sourceKey);
89
+ let isNewFile = false;
90
+ if (!finalName) {
91
+ const basename = safeArchiveBasename(sourceKey || "narration", "narration");
92
+ const withExtension = basename.includes(".") ? basename : `${basename}.mp3`;
93
+ finalName = uniqueFilename(withExtension, usedAudioFilenames);
94
+ audioFilenameBySource.set(sourceKey, finalName);
95
+ usedAudioFilenames.add(finalName);
96
+ zip.file(`OEBPS/audio/${finalName}`, data);
97
+ isNewFile = true;
98
+ }
91
99
  const info = { filename: finalName, mime: inferMimeType(finalName) };
92
100
  segmentAudioFiles.push(info);
93
- allAudioFiles.push(info);
101
+ if (isNewFile) allAudioFiles.push(info);
94
102
  } else {
95
103
  segmentAudioFiles.push(null);
96
104
  }
@@ -329,13 +337,17 @@ function inlineToXhtml(node, images) {
329
337
  case "inlineCode":
330
338
  return `<code>${escapeXml(node.value)}</code>`;
331
339
  case "link": {
340
+ const href = sanitizeUrl(node.url, "link");
341
+ if (!href) return inlinesToXhtml(node.children, images);
332
342
  const titleAttr = node.title ? ` title="${escapeXml(node.title)}"` : "";
333
- return `<a href="${escapeXml(node.url)}"${titleAttr}>${inlinesToXhtml(node.children, images)}</a>`;
343
+ return `<a href="${escapeXml(href)}"${titleAttr}>${inlinesToXhtml(node.children, images)}</a>`;
334
344
  }
335
345
  case "image": {
336
346
  const alt = escapeXml(node.alt ?? "");
337
347
  const resolved = images.get(node.url);
338
- const src = resolved ? `../images/${resolved.filename}` : escapeXml(node.url);
348
+ const safeSrc = resolved ? `../images/${resolved.filename}` : sanitizeUrl(node.url, "media");
349
+ if (!safeSrc) return alt;
350
+ const src = escapeXml(safeSrc);
339
351
  return `<img src="${src}" alt="${alt}"/>`;
340
352
  }
341
353
  case "break":
@@ -348,6 +360,41 @@ function inlineToXhtml(node, images) {
348
360
  return "";
349
361
  }
350
362
  }
363
+ function createEpubUuid() {
364
+ const webCrypto = globalThis.crypto;
365
+ if (typeof webCrypto?.randomUUID === "function") return webCrypto.randomUUID();
366
+ const bytes = new Uint8Array(16);
367
+ if (typeof webCrypto?.getRandomValues === "function") {
368
+ webCrypto.getRandomValues(bytes);
369
+ } else {
370
+ let seed = Date.now() ^ Math.floor(Math.random() * 2147483647);
371
+ for (let i = 0; i < bytes.length; i++) {
372
+ seed = Math.imul(seed ^ seed >>> 15, 1 | seed);
373
+ bytes[i] = seed & 255;
374
+ }
375
+ }
376
+ bytes[6] = bytes[6] & 15 | 64;
377
+ bytes[8] = bytes[8] & 63 | 128;
378
+ const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
379
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
380
+ }
381
+ function safeArchiveBasename(path, fallback) {
382
+ const filename = extractFilename(path.replace(/\\/g, "/"));
383
+ const safe = Array.from(filename, (character) => {
384
+ const codePoint = character.codePointAt(0) ?? 0;
385
+ return character === "\\" || character === "/" || codePoint < 32 || codePoint === 127 ? "_" : character;
386
+ }).join("").trim();
387
+ return !safe || safe === "." || safe === ".." ? fallback : safe;
388
+ }
389
+ function uniqueFilename(filename, used) {
390
+ if (!used.has(filename)) return filename;
391
+ const dot = filename.lastIndexOf(".");
392
+ const base = dot > 0 ? filename.slice(0, dot) : filename;
393
+ const ext = dot > 0 ? filename.slice(dot) : "";
394
+ let suffix = 2;
395
+ while (used.has(`${base}-${suffix}${ext}`)) suffix++;
396
+ return `${base}-${suffix}${ext}`;
397
+ }
351
398
  function generateSmil(chapterFilename, audioInfo, nodes) {
352
399
  let elementCount = 0;
353
400
  function countBlocks(node) {
@@ -506,10 +553,10 @@ ${navItems}
506
553
  </body>
507
554
  </html>`;
508
555
  }
509
- function generateStylesheet(themeId, doc) {
556
+ function generateStylesheet(themeId, doc, registry) {
510
557
  let themeVars = "";
511
558
  if (themeId) {
512
- const theme = resolveThemeForDoc(doc, themeId);
559
+ const theme = resolveThemeForDoc(doc, themeId, registry);
513
560
  themeVars = `
514
561
  --epub-bg: ${theme.colors.background};
515
562
  --epub-text: ${theme.colors.text};
@@ -629,4 +676,4 @@ export {
629
676
  markdownDocToEpub,
630
677
  docToEpub
631
678
  };
632
- //# sourceMappingURL=chunk-5LSSWZPU.js.map
679
+ //# sourceMappingURL=chunk-4VUWTSGM.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/epub/export.ts"],"sourcesContent":["/**\n * EPUB 3 Export\n *\n * Converts a MarkdownDocument (or Doc) to an EPUB 3 file (.epub).\n *\n * An EPUB is a ZIP archive containing XHTML chapter files, images,\n * a package manifest (content.opf), and a navigation document (toc.xhtml).\n * Content is split into chapters at H1/H2 heading boundaries.\n *\n * Uses JSZip for packaging (already a dependency), escapeXml from the\n * OOXML utils, and image utilities from the HTML exporter.\n *\n * @example\n * ```ts\n * import { markdownDocToEpub } from '@bendyline/squisq-formats/epub';\n *\n * const epub = await markdownDocToEpub(markdownDoc, {\n * title: 'My Book',\n * author: 'Jane Doe',\n * });\n * ```\n */\n\nimport JSZip from 'jszip';\nimport type { Doc, AudioSegment, ThemeRegistry } from '@bendyline/squisq/schemas';\nimport { resolveFontFamily } from '@bendyline/squisq/schemas';\nimport { resolveThemeForDoc } from '@bendyline/squisq/doc';\nimport type {\n MarkdownDocument,\n MarkdownBlockNode,\n MarkdownInlineNode,\n MarkdownHeading,\n MarkdownParagraph,\n MarkdownListItem,\n MarkdownTable,\n MarkdownTableRow,\n MarkdownTableCell,\n} from '@bendyline/squisq/markdown';\nimport { readFrontmatterThemeId, sanitizeUrl } from '@bendyline/squisq/markdown';\nimport { escapeXml } from '../ooxml/xmlUtils.js';\nimport { inferMimeType, extractFilename } from '../html/imageUtils.js';\nimport { extractPlainText } from '../shared/text.js';\n\n// ── Public API ────────────────────────────────────────────────────\n\nexport interface EpubExportOptions {\n /** Book title (default: 'Untitled') */\n title?: string;\n /** Author name */\n author?: string;\n /** Book description / summary */\n description?: string;\n /** BCP-47 language code (default: 'en') */\n language?: string;\n /** Publisher name */\n publisher?: string;\n /** Squisq theme ID for CSS styling */\n themeId?: string;\n /** Explicit caller-owned registry for non-document custom themes. */\n themeRegistry?: ThemeRegistry;\n /** Pre-resolved image data keyed by relative path as it appears in the markdown */\n images?: Map<string, ArrayBuffer>;\n /** Cover image data (JPEG or PNG) */\n coverImage?: ArrayBuffer;\n /**\n * Audio narration data keyed by segment src/name.\n * When provided alongside audioSegments, EPUB 3 Media Overlays (SMIL)\n * are generated for synchronized audio playback.\n */\n audio?: Map<string, ArrayBuffer>;\n /**\n * Audio segment metadata (from Doc.audio.segments).\n * Required together with `audio` to generate Media Overlays.\n * Each segment's duration and startTime are used to build SMIL timing.\n */\n audioSegments?: AudioSegment[];\n /** Total document duration in seconds (used for Media Overlay metadata) */\n totalDuration?: number;\n}\n\n/**\n * Convert a MarkdownDocument to an EPUB 3 file.\n *\n * Chapters are split at H1/H2 heading boundaries. All referenced images\n * (provided via `options.images`) are embedded in the archive.\n */\nexport async function markdownDocToEpub(\n doc: MarkdownDocument,\n options: EpubExportOptions = {},\n): Promise<ArrayBuffer> {\n const fmTitle = doc.frontmatter?.title;\n const fmAuthor = doc.frontmatter?.author;\n const title = options.title ?? (typeof fmTitle === 'string' ? fmTitle : 'Untitled');\n const author = options.author ?? (typeof fmAuthor === 'string' ? fmAuthor : '');\n const language = options.language ?? 'en';\n const description = options.description ?? '';\n const publisher = options.publisher ?? '';\n const uuid = createEpubUuid();\n\n // Split document into chapters\n const chapters = splitIntoChapters(doc.children);\n\n // Collect images referenced in the document, deduplicating filenames\n const imageEntries = collectDocImages(doc.children);\n const resolvedImages = new Map<string, { data: ArrayBuffer; mime: string; filename: string }>();\n const usedImageNames = new Set<string>();\n if (options.images) {\n for (const src of imageEntries) {\n const data = options.images.get(src);\n if (data) {\n const filename = uniqueFilename(safeArchiveBasename(src, 'image'), usedImageNames);\n usedImageNames.add(filename);\n resolvedImages.set(src, { data, mime: inferMimeType(filename), filename });\n }\n }\n }\n\n // Generate theme CSS — honor an explicit themeId, else the doc's frontmatter\n // theme (`squisq-theme` / legacy), mirroring the other export formats.\n const css = generateStylesheet(\n options.themeId ?? readFrontmatterThemeId(doc.frontmatter),\n doc,\n options.themeRegistry,\n );\n\n // Build the ZIP\n const zip = new JSZip();\n\n // mimetype must be first entry, stored (not compressed)\n zip.file('mimetype', 'application/epub+zip', { compression: 'STORE' });\n\n // META-INF/container.xml\n zip.file('META-INF/container.xml', CONTAINER_XML);\n\n // OEBPS/styles.css\n zip.file('OEBPS/styles.css', css);\n\n // OEBPS/images/*\n for (const [, img] of resolvedImages) {\n zip.file(`OEBPS/images/${img.filename}`, img.data);\n }\n\n // Cover image — detect PNG vs JPEG from magic bytes.\n // When provided, generates a cover.xhtml page in the spine so e-readers\n // (especially Kindle) display the cover as both thumbnail and first page.\n let coverFilename: string | undefined;\n if (options.coverImage) {\n const bytes = new Uint8Array(options.coverImage);\n const isPng =\n bytes.length >= 4 &&\n bytes[0] === 0x89 &&\n bytes[1] === 0x50 &&\n bytes[2] === 0x4e &&\n bytes[3] === 0x47;\n coverFilename = uniqueFilename(isPng ? 'cover.png' : 'cover.jpg', usedImageNames);\n usedImageNames.add(coverFilename);\n zip.file(`OEBPS/images/${coverFilename}`, options.coverImage);\n\n // Generate cover XHTML page — full-bleed image, no margins\n const coverXhtml = `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE html>\n<html xmlns=\"http://www.w3.org/1999/xhtml\" xmlns:epub=\"http://www.idpf.org/2007/ops\">\n<head>\n <meta charset=\"UTF-8\"/>\n <title>Cover</title>\n <style>\n body { margin: 0; padding: 0; text-align: center; }\n img { max-width: 100%; max-height: 100%; }\n </style>\n</head>\n<body>\n <img src=\"../images/${escapeXml(coverFilename)}\" alt=\"Cover\"/>\n</body>\n</html>`;\n zip.file('OEBPS/chapters/cover.xhtml', coverXhtml);\n }\n\n // ── Audio narration ──────────────────────────────────────────────\n const audioMap = options.audio;\n const audioSegments = options.audioSegments;\n const hasAudio = audioMap && audioSegments && audioSegments.length > 0;\n // Per-segment audio file info, indexed by segment index (null if data missing)\n const segmentAudioFiles: ({ filename: string; mime: string } | null)[] = [];\n const allAudioFiles: { filename: string; mime: string }[] = [];\n const audioFilenameBySource = new Map<string, string>();\n const usedAudioFilenames = new Set<string>();\n\n if (hasAudio) {\n for (const seg of audioSegments) {\n const data = audioMap.get(seg.src) ?? audioMap.get(seg.name);\n if (data) {\n const sourceKey = seg.src || seg.name;\n let finalName = audioFilenameBySource.get(sourceKey);\n let isNewFile = false;\n if (!finalName) {\n const basename = safeArchiveBasename(sourceKey || 'narration', 'narration');\n const withExtension = basename.includes('.') ? basename : `${basename}.mp3`;\n finalName = uniqueFilename(withExtension, usedAudioFilenames);\n audioFilenameBySource.set(sourceKey, finalName);\n usedAudioFilenames.add(finalName);\n zip.file(`OEBPS/audio/${finalName}`, data);\n isNewFile = true;\n }\n const info = { filename: finalName, mime: inferMimeType(finalName) };\n segmentAudioFiles.push(info);\n if (isNewFile) allAudioFiles.push(info);\n } else {\n segmentAudioFiles.push(null);\n }\n }\n }\n\n // Build chapter-to-audio mapping for SMIL overlays\n const chapterAudio: (ChapterAudioInfo | null)[] = [];\n if (hasAudio && allAudioFiles.length > 0) {\n if (chapters.length !== audioSegments.length) {\n console.warn(\n `EPUB: ${chapters.length} chapters but ${audioSegments.length} audio segments — ` +\n `extra chapters will reuse the last segment's audio`,\n );\n }\n for (let i = 0; i < chapters.length; i++) {\n const segIdx = Math.min(i, audioSegments.length - 1);\n const seg = audioSegments[segIdx];\n const audioFile = segmentAudioFiles[segIdx];\n if (audioFile) {\n chapterAudio.push({\n audioFilename: audioFile.filename,\n clipStart: 0,\n clipEnd: seg.duration,\n duration: seg.duration,\n });\n } else {\n chapterAudio.push(null);\n }\n }\n }\n\n // OEBPS/chapters/*.xhtml + optional SMIL overlays\n const chapterFiles: ChapterFileInfo[] = [];\n for (let i = 0; i < chapters.length; i++) {\n const chap = chapters[i];\n const num = String(i + 1).padStart(3, '0');\n const id = `chapter-${num}`;\n const filename = `${id}.xhtml`;\n const audioInfo = chapterAudio[i] ?? null;\n\n // Render XHTML with element IDs for SMIL references when audio is present\n const xhtml = renderChapterXhtml(chap.nodes, title, resolvedImages, audioInfo !== null);\n zip.file(`OEBPS/chapters/${filename}`, xhtml);\n\n let smilFilename: string | undefined;\n if (audioInfo) {\n smilFilename = `${id}.smil`;\n const smil = generateSmil(filename, audioInfo, chap.nodes);\n zip.file(`OEBPS/chapters/${smilFilename}`, smil);\n }\n\n chapterFiles.push({\n id,\n filename,\n title: chap.title,\n smilFilename,\n duration: audioInfo?.duration,\n });\n }\n\n // OEBPS/toc.xhtml (EPUB 3 nav)\n zip.file('OEBPS/toc.xhtml', generateTocXhtml(chapterFiles, title));\n\n // OEBPS/content.opf\n zip.file(\n 'OEBPS/content.opf',\n generateContentOpf({\n uuid,\n title,\n author,\n language,\n description,\n publisher,\n chapters: chapterFiles,\n images: resolvedImages,\n coverFilename,\n audioFiles: allAudioFiles,\n totalDuration: options.totalDuration,\n }),\n );\n\n const blob = await zip.generateAsync({\n type: 'arraybuffer',\n compression: 'DEFLATE',\n compressionOptions: { level: 6 },\n // mimetype was already set to STORE above; JSZip respects per-file options\n });\n\n return blob;\n}\n\n/**\n * Convert a squisq Doc to an EPUB 3 file.\n *\n * Convenience wrapper: Doc → MarkdownDocument → EPUB.\n * When the Doc has audio segments and `options.audio` is provided,\n * EPUB 3 Media Overlays are generated for narrated playback.\n */\nexport async function docToEpub(doc: Doc, options: EpubExportOptions = {}): Promise<ArrayBuffer> {\n const { docToMarkdown } = await import('@bendyline/squisq/doc');\n const markdownDoc = docToMarkdown(doc);\n\n // Thread audio segment metadata from the Doc into options\n const epubOptions: EpubExportOptions = { ...options };\n if (doc.audio?.segments?.length && !epubOptions.audioSegments) {\n epubOptions.audioSegments = doc.audio.segments;\n }\n if (doc.duration && !epubOptions.totalDuration) {\n epubOptions.totalDuration = doc.duration;\n }\n\n return markdownDocToEpub(markdownDoc, epubOptions);\n}\n\n// ── Chapter Splitting ─────────────────────────────────────────────\n\ninterface Chapter {\n title: string;\n nodes: MarkdownBlockNode[];\n}\n\ninterface ChapterFileInfo {\n id: string;\n filename: string;\n title: string;\n smilFilename?: string;\n duration?: number;\n}\n\ninterface ChapterAudioInfo {\n audioFilename: string;\n clipStart: number;\n clipEnd: number;\n duration: number;\n}\n\nfunction splitIntoChapters(nodes: MarkdownBlockNode[]): Chapter[] {\n const chapters: Chapter[] = [];\n let currentNodes: MarkdownBlockNode[] = [];\n let currentTitle = 'Untitled';\n\n for (const node of nodes) {\n if (node.type === 'heading' && node.depth <= 2) {\n // Flush previous chapter\n if (currentNodes.length > 0) {\n chapters.push({ title: currentTitle, nodes: currentNodes });\n }\n currentTitle = extractHeadingText(node);\n currentNodes = [node];\n } else {\n currentNodes.push(node);\n }\n }\n\n // Flush remaining\n if (currentNodes.length > 0) {\n chapters.push({ title: currentTitle, nodes: currentNodes });\n }\n\n // If no chapters were created, wrap everything as one\n if (chapters.length === 0) {\n chapters.push({ title: 'Untitled', nodes: [] });\n }\n\n return chapters;\n}\n\nfunction extractHeadingText(heading: MarkdownHeading): string {\n return extractPlainText(heading.children);\n}\n\n// ── Image Collection ──────────────────────────────────────────────\n\nfunction collectDocImages(nodes: MarkdownBlockNode[]): Set<string> {\n const images = new Set<string>();\n\n function walkBlock(node: MarkdownBlockNode): void {\n switch (node.type) {\n case 'paragraph':\n case 'heading':\n node.children.forEach(walkInline);\n break;\n case 'blockquote':\n node.children.forEach(walkBlock);\n break;\n case 'list':\n node.children.forEach((item) => item.children.forEach(walkBlock));\n break;\n case 'table':\n node.children.forEach((row) =>\n row.children.forEach((cell) => cell.children.forEach(walkInline)),\n );\n break;\n default:\n break;\n }\n }\n\n function walkInline(node: MarkdownInlineNode): void {\n if (node.type === 'image' && node.url && !node.url.startsWith('data:')) {\n images.add(node.url);\n }\n if ('children' in node && Array.isArray(node.children)) {\n (node.children as MarkdownInlineNode[]).forEach(walkInline);\n }\n }\n\n nodes.forEach(walkBlock);\n return images;\n}\n\n// ── XHTML Rendering ───────────────────────────────────────────────\n\ntype ImageMap = Map<string, { data: ArrayBuffer; mime: string; filename: string }>;\n\nfunction renderChapterXhtml(\n nodes: MarkdownBlockNode[],\n bookTitle: string,\n images: ImageMap,\n addIds = false,\n): string {\n let elementCounter = 0;\n const nextId = () => `p${++elementCounter}`;\n const body = nodes.map((n) => blockToXhtml(n, images, addIds ? nextId : undefined)).join('\\n');\n return `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE html>\n<html xmlns=\"http://www.w3.org/1999/xhtml\" xmlns:epub=\"http://www.idpf.org/2007/ops\">\n<head>\n <meta charset=\"UTF-8\"/>\n <title>${escapeXml(bookTitle)}</title>\n <link rel=\"stylesheet\" type=\"text/css\" href=\"../styles.css\"/>\n</head>\n<body>\n${body}\n</body>\n</html>`;\n}\n\nfunction blockToXhtml(node: MarkdownBlockNode, images: ImageMap, nextId?: () => string): string {\n const idAttr = nextId ? ` id=\"${nextId()}\"` : '';\n\n switch (node.type) {\n case 'heading': {\n const tag = `h${node.depth}`;\n return `<${tag}${idAttr}>${inlinesToXhtml(node.children, images)}</${tag}>`;\n }\n\n case 'paragraph':\n return `<p${idAttr}>${inlinesToXhtml(node.children, images)}</p>`;\n\n case 'blockquote':\n return `<blockquote${idAttr}>\\n${node.children.map((c) => blockToXhtml(c, images, nextId)).join('\\n')}\\n</blockquote>`;\n\n case 'list': {\n const tag = node.ordered ? 'ol' : 'ul';\n const startAttr =\n node.ordered && node.start && node.start !== 1 ? ` start=\"${node.start}\"` : '';\n const items = node.children.map((item) => listItemToXhtml(item, images)).join('\\n');\n return `<${tag}${idAttr}${startAttr}>\\n${items}\\n</${tag}>`;\n }\n\n case 'code': {\n const langAttr = node.lang ? ` class=\"language-${escapeXml(node.lang)}\"` : '';\n return `<pre${idAttr}><code${langAttr}>${escapeXml(node.value)}</code></pre>`;\n }\n\n case 'thematicBreak':\n return `<hr${idAttr}/>`;\n\n case 'table':\n return tableToXhtml(node as MarkdownTable, images, idAttr);\n\n case 'htmlBlock':\n // Strip HTML tags for XHTML safety — raw HTML may not be well-formed XML\n return `<p${idAttr}>${escapeXml(node.rawHtml.replace(/<[^>]+>/g, ''))}</p>`;\n\n case 'math':\n return `<p${idAttr} class=\"math\">${escapeXml(node.value)}</p>`;\n\n default:\n return '';\n }\n}\n\nfunction listItemToXhtml(item: MarkdownListItem, images: ImageMap): string {\n const content = item.children.map((c) => blockToXhtml(c, images)).join('\\n');\n // Unwrap single <p> inside <li> for cleaner output\n const unwrapped =\n item.children.length === 1 && item.children[0].type === 'paragraph'\n ? inlinesToXhtml((item.children[0] as MarkdownParagraph).children, images)\n : content;\n return `<li>${unwrapped}</li>`;\n}\n\nfunction tableToXhtml(table: MarkdownTable, images: ImageMap, idAttr = ''): string {\n const rows = table.children;\n if (rows.length === 0) return `<table${idAttr}></table>`;\n\n const headerRow = rows[0];\n const bodyRows = rows.slice(1);\n const align = table.align ?? [];\n\n function cellToXhtml(cell: MarkdownTableCell, tag: 'th' | 'td', colIndex: number): string {\n const a = align[colIndex];\n const style = a ? ` style=\"text-align: ${a}\"` : '';\n return `<${tag}${style}>${inlinesToXhtml(cell.children, images)}</${tag}>`;\n }\n\n const thead = `<thead><tr>${headerRow.children.map((c, i) => cellToXhtml(c, 'th', i)).join('')}</tr></thead>`;\n const tbody =\n bodyRows.length > 0\n ? `<tbody>${bodyRows.map((row: MarkdownTableRow) => `<tr>${row.children.map((c, i) => cellToXhtml(c, 'td', i)).join('')}</tr>`).join('')}</tbody>`\n : '';\n\n return `<table${idAttr}>${thead}${tbody}</table>`;\n}\n\nfunction inlinesToXhtml(nodes: MarkdownInlineNode[], images: ImageMap): string {\n return nodes.map((n) => inlineToXhtml(n, images)).join('');\n}\n\nfunction inlineToXhtml(node: MarkdownInlineNode, images: ImageMap): string {\n switch (node.type) {\n case 'text':\n return escapeXml(node.value);\n\n case 'strong':\n return `<strong>${inlinesToXhtml(node.children, images)}</strong>`;\n\n case 'emphasis':\n return `<em>${inlinesToXhtml(node.children, images)}</em>`;\n\n case 'delete':\n return `<del>${inlinesToXhtml(node.children, images)}</del>`;\n\n case 'inlineCode':\n return `<code>${escapeXml(node.value)}</code>`;\n\n case 'link': {\n const href = sanitizeUrl(node.url, 'link');\n if (!href) return inlinesToXhtml(node.children, images);\n const titleAttr = node.title ? ` title=\"${escapeXml(node.title)}\"` : '';\n return `<a href=\"${escapeXml(href)}\"${titleAttr}>${inlinesToXhtml(node.children, images)}</a>`;\n }\n\n case 'image': {\n const alt = escapeXml(node.alt ?? '');\n const resolved = images.get(node.url);\n const safeSrc = resolved ? `../images/${resolved.filename}` : sanitizeUrl(node.url, 'media');\n if (!safeSrc) return alt;\n const src = escapeXml(safeSrc);\n return `<img src=\"${src}\" alt=\"${alt}\"/>`;\n }\n\n case 'break':\n return '<br/>';\n\n case 'inlineMath':\n return `<span class=\"math\">${escapeXml(node.value)}</span>`;\n\n case 'htmlInline':\n // Strip tags for XHTML safety\n return escapeXml(node.rawHtml.replace(/<[^>]+>/g, ''));\n\n default:\n return '';\n }\n}\n\n/**\n * Produce an EPUB identifier without assuming `crypto.randomUUID()` exists.\n * `randomUUID` is unavailable in older/non-secure browser contexts; use\n * `getRandomValues` when possible and retain a non-cryptographic last resort\n * because this UUID is an identifier, not a security token.\n */\nfunction createEpubUuid(): string {\n const webCrypto = globalThis.crypto;\n if (typeof webCrypto?.randomUUID === 'function') return webCrypto.randomUUID();\n\n const bytes = new Uint8Array(16);\n if (typeof webCrypto?.getRandomValues === 'function') {\n webCrypto.getRandomValues(bytes);\n } else {\n let seed = Date.now() ^ Math.floor(Math.random() * 0x7fffffff);\n for (let i = 0; i < bytes.length; i++) {\n seed = Math.imul(seed ^ (seed >>> 15), 1 | seed);\n bytes[i] = seed & 0xff;\n }\n }\n // RFC 4122 version 4 / variant 1 bits.\n bytes[6] = (bytes[6]! & 0x0f) | 0x40;\n bytes[8] = (bytes[8]! & 0x3f) | 0x80;\n const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('');\n return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;\n}\n\n/** Flatten an external path to a safe archive basename. */\nfunction safeArchiveBasename(path: string, fallback: string): string {\n const filename = extractFilename(path.replace(/\\\\/g, '/'));\n const safe = Array.from(filename, (character) => {\n const codePoint = character.codePointAt(0) ?? 0;\n return character === '\\\\' || character === '/' || codePoint < 0x20 || codePoint === 0x7f\n ? '_'\n : character;\n })\n .join('')\n .trim();\n return !safe || safe === '.' || safe === '..' ? fallback : safe;\n}\n\n/** Return a basename that does not collide with a prior archive member. */\nfunction uniqueFilename(filename: string, used: ReadonlySet<string>): string {\n if (!used.has(filename)) return filename;\n const dot = filename.lastIndexOf('.');\n const base = dot > 0 ? filename.slice(0, dot) : filename;\n const ext = dot > 0 ? filename.slice(dot) : '';\n let suffix = 2;\n while (used.has(`${base}-${suffix}${ext}`)) suffix++;\n return `${base}-${suffix}${ext}`;\n}\n\n// ── SMIL Media Overlays ───────────────────────────────────────────\n\n/**\n * Generate an EPUB 3 Media Overlay (SMIL) file for a chapter.\n * Maps block-level elements to audio clip ranges for synchronized narration.\n */\nfunction generateSmil(\n chapterFilename: string,\n audioInfo: ChapterAudioInfo,\n nodes: MarkdownBlockNode[],\n): string {\n // Count block elements to match the IDs generated by blockToXhtml.\n // Must mirror blockToXhtml's recursion: each block gets an ID,\n // blockquote children recurse (they pass nextId), but list items do not.\n let elementCount = 0;\n function countBlocks(node: MarkdownBlockNode): void {\n elementCount++;\n if (node.type === 'blockquote') node.children.forEach(countBlocks);\n }\n nodes.forEach(countBlocks);\n\n if (elementCount === 0) elementCount = 1;\n\n // Distribute audio duration evenly across elements (best effort without word-level timing)\n const clipDuration = audioInfo.duration / elementCount;\n const pars: string[] = [];\n\n for (let i = 0; i < elementCount; i++) {\n const clipStart = formatTime(audioInfo.clipStart + i * clipDuration, true);\n const clipEnd = formatTime(audioInfo.clipStart + (i + 1) * clipDuration, true);\n pars.push(\n ` <par id=\"par-${i + 1}\">` +\n `\\n <text src=\"${chapterFilename}#p${i + 1}\"/>` +\n `\\n <audio src=\"../audio/${escapeXml(audioInfo.audioFilename)}\" clipBegin=\"${clipStart}\" clipEnd=\"${clipEnd}\"/>` +\n `\\n </par>`,\n );\n }\n\n return `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<smil xmlns=\"http://www.w3.org/ns/SMIL\" xmlns:epub=\"http://www.idpf.org/2007/ops\" version=\"3.0\">\n <body>\n <seq id=\"seq-1\" epub:textref=\"${chapterFilename}\">\n${pars.join('\\n')}\n </seq>\n </body>\n</smil>`;\n}\n\nfunction formatTime(seconds: number, fractional = false): string {\n const h = Math.floor(seconds / 3600);\n const m = Math.floor((seconds % 3600) / 60);\n const s = seconds % 60;\n const sPart = fractional ? s.toFixed(3).padStart(6, '0') : String(Math.floor(s)).padStart(2, '0');\n return `${h}:${String(m).padStart(2, '0')}:${sPart}`;\n}\n\n// ── Package Documents ─────────────────────────────────────────────\n\nconst CONTAINER_XML = `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<container version=\"1.0\" xmlns=\"urn:oasis:names:tc:opendocument:xmlns:container\">\n <rootfiles>\n <rootfile full-path=\"OEBPS/content.opf\" media-type=\"application/oebps-package+xml\"/>\n </rootfiles>\n</container>`;\n\ninterface OpfParams {\n uuid: string;\n title: string;\n author: string;\n language: string;\n description: string;\n publisher: string;\n chapters: ChapterFileInfo[];\n images: ImageMap;\n coverFilename?: string;\n audioFiles?: { filename: string; mime: string }[];\n totalDuration?: number;\n}\n\nfunction generateContentOpf(params: OpfParams): string {\n const {\n uuid,\n title,\n author,\n language,\n description,\n publisher,\n chapters,\n images,\n coverFilename,\n audioFiles,\n totalDuration,\n } = params;\n const modified = new Date().toISOString().replace(/\\.\\d+Z$/, 'Z');\n const hasOverlays = chapters.some((c) => c.smilFilename);\n\n // Manifest items\n const manifestItems: string[] = [\n ' <item id=\"toc\" href=\"toc.xhtml\" media-type=\"application/xhtml+xml\" properties=\"nav\"/>',\n ' <item id=\"css\" href=\"styles.css\" media-type=\"text/css\"/>',\n ];\n\n if (coverFilename) {\n const mime = inferMimeType(coverFilename);\n manifestItems.push(\n ` <item id=\"cover-image\" href=\"images/${escapeXml(coverFilename)}\" media-type=\"${mime}\" properties=\"cover-image\"/>`,\n );\n manifestItems.push(\n ' <item id=\"cover-page\" href=\"chapters/cover.xhtml\" media-type=\"application/xhtml+xml\"/>',\n );\n }\n\n for (const chap of chapters) {\n const overlayAttr = chap.smilFilename ? ` media-overlay=\"${chap.id}-overlay\"` : '';\n manifestItems.push(\n ` <item id=\"${chap.id}\" href=\"chapters/${escapeXml(chap.filename)}\" media-type=\"application/xhtml+xml\"${overlayAttr}/>`,\n );\n if (chap.smilFilename) {\n manifestItems.push(\n ` <item id=\"${chap.id}-overlay\" href=\"chapters/${escapeXml(chap.smilFilename)}\" media-type=\"application/smil+xml\"/>`,\n );\n }\n }\n\n // Audio files in manifest\n if (audioFiles) {\n const usedAudioNames = new Set<string>();\n for (const af of audioFiles) {\n if (usedAudioNames.has(af.filename)) continue;\n usedAudioNames.add(af.filename);\n const audioId = `audio-${af.filename.replace(/[^a-zA-Z0-9]/g, '-')}`;\n manifestItems.push(\n ` <item id=\"${audioId}\" href=\"audio/${escapeXml(af.filename)}\" media-type=\"${af.mime}\"/>`,\n );\n }\n }\n\n const usedFilenames = new Set<string>();\n for (const [, img] of images) {\n if (usedFilenames.has(img.filename)) continue;\n usedFilenames.add(img.filename);\n const imgId = `img-${img.filename.replace(/[^a-zA-Z0-9]/g, '-')}`;\n manifestItems.push(\n ` <item id=\"${imgId}\" href=\"images/${escapeXml(img.filename)}\" media-type=\"${img.mime}\"/>`,\n );\n }\n\n // Spine — cover page first (if present), then chapters\n const spineEntries: string[] = [];\n if (coverFilename) {\n spineEntries.push(' <itemref idref=\"cover-page\"/>');\n }\n for (const chap of chapters) {\n spineEntries.push(` <itemref idref=\"${chap.id}\"/>`);\n }\n const spineItems = spineEntries.join('\\n');\n\n // Metadata\n const metaParts = [\n ` <dc:identifier id=\"uid\">urn:uuid:${uuid}</dc:identifier>`,\n ` <dc:title>${escapeXml(title)}</dc:title>`,\n ` <dc:language>${escapeXml(language)}</dc:language>`,\n ` <meta property=\"dcterms:modified\">${modified}</meta>`,\n ];\n if (author) metaParts.push(` <dc:creator>${escapeXml(author)}</dc:creator>`);\n if (description) metaParts.push(` <dc:description>${escapeXml(description)}</dc:description>`);\n if (publisher) metaParts.push(` <dc:publisher>${escapeXml(publisher)}</dc:publisher>`);\n\n // Media Overlay metadata\n if (hasOverlays && totalDuration) {\n metaParts.push(` <meta property=\"media:duration\">${formatTime(totalDuration)}</meta>`);\n metaParts.push(' <meta property=\"media:active-class\">epub-media-overlay-active</meta>');\n }\n\n return `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<package xmlns=\"http://www.idpf.org/2007/opf\" version=\"3.0\" unique-identifier=\"uid\">\n <metadata xmlns:dc=\"http://purl.org/dc/elements/1.1/\">\n${metaParts.join('\\n')}\n </metadata>\n <manifest>\n${manifestItems.join('\\n')}\n </manifest>\n <spine>\n${spineItems}\n </spine>\n</package>`;\n}\n\nfunction generateTocXhtml(chapters: ChapterFileInfo[], bookTitle: string): string {\n const navItems = chapters\n .map(\n (chap) =>\n ` <li><a href=\"chapters/${escapeXml(chap.filename)}\">${escapeXml(chap.title)}</a></li>`,\n )\n .join('\\n');\n\n return `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE html>\n<html xmlns=\"http://www.w3.org/1999/xhtml\" xmlns:epub=\"http://www.idpf.org/2007/ops\">\n<head>\n <meta charset=\"UTF-8\"/>\n <title>${escapeXml(bookTitle)}</title>\n <link rel=\"stylesheet\" type=\"text/css\" href=\"styles.css\"/>\n</head>\n<body>\n <nav epub:type=\"toc\">\n <h1>Table of Contents</h1>\n <ol>\n${navItems}\n </ol>\n </nav>\n</body>\n</html>`;\n}\n\n// ── Stylesheet ────────────────────────────────────────────────────\n\nfunction generateStylesheet(\n themeId: string | undefined,\n doc?: MarkdownDocument,\n registry?: ThemeRegistry,\n): string {\n let themeVars = '';\n if (themeId) {\n const theme = resolveThemeForDoc(doc, themeId, registry);\n themeVars = `\n --epub-bg: ${theme.colors.background};\n --epub-text: ${theme.colors.text};\n --epub-primary: ${theme.colors.primary};\n --epub-heading-font: ${resolveFontFamily(theme.typography.titleFont, 'serif')};\n --epub-body-font: ${resolveFontFamily(theme.typography.bodyFont, 'sans-serif')};`;\n }\n\n return `/* Squisq EPUB Stylesheet */\n:root {${themeVars}\n}\n\nbody {\n font-family: var(--epub-body-font, Georgia, 'Times New Roman', serif);\n color: var(--epub-text, #1a1a1a);\n line-height: 1.7;\n margin: 1em 2em;\n max-width: 40em;\n}\n\nh1, h2, h3, h4, h5, h6 {\n font-family: var(--epub-heading-font, system-ui, sans-serif);\n color: var(--epub-primary, #1a1a1a);\n margin-top: 1.5em;\n margin-bottom: 0.5em;\n line-height: 1.3;\n}\n\nh1 { font-size: 2em; }\nh2 { font-size: 1.5em; }\nh3 { font-size: 1.25em; }\n\np {\n margin: 0.8em 0;\n}\n\na {\n color: var(--epub-primary, #2563eb);\n}\n\nimg {\n max-width: 100%;\n height: auto;\n}\n\npre {\n background: #f5f5f5;\n padding: 1em;\n overflow-x: auto;\n border-radius: 4px;\n font-size: 0.9em;\n line-height: 1.4;\n}\n\ncode {\n font-family: 'Courier New', Courier, monospace;\n font-size: 0.9em;\n}\n\np > code, li > code {\n background: #f0f0f0;\n padding: 0.1em 0.3em;\n border-radius: 3px;\n}\n\nblockquote {\n border-left: 3px solid var(--epub-primary, #d1d5db);\n margin: 1em 0;\n padding: 0.5em 1em;\n color: #4b5563;\n}\n\ntable {\n border-collapse: collapse;\n width: 100%;\n margin: 1em 0;\n}\n\nth, td {\n border: 1px solid #d1d5db;\n padding: 0.5em 0.75em;\n text-align: left;\n}\n\nth {\n background: #f3f4f6;\n font-weight: 600;\n}\n\nhr {\n border: none;\n border-top: 1px solid #d1d5db;\n margin: 2em 0;\n}\n\nul, ol {\n margin: 0.8em 0;\n padding-left: 1.5em;\n}\n\nli {\n margin: 0.3em 0;\n}\n\n.math {\n font-family: 'Courier New', Courier, monospace;\n font-style: italic;\n}\n\n/* Media Overlay active highlight (narration sync) */\n.epub-media-overlay-active {\n background-color: rgba(37, 99, 235, 0.12);\n}\n`;\n}\n"],"mappings":"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","names":[]}
@@ -6,6 +6,18 @@ import {
6
6
  extractPlainText,
7
7
  stripHtmlTags
8
8
  } from "./chunk-PN52A5AA.js";
9
+ import {
10
+ attrNS,
11
+ baseDirOf,
12
+ resolveTarget
13
+ } from "./chunk-YRT7GQ5Y.js";
14
+ import {
15
+ createPackage
16
+ } from "./chunk-XTLQXR4U.js";
17
+ import {
18
+ escapeXml,
19
+ xmlDeclaration
20
+ } from "./chunk-U4MRIFKL.js";
9
21
  import {
10
22
  CONTENT_TYPE_PPTX_PRESENTATION,
11
23
  CONTENT_TYPE_PPTX_SLIDE,
@@ -24,25 +36,20 @@ import {
24
36
  REL_SLIDE_LAYOUT,
25
37
  REL_SLIDE_MASTER,
26
38
  REL_THEME,
27
- createPackage,
28
39
  getPartBinary,
29
40
  getPartRelationships,
30
41
  getPartXml,
31
42
  openPackage
32
- } from "./chunk-SHLPSADL.js";
33
- import {
34
- escapeXml,
35
- xmlDeclaration
36
- } from "./chunk-U4MRIFKL.js";
43
+ } from "./chunk-4HJ6AK2Q.js";
37
44
  import {
38
45
  buildContainer
39
46
  } from "./chunk-SSUPBUF5.js";
40
47
  import {
41
48
  inferMimeType
42
- } from "./chunk-MJGRI6XR.js";
49
+ } from "./chunk-MLX2BOJC.js";
43
50
  import {
44
51
  extToMime
45
- } from "./chunk-DQAZR57U.js";
52
+ } from "./chunk-O3GVVND4.js";
46
53
 
47
54
  // src/pptx/export.ts
48
55
  import { resolveFontFamily } from "@bendyline/squisq/schemas";
@@ -124,7 +131,7 @@ function resolveSlideStyle(themeId, options, doc) {
124
131
  hasTheme: false
125
132
  };
126
133
  }
127
- const theme = resolveThemeForDoc(doc, themeId);
134
+ const theme = resolveThemeForDoc(doc, themeId, options.themeRegistry);
128
135
  const c = theme.colors;
129
136
  return {
130
137
  background: stripHash(c.background),
@@ -748,18 +755,6 @@ import { markdownToDoc } from "@bendyline/squisq/doc";
748
755
 
749
756
  // src/pptx/import.ts
750
757
  import { stringifyMarkdown } from "@bendyline/squisq/markdown";
751
- function attrNS(el, ns, local, fallback) {
752
- return el.getAttributeNS(ns, local) ?? el.getAttribute(fallback);
753
- }
754
- function resolveTarget(baseDir, target) {
755
- if (target.startsWith("/")) return target.replace(/^\//, "");
756
- const stack = baseDir ? baseDir.split("/") : [];
757
- for (const seg of target.split("/")) {
758
- if (seg === "..") stack.pop();
759
- else if (seg !== ".") stack.push(seg);
760
- }
761
- return stack.join("/");
762
- }
763
758
  async function orderedSlidePaths(pkg) {
764
759
  const pres = await getPartXml(pkg, "ppt/presentation.xml");
765
760
  if (!pres) return [];
@@ -805,10 +800,6 @@ function tableToMarkdown(tbl) {
805
800
  }
806
801
  return { type: "table", children: rows };
807
802
  }
808
- function baseDirOf(path) {
809
- const slash = path.lastIndexOf("/");
810
- return slash === -1 ? "" : path.slice(0, slash);
811
- }
812
803
  async function extractSlideImages(doc, slidePath, ctx) {
813
804
  const rels = await getPartRelationships(ctx.pkg, slidePath);
814
805
  const relById = new Map(rels.map((r) => [r.id, r.target]));
@@ -838,13 +829,72 @@ async function extractSlideImages(doc, slidePath, ctx) {
838
829
  }
839
830
  return images;
840
831
  }
832
+ function cleanParamText(text) {
833
+ return text.replace(/\s+/g, " ").trim();
834
+ }
835
+ function buildSlideAnnotation(analyzed, entries, images, ctx) {
836
+ const omitted = /* @__PURE__ */ new Set();
837
+ if (!analyzed) return { omitted };
838
+ const verdict = analyzed.verdict;
839
+ if (verdict.kind === "custom") {
840
+ ctx.usedCustomTemplates.set(verdict.def.name, verdict.def);
841
+ return { annotation: { template: verdict.def.name }, omitted };
842
+ }
843
+ if (verdict.kind !== "builtin") return { omitted };
844
+ switch (verdict.paramSpec) {
845
+ case "titleSubtitle": {
846
+ const sub = entries.find((e) => e.rawType === "subTitle");
847
+ const subtitle = sub ? cleanParamText(sub.texts.join(" ")) : "";
848
+ if (sub && subtitle) omitted.add(sub);
849
+ return {
850
+ annotation: {
851
+ template: verdict.template,
852
+ ...subtitle ? { params: { subtitle } } : {}
853
+ },
854
+ omitted
855
+ };
856
+ }
857
+ case "comparisonPairs": {
858
+ if (!verdict.columns) return { omitted };
859
+ const textAt = (idx) => idx === void 0 ? [] : entries.find((e) => e.idx === idx)?.texts ?? [];
860
+ const side = (idxs) => {
861
+ const header = cleanParamText(textAt(idxs[0])[0] ?? "");
862
+ const body = cleanParamText(textAt(idxs[1])[0] ?? "");
863
+ return header ? body ? `${header}|${body}` : header : "";
864
+ };
865
+ const left = side(verdict.columns.left);
866
+ const right = side(verdict.columns.right);
867
+ if (!left || !right) return { omitted };
868
+ return { annotation: { template: "twoColumn", params: { left, right } }, omitted };
869
+ }
870
+ case "featureImage": {
871
+ const image = images[0];
872
+ if (!image) return { omitted };
873
+ const params = { imageSrc: image.url };
874
+ if (image.alt && image.alt !== "Image") params.imageAlt = cleanParamText(image.alt);
875
+ return { annotation: { template: verdict.template, params }, omitted };
876
+ }
877
+ case "photoGridGate": {
878
+ if (images.length < 2) return { omitted };
879
+ return {
880
+ annotation: {
881
+ template: "photoGrid",
882
+ params: { images: images.map((i) => i.url).join(",") }
883
+ },
884
+ omitted
885
+ };
886
+ }
887
+ default:
888
+ return { annotation: { template: verdict.template }, omitted };
889
+ }
890
+ }
841
891
  async function convertSlide(path, index, ctx) {
842
892
  const pkg = ctx.pkg;
843
893
  const doc = await getPartXml(pkg, path);
844
894
  if (!doc) return [];
845
895
  const out = [];
846
896
  let title = "";
847
- const bullets = [];
897
+ const entries = [];
848
898
  const shapes = doc.getElementsByTagNameNS(NS_PML, "sp");
849
899
  for (let s = 0; s < shapes.length; s++) {
850
900
  const sp = shapes[s];
@@ -859,15 +909,25 @@ async function convertSlide(path, index, ctx) {
859
909
  if (texts.length === 0) continue;
860
910
  if (isTitleShape(sp) && !title) {
861
911
  title = texts.join(" ");
862
- } else {
863
- bullets.push(...texts);
912
+ continue;
864
913
  }
914
+ const phs = sp.getElementsByTagNameNS(NS_PML, "ph");
915
+ const rawType = phs.length ? phs[0].getAttribute("type") ?? "" : "";
916
+ const idxRaw = phs.length ? phs[0].getAttribute("idx") : null;
917
+ const idx = idxRaw ? parseInt(idxRaw, 10) || 0 : 0;
918
+ entries.push({ rawType, idx, texts });
865
919
  }
920
+ const images = ctx.extractImages ? await extractSlideImages(doc, path, ctx) : [];
921
+ const layoutPath = ctx.inference?.layoutPathBySlide.get(path);
922
+ const analyzed = layoutPath ? ctx.inference?.byLayoutPath.get(layoutPath) : void 0;
923
+ const { annotation, omitted } = buildSlideAnnotation(analyzed, entries, images, ctx);
866
924
  out.push({
867
925
  type: "heading",
868
926
  depth: 2,
869
- children: [{ type: "text", value: title || `Slide ${index + 1}` }]
927
+ children: [{ type: "text", value: title || `Slide ${index + 1}` }],
928
+ ...annotation ? { templateAnnotation: annotation } : {}
870
929
  });
930
+ const bullets = entries.filter((e) => !omitted.has(e)).flatMap((e) => e.texts);
871
931
  if (bullets.length > 0) {
872
932
  const items = bullets.map((text) => ({
873
933
  type: "listItem",
@@ -875,37 +935,92 @@ async function convertSlide(path, index, ctx) {
875
935
  }));
876
936
  out.push({ type: "list", ordered: false, children: items });
877
937
  }
878
- if (ctx.extractImages) {
879
- const images = await extractSlideImages(doc, path, ctx);
880
- for (const image of images) {
881
- out.push({ type: "paragraph", children: [image] });
882
- }
938
+ for (const image of images) {
939
+ out.push({ type: "paragraph", children: [image] });
883
940
  }
884
941
  const tbls = doc.getElementsByTagNameNS(NS_DRAWINGML, "tbl");
885
942
  for (let t = 0; t < tbls.length; t++) out.push(tableToMarkdown(tbls[t]));
886
943
  return out;
887
944
  }
945
+ function warnInferenceFailure(step, err) {
946
+ const message = err instanceof Error ? err.message : String(err);
947
+ console.warn(`pptx import: ${step} failed; importing without it \u2014 ${message}`);
948
+ }
888
949
  async function importDocument(pkg, options) {
889
950
  const ctx = {
890
951
  pkg,
891
952
  extractImages: options.extractImages ?? false,
892
953
  extractedImages: /* @__PURE__ */ new Map(),
893
- imageCounter: 0
954
+ imageCounter: 0,
955
+ usedCustomTemplates: /* @__PURE__ */ new Map()
894
956
  };
957
+ const inferTheme = options.inferTheme !== false;
958
+ const inferLayouts = options.inferLayouts !== false;
959
+ let extraction = null;
960
+ if (inferTheme || inferLayouts) {
961
+ try {
962
+ const { extractPptxTheme } = await import("./extract-LNOA7DBK.js");
963
+ extraction = await extractPptxTheme(pkg);
964
+ } catch (err) {
965
+ warnInferenceFailure("theme extraction", err);
966
+ }
967
+ }
968
+ let theme;
969
+ if (inferTheme && extraction) {
970
+ try {
971
+ const { compileExtractedTheme } = await import("./mapTheme-IR27S6IV.js");
972
+ theme = compileExtractedTheme(extraction).theme;
973
+ } catch (err) {
974
+ warnInferenceFailure("theme compilation", err);
975
+ }
976
+ }
977
+ if (inferLayouts) {
978
+ try {
979
+ const { analyzePptxLayouts: analyzePptxLayouts2 } = await import("./layouts-7OUUVAGU.js");
980
+ const { colorHintsFromExtraction } = await import("./mapTheme-IR27S6IV.js");
981
+ ctx.inference = await analyzePptxLayouts2(pkg, {
982
+ colors: extraction ? colorHintsFromExtraction(extraction) : {}
983
+ });
984
+ } catch (err) {
985
+ warnInferenceFailure("layout inference", err);
986
+ }
987
+ }
895
988
  const paths = await orderedSlidePaths(pkg);
896
989
  const children = [];
897
990
  for (let i = 0; i < paths.length; i++) {
898
991
  children.push(...await convertSlide(paths[i], i, ctx));
899
992
  }
900
- return { doc: { type: "document", children }, ctx };
993
+ const doc = { type: "document", children };
994
+ const frontmatter = {};
995
+ if (theme || ctx.usedCustomTemplates.size > 0) {
996
+ const {
997
+ writeCustomThemesToFrontmatter,
998
+ writeCustomTemplatesToFrontmatter,
999
+ FRONTMATTER_CUSTOM_THEMES_KEY,
1000
+ FRONTMATTER_CUSTOM_TEMPLATES_KEY
1001
+ } = await import("@bendyline/squisq/doc");
1002
+ if (theme) {
1003
+ const payload = writeCustomThemesToFrontmatter([theme]);
1004
+ if (payload) {
1005
+ frontmatter[FRONTMATTER_CUSTOM_THEMES_KEY] = payload;
1006
+ frontmatter["squisq-theme"] = theme.id;
1007
+ }
1008
+ }
1009
+ if (ctx.usedCustomTemplates.size > 0) {
1010
+ const payload = writeCustomTemplatesToFrontmatter([...ctx.usedCustomTemplates.values()]);
1011
+ if (payload) frontmatter[FRONTMATTER_CUSTOM_TEMPLATES_KEY] = payload;
1012
+ }
1013
+ }
1014
+ if (Object.keys(frontmatter).length > 0) doc.frontmatter = frontmatter;
1015
+ return { doc, ctx };
901
1016
  }
902
1017
  async function pptxToMarkdownDoc(data, options = {}) {
903
- const pkg = await openPackage(data);
1018
+ const pkg = await openPackage(data, options);
904
1019
  const { doc } = await importDocument(pkg, options);
905
1020
  return doc;
906
1021
  }
907
1022
  async function pptxToContainer(data, options = {}) {
908
- const pkg = await openPackage(data);
1023
+ const pkg = await openPackage(data, options);
909
1024
  const { doc, ctx } = await importDocument(pkg, { ...options, extractImages: true });
910
1025
  return buildContainer(stringifyMarkdown(doc), ctx.extractedImages);
911
1026
  }
@@ -922,4 +1037,4 @@ export {
922
1037
  pptxToContainer,
923
1038
  pptxToDoc
924
1039
  };
925
- //# sourceMappingURL=chunk-YESS7VY3.js.map
1040
+ //# sourceMappingURL=chunk-CVNVD7VH.js.map