@cj-tech-master/excelts 4.2.3-canary.20260115111903.b80904d → 4.2.3-canary.20260122073152.a9bb6b0

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 (40) hide show
  1. package/dist/browser/modules/csv/csv-core.d.ts +0 -9
  2. package/dist/browser/modules/csv/csv.browser.js +3 -3
  3. package/dist/browser/modules/excel/utils/passthrough-manager.d.ts +77 -0
  4. package/dist/browser/modules/excel/utils/passthrough-manager.js +129 -0
  5. package/dist/browser/modules/excel/workbook.d.ts +8 -0
  6. package/dist/browser/modules/excel/workbook.js +9 -1
  7. package/dist/browser/modules/excel/worksheet.d.ts +4 -0
  8. package/dist/browser/modules/excel/worksheet.js +4 -1
  9. package/dist/browser/modules/excel/xlsx/xform/core/content-types-xform.js +16 -10
  10. package/dist/browser/modules/excel/xlsx/xform/pivot-table/pivot-table-xform.d.ts +34 -11
  11. package/dist/browser/modules/excel/xlsx/xform/pivot-table/pivot-table-xform.js +256 -86
  12. package/dist/browser/modules/excel/xlsx/xform/sheet/worksheet-xform.js +38 -11
  13. package/dist/browser/modules/excel/xlsx/xlsx.browser.d.ts +36 -1
  14. package/dist/browser/modules/excel/xlsx/xlsx.browser.js +213 -131
  15. package/dist/cjs/modules/csv/csv.browser.js +3 -3
  16. package/dist/cjs/modules/excel/utils/passthrough-manager.js +133 -0
  17. package/dist/cjs/modules/excel/workbook.js +9 -1
  18. package/dist/cjs/modules/excel/worksheet.js +4 -1
  19. package/dist/cjs/modules/excel/xlsx/xform/core/content-types-xform.js +16 -10
  20. package/dist/cjs/modules/excel/xlsx/xform/pivot-table/pivot-table-xform.js +256 -86
  21. package/dist/cjs/modules/excel/xlsx/xform/sheet/worksheet-xform.js +38 -11
  22. package/dist/cjs/modules/excel/xlsx/xlsx.browser.js +213 -131
  23. package/dist/esm/modules/csv/csv.browser.js +3 -3
  24. package/dist/esm/modules/excel/utils/passthrough-manager.js +129 -0
  25. package/dist/esm/modules/excel/workbook.js +9 -1
  26. package/dist/esm/modules/excel/worksheet.js +4 -1
  27. package/dist/esm/modules/excel/xlsx/xform/core/content-types-xform.js +16 -10
  28. package/dist/esm/modules/excel/xlsx/xform/pivot-table/pivot-table-xform.js +256 -86
  29. package/dist/esm/modules/excel/xlsx/xform/sheet/worksheet-xform.js +38 -11
  30. package/dist/esm/modules/excel/xlsx/xlsx.browser.js +213 -131
  31. package/dist/iife/excelts.iife.js +512 -241
  32. package/dist/iife/excelts.iife.js.map +1 -1
  33. package/dist/iife/excelts.iife.min.js +24 -51
  34. package/dist/types/modules/csv/csv-core.d.ts +0 -9
  35. package/dist/types/modules/excel/utils/passthrough-manager.d.ts +77 -0
  36. package/dist/types/modules/excel/workbook.d.ts +8 -0
  37. package/dist/types/modules/excel/worksheet.d.ts +4 -0
  38. package/dist/types/modules/excel/xlsx/xform/pivot-table/pivot-table-xform.d.ts +34 -11
  39. package/dist/types/modules/excel/xlsx/xlsx.browser.d.ts +36 -1
  40. package/package.json +2 -2
@@ -93,11 +93,16 @@ class ContentTypesXform extends BaseXform {
93
93
  });
94
94
  });
95
95
  }
96
- if (model.commentRefs) {
96
+ // VML extension is needed for comments or form controls
97
+ const hasComments = model.commentRefs && model.commentRefs.length > 0;
98
+ const hasFormControls = model.formControlRefs && model.formControlRefs.length > 0;
99
+ if (hasComments || hasFormControls) {
97
100
  xmlStream.leafNode("Default", {
98
101
  Extension: "vml",
99
102
  ContentType: "application/vnd.openxmlformats-officedocument.vmlDrawing"
100
103
  });
104
+ }
105
+ if (hasComments) {
101
106
  model.commentRefs.forEach(({ commentName }) => {
102
107
  xmlStream.leafNode("Override", {
103
108
  PartName: toContentTypesPartName(commentsPathFromName(commentName)),
@@ -105,15 +110,7 @@ class ContentTypesXform extends BaseXform {
105
110
  });
106
111
  });
107
112
  }
108
- // Add form control (ctrlProps) content types
109
- if (model.formControlRefs) {
110
- // Ensure vml extension is declared (may already be declared for comments)
111
- if (!model.commentRefs) {
112
- xmlStream.leafNode("Default", {
113
- Extension: "vml",
114
- ContentType: "application/vnd.openxmlformats-officedocument.vmlDrawing"
115
- });
116
- }
113
+ if (hasFormControls) {
117
114
  for (const ctrlPropId of model.formControlRefs) {
118
115
  xmlStream.leafNode("Override", {
119
116
  PartName: toContentTypesPartName(ctrlPropPath(ctrlPropId)),
@@ -121,6 +118,15 @@ class ContentTypesXform extends BaseXform {
121
118
  });
122
119
  }
123
120
  }
121
+ // Add passthrough content types (charts, etc.)
122
+ if (model.passthroughContentTypes) {
123
+ for (const { partName, contentType } of model.passthroughContentTypes) {
124
+ xmlStream.leafNode("Override", {
125
+ PartName: toContentTypesPartName(partName),
126
+ ContentType: contentType
127
+ });
128
+ }
129
+ }
124
130
  xmlStream.leafNode("Override", {
125
131
  PartName: toContentTypesPartName(OOXML_PATHS.docPropsCore),
126
132
  ContentType: "application/vnd.openxmlformats-package.core-properties+xml"
@@ -4,18 +4,30 @@ import { BaseXform } from "../base-xform.js";
4
4
  class PivotTableXform extends BaseXform {
5
5
  constructor() {
6
6
  super();
7
+ // Parser state consolidated into object for easier reset
8
+ this.state = {
9
+ inPivotFields: false,
10
+ inRowFields: false,
11
+ inColFields: false,
12
+ inDataFields: false,
13
+ inRowItems: false,
14
+ inColItems: false,
15
+ inLocation: false,
16
+ inItems: false,
17
+ inPivotTableStyleInfo: false,
18
+ inChartFormats: false,
19
+ inPivotArea: false
20
+ };
21
+ // Current parsing context
22
+ this.currentPivotField = null;
23
+ this.currentRowItem = null;
24
+ this.currentColItem = null;
25
+ this.currentChartFormat = null;
26
+ // Buffer for collecting pivotArea XML
27
+ this.pivotAreaXmlBuffer = [];
28
+ this.pivotAreaDepth = 0;
7
29
  this.map = {};
8
30
  this.model = null;
9
- this.inPivotFields = false;
10
- this.inRowFields = false;
11
- this.inColFields = false;
12
- this.inDataFields = false;
13
- this.inRowItems = false;
14
- this.inColItems = false;
15
- this.inLocation = false;
16
- this.currentPivotField = null;
17
- this.inItems = false;
18
- this.inPivotTableStyleInfo = false;
19
31
  }
20
32
  prepare(_model) {
21
33
  // No preparation needed
@@ -26,16 +38,17 @@ class PivotTableXform extends BaseXform {
26
38
  }
27
39
  reset() {
28
40
  this.model = null;
29
- this.inPivotFields = false;
30
- this.inRowFields = false;
31
- this.inColFields = false;
32
- this.inDataFields = false;
33
- this.inRowItems = false;
34
- this.inColItems = false;
35
- this.inLocation = false;
41
+ // Reset all parser state flags using object
42
+ Object.keys(this.state).forEach(key => {
43
+ this.state[key] = false;
44
+ });
45
+ // Reset current context
36
46
  this.currentPivotField = null;
37
- this.inItems = false;
38
- this.inPivotTableStyleInfo = false;
47
+ this.currentRowItem = null;
48
+ this.currentColItem = null;
49
+ this.currentChartFormat = null;
50
+ this.pivotAreaXmlBuffer = [];
51
+ this.pivotAreaDepth = 0;
39
52
  }
40
53
  /**
41
54
  * Render pivot table XML.
@@ -156,8 +169,7 @@ class PivotTableXform extends BaseXform {
156
169
  * Render loaded pivot table (preserving original structure)
157
170
  */
158
171
  renderLoaded(xmlStream, model) {
159
- xmlStream.openXml(XmlStream.StdDocAttributes);
160
- xmlStream.openNode(this.tag, {
172
+ const attrs = {
161
173
  ...PivotTableXform.PIVOT_TABLE_ATTRIBUTES,
162
174
  name: model.name || "PivotTable1",
163
175
  cacheId: model.cacheId,
@@ -166,7 +178,8 @@ class PivotTableXform extends BaseXform {
166
178
  applyFontFormats: model.applyFontFormats || "0",
167
179
  applyPatternFormats: model.applyPatternFormats || "0",
168
180
  applyAlignmentFormats: model.applyAlignmentFormats || "0",
169
- applyWidthHeightFormats: model.applyWidthHeightFormats || "0",
181
+ // Preserve original value when present; default to Excel's typical "0".
182
+ applyWidthHeightFormats: model.applyWidthHeightFormats ?? "0",
170
183
  dataCaption: model.dataCaption || "Values",
171
184
  updatedVersion: model.updatedVersion || "8",
172
185
  minRefreshableVersion: model.minRefreshableVersion || "3",
@@ -174,10 +187,27 @@ class PivotTableXform extends BaseXform {
174
187
  itemPrintTitles: model.itemPrintTitles ? "1" : "0",
175
188
  createdVersion: model.createdVersion || "8",
176
189
  indent: model.indent !== undefined ? String(model.indent) : "0",
177
- compact: model.compact ? "1" : "0",
178
- compactData: model.compactData ? "1" : "0",
179
190
  multipleFieldFilters: model.multipleFieldFilters ? "1" : "0"
180
- });
191
+ };
192
+ // Add outline attributes if present
193
+ if (model.outline) {
194
+ attrs.outline = "1";
195
+ }
196
+ if (model.outlineData) {
197
+ attrs.outlineData = "1";
198
+ }
199
+ if (model.chartFormat !== undefined) {
200
+ attrs.chartFormat = String(model.chartFormat);
201
+ }
202
+ // Only add compact/compactData if they are true (some files don't have them)
203
+ if (model.compact) {
204
+ attrs.compact = "1";
205
+ }
206
+ if (model.compactData) {
207
+ attrs.compactData = "1";
208
+ }
209
+ xmlStream.openXml(XmlStream.StdDocAttributes);
210
+ xmlStream.openNode(this.tag, attrs);
181
211
  // Location
182
212
  if (model.location) {
183
213
  xmlStream.leafNode("location", {
@@ -203,12 +233,19 @@ class PivotTableXform extends BaseXform {
203
233
  }
204
234
  xmlStream.closeNode();
205
235
  }
206
- // Row items (simplified - just grand total)
207
- xmlStream.writeXml(`
208
- <rowItems count="1">
209
- <i t="grand"><x /></i>
210
- </rowItems>`);
236
+ // Row items - use parsed items if available; otherwise emit a minimal grand total
237
+ if (model.rowItems && model.rowItems.length > 0) {
238
+ xmlStream.openNode("rowItems", { count: model.rowItems.length });
239
+ for (const item of model.rowItems) {
240
+ this.renderRowColItem(xmlStream, item);
241
+ }
242
+ xmlStream.closeNode();
243
+ }
244
+ else {
245
+ xmlStream.writeXml('<rowItems count="1"><i t="grand"><x/></i></rowItems>');
246
+ }
211
247
  // Col fields
248
+ // Excel commonly emits a synthetic field x=-2 when there are no column fields.
212
249
  const colFieldCount = model.colFields.length === 0 ? 1 : model.colFields.length;
213
250
  xmlStream.openNode("colFields", { count: colFieldCount });
214
251
  if (model.colFields.length === 0) {
@@ -220,25 +257,52 @@ class PivotTableXform extends BaseXform {
220
257
  }
221
258
  }
222
259
  xmlStream.closeNode();
223
- // Col items (simplified - just grand total)
224
- xmlStream.writeXml(`
225
- <colItems count="1">
226
- <i t="grand"><x /></i>
227
- </colItems>`);
260
+ // Col items - use parsed items if available
261
+ if (model.colItems && model.colItems.length > 0) {
262
+ xmlStream.openNode("colItems", { count: model.colItems.length });
263
+ for (const item of model.colItems) {
264
+ this.renderRowColItem(xmlStream, item);
265
+ }
266
+ xmlStream.closeNode();
267
+ }
268
+ else {
269
+ xmlStream.writeXml('<colItems count="1"><i t="grand"><x/></i></colItems>');
270
+ }
228
271
  // Data fields
229
272
  if (model.dataFields.length > 0) {
230
273
  xmlStream.openNode("dataFields", { count: model.dataFields.length });
231
274
  for (const dataField of model.dataFields) {
232
- const attrs = {
275
+ const dfAttrs = {
233
276
  name: dataField.name,
234
277
  fld: dataField.fld,
235
278
  baseField: dataField.baseField ?? 0,
236
279
  baseItem: dataField.baseItem ?? 0
237
280
  };
238
281
  if (dataField.subtotal && dataField.subtotal !== "sum") {
239
- attrs.subtotal = dataField.subtotal;
282
+ dfAttrs.subtotal = dataField.subtotal;
283
+ }
284
+ xmlStream.leafNode("dataField", dfAttrs);
285
+ }
286
+ xmlStream.closeNode();
287
+ }
288
+ // Chart formats (for pivot charts) - preserve original pivotArea XML
289
+ if (model.chartFormats && model.chartFormats.length > 0) {
290
+ xmlStream.openNode("chartFormats", { count: model.chartFormats.length });
291
+ for (const cf of model.chartFormats) {
292
+ xmlStream.openNode("chartFormat", {
293
+ chart: cf.chart,
294
+ format: cf.format,
295
+ series: cf.series ? "1" : undefined
296
+ });
297
+ // Use preserved pivotArea XML or fallback to default
298
+ if (cf.pivotAreaXml) {
299
+ xmlStream.writeXml(cf.pivotAreaXml);
300
+ }
301
+ else {
302
+ // Fallback for newly created chart formats (shouldn't happen for loaded models)
303
+ xmlStream.writeXml(`<pivotArea type="data" outline="0" fieldPosition="0"><references count="1"><reference field="4294967294" count="1" selected="0"><x v="0"/></reference></references></pivotArea>`);
240
304
  }
241
- xmlStream.leafNode("dataField", attrs);
305
+ xmlStream.closeNode();
242
306
  }
243
307
  xmlStream.closeNode();
244
308
  }
@@ -252,46 +316,48 @@ class PivotTableXform extends BaseXform {
252
316
  showLastColumn: "1"
253
317
  });
254
318
  // Extensions
255
- xmlStream.writeXml(`
256
- <extLst>
257
- <ext
258
- uri="{962EF5D1-5CA2-4c93-8EF4-DBF5C05439D2}"
259
- xmlns:x14="http://schemas.microsoft.com/office/spreadsheetml/2009/9/main"
260
- >
261
- <x14:pivotTableDefinition
262
- hideValuesRow="1"
263
- xmlns:xm="http://schemas.microsoft.com/office/excel/2006/main"
264
- />
265
- </ext>
266
- <ext
267
- uri="{747A6164-185A-40DC-8AA5-F01512510D54}"
268
- xmlns:xpdl="http://schemas.microsoft.com/office/spreadsheetml/2016/pivotdefaultlayout"
269
- >
270
- <xpdl:pivotTableDefinition16
271
- EnabledSubtotalsDefault="0"
272
- SubtotalsOnTopDefault="0"
273
- />
274
- </ext>
275
- </extLst>
276
- `);
319
+ xmlStream.writeXml(`<extLst><ext uri="{962EF5D1-5CA2-4c93-8EF4-DBF5C05439D2}" xmlns:x14="http://schemas.microsoft.com/office/spreadsheetml/2009/9/main"><x14:pivotTableDefinition hideValuesRow="1" xmlns:xm="http://schemas.microsoft.com/office/excel/2006/main"/></ext><ext uri="{747A6164-185A-40DC-8AA5-F01512510D54}" xmlns:xpdl="http://schemas.microsoft.com/office/spreadsheetml/2016/pivotdefaultlayout"><xpdl:pivotTableDefinition16/></ext></extLst>`);
277
320
  xmlStream.closeNode();
278
321
  }
322
+ /**
323
+ * Render a row or column item element
324
+ */
325
+ renderRowColItem(xmlStream, item) {
326
+ const attrs = {};
327
+ if (item.t) {
328
+ attrs.t = item.t;
329
+ }
330
+ if (item.x && item.x.length > 0) {
331
+ xmlStream.openNode("i", attrs);
332
+ for (const x of item.x) {
333
+ if (x.v && x.v !== 0) {
334
+ xmlStream.leafNode("x", { v: x.v });
335
+ }
336
+ else {
337
+ xmlStream.leafNode("x");
338
+ }
339
+ }
340
+ xmlStream.closeNode();
341
+ }
342
+ else {
343
+ // Empty item (like <i/> in colItems)
344
+ xmlStream.leafNode("i", attrs);
345
+ }
346
+ }
279
347
  /**
280
348
  * Render a loaded pivot field
281
349
  */
282
350
  renderPivotFieldLoaded(xmlStream, field) {
283
- const attrs = {
284
- compact: field.compact ? "1" : "0",
285
- outline: field.outline ? "1" : "0",
286
- showAll: field.showAll ? "1" : "0",
287
- defaultSubtotal: field.defaultSubtotal ? "1" : "0"
288
- };
351
+ const attrs = {};
352
+ // Only add attributes that were present in the original
289
353
  if (field.axis) {
290
354
  attrs.axis = field.axis;
291
355
  }
292
356
  if (field.dataField) {
293
357
  attrs.dataField = "1";
294
358
  }
359
+ // showAll is typically always present
360
+ attrs.showAll = field.showAll ? "1" : "0";
295
361
  if (field.items && field.items.length > 0) {
296
362
  xmlStream.openNode("pivotField", attrs);
297
363
  xmlStream.openNode("items", { count: field.items.length + 1 });
@@ -299,7 +365,7 @@ class PivotTableXform extends BaseXform {
299
365
  xmlStream.leafNode("item", { x: itemIndex });
300
366
  }
301
367
  // Grand total item
302
- xmlStream.writeXml('<item t="default" />');
368
+ xmlStream.writeXml('<item t="default"/>');
303
369
  xmlStream.closeNode(); // items
304
370
  xmlStream.closeNode(); // pivotField
305
371
  }
@@ -337,6 +403,12 @@ class PivotTableXform extends BaseXform {
337
403
  compact: attributes.compact === "1",
338
404
  compactData: attributes.compactData === "1",
339
405
  multipleFieldFilters: attributes.multipleFieldFilters === "1",
406
+ outline: attributes.outline === "1",
407
+ outlineData: attributes.outlineData === "1",
408
+ chartFormat: attributes.chartFormat ? parseInt(attributes.chartFormat, 10) : undefined,
409
+ rowItems: [],
410
+ colItems: [],
411
+ chartFormats: [],
340
412
  isLoaded: true
341
413
  };
342
414
  break;
@@ -357,10 +429,10 @@ class PivotTableXform extends BaseXform {
357
429
  }
358
430
  break;
359
431
  case "pivotFields":
360
- this.inPivotFields = true;
432
+ this.state.inPivotFields = true;
361
433
  break;
362
434
  case "pivotField":
363
- if (this.inPivotFields) {
435
+ if (this.state.inPivotFields) {
364
436
  this.currentPivotField = {
365
437
  axis: attributes.axis,
366
438
  dataField: attributes.dataField === "1",
@@ -374,43 +446,101 @@ class PivotTableXform extends BaseXform {
374
446
  break;
375
447
  case "items":
376
448
  if (this.currentPivotField) {
377
- this.inItems = true;
449
+ this.state.inItems = true;
378
450
  }
379
451
  break;
380
452
  case "item":
381
- if (this.inItems && this.currentPivotField && attributes.x !== undefined) {
453
+ if (this.state.inItems && this.currentPivotField && attributes.x !== undefined) {
382
454
  this.currentPivotField.items.push(parseInt(attributes.x, 10));
383
455
  }
384
456
  break;
385
457
  case "rowFields":
386
- this.inRowFields = true;
458
+ this.state.inRowFields = true;
387
459
  break;
388
460
  case "colFields":
389
- this.inColFields = true;
461
+ this.state.inColFields = true;
390
462
  break;
391
463
  case "dataFields":
392
- this.inDataFields = true;
464
+ this.state.inDataFields = true;
393
465
  break;
394
466
  case "rowItems":
395
- this.inRowItems = true;
467
+ this.state.inRowItems = true;
396
468
  break;
397
469
  case "colItems":
398
- this.inColItems = true;
470
+ this.state.inColItems = true;
471
+ break;
472
+ case "i":
473
+ // Handle row/col item element
474
+ if (this.state.inRowItems && this.model) {
475
+ this.currentRowItem = { t: attributes.t, x: [] };
476
+ }
477
+ else if (this.state.inColItems && this.model) {
478
+ this.currentColItem = { t: attributes.t, x: [] };
479
+ }
480
+ break;
481
+ case "x":
482
+ // Handle x element inside row/col items or pivotArea
483
+ if (this.state.inPivotArea) {
484
+ // Collect x element for pivotArea XML
485
+ const xAttrs = Object.entries(attributes)
486
+ .map(([k, v]) => `${k}="${v}"`)
487
+ .join(" ");
488
+ this.pivotAreaXmlBuffer.push(xAttrs ? `<x ${xAttrs}/>` : "<x/>");
489
+ }
490
+ else if (this.currentRowItem) {
491
+ this.currentRowItem.x.push({ v: attributes.v ? parseInt(attributes.v, 10) : 0 });
492
+ }
493
+ else if (this.currentColItem) {
494
+ this.currentColItem.x.push({ v: attributes.v ? parseInt(attributes.v, 10) : 0 });
495
+ }
496
+ break;
497
+ case "chartFormats":
498
+ this.state.inChartFormats = true;
499
+ break;
500
+ case "chartFormat":
501
+ if (this.state.inChartFormats && this.model) {
502
+ this.currentChartFormat = {
503
+ chart: attributes.chart ? parseInt(attributes.chart, 10) : 0,
504
+ format: attributes.format ? parseInt(attributes.format, 10) : 0,
505
+ series: attributes.series === "1"
506
+ };
507
+ }
508
+ break;
509
+ case "pivotArea":
510
+ // Start collecting pivotArea XML for chartFormat
511
+ if (this.currentChartFormat) {
512
+ this.state.inPivotArea = true;
513
+ const attrsStr = Object.entries(attributes)
514
+ .map(([k, v]) => `${k}="${v}"`)
515
+ .join(" ");
516
+ this.pivotAreaXmlBuffer = [attrsStr ? `<pivotArea ${attrsStr}>` : "<pivotArea>"];
517
+ }
518
+ break;
519
+ case "references":
520
+ case "reference":
521
+ // Collect nested elements in pivotArea
522
+ if (this.state.inPivotArea) {
523
+ this.pivotAreaDepth++;
524
+ const attrsStr = Object.entries(attributes)
525
+ .map(([k, v]) => `${k}="${v}"`)
526
+ .join(" ");
527
+ this.pivotAreaXmlBuffer.push(`<${name}${attrsStr ? " " + attrsStr : ""}>`);
528
+ }
399
529
  break;
400
530
  case "field":
401
531
  // Handle field element (used in rowFields, colFields)
402
532
  if (this.model) {
403
533
  const fieldIndex = parseInt(attributes.x || "0", 10);
404
- if (this.inRowFields) {
534
+ if (this.state.inRowFields) {
405
535
  this.model.rowFields.push(fieldIndex);
406
536
  }
407
- else if (this.inColFields) {
537
+ else if (this.state.inColFields) {
408
538
  this.model.colFields.push(fieldIndex);
409
539
  }
410
540
  }
411
541
  break;
412
542
  case "dataField":
413
- if (this.inDataFields && this.model) {
543
+ if (this.state.inDataFields && this.model) {
414
544
  this.model.dataFields.push({
415
545
  name: xmlDecode(attributes.name || ""),
416
546
  fld: parseInt(attributes.fld || "0", 10),
@@ -432,12 +562,32 @@ class PivotTableXform extends BaseXform {
432
562
  // No text content in pivot table elements
433
563
  }
434
564
  parseClose(name) {
565
+ // Handle pivotArea nested elements - close tags
566
+ if (this.state.inPivotArea) {
567
+ if (name === "pivotArea") {
568
+ this.pivotAreaXmlBuffer.push("</pivotArea>");
569
+ if (this.currentChartFormat) {
570
+ this.currentChartFormat.pivotAreaXml = this.pivotAreaXmlBuffer.join("");
571
+ }
572
+ this.state.inPivotArea = false;
573
+ this.pivotAreaXmlBuffer = [];
574
+ this.pivotAreaDepth = 0;
575
+ return true;
576
+ }
577
+ else if (name === "references" || name === "reference") {
578
+ this.pivotAreaXmlBuffer.push(`</${name}>`);
579
+ this.pivotAreaDepth--;
580
+ return true;
581
+ }
582
+ // x elements are self-closing, no need to handle close
583
+ return true;
584
+ }
435
585
  switch (name) {
436
586
  case this.tag:
437
587
  // End of pivotTableDefinition
438
588
  return false;
439
589
  case "pivotFields":
440
- this.inPivotFields = false;
590
+ this.state.inPivotFields = false;
441
591
  break;
442
592
  case "pivotField":
443
593
  if (this.currentPivotField && this.model) {
@@ -446,22 +596,42 @@ class PivotTableXform extends BaseXform {
446
596
  }
447
597
  break;
448
598
  case "items":
449
- this.inItems = false;
599
+ this.state.inItems = false;
450
600
  break;
451
601
  case "rowFields":
452
- this.inRowFields = false;
602
+ this.state.inRowFields = false;
453
603
  break;
454
604
  case "colFields":
455
- this.inColFields = false;
605
+ this.state.inColFields = false;
456
606
  break;
457
607
  case "dataFields":
458
- this.inDataFields = false;
608
+ this.state.inDataFields = false;
459
609
  break;
460
610
  case "rowItems":
461
- this.inRowItems = false;
611
+ this.state.inRowItems = false;
462
612
  break;
463
613
  case "colItems":
464
- this.inColItems = false;
614
+ this.state.inColItems = false;
615
+ break;
616
+ case "i":
617
+ // Finish row/col item
618
+ if (this.currentRowItem && this.model) {
619
+ this.model.rowItems.push(this.currentRowItem);
620
+ this.currentRowItem = null;
621
+ }
622
+ else if (this.currentColItem && this.model) {
623
+ this.model.colItems.push(this.currentColItem);
624
+ this.currentColItem = null;
625
+ }
626
+ break;
627
+ case "chartFormats":
628
+ this.state.inChartFormats = false;
629
+ break;
630
+ case "chartFormat":
631
+ if (this.currentChartFormat && this.model) {
632
+ this.model.chartFormats.push(this.currentChartFormat);
633
+ this.currentChartFormat = null;
634
+ }
465
635
  break;
466
636
  }
467
637
  return true;
@@ -188,6 +188,23 @@ class WorkSheetXform extends BaseXform {
188
188
  vmlDrawing: `vmlDrawing${model.id}`
189
189
  });
190
190
  }
191
+ // Handle pre-loaded drawing (from file read) that may contain charts or other non-image content
192
+ // This preserves drawings through round-trip even if they don't contain images
193
+ // Note: Always assign a new rId since rels are rebuilt during write
194
+ if (model.drawing && model.drawing.anchors) {
195
+ // This is a loaded drawing that needs to be added to relationships
196
+ const drawing = model.drawing;
197
+ drawing.rId = nextRid(rels);
198
+ if (!drawing.name) {
199
+ drawing.name = `drawing${++options.drawingsCount}`;
200
+ }
201
+ options.drawings.push(drawing);
202
+ rels.push({
203
+ Id: drawing.rId,
204
+ Type: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing",
205
+ Target: drawingRelTargetFromWorksheet(drawing.name)
206
+ });
207
+ }
191
208
  const drawingRelsHash = [];
192
209
  let bookImage;
193
210
  model.media.forEach(medium => {
@@ -599,17 +616,27 @@ class WorkSheetXform extends BaseXform {
599
616
  if (match) {
600
617
  const drawingName = match[1];
601
618
  const drawing = options.drawings[drawingName];
602
- drawing.anchors.forEach(anchor => {
603
- if (anchor.medium) {
604
- const image = {
605
- type: "image",
606
- imageId: anchor.medium.index,
607
- range: anchor.range,
608
- hyperlinks: anchor.picture.hyperlinks
609
- };
610
- model.media.push(image);
611
- }
612
- });
619
+ if (drawing) {
620
+ // Preserve the drawing object for round-trip (charts, etc.)
621
+ // This includes the name, anchors, and rels
622
+ model.drawing = {
623
+ ...drawing,
624
+ name: drawingName,
625
+ rels: options.drawingRels?.[drawingName] || drawing.rels || []
626
+ };
627
+ // Also extract images to model.media for backward compatibility
628
+ drawing.anchors.forEach(anchor => {
629
+ if (anchor.medium) {
630
+ const image = {
631
+ type: "image",
632
+ imageId: anchor.medium.index,
633
+ range: anchor.range,
634
+ hyperlinks: anchor.picture.hyperlinks
635
+ };
636
+ model.media.push(image);
637
+ }
638
+ });
639
+ }
613
640
  }
614
641
  }
615
642
  const backgroundRel = model.background && rels[model.background.rId];