@bendyline/squisq-formats 1.1.1 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/README.md +14 -0
  2. package/dist/__tests__/epub.test.d.ts +8 -0
  3. package/dist/__tests__/epub.test.d.ts.map +1 -0
  4. package/dist/__tests__/epub.test.js +472 -0
  5. package/dist/__tests__/epub.test.js.map +1 -0
  6. package/dist/chunk-2FPJKJ6I.js +621 -0
  7. package/dist/chunk-2FPJKJ6I.js.map +1 -0
  8. package/dist/{chunk-BHHEDPRB.js → chunk-7DEUGIOO.js} +6 -41
  9. package/dist/chunk-7DEUGIOO.js.map +1 -0
  10. package/dist/chunk-A3FHLTY5.js +42 -0
  11. package/dist/chunk-A3FHLTY5.js.map +1 -0
  12. package/dist/{chunk-6OVREALI.js → chunk-MEZF76JA.js} +168 -14
  13. package/dist/chunk-MEZF76JA.js.map +1 -0
  14. package/dist/{chunk-FIFCQN3W.js → chunk-NGWHV77G.js} +5 -3
  15. package/dist/{chunk-FIFCQN3W.js.map → chunk-NGWHV77G.js.map} +1 -1
  16. package/dist/{chunk-PP5N46YD.js → chunk-S3Y7H2BK.js} +129 -1
  17. package/dist/chunk-S3Y7H2BK.js.map +1 -0
  18. package/dist/chunk-U4MRIFKL.js +43 -0
  19. package/dist/chunk-U4MRIFKL.js.map +1 -0
  20. package/dist/{chunk-743COJWQ.js → chunk-UM5V2XZG.js} +7 -40
  21. package/dist/chunk-UM5V2XZG.js.map +1 -0
  22. package/dist/{chunk-KJ4NS4DX.js → chunk-YN5HFCEW.js} +2 -2
  23. package/dist/docx/export.d.ts +9 -0
  24. package/dist/docx/export.d.ts.map +1 -1
  25. package/dist/docx/export.js +120 -12
  26. package/dist/docx/export.js.map +1 -1
  27. package/dist/docx/import.d.ts +13 -0
  28. package/dist/docx/import.d.ts.map +1 -1
  29. package/dist/docx/import.js +102 -10
  30. package/dist/docx/import.js.map +1 -1
  31. package/dist/docx/index.d.ts +1 -1
  32. package/dist/docx/index.d.ts.map +1 -1
  33. package/dist/docx/index.js +1 -1
  34. package/dist/docx/index.js.map +1 -1
  35. package/dist/epub/export.d.ts +72 -0
  36. package/dist/epub/export.d.ts.map +1 -0
  37. package/dist/epub/export.js +674 -0
  38. package/dist/epub/export.js.map +1 -0
  39. package/dist/epub/index.d.ts +20 -0
  40. package/dist/epub/index.d.ts.map +1 -0
  41. package/dist/epub/index.js +19 -0
  42. package/dist/epub/index.js.map +1 -0
  43. package/dist/index.d.ts +2 -0
  44. package/dist/index.d.ts.map +1 -1
  45. package/dist/index.js +2 -0
  46. package/dist/index.js.map +1 -1
  47. package/dist/pdf/import.d.ts +16 -0
  48. package/dist/pdf/import.d.ts.map +1 -1
  49. package/dist/pdf/import.js +177 -0
  50. package/dist/pdf/import.js.map +1 -1
  51. package/dist/pdf/index.d.ts +1 -1
  52. package/dist/pdf/index.d.ts.map +1 -1
  53. package/dist/pdf/index.js +1 -1
  54. package/dist/pdf/index.js.map +1 -1
  55. package/package.json +13 -7
  56. package/src/__tests__/epub.test.ts +548 -0
  57. package/src/docx/export.ts +147 -12
  58. package/src/docx/import.ts +127 -10
  59. package/src/docx/index.ts +1 -1
  60. package/src/epub/export.ts +891 -0
  61. package/src/epub/index.ts +20 -0
  62. package/src/index.ts +4 -0
  63. package/src/pdf/import.ts +228 -0
  64. package/src/pdf/index.ts +1 -1
  65. package/dist/chunk-6OVREALI.js.map +0 -1
  66. package/dist/chunk-743COJWQ.js.map +0 -1
  67. package/dist/chunk-BHHEDPRB.js.map +0 -1
  68. package/dist/chunk-PP5N46YD.js.map +0 -1
  69. /package/dist/{chunk-KJ4NS4DX.js.map → chunk-YN5HFCEW.js.map} +0 -0
@@ -0,0 +1,621 @@
1
+ import {
2
+ escapeXml
3
+ } from "./chunk-U4MRIFKL.js";
4
+ import {
5
+ extractFilename,
6
+ inferMimeType
7
+ } from "./chunk-A3FHLTY5.js";
8
+
9
+ // src/epub/export.ts
10
+ import JSZip from "jszip";
11
+ import { resolveTheme } from "@bendyline/squisq/schemas";
12
+ async function markdownDocToEpub(doc, options = {}) {
13
+ const fmTitle = doc.frontmatter?.title;
14
+ const fmAuthor = doc.frontmatter?.author;
15
+ const title = options.title ?? (typeof fmTitle === "string" ? fmTitle : "Untitled");
16
+ const author = options.author ?? (typeof fmAuthor === "string" ? fmAuthor : "");
17
+ const language = options.language ?? "en";
18
+ const description = options.description ?? "";
19
+ const publisher = options.publisher ?? "";
20
+ const uuid = crypto.randomUUID();
21
+ const chapters = splitIntoChapters(doc.children);
22
+ const imageEntries = collectDocImages(doc.children);
23
+ const resolvedImages = /* @__PURE__ */ new Map();
24
+ if (options.images) {
25
+ const usedNames = /* @__PURE__ */ new Set();
26
+ for (const src of imageEntries) {
27
+ const data = options.images.get(src);
28
+ if (data) {
29
+ let filename = extractFilename(src);
30
+ if (usedNames.has(filename)) {
31
+ const dot = filename.lastIndexOf(".");
32
+ const base = dot > 0 ? filename.slice(0, dot) : filename;
33
+ const ext = dot > 0 ? filename.slice(dot) : "";
34
+ let counter = 2;
35
+ while (usedNames.has(`${base}-${counter}${ext}`)) counter++;
36
+ filename = `${base}-${counter}${ext}`;
37
+ }
38
+ usedNames.add(filename);
39
+ resolvedImages.set(src, { data, mime: inferMimeType(filename), filename });
40
+ }
41
+ }
42
+ }
43
+ const css = generateStylesheet(options.themeId);
44
+ const zip = new JSZip();
45
+ zip.file("mimetype", "application/epub+zip", { compression: "STORE" });
46
+ zip.file("META-INF/container.xml", CONTAINER_XML);
47
+ zip.file("OEBPS/styles.css", css);
48
+ for (const [, img] of resolvedImages) {
49
+ zip.file(`OEBPS/images/${img.filename}`, img.data);
50
+ }
51
+ let coverFilename;
52
+ if (options.coverImage) {
53
+ const bytes = new Uint8Array(options.coverImage);
54
+ const isPng = bytes.length >= 4 && bytes[0] === 137 && bytes[1] === 80 && bytes[2] === 78 && bytes[3] === 71;
55
+ coverFilename = isPng ? "cover.png" : "cover.jpg";
56
+ zip.file(`OEBPS/images/${coverFilename}`, options.coverImage);
57
+ }
58
+ const audioMap = options.audio;
59
+ const audioSegments = options.audioSegments;
60
+ const hasAudio = audioMap && audioSegments && audioSegments.length > 0;
61
+ const segmentAudioFiles = [];
62
+ const allAudioFiles = [];
63
+ if (hasAudio) {
64
+ for (const seg of audioSegments) {
65
+ const data = audioMap.get(seg.src) ?? audioMap.get(seg.name);
66
+ if (data) {
67
+ const filename = extractFilename(seg.src);
68
+ const finalName = filename.includes(".") ? filename : `${filename}.mp3`;
69
+ zip.file(`OEBPS/audio/${finalName}`, data);
70
+ const info = { filename: finalName, mime: inferMimeType(finalName) };
71
+ segmentAudioFiles.push(info);
72
+ allAudioFiles.push(info);
73
+ } else {
74
+ segmentAudioFiles.push(null);
75
+ }
76
+ }
77
+ }
78
+ const chapterAudio = [];
79
+ if (hasAudio && allAudioFiles.length > 0) {
80
+ if (chapters.length !== audioSegments.length) {
81
+ console.warn(
82
+ `EPUB: ${chapters.length} chapters but ${audioSegments.length} audio segments \u2014 extra chapters will reuse the last segment's audio`
83
+ );
84
+ }
85
+ for (let i = 0; i < chapters.length; i++) {
86
+ const segIdx = Math.min(i, audioSegments.length - 1);
87
+ const seg = audioSegments[segIdx];
88
+ const audioFile = segmentAudioFiles[segIdx];
89
+ if (audioFile) {
90
+ chapterAudio.push({
91
+ audioFilename: audioFile.filename,
92
+ clipStart: 0,
93
+ clipEnd: seg.duration,
94
+ duration: seg.duration
95
+ });
96
+ } else {
97
+ chapterAudio.push(null);
98
+ }
99
+ }
100
+ }
101
+ const chapterFiles = [];
102
+ for (let i = 0; i < chapters.length; i++) {
103
+ const chap = chapters[i];
104
+ const num = String(i + 1).padStart(3, "0");
105
+ const id = `chapter-${num}`;
106
+ const filename = `${id}.xhtml`;
107
+ const audioInfo = chapterAudio[i] ?? null;
108
+ const xhtml = renderChapterXhtml(chap.nodes, title, resolvedImages, audioInfo !== null);
109
+ zip.file(`OEBPS/chapters/${filename}`, xhtml);
110
+ let smilFilename;
111
+ if (audioInfo) {
112
+ smilFilename = `${id}.smil`;
113
+ const smil = generateSmil(filename, audioInfo, chap.nodes);
114
+ zip.file(`OEBPS/chapters/${smilFilename}`, smil);
115
+ }
116
+ chapterFiles.push({
117
+ id,
118
+ filename,
119
+ title: chap.title,
120
+ smilFilename,
121
+ duration: audioInfo?.duration
122
+ });
123
+ }
124
+ zip.file("OEBPS/toc.xhtml", generateTocXhtml(chapterFiles, title));
125
+ zip.file(
126
+ "OEBPS/content.opf",
127
+ generateContentOpf({
128
+ uuid,
129
+ title,
130
+ author,
131
+ language,
132
+ description,
133
+ publisher,
134
+ chapters: chapterFiles,
135
+ images: resolvedImages,
136
+ coverFilename,
137
+ audioFiles: allAudioFiles,
138
+ totalDuration: options.totalDuration
139
+ })
140
+ );
141
+ const blob = await zip.generateAsync({
142
+ type: "arraybuffer",
143
+ compression: "DEFLATE",
144
+ compressionOptions: { level: 6 }
145
+ // mimetype was already set to STORE above; JSZip respects per-file options
146
+ });
147
+ return blob;
148
+ }
149
+ async function docToEpub(doc, options = {}) {
150
+ const { docToMarkdown } = await import("@bendyline/squisq/doc");
151
+ const markdownDoc = docToMarkdown(doc);
152
+ const epubOptions = { ...options };
153
+ if (doc.audio?.segments?.length && !epubOptions.audioSegments) {
154
+ epubOptions.audioSegments = doc.audio.segments;
155
+ }
156
+ if (doc.duration && !epubOptions.totalDuration) {
157
+ epubOptions.totalDuration = doc.duration;
158
+ }
159
+ return markdownDocToEpub(markdownDoc, epubOptions);
160
+ }
161
+ function splitIntoChapters(nodes) {
162
+ const chapters = [];
163
+ let currentNodes = [];
164
+ let currentTitle = "Untitled";
165
+ for (const node of nodes) {
166
+ if (node.type === "heading" && node.depth <= 2) {
167
+ if (currentNodes.length > 0) {
168
+ chapters.push({ title: currentTitle, nodes: currentNodes });
169
+ }
170
+ currentTitle = extractHeadingText(node);
171
+ currentNodes = [node];
172
+ } else {
173
+ currentNodes.push(node);
174
+ }
175
+ }
176
+ if (currentNodes.length > 0) {
177
+ chapters.push({ title: currentTitle, nodes: currentNodes });
178
+ }
179
+ if (chapters.length === 0) {
180
+ chapters.push({ title: "Untitled", nodes: [] });
181
+ }
182
+ return chapters;
183
+ }
184
+ function extractHeadingText(heading) {
185
+ return heading.children.map(inlineToText).join("");
186
+ }
187
+ function inlineToText(node) {
188
+ switch (node.type) {
189
+ case "text":
190
+ return node.value;
191
+ case "emphasis":
192
+ case "strong":
193
+ case "delete":
194
+ return node.children.map(inlineToText).join("");
195
+ case "inlineCode":
196
+ return node.value;
197
+ case "link":
198
+ return node.children.map(inlineToText).join("");
199
+ case "image":
200
+ return node.alt ?? "";
201
+ case "break":
202
+ return " ";
203
+ default:
204
+ return "";
205
+ }
206
+ }
207
+ function collectDocImages(nodes) {
208
+ const images = /* @__PURE__ */ new Set();
209
+ function walkBlock(node) {
210
+ switch (node.type) {
211
+ case "paragraph":
212
+ case "heading":
213
+ node.children.forEach(walkInline);
214
+ break;
215
+ case "blockquote":
216
+ node.children.forEach(walkBlock);
217
+ break;
218
+ case "list":
219
+ node.children.forEach((item) => item.children.forEach(walkBlock));
220
+ break;
221
+ case "table":
222
+ node.children.forEach(
223
+ (row) => row.children.forEach((cell) => cell.children.forEach(walkInline))
224
+ );
225
+ break;
226
+ default:
227
+ break;
228
+ }
229
+ }
230
+ function walkInline(node) {
231
+ if (node.type === "image" && node.url && !node.url.startsWith("data:") && !node.url.startsWith("http")) {
232
+ images.add(node.url);
233
+ }
234
+ if ("children" in node && Array.isArray(node.children)) {
235
+ node.children.forEach(walkInline);
236
+ }
237
+ }
238
+ nodes.forEach(walkBlock);
239
+ return images;
240
+ }
241
+ function renderChapterXhtml(nodes, bookTitle, images, addIds = false) {
242
+ let elementCounter = 0;
243
+ const nextId = () => `p${++elementCounter}`;
244
+ const body = nodes.map((n) => blockToXhtml(n, images, addIds ? nextId : void 0)).join("\n");
245
+ return `<?xml version="1.0" encoding="UTF-8"?>
246
+ <!DOCTYPE html>
247
+ <html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops">
248
+ <head>
249
+ <meta charset="UTF-8"/>
250
+ <title>${escapeXml(bookTitle)}</title>
251
+ <link rel="stylesheet" type="text/css" href="../styles.css"/>
252
+ </head>
253
+ <body>
254
+ ${body}
255
+ </body>
256
+ </html>`;
257
+ }
258
+ function blockToXhtml(node, images, nextId) {
259
+ const idAttr = nextId ? ` id="${nextId()}"` : "";
260
+ switch (node.type) {
261
+ case "heading": {
262
+ const tag = `h${node.depth}`;
263
+ return `<${tag}${idAttr}>${inlinesToXhtml(node.children, images)}</${tag}>`;
264
+ }
265
+ case "paragraph":
266
+ return `<p${idAttr}>${inlinesToXhtml(node.children, images)}</p>`;
267
+ case "blockquote":
268
+ return `<blockquote${idAttr}>
269
+ ${node.children.map((c) => blockToXhtml(c, images, nextId)).join("\n")}
270
+ </blockquote>`;
271
+ case "list": {
272
+ const tag = node.ordered ? "ol" : "ul";
273
+ const startAttr = node.ordered && node.start && node.start !== 1 ? ` start="${node.start}"` : "";
274
+ const items = node.children.map((item) => listItemToXhtml(item, images)).join("\n");
275
+ return `<${tag}${idAttr}${startAttr}>
276
+ ${items}
277
+ </${tag}>`;
278
+ }
279
+ case "code": {
280
+ const langAttr = node.lang ? ` class="language-${escapeXml(node.lang)}"` : "";
281
+ return `<pre${idAttr}><code${langAttr}>${escapeXml(node.value)}</code></pre>`;
282
+ }
283
+ case "thematicBreak":
284
+ return `<hr${idAttr}/>`;
285
+ case "table":
286
+ return tableToXhtml(node, images, idAttr);
287
+ case "htmlBlock":
288
+ return `<p${idAttr}>${escapeXml(node.rawHtml.replace(/<[^>]+>/g, ""))}</p>`;
289
+ case "math":
290
+ return `<p${idAttr} class="math">${escapeXml(node.value)}</p>`;
291
+ default:
292
+ return "";
293
+ }
294
+ }
295
+ function listItemToXhtml(item, images) {
296
+ const content = item.children.map((c) => blockToXhtml(c, images)).join("\n");
297
+ const unwrapped = item.children.length === 1 && item.children[0].type === "paragraph" ? inlinesToXhtml(item.children[0].children, images) : content;
298
+ return `<li>${unwrapped}</li>`;
299
+ }
300
+ function tableToXhtml(table, images, idAttr = "") {
301
+ const rows = table.children;
302
+ if (rows.length === 0) return `<table${idAttr}></table>`;
303
+ const headerRow = rows[0];
304
+ const bodyRows = rows.slice(1);
305
+ const align = table.align ?? [];
306
+ function cellToXhtml(cell, tag, colIndex) {
307
+ const a = align[colIndex];
308
+ const style = a ? ` style="text-align: ${a}"` : "";
309
+ return `<${tag}${style}>${inlinesToXhtml(cell.children, images)}</${tag}>`;
310
+ }
311
+ const thead = `<thead><tr>${headerRow.children.map((c, i) => cellToXhtml(c, "th", i)).join("")}</tr></thead>`;
312
+ const tbody = bodyRows.length > 0 ? `<tbody>${bodyRows.map((row) => `<tr>${row.children.map((c, i) => cellToXhtml(c, "td", i)).join("")}</tr>`).join("")}</tbody>` : "";
313
+ return `<table${idAttr}>${thead}${tbody}</table>`;
314
+ }
315
+ function inlinesToXhtml(nodes, images) {
316
+ return nodes.map((n) => inlineToXhtml(n, images)).join("");
317
+ }
318
+ function inlineToXhtml(node, images) {
319
+ switch (node.type) {
320
+ case "text":
321
+ return escapeXml(node.value);
322
+ case "strong":
323
+ return `<strong>${inlinesToXhtml(node.children, images)}</strong>`;
324
+ case "emphasis":
325
+ return `<em>${inlinesToXhtml(node.children, images)}</em>`;
326
+ case "delete":
327
+ return `<del>${inlinesToXhtml(node.children, images)}</del>`;
328
+ case "inlineCode":
329
+ return `<code>${escapeXml(node.value)}</code>`;
330
+ case "link": {
331
+ const titleAttr = node.title ? ` title="${escapeXml(node.title)}"` : "";
332
+ return `<a href="${escapeXml(node.url)}"${titleAttr}>${inlinesToXhtml(node.children, images)}</a>`;
333
+ }
334
+ case "image": {
335
+ const alt = escapeXml(node.alt ?? "");
336
+ const resolved = images.get(node.url);
337
+ const src = resolved ? `../images/${resolved.filename}` : escapeXml(node.url);
338
+ return `<img src="${src}" alt="${alt}"/>`;
339
+ }
340
+ case "break":
341
+ return "<br/>";
342
+ case "inlineMath":
343
+ return `<span class="math">${escapeXml(node.value)}</span>`;
344
+ case "htmlInline":
345
+ return escapeXml(node.rawHtml.replace(/<[^>]+>/g, ""));
346
+ default:
347
+ return "";
348
+ }
349
+ }
350
+ function generateSmil(chapterFilename, audioInfo, nodes) {
351
+ let elementCount = 0;
352
+ function countBlocks(node) {
353
+ elementCount++;
354
+ if (node.type === "blockquote") node.children.forEach(countBlocks);
355
+ }
356
+ nodes.forEach(countBlocks);
357
+ if (elementCount === 0) elementCount = 1;
358
+ const clipDuration = audioInfo.duration / elementCount;
359
+ const pars = [];
360
+ for (let i = 0; i < elementCount; i++) {
361
+ const clipStart = formatTime(audioInfo.clipStart + i * clipDuration, true);
362
+ const clipEnd = formatTime(audioInfo.clipStart + (i + 1) * clipDuration, true);
363
+ pars.push(
364
+ ` <par id="par-${i + 1}">
365
+ <text src="${chapterFilename}#p${i + 1}"/>
366
+ <audio src="../audio/${escapeXml(audioInfo.audioFilename)}" clipBegin="${clipStart}" clipEnd="${clipEnd}"/>
367
+ </par>`
368
+ );
369
+ }
370
+ return `<?xml version="1.0" encoding="UTF-8"?>
371
+ <smil xmlns="http://www.w3.org/ns/SMIL" xmlns:epub="http://www.idpf.org/2007/ops" version="3.0">
372
+ <body>
373
+ <seq id="seq-1" epub:textref="${chapterFilename}">
374
+ ${pars.join("\n")}
375
+ </seq>
376
+ </body>
377
+ </smil>`;
378
+ }
379
+ function formatTime(seconds, fractional = false) {
380
+ const h = Math.floor(seconds / 3600);
381
+ const m = Math.floor(seconds % 3600 / 60);
382
+ const s = seconds % 60;
383
+ const sPart = fractional ? s.toFixed(3).padStart(6, "0") : String(Math.floor(s)).padStart(2, "0");
384
+ return `${h}:${String(m).padStart(2, "0")}:${sPart}`;
385
+ }
386
+ var CONTAINER_XML = `<?xml version="1.0" encoding="UTF-8"?>
387
+ <container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container">
388
+ <rootfiles>
389
+ <rootfile full-path="OEBPS/content.opf" media-type="application/oebps-package+xml"/>
390
+ </rootfiles>
391
+ </container>`;
392
+ function generateContentOpf(params) {
393
+ const {
394
+ uuid,
395
+ title,
396
+ author,
397
+ language,
398
+ description,
399
+ publisher,
400
+ chapters,
401
+ images,
402
+ coverFilename,
403
+ audioFiles,
404
+ totalDuration
405
+ } = params;
406
+ const modified = (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d+Z$/, "Z");
407
+ const hasOverlays = chapters.some((c) => c.smilFilename);
408
+ const manifestItems = [
409
+ ' <item id="toc" href="toc.xhtml" media-type="application/xhtml+xml" properties="nav"/>',
410
+ ' <item id="css" href="styles.css" media-type="text/css"/>'
411
+ ];
412
+ if (coverFilename) {
413
+ const mime = inferMimeType(coverFilename);
414
+ manifestItems.push(
415
+ ` <item id="cover-image" href="images/${escapeXml(coverFilename)}" media-type="${mime}" properties="cover-image"/>`
416
+ );
417
+ }
418
+ for (const chap of chapters) {
419
+ const overlayAttr = chap.smilFilename ? ` media-overlay="${chap.id}-overlay"` : "";
420
+ manifestItems.push(
421
+ ` <item id="${chap.id}" href="chapters/${escapeXml(chap.filename)}" media-type="application/xhtml+xml"${overlayAttr}/>`
422
+ );
423
+ if (chap.smilFilename) {
424
+ manifestItems.push(
425
+ ` <item id="${chap.id}-overlay" href="chapters/${escapeXml(chap.smilFilename)}" media-type="application/smil+xml"/>`
426
+ );
427
+ }
428
+ }
429
+ if (audioFiles) {
430
+ const usedAudioNames = /* @__PURE__ */ new Set();
431
+ for (const af of audioFiles) {
432
+ if (usedAudioNames.has(af.filename)) continue;
433
+ usedAudioNames.add(af.filename);
434
+ const audioId = `audio-${af.filename.replace(/[^a-zA-Z0-9]/g, "-")}`;
435
+ manifestItems.push(
436
+ ` <item id="${audioId}" href="audio/${escapeXml(af.filename)}" media-type="${af.mime}"/>`
437
+ );
438
+ }
439
+ }
440
+ const usedFilenames = /* @__PURE__ */ new Set();
441
+ for (const [, img] of images) {
442
+ if (usedFilenames.has(img.filename)) continue;
443
+ usedFilenames.add(img.filename);
444
+ const imgId = `img-${img.filename.replace(/[^a-zA-Z0-9]/g, "-")}`;
445
+ manifestItems.push(
446
+ ` <item id="${imgId}" href="images/${escapeXml(img.filename)}" media-type="${img.mime}"/>`
447
+ );
448
+ }
449
+ const spineItems = chapters.map((chap) => ` <itemref idref="${chap.id}"/>`).join("\n");
450
+ const metaParts = [
451
+ ` <dc:identifier id="uid">urn:uuid:${uuid}</dc:identifier>`,
452
+ ` <dc:title>${escapeXml(title)}</dc:title>`,
453
+ ` <dc:language>${escapeXml(language)}</dc:language>`,
454
+ ` <meta property="dcterms:modified">${modified}</meta>`
455
+ ];
456
+ if (author) metaParts.push(` <dc:creator>${escapeXml(author)}</dc:creator>`);
457
+ if (description) metaParts.push(` <dc:description>${escapeXml(description)}</dc:description>`);
458
+ if (publisher) metaParts.push(` <dc:publisher>${escapeXml(publisher)}</dc:publisher>`);
459
+ if (hasOverlays && totalDuration) {
460
+ metaParts.push(` <meta property="media:duration">${formatTime(totalDuration)}</meta>`);
461
+ metaParts.push(' <meta property="media:active-class">epub-media-overlay-active</meta>');
462
+ }
463
+ return `<?xml version="1.0" encoding="UTF-8"?>
464
+ <package xmlns="http://www.idpf.org/2007/opf" version="3.0" unique-identifier="uid">
465
+ <metadata xmlns:dc="http://purl.org/dc/elements/1.1/">
466
+ ${metaParts.join("\n")}
467
+ </metadata>
468
+ <manifest>
469
+ ${manifestItems.join("\n")}
470
+ </manifest>
471
+ <spine>
472
+ ${spineItems}
473
+ </spine>
474
+ </package>`;
475
+ }
476
+ function generateTocXhtml(chapters, bookTitle) {
477
+ const navItems = chapters.map(
478
+ (chap) => ` <li><a href="chapters/${escapeXml(chap.filename)}">${escapeXml(chap.title)}</a></li>`
479
+ ).join("\n");
480
+ return `<?xml version="1.0" encoding="UTF-8"?>
481
+ <!DOCTYPE html>
482
+ <html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops">
483
+ <head>
484
+ <meta charset="UTF-8"/>
485
+ <title>${escapeXml(bookTitle)}</title>
486
+ <link rel="stylesheet" type="text/css" href="styles.css"/>
487
+ </head>
488
+ <body>
489
+ <nav epub:type="toc">
490
+ <h1>Table of Contents</h1>
491
+ <ol>
492
+ ${navItems}
493
+ </ol>
494
+ </nav>
495
+ </body>
496
+ </html>`;
497
+ }
498
+ function generateStylesheet(themeId) {
499
+ let themeVars = "";
500
+ if (themeId) {
501
+ const theme = resolveTheme(themeId);
502
+ themeVars = `
503
+ --epub-bg: ${theme.colors.background};
504
+ --epub-text: ${theme.colors.text};
505
+ --epub-primary: ${theme.colors.primary};
506
+ --epub-heading-font: ${theme.typography.titleFontFamily};
507
+ --epub-body-font: ${theme.typography.bodyFontFamily};`;
508
+ }
509
+ return `/* Squisq EPUB Stylesheet */
510
+ :root {${themeVars}
511
+ }
512
+
513
+ body {
514
+ font-family: var(--epub-body-font, Georgia, 'Times New Roman', serif);
515
+ color: var(--epub-text, #1a1a1a);
516
+ line-height: 1.7;
517
+ margin: 1em 2em;
518
+ max-width: 40em;
519
+ }
520
+
521
+ h1, h2, h3, h4, h5, h6 {
522
+ font-family: var(--epub-heading-font, system-ui, sans-serif);
523
+ color: var(--epub-primary, #1a1a1a);
524
+ margin-top: 1.5em;
525
+ margin-bottom: 0.5em;
526
+ line-height: 1.3;
527
+ }
528
+
529
+ h1 { font-size: 2em; }
530
+ h2 { font-size: 1.5em; }
531
+ h3 { font-size: 1.25em; }
532
+
533
+ p {
534
+ margin: 0.8em 0;
535
+ }
536
+
537
+ a {
538
+ color: var(--epub-primary, #2563eb);
539
+ }
540
+
541
+ img {
542
+ max-width: 100%;
543
+ height: auto;
544
+ }
545
+
546
+ pre {
547
+ background: #f5f5f5;
548
+ padding: 1em;
549
+ overflow-x: auto;
550
+ border-radius: 4px;
551
+ font-size: 0.9em;
552
+ line-height: 1.4;
553
+ }
554
+
555
+ code {
556
+ font-family: 'Courier New', Courier, monospace;
557
+ font-size: 0.9em;
558
+ }
559
+
560
+ p > code, li > code {
561
+ background: #f0f0f0;
562
+ padding: 0.1em 0.3em;
563
+ border-radius: 3px;
564
+ }
565
+
566
+ blockquote {
567
+ border-left: 3px solid var(--epub-primary, #d1d5db);
568
+ margin: 1em 0;
569
+ padding: 0.5em 1em;
570
+ color: #4b5563;
571
+ }
572
+
573
+ table {
574
+ border-collapse: collapse;
575
+ width: 100%;
576
+ margin: 1em 0;
577
+ }
578
+
579
+ th, td {
580
+ border: 1px solid #d1d5db;
581
+ padding: 0.5em 0.75em;
582
+ text-align: left;
583
+ }
584
+
585
+ th {
586
+ background: #f3f4f6;
587
+ font-weight: 600;
588
+ }
589
+
590
+ hr {
591
+ border: none;
592
+ border-top: 1px solid #d1d5db;
593
+ margin: 2em 0;
594
+ }
595
+
596
+ ul, ol {
597
+ margin: 0.8em 0;
598
+ padding-left: 1.5em;
599
+ }
600
+
601
+ li {
602
+ margin: 0.3em 0;
603
+ }
604
+
605
+ .math {
606
+ font-family: 'Courier New', Courier, monospace;
607
+ font-style: italic;
608
+ }
609
+
610
+ /* Media Overlay active highlight (narration sync) */
611
+ .epub-media-overlay-active {
612
+ background-color: rgba(37, 99, 235, 0.12);
613
+ }
614
+ `;
615
+ }
616
+
617
+ export {
618
+ markdownDocToEpub,
619
+ docToEpub
620
+ };
621
+ //# sourceMappingURL=chunk-2FPJKJ6I.js.map