@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
|
@@ -90,6 +90,16 @@ function renderPageBorders(xml, borders) {
|
|
|
90
90
|
/** Render w:sectPr element. */
|
|
91
91
|
function renderSectionProperties(xml, sect, insidePropertyChange = false) {
|
|
92
92
|
xml.openNode("w:sectPr");
|
|
93
|
+
// NOTE: Child element ordering below is dictated by the OOXML schema
|
|
94
|
+
// (ISO/IEC 29500 — CT_SectPr / EG_SectPrContents). Microsoft Word rejects
|
|
95
|
+
// (or silently "repairs") documents whose sectPr children are out of order,
|
|
96
|
+
// so the sequence here MUST stay:
|
|
97
|
+
// headerReference*, footerReference*, footnotePr, endnotePr, type, pgSz,
|
|
98
|
+
// pgMar, paperSrc, pgBorders, lnNumType, pgNumType, cols, formProt,
|
|
99
|
+
// vAlign, noEndnote, titlePg, textDirection, bidi, rtlGutter, docGrid,
|
|
100
|
+
// printerSettings
|
|
101
|
+
// (headerReference/footerReference come from EG_HdrFtrReferences, which the
|
|
102
|
+
// schema places ahead of EG_SectPrContents.)
|
|
93
103
|
// Header references
|
|
94
104
|
if (sect.headers) {
|
|
95
105
|
for (const ref of sect.headers) {
|
|
@@ -102,6 +112,14 @@ function renderSectionProperties(xml, sect, insidePropertyChange = false) {
|
|
|
102
112
|
renderHeaderFooterRef(xml, "w:footerReference", ref);
|
|
103
113
|
}
|
|
104
114
|
}
|
|
115
|
+
// Footnote properties
|
|
116
|
+
if (sect.footnoteProperties) {
|
|
117
|
+
renderNoteProperties(xml, "w:footnotePr", sect.footnoteProperties);
|
|
118
|
+
}
|
|
119
|
+
// Endnote properties
|
|
120
|
+
if (sect.endnoteProperties) {
|
|
121
|
+
renderNoteProperties(xml, "w:endnotePr", sect.endnoteProperties);
|
|
122
|
+
}
|
|
105
123
|
// Section break type
|
|
106
124
|
if (sect.breakType) {
|
|
107
125
|
xml.leafNode("w:type", { "w:val": sect.breakType });
|
|
@@ -134,29 +152,7 @@ function renderSectionProperties(xml, sect, insidePropertyChange = false) {
|
|
|
134
152
|
if (sect.pageBorders) {
|
|
135
153
|
renderPageBorders(xml, sect.pageBorders);
|
|
136
154
|
}
|
|
137
|
-
//
|
|
138
|
-
if (sect.columns) {
|
|
139
|
-
renderColumns(xml, sect.columns);
|
|
140
|
-
}
|
|
141
|
-
else {
|
|
142
|
-
xml.leafNode("w:cols", { "w:space": String(constants_1.DEFAULT_COLUMN_SPACE) });
|
|
143
|
-
}
|
|
144
|
-
// Title page (different first page header/footer)
|
|
145
|
-
if (sect.titlePage) {
|
|
146
|
-
xml.leafNode("w:titlePg");
|
|
147
|
-
}
|
|
148
|
-
// Page numbering
|
|
149
|
-
if (sect.pageNumbering) {
|
|
150
|
-
const attrs = {};
|
|
151
|
-
if (sect.pageNumbering.start !== undefined) {
|
|
152
|
-
attrs["w:start"] = String(sect.pageNumbering.start);
|
|
153
|
-
}
|
|
154
|
-
if (sect.pageNumbering.format) {
|
|
155
|
-
attrs["w:fmt"] = sect.pageNumbering.format;
|
|
156
|
-
}
|
|
157
|
-
xml.leafNode("w:pgNumType", attrs);
|
|
158
|
-
}
|
|
159
|
-
// Line numbers
|
|
155
|
+
// Line numbers (must precede pgNumType per schema)
|
|
160
156
|
if (sect.lineNumbers) {
|
|
161
157
|
const attrs = {};
|
|
162
158
|
if (sect.lineNumbers.countBy !== undefined) {
|
|
@@ -173,10 +169,37 @@ function renderSectionProperties(xml, sect, insidePropertyChange = false) {
|
|
|
173
169
|
}
|
|
174
170
|
xml.leafNode("w:lnNumType", attrs);
|
|
175
171
|
}
|
|
172
|
+
// Page numbering
|
|
173
|
+
if (sect.pageNumbering) {
|
|
174
|
+
const attrs = {};
|
|
175
|
+
if (sect.pageNumbering.start !== undefined) {
|
|
176
|
+
attrs["w:start"] = String(sect.pageNumbering.start);
|
|
177
|
+
}
|
|
178
|
+
if (sect.pageNumbering.format) {
|
|
179
|
+
attrs["w:fmt"] = sect.pageNumbering.format;
|
|
180
|
+
}
|
|
181
|
+
xml.leafNode("w:pgNumType", attrs);
|
|
182
|
+
}
|
|
183
|
+
// Columns
|
|
184
|
+
if (sect.columns) {
|
|
185
|
+
renderColumns(xml, sect.columns);
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
xml.leafNode("w:cols", { "w:space": String(constants_1.DEFAULT_COLUMN_SPACE) });
|
|
189
|
+
}
|
|
190
|
+
// Form protection
|
|
191
|
+
if (sect.formProtection) {
|
|
192
|
+
xml.leafNode("w:formProt", { "w:val": "1" });
|
|
193
|
+
}
|
|
176
194
|
// Vertical alignment
|
|
177
195
|
if (sect.verticalAlign) {
|
|
178
196
|
xml.leafNode("w:vAlign", { "w:val": sect.verticalAlign });
|
|
179
197
|
}
|
|
198
|
+
// Title page (different first page header/footer) — schema places this
|
|
199
|
+
// after vAlign/noEndnote and before textDirection.
|
|
200
|
+
if (sect.titlePage) {
|
|
201
|
+
xml.leafNode("w:titlePg");
|
|
202
|
+
}
|
|
180
203
|
// Text direction
|
|
181
204
|
if (sect.textDirection) {
|
|
182
205
|
xml.leafNode("w:textDirection", { "w:val": sect.textDirection });
|
|
@@ -189,18 +212,6 @@ function renderSectionProperties(xml, sect, insidePropertyChange = false) {
|
|
|
189
212
|
if (sect.rtlGutter) {
|
|
190
213
|
xml.leafNode("w:rtlGutter");
|
|
191
214
|
}
|
|
192
|
-
// Form protection
|
|
193
|
-
if (sect.formProtection) {
|
|
194
|
-
xml.leafNode("w:formProt", { "w:val": "1" });
|
|
195
|
-
}
|
|
196
|
-
// Footnote properties
|
|
197
|
-
if (sect.footnoteProperties) {
|
|
198
|
-
renderNoteProperties(xml, "w:footnotePr", sect.footnoteProperties);
|
|
199
|
-
}
|
|
200
|
-
// Endnote properties
|
|
201
|
-
if (sect.endnoteProperties) {
|
|
202
|
-
renderNoteProperties(xml, "w:endnotePr", sect.endnoteProperties);
|
|
203
|
-
}
|
|
204
215
|
// Document grid
|
|
205
216
|
if (sect.docGrid) {
|
|
206
217
|
const gridAttrs = {
|
|
@@ -904,6 +904,7 @@ class StreamingDocxWriter {
|
|
|
904
904
|
addXmlFile(constants_1.PartPath.Comments, xml => (0, comment_writer_1.renderComments)(xml, this._options.comments, {
|
|
905
905
|
imageRemap: this._renderCtx.imageRIdRemap,
|
|
906
906
|
hyperlinkRIds: this._renderCtx.hyperlinkRIds,
|
|
907
|
+
nextDocPrId: this._renderCtx.ids.nextDocPrId,
|
|
907
908
|
rawXmlPolicy: this._renderCtx.rawXmlPolicy
|
|
908
909
|
}));
|
|
909
910
|
// Also write commentsExtended if any have done/parentId
|
|
@@ -977,6 +978,7 @@ class StreamingDocxWriter {
|
|
|
977
978
|
addXmlFile(headerPath, xml => (0, header_footer_writer_1.renderHeader)(xml, headerDef.content, {
|
|
978
979
|
imageRemap: this._renderCtx.imageRIdRemap,
|
|
979
980
|
hyperlinkRIds: this._renderCtx.hyperlinkRIds,
|
|
981
|
+
nextDocPrId: this._renderCtx.ids.nextDocPrId,
|
|
980
982
|
rawXmlPolicy: this._renderCtx.rawXmlPolicy
|
|
981
983
|
}));
|
|
982
984
|
if ((0, relationships_1.getRelationshipCount)(hRels) > 0) {
|
|
@@ -1047,6 +1049,7 @@ class StreamingDocxWriter {
|
|
|
1047
1049
|
addXmlFile(footerPath, xml => (0, header_footer_writer_1.renderFooter)(xml, footerDef.content, {
|
|
1048
1050
|
imageRemap: this._renderCtx.imageRIdRemap,
|
|
1049
1051
|
hyperlinkRIds: this._renderCtx.hyperlinkRIds,
|
|
1052
|
+
nextDocPrId: this._renderCtx.ids.nextDocPrId,
|
|
1050
1053
|
rawXmlPolicy: this._renderCtx.rawXmlPolicy
|
|
1051
1054
|
}));
|
|
1052
1055
|
if ((0, relationships_1.getRelationshipCount)(fRels) > 0) {
|
|
@@ -1225,6 +1228,7 @@ class StreamingDocxWriter {
|
|
|
1225
1228
|
notesHelpers: {
|
|
1226
1229
|
imageRemap: this._renderCtx.imageRIdRemap,
|
|
1227
1230
|
hyperlinkRIds: this._renderCtx.hyperlinkRIds,
|
|
1231
|
+
nextDocPrId: this._renderCtx.ids.nextDocPrId,
|
|
1228
1232
|
rawXmlPolicy: this._renderCtx.rawXmlPolicy
|
|
1229
1233
|
}
|
|
1230
1234
|
});
|
|
@@ -173,6 +173,17 @@ function renderStyle(xml, style) {
|
|
|
173
173
|
// For table styles
|
|
174
174
|
if (style.tableProperties) {
|
|
175
175
|
xml.openNode("w:tblPr");
|
|
176
|
+
// Per CT_TblPrBase order, band sizes precede w:tblW.
|
|
177
|
+
if (style.tableProperties.rowBandSize !== undefined) {
|
|
178
|
+
xml.leafNode("w:tblStyleRowBandSize", {
|
|
179
|
+
"w:val": String(style.tableProperties.rowBandSize)
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
if (style.tableProperties.colBandSize !== undefined) {
|
|
183
|
+
xml.leafNode("w:tblStyleColBandSize", {
|
|
184
|
+
"w:val": String(style.tableProperties.colBandSize)
|
|
185
|
+
});
|
|
186
|
+
}
|
|
176
187
|
if (style.tableProperties.width) {
|
|
177
188
|
xml.leafNode("w:tblW", {
|
|
178
189
|
"w:w": String(style.tableProperties.width.value),
|
|
@@ -171,6 +171,12 @@ function renderTableProperties(xml, tPr, insidePropertyChange = false) {
|
|
|
171
171
|
xml.leafNode("w:tblOverlap", { "w:val": tPr.float.overlap });
|
|
172
172
|
}
|
|
173
173
|
}
|
|
174
|
+
if (tPr.rowBandSize !== undefined) {
|
|
175
|
+
xml.leafNode("w:tblStyleRowBandSize", { "w:val": String(tPr.rowBandSize) });
|
|
176
|
+
}
|
|
177
|
+
if (tPr.colBandSize !== undefined) {
|
|
178
|
+
xml.leafNode("w:tblStyleColBandSize", { "w:val": String(tPr.colBandSize) });
|
|
179
|
+
}
|
|
174
180
|
if (tPr.width) {
|
|
175
181
|
renderTableWidth(xml, "w:tblW", tPr.width);
|
|
176
182
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { BaseXform } from "../base-xform.js";
|
|
2
2
|
import { RichTextXform } from "../strings/rich-text-xform.js";
|
|
3
|
+
import { TextXform } from "../strings/text-xform.js";
|
|
3
4
|
class CommentXform extends BaseXform {
|
|
4
5
|
constructor(model) {
|
|
5
6
|
super();
|
|
@@ -14,6 +15,12 @@ class CommentXform extends BaseXform {
|
|
|
14
15
|
}
|
|
15
16
|
return this._richTextXform;
|
|
16
17
|
}
|
|
18
|
+
get textXform() {
|
|
19
|
+
if (!this._textXform) {
|
|
20
|
+
this._textXform = new TextXform();
|
|
21
|
+
}
|
|
22
|
+
return this._textXform;
|
|
23
|
+
}
|
|
17
24
|
render(xmlStream, model) {
|
|
18
25
|
const renderModel = model || this.model;
|
|
19
26
|
xmlStream.openNode("comment", {
|
|
@@ -49,6 +56,13 @@ class CommentXform extends BaseXform {
|
|
|
49
56
|
this.parser = this.richTextXform;
|
|
50
57
|
this.parser.parseOpen(node);
|
|
51
58
|
return true;
|
|
59
|
+
case "t":
|
|
60
|
+
// Legacy comments (e.g. produced by openpyxl/LibreOffice) may store the
|
|
61
|
+
// body as a bare <t> directly under <text> with no <r> run wrapper.
|
|
62
|
+
// This is valid for the CT_Rst type, so treat it like a run without font.
|
|
63
|
+
this.parser = this.textXform;
|
|
64
|
+
this.parser.parseOpen(node);
|
|
65
|
+
return true;
|
|
52
66
|
default:
|
|
53
67
|
return false;
|
|
54
68
|
}
|
|
@@ -59,17 +73,26 @@ class CommentXform extends BaseXform {
|
|
|
59
73
|
}
|
|
60
74
|
}
|
|
61
75
|
parseClose(name) {
|
|
76
|
+
if (this.parser) {
|
|
77
|
+
if (!this.parser.parseClose(name)) {
|
|
78
|
+
// The active sub-parser has finished. Collect its result.
|
|
79
|
+
if (this.parser === this._richTextXform) {
|
|
80
|
+
// <r> run: model is already a { font?, text } run.
|
|
81
|
+
this.model.note.texts.push(this.parser.model);
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
// Bare <t> body (e.g. openpyxl/LibreOffice): wrap the plain string
|
|
85
|
+
// as a single run without font, mirroring a <r><t> run.
|
|
86
|
+
this.model.note.texts.push({ text: this.parser.model });
|
|
87
|
+
}
|
|
88
|
+
this.parser = undefined;
|
|
89
|
+
}
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
62
92
|
switch (name) {
|
|
63
93
|
case "comment":
|
|
64
94
|
return false;
|
|
65
|
-
case "r":
|
|
66
|
-
this.model.note.texts.push(this.parser.model);
|
|
67
|
-
this.parser = undefined;
|
|
68
|
-
return true;
|
|
69
95
|
default:
|
|
70
|
-
if (this.parser) {
|
|
71
|
-
this.parser.parseClose(name);
|
|
72
|
-
}
|
|
73
96
|
return true;
|
|
74
97
|
}
|
|
75
98
|
}
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
// Consumers that convert workbooks with charts must call
|
|
22
22
|
// `installChartSupport()` from `@cj-tech-master/excelts/chart` before
|
|
23
23
|
// invoking `excelToPdf()`.
|
|
24
|
-
import { getChartSupport } from "../excel/chart-host-registry.js";
|
|
24
|
+
import { getChartSupport, tryGetChartSupport } from "../excel/chart-host-registry.js";
|
|
25
25
|
import { ValueType } from "../excel/enums.js";
|
|
26
26
|
import { formatCellValue } from "../excel/utils/cell-format.js";
|
|
27
27
|
import { tryInvokeFormulaEngine } from "../formula/host-registry.js";
|
|
@@ -1172,3 +1172,69 @@ export function createWordChartPdfRenderer() {
|
|
|
1172
1172
|
support.drawChartPdf(page, model, rect);
|
|
1173
1173
|
};
|
|
1174
1174
|
}
|
|
1175
|
+
/**
|
|
1176
|
+
* Create a layout-aware chart renderer for use as the internal
|
|
1177
|
+
* `RenderLayoutOptions.chartRenderer` of the Word→PDF bridge.
|
|
1178
|
+
*
|
|
1179
|
+
* Unlike {@link createWordChartPdfRenderer} (which only sees the inner
|
|
1180
|
+
* classic `Chart` model), this renderer receives the full
|
|
1181
|
+
* {@link LayoutChart} and therefore handles **both** chart families
|
|
1182
|
+
* with the full Excel rendering engine:
|
|
1183
|
+
*
|
|
1184
|
+
* - Classic `<c:chart>` (`chartKind === "chart"`) → `wordChartToChartModel`
|
|
1185
|
+
* → `drawChartPdf` (vector).
|
|
1186
|
+
* - Modern `<cx:chartSpace>` ChartEx (`chartKind === "chartEx"`,
|
|
1187
|
+
* e.g. sunburst / treemap / waterfall / funnel / boxWhisker /
|
|
1188
|
+
* histogram / pareto / regionMap) → `parseChartEx` → `drawChartExPdf`
|
|
1189
|
+
* (vector) when the layout is vector-capable, otherwise the
|
|
1190
|
+
* pre-rendered SVG carried on the `LayoutChart` is left for the
|
|
1191
|
+
* translator's fallback.
|
|
1192
|
+
*
|
|
1193
|
+
* Returns `false` to decline a chart so the translator's built-in
|
|
1194
|
+
* fallback (inline SVG, then a titled placeholder box) takes over. This
|
|
1195
|
+
* keeps "fail soft" behaviour: a chart the engine can't draw still
|
|
1196
|
+
* renders *something* rather than a blank slot.
|
|
1197
|
+
*
|
|
1198
|
+
* Requires `installChartSupport()` to have been called.
|
|
1199
|
+
*/
|
|
1200
|
+
export function createWordLayoutChartPdfRenderer() {
|
|
1201
|
+
return (layoutChart, page, rect) => {
|
|
1202
|
+
const support = tryGetChartSupport();
|
|
1203
|
+
if (!support) {
|
|
1204
|
+
// Chart support not installed — decline so the Word→PDF
|
|
1205
|
+
// translator falls back to the inline SVG / placeholder. This
|
|
1206
|
+
// mirrors the auto-detect contract in `word-bridge.ts`, where a
|
|
1207
|
+
// missing chart runtime must degrade gracefully rather than throw.
|
|
1208
|
+
return false;
|
|
1209
|
+
}
|
|
1210
|
+
const source = layoutChart.source;
|
|
1211
|
+
if (layoutChart.chartKind === "chart") {
|
|
1212
|
+
// Classic chart: prefer the structured source, fall back to nothing.
|
|
1213
|
+
if (source && source.type === "chart") {
|
|
1214
|
+
support.drawChartPdf(page, wordChartToChartModel(source.chart), rect);
|
|
1215
|
+
return;
|
|
1216
|
+
}
|
|
1217
|
+
return false;
|
|
1218
|
+
}
|
|
1219
|
+
// ChartEx. Parse the carried `cx:chartSpace` XML into a ChartExModel
|
|
1220
|
+
// and render it as vector PDF when the layout IDs are supported.
|
|
1221
|
+
if (source && source.type === "chartEx" && source.chartExXml) {
|
|
1222
|
+
let model;
|
|
1223
|
+
try {
|
|
1224
|
+
model = support.parseChartEx(source.chartExXml);
|
|
1225
|
+
}
|
|
1226
|
+
catch {
|
|
1227
|
+
return false; // Malformed XML — let the fallback path handle it.
|
|
1228
|
+
}
|
|
1229
|
+
if (model && support.canRenderChartExAsVectorPdf(model)) {
|
|
1230
|
+
support.drawChartExPdf(page, model, rect, {
|
|
1231
|
+
title: layoutChart.title
|
|
1232
|
+
});
|
|
1233
|
+
return;
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
// Not vector-capable (or no source): decline so the translator
|
|
1237
|
+
// falls back to the inline SVG / placeholder.
|
|
1238
|
+
return false;
|
|
1239
|
+
};
|
|
1240
|
+
}
|
|
@@ -46,47 +46,59 @@ export async function docxToPdf(doc, options) {
|
|
|
46
46
|
// caller explicitly overrode an axis. Margins are independent: the
|
|
47
47
|
// section's margins are applied unless the caller overrode them.
|
|
48
48
|
const layoutOptions = mapToLayoutOptions(doc, options);
|
|
49
|
-
// 2.
|
|
50
|
-
//
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
// Chart support not available — placeholder rendering takes over.
|
|
49
|
+
// 2. Try to obtain the built-in, layout-aware Excel chart renderer.
|
|
50
|
+
// It handles BOTH classic `<c:chart>` and modern `<cx:chartSpace>`
|
|
51
|
+
// ChartEx families (sunburst / treemap / waterfall / …). It is used
|
|
52
|
+
// directly when the caller supplies no `chartRenderer`, and as the
|
|
53
|
+
// fallback for ChartEx (which the public `Chart`-typed callback
|
|
54
|
+
// cannot express) or whenever a user callback declines a chart.
|
|
55
|
+
let builtInLayoutRenderer;
|
|
56
|
+
try {
|
|
57
|
+
const mod = await import("./excel-bridge.js");
|
|
58
|
+
if (typeof mod.createWordLayoutChartPdfRenderer === "function") {
|
|
59
|
+
builtInLayoutRenderer = mod.createWordLayoutChartPdfRenderer();
|
|
61
60
|
}
|
|
62
61
|
}
|
|
62
|
+
catch {
|
|
63
|
+
// Chart support not available — placeholder rendering takes over.
|
|
64
|
+
}
|
|
63
65
|
// 3. Run the layout engine. Everything from line wrapping to page
|
|
64
66
|
// breaks happens here; word-bridge no longer carries any of that.
|
|
65
67
|
const layout = layoutDocumentFull(doc, layoutOptions);
|
|
66
68
|
// 4. Build a render-options object for the PDF translator. The
|
|
67
|
-
//
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
//
|
|
69
|
+
// chart-rendering precedence is:
|
|
70
|
+
// a. classic chart + user callback → user callback (its `false`
|
|
71
|
+
// return falls through to the built-in layout renderer);
|
|
72
|
+
// b. ChartEx, or classic chart with no user callback, or a
|
|
73
|
+
// declined user callback → built-in layout-aware renderer;
|
|
74
|
+
// c. neither available / both decline → translator fallback
|
|
75
|
+
// (inline SVG, then a titled placeholder box).
|
|
76
|
+
const userChartRenderer = options?.chartRenderer;
|
|
71
77
|
const renderOptions = {
|
|
72
78
|
title: doc.coreProperties?.title,
|
|
73
79
|
author: doc.coreProperties?.creator,
|
|
74
80
|
subject: doc.coreProperties?.subject,
|
|
75
81
|
defaultFont: options?.defaultFont ?? "Helvetica",
|
|
76
82
|
defaultFontSize: options?.defaultFontSize ?? 11,
|
|
77
|
-
chartRenderer:
|
|
83
|
+
chartRenderer: userChartRenderer || builtInLayoutRenderer
|
|
78
84
|
? (layoutChart, page, rect) => {
|
|
79
85
|
const src = layoutChart.source;
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
+
// (a) Classic chart with a user-supplied callback: honour it
|
|
87
|
+
// first. Only fall through to the built-in renderer when
|
|
88
|
+
// it explicitly declines (`false`).
|
|
89
|
+
if (userChartRenderer && src && src.type === "chart") {
|
|
90
|
+
const handled = userChartRenderer(src.chart, page, rect);
|
|
91
|
+
if (handled !== false) {
|
|
92
|
+
return handled;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
// (b) Built-in layout-aware renderer handles classic charts
|
|
96
|
+
// without a user callback AND all ChartEx charts.
|
|
97
|
+
if (builtInLayoutRenderer) {
|
|
98
|
+
return builtInLayoutRenderer(layoutChart, page, rect);
|
|
86
99
|
}
|
|
87
|
-
//
|
|
88
|
-
//
|
|
89
|
-
// placeholder runs instead of leaving a blank slot.
|
|
100
|
+
// (c) Decline so the translator's placeholder runs instead of
|
|
101
|
+
// leaving a blank slot.
|
|
90
102
|
return false;
|
|
91
103
|
}
|
|
92
104
|
: undefined
|
|
@@ -105,13 +117,11 @@ export async function docxToPdf(doc, options) {
|
|
|
105
117
|
* portrait-oriented numbers); otherwise the layout engine's defaults
|
|
106
118
|
* (US Letter, 1-inch margins) take over.
|
|
107
119
|
*
|
|
108
|
-
* `headerMargin` / `footerMargin` are
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
* rely on those parameters still get the section's `headerMargin` /
|
|
114
|
-
* `footerMargin` from sectionProperties when they exist.
|
|
120
|
+
* `headerMargin` / `footerMargin` are forwarded to the layout engine
|
|
121
|
+
* as header / footer band offsets (ECMA-376 `pgMar.header` /
|
|
122
|
+
* `pgMar.footer`). When omitted, the section's own header / footer
|
|
123
|
+
* margins apply; when neither exists the engine default of 36pt (0.5")
|
|
124
|
+
* is used.
|
|
115
125
|
*/
|
|
116
126
|
function mapToLayoutOptions(doc, options) {
|
|
117
127
|
const sectProps = doc.sectionProperties;
|
|
@@ -136,7 +146,12 @@ function mapToLayoutOptions(doc, options) {
|
|
|
136
146
|
marginTop: options?.marginTop ?? sectionMarginTopPt,
|
|
137
147
|
marginBottom: options?.marginBottom ?? sectionMarginBottomPt,
|
|
138
148
|
marginLeft: options?.marginLeft ?? sectionMarginLeftPt,
|
|
139
|
-
marginRight: options?.marginRight ?? sectionMarginRightPt
|
|
149
|
+
marginRight: options?.marginRight ?? sectionMarginRightPt,
|
|
150
|
+
// Header / footer offsets: only forward an explicit caller value.
|
|
151
|
+
// Leaving these undefined lets the layout engine fall back to the
|
|
152
|
+
// section's `pgMar.header` / `pgMar.footer` (then the 36pt default).
|
|
153
|
+
headerMargin: options?.headerMargin,
|
|
154
|
+
footerMargin: options?.footerMargin
|
|
140
155
|
};
|
|
141
156
|
const layoutOpts = {};
|
|
142
157
|
// Only attach pageGeometry when at least one axis is actually
|
|
@@ -144,24 +144,136 @@ function computeDiff(oldTexts, newTexts) {
|
|
|
144
144
|
}
|
|
145
145
|
// Reverse since we built it backwards
|
|
146
146
|
entries.reverse();
|
|
147
|
-
|
|
147
|
+
return pairModifications(entries);
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Pair deletions with insertions that represent the *same* paragraph in a
|
|
151
|
+
* modified form, based on text similarity.
|
|
152
|
+
*
|
|
153
|
+
* The LCS pass only matches paragraphs whose text is byte-identical, so when
|
|
154
|
+
* every paragraph is lightly edited it produces all-deletions + all-insertions
|
|
155
|
+
* with no "modified" at all. Within each contiguous change block (a run of
|
|
156
|
+
* deletions/insertions bounded by unchanged entries), we greedily pair each
|
|
157
|
+
* deletion with the most similar insertion whose similarity clears
|
|
158
|
+
* {@link MODIFY_SIMILARITY_THRESHOLD}. Pairs become "modified"; anything left
|
|
159
|
+
* unpaired stays a pure deletion or insertion. This yields, e.g., a recipe
|
|
160
|
+
* whose steps were all tweaked → mostly "modified", with a removed step as a
|
|
161
|
+
* pure deletion and a brand-new step as a pure insertion.
|
|
162
|
+
*/
|
|
163
|
+
function pairModifications(entries) {
|
|
148
164
|
const result = [];
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
const
|
|
152
|
-
if (
|
|
153
|
-
result.push(
|
|
165
|
+
let k = 0;
|
|
166
|
+
while (k < entries.length) {
|
|
167
|
+
const entry = entries[k];
|
|
168
|
+
if (entry.type !== "deleted" && entry.type !== "added") {
|
|
169
|
+
result.push(entry);
|
|
170
|
+
k++;
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
// Collect the maximal contiguous run of deleted/added entries.
|
|
174
|
+
const dels = [];
|
|
175
|
+
const adds = [];
|
|
176
|
+
let j = k;
|
|
177
|
+
while (j < entries.length && (entries[j].type === "deleted" || entries[j].type === "added")) {
|
|
178
|
+
if (entries[j].type === "deleted") {
|
|
179
|
+
dels.push(entries[j]);
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
adds.push(entries[j]);
|
|
183
|
+
}
|
|
184
|
+
j++;
|
|
185
|
+
}
|
|
186
|
+
result.push(...pairChangeBlock(dels, adds));
|
|
187
|
+
k = j;
|
|
188
|
+
}
|
|
189
|
+
return result;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Pair one change block's deletions and insertions by similarity. Greedy:
|
|
193
|
+
* process deletions in order, each claiming the most similar still-unclaimed
|
|
194
|
+
* insertion above the threshold. Emits entries in old-index / new-index order.
|
|
195
|
+
*/
|
|
196
|
+
function pairChangeBlock(dels, adds) {
|
|
197
|
+
const usedAdd = new Array(adds.length).fill(false);
|
|
198
|
+
const out = [];
|
|
199
|
+
const leftoverAdds = [];
|
|
200
|
+
for (const del of dels) {
|
|
201
|
+
let bestIdx = -1;
|
|
202
|
+
let bestScore = MODIFY_SIMILARITY_THRESHOLD;
|
|
203
|
+
for (let a = 0; a < adds.length; a++) {
|
|
204
|
+
if (usedAdd[a]) {
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
const score = textSimilarity(del.oldText ?? "", adds[a].newText ?? "");
|
|
208
|
+
if (score >= bestScore) {
|
|
209
|
+
bestScore = score;
|
|
210
|
+
bestIdx = a;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
if (bestIdx >= 0) {
|
|
214
|
+
usedAdd[bestIdx] = true;
|
|
215
|
+
out.push({
|
|
154
216
|
type: "modified",
|
|
155
|
-
oldIndex:
|
|
156
|
-
newIndex:
|
|
157
|
-
oldText:
|
|
158
|
-
newText:
|
|
217
|
+
oldIndex: del.oldIndex,
|
|
218
|
+
newIndex: adds[bestIdx].newIndex,
|
|
219
|
+
oldText: del.oldText,
|
|
220
|
+
newText: adds[bestIdx].newText
|
|
159
221
|
});
|
|
160
|
-
k++; // skip next
|
|
161
222
|
}
|
|
162
223
|
else {
|
|
163
|
-
|
|
224
|
+
out.push(del); // pure deletion
|
|
164
225
|
}
|
|
165
226
|
}
|
|
166
|
-
|
|
227
|
+
for (let a = 0; a < adds.length; a++) {
|
|
228
|
+
if (!usedAdd[a]) {
|
|
229
|
+
leftoverAdds.push(adds[a]); // pure insertion
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
// Order the block by position: modifications and deletions (old order)
|
|
233
|
+
// first, then the surviving pure insertions (new order). Both arrays are
|
|
234
|
+
// already in their natural index order from the LCS walk.
|
|
235
|
+
out.push(...leftoverAdds);
|
|
236
|
+
return out;
|
|
237
|
+
}
|
|
238
|
+
/** Minimum similarity (0..1) for a delete+add to be treated as a modification. */
|
|
239
|
+
const MODIFY_SIMILARITY_THRESHOLD = 0.5;
|
|
240
|
+
/**
|
|
241
|
+
* Similarity of two strings in [0, 1], combining word-set overlap (Jaccard)
|
|
242
|
+
* with a shared-prefix bonus. Cheap and dependency-free — good enough to tell
|
|
243
|
+
* "same paragraph, lightly edited" from "completely different paragraph".
|
|
244
|
+
*/
|
|
245
|
+
function textSimilarity(a, b) {
|
|
246
|
+
if (a === b) {
|
|
247
|
+
return 1;
|
|
248
|
+
}
|
|
249
|
+
if (a.length === 0 || b.length === 0) {
|
|
250
|
+
return 0;
|
|
251
|
+
}
|
|
252
|
+
const wordsA = a.toLowerCase().split(/\s+/).filter(Boolean);
|
|
253
|
+
const wordsB = b.toLowerCase().split(/\s+/).filter(Boolean);
|
|
254
|
+
if (wordsA.length === 0 || wordsB.length === 0) {
|
|
255
|
+
return 0;
|
|
256
|
+
}
|
|
257
|
+
const setA = new Set(wordsA);
|
|
258
|
+
const setB = new Set(wordsB);
|
|
259
|
+
let intersection = 0;
|
|
260
|
+
for (const w of setA) {
|
|
261
|
+
if (setB.has(w)) {
|
|
262
|
+
intersection++;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
const union = setA.size + setB.size - intersection;
|
|
266
|
+
const jaccard = union === 0 ? 0 : intersection / union;
|
|
267
|
+
// Shared-prefix bonus: paragraphs that begin the same ("Step 3: …") are very
|
|
268
|
+
// likely the same item edited, even if many words changed.
|
|
269
|
+
let prefix = 0;
|
|
270
|
+
const max = Math.min(a.length, b.length);
|
|
271
|
+
while (prefix < max && a[prefix] === b[prefix]) {
|
|
272
|
+
prefix++;
|
|
273
|
+
}
|
|
274
|
+
const prefixRatio = prefix / Math.max(a.length, b.length);
|
|
275
|
+
// Weight word overlap most, with shared prefix as a strong booster so
|
|
276
|
+
// "Hello World" → "Hello Earth" (half the words, same prefix) reads as a
|
|
277
|
+
// modification while unrelated text stays a delete+add.
|
|
278
|
+
return Math.min(1, jaccard * 0.7 + prefixRatio * 0.5);
|
|
167
279
|
}
|
|
@@ -62,6 +62,7 @@ export function createShape(options) {
|
|
|
62
62
|
outlineWidth,
|
|
63
63
|
noOutline,
|
|
64
64
|
textContent: options.textBody?.paragraphs,
|
|
65
|
+
textBodyAnchor: options.textBody?.anchor,
|
|
65
66
|
altText: options.altText,
|
|
66
67
|
name: options.name,
|
|
67
68
|
horizontalPosition: options.horizontalPosition,
|
|
@@ -69,6 +70,8 @@ export function createShape(options) {
|
|
|
69
70
|
wrap: options.wrap,
|
|
70
71
|
behindDoc: options.behindDoc,
|
|
71
72
|
rotation: options.rotation,
|
|
73
|
+
flipHorizontal: options.flipH,
|
|
74
|
+
flipVertical: options.flipV,
|
|
72
75
|
rawXml: rawXml.length > 0 ? rawXml : undefined,
|
|
73
76
|
_advancedFillXml: advanced.fillXml,
|
|
74
77
|
_advancedEffectsXml: advanced.effectsXml
|
|
@@ -229,7 +229,7 @@ function convertCell(cell, opts) {
|
|
|
229
229
|
const para = {
|
|
230
230
|
type: "paragraph",
|
|
231
231
|
properties: opts.preserveFormatting ? alignmentToParaProps(cell.alignment) : undefined,
|
|
232
|
-
children:
|
|
232
|
+
children: wrapHyperlink(cell.hyperlink, children)
|
|
233
233
|
};
|
|
234
234
|
const cellProps = {};
|
|
235
235
|
if (opts.preserveFormatting && cell.fill) {
|
|
@@ -249,6 +249,26 @@ function convertCell(cell, opts) {
|
|
|
249
249
|
properties: Object.keys(cellProps).length > 0 ? cellProps : undefined
|
|
250
250
|
};
|
|
251
251
|
}
|
|
252
|
+
/**
|
|
253
|
+
* Build a paragraph's children for a converted cell, wrapping the runs
|
|
254
|
+
* in a Word {@link Hyperlink} when the source Excel cell carries one.
|
|
255
|
+
*
|
|
256
|
+
* Excel cell hyperlinks are external URLs (or `#Sheet!A1` internal
|
|
257
|
+
* references). We map `#…` targets to a Word anchor and everything else
|
|
258
|
+
* to an external `url`; the packager assigns the relationship id on
|
|
259
|
+
* write. An empty cell still produces a single empty run so the table
|
|
260
|
+
* structure stays intact.
|
|
261
|
+
*/
|
|
262
|
+
function wrapHyperlink(hyperlink, runs) {
|
|
263
|
+
const children = runs.length > 0 ? [...runs] : [{ content: [{ type: "text", text: "" }] }];
|
|
264
|
+
if (!hyperlink) {
|
|
265
|
+
return children;
|
|
266
|
+
}
|
|
267
|
+
const link = hyperlink.startsWith("#")
|
|
268
|
+
? { type: "hyperlink", anchor: hyperlink.slice(1), children }
|
|
269
|
+
: { type: "hyperlink", url: hyperlink, children };
|
|
270
|
+
return [link];
|
|
271
|
+
}
|
|
252
272
|
function cellText(cell) {
|
|
253
273
|
if (cell.type === ValueType.Null || cell.type === ValueType.Merge) {
|
|
254
274
|
return "";
|