@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
  stripHtmlTags
3
- } from "./chunk-6X5XN3CZ.js";
3
+ } from "./chunk-AVOZAKGP.js";
4
4
 
5
5
  // src/ooxml/relIds.ts
6
6
  var RelIdAllocator = class {
@@ -92,6 +92,10 @@ function inlineNodeToRuns(node, handlers, format) {
92
92
  return inlineNodesToRuns(node.children, handlers, { ...format, italic: true });
93
93
  case "delete":
94
94
  return inlineNodesToRuns(node.children, handlers, { ...format, strike: true });
95
+ case "superscript":
96
+ return inlineNodesToRuns(node.children, handlers, { ...format, vertAlign: "superscript" });
97
+ case "subscript":
98
+ return inlineNodesToRuns(node.children, handlers, { ...format, vertAlign: "subscript" });
95
99
  case "inlineCode":
96
100
  return handlers.run(node.value, { ...format, code: true });
97
101
  case "inlineMath":
@@ -11,6 +11,8 @@ function inlineToPlainText(node) {
11
11
  case "emphasis":
12
12
  case "strong":
13
13
  case "delete":
14
+ case "superscript":
15
+ case "subscript":
14
16
  case "link":
15
17
  return extractPlainText(node.children);
16
18
  case "image":
@@ -29,5 +31,6 @@ function extractPlainText(nodes) {
29
31
 
30
32
  export {
31
33
  stripHtmlTags,
34
+ inlineToPlainText,
32
35
  extractPlainText
33
36
  };
@@ -0,0 +1,108 @@
1
+ // src/shared/footnotes.ts
2
+ var FootnoteIndex = class {
3
+ constructor(doc) {
4
+ this.numbers = /* @__PURE__ */ new Map();
5
+ this.definitions = /* @__PURE__ */ new Map();
6
+ /** Citations rendered so far, per identifier — see {@link cite}. */
7
+ this.citations = /* @__PURE__ */ new Map();
8
+ collectReferences(doc.children, (id) => {
9
+ if (!this.numbers.has(id)) this.numbers.set(id, this.numbers.size + 1);
10
+ });
11
+ collectDefinitions(doc.children, (def) => {
12
+ this.definitions.set(def.identifier, def);
13
+ if (!this.numbers.has(def.identifier))
14
+ this.numbers.set(def.identifier, this.numbers.size + 1);
15
+ });
16
+ }
17
+ /** Whether the document has any footnotes at all. */
18
+ get isEmpty() {
19
+ return this.numbers.size === 0;
20
+ }
21
+ /** The reader-facing number for an identifier (assigning one if unseen). */
22
+ numberFor(identifier) {
23
+ const existing = this.numbers.get(identifier);
24
+ if (existing !== void 0) return existing;
25
+ const next = this.numbers.size + 1;
26
+ this.numbers.set(identifier, next);
27
+ return next;
28
+ }
29
+ /**
30
+ * Record one citation and return how to anchor it.
31
+ *
32
+ * Prose may cite the same footnote repeatedly. Every citation needs its own
33
+ * element id — reusing one id would emit duplicate ids, which is invalid HTML
34
+ * and leaves the backlink pointing at whichever copy the browser found first.
35
+ * Call this exactly once per rendered reference, in document order.
36
+ */
37
+ cite(identifier) {
38
+ const number = this.numberFor(identifier);
39
+ const occurrence = (this.citations.get(identifier) ?? 0) + 1;
40
+ this.citations.set(identifier, occurrence);
41
+ return { number, occurrence };
42
+ }
43
+ /** Every footnote in reading order. */
44
+ ordered() {
45
+ return [...this.numbers.entries()].map(([identifier, number]) => ({
46
+ identifier,
47
+ number,
48
+ definition: this.definitions.get(identifier),
49
+ citations: this.citations.get(identifier) ?? 0
50
+ })).sort((a, b) => a.number - b.number);
51
+ }
52
+ };
53
+ function isFootnoteDefinition(node) {
54
+ return node.type === "footnoteDefinition";
55
+ }
56
+ function childrenOf(node) {
57
+ return "children" in node && Array.isArray(node.children) ? node.children : [];
58
+ }
59
+ function collectReferences(nodes, visit) {
60
+ for (const node of nodes) {
61
+ if (node.type === "footnoteDefinition") continue;
62
+ if (node.type === "footnoteReference") {
63
+ visit(node.identifier);
64
+ continue;
65
+ }
66
+ collectReferences(childrenOf(node), visit);
67
+ }
68
+ }
69
+ function collectDefinitions(nodes, visit) {
70
+ for (const node of nodes) {
71
+ if (isFootnoteDefinition(node)) {
72
+ visit(node);
73
+ continue;
74
+ }
75
+ collectDefinitions(childrenOf(node), visit);
76
+ }
77
+ }
78
+ function footnoteIds(identifier, occurrence = 1) {
79
+ const safe = identifier.replace(/[^A-Za-z0-9_-]/g, "-");
80
+ const suffix = occurrence > 1 ? `-${occurrence}` : "";
81
+ return { ref: `fnref-${safe}${suffix}`, def: `fn-${safe}` };
82
+ }
83
+ function renumberFootnotes(doc) {
84
+ const index = new FootnoteIndex(doc);
85
+ if (index.isEmpty) return;
86
+ const renamed = /* @__PURE__ */ new Map();
87
+ for (const fn of index.ordered()) renamed.set(fn.identifier, String(fn.number));
88
+ if ([...renamed].every(([from, to]) => from === to)) return;
89
+ const walk = (nodes) => {
90
+ for (const node of nodes) {
91
+ if (node.type === "footnoteReference" || node.type === "footnoteDefinition") {
92
+ const next = renamed.get(node.identifier);
93
+ if (next !== void 0) {
94
+ node.identifier = next;
95
+ if ("label" in node && node.label !== void 0) node.label = next;
96
+ }
97
+ }
98
+ if ("children" in node && Array.isArray(node.children)) walk(node.children);
99
+ }
100
+ };
101
+ walk(doc.children);
102
+ }
103
+
104
+ export {
105
+ FootnoteIndex,
106
+ footnoteIds,
107
+ renumberFootnotes
108
+ };
@@ -4,10 +4,10 @@ import {
4
4
  inlineNodesToRuns,
5
5
  normalizeOoxmlHex,
6
6
  sanitizeOfficeHyperlink
7
- } from "./chunk-DWNNYO5H.js";
7
+ } from "./chunk-A6N6IN3I.js";
8
8
  import {
9
9
  stripHtmlTags
10
- } from "./chunk-6X5XN3CZ.js";
10
+ } from "./chunk-AVOZAKGP.js";
11
11
  import {
12
12
  createPackage
13
13
  } from "./chunk-ILCJ3WFD.js";
@@ -59,6 +59,9 @@ import {
59
59
  import {
60
60
  extToMime
61
61
  } from "./chunk-AONELFLA.js";
62
+ import {
63
+ renumberFootnotes
64
+ } from "./chunk-BXWNU4T5.js";
62
65
 
63
66
  // src/docx/export.ts
64
67
  import { resolveFontFamily } from "@bendyline/squisq/schemas";
@@ -471,6 +474,7 @@ function makeRun(text, format) {
471
474
  if (format.strike) rPrParts.push("<w:strike/>");
472
475
  if (format.color) rPrParts.push(`<w:color w:val="${format.color}"/>`);
473
476
  if (format.code) rPrParts.push(`<w:sz w:val="${DEFAULT_CODE_FONT_SIZE}"/>`);
477
+ if (format.vertAlign) rPrParts.push(`<w:vertAlign w:val="${format.vertAlign}"/>`);
474
478
  const rPr = rPrParts.length > 0 ? `<w:rPr>${rPrParts.join("")}</w:rPr>` : "";
475
479
  return `<w:r>${rPr}<w:t xml:space="preserve">${escapeXml(text)}</w:t></w:r>`;
476
480
  }
@@ -797,7 +801,9 @@ async function docxToMarkdownDoc(data, options = {}) {
797
801
  return { type: "document", children: [] };
798
802
  }
799
803
  const blocks = await convertDocumentStories(body, ctx);
800
- return { type: "document", children: blocks };
804
+ const doc = { type: "document", children: blocks };
805
+ renumberFootnotes(doc);
806
+ return doc;
801
807
  }
802
808
  async function docxToDoc(data, options = {}) {
803
809
  const markdownDoc = await docxToMarkdownDoc(data, options);
@@ -1062,7 +1068,9 @@ async function convertParagraph2(el, ctx) {
1062
1068
  return { type: "paragraph", children: inlines };
1063
1069
  }
1064
1070
  async function convertRuns(paragraphEl, ctx) {
1065
- return mergeAdjacentText(await convertInlineElements(Array.from(paragraphEl.children), ctx));
1071
+ return normalizeParagraphWhitespace(
1072
+ await convertInlineElements(Array.from(paragraphEl.children), ctx)
1073
+ );
1066
1074
  }
1067
1075
  async function convertInlineElements(children, ctx) {
1068
1076
  const result = [];
@@ -1105,6 +1113,9 @@ async function convertRun(runEl, ctx) {
1105
1113
  result.push({ type: "inlineCode", value: text });
1106
1114
  } else {
1107
1115
  let node = { type: "text", value: text };
1116
+ if (format.vertAlign) {
1117
+ node = format.vertAlign === "superscript" ? { type: "superscript", children: [node] } : { type: "subscript", children: [node] };
1118
+ }
1108
1119
  if (format.strike) {
1109
1120
  node = { type: "delete", children: [node] };
1110
1121
  }
@@ -1202,7 +1213,7 @@ function appendInlineGroup(target, group) {
1202
1213
  target.push(...group);
1203
1214
  }
1204
1215
  function parseRunFormat(rPr, ctx) {
1205
- if (!rPr) return { bold: false, italic: false, strike: false, code: false };
1216
+ if (!rPr) return { bold: false, italic: false, strike: false, code: false, vertAlign: null };
1206
1217
  const bold = hasChildElement(rPr, "b") && !isFalseToggle(getFirstChildElement(rPr, "b"));
1207
1218
  const italic = hasChildElement(rPr, "i") && !isFalseToggle(getFirstChildElement(rPr, "i"));
1208
1219
  const strike = hasChildElement(rPr, "strike") && !isFalseToggle(getFirstChildElement(rPr, "strike"));
@@ -1212,7 +1223,10 @@ function parseRunFormat(rPr, ctx) {
1212
1223
  const rFonts = getFirstChildElement(rPr, "rFonts");
1213
1224
  const fontName = rFonts ? getAttr(rFonts, "ascii") ?? getAttr(rFonts, "hAnsi") ?? "" : "";
1214
1225
  const isMonospace = /consolas|courier|mono/i.test(fontName);
1215
- return { bold, italic, strike, code: isCodeStyle || isMonospace };
1226
+ const vertAlignEl = getFirstChildElement(rPr, "vertAlign");
1227
+ const vertAlignVal = vertAlignEl ? getAttr(vertAlignEl, "val") : null;
1228
+ const vertAlign = vertAlignVal === "superscript" || vertAlignVal === "subscript" ? vertAlignVal : null;
1229
+ return { bold, italic, strike, code: isCodeStyle || isMonospace, vertAlign };
1216
1230
  }
1217
1231
  function isFalseToggle(el) {
1218
1232
  const val = getAttr(el, "val");
@@ -1486,6 +1500,64 @@ function mergeAdjacentText(nodes) {
1486
1500
  }
1487
1501
  return result;
1488
1502
  }
1503
+ function isImportedInlineContainer(node) {
1504
+ return node.type === "emphasis" || node.type === "strong" || node.type === "delete" || node.type === "superscript" || node.type === "subscript" || node.type === "link";
1505
+ }
1506
+ function normalizeParagraphWhitespace(nodes) {
1507
+ const result = [];
1508
+ let line = [];
1509
+ const flushLine = () => {
1510
+ const normalized = liftFormattedBoundaryWhitespace(line);
1511
+ takeLeadingHorizontalWhitespace(normalized);
1512
+ takeTrailingHorizontalWhitespace(normalized);
1513
+ result.push(...mergeAdjacentText(normalized));
1514
+ line = [];
1515
+ };
1516
+ for (const node of nodes) {
1517
+ if (node.type === "break") {
1518
+ flushLine();
1519
+ result.push(node);
1520
+ } else {
1521
+ line.push(node);
1522
+ }
1523
+ }
1524
+ flushLine();
1525
+ return result;
1526
+ }
1527
+ function liftFormattedBoundaryWhitespace(nodes) {
1528
+ const result = [];
1529
+ for (const node of nodes) {
1530
+ if (!isImportedInlineContainer(node)) {
1531
+ result.push(node);
1532
+ continue;
1533
+ }
1534
+ const children = liftFormattedBoundaryWhitespace(node.children);
1535
+ const leading = takeLeadingHorizontalWhitespace(children);
1536
+ const trailing = takeTrailingHorizontalWhitespace(children);
1537
+ if (leading) result.push({ type: "text", value: leading });
1538
+ if (children.length > 0) result.push({ ...node, children });
1539
+ if (trailing) result.push({ type: "text", value: trailing });
1540
+ }
1541
+ return mergeAdjacentText(result);
1542
+ }
1543
+ function takeLeadingHorizontalWhitespace(nodes) {
1544
+ const first = nodes[0];
1545
+ if (first?.type !== "text") return "";
1546
+ const match = /^[\t ]+/.exec(first.value);
1547
+ if (!match) return "";
1548
+ first.value = first.value.slice(match[0].length);
1549
+ if (!first.value) nodes.shift();
1550
+ return match[0];
1551
+ }
1552
+ function takeTrailingHorizontalWhitespace(nodes) {
1553
+ const last = nodes[nodes.length - 1];
1554
+ if (last?.type !== "text") return "";
1555
+ const match = /[\t ]+$/.exec(last.value);
1556
+ if (!match) return "";
1557
+ last.value = last.value.slice(0, -match[0].length);
1558
+ if (!last.value) nodes.pop();
1559
+ return match[0];
1560
+ }
1489
1561
 
1490
1562
  export {
1491
1563
  markdownDocToDocx,
@@ -4,11 +4,11 @@ import {
4
4
  inlineNodesToRuns,
5
5
  sanitizeOfficeHyperlink,
6
6
  toOoxmlHex
7
- } from "./chunk-DWNNYO5H.js";
7
+ } from "./chunk-A6N6IN3I.js";
8
8
  import {
9
9
  extractPlainText,
10
10
  stripHtmlTags
11
- } from "./chunk-6X5XN3CZ.js";
11
+ } from "./chunk-AVOZAKGP.js";
12
12
  import {
13
13
  createPackage
14
14
  } from "./chunk-ILCJ3WFD.js";
@@ -1232,6 +1232,9 @@ function makeRun(text, format, style) {
1232
1232
  if (format.bold) rPrParts.push(`b="1"`);
1233
1233
  if (format.italic) rPrParts.push(`i="1"`);
1234
1234
  if (format.strike) rPrParts.push(`strike="sngStrike"`);
1235
+ if (format.vertAlign) {
1236
+ rPrParts.push(`baseline="${format.vertAlign === "superscript" ? 3e4 : -25e3}"`);
1237
+ }
1235
1238
  let innerParts = "";
1236
1239
  if (format.code) {
1237
1240
  innerParts += `<a:solidFill><a:srgbClr val="${style.codeColor}"/></a:solidFill>`;
@@ -105,6 +105,8 @@ var CONTENT_NODE_TYPES = {
105
105
  emphasis: true,
106
106
  strong: true,
107
107
  delete: true,
108
+ superscript: true,
109
+ subscript: true,
108
110
  inlineCode: true,
109
111
  link: true,
110
112
  image: true,
@@ -132,6 +134,8 @@ var COMMON_SUPPORTED = [
132
134
  "emphasis",
133
135
  "strong",
134
136
  "delete",
137
+ "superscript",
138
+ "subscript",
135
139
  "inlineCode",
136
140
  "link",
137
141
  "image",
@@ -407,7 +411,10 @@ function defaultFormats() {
407
411
  };
408
412
  const xlsx = {
409
413
  id: "xlsx",
410
- templateAnnotationHandling: "ignored",
414
+ // Not 'ignored': a `{[dataTable sheet=… anchor=…]}` annotation decides which
415
+ // worksheet a table lands on and at which cell, so annotations demonstrably
416
+ // affect the exported result.
417
+ templateAnnotationHandling: "preserved",
411
418
  label: "Excel (XLSX)",
412
419
  mimeType: MIME.xlsx,
413
420
  extensions: [".xlsx"],
@@ -424,7 +431,10 @@ function defaultFormats() {
424
431
  const warnings = omitted > 0 ? [`XLSX export is tables-only; ${omitted} non-table block(s) were omitted.`] : [];
425
432
  const blob = await markdownDocToXlsx(markdownDoc, {
426
433
  ...optionsFor(options, "xlsx"),
427
- ...options.title !== void 0 ? { title: options.title } : {}
434
+ ...options.title !== void 0 ? { title: options.title } : {},
435
+ // Placement problems (a bad anchor, overlapping regions) degrade rather
436
+ // than throw, so they have to reach the caller as conversion warnings.
437
+ onWarning: (message) => warnings.push(message)
428
438
  });
429
439
  return ok(await toBytes(blob), MIME.xlsx, warnings);
430
440
  }
@@ -6,6 +6,9 @@ import {
6
6
  fontAwesomeFaces,
7
7
  fontAwesomeGlyph
8
8
  } from "./chunk-USU6HTKB.js";
9
+ import {
10
+ FootnoteIndex
11
+ } from "./chunk-BXWNU4T5.js";
9
12
 
10
13
  // src/pdf/export.ts
11
14
  import { PDFDocument, StandardFonts, rgb, PDFString } from "pdf-lib";
@@ -450,6 +453,7 @@ async function createExportContext(pdfDoc, options, doc) {
450
453
  contentWidth: pageWidth - 2 * margin,
451
454
  bottomY: margin,
452
455
  colors: { text: colorText, heading: colorHeading, link: colorLink },
456
+ footnotes: new FootnoteIndex(doc),
453
457
  text: new WinAnsiTracker(),
454
458
  signal: options.signal
455
459
  };
@@ -464,19 +468,25 @@ function newPage(ctx) {
464
468
  ctx.page = page;
465
469
  ctx.y = ctx.pageHeight - ctx.margin;
466
470
  }
471
+ var VERT_ALIGN_SCALE = 0.72;
472
+ function vertAlignShift(kind, baseSize) {
473
+ return kind === "superscript" ? baseSize * 0.33 : baseSize * -0.14;
474
+ }
467
475
  function flattenInlines(nodes, ctx, state) {
468
476
  const spans = [];
469
477
  for (const node of nodes) {
470
478
  switch (node.type) {
471
479
  case "text": {
472
480
  const font = state.code ? state.bold ? ctx.fonts.monoBold : ctx.fonts.mono : pickFont(ctx, state.bold, state.italic);
481
+ const baseSize = state.code ? CODE_FONT_SIZE : ctx.fontSize;
473
482
  spans.push({
474
483
  text: ctx.text.clean(node.value),
475
484
  font,
476
- fontSize: state.code ? CODE_FONT_SIZE : ctx.fontSize,
485
+ fontSize: state.vertAlign ? baseSize * VERT_ALIGN_SCALE : baseSize,
477
486
  color: state.code ? COLOR_CODE_TEXT : state.color ?? ctx.colors.text,
478
487
  link: state.link,
479
- strikethrough: state.strikethrough
488
+ strikethrough: state.strikethrough,
489
+ ...state.vertAlign ? { baselineShift: vertAlignShift(state.vertAlign, baseSize) } : {}
480
490
  });
481
491
  break;
482
492
  }
@@ -498,6 +508,15 @@ function flattenInlines(nodes, ctx, state) {
498
508
  })
499
509
  );
500
510
  break;
511
+ case "superscript":
512
+ case "subscript":
513
+ spans.push(
514
+ ...flattenInlines(node.children, ctx, {
515
+ ...state,
516
+ vertAlign: node.type === "superscript" ? "superscript" : "subscript"
517
+ })
518
+ );
519
+ break;
501
520
  case "inlineCode": {
502
521
  spans.push({
503
522
  text: ctx.text.clean(node.value),
@@ -585,11 +604,13 @@ function flattenInlines(nodes, ctx, state) {
585
604
  break;
586
605
  case "footnoteReference": {
587
606
  const ref = node;
607
+ const marker = ctx.footnotes ? String(ctx.footnotes.cite(ref.identifier).number) : ref.label ?? ref.identifier;
588
608
  spans.push({
589
- text: ctx.text.clean(`[${ref.identifier}]`),
609
+ text: ctx.text.clean(marker),
590
610
  font: ctx.fonts.regular,
591
- fontSize: ctx.fontSize * 0.75,
592
- color: ctx.colors.link
611
+ fontSize: ctx.fontSize * VERT_ALIGN_SCALE,
612
+ color: ctx.colors.link,
613
+ baselineShift: vertAlignShift("superscript", ctx.fontSize)
593
614
  });
594
615
  break;
595
616
  }
@@ -626,9 +647,10 @@ function drawSpans(spans, ctx, availableWidth, x0) {
626
647
  ensureSpace(ctx, lineHeight);
627
648
  let x = x0;
628
649
  for (const span of line) {
650
+ const spanBaseline = ctx.y - span.fontSize + (span.baselineShift ?? 0);
629
651
  ctx.page.drawText(span.text, {
630
652
  x,
631
- y: ctx.y - span.fontSize,
653
+ y: spanBaseline,
632
654
  // pdf-lib y is baseline
633
655
  size: span.fontSize,
634
656
  font: span.font,
@@ -636,7 +658,7 @@ function drawSpans(spans, ctx, availableWidth, x0) {
636
658
  });
637
659
  const textWidth = span.font.widthOfTextAtSize(span.text, span.fontSize);
638
660
  if (span.link) {
639
- const baseline = ctx.y - span.fontSize;
661
+ const baseline = spanBaseline;
640
662
  ctx.page.drawLine({
641
663
  start: { x, y: baseline - 1 },
642
664
  end: { x: x + textWidth, y: baseline - 1 },
@@ -659,7 +681,7 @@ function drawSpans(spans, ctx, availableWidth, x0) {
659
681
  ctx.page.node.addAnnot(annotation);
660
682
  }
661
683
  if (span.strikethrough) {
662
- const midY = ctx.y - span.fontSize * 0.6;
684
+ const midY = spanBaseline + span.fontSize * 0.4;
663
685
  ctx.page.drawLine({
664
686
  start: { x, y: midY },
665
687
  end: { x: x + textWidth, y: midY },
@@ -1022,7 +1044,8 @@ function renderMathBlock(node, ctx, extraIndent) {
1022
1044
  ctx.y -= PARAGRAPH_SPACING;
1023
1045
  }
1024
1046
  function renderFootnoteDefinition(node, ctx, extraIndent) {
1025
- const label = ctx.text.clean(`[${node.identifier}]`);
1047
+ const number = ctx.footnotes?.numberFor(node.identifier);
1048
+ const label = ctx.text.clean(number !== void 0 ? `${number}.` : `[${node.identifier}]`);
1026
1049
  const lineH = ctx.fontSize * LINE_HEIGHT_FACTOR;
1027
1050
  ensureSpace(ctx, lineH);
1028
1051
  ctx.page.drawText(label, {