@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.
- package/dist/browser/modules/archive/io/random-access.d.ts +1 -1
- package/dist/browser/modules/excel/workbook.browser.d.ts +1 -1
- package/dist/browser/modules/excel/xlsx/xform/comment/comment-xform.d.ts +3 -0
- package/dist/browser/modules/excel/xlsx/xform/comment/comment-xform.js +30 -7
- package/dist/browser/modules/pdf/excel-bridge.d.ts +32 -0
- package/dist/browser/modules/pdf/excel-bridge.js +67 -1
- package/dist/browser/modules/pdf/word-bridge.d.ts +20 -15
- package/dist/browser/modules/pdf/word-bridge.js +49 -34
- package/dist/browser/modules/stream/common/consumers.d.ts +2 -1
- package/dist/browser/modules/word/advanced/diff.js +125 -13
- package/dist/browser/modules/word/advanced/drawing-shapes.js +3 -0
- package/dist/browser/modules/word/bridge/excel-bridge.js +21 -1
- package/dist/browser/modules/word/builder/document-handle.d.ts +2 -0
- package/dist/browser/modules/word/builder/document-handle.js +14 -2
- package/dist/browser/modules/word/builder/paragraph-builders.js +10 -1
- package/dist/browser/modules/word/builder/run-builders.d.ts +19 -2
- package/dist/browser/modules/word/builder/run-builders.js +2 -6
- package/dist/browser/modules/word/convert/odt/odt.js +6 -1
- package/dist/browser/modules/word/layout/layout-full.d.ts +12 -0
- package/dist/browser/modules/word/layout/layout-full.js +74 -9
- package/dist/browser/modules/word/layout/layout-model.d.ts +12 -0
- package/dist/browser/modules/word/query/merge.js +26 -10
- package/dist/browser/modules/word/query/split.js +68 -2
- package/dist/browser/modules/word/reader/docx-reader.js +23 -0
- package/dist/browser/modules/word/security/cfb-reader.d.ts +14 -3
- package/dist/browser/modules/word/security/cfb-reader.js +271 -153
- package/dist/browser/modules/word/security/document-protection.js +10 -4
- package/dist/browser/modules/word/security/encryption.js +194 -32
- package/dist/browser/modules/word/types.d.ts +17 -0
- package/dist/browser/modules/word/units.d.ts +10 -4
- package/dist/browser/modules/word/units.js +10 -4
- package/dist/browser/modules/word/writer/document-writer.js +28 -4
- package/dist/browser/modules/word/writer/docx-packager.js +45 -5
- package/dist/browser/modules/word/writer/image-writer.d.ts +1 -1
- package/dist/browser/modules/word/writer/image-writer.js +2 -2
- package/dist/browser/modules/word/writer/render-context.d.ts +15 -0
- package/dist/browser/modules/word/writer/run-writer.js +8 -4
- package/dist/browser/modules/word/writer/section-writer.js +46 -35
- package/dist/browser/modules/word/writer/streaming-writer.js +4 -0
- package/dist/browser/modules/word/writer/styles-writer.js +11 -0
- package/dist/browser/modules/word/writer/table-writer.js +6 -0
- package/dist/cjs/modules/excel/xlsx/xform/comment/comment-xform.js +30 -7
- package/dist/cjs/modules/pdf/excel-bridge.js +67 -0
- package/dist/cjs/modules/pdf/word-bridge.js +49 -34
- package/dist/cjs/modules/word/advanced/diff.js +125 -13
- package/dist/cjs/modules/word/advanced/drawing-shapes.js +3 -0
- package/dist/cjs/modules/word/bridge/excel-bridge.js +21 -1
- package/dist/cjs/modules/word/builder/document-handle.js +14 -2
- package/dist/cjs/modules/word/builder/paragraph-builders.js +10 -1
- package/dist/cjs/modules/word/builder/run-builders.js +2 -6
- package/dist/cjs/modules/word/convert/odt/odt.js +6 -1
- package/dist/cjs/modules/word/layout/layout-full.js +74 -9
- package/dist/cjs/modules/word/query/merge.js +26 -10
- package/dist/cjs/modules/word/query/split.js +68 -2
- package/dist/cjs/modules/word/reader/docx-reader.js +23 -0
- package/dist/cjs/modules/word/security/cfb-reader.js +271 -153
- package/dist/cjs/modules/word/security/document-protection.js +10 -4
- package/dist/cjs/modules/word/security/encryption.js +193 -31
- package/dist/cjs/modules/word/units.js +10 -4
- package/dist/cjs/modules/word/writer/document-writer.js +28 -4
- package/dist/cjs/modules/word/writer/docx-packager.js +45 -5
- package/dist/cjs/modules/word/writer/image-writer.js +2 -2
- package/dist/cjs/modules/word/writer/run-writer.js +8 -4
- package/dist/cjs/modules/word/writer/section-writer.js +46 -35
- package/dist/cjs/modules/word/writer/streaming-writer.js +4 -0
- package/dist/cjs/modules/word/writer/styles-writer.js +11 -0
- package/dist/cjs/modules/word/writer/table-writer.js +6 -0
- package/dist/esm/modules/excel/xlsx/xform/comment/comment-xform.js +30 -7
- package/dist/esm/modules/pdf/excel-bridge.js +67 -1
- package/dist/esm/modules/pdf/word-bridge.js +49 -34
- package/dist/esm/modules/word/advanced/diff.js +125 -13
- package/dist/esm/modules/word/advanced/drawing-shapes.js +3 -0
- package/dist/esm/modules/word/bridge/excel-bridge.js +21 -1
- package/dist/esm/modules/word/builder/document-handle.js +14 -2
- package/dist/esm/modules/word/builder/paragraph-builders.js +10 -1
- package/dist/esm/modules/word/builder/run-builders.js +2 -6
- package/dist/esm/modules/word/convert/odt/odt.js +6 -1
- package/dist/esm/modules/word/layout/layout-full.js +74 -9
- package/dist/esm/modules/word/query/merge.js +26 -10
- package/dist/esm/modules/word/query/split.js +68 -2
- package/dist/esm/modules/word/reader/docx-reader.js +23 -0
- package/dist/esm/modules/word/security/cfb-reader.js +271 -153
- package/dist/esm/modules/word/security/document-protection.js +10 -4
- package/dist/esm/modules/word/security/encryption.js +194 -32
- package/dist/esm/modules/word/units.js +10 -4
- package/dist/esm/modules/word/writer/document-writer.js +28 -4
- package/dist/esm/modules/word/writer/docx-packager.js +45 -5
- package/dist/esm/modules/word/writer/image-writer.js +2 -2
- package/dist/esm/modules/word/writer/run-writer.js +8 -4
- package/dist/esm/modules/word/writer/section-writer.js +46 -35
- package/dist/esm/modules/word/writer/streaming-writer.js +4 -0
- package/dist/esm/modules/word/writer/styles-writer.js +11 -0
- package/dist/esm/modules/word/writer/table-writer.js +6 -0
- package/dist/iife/excelts.iife.js +20 -8
- package/dist/iife/excelts.iife.js.map +1 -1
- package/dist/iife/excelts.iife.min.js +2 -2
- package/dist/types/modules/archive/io/random-access.d.ts +1 -1
- package/dist/types/modules/excel/workbook.browser.d.ts +1 -1
- package/dist/types/modules/excel/xlsx/xform/comment/comment-xform.d.ts +3 -0
- package/dist/types/modules/pdf/excel-bridge.d.ts +32 -0
- package/dist/types/modules/pdf/word-bridge.d.ts +20 -15
- package/dist/types/modules/stream/common/consumers.d.ts +2 -1
- package/dist/types/modules/word/builder/document-handle.d.ts +2 -0
- package/dist/types/modules/word/builder/run-builders.d.ts +19 -2
- package/dist/types/modules/word/layout/layout-full.d.ts +12 -0
- package/dist/types/modules/word/layout/layout-model.d.ts +12 -0
- package/dist/types/modules/word/security/cfb-reader.d.ts +14 -3
- package/dist/types/modules/word/types.d.ts +17 -0
- package/dist/types/modules/word/units.d.ts +10 -4
- package/dist/types/modules/word/writer/image-writer.d.ts +1 -1
- package/dist/types/modules/word/writer/render-context.d.ts +15 -0
- 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:
|
|
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 ??
|
|
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
|
-
|
|
351
|
-
|
|
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
|
-
|
|
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 -
|
|
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 =
|
|
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 -
|
|
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
|
-
|
|
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
|
|
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,
|
|
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
|
-
|
|
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:
|
|
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
|
-
//
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
|
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
|
|
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) {
|