@rowsncolumns/toolkit 24.4.2 → 24.4.4

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 (166) hide show
  1. package/dist/cjs/excel-parser/cfb-decryptor.d.ts +6 -0
  2. package/dist/cjs/excel-parser/cfb-decryptor.d.ts.map +1 -1
  3. package/dist/cjs/excel-parser/cfb-decryptor.js +6 -0
  4. package/dist/cjs/excel-parser/helpers.d.ts +9 -1
  5. package/dist/cjs/excel-parser/helpers.d.ts.map +1 -1
  6. package/dist/cjs/excel-parser/helpers.js +37 -0
  7. package/dist/cjs/excel-parser/interface.d.ts +15 -2
  8. package/dist/cjs/excel-parser/interface.d.ts.map +1 -1
  9. package/dist/cjs/excel-parser/plugins/base/index.d.ts +2 -0
  10. package/dist/cjs/excel-parser/plugins/base/index.d.ts.map +1 -1
  11. package/dist/cjs/excel-parser/plugins/base/index.js +3 -0
  12. package/dist/cjs/excel-parser/plugins/ods/index.d.ts +19 -0
  13. package/dist/cjs/excel-parser/plugins/ods/index.d.ts.map +1 -1
  14. package/dist/cjs/excel-parser/plugins/ods/index.js +676 -304
  15. package/dist/cjs/excel-parser/plugins/ods/styles.d.ts +43 -1
  16. package/dist/cjs/excel-parser/plugins/ods/styles.d.ts.map +1 -1
  17. package/dist/cjs/excel-parser/plugins/ods/styles.js +343 -45
  18. package/dist/cjs/excel-parser/plugins/xls/index.d.ts +55 -1
  19. package/dist/cjs/excel-parser/plugins/xls/index.d.ts.map +1 -1
  20. package/dist/cjs/excel-parser/plugins/xls/index.js +518 -6
  21. package/dist/cjs/excel-parser/plugins/xlsb/index.d.ts +4 -0
  22. package/dist/cjs/excel-parser/plugins/xlsb/index.d.ts.map +1 -1
  23. package/dist/cjs/excel-parser/plugins/xlsb/index.js +33 -14
  24. package/dist/cjs/excel-parser/plugins/xlsx/chartex.d.ts +37 -0
  25. package/dist/cjs/excel-parser/plugins/xlsx/chartex.d.ts.map +1 -0
  26. package/dist/cjs/excel-parser/plugins/xlsx/chartex.js +244 -0
  27. package/dist/cjs/excel-parser/plugins/xlsx/charts.d.ts +10 -0
  28. package/dist/cjs/excel-parser/plugins/xlsx/charts.d.ts.map +1 -1
  29. package/dist/cjs/excel-parser/plugins/xlsx/charts.js +68 -9
  30. package/dist/cjs/excel-parser/plugins/xlsx/drawing.d.ts +28 -1
  31. package/dist/cjs/excel-parser/plugins/xlsx/drawing.d.ts.map +1 -1
  32. package/dist/cjs/excel-parser/plugins/xlsx/drawing.js +155 -20
  33. package/dist/cjs/excel-parser/plugins/xlsx/form-controls.d.ts +70 -0
  34. package/dist/cjs/excel-parser/plugins/xlsx/form-controls.d.ts.map +1 -0
  35. package/dist/cjs/excel-parser/plugins/xlsx/form-controls.js +295 -0
  36. package/dist/cjs/excel-parser/plugins/xlsx/index.d.ts +35 -2
  37. package/dist/cjs/excel-parser/plugins/xlsx/index.d.ts.map +1 -1
  38. package/dist/cjs/excel-parser/plugins/xlsx/index.js +737 -257
  39. package/dist/cjs/excel-parser/plugins/xlsx/pivots.d.ts.map +1 -1
  40. package/dist/cjs/excel-parser/plugins/xlsx/pivots.js +125 -37
  41. package/dist/cjs/excel-parser/plugins/xlsx/sheet-views.d.ts +3 -0
  42. package/dist/cjs/excel-parser/plugins/xlsx/sheet-views.d.ts.map +1 -1
  43. package/dist/cjs/excel-parser/plugins/xlsx/sheet-views.js +4 -1
  44. package/dist/cjs/excel-parser/plugins/xlsx/styles.d.ts +15 -1
  45. package/dist/cjs/excel-parser/plugins/xlsx/styles.d.ts.map +1 -1
  46. package/dist/cjs/excel-parser/plugins/xlsx/styles.js +102 -6
  47. package/dist/cjs/excel-parser/registry.d.ts +7 -0
  48. package/dist/cjs/excel-parser/registry.d.ts.map +1 -1
  49. package/dist/cjs/excel-parser/registry.js +29 -0
  50. package/dist/cjs/excel-parser/types.d.ts +17 -2
  51. package/dist/cjs/excel-parser/types.d.ts.map +1 -1
  52. package/dist/cjs/excel-parser/workbook.d.ts +3 -0
  53. package/dist/cjs/excel-parser/workbook.d.ts.map +1 -1
  54. package/dist/cjs/excel-parser/workbook.js +24 -14
  55. package/dist/cjs/excel-parser/worker.js +131 -131
  56. package/dist/cjs/exporter/ods/ods-export.d.ts.map +1 -1
  57. package/dist/cjs/exporter/ods/ods-export.js +432 -175
  58. package/dist/cjs/exporter/xlsx/chart-builder.d.ts.map +1 -1
  59. package/dist/cjs/exporter/xlsx/chart-builder.js +215 -39
  60. package/dist/cjs/exporter/xlsx/chartex-builder.d.ts +36 -0
  61. package/dist/cjs/exporter/xlsx/chartex-builder.d.ts.map +1 -0
  62. package/dist/cjs/exporter/xlsx/chartex-builder.js +109 -0
  63. package/dist/cjs/exporter/xlsx/form-controls-builder.d.ts +61 -0
  64. package/dist/cjs/exporter/xlsx/form-controls-builder.d.ts.map +1 -0
  65. package/dist/cjs/exporter/xlsx/form-controls-builder.js +197 -0
  66. package/dist/cjs/exporter/xlsx/pivot-builder.d.ts +8 -0
  67. package/dist/cjs/exporter/xlsx/pivot-builder.d.ts.map +1 -1
  68. package/dist/cjs/exporter/xlsx/pivot-builder.js +141 -42
  69. package/dist/cjs/exporter/xlsx/style-builder.js +9 -3
  70. package/dist/cjs/exporter/xlsx/threaded-comments-builder.d.ts +7 -1
  71. package/dist/cjs/exporter/xlsx/threaded-comments-builder.d.ts.map +1 -1
  72. package/dist/cjs/exporter/xlsx/threaded-comments-builder.js +15 -3
  73. package/dist/cjs/exporter/xlsx/xlsx-export.d.ts +11 -2
  74. package/dist/cjs/exporter/xlsx/xlsx-export.d.ts.map +1 -1
  75. package/dist/cjs/exporter/xlsx/xlsx-export.js +991 -297
  76. package/dist/cjs/helpers/implicit-intersection.d.ts +16 -0
  77. package/dist/cjs/helpers/implicit-intersection.d.ts.map +1 -0
  78. package/dist/cjs/helpers/implicit-intersection.js +197 -0
  79. package/dist/cjs/print-settings.d.ts +105 -0
  80. package/dist/cjs/print-settings.d.ts.map +1 -0
  81. package/dist/cjs/print-settings.js +494 -0
  82. package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
  83. package/dist/esm/excel-parser/cfb-decryptor.d.ts +6 -0
  84. package/dist/esm/excel-parser/cfb-decryptor.d.ts.map +1 -1
  85. package/dist/esm/excel-parser/cfb-decryptor.js +6 -0
  86. package/dist/esm/excel-parser/helpers.d.ts +9 -1
  87. package/dist/esm/excel-parser/helpers.d.ts.map +1 -1
  88. package/dist/esm/excel-parser/helpers.js +34 -0
  89. package/dist/esm/excel-parser/interface.d.ts +15 -2
  90. package/dist/esm/excel-parser/interface.d.ts.map +1 -1
  91. package/dist/esm/excel-parser/plugins/base/index.d.ts +2 -0
  92. package/dist/esm/excel-parser/plugins/base/index.d.ts.map +1 -1
  93. package/dist/esm/excel-parser/plugins/base/index.js +3 -0
  94. package/dist/esm/excel-parser/plugins/ods/index.d.ts +19 -0
  95. package/dist/esm/excel-parser/plugins/ods/index.d.ts.map +1 -1
  96. package/dist/esm/excel-parser/plugins/ods/index.js +347 -33
  97. package/dist/esm/excel-parser/plugins/ods/styles.d.ts +43 -1
  98. package/dist/esm/excel-parser/plugins/ods/styles.d.ts.map +1 -1
  99. package/dist/esm/excel-parser/plugins/ods/styles.js +276 -2
  100. package/dist/esm/excel-parser/plugins/xls/index.d.ts +55 -1
  101. package/dist/esm/excel-parser/plugins/xls/index.d.ts.map +1 -1
  102. package/dist/esm/excel-parser/plugins/xls/index.js +441 -3
  103. package/dist/esm/excel-parser/plugins/xlsb/index.d.ts +4 -0
  104. package/dist/esm/excel-parser/plugins/xlsb/index.d.ts.map +1 -1
  105. package/dist/esm/excel-parser/plugins/xlsb/index.js +30 -9
  106. package/dist/esm/excel-parser/plugins/xlsx/chartex.d.ts +37 -0
  107. package/dist/esm/excel-parser/plugins/xlsx/chartex.d.ts.map +1 -0
  108. package/dist/esm/excel-parser/plugins/xlsx/chartex.js +199 -0
  109. package/dist/esm/excel-parser/plugins/xlsx/charts.d.ts +10 -0
  110. package/dist/esm/excel-parser/plugins/xlsx/charts.d.ts.map +1 -1
  111. package/dist/esm/excel-parser/plugins/xlsx/charts.js +69 -6
  112. package/dist/esm/excel-parser/plugins/xlsx/drawing.d.ts +28 -1
  113. package/dist/esm/excel-parser/plugins/xlsx/drawing.d.ts.map +1 -1
  114. package/dist/esm/excel-parser/plugins/xlsx/drawing.js +99 -0
  115. package/dist/esm/excel-parser/plugins/xlsx/form-controls.d.ts +70 -0
  116. package/dist/esm/excel-parser/plugins/xlsx/form-controls.d.ts.map +1 -0
  117. package/dist/esm/excel-parser/plugins/xlsx/form-controls.js +246 -0
  118. package/dist/esm/excel-parser/plugins/xlsx/index.d.ts +35 -2
  119. package/dist/esm/excel-parser/plugins/xlsx/index.d.ts.map +1 -1
  120. package/dist/esm/excel-parser/plugins/xlsx/index.js +454 -32
  121. package/dist/esm/excel-parser/plugins/xlsx/pivots.d.ts.map +1 -1
  122. package/dist/esm/excel-parser/plugins/xlsx/pivots.js +86 -6
  123. package/dist/esm/excel-parser/plugins/xlsx/sheet-views.d.ts +3 -0
  124. package/dist/esm/excel-parser/plugins/xlsx/sheet-views.d.ts.map +1 -1
  125. package/dist/esm/excel-parser/plugins/xlsx/sheet-views.js +2 -0
  126. package/dist/esm/excel-parser/plugins/xlsx/styles.d.ts +15 -1
  127. package/dist/esm/excel-parser/plugins/xlsx/styles.d.ts.map +1 -1
  128. package/dist/esm/excel-parser/plugins/xlsx/styles.js +92 -2
  129. package/dist/esm/excel-parser/registry.d.ts +7 -0
  130. package/dist/esm/excel-parser/registry.d.ts.map +1 -1
  131. package/dist/esm/excel-parser/registry.js +19 -0
  132. package/dist/esm/excel-parser/types.d.ts +17 -2
  133. package/dist/esm/excel-parser/types.d.ts.map +1 -1
  134. package/dist/esm/excel-parser/workbook.d.ts +3 -0
  135. package/dist/esm/excel-parser/workbook.d.ts.map +1 -1
  136. package/dist/esm/excel-parser/workbook.js +19 -10
  137. package/dist/esm/excel-parser/worker.js +12 -18
  138. package/dist/esm/exporter/ods/ods-export.d.ts.map +1 -1
  139. package/dist/esm/exporter/ods/ods-export.js +306 -35
  140. package/dist/esm/exporter/xlsx/chart-builder.d.ts.map +1 -1
  141. package/dist/esm/exporter/xlsx/chart-builder.js +161 -28
  142. package/dist/esm/exporter/xlsx/chartex-builder.d.ts +36 -0
  143. package/dist/esm/exporter/xlsx/chartex-builder.d.ts.map +1 -0
  144. package/dist/esm/exporter/xlsx/chartex-builder.js +97 -0
  145. package/dist/esm/exporter/xlsx/form-controls-builder.d.ts +61 -0
  146. package/dist/esm/exporter/xlsx/form-controls-builder.d.ts.map +1 -0
  147. package/dist/esm/exporter/xlsx/form-controls-builder.js +173 -0
  148. package/dist/esm/exporter/xlsx/pivot-builder.d.ts +8 -0
  149. package/dist/esm/exporter/xlsx/pivot-builder.d.ts.map +1 -1
  150. package/dist/esm/exporter/xlsx/pivot-builder.js +104 -19
  151. package/dist/esm/exporter/xlsx/style-builder.js +9 -3
  152. package/dist/esm/exporter/xlsx/threaded-comments-builder.d.ts +7 -1
  153. package/dist/esm/exporter/xlsx/threaded-comments-builder.d.ts.map +1 -1
  154. package/dist/esm/exporter/xlsx/threaded-comments-builder.js +15 -3
  155. package/dist/esm/exporter/xlsx/xlsx-export.d.ts +11 -2
  156. package/dist/esm/exporter/xlsx/xlsx-export.d.ts.map +1 -1
  157. package/dist/esm/exporter/xlsx/xlsx-export.js +675 -64
  158. package/dist/esm/helpers/implicit-intersection.d.ts +16 -0
  159. package/dist/esm/helpers/implicit-intersection.d.ts.map +1 -0
  160. package/dist/esm/helpers/implicit-intersection.js +184 -0
  161. package/dist/esm/print-settings.d.ts +105 -0
  162. package/dist/esm/print-settings.d.ts.map +1 -0
  163. package/dist/esm/print-settings.js +422 -0
  164. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
  165. package/package.json +3 -3
  166. package/xlsx.d.ts +14 -0
@@ -11,6 +11,7 @@ import { extractDataValidations } from "./data-validations";
11
11
  import { convertOdsFormulaToExcel } from "./formula";
12
12
  import { extractSparklines, extractSparklineData, } from "./sparklines";
13
13
  import { OdsStyles } from "./styles";
14
+ import { compactPrintSettings } from "../../../print-settings";
14
15
  const ODS_XML_OPTIONS = {
15
16
  ignoreAttributes: false,
16
17
  attributeNamePrefix: "@_",
@@ -48,6 +49,112 @@ function getAttribute(node, attributeName) {
48
49
  }
49
50
  return undefined;
50
51
  }
52
+ const ROW_WRAPPER_TAGS = [
53
+ "table:table-row-group",
54
+ "table:table-header-rows",
55
+ "table:table-rows",
56
+ "table-row-group",
57
+ "table-header-rows",
58
+ "table-rows",
59
+ ];
60
+ const COLUMN_WRAPPER_TAGS = [
61
+ "table:table-column-group",
62
+ "table:table-header-columns",
63
+ "table:table-columns",
64
+ "table-column-group",
65
+ "table-header-columns",
66
+ "table-columns",
67
+ ];
68
+ /**
69
+ * Order-preserving `<table:table>` children → the `<table:table-row>` nodes in
70
+ * document order, descending into `<table:table-row-group>` (outline),
71
+ * `<table:table-header-rows>` (print titles) and `<table:table-rows>`
72
+ * wrappers. Rows inside a wrapper are ordinary rows for cell data, merges,
73
+ * metadata and positions.
74
+ */
75
+ function flattenOrderedRows(children) {
76
+ const rows = [];
77
+ for (const child of children ?? []) {
78
+ if (!child || typeof child !== "object")
79
+ continue;
80
+ if (child["table:table-row"] || child["table-row"]) {
81
+ rows.push(child);
82
+ continue;
83
+ }
84
+ const wrapper = ROW_WRAPPER_TAGS.find((tag) => child[tag]);
85
+ if (wrapper)
86
+ rows.push(...flattenOrderedRows(child[wrapper]));
87
+ }
88
+ return rows;
89
+ }
90
+ /** Column counterpart of {@link flattenOrderedRows}. */
91
+ function flattenOrderedColumns(children) {
92
+ const columns = [];
93
+ for (const child of children ?? []) {
94
+ if (!child || typeof child !== "object")
95
+ continue;
96
+ if (child["table:table-column"] || child["table-column"]) {
97
+ columns.push(child);
98
+ continue;
99
+ }
100
+ const wrapper = COLUMN_WRAPPER_TAGS.find((tag) => child[tag]);
101
+ if (wrapper)
102
+ columns.push(...flattenOrderedColumns(child[wrapper]));
103
+ }
104
+ return columns;
105
+ }
106
+ /**
107
+ * The 1-based band covered by the `<table:table-header-rows>` /
108
+ * `<table:table-header-columns>` wrappers (ODS print titles), or undefined.
109
+ * A band that straddles an outline-group edge is written as several
110
+ * wrappers (ODF forbids a group inside header rows; LibreOffice closes and
111
+ * re-opens the wrapper around the group), so the segments are unioned back
112
+ * into one band — the same rule LibreOffice applies on import.
113
+ */
114
+ function findOrderedHeaderBand(children, axis) {
115
+ const itemTags = axis === "row"
116
+ ? ["table:table-row", "table-row"]
117
+ : ["table:table-column", "table-column"];
118
+ const headerTags = axis === "row"
119
+ ? ["table:table-header-rows", "table-header-rows"]
120
+ : ["table:table-header-columns", "table-header-columns"];
121
+ const wrapperTags = axis === "row" ? ROW_WRAPPER_TAGS : COLUMN_WRAPPER_TAGS;
122
+ const repeatAttr = axis === "row" ? "table:number-rows-repeated" : "table:number-columns-repeated";
123
+ const flatten = axis === "row" ? flattenOrderedRows : flattenOrderedColumns;
124
+ const countItems = (nodes) => flatten(nodes).reduce((total, node) => {
125
+ const repeated = parseInt(getAttribute(node, repeatAttr) ?? "1", 10);
126
+ return total + (Number.isFinite(repeated) && repeated > 0 ? repeated : 1);
127
+ }, 0);
128
+ let position = 1;
129
+ let band;
130
+ const visit = (nodes) => {
131
+ for (const child of nodes ?? []) {
132
+ if (!child || typeof child !== "object")
133
+ continue;
134
+ if (itemTags.some((tag) => child[tag])) {
135
+ position += countItems([child]);
136
+ continue;
137
+ }
138
+ const header = headerTags.find((tag) => child[tag]);
139
+ if (header) {
140
+ const count = countItems(child[header]);
141
+ if (count) {
142
+ const end = position + count - 1;
143
+ band = band
144
+ ? { start: Math.min(band.start, position), end: Math.max(band.end, end) }
145
+ : { start: position, end };
146
+ position = end + 1;
147
+ }
148
+ continue;
149
+ }
150
+ const wrapper = wrapperTags.find((tag) => child[tag]);
151
+ if (wrapper)
152
+ visit(child[wrapper]);
153
+ }
154
+ };
155
+ visit(children);
156
+ return band;
157
+ }
51
158
  function fastMergeCellFormats(...formats) {
52
159
  let result;
53
160
  for (const format of formats) {
@@ -160,10 +267,24 @@ export class OdsPlugin extends BaseWorkbookPlugin {
160
267
  if (styles) {
161
268
  const parsedStyles = this.xmlParser.parse(styles);
162
269
  this.odsStyles.parseStylesXml(parsedStyles);
163
- }
164
- const settings = this.zipParser.Get("settings.xml");
165
- if (settings) {
166
- this.settingsXml = this.xmlParser.parse(settings);
270
+ // Master pages carry mixed-content header/footer paragraphs
271
+ // (`Page <text:page-number/> of …`), so they are read from an
272
+ // order-preserving parse that also keeps the significant spaces
273
+ // around the fields and leaves the text verbatim (a `+1` after the
274
+ // page number is header text, not the number 1).
275
+ this.odsStyles.parseMasterPagesOrdered(new XMLParser({
276
+ ...ODS_ORDERED_XML_OPTIONS,
277
+ trimValues: false,
278
+ parseTagValue: false,
279
+ }).parse(styles));
280
+ }
281
+ // settings.xml is optional in ODF (our own exporter omits it when no
282
+ // sheet has view settings); `Get` throws on a missing part.
283
+ if (this.zipParser.Has("settings.xml")) {
284
+ const settings = this.zipParser.Get("settings.xml");
285
+ if (settings) {
286
+ this.settingsXml = this.xmlParser.parse(settings);
287
+ }
167
288
  }
168
289
  }
169
290
  catch (e) {
@@ -229,14 +350,26 @@ export class OdsPlugin extends BaseWorkbookPlugin {
229
350
  }
230
351
  buildSheetCache() {
231
352
  const tables = this.getTables();
353
+ const orderedTables = this.getOrderedTableWrappers();
232
354
  const sheets = [];
233
355
  const sheetContexts = [];
356
+ // `table:print-ranges` per sheet, resolved once the sheet-name map exists.
357
+ const printRangesBySheetId = new Map();
234
358
  tables.forEach((table, index) => {
235
359
  const name = table["@_table:name"] ||
236
360
  table["@_name"] ||
237
361
  table["table:name"] ||
238
362
  table["name"] ||
239
363
  `Sheet${index + 1}`;
364
+ // Order-preserving children of the same table: rows/columns nested in
365
+ // `<table:table-header-rows>` / `<table:table-row-group>` only appear in
366
+ // document order there.
367
+ const orderedTable = orderedTables[index];
368
+ const orderedTableChildren = orderedTable
369
+ ? orderedTable[orderedTable["table:table"] ? "table:table" : "table"]
370
+ : undefined;
371
+ const rowBreaks = [];
372
+ const columnBreaks = [];
240
373
  const sheet = {
241
374
  sheetId: index + 1,
242
375
  title: name,
@@ -247,13 +380,27 @@ export class OdsPlugin extends BaseWorkbookPlugin {
247
380
  columnMetadata: [],
248
381
  merges: [],
249
382
  };
383
+ // `table:protected="true"` is ODF's sheet-protection switch. The
384
+ // `table:protection-key` digest is not an OOXML hash, so only the master
385
+ // flag is modeled — the key itself cannot round-trip into `<sheetProtection>`.
386
+ const protectedAttr = table["@_table:protected"] ?? table["table:protected"];
387
+ if (protectedAttr === "true" || protectedAttr === true) {
388
+ sheet.sheetProtection = { sheet: true };
389
+ }
390
+ // Right-to-left sheets live on the table style's `style:writing-mode`
391
+ // (the ODF counterpart of `<sheetView rightToLeft="1">`).
392
+ if (this.odsStyles.isTableStyleRightToLeft(table["@_table:style-name"] || table["table:style-name"])) {
393
+ sheet.rightToLeft = true;
394
+ }
250
395
  // Column metadata from table-column elements
251
396
  const columnNodes = get(table, "table:table-column") || get(table, "table-column") || [];
252
- const allColumns = Array.isArray(columnNodes)
253
- ? columnNodes
254
- : columnNodes
255
- ? [columnNodes]
256
- : [];
397
+ const allColumns = orderedTableChildren
398
+ ? flattenOrderedColumns(orderedTableChildren)
399
+ : Array.isArray(columnNodes)
400
+ ? columnNodes
401
+ : columnNodes
402
+ ? [columnNodes]
403
+ : [];
257
404
  let currentColumnIndex = 1;
258
405
  let detectedColumnCount = 0;
259
406
  let defaultColumnWidth;
@@ -276,6 +423,10 @@ export class OdsPlugin extends BaseWorkbookPlugin {
276
423
  visibility === "hidden" ||
277
424
  visibility === "none" ||
278
425
  columnStyle?.hidden;
426
+ // `fo:break-before="page"` on column N = a manual break after N-1.
427
+ if (columnStyle?.breakBefore && currentColumnIndex > 1) {
428
+ columnBreaks.push(currentColumnIndex - 1);
429
+ }
279
430
  for (let c = 0; c < colsRepeated; c++) {
280
431
  const props = {};
281
432
  let shouldAssign = false;
@@ -309,7 +460,11 @@ export class OdsPlugin extends BaseWorkbookPlugin {
309
460
  }
310
461
  // Row metadata from table-row elements
311
462
  const rowNodes = get(table, "table:table-row") || get(table, "table-row") || [];
312
- const allRows = Array.isArray(rowNodes) ? rowNodes : [rowNodes];
463
+ const allRows = orderedTableChildren
464
+ ? flattenOrderedRows(orderedTableChildren)
465
+ : Array.isArray(rowNodes)
466
+ ? rowNodes
467
+ : [rowNodes];
313
468
  // DO NOT DELETE
314
469
  // console.log("allRows", allRows);
315
470
  let currentRowIndex = 1;
@@ -335,6 +490,10 @@ export class OdsPlugin extends BaseWorkbookPlugin {
335
490
  visibility === "none" ||
336
491
  rowStyle?.hidden;
337
492
  const effectiveRepeats = Math.min(rowsRepeated, 10000);
493
+ // `fo:break-before="page"` on row N = a manual break after N-1.
494
+ if (rowStyle?.breakBefore && currentRowIndex > 1) {
495
+ rowBreaks.push(currentRowIndex - 1);
496
+ }
338
497
  const shouldProcessRowMetadata = typeof height === "number" || isHidden;
339
498
  if (!shouldProcessRowMetadata) {
340
499
  currentRowIndex += effectiveRepeats;
@@ -378,6 +537,53 @@ export class OdsPlugin extends BaseWorkbookPlugin {
378
537
  if (detectedColumnCount > 0) {
379
538
  sheet.columnCount = Math.max(sheet.columnCount ?? 0, detectedColumnCount);
380
539
  }
540
+ // Print settings: the table style's master page (page layout +
541
+ // header/footer), manual breaks, and the header-rows/columns bands
542
+ // (print titles). `table:print-ranges` is resolved below.
543
+ const tableStyleName = table["@_table:style-name"] || table["table:style-name"];
544
+ const masterPageName = tableStyleName
545
+ ? this.odsStyles.getTableMasterPageName(tableStyleName)
546
+ : undefined;
547
+ const masterPage = masterPageName
548
+ ? this.odsStyles.getMasterPage(masterPageName)
549
+ : undefined;
550
+ const pageLayout = masterPage?.pageLayoutName
551
+ ? this.odsStyles.getPageLayout(masterPage.pageLayoutName)
552
+ : undefined;
553
+ const headerRows = findOrderedHeaderBand(orderedTableChildren, "row");
554
+ const headerColumns = findOrderedHeaderBand(orderedTableChildren, "column");
555
+ const printSettings = compactPrintSettings({
556
+ pageSetup: pageLayout?.pageSetup,
557
+ pageMargins: pageLayout?.pageMargins,
558
+ printOptions: pageLayout?.printOptions,
559
+ headerFooter: masterPage?.header || masterPage?.footer
560
+ ? {
561
+ ...(masterPage.header ? { oddHeader: masterPage.header } : {}),
562
+ ...(masterPage.footer ? { oddFooter: masterPage.footer } : {}),
563
+ }
564
+ : undefined,
565
+ printTitles: headerRows || headerColumns
566
+ ? {
567
+ ...(headerRows
568
+ ? { rowStart: headerRows.start, rowEnd: headerRows.end }
569
+ : {}),
570
+ ...(headerColumns
571
+ ? {
572
+ columnStart: headerColumns.start,
573
+ columnEnd: headerColumns.end,
574
+ }
575
+ : {}),
576
+ }
577
+ : undefined,
578
+ rowBreaks,
579
+ columnBreaks,
580
+ });
581
+ if (printSettings)
582
+ sheet.printSettings = printSettings;
583
+ const printRanges = table["@_table:print-ranges"] || table["table:print-ranges"];
584
+ if (typeof printRanges === "string" && printRanges.trim()) {
585
+ printRangesBySheetId.set(sheet.sheetId, printRanges);
586
+ }
381
587
  sheets.push(sheet);
382
588
  sheetContexts.push({ node: table, sheetId: sheet.sheetId, name });
383
589
  });
@@ -420,6 +626,16 @@ export class OdsPlugin extends BaseWorkbookPlugin {
420
626
  const basicFilters = this.extractBasicFilters(sheetContexts, sheetNameMap, spreadsheetNode);
421
627
  for (const sheet of sheets) {
422
628
  sheet.merges = mergesMap.get(sheet.sheetId) ?? sheet.merges ?? [];
629
+ const printRanges = printRangesBySheetId.get(sheet.sheetId);
630
+ if (printRanges) {
631
+ const context = sheetContexts.find((c) => c.sheetId === sheet.sheetId);
632
+ const printArea = this.parseRangeList(printRanges, sheetNameMap, context)
633
+ .filter((range) => range.sheetId === sheet.sheetId)
634
+ .map(({ sheetId: _sheetId, ...range }) => range);
635
+ if (printArea.length) {
636
+ sheet.printSettings = { ...sheet.printSettings, printArea };
637
+ }
638
+ }
423
639
  const settings = sheetSettings.get(sheet.sheetId);
424
640
  if (settings) {
425
641
  if (settings.frozenRowCount !== undefined) {
@@ -934,10 +1150,40 @@ export class OdsPlugin extends BaseWorkbookPlugin {
934
1150
  const tableChildren = tableWrapper[tableNameKey];
935
1151
  if (!tableChildren)
936
1152
  continue;
937
- const rows = tableChildren.filter((child) => child["table:table-row"] || child["table-row"]);
1153
+ const rows = flattenOrderedRows(tableChildren);
938
1154
  let currentRowIndex = 1;
939
1155
  const sheetContext = sheetContexts[sheetIndex];
940
1156
  const sheetId = sheetContext?.sheetId ?? sheetIndex + 1;
1157
+ // Sheet-level `<table:shapes>` frames (what LibreOffice and our own
1158
+ // ODS exporter write for charts/images floating over the grid). They
1159
+ // anchor through `table:anchor-cell-address`; their `svg:x`/`svg:y`
1160
+ // are sheet-absolute, so the cell anchor alone positions them.
1161
+ for (const shapesWrapper of tableChildren.filter((child) => child["table:shapes"] || child["shapes"])) {
1162
+ const shapeChildren = shapesWrapper["table:shapes"] || shapesWrapper["shapes"];
1163
+ // `collectFrameNodes` yields each frame child (the `draw:object` /
1164
+ // `draw:image` wrapper) as `frame` and the `<draw:frame>` attributes
1165
+ // as `cellAttrs`.
1166
+ for (const { frame, cellAttrs: frameAttrs } of this.collectFrameNodes(shapeChildren)) {
1167
+ const anchorAddress = frameAttrs?.["@_table:anchor-cell-address"] ||
1168
+ frameAttrs?.["@_anchor-cell-address"];
1169
+ const anchor = typeof anchorAddress === "string"
1170
+ ? addressToCell(anchorAddress.split(".").pop() ?? anchorAddress)
1171
+ : null;
1172
+ const anchorRow = anchor?.rowIndex ?? 1;
1173
+ const anchorColumn = anchor?.columnIndex ?? 1;
1174
+ const placementAttrs = this.withoutAbsoluteOffsets(frameAttrs);
1175
+ const chart = this.convertFrameToEmbeddedChart(frame, sheetContext, sheetId, anchorRow, anchorColumn, sheetNameMap, placementAttrs);
1176
+ if (chart) {
1177
+ chartAnchors.add(this.buildAnchorKey(sheetId, anchorRow, anchorColumn));
1178
+ charts.push(chart);
1179
+ continue;
1180
+ }
1181
+ const embedded = this.convertFrameToEmbeddedObject(frame, sheetId, anchorRow, anchorColumn, imageCache, placementAttrs);
1182
+ if (embedded) {
1183
+ drawings.push(embedded);
1184
+ }
1185
+ }
1186
+ }
941
1187
  for (const rowWrapper of rows) {
942
1188
  const rowNameKey = rowWrapper["table:table-row"]
943
1189
  ? "table:table-row"
@@ -1005,6 +1251,18 @@ export class OdsPlugin extends BaseWorkbookPlugin {
1005
1251
  buildAnchorKey(sheetId, rowIndex, columnIndex) {
1006
1252
  return `${sheetId}:${rowIndex}:${columnIndex}`;
1007
1253
  }
1254
+ /**
1255
+ * Copy of a `<draw:frame>` attribute record without `svg:x` / `svg:y`.
1256
+ * Frames in `<table:shapes>` carry sheet-absolute coordinates there,
1257
+ * which `buildOverlayPlacement` would otherwise read as an offset inside
1258
+ * the anchor cell.
1259
+ */
1260
+ withoutAbsoluteOffsets(frameAttrs) {
1261
+ if (!frameAttrs || typeof frameAttrs !== "object")
1262
+ return frameAttrs;
1263
+ const { "@_svg:x": _x, "@_svg:y": _y, "@_x": _bareX, "@_y": _bareY, ...rest } = frameAttrs;
1264
+ return rest;
1265
+ }
1008
1266
  /**
1009
1267
  * Walk the ordered content XML for each table and assign outline
1010
1268
  * level + collapsed/hiddenByGroup state on `rowMetadata` and
@@ -1051,7 +1309,7 @@ export class OdsPlugin extends BaseWorkbookPlugin {
1051
1309
  meta[index] = {};
1052
1310
  Object.assign(meta[index], patch);
1053
1311
  };
1054
- const walk = (nodes, itemTag, groupTag, meta, repeatAttrs, summaryAfter) => {
1312
+ const walk = (nodes, itemTag, groupTag, wrapperTags, meta, repeatAttrs, summaryAfter) => {
1055
1313
  let cursor = 1;
1056
1314
  const openStack = [];
1057
1315
  const collapsedDepth = () => openStack.some((g) => g.collapsed);
@@ -1060,6 +1318,14 @@ export class OdsPlugin extends BaseWorkbookPlugin {
1060
1318
  for (const child of children) {
1061
1319
  if (!child)
1062
1320
  continue;
1321
+ // `<table:table-header-rows>` / `<table:table-rows>` (and the
1322
+ // column twins) only group rows for printing; their rows keep
1323
+ // the surrounding outline level and still advance the cursor.
1324
+ const wrapper = wrapperTags.find((tag) => tag !== groupTag && child[tag]);
1325
+ if (wrapper) {
1326
+ visit(child[wrapper] || []);
1327
+ continue;
1328
+ }
1063
1329
  if (child[groupTag]) {
1064
1330
  const attrs = child[":@"] || {};
1065
1331
  const display = attrs["@_table:display"] ?? attrs["@_display"];
@@ -1110,8 +1376,8 @@ export class OdsPlugin extends BaseWorkbookPlugin {
1110
1376
  };
1111
1377
  visit(nodes);
1112
1378
  };
1113
- walk(tableChildren, "table:table-column", "table:table-column-group", columnMetadata, ["table:number-columns-repeated", "number-columns-repeated"], summaryRight);
1114
- walk(tableChildren, "table:table-row", "table:table-row-group", rowMetadata, ["table:number-rows-repeated", "number-rows-repeated"], summaryBelow);
1379
+ walk(tableChildren, "table:table-column", "table:table-column-group", COLUMN_WRAPPER_TAGS, columnMetadata, ["table:number-columns-repeated", "number-columns-repeated"], summaryRight);
1380
+ walk(tableChildren, "table:table-row", "table:table-row-group", ROW_WRAPPER_TAGS, rowMetadata, ["table:number-rows-repeated", "number-rows-repeated"], summaryBelow);
1115
1381
  if (columnMetadata.length > 0)
1116
1382
  sheet.columnMetadata = columnMetadata;
1117
1383
  if (rowMetadata.length > 0)
@@ -1145,7 +1411,7 @@ export class OdsPlugin extends BaseWorkbookPlugin {
1145
1411
  const tableChildren = tableWrapper[tableNameKey];
1146
1412
  if (!tableChildren)
1147
1413
  continue;
1148
- const rows = tableChildren.filter((child) => child["table:table-row"] || child["table-row"]);
1414
+ const rows = flattenOrderedRows(tableChildren);
1149
1415
  const sheetId = sheetContexts[sheetIndex]?.sheetId ?? sheetIndex + 1;
1150
1416
  let currentRowIndex = 1;
1151
1417
  for (const rowWrapper of rows) {
@@ -1274,7 +1540,7 @@ export class OdsPlugin extends BaseWorkbookPlugin {
1274
1540
  if (!tableChildren) {
1275
1541
  return buildDefaultColumns();
1276
1542
  }
1277
- const rows = tableChildren.filter((child) => child["table:table-row"] || child["table-row"]);
1543
+ const rows = flattenOrderedRows(tableChildren);
1278
1544
  if (!rows.length) {
1279
1545
  return buildDefaultColumns();
1280
1546
  }
@@ -1551,14 +1817,24 @@ export class OdsPlugin extends BaseWorkbookPlugin {
1551
1817
  const frameAttrs = this.getAttributes(frameNode);
1552
1818
  const inlineChart = this.findInlineChartNode(frameNode);
1553
1819
  let chartNode = inlineChart;
1820
+ // An inline chart's styles live in the workbook's content.xml, so the
1821
+ // workbook-level table applies. An embedded object is its own ODF
1822
+ // document with its own <office:automatic-styles>, and every object
1823
+ // reuses the same automatic style names (LibreOffice: ch1/ch2…, our
1824
+ // exporter: plot1/series0…), so its styles are scoped to the object —
1825
+ // sharing one table would let the previous chart's
1826
+ // `chart:three-dimensional` flag or series colours leak into this one.
1827
+ let chartStyles = this.odsStyles;
1554
1828
  if (!chartNode) {
1555
1829
  const objectNode = this.findFrameObjectNode(frameNode);
1556
- const chartFromObject = this.loadChartNodeFromObject(objectNode, frameAttrs);
1830
+ const objectStyles = new OdsStyles();
1831
+ const chartFromObject = this.loadChartNodeFromObject(objectNode, frameAttrs, objectStyles);
1557
1832
  chartNode = chartFromObject;
1833
+ chartStyles = objectStyles;
1558
1834
  }
1559
1835
  if (!chartNode)
1560
1836
  return undefined;
1561
- const spec = this.convertChartNodeToSpec(chartNode, sheetNameMap, sheetContext);
1837
+ const spec = this.convertChartNodeToSpec(chartNode, sheetNameMap, sheetContext, chartStyles);
1562
1838
  if (!spec)
1563
1839
  return undefined;
1564
1840
  const { overlayPosition } = this.buildOverlayPlacement(frameAttrs, cellAttrs, rowIndex, columnIndex);
@@ -1625,24 +1901,32 @@ export class OdsPlugin extends BaseWorkbookPlugin {
1625
1901
  }
1626
1902
  return undefined;
1627
1903
  }
1628
- loadChartNodeFromObject(objectNode, frameAttrs) {
1629
- if (!objectNode)
1904
+ loadChartNodeFromObject(objectNode, frameAttrs, styles) {
1905
+ // A self-closing `<draw:object xlink:href="…"/>` parses to an empty
1906
+ // child list with its attributes on the wrapper (`frameAttrs` here), so
1907
+ // the href must still be honoured when there is no object node.
1908
+ if (!objectNode && !frameAttrs)
1630
1909
  return undefined;
1631
1910
  const objectAttrs = this.getAttributes(objectNode);
1632
1911
  const href = objectAttrs?.["@_xlink:href"] ||
1633
1912
  objectAttrs?.["@_href"] ||
1634
1913
  frameAttrs?.["@_xlink:href"] ||
1635
1914
  frameAttrs?.["@_href"];
1636
- return this.loadChartNodeFromHref(href);
1915
+ return this.loadChartNodeFromHref(href, styles);
1637
1916
  }
1638
- loadChartNodeFromHref(href) {
1917
+ /**
1918
+ * Loads an embedded chart object's `content.xml` and registers its
1919
+ * `<office:automatic-styles>` into `styles` — the per-object table the
1920
+ * caller resolves plot-area / series styles against.
1921
+ */
1922
+ loadChartNodeFromHref(href, styles) {
1639
1923
  const path = this.resolveObjectPath(href);
1640
1924
  if (!path)
1641
1925
  return undefined;
1642
1926
  try {
1643
1927
  const content = this.zipParser.Get(path);
1644
1928
  const chartDoc = this.xmlParser.parse(content);
1645
- this.odsStyles.parseContentXml(chartDoc);
1929
+ styles.parseContentXml(chartDoc);
1646
1930
  return this.findChartNodeInDocument(chartDoc);
1647
1931
  }
1648
1932
  catch {
@@ -1718,11 +2002,12 @@ export class OdsPlugin extends BaseWorkbookPlugin {
1718
2002
  }
1719
2003
  return undefined;
1720
2004
  }
1721
- convertChartNodeToSpec(chartNode, sheetNameMap, context) {
2005
+ convertChartNodeToSpec(chartNode, sheetNameMap, context, styles) {
1722
2006
  if (!chartNode)
1723
2007
  return undefined;
1724
2008
  const chartAttrs = this.getAttributes(chartNode);
1725
- const chartType = this.mapOdsChartClassToType(chartAttrs?.["@_chart:class"] || chartAttrs?.["@_class"]);
2009
+ const chartClass = chartAttrs?.["@_chart:class"] || chartAttrs?.["@_class"];
2010
+ const chartType = this.mapOdsChartClassToType(chartClass);
1726
2011
  const titleNode = get(chartNode, "chart:title.chart:title-text") ||
1727
2012
  get(chartNode, "chart:title") ||
1728
2013
  get(chartNode, "title");
@@ -1731,6 +2016,8 @@ export class OdsPlugin extends BaseWorkbookPlugin {
1731
2016
  const plotArea = get(chartNode, "chart:plot-area") || get(chartNode, "plot-area");
1732
2017
  if (!plotArea)
1733
2018
  return undefined;
2019
+ const plotAreaAttrs = this.getAttributes(plotArea);
2020
+ const sourceChartType = this.resolveOdsSourceChartType(chartClass, chartType, plotAreaAttrs?.["@_chart:style-name"] || plotAreaAttrs?.["@_style-name"], styles);
1734
2021
  const seriesNodes = this.toArray(get(plotArea, "chart:series") ||
1735
2022
  get(plotArea, "chart:data-series") ||
1736
2023
  get(plotArea, "series"));
@@ -1758,7 +2045,7 @@ export class OdsPlugin extends BaseWorkbookPlugin {
1758
2045
  if (seriesData) {
1759
2046
  const label = this.extractSeriesLabel(seriesNode);
1760
2047
  seriesData.dataLabel = label || `Series ${seriesIndex}`;
1761
- const color = this.extractChartSeriesColor(seriesNode);
2048
+ const color = this.extractChartSeriesColor(seriesNode, styles);
1762
2049
  if (color) {
1763
2050
  seriesData.color = color;
1764
2051
  }
@@ -1776,6 +2063,7 @@ export class OdsPlugin extends BaseWorkbookPlugin {
1776
2063
  title,
1777
2064
  domains: finalizedDomains,
1778
2065
  series,
2066
+ ...(sourceChartType ? { sourceChartType } : {}),
1779
2067
  };
1780
2068
  }
1781
2069
  extractSeriesLabel(seriesNode) {
@@ -1787,7 +2075,7 @@ export class OdsPlugin extends BaseWorkbookPlugin {
1787
2075
  const label = labelNode ? this.extractTextContent(labelNode) : undefined;
1788
2076
  return label?.trim() || undefined;
1789
2077
  }
1790
- extractChartSeriesColor(seriesNode) {
2078
+ extractChartSeriesColor(seriesNode, styles) {
1791
2079
  if (!seriesNode)
1792
2080
  return undefined;
1793
2081
  const attrs = this.getAttributes(seriesNode);
@@ -1797,7 +2085,7 @@ export class OdsPlugin extends BaseWorkbookPlugin {
1797
2085
  attrs?.["@_style-name"],
1798
2086
  ];
1799
2087
  for (const candidate of styleCandidates) {
1800
- const color = this.odsStyles.getChartStyleColor(candidate);
2088
+ const color = styles.getChartStyleColor(candidate);
1801
2089
  if (color) {
1802
2090
  return typeof color === "string" ? color : undefined;
1803
2091
  }
@@ -1819,7 +2107,7 @@ export class OdsPlugin extends BaseWorkbookPlugin {
1819
2107
  pointAttrs?.["@_style-name"],
1820
2108
  ];
1821
2109
  for (const candidate of pointStyleCandidates) {
1822
- const color = this.odsStyles.getChartStyleColor(candidate);
2110
+ const color = styles.getChartStyleColor(candidate);
1823
2111
  if (color)
1824
2112
  return typeof color === "string" ? color : undefined;
1825
2113
  }
@@ -1855,14 +2143,40 @@ export class OdsPlugin extends BaseWorkbookPlugin {
1855
2143
  if (normalized.includes("line") || normalized.includes("scatter")) {
1856
2144
  return "line";
1857
2145
  }
1858
- if (normalized.includes("pie")) {
2146
+ if (normalized.includes("pie") || normalized.includes("circle")) {
1859
2147
  return "pie";
1860
2148
  }
1861
- if (normalized.includes("area")) {
2149
+ // `chart:surface` degrades to area — the same 2-D base the OOXML
2150
+ // surfaceChart / surface3DChart import uses (sourceChartType SURFACE).
2151
+ if (normalized.includes("area") || normalized.includes("surface")) {
1862
2152
  return "area";
1863
2153
  }
1864
2154
  return "bar";
1865
2155
  }
2156
+ /**
2157
+ * The authored 3-D class token for an ODS chart: `chart:surface` is a
2158
+ * surface, and any other class whose plot-area style declares
2159
+ * `chart:three-dimensional="true"` maps to the OOXML 3-D variant of its
2160
+ * 2-D base (`BAR_3D`, `LINE_3D`, `PIE_3D`, `AREA_3D`). Undefined for 2-D.
2161
+ */
2162
+ resolveOdsSourceChartType(chartClass, chartType, plotAreaStyleName, styles) {
2163
+ const threeDimensional = styles.isChartStyleThreeDimensional(plotAreaStyleName);
2164
+ if ((chartClass?.toLowerCase() || "").includes("surface")) {
2165
+ return threeDimensional ? "SURFACE_3D" : "SURFACE";
2166
+ }
2167
+ if (!threeDimensional)
2168
+ return undefined;
2169
+ switch (chartType) {
2170
+ case "bar":
2171
+ return "BAR_3D";
2172
+ case "line":
2173
+ return "LINE_3D";
2174
+ case "pie":
2175
+ return "PIE_3D";
2176
+ case "area":
2177
+ return "AREA_3D";
2178
+ }
2179
+ }
1866
2180
  findRangeString(node, skipKeys) {
1867
2181
  if (!node)
1868
2182
  return undefined;
@@ -2472,8 +2786,8 @@ export class OdsPlugin extends BaseWorkbookPlugin {
2472
2786
  ? this.odsStyles.getTableCellFormat(tableStyleName)
2473
2787
  : undefined;
2474
2788
  // Get rows
2475
- const rows = tableChildren.filter((child) => child["table:table-row"] || child["table-row"]);
2476
- const columnWrappers = tableChildren.filter((child) => child["table:table-column"] || child["table-column"]);
2789
+ const rows = flattenOrderedRows(tableChildren);
2790
+ const columnWrappers = flattenOrderedColumns(tableChildren);
2477
2791
  const columnFormats = new Map();
2478
2792
  let formattedColumnIndex = 1;
2479
2793
  for (const columnWrapper of columnWrappers) {