@cj-tech-master/excelts 4.2.3-canary.20260122080306.cc11b20 → 5.0.0-canary.20260123012457.1fdf506

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 (53) hide show
  1. package/dist/browser/modules/excel/column.d.ts +5 -0
  2. package/dist/browser/modules/excel/column.js +10 -2
  3. package/dist/browser/modules/excel/row.d.ts +2 -0
  4. package/dist/browser/modules/excel/row.js +3 -1
  5. package/dist/browser/modules/excel/workbook.d.ts +4 -0
  6. package/dist/browser/modules/excel/workbook.js +4 -1
  7. package/dist/browser/modules/excel/xlsx/xform/pivot-table/pivot-table-xform.d.ts +1 -0
  8. package/dist/browser/modules/excel/xlsx/xform/pivot-table/pivot-table-xform.js +17 -10
  9. package/dist/browser/modules/excel/xlsx/xform/sheet/row-xform.d.ts +1 -0
  10. package/dist/browser/modules/excel/xlsx/xform/sheet/row-xform.js +7 -1
  11. package/dist/browser/modules/excel/xlsx/xform/sheet/sheet-format-properties-xform.d.ts +1 -0
  12. package/dist/browser/modules/excel/xlsx/xform/sheet/sheet-format-properties-xform.js +9 -4
  13. package/dist/browser/modules/excel/xlsx/xform/sheet/sheet-view-xform.js +4 -2
  14. package/dist/browser/modules/excel/xlsx/xform/sheet/worksheet-xform.js +2 -1
  15. package/dist/browser/modules/excel/xlsx/xform/style/style-xform.d.ts +7 -0
  16. package/dist/browser/modules/excel/xlsx/xform/style/style-xform.js +26 -6
  17. package/dist/browser/modules/excel/xlsx/xform/style/styles-xform.d.ts +6 -0
  18. package/dist/browser/modules/excel/xlsx/xform/style/styles-xform.js +52 -4
  19. package/dist/browser/modules/excel/xlsx/xlsx.browser.js +7 -0
  20. package/dist/cjs/modules/excel/column.js +10 -2
  21. package/dist/cjs/modules/excel/row.js +3 -1
  22. package/dist/cjs/modules/excel/workbook.js +4 -1
  23. package/dist/cjs/modules/excel/xlsx/xform/pivot-table/pivot-table-xform.js +17 -10
  24. package/dist/cjs/modules/excel/xlsx/xform/sheet/row-xform.js +7 -1
  25. package/dist/cjs/modules/excel/xlsx/xform/sheet/sheet-format-properties-xform.js +9 -4
  26. package/dist/cjs/modules/excel/xlsx/xform/sheet/sheet-view-xform.js +4 -2
  27. package/dist/cjs/modules/excel/xlsx/xform/sheet/worksheet-xform.js +2 -1
  28. package/dist/cjs/modules/excel/xlsx/xform/style/style-xform.js +26 -6
  29. package/dist/cjs/modules/excel/xlsx/xform/style/styles-xform.js +52 -4
  30. package/dist/cjs/modules/excel/xlsx/xlsx.browser.js +7 -0
  31. package/dist/esm/modules/excel/column.js +10 -2
  32. package/dist/esm/modules/excel/row.js +3 -1
  33. package/dist/esm/modules/excel/workbook.js +4 -1
  34. package/dist/esm/modules/excel/xlsx/xform/pivot-table/pivot-table-xform.js +17 -10
  35. package/dist/esm/modules/excel/xlsx/xform/sheet/row-xform.js +7 -1
  36. package/dist/esm/modules/excel/xlsx/xform/sheet/sheet-format-properties-xform.js +9 -4
  37. package/dist/esm/modules/excel/xlsx/xform/sheet/sheet-view-xform.js +4 -2
  38. package/dist/esm/modules/excel/xlsx/xform/sheet/worksheet-xform.js +2 -1
  39. package/dist/esm/modules/excel/xlsx/xform/style/style-xform.js +26 -6
  40. package/dist/esm/modules/excel/xlsx/xform/style/styles-xform.js +52 -4
  41. package/dist/esm/modules/excel/xlsx/xlsx.browser.js +7 -0
  42. package/dist/iife/excelts.iife.js +450 -394
  43. package/dist/iife/excelts.iife.js.map +1 -1
  44. package/dist/iife/excelts.iife.min.js +7 -7
  45. package/dist/types/modules/excel/column.d.ts +5 -0
  46. package/dist/types/modules/excel/row.d.ts +2 -0
  47. package/dist/types/modules/excel/workbook.d.ts +4 -0
  48. package/dist/types/modules/excel/xlsx/xform/pivot-table/pivot-table-xform.d.ts +1 -0
  49. package/dist/types/modules/excel/xlsx/xform/sheet/row-xform.d.ts +1 -0
  50. package/dist/types/modules/excel/xlsx/xform/sheet/sheet-format-properties-xform.d.ts +1 -0
  51. package/dist/types/modules/excel/xlsx/xform/style/style-xform.d.ts +7 -0
  52. package/dist/types/modules/excel/xlsx/xform/style/styles-xform.d.ts +6 -0
  53. package/package.json +14 -14
@@ -248,18 +248,21 @@ class PivotTableXform extends base_xform_1.BaseXform {
248
248
  xmlStream.writeXml('<rowItems count="1"><i t="grand"><x/></i></rowItems>');
249
249
  }
250
250
  // Col fields
251
- // Excel commonly emits a synthetic field x=-2 when there are no column fields.
252
- const colFieldCount = model.colFields.length === 0 ? 1 : model.colFields.length;
253
- xmlStream.openNode("colFields", { count: colFieldCount });
254
- if (model.colFields.length === 0) {
255
- xmlStream.leafNode("field", { x: -2 });
256
- }
257
- else {
258
- for (const fieldIndex of model.colFields) {
259
- xmlStream.leafNode("field", { x: fieldIndex });
251
+ // Only render colFields if it was present in the original file or if there are actual column fields
252
+ // Some pivot tables don't have colFields element at all
253
+ if (model.hasColFields || model.colFields.length > 0) {
254
+ const colFieldCount = model.colFields.length === 0 ? 1 : model.colFields.length;
255
+ xmlStream.openNode("colFields", { count: colFieldCount });
256
+ if (model.colFields.length === 0) {
257
+ xmlStream.leafNode("field", { x: -2 });
258
+ }
259
+ else {
260
+ for (const fieldIndex of model.colFields) {
261
+ xmlStream.leafNode("field", { x: fieldIndex });
262
+ }
260
263
  }
264
+ xmlStream.closeNode();
261
265
  }
262
- xmlStream.closeNode();
263
266
  // Col items - use parsed items if available
264
267
  if (model.colItems && model.colItems.length > 0) {
265
268
  xmlStream.openNode("colItems", { count: model.colItems.length });
@@ -462,6 +465,10 @@ class PivotTableXform extends base_xform_1.BaseXform {
462
465
  break;
463
466
  case "colFields":
464
467
  this.state.inColFields = true;
468
+ // Track that colFields element was present in original file
469
+ if (this.model) {
470
+ this.model.hasColFields = true;
471
+ }
465
472
  break;
466
473
  case "dataFields":
467
474
  this.state.inDataFields = true;
@@ -51,7 +51,10 @@ class RowXform extends base_xform_1.BaseXform {
51
51
  xmlStream.addAttribute("s", model.styleId);
52
52
  xmlStream.addAttribute("customFormat", "1");
53
53
  }
54
- // Note: dyDescent is MS extension, not output by default (Excel auto-calculates)
54
+ // Output dyDescent if present (MS extension for font descent)
55
+ if (model.dyDescent !== undefined) {
56
+ xmlStream.addAttribute("x14ac:dyDescent", model.dyDescent);
57
+ }
55
58
  if (model.outlineLevel) {
56
59
  xmlStream.addAttribute("outlineLevel", model.outlineLevel);
57
60
  }
@@ -102,6 +105,9 @@ class RowXform extends base_xform_1.BaseXform {
102
105
  if ((0, utils_1.parseBoolean)(node.attributes.collapsed)) {
103
106
  model.collapsed = true;
104
107
  }
108
+ if (node.attributes["x14ac:dyDescent"] !== undefined) {
109
+ model.dyDescent = parseFloat(node.attributes["x14ac:dyDescent"]);
110
+ }
105
111
  return true;
106
112
  }
107
113
  this.parser = this.map[node.name];
@@ -14,14 +14,14 @@ class SheetFormatPropertiesXform extends base_xform_1.BaseXform {
14
14
  outlineLevelRow: model.outlineLevelRow || undefined,
15
15
  outlineLevelCol: model.outlineLevelCol || undefined,
16
16
  // Only output dyDescent if explicitly set (MS extension, not ECMA-376 standard)
17
- "x14ac:dyDescent": model.dyDescent || undefined
17
+ "x14ac:dyDescent": model.dyDescent !== undefined && model.dyDescent !== 0 ? model.dyDescent : undefined
18
18
  };
19
19
  // Only output defaultColWidth if explicitly set
20
20
  if (model.defaultColWidth) {
21
21
  attributes.defaultColWidth = model.defaultColWidth;
22
22
  }
23
- // default value for 'defaultRowHeight' is 15, this should not be 'custom'
24
- if (!model.defaultRowHeight || model.defaultRowHeight !== 15) {
23
+ // Only output customHeight if it was present in the original file
24
+ if (model.customHeight) {
25
25
  attributes.customHeight = "1";
26
26
  }
27
27
  if (Object.values(attributes).some((value) => value !== undefined)) {
@@ -33,13 +33,18 @@ class SheetFormatPropertiesXform extends base_xform_1.BaseXform {
33
33
  if (node.name === "sheetFormatPr") {
34
34
  this.model = {
35
35
  defaultRowHeight: parseFloat(node.attributes.defaultRowHeight || "0"),
36
- dyDescent: parseFloat(node.attributes["x14ac:dyDescent"] || "0"),
36
+ dyDescent: node.attributes["x14ac:dyDescent"] !== undefined
37
+ ? parseFloat(node.attributes["x14ac:dyDescent"])
38
+ : undefined,
37
39
  outlineLevelRow: parseInt(node.attributes.outlineLevelRow || "0", 10),
38
40
  outlineLevelCol: parseInt(node.attributes.outlineLevelCol || "0", 10)
39
41
  };
40
42
  if (node.attributes.defaultColWidth) {
41
43
  this.model.defaultColWidth = parseFloat(node.attributes.defaultColWidth);
42
44
  }
45
+ if (node.attributes.customHeight === "1") {
46
+ this.model.customHeight = true;
47
+ }
43
48
  return true;
44
49
  }
45
50
  return false;
@@ -40,8 +40,8 @@ class SheetViewXform extends base_xform_1.BaseXform {
40
40
  add("showRuler", "0", model.showRuler === false);
41
41
  add("showRowColHeaders", "0", model.showRowColHeaders === false);
42
42
  add("showGridLines", "0", model.showGridLines === false);
43
- add("zoomScale", model.zoomScale, model.zoomScale);
44
- add("zoomScaleNormal", model.zoomScaleNormal, model.zoomScaleNormal);
43
+ add("zoomScale", model.zoomScale, model.zoomScale !== undefined && model.zoomScale !== 100);
44
+ add("zoomScaleNormal", model.zoomScaleNormal, model.zoomScaleNormal !== undefined && model.zoomScaleNormal !== 100);
45
45
  add("view", model.style, model.style);
46
46
  let topLeftCell;
47
47
  let xSplit;
@@ -146,6 +146,7 @@ class SheetViewXform extends base_xform_1.BaseXform {
146
146
  model = this.model = {
147
147
  workbookViewId: this.sheetView.workbookViewId,
148
148
  rightToLeft: this.sheetView.rightToLeft,
149
+ tabSelected: this.sheetView.tabSelected,
149
150
  state: VIEW_STATES[this.pane.state] || "split", // split is default
150
151
  xSplit: this.pane.xSplit,
151
152
  ySplit: this.pane.ySplit,
@@ -171,6 +172,7 @@ class SheetViewXform extends base_xform_1.BaseXform {
171
172
  model = this.model = {
172
173
  workbookViewId: this.sheetView.workbookViewId,
173
174
  rightToLeft: this.sheetView.rightToLeft,
175
+ tabSelected: this.sheetView.tabSelected,
174
176
  state: "normal",
175
177
  showRuler: this.sheetView.showRuler,
176
178
  showRowColHeaders: this.sheetView.showRowColHeaders,
@@ -387,7 +387,8 @@ class WorkSheetXform extends base_xform_1.BaseXform {
387
387
  defaultRowHeight: model.properties.defaultRowHeight,
388
388
  dyDescent: model.properties.dyDescent,
389
389
  outlineLevelCol: model.properties.outlineLevelCol,
390
- outlineLevelRow: model.properties.outlineLevelRow
390
+ outlineLevelRow: model.properties.outlineLevelRow,
391
+ customHeight: model.properties.customHeight
391
392
  }
392
393
  : undefined;
393
394
  if (model.properties && model.properties.defaultColWidth) {
@@ -27,24 +27,27 @@ class StyleXform extends base_xform_1.BaseXform {
27
27
  if (this.xfId) {
28
28
  xmlStream.addAttribute("xfId", model.xfId || 0);
29
29
  }
30
- if (model.numFmtId) {
30
+ if (model.applyNumberFormat || model.numFmtId) {
31
31
  xmlStream.addAttribute("applyNumberFormat", "1");
32
32
  }
33
- if (model.fontId) {
33
+ if (model.applyFont || model.fontId) {
34
34
  xmlStream.addAttribute("applyFont", "1");
35
35
  }
36
- if (model.fillId) {
36
+ if (model.applyFill || model.fillId) {
37
37
  xmlStream.addAttribute("applyFill", "1");
38
38
  }
39
- if (model.borderId) {
39
+ if (model.applyBorder || model.borderId) {
40
40
  xmlStream.addAttribute("applyBorder", "1");
41
41
  }
42
- if (model.alignment) {
42
+ if (model.applyAlignment || model.alignment) {
43
43
  xmlStream.addAttribute("applyAlignment", "1");
44
44
  }
45
- if (model.protection) {
45
+ if (model.applyProtection || model.protection) {
46
46
  xmlStream.addAttribute("applyProtection", "1");
47
47
  }
48
+ if (model.pivotButton) {
49
+ xmlStream.addAttribute("pivotButton", "1");
50
+ }
48
51
  /**
49
52
  * Rendering tags causes close of XML stream.
50
53
  * Therefore adding attributes must be done before rendering tags.
@@ -85,6 +88,23 @@ class StyleXform extends base_xform_1.BaseXform {
85
88
  if (this.xfId) {
86
89
  this.model.xfId = parseInt(node.attributes.xfId, 10);
87
90
  }
91
+ if (node.attributes.pivotButton === "1") {
92
+ this.model.pivotButton = true;
93
+ }
94
+ // Preserve apply* flags from original file
95
+ const applyFlags = [
96
+ "applyNumberFormat",
97
+ "applyFont",
98
+ "applyFill",
99
+ "applyBorder",
100
+ "applyAlignment",
101
+ "applyProtection"
102
+ ];
103
+ for (const flag of applyFlags) {
104
+ if (node.attributes[flag] === "1") {
105
+ this.model[flag] = true;
106
+ }
107
+ }
88
108
  return true;
89
109
  case "alignment":
90
110
  this.parser = this.map.alignment;
@@ -87,6 +87,13 @@ class StylesXform extends base_xform_1.BaseXform {
87
87
  this.weakMap = new WeakMap();
88
88
  this._hasCheckboxes = false;
89
89
  }
90
+ /**
91
+ * Set the default font to use when no font is explicitly specified.
92
+ * This preserves the original file's default font during round-trip.
93
+ */
94
+ setDefaultFont(font) {
95
+ this.defaultFont = font;
96
+ }
90
97
  render(xmlStream, model) {
91
98
  const renderModel = model || this.model;
92
99
  //
@@ -103,8 +110,8 @@ class StylesXform extends base_xform_1.BaseXform {
103
110
  xmlStream.closeNode();
104
111
  }
105
112
  if (!renderModel.fonts.length) {
106
- // default (zero) font
107
- this._addFont({
113
+ // default (zero) font - use preserved font or fallback to Calibri
114
+ this._addFont(this.defaultFont || {
108
115
  size: 11,
109
116
  color: { theme: 1 },
110
117
  name: "Calibri",
@@ -196,6 +203,10 @@ class StylesXform extends base_xform_1.BaseXform {
196
203
  add("borders", this.map.borders);
197
204
  add("styles", this.map.cellXfs);
198
205
  add("dxfs", this.map.dxfs);
206
+ // preserve the default (first) font from the original file
207
+ if (this.map.fonts.model && this.map.fonts.model.length > 0) {
208
+ this.defaultFont = this.map.fonts.model[0];
209
+ }
199
210
  // index numFmts
200
211
  this.index = {
201
212
  model: [],
@@ -223,8 +234,14 @@ class StylesXform extends base_xform_1.BaseXform {
223
234
  }
224
235
  // if we have no default font, add it here now
225
236
  if (!this.model.fonts.length) {
226
- // default (zero) font
227
- this._addFont({ size: 11, color: { theme: 1 }, name: "Calibri", family: 2, scheme: "minor" });
237
+ // default (zero) font - use preserved font or fallback to Calibri
238
+ this._addFont(this.defaultFont || {
239
+ size: 11,
240
+ color: { theme: 1 },
241
+ name: "Calibri",
242
+ family: 2,
243
+ scheme: "minor"
244
+ });
228
245
  }
229
246
  const type = cellType || enums_1.Enums.ValueType.Number;
230
247
  // If we have seen this style object before, assume it has the same styleId.
@@ -265,6 +282,21 @@ class StylesXform extends base_xform_1.BaseXform {
265
282
  if (model.protection) {
266
283
  style.protection = model.protection;
267
284
  }
285
+ // Preserve xf-level attributes (pivotButton, apply* flags)
286
+ const xfFlags = [
287
+ "pivotButton",
288
+ "applyNumberFormat",
289
+ "applyFont",
290
+ "applyFill",
291
+ "applyBorder",
292
+ "applyAlignment",
293
+ "applyProtection"
294
+ ];
295
+ for (const flag of xfFlags) {
296
+ if (model[flag]) {
297
+ style[flag] = true;
298
+ }
299
+ }
268
300
  if (type === enums_1.Enums.ValueType.Checkbox) {
269
301
  // Checkbox rendering relies on style extensions (extLst) and workbook-level parts.
270
302
  // Force applyAlignment="1" (without emitting an <alignment/> node) by providing
@@ -325,6 +357,22 @@ class StylesXform extends base_xform_1.BaseXform {
325
357
  if (style.protection) {
326
358
  model.protection = style.protection;
327
359
  }
360
+ // -------------------------------------------------------
361
+ // xf-level attributes (pivotButton, apply* flags)
362
+ const xfFlags = [
363
+ "pivotButton",
364
+ "applyNumberFormat",
365
+ "applyFont",
366
+ "applyFill",
367
+ "applyBorder",
368
+ "applyAlignment",
369
+ "applyProtection"
370
+ ];
371
+ for (const flag of xfFlags) {
372
+ if (style[flag]) {
373
+ model[flag] = true;
374
+ }
375
+ }
328
376
  return model;
329
377
  }
330
378
  addDxfStyle(style) {
@@ -603,6 +603,8 @@ class XLSX {
603
603
  delete model.sharedStrings;
604
604
  delete model.workbookRels;
605
605
  delete model.sheetDefs;
606
+ // Preserve default font before deleting styles
607
+ model.defaultFont = model.styles?.defaultFont;
606
608
  delete model.styles;
607
609
  delete model.mediaIndex;
608
610
  delete model.drawings;
@@ -1238,7 +1240,12 @@ class XLSX {
1238
1240
  options.useSharedStrings !== undefined ? options.useSharedStrings : true;
1239
1241
  model.useStyles = options.useStyles !== undefined ? options.useStyles : true;
1240
1242
  model.sharedStrings = new shared_strings_xform_1.SharedStringsXform();
1243
+ // Preserve default font from parsed styles if available
1244
+ const oldDefaultFont = model.defaultFont;
1241
1245
  model.styles = model.useStyles ? new styles_xform_1.StylesXform(true) : new styles_xform_1.StylesXform.Mock();
1246
+ if (oldDefaultFont && model.styles.setDefaultFont) {
1247
+ model.styles.setDefaultFont(oldDefaultFont);
1248
+ }
1242
1249
  const workbookXform = new workbook_xform_1.WorkbookXform();
1243
1250
  const worksheetXform = new worksheet_xform_1.WorkSheetXform();
1244
1251
  workbookXform.prepare(model);
@@ -38,7 +38,8 @@ class Column {
38
38
  width: this.width,
39
39
  style: this.style,
40
40
  hidden: this.hidden,
41
- outlineLevel: this.outlineLevel
41
+ outlineLevel: this.outlineLevel,
42
+ bestFit: this.bestFit
42
43
  };
43
44
  }
44
45
  set defn(value) {
@@ -55,6 +56,7 @@ class Column {
55
56
  // headers must be set after style
56
57
  this.header = value.header;
57
58
  this._hidden = !!value.hidden;
59
+ this.bestFit = value.bestFit;
58
60
  }
59
61
  else {
60
62
  delete this._header;
@@ -62,6 +64,7 @@ class Column {
62
64
  delete this.width;
63
65
  this.style = {};
64
66
  this.outlineLevel = 0;
67
+ delete this.bestFit;
65
68
  }
66
69
  }
67
70
  /**
@@ -151,6 +154,7 @@ class Column {
151
154
  return (this.width === model.width &&
152
155
  this.hidden === model.hidden &&
153
156
  this.outlineLevel === model.outlineLevel &&
157
+ this.bestFit === model.bestFit &&
154
158
  isEqual(this.style, model.style));
155
159
  }
156
160
  get isDefault() {
@@ -163,6 +167,9 @@ class Column {
163
167
  if (this.outlineLevel) {
164
168
  return false;
165
169
  }
170
+ if (this.bestFit) {
171
+ return false;
172
+ }
166
173
  const s = this.style;
167
174
  if (s && (s.font || s.numFmt || s.alignment || s.border || s.fill || s.protection)) {
168
175
  return false;
@@ -313,7 +320,8 @@ class Column {
313
320
  isCustomWidth: column.isCustomWidth,
314
321
  hidden: column.hidden,
315
322
  outlineLevel: column.outlineLevel,
316
- collapsed: column.collapsed
323
+ collapsed: column.collapsed,
324
+ bestFit: column.bestFit
317
325
  };
318
326
  cols.push(col);
319
327
  }
@@ -388,7 +388,8 @@ class Row {
388
388
  style: this.style,
389
389
  hidden: this.hidden,
390
390
  outlineLevel: this.outlineLevel,
391
- collapsed: this.collapsed
391
+ collapsed: this.collapsed,
392
+ dyDescent: this.dyDescent
392
393
  }
393
394
  : null;
394
395
  }
@@ -435,6 +436,7 @@ class Row {
435
436
  }
436
437
  this.hidden = value.hidden;
437
438
  this.outlineLevel = value.outlineLevel || 0;
439
+ this.dyDescent = value.dyDescent;
438
440
  this.style = (value.style && JSON.parse(JSON.stringify(value.style))) || {};
439
441
  }
440
442
  }
@@ -255,7 +255,8 @@ class Workbook {
255
255
  pivotTables: this.pivotTables,
256
256
  calcProperties: this.calcProperties,
257
257
  passthrough: this._passthrough,
258
- rawDrawings: this._rawDrawings
258
+ rawDrawings: this._rawDrawings,
259
+ defaultFont: this._defaultFont
259
260
  };
260
261
  }
261
262
  set model(value) {
@@ -300,6 +301,8 @@ class Workbook {
300
301
  this._passthrough = value.passthrough || {};
301
302
  // Preserve raw drawing data for drawings with chart references
302
303
  this._rawDrawings = value.rawDrawings || {};
304
+ // Preserve default font for round-trip fidelity
305
+ this._defaultFont = value.defaultFont;
303
306
  }
304
307
  }
305
308
  // ===========================================================================
@@ -245,18 +245,21 @@ class PivotTableXform extends BaseXform {
245
245
  xmlStream.writeXml('<rowItems count="1"><i t="grand"><x/></i></rowItems>');
246
246
  }
247
247
  // Col fields
248
- // Excel commonly emits a synthetic field x=-2 when there are no column fields.
249
- const colFieldCount = model.colFields.length === 0 ? 1 : model.colFields.length;
250
- xmlStream.openNode("colFields", { count: colFieldCount });
251
- if (model.colFields.length === 0) {
252
- xmlStream.leafNode("field", { x: -2 });
253
- }
254
- else {
255
- for (const fieldIndex of model.colFields) {
256
- xmlStream.leafNode("field", { x: fieldIndex });
248
+ // Only render colFields if it was present in the original file or if there are actual column fields
249
+ // Some pivot tables don't have colFields element at all
250
+ if (model.hasColFields || model.colFields.length > 0) {
251
+ const colFieldCount = model.colFields.length === 0 ? 1 : model.colFields.length;
252
+ xmlStream.openNode("colFields", { count: colFieldCount });
253
+ if (model.colFields.length === 0) {
254
+ xmlStream.leafNode("field", { x: -2 });
255
+ }
256
+ else {
257
+ for (const fieldIndex of model.colFields) {
258
+ xmlStream.leafNode("field", { x: fieldIndex });
259
+ }
257
260
  }
261
+ xmlStream.closeNode();
258
262
  }
259
- xmlStream.closeNode();
260
263
  // Col items - use parsed items if available
261
264
  if (model.colItems && model.colItems.length > 0) {
262
265
  xmlStream.openNode("colItems", { count: model.colItems.length });
@@ -459,6 +462,10 @@ class PivotTableXform extends BaseXform {
459
462
  break;
460
463
  case "colFields":
461
464
  this.state.inColFields = true;
465
+ // Track that colFields element was present in original file
466
+ if (this.model) {
467
+ this.model.hasColFields = true;
468
+ }
462
469
  break;
463
470
  case "dataFields":
464
471
  this.state.inDataFields = true;
@@ -48,7 +48,10 @@ class RowXform extends BaseXform {
48
48
  xmlStream.addAttribute("s", model.styleId);
49
49
  xmlStream.addAttribute("customFormat", "1");
50
50
  }
51
- // Note: dyDescent is MS extension, not output by default (Excel auto-calculates)
51
+ // Output dyDescent if present (MS extension for font descent)
52
+ if (model.dyDescent !== undefined) {
53
+ xmlStream.addAttribute("x14ac:dyDescent", model.dyDescent);
54
+ }
52
55
  if (model.outlineLevel) {
53
56
  xmlStream.addAttribute("outlineLevel", model.outlineLevel);
54
57
  }
@@ -99,6 +102,9 @@ class RowXform extends BaseXform {
99
102
  if (parseBoolean(node.attributes.collapsed)) {
100
103
  model.collapsed = true;
101
104
  }
105
+ if (node.attributes["x14ac:dyDescent"] !== undefined) {
106
+ model.dyDescent = parseFloat(node.attributes["x14ac:dyDescent"]);
107
+ }
102
108
  return true;
103
109
  }
104
110
  this.parser = this.map[node.name];
@@ -11,14 +11,14 @@ class SheetFormatPropertiesXform extends BaseXform {
11
11
  outlineLevelRow: model.outlineLevelRow || undefined,
12
12
  outlineLevelCol: model.outlineLevelCol || undefined,
13
13
  // Only output dyDescent if explicitly set (MS extension, not ECMA-376 standard)
14
- "x14ac:dyDescent": model.dyDescent || undefined
14
+ "x14ac:dyDescent": model.dyDescent !== undefined && model.dyDescent !== 0 ? model.dyDescent : undefined
15
15
  };
16
16
  // Only output defaultColWidth if explicitly set
17
17
  if (model.defaultColWidth) {
18
18
  attributes.defaultColWidth = model.defaultColWidth;
19
19
  }
20
- // default value for 'defaultRowHeight' is 15, this should not be 'custom'
21
- if (!model.defaultRowHeight || model.defaultRowHeight !== 15) {
20
+ // Only output customHeight if it was present in the original file
21
+ if (model.customHeight) {
22
22
  attributes.customHeight = "1";
23
23
  }
24
24
  if (Object.values(attributes).some((value) => value !== undefined)) {
@@ -30,13 +30,18 @@ class SheetFormatPropertiesXform extends BaseXform {
30
30
  if (node.name === "sheetFormatPr") {
31
31
  this.model = {
32
32
  defaultRowHeight: parseFloat(node.attributes.defaultRowHeight || "0"),
33
- dyDescent: parseFloat(node.attributes["x14ac:dyDescent"] || "0"),
33
+ dyDescent: node.attributes["x14ac:dyDescent"] !== undefined
34
+ ? parseFloat(node.attributes["x14ac:dyDescent"])
35
+ : undefined,
34
36
  outlineLevelRow: parseInt(node.attributes.outlineLevelRow || "0", 10),
35
37
  outlineLevelCol: parseInt(node.attributes.outlineLevelCol || "0", 10)
36
38
  };
37
39
  if (node.attributes.defaultColWidth) {
38
40
  this.model.defaultColWidth = parseFloat(node.attributes.defaultColWidth);
39
41
  }
42
+ if (node.attributes.customHeight === "1") {
43
+ this.model.customHeight = true;
44
+ }
40
45
  return true;
41
46
  }
42
47
  return false;
@@ -37,8 +37,8 @@ class SheetViewXform extends BaseXform {
37
37
  add("showRuler", "0", model.showRuler === false);
38
38
  add("showRowColHeaders", "0", model.showRowColHeaders === false);
39
39
  add("showGridLines", "0", model.showGridLines === false);
40
- add("zoomScale", model.zoomScale, model.zoomScale);
41
- add("zoomScaleNormal", model.zoomScaleNormal, model.zoomScaleNormal);
40
+ add("zoomScale", model.zoomScale, model.zoomScale !== undefined && model.zoomScale !== 100);
41
+ add("zoomScaleNormal", model.zoomScaleNormal, model.zoomScaleNormal !== undefined && model.zoomScaleNormal !== 100);
42
42
  add("view", model.style, model.style);
43
43
  let topLeftCell;
44
44
  let xSplit;
@@ -143,6 +143,7 @@ class SheetViewXform extends BaseXform {
143
143
  model = this.model = {
144
144
  workbookViewId: this.sheetView.workbookViewId,
145
145
  rightToLeft: this.sheetView.rightToLeft,
146
+ tabSelected: this.sheetView.tabSelected,
146
147
  state: VIEW_STATES[this.pane.state] || "split", // split is default
147
148
  xSplit: this.pane.xSplit,
148
149
  ySplit: this.pane.ySplit,
@@ -168,6 +169,7 @@ class SheetViewXform extends BaseXform {
168
169
  model = this.model = {
169
170
  workbookViewId: this.sheetView.workbookViewId,
170
171
  rightToLeft: this.sheetView.rightToLeft,
172
+ tabSelected: this.sheetView.tabSelected,
171
173
  state: "normal",
172
174
  showRuler: this.sheetView.showRuler,
173
175
  showRowColHeaders: this.sheetView.showRowColHeaders,
@@ -384,7 +384,8 @@ class WorkSheetXform extends BaseXform {
384
384
  defaultRowHeight: model.properties.defaultRowHeight,
385
385
  dyDescent: model.properties.dyDescent,
386
386
  outlineLevelCol: model.properties.outlineLevelCol,
387
- outlineLevelRow: model.properties.outlineLevelRow
387
+ outlineLevelRow: model.properties.outlineLevelRow,
388
+ customHeight: model.properties.customHeight
388
389
  }
389
390
  : undefined;
390
391
  if (model.properties && model.properties.defaultColWidth) {
@@ -24,24 +24,27 @@ class StyleXform extends BaseXform {
24
24
  if (this.xfId) {
25
25
  xmlStream.addAttribute("xfId", model.xfId || 0);
26
26
  }
27
- if (model.numFmtId) {
27
+ if (model.applyNumberFormat || model.numFmtId) {
28
28
  xmlStream.addAttribute("applyNumberFormat", "1");
29
29
  }
30
- if (model.fontId) {
30
+ if (model.applyFont || model.fontId) {
31
31
  xmlStream.addAttribute("applyFont", "1");
32
32
  }
33
- if (model.fillId) {
33
+ if (model.applyFill || model.fillId) {
34
34
  xmlStream.addAttribute("applyFill", "1");
35
35
  }
36
- if (model.borderId) {
36
+ if (model.applyBorder || model.borderId) {
37
37
  xmlStream.addAttribute("applyBorder", "1");
38
38
  }
39
- if (model.alignment) {
39
+ if (model.applyAlignment || model.alignment) {
40
40
  xmlStream.addAttribute("applyAlignment", "1");
41
41
  }
42
- if (model.protection) {
42
+ if (model.applyProtection || model.protection) {
43
43
  xmlStream.addAttribute("applyProtection", "1");
44
44
  }
45
+ if (model.pivotButton) {
46
+ xmlStream.addAttribute("pivotButton", "1");
47
+ }
45
48
  /**
46
49
  * Rendering tags causes close of XML stream.
47
50
  * Therefore adding attributes must be done before rendering tags.
@@ -82,6 +85,23 @@ class StyleXform extends BaseXform {
82
85
  if (this.xfId) {
83
86
  this.model.xfId = parseInt(node.attributes.xfId, 10);
84
87
  }
88
+ if (node.attributes.pivotButton === "1") {
89
+ this.model.pivotButton = true;
90
+ }
91
+ // Preserve apply* flags from original file
92
+ const applyFlags = [
93
+ "applyNumberFormat",
94
+ "applyFont",
95
+ "applyFill",
96
+ "applyBorder",
97
+ "applyAlignment",
98
+ "applyProtection"
99
+ ];
100
+ for (const flag of applyFlags) {
101
+ if (node.attributes[flag] === "1") {
102
+ this.model[flag] = true;
103
+ }
104
+ }
85
105
  return true;
86
106
  case "alignment":
87
107
  this.parser = this.map.alignment;