@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
@@ -55,7 +55,7 @@ export interface HttpRangeReaderOptions {
55
55
  * Whether to use credentials (cookies) for cross-origin requests.
56
56
  * @default "same-origin"
57
57
  */
58
- credentials?: RequestCredentials;
58
+ credentials?: NonNullable<RequestInit["credentials"]>;
59
59
  /**
60
60
  * Optional: Pre-known file size to skip HEAD request.
61
61
  * If not provided, a HEAD request will be made to determine size.
@@ -241,7 +241,7 @@ interface CsvOptionsExtras {
241
241
  map?(value: CellValue, index: number): CellValue;
242
242
  includeEmptyRows?: boolean;
243
243
  requestHeaders?: Record<string, string>;
244
- requestBody?: BodyInit;
244
+ requestBody?: NonNullable<RequestInit["body"]>;
245
245
  withCredentials?: boolean;
246
246
  signal?: AbortSignal;
247
247
  encoding?: string;
@@ -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
  interface NoteText {
4
5
  font?: any;
5
6
  text: string;
@@ -16,9 +17,11 @@ interface CommentModel {
16
17
  declare class CommentXform extends BaseXform<CommentModel> {
17
18
  parser: any;
18
19
  private _richTextXform?;
20
+ private _textXform?;
19
21
  constructor(model?: CommentModel);
20
22
  get tag(): string;
21
23
  get richTextXform(): RichTextXform;
24
+ get textXform(): TextXform;
22
25
  render(xmlStream: any, model?: CommentModel): void;
23
26
  parseOpen(node: any): boolean;
24
27
  parseText(text: string): void;
@@ -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
  }
@@ -17,6 +17,7 @@
17
17
  */
18
18
  import type { Chart, RegionMapDataOptions } from "../excel/chart/index.js";
19
19
  import type { Workbook } from "../excel/workbook.browser.js";
20
+ import type { LayoutChart } from "../word/layout/layout-model.js";
20
21
  import type { Chart as WordChart } from "../word/types.js";
21
22
  import { type PdfPageBuilder } from "./builder/document-builder.js";
22
23
  import { type PdfExportOptions } from "./types.js";
@@ -126,3 +127,34 @@ export declare function createWordChartPdfRenderer(): (chart: WordChart, page: P
126
127
  width: number;
127
128
  height: number;
128
129
  }) => void;
130
+ /**
131
+ * Create a layout-aware chart renderer for use as the internal
132
+ * `RenderLayoutOptions.chartRenderer` of the Word→PDF bridge.
133
+ *
134
+ * Unlike {@link createWordChartPdfRenderer} (which only sees the inner
135
+ * classic `Chart` model), this renderer receives the full
136
+ * {@link LayoutChart} and therefore handles **both** chart families
137
+ * with the full Excel rendering engine:
138
+ *
139
+ * - Classic `<c:chart>` (`chartKind === "chart"`) → `wordChartToChartModel`
140
+ * → `drawChartPdf` (vector).
141
+ * - Modern `<cx:chartSpace>` ChartEx (`chartKind === "chartEx"`,
142
+ * e.g. sunburst / treemap / waterfall / funnel / boxWhisker /
143
+ * histogram / pareto / regionMap) → `parseChartEx` → `drawChartExPdf`
144
+ * (vector) when the layout is vector-capable, otherwise the
145
+ * pre-rendered SVG carried on the `LayoutChart` is left for the
146
+ * translator's fallback.
147
+ *
148
+ * Returns `false` to decline a chart so the translator's built-in
149
+ * fallback (inline SVG, then a titled placeholder box) takes over. This
150
+ * keeps "fail soft" behaviour: a chart the engine can't draw still
151
+ * renders *something* rather than a blank slot.
152
+ *
153
+ * Requires `installChartSupport()` to have been called.
154
+ */
155
+ export declare function createWordLayoutChartPdfRenderer(): (chart: LayoutChart, page: PdfPageBuilder, rect: {
156
+ x: number;
157
+ y: number;
158
+ width: number;
159
+ height: number;
160
+ }) => boolean | void;
@@ -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
+ }
@@ -53,19 +53,21 @@ export interface DocxToPdfOptions {
53
53
  /** Default font size in points (default: 11). */
54
54
  readonly defaultFontSize?: number;
55
55
  /**
56
- * Header margin from top edge in points (default: 36).
56
+ * Header band distance from the top edge of the page, in points
57
+ * (default: section's `pgMar.header`, or 36pt / 0.5").
57
58
  *
58
- * @deprecated Currently has no effect on the layout-driven render
59
- * path: header paragraphs are rendered into the page's `marginTop`
60
- * strip starting at y=0 without an additional offset. Influence
61
- * header placement by widening `marginTop` instead.
59
+ * Header paragraphs are laid out starting at this y-offset from the
60
+ * page top. Overriding it moves the entire header band — useful when
61
+ * the source document declares no section properties or you want to
62
+ * tighten / loosen the header position without touching `marginTop`.
62
63
  */
63
64
  readonly headerMargin?: number;
64
65
  /**
65
- * Footer margin from bottom edge in points (default: 36).
66
+ * Footer band distance from the bottom edge of the page, in points
67
+ * (default: section's `pgMar.footer`, or 36pt / 0.5").
66
68
  *
67
- * @deprecated See {@link DocxToPdfOptions.headerMargin}. Footer
68
- * paragraphs are rendered starting at `pageHeight - marginBottom`.
69
+ * The footer band's top sits at `pageHeight - footerMargin`. The
70
+ * footnote stack (if any) is placed directly above this line.
69
71
  */
70
72
  readonly footerMargin?: number;
71
73
  /**
@@ -89,14 +91,17 @@ export interface DocxToPdfOptions {
89
91
  * destination rectangle in PDF coordinates. The implementation
90
92
  * should draw the chart into the rectangle.
91
93
  *
92
- * Return `false` to decline a chart (the translator then falls back
93
- * to its built-in placeholder rendering: an outlined rectangle with
94
- * the chart title centred). Return `void` or `true` to indicate the
95
- * chart was handled.
94
+ * Return `false` to decline a chart. The translator then falls back
95
+ * to the built-in layout-aware Excel renderer (which also handles
96
+ * `chartEx` charts), then to the inline `LayoutChart.svg` if present,
97
+ * and finally to a placeholder rectangle with the chart title
98
+ * centred. Return `void` or `true` to indicate the chart was handled.
96
99
  *
97
- * Note: `chartEx` charts never reach this callback because there is
98
- * no `Chart` instance to pass; they always render through the
99
- * translator's built-in path.
100
+ * Note: `chartEx` charts (sunburst / treemap / waterfall / funnel /
101
+ * boxWhisker / …) never reach this `Chart`-typed callback because
102
+ * there is no classic `Chart` instance to pass. They are rendered by
103
+ * the built-in layout-aware renderer instead (full vector output when
104
+ * `installChartSupport()` has been called).
100
105
  */
101
106
  readonly chartRenderer?: (chart: Chart, page: PdfPageBuilder, rect: {
102
107
  x: number;
@@ -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. Auto-detect chart support: if no explicit chartRenderer is
50
- // provided, try to import the high-quality Excel-based renderer.
51
- let chartRendererForChart = options?.chartRenderer;
52
- if (!chartRendererForChart) {
53
- try {
54
- const mod = await import("./excel-bridge.js");
55
- if (typeof mod.createWordChartPdfRenderer === "function") {
56
- chartRendererForChart = mod.createWordChartPdfRenderer();
57
- }
58
- }
59
- catch {
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
- // chartRenderer adaptation: layout produces `LayoutChart` (which
68
- // contains the original `ChartContent` in `source`); the public
69
- // chartRenderer API takes the inner `Chart` model. We unwrap so
70
- // existing callers keep working unchanged.
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: chartRendererForChart
83
+ chartRenderer: userChartRenderer || builtInLayoutRenderer
78
84
  ? (layoutChart, page, rect) => {
79
85
  const src = layoutChart.source;
80
- if (src && src.type === "chart") {
81
- // Forward the user's return value so a renderer that knows
82
- // how to draw classic charts but declines a particular
83
- // family (e.g. unsupported axis combination) gets the
84
- // translator's placeholder fallback.
85
- return chartRendererForChart(src.chart, page, rect);
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
- // ChartEx (or absent source) cannot be passed to the user's
88
- // `Chart`-typed callback. Decline so the translator's
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 accepted for API compatibility
109
- * with the previous flow renderer but currently have no effect on the
110
- * layout-driven path: the layout engine does not yet position header /
111
- * footer paragraphs against custom inset values, and forwarding the
112
- * field would silently mislead callers. Pre-existing documents that
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
@@ -5,9 +5,10 @@
5
5
  * Used by both Node.js and browser implementations.
6
6
  */
7
7
  type StreamInput = AsyncIterable<Uint8Array> | ReadableStream<Uint8Array>;
8
+ type BlobOptions = NonNullable<ConstructorParameters<typeof Blob>[1]>;
8
9
  export interface StreamConsumers {
9
10
  arrayBuffer(stream: StreamInput): Promise<ArrayBuffer>;
10
- blob(stream: StreamInput, options?: BlobPropertyBag): Promise<Blob>;
11
+ blob(stream: StreamInput, options?: BlobOptions): Promise<Blob>;
11
12
  buffer(stream: StreamInput): Promise<Uint8Array>;
12
13
  json(stream: StreamInput): Promise<unknown>;
13
14
  text(stream: StreamInput, encoding?: string): Promise<string>;
@@ -144,24 +144,136 @@ function computeDiff(oldTexts, newTexts) {
144
144
  }
145
145
  // Reverse since we built it backwards
146
146
  entries.reverse();
147
- // Post-process: pair adjacent delete+add as "modified" when they're at the same position
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
- for (let k = 0; k < entries.length; k++) {
150
- const curr = entries[k];
151
- const next = k + 1 < entries.length ? entries[k + 1] : undefined;
152
- if (curr.type === "deleted" && next?.type === "added") {
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: curr.oldIndex,
156
- newIndex: next.newIndex,
157
- oldText: curr.oldText,
158
- newText: next.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
- result.push(curr);
224
+ out.push(del); // pure deletion
164
225
  }
165
226
  }
166
- return result;
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: children.length > 0 ? children : [{ content: [{ type: "text", text: "" }] }]
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 "";
@@ -66,6 +66,7 @@ export declare const Document: {
66
66
  addImage(doc: DocumentHandle, data: Uint8Array, mediaType: ImageMediaType, width: Emu, height: Emu, options?: {
67
67
  altText?: string;
68
68
  name?: string;
69
+ fallbackData?: Uint8Array;
69
70
  }): {
70
71
  rId: string;
71
72
  drawingId: number;
@@ -88,6 +89,7 @@ export declare const Document: {
88
89
  rotation?: number;
89
90
  flipHorizontal?: boolean;
90
91
  flipVertical?: boolean;
92
+ fallbackData?: Uint8Array;
91
93
  }): string;
92
94
  /** Add a custom font definition. */
93
95
  addFont(doc: DocumentHandle, font: FontDef): void;