@cj-tech-master/excelts 1.5.0 → 1.6.1

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 (45) hide show
  1. package/dist/browser/excelts.iife.js +1057 -201
  2. package/dist/browser/excelts.iife.js.map +1 -1
  3. package/dist/browser/excelts.iife.min.js +63 -33
  4. package/dist/cjs/doc/column.js +7 -3
  5. package/dist/cjs/doc/pivot-table.js +149 -61
  6. package/dist/cjs/doc/workbook.js +3 -1
  7. package/dist/cjs/doc/worksheet.js +0 -2
  8. package/dist/cjs/stream/xlsx/worksheet-writer.js +1 -1
  9. package/dist/cjs/utils/unzip/zip-parser.js +2 -5
  10. package/dist/cjs/xlsx/xform/book/workbook-xform.js +3 -0
  11. package/dist/cjs/xlsx/xform/core/content-types-xform.js +19 -14
  12. package/dist/cjs/xlsx/xform/pivot-table/cache-field-xform.js +135 -0
  13. package/dist/cjs/xlsx/xform/pivot-table/cache-field.js +7 -4
  14. package/dist/cjs/xlsx/xform/pivot-table/pivot-cache-definition-xform.js +135 -13
  15. package/dist/cjs/xlsx/xform/pivot-table/pivot-cache-records-xform.js +193 -45
  16. package/dist/cjs/xlsx/xform/pivot-table/pivot-table-xform.js +390 -39
  17. package/dist/cjs/xlsx/xform/sheet/cell-xform.js +6 -0
  18. package/dist/cjs/xlsx/xform/sheet/worksheet-xform.js +14 -3
  19. package/dist/cjs/xlsx/xlsx.js +261 -38
  20. package/dist/esm/doc/column.js +7 -3
  21. package/dist/esm/doc/pivot-table.js +150 -62
  22. package/dist/esm/doc/workbook.js +3 -1
  23. package/dist/esm/doc/worksheet.js +0 -2
  24. package/dist/esm/stream/xlsx/worksheet-writer.js +1 -1
  25. package/dist/esm/utils/unzip/zip-parser.js +2 -5
  26. package/dist/esm/xlsx/xform/book/workbook-xform.js +3 -0
  27. package/dist/esm/xlsx/xform/core/content-types-xform.js +19 -14
  28. package/dist/esm/xlsx/xform/pivot-table/cache-field-xform.js +132 -0
  29. package/dist/esm/xlsx/xform/pivot-table/cache-field.js +7 -4
  30. package/dist/esm/xlsx/xform/pivot-table/pivot-cache-definition-xform.js +135 -13
  31. package/dist/esm/xlsx/xform/pivot-table/pivot-cache-records-xform.js +193 -45
  32. package/dist/esm/xlsx/xform/pivot-table/pivot-table-xform.js +390 -39
  33. package/dist/esm/xlsx/xform/sheet/cell-xform.js +6 -0
  34. package/dist/esm/xlsx/xform/sheet/worksheet-xform.js +14 -3
  35. package/dist/esm/xlsx/xlsx.js +261 -38
  36. package/dist/types/doc/column.d.ts +13 -6
  37. package/dist/types/doc/pivot-table.d.ts +135 -9
  38. package/dist/types/doc/workbook.d.ts +2 -0
  39. package/dist/types/index.d.ts +1 -0
  40. package/dist/types/xlsx/xform/pivot-table/cache-field-xform.d.ts +42 -0
  41. package/dist/types/xlsx/xform/pivot-table/pivot-cache-definition-xform.d.ts +45 -6
  42. package/dist/types/xlsx/xform/pivot-table/pivot-cache-records-xform.d.ts +52 -5
  43. package/dist/types/xlsx/xform/pivot-table/pivot-table-xform.d.ts +98 -5
  44. package/dist/types/xlsx/xlsx.d.ts +27 -0
  45. package/package.json +17 -17
@@ -67,6 +67,9 @@ class Column {
67
67
  this.outlineLevel = 0;
68
68
  }
69
69
  }
70
+ /**
71
+ * Get header values as an array (for multi-row header support)
72
+ */
70
73
  get headers() {
71
74
  if (Array.isArray(this._header)) {
72
75
  return this._header;
@@ -77,7 +80,8 @@ class Column {
77
80
  return [];
78
81
  }
79
82
  /**
80
- * Can be a string to set one row high header or an array to set multi-row high header
83
+ * Can be a single value or an array for multi-row headers.
84
+ * Supports any CellValue type including Date, number, string, etc.
81
85
  */
82
86
  get header() {
83
87
  return this._header;
@@ -85,8 +89,8 @@ class Column {
85
89
  set header(value) {
86
90
  if (value !== undefined) {
87
91
  this._header = value;
88
- this.headers.forEach((text, index) => {
89
- this._worksheet.getCell(index + 1, this.number).value = text;
92
+ this.headers.forEach((cellValue, index) => {
93
+ this._worksheet.getCell(index + 1, this.number).value = cellValue;
90
94
  });
91
95
  }
92
96
  else {
@@ -2,25 +2,116 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.makePivotTable = makePivotTable;
4
4
  const utils_js_1 = require("../utils/utils");
5
+ const col_cache_js_1 = require("../utils/col-cache");
5
6
  // TK(2023-10-10): turn this into a class constructor.
7
+ /**
8
+ * Creates a PivotTableSource adapter from a Table object.
9
+ * This allows Tables to be used as pivot table data sources with the same interface as Worksheets.
10
+ */
11
+ function createTableSourceAdapter(table) {
12
+ const tableModel = table.model;
13
+ // Validate that table has headerRow enabled (required for pivot table column names)
14
+ if (tableModel.headerRow === false) {
15
+ throw new Error("Cannot create pivot table from a table without headers. Set headerRow: true on the table.");
16
+ }
17
+ // Validate table has data rows
18
+ if (!tableModel.rows || tableModel.rows.length === 0) {
19
+ throw new Error("Cannot create pivot table from an empty table. Add data rows to the table.");
20
+ }
21
+ const columnNames = tableModel.columns.map(col => col.name);
22
+ // Check for duplicate column names
23
+ const nameSet = new Set();
24
+ for (const name of columnNames) {
25
+ if (nameSet.has(name)) {
26
+ throw new Error(`Duplicate column name "${name}" found in table. Pivot tables require unique column names.`);
27
+ }
28
+ nameSet.add(name);
29
+ }
30
+ // Build the full data array: headers + rows
31
+ const headerRow = [undefined, ...columnNames]; // sparse array starting at index 1
32
+ const dataRows = tableModel.rows.map(row => [undefined, ...row]); // sparse array starting at index 1
33
+ // Calculate the range reference for the table
34
+ const tl = tableModel.tl;
35
+ const startRow = tl.row;
36
+ const startCol = tl.col;
37
+ const endRow = startRow + tableModel.rows.length; // header row + data rows
38
+ const endCol = startCol + columnNames.length - 1;
39
+ const shortRange = col_cache_js_1.colCache.encode(startRow, startCol, endRow, endCol);
40
+ return {
41
+ name: tableModel.name,
42
+ getRow(rowNumber) {
43
+ if (rowNumber === 1) {
44
+ return { values: headerRow };
45
+ }
46
+ const dataIndex = rowNumber - 2; // rowNumber 2 maps to index 0
47
+ if (dataIndex >= 0 && dataIndex < dataRows.length) {
48
+ return { values: dataRows[dataIndex] };
49
+ }
50
+ return { values: [] };
51
+ },
52
+ getColumn(columnNumber) {
53
+ // Validate column number is within bounds
54
+ if (columnNumber < 1 || columnNumber > columnNames.length) {
55
+ return { values: [] };
56
+ }
57
+ // Values should be sparse array with header at index 1, data starting at index 2
58
+ const values = [];
59
+ values[1] = columnNames[columnNumber - 1];
60
+ for (let i = 0; i < tableModel.rows.length; i++) {
61
+ values[i + 2] = tableModel.rows[i][columnNumber - 1];
62
+ }
63
+ return { values };
64
+ },
65
+ getSheetValues() {
66
+ // Return sparse array where index 1 is header row, and subsequent indices are data rows
67
+ const result = [];
68
+ result[1] = headerRow;
69
+ for (let i = 0; i < dataRows.length; i++) {
70
+ result[i + 2] = dataRows[i];
71
+ }
72
+ return result;
73
+ },
74
+ dimensions: { shortRange }
75
+ };
76
+ }
77
+ /**
78
+ * Resolves the data source from the model, supporting both sourceSheet and sourceTable.
79
+ */
80
+ function resolveSource(model) {
81
+ if (model.sourceTable) {
82
+ return createTableSourceAdapter(model.sourceTable);
83
+ }
84
+ // For sourceSheet, it already implements the required interface
85
+ return model.sourceSheet;
86
+ }
6
87
  function makePivotTable(worksheet, model) {
7
88
  // Example `model`:
8
89
  // {
9
- // // Source of data: the entire sheet range is taken,
10
- // // akin to `worksheet1.getSheetValues()`.
11
- // sourceSheet: worksheet1,
90
+ // // Source of data (either sourceSheet OR sourceTable):
91
+ // sourceSheet: worksheet1, // Use entire sheet range
92
+ // // OR
93
+ // sourceTable: table, // Use table data
12
94
  //
13
95
  // // Pivot table fields: values indicate field names;
14
- // // they come from the first row in `worksheet1`.
96
+ // // they come from the first row in `worksheet1` or table column names.
15
97
  // rows: ['A', 'B'],
16
98
  // columns: ['C'],
17
99
  // values: ['E'], // only 1 item possible for now
18
100
  // metric: 'sum', // only 'sum' possible for now
19
101
  // }
20
- validate(worksheet, model);
21
- const { sourceSheet } = model;
22
- const { rows, columns, values } = model;
23
- const cacheFields = makeCacheFields(sourceSheet, [...rows, ...columns]);
102
+ // Validate source exists before trying to resolve it
103
+ if (!model.sourceSheet && !model.sourceTable) {
104
+ throw new Error("Either sourceSheet or sourceTable must be provided.");
105
+ }
106
+ if (model.sourceSheet && model.sourceTable) {
107
+ throw new Error("Cannot specify both sourceSheet and sourceTable. Choose one.");
108
+ }
109
+ // Resolve source first to avoid creating adapter multiple times
110
+ const source = resolveSource(model);
111
+ validate(worksheet, model, source);
112
+ const { rows, values } = model;
113
+ const columns = model.columns ?? [];
114
+ const cacheFields = makeCacheFields(source, [...rows, ...columns]);
24
115
  const nameToIndex = cacheFields.reduce((result, cacheField, index) => {
25
116
  result[cacheField.name] = index;
26
117
  return result;
@@ -28,82 +119,79 @@ function makePivotTable(worksheet, model) {
28
119
  const rowIndices = rows.map(row => nameToIndex[row]);
29
120
  const columnIndices = columns.map(column => nameToIndex[column]);
30
121
  const valueIndices = values.map(value => nameToIndex[value]);
122
+ // Calculate tableNumber based on existing pivot tables (1-indexed)
123
+ const tableNumber = worksheet.workbook.pivotTables.length + 1;
124
+ // Base cache ID starts at 10 (Excel convention), each subsequent table increments
125
+ const BASE_CACHE_ID = 10;
31
126
  // form pivot table object
32
127
  return {
33
- sourceSheet,
128
+ source,
34
129
  rows: rowIndices,
35
130
  columns: columnIndices,
36
131
  values: valueIndices,
37
- metric: "sum",
132
+ metric: model.metric ?? "sum",
38
133
  cacheFields,
39
- // defined in <pivotTableDefinition> of xl/pivotTables/pivotTable1.xml;
40
- // also used in xl/workbook.xml
41
- cacheId: "10"
134
+ // Dynamic cacheId: 10 for first table, 11 for second, etc.
135
+ // Used in <pivotTableDefinition> and xl/workbook.xml
136
+ cacheId: String(BASE_CACHE_ID + tableNumber - 1),
137
+ // Control whether pivot table style overrides worksheet column widths
138
+ // '0' = preserve worksheet column widths (useful for custom sizing)
139
+ // '1' = apply pivot table style width/height (default Excel behavior)
140
+ applyWidthHeightFormats: model.applyWidthHeightFormats ?? "1",
141
+ // Table number for file naming (pivotTable1.xml, pivotTable2.xml, etc.)
142
+ tableNumber
42
143
  };
43
144
  }
44
- function validate(worksheet, model) {
45
- if (worksheet.workbook.pivotTables.length === 1) {
46
- throw new Error("A pivot table was already added. At this time, ExcelTS supports at most one pivot table per file.");
145
+ function validate(_worksheet, model, source) {
146
+ if (model.metric && model.metric !== "sum" && model.metric !== "count") {
147
+ throw new Error('Only the "sum" and "count" metrics are supported at this time.');
47
148
  }
48
- if (model.metric && model.metric !== "sum") {
49
- throw new Error('Only the "sum" metric is supported at this time.');
50
- }
51
- const headerNames = model.sourceSheet.getRow(1).values.slice(1);
52
- const isInHeaderNames = (0, utils_js_1.objectFromProps)(headerNames, true);
53
- for (const name of [...model.rows, ...model.columns, ...model.values]) {
54
- if (!isInHeaderNames[name]) {
55
- throw new Error(`The header name "${name}" was not found in ${model.sourceSheet.name}.`);
149
+ const columns = model.columns ?? [];
150
+ // Get header names from source (already resolved)
151
+ const headerNames = source.getRow(1).values.slice(1);
152
+ // Use Set for O(1) lookup
153
+ const headerNameSet = new Set(headerNames);
154
+ for (const name of [...model.rows, ...columns, ...model.values]) {
155
+ if (!headerNameSet.has(name)) {
156
+ throw new Error(`The header name "${name}" was not found in ${source.name}.`);
56
157
  }
57
158
  }
58
159
  if (!model.rows.length) {
59
160
  throw new Error("No pivot table rows specified.");
60
161
  }
61
- if (!model.columns.length) {
62
- throw new Error("No pivot table columns specified.");
162
+ // Allow empty columns - Excel will use "Values" as column field
163
+ // But can't have multiple values with columns specified
164
+ if (model.values.length < 1) {
165
+ throw new Error("Must have at least one value.");
63
166
  }
64
- if (model.values.length !== 1) {
65
- throw new Error("Exactly 1 value needs to be specified at this time.");
167
+ if (model.values.length > 1 && columns.length > 0) {
168
+ throw new Error("It is currently not possible to have multiple values when columns are specified. Please either supply an empty array for columns or a single value.");
66
169
  }
67
170
  }
68
- function makeCacheFields(worksheet, fieldNamesWithSharedItems) {
171
+ function makeCacheFields(source, fieldNamesWithSharedItems) {
69
172
  // Cache fields are used in pivot tables to reference source data.
70
- //
71
- // Example
72
- // -------
73
- // Turn
74
- //
75
- // `worksheet` sheet values [
76
- // ['A', 'B', 'C', 'D', 'E'],
77
- // ['a1', 'b1', 'c1', 4, 5],
78
- // ['a1', 'b2', 'c1', 4, 5],
79
- // ['a2', 'b1', 'c2', 14, 24],
80
- // ['a2', 'b2', 'c2', 24, 35],
81
- // ['a3', 'b1', 'c3', 34, 45],
82
- // ['a3', 'b2', 'c3', 44, 45]
83
- // ];
84
- // fieldNamesWithSharedItems = ['A', 'B', 'C'];
85
- //
86
- // into
87
- //
88
- // [
89
- // { name: 'A', sharedItems: ['a1', 'a2', 'a3'] },
90
- // { name: 'B', sharedItems: ['b1', 'b2'] },
91
- // { name: 'C', sharedItems: ['c1', 'c2', 'c3'] },
92
- // { name: 'D', sharedItems: null },
93
- // { name: 'E', sharedItems: null }
94
- // ]
95
- const names = worksheet.getRow(1).values;
96
- const nameToHasSharedItems = (0, utils_js_1.objectFromProps)(fieldNamesWithSharedItems, true);
173
+ // Fields in fieldNamesWithSharedItems get their unique values extracted as sharedItems.
174
+ // Other fields (typically numeric) have sharedItems = null.
175
+ const names = source.getRow(1).values;
176
+ // Use Set for O(1) lookup instead of object
177
+ const sharedItemsFields = new Set(fieldNamesWithSharedItems);
97
178
  const aggregate = (columnIndex) => {
98
- const columnValues = worksheet.getColumn(columnIndex).values.splice(2);
99
- const columnValuesAsSet = new Set(columnValues);
100
- return (0, utils_js_1.toSortedArray)(columnValuesAsSet);
179
+ const columnValues = source.getColumn(columnIndex).values;
180
+ // Build unique values set directly, skipping header (index 0,1) and null/undefined
181
+ const uniqueValues = new Set();
182
+ for (let i = 2; i < columnValues.length; i++) {
183
+ const v = columnValues[i];
184
+ if (v !== null && v !== undefined) {
185
+ uniqueValues.add(v);
186
+ }
187
+ }
188
+ return (0, utils_js_1.toSortedArray)(uniqueValues);
101
189
  };
102
- // make result
190
+ // Build result array
103
191
  const result = [];
104
192
  for (const columnIndex of (0, utils_js_1.range)(1, names.length)) {
105
193
  const name = names[columnIndex];
106
- const sharedItems = nameToHasSharedItems[name] ? aggregate(columnIndex) : null;
194
+ const sharedItems = sharedItemsFields.has(name) ? aggregate(columnIndex) : null;
107
195
  result.push({ name, sharedItems });
108
196
  }
109
197
  return result;
@@ -199,7 +199,9 @@ class Workbook {
199
199
  this.views = value.views;
200
200
  this._themes = value.themes;
201
201
  this.media = value.media || [];
202
- this.pivotTables = value.pivotTables || [];
202
+ // Handle pivot tables - either newly created or loaded from file
203
+ // Loaded pivot tables come from loadedPivotTables after reconciliation
204
+ this.pivotTables = value.pivotTables || value.loadedPivotTables || [];
203
205
  }
204
206
  }
205
207
  exports.Workbook = Workbook;
@@ -833,8 +833,6 @@ class Worksheet {
833
833
  // =========================================================================
834
834
  // Pivot Tables
835
835
  addPivotTable(model) {
836
- console.warn(`Warning: Pivot Table support is experimental.
837
- Please leave feedback at https://github.com/excelts/excelts/discussions/2575`);
838
836
  const pivotTable = (0, pivot_table_js_1.makePivotTable)(this, model);
839
837
  this.pivotTables.push(pivotTable);
840
838
  this.workbook.pivotTables.push(pivotTable);
@@ -183,6 +183,7 @@ class WorksheetWriter {
183
183
  this._writeOpenSheetData();
184
184
  }
185
185
  this._writeCloseSheetData();
186
+ this._writeSheetProtection(); // Note: must be after sheetData and before autoFilter
186
187
  this._writeAutoFilter();
187
188
  this._writeMergeCells();
188
189
  // for some reason, Excel can't handle dimensions at the bottom of the file
@@ -190,7 +191,6 @@ class WorksheetWriter {
190
191
  this._writeHyperlinks();
191
192
  this._writeConditionalFormatting();
192
193
  this._writeDataValidations();
193
- this._writeSheetProtection();
194
194
  this._writePageMargins();
195
195
  this._writePageSetup();
196
196
  this._writeBackground();
@@ -181,7 +181,7 @@ function parseZipEntries(data, options = {}) {
181
181
  reader.skip(2); // disk where central dir starts
182
182
  reader.skip(2); // entries on this disk
183
183
  let totalEntries = reader.readUint16(); // total entries
184
- let centralDirSize = reader.readUint32();
184
+ reader.skip(4); // central directory size (unused)
185
185
  let centralDirOffset = reader.readUint32();
186
186
  // Check for ZIP64
187
187
  const zip64LocatorOffset = findZip64EOCDLocator(data, eocdOffset);
@@ -200,15 +200,12 @@ function parseZipEntries(data, options = {}) {
200
200
  zip64Reader.skip(4); // disk number
201
201
  zip64Reader.skip(4); // disk with central dir
202
202
  const zip64TotalEntries = Number(zip64Reader.readBigUint64());
203
- const zip64CentralDirSize = Number(zip64Reader.readBigUint64());
203
+ zip64Reader.skip(8); // central directory size (unused)
204
204
  const zip64CentralDirOffset = Number(zip64Reader.readBigUint64());
205
205
  // Use ZIP64 values if standard values are maxed out
206
206
  if (totalEntries === 0xffff) {
207
207
  totalEntries = zip64TotalEntries;
208
208
  }
209
- if (centralDirSize === 0xffffffff) {
210
- centralDirSize = zip64CentralDirSize;
211
- }
212
209
  if (centralDirOffset === 0xffffffff) {
213
210
  centralDirOffset = zip64CentralDirOffset;
214
211
  }
@@ -133,6 +133,9 @@ class WorkbookXform extends base_xform_js_1.BaseXform {
133
133
  if (this.map.definedNames.model) {
134
134
  this.model.definedNames = this.map.definedNames.model;
135
135
  }
136
+ if (this.map.pivotCaches.model && this.map.pivotCaches.model.length > 0) {
137
+ this.model.pivotCaches = this.map.pivotCaches.model;
138
+ }
136
139
  return false;
137
140
  default:
138
141
  // not quite sure how we get here!
@@ -31,26 +31,31 @@ class ContentTypesXform extends base_xform_js_1.BaseXform {
31
31
  PartName: "/xl/workbook.xml",
32
32
  ContentType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"
33
33
  });
34
- model.worksheets.forEach((worksheet) => {
35
- const name = `/xl/worksheets/sheet${worksheet.id}.xml`;
34
+ model.worksheets.forEach((worksheet, index) => {
35
+ // Use fileIndex if set, otherwise use sequential index (1-based)
36
+ const fileIndex = worksheet.fileIndex || index + 1;
37
+ const name = `/xl/worksheets/sheet${fileIndex}.xml`;
36
38
  xmlStream.leafNode("Override", {
37
39
  PartName: name,
38
40
  ContentType: "application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"
39
41
  });
40
42
  });
41
43
  if ((model.pivotTables || []).length) {
42
- // Note(2023-10-06): assuming at most one pivot table for now.
43
- xmlStream.leafNode("Override", {
44
- PartName: "/xl/pivotCache/pivotCacheDefinition1.xml",
45
- ContentType: "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCacheDefinition+xml"
46
- });
47
- xmlStream.leafNode("Override", {
48
- PartName: "/xl/pivotCache/pivotCacheRecords1.xml",
49
- ContentType: "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCacheRecords+xml"
50
- });
51
- xmlStream.leafNode("Override", {
52
- PartName: "/xl/pivotTables/pivotTable1.xml",
53
- ContentType: "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotTable+xml"
44
+ // Add content types for each pivot table
45
+ (model.pivotTables || []).forEach((pivotTable) => {
46
+ const n = pivotTable.tableNumber;
47
+ xmlStream.leafNode("Override", {
48
+ PartName: `/xl/pivotCache/pivotCacheDefinition${n}.xml`,
49
+ ContentType: "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCacheDefinition+xml"
50
+ });
51
+ xmlStream.leafNode("Override", {
52
+ PartName: `/xl/pivotCache/pivotCacheRecords${n}.xml`,
53
+ ContentType: "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCacheRecords+xml"
54
+ });
55
+ xmlStream.leafNode("Override", {
56
+ PartName: `/xl/pivotTables/pivotTable${n}.xml`,
57
+ ContentType: "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotTable+xml"
58
+ });
54
59
  });
55
60
  }
56
61
  xmlStream.leafNode("Override", {
@@ -0,0 +1,135 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CacheFieldXform = void 0;
4
+ const base_xform_js_1 = require("../base-xform");
5
+ const utils_js_1 = require("../../../utils/utils");
6
+ /**
7
+ * Xform for parsing individual <cacheField> elements within a pivot cache definition.
8
+ *
9
+ * Example XML:
10
+ * ```xml
11
+ * <cacheField name="Category" numFmtId="0">
12
+ * <sharedItems count="3">
13
+ * <s v="A" />
14
+ * <s v="B" />
15
+ * <s v="C" />
16
+ * </sharedItems>
17
+ * </cacheField>
18
+ *
19
+ * <cacheField name="Value" numFmtId="0">
20
+ * <sharedItems containsSemiMixedTypes="0" containsString="0"
21
+ * containsNumber="1" containsInteger="1" minValue="5" maxValue="45" />
22
+ * </cacheField>
23
+ * ```
24
+ */
25
+ class CacheFieldXform extends base_xform_js_1.BaseXform {
26
+ constructor() {
27
+ super();
28
+ this.model = null;
29
+ this.inSharedItems = false;
30
+ }
31
+ get tag() {
32
+ return "cacheField";
33
+ }
34
+ reset() {
35
+ this.model = null;
36
+ this.inSharedItems = false;
37
+ }
38
+ parseOpen(node) {
39
+ const { name, attributes } = node;
40
+ switch (name) {
41
+ case "cacheField":
42
+ // Initialize the model with field name
43
+ this.model = {
44
+ name: (0, utils_js_1.xmlDecode)(attributes.name || ""),
45
+ sharedItems: null
46
+ };
47
+ break;
48
+ case "sharedItems":
49
+ this.inSharedItems = true;
50
+ // Check if this is a numeric field (no string items)
51
+ if (attributes.containsNumber === "1" || attributes.containsInteger === "1") {
52
+ if (this.model) {
53
+ this.model.containsNumber = attributes.containsNumber === "1";
54
+ this.model.containsInteger = attributes.containsInteger === "1";
55
+ if (attributes.minValue !== undefined) {
56
+ this.model.minValue = parseFloat(attributes.minValue);
57
+ }
58
+ if (attributes.maxValue !== undefined) {
59
+ this.model.maxValue = parseFloat(attributes.maxValue);
60
+ }
61
+ // Numeric fields have sharedItems = null
62
+ this.model.sharedItems = null;
63
+ }
64
+ }
65
+ else {
66
+ // String field - initialize sharedItems array if count > 0
67
+ if (this.model) {
68
+ const count = parseInt(attributes.count || "0", 10);
69
+ if (count > 0) {
70
+ this.model.sharedItems = [];
71
+ }
72
+ }
73
+ }
74
+ break;
75
+ case "s":
76
+ // String value in sharedItems
77
+ if (this.inSharedItems && this.model?.sharedItems !== null) {
78
+ // Decode XML entities in the value
79
+ const value = (0, utils_js_1.xmlDecode)(attributes.v || "");
80
+ this.model.sharedItems.push(value);
81
+ }
82
+ break;
83
+ case "n":
84
+ // Numeric value in sharedItems (less common, but possible)
85
+ if (this.inSharedItems && this.model?.sharedItems !== null) {
86
+ const value = parseFloat(attributes.v || "0");
87
+ this.model.sharedItems.push(value);
88
+ }
89
+ break;
90
+ case "b":
91
+ // Boolean value in sharedItems
92
+ if (this.inSharedItems && this.model?.sharedItems !== null) {
93
+ const value = attributes.v === "1";
94
+ this.model.sharedItems.push(value);
95
+ }
96
+ break;
97
+ case "e":
98
+ // Error value in sharedItems
99
+ if (this.inSharedItems && this.model?.sharedItems !== null) {
100
+ const value = `#${attributes.v || "ERROR!"}`;
101
+ this.model.sharedItems.push(value);
102
+ }
103
+ break;
104
+ case "m":
105
+ // Missing/null value in sharedItems
106
+ if (this.inSharedItems && this.model?.sharedItems !== null) {
107
+ this.model.sharedItems.push(null);
108
+ }
109
+ break;
110
+ case "d":
111
+ // Date value in sharedItems
112
+ if (this.inSharedItems && this.model?.sharedItems !== null) {
113
+ const value = new Date(attributes.v || "");
114
+ this.model.sharedItems.push(value);
115
+ }
116
+ break;
117
+ }
118
+ return true;
119
+ }
120
+ parseText(_text) {
121
+ // No text content in cacheField elements
122
+ }
123
+ parseClose(name) {
124
+ switch (name) {
125
+ case "cacheField":
126
+ // End of this cacheField element
127
+ return false;
128
+ case "sharedItems":
129
+ this.inSharedItems = false;
130
+ break;
131
+ }
132
+ return true;
133
+ }
134
+ }
135
+ exports.CacheFieldXform = CacheFieldXform;
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.CacheField = void 0;
4
+ const utils_js_1 = require("../../../utils/utils");
4
5
  class CacheField {
5
6
  constructor({ name, sharedItems }) {
6
7
  // string type
@@ -24,17 +25,19 @@ class CacheField {
24
25
  render() {
25
26
  // PivotCache Field: http://www.datypic.com/sc/ooxml/e-ssml_cacheField-1.html
26
27
  // Shared Items: http://www.datypic.com/sc/ooxml/e-ssml_sharedItems-1.html
28
+ // Escape XML special characters in name attribute
29
+ const escapedName = (0, utils_js_1.xmlEncode)(this.name);
27
30
  // integer types
28
31
  if (this.sharedItems === null) {
29
32
  // TK(2023-07-18): left out attributes... minValue="5" maxValue="45"
30
- return `<cacheField name="${this.name}" numFmtId="0">
33
+ return `<cacheField name="${escapedName}" numFmtId="0">
31
34
  <sharedItems containsSemiMixedTypes="0" containsString="0" containsNumber="1" containsInteger="1" />
32
35
  </cacheField>`;
33
36
  }
34
- // string types
35
- return `<cacheField name="${this.name}" numFmtId="0">
37
+ // string types - escape XML special characters in each shared item value
38
+ return `<cacheField name="${escapedName}" numFmtId="0">
36
39
  <sharedItems count="${this.sharedItems.length}">
37
- ${this.sharedItems.map(item => `<s v="${item}" />`).join("")}
40
+ ${this.sharedItems.map(item => `<s v="${(0, utils_js_1.xmlEncode)(String(item))}" />`).join("")}
38
41
  </sharedItems>
39
42
  </cacheField>`;
40
43
  }