@bendyline/squisq-formats 2.0.1 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (195) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE.md +20 -0
  3. package/README.md +1 -1
  4. package/dist/{chunk-CRAVSMPZ.js → chunk-26ISNJ7Y.js} +356 -114
  5. package/dist/{chunk-NKAJPJ4G.js → chunk-2JJ5RFDZ.js} +0 -1
  6. package/dist/{chunk-HTW2M27H.js → chunk-3NKXBZSR.js} +193 -42
  7. package/dist/{chunk-U32AG3G3.js → chunk-4V3KCHAP.js} +3 -4
  8. package/dist/{chunk-MLX2BOJC.js → chunk-6RQOV3B3.js} +1 -2
  9. package/dist/{chunk-QRVN6A6E.js → chunk-6S6GU3ZG.js} +5 -6
  10. package/dist/{chunk-FE6OJV6O.js → chunk-7AWFHP5U.js} +1 -1
  11. package/dist/{chunk-RFAPOKHJ.js → chunk-AD2WT564.js} +59 -9
  12. package/dist/{chunk-O3GVVND4.js → chunk-AONELFLA.js} +0 -1
  13. package/dist/{chunk-XKUMNGBW.js → chunk-EJTNGKEA.js} +5 -8
  14. package/dist/chunk-GX7RAUME.js +121 -0
  15. package/dist/{chunk-SSUPBUF5.js → chunk-IIQYS2YH.js} +0 -1
  16. package/dist/{chunk-ABVI556T.js → chunk-IPN56VLW.js} +83 -58
  17. package/dist/{chunk-LXYLOOST.js → chunk-JE6LSIHE.js} +83 -22
  18. package/dist/{chunk-U4MRIFKL.js → chunk-JU2RHXUB.js} +0 -1
  19. package/dist/{chunk-4VUWTSGM.js → chunk-K6XRMVPW.js} +64 -31
  20. package/dist/{chunk-ODL3SSPT.js → chunk-KXOZMWBS.js} +0 -1
  21. package/dist/chunk-OGS5VCGJ.js +446 -0
  22. package/dist/{chunk-GVS2XXV6.js → chunk-PJXJI2LY.js} +449 -57
  23. package/dist/{chunk-2KPARF2P.js → chunk-PU7REGWV.js} +5 -8
  24. package/dist/{chunk-PN52A5AA.js → chunk-SBUW7NHR.js} +0 -1
  25. package/dist/{chunk-VSYHZECT.js → chunk-TAAENIRB.js} +5 -8
  26. package/dist/{chunk-WC7WULGV.js → chunk-X2DEAXNK.js} +62 -2
  27. package/dist/container/index.js +1 -2
  28. package/dist/csv/index.d.ts +27 -2
  29. package/dist/csv/index.js +1 -2
  30. package/dist/docx/index.d.ts +5 -1
  31. package/dist/docx/index.js +9 -10
  32. package/dist/epub/index.d.ts +2 -0
  33. package/dist/epub/index.js +5 -6
  34. package/dist/{export-D2NkylDT.d.ts → export-D9msROJS.d.ts} +18 -6
  35. package/dist/extract-MN7LA3NL.js +13 -0
  36. package/dist/html/index.d.ts +11 -4
  37. package/dist/html/index.js +3 -4
  38. package/dist/images-ESPQKVTW.js +6 -0
  39. package/dist/{import-K8mfc0fz.d.ts → import-C3htUTss.d.ts} +5 -1
  40. package/dist/{import-DTkDxHmZ.d.ts → import-C8whCC7_.d.ts} +6 -0
  41. package/dist/index.d.ts +7 -7
  42. package/dist/index.js +28 -26
  43. package/dist/infer/index.d.ts +3 -3
  44. package/dist/infer/index.js +7 -9
  45. package/dist/{layouts-BHrgZ5FS.d.ts → layouts-CTdPlB-u.d.ts} +1 -1
  46. package/dist/layouts-DRWZGSPD.js +10 -0
  47. package/dist/{mapTheme-IR27S6IV.js → mapTheme-4TWH25FT.js} +1 -2
  48. package/dist/ooxml/index.d.ts +3 -3
  49. package/dist/ooxml/index.js +14 -13
  50. package/dist/pdf/index.d.ts +18 -0
  51. package/dist/pdf/index.js +2 -3
  52. package/dist/pptx/index.d.ts +4 -4
  53. package/dist/pptx/index.js +11 -13
  54. package/dist/{reader-B9L8Ucbj.d.ts → reader-B_m1aKZC.d.ts} +30 -1
  55. package/dist/registry/index.d.ts +21 -5
  56. package/dist/registry/index.js +9 -6
  57. package/dist/{themeReader-DJKErl_j.d.ts → themeReader-DCtwC83Q.d.ts} +1 -1
  58. package/dist/xlsx/index.d.ts +3 -3
  59. package/dist/xlsx/index.js +6 -7
  60. package/package.json +6 -3
  61. package/dist/chunk-2KPARF2P.js.map +0 -1
  62. package/dist/chunk-4VUWTSGM.js.map +0 -1
  63. package/dist/chunk-6M7Z25LA.js +0 -46
  64. package/dist/chunk-6M7Z25LA.js.map +0 -1
  65. package/dist/chunk-ABVI556T.js.map +0 -1
  66. package/dist/chunk-CRAVSMPZ.js.map +0 -1
  67. package/dist/chunk-FE6OJV6O.js.map +0 -1
  68. package/dist/chunk-GVS2XXV6.js.map +0 -1
  69. package/dist/chunk-HTW2M27H.js.map +0 -1
  70. package/dist/chunk-LXYLOOST.js.map +0 -1
  71. package/dist/chunk-MLX2BOJC.js.map +0 -1
  72. package/dist/chunk-NKAJPJ4G.js.map +0 -1
  73. package/dist/chunk-O3GVVND4.js.map +0 -1
  74. package/dist/chunk-ODL3SSPT.js.map +0 -1
  75. package/dist/chunk-PN52A5AA.js.map +0 -1
  76. package/dist/chunk-QRVN6A6E.js.map +0 -1
  77. package/dist/chunk-RFAPOKHJ.js.map +0 -1
  78. package/dist/chunk-SSUPBUF5.js.map +0 -1
  79. package/dist/chunk-U32AG3G3.js.map +0 -1
  80. package/dist/chunk-U4MRIFKL.js.map +0 -1
  81. package/dist/chunk-VJJM2SSH.js +0 -275
  82. package/dist/chunk-VJJM2SSH.js.map +0 -1
  83. package/dist/chunk-VSYHZECT.js.map +0 -1
  84. package/dist/chunk-WC7WULGV.js.map +0 -1
  85. package/dist/chunk-XKUMNGBW.js.map +0 -1
  86. package/dist/chunk-YRT7GQ5Y.js +0 -28
  87. package/dist/chunk-YRT7GQ5Y.js.map +0 -1
  88. package/dist/container/index.js.map +0 -1
  89. package/dist/csv/index.js.map +0 -1
  90. package/dist/docx/index.js.map +0 -1
  91. package/dist/epub/index.js.map +0 -1
  92. package/dist/extract-H6RXJMHP.js +0 -15
  93. package/dist/extract-H6RXJMHP.js.map +0 -1
  94. package/dist/html/index.js.map +0 -1
  95. package/dist/images-7FBWPKE3.js +0 -7
  96. package/dist/images-7FBWPKE3.js.map +0 -1
  97. package/dist/index.js.map +0 -1
  98. package/dist/infer/index.js.map +0 -1
  99. package/dist/layouts-QVPK3ZCU.js +0 -12
  100. package/dist/layouts-QVPK3ZCU.js.map +0 -1
  101. package/dist/mapTheme-IR27S6IV.js.map +0 -1
  102. package/dist/ooxml/index.js.map +0 -1
  103. package/dist/pdf/index.js.map +0 -1
  104. package/dist/pptx/index.js.map +0 -1
  105. package/dist/registry/index.js.map +0 -1
  106. package/dist/xlsx/index.js.map +0 -1
  107. package/src/__tests__/container.test.ts +0 -230
  108. package/src/__tests__/convert.test.ts +0 -495
  109. package/src/__tests__/csvImport.test.ts +0 -84
  110. package/src/__tests__/docxExport.test.ts +0 -457
  111. package/src/__tests__/docxImport.test.ts +0 -410
  112. package/src/__tests__/epub.test.ts +0 -649
  113. package/src/__tests__/exportThemeReconciliation.test.ts +0 -87
  114. package/src/__tests__/formatRegistry.test.ts +0 -174
  115. package/src/__tests__/html.test.ts +0 -435
  116. package/src/__tests__/htmlImport.test.ts +0 -57
  117. package/src/__tests__/inferTheme.test.ts +0 -135
  118. package/src/__tests__/lossyWarnings.test.ts +0 -146
  119. package/src/__tests__/ooxml.test.ts +0 -271
  120. package/src/__tests__/ooxmlCancellation.test.ts +0 -113
  121. package/src/__tests__/ooxmlThemeReader.test.ts +0 -92
  122. package/src/__tests__/pdfExport.test.ts +0 -322
  123. package/src/__tests__/pdfImport.test.ts +0 -384
  124. package/src/__tests__/plainHtml.test.ts +0 -417
  125. package/src/__tests__/plainHtmlBundle.test.ts +0 -253
  126. package/src/__tests__/pptxExport.test.ts +0 -138
  127. package/src/__tests__/pptxImport.test.ts +0 -145
  128. package/src/__tests__/pptxInferFixtures.ts +0 -314
  129. package/src/__tests__/pptxLayoutInfer.test.ts +0 -395
  130. package/src/__tests__/roundTrip.test.ts +0 -201
  131. package/src/__tests__/roundTripAssets.test.ts +0 -50
  132. package/src/__tests__/roundTripMatrix.fixtures.ts +0 -86
  133. package/src/__tests__/roundTripMatrix.helpers.ts +0 -154
  134. package/src/__tests__/roundTripMatrix.test.ts +0 -142
  135. package/src/__tests__/sharedContainer.test.ts +0 -41
  136. package/src/__tests__/sharedImages.test.ts +0 -61
  137. package/src/__tests__/xlsxExport.test.ts +0 -164
  138. package/src/__tests__/xlsxImport.test.ts +0 -80
  139. package/src/__tests__/zipSafety.test.ts +0 -317
  140. package/src/container/index.ts +0 -94
  141. package/src/csv/index.ts +0 -188
  142. package/src/docx/export.ts +0 -1267
  143. package/src/docx/import.ts +0 -995
  144. package/src/docx/index.ts +0 -26
  145. package/src/docx/styles.ts +0 -145
  146. package/src/epub/export.ts +0 -968
  147. package/src/epub/index.ts +0 -20
  148. package/src/html/docsHtmlBundle.ts +0 -373
  149. package/src/html/htmlTemplate.ts +0 -385
  150. package/src/html/imageUtils.ts +0 -61
  151. package/src/html/import.ts +0 -297
  152. package/src/html/index.ts +0 -212
  153. package/src/html/plainHtml.ts +0 -790
  154. package/src/html/plainHtmlBundle.ts +0 -421
  155. package/src/index.ts +0 -109
  156. package/src/infer/extract.ts +0 -127
  157. package/src/infer/index.ts +0 -199
  158. package/src/infer/mapTheme.ts +0 -176
  159. package/src/infer/types.ts +0 -27
  160. package/src/ooxml/index.ts +0 -111
  161. package/src/ooxml/namespaces.ts +0 -196
  162. package/src/ooxml/readUtils.ts +0 -44
  163. package/src/ooxml/reader.ts +0 -318
  164. package/src/ooxml/themeReader.ts +0 -197
  165. package/src/ooxml/types.ts +0 -103
  166. package/src/ooxml/writer.ts +0 -339
  167. package/src/ooxml/xmlUtils.ts +0 -123
  168. package/src/pdf/export.ts +0 -1084
  169. package/src/pdf/import.ts +0 -1164
  170. package/src/pdf/index.ts +0 -29
  171. package/src/pdf/styles.ts +0 -180
  172. package/src/pptx/export.ts +0 -1184
  173. package/src/pptx/import.ts +0 -455
  174. package/src/pptx/index.ts +0 -52
  175. package/src/pptx/layouts.ts +0 -1222
  176. package/src/pptx/styles.ts +0 -96
  177. package/src/pptx/templates.ts +0 -187
  178. package/src/registry/convert.ts +0 -433
  179. package/src/registry/defaultFormats.ts +0 -413
  180. package/src/registry/errors.ts +0 -46
  181. package/src/registry/index.ts +0 -43
  182. package/src/registry/registry.ts +0 -48
  183. package/src/registry/types.ts +0 -170
  184. package/src/shared/boundedZipArchive.ts +0 -383
  185. package/src/shared/container.ts +0 -28
  186. package/src/shared/fidelity.ts +0 -130
  187. package/src/shared/images.ts +0 -44
  188. package/src/shared/inlineRuns.ts +0 -99
  189. package/src/shared/text.ts +0 -41
  190. package/src/shared/zipEntryCount.ts +0 -151
  191. package/src/shared/zipLimits.ts +0 -296
  192. package/src/shared/zipSafety.ts +0 -19
  193. package/src/xlsx/export.ts +0 -253
  194. package/src/xlsx/import.ts +0 -160
  195. package/src/xlsx/index.ts +0 -35
@@ -2,7 +2,7 @@ import {
2
2
  arrayBufferToBase64DataUrl,
3
3
  extractFilename,
4
4
  inferMimeType
5
- } from "./chunk-MLX2BOJC.js";
5
+ } from "./chunk-6RQOV3B3.js";
6
6
 
7
7
  // src/html/index.ts
8
8
  import JSZip3 from "jszip";
@@ -119,7 +119,23 @@ function scanTemplateImageFields(obj, paths) {
119
119
  }
120
120
  }
121
121
  function escapeForScript(str) {
122
- return str.replace(/<\/(script)/gi, "<\\/$1");
122
+ return str.replace(/<(?=!--|\/?script)/gi, "\\u003c");
123
+ }
124
+ function buildInlineImageMaps(images) {
125
+ const imageMap = {};
126
+ const imageAliases = {};
127
+ if (!images) return { imageMap, imageAliases };
128
+ const canonicalKeyFor = /* @__PURE__ */ new Map();
129
+ for (const [path, buffer] of images.entries()) {
130
+ const canonical = canonicalKeyFor.get(buffer);
131
+ if (canonical !== void 0) {
132
+ imageAliases[path] = canonical;
133
+ continue;
134
+ }
135
+ canonicalKeyFor.set(buffer, path);
136
+ imageMap[path] = arrayBufferToBase64DataUrl(buffer, inferMimeType(path));
137
+ }
138
+ return { imageMap, imageAliases };
123
139
  }
124
140
  function generateInlineHtml(doc, options) {
125
141
  const {
@@ -132,15 +148,10 @@ function generateInlineHtml(doc, options) {
132
148
  themeRegistry
133
149
  } = options;
134
150
  doc = applyThemeSelection(doc, themeId, themeRegistry);
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
- }
151
+ const { imageMap, imageAliases } = buildInlineImageMaps(images);
142
152
  const docJson = escapeForScript(JSON.stringify(doc));
143
153
  const imageMapJson = escapeForScript(JSON.stringify(imageMap));
154
+ const imageAliasJson = escapeForScript(JSON.stringify(imageAliases));
144
155
  return `<!DOCTYPE html>
145
156
  <html lang="en">
146
157
  <head>
@@ -151,7 +162,7 @@ function generateInlineHtml(doc, options) {
151
162
  *,*::before,*::after{box-sizing:border-box}
152
163
  html,body{margin:0;padding:0;height:100%;background:#1a1a2e;color:#e0e0e0;font-family:system-ui,-apple-system,sans-serif}
153
164
  #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}" : ""}
165
+ ${mode === "static" ? "#squisq-root{display:block}" : ""}
155
166
  </style>
156
167
  </head>
157
168
  <body>
@@ -161,6 +172,14 @@ ${mode === "static" ? "#squisq-root{align-items:flex-start;overflow-y:auto;backg
161
172
  (function(){
162
173
  var doc = JSON.parse(${JSON.stringify(docJson)});
163
174
  var images = JSON.parse(${JSON.stringify(imageMapJson)});
175
+ // Re-expand path/basename aliases that share a payload (see
176
+ // buildInlineImageMaps) so \`images\` has every key the resolver may ask for.
177
+ var aliases = JSON.parse(${JSON.stringify(imageAliasJson)});
178
+ for (var alias in aliases) {
179
+ if (Object.prototype.hasOwnProperty.call(aliases, alias)) {
180
+ images[alias] = images[aliases[alias]];
181
+ }
182
+ }
164
183
  SquisqPlayer.mount(document.getElementById("squisq-root"), doc, {
165
184
  mode: ${JSON.stringify(mode)},
166
185
  images: images,
@@ -197,7 +216,7 @@ function generateExternalHtml(doc, options) {
197
216
  *,*::before,*::after{box-sizing:border-box}
198
217
  html,body{margin:0;padding:0;height:100%;background:#1a1a2e;color:#e0e0e0;font-family:system-ui,-apple-system,sans-serif}
199
218
  #squisq-root{width:100%;height:100%;display:flex;align-items:center;justify-content:center}
200
- ${mode === "static" ? "#squisq-root{align-items:flex-start;overflow-y:auto;background:#fff;color:#1f2937}" : ""}
219
+ ${mode === "static" ? "#squisq-root{display:block}" : ""}
201
220
  </style>
202
221
  </head>
203
222
  <body>
@@ -248,21 +267,31 @@ import {
248
267
  import { resolveFontFamily, buildGoogleFontsUrl } from "@bendyline/squisq/schemas";
249
268
  import { resolveThemeForDoc } from "@bendyline/squisq/doc";
250
269
  function markdownDocToPlainHtml(doc, options = {}) {
251
- const { title = "Document", images, links, themeId, iconsCss, htmlPolicy = "sanitize" } = options;
270
+ const {
271
+ title = "Document",
272
+ images,
273
+ links,
274
+ themeId,
275
+ iconsCss,
276
+ htmlPolicy = "sanitize",
277
+ externalResources = "deny"
278
+ } = options;
252
279
  const resolveId = themeId ?? readFrontmatterThemeId2(doc.frontmatter);
253
280
  const theme = options.theme ?? (resolveId ? resolveThemeForDoc(doc, resolveId, options.themeRegistry) : void 0);
254
281
  const ctx = { images, links, htmlPolicy };
255
282
  const body = renderTopLevel(doc.children, ctx);
256
- const fontsLink = theme ? renderFontsLink(theme) : "";
283
+ const fontsLink = theme && externalResources === "allow" ? renderFontsLink(theme) : "";
257
284
  const usesIcons = docUsesIcons(doc);
258
285
  let iconsLink = "";
259
286
  if (usesIcons) {
260
287
  iconsLink = iconsCss ? `<style data-fa-inline>
261
- ${iconsCss}
288
+ ${escapeStyleText(iconsCss)}
262
289
  </style>
263
- ` : FONT_AWESOME_LINK;
290
+ ` : externalResources === "allow" ? FONT_AWESOME_LINK : "";
264
291
  }
265
292
  const themedCss = theme ? renderThemedCss(theme) : DEFAULT_CSS;
293
+ const safeCss = escapeStyleText(`${themedCss}
294
+ ${FEATURE_CSS}`);
266
295
  return `<!DOCTYPE html>
267
296
  <html lang="en">
268
297
  <head>
@@ -270,8 +299,7 @@ ${iconsCss}
270
299
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
271
300
  <title>${escapeHtml2(title)}</title>
272
301
  ${fontsLink}${iconsLink}<style>
273
- ${themedCss}
274
- ${FEATURE_CSS}
302
+ ${safeCss}
275
303
  </style>
276
304
  </head>
277
305
  <body>
@@ -279,7 +307,10 @@ ${body}
279
307
  </body>
280
308
  </html>`;
281
309
  }
282
- 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">
310
+ function escapeStyleText(css) {
311
+ return css.replace(/</g, "\\3c ");
312
+ }
313
+ var FONT_AWESOME_LINK = `<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/7.2.0/css/all.min.css" crossorigin="anonymous" referrerpolicy="no-referrer">
283
314
  `;
284
315
  function docUsesIcons(doc) {
285
316
  function visit(node) {
@@ -1379,6 +1410,14 @@ function toHtmlString(data) {
1379
1410
  return new TextDecoder("utf-8").decode(bytes);
1380
1411
  }
1381
1412
  function htmlToMarkdownDocSync(html, options = {}) {
1413
+ options.signal?.throwIfAborted();
1414
+ const maxInputChars = options.maxInputChars ?? 16 * 1024 * 1024;
1415
+ if (!Number.isSafeInteger(maxInputChars) || maxInputChars < 0) {
1416
+ throw new RangeError("maxInputChars must be a non-negative safe integer");
1417
+ }
1418
+ if (html.length > maxInputChars) {
1419
+ throw new RangeError(`HTML exceeds the ${maxInputChars}-character safety limit`);
1420
+ }
1382
1421
  let nodes = parseHtmlToNodes(html);
1383
1422
  if (options.sanitize !== false) nodes = sanitizeHtmlNodes2(nodes);
1384
1423
  return { type: "document", children: blocksFromNodes(nodes) };
@@ -1387,7 +1426,7 @@ async function htmlToMarkdownDoc(data, options = {}) {
1387
1426
  return htmlToMarkdownDocSync(toHtmlString(data), options);
1388
1427
  }
1389
1428
  function htmlToMarkdown(html, options = {}) {
1390
- return stringifyMarkdown(htmlToMarkdownDocSync(html, options));
1429
+ return stringifyMarkdown(htmlToMarkdownDocSync(html, options), { signal: options.signal });
1391
1430
  }
1392
1431
 
1393
1432
  // src/html/index.ts
@@ -1397,10 +1436,21 @@ function sanitizeZipPath3(path) {
1397
1436
  if (normalized.split("/").some((seg) => seg === "..")) return null;
1398
1437
  return normalized;
1399
1438
  }
1439
+ function uniqueFilename(filename, used) {
1440
+ if (!used.has(filename)) return filename;
1441
+ const dot = filename.lastIndexOf(".");
1442
+ const base = dot > 0 ? filename.slice(0, dot) : filename;
1443
+ const ext = dot > 0 ? filename.slice(dot) : "";
1444
+ let suffix = 2;
1445
+ while (used.has(`${base}-${suffix}${ext}`)) suffix++;
1446
+ return `${base}-${suffix}${ext}`;
1447
+ }
1400
1448
  function docToHtml(doc, options) {
1449
+ options.signal?.throwIfAborted();
1401
1450
  return generateInlineHtml(doc, options);
1402
1451
  }
1403
1452
  async function docToHtmlZip(doc, options) {
1453
+ options.signal?.throwIfAborted();
1404
1454
  const {
1405
1455
  playerScript,
1406
1456
  images,
@@ -1415,18 +1465,30 @@ async function docToHtmlZip(doc, options) {
1415
1465
  zip.file("squisq-player.js", playerScript);
1416
1466
  const imagePathMap = {};
1417
1467
  if (images) {
1468
+ let imageIndex = 0;
1418
1469
  for (const [originalPath, buffer] of images.entries()) {
1470
+ if ((imageIndex++ & 63) === 0) options.signal?.throwIfAborted();
1419
1471
  const zipPath = sanitizeZipPath3(originalPath);
1420
- if (!zipPath) continue;
1472
+ if (!zipPath) {
1473
+ console.warn(
1474
+ `[squisq] docToHtmlZip: skipped image with unsafe path "${originalPath}" \u2014 the exported document will have a broken reference to it.`
1475
+ );
1476
+ continue;
1477
+ }
1421
1478
  zip.file(zipPath, buffer);
1422
1479
  imagePathMap[originalPath] = zipPath;
1423
1480
  }
1424
1481
  }
1425
1482
  const audioPathMap = {};
1426
1483
  if (audio) {
1484
+ let audioIndex = 0;
1485
+ const usedAudioNames = /* @__PURE__ */ new Set();
1427
1486
  for (const [segmentKey, buffer] of audio.entries()) {
1487
+ if ((audioIndex++ & 63) === 0) options.signal?.throwIfAborted();
1428
1488
  const filename = extractFilename(segmentKey);
1429
- const finalName = filename.includes(".") ? filename : `${filename}.mp3`;
1489
+ const named = filename.includes(".") ? filename : `${filename}.mp3`;
1490
+ const finalName = uniqueFilename(named, usedAudioNames);
1491
+ usedAudioNames.add(finalName);
1430
1492
  const zipPath = `audio/${finalName}`;
1431
1493
  zip.file(zipPath, buffer);
1432
1494
  audioPathMap[segmentKey] = zipPath;
@@ -1462,4 +1524,3 @@ export {
1462
1524
  docToHtml,
1463
1525
  docToHtmlZip
1464
1526
  };
1465
- //# sourceMappingURL=chunk-LXYLOOST.js.map
@@ -40,4 +40,3 @@ export {
40
40
  xmlElement,
41
41
  textElement
42
42
  };
43
- //# sourceMappingURL=chunk-U4MRIFKL.js.map
@@ -1,13 +1,13 @@
1
1
  import {
2
2
  extractPlainText
3
- } from "./chunk-PN52A5AA.js";
3
+ } from "./chunk-SBUW7NHR.js";
4
4
  import {
5
5
  escapeXml
6
- } from "./chunk-U4MRIFKL.js";
6
+ } from "./chunk-JU2RHXUB.js";
7
7
  import {
8
8
  extractFilename,
9
9
  inferMimeType
10
- } from "./chunk-MLX2BOJC.js";
10
+ } from "./chunk-6RQOV3B3.js";
11
11
 
12
12
  // src/epub/export.ts
13
13
  import JSZip from "jszip";
@@ -15,6 +15,7 @@ import { resolveFontFamily } from "@bendyline/squisq/schemas";
15
15
  import { resolveThemeForDoc } from "@bendyline/squisq/doc";
16
16
  import { readFrontmatterThemeId, sanitizeUrl } from "@bendyline/squisq/markdown";
17
17
  async function markdownDocToEpub(doc, options = {}) {
18
+ options.signal?.throwIfAborted();
18
19
  const fmTitle = doc.frontmatter?.title;
19
20
  const fmAuthor = doc.frontmatter?.author;
20
21
  const title = options.title ?? (typeof fmTitle === "string" ? fmTitle : "Untitled");
@@ -28,7 +29,9 @@ async function markdownDocToEpub(doc, options = {}) {
28
29
  const resolvedImages = /* @__PURE__ */ new Map();
29
30
  const usedImageNames = /* @__PURE__ */ new Set();
30
31
  if (options.images) {
32
+ let imageIndex = 0;
31
33
  for (const src of imageEntries) {
34
+ if ((imageIndex++ & 63) === 0) options.signal?.throwIfAborted();
32
35
  const data = options.images.get(src);
33
36
  if (data) {
34
37
  const filename = uniqueFilename(safeArchiveBasename(src, "image"), usedImageNames);
@@ -47,6 +50,7 @@ async function markdownDocToEpub(doc, options = {}) {
47
50
  zip.file("META-INF/container.xml", CONTAINER_XML);
48
51
  zip.file("OEBPS/styles.css", css);
49
52
  for (const [, img] of resolvedImages) {
53
+ options.signal?.throwIfAborted();
50
54
  zip.file(`OEBPS/images/${img.filename}`, img.data);
51
55
  }
52
56
  let coverFilename;
@@ -134,12 +138,17 @@ async function markdownDocToEpub(doc, options = {}) {
134
138
  const id = `chapter-${num}`;
135
139
  const filename = `${id}.xhtml`;
136
140
  const audioInfo = chapterAudio[i] ?? null;
137
- const xhtml = renderChapterXhtml(chap.nodes, title, resolvedImages, audioInfo !== null);
141
+ const { xhtml, ids } = renderChapterXhtml(
142
+ chap.nodes,
143
+ title,
144
+ resolvedImages,
145
+ audioInfo !== null
146
+ );
138
147
  zip.file(`OEBPS/chapters/${filename}`, xhtml);
139
148
  let smilFilename;
140
149
  if (audioInfo) {
141
150
  smilFilename = `${id}.smil`;
142
- const smil = generateSmil(filename, audioInfo, chap.nodes);
151
+ const smil = generateSmil(filename, audioInfo, ids);
143
152
  zip.file(`OEBPS/chapters/${smilFilename}`, smil);
144
153
  }
145
154
  chapterFiles.push({
@@ -248,10 +257,14 @@ function collectDocImages(nodes) {
248
257
  return images;
249
258
  }
250
259
  function renderChapterXhtml(nodes, bookTitle, images, addIds = false) {
251
- let elementCounter = 0;
252
- const nextId = () => `p${++elementCounter}`;
260
+ const ids = [];
261
+ const nextId = () => {
262
+ const id = `p${ids.length + 1}`;
263
+ ids.push(id);
264
+ return id;
265
+ };
253
266
  const body = nodes.map((n) => blockToXhtml(n, images, addIds ? nextId : void 0)).join("\n");
254
- return `<?xml version="1.0" encoding="UTF-8"?>
267
+ const xhtml = `<?xml version="1.0" encoding="UTF-8"?>
255
268
  <!DOCTYPE html>
256
269
  <html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops">
257
270
  <head>
@@ -263,40 +276,46 @@ function renderChapterXhtml(nodes, bookTitle, images, addIds = false) {
263
276
  ${body}
264
277
  </body>
265
278
  </html>`;
279
+ return { xhtml, ids };
266
280
  }
267
281
  function blockToXhtml(node, images, nextId) {
268
- const idAttr = nextId ? ` id="${nextId()}"` : "";
282
+ let cached;
283
+ const idAttr = () => {
284
+ if (!nextId) return "";
285
+ cached ?? (cached = ` id="${nextId()}"`);
286
+ return cached;
287
+ };
269
288
  switch (node.type) {
270
289
  case "heading": {
271
290
  const tag = `h${node.depth}`;
272
- return `<${tag}${idAttr}>${inlinesToXhtml(node.children, images)}</${tag}>`;
291
+ return `<${tag}${idAttr()}>${inlinesToXhtml(node.children, images)}</${tag}>`;
273
292
  }
274
293
  case "paragraph":
275
- return `<p${idAttr}>${inlinesToXhtml(node.children, images)}</p>`;
294
+ return `<p${idAttr()}>${inlinesToXhtml(node.children, images)}</p>`;
276
295
  case "blockquote":
277
- return `<blockquote${idAttr}>
296
+ return `<blockquote${idAttr()}>
278
297
  ${node.children.map((c) => blockToXhtml(c, images, nextId)).join("\n")}
279
298
  </blockquote>`;
280
299
  case "list": {
281
300
  const tag = node.ordered ? "ol" : "ul";
282
301
  const startAttr = node.ordered && node.start && node.start !== 1 ? ` start="${node.start}"` : "";
283
302
  const items = node.children.map((item) => listItemToXhtml(item, images)).join("\n");
284
- return `<${tag}${idAttr}${startAttr}>
303
+ return `<${tag}${idAttr()}${startAttr}>
285
304
  ${items}
286
305
  </${tag}>`;
287
306
  }
288
307
  case "code": {
289
308
  const langAttr = node.lang ? ` class="language-${escapeXml(node.lang)}"` : "";
290
- return `<pre${idAttr}><code${langAttr}>${escapeXml(node.value)}</code></pre>`;
309
+ return `<pre${idAttr()}><code${langAttr}>${escapeXml(node.value)}</code></pre>`;
291
310
  }
292
311
  case "thematicBreak":
293
- return `<hr${idAttr}/>`;
312
+ return `<hr${idAttr()}/>`;
294
313
  case "table":
295
- return tableToXhtml(node, images, idAttr);
314
+ return tableToXhtml(node, images, idAttr());
296
315
  case "htmlBlock":
297
- return `<p${idAttr}>${escapeXml(node.rawHtml.replace(/<[^>]+>/g, ""))}</p>`;
316
+ return `<p${idAttr()}>${escapeXml(node.rawHtml.replace(/<[^>]+>/g, ""))}</p>`;
298
317
  case "math":
299
- return `<p${idAttr} class="math">${escapeXml(node.value)}</p>`;
318
+ return `<p${idAttr()} class="math">${escapeXml(node.value)}</p>`;
300
319
  default:
301
320
  return "";
302
321
  }
@@ -386,6 +405,14 @@ function safeArchiveBasename(path, fallback) {
386
405
  }).join("").trim();
387
406
  return !safe || safe === "." || safe === ".." ? fallback : safe;
388
407
  }
408
+ function uniqueManifestId(prefix, filename, used) {
409
+ const base = `${prefix}-${filename.replace(/[^a-zA-Z0-9]/g, "-")}`;
410
+ let id = base;
411
+ let suffix = 2;
412
+ while (used.has(id)) id = `${base}-${suffix++}`;
413
+ used.add(id);
414
+ return id;
415
+ }
389
416
  function uniqueFilename(filename, used) {
390
417
  if (!used.has(filename)) return filename;
391
418
  const dot = filename.lastIndexOf(".");
@@ -395,22 +422,24 @@ function uniqueFilename(filename, used) {
395
422
  while (used.has(`${base}-${suffix}${ext}`)) suffix++;
396
423
  return `${base}-${suffix}${ext}`;
397
424
  }
398
- function generateSmil(chapterFilename, audioInfo, nodes) {
399
- let elementCount = 0;
400
- function countBlocks(node) {
401
- elementCount++;
402
- if (node.type === "blockquote") node.children.forEach(countBlocks);
425
+ function generateSmil(chapterFilename, audioInfo, elementIds) {
426
+ if (elementIds.length === 0) {
427
+ return `<?xml version="1.0" encoding="UTF-8"?>
428
+ <smil xmlns="http://www.w3.org/ns/SMIL" xmlns:epub="http://www.idpf.org/2007/ops" version="3.0">
429
+ <body>
430
+ <seq id="seq-1" epub:textref="${chapterFilename}">
431
+ </seq>
432
+ </body>
433
+ </smil>`;
403
434
  }
404
- nodes.forEach(countBlocks);
405
- if (elementCount === 0) elementCount = 1;
406
- const clipDuration = audioInfo.duration / elementCount;
435
+ const clipDuration = audioInfo.duration / elementIds.length;
407
436
  const pars = [];
408
- for (let i = 0; i < elementCount; i++) {
437
+ for (let i = 0; i < elementIds.length; i++) {
409
438
  const clipStart = formatTime(audioInfo.clipStart + i * clipDuration, true);
410
439
  const clipEnd = formatTime(audioInfo.clipStart + (i + 1) * clipDuration, true);
411
440
  pars.push(
412
441
  ` <par id="par-${i + 1}">
413
- <text src="${chapterFilename}#p${i + 1}"/>
442
+ <text src="${chapterFilename}#${escapeXml(elementIds[i])}"/>
414
443
  <audio src="../audio/${escapeXml(audioInfo.audioFilename)}" clipBegin="${clipStart}" clipEnd="${clipEnd}"/>
415
444
  </par>`
416
445
  );
@@ -477,12 +506,17 @@ function generateContentOpf(params) {
477
506
  );
478
507
  }
479
508
  }
509
+ const usedManifestIds = /* @__PURE__ */ new Set(["cover-image", "cover-page"]);
510
+ for (const chap of chapters) {
511
+ usedManifestIds.add(chap.id);
512
+ if (chap.smilFilename) usedManifestIds.add(`${chap.id}-overlay`);
513
+ }
480
514
  if (audioFiles) {
481
515
  const usedAudioNames = /* @__PURE__ */ new Set();
482
516
  for (const af of audioFiles) {
483
517
  if (usedAudioNames.has(af.filename)) continue;
484
518
  usedAudioNames.add(af.filename);
485
- const audioId = `audio-${af.filename.replace(/[^a-zA-Z0-9]/g, "-")}`;
519
+ const audioId = uniqueManifestId("audio", af.filename, usedManifestIds);
486
520
  manifestItems.push(
487
521
  ` <item id="${audioId}" href="audio/${escapeXml(af.filename)}" media-type="${af.mime}"/>`
488
522
  );
@@ -492,7 +526,7 @@ function generateContentOpf(params) {
492
526
  for (const [, img] of images) {
493
527
  if (usedFilenames.has(img.filename)) continue;
494
528
  usedFilenames.add(img.filename);
495
- const imgId = `img-${img.filename.replace(/[^a-zA-Z0-9]/g, "-")}`;
529
+ const imgId = uniqueManifestId("img", img.filename, usedManifestIds);
496
530
  manifestItems.push(
497
531
  ` <item id="${imgId}" href="images/${escapeXml(img.filename)}" media-type="${img.mime}"/>`
498
532
  );
@@ -676,4 +710,3 @@ export {
676
710
  markdownDocToEpub,
677
711
  docToEpub
678
712
  };
679
- //# sourceMappingURL=chunk-4VUWTSGM.js.map
@@ -15,4 +15,3 @@ var ConversionError = class extends Error {
15
15
  export {
16
16
  ConversionError
17
17
  };
18
- //# sourceMappingURL=chunk-ODL3SSPT.js.map