@cj-tech-master/excelts 4.2.3-canary.20260115111903.b80904d → 4.2.3-canary.20260122075539.cc11b20

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 (48) 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/parse-sax.d.ts +0 -3
  4. package/dist/browser/modules/excel/utils/parse-sax.js +13 -32
  5. package/dist/browser/modules/excel/utils/passthrough-manager.d.ts +77 -0
  6. package/dist/browser/modules/excel/utils/passthrough-manager.js +129 -0
  7. package/dist/browser/modules/excel/workbook.d.ts +8 -0
  8. package/dist/browser/modules/excel/workbook.js +9 -1
  9. package/dist/browser/modules/excel/worksheet.d.ts +4 -0
  10. package/dist/browser/modules/excel/worksheet.js +4 -1
  11. package/dist/browser/modules/excel/xlsx/xform/base-xform.js +68 -1
  12. package/dist/browser/modules/excel/xlsx/xform/core/content-types-xform.js +16 -10
  13. package/dist/browser/modules/excel/xlsx/xform/pivot-table/pivot-table-xform.d.ts +34 -11
  14. package/dist/browser/modules/excel/xlsx/xform/pivot-table/pivot-table-xform.js +256 -86
  15. package/dist/browser/modules/excel/xlsx/xform/sheet/worksheet-xform.js +38 -11
  16. package/dist/browser/modules/excel/xlsx/xlsx.browser.d.ts +36 -1
  17. package/dist/browser/modules/excel/xlsx/xlsx.browser.js +213 -131
  18. package/dist/cjs/modules/csv/csv.browser.js +3 -3
  19. package/dist/cjs/modules/excel/utils/parse-sax.js +13 -32
  20. package/dist/cjs/modules/excel/utils/passthrough-manager.js +133 -0
  21. package/dist/cjs/modules/excel/workbook.js +9 -1
  22. package/dist/cjs/modules/excel/worksheet.js +4 -1
  23. package/dist/cjs/modules/excel/xlsx/xform/base-xform.js +68 -1
  24. package/dist/cjs/modules/excel/xlsx/xform/core/content-types-xform.js +16 -10
  25. package/dist/cjs/modules/excel/xlsx/xform/pivot-table/pivot-table-xform.js +256 -86
  26. package/dist/cjs/modules/excel/xlsx/xform/sheet/worksheet-xform.js +38 -11
  27. package/dist/cjs/modules/excel/xlsx/xlsx.browser.js +213 -131
  28. package/dist/esm/modules/csv/csv.browser.js +3 -3
  29. package/dist/esm/modules/excel/utils/parse-sax.js +13 -32
  30. package/dist/esm/modules/excel/utils/passthrough-manager.js +129 -0
  31. package/dist/esm/modules/excel/workbook.js +9 -1
  32. package/dist/esm/modules/excel/worksheet.js +4 -1
  33. package/dist/esm/modules/excel/xlsx/xform/base-xform.js +68 -1
  34. package/dist/esm/modules/excel/xlsx/xform/core/content-types-xform.js +16 -10
  35. package/dist/esm/modules/excel/xlsx/xform/pivot-table/pivot-table-xform.js +256 -86
  36. package/dist/esm/modules/excel/xlsx/xform/sheet/worksheet-xform.js +38 -11
  37. package/dist/esm/modules/excel/xlsx/xlsx.browser.js +213 -131
  38. package/dist/iife/excelts.iife.js +571 -267
  39. package/dist/iife/excelts.iife.js.map +1 -1
  40. package/dist/iife/excelts.iife.min.js +25 -52
  41. package/dist/types/modules/csv/csv-core.d.ts +0 -9
  42. package/dist/types/modules/excel/utils/parse-sax.d.ts +0 -3
  43. package/dist/types/modules/excel/utils/passthrough-manager.d.ts +77 -0
  44. package/dist/types/modules/excel/workbook.d.ts +8 -0
  45. package/dist/types/modules/excel/worksheet.d.ts +4 -0
  46. package/dist/types/modules/excel/xlsx/xform/pivot-table/pivot-table-xform.d.ts +34 -11
  47. package/dist/types/modules/excel/xlsx/xlsx.browser.d.ts +36 -1
  48. package/package.json +2 -2
@@ -34,6 +34,7 @@ import { StreamingZip, ZipDeflateFile } from "../../archive/zip/stream.js";
34
34
  import { ZipParser } from "../../archive/unzip/zip-parser.js";
35
35
  import { PassThrough, concatUint8Arrays } from "../../stream/index.browser.js";
36
36
  import { commentsPath, commentsRelTargetFromWorksheetName, ctrlPropPath, drawingPath, drawingRelsPath, OOXML_REL_TARGETS, pivotTableRelTargetFromWorksheetName, pivotCacheDefinitionRelTargetFromWorkbook, getCommentsIndexFromPath, getDrawingNameFromPath, getDrawingNameFromRelsPath, getMediaFilenameFromPath, mediaPath, getPivotCacheDefinitionNameFromPath, getPivotCacheDefinitionNameFromRelsPath, getPivotCacheRecordsNameFromPath, getPivotTableNameFromPath, getPivotTableNameFromRelsPath, pivotCacheDefinitionPath, pivotCacheDefinitionRelsPath, pivotCacheDefinitionRelTargetFromPivotTable, pivotCacheRecordsPath, pivotCacheRecordsRelTarget, pivotTablePath, pivotTableRelsPath, getTableNameFromPath, tablePath, tableRelTargetFromWorksheetName, themePath, getThemeNameFromPath, getVmlDrawingNameFromPath, getWorksheetNoFromWorksheetPath, getWorksheetNoFromWorksheetRelsPath, isBinaryEntryPath, normalizeZipPath, OOXML_PATHS, vmlDrawingRelTargetFromWorksheetName, vmlDrawingPath, worksheetPath, worksheetRelsPath } from "../utils/ooxml-paths.js";
37
+ import { PassthroughManager } from "../utils/passthrough-manager.js";
37
38
  class StreamingZipWriterAdapter {
38
39
  constructor(options) {
39
40
  this.events = new Map();
@@ -201,6 +202,7 @@ class XLSX {
201
202
  this.addDrawings(zip, model);
202
203
  this.addTables(zip, model);
203
204
  this.addPivotTables(zip, model);
205
+ this.addPassthrough(zip, model);
204
206
  await Promise.all([this.addThemes(zip, model), this.addStyles(zip, model)]);
205
207
  await this.addFeaturePropertyBag(zip, model);
206
208
  await this.addMedia(zip, model);
@@ -300,8 +302,12 @@ class XLSX {
300
302
  * This is the foundation for TRUE streaming reads on platforms that have a
301
303
  * streaming ZIP parser (e.g. Node.js `modules/archive` Parse).
302
304
  */
303
- async loadFromZipEntries(entries, options) {
304
- const model = {
305
+ /**
306
+ * Create an empty model for parsing XLSX files.
307
+ * Shared by loadFromZipEntries and loadFromFiles.
308
+ */
309
+ createEmptyModel() {
310
+ return {
305
311
  worksheets: [],
306
312
  worksheetHash: {},
307
313
  worksheetRels: [],
@@ -310,6 +316,8 @@ class XLSX {
310
316
  mediaIndex: {},
311
317
  drawings: {},
312
318
  drawingRels: {},
319
+ // Raw drawing XML data for passthrough (when drawing contains chart references)
320
+ rawDrawings: {},
313
321
  comments: {},
314
322
  tables: {},
315
323
  vmlDrawings: {},
@@ -317,8 +325,104 @@ class XLSX {
317
325
  pivotTableRels: {},
318
326
  pivotCacheDefinitions: {},
319
327
  pivotCacheDefinitionRels: {},
320
- pivotCacheRecords: {}
328
+ pivotCacheRecords: {},
329
+ // Passthrough storage for unknown/unsupported files (charts, etc.)
330
+ passthrough: {}
321
331
  };
332
+ }
333
+ /**
334
+ * Collect all data from a stream into a single Uint8Array.
335
+ * Reusable helper for passthrough and drawing processing.
336
+ */
337
+ async collectStreamData(stream) {
338
+ const chunks = [];
339
+ await new Promise((resolve, reject) => {
340
+ stream.on("data", (chunk) => {
341
+ if (typeof chunk === "string") {
342
+ chunks.push(new TextEncoder().encode(chunk));
343
+ }
344
+ else if (chunk instanceof Uint8Array) {
345
+ chunks.push(chunk);
346
+ }
347
+ else {
348
+ chunks.push(new Uint8Array(chunk));
349
+ }
350
+ });
351
+ stream.on("end", () => resolve());
352
+ stream.on("error", reject);
353
+ });
354
+ return concatUint8Arrays(chunks);
355
+ }
356
+ /**
357
+ * Check if a drawing has chart references in its relationships
358
+ */
359
+ drawingHasChartReference(drawing) {
360
+ return (drawing.rels && drawing.rels.some((rel) => rel.Target && rel.Target.includes("/charts/")));
361
+ }
362
+ /**
363
+ * Check if a drawing rels list references charts.
364
+ * Used to decide whether we need to keep raw drawing XML for passthrough.
365
+ */
366
+ drawingRelsHasChartReference(drawingRels) {
367
+ return (Array.isArray(drawingRels) &&
368
+ drawingRels.some(rel => typeof rel?.Target === "string" && rel.Target.includes("/charts/")));
369
+ }
370
+ /**
371
+ * Process a known OOXML entry (workbook, styles, shared strings, etc.)
372
+ * Returns true if handled, false if should be passed to _processDefaultEntry
373
+ */
374
+ async _processKnownEntry(stream, model, entryName, options) {
375
+ const sheetNo = getWorksheetNoFromWorksheetPath(entryName);
376
+ if (sheetNo !== undefined) {
377
+ await this._processWorksheetEntry(stream, model, sheetNo, options, entryName);
378
+ return true;
379
+ }
380
+ switch (entryName) {
381
+ case OOXML_PATHS.rootRels:
382
+ model.globalRels = await this.parseRels(stream);
383
+ return true;
384
+ case OOXML_PATHS.xlWorkbook: {
385
+ const workbook = await this.parseWorkbook(stream);
386
+ model.sheets = workbook.sheets;
387
+ model.definedNames = workbook.definedNames;
388
+ model.views = workbook.views;
389
+ model.properties = workbook.properties;
390
+ model.calcProperties = workbook.calcProperties;
391
+ model.pivotCaches = workbook.pivotCaches;
392
+ return true;
393
+ }
394
+ case OOXML_PATHS.xlSharedStrings:
395
+ model.sharedStrings = new SharedStringsXform();
396
+ await model.sharedStrings.parseStream(stream);
397
+ return true;
398
+ case OOXML_PATHS.xlWorkbookRels:
399
+ model.workbookRels = await this.parseRels(stream);
400
+ return true;
401
+ case OOXML_PATHS.docPropsApp: {
402
+ const appXform = new AppXform();
403
+ const appProperties = await appXform.parseStream(stream);
404
+ if (appProperties) {
405
+ model.company = appProperties.company;
406
+ model.manager = appProperties.manager;
407
+ }
408
+ return true;
409
+ }
410
+ case OOXML_PATHS.docPropsCore: {
411
+ const coreXform = new CoreXform();
412
+ const coreProperties = await coreXform.parseStream(stream);
413
+ Object.assign(model, coreProperties);
414
+ return true;
415
+ }
416
+ case OOXML_PATHS.xlStyles:
417
+ model.styles = new StylesXform();
418
+ await model.styles.parseStream(stream);
419
+ return true;
420
+ default:
421
+ return false;
422
+ }
423
+ }
424
+ async loadFromZipEntries(entries, options) {
425
+ const model = this.createEmptyModel();
322
426
  for await (const entry of entries) {
323
427
  let drained = false;
324
428
  const drainEntry = async () => {
@@ -335,58 +439,12 @@ class XLSX {
335
439
  const entryName = normalizeZipPath(entry.name);
336
440
  const stream = entry.stream;
337
441
  try {
338
- const sheetNo = getWorksheetNoFromWorksheetPath(entryName);
339
- if (sheetNo !== undefined) {
340
- await this._processWorksheetEntry(stream, model, sheetNo, options, entryName);
341
- continue;
342
- }
343
- switch (entryName) {
344
- case OOXML_PATHS.rootRels:
345
- model.globalRels = await this.parseRels(stream);
346
- break;
347
- case OOXML_PATHS.xlWorkbook: {
348
- const workbook = await this.parseWorkbook(stream);
349
- model.sheets = workbook.sheets;
350
- model.definedNames = workbook.definedNames;
351
- model.views = workbook.views;
352
- model.properties = workbook.properties;
353
- model.calcProperties = workbook.calcProperties;
354
- model.pivotCaches = workbook.pivotCaches;
355
- break;
356
- }
357
- case OOXML_PATHS.xlSharedStrings:
358
- model.sharedStrings = new SharedStringsXform();
359
- await model.sharedStrings.parseStream(stream);
360
- break;
361
- case OOXML_PATHS.xlWorkbookRels:
362
- model.workbookRels = await this.parseRels(stream);
363
- break;
364
- case OOXML_PATHS.docPropsApp: {
365
- const appXform = new AppXform();
366
- const appProperties = await appXform.parseStream(stream);
367
- if (appProperties) {
368
- model.company = appProperties.company;
369
- model.manager = appProperties.manager;
370
- }
371
- break;
372
- }
373
- case OOXML_PATHS.docPropsCore: {
374
- const coreXform = new CoreXform();
375
- const coreProperties = await coreXform.parseStream(stream);
376
- Object.assign(model, coreProperties);
377
- break;
378
- }
379
- case OOXML_PATHS.xlStyles:
380
- model.styles = new StylesXform();
381
- await model.styles.parseStream(stream);
382
- break;
383
- default: {
384
- const handled = await this._processDefaultEntry(stream, model, entryName);
385
- if (!handled) {
386
- // Important for true streaming parsers: always consume unknown entries
387
- await drainEntry();
388
- }
389
- break;
442
+ const handled = await this._processKnownEntry(stream, model, entryName, options);
443
+ if (!handled) {
444
+ const defaultHandled = await this._processDefaultEntry(stream, model, entryName);
445
+ if (!defaultHandled) {
446
+ // Important for true streaming parsers: always consume unknown entries
447
+ await drainEntry();
390
448
  }
391
449
  }
392
450
  }
@@ -500,6 +558,15 @@ class XLSX {
500
558
  drawingXform.reconcile(drawing, drawingOptions);
501
559
  }
502
560
  });
561
+ // Trim raw drawings for non-chart drawings to avoid bloating the serialized workbook model.
562
+ if (model.rawDrawings && model.drawingRels) {
563
+ for (const name of Object.keys(model.rawDrawings)) {
564
+ const drawingRel = model.drawingRels[name];
565
+ if (drawingRel && !this.drawingRelsHasChartReference(drawingRel)) {
566
+ delete model.rawDrawings[name];
567
+ }
568
+ }
569
+ }
503
570
  // reconcile tables with the default styles
504
571
  const tableOptions = {
505
572
  styles: model.styles
@@ -516,6 +583,7 @@ class XLSX {
516
583
  mediaIndex: model.mediaIndex,
517
584
  date1904: model.properties && model.properties.date1904,
518
585
  drawings: model.drawings,
586
+ drawingRels: model.drawingRels,
519
587
  comments: model.comments,
520
588
  tables: model.tables,
521
589
  vmlDrawings: model.vmlDrawings,
@@ -692,9 +760,15 @@ class XLSX {
692
760
  }
693
761
  }
694
762
  async _processDrawingEntry(entry, model, name) {
763
+ // Collect raw data first so we can preserve drawings that reference charts.
764
+ const rawData = await this.collectStreamData(entry);
765
+ // Parse the drawing for normal processing (images, etc.)
695
766
  const xform = new DrawingXform();
696
- const drawing = await xform.parseStream(entry);
767
+ const xmlString = this.bufferToString(rawData);
768
+ const drawing = await xform.parseStream(this.createTextStream(xmlString));
697
769
  model.drawings[name] = drawing;
770
+ // Store raw data; reconcile() may later drop it if charts are not referenced.
771
+ model.rawDrawings[name] = rawData;
698
772
  }
699
773
  async _processDrawingRelsEntry(entry, model, name) {
700
774
  const xform = new RelationshipsXform();
@@ -769,24 +843,7 @@ class XLSX {
769
843
  // loadFromFiles - shared logic for loading from pre-extracted ZIP data
770
844
  // ===========================================================================
771
845
  async loadFromFiles(zipData, options) {
772
- const model = {
773
- worksheets: [],
774
- worksheetHash: {},
775
- worksheetRels: [],
776
- themes: {},
777
- media: [],
778
- mediaIndex: {},
779
- drawings: {},
780
- drawingRels: {},
781
- comments: {},
782
- tables: {},
783
- vmlDrawings: {},
784
- pivotTables: {},
785
- pivotTableRels: {},
786
- pivotCacheDefinitions: {},
787
- pivotCacheDefinitionRels: {},
788
- pivotCacheRecords: {}
789
- };
846
+ const model = this.createEmptyModel();
790
847
  const entries = Object.keys(zipData).map(name => ({
791
848
  name,
792
849
  dir: name.endsWith("/"),
@@ -800,54 +857,10 @@ class XLSX {
800
857
  const stream = isBinaryEntry
801
858
  ? this.createBinaryStream(entry.data)
802
859
  : this.createTextStream(this.bufferToString(entry.data));
803
- const sheetNo = getWorksheetNoFromWorksheetPath(entryName);
804
- if (sheetNo !== undefined) {
805
- await this._processWorksheetEntry(stream, model, sheetNo, options, entryName);
806
- }
807
- else {
808
- switch (entryName) {
809
- case OOXML_PATHS.rootRels:
810
- model.globalRels = await this.parseRels(stream);
811
- break;
812
- case OOXML_PATHS.xlWorkbook: {
813
- const workbook = await this.parseWorkbook(stream);
814
- model.sheets = workbook.sheets;
815
- model.definedNames = workbook.definedNames;
816
- model.views = workbook.views;
817
- model.properties = workbook.properties;
818
- model.calcProperties = workbook.calcProperties;
819
- model.pivotCaches = workbook.pivotCaches;
820
- break;
821
- }
822
- case OOXML_PATHS.xlSharedStrings:
823
- model.sharedStrings = new SharedStringsXform();
824
- await model.sharedStrings.parseStream(stream);
825
- break;
826
- case OOXML_PATHS.xlWorkbookRels:
827
- model.workbookRels = await this.parseRels(stream);
828
- break;
829
- case OOXML_PATHS.docPropsApp: {
830
- const appXform = new AppXform();
831
- const appProperties = await appXform.parseStream(stream);
832
- if (appProperties) {
833
- model.company = appProperties.company;
834
- model.manager = appProperties.manager;
835
- }
836
- break;
837
- }
838
- case OOXML_PATHS.docPropsCore: {
839
- const coreXform = new CoreXform();
840
- const coreProperties = await coreXform.parseStream(stream);
841
- Object.assign(model, coreProperties);
842
- break;
843
- }
844
- case OOXML_PATHS.xlStyles:
845
- model.styles = new StylesXform();
846
- await model.styles.parseStream(stream);
847
- break;
848
- default:
849
- await this._processDefaultEntry(stream, model, entryName);
850
- }
860
+ const handled = await this._processKnownEntry(stream, model, entryName, options);
861
+ if (!handled) {
862
+ // Pass raw entry data for drawings to enable passthrough
863
+ await this._processDefaultEntry(stream, model, entryName, entry.data);
851
864
  }
852
865
  }
853
866
  }
@@ -857,11 +870,11 @@ class XLSX {
857
870
  }
858
871
  /**
859
872
  * Process default entries (drawings, comments, tables, etc.)
873
+ * @param rawData Optional raw entry data for passthrough preservation (used by loadFromFiles)
860
874
  */
861
- async _processDefaultEntry(stream, model, entryName) {
862
- const worksheetRelsSheetNo = getWorksheetNoFromWorksheetRelsPath(entryName);
863
- if (worksheetRelsSheetNo !== undefined) {
864
- const sheetNo = worksheetRelsSheetNo;
875
+ async _processDefaultEntry(stream, model, entryName, rawData) {
876
+ const sheetNo = getWorksheetNoFromWorksheetRelsPath(entryName);
877
+ if (sheetNo !== undefined) {
865
878
  await this._processWorksheetRelsEntry(stream, model, sheetNo);
866
879
  return true;
867
880
  }
@@ -873,6 +886,10 @@ class XLSX {
873
886
  const drawingName = getDrawingNameFromPath(entryName);
874
887
  if (drawingName) {
875
888
  await this._processDrawingEntry(stream, model, drawingName);
889
+ // For loadFromFiles path, store raw data for passthrough (drawings with charts)
890
+ if (rawData) {
891
+ model.rawDrawings[drawingName] = rawData;
892
+ }
876
893
  return true;
877
894
  }
878
895
  const drawingRelsName = getDrawingNameFromRelsPath(entryName);
@@ -927,8 +944,27 @@ class XLSX {
927
944
  await this._processPivotCacheRecordsEntry(stream, model, pivotCacheRecordsName);
928
945
  return true;
929
946
  }
947
+ // Store passthrough files (charts, etc.) for preservation
948
+ if (PassthroughManager.isPassthroughPath(entryName)) {
949
+ // If raw data is available (loadFromFiles path), use it directly
950
+ if (rawData) {
951
+ model.passthrough[entryName] = rawData;
952
+ }
953
+ else {
954
+ await this._processPassthroughEntry(stream, model, entryName);
955
+ }
956
+ return true;
957
+ }
930
958
  return false;
931
959
  }
960
+ /**
961
+ * Store a passthrough file for preservation during read/write cycles.
962
+ * These files are not parsed but stored as raw bytes to be written back unchanged.
963
+ */
964
+ async _processPassthroughEntry(stream, model, entryName) {
965
+ const data = await this.collectStreamData(stream);
966
+ model.passthrough[entryName] = data;
967
+ }
932
968
  // ===========================================================================
933
969
  // Write methods - shared by all platforms
934
970
  // ===========================================================================
@@ -1071,14 +1107,46 @@ class XLSX {
1071
1107
  addDrawings(zip, model) {
1072
1108
  const drawingXform = new DrawingXform();
1073
1109
  const relsXform = new RelationshipsXform();
1110
+ const rawDrawings = model.rawDrawings || {};
1074
1111
  model.worksheets.forEach((worksheet) => {
1075
1112
  const { drawing } = worksheet;
1076
1113
  if (drawing) {
1077
- drawingXform.prepare(drawing);
1078
- let xml = drawingXform.toXml(drawing);
1079
- zip.append(xml, { name: drawingPath(drawing.name) });
1080
- xml = relsXform.toXml(drawing.rels);
1081
- zip.append(xml, { name: drawingRelsPath(drawing.name) });
1114
+ // Check if drawing rels contain chart references using helper
1115
+ const hasChartReference = this.drawingHasChartReference(drawing);
1116
+ if (hasChartReference && rawDrawings[drawing.name]) {
1117
+ // Use raw data for drawings with chart references (passthrough)
1118
+ zip.append(rawDrawings[drawing.name], { name: drawingPath(drawing.name) });
1119
+ }
1120
+ else {
1121
+ // Use regenerated XML for normal drawings (images, shapes)
1122
+ // Filter out invalid anchors (null, undefined, or missing content)
1123
+ const filteredAnchors = (drawing.anchors || []).filter((a) => {
1124
+ if (a == null) {
1125
+ return false;
1126
+ }
1127
+ // Form controls have range.br and shape properties
1128
+ if (a.range?.br && a.shape) {
1129
+ return true;
1130
+ }
1131
+ // One-cell anchors need a valid picture
1132
+ if (!a.br && !a.picture) {
1133
+ return false;
1134
+ }
1135
+ // Two-cell anchors need either picture or shape
1136
+ if (a.br && !a.picture && !a.shape) {
1137
+ return false;
1138
+ }
1139
+ return true;
1140
+ });
1141
+ const drawingForWrite = drawing.anchors
1142
+ ? { ...drawing, anchors: filteredAnchors }
1143
+ : drawing;
1144
+ drawingXform.prepare(drawingForWrite);
1145
+ const xml = drawingXform.toXml(drawingForWrite);
1146
+ zip.append(xml, { name: drawingPath(drawing.name) });
1147
+ }
1148
+ const relsXml = relsXform.toXml(drawing.rels);
1149
+ zip.append(relsXml, { name: drawingRelsPath(drawing.name) });
1082
1150
  }
1083
1151
  });
1084
1152
  }
@@ -1093,6 +1161,15 @@ class XLSX {
1093
1161
  });
1094
1162
  });
1095
1163
  }
1164
+ /**
1165
+ * Write passthrough files (charts, etc.) that were preserved during read.
1166
+ * These files are written back unchanged to preserve unsupported features.
1167
+ */
1168
+ addPassthrough(zip, model) {
1169
+ const passthroughManager = new PassthroughManager();
1170
+ passthroughManager.fromRecord(model.passthrough || {});
1171
+ passthroughManager.writeToZip(zip);
1172
+ }
1096
1173
  addPivotTables(zip, model) {
1097
1174
  if (!model.pivotTables.length) {
1098
1175
  return;
@@ -1185,6 +1262,11 @@ class XLSX {
1185
1262
  });
1186
1263
  // ContentTypesXform expects this flag
1187
1264
  model.hasCheckboxes = model.styles.hasCheckboxes;
1265
+ // Build passthroughContentTypes for ContentTypesXform using PassthroughManager
1266
+ const passthrough = model.passthrough || {};
1267
+ const passthroughManager = new PassthroughManager();
1268
+ passthroughManager.fromRecord(passthrough);
1269
+ model.passthroughContentTypes = passthroughManager.getContentTypes();
1188
1270
  }
1189
1271
  }
1190
1272
  XLSX.RelType = RelType;
@@ -119,7 +119,7 @@ class CSV {
119
119
  const dateFormats = options?.dateFormats ?? DEFAULT_DATE_FORMATS;
120
120
  const map = options?.map ||
121
121
  createDefaultValueMapper(dateFormats, {
122
- decimalSeparator: options?.valueMapperOptions?.decimalSeparator ?? options?.parserOptions?.decimalSeparator
122
+ decimalSeparator: options?.valueMapperOptions?.decimalSeparator
123
123
  });
124
124
  const rows = (0, csv_core_1.parseCsv)(str, options?.parserOptions);
125
125
  for (const row of rows) {
@@ -161,7 +161,7 @@ class CSV {
161
161
  const dateFormats = options?.dateFormats ?? DEFAULT_DATE_FORMATS;
162
162
  const map = options?.map ||
163
163
  createDefaultValueMapper(dateFormats, {
164
- decimalSeparator: options?.valueMapperOptions?.decimalSeparator ?? options?.parserOptions?.decimalSeparator
164
+ decimalSeparator: options?.valueMapperOptions?.decimalSeparator
165
165
  });
166
166
  const parser = new csv_stream_1.CsvParserStream(options?.parserOptions);
167
167
  return new Promise((resolve, reject) => {
@@ -231,7 +231,7 @@ class CSV {
231
231
  const dateFormats = options?.dateFormats ?? DEFAULT_DATE_FORMATS;
232
232
  const map = options?.map ||
233
233
  createDefaultValueMapper(dateFormats, {
234
- decimalSeparator: options?.valueMapperOptions?.decimalSeparator ?? options?.parserOptions?.decimalSeparator
234
+ decimalSeparator: options?.valueMapperOptions?.decimalSeparator
235
235
  });
236
236
  const parser = new csv_stream_1.CsvParserStream(options?.parserOptions);
237
237
  parser.on("data", (row) => worksheet.addRow(row.map(map)));
@@ -146,12 +146,6 @@ const XML_ENTITIES = {
146
146
  quot: '"',
147
147
  apos: "'"
148
148
  };
149
- // HAN CELL namespace prefix normalization
150
- // HAN CELL uses non-standard namespace prefixes (ep:, cp:, dc:, etc.)
151
- // The x: prefix for spreadsheetml is detected dynamically from xmlns declarations
152
- // See: https://github.com/exceljs/exceljs/issues/3014
153
- const HAN_CELL_PREFIXES = /^(ep|cp|dc|dcterms|dcmitype|vt):/;
154
- const SPREADSHEETML_NS = "http://schemas.openxmlformats.org/spreadsheetml/2006/main";
155
149
  // ============================================================================
156
150
  // Parser States
157
151
  // ============================================================================
@@ -212,8 +206,6 @@ class SaxesParser {
212
206
  this.chunkPosition = 0;
213
207
  // Entity storage
214
208
  this.ENTITIES = { ...XML_ENTITIES };
215
- // HAN CELL compatibility: spreadsheetml namespace prefix (e.g., "x")
216
- this.nsPrefix = null;
217
209
  this.trackPosition = opt?.position !== false;
218
210
  this.fileName = opt?.fileName;
219
211
  this.fragment = opt?.fragment ?? false;
@@ -248,14 +240,6 @@ class SaxesParser {
248
240
  this.chunk = "";
249
241
  this.i = 0;
250
242
  this.prevI = 0;
251
- this.nsPrefix = null;
252
- }
253
- // Strip HAN CELL namespace prefixes from element names
254
- stripNsPrefix(name) {
255
- const n = name.replace(HAN_CELL_PREFIXES, "");
256
- return this.nsPrefix && n.startsWith(this.nsPrefix + ":")
257
- ? n.slice(this.nsPrefix.length + 1)
258
- : n;
259
243
  }
260
244
  on(name, handler) {
261
245
  switch (name) {
@@ -671,8 +655,9 @@ class SaxesParser {
671
655
  this.name += charFromCode(c);
672
656
  return;
673
657
  }
658
+ // Tag name complete
674
659
  this.tag = {
675
- name: this.stripNsPrefix(this.name),
660
+ name: this.name,
676
661
  attributes: Object.create(null),
677
662
  isSelfClosing: false
678
663
  };
@@ -1111,7 +1096,11 @@ class SaxesParser {
1111
1096
  openTag() {
1112
1097
  const tag = this.tag;
1113
1098
  tag.isSelfClosing = false;
1114
- this.processAttributes(tag);
1099
+ // Copy attributes from list to object
1100
+ for (const { name, value } of this.attribList) {
1101
+ tag.attributes[name] = value;
1102
+ }
1103
+ this.attribList = [];
1115
1104
  this.openTagHandler?.(tag);
1116
1105
  this.tags.push(tag);
1117
1106
  this.name = "";
@@ -1120,7 +1109,11 @@ class SaxesParser {
1120
1109
  openSelfClosingTag() {
1121
1110
  const tag = this.tag;
1122
1111
  tag.isSelfClosing = true;
1123
- this.processAttributes(tag);
1112
+ // Copy attributes from list to object
1113
+ for (const { name, value } of this.attribList) {
1114
+ tag.attributes[name] = value;
1115
+ }
1116
+ this.attribList = [];
1124
1117
  this.openTagHandler?.(tag);
1125
1118
  this.closeTagHandler?.(tag);
1126
1119
  if (this.tags.length === 0) {
@@ -1129,20 +1122,8 @@ class SaxesParser {
1129
1122
  this.name = "";
1130
1123
  this.state = S_TEXT;
1131
1124
  }
1132
- // Process attributes and detect spreadsheetml namespace prefix
1133
- processAttributes(tag) {
1134
- for (const { name, value } of this.attribList) {
1135
- tag.attributes[name] = value;
1136
- if (name.startsWith("xmlns:") && value === SPREADSHEETML_NS) {
1137
- this.nsPrefix = name.slice(6);
1138
- tag.name = this.stripNsPrefix(tag.name);
1139
- }
1140
- }
1141
- this.attribList = [];
1142
- }
1143
1125
  closeTag() {
1144
- const { tags } = this;
1145
- const name = this.stripNsPrefix(this.name);
1126
+ const { tags, name } = this;
1146
1127
  this.state = S_TEXT;
1147
1128
  this.name = "";
1148
1129
  if (name === "") {