odf-kit 0.9.8 → 0.10.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 (51) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +49 -2
  3. package/dist/docx/body-reader.d.ts +54 -0
  4. package/dist/docx/body-reader.d.ts.map +1 -0
  5. package/dist/docx/body-reader.js +1124 -0
  6. package/dist/docx/body-reader.js.map +1 -0
  7. package/dist/docx/converter.d.ts +51 -0
  8. package/dist/docx/converter.d.ts.map +1 -0
  9. package/dist/docx/converter.js +799 -0
  10. package/dist/docx/converter.js.map +1 -0
  11. package/dist/docx/index.d.ts +81 -0
  12. package/dist/docx/index.d.ts.map +1 -0
  13. package/dist/docx/index.js +69 -0
  14. package/dist/docx/index.js.map +1 -0
  15. package/dist/docx/numbering.d.ts +42 -0
  16. package/dist/docx/numbering.d.ts.map +1 -0
  17. package/dist/docx/numbering.js +236 -0
  18. package/dist/docx/numbering.js.map +1 -0
  19. package/dist/docx/reader.d.ts +38 -0
  20. package/dist/docx/reader.d.ts.map +1 -0
  21. package/dist/docx/reader.js +512 -0
  22. package/dist/docx/reader.js.map +1 -0
  23. package/dist/docx/relationships.d.ts +27 -0
  24. package/dist/docx/relationships.d.ts.map +1 -0
  25. package/dist/docx/relationships.js +89 -0
  26. package/dist/docx/relationships.js.map +1 -0
  27. package/dist/docx/styles.d.ts +46 -0
  28. package/dist/docx/styles.d.ts.map +1 -0
  29. package/dist/docx/styles.js +383 -0
  30. package/dist/docx/styles.js.map +1 -0
  31. package/dist/docx/types.d.ts +266 -0
  32. package/dist/docx/types.d.ts.map +1 -0
  33. package/dist/docx/types.js +38 -0
  34. package/dist/docx/types.js.map +1 -0
  35. package/dist/index.d.ts +2 -0
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +1 -0
  38. package/dist/index.js.map +1 -1
  39. package/dist/xlsx/converter.d.ts +31 -0
  40. package/dist/xlsx/converter.d.ts.map +1 -0
  41. package/dist/xlsx/converter.js +132 -0
  42. package/dist/xlsx/converter.js.map +1 -0
  43. package/dist/xlsx/index.d.ts +23 -0
  44. package/dist/xlsx/index.d.ts.map +1 -0
  45. package/dist/xlsx/index.js +25 -0
  46. package/dist/xlsx/index.js.map +1 -0
  47. package/dist/xlsx/reader.d.ts +82 -0
  48. package/dist/xlsx/reader.d.ts.map +1 -0
  49. package/dist/xlsx/reader.js +449 -0
  50. package/dist/xlsx/reader.js.map +1 -0
  51. package/package.json +9 -1
@@ -0,0 +1,799 @@
1
+ /**
2
+ * odf-kit — DOCX → ODT converter
3
+ *
4
+ * Walks a DocxDocument model and drives the OdtDocument API to produce
5
+ * an equivalent ODT document.
6
+ *
7
+ * Design decisions:
8
+ * - Style inheritance: the basedOn chain is walked at conversion time so
9
+ * the reader stores only explicitly present properties.
10
+ * - List grouping: consecutive paragraphs sharing a numId are collected
11
+ * into a flat array then converted to a nested ListData tree before
12
+ * being passed to doc.addList().
13
+ * - Footnotes/endnotes: OdtDocument has no text:note API. References are
14
+ * rendered as superscript markers ([1], [2], …); all note content is
15
+ * appended as a "Footnotes" / "Endnotes" section at the document end.
16
+ * - Headers/footers: only the "default" type is mapped (first/even are
17
+ * out of scope for the current OdtDocument API).
18
+ * - Images: bytes and dimensions are taken directly from the DOCX model;
19
+ * EMU→cm conversion already done by the reader.
20
+ */
21
+ import { OdtDocument } from "../odt/document.js";
22
+ // ---------------------------------------------------------------------------
23
+ // Public entry point
24
+ // ---------------------------------------------------------------------------
25
+ /**
26
+ * Convert a parsed DocxDocument into an OdtDocument and return its bytes.
27
+ */
28
+ export async function convertDocxToOdt(docxDoc, options, warnings) {
29
+ const ctx = {
30
+ doc: docxDoc,
31
+ options,
32
+ warnings,
33
+ noteCounter: 0,
34
+ pendingFootnotes: [],
35
+ pendingEndnotes: [],
36
+ };
37
+ const odt = new OdtDocument();
38
+ // -------------------------------------------------------------------------
39
+ // Metadata
40
+ // -------------------------------------------------------------------------
41
+ const meta = options.metadata ?? {};
42
+ const srcMeta = docxDoc.metadata;
43
+ odt.setMetadata({
44
+ title: meta.title ?? srcMeta.title ?? undefined,
45
+ creator: meta.creator ?? srcMeta.creator ?? undefined,
46
+ description: meta.description ?? srcMeta.description ?? undefined,
47
+ });
48
+ // -------------------------------------------------------------------------
49
+ // Page layout
50
+ // -------------------------------------------------------------------------
51
+ const preserveLayout = options.preservePageLayout !== false; // default true
52
+ const layout = docxDoc.pageLayout;
53
+ const pageLayout = {};
54
+ if (preserveLayout && layout.width)
55
+ pageLayout.width = `${layout.width}cm`;
56
+ if (preserveLayout && layout.height)
57
+ pageLayout.height = `${layout.height}cm`;
58
+ if (preserveLayout && layout.orientation)
59
+ pageLayout.orientation = layout.orientation;
60
+ if (preserveLayout && layout.marginTop)
61
+ pageLayout.marginTop = `${layout.marginTop}cm`;
62
+ if (preserveLayout && layout.marginBottom)
63
+ pageLayout.marginBottom = `${layout.marginBottom}cm`;
64
+ if (preserveLayout && layout.marginLeft)
65
+ pageLayout.marginLeft = `${layout.marginLeft}cm`;
66
+ if (preserveLayout && layout.marginRight)
67
+ pageLayout.marginRight = `${layout.marginRight}cm`;
68
+ // Explicit option overrides
69
+ if (options.orientation)
70
+ pageLayout.orientation = options.orientation;
71
+ if (options.pageFormat && !pageLayout.width) {
72
+ const dims = PAGE_FORMAT_DIMS[options.pageFormat];
73
+ if (dims) {
74
+ const isLandscape = (options.orientation ?? layout.orientation) === "landscape";
75
+ pageLayout.width = isLandscape ? dims[1] : dims[0];
76
+ pageLayout.height = isLandscape ? dims[0] : dims[1];
77
+ }
78
+ }
79
+ if (Object.keys(pageLayout).length > 0) {
80
+ odt.setPageLayout(pageLayout);
81
+ }
82
+ // -------------------------------------------------------------------------
83
+ // Header and footer — "default" type only
84
+ // -------------------------------------------------------------------------
85
+ const defaultHeader = docxDoc.headers.find((h) => h.headerType === "default");
86
+ const defaultFooter = docxDoc.footers.find((f) => f.headerType === "default");
87
+ if (defaultHeader) {
88
+ const text = extractPlainText(defaultHeader.body);
89
+ if (text)
90
+ odt.setHeader(text);
91
+ }
92
+ if (defaultFooter) {
93
+ const text = extractPlainText(defaultFooter.body);
94
+ if (text)
95
+ odt.setFooter(text);
96
+ }
97
+ // -------------------------------------------------------------------------
98
+ // Body
99
+ // -------------------------------------------------------------------------
100
+ const grouped = groupListItems(docxDoc.body, docxDoc);
101
+ convertGroupedElements(grouped, odt, ctx);
102
+ // -------------------------------------------------------------------------
103
+ // Footnotes section (appended at end)
104
+ // -------------------------------------------------------------------------
105
+ if (ctx.pendingFootnotes.length > 0) {
106
+ odt.addParagraph(""); // spacer
107
+ odt.addHeading("Footnotes", 6);
108
+ for (const { marker, note } of ctx.pendingFootnotes) {
109
+ const bodyText = extractPlainText(note.body);
110
+ odt.addParagraph(`${marker} ${bodyText}`);
111
+ }
112
+ }
113
+ if (ctx.pendingEndnotes.length > 0) {
114
+ odt.addParagraph("");
115
+ odt.addHeading("Endnotes", 6);
116
+ for (const { marker, note } of ctx.pendingEndnotes) {
117
+ const bodyText = extractPlainText(note.body);
118
+ odt.addParagraph(`${marker} ${bodyText}`);
119
+ }
120
+ }
121
+ return odt.save();
122
+ }
123
+ // ---------------------------------------------------------------------------
124
+ // Page format dimensions [portrait-width, portrait-height]
125
+ // ---------------------------------------------------------------------------
126
+ const PAGE_FORMAT_DIMS = {
127
+ A4: ["21cm", "29.7cm"],
128
+ letter: ["21.59cm", "27.94cm"],
129
+ legal: ["21.59cm", "35.56cm"],
130
+ A3: ["29.7cm", "42cm"],
131
+ A5: ["14.8cm", "21cm"],
132
+ };
133
+ function groupListItems(elements, docxDoc) {
134
+ const result = [];
135
+ let i = 0;
136
+ while (i < elements.length) {
137
+ const el = elements[i];
138
+ if (el.type === "paragraph" && el.props.list) {
139
+ const numId = el.props.list.numId;
140
+ const group = { kind: "listGroup", numId, items: [] };
141
+ while (i < elements.length) {
142
+ const cur = elements[i];
143
+ if (cur.type !== "paragraph" || !cur.props.list || cur.props.list.numId !== numId)
144
+ break;
145
+ const level = cur.props.list.level;
146
+ const numEntry = resolveNumberingLevel(numId, level, docxDoc);
147
+ group.items.push({
148
+ level,
149
+ runs: cur.runs,
150
+ isOrdered: numEntry?.isOrdered ?? false,
151
+ numFormat: numEntry?.numFormat ?? "bullet",
152
+ start: numEntry?.start ?? 1,
153
+ });
154
+ i++;
155
+ }
156
+ result.push(group);
157
+ }
158
+ else {
159
+ result.push(el);
160
+ i++;
161
+ }
162
+ }
163
+ return result;
164
+ }
165
+ // ---------------------------------------------------------------------------
166
+ // Body element conversion
167
+ // ---------------------------------------------------------------------------
168
+ function convertGroupedElements(elements, odt, ctx) {
169
+ for (const el of elements) {
170
+ if ("kind" in el && el.kind === "listGroup") {
171
+ convertListGroup(el, odt, ctx);
172
+ }
173
+ else {
174
+ convertBodyElement(el, odt, ctx);
175
+ }
176
+ }
177
+ }
178
+ function convertBodyElement(el, odt, ctx) {
179
+ switch (el.type) {
180
+ case "pageBreak":
181
+ odt.addPageBreak();
182
+ break;
183
+ case "paragraph":
184
+ convertParagraph(el, odt, ctx);
185
+ break;
186
+ case "table":
187
+ convertTable(el, odt, ctx);
188
+ break;
189
+ }
190
+ }
191
+ // ---------------------------------------------------------------------------
192
+ // Paragraph conversion
193
+ // ---------------------------------------------------------------------------
194
+ function convertParagraph(para, odt, ctx) {
195
+ // Resolve effective heading level
196
+ const headingLevel = resolveHeadingLevel(para, ctx);
197
+ // Resolve paragraph options from style chain + direct props
198
+ const paraOptions = resolveParaOptions(para, ctx);
199
+ // Build content callback
200
+ const content = (p) => buildParagraphContent(para.runs, p, ctx);
201
+ if (headingLevel !== null) {
202
+ odt.addHeading(content, headingLevel, paraOptions);
203
+ }
204
+ else {
205
+ odt.addParagraph(content, paraOptions);
206
+ }
207
+ }
208
+ // ---------------------------------------------------------------------------
209
+ // Heading level resolution
210
+ // ---------------------------------------------------------------------------
211
+ function resolveHeadingLevel(para, ctx) {
212
+ // 1. Caller styleMap option — highest priority
213
+ if (ctx.options.styleMap && para.styleId) {
214
+ const styleName = ctx.doc.styles.get(para.styleId)?.name;
215
+ if (styleName && ctx.options.styleMap[styleName] !== undefined) {
216
+ return ctx.options.styleMap[styleName];
217
+ }
218
+ }
219
+ // 2. Paragraph-level heading from reader (outlineLvl or style name)
220
+ if (para.headingLevel !== null) {
221
+ return para.headingLevel;
222
+ }
223
+ // 3. Style chain — check each style in basedOn chain for heading level
224
+ if (para.styleId) {
225
+ let entry = ctx.doc.styles.get(para.styleId);
226
+ while (entry) {
227
+ if (entry.headingLevel !== null)
228
+ return entry.headingLevel;
229
+ entry = entry.basedOn ? (ctx.doc.styles.get(entry.basedOn) ?? undefined) : undefined;
230
+ }
231
+ }
232
+ return null;
233
+ }
234
+ // ---------------------------------------------------------------------------
235
+ // Paragraph options resolution (style chain + direct props)
236
+ // ---------------------------------------------------------------------------
237
+ function resolveParaOptions(para, ctx) {
238
+ // Collect inherited props from style chain (root → leaf order)
239
+ const chain = getStyleChain(para.styleId, ctx.doc.styles);
240
+ const inherited = {};
241
+ for (const entry of chain) {
242
+ if (entry.pPr)
243
+ mergeParaProps(inherited, entry.pPr);
244
+ }
245
+ // Direct paragraph props override inherited
246
+ mergeParaProps(inherited, para.props);
247
+ return paraPropsToOptions(inherited);
248
+ }
249
+ function paraPropsToOptions(props) {
250
+ const opts = {};
251
+ let hasAny = false;
252
+ if (props.alignment) {
253
+ opts.align = props.alignment;
254
+ hasAny = true;
255
+ }
256
+ if (props.spaceBefore != null) {
257
+ opts.spaceBefore = `${props.spaceBefore}cm`;
258
+ hasAny = true;
259
+ }
260
+ if (props.spaceAfter != null) {
261
+ opts.spaceAfter = `${props.spaceAfter}cm`;
262
+ hasAny = true;
263
+ }
264
+ if (props.lineHeight != null) {
265
+ opts.lineHeight = props.lineHeight;
266
+ hasAny = true;
267
+ }
268
+ if (props.indentLeft != null) {
269
+ opts.indentLeft = `${props.indentLeft}cm`;
270
+ hasAny = true;
271
+ }
272
+ if (props.indentFirstLine != null) {
273
+ opts.indentFirst = `${props.indentFirstLine}cm`;
274
+ hasAny = true;
275
+ }
276
+ if (props.borderBottom) {
277
+ const b = props.borderBottom;
278
+ opts.borderBottom = `${b.widthPt}pt ${b.style} #${b.color}`;
279
+ hasAny = true;
280
+ }
281
+ return hasAny ? opts : undefined;
282
+ }
283
+ // ---------------------------------------------------------------------------
284
+ // Inline content builder
285
+ // ---------------------------------------------------------------------------
286
+ function buildParagraphContent(runs, p, ctx) {
287
+ for (const el of runs) {
288
+ switch (el.type) {
289
+ case "run":
290
+ convertRun(el, p, ctx);
291
+ break;
292
+ case "hyperlink": {
293
+ if (el.runs.length === 0)
294
+ break;
295
+ // Merge all run text into one link; use formatting of first run
296
+ const text = el.runs.map((r) => r.text).join("");
297
+ const fmt = el.runs[0] ? resolveRunFormatting(el.runs[0], ctx) : undefined;
298
+ p.addLink(text, el.url, fmt ?? undefined);
299
+ break;
300
+ }
301
+ case "inlineImage": {
302
+ const imgEntry = ctx.doc.images.get(el.rId);
303
+ if (!imgEntry) {
304
+ ctx.warnings.push(`Image rId "${el.rId}" not found in image map — skipped`);
305
+ break;
306
+ }
307
+ p.addImage(imgEntry.bytes, {
308
+ width: `${el.widthCm}cm`,
309
+ height: `${el.heightCm}cm`,
310
+ mimeType: imgEntry.mimeType,
311
+ anchor: "as-character",
312
+ alt: el.altText ?? undefined,
313
+ });
314
+ break;
315
+ }
316
+ case "footnoteReference": {
317
+ ctx.noteCounter++;
318
+ const marker = `[${ctx.noteCounter}]`;
319
+ p.addText(marker, { superscript: true });
320
+ const note = ctx.doc.footnotes.get(el.id);
321
+ if (note)
322
+ ctx.pendingFootnotes.push({ marker, note });
323
+ break;
324
+ }
325
+ case "endnoteReference": {
326
+ ctx.noteCounter++;
327
+ const marker = `[${ctx.noteCounter}]`;
328
+ p.addText(marker, { superscript: true });
329
+ const note = ctx.doc.endnotes.get(el.id);
330
+ if (note)
331
+ ctx.pendingEndnotes.push({ marker, note });
332
+ break;
333
+ }
334
+ case "bookmark":
335
+ if (el.position === "start") {
336
+ p.addBookmark(el.name);
337
+ }
338
+ // "end" bookmarks have no ODT equivalent at the inline level — skip
339
+ break;
340
+ case "tab":
341
+ p.addTab();
342
+ break;
343
+ case "lineBreak":
344
+ p.addLineBreak();
345
+ break;
346
+ }
347
+ }
348
+ }
349
+ // ---------------------------------------------------------------------------
350
+ // Run conversion
351
+ // ---------------------------------------------------------------------------
352
+ function convertRun(run, p, ctx) {
353
+ if (!run.text)
354
+ return;
355
+ const fmt = resolveRunFormatting(run, ctx);
356
+ p.addText(run.text, fmt ?? undefined);
357
+ }
358
+ // ---------------------------------------------------------------------------
359
+ // Run formatting resolution (style chain + direct props)
360
+ // ---------------------------------------------------------------------------
361
+ function resolveRunFormatting(run, ctx) {
362
+ // Collect inherited props from character style chain
363
+ const chain = getStyleChain(run.props.rStyleId, ctx.doc.styles);
364
+ const inherited = {};
365
+ for (const entry of chain) {
366
+ if (entry.rPr)
367
+ mergeRunProps(inherited, entry.rPr);
368
+ }
369
+ // Direct run props override inherited
370
+ mergeRunProps(inherited, run.props);
371
+ return runPropsToFormatting(inherited);
372
+ }
373
+ function runPropsToFormatting(props) {
374
+ const fmt = {};
375
+ let hasAny = false;
376
+ if (props.bold) {
377
+ fmt.bold = true;
378
+ hasAny = true;
379
+ }
380
+ if (props.italic) {
381
+ fmt.italic = true;
382
+ hasAny = true;
383
+ }
384
+ if (props.underline) {
385
+ fmt.underline = true;
386
+ hasAny = true;
387
+ }
388
+ if (props.strikethrough || props.doubleStrikethrough) {
389
+ fmt.strikethrough = true;
390
+ hasAny = true;
391
+ }
392
+ if (props.superscript) {
393
+ fmt.superscript = true;
394
+ hasAny = true;
395
+ }
396
+ if (props.subscript) {
397
+ fmt.subscript = true;
398
+ hasAny = true;
399
+ }
400
+ if (props.smallCaps) {
401
+ fmt.smallCaps = true;
402
+ hasAny = true;
403
+ }
404
+ if (props.allCaps) {
405
+ fmt.textTransform = "uppercase";
406
+ hasAny = true;
407
+ }
408
+ if (props.color) {
409
+ fmt.color = `#${props.color}`;
410
+ hasAny = true;
411
+ }
412
+ if (props.fontSize != null) {
413
+ fmt.fontSize = props.fontSize; // already in points from reader
414
+ hasAny = true;
415
+ }
416
+ if (props.fontFamily) {
417
+ fmt.fontFamily = props.fontFamily;
418
+ hasAny = true;
419
+ }
420
+ if (props.highlight) {
421
+ const hex = HIGHLIGHT_COLORS[props.highlight.toLowerCase()];
422
+ if (hex) {
423
+ fmt.highlightColor = hex;
424
+ hasAny = true;
425
+ }
426
+ }
427
+ return hasAny ? fmt : null;
428
+ }
429
+ // DOCX highlight color names → CSS hex values
430
+ const HIGHLIGHT_COLORS = {
431
+ yellow: "#FFFF00",
432
+ green: "#00FF00",
433
+ cyan: "#00FFFF",
434
+ magenta: "#FF00FF",
435
+ red: "#FF0000",
436
+ blue: "#0000FF",
437
+ darkblue: "#00008B",
438
+ darkcyan: "#008B8B",
439
+ darkgreen: "#006400",
440
+ darkmagenta: "#8B008B",
441
+ darkred: "#8B0000",
442
+ darkyellow: "#8B8B00",
443
+ darkgray: "#A9A9A9",
444
+ lightgray: "#D3D3D3",
445
+ black: "#000000",
446
+ white: "#FFFFFF",
447
+ };
448
+ // ---------------------------------------------------------------------------
449
+ // Table conversion
450
+ // ---------------------------------------------------------------------------
451
+ function convertTable(table, odt, ctx) {
452
+ const tableOptions = {};
453
+ if (table.columnWidths.length > 0) {
454
+ tableOptions.columnWidths = table.columnWidths.map((w) => `${w}cm`);
455
+ }
456
+ // Build rowspan map: track which cells are covered by vertical merges
457
+ // Key: "rowIndex:colIndex", value: remaining rows still covered
458
+ const coveredCells = new Map();
459
+ odt.addTable((t) => {
460
+ table.rows.forEach((row, rowIdx) => {
461
+ t.addRow((r) => {
462
+ let colIdx = 0;
463
+ for (const cell of row.cells) {
464
+ // Skip cells covered by a rowspan from a previous row
465
+ while (coveredCells.get(`${rowIdx}:${colIdx}`) ?? 0 > 0) {
466
+ colIdx++;
467
+ }
468
+ const cellOptions = buildCellOptions(cell);
469
+ // Register this cell's rowspan coverage in subsequent rows
470
+ if (cell.vMerge === "restart" && cell.colSpan >= 1) {
471
+ // We need to look ahead to count how many rows this spans
472
+ const rowsSpanned = countRowSpan(table.rows, rowIdx, colIdx);
473
+ if (rowsSpanned > 1) {
474
+ cellOptions.rowSpan = rowsSpanned;
475
+ for (let r2 = rowIdx + 1; r2 < rowIdx + rowsSpanned; r2++) {
476
+ for (let c2 = colIdx; c2 < colIdx + cell.colSpan; c2++) {
477
+ coveredCells.set(`${r2}:${c2}`, 1);
478
+ }
479
+ }
480
+ }
481
+ }
482
+ // Skip continuation cells (covered by a vMerge restart)
483
+ if (cell.vMerge === "continue") {
484
+ colIdx += cell.colSpan;
485
+ continue;
486
+ }
487
+ const cellContent = buildCellContent(cell, ctx);
488
+ r.addCell(cellContent, cellOptions);
489
+ colIdx += cell.colSpan;
490
+ }
491
+ });
492
+ });
493
+ }, tableOptions);
494
+ }
495
+ function buildCellOptions(cell) {
496
+ const opts = {};
497
+ if (cell.colSpan > 1)
498
+ opts.colSpan = cell.colSpan;
499
+ if (cell.backgroundColor)
500
+ opts.backgroundColor = `#${cell.backgroundColor}`;
501
+ if (cell.verticalAlign) {
502
+ opts.verticalAlign = cell.verticalAlign === "center" ? "middle" : cell.verticalAlign;
503
+ }
504
+ return opts;
505
+ }
506
+ function buildCellContent(cell, ctx) {
507
+ return (c) => {
508
+ // Collect all text from all paragraphs in the cell
509
+ let first = true;
510
+ for (const bodyEl of cell.body) {
511
+ if (bodyEl.type !== "paragraph")
512
+ continue;
513
+ // Add a separator between multiple paragraphs in one cell
514
+ if (!first)
515
+ c.addText(" / ");
516
+ first = false;
517
+ for (const run of bodyEl.runs) {
518
+ if (run.type === "run" && run.text) {
519
+ const fmt = resolveRunFormatting(run, ctx);
520
+ if (fmt) {
521
+ c.addText(run.text, fmt);
522
+ }
523
+ else {
524
+ c.addText(run.text);
525
+ }
526
+ }
527
+ else if (run.type === "hyperlink") {
528
+ const text = run.runs.map((r) => r.text).join("");
529
+ if (text)
530
+ c.addText(text);
531
+ }
532
+ // Other inline types (images, bookmarks etc.) not supported in cells — skip
533
+ }
534
+ }
535
+ };
536
+ }
537
+ /**
538
+ * Count how many consecutive rows a cell at (rowIdx, colIdx) spans,
539
+ * by looking for vMerge="continue" cells at the same column position
540
+ * in subsequent rows.
541
+ */
542
+ function countRowSpan(rows, startRow, colIdx) {
543
+ let count = 1;
544
+ for (let r = startRow + 1; r < rows.length; r++) {
545
+ let col = 0;
546
+ let found = false;
547
+ for (const cell of rows[r].cells) {
548
+ if (col === colIdx && cell.vMerge === "continue") {
549
+ found = true;
550
+ break;
551
+ }
552
+ col += cell.colSpan;
553
+ }
554
+ if (!found)
555
+ break;
556
+ count++;
557
+ }
558
+ return count;
559
+ }
560
+ // ---------------------------------------------------------------------------
561
+ // List conversion
562
+ // ---------------------------------------------------------------------------
563
+ function convertListGroup(group, odt, ctx) {
564
+ if (group.items.length === 0)
565
+ return;
566
+ // Determine list type from the level-0 items' format
567
+ const level0 = group.items.find((i) => i.level === 0);
568
+ const listOptions = buildListOptions(level0?.isOrdered ?? false, level0?.numFormat ?? "bullet", level0?.start ?? 1);
569
+ const listData = buildNestedListData(group.items, 0, 0, ctx);
570
+ listData.options = listOptions;
571
+ odt.addList((builder) => {
572
+ populateListBuilder(builder, listData, ctx);
573
+ }, listOptions);
574
+ }
575
+ function buildListOptions(isOrdered, numFormat, start) {
576
+ if (!isOrdered)
577
+ return { type: "bullet" };
578
+ const fmt = docxNumFormatToOdt(numFormat);
579
+ const opts = { type: "numbered", numFormat: fmt };
580
+ if (start !== 1)
581
+ opts.startValue = start;
582
+ return opts;
583
+ }
584
+ function docxNumFormatToOdt(numFormat) {
585
+ switch (numFormat) {
586
+ case "lowerLetter":
587
+ return "a";
588
+ case "upperLetter":
589
+ return "A";
590
+ case "lowerRoman":
591
+ return "i";
592
+ case "upperRoman":
593
+ return "I";
594
+ default:
595
+ return "1"; // decimal, ordinal, etc.
596
+ }
597
+ }
598
+ /**
599
+ * Build a nested ListData tree from a flat array of ListGroupItems.
600
+ * Processes items starting at `startIdx`, at `currentLevel`, and
601
+ * returns the tree plus the index of the next unconsumed item.
602
+ */
603
+ function buildNestedListData(items, startIdx, currentLevel, ctx) {
604
+ const listItems = [];
605
+ let i = startIdx;
606
+ while (i < items.length) {
607
+ const item = items[i];
608
+ if (item.level < currentLevel) {
609
+ // Return to parent level — stop processing here
610
+ break;
611
+ }
612
+ if (item.level > currentLevel) {
613
+ // Higher level than expected — attach as nested to last item if possible
614
+ // (handles malformed DOCX where level jumps without a parent)
615
+ if (listItems.length === 0) {
616
+ listItems.push({ runs: [] });
617
+ }
618
+ const nested = buildNestedListData(items, i, item.level, ctx);
619
+ const lastItem = listItems[listItems.length - 1];
620
+ lastItem.nested = nested;
621
+ // Advance past all items consumed at this level
622
+ i = advancePastLevel(items, i, item.level);
623
+ continue;
624
+ }
625
+ // Same level — add this item
626
+ const runs = buildListItemRuns(item.runs, ctx);
627
+ const listItem = { runs };
628
+ // Check if next items are at a deeper level — if so, attach as nested
629
+ const nextIdx = i + 1;
630
+ if (nextIdx < items.length && items[nextIdx].level > currentLevel) {
631
+ const nestedOptions = buildListOptions(items[nextIdx].isOrdered, items[nextIdx].numFormat, items[nextIdx].start);
632
+ const nested = buildNestedListData(items, nextIdx, items[nextIdx].level, ctx);
633
+ nested.options = nestedOptions;
634
+ listItem.nested = nested;
635
+ i = advancePastLevel(items, nextIdx, items[nextIdx].level);
636
+ }
637
+ else {
638
+ i++;
639
+ }
640
+ listItems.push(listItem);
641
+ }
642
+ return { items: listItems };
643
+ }
644
+ function advancePastLevel(items, startIdx, level) {
645
+ let i = startIdx;
646
+ while (i < items.length && items[i].level >= level)
647
+ i++;
648
+ return i;
649
+ }
650
+ function buildListItemRuns(inlines, ctx) {
651
+ const runs = [];
652
+ for (const el of inlines) {
653
+ if (el.type === "run" && el.text) {
654
+ const fmt = resolveRunFormatting(el, ctx);
655
+ runs.push({ text: el.text, formatting: fmt ?? undefined });
656
+ }
657
+ else if (el.type === "hyperlink") {
658
+ const text = el.runs.map((r) => r.text).join("");
659
+ if (text)
660
+ runs.push({ text, link: el.url });
661
+ }
662
+ else if (el.type === "tab") {
663
+ runs.push({ text: "", field: "tab" });
664
+ }
665
+ else if (el.type === "lineBreak") {
666
+ runs.push({ text: "", lineBreak: true });
667
+ }
668
+ }
669
+ return runs;
670
+ }
671
+ function populateListBuilder(builder, listData, ctx) {
672
+ for (const item of listData.items) {
673
+ if (item.runs.length > 0) {
674
+ builder.addItem((p) => {
675
+ for (const run of item.runs) {
676
+ if (run.text)
677
+ p.addText(run.text, run.formatting);
678
+ else if (run.field === "tab")
679
+ p.addTab();
680
+ else if (run.lineBreak)
681
+ p.addLineBreak();
682
+ }
683
+ });
684
+ }
685
+ else {
686
+ builder.addItem("");
687
+ }
688
+ if (item.nested && item.nested.items.length > 0) {
689
+ builder.addNested((sub) => {
690
+ populateListBuilder(sub, item.nested, ctx);
691
+ });
692
+ }
693
+ }
694
+ }
695
+ // ---------------------------------------------------------------------------
696
+ // Style chain utilities
697
+ // ---------------------------------------------------------------------------
698
+ /**
699
+ * Collect the style chain from the root basedOn style down to the given
700
+ * styleId (root-first order so child overrides parent).
701
+ */
702
+ function getStyleChain(styleId, styles) {
703
+ if (!styleId)
704
+ return [];
705
+ const chain = [];
706
+ let id = styleId;
707
+ const visited = new Set();
708
+ while (id && !visited.has(id)) {
709
+ visited.add(id);
710
+ const entry = styles.get(id);
711
+ if (!entry)
712
+ break;
713
+ chain.unshift(entry); // prepend so chain is root-first
714
+ id = entry.basedOn;
715
+ }
716
+ return chain;
717
+ }
718
+ function mergeParaProps(base, override) {
719
+ if (override.alignment !== undefined)
720
+ base.alignment = override.alignment;
721
+ if (override.pageBreakBefore !== undefined)
722
+ base.pageBreakBefore = override.pageBreakBefore;
723
+ if (override.spaceBefore !== undefined)
724
+ base.spaceBefore = override.spaceBefore;
725
+ if (override.spaceAfter !== undefined)
726
+ base.spaceAfter = override.spaceAfter;
727
+ if (override.lineHeight !== undefined)
728
+ base.lineHeight = override.lineHeight;
729
+ if (override.indentLeft !== undefined)
730
+ base.indentLeft = override.indentLeft;
731
+ if (override.indentRight !== undefined)
732
+ base.indentRight = override.indentRight;
733
+ if (override.indentFirstLine !== undefined)
734
+ base.indentFirstLine = override.indentFirstLine;
735
+ if (override.list !== undefined)
736
+ base.list = override.list;
737
+ if (override.borderBottom !== undefined)
738
+ base.borderBottom = override.borderBottom;
739
+ }
740
+ function mergeRunProps(base, override) {
741
+ if (override.bold !== undefined)
742
+ base.bold = override.bold;
743
+ if (override.italic !== undefined)
744
+ base.italic = override.italic;
745
+ if (override.underline !== undefined)
746
+ base.underline = override.underline;
747
+ if (override.strikethrough !== undefined)
748
+ base.strikethrough = override.strikethrough;
749
+ if (override.doubleStrikethrough !== undefined)
750
+ base.doubleStrikethrough = override.doubleStrikethrough;
751
+ if (override.superscript !== undefined)
752
+ base.superscript = override.superscript;
753
+ if (override.subscript !== undefined)
754
+ base.subscript = override.subscript;
755
+ if (override.smallCaps !== undefined)
756
+ base.smallCaps = override.smallCaps;
757
+ if (override.allCaps !== undefined)
758
+ base.allCaps = override.allCaps;
759
+ if (override.color !== undefined)
760
+ base.color = override.color;
761
+ if (override.fontSize !== undefined)
762
+ base.fontSize = override.fontSize;
763
+ if (override.highlight !== undefined)
764
+ base.highlight = override.highlight;
765
+ if (override.fontFamily !== undefined)
766
+ base.fontFamily = override.fontFamily;
767
+ if (override.lang !== undefined)
768
+ base.lang = override.lang;
769
+ if (override.rStyleId !== undefined)
770
+ base.rStyleId = override.rStyleId;
771
+ }
772
+ // ---------------------------------------------------------------------------
773
+ // Numbering lookup — resolves a numId + level to a NumberingLevel
774
+ // ---------------------------------------------------------------------------
775
+ function resolveNumberingLevel(numId, level, docxDoc) {
776
+ const levels = docxDoc.numbering?.get(numId);
777
+ if (!levels)
778
+ return null;
779
+ return levels[level] ?? levels[0] ?? null;
780
+ }
781
+ // ---------------------------------------------------------------------------
782
+ // Plain text extraction — used for headers/footers and footnote content
783
+ // ---------------------------------------------------------------------------
784
+ function extractPlainText(elements) {
785
+ const parts = [];
786
+ for (const el of elements) {
787
+ if (el.type !== "paragraph")
788
+ continue;
789
+ for (const run of el.runs) {
790
+ if (run.type === "run")
791
+ parts.push(run.text);
792
+ else if (run.type === "hyperlink") {
793
+ parts.push(run.runs.map((r) => r.text).join(""));
794
+ }
795
+ }
796
+ }
797
+ return parts.join("").trim();
798
+ }
799
+ //# sourceMappingURL=converter.js.map