@bendyline/squisq-formats 2.4.5 → 2.5.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.
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  extractPlainText
3
- } from "./chunk-6X5XN3CZ.js";
3
+ } from "./chunk-AVOZAKGP.js";
4
4
  import {
5
5
  escapeXml
6
6
  } from "./chunk-JU2RHXUB.js";
@@ -13,6 +13,10 @@ import {
13
13
  extractFilename,
14
14
  inferMimeType
15
15
  } from "./chunk-6RQOV3B3.js";
16
+ import {
17
+ FootnoteIndex,
18
+ footnoteIds
19
+ } from "./chunk-BXWNU4T5.js";
16
20
 
17
21
  // src/epub/export.ts
18
22
  import JSZip from "jszip";
@@ -31,6 +35,10 @@ async function markdownDocToEpub(doc, options = {}) {
31
35
  const uuid = createEpubUuid();
32
36
  const embeddedIconFonts = fontAwesomeFaces(collectInlineIconFamilies(doc));
33
37
  const chapters = splitIntoChapters(doc.children);
38
+ const footnoteIndex = new FootnoteIndex(doc);
39
+ const citedAnywhere = /* @__PURE__ */ new Set();
40
+ collectCitedIdentifiers(doc.children, citedAnywhere);
41
+ const uncitedFootnotes = footnoteIndex.ordered().filter((fn) => fn.definition && !citedAnywhere.has(fn.identifier)).map((fn) => fn.identifier);
34
42
  const imageEntries = collectDocImages(doc.children);
35
43
  const resolvedImages = /* @__PURE__ */ new Map();
36
44
  const usedImageNames = /* @__PURE__ */ new Set();
@@ -148,11 +156,14 @@ async function markdownDocToEpub(doc, options = {}) {
148
156
  const id = `chapter-${num}`;
149
157
  const filename = `${id}.xhtml`;
150
158
  const audioInfo = chapterAudio[i] ?? null;
159
+ const isLastChapter = i === chapters.length - 1;
151
160
  const { xhtml, ids } = renderChapterXhtml(
152
161
  chap.nodes,
153
162
  title,
154
163
  resolvedImages,
155
- audioInfo !== null
164
+ audioInfo !== null,
165
+ footnoteIndex,
166
+ isLastChapter ? uncitedFootnotes : []
156
167
  );
157
168
  zip.file(`OEBPS/chapters/${filename}`, xhtml);
158
169
  let smilFilename;
@@ -267,14 +278,43 @@ function collectDocImages(nodes) {
267
278
  nodes.forEach(walkBlock);
268
279
  return images;
269
280
  }
270
- function renderChapterXhtml(nodes, bookTitle, images, addIds = false) {
281
+ function collectCitedIdentifiers(nodes, into) {
282
+ for (const raw of nodes) {
283
+ const node = raw;
284
+ if (node?.type === "footnoteReference" && node.identifier) into.add(node.identifier);
285
+ if (Array.isArray(node?.children)) collectCitedIdentifiers(node.children, into);
286
+ }
287
+ }
288
+ function renderFootnotesXhtml(identifiers, ctx) {
289
+ const index = ctx.footnotes;
290
+ if (!index || identifiers.length === 0) return "";
291
+ const seen = /* @__PURE__ */ new Set();
292
+ const notes = index.ordered().filter(
293
+ (fn) => identifiers.includes(fn.identifier) && !seen.has(fn.identifier) && seen.add(fn.identifier)
294
+ );
295
+ if (notes.length === 0) return "";
296
+ const items = notes.map((fn) => {
297
+ const { def } = footnoteIds(fn.identifier);
298
+ const body = fn.definition ? fn.definition.children.map((c) => blockToXhtml(c, ctx)).join("") : "";
299
+ return `<li id="${escapeXml(def)}" epub:type="footnote"><span class="squisq-footnote-num">${fn.number}.</span> ${body}</li>`;
300
+ });
301
+ return `
302
+ <aside class="squisq-footnotes" epub:type="footnotes"><hr/>
303
+ <ol>
304
+ ` + items.join("\n") + `
305
+ </ol>
306
+ </aside>`;
307
+ }
308
+ function renderChapterXhtml(nodes, bookTitle, images, addIds = false, footnotes, trailingFootnotes = []) {
271
309
  const ids = [];
272
310
  const nextId = () => {
273
311
  const id = `p${ids.length + 1}`;
274
312
  ids.push(id);
275
313
  return id;
276
314
  };
277
- const body = nodes.map((n) => blockToXhtml(n, images, addIds ? nextId : void 0)).join("\n");
315
+ const ctx = { images, footnotes, cited: /* @__PURE__ */ new Set() };
316
+ const rendered = nodes.map((n) => blockToXhtml(n, ctx, addIds ? nextId : void 0)).join("\n");
317
+ const body = rendered + renderFootnotesXhtml([...ctx.cited ?? [], ...trailingFootnotes], ctx);
278
318
  const xhtml = `<?xml version="1.0" encoding="UTF-8"?>
279
319
  <!DOCTYPE html>
280
320
  <html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops">
@@ -289,7 +329,7 @@ ${body}
289
329
  </html>`;
290
330
  return { xhtml, ids };
291
331
  }
292
- function blockToXhtml(node, images, nextId) {
332
+ function blockToXhtml(node, ctx, nextId) {
293
333
  let cached;
294
334
  const idAttr = () => {
295
335
  if (!nextId) return "";
@@ -299,18 +339,18 @@ function blockToXhtml(node, images, nextId) {
299
339
  switch (node.type) {
300
340
  case "heading": {
301
341
  const tag = `h${node.depth}`;
302
- return `<${tag}${idAttr()}>${inlinesToXhtml(node.children, images)}</${tag}>`;
342
+ return `<${tag}${idAttr()}>${inlinesToXhtml(node.children, ctx)}</${tag}>`;
303
343
  }
304
344
  case "paragraph":
305
- return `<p${idAttr()}>${inlinesToXhtml(node.children, images)}</p>`;
345
+ return `<p${idAttr()}>${inlinesToXhtml(node.children, ctx)}</p>`;
306
346
  case "blockquote":
307
347
  return `<blockquote${idAttr()}>
308
- ${node.children.map((c) => blockToXhtml(c, images, nextId)).join("\n")}
348
+ ${node.children.map((c) => blockToXhtml(c, ctx, nextId)).join("\n")}
309
349
  </blockquote>`;
310
350
  case "list": {
311
351
  const tag = node.ordered ? "ol" : "ul";
312
352
  const startAttr = node.ordered && node.start && node.start !== 1 ? ` start="${node.start}"` : "";
313
- const items = node.children.map((item) => listItemToXhtml(item, images)).join("\n");
353
+ const items = node.children.map((item) => listItemToXhtml(item, ctx)).join("\n");
314
354
  return `<${tag}${idAttr()}${startAttr}>
315
355
  ${items}
316
356
  </${tag}>`;
@@ -322,7 +362,7 @@ ${items}
322
362
  case "thematicBreak":
323
363
  return `<hr${idAttr()}/>`;
324
364
  case "table":
325
- return tableToXhtml(node, images, idAttr());
365
+ return tableToXhtml(node, ctx, idAttr());
326
366
  case "htmlBlock":
327
367
  return `<p${idAttr()}>${escapeXml(node.rawHtml.replace(/<[^>]+>/g, ""))}</p>`;
328
368
  case "math":
@@ -331,12 +371,12 @@ ${items}
331
371
  return "";
332
372
  }
333
373
  }
334
- function listItemToXhtml(item, images) {
335
- const content = item.children.map((c) => blockToXhtml(c, images)).join("\n");
336
- const unwrapped = item.children.length === 1 && item.children[0].type === "paragraph" ? inlinesToXhtml(item.children[0].children, images) : content;
374
+ function listItemToXhtml(item, ctx) {
375
+ const content = item.children.map((c) => blockToXhtml(c, ctx)).join("\n");
376
+ const unwrapped = item.children.length === 1 && item.children[0].type === "paragraph" ? inlinesToXhtml(item.children[0].children, ctx) : content;
337
377
  return `<li>${unwrapped}</li>`;
338
378
  }
339
- function tableToXhtml(table, images, idAttr = "") {
379
+ function tableToXhtml(table, ctx, idAttr = "") {
340
380
  const rows = table.children;
341
381
  if (rows.length === 0) return `<table${idAttr}></table>`;
342
382
  const headerRow = rows[0];
@@ -345,36 +385,47 @@ function tableToXhtml(table, images, idAttr = "") {
345
385
  function cellToXhtml(cell, tag, colIndex) {
346
386
  const a = align[colIndex];
347
387
  const style = a ? ` style="text-align: ${a}"` : "";
348
- return `<${tag}${style}>${inlinesToXhtml(cell.children, images)}</${tag}>`;
388
+ return `<${tag}${style}>${inlinesToXhtml(cell.children, ctx)}</${tag}>`;
349
389
  }
350
390
  const thead = `<thead><tr>${headerRow.children.map((c, i) => cellToXhtml(c, "th", i)).join("")}</tr></thead>`;
351
391
  const tbody = bodyRows.length > 0 ? `<tbody>${bodyRows.map((row) => `<tr>${row.children.map((c, i) => cellToXhtml(c, "td", i)).join("")}</tr>`).join("")}</tbody>` : "";
352
392
  return `<table${idAttr}>${thead}${tbody}</table>`;
353
393
  }
354
- function inlinesToXhtml(nodes, images) {
355
- return nodes.map((n) => inlineToXhtml(n, images)).join("");
394
+ function inlinesToXhtml(nodes, ctx) {
395
+ return nodes.map((n) => inlineToXhtml(n, ctx)).join("");
356
396
  }
357
- function inlineToXhtml(node, images) {
397
+ function inlineToXhtml(node, ctx) {
358
398
  switch (node.type) {
359
399
  case "text":
360
400
  return escapeXml(node.value);
361
401
  case "strong":
362
- return `<strong>${inlinesToXhtml(node.children, images)}</strong>`;
402
+ return `<strong>${inlinesToXhtml(node.children, ctx)}</strong>`;
363
403
  case "emphasis":
364
- return `<em>${inlinesToXhtml(node.children, images)}</em>`;
404
+ return `<em>${inlinesToXhtml(node.children, ctx)}</em>`;
365
405
  case "delete":
366
- return `<del>${inlinesToXhtml(node.children, images)}</del>`;
406
+ return `<del>${inlinesToXhtml(node.children, ctx)}</del>`;
407
+ case "superscript":
408
+ return `<sup>${inlinesToXhtml(node.children, ctx)}</sup>`;
409
+ case "footnoteReference": {
410
+ if (!ctx.footnotes) return "";
411
+ const { number, occurrence } = ctx.footnotes.cite(node.identifier);
412
+ const { ref, def } = footnoteIds(node.identifier, occurrence);
413
+ ctx.cited?.add(node.identifier);
414
+ return `<sup class="squisq-footnote-ref" epub:type="noteref"><a href="#${escapeXml(def)}" id="${escapeXml(ref)}">${number}</a></sup>`;
415
+ }
416
+ case "subscript":
417
+ return `<sub>${inlinesToXhtml(node.children, ctx)}</sub>`;
367
418
  case "inlineCode":
368
419
  return `<code>${escapeXml(node.value)}</code>`;
369
420
  case "link": {
370
421
  const href = sanitizeUrl(node.url, "link");
371
- if (!href) return inlinesToXhtml(node.children, images);
422
+ if (!href) return inlinesToXhtml(node.children, ctx);
372
423
  const titleAttr = node.title ? ` title="${escapeXml(node.title)}"` : "";
373
- return `<a href="${escapeXml(href)}"${titleAttr}>${inlinesToXhtml(node.children, images)}</a>`;
424
+ return `<a href="${escapeXml(href)}"${titleAttr}>${inlinesToXhtml(node.children, ctx)}</a>`;
374
425
  }
375
426
  case "image": {
376
427
  const alt = escapeXml(node.alt ?? "");
377
- const resolved = images.get(node.url);
428
+ const resolved = ctx.images.get(node.url);
378
429
  const safeSrc = resolved ? `../images/${resolved.filename}` : sanitizeUrl(node.url, "media");
379
430
  if (!safeSrc) return alt;
380
431
  const src = escapeXml(safeSrc);
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  convert,
3
3
  defaultRegistry
4
- } from "./chunk-DNGNODJP.js";
4
+ } from "./chunk-JTGWQK5V.js";
5
5
  import {
6
6
  ConversionError
7
7
  } from "./chunk-KXOZMWBS.js";