@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
|
@@ -129,7 +129,7 @@ export const Document = {
|
|
|
129
129
|
const fileName = `image${s.nextImageId}.${mediaType}`;
|
|
130
130
|
const rId = `__img_${s.nextImageId}`;
|
|
131
131
|
const drawingId = s.nextDrawingId++;
|
|
132
|
-
s.images.push({ data, mediaType, fileName, rId });
|
|
132
|
+
s.images.push({ data, mediaType, fileName, rId, fallbackData: options?.fallbackData });
|
|
133
133
|
s.body.push(paragraph([
|
|
134
134
|
{
|
|
135
135
|
content: [
|
|
@@ -153,7 +153,7 @@ export const Document = {
|
|
|
153
153
|
const s = _toState(doc);
|
|
154
154
|
const fileName = `image${s.nextImageId}.${mediaType}`;
|
|
155
155
|
const rId = `__img_${s.nextImageId}`;
|
|
156
|
-
s.images.push({ data, mediaType, fileName, rId });
|
|
156
|
+
s.images.push({ data, mediaType, fileName, rId, fallbackData: options?.fallbackData });
|
|
157
157
|
s.body.push(floatingImage({
|
|
158
158
|
rId,
|
|
159
159
|
width,
|
|
@@ -490,6 +490,18 @@ export const Document = {
|
|
|
490
490
|
uiPriority: 99,
|
|
491
491
|
unhideWhenUsed: true,
|
|
492
492
|
runProperties: { color: "0563C1", underline: "single" }
|
|
493
|
+
}, {
|
|
494
|
+
// Word's built-in "visited hyperlink" character style. Referenced by
|
|
495
|
+
// hyperlinks created with `history: true` so visited links render in
|
|
496
|
+
// the standard purple instead of the unvisited blue.
|
|
497
|
+
type: "character",
|
|
498
|
+
styleId: "FollowedHyperlink",
|
|
499
|
+
name: "FollowedHyperlink",
|
|
500
|
+
basedOn: "DefaultParagraphFont",
|
|
501
|
+
uiPriority: 99,
|
|
502
|
+
semiHidden: true,
|
|
503
|
+
unhideWhenUsed: true,
|
|
504
|
+
runProperties: { color: "954F72", underline: "single" }
|
|
493
505
|
}, {
|
|
494
506
|
// Word's built-in zero-formatting base table style. Required so
|
|
495
507
|
// that styles like TableGrid (which sets `basedOn: "TableNormal"`)
|
|
@@ -24,6 +24,15 @@ export function heading(content, level) {
|
|
|
24
24
|
}
|
|
25
25
|
/** Create a hyperlink. */
|
|
26
26
|
export function hyperlink(linkText, options) {
|
|
27
|
+
// Reference the built-in character style so the colour is governed by the
|
|
28
|
+
// style table (and follows the theme) — Hyperlink for unvisited links,
|
|
29
|
+
// FollowedHyperlink for visited ones (`history: true`). We also emit the
|
|
30
|
+
// matching colour + underline as direct formatting so the link still renders
|
|
31
|
+
// correctly when the document has no style table (Word does the same).
|
|
32
|
+
const visited = options.history === true;
|
|
33
|
+
const defaultProps = visited
|
|
34
|
+
? { style: "FollowedHyperlink", color: "954F72", underline: "single" }
|
|
35
|
+
: { style: "Hyperlink", color: "0563C1", underline: "single" };
|
|
27
36
|
return {
|
|
28
37
|
type: "hyperlink",
|
|
29
38
|
rId: options.rId,
|
|
@@ -33,7 +42,7 @@ export function hyperlink(linkText, options) {
|
|
|
33
42
|
docLocation: options.docLocation,
|
|
34
43
|
tgtFrame: options.tgtFrame,
|
|
35
44
|
history: options.history,
|
|
36
|
-
children: [text(linkText, options.properties ??
|
|
45
|
+
children: [text(linkText, options.properties ?? defaultProps)]
|
|
37
46
|
};
|
|
38
47
|
}
|
|
39
48
|
/** Create a bookmark start. */
|
|
@@ -151,9 +151,26 @@ export declare function tocField(options?: {
|
|
|
151
151
|
tcLevels?: string;
|
|
152
152
|
/** Hyperlinks for entries. */
|
|
153
153
|
hyperlink?: boolean;
|
|
154
|
-
/**
|
|
154
|
+
/**
|
|
155
|
+
* Right-align page numbers.
|
|
156
|
+
*
|
|
157
|
+
* NOTE: Right-aligned page numbers are the TOC default — they come from the
|
|
158
|
+
* right-aligned tab stop in the TOC paragraph styles, not from a field
|
|
159
|
+
* switch. The `\z` switch does NOT mean "right align": per ECMA-376 it
|
|
160
|
+
* *hides* the tab leader and page numbers in Web layout view. Mapping this
|
|
161
|
+
* option to `\z` therefore broke the layout, so we no longer emit it.
|
|
162
|
+
*/
|
|
155
163
|
rightAlignedPageNumbers?: boolean;
|
|
156
|
-
/**
|
|
164
|
+
/**
|
|
165
|
+
* Tab leader style between an entry and its page number.
|
|
166
|
+
*
|
|
167
|
+
* NOTE: The dotted leader is already the TOC default (a tab stop with dot
|
|
168
|
+
* leader defined by the TOC paragraph styles). The TOC field has no switch
|
|
169
|
+
* for choosing the leader glyph — the `\p` switch sets the *separator
|
|
170
|
+
* character* (replacing the tab entirely), which would DISABLE the leader
|
|
171
|
+
* dots and the right-aligned page number. We therefore do not translate
|
|
172
|
+
* this option into `\p`; the leader is controlled by the TOC styles.
|
|
173
|
+
*/
|
|
157
174
|
tabLeader?: "." | "-" | "_" | " ";
|
|
158
175
|
/** Suppress page numbers. */
|
|
159
176
|
noPageNumbers?: boolean;
|
|
@@ -271,12 +271,8 @@ export function tocField(options) {
|
|
|
271
271
|
if (options?.hyperlink) {
|
|
272
272
|
instruction += "\\h ";
|
|
273
273
|
}
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
}
|
|
277
|
-
if (options?.tabLeader) {
|
|
278
|
-
instruction += `\\p "${options.tabLeader}" `;
|
|
279
|
-
}
|
|
274
|
+
// Intentionally NOT emitting `\z` for rightAlignedPageNumbers — see note above.
|
|
275
|
+
// Intentionally NOT emitting `\p` for tabLeader — see the field note above.
|
|
280
276
|
if (options?.noPageNumbers) {
|
|
281
277
|
instruction += "\\n ";
|
|
282
278
|
}
|
|
@@ -1168,7 +1168,12 @@ function sanitizeOdtPictureName(raw) {
|
|
|
1168
1168
|
*/
|
|
1169
1169
|
export async function writeOdt(doc) {
|
|
1170
1170
|
const encoder = utf8Encoder;
|
|
1171
|
-
|
|
1171
|
+
// `noSort: true` preserves insertion order. The ODF spec (OpenDocument
|
|
1172
|
+
// v1.2 part 3, §3.3) requires the `mimetype` entry to be the FIRST entry in
|
|
1173
|
+
// the package and STORED (uncompressed) so the file type can be detected by
|
|
1174
|
+
// magic bytes. The default ZipArchive behaviour sorts entries alphabetically,
|
|
1175
|
+
// which would push `mimetype` after `content.xml` and break ODF detection.
|
|
1176
|
+
const archive = zip({ noSort: true });
|
|
1172
1177
|
// Mimetype MUST be the first entry in the ZIP, uncompressed (ODF spec requirement)
|
|
1173
1178
|
archive.add("mimetype", encoder.encode("application/vnd.oasis.opendocument.text"), { level: 0 });
|
|
1174
1179
|
// Compute a sanitised rId → Pictures/<safe>.<ext> map up front so the
|
|
@@ -29,6 +29,18 @@ export interface PageGeometryOverride {
|
|
|
29
29
|
readonly marginBottom?: number;
|
|
30
30
|
readonly marginLeft?: number;
|
|
31
31
|
readonly marginRight?: number;
|
|
32
|
+
/**
|
|
33
|
+
* Distance of the header band from the top edge of the page, in
|
|
34
|
+
* points. Overrides the section's `pgMar.header`. Header paragraphs
|
|
35
|
+
* are laid out starting at this y-offset from the page top.
|
|
36
|
+
*/
|
|
37
|
+
readonly headerMargin?: number;
|
|
38
|
+
/**
|
|
39
|
+
* Distance of the footer band from the bottom edge of the page, in
|
|
40
|
+
* points. Overrides the section's `pgMar.footer`. The footer band's
|
|
41
|
+
* top is placed at `pageHeight - footerMargin`.
|
|
42
|
+
*/
|
|
43
|
+
readonly footerMargin?: number;
|
|
32
44
|
}
|
|
33
45
|
/** Options for the full layout engine. */
|
|
34
46
|
export interface FullLayoutOptions extends LayoutOptions {
|
|
@@ -370,7 +370,7 @@ function layoutFootnotes(doc, ids, geometry, options, bodyBottomPageY, imageMap)
|
|
|
370
370
|
noteById.set(note.id, note);
|
|
371
371
|
}
|
|
372
372
|
}
|
|
373
|
-
const footerOffsetPt = geometry.height -
|
|
373
|
+
const footerOffsetPt = geometry.height - geometry.footerOffset;
|
|
374
374
|
/**
|
|
375
375
|
* Vertical room available for the footnote stack on this page.
|
|
376
376
|
* The stack must sit between `bodyBottomPageY` (top) and
|
|
@@ -604,7 +604,7 @@ function layoutHeader(doc, pageNumber, geometry, options, imageMap) {
|
|
|
604
604
|
if (!part) {
|
|
605
605
|
return [];
|
|
606
606
|
}
|
|
607
|
-
const headerOffsetPt =
|
|
607
|
+
const headerOffsetPt = geometry.headerOffset;
|
|
608
608
|
return layoutHeaderFooterChildren(part.content.children, headerOffsetPt, geometry, options, imageMap);
|
|
609
609
|
}
|
|
610
610
|
function layoutFooter(doc, pageNumber, geometry, options, imageMap) {
|
|
@@ -627,7 +627,7 @@ function layoutFooter(doc, pageNumber, geometry, options, imageMap) {
|
|
|
627
627
|
// where `pgMar.header` is the absolute offset of the band from the
|
|
628
628
|
// page top). Renderers consume both bands with the same
|
|
629
629
|
// "treat layout-y as page-y" rule.
|
|
630
|
-
const footerOffsetPt = geometry.height -
|
|
630
|
+
const footerOffsetPt = geometry.height - geometry.footerOffset;
|
|
631
631
|
return layoutHeaderFooterChildren(part.content.children, footerOffsetPt, geometry, options, imageMap);
|
|
632
632
|
}
|
|
633
633
|
function layoutHeaderFooterChildren(children, initialCursorY, geometry, options, imageMap) {
|
|
@@ -657,6 +657,11 @@ function computePageGeometry(sectionProps, override) {
|
|
|
657
657
|
const sectionMarginBottom = twipsToPt(sectionProps?.margins?.bottom ?? DEFAULT_PAGE_MARGIN_TWIPS);
|
|
658
658
|
const sectionMarginLeft = twipsToPt(sectionProps?.margins?.left ?? DEFAULT_PAGE_MARGIN_TWIPS);
|
|
659
659
|
const sectionMarginRight = twipsToPt(sectionProps?.margins?.right ?? DEFAULT_PAGE_MARGIN_TWIPS);
|
|
660
|
+
// Header / footer band offsets. Word's default `pgMar.header` /
|
|
661
|
+
// `pgMar.footer` is 720 twips (0.5") — the same default used by the
|
|
662
|
+
// header / footer layout helpers historically.
|
|
663
|
+
const sectionHeaderOffset = twipsToPt(sectionProps?.margins?.header ?? 720);
|
|
664
|
+
const sectionFooterOffset = twipsToPt(sectionProps?.margins?.footer ?? 720);
|
|
660
665
|
// Per-axis override: callers (PDF bridge, custom hosts) may want to
|
|
661
666
|
// pin the page size or margin on one axis without disturbing the
|
|
662
667
|
// others — `pageWidth` doesn't imply overriding margins, etc.
|
|
@@ -666,6 +671,8 @@ function computePageGeometry(sectionProps, override) {
|
|
|
666
671
|
const marginBottom = override?.marginBottom ?? sectionMarginBottom;
|
|
667
672
|
const marginLeft = override?.marginLeft ?? sectionMarginLeft;
|
|
668
673
|
const marginRight = override?.marginRight ?? sectionMarginRight;
|
|
674
|
+
const headerOffset = override?.headerMargin ?? sectionHeaderOffset;
|
|
675
|
+
const footerOffset = override?.footerMargin ?? sectionFooterOffset;
|
|
669
676
|
return {
|
|
670
677
|
width,
|
|
671
678
|
height,
|
|
@@ -674,7 +681,9 @@ function computePageGeometry(sectionProps, override) {
|
|
|
674
681
|
marginLeft,
|
|
675
682
|
marginRight,
|
|
676
683
|
contentWidth: width - marginLeft - marginRight,
|
|
677
|
-
contentHeight: height - marginTop - marginBottom
|
|
684
|
+
contentHeight: height - marginTop - marginBottom,
|
|
685
|
+
headerOffset,
|
|
686
|
+
footerOffset
|
|
678
687
|
};
|
|
679
688
|
}
|
|
680
689
|
function computeSectionBreaks(layout) {
|
|
@@ -869,35 +878,64 @@ function layoutParagraph(para, startY, contentWidth, options, pageContext, image
|
|
|
869
878
|
// =============================================================================
|
|
870
879
|
function layoutTable(table, startY, contentWidth, sourceIndex, options, imageMap) {
|
|
871
880
|
const numCols = table.rows.length > 0 ? table.rows[0].cells.length : 0;
|
|
872
|
-
|
|
881
|
+
// Resolve per-column widths (in points). Prefer the table's explicit
|
|
882
|
+
// `columnWidths` (twips) — populated e.g. by the Excel→Word bridge —
|
|
883
|
+
// scaled to fit the available content width so a table authored wider
|
|
884
|
+
// than the page still renders proportionally. Fall back to equal
|
|
885
|
+
// division when no column widths are declared. This mirrors the
|
|
886
|
+
// sister layout engine in `layout.ts` (which also honours
|
|
887
|
+
// `columnWidths` + `gridSpan`).
|
|
888
|
+
const colWidths = resolveColumnWidthsPt(table, numCols, contentWidth);
|
|
889
|
+
// Prefix sums so a cell at column `ci` starts at `colOffsets[ci]` and
|
|
890
|
+
// a `gridSpan` cell can sum the widths it covers.
|
|
891
|
+
const colOffsets = [0];
|
|
892
|
+
for (let i = 0; i < colWidths.length; i++) {
|
|
893
|
+
colOffsets.push(colOffsets[i] + colWidths[i]);
|
|
894
|
+
}
|
|
873
895
|
const cells = [];
|
|
874
896
|
let cursorY = 0;
|
|
875
897
|
for (let ri = 0; ri < table.rows.length; ri++) {
|
|
876
898
|
const row = table.rows[ri];
|
|
877
899
|
let maxRowHeight = DEFAULT_FONT_SIZE_PT * 1.5; // minimum row height
|
|
900
|
+
// Track the grid column each cell occupies, honouring gridSpan so a
|
|
901
|
+
// 2-wide cell pushes the next cell two grid columns to the right.
|
|
902
|
+
let gridCol = 0;
|
|
878
903
|
for (let ci = 0; ci < row.cells.length; ci++) {
|
|
879
904
|
const cell = row.cells[ci];
|
|
880
|
-
const
|
|
905
|
+
const span = Math.max(1, cell.properties?.gridSpan ?? 1);
|
|
906
|
+
const startCol = Math.min(gridCol, colWidths.length - 1);
|
|
907
|
+
const endCol = Math.min(gridCol + span, colWidths.length);
|
|
908
|
+
const cellX = colOffsets[startCol] ?? 0;
|
|
909
|
+
const cellWidth = (colOffsets[endCol] ?? contentWidth) - cellX;
|
|
881
910
|
const cellContent = [];
|
|
882
911
|
let cellCursorY = 2; // cell padding top
|
|
883
912
|
for (const block of cell.content) {
|
|
884
913
|
if (block.type === "paragraph") {
|
|
885
|
-
const laid = layoutParagraph(block, cellCursorY,
|
|
914
|
+
const laid = layoutParagraph(block, cellCursorY, cellWidth - 4, options, undefined, imageMap);
|
|
886
915
|
cellContent.push({ ...laid, sourceIndex: -1 });
|
|
887
916
|
cellCursorY = laid.rect.y + laid.rect.height;
|
|
888
917
|
}
|
|
889
|
-
|
|
918
|
+
else if (block.type === "table") {
|
|
919
|
+
// Nested table: lay it out within the cell's content width and
|
|
920
|
+
// stack it below preceding content. The PDF/SVG renderers
|
|
921
|
+
// already translate nested `LayoutTable` rects by the cell
|
|
922
|
+
// origin, so emitting it here is all that's needed.
|
|
923
|
+
const laidNested = layoutTable(block, cellCursorY, cellWidth - 4, -1, options, imageMap);
|
|
924
|
+
cellContent.push(laidNested);
|
|
925
|
+
cellCursorY = laidNested.rect.y + laidNested.rect.height;
|
|
926
|
+
}
|
|
890
927
|
}
|
|
891
928
|
const cellHeight = cellCursorY + 2; // cell padding bottom
|
|
892
929
|
if (cellHeight > maxRowHeight) {
|
|
893
930
|
maxRowHeight = cellHeight;
|
|
894
931
|
}
|
|
895
932
|
cells.push({
|
|
896
|
-
rect: { x: cellX, y: startY + cursorY, width:
|
|
933
|
+
rect: { x: cellX, y: startY + cursorY, width: cellWidth, height: cellHeight },
|
|
897
934
|
row: ri,
|
|
898
935
|
col: ci,
|
|
899
936
|
content: cellContent
|
|
900
937
|
});
|
|
938
|
+
gridCol += span;
|
|
901
939
|
}
|
|
902
940
|
// Normalize cell heights to row max
|
|
903
941
|
for (const c of cells) {
|
|
@@ -914,6 +952,33 @@ function layoutTable(table, startY, contentWidth, sourceIndex, options, imageMap
|
|
|
914
952
|
sourceIndex
|
|
915
953
|
};
|
|
916
954
|
}
|
|
955
|
+
/**
|
|
956
|
+
* Resolve a table's per-column widths in points.
|
|
957
|
+
*
|
|
958
|
+
* If `table.columnWidths` (twips) is present and covers all columns, it
|
|
959
|
+
* is used and proportionally scaled to fit `contentWidth` (so a table
|
|
960
|
+
* authored wider than the page shrinks to fit rather than overflowing).
|
|
961
|
+
* Otherwise the content width is divided equally among the columns.
|
|
962
|
+
*/
|
|
963
|
+
function resolveColumnWidthsPt(table, numCols, contentWidth) {
|
|
964
|
+
if (numCols <= 0) {
|
|
965
|
+
return [];
|
|
966
|
+
}
|
|
967
|
+
const declared = table.columnWidths;
|
|
968
|
+
if (declared && declared.length >= numCols) {
|
|
969
|
+
const pts = declared.slice(0, numCols).map(twipsToPt);
|
|
970
|
+
const total = pts.reduce((a, b) => a + b, 0);
|
|
971
|
+
if (total > 0) {
|
|
972
|
+
// Scale to fit the content width (shrink overflow, expand
|
|
973
|
+
// under-wide tables to use the full measure — matching how Word
|
|
974
|
+
// distributes a table set to a percentage / auto width).
|
|
975
|
+
const scale = contentWidth / total;
|
|
976
|
+
return pts.map(w => w * scale);
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
const equal = contentWidth / numCols;
|
|
980
|
+
return new Array(numCols).fill(equal);
|
|
981
|
+
}
|
|
917
982
|
/**
|
|
918
983
|
* Walk a paragraph's children and emit a flat sequence of paragraph
|
|
919
984
|
* segments — text runs preserve their formatting; inline images become
|
|
@@ -290,6 +290,18 @@ export interface PageGeometry {
|
|
|
290
290
|
/** Usable content area. */
|
|
291
291
|
readonly contentWidth: number;
|
|
292
292
|
readonly contentHeight: number;
|
|
293
|
+
/**
|
|
294
|
+
* Distance of the header band from the top edge of the page, in
|
|
295
|
+
* points (ECMA-376 `pgMar.header`). Header paragraphs are laid out
|
|
296
|
+
* starting at this y-offset from the page top.
|
|
297
|
+
*/
|
|
298
|
+
readonly headerOffset: number;
|
|
299
|
+
/**
|
|
300
|
+
* Distance of the footer band from the bottom edge of the page, in
|
|
301
|
+
* points (ECMA-376 `pgMar.footer`). The footer band's top sits at
|
|
302
|
+
* `height - footerOffset`.
|
|
303
|
+
*/
|
|
304
|
+
readonly footerOffset: number;
|
|
293
305
|
}
|
|
294
306
|
/** A fully laid-out page. */
|
|
295
307
|
export interface LayoutPage {
|
|
@@ -68,17 +68,33 @@ export function mergeDocuments(documents, options) {
|
|
|
68
68
|
const mergedComments = base.comments ? [...base.comments] : [];
|
|
69
69
|
for (let i = 1; i < documents.length; i++) {
|
|
70
70
|
const doc = documents[i];
|
|
71
|
-
//
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
71
|
+
// Mark a section break BEFORE appending the next document. In OOXML a
|
|
72
|
+
// section break is carried by the `sectPr` of the LAST paragraph of the
|
|
73
|
+
// preceding section — NOT by an extra empty paragraph. Appending an empty
|
|
74
|
+
// <w:p> with only a sectPr makes Word render a stray blank line / blank
|
|
75
|
+
// page. So we attach the break to the last paragraph already in the body;
|
|
76
|
+
// only if the body currently ends with a non-paragraph block (e.g. a
|
|
77
|
+
// table, which cannot carry a sectPr directly) do we fall back to a
|
|
78
|
+
// minimal carrier paragraph.
|
|
79
|
+
const lastBlock = mergedBody[mergedBody.length - 1];
|
|
80
|
+
if (lastBlock && lastBlock.type === "paragraph") {
|
|
81
|
+
const para = lastBlock;
|
|
82
|
+
mergedBody[mergedBody.length - 1] = {
|
|
83
|
+
...para,
|
|
84
|
+
properties: {
|
|
85
|
+
...para.properties,
|
|
86
|
+
sectionProperties: { ...para.properties?.sectionProperties, breakType }
|
|
77
87
|
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
const sectionBreakPara = {
|
|
92
|
+
type: "paragraph",
|
|
93
|
+
properties: { sectionProperties: { breakType } },
|
|
94
|
+
children: []
|
|
95
|
+
};
|
|
96
|
+
mergedBody.push(sectionBreakPara);
|
|
97
|
+
}
|
|
82
98
|
// Compute id remappings BEFORE cloning the body so we can rewrite refs
|
|
83
99
|
// during the clone in a single pass.
|
|
84
100
|
const numMaps = mergeNumberingDefinitions(doc, mergedAbstractNums, mergedNumInstances);
|
|
@@ -119,17 +119,83 @@ function paragraphHasExplicitPageBreakRun(para) {
|
|
|
119
119
|
}
|
|
120
120
|
return false;
|
|
121
121
|
}
|
|
122
|
+
/**
|
|
123
|
+
* Return a copy of `para` with every explicit page-break run content removed.
|
|
124
|
+
* Runs left empty by the removal are dropped. Used when splitting by page
|
|
125
|
+
* break so the trailing break that separated this segment from the next does
|
|
126
|
+
* not render as a blank page in the standalone split document.
|
|
127
|
+
*/
|
|
128
|
+
function stripPageBreakRuns(para) {
|
|
129
|
+
const newChildren = [];
|
|
130
|
+
for (const child of para.children) {
|
|
131
|
+
if (isRun(child)) {
|
|
132
|
+
const content = child.content.filter(c => !(c.type === "break" && c.breakType === "page"));
|
|
133
|
+
if (content.length > 0) {
|
|
134
|
+
newChildren.push({ ...child, content });
|
|
135
|
+
}
|
|
136
|
+
// Drop runs that became empty after removing the page break.
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
newChildren.push(child);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return { ...para, children: newChildren };
|
|
143
|
+
}
|
|
144
|
+
/** True when a paragraph has no children (after page-break stripping). */
|
|
145
|
+
function paragraphIsEmpty(para) {
|
|
146
|
+
return para.children.length === 0;
|
|
147
|
+
}
|
|
122
148
|
function buildSplitDoc(source, segment, opts) {
|
|
149
|
+
// When splitting by section, the segment's last paragraph carries the
|
|
150
|
+
// section break (often `nextPage`) that originally separated it from the
|
|
151
|
+
// following section. In a standalone split document that break has nothing
|
|
152
|
+
// after it, so Word renders a trailing blank page. Strip the paragraph-level
|
|
153
|
+
// sectPr and promote its page setup to the document's own section
|
|
154
|
+
// properties instead.
|
|
155
|
+
let body = segment;
|
|
156
|
+
let promotedSectPr;
|
|
157
|
+
if (opts.by === "section" && segment.length > 0) {
|
|
158
|
+
const last = segment[segment.length - 1];
|
|
159
|
+
if (last.type === "paragraph" && last.properties?.sectionProperties) {
|
|
160
|
+
const para = last;
|
|
161
|
+
const props = para.properties;
|
|
162
|
+
const sectionProperties = props.sectionProperties;
|
|
163
|
+
const restProps = { ...props };
|
|
164
|
+
delete restProps.sectionProperties;
|
|
165
|
+
// Drop the inter-section break type: in a standalone document this is
|
|
166
|
+
// the (only/first) section, so a "nextPage" break would just push all
|
|
167
|
+
// content onto page 2, leaving page 1 blank.
|
|
168
|
+
const { breakType: _drop, ...sectWithoutBreak } = sectionProperties;
|
|
169
|
+
promotedSectPr = sectWithoutBreak;
|
|
170
|
+
const cleaned = { ...para, properties: restProps };
|
|
171
|
+
body = [...segment.slice(0, -1), cleaned];
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
else if (opts.by === "pageBreak" && segment.length > 0) {
|
|
175
|
+
// The segment ends with the paragraph that held the splitting page break.
|
|
176
|
+
// In a standalone document that trailing page break has nothing after it
|
|
177
|
+
// and renders a blank page, so remove it. If the paragraph held only the
|
|
178
|
+
// page break, drop the now-empty paragraph entirely.
|
|
179
|
+
const last = segment[segment.length - 1];
|
|
180
|
+
if (last.type === "paragraph" && paragraphHasExplicitPageBreakRun(last)) {
|
|
181
|
+
const stripped = stripPageBreakRuns(last);
|
|
182
|
+
body = paragraphIsEmpty(stripped)
|
|
183
|
+
? segment.slice(0, -1)
|
|
184
|
+
: [...segment.slice(0, -1), stripped];
|
|
185
|
+
}
|
|
186
|
+
}
|
|
123
187
|
if (!opts.preserveSharedParts) {
|
|
124
188
|
// Minimal split: just body + docType
|
|
125
189
|
return {
|
|
126
190
|
docType: source.docType,
|
|
127
|
-
body
|
|
191
|
+
body,
|
|
192
|
+
...(promotedSectPr ? { sectionProperties: promotedSectPr } : {})
|
|
128
193
|
};
|
|
129
194
|
}
|
|
130
195
|
// Full split: preserve all shared parts
|
|
131
196
|
return {
|
|
132
197
|
...source,
|
|
133
|
-
body
|
|
198
|
+
body,
|
|
199
|
+
...(promotedSectPr ? { sectionProperties: promotedSectPr } : {})
|
|
134
200
|
};
|
|
135
201
|
}
|
|
@@ -246,6 +246,23 @@ function parseDrawingShape(anchorEl, wspEl, ctx) {
|
|
|
246
246
|
shape.noOutline = true;
|
|
247
247
|
}
|
|
248
248
|
}
|
|
249
|
+
// Parse transform (rotation / flip) from a:xfrm
|
|
250
|
+
const xfrmEl = findChild(spPrEl, "a:xfrm") ?? findChildNs(spPrEl, "xfrm");
|
|
251
|
+
if (xfrmEl) {
|
|
252
|
+
const rot = xfrmEl.attributes["rot"];
|
|
253
|
+
if (rot) {
|
|
254
|
+
const n = parseInt(rot, 10);
|
|
255
|
+
if (!Number.isNaN(n)) {
|
|
256
|
+
shape.rotation = n;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
if (xfrmEl.attributes["flipH"] === "1") {
|
|
260
|
+
shape.flipHorizontal = true;
|
|
261
|
+
}
|
|
262
|
+
if (xfrmEl.attributes["flipV"] === "1") {
|
|
263
|
+
shape.flipVertical = true;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
249
266
|
}
|
|
250
267
|
// Parse text content (wps:txbx > w:txbxContent)
|
|
251
268
|
const txbxEl = findChild(wspEl, "wps:txbx") ?? findChildNs(wspEl, "txbx");
|
|
@@ -263,6 +280,12 @@ function parseDrawingShape(anchorEl, wspEl, ctx) {
|
|
|
263
280
|
shape.textContent = paras;
|
|
264
281
|
}
|
|
265
282
|
}
|
|
283
|
+
// Parse text body vertical anchor (wps:bodyPr/@anchor)
|
|
284
|
+
const bodyPrEl = findChild(wspEl, "wps:bodyPr") ?? findChildNs(wspEl, "bodyPr");
|
|
285
|
+
const anchorAttr = bodyPrEl?.attributes["anchor"];
|
|
286
|
+
if (anchorAttr === "t" || anchorAttr === "ctr" || anchorAttr === "b") {
|
|
287
|
+
shape.textBodyAnchor = anchorAttr;
|
|
288
|
+
}
|
|
266
289
|
// Parse positioning
|
|
267
290
|
const posH = findChild(anchorEl, "wp:positionH");
|
|
268
291
|
if (posH) {
|
|
@@ -14,6 +14,15 @@ export interface CfbEntry {
|
|
|
14
14
|
readonly name: string;
|
|
15
15
|
/** Stream data. */
|
|
16
16
|
readonly data: Uint8Array;
|
|
17
|
+
/**
|
|
18
|
+
* Optional storage path for the stream. Each element is a storage
|
|
19
|
+
* (directory) name; the stream lives inside the nested storages.
|
|
20
|
+
* For example `path: ["\u0006DataSpaces", "TransformInfo"]` with
|
|
21
|
+
* `name: "..."` places the stream at
|
|
22
|
+
* `\u0006DataSpaces/TransformInfo/<name>`. Omit or use `[]` for a
|
|
23
|
+
* top-level stream under the root storage.
|
|
24
|
+
*/
|
|
25
|
+
readonly path?: readonly string[];
|
|
17
26
|
}
|
|
18
27
|
/**
|
|
19
28
|
* Read a CFB (OLE2 Compound File) and extract all stream entries.
|
|
@@ -27,9 +36,11 @@ export declare function readCfb(buffer: Uint8Array): CfbEntry[];
|
|
|
27
36
|
/**
|
|
28
37
|
* Write a set of named stream entries into a CFB (OLE2 Compound File) container.
|
|
29
38
|
*
|
|
30
|
-
* Produces a
|
|
31
|
-
*
|
|
32
|
-
*
|
|
39
|
+
* Produces a v3 CFB with 512-byte sectors. Streams smaller than 4096 bytes are
|
|
40
|
+
* stored in the mini-stream (64-byte mini-sectors) exactly as Office does;
|
|
41
|
+
* larger streams use regular sectors. Entries may declare a `path` to nest the
|
|
42
|
+
* stream inside one or more storages — required for the `\u0006DataSpaces`
|
|
43
|
+
* structure that Office demands in encrypted documents.
|
|
33
44
|
*
|
|
34
45
|
* @param entries - Named stream entries to include.
|
|
35
46
|
* @returns The CFB file as a Uint8Array.
|