@bendyline/squisq-formats 2.4.6 → 2.5.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.
@@ -3,6 +3,10 @@ import {
3
3
  extractFilename,
4
4
  inferMimeType
5
5
  } from "./chunk-6RQOV3B3.js";
6
+ import {
7
+ FootnoteIndex,
8
+ footnoteIds
9
+ } from "./chunk-BXWNU4T5.js";
6
10
 
7
11
  // src/html/index.ts
8
12
  import JSZip3 from "jszip";
@@ -290,8 +294,9 @@ function markdownDocToPlainHtml(doc, options = {}) {
290
294
  } = options;
291
295
  const resolveId = themeId ?? readFrontmatterThemeId2(doc.frontmatter);
292
296
  const theme = options.theme ?? (resolveId ? resolveThemeForDoc(doc, resolveId, options.themeRegistry) : void 0);
293
- const ctx = { images, links, htmlPolicy };
294
- const body = renderTopLevel(doc.children, ctx);
297
+ const footnotes = new FootnoteIndex(doc);
298
+ const ctx = { images, links, htmlPolicy, footnotes };
299
+ const body = renderTopLevel(doc.children, ctx) + renderFootnotesSection(footnotes, ctx);
295
300
  const fontsLink = theme && externalResources === "allow" ? renderFontsLink(theme) : "";
296
301
  const usesIcons = docUsesIcons(doc);
297
302
  let iconsLink = "";
@@ -355,6 +360,28 @@ function renderTopLevel(children, ctx) {
355
360
  }
356
361
  return out.join("\n");
357
362
  }
363
+ function renderFootnotesSection(footnotes, ctx) {
364
+ if (footnotes.isEmpty) return "";
365
+ const items = footnotes.ordered().map((fn) => {
366
+ const { def } = footnoteIds(fn.identifier);
367
+ const body = fn.definition ? childrenToHtml(fn.definition, ctx) : "";
368
+ const backlinks = Array.from({ length: Math.max(fn.citations, 1) }, (_unused, i) => {
369
+ const { ref } = footnoteIds(fn.identifier, i + 1);
370
+ const label = fn.citations > 1 ? `\u21A9${String.fromCharCode(65038)}${i + 1}` : "\u21A9";
371
+ return `<a href="#${escapeAttr(ref)}" class="squisq-footnote-backref" aria-label="Back to reference ${fn.number}">${label}</a>`;
372
+ }).join(" ");
373
+ return `<li id="${escapeAttr(def)}">${body}${backlinks}</li>`;
374
+ });
375
+ const lines = [
376
+ "",
377
+ '<section class="squisq-footnotes" data-footnotes><hr>',
378
+ "<ol>",
379
+ ...items,
380
+ "</ol>",
381
+ "</section>"
382
+ ];
383
+ return lines.join("\n");
384
+ }
358
385
  function findSectionEnd(nodes, from) {
359
386
  for (let i = from + 1; i < nodes.length; i++) {
360
387
  const n = nodes[i];
@@ -645,6 +672,20 @@ function nodeToHtml(node, ctx) {
645
672
  return `<em>${childrenToHtml(node, ctx)}</em>`;
646
673
  case "delete":
647
674
  return `<del>${childrenToHtml(node, ctx)}</del>`;
675
+ case "superscript":
676
+ return `<sup>${childrenToHtml(node, ctx)}</sup>`;
677
+ case "footnoteReference": {
678
+ if (!ctx?.footnotes) {
679
+ return `<sup class="squisq-footnote-ref">${escapeHtml2(node.label ?? node.identifier)}</sup>`;
680
+ }
681
+ const { number, occurrence } = ctx.footnotes.cite(node.identifier);
682
+ const { ref, def } = footnoteIds(node.identifier, occurrence);
683
+ return `<sup class="squisq-footnote-ref"><a href="#${escapeAttr(def)}" id="${escapeAttr(ref)}">${number}</a></sup>`;
684
+ }
685
+ case "footnoteDefinition":
686
+ return "";
687
+ case "subscript":
688
+ return `<sub>${childrenToHtml(node, ctx)}</sub>`;
648
689
  case "inlineCode":
649
690
  return `<code>${escapeHtml2(node.value ?? "")}</code>`;
650
691
  case "code": {
@@ -1238,6 +1279,199 @@ import {
1238
1279
  stringifyMarkdown
1239
1280
  } from "@bendyline/squisq/markdown";
1240
1281
  import { docToMarkdown } from "@bendyline/squisq/doc";
1282
+
1283
+ // src/html/footnoteImport.ts
1284
+ var TRAILING_WHITESPACE = /[ \t\r\n]+$/;
1285
+ var WHITESPACE = /[ \t\r\n]+/;
1286
+ var VARIATION_SELECTORS = /[\ufe0e\ufe0f]/gu;
1287
+ var RETURN_ARROW = /^[ \t\r\n\u2191\u21a9\u21b5^]+$/u;
1288
+ var MARKER_LABEL = /^[ \t[(]*(?:[0-9]{1,4}|[a-z]|[*†‡])[ \t\])]*$/i;
1289
+ function isElement(node) {
1290
+ return node.type === "htmlElement";
1291
+ }
1292
+ function attr(el, name) {
1293
+ return el.attributes[name];
1294
+ }
1295
+ function classList(el) {
1296
+ return (attr(el, "class") ?? "").split(WHITESPACE).filter(Boolean);
1297
+ }
1298
+ function isFootnotesContainer(el) {
1299
+ if (attr(el, "data-footnotes") !== void 0) return true;
1300
+ if (attr(el, "epub:type")?.includes("footnotes")) return true;
1301
+ const tag = el.tagName.toLowerCase();
1302
+ if (tag !== "section" && tag !== "aside" && tag !== "div" && tag !== "ol") return false;
1303
+ return classList(el).some(
1304
+ (c) => c === "footnotes" || c === "footnote-list" || c === "squisq-footnotes"
1305
+ );
1306
+ }
1307
+ function textOf(node) {
1308
+ if (node.type === "htmlText") return node.value;
1309
+ if (!isElement(node)) return "";
1310
+ return node.children.map(textOf).join("");
1311
+ }
1312
+ function isBacklink(el) {
1313
+ if (el.tagName.toLowerCase() !== "a") return false;
1314
+ if (attr(el, "data-footnote-backref") !== void 0) return true;
1315
+ if (classList(el).some((c) => c.includes("backref") || c === "footnote-back")) return true;
1316
+ return RETURN_ARROW.test(textOf(el).replace(VARIATION_SELECTORS, ""));
1317
+ }
1318
+ function findElement(nodes, match) {
1319
+ for (const node of nodes) {
1320
+ if (!isElement(node)) continue;
1321
+ if (match(node)) return node;
1322
+ const nested = findElement(node.children, match);
1323
+ if (nested) return nested;
1324
+ }
1325
+ return null;
1326
+ }
1327
+ function stripBacklinks(nodes) {
1328
+ const out = [];
1329
+ for (const node of nodes) {
1330
+ if (isElement(node)) {
1331
+ if (isBacklink(node)) continue;
1332
+ out.push({ ...node, children: stripBacklinks(node.children) });
1333
+ continue;
1334
+ }
1335
+ out.push(node);
1336
+ }
1337
+ return out;
1338
+ }
1339
+ function removeElement(nodes, target) {
1340
+ const out = [];
1341
+ for (const node of nodes) {
1342
+ if (node === target) continue;
1343
+ if (isElement(node)) {
1344
+ out.push({ ...node, children: removeElement(node.children, target) });
1345
+ continue;
1346
+ }
1347
+ out.push(node);
1348
+ }
1349
+ return out;
1350
+ }
1351
+ function trimTrailingText(nodes) {
1352
+ const out = [...nodes];
1353
+ for (let i = out.length - 1; i >= 0; i--) {
1354
+ const node = out[i];
1355
+ if (node.type === "htmlText") {
1356
+ const trimmed = node.value.replace(TRAILING_WHITESPACE, "");
1357
+ if (trimmed === "") {
1358
+ out.pop();
1359
+ continue;
1360
+ }
1361
+ out[i] = { ...node, value: trimmed };
1362
+ break;
1363
+ }
1364
+ if (isElement(node)) {
1365
+ out[i] = { ...node, children: trimTrailingText(node.children) };
1366
+ }
1367
+ break;
1368
+ }
1369
+ return out;
1370
+ }
1371
+ function collectDefinitionItems(container) {
1372
+ const items = /* @__PURE__ */ new Map();
1373
+ const walk = (nodes) => {
1374
+ for (const node of nodes) {
1375
+ if (!isElement(node)) continue;
1376
+ const id = attr(node, "id");
1377
+ if (node.tagName.toLowerCase() === "li" && id) {
1378
+ items.set(id, trimTrailingText(stripBacklinks(node.children)));
1379
+ continue;
1380
+ }
1381
+ walk(node.children);
1382
+ }
1383
+ };
1384
+ walk(container.children);
1385
+ return items;
1386
+ }
1387
+ function readableIdentifier(rawId, taken) {
1388
+ const short = rawId.replace(/^user-content-/, "").replace(/^fn[-_:]?/i, "").replace(/^footnote[-_:]?/i, "");
1389
+ if (short === "" || taken.has(short)) return rawId;
1390
+ return short;
1391
+ }
1392
+ function extractFootnoteSection(nodes) {
1393
+ const container = findElement(nodes, isFootnotesContainer);
1394
+ if (!container) return { nodes, bodies: /* @__PURE__ */ new Map() };
1395
+ const raw = collectDefinitionItems(container);
1396
+ if (raw.size === 0) return { nodes, bodies: /* @__PURE__ */ new Map() };
1397
+ const bodies = /* @__PURE__ */ new Map();
1398
+ const identifiers = /* @__PURE__ */ new Map();
1399
+ const taken = /* @__PURE__ */ new Set();
1400
+ for (const [rawId, body] of raw) {
1401
+ const identifier = readableIdentifier(rawId, taken);
1402
+ taken.add(identifier);
1403
+ identifiers.set(rawId, identifier);
1404
+ bodies.set(identifier, body);
1405
+ }
1406
+ return { nodes: removeElement(nodes, container), bodies, identifiers };
1407
+ }
1408
+ function inlineText(nodes) {
1409
+ return nodes.map(
1410
+ (n) => "value" in n && typeof n.value === "string" ? n.value : "children" in n && Array.isArray(n.children) ? inlineText(n.children) : ""
1411
+ ).join("");
1412
+ }
1413
+ function isBlockish(node) {
1414
+ switch (node.type) {
1415
+ case "paragraph":
1416
+ case "heading":
1417
+ case "blockquote":
1418
+ case "list":
1419
+ case "listItem":
1420
+ case "table":
1421
+ case "tableRow":
1422
+ case "tableCell":
1423
+ case "code":
1424
+ case "footnoteDefinition":
1425
+ return true;
1426
+ default:
1427
+ return false;
1428
+ }
1429
+ }
1430
+ function linkFootnoteReferences(blocks, known) {
1431
+ const rewriteInlines = (nodes, inSuperscript) => {
1432
+ for (let i = 0; i < nodes.length; i++) {
1433
+ const node = nodes[i];
1434
+ if (node.type === "link") {
1435
+ const id = node.url.startsWith("#") ? node.url.slice(1) : null;
1436
+ const identifier = id ? known.get(id) : void 0;
1437
+ if (identifier && (inSuperscript || MARKER_LABEL.test(inlineText(node.children)))) {
1438
+ nodes[i] = { type: "footnoteReference", identifier };
1439
+ }
1440
+ continue;
1441
+ }
1442
+ if (node.type === "superscript") {
1443
+ rewriteInlines(node.children, true);
1444
+ const only = node.children.length === 1 ? node.children[0] : null;
1445
+ if (only?.type === "footnoteReference") nodes[i] = only;
1446
+ continue;
1447
+ }
1448
+ if ("children" in node && Array.isArray(node.children)) {
1449
+ rewriteInlines(node.children, inSuperscript);
1450
+ }
1451
+ }
1452
+ };
1453
+ const walk = (list) => {
1454
+ for (const block of list) {
1455
+ if (!("children" in block) || !Array.isArray(block.children)) continue;
1456
+ const children = block.children;
1457
+ if (children.length > 0 && children.every(isBlockish)) {
1458
+ walk(children);
1459
+ } else {
1460
+ rewriteInlines(children, false);
1461
+ }
1462
+ }
1463
+ };
1464
+ walk(blocks);
1465
+ }
1466
+ function buildFootnoteDefinitions(bodies) {
1467
+ return [...bodies.entries()].map(([identifier, children]) => ({
1468
+ type: "footnoteDefinition",
1469
+ identifier,
1470
+ children
1471
+ }));
1472
+ }
1473
+
1474
+ // src/html/import.ts
1241
1475
  var HEADINGS = {
1242
1476
  h1: 1,
1243
1477
  h2: 2,
@@ -1267,8 +1501,10 @@ var DROP = /* @__PURE__ */ new Set(["script", "style", "head", "title", "noscrip
1267
1501
  var INLINE_STRONG = /* @__PURE__ */ new Set(["strong", "b"]);
1268
1502
  var INLINE_EM = /* @__PURE__ */ new Set(["em", "i"]);
1269
1503
  var INLINE_DEL = /* @__PURE__ */ new Set(["del", "s", "strike"]);
1270
- var INLINE_TRANSPARENT = /* @__PURE__ */ new Set(["span", "font", "abbr", "mark", "small", "sub", "sup", "u"]);
1271
- var isElement = (n) => n.type === "htmlElement";
1504
+ var INLINE_SUP = /* @__PURE__ */ new Set(["sup"]);
1505
+ var INLINE_SUB = /* @__PURE__ */ new Set(["sub"]);
1506
+ var INLINE_TRANSPARENT = /* @__PURE__ */ new Set(["span", "font", "abbr", "mark", "small", "u"]);
1507
+ var isElement2 = (n) => n.type === "htmlElement";
1272
1508
  var isText = (n) => n.type === "htmlText";
1273
1509
  var collapseWs = (s) => s.replace(/\s+/g, " ");
1274
1510
  function inlinesFromNodes(nodes) {
@@ -1279,7 +1515,7 @@ function inlinesFromNodes(nodes) {
1279
1515
  if (value) out.push({ type: "text", value });
1280
1516
  continue;
1281
1517
  }
1282
- if (!isElement(node)) continue;
1518
+ if (!isElement2(node)) continue;
1283
1519
  const tag = node.tagName.toLowerCase();
1284
1520
  if (DROP.has(tag)) continue;
1285
1521
  if (tag === "br") {
@@ -1290,6 +1526,10 @@ function inlinesFromNodes(nodes) {
1290
1526
  out.push({ type: "emphasis", children: inlinesFromNodes(node.children) });
1291
1527
  } else if (INLINE_DEL.has(tag)) {
1292
1528
  out.push({ type: "delete", children: inlinesFromNodes(node.children) });
1529
+ } else if (INLINE_SUP.has(tag)) {
1530
+ out.push({ type: "superscript", children: inlinesFromNodes(node.children) });
1531
+ } else if (INLINE_SUB.has(tag)) {
1532
+ out.push({ type: "subscript", children: inlinesFromNodes(node.children) });
1293
1533
  } else if (tag === "code" || tag === "kbd" || tag === "samp" || tag === "tt") {
1294
1534
  out.push({ type: "inlineCode", value: textContent(node) });
1295
1535
  } else if (tag === "a") {
@@ -1312,7 +1552,7 @@ function inlinesFromNodes(nodes) {
1312
1552
  }
1313
1553
  function textContent(node) {
1314
1554
  if (isText(node)) return node.value;
1315
- if (isElement(node)) return node.children.map(textContent).join("");
1555
+ if (isElement2(node)) return node.children.map(textContent).join("");
1316
1556
  return "";
1317
1557
  }
1318
1558
  var onlyWhitespace = (inlines) => inlines.every((n) => n.type === "text" && n.value.trim() === "");
@@ -1332,7 +1572,7 @@ function blocksFromNodes(nodes) {
1332
1572
  inlineBuffer.push(node);
1333
1573
  continue;
1334
1574
  }
1335
- if (!isElement(node)) continue;
1575
+ if (!isElement2(node)) continue;
1336
1576
  const tag = node.tagName.toLowerCase();
1337
1577
  if (DROP.has(tag)) continue;
1338
1578
  const block = blockForElement(node, tag);
@@ -1381,7 +1621,7 @@ function blockForElement(node, tag) {
1381
1621
  function listFromElement(node, ordered) {
1382
1622
  const items = [];
1383
1623
  for (const child of node.children) {
1384
- if (isElement(child) && child.tagName.toLowerCase() === "li") {
1624
+ if (isElement2(child) && child.tagName.toLowerCase() === "li") {
1385
1625
  const blocks = blocksFromNodes(child.children);
1386
1626
  items.push({ type: "listItem", children: blocks });
1387
1627
  }
@@ -1399,12 +1639,12 @@ function tableFromElement(node) {
1399
1639
  const rows = [];
1400
1640
  const collectRows = (n) => {
1401
1641
  for (const child of n.children) {
1402
- if (!isElement(child)) continue;
1642
+ if (!isElement2(child)) continue;
1403
1643
  const t = child.tagName.toLowerCase();
1404
1644
  if (t === "tr") {
1405
1645
  const cells = [];
1406
1646
  for (const cell of child.children) {
1407
- if (isElement(cell) && (cell.tagName.toLowerCase() === "td" || cell.tagName.toLowerCase() === "th")) {
1647
+ if (isElement2(cell) && (cell.tagName.toLowerCase() === "td" || cell.tagName.toLowerCase() === "th")) {
1408
1648
  cells.push({ type: "tableCell", children: inlinesFromNodes(cell.children) });
1409
1649
  }
1410
1650
  }
@@ -1426,7 +1666,7 @@ function toHtmlString(data) {
1426
1666
  }
1427
1667
  function findEmbeddedSquisqDoc(nodes) {
1428
1668
  for (const node of nodes) {
1429
- if (!isElement(node)) continue;
1669
+ if (!isElement2(node)) continue;
1430
1670
  if (node.tagName.toLowerCase() === "script" && "data-squisq-doc" in node.attributes) {
1431
1671
  return node;
1432
1672
  }
@@ -1460,11 +1700,19 @@ function htmlToMarkdownDocSync(html, options = {}) {
1460
1700
  if (html.length > maxInputChars) {
1461
1701
  throw new RangeError(`HTML exceeds the ${maxInputChars}-character safety limit`);
1462
1702
  }
1463
- let nodes = parseHtmlToNodes(html);
1703
+ const nodes = parseHtmlToNodes(html);
1464
1704
  const embedded = findEmbeddedSquisqDoc(nodes);
1465
1705
  if (embedded) return docToMarkdown(parseEmbeddedSquisqDoc(embedded));
1466
- if (options.sanitize !== false) nodes = sanitizeHtmlNodes2(nodes);
1467
- return { type: "document", children: blocksFromNodes(nodes) };
1706
+ const { nodes: bodyNodes, bodies, identifiers } = extractFootnoteSection(nodes);
1707
+ const clean = (input) => options.sanitize === false ? input : sanitizeHtmlNodes2(input);
1708
+ const children = blocksFromNodes(clean(bodyNodes));
1709
+ if (bodies.size > 0) {
1710
+ linkFootnoteReferences(children, identifiers ?? /* @__PURE__ */ new Map());
1711
+ const definitions = /* @__PURE__ */ new Map();
1712
+ for (const [id, body] of bodies) definitions.set(id, blocksFromNodes(clean(body)));
1713
+ children.push(...buildFootnoteDefinitions(definitions));
1714
+ }
1715
+ return { type: "document", children };
1468
1716
  }
1469
1717
  async function htmlToMarkdownDoc(data, options = {}) {
1470
1718
  return htmlToMarkdownDocSync(toHtmlString(data), options);
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  convert,
3
3
  defaultRegistry
4
- } from "./chunk-DNGNODJP.js";
4
+ } from "./chunk-5JQ5NDRC.js";
5
5
  import {
6
6
  ConversionError
7
7
  } from "./chunk-KXOZMWBS.js";
@@ -4,9 +4,9 @@ import {
4
4
  docxToDoc,
5
5
  docxToMarkdownDoc,
6
6
  markdownDocToDocx
7
- } from "../chunk-BGDN7BXK.js";
8
- import "../chunk-DWNNYO5H.js";
9
- import "../chunk-6X5XN3CZ.js";
7
+ } from "../chunk-FIOSE4BO.js";
8
+ import "../chunk-A6N6IN3I.js";
9
+ import "../chunk-AVOZAKGP.js";
10
10
  import "../chunk-ILCJ3WFD.js";
11
11
  import "../chunk-JU2RHXUB.js";
12
12
  import "../chunk-S5PCVMKU.js";
@@ -14,6 +14,7 @@ import "../chunk-7AWFHP5U.js";
14
14
  import "../chunk-IIQYS2YH.js";
15
15
  import "../chunk-USU6HTKB.js";
16
16
  import "../chunk-AONELFLA.js";
17
+ import "../chunk-BXWNU4T5.js";
17
18
  export {
18
19
  docToDocx,
19
20
  docxToContainer,
@@ -1,12 +1,13 @@
1
1
  import {
2
2
  docToEpub,
3
3
  markdownDocToEpub
4
- } from "../chunk-KN62QJYK.js";
5
- import "../chunk-6X5XN3CZ.js";
4
+ } from "../chunk-2LT3JL7U.js";
5
+ import "../chunk-AVOZAKGP.js";
6
6
  import "../chunk-JU2RHXUB.js";
7
7
  import "../chunk-USU6HTKB.js";
8
8
  import "../chunk-6RQOV3B3.js";
9
9
  import "../chunk-AONELFLA.js";
10
+ import "../chunk-BXWNU4T5.js";
10
11
  export {
11
12
  docToEpub,
12
13
  markdownDocToEpub