@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.
package/NOTICE.md CHANGED
@@ -10,7 +10,7 @@ Third-party components remain under their respective license terms.
10
10
  | ---------------- | ------- | ----------------------- | ---------------------------------- |
11
11
  | @pdf-lib/fontkit | 1.1.1 | MIT | https://github.com/Hopding/fontkit |
12
12
  | @pdf-lib/upng | 1.0.1 | MIT | https://github.com/Hopding/upng |
13
- | @xmldom/xmldom | 0.9.11 | MIT | https://github.com/xmldom/xmldom |
13
+ | @xmldom/xmldom | 0.9.12 | MIT | https://github.com/xmldom/xmldom |
14
14
  | jszip | 3.10.1 | MIT OR GPL-3.0-or-later | https://github.com/Stuk/jszip |
15
15
  | pdf-lib | 1.17.1 | MIT | https://pdf-lib.js.org |
16
16
  | pdfjs-dist | 4.10.38 | Apache-2.0 | https://mozilla.github.io/pdf.js |
@@ -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);
@@ -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
  }
@@ -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>`;