@cj-tech-master/excelts 9.6.0 → 9.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/dist/browser/modules/archive/io/random-access.d.ts +1 -1
  2. package/dist/browser/modules/excel/workbook.browser.d.ts +1 -1
  3. package/dist/browser/modules/excel/xlsx/xform/comment/comment-xform.d.ts +3 -0
  4. package/dist/browser/modules/excel/xlsx/xform/comment/comment-xform.js +30 -7
  5. package/dist/browser/modules/pdf/excel-bridge.d.ts +32 -0
  6. package/dist/browser/modules/pdf/excel-bridge.js +67 -1
  7. package/dist/browser/modules/pdf/word-bridge.d.ts +20 -15
  8. package/dist/browser/modules/pdf/word-bridge.js +49 -34
  9. package/dist/browser/modules/stream/common/consumers.d.ts +2 -1
  10. package/dist/browser/modules/word/advanced/diff.js +125 -13
  11. package/dist/browser/modules/word/advanced/drawing-shapes.js +3 -0
  12. package/dist/browser/modules/word/bridge/excel-bridge.js +21 -1
  13. package/dist/browser/modules/word/builder/document-handle.d.ts +2 -0
  14. package/dist/browser/modules/word/builder/document-handle.js +14 -2
  15. package/dist/browser/modules/word/builder/paragraph-builders.js +10 -1
  16. package/dist/browser/modules/word/builder/run-builders.d.ts +19 -2
  17. package/dist/browser/modules/word/builder/run-builders.js +2 -6
  18. package/dist/browser/modules/word/convert/odt/odt.js +6 -1
  19. package/dist/browser/modules/word/layout/layout-full.d.ts +12 -0
  20. package/dist/browser/modules/word/layout/layout-full.js +74 -9
  21. package/dist/browser/modules/word/layout/layout-model.d.ts +12 -0
  22. package/dist/browser/modules/word/query/merge.js +26 -10
  23. package/dist/browser/modules/word/query/split.js +68 -2
  24. package/dist/browser/modules/word/reader/docx-reader.js +23 -0
  25. package/dist/browser/modules/word/security/cfb-reader.d.ts +14 -3
  26. package/dist/browser/modules/word/security/cfb-reader.js +271 -153
  27. package/dist/browser/modules/word/security/document-protection.js +10 -4
  28. package/dist/browser/modules/word/security/encryption.js +194 -32
  29. package/dist/browser/modules/word/types.d.ts +17 -0
  30. package/dist/browser/modules/word/units.d.ts +10 -4
  31. package/dist/browser/modules/word/units.js +10 -4
  32. package/dist/browser/modules/word/writer/document-writer.js +28 -4
  33. package/dist/browser/modules/word/writer/docx-packager.js +45 -5
  34. package/dist/browser/modules/word/writer/image-writer.d.ts +1 -1
  35. package/dist/browser/modules/word/writer/image-writer.js +2 -2
  36. package/dist/browser/modules/word/writer/render-context.d.ts +15 -0
  37. package/dist/browser/modules/word/writer/run-writer.js +8 -4
  38. package/dist/browser/modules/word/writer/section-writer.js +46 -35
  39. package/dist/browser/modules/word/writer/streaming-writer.js +4 -0
  40. package/dist/browser/modules/word/writer/styles-writer.js +11 -0
  41. package/dist/browser/modules/word/writer/table-writer.js +6 -0
  42. package/dist/cjs/modules/excel/xlsx/xform/comment/comment-xform.js +30 -7
  43. package/dist/cjs/modules/pdf/excel-bridge.js +67 -0
  44. package/dist/cjs/modules/pdf/word-bridge.js +49 -34
  45. package/dist/cjs/modules/word/advanced/diff.js +125 -13
  46. package/dist/cjs/modules/word/advanced/drawing-shapes.js +3 -0
  47. package/dist/cjs/modules/word/bridge/excel-bridge.js +21 -1
  48. package/dist/cjs/modules/word/builder/document-handle.js +14 -2
  49. package/dist/cjs/modules/word/builder/paragraph-builders.js +10 -1
  50. package/dist/cjs/modules/word/builder/run-builders.js +2 -6
  51. package/dist/cjs/modules/word/convert/odt/odt.js +6 -1
  52. package/dist/cjs/modules/word/layout/layout-full.js +74 -9
  53. package/dist/cjs/modules/word/query/merge.js +26 -10
  54. package/dist/cjs/modules/word/query/split.js +68 -2
  55. package/dist/cjs/modules/word/reader/docx-reader.js +23 -0
  56. package/dist/cjs/modules/word/security/cfb-reader.js +271 -153
  57. package/dist/cjs/modules/word/security/document-protection.js +10 -4
  58. package/dist/cjs/modules/word/security/encryption.js +193 -31
  59. package/dist/cjs/modules/word/units.js +10 -4
  60. package/dist/cjs/modules/word/writer/document-writer.js +28 -4
  61. package/dist/cjs/modules/word/writer/docx-packager.js +45 -5
  62. package/dist/cjs/modules/word/writer/image-writer.js +2 -2
  63. package/dist/cjs/modules/word/writer/run-writer.js +8 -4
  64. package/dist/cjs/modules/word/writer/section-writer.js +46 -35
  65. package/dist/cjs/modules/word/writer/streaming-writer.js +4 -0
  66. package/dist/cjs/modules/word/writer/styles-writer.js +11 -0
  67. package/dist/cjs/modules/word/writer/table-writer.js +6 -0
  68. package/dist/esm/modules/excel/xlsx/xform/comment/comment-xform.js +30 -7
  69. package/dist/esm/modules/pdf/excel-bridge.js +67 -1
  70. package/dist/esm/modules/pdf/word-bridge.js +49 -34
  71. package/dist/esm/modules/word/advanced/diff.js +125 -13
  72. package/dist/esm/modules/word/advanced/drawing-shapes.js +3 -0
  73. package/dist/esm/modules/word/bridge/excel-bridge.js +21 -1
  74. package/dist/esm/modules/word/builder/document-handle.js +14 -2
  75. package/dist/esm/modules/word/builder/paragraph-builders.js +10 -1
  76. package/dist/esm/modules/word/builder/run-builders.js +2 -6
  77. package/dist/esm/modules/word/convert/odt/odt.js +6 -1
  78. package/dist/esm/modules/word/layout/layout-full.js +74 -9
  79. package/dist/esm/modules/word/query/merge.js +26 -10
  80. package/dist/esm/modules/word/query/split.js +68 -2
  81. package/dist/esm/modules/word/reader/docx-reader.js +23 -0
  82. package/dist/esm/modules/word/security/cfb-reader.js +271 -153
  83. package/dist/esm/modules/word/security/document-protection.js +10 -4
  84. package/dist/esm/modules/word/security/encryption.js +194 -32
  85. package/dist/esm/modules/word/units.js +10 -4
  86. package/dist/esm/modules/word/writer/document-writer.js +28 -4
  87. package/dist/esm/modules/word/writer/docx-packager.js +45 -5
  88. package/dist/esm/modules/word/writer/image-writer.js +2 -2
  89. package/dist/esm/modules/word/writer/run-writer.js +8 -4
  90. package/dist/esm/modules/word/writer/section-writer.js +46 -35
  91. package/dist/esm/modules/word/writer/streaming-writer.js +4 -0
  92. package/dist/esm/modules/word/writer/styles-writer.js +11 -0
  93. package/dist/esm/modules/word/writer/table-writer.js +6 -0
  94. package/dist/iife/excelts.iife.js +20 -8
  95. package/dist/iife/excelts.iife.js.map +1 -1
  96. package/dist/iife/excelts.iife.min.js +2 -2
  97. package/dist/types/modules/archive/io/random-access.d.ts +1 -1
  98. package/dist/types/modules/excel/workbook.browser.d.ts +1 -1
  99. package/dist/types/modules/excel/xlsx/xform/comment/comment-xform.d.ts +3 -0
  100. package/dist/types/modules/pdf/excel-bridge.d.ts +32 -0
  101. package/dist/types/modules/pdf/word-bridge.d.ts +20 -15
  102. package/dist/types/modules/stream/common/consumers.d.ts +2 -1
  103. package/dist/types/modules/word/builder/document-handle.d.ts +2 -0
  104. package/dist/types/modules/word/builder/run-builders.d.ts +19 -2
  105. package/dist/types/modules/word/layout/layout-full.d.ts +12 -0
  106. package/dist/types/modules/word/layout/layout-model.d.ts +12 -0
  107. package/dist/types/modules/word/security/cfb-reader.d.ts +14 -3
  108. package/dist/types/modules/word/types.d.ts +17 -0
  109. package/dist/types/modules/word/units.d.ts +10 -4
  110. package/dist/types/modules/word/writer/image-writer.d.ts +1 -1
  111. package/dist/types/modules/word/writer/render-context.d.ts +15 -0
  112. package/package.json +2 -2
@@ -316,8 +316,8 @@ export function renderShading(xml, shd) {
316
316
  });
317
317
  }
318
318
  /** Render an inline image (w:drawing > wp:inline). */
319
- function renderInlineImage(xml, img, imageRemap) {
320
- const drawingId = img.drawingId ?? 1;
319
+ function renderInlineImage(xml, img, imageRemap, nextDocPrId) {
320
+ const drawingId = nextDocPrId?.() ?? img.drawingId ?? 1;
321
321
  const name = img.name ?? "Picture";
322
322
  // Resolve the relationship id used in r:embed: prefer a packager-provided
323
323
  // remap (used when the model rId clashed with an existing relationship in
@@ -481,7 +481,11 @@ function renderFfData(xml, ff) {
481
481
  }
482
482
  if (ff.entries) {
483
483
  for (const entry of ff.entries) {
484
- xml.leafNode("w:listEntry", { "w:val": entry });
484
+ // Word rejects FORMDROPDOWN list entries with an empty value
485
+ // ("Word experienced an error trying to open the file"). Substitute a
486
+ // single space so an intended blank/placeholder item still renders and
487
+ // the entry indices (and `w:default`) stay aligned.
488
+ xml.leafNode("w:listEntry", { "w:val": entry === "" ? " " : entry });
485
489
  }
486
490
  }
487
491
  xml.closeNode();
@@ -668,7 +672,7 @@ function renderRunContent(xml, content, helpers) {
668
672
  }
669
673
  return true;
670
674
  }
671
- renderInlineImage(xml, content, helpers?.imageRemap);
675
+ renderInlineImage(xml, content, helpers?.imageRemap, helpers?.nextDocPrId);
672
676
  return true;
673
677
  case "field":
674
678
  // Fields create their own runs — must be rendered outside the current run
@@ -87,6 +87,16 @@ function renderPageBorders(xml, borders) {
87
87
  /** Render w:sectPr element. */
88
88
  export function renderSectionProperties(xml, sect, insidePropertyChange = false) {
89
89
  xml.openNode("w:sectPr");
90
+ // NOTE: Child element ordering below is dictated by the OOXML schema
91
+ // (ISO/IEC 29500 — CT_SectPr / EG_SectPrContents). Microsoft Word rejects
92
+ // (or silently "repairs") documents whose sectPr children are out of order,
93
+ // so the sequence here MUST stay:
94
+ // headerReference*, footerReference*, footnotePr, endnotePr, type, pgSz,
95
+ // pgMar, paperSrc, pgBorders, lnNumType, pgNumType, cols, formProt,
96
+ // vAlign, noEndnote, titlePg, textDirection, bidi, rtlGutter, docGrid,
97
+ // printerSettings
98
+ // (headerReference/footerReference come from EG_HdrFtrReferences, which the
99
+ // schema places ahead of EG_SectPrContents.)
90
100
  // Header references
91
101
  if (sect.headers) {
92
102
  for (const ref of sect.headers) {
@@ -99,6 +109,14 @@ export function renderSectionProperties(xml, sect, insidePropertyChange = false)
99
109
  renderHeaderFooterRef(xml, "w:footerReference", ref);
100
110
  }
101
111
  }
112
+ // Footnote properties
113
+ if (sect.footnoteProperties) {
114
+ renderNoteProperties(xml, "w:footnotePr", sect.footnoteProperties);
115
+ }
116
+ // Endnote properties
117
+ if (sect.endnoteProperties) {
118
+ renderNoteProperties(xml, "w:endnotePr", sect.endnoteProperties);
119
+ }
102
120
  // Section break type
103
121
  if (sect.breakType) {
104
122
  xml.leafNode("w:type", { "w:val": sect.breakType });
@@ -131,29 +149,7 @@ export function renderSectionProperties(xml, sect, insidePropertyChange = false)
131
149
  if (sect.pageBorders) {
132
150
  renderPageBorders(xml, sect.pageBorders);
133
151
  }
134
- // Columns
135
- if (sect.columns) {
136
- renderColumns(xml, sect.columns);
137
- }
138
- else {
139
- xml.leafNode("w:cols", { "w:space": String(DEFAULT_COLUMN_SPACE) });
140
- }
141
- // Title page (different first page header/footer)
142
- if (sect.titlePage) {
143
- xml.leafNode("w:titlePg");
144
- }
145
- // Page numbering
146
- if (sect.pageNumbering) {
147
- const attrs = {};
148
- if (sect.pageNumbering.start !== undefined) {
149
- attrs["w:start"] = String(sect.pageNumbering.start);
150
- }
151
- if (sect.pageNumbering.format) {
152
- attrs["w:fmt"] = sect.pageNumbering.format;
153
- }
154
- xml.leafNode("w:pgNumType", attrs);
155
- }
156
- // Line numbers
152
+ // Line numbers (must precede pgNumType per schema)
157
153
  if (sect.lineNumbers) {
158
154
  const attrs = {};
159
155
  if (sect.lineNumbers.countBy !== undefined) {
@@ -170,10 +166,37 @@ export function renderSectionProperties(xml, sect, insidePropertyChange = false)
170
166
  }
171
167
  xml.leafNode("w:lnNumType", attrs);
172
168
  }
169
+ // Page numbering
170
+ if (sect.pageNumbering) {
171
+ const attrs = {};
172
+ if (sect.pageNumbering.start !== undefined) {
173
+ attrs["w:start"] = String(sect.pageNumbering.start);
174
+ }
175
+ if (sect.pageNumbering.format) {
176
+ attrs["w:fmt"] = sect.pageNumbering.format;
177
+ }
178
+ xml.leafNode("w:pgNumType", attrs);
179
+ }
180
+ // Columns
181
+ if (sect.columns) {
182
+ renderColumns(xml, sect.columns);
183
+ }
184
+ else {
185
+ xml.leafNode("w:cols", { "w:space": String(DEFAULT_COLUMN_SPACE) });
186
+ }
187
+ // Form protection
188
+ if (sect.formProtection) {
189
+ xml.leafNode("w:formProt", { "w:val": "1" });
190
+ }
173
191
  // Vertical alignment
174
192
  if (sect.verticalAlign) {
175
193
  xml.leafNode("w:vAlign", { "w:val": sect.verticalAlign });
176
194
  }
195
+ // Title page (different first page header/footer) — schema places this
196
+ // after vAlign/noEndnote and before textDirection.
197
+ if (sect.titlePage) {
198
+ xml.leafNode("w:titlePg");
199
+ }
177
200
  // Text direction
178
201
  if (sect.textDirection) {
179
202
  xml.leafNode("w:textDirection", { "w:val": sect.textDirection });
@@ -186,18 +209,6 @@ export function renderSectionProperties(xml, sect, insidePropertyChange = false)
186
209
  if (sect.rtlGutter) {
187
210
  xml.leafNode("w:rtlGutter");
188
211
  }
189
- // Form protection
190
- if (sect.formProtection) {
191
- xml.leafNode("w:formProt", { "w:val": "1" });
192
- }
193
- // Footnote properties
194
- if (sect.footnoteProperties) {
195
- renderNoteProperties(xml, "w:footnotePr", sect.footnoteProperties);
196
- }
197
- // Endnote properties
198
- if (sect.endnoteProperties) {
199
- renderNoteProperties(xml, "w:endnotePr", sect.endnoteProperties);
200
- }
201
212
  // Document grid
202
213
  if (sect.docGrid) {
203
214
  const gridAttrs = {
@@ -900,6 +900,7 @@ export class StreamingDocxWriter {
900
900
  addXmlFile(PartPath.Comments, xml => renderComments(xml, this._options.comments, {
901
901
  imageRemap: this._renderCtx.imageRIdRemap,
902
902
  hyperlinkRIds: this._renderCtx.hyperlinkRIds,
903
+ nextDocPrId: this._renderCtx.ids.nextDocPrId,
903
904
  rawXmlPolicy: this._renderCtx.rawXmlPolicy
904
905
  }));
905
906
  // Also write commentsExtended if any have done/parentId
@@ -973,6 +974,7 @@ export class StreamingDocxWriter {
973
974
  addXmlFile(headerPath, xml => renderHeader(xml, headerDef.content, {
974
975
  imageRemap: this._renderCtx.imageRIdRemap,
975
976
  hyperlinkRIds: this._renderCtx.hyperlinkRIds,
977
+ nextDocPrId: this._renderCtx.ids.nextDocPrId,
976
978
  rawXmlPolicy: this._renderCtx.rawXmlPolicy
977
979
  }));
978
980
  if (getRelationshipCount(hRels) > 0) {
@@ -1043,6 +1045,7 @@ export class StreamingDocxWriter {
1043
1045
  addXmlFile(footerPath, xml => renderFooter(xml, footerDef.content, {
1044
1046
  imageRemap: this._renderCtx.imageRIdRemap,
1045
1047
  hyperlinkRIds: this._renderCtx.hyperlinkRIds,
1048
+ nextDocPrId: this._renderCtx.ids.nextDocPrId,
1046
1049
  rawXmlPolicy: this._renderCtx.rawXmlPolicy
1047
1050
  }));
1048
1051
  if (getRelationshipCount(fRels) > 0) {
@@ -1221,6 +1224,7 @@ export class StreamingDocxWriter {
1221
1224
  notesHelpers: {
1222
1225
  imageRemap: this._renderCtx.imageRIdRemap,
1223
1226
  hyperlinkRIds: this._renderCtx.hyperlinkRIds,
1227
+ nextDocPrId: this._renderCtx.ids.nextDocPrId,
1224
1228
  rawXmlPolicy: this._renderCtx.rawXmlPolicy
1225
1229
  }
1226
1230
  });
@@ -170,6 +170,17 @@ function renderStyle(xml, style) {
170
170
  // For table styles
171
171
  if (style.tableProperties) {
172
172
  xml.openNode("w:tblPr");
173
+ // Per CT_TblPrBase order, band sizes precede w:tblW.
174
+ if (style.tableProperties.rowBandSize !== undefined) {
175
+ xml.leafNode("w:tblStyleRowBandSize", {
176
+ "w:val": String(style.tableProperties.rowBandSize)
177
+ });
178
+ }
179
+ if (style.tableProperties.colBandSize !== undefined) {
180
+ xml.leafNode("w:tblStyleColBandSize", {
181
+ "w:val": String(style.tableProperties.colBandSize)
182
+ });
183
+ }
173
184
  if (style.tableProperties.width) {
174
185
  xml.leafNode("w:tblW", {
175
186
  "w:w": String(style.tableProperties.width.value),
@@ -168,6 +168,12 @@ function renderTableProperties(xml, tPr, insidePropertyChange = false) {
168
168
  xml.leafNode("w:tblOverlap", { "w:val": tPr.float.overlap });
169
169
  }
170
170
  }
171
+ if (tPr.rowBandSize !== undefined) {
172
+ xml.leafNode("w:tblStyleRowBandSize", { "w:val": String(tPr.rowBandSize) });
173
+ }
174
+ if (tPr.colBandSize !== undefined) {
175
+ xml.leafNode("w:tblStyleColBandSize", { "w:val": String(tPr.colBandSize) });
176
+ }
171
177
  if (tPr.width) {
172
178
  renderTableWidth(xml, "w:tblW", tPr.width);
173
179
  }
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.CommentXform = void 0;
4
4
  const base_xform_1 = require("../base-xform.js");
5
5
  const rich_text_xform_1 = require("../strings/rich-text-xform.js");
6
+ const text_xform_1 = require("../strings/text-xform.js");
6
7
  class CommentXform extends base_xform_1.BaseXform {
7
8
  constructor(model) {
8
9
  super();
@@ -17,6 +18,12 @@ class CommentXform extends base_xform_1.BaseXform {
17
18
  }
18
19
  return this._richTextXform;
19
20
  }
21
+ get textXform() {
22
+ if (!this._textXform) {
23
+ this._textXform = new text_xform_1.TextXform();
24
+ }
25
+ return this._textXform;
26
+ }
20
27
  render(xmlStream, model) {
21
28
  const renderModel = model || this.model;
22
29
  xmlStream.openNode("comment", {
@@ -52,6 +59,13 @@ class CommentXform extends base_xform_1.BaseXform {
52
59
  this.parser = this.richTextXform;
53
60
  this.parser.parseOpen(node);
54
61
  return true;
62
+ case "t":
63
+ // Legacy comments (e.g. produced by openpyxl/LibreOffice) may store the
64
+ // body as a bare <t> directly under <text> with no <r> run wrapper.
65
+ // This is valid for the CT_Rst type, so treat it like a run without font.
66
+ this.parser = this.textXform;
67
+ this.parser.parseOpen(node);
68
+ return true;
55
69
  default:
56
70
  return false;
57
71
  }
@@ -62,17 +76,26 @@ class CommentXform extends base_xform_1.BaseXform {
62
76
  }
63
77
  }
64
78
  parseClose(name) {
79
+ if (this.parser) {
80
+ if (!this.parser.parseClose(name)) {
81
+ // The active sub-parser has finished. Collect its result.
82
+ if (this.parser === this._richTextXform) {
83
+ // <r> run: model is already a { font?, text } run.
84
+ this.model.note.texts.push(this.parser.model);
85
+ }
86
+ else {
87
+ // Bare <t> body (e.g. openpyxl/LibreOffice): wrap the plain string
88
+ // as a single run without font, mirroring a <r><t> run.
89
+ this.model.note.texts.push({ text: this.parser.model });
90
+ }
91
+ this.parser = undefined;
92
+ }
93
+ return true;
94
+ }
65
95
  switch (name) {
66
96
  case "comment":
67
97
  return false;
68
- case "r":
69
- this.model.note.texts.push(this.parser.model);
70
- this.parser = undefined;
71
- return true;
72
98
  default:
73
- if (this.parser) {
74
- this.parser.parseClose(name);
75
- }
76
99
  return true;
77
100
  }
78
101
  }
@@ -20,6 +20,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
20
20
  exports.excelToPdf = excelToPdf;
21
21
  exports.chartToPdf = chartToPdf;
22
22
  exports.createWordChartPdfRenderer = createWordChartPdfRenderer;
23
+ exports.createWordLayoutChartPdfRenderer = createWordLayoutChartPdfRenderer;
23
24
  // Chart runtime is accessed through the chart-host-registry slot so
24
25
  // `@pkg/pdf` can do excel-to-PDF conversion without statically pulling
25
26
  // ~1.2 MB of chart rendering code into every consumer's bundle.
@@ -1177,3 +1178,69 @@ function createWordChartPdfRenderer() {
1177
1178
  support.drawChartPdf(page, model, rect);
1178
1179
  };
1179
1180
  }
1181
+ /**
1182
+ * Create a layout-aware chart renderer for use as the internal
1183
+ * `RenderLayoutOptions.chartRenderer` of the Word→PDF bridge.
1184
+ *
1185
+ * Unlike {@link createWordChartPdfRenderer} (which only sees the inner
1186
+ * classic `Chart` model), this renderer receives the full
1187
+ * {@link LayoutChart} and therefore handles **both** chart families
1188
+ * with the full Excel rendering engine:
1189
+ *
1190
+ * - Classic `<c:chart>` (`chartKind === "chart"`) → `wordChartToChartModel`
1191
+ * → `drawChartPdf` (vector).
1192
+ * - Modern `<cx:chartSpace>` ChartEx (`chartKind === "chartEx"`,
1193
+ * e.g. sunburst / treemap / waterfall / funnel / boxWhisker /
1194
+ * histogram / pareto / regionMap) → `parseChartEx` → `drawChartExPdf`
1195
+ * (vector) when the layout is vector-capable, otherwise the
1196
+ * pre-rendered SVG carried on the `LayoutChart` is left for the
1197
+ * translator's fallback.
1198
+ *
1199
+ * Returns `false` to decline a chart so the translator's built-in
1200
+ * fallback (inline SVG, then a titled placeholder box) takes over. This
1201
+ * keeps "fail soft" behaviour: a chart the engine can't draw still
1202
+ * renders *something* rather than a blank slot.
1203
+ *
1204
+ * Requires `installChartSupport()` to have been called.
1205
+ */
1206
+ function createWordLayoutChartPdfRenderer() {
1207
+ return (layoutChart, page, rect) => {
1208
+ const support = (0, chart_host_registry_1.tryGetChartSupport)();
1209
+ if (!support) {
1210
+ // Chart support not installed — decline so the Word→PDF
1211
+ // translator falls back to the inline SVG / placeholder. This
1212
+ // mirrors the auto-detect contract in `word-bridge.ts`, where a
1213
+ // missing chart runtime must degrade gracefully rather than throw.
1214
+ return false;
1215
+ }
1216
+ const source = layoutChart.source;
1217
+ if (layoutChart.chartKind === "chart") {
1218
+ // Classic chart: prefer the structured source, fall back to nothing.
1219
+ if (source && source.type === "chart") {
1220
+ support.drawChartPdf(page, (0, excel_bridge_1.wordChartToChartModel)(source.chart), rect);
1221
+ return;
1222
+ }
1223
+ return false;
1224
+ }
1225
+ // ChartEx. Parse the carried `cx:chartSpace` XML into a ChartExModel
1226
+ // and render it as vector PDF when the layout IDs are supported.
1227
+ if (source && source.type === "chartEx" && source.chartExXml) {
1228
+ let model;
1229
+ try {
1230
+ model = support.parseChartEx(source.chartExXml);
1231
+ }
1232
+ catch {
1233
+ return false; // Malformed XML — let the fallback path handle it.
1234
+ }
1235
+ if (model && support.canRenderChartExAsVectorPdf(model)) {
1236
+ support.drawChartExPdf(page, model, rect, {
1237
+ title: layoutChart.title
1238
+ });
1239
+ return;
1240
+ }
1241
+ }
1242
+ // Not vector-capable (or no source): decline so the translator
1243
+ // falls back to the inline SVG / placeholder.
1244
+ return false;
1245
+ };
1246
+ }
@@ -82,47 +82,59 @@ async function docxToPdf(doc, options) {
82
82
  // caller explicitly overrode an axis. Margins are independent: the
83
83
  // section's margins are applied unless the caller overrode them.
84
84
  const layoutOptions = mapToLayoutOptions(doc, options);
85
- // 2. Auto-detect chart support: if no explicit chartRenderer is
86
- // provided, try to import the high-quality Excel-based renderer.
87
- let chartRendererForChart = options?.chartRenderer;
88
- if (!chartRendererForChart) {
89
- try {
90
- const mod = await Promise.resolve().then(() => __importStar(require("./excel-bridge")));
91
- if (typeof mod.createWordChartPdfRenderer === "function") {
92
- chartRendererForChart = mod.createWordChartPdfRenderer();
93
- }
94
- }
95
- catch {
96
- // Chart support not available — placeholder rendering takes over.
85
+ // 2. Try to obtain the built-in, layout-aware Excel chart renderer.
86
+ // It handles BOTH classic `<c:chart>` and modern `<cx:chartSpace>`
87
+ // ChartEx families (sunburst / treemap / waterfall / …). It is used
88
+ // directly when the caller supplies no `chartRenderer`, and as the
89
+ // fallback for ChartEx (which the public `Chart`-typed callback
90
+ // cannot express) or whenever a user callback declines a chart.
91
+ let builtInLayoutRenderer;
92
+ try {
93
+ const mod = await Promise.resolve().then(() => __importStar(require("./excel-bridge")));
94
+ if (typeof mod.createWordLayoutChartPdfRenderer === "function") {
95
+ builtInLayoutRenderer = mod.createWordLayoutChartPdfRenderer();
97
96
  }
98
97
  }
98
+ catch {
99
+ // Chart support not available — placeholder rendering takes over.
100
+ }
99
101
  // 3. Run the layout engine. Everything from line wrapping to page
100
102
  // breaks happens here; word-bridge no longer carries any of that.
101
103
  const layout = (0, layout_full_1.layoutDocumentFull)(doc, layoutOptions);
102
104
  // 4. Build a render-options object for the PDF translator. The
103
- // chartRenderer adaptation: layout produces `LayoutChart` (which
104
- // contains the original `ChartContent` in `source`); the public
105
- // chartRenderer API takes the inner `Chart` model. We unwrap so
106
- // existing callers keep working unchanged.
105
+ // chart-rendering precedence is:
106
+ // a. classic chart + user callback → user callback (its `false`
107
+ // return falls through to the built-in layout renderer);
108
+ // b. ChartEx, or classic chart with no user callback, or a
109
+ // declined user callback → built-in layout-aware renderer;
110
+ // c. neither available / both decline → translator fallback
111
+ // (inline SVG, then a titled placeholder box).
112
+ const userChartRenderer = options?.chartRenderer;
107
113
  const renderOptions = {
108
114
  title: doc.coreProperties?.title,
109
115
  author: doc.coreProperties?.creator,
110
116
  subject: doc.coreProperties?.subject,
111
117
  defaultFont: options?.defaultFont ?? "Helvetica",
112
118
  defaultFontSize: options?.defaultFontSize ?? 11,
113
- chartRenderer: chartRendererForChart
119
+ chartRenderer: userChartRenderer || builtInLayoutRenderer
114
120
  ? (layoutChart, page, rect) => {
115
121
  const src = layoutChart.source;
116
- if (src && src.type === "chart") {
117
- // Forward the user's return value so a renderer that knows
118
- // how to draw classic charts but declines a particular
119
- // family (e.g. unsupported axis combination) gets the
120
- // translator's placeholder fallback.
121
- return chartRendererForChart(src.chart, page, rect);
122
+ // (a) Classic chart with a user-supplied callback: honour it
123
+ // first. Only fall through to the built-in renderer when
124
+ // it explicitly declines (`false`).
125
+ if (userChartRenderer && src && src.type === "chart") {
126
+ const handled = userChartRenderer(src.chart, page, rect);
127
+ if (handled !== false) {
128
+ return handled;
129
+ }
130
+ }
131
+ // (b) Built-in layout-aware renderer handles classic charts
132
+ // without a user callback AND all ChartEx charts.
133
+ if (builtInLayoutRenderer) {
134
+ return builtInLayoutRenderer(layoutChart, page, rect);
122
135
  }
123
- // ChartEx (or absent source) cannot be passed to the user's
124
- // `Chart`-typed callback. Decline so the translator's
125
- // placeholder runs instead of leaving a blank slot.
136
+ // (c) Decline so the translator's placeholder runs instead of
137
+ // leaving a blank slot.
126
138
  return false;
127
139
  }
128
140
  : undefined
@@ -141,13 +153,11 @@ async function docxToPdf(doc, options) {
141
153
  * portrait-oriented numbers); otherwise the layout engine's defaults
142
154
  * (US Letter, 1-inch margins) take over.
143
155
  *
144
- * `headerMargin` / `footerMargin` are accepted for API compatibility
145
- * with the previous flow renderer but currently have no effect on the
146
- * layout-driven path: the layout engine does not yet position header /
147
- * footer paragraphs against custom inset values, and forwarding the
148
- * field would silently mislead callers. Pre-existing documents that
149
- * rely on those parameters still get the section's `headerMargin` /
150
- * `footerMargin` from sectionProperties when they exist.
156
+ * `headerMargin` / `footerMargin` are forwarded to the layout engine
157
+ * as header / footer band offsets (ECMA-376 `pgMar.header` /
158
+ * `pgMar.footer`). When omitted, the section's own header / footer
159
+ * margins apply; when neither exists the engine default of 36pt (0.5")
160
+ * is used.
151
161
  */
152
162
  function mapToLayoutOptions(doc, options) {
153
163
  const sectProps = doc.sectionProperties;
@@ -172,7 +182,12 @@ function mapToLayoutOptions(doc, options) {
172
182
  marginTop: options?.marginTop ?? sectionMarginTopPt,
173
183
  marginBottom: options?.marginBottom ?? sectionMarginBottomPt,
174
184
  marginLeft: options?.marginLeft ?? sectionMarginLeftPt,
175
- marginRight: options?.marginRight ?? sectionMarginRightPt
185
+ marginRight: options?.marginRight ?? sectionMarginRightPt,
186
+ // Header / footer offsets: only forward an explicit caller value.
187
+ // Leaving these undefined lets the layout engine fall back to the
188
+ // section's `pgMar.header` / `pgMar.footer` (then the 36pt default).
189
+ headerMargin: options?.headerMargin,
190
+ footerMargin: options?.footerMargin
176
191
  };
177
192
  const layoutOpts = {};
178
193
  // Only attach pageGeometry when at least one axis is actually
@@ -147,24 +147,136 @@ function computeDiff(oldTexts, newTexts) {
147
147
  }
148
148
  // Reverse since we built it backwards
149
149
  entries.reverse();
150
- // Post-process: pair adjacent delete+add as "modified" when they're at the same position
150
+ return pairModifications(entries);
151
+ }
152
+ /**
153
+ * Pair deletions with insertions that represent the *same* paragraph in a
154
+ * modified form, based on text similarity.
155
+ *
156
+ * The LCS pass only matches paragraphs whose text is byte-identical, so when
157
+ * every paragraph is lightly edited it produces all-deletions + all-insertions
158
+ * with no "modified" at all. Within each contiguous change block (a run of
159
+ * deletions/insertions bounded by unchanged entries), we greedily pair each
160
+ * deletion with the most similar insertion whose similarity clears
161
+ * {@link MODIFY_SIMILARITY_THRESHOLD}. Pairs become "modified"; anything left
162
+ * unpaired stays a pure deletion or insertion. This yields, e.g., a recipe
163
+ * whose steps were all tweaked → mostly "modified", with a removed step as a
164
+ * pure deletion and a brand-new step as a pure insertion.
165
+ */
166
+ function pairModifications(entries) {
151
167
  const result = [];
152
- for (let k = 0; k < entries.length; k++) {
153
- const curr = entries[k];
154
- const next = k + 1 < entries.length ? entries[k + 1] : undefined;
155
- if (curr.type === "deleted" && next?.type === "added") {
156
- result.push({
168
+ let k = 0;
169
+ while (k < entries.length) {
170
+ const entry = entries[k];
171
+ if (entry.type !== "deleted" && entry.type !== "added") {
172
+ result.push(entry);
173
+ k++;
174
+ continue;
175
+ }
176
+ // Collect the maximal contiguous run of deleted/added entries.
177
+ const dels = [];
178
+ const adds = [];
179
+ let j = k;
180
+ while (j < entries.length && (entries[j].type === "deleted" || entries[j].type === "added")) {
181
+ if (entries[j].type === "deleted") {
182
+ dels.push(entries[j]);
183
+ }
184
+ else {
185
+ adds.push(entries[j]);
186
+ }
187
+ j++;
188
+ }
189
+ result.push(...pairChangeBlock(dels, adds));
190
+ k = j;
191
+ }
192
+ return result;
193
+ }
194
+ /**
195
+ * Pair one change block's deletions and insertions by similarity. Greedy:
196
+ * process deletions in order, each claiming the most similar still-unclaimed
197
+ * insertion above the threshold. Emits entries in old-index / new-index order.
198
+ */
199
+ function pairChangeBlock(dels, adds) {
200
+ const usedAdd = new Array(adds.length).fill(false);
201
+ const out = [];
202
+ const leftoverAdds = [];
203
+ for (const del of dels) {
204
+ let bestIdx = -1;
205
+ let bestScore = MODIFY_SIMILARITY_THRESHOLD;
206
+ for (let a = 0; a < adds.length; a++) {
207
+ if (usedAdd[a]) {
208
+ continue;
209
+ }
210
+ const score = textSimilarity(del.oldText ?? "", adds[a].newText ?? "");
211
+ if (score >= bestScore) {
212
+ bestScore = score;
213
+ bestIdx = a;
214
+ }
215
+ }
216
+ if (bestIdx >= 0) {
217
+ usedAdd[bestIdx] = true;
218
+ out.push({
157
219
  type: "modified",
158
- oldIndex: curr.oldIndex,
159
- newIndex: next.newIndex,
160
- oldText: curr.oldText,
161
- newText: next.newText
220
+ oldIndex: del.oldIndex,
221
+ newIndex: adds[bestIdx].newIndex,
222
+ oldText: del.oldText,
223
+ newText: adds[bestIdx].newText
162
224
  });
163
- k++; // skip next
164
225
  }
165
226
  else {
166
- result.push(curr);
227
+ out.push(del); // pure deletion
167
228
  }
168
229
  }
169
- return result;
230
+ for (let a = 0; a < adds.length; a++) {
231
+ if (!usedAdd[a]) {
232
+ leftoverAdds.push(adds[a]); // pure insertion
233
+ }
234
+ }
235
+ // Order the block by position: modifications and deletions (old order)
236
+ // first, then the surviving pure insertions (new order). Both arrays are
237
+ // already in their natural index order from the LCS walk.
238
+ out.push(...leftoverAdds);
239
+ return out;
240
+ }
241
+ /** Minimum similarity (0..1) for a delete+add to be treated as a modification. */
242
+ const MODIFY_SIMILARITY_THRESHOLD = 0.5;
243
+ /**
244
+ * Similarity of two strings in [0, 1], combining word-set overlap (Jaccard)
245
+ * with a shared-prefix bonus. Cheap and dependency-free — good enough to tell
246
+ * "same paragraph, lightly edited" from "completely different paragraph".
247
+ */
248
+ function textSimilarity(a, b) {
249
+ if (a === b) {
250
+ return 1;
251
+ }
252
+ if (a.length === 0 || b.length === 0) {
253
+ return 0;
254
+ }
255
+ const wordsA = a.toLowerCase().split(/\s+/).filter(Boolean);
256
+ const wordsB = b.toLowerCase().split(/\s+/).filter(Boolean);
257
+ if (wordsA.length === 0 || wordsB.length === 0) {
258
+ return 0;
259
+ }
260
+ const setA = new Set(wordsA);
261
+ const setB = new Set(wordsB);
262
+ let intersection = 0;
263
+ for (const w of setA) {
264
+ if (setB.has(w)) {
265
+ intersection++;
266
+ }
267
+ }
268
+ const union = setA.size + setB.size - intersection;
269
+ const jaccard = union === 0 ? 0 : intersection / union;
270
+ // Shared-prefix bonus: paragraphs that begin the same ("Step 3: …") are very
271
+ // likely the same item edited, even if many words changed.
272
+ let prefix = 0;
273
+ const max = Math.min(a.length, b.length);
274
+ while (prefix < max && a[prefix] === b[prefix]) {
275
+ prefix++;
276
+ }
277
+ const prefixRatio = prefix / Math.max(a.length, b.length);
278
+ // Weight word overlap most, with shared prefix as a strong booster so
279
+ // "Hello World" → "Hello Earth" (half the words, same prefix) reads as a
280
+ // modification while unrelated text stays a delete+add.
281
+ return Math.min(1, jaccard * 0.7 + prefixRatio * 0.5);
170
282
  }
@@ -73,6 +73,7 @@ function createShape(options) {
73
73
  outlineWidth,
74
74
  noOutline,
75
75
  textContent: options.textBody?.paragraphs,
76
+ textBodyAnchor: options.textBody?.anchor,
76
77
  altText: options.altText,
77
78
  name: options.name,
78
79
  horizontalPosition: options.horizontalPosition,
@@ -80,6 +81,8 @@ function createShape(options) {
80
81
  wrap: options.wrap,
81
82
  behindDoc: options.behindDoc,
82
83
  rotation: options.rotation,
84
+ flipHorizontal: options.flipH,
85
+ flipVertical: options.flipV,
83
86
  rawXml: rawXml.length > 0 ? rawXml : undefined,
84
87
  _advancedFillXml: advanced.fillXml,
85
88
  _advancedEffectsXml: advanced.effectsXml