@cj-tech-master/excelts 4.2.2 → 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 (63) 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 +3 -0
  4. package/dist/browser/modules/excel/utils/parse-sax.js +32 -13
  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/core/app-xform.js +3 -3
  12. package/dist/browser/modules/excel/xlsx/xform/core/content-types-xform.js +16 -10
  13. package/dist/browser/modules/excel/xlsx/xform/core/core-xform.js +56 -68
  14. package/dist/browser/modules/excel/xlsx/xform/list-xform.js +8 -10
  15. package/dist/browser/modules/excel/xlsx/xform/pivot-table/pivot-table-xform.d.ts +34 -11
  16. package/dist/browser/modules/excel/xlsx/xform/pivot-table/pivot-table-xform.js +256 -86
  17. package/dist/browser/modules/excel/xlsx/xform/sheet/worksheet-xform.js +38 -11
  18. package/dist/browser/modules/excel/xlsx/xform/strings/shared-string-xform.js +2 -3
  19. package/dist/browser/modules/excel/xlsx/xform/strings/text-xform.js +5 -7
  20. package/dist/browser/modules/excel/xlsx/xlsx.browser.d.ts +36 -1
  21. package/dist/browser/modules/excel/xlsx/xlsx.browser.js +213 -127
  22. package/dist/browser/modules/stream/streams.browser.js +0 -3
  23. package/dist/cjs/modules/csv/csv.browser.js +3 -3
  24. package/dist/cjs/modules/excel/utils/parse-sax.js +32 -13
  25. package/dist/cjs/modules/excel/utils/passthrough-manager.js +133 -0
  26. package/dist/cjs/modules/excel/workbook.js +9 -1
  27. package/dist/cjs/modules/excel/worksheet.js +4 -1
  28. package/dist/cjs/modules/excel/xlsx/xform/core/app-xform.js +3 -3
  29. package/dist/cjs/modules/excel/xlsx/xform/core/content-types-xform.js +16 -10
  30. package/dist/cjs/modules/excel/xlsx/xform/core/core-xform.js +56 -68
  31. package/dist/cjs/modules/excel/xlsx/xform/list-xform.js +8 -10
  32. package/dist/cjs/modules/excel/xlsx/xform/pivot-table/pivot-table-xform.js +256 -86
  33. package/dist/cjs/modules/excel/xlsx/xform/sheet/worksheet-xform.js +38 -11
  34. package/dist/cjs/modules/excel/xlsx/xform/strings/shared-string-xform.js +2 -3
  35. package/dist/cjs/modules/excel/xlsx/xform/strings/text-xform.js +5 -7
  36. package/dist/cjs/modules/excel/xlsx/xlsx.browser.js +213 -127
  37. package/dist/cjs/modules/stream/streams.browser.js +0 -3
  38. package/dist/esm/modules/csv/csv.browser.js +3 -3
  39. package/dist/esm/modules/excel/utils/parse-sax.js +32 -13
  40. package/dist/esm/modules/excel/utils/passthrough-manager.js +129 -0
  41. package/dist/esm/modules/excel/workbook.js +9 -1
  42. package/dist/esm/modules/excel/worksheet.js +4 -1
  43. package/dist/esm/modules/excel/xlsx/xform/core/app-xform.js +3 -3
  44. package/dist/esm/modules/excel/xlsx/xform/core/content-types-xform.js +16 -10
  45. package/dist/esm/modules/excel/xlsx/xform/core/core-xform.js +56 -68
  46. package/dist/esm/modules/excel/xlsx/xform/list-xform.js +8 -10
  47. package/dist/esm/modules/excel/xlsx/xform/pivot-table/pivot-table-xform.js +256 -86
  48. package/dist/esm/modules/excel/xlsx/xform/sheet/worksheet-xform.js +38 -11
  49. package/dist/esm/modules/excel/xlsx/xform/strings/shared-string-xform.js +2 -3
  50. package/dist/esm/modules/excel/xlsx/xform/strings/text-xform.js +5 -7
  51. package/dist/esm/modules/excel/xlsx/xlsx.browser.js +213 -127
  52. package/dist/esm/modules/stream/streams.browser.js +0 -3
  53. package/dist/iife/excelts.iife.js +603 -333
  54. package/dist/iife/excelts.iife.js.map +1 -1
  55. package/dist/iife/excelts.iife.min.js +25 -52
  56. package/dist/types/modules/csv/csv-core.d.ts +0 -9
  57. package/dist/types/modules/excel/utils/parse-sax.d.ts +3 -0
  58. package/dist/types/modules/excel/utils/passthrough-manager.d.ts +77 -0
  59. package/dist/types/modules/excel/workbook.d.ts +8 -0
  60. package/dist/types/modules/excel/worksheet.d.ts +4 -0
  61. package/dist/types/modules/excel/xlsx/xform/pivot-table/pivot-table-xform.d.ts +34 -11
  62. package/dist/types/modules/excel/xlsx/xlsx.browser.d.ts +36 -1
  63. package/package.json +2 -2
@@ -58,15 +58,6 @@ export interface CsvParseOptions {
58
58
  renameHeaders?: boolean;
59
59
  /** Comment character - lines starting with this are ignored */
60
60
  comment?: string;
61
- /**
62
- * Decimal separator used when parsing numbers from CSV (default: ".").
63
- *
64
- * Note: core CSV parsing returns strings; number conversion is handled by higher-level
65
- * consumers (e.g. the default value mapper in the CSV module).
66
- *
67
- * @deprecated Use CsvReadOptions.valueMapperOptions.decimalSeparator instead.
68
- */
69
- decimalSeparator?: "." | ",";
70
61
  /** Maximum number of data rows to parse (excluding header) */
71
62
  maxRows?: number;
72
63
  /** Number of lines to skip at the beginning (before header detection) */
@@ -112,7 +112,7 @@ class CSV {
112
112
  const dateFormats = options?.dateFormats ?? DEFAULT_DATE_FORMATS;
113
113
  const map = options?.map ||
114
114
  createDefaultValueMapper(dateFormats, {
115
- decimalSeparator: options?.valueMapperOptions?.decimalSeparator ?? options?.parserOptions?.decimalSeparator
115
+ decimalSeparator: options?.valueMapperOptions?.decimalSeparator
116
116
  });
117
117
  const rows = parseCsv(str, options?.parserOptions);
118
118
  for (const row of rows) {
@@ -154,7 +154,7 @@ class CSV {
154
154
  const dateFormats = options?.dateFormats ?? DEFAULT_DATE_FORMATS;
155
155
  const map = options?.map ||
156
156
  createDefaultValueMapper(dateFormats, {
157
- decimalSeparator: options?.valueMapperOptions?.decimalSeparator ?? options?.parserOptions?.decimalSeparator
157
+ decimalSeparator: options?.valueMapperOptions?.decimalSeparator
158
158
  });
159
159
  const parser = new CsvParserStream(options?.parserOptions);
160
160
  return new Promise((resolve, reject) => {
@@ -224,7 +224,7 @@ class CSV {
224
224
  const dateFormats = options?.dateFormats ?? DEFAULT_DATE_FORMATS;
225
225
  const map = options?.map ||
226
226
  createDefaultValueMapper(dateFormats, {
227
- decimalSeparator: options?.valueMapperOptions?.decimalSeparator ?? options?.parserOptions?.decimalSeparator
227
+ decimalSeparator: options?.valueMapperOptions?.decimalSeparator
228
228
  });
229
229
  const parser = new CsvParserStream(options?.parserOptions);
230
230
  parser.on("data", (row) => worksheet.addRow(row.map(map)));
@@ -54,6 +54,7 @@ export declare class SaxesParser {
54
54
  private positionAtNewLine;
55
55
  private chunkPosition;
56
56
  ENTITIES: Record<string, string>;
57
+ private nsPrefix;
57
58
  private textHandler?;
58
59
  private openTagHandler?;
59
60
  private closeTagHandler?;
@@ -62,6 +63,7 @@ export declare class SaxesParser {
62
63
  get closed(): boolean;
63
64
  get position(): number;
64
65
  private _init;
66
+ private stripNsPrefix;
65
67
  on(name: "text", handler: TextHandler): void;
66
68
  on(name: "opentag", handler: OpenTagHandler): void;
67
69
  on(name: "closetag", handler: CloseTagHandler): void;
@@ -106,6 +108,7 @@ export declare class SaxesParser {
106
108
  private skipSpaces;
107
109
  private openTag;
108
110
  private openSelfClosingTag;
111
+ private processAttributes;
109
112
  private closeTag;
110
113
  private end;
111
114
  }
@@ -142,6 +142,12 @@ const XML_ENTITIES = {
142
142
  quot: '"',
143
143
  apos: "'"
144
144
  };
145
+ // HAN CELL namespace prefix normalization
146
+ // HAN CELL uses non-standard namespace prefixes (ep:, cp:, dc:, etc.)
147
+ // The x: prefix for spreadsheetml is detected dynamically from xmlns declarations
148
+ // See: https://github.com/exceljs/exceljs/issues/3014
149
+ const HAN_CELL_PREFIXES = /^(ep|cp|dc|dcterms|dcmitype|vt):/;
150
+ const SPREADSHEETML_NS = "http://schemas.openxmlformats.org/spreadsheetml/2006/main";
145
151
  // ============================================================================
146
152
  // Parser States
147
153
  // ============================================================================
@@ -202,6 +208,8 @@ export class SaxesParser {
202
208
  this.chunkPosition = 0;
203
209
  // Entity storage
204
210
  this.ENTITIES = { ...XML_ENTITIES };
211
+ // HAN CELL compatibility: spreadsheetml namespace prefix (e.g., "x")
212
+ this.nsPrefix = null;
205
213
  this.trackPosition = opt?.position !== false;
206
214
  this.fileName = opt?.fileName;
207
215
  this.fragment = opt?.fragment ?? false;
@@ -236,6 +244,14 @@ export class SaxesParser {
236
244
  this.chunk = "";
237
245
  this.i = 0;
238
246
  this.prevI = 0;
247
+ this.nsPrefix = null;
248
+ }
249
+ // Strip HAN CELL namespace prefixes from element names
250
+ stripNsPrefix(name) {
251
+ const n = name.replace(HAN_CELL_PREFIXES, "");
252
+ return this.nsPrefix && n.startsWith(this.nsPrefix + ":")
253
+ ? n.slice(this.nsPrefix.length + 1)
254
+ : n;
239
255
  }
240
256
  on(name, handler) {
241
257
  switch (name) {
@@ -651,9 +667,8 @@ export class SaxesParser {
651
667
  this.name += charFromCode(c);
652
668
  return;
653
669
  }
654
- // Tag name complete
655
670
  this.tag = {
656
- name: this.name,
671
+ name: this.stripNsPrefix(this.name),
657
672
  attributes: Object.create(null),
658
673
  isSelfClosing: false
659
674
  };
@@ -1092,11 +1107,7 @@ export class SaxesParser {
1092
1107
  openTag() {
1093
1108
  const tag = this.tag;
1094
1109
  tag.isSelfClosing = false;
1095
- // Copy attributes from list to object
1096
- for (const { name, value } of this.attribList) {
1097
- tag.attributes[name] = value;
1098
- }
1099
- this.attribList = [];
1110
+ this.processAttributes(tag);
1100
1111
  this.openTagHandler?.(tag);
1101
1112
  this.tags.push(tag);
1102
1113
  this.name = "";
@@ -1105,11 +1116,7 @@ export class SaxesParser {
1105
1116
  openSelfClosingTag() {
1106
1117
  const tag = this.tag;
1107
1118
  tag.isSelfClosing = true;
1108
- // Copy attributes from list to object
1109
- for (const { name, value } of this.attribList) {
1110
- tag.attributes[name] = value;
1111
- }
1112
- this.attribList = [];
1119
+ this.processAttributes(tag);
1113
1120
  this.openTagHandler?.(tag);
1114
1121
  this.closeTagHandler?.(tag);
1115
1122
  if (this.tags.length === 0) {
@@ -1118,8 +1125,20 @@ export class SaxesParser {
1118
1125
  this.name = "";
1119
1126
  this.state = S_TEXT;
1120
1127
  }
1128
+ // Process attributes and detect spreadsheetml namespace prefix
1129
+ processAttributes(tag) {
1130
+ for (const { name, value } of this.attribList) {
1131
+ tag.attributes[name] = value;
1132
+ if (name.startsWith("xmlns:") && value === SPREADSHEETML_NS) {
1133
+ this.nsPrefix = name.slice(6);
1134
+ tag.name = this.stripNsPrefix(tag.name);
1135
+ }
1136
+ }
1137
+ this.attribList = [];
1138
+ }
1121
1139
  closeTag() {
1122
- const { tags, name } = this;
1140
+ const { tags } = this;
1141
+ const name = this.stripNsPrefix(this.name);
1123
1142
  this.state = S_TEXT;
1124
1143
  this.name = "";
1125
1144
  if (name === "") {
@@ -0,0 +1,77 @@
1
+ /**
2
+ * PassthroughManager - Manages passthrough files for round-trip preservation
3
+ *
4
+ * This module handles files that are not fully parsed by the library but need to be
5
+ * preserved during read/write cycles (e.g., charts, sparklines, slicers).
6
+ */
7
+ /**
8
+ * Content type entry for ZIP content types
9
+ */
10
+ export interface PassthroughContentType {
11
+ partName: string;
12
+ contentType: string;
13
+ }
14
+ /**
15
+ * ZIP writer interface for passthrough files
16
+ */
17
+ export interface IPassthroughZipWriter {
18
+ append(data: Uint8Array, options: {
19
+ name: string;
20
+ }): void;
21
+ }
22
+ /**
23
+ * PassthroughManager handles storage and retrieval of passthrough files
24
+ * that need to be preserved during Excel read/write cycles.
25
+ */
26
+ export declare class PassthroughManager {
27
+ private files;
28
+ /**
29
+ * Check if a path should be treated as passthrough
30
+ */
31
+ static isPassthroughPath(path: string): boolean;
32
+ /**
33
+ * Get the content type for a passthrough file path
34
+ * @returns Content type string or undefined if unknown
35
+ */
36
+ static getContentType(path: string): string | undefined;
37
+ /**
38
+ * Add a file to passthrough storage
39
+ */
40
+ add(path: string, data: Uint8Array): void;
41
+ /**
42
+ * Get a file from passthrough storage
43
+ */
44
+ get(path: string): Uint8Array | undefined;
45
+ /**
46
+ * Check if a file exists in passthrough storage
47
+ */
48
+ has(path: string): boolean;
49
+ /**
50
+ * Get all stored paths
51
+ */
52
+ getPaths(): string[];
53
+ /**
54
+ * Get all files as a record (for serialization)
55
+ */
56
+ toRecord(): Record<string, Uint8Array>;
57
+ /**
58
+ * Load files from a record (for deserialization)
59
+ */
60
+ fromRecord(record: Record<string, Uint8Array>): void;
61
+ /**
62
+ * Get content types for all stored files that have known types
63
+ */
64
+ getContentTypes(): PassthroughContentType[];
65
+ /**
66
+ * Write all passthrough files to a ZIP writer
67
+ */
68
+ writeToZip(zip: IPassthroughZipWriter): void;
69
+ /**
70
+ * Clear all stored files
71
+ */
72
+ clear(): void;
73
+ /**
74
+ * Get the number of stored files
75
+ */
76
+ get size(): number;
77
+ }
@@ -0,0 +1,129 @@
1
+ /**
2
+ * PassthroughManager - Manages passthrough files for round-trip preservation
3
+ *
4
+ * This module handles files that are not fully parsed by the library but need to be
5
+ * preserved during read/write cycles (e.g., charts, sparklines, slicers).
6
+ */
7
+ // Pre-compiled regex patterns for content type detection (performance optimization)
8
+ const chartXmlRegex = /^xl\/charts\/chart\d+\.xml$/;
9
+ const chartStyleXmlRegex = /^xl\/charts\/style\d+\.xml$/;
10
+ const chartColorsXmlRegex = /^xl\/charts\/colors\d+\.xml$/;
11
+ /**
12
+ * Content type definitions for passthrough files
13
+ */
14
+ const PASSTHROUGH_CONTENT_TYPES = new Map([
15
+ [chartXmlRegex, "application/vnd.openxmlformats-officedocument.drawingml.chart+xml"],
16
+ [chartStyleXmlRegex, "application/vnd.ms-office.chartstyle+xml"],
17
+ [chartColorsXmlRegex, "application/vnd.ms-office.chartcolorstyle+xml"]
18
+ ]);
19
+ /**
20
+ * Passthrough path prefixes that should be preserved
21
+ */
22
+ const PASSTHROUGH_PREFIXES = ["xl/charts/"];
23
+ /**
24
+ * PassthroughManager handles storage and retrieval of passthrough files
25
+ * that need to be preserved during Excel read/write cycles.
26
+ */
27
+ export class PassthroughManager {
28
+ constructor() {
29
+ this.files = new Map();
30
+ }
31
+ /**
32
+ * Check if a path should be treated as passthrough
33
+ */
34
+ static isPassthroughPath(path) {
35
+ return PASSTHROUGH_PREFIXES.some(prefix => path.startsWith(prefix));
36
+ }
37
+ /**
38
+ * Get the content type for a passthrough file path
39
+ * @returns Content type string or undefined if unknown
40
+ */
41
+ static getContentType(path) {
42
+ // Chart relationships are handled by Default extension="rels"
43
+ if (path.startsWith("xl/charts/_rels/")) {
44
+ return undefined;
45
+ }
46
+ for (const [regex, contentType] of PASSTHROUGH_CONTENT_TYPES) {
47
+ if (regex.test(path)) {
48
+ return contentType;
49
+ }
50
+ }
51
+ return undefined;
52
+ }
53
+ /**
54
+ * Add a file to passthrough storage
55
+ */
56
+ add(path, data) {
57
+ this.files.set(path, data);
58
+ }
59
+ /**
60
+ * Get a file from passthrough storage
61
+ */
62
+ get(path) {
63
+ return this.files.get(path);
64
+ }
65
+ /**
66
+ * Check if a file exists in passthrough storage
67
+ */
68
+ has(path) {
69
+ return this.files.has(path);
70
+ }
71
+ /**
72
+ * Get all stored paths
73
+ */
74
+ getPaths() {
75
+ return [...this.files.keys()];
76
+ }
77
+ /**
78
+ * Get all files as a record (for serialization)
79
+ */
80
+ toRecord() {
81
+ const record = {};
82
+ for (const [path, data] of this.files) {
83
+ record[path] = data;
84
+ }
85
+ return record;
86
+ }
87
+ /**
88
+ * Load files from a record (for deserialization)
89
+ */
90
+ fromRecord(record) {
91
+ this.files.clear();
92
+ for (const [path, data] of Object.entries(record)) {
93
+ this.files.set(path, data);
94
+ }
95
+ }
96
+ /**
97
+ * Get content types for all stored files that have known types
98
+ */
99
+ getContentTypes() {
100
+ const contentTypes = [];
101
+ for (const path of this.files.keys()) {
102
+ const contentType = PassthroughManager.getContentType(path);
103
+ if (contentType) {
104
+ contentTypes.push({ partName: path, contentType });
105
+ }
106
+ }
107
+ return contentTypes;
108
+ }
109
+ /**
110
+ * Write all passthrough files to a ZIP writer
111
+ */
112
+ writeToZip(zip) {
113
+ for (const [path, data] of this.files) {
114
+ zip.append(data, { name: path });
115
+ }
116
+ }
117
+ /**
118
+ * Clear all stored files
119
+ */
120
+ clear() {
121
+ this.files.clear();
122
+ }
123
+ /**
124
+ * Get the number of stored files
125
+ */
126
+ get size() {
127
+ return this.files.size;
128
+ }
129
+ }
@@ -55,6 +55,10 @@ export interface WorkbookModel {
55
55
  /** Loaded pivot tables from file - used during reconciliation */
56
56
  loadedPivotTables?: any[];
57
57
  calcProperties: Partial<CalculationProperties>;
58
+ /** Passthrough files (charts, etc.) preserved for round-trip */
59
+ passthrough?: Record<string, Uint8Array>;
60
+ /** Raw drawing XML data for passthrough (when drawing contains chart references) */
61
+ rawDrawings?: Record<string, Uint8Array>;
58
62
  }
59
63
  declare class Workbook {
60
64
  /**
@@ -94,6 +98,10 @@ declare class Workbook {
94
98
  private _worksheets;
95
99
  private _definedNames;
96
100
  private _themes?;
101
+ /** Passthrough files (charts, etc.) preserved for round-trip */
102
+ private _passthrough;
103
+ /** Raw drawing XML data for passthrough (when drawing contains chart references) */
104
+ private _rawDrawings;
97
105
  private _xlsx?;
98
106
  private _csv?;
99
107
  constructor();
@@ -38,6 +38,8 @@ class Workbook {
38
38
  this.views = [];
39
39
  this.media = [];
40
40
  this.pivotTables = [];
41
+ this._passthrough = {};
42
+ this._rawDrawings = {};
41
43
  this._definedNames = new DefinedNames();
42
44
  }
43
45
  // ===========================================================================
@@ -251,7 +253,9 @@ class Workbook {
251
253
  themes: this._themes,
252
254
  media: this.media,
253
255
  pivotTables: this.pivotTables,
254
- calcProperties: this.calcProperties
256
+ calcProperties: this.calcProperties,
257
+ passthrough: this._passthrough,
258
+ rawDrawings: this._rawDrawings
255
259
  };
256
260
  }
257
261
  set model(value) {
@@ -292,6 +296,10 @@ class Workbook {
292
296
  // Handle pivot tables - either newly created or loaded from file
293
297
  // Loaded pivot tables come from loadedPivotTables after reconciliation
294
298
  this.pivotTables = value.pivotTables || value.loadedPivotTables || [];
299
+ // Preserve passthrough files (charts, etc.) for round-trip preservation
300
+ this._passthrough = value.passthrough || {};
301
+ // Preserve raw drawing data for drawings with chart references
302
+ this._rawDrawings = value.rawDrawings || {};
295
303
  }
296
304
  }
297
305
  // ===========================================================================
@@ -111,6 +111,8 @@ interface WorksheetModel {
111
111
  dimensions?: Range;
112
112
  merges?: string[];
113
113
  mergeCells?: string[];
114
+ /** Loaded drawing data (for charts, etc.) - preserved for round-trip */
115
+ drawing?: any;
114
116
  }
115
117
  declare class Worksheet {
116
118
  private _workbook;
@@ -139,6 +141,8 @@ declare class Worksheet {
139
141
  conditionalFormattings: ConditionalFormattingOptions[];
140
142
  formControls: FormCheckbox[];
141
143
  private _headerRowCount?;
144
+ /** Loaded drawing data (for charts, etc.) - preserved for round-trip */
145
+ private _drawing;
142
146
  constructor(options: WorksheetOptions);
143
147
  get name(): string;
144
148
  set name(name: string | undefined);
@@ -897,7 +897,8 @@ class Worksheet {
897
897
  tables: Object.values(this.tables).map(table => table.model),
898
898
  pivotTables: this.pivotTables,
899
899
  conditionalFormattings: this.conditionalFormattings,
900
- formControls: this.formControls.map(fc => fc.model)
900
+ formControls: this.formControls.map(fc => fc.model),
901
+ drawing: this._drawing
901
902
  };
902
903
  // =================================================
903
904
  // columns
@@ -965,6 +966,8 @@ class Worksheet {
965
966
  this.conditionalFormattings = value.conditionalFormattings;
966
967
  // Form controls are currently write-only (not parsed from XLSX)
967
968
  this.formControls = [];
969
+ // Preserve loaded drawing data (charts, etc.)
970
+ this._drawing = value.drawing;
968
971
  }
969
972
  }
970
973
  export { Worksheet };
@@ -10,7 +10,7 @@ class AppXform extends BaseXform {
10
10
  Company: new StringXform({ tag: "Company" }),
11
11
  Manager: new StringXform({ tag: "Manager" }),
12
12
  HeadingPairs: new AppHeadingPairsXform(),
13
- TitleOfParts: new AppTitlesOfPartsXform()
13
+ TitlesOfParts: new AppTitlesOfPartsXform()
14
14
  };
15
15
  }
16
16
  render(xmlStream, model) {
@@ -20,7 +20,7 @@ class AppXform extends BaseXform {
20
20
  xmlStream.leafNode("DocSecurity", undefined, "0");
21
21
  xmlStream.leafNode("ScaleCrop", undefined, "false");
22
22
  this.map.HeadingPairs.render(xmlStream, model.worksheets);
23
- this.map.TitleOfParts.render(xmlStream, model.worksheets);
23
+ this.map.TitlesOfParts.render(xmlStream, model.worksheets);
24
24
  this.map.Company.render(xmlStream, model.company || "");
25
25
  this.map.Manager.render(xmlStream, model.manager);
26
26
  xmlStream.leafNode("LinksUpToDate", undefined, "false");
@@ -62,7 +62,7 @@ class AppXform extends BaseXform {
62
62
  switch (name) {
63
63
  case "Properties":
64
64
  this.model = {
65
- worksheets: this.map.TitleOfParts.model,
65
+ worksheets: this.map.TitlesOfParts.model,
66
66
  company: this.map.Company.model,
67
67
  manager: this.map.Manager.model
68
68
  };
@@ -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"