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