@cj-tech-master/excelts 9.6.0 → 9.6.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.
Files changed (112) hide show
  1. package/dist/browser/modules/archive/io/random-access.d.ts +1 -1
  2. package/dist/browser/modules/excel/workbook.browser.d.ts +1 -1
  3. package/dist/browser/modules/excel/xlsx/xform/comment/comment-xform.d.ts +3 -0
  4. package/dist/browser/modules/excel/xlsx/xform/comment/comment-xform.js +30 -7
  5. package/dist/browser/modules/pdf/excel-bridge.d.ts +32 -0
  6. package/dist/browser/modules/pdf/excel-bridge.js +67 -1
  7. package/dist/browser/modules/pdf/word-bridge.d.ts +20 -15
  8. package/dist/browser/modules/pdf/word-bridge.js +49 -34
  9. package/dist/browser/modules/stream/common/consumers.d.ts +2 -1
  10. package/dist/browser/modules/word/advanced/diff.js +125 -13
  11. package/dist/browser/modules/word/advanced/drawing-shapes.js +3 -0
  12. package/dist/browser/modules/word/bridge/excel-bridge.js +21 -1
  13. package/dist/browser/modules/word/builder/document-handle.d.ts +2 -0
  14. package/dist/browser/modules/word/builder/document-handle.js +14 -2
  15. package/dist/browser/modules/word/builder/paragraph-builders.js +10 -1
  16. package/dist/browser/modules/word/builder/run-builders.d.ts +19 -2
  17. package/dist/browser/modules/word/builder/run-builders.js +2 -6
  18. package/dist/browser/modules/word/convert/odt/odt.js +6 -1
  19. package/dist/browser/modules/word/layout/layout-full.d.ts +12 -0
  20. package/dist/browser/modules/word/layout/layout-full.js +74 -9
  21. package/dist/browser/modules/word/layout/layout-model.d.ts +12 -0
  22. package/dist/browser/modules/word/query/merge.js +26 -10
  23. package/dist/browser/modules/word/query/split.js +68 -2
  24. package/dist/browser/modules/word/reader/docx-reader.js +23 -0
  25. package/dist/browser/modules/word/security/cfb-reader.d.ts +14 -3
  26. package/dist/browser/modules/word/security/cfb-reader.js +271 -153
  27. package/dist/browser/modules/word/security/document-protection.js +10 -4
  28. package/dist/browser/modules/word/security/encryption.js +194 -32
  29. package/dist/browser/modules/word/types.d.ts +17 -0
  30. package/dist/browser/modules/word/units.d.ts +10 -4
  31. package/dist/browser/modules/word/units.js +10 -4
  32. package/dist/browser/modules/word/writer/document-writer.js +28 -4
  33. package/dist/browser/modules/word/writer/docx-packager.js +45 -5
  34. package/dist/browser/modules/word/writer/image-writer.d.ts +1 -1
  35. package/dist/browser/modules/word/writer/image-writer.js +2 -2
  36. package/dist/browser/modules/word/writer/render-context.d.ts +15 -0
  37. package/dist/browser/modules/word/writer/run-writer.js +8 -4
  38. package/dist/browser/modules/word/writer/section-writer.js +46 -35
  39. package/dist/browser/modules/word/writer/streaming-writer.js +4 -0
  40. package/dist/browser/modules/word/writer/styles-writer.js +11 -0
  41. package/dist/browser/modules/word/writer/table-writer.js +6 -0
  42. package/dist/cjs/modules/excel/xlsx/xform/comment/comment-xform.js +30 -7
  43. package/dist/cjs/modules/pdf/excel-bridge.js +67 -0
  44. package/dist/cjs/modules/pdf/word-bridge.js +49 -34
  45. package/dist/cjs/modules/word/advanced/diff.js +125 -13
  46. package/dist/cjs/modules/word/advanced/drawing-shapes.js +3 -0
  47. package/dist/cjs/modules/word/bridge/excel-bridge.js +21 -1
  48. package/dist/cjs/modules/word/builder/document-handle.js +14 -2
  49. package/dist/cjs/modules/word/builder/paragraph-builders.js +10 -1
  50. package/dist/cjs/modules/word/builder/run-builders.js +2 -6
  51. package/dist/cjs/modules/word/convert/odt/odt.js +6 -1
  52. package/dist/cjs/modules/word/layout/layout-full.js +74 -9
  53. package/dist/cjs/modules/word/query/merge.js +26 -10
  54. package/dist/cjs/modules/word/query/split.js +68 -2
  55. package/dist/cjs/modules/word/reader/docx-reader.js +23 -0
  56. package/dist/cjs/modules/word/security/cfb-reader.js +271 -153
  57. package/dist/cjs/modules/word/security/document-protection.js +10 -4
  58. package/dist/cjs/modules/word/security/encryption.js +193 -31
  59. package/dist/cjs/modules/word/units.js +10 -4
  60. package/dist/cjs/modules/word/writer/document-writer.js +28 -4
  61. package/dist/cjs/modules/word/writer/docx-packager.js +45 -5
  62. package/dist/cjs/modules/word/writer/image-writer.js +2 -2
  63. package/dist/cjs/modules/word/writer/run-writer.js +8 -4
  64. package/dist/cjs/modules/word/writer/section-writer.js +46 -35
  65. package/dist/cjs/modules/word/writer/streaming-writer.js +4 -0
  66. package/dist/cjs/modules/word/writer/styles-writer.js +11 -0
  67. package/dist/cjs/modules/word/writer/table-writer.js +6 -0
  68. package/dist/esm/modules/excel/xlsx/xform/comment/comment-xform.js +30 -7
  69. package/dist/esm/modules/pdf/excel-bridge.js +67 -1
  70. package/dist/esm/modules/pdf/word-bridge.js +49 -34
  71. package/dist/esm/modules/word/advanced/diff.js +125 -13
  72. package/dist/esm/modules/word/advanced/drawing-shapes.js +3 -0
  73. package/dist/esm/modules/word/bridge/excel-bridge.js +21 -1
  74. package/dist/esm/modules/word/builder/document-handle.js +14 -2
  75. package/dist/esm/modules/word/builder/paragraph-builders.js +10 -1
  76. package/dist/esm/modules/word/builder/run-builders.js +2 -6
  77. package/dist/esm/modules/word/convert/odt/odt.js +6 -1
  78. package/dist/esm/modules/word/layout/layout-full.js +74 -9
  79. package/dist/esm/modules/word/query/merge.js +26 -10
  80. package/dist/esm/modules/word/query/split.js +68 -2
  81. package/dist/esm/modules/word/reader/docx-reader.js +23 -0
  82. package/dist/esm/modules/word/security/cfb-reader.js +271 -153
  83. package/dist/esm/modules/word/security/document-protection.js +10 -4
  84. package/dist/esm/modules/word/security/encryption.js +194 -32
  85. package/dist/esm/modules/word/units.js +10 -4
  86. package/dist/esm/modules/word/writer/document-writer.js +28 -4
  87. package/dist/esm/modules/word/writer/docx-packager.js +45 -5
  88. package/dist/esm/modules/word/writer/image-writer.js +2 -2
  89. package/dist/esm/modules/word/writer/run-writer.js +8 -4
  90. package/dist/esm/modules/word/writer/section-writer.js +46 -35
  91. package/dist/esm/modules/word/writer/streaming-writer.js +4 -0
  92. package/dist/esm/modules/word/writer/styles-writer.js +11 -0
  93. package/dist/esm/modules/word/writer/table-writer.js +6 -0
  94. package/dist/iife/excelts.iife.js +20 -8
  95. package/dist/iife/excelts.iife.js.map +1 -1
  96. package/dist/iife/excelts.iife.min.js +2 -2
  97. package/dist/types/modules/archive/io/random-access.d.ts +1 -1
  98. package/dist/types/modules/excel/workbook.browser.d.ts +1 -1
  99. package/dist/types/modules/excel/xlsx/xform/comment/comment-xform.d.ts +3 -0
  100. package/dist/types/modules/pdf/excel-bridge.d.ts +32 -0
  101. package/dist/types/modules/pdf/word-bridge.d.ts +20 -15
  102. package/dist/types/modules/stream/common/consumers.d.ts +2 -1
  103. package/dist/types/modules/word/builder/document-handle.d.ts +2 -0
  104. package/dist/types/modules/word/builder/run-builders.d.ts +19 -2
  105. package/dist/types/modules/word/layout/layout-full.d.ts +12 -0
  106. package/dist/types/modules/word/layout/layout-model.d.ts +12 -0
  107. package/dist/types/modules/word/security/cfb-reader.d.ts +14 -3
  108. package/dist/types/modules/word/types.d.ts +17 -0
  109. package/dist/types/modules/word/units.d.ts +10 -4
  110. package/dist/types/modules/word/writer/image-writer.d.ts +1 -1
  111. package/dist/types/modules/word/writer/render-context.d.ts +15 -0
  112. package/package.json +2 -2
@@ -269,7 +269,7 @@ function convertCell(cell, opts) {
269
269
  const para = {
270
270
  type: "paragraph",
271
271
  properties: opts.preserveFormatting ? alignmentToParaProps(cell.alignment) : undefined,
272
- children: children.length > 0 ? children : [{ content: [{ type: "text", text: "" }] }]
272
+ children: wrapHyperlink(cell.hyperlink, children)
273
273
  };
274
274
  const cellProps = {};
275
275
  if (opts.preserveFormatting && cell.fill) {
@@ -289,6 +289,26 @@ function convertCell(cell, opts) {
289
289
  properties: Object.keys(cellProps).length > 0 ? cellProps : undefined
290
290
  };
291
291
  }
292
+ /**
293
+ * Build a paragraph's children for a converted cell, wrapping the runs
294
+ * in a Word {@link Hyperlink} when the source Excel cell carries one.
295
+ *
296
+ * Excel cell hyperlinks are external URLs (or `#Sheet!A1` internal
297
+ * references). We map `#…` targets to a Word anchor and everything else
298
+ * to an external `url`; the packager assigns the relationship id on
299
+ * write. An empty cell still produces a single empty run so the table
300
+ * structure stays intact.
301
+ */
302
+ function wrapHyperlink(hyperlink, runs) {
303
+ const children = runs.length > 0 ? [...runs] : [{ content: [{ type: "text", text: "" }] }];
304
+ if (!hyperlink) {
305
+ return children;
306
+ }
307
+ const link = hyperlink.startsWith("#")
308
+ ? { type: "hyperlink", anchor: hyperlink.slice(1), children }
309
+ : { type: "hyperlink", url: hyperlink, children };
310
+ return [link];
311
+ }
292
312
  function cellText(cell) {
293
313
  if (cell.type === enums_1.ValueType.Null || cell.type === enums_1.ValueType.Merge) {
294
314
  return "";
@@ -132,7 +132,7 @@ exports.Document = {
132
132
  const fileName = `image${s.nextImageId}.${mediaType}`;
133
133
  const rId = `__img_${s.nextImageId}`;
134
134
  const drawingId = s.nextDrawingId++;
135
- s.images.push({ data, mediaType, fileName, rId });
135
+ s.images.push({ data, mediaType, fileName, rId, fallbackData: options?.fallbackData });
136
136
  s.body.push((0, paragraph_builders_1.paragraph)([
137
137
  {
138
138
  content: [
@@ -156,7 +156,7 @@ exports.Document = {
156
156
  const s = _toState(doc);
157
157
  const fileName = `image${s.nextImageId}.${mediaType}`;
158
158
  const rId = `__img_${s.nextImageId}`;
159
- s.images.push({ data, mediaType, fileName, rId });
159
+ s.images.push({ data, mediaType, fileName, rId, fallbackData: options?.fallbackData });
160
160
  s.body.push((0, run_builders_1.floatingImage)({
161
161
  rId,
162
162
  width,
@@ -493,6 +493,18 @@ exports.Document = {
493
493
  uiPriority: 99,
494
494
  unhideWhenUsed: true,
495
495
  runProperties: { color: "0563C1", underline: "single" }
496
+ }, {
497
+ // Word's built-in "visited hyperlink" character style. Referenced by
498
+ // hyperlinks created with `history: true` so visited links render in
499
+ // the standard purple instead of the unvisited blue.
500
+ type: "character",
501
+ styleId: "FollowedHyperlink",
502
+ name: "FollowedHyperlink",
503
+ basedOn: "DefaultParagraphFont",
504
+ uiPriority: 99,
505
+ semiHidden: true,
506
+ unhideWhenUsed: true,
507
+ runProperties: { color: "954F72", underline: "single" }
496
508
  }, {
497
509
  // Word's built-in zero-formatting base table style. Required so
498
510
  // that styles like TableGrid (which sets `basedOn: "TableNormal"`)
@@ -43,6 +43,15 @@ function heading(content, level) {
43
43
  }
44
44
  /** Create a hyperlink. */
45
45
  function hyperlink(linkText, options) {
46
+ // Reference the built-in character style so the colour is governed by the
47
+ // style table (and follows the theme) — Hyperlink for unvisited links,
48
+ // FollowedHyperlink for visited ones (`history: true`). We also emit the
49
+ // matching colour + underline as direct formatting so the link still renders
50
+ // correctly when the document has no style table (Word does the same).
51
+ const visited = options.history === true;
52
+ const defaultProps = visited
53
+ ? { style: "FollowedHyperlink", color: "954F72", underline: "single" }
54
+ : { style: "Hyperlink", color: "0563C1", underline: "single" };
46
55
  return {
47
56
  type: "hyperlink",
48
57
  rId: options.rId,
@@ -52,7 +61,7 @@ function hyperlink(linkText, options) {
52
61
  docLocation: options.docLocation,
53
62
  tgtFrame: options.tgtFrame,
54
63
  history: options.history,
55
- children: [(0, run_builders_1.text)(linkText, options.properties ?? { color: "0563C1", underline: "single" })]
64
+ children: [(0, run_builders_1.text)(linkText, options.properties ?? defaultProps)]
56
65
  };
57
66
  }
58
67
  /** Create a bookmark start. */
@@ -347,12 +347,8 @@ function tocField(options) {
347
347
  if (options?.hyperlink) {
348
348
  instruction += "\\h ";
349
349
  }
350
- if (options?.rightAlignedPageNumbers) {
351
- instruction += "\\z ";
352
- }
353
- if (options?.tabLeader) {
354
- instruction += `\\p "${options.tabLeader}" `;
355
- }
350
+ // Intentionally NOT emitting `\z` for rightAlignedPageNumbers — see note above.
351
+ // Intentionally NOT emitting `\p` for tabLeader — see the field note above.
356
352
  if (options?.noPageNumbers) {
357
353
  instruction += "\\n ";
358
354
  }
@@ -1172,7 +1172,12 @@ function sanitizeOdtPictureName(raw) {
1172
1172
  */
1173
1173
  async function writeOdt(doc) {
1174
1174
  const encoder = internal_utils_1.utf8Encoder;
1175
- const archive = (0, create_archive_1.zip)();
1175
+ // `noSort: true` preserves insertion order. The ODF spec (OpenDocument
1176
+ // v1.2 part 3, §3.3) requires the `mimetype` entry to be the FIRST entry in
1177
+ // the package and STORED (uncompressed) so the file type can be detected by
1178
+ // magic bytes. The default ZipArchive behaviour sorts entries alphabetically,
1179
+ // which would push `mimetype` after `content.xml` and break ODF detection.
1180
+ const archive = (0, create_archive_1.zip)({ noSort: true });
1176
1181
  // Mimetype MUST be the first entry in the ZIP, uncompressed (ODF spec requirement)
1177
1182
  archive.add("mimetype", encoder.encode("application/vnd.oasis.opendocument.text"), { level: 0 });
1178
1183
  // Compute a sanitised rId → Pictures/<safe>.<ext> map up front so the
@@ -373,7 +373,7 @@ function layoutFootnotes(doc, ids, geometry, options, bodyBottomPageY, imageMap)
373
373
  noteById.set(note.id, note);
374
374
  }
375
375
  }
376
- const footerOffsetPt = geometry.height - twipsToPt(doc.sectionProperties?.margins?.footer ?? 720);
376
+ const footerOffsetPt = geometry.height - geometry.footerOffset;
377
377
  /**
378
378
  * Vertical room available for the footnote stack on this page.
379
379
  * The stack must sit between `bodyBottomPageY` (top) and
@@ -607,7 +607,7 @@ function layoutHeader(doc, pageNumber, geometry, options, imageMap) {
607
607
  if (!part) {
608
608
  return [];
609
609
  }
610
- const headerOffsetPt = twipsToPt(doc.sectionProperties?.margins?.header ?? 720);
610
+ const headerOffsetPt = geometry.headerOffset;
611
611
  return layoutHeaderFooterChildren(part.content.children, headerOffsetPt, geometry, options, imageMap);
612
612
  }
613
613
  function layoutFooter(doc, pageNumber, geometry, options, imageMap) {
@@ -630,7 +630,7 @@ function layoutFooter(doc, pageNumber, geometry, options, imageMap) {
630
630
  // where `pgMar.header` is the absolute offset of the band from the
631
631
  // page top). Renderers consume both bands with the same
632
632
  // "treat layout-y as page-y" rule.
633
- const footerOffsetPt = geometry.height - twipsToPt(doc.sectionProperties?.margins?.footer ?? 720);
633
+ const footerOffsetPt = geometry.height - geometry.footerOffset;
634
634
  return layoutHeaderFooterChildren(part.content.children, footerOffsetPt, geometry, options, imageMap);
635
635
  }
636
636
  function layoutHeaderFooterChildren(children, initialCursorY, geometry, options, imageMap) {
@@ -660,6 +660,11 @@ function computePageGeometry(sectionProps, override) {
660
660
  const sectionMarginBottom = twipsToPt(sectionProps?.margins?.bottom ?? layout_constants_1.DEFAULT_PAGE_MARGIN_TWIPS);
661
661
  const sectionMarginLeft = twipsToPt(sectionProps?.margins?.left ?? layout_constants_1.DEFAULT_PAGE_MARGIN_TWIPS);
662
662
  const sectionMarginRight = twipsToPt(sectionProps?.margins?.right ?? layout_constants_1.DEFAULT_PAGE_MARGIN_TWIPS);
663
+ // Header / footer band offsets. Word's default `pgMar.header` /
664
+ // `pgMar.footer` is 720 twips (0.5") — the same default used by the
665
+ // header / footer layout helpers historically.
666
+ const sectionHeaderOffset = twipsToPt(sectionProps?.margins?.header ?? 720);
667
+ const sectionFooterOffset = twipsToPt(sectionProps?.margins?.footer ?? 720);
663
668
  // Per-axis override: callers (PDF bridge, custom hosts) may want to
664
669
  // pin the page size or margin on one axis without disturbing the
665
670
  // others — `pageWidth` doesn't imply overriding margins, etc.
@@ -669,6 +674,8 @@ function computePageGeometry(sectionProps, override) {
669
674
  const marginBottom = override?.marginBottom ?? sectionMarginBottom;
670
675
  const marginLeft = override?.marginLeft ?? sectionMarginLeft;
671
676
  const marginRight = override?.marginRight ?? sectionMarginRight;
677
+ const headerOffset = override?.headerMargin ?? sectionHeaderOffset;
678
+ const footerOffset = override?.footerMargin ?? sectionFooterOffset;
672
679
  return {
673
680
  width,
674
681
  height,
@@ -677,7 +684,9 @@ function computePageGeometry(sectionProps, override) {
677
684
  marginLeft,
678
685
  marginRight,
679
686
  contentWidth: width - marginLeft - marginRight,
680
- contentHeight: height - marginTop - marginBottom
687
+ contentHeight: height - marginTop - marginBottom,
688
+ headerOffset,
689
+ footerOffset
681
690
  };
682
691
  }
683
692
  function computeSectionBreaks(layout) {
@@ -872,35 +881,64 @@ function layoutParagraph(para, startY, contentWidth, options, pageContext, image
872
881
  // =============================================================================
873
882
  function layoutTable(table, startY, contentWidth, sourceIndex, options, imageMap) {
874
883
  const numCols = table.rows.length > 0 ? table.rows[0].cells.length : 0;
875
- const colWidth = numCols > 0 ? contentWidth / numCols : contentWidth;
884
+ // Resolve per-column widths (in points). Prefer the table's explicit
885
+ // `columnWidths` (twips) — populated e.g. by the Excel→Word bridge —
886
+ // scaled to fit the available content width so a table authored wider
887
+ // than the page still renders proportionally. Fall back to equal
888
+ // division when no column widths are declared. This mirrors the
889
+ // sister layout engine in `layout.ts` (which also honours
890
+ // `columnWidths` + `gridSpan`).
891
+ const colWidths = resolveColumnWidthsPt(table, numCols, contentWidth);
892
+ // Prefix sums so a cell at column `ci` starts at `colOffsets[ci]` and
893
+ // a `gridSpan` cell can sum the widths it covers.
894
+ const colOffsets = [0];
895
+ for (let i = 0; i < colWidths.length; i++) {
896
+ colOffsets.push(colOffsets[i] + colWidths[i]);
897
+ }
876
898
  const cells = [];
877
899
  let cursorY = 0;
878
900
  for (let ri = 0; ri < table.rows.length; ri++) {
879
901
  const row = table.rows[ri];
880
902
  let maxRowHeight = DEFAULT_FONT_SIZE_PT * 1.5; // minimum row height
903
+ // Track the grid column each cell occupies, honouring gridSpan so a
904
+ // 2-wide cell pushes the next cell two grid columns to the right.
905
+ let gridCol = 0;
881
906
  for (let ci = 0; ci < row.cells.length; ci++) {
882
907
  const cell = row.cells[ci];
883
- const cellX = ci * colWidth;
908
+ const span = Math.max(1, cell.properties?.gridSpan ?? 1);
909
+ const startCol = Math.min(gridCol, colWidths.length - 1);
910
+ const endCol = Math.min(gridCol + span, colWidths.length);
911
+ const cellX = colOffsets[startCol] ?? 0;
912
+ const cellWidth = (colOffsets[endCol] ?? contentWidth) - cellX;
884
913
  const cellContent = [];
885
914
  let cellCursorY = 2; // cell padding top
886
915
  for (const block of cell.content) {
887
916
  if (block.type === "paragraph") {
888
- const laid = layoutParagraph(block, cellCursorY, colWidth - 4, options, undefined, imageMap);
917
+ const laid = layoutParagraph(block, cellCursorY, cellWidth - 4, options, undefined, imageMap);
889
918
  cellContent.push({ ...laid, sourceIndex: -1 });
890
919
  cellCursorY = laid.rect.y + laid.rect.height;
891
920
  }
892
- // nested tables skipped for simplicity
921
+ else if (block.type === "table") {
922
+ // Nested table: lay it out within the cell's content width and
923
+ // stack it below preceding content. The PDF/SVG renderers
924
+ // already translate nested `LayoutTable` rects by the cell
925
+ // origin, so emitting it here is all that's needed.
926
+ const laidNested = layoutTable(block, cellCursorY, cellWidth - 4, -1, options, imageMap);
927
+ cellContent.push(laidNested);
928
+ cellCursorY = laidNested.rect.y + laidNested.rect.height;
929
+ }
893
930
  }
894
931
  const cellHeight = cellCursorY + 2; // cell padding bottom
895
932
  if (cellHeight > maxRowHeight) {
896
933
  maxRowHeight = cellHeight;
897
934
  }
898
935
  cells.push({
899
- rect: { x: cellX, y: startY + cursorY, width: colWidth, height: cellHeight },
936
+ rect: { x: cellX, y: startY + cursorY, width: cellWidth, height: cellHeight },
900
937
  row: ri,
901
938
  col: ci,
902
939
  content: cellContent
903
940
  });
941
+ gridCol += span;
904
942
  }
905
943
  // Normalize cell heights to row max
906
944
  for (const c of cells) {
@@ -917,6 +955,33 @@ function layoutTable(table, startY, contentWidth, sourceIndex, options, imageMap
917
955
  sourceIndex
918
956
  };
919
957
  }
958
+ /**
959
+ * Resolve a table's per-column widths in points.
960
+ *
961
+ * If `table.columnWidths` (twips) is present and covers all columns, it
962
+ * is used and proportionally scaled to fit `contentWidth` (so a table
963
+ * authored wider than the page shrinks to fit rather than overflowing).
964
+ * Otherwise the content width is divided equally among the columns.
965
+ */
966
+ function resolveColumnWidthsPt(table, numCols, contentWidth) {
967
+ if (numCols <= 0) {
968
+ return [];
969
+ }
970
+ const declared = table.columnWidths;
971
+ if (declared && declared.length >= numCols) {
972
+ const pts = declared.slice(0, numCols).map(twipsToPt);
973
+ const total = pts.reduce((a, b) => a + b, 0);
974
+ if (total > 0) {
975
+ // Scale to fit the content width (shrink overflow, expand
976
+ // under-wide tables to use the full measure — matching how Word
977
+ // distributes a table set to a percentage / auto width).
978
+ const scale = contentWidth / total;
979
+ return pts.map(w => w * scale);
980
+ }
981
+ }
982
+ const equal = contentWidth / numCols;
983
+ return new Array(numCols).fill(equal);
984
+ }
920
985
  /**
921
986
  * Walk a paragraph's children and emit a flat sequence of paragraph
922
987
  * segments — text runs preserve their formatting; inline images become
@@ -71,17 +71,33 @@ function mergeDocuments(documents, options) {
71
71
  const mergedComments = base.comments ? [...base.comments] : [];
72
72
  for (let i = 1; i < documents.length; i++) {
73
73
  const doc = documents[i];
74
- // Insert section break before appending next document
75
- const sectionBreakPara = {
76
- type: "paragraph",
77
- properties: {
78
- sectionProperties: {
79
- breakType
74
+ // Mark a section break BEFORE appending the next document. In OOXML a
75
+ // section break is carried by the `sectPr` of the LAST paragraph of the
76
+ // preceding section — NOT by an extra empty paragraph. Appending an empty
77
+ // <w:p> with only a sectPr makes Word render a stray blank line / blank
78
+ // page. So we attach the break to the last paragraph already in the body;
79
+ // only if the body currently ends with a non-paragraph block (e.g. a
80
+ // table, which cannot carry a sectPr directly) do we fall back to a
81
+ // minimal carrier paragraph.
82
+ const lastBlock = mergedBody[mergedBody.length - 1];
83
+ if (lastBlock && lastBlock.type === "paragraph") {
84
+ const para = lastBlock;
85
+ mergedBody[mergedBody.length - 1] = {
86
+ ...para,
87
+ properties: {
88
+ ...para.properties,
89
+ sectionProperties: { ...para.properties?.sectionProperties, breakType }
80
90
  }
81
- },
82
- children: []
83
- };
84
- mergedBody.push(sectionBreakPara);
91
+ };
92
+ }
93
+ else {
94
+ const sectionBreakPara = {
95
+ type: "paragraph",
96
+ properties: { sectionProperties: { breakType } },
97
+ children: []
98
+ };
99
+ mergedBody.push(sectionBreakPara);
100
+ }
85
101
  // Compute id remappings BEFORE cloning the body so we can rewrite refs
86
102
  // during the clone in a single pass.
87
103
  const numMaps = mergeNumberingDefinitions(doc, mergedAbstractNums, mergedNumInstances);
@@ -122,17 +122,83 @@ function paragraphHasExplicitPageBreakRun(para) {
122
122
  }
123
123
  return false;
124
124
  }
125
+ /**
126
+ * Return a copy of `para` with every explicit page-break run content removed.
127
+ * Runs left empty by the removal are dropped. Used when splitting by page
128
+ * break so the trailing break that separated this segment from the next does
129
+ * not render as a blank page in the standalone split document.
130
+ */
131
+ function stripPageBreakRuns(para) {
132
+ const newChildren = [];
133
+ for (const child of para.children) {
134
+ if ((0, text_utils_1.isRun)(child)) {
135
+ const content = child.content.filter(c => !(c.type === "break" && c.breakType === "page"));
136
+ if (content.length > 0) {
137
+ newChildren.push({ ...child, content });
138
+ }
139
+ // Drop runs that became empty after removing the page break.
140
+ }
141
+ else {
142
+ newChildren.push(child);
143
+ }
144
+ }
145
+ return { ...para, children: newChildren };
146
+ }
147
+ /** True when a paragraph has no children (after page-break stripping). */
148
+ function paragraphIsEmpty(para) {
149
+ return para.children.length === 0;
150
+ }
125
151
  function buildSplitDoc(source, segment, opts) {
152
+ // When splitting by section, the segment's last paragraph carries the
153
+ // section break (often `nextPage`) that originally separated it from the
154
+ // following section. In a standalone split document that break has nothing
155
+ // after it, so Word renders a trailing blank page. Strip the paragraph-level
156
+ // sectPr and promote its page setup to the document's own section
157
+ // properties instead.
158
+ let body = segment;
159
+ let promotedSectPr;
160
+ if (opts.by === "section" && segment.length > 0) {
161
+ const last = segment[segment.length - 1];
162
+ if (last.type === "paragraph" && last.properties?.sectionProperties) {
163
+ const para = last;
164
+ const props = para.properties;
165
+ const sectionProperties = props.sectionProperties;
166
+ const restProps = { ...props };
167
+ delete restProps.sectionProperties;
168
+ // Drop the inter-section break type: in a standalone document this is
169
+ // the (only/first) section, so a "nextPage" break would just push all
170
+ // content onto page 2, leaving page 1 blank.
171
+ const { breakType: _drop, ...sectWithoutBreak } = sectionProperties;
172
+ promotedSectPr = sectWithoutBreak;
173
+ const cleaned = { ...para, properties: restProps };
174
+ body = [...segment.slice(0, -1), cleaned];
175
+ }
176
+ }
177
+ else if (opts.by === "pageBreak" && segment.length > 0) {
178
+ // The segment ends with the paragraph that held the splitting page break.
179
+ // In a standalone document that trailing page break has nothing after it
180
+ // and renders a blank page, so remove it. If the paragraph held only the
181
+ // page break, drop the now-empty paragraph entirely.
182
+ const last = segment[segment.length - 1];
183
+ if (last.type === "paragraph" && paragraphHasExplicitPageBreakRun(last)) {
184
+ const stripped = stripPageBreakRuns(last);
185
+ body = paragraphIsEmpty(stripped)
186
+ ? segment.slice(0, -1)
187
+ : [...segment.slice(0, -1), stripped];
188
+ }
189
+ }
126
190
  if (!opts.preserveSharedParts) {
127
191
  // Minimal split: just body + docType
128
192
  return {
129
193
  docType: source.docType,
130
- body: segment
194
+ body,
195
+ ...(promotedSectPr ? { sectionProperties: promotedSectPr } : {})
131
196
  };
132
197
  }
133
198
  // Full split: preserve all shared parts
134
199
  return {
135
200
  ...source,
136
- body: segment
201
+ body,
202
+ ...(promotedSectPr ? { sectionProperties: promotedSectPr } : {})
137
203
  };
138
204
  }
@@ -249,6 +249,23 @@ function parseDrawingShape(anchorEl, wspEl, ctx) {
249
249
  shape.noOutline = true;
250
250
  }
251
251
  }
252
+ // Parse transform (rotation / flip) from a:xfrm
253
+ const xfrmEl = (0, dom_1.findChild)(spPrEl, "a:xfrm") ?? (0, parse_utils_1.findChildNs)(spPrEl, "xfrm");
254
+ if (xfrmEl) {
255
+ const rot = xfrmEl.attributes["rot"];
256
+ if (rot) {
257
+ const n = parseInt(rot, 10);
258
+ if (!Number.isNaN(n)) {
259
+ shape.rotation = n;
260
+ }
261
+ }
262
+ if (xfrmEl.attributes["flipH"] === "1") {
263
+ shape.flipHorizontal = true;
264
+ }
265
+ if (xfrmEl.attributes["flipV"] === "1") {
266
+ shape.flipVertical = true;
267
+ }
268
+ }
252
269
  }
253
270
  // Parse text content (wps:txbx > w:txbxContent)
254
271
  const txbxEl = (0, dom_1.findChild)(wspEl, "wps:txbx") ?? (0, parse_utils_1.findChildNs)(wspEl, "txbx");
@@ -266,6 +283,12 @@ function parseDrawingShape(anchorEl, wspEl, ctx) {
266
283
  shape.textContent = paras;
267
284
  }
268
285
  }
286
+ // Parse text body vertical anchor (wps:bodyPr/@anchor)
287
+ const bodyPrEl = (0, dom_1.findChild)(wspEl, "wps:bodyPr") ?? (0, parse_utils_1.findChildNs)(wspEl, "bodyPr");
288
+ const anchorAttr = bodyPrEl?.attributes["anchor"];
289
+ if (anchorAttr === "t" || anchorAttr === "ctr" || anchorAttr === "b") {
290
+ shape.textBodyAnchor = anchorAttr;
291
+ }
269
292
  // Parse positioning
270
293
  const posH = (0, dom_1.findChild)(anchorEl, "wp:positionH");
271
294
  if (posH) {