@cj-tech-master/excelts 2.0.0 → 2.0.1-canary.20251228093548.379d895

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 (59) hide show
  1. package/dist/browser/excelts.esm.js +217 -126
  2. package/dist/browser/excelts.esm.js.map +1 -1
  3. package/dist/browser/excelts.esm.min.js +37 -34
  4. package/dist/browser/excelts.iife.js +217 -126
  5. package/dist/browser/excelts.iife.js.map +1 -1
  6. package/dist/browser/excelts.iife.min.js +37 -34
  7. package/dist/cjs/doc/pivot-table.js +37 -3
  8. package/dist/cjs/doc/worksheet.js +0 -1
  9. package/dist/cjs/stream/xlsx/worksheet-writer.js +0 -1
  10. package/dist/cjs/utils/datetime.js +7 -156
  11. package/dist/cjs/xlsx/xform/book/sheet-xform.js +3 -2
  12. package/dist/cjs/xlsx/xform/book/workbook-properties-xform.js +1 -1
  13. package/dist/cjs/xlsx/xform/core/content-types-xform.js +12 -6
  14. package/dist/cjs/xlsx/xform/pivot-table/cache-field.js +11 -4
  15. package/dist/cjs/xlsx/xform/pivot-table/pivot-cache-definition-xform.js +4 -11
  16. package/dist/cjs/xlsx/xform/pivot-table/pivot-table-xform.js +124 -17
  17. package/dist/cjs/xlsx/xform/sheet/page-setup-xform.js +4 -3
  18. package/dist/cjs/xlsx/xform/sheet/row-xform.js +1 -1
  19. package/dist/cjs/xlsx/xform/sheet/sheet-format-properties-xform.js +6 -3
  20. package/dist/cjs/xlsx/xform/sheet/sheet-view-xform.js +8 -4
  21. package/dist/cjs/xlsx/xform/sheet/worksheet-xform.js +2 -2
  22. package/dist/cjs/xlsx/xform/style/font-xform.js +36 -23
  23. package/dist/cjs/xlsx/xform/table/auto-filter-xform.js +3 -1
  24. package/dist/cjs/xlsx/xform/table/table-column-xform.js +2 -1
  25. package/dist/cjs/xlsx/xform/table/table-xform.js +5 -9
  26. package/dist/esm/doc/pivot-table.js +37 -3
  27. package/dist/esm/doc/worksheet.js +0 -1
  28. package/dist/esm/stream/xlsx/worksheet-writer.js +0 -1
  29. package/dist/esm/utils/datetime.js +7 -153
  30. package/dist/esm/xlsx/xform/book/sheet-xform.js +3 -2
  31. package/dist/esm/xlsx/xform/book/workbook-properties-xform.js +1 -1
  32. package/dist/esm/xlsx/xform/core/content-types-xform.js +12 -6
  33. package/dist/esm/xlsx/xform/pivot-table/cache-field.js +11 -4
  34. package/dist/esm/xlsx/xform/pivot-table/pivot-cache-definition-xform.js +4 -11
  35. package/dist/esm/xlsx/xform/pivot-table/pivot-table-xform.js +124 -17
  36. package/dist/esm/xlsx/xform/sheet/page-setup-xform.js +4 -3
  37. package/dist/esm/xlsx/xform/sheet/row-xform.js +1 -1
  38. package/dist/esm/xlsx/xform/sheet/sheet-format-properties-xform.js +6 -3
  39. package/dist/esm/xlsx/xform/sheet/sheet-view-xform.js +8 -4
  40. package/dist/esm/xlsx/xform/sheet/worksheet-xform.js +2 -2
  41. package/dist/esm/xlsx/xform/style/font-xform.js +36 -23
  42. package/dist/esm/xlsx/xform/table/auto-filter-xform.js +3 -1
  43. package/dist/esm/xlsx/xform/table/table-column-xform.js +2 -1
  44. package/dist/esm/xlsx/xform/table/table-xform.js +5 -9
  45. package/dist/types/csv/csv-core.d.ts +0 -6
  46. package/dist/types/doc/pivot-table.d.ts +5 -1
  47. package/dist/types/stream/xlsx/workbook-reader.d.ts +1 -1
  48. package/dist/types/stream/xlsx/worksheet-reader.d.ts +1 -1
  49. package/dist/types/stream/xlsx/worksheet-writer.d.ts +1 -1
  50. package/dist/types/types.d.ts +1 -1
  51. package/dist/types/utils/datetime.d.ts +0 -29
  52. package/dist/types/xlsx/xform/pivot-table/cache-field.d.ts +5 -1
  53. package/dist/types/xlsx/xform/pivot-table/pivot-cache-definition-xform.d.ts +0 -5
  54. package/dist/types/xlsx/xform/pivot-table/pivot-table-xform.d.ts +0 -3
  55. package/dist/types/xlsx/xform/sheet/sheet-format-properties-xform.d.ts +1 -1
  56. package/dist/types/xlsx/xform/sheet/worksheet-xform.d.ts +1 -1
  57. package/dist/types/xlsx/xform/style/font-xform.d.ts +1 -0
  58. package/dist/types/xlsx/xform/table/table-xform.d.ts +0 -4
  59. package/package.json +1 -1
@@ -37,8 +37,11 @@ function createTableSourceAdapter(table) {
37
37
  const endRow = startRow + tableModel.rows.length; // header row + data rows
38
38
  const endCol = startCol + columnNames.length - 1;
39
39
  const shortRange = col_cache_1.colCache.encode(startRow, startCol, endRow, endCol);
40
+ // Use the worksheet name (not table name) for pivotCacheDefinition's worksheetSource
41
+ // The sheet attribute in worksheetSource must reference the actual worksheet name
42
+ const worksheetName = table.worksheet.name;
40
43
  return {
41
- name: tableModel.name,
44
+ name: worksheetName,
42
45
  getRow(rowNumber) {
43
46
  if (rowNumber === 1) {
44
47
  return { values: headerRow };
@@ -187,12 +190,43 @@ function makeCacheFields(source, fieldNamesWithSharedItems) {
187
190
  }
188
191
  return (0, utils_1.toSortedArray)(uniqueValues);
189
192
  };
193
+ // Calculate min/max for numeric fields
194
+ const getMinMax = (columnIndex) => {
195
+ const columnValues = source.getColumn(columnIndex).values;
196
+ let min = Infinity;
197
+ let max = -Infinity;
198
+ let hasNumeric = false;
199
+ for (let i = 2; i < columnValues.length; i++) {
200
+ const v = columnValues[i];
201
+ if (typeof v === "number" && !isNaN(v)) {
202
+ hasNumeric = true;
203
+ if (v < min) {
204
+ min = v;
205
+ }
206
+ if (v > max) {
207
+ max = v;
208
+ }
209
+ }
210
+ }
211
+ return hasNumeric ? { minValue: min, maxValue: max } : null;
212
+ };
190
213
  // Build result array
191
214
  const result = [];
192
215
  for (const columnIndex of (0, utils_1.range)(1, names.length)) {
193
216
  const name = names[columnIndex];
194
- const sharedItems = sharedItemsFields.has(name) ? aggregate(columnIndex) : null;
195
- result.push({ name, sharedItems });
217
+ if (sharedItemsFields.has(name)) {
218
+ result.push({ name, sharedItems: aggregate(columnIndex) });
219
+ }
220
+ else {
221
+ // Numeric field - calculate min/max
222
+ const minMax = getMinMax(columnIndex);
223
+ result.push({
224
+ name,
225
+ sharedItems: null,
226
+ minValue: minMax?.minValue,
227
+ maxValue: minMax?.maxValue
228
+ });
229
+ }
196
230
  }
197
231
  return result;
198
232
  }
@@ -43,7 +43,6 @@ class Worksheet {
43
43
  // for tabColor, default row height, outline levels, etc
44
44
  this.properties = Object.assign({}, {
45
45
  defaultRowHeight: 15,
46
- dyDescent: 55,
47
46
  outlineLevelCol: 0,
48
47
  outlineLevelRow: 0
49
48
  }, options.properties);
@@ -98,7 +98,6 @@ class WorksheetWriter {
98
98
  // for default row height, outline levels, etc
99
99
  this.properties = Object.assign({}, {
100
100
  defaultRowHeight: 15,
101
- dyDescent: 55,
102
101
  outlineLevelCol: 0,
103
102
  outlineLevelRow: 0
104
103
  }, options.properties);
@@ -7,9 +7,6 @@
7
7
  */
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
9
  exports.DateFormatter = exports.DateParser = void 0;
10
- exports.mightBeDate = mightBeDate;
11
- exports.parseDate = parseDate;
12
- exports.formatDate = formatDate;
13
10
  exports.getSupportedFormats = getSupportedFormats;
14
11
  // ============================================================================
15
12
  // Constants - Pre-computed lookup tables for zero-allocation operations
@@ -18,7 +15,6 @@ exports.getSupportedFormats = getSupportedFormats;
18
15
  const PAD2 = Array.from({ length: 60 }, (_, i) => (i < 10 ? `0${i}` : `${i}`));
19
16
  // Character codes for fast comparison
20
17
  const C_0 = 48;
21
- const C_9 = 57;
22
18
  const C_DASH = 45;
23
19
  const C_SLASH = 47;
24
20
  const C_COLON = 58;
@@ -194,158 +190,6 @@ const AUTO_DETECT = [
194
190
  [29, [parseISOMsOffset]]
195
191
  ];
196
192
  // ============================================================================
197
- // Public API
198
- // ============================================================================
199
- /**
200
- * Quick pre-filter to reject non-date strings
201
- */
202
- function mightBeDate(s) {
203
- const len = s.length;
204
- if (len < 10 || len > 30) {
205
- return false;
206
- }
207
- const c0 = s.charCodeAt(0);
208
- const c1 = s.charCodeAt(1);
209
- // Must start with digits
210
- if (c0 < C_0 || c0 > C_9 || c1 < C_0 || c1 > C_9) {
211
- return false;
212
- }
213
- const c2 = s.charCodeAt(2);
214
- // Either YYYY format (4 digits) or MM/DD format (separator at pos 2)
215
- return (c2 >= C_0 && c2 <= C_9) || c2 === C_DASH || c2 === C_SLASH;
216
- }
217
- /**
218
- * Parse a date string
219
- *
220
- * @param value - String to parse
221
- * @param formats - Specific formats to try (optional, auto-detects ISO if omitted)
222
- * @returns Date object or null
223
- *
224
- * @example
225
- * parseDate("2024-12-26") // auto-detect ISO
226
- * parseDate("12-26-2024", ["MM-DD-YYYY"]) // explicit US format
227
- */
228
- function parseDate(value, formats) {
229
- if (!value || typeof value !== "string") {
230
- return null;
231
- }
232
- const s = value.trim();
233
- if (!s) {
234
- return null;
235
- }
236
- // Explicit formats
237
- if (formats?.length) {
238
- for (const fmt of formats) {
239
- const result = PARSERS[fmt]?.(s);
240
- if (result) {
241
- return result;
242
- }
243
- }
244
- return null;
245
- }
246
- // Auto-detect by length
247
- const len = s.length;
248
- for (const [l, parsers] of AUTO_DETECT) {
249
- if (len === l) {
250
- for (const p of parsers) {
251
- const result = p(s);
252
- if (result) {
253
- return result;
254
- }
255
- }
256
- }
257
- }
258
- return null;
259
- }
260
- /**
261
- * Format a Date to string
262
- *
263
- * @param date - Date to format
264
- * @param format - Format template (optional, defaults to ISO)
265
- * @param utc - Use UTC time (default: false)
266
- * @returns Formatted string or empty string if invalid
267
- *
268
- * @example
269
- * formatDate(date) // "2024-12-26T10:30:00.000+08:00"
270
- * formatDate(date, "YYYY-MM-DD", true) // "2024-12-26"
271
- */
272
- function formatDate(date, format, utc = false) {
273
- if (!(date instanceof Date)) {
274
- return "";
275
- }
276
- const t = date.getTime();
277
- if (t !== t) {
278
- return "";
279
- } // NaN check (faster than isNaN)
280
- // Default ISO format - direct string building (faster than toISOString)
281
- if (!format) {
282
- if (utc) {
283
- const y = date.getUTCFullYear();
284
- const M = date.getUTCMonth() + 1;
285
- const D = date.getUTCDate();
286
- const H = date.getUTCHours();
287
- const m = date.getUTCMinutes();
288
- const s = date.getUTCSeconds();
289
- const ms = date.getUTCMilliseconds();
290
- return `${y}-${PAD2[M]}-${PAD2[D]}T${PAD2[H]}:${PAD2[m]}:${PAD2[s]}.${ms < 10 ? "00" + ms : ms < 100 ? "0" + ms : ms}Z`;
291
- }
292
- const y = date.getFullYear();
293
- const M = date.getMonth() + 1;
294
- const D = date.getDate();
295
- const H = date.getHours();
296
- const m = date.getMinutes();
297
- const s = date.getSeconds();
298
- const ms = date.getMilliseconds();
299
- return `${y}-${PAD2[M]}-${PAD2[D]}T${PAD2[H]}:${PAD2[m]}:${PAD2[s]}.${ms < 10 ? "00" + ms : ms < 100 ? "0" + ms : ms}${tzOffset(date)}`;
300
- }
301
- // Fast paths for common formats
302
- if (format === "YYYY-MM-DD") {
303
- return utc
304
- ? `${date.getUTCFullYear()}-${PAD2[date.getUTCMonth() + 1]}-${PAD2[date.getUTCDate()]}`
305
- : `${date.getFullYear()}-${PAD2[date.getMonth() + 1]}-${PAD2[date.getDate()]}`;
306
- }
307
- return renderFormat(date, format, utc);
308
- }
309
- function tzOffset(d) {
310
- const off = -d.getTimezoneOffset();
311
- const sign = off >= 0 ? "+" : "-";
312
- const h = (Math.abs(off) / 60) | 0; // Bitwise OR faster than Math.floor
313
- const m = Math.abs(off) % 60;
314
- return `${sign}${PAD2[h]}:${PAD2[m]}`;
315
- }
316
- function renderFormat(d, fmt, utc) {
317
- // Handle escaped sections [...]
318
- const esc = [];
319
- let out = fmt.replace(/\[([^\]]*)\]/g, (_, c) => {
320
- esc.push(c);
321
- return `\x00${esc.length - 1}\x00`;
322
- });
323
- // Get components once
324
- const y = utc ? d.getUTCFullYear() : d.getFullYear();
325
- const M = utc ? d.getUTCMonth() + 1 : d.getMonth() + 1;
326
- const D = utc ? d.getUTCDate() : d.getDate();
327
- const H = utc ? d.getUTCHours() : d.getHours();
328
- const m = utc ? d.getUTCMinutes() : d.getMinutes();
329
- const s = utc ? d.getUTCSeconds() : d.getSeconds();
330
- const ms = utc ? d.getUTCMilliseconds() : d.getMilliseconds();
331
- // Replace tokens
332
- out = out
333
- .replace(/YYYY/g, String(y))
334
- .replace(/SSS/g, ms < 10 ? `00${ms}` : ms < 100 ? `0${ms}` : String(ms))
335
- .replace(/MM/g, PAD2[M])
336
- .replace(/DD/g, PAD2[D])
337
- .replace(/HH/g, PAD2[H])
338
- .replace(/mm/g, PAD2[m])
339
- .replace(/ss/g, PAD2[s])
340
- .replace(/Z/g, utc ? "Z" : tzOffset(d));
341
- // Restore escaped
342
- if (esc.length) {
343
- // oxlint-disable-next-line no-control-regex
344
- out = out.replace(/\x00(\d+)\x00/g, (_, i) => esc[+i]);
345
- }
346
- return out;
347
- }
348
- // ============================================================================
349
193
  // High-performance batch processors (class-based for state encapsulation)
350
194
  // ============================================================================
351
195
  /**
@@ -419,6 +263,13 @@ class DateParser {
419
263
  }
420
264
  }
421
265
  exports.DateParser = DateParser;
266
+ function tzOffset(d) {
267
+ const off = -d.getTimezoneOffset();
268
+ const sign = off >= 0 ? "+" : "-";
269
+ const h = (Math.abs(off) / 60) | 0; // Bitwise OR faster than Math.floor
270
+ const m = Math.abs(off) % 60;
271
+ return `${sign}${PAD2[h]}:${PAD2[m]}`;
272
+ }
422
273
  /**
423
274
  * Optimized date formatter for batch processing
424
275
  *
@@ -6,9 +6,10 @@ const base_xform_1 = require("../base-xform");
6
6
  class WorksheetXform extends base_xform_1.BaseXform {
7
7
  render(xmlStream, model) {
8
8
  xmlStream.leafNode("sheet", {
9
- sheetId: model.id,
10
9
  name: model.name,
11
- state: model.state,
10
+ sheetId: model.id,
11
+ // Excel doesn't output state when it's 'visible' (default)
12
+ state: model.state === "visible" ? undefined : model.state,
12
13
  "r:id": model.rId
13
14
  });
14
15
  }
@@ -6,7 +6,7 @@ class WorkbookPropertiesXform extends base_xform_1.BaseXform {
6
6
  render(xmlStream, model) {
7
7
  xmlStream.leafNode("workbookPr", {
8
8
  date1904: model.date1904 ? 1 : undefined,
9
- defaultThemeVersion: 164011,
9
+ // Excel doesn't output defaultThemeVersion
10
10
  filterPrivacy: 1
11
11
  });
12
12
  }
@@ -41,7 +41,7 @@ class ContentTypesXform extends base_xform_1.BaseXform {
41
41
  });
42
42
  });
43
43
  if ((model.pivotTables || []).length) {
44
- // Add content types for each pivot table
44
+ // Add content types for pivot cache (definition and records)
45
45
  (model.pivotTables || []).forEach((pivotTable) => {
46
46
  const n = pivotTable.tableNumber;
47
47
  xmlStream.leafNode("Override", {
@@ -52,10 +52,6 @@ class ContentTypesXform extends base_xform_1.BaseXform {
52
52
  PartName: `/xl/pivotCache/pivotCacheRecords${n}.xml`,
53
53
  ContentType: "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCacheRecords+xml"
54
54
  });
55
- xmlStream.leafNode("Override", {
56
- PartName: `/xl/pivotTables/pivotTable${n}.xml`,
57
- ContentType: "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotTable+xml"
58
- });
59
55
  });
60
56
  }
61
57
  xmlStream.leafNode("Override", {
@@ -81,6 +77,16 @@ class ContentTypesXform extends base_xform_1.BaseXform {
81
77
  });
82
78
  });
83
79
  }
80
+ // Add pivot table overrides after tables (matches Excel order)
81
+ if ((model.pivotTables || []).length) {
82
+ (model.pivotTables || []).forEach((pivotTable) => {
83
+ const n = pivotTable.tableNumber;
84
+ xmlStream.leafNode("Override", {
85
+ PartName: `/xl/pivotTables/pivotTable${n}.xml`,
86
+ ContentType: "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotTable+xml"
87
+ });
88
+ });
89
+ }
84
90
  if (model.drawings) {
85
91
  model.drawings.forEach((drawing) => {
86
92
  xmlStream.leafNode("Override", {
@@ -89,7 +95,7 @@ class ContentTypesXform extends base_xform_1.BaseXform {
89
95
  });
90
96
  });
91
97
  }
92
- if (model.commentRefs) {
98
+ if (model.commentRefs && model.commentRefs.length) {
93
99
  xmlStream.leafNode("Default", {
94
100
  Extension: "vml",
95
101
  ContentType: "application/vnd.openxmlformats-officedocument.vmlDrawing"
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.CacheField = void 0;
4
4
  const utils_1 = require("../../../utils/utils");
5
5
  class CacheField {
6
- constructor({ name, sharedItems }) {
6
+ constructor({ name, sharedItems, minValue, maxValue }) {
7
7
  // string type
8
8
  //
9
9
  // {
@@ -17,10 +17,14 @@ class CacheField {
17
17
  //
18
18
  // {
19
19
  // 'name': 'D',
20
- // 'sharedItems': null
20
+ // 'sharedItems': null,
21
+ // 'minValue': 5,
22
+ // 'maxValue': 45
21
23
  // }
22
24
  this.name = name;
23
25
  this.sharedItems = sharedItems;
26
+ this.minValue = minValue;
27
+ this.maxValue = maxValue;
24
28
  }
25
29
  render() {
26
30
  // PivotCache Field: http://www.datypic.com/sc/ooxml/e-ssml_cacheField-1.html
@@ -29,9 +33,12 @@ class CacheField {
29
33
  const escapedName = (0, utils_1.xmlEncode)(this.name);
30
34
  // integer types
31
35
  if (this.sharedItems === null) {
32
- // TK(2023-07-18): left out attributes... minValue="5" maxValue="45"
36
+ // Build minValue/maxValue attributes if available
37
+ const minMaxAttrs = this.minValue !== undefined && this.maxValue !== undefined
38
+ ? ` minValue="${this.minValue}" maxValue="${this.maxValue}"`
39
+ : "";
33
40
  return `<cacheField name="${escapedName}" numFmtId="0">
34
- <sharedItems containsSemiMixedTypes="0" containsString="0" containsNumber="1" containsInteger="1" />
41
+ <sharedItems containsSemiMixedTypes="0" containsString="0" containsNumber="1" containsInteger="1"${minMaxAttrs} />
35
42
  </cacheField>`;
36
43
  }
37
44
  // string types - escape XML special characters in each shared item value
@@ -46,17 +46,17 @@ class PivotCacheDefinitionXform extends base_xform_1.BaseXform {
46
46
  */
47
47
  renderNew(xmlStream, model) {
48
48
  const { source, cacheFields } = model;
49
+ // Record count = number of data rows (excluding header row)
50
+ const recordCount = source.getSheetValues().slice(2).length;
49
51
  xmlStream.openXml(xml_stream_1.XmlStream.StdDocAttributes);
50
52
  xmlStream.openNode(this.tag, {
51
53
  ...PivotCacheDefinitionXform.PIVOT_CACHE_DEFINITION_ATTRIBUTES,
52
54
  "r:id": "rId1",
53
55
  refreshOnLoad: "1", // important for our implementation to work
54
- refreshedBy: "Author",
55
- refreshedDate: "45125.026046874998",
56
56
  createdVersion: "8",
57
57
  refreshedVersion: "8",
58
58
  minRefreshableVersion: "3",
59
- recordCount: cacheFields.length + 1
59
+ recordCount
60
60
  });
61
61
  xmlStream.openNode("cacheSource", { type: "worksheet" });
62
62
  xmlStream.leafNode("worksheetSource", {
@@ -80,8 +80,6 @@ class PivotCacheDefinitionXform extends base_xform_1.BaseXform {
80
80
  ...PivotCacheDefinitionXform.PIVOT_CACHE_DEFINITION_ATTRIBUTES,
81
81
  "r:id": model.rId || "rId1",
82
82
  refreshOnLoad: model.refreshOnLoad || "1",
83
- refreshedBy: model.refreshedBy || "Author",
84
- refreshedDate: model.refreshedDate || "45125.026046874998",
85
83
  createdVersion: model.createdVersion || "8",
86
84
  refreshedVersion: model.refreshedVersion || "8",
87
85
  minRefreshableVersion: model.minRefreshableVersion || "3",
@@ -115,8 +113,6 @@ class PivotCacheDefinitionXform extends base_xform_1.BaseXform {
115
113
  cacheFields: [],
116
114
  rId: attributes["r:id"],
117
115
  refreshOnLoad: attributes.refreshOnLoad,
118
- refreshedBy: attributes.refreshedBy,
119
- refreshedDate: attributes.refreshedDate,
120
116
  createdVersion: attributes.createdVersion,
121
117
  refreshedVersion: attributes.refreshedVersion,
122
118
  minRefreshableVersion: attributes.minRefreshableVersion,
@@ -182,8 +178,5 @@ class PivotCacheDefinitionXform extends base_xform_1.BaseXform {
182
178
  exports.PivotCacheDefinitionXform = PivotCacheDefinitionXform;
183
179
  PivotCacheDefinitionXform.PIVOT_CACHE_DEFINITION_ATTRIBUTES = {
184
180
  xmlns: "http://schemas.openxmlformats.org/spreadsheetml/2006/main",
185
- "xmlns:r": "http://schemas.openxmlformats.org/officeDocument/2006/relationships",
186
- "xmlns:mc": "http://schemas.openxmlformats.org/markup-compatibility/2006",
187
- "mc:Ignorable": "xr",
188
- "xmlns:xr": "http://schemas.microsoft.com/office/spreadsheetml/2014/revision"
181
+ "xmlns:r": "http://schemas.openxmlformats.org/officeDocument/2006/relationships"
189
182
  };
@@ -58,12 +58,31 @@ class PivotTableXform extends base_xform_1.BaseXform {
58
58
  */
59
59
  renderNew(xmlStream, model) {
60
60
  const { rows, columns, values, cacheFields, cacheId, applyWidthHeightFormats } = model;
61
- // Generate unique UID for each pivot table to prevent Excel treating them as identical
62
- const uniqueUid = `{${crypto.randomUUID().toUpperCase()}}`;
61
+ // Build rowItems - need one <i> for each unique value in row fields, plus grand total
62
+ const rowItems = buildRowItems(rows, cacheFields);
63
+ // Build colItems - need one <i> for each unique value in col fields, plus grand total
64
+ const colItems = buildColItems(columns, cacheFields, values.length);
65
+ // Calculate pivot table dimensions
66
+ const rowFieldItemCount = rows.length > 0 ? cacheFields[rows[0]]?.sharedItems?.length || 0 : 0;
67
+ const colFieldItemCount = columns.length > 0 ? cacheFields[columns[0]]?.sharedItems?.length || 0 : 0;
68
+ // Location: A3 is where pivot table starts
69
+ // - firstHeaderRow: 1 (column headers are in first row of pivot table)
70
+ // - firstDataRow: 2 (data starts in second row)
71
+ // - firstDataCol: 1 (data starts in second column, after row labels)
72
+ // Calculate ref based on actual data size:
73
+ // - Start row: 3
74
+ // - Header rows: 2 (column label row + subheader row)
75
+ // - Data rows: rowFieldItemCount
76
+ // - Grand total row: 1
77
+ // endRow = 3 + 2 + rowFieldItemCount + 1 - 1 = 5 + rowFieldItemCount
78
+ // Or simplified: startRow (3) + 1 (column labels) + rowFieldItemCount (data) + 1 (grand total)
79
+ const endRow = 3 + 1 + rowFieldItemCount + 1; // = 5 + rowFieldItemCount
80
+ const endCol = 1 + colFieldItemCount + 1; // start col + data cols + grand total
81
+ const endColLetter = String.fromCharCode(64 + endCol);
82
+ const locationRef = `A3:${endColLetter}${endRow}`;
63
83
  xmlStream.openXml(xml_stream_1.XmlStream.StdDocAttributes);
64
84
  xmlStream.openNode(this.tag, {
65
85
  ...PivotTableXform.PIVOT_TABLE_ATTRIBUTES,
66
- "xr:uid": uniqueUid,
67
86
  name: "PivotTable2",
68
87
  cacheId,
69
88
  applyNumberFormats: "0",
@@ -84,23 +103,23 @@ class PivotTableXform extends base_xform_1.BaseXform {
84
103
  multipleFieldFilters: "0"
85
104
  });
86
105
  xmlStream.writeXml(`
87
- <location ref="A3:E15" firstHeaderRow="1" firstDataRow="2" firstDataCol="1" />
106
+ <location ref="${locationRef}" firstHeaderRow="1" firstDataRow="2" firstDataCol="1" />
88
107
  <pivotFields count="${cacheFields.length}">
89
108
  ${renderPivotFields(model)}
90
109
  </pivotFields>
91
110
  <rowFields count="${rows.length}">
92
111
  ${rows.map(rowIndex => `<field x="${rowIndex}" />`).join("\n ")}
93
112
  </rowFields>
94
- <rowItems count="1">
95
- <i t="grand"><x /></i>
113
+ <rowItems count="${rowItems.count}">
114
+ ${rowItems.xml}
96
115
  </rowItems>
97
116
  <colFields count="${columns.length === 0 ? 1 : columns.length}">
98
117
  ${columns.length === 0
99
118
  ? '<field x="-2" />'
100
119
  : columns.map(columnIndex => `<field x="${columnIndex}" />`).join("\n ")}
101
120
  </colFields>
102
- <colItems count="1">
103
- <i t="grand"><x /></i>
121
+ <colItems count="${colItems.count}">
122
+ ${colItems.xml}
104
123
  </colItems>
105
124
  <dataFields count="${values.length}">
106
125
  ${buildDataFields(cacheFields, values, model.metric)}
@@ -140,11 +159,9 @@ class PivotTableXform extends base_xform_1.BaseXform {
140
159
  * Render loaded pivot table (preserving original structure)
141
160
  */
142
161
  renderLoaded(xmlStream, model) {
143
- const uniqueUid = model.uid || `{${crypto.randomUUID().toUpperCase()}}`;
144
162
  xmlStream.openXml(xml_stream_1.XmlStream.StdDocAttributes);
145
163
  xmlStream.openNode(this.tag, {
146
164
  ...PivotTableXform.PIVOT_TABLE_ATTRIBUTES,
147
- "xr:uid": uniqueUid,
148
165
  name: model.name || "PivotTable1",
149
166
  cacheId: model.cacheId,
150
167
  applyNumberFormats: model.applyNumberFormats || "0",
@@ -458,12 +475,92 @@ class PivotTableXform extends base_xform_1.BaseXform {
458
475
  }
459
476
  exports.PivotTableXform = PivotTableXform;
460
477
  PivotTableXform.PIVOT_TABLE_ATTRIBUTES = {
461
- xmlns: "http://schemas.openxmlformats.org/spreadsheetml/2006/main",
462
- "xmlns:mc": "http://schemas.openxmlformats.org/markup-compatibility/2006",
463
- "mc:Ignorable": "xr",
464
- "xmlns:xr": "http://schemas.microsoft.com/office/spreadsheetml/2014/revision"
478
+ xmlns: "http://schemas.openxmlformats.org/spreadsheetml/2006/main"
465
479
  };
466
480
  // Helpers
481
+ /**
482
+ * Build rowItems XML - one item for each unique value in row fields, plus grand total.
483
+ * Each <i> represents a row in the pivot table.
484
+ * - Regular items: <i><x/></i> for index 0, <i><x v="index"/></i> for index > 0
485
+ * - Grand total: <i t="grand"><x/></i>
486
+ * Note: When v=0, the v attribute should be omitted (Excel convention)
487
+ */
488
+ function buildRowItems(rows, cacheFields) {
489
+ if (rows.length === 0) {
490
+ // No row fields - just grand total
491
+ return { count: 1, xml: '<i t="grand"><x /></i>' };
492
+ }
493
+ // Get unique values count from the first row field
494
+ const rowFieldIndex = rows[0];
495
+ const sharedItems = cacheFields[rowFieldIndex]?.sharedItems || [];
496
+ const itemCount = sharedItems.length;
497
+ // Build items: one for each unique value + grand total
498
+ const items = [];
499
+ // Regular items - reference each unique value by index
500
+ // Note: v="0" should be omitted (Excel uses <x/> instead of <x v="0"/>)
501
+ for (let i = 0; i < itemCount; i++) {
502
+ if (i === 0) {
503
+ items.push("<i><x /></i>");
504
+ }
505
+ else {
506
+ items.push(`<i><x v="${i}" /></i>`);
507
+ }
508
+ }
509
+ // Grand total row
510
+ items.push('<i t="grand"><x /></i>');
511
+ return {
512
+ count: items.length,
513
+ xml: items.join("\n ")
514
+ };
515
+ }
516
+ /**
517
+ * Build colItems XML - one item for each unique value in column fields, plus grand total.
518
+ * When there are multiple data fields (values), each column value may have sub-columns.
519
+ * Note: When v=0, the v attribute should be omitted (Excel convention)
520
+ */
521
+ function buildColItems(columns, cacheFields, valueCount) {
522
+ if (columns.length === 0) {
523
+ // No column fields - columns are based on data fields (values)
524
+ if (valueCount > 1) {
525
+ // Multiple values: one column per value + grand total
526
+ const items = [];
527
+ for (let i = 0; i < valueCount; i++) {
528
+ if (i === 0) {
529
+ items.push("<i><x /></i>");
530
+ }
531
+ else {
532
+ items.push(`<i><x v="${i}" /></i>`);
533
+ }
534
+ }
535
+ items.push('<i t="grand"><x /></i>');
536
+ return { count: items.length, xml: items.join("\n ") };
537
+ }
538
+ // Single value: just grand total
539
+ return { count: 1, xml: '<i t="grand"><x /></i>' };
540
+ }
541
+ // Get unique values count from the first column field
542
+ const colFieldIndex = columns[0];
543
+ const sharedItems = cacheFields[colFieldIndex]?.sharedItems || [];
544
+ const itemCount = sharedItems.length;
545
+ // Build items: one for each unique value + grand total
546
+ const items = [];
547
+ // Regular items - reference each unique value by index
548
+ // Note: v="0" should be omitted (Excel uses <x/> instead of <x v="0"/>)
549
+ for (let i = 0; i < itemCount; i++) {
550
+ if (i === 0) {
551
+ items.push("<i><x /></i>");
552
+ }
553
+ else {
554
+ items.push(`<i><x v="${i}" /></i>`);
555
+ }
556
+ }
557
+ // Grand total column
558
+ items.push('<i t="grand"><x /></i>');
559
+ return {
560
+ count: items.length,
561
+ xml: items.join("\n ")
562
+ };
563
+ }
467
564
  /**
468
565
  * Build dataField XML elements for all values in the pivot table.
469
566
  * Supports multiple values when columns is empty.
@@ -501,17 +598,27 @@ function renderPivotFields(pivotTable) {
501
598
  }
502
599
  function renderPivotField(fieldType, sharedItems) {
503
600
  // fieldType: 'row', 'column', 'value', null
504
- const defaultAttributes = 'compact="0" outline="0" showAll="0" defaultSubtotal="0"';
601
+ // Note: defaultSubtotal="0" should only be on value fields and non-axis fields,
602
+ // NOT on row/column axis fields (Excel will auto-calculate subtotals for them)
505
603
  if (fieldType === "row" || fieldType === "column") {
506
604
  const axis = fieldType === "row" ? "axisRow" : "axisCol";
605
+ // Row and column fields should NOT have defaultSubtotal="0"
606
+ const axisAttributes = 'compact="0" outline="0" showAll="0"';
607
+ // items = one for each shared item + one default item
608
+ const itemsXml = [
609
+ ...sharedItems.map((_item, index) => `<item x="${index}" />`),
610
+ '<item t="default" />' // Required default item for subtotals/grand totals
611
+ ].join("\n ");
507
612
  return `
508
- <pivotField axis="${axis}" ${defaultAttributes}>
613
+ <pivotField axis="${axis}" ${axisAttributes}>
509
614
  <items count="${sharedItems.length + 1}">
510
- ${sharedItems.map((_item, index) => `<item x="${index}" />`).join("\n ")}
615
+ ${itemsXml}
511
616
  </items>
512
617
  </pivotField>
513
618
  `;
514
619
  }
620
+ // Value fields and non-axis fields should have defaultSubtotal="0"
621
+ const defaultAttributes = 'compact="0" outline="0" showAll="0" defaultSubtotal="0"';
515
622
  return `
516
623
  <pivotField
517
624
  ${fieldType === "value" ? 'dataField="1"' : ""}
@@ -51,9 +51,10 @@ class PageSetupXform extends base_xform_1.BaseXform {
51
51
  draft: booleanToXml(model.draft),
52
52
  cellComments: cellCommentsToXml(model.cellComments),
53
53
  errors: errorsToXml(model.errors),
54
- scale: model.scale,
55
- fitToWidth: model.fitToWidth,
56
- fitToHeight: model.fitToHeight,
54
+ // Only output non-default values (matches Excel behavior)
55
+ scale: model.scale !== 100 ? model.scale : undefined,
56
+ fitToWidth: model.fitToWidth !== 1 ? model.fitToWidth : undefined,
57
+ fitToHeight: model.fitToHeight !== 1 ? model.fitToHeight : undefined,
57
58
  firstPageNumber: model.firstPageNumber,
58
59
  useFirstPageNumber: booleanToXml(!!model.firstPageNumber),
59
60
  usePrinterDefaults: booleanToXml(model.usePrinterDefaults),
@@ -51,7 +51,7 @@ class RowXform extends base_xform_1.BaseXform {
51
51
  xmlStream.addAttribute("s", model.styleId);
52
52
  xmlStream.addAttribute("customFormat", "1");
53
53
  }
54
- xmlStream.addAttribute("x14ac:dyDescent", "0.25");
54
+ // Note: dyDescent is MS extension, not output by default (Excel auto-calculates)
55
55
  if (model.outlineLevel) {
56
56
  xmlStream.addAttribute("outlineLevel", model.outlineLevel);
57
57
  }