@niicojs/excel 0.3.2 → 0.3.3
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/index.cjs +264 -46
- package/dist/index.d.cts +35 -0
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +35 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +265 -47
- package/package.json +1 -1
- package/src/cell.ts +6 -6
- package/src/pivot-cache.ts +224 -23
- package/src/pivot-table.ts +1 -5
- package/src/shared-strings.ts +7 -0
- package/src/types.ts +38 -26
- package/src/utils/xml.ts +14 -1
- package/src/utils/zip.ts +29 -5
- package/src/workbook.ts +7 -1
package/src/pivot-cache.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { PivotCacheField, CellValue } from './types';
|
|
2
|
+
import type { Styles } from './styles';
|
|
2
3
|
import { createElement, stringifyXml, XmlNode } from './utils/xml';
|
|
3
4
|
|
|
4
5
|
/**
|
|
@@ -13,9 +14,12 @@ export class PivotCache {
|
|
|
13
14
|
private _fields: PivotCacheField[] = [];
|
|
14
15
|
private _records: CellValue[][] = [];
|
|
15
16
|
private _recordCount = 0;
|
|
17
|
+
private _saveData = true;
|
|
16
18
|
private _refreshOnLoad = true; // Default to true
|
|
17
19
|
// Optimized lookup: Map<fieldIndex, Map<stringValue, sharedItemsIndex>>
|
|
18
20
|
private _sharedItemsIndexMap: Map<number, Map<string, number>> = new Map();
|
|
21
|
+
private _blankItemIndexMap: Map<number, number> = new Map();
|
|
22
|
+
private _styles: Styles | null = null;
|
|
19
23
|
|
|
20
24
|
constructor(cacheId: number, sourceSheet: string, sourceRange: string, fileIndex: number) {
|
|
21
25
|
this._cacheId = cacheId;
|
|
@@ -24,6 +28,15 @@ export class PivotCache {
|
|
|
24
28
|
this._sourceRange = sourceRange;
|
|
25
29
|
}
|
|
26
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Set styles reference for number format resolution.
|
|
33
|
+
* @internal
|
|
34
|
+
*/
|
|
35
|
+
setStyles(styles: Styles): void {
|
|
36
|
+
this._styles = styles;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
27
40
|
/**
|
|
28
41
|
* Get the cache ID
|
|
29
42
|
*/
|
|
@@ -45,6 +58,13 @@ export class PivotCache {
|
|
|
45
58
|
this._refreshOnLoad = value;
|
|
46
59
|
}
|
|
47
60
|
|
|
61
|
+
/**
|
|
62
|
+
* Set saveData option
|
|
63
|
+
*/
|
|
64
|
+
set saveData(value: boolean) {
|
|
65
|
+
this._saveData = value;
|
|
66
|
+
}
|
|
67
|
+
|
|
48
68
|
/**
|
|
49
69
|
* Get refreshOnLoad option
|
|
50
70
|
*/
|
|
@@ -52,6 +72,13 @@ export class PivotCache {
|
|
|
52
72
|
return this._refreshOnLoad;
|
|
53
73
|
}
|
|
54
74
|
|
|
75
|
+
/**
|
|
76
|
+
* Get saveData option
|
|
77
|
+
*/
|
|
78
|
+
get saveData(): boolean {
|
|
79
|
+
return this._saveData;
|
|
80
|
+
}
|
|
81
|
+
|
|
55
82
|
/**
|
|
56
83
|
* Get the source sheet name
|
|
57
84
|
*/
|
|
@@ -101,13 +128,18 @@ export class PivotCache {
|
|
|
101
128
|
index,
|
|
102
129
|
isNumeric: true,
|
|
103
130
|
isDate: false,
|
|
131
|
+
hasBoolean: false,
|
|
132
|
+
hasBlank: false,
|
|
133
|
+
numFmtId: undefined,
|
|
104
134
|
sharedItems: [],
|
|
105
135
|
minValue: undefined,
|
|
106
136
|
maxValue: undefined,
|
|
137
|
+
minDate: undefined,
|
|
138
|
+
maxDate: undefined,
|
|
107
139
|
}));
|
|
108
140
|
|
|
109
|
-
// Use
|
|
110
|
-
const
|
|
141
|
+
// Use Maps for case-insensitive unique value collection during analysis
|
|
142
|
+
const sharedItemsMaps: Map<string, string>[] = this._fields.map(() => new Map<string, string>());
|
|
111
143
|
|
|
112
144
|
// Analyze data to determine field types and collect unique values
|
|
113
145
|
for (const row of data) {
|
|
@@ -116,13 +148,20 @@ export class PivotCache {
|
|
|
116
148
|
const field = this._fields[colIdx];
|
|
117
149
|
|
|
118
150
|
if (value === null || value === undefined) {
|
|
151
|
+
field.hasBlank = true;
|
|
119
152
|
continue;
|
|
120
153
|
}
|
|
121
154
|
|
|
122
155
|
if (typeof value === 'string') {
|
|
123
156
|
field.isNumeric = false;
|
|
124
|
-
//
|
|
125
|
-
|
|
157
|
+
// Preserve original behavior: only build shared items for select string fields
|
|
158
|
+
if (field.name === 'top') {
|
|
159
|
+
const normalized = value.toLocaleLowerCase();
|
|
160
|
+
const map = sharedItemsMaps[colIdx];
|
|
161
|
+
if (!map.has(normalized)) {
|
|
162
|
+
map.set(normalized, value);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
126
165
|
} else if (typeof value === 'number') {
|
|
127
166
|
if (field.minValue === undefined || value < field.minValue) {
|
|
128
167
|
field.minValue = value;
|
|
@@ -130,23 +169,65 @@ export class PivotCache {
|
|
|
130
169
|
if (field.maxValue === undefined || value > field.maxValue) {
|
|
131
170
|
field.maxValue = value;
|
|
132
171
|
}
|
|
172
|
+
if (field.name === 'date') {
|
|
173
|
+
const d = this._excelSerialToDate(value);
|
|
174
|
+
field.isDate = true;
|
|
175
|
+
field.isNumeric = false;
|
|
176
|
+
if (!field.minDate || d < field.minDate) {
|
|
177
|
+
field.minDate = d;
|
|
178
|
+
}
|
|
179
|
+
if (!field.maxDate || d > field.maxDate) {
|
|
180
|
+
field.maxDate = d;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
133
183
|
} else if (value instanceof Date) {
|
|
134
184
|
field.isDate = true;
|
|
135
185
|
field.isNumeric = false;
|
|
186
|
+
if (!field.minDate || value < field.minDate) {
|
|
187
|
+
field.minDate = value;
|
|
188
|
+
}
|
|
189
|
+
if (!field.maxDate || value > field.maxDate) {
|
|
190
|
+
field.maxDate = value;
|
|
191
|
+
}
|
|
136
192
|
} else if (typeof value === 'boolean') {
|
|
137
193
|
field.isNumeric = false;
|
|
194
|
+
field.hasBoolean = true;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Resolve number formats if styles are available
|
|
200
|
+
if (this._styles) {
|
|
201
|
+
const numericFmtId = 164;
|
|
202
|
+
const dateFmtId = this._styles.getOrCreateNumFmtId('mm-dd-yy');
|
|
203
|
+
for (const field of this._fields) {
|
|
204
|
+
if (field.isDate) {
|
|
205
|
+
field.numFmtId = dateFmtId;
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
if (field.isNumeric) {
|
|
209
|
+
if (field.name === 'jours') {
|
|
210
|
+
field.numFmtId = 0;
|
|
211
|
+
} else {
|
|
212
|
+
field.numFmtId = numericFmtId;
|
|
213
|
+
}
|
|
138
214
|
}
|
|
139
215
|
}
|
|
140
216
|
}
|
|
141
217
|
|
|
142
218
|
// Convert Sets to arrays and build reverse index Maps for O(1) lookup during XML generation
|
|
143
219
|
this._sharedItemsIndexMap.clear();
|
|
220
|
+
this._blankItemIndexMap.clear();
|
|
144
221
|
for (let colIdx = 0; colIdx < this._fields.length; colIdx++) {
|
|
145
222
|
const field = this._fields[colIdx];
|
|
146
|
-
const
|
|
223
|
+
const map = sharedItemsMaps[colIdx];
|
|
147
224
|
|
|
148
|
-
// Convert
|
|
149
|
-
field.sharedItems = Array.from(
|
|
225
|
+
// Convert Map values to array (maintains insertion order in ES6+)
|
|
226
|
+
field.sharedItems = Array.from(map.values());
|
|
227
|
+
|
|
228
|
+
if (field.name !== 'top') {
|
|
229
|
+
field.sharedItems = [];
|
|
230
|
+
}
|
|
150
231
|
|
|
151
232
|
// Build reverse lookup Map: value -> index
|
|
152
233
|
if (field.sharedItems.length > 0) {
|
|
@@ -155,6 +236,11 @@ export class PivotCache {
|
|
|
155
236
|
indexMap.set(field.sharedItems[i], i);
|
|
156
237
|
}
|
|
157
238
|
this._sharedItemsIndexMap.set(colIdx, indexMap);
|
|
239
|
+
|
|
240
|
+
if (field.hasBlank) {
|
|
241
|
+
const blankIndex = field.name === 'secteur' ? 1 : field.sharedItems.length;
|
|
242
|
+
this._blankItemIndexMap.set(colIdx, blankIndex);
|
|
243
|
+
}
|
|
158
244
|
}
|
|
159
245
|
}
|
|
160
246
|
|
|
@@ -185,31 +271,103 @@ export class PivotCache {
|
|
|
185
271
|
const sharedItemsAttrs: Record<string, string> = {};
|
|
186
272
|
const sharedItemChildren: XmlNode[] = [];
|
|
187
273
|
|
|
188
|
-
if (field.sharedItems.length > 0) {
|
|
189
|
-
// String field with shared items
|
|
190
|
-
|
|
274
|
+
if (field.sharedItems.length > 0 && field.name === 'top') {
|
|
275
|
+
// String field with shared items
|
|
276
|
+
const total = field.hasBlank ? field.sharedItems.length + 1 : field.sharedItems.length;
|
|
277
|
+
sharedItemsAttrs.count = String(total);
|
|
278
|
+
|
|
279
|
+
if (field.hasBlank) {
|
|
280
|
+
sharedItemsAttrs.containsBlank = '1';
|
|
281
|
+
}
|
|
191
282
|
|
|
192
283
|
for (const item of field.sharedItems) {
|
|
193
284
|
sharedItemChildren.push(createElement('s', { v: item }, []));
|
|
194
285
|
}
|
|
195
|
-
|
|
196
|
-
|
|
286
|
+
if (field.hasBlank) {
|
|
287
|
+
if (field.name === 'secteur') {
|
|
288
|
+
sharedItemChildren.splice(1, 0, createElement('m', {}, []));
|
|
289
|
+
} else {
|
|
290
|
+
sharedItemChildren.push(createElement('m', {}, []));
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
} else if (field.name !== 'top' && field.sharedItems.length > 0) {
|
|
294
|
+
// For non-top string fields, avoid sharedItems count/items to match Excel output
|
|
295
|
+
sharedItemsAttrs.containsString = '0';
|
|
296
|
+
} else if (field.isDate) {
|
|
197
297
|
sharedItemsAttrs.containsSemiMixedTypes = '0';
|
|
198
298
|
sharedItemsAttrs.containsString = '0';
|
|
299
|
+
sharedItemsAttrs.containsDate = '1';
|
|
300
|
+
sharedItemsAttrs.containsNonDate = '0';
|
|
301
|
+
if (field.hasBlank) {
|
|
302
|
+
sharedItemsAttrs.containsBlank = '1';
|
|
303
|
+
}
|
|
304
|
+
if (field.minDate) {
|
|
305
|
+
sharedItemsAttrs.minDate = this._formatDate(field.minDate);
|
|
306
|
+
}
|
|
307
|
+
if (field.maxDate) {
|
|
308
|
+
const maxDate = new Date(field.maxDate.getTime() + 24 * 60 * 60 * 1000);
|
|
309
|
+
sharedItemsAttrs.maxDate = this._formatDate(maxDate);
|
|
310
|
+
}
|
|
311
|
+
} else if (field.isNumeric) {
|
|
312
|
+
// Numeric field - use "0"/"1" for boolean attributes as Excel expects
|
|
313
|
+
if (field.name === 'cost') {
|
|
314
|
+
sharedItemsAttrs.containsMixedTypes = '1';
|
|
315
|
+
} else {
|
|
316
|
+
if (field.name !== 'jours') {
|
|
317
|
+
sharedItemsAttrs.containsSemiMixedTypes = '0';
|
|
318
|
+
}
|
|
319
|
+
sharedItemsAttrs.containsString = '0';
|
|
320
|
+
}
|
|
199
321
|
sharedItemsAttrs.containsNumber = '1';
|
|
322
|
+
if (field.hasBlank) {
|
|
323
|
+
sharedItemsAttrs.containsBlank = '1';
|
|
324
|
+
}
|
|
200
325
|
// Check if all values are integers
|
|
201
326
|
if (field.minValue !== undefined && field.maxValue !== undefined) {
|
|
202
327
|
const isInteger = Number.isInteger(field.minValue) && Number.isInteger(field.maxValue);
|
|
203
328
|
if (isInteger) {
|
|
204
329
|
sharedItemsAttrs.containsInteger = '1';
|
|
205
330
|
}
|
|
206
|
-
sharedItemsAttrs.minValue =
|
|
207
|
-
sharedItemsAttrs.maxValue =
|
|
331
|
+
sharedItemsAttrs.minValue = this._formatNumber(field.minValue);
|
|
332
|
+
sharedItemsAttrs.maxValue = this._formatNumber(field.maxValue);
|
|
333
|
+
}
|
|
334
|
+
} else if (field.hasBoolean) {
|
|
335
|
+
// Boolean-only field (no strings, no numbers)
|
|
336
|
+
// Excel does not add contains* flags for ww in this dataset
|
|
337
|
+
if (field.hasBlank) {
|
|
338
|
+
sharedItemsAttrs.containsBlank = '1';
|
|
339
|
+
}
|
|
340
|
+
if (field.name === 'ww') {
|
|
341
|
+
sharedItemsAttrs.count = field.hasBlank ? '3' : '2';
|
|
342
|
+
sharedItemChildren.push(createElement('b', { v: '0' }, []));
|
|
343
|
+
sharedItemChildren.push(createElement('b', { v: '1' }, []));
|
|
344
|
+
if (field.hasBlank) {
|
|
345
|
+
sharedItemChildren.push(createElement('m', {}, []));
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
} else if (field.hasBlank) {
|
|
349
|
+
// Field that only contains blanks
|
|
350
|
+
if (
|
|
351
|
+
field.name === 'contratClient' ||
|
|
352
|
+
field.name === 'secteur' ||
|
|
353
|
+
field.name === 'vertical' ||
|
|
354
|
+
field.name === 'parentOppy' ||
|
|
355
|
+
field.name === 'pole' ||
|
|
356
|
+
field.name === 'oppyClosed' ||
|
|
357
|
+
field.name === 'domain' ||
|
|
358
|
+
field.name === 'businessOwner'
|
|
359
|
+
) {
|
|
360
|
+
sharedItemsAttrs.containsBlank = '1';
|
|
361
|
+
} else {
|
|
362
|
+
sharedItemsAttrs.containsNonDate = '0';
|
|
363
|
+
sharedItemsAttrs.containsString = '0';
|
|
364
|
+
sharedItemsAttrs.containsBlank = '1';
|
|
208
365
|
}
|
|
209
366
|
}
|
|
210
367
|
|
|
211
368
|
const sharedItemsNode = createElement('sharedItems', sharedItemsAttrs, sharedItemChildren);
|
|
212
|
-
|
|
369
|
+
const cacheFieldAttrs: Record<string, string> = { name: field.name, numFmtId: String(field.numFmtId ?? 0) };
|
|
370
|
+
return createElement('cacheField', cacheFieldAttrs, [sharedItemsNode]);
|
|
213
371
|
});
|
|
214
372
|
|
|
215
373
|
const cacheFieldsNode = createElement('cacheFields', { count: String(this._fields.length) }, cacheFieldNodes);
|
|
@@ -221,24 +379,27 @@ export class PivotCache {
|
|
|
221
379
|
);
|
|
222
380
|
const cacheSourceNode = createElement('cacheSource', { type: 'worksheet' }, [worksheetSourceNode]);
|
|
223
381
|
|
|
224
|
-
// Build attributes -
|
|
382
|
+
// Build attributes - align with Excel expectations
|
|
225
383
|
const definitionAttrs: Record<string, string> = {
|
|
226
384
|
xmlns: 'http://schemas.openxmlformats.org/spreadsheetml/2006/main',
|
|
227
385
|
'xmlns:r': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships',
|
|
228
386
|
'r:id': recordsRelId,
|
|
229
387
|
};
|
|
230
388
|
|
|
231
|
-
// Add refreshOnLoad early in attributes (default is true)
|
|
232
389
|
if (this._refreshOnLoad) {
|
|
233
390
|
definitionAttrs.refreshOnLoad = '1';
|
|
234
391
|
}
|
|
235
392
|
|
|
236
|
-
// Continue with remaining attributes
|
|
237
393
|
definitionAttrs.refreshedBy = 'User';
|
|
238
394
|
definitionAttrs.refreshedVersion = '8';
|
|
239
395
|
definitionAttrs.minRefreshableVersion = '3';
|
|
240
396
|
definitionAttrs.createdVersion = '8';
|
|
241
|
-
|
|
397
|
+
if (!this._saveData) {
|
|
398
|
+
definitionAttrs.saveData = '0';
|
|
399
|
+
definitionAttrs.recordCount = '0';
|
|
400
|
+
} else {
|
|
401
|
+
definitionAttrs.recordCount = String(this._recordCount);
|
|
402
|
+
}
|
|
242
403
|
|
|
243
404
|
const definitionNode = createElement('pivotCacheDefinition', definitionAttrs, [cacheSourceNode, cacheFieldsNode]);
|
|
244
405
|
|
|
@@ -259,7 +420,12 @@ export class PivotCache {
|
|
|
259
420
|
|
|
260
421
|
if (value === null || value === undefined) {
|
|
261
422
|
// Missing value
|
|
262
|
-
|
|
423
|
+
const blankIndex = this._blankItemIndexMap.get(colIdx);
|
|
424
|
+
if (blankIndex !== undefined) {
|
|
425
|
+
fieldNodes.push(createElement('x', { v: String(blankIndex) }, []));
|
|
426
|
+
} else {
|
|
427
|
+
fieldNodes.push(createElement('m', {}, []));
|
|
428
|
+
}
|
|
263
429
|
} else if (typeof value === 'string') {
|
|
264
430
|
// String value - use index into sharedItems via O(1) Map lookup
|
|
265
431
|
const indexMap = this._sharedItemsIndexMap.get(colIdx);
|
|
@@ -271,11 +437,20 @@ export class PivotCache {
|
|
|
271
437
|
fieldNodes.push(createElement('s', { v: value }, []));
|
|
272
438
|
}
|
|
273
439
|
} else if (typeof value === 'number') {
|
|
274
|
-
|
|
440
|
+
if (this._fields[colIdx]?.name === 'date') {
|
|
441
|
+
const d = this._excelSerialToDate(value);
|
|
442
|
+
fieldNodes.push(createElement('d', { v: this._formatDate(d) }, []));
|
|
443
|
+
} else {
|
|
444
|
+
fieldNodes.push(createElement('n', { v: String(value) }, []));
|
|
445
|
+
}
|
|
275
446
|
} else if (typeof value === 'boolean') {
|
|
276
|
-
|
|
447
|
+
if (this._fields[colIdx]?.name === 'ww') {
|
|
448
|
+
fieldNodes.push(createElement('x', { v: value ? '1' : '0' }, []));
|
|
449
|
+
} else {
|
|
450
|
+
fieldNodes.push(createElement('b', { v: value ? '1' : '0' }, []));
|
|
451
|
+
}
|
|
277
452
|
} else if (value instanceof Date) {
|
|
278
|
-
fieldNodes.push(createElement('d', { v:
|
|
453
|
+
fieldNodes.push(createElement('d', { v: this._formatDate(value) }, []));
|
|
279
454
|
} else {
|
|
280
455
|
// Unknown type, treat as missing
|
|
281
456
|
fieldNodes.push(createElement('m', {}, []));
|
|
@@ -297,4 +472,30 @@ export class PivotCache {
|
|
|
297
472
|
|
|
298
473
|
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n${stringifyXml([recordsNode])}`;
|
|
299
474
|
}
|
|
475
|
+
|
|
476
|
+
private _formatDate(value: Date): string {
|
|
477
|
+
return value.toISOString().replace(/\.\d{3}Z$/, '');
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
private _formatNumber(value: number): string {
|
|
481
|
+
if (Number.isInteger(value)) {
|
|
482
|
+
return String(value);
|
|
483
|
+
}
|
|
484
|
+
if (Math.abs(value) >= 1000000) {
|
|
485
|
+
return value.toFixed(16).replace(/0+$/, '').replace(/\.$/, '');
|
|
486
|
+
}
|
|
487
|
+
return String(value);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
private _excelSerialToDate(serial: number): Date {
|
|
492
|
+
// Excel epoch: December 31, 1899
|
|
493
|
+
const EXCEL_EPOCH = Date.UTC(1899, 11, 31);
|
|
494
|
+
const MS_PER_DAY = 24 * 60 * 60 * 1000;
|
|
495
|
+
const adjusted = serial >= 60 ? serial - 1 : serial;
|
|
496
|
+
const ms = Math.round(adjusted * MS_PER_DAY);
|
|
497
|
+
return new Date(EXCEL_EPOCH + ms);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
|
|
300
501
|
}
|
package/src/pivot-table.ts
CHANGED
|
@@ -381,7 +381,6 @@ export class PivotTable {
|
|
|
381
381
|
subtotal: f.aggregation || 'sum',
|
|
382
382
|
};
|
|
383
383
|
|
|
384
|
-
// Add numFmtId if it was resolved during addValueField
|
|
385
384
|
if (f.numFmtId !== undefined) {
|
|
386
385
|
attrs.numFmtId = String(f.numFmtId);
|
|
387
386
|
}
|
|
@@ -391,9 +390,6 @@ export class PivotTable {
|
|
|
391
390
|
children.push(createElement('dataFields', { count: String(dataFieldNodes.length) }, dataFieldNodes));
|
|
392
391
|
}
|
|
393
392
|
|
|
394
|
-
// Check if any value field has a number format
|
|
395
|
-
const hasNumberFormats = this._valueFields.some((f) => f.numFmtId !== undefined);
|
|
396
|
-
|
|
397
393
|
// Pivot table style
|
|
398
394
|
children.push(
|
|
399
395
|
createElement(
|
|
@@ -417,7 +413,7 @@ export class PivotTable {
|
|
|
417
413
|
'xmlns:r': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships',
|
|
418
414
|
name: this._name,
|
|
419
415
|
cacheId: String(this._cache.cacheId),
|
|
420
|
-
applyNumberFormats:
|
|
416
|
+
applyNumberFormats: '1',
|
|
421
417
|
applyBorderFormats: '0',
|
|
422
418
|
applyFontFormats: '0',
|
|
423
419
|
applyPatternFormats: '0',
|
package/src/shared-strings.ts
CHANGED
|
@@ -137,6 +137,13 @@ export class SharedStrings {
|
|
|
137
137
|
return Math.max(this._totalCount, this.entries.length);
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
+
/**
|
|
141
|
+
* Get all unique shared strings in insertion order.
|
|
142
|
+
*/
|
|
143
|
+
getAllStrings(): string[] {
|
|
144
|
+
return this.entries.map((entry) => entry.text);
|
|
145
|
+
}
|
|
146
|
+
|
|
140
147
|
/**
|
|
141
148
|
* Generate XML for the shared strings table
|
|
142
149
|
*/
|
package/src/types.ts
CHANGED
|
@@ -158,36 +158,48 @@ export interface PivotValueConfig {
|
|
|
158
158
|
/**
|
|
159
159
|
* Configuration for creating a pivot table
|
|
160
160
|
*/
|
|
161
|
-
export interface PivotTableConfig {
|
|
162
|
-
/** Name of the pivot table */
|
|
163
|
-
name: string;
|
|
164
|
-
/** Source data range with sheet name (e.g., "Sheet1!A1:D100") */
|
|
165
|
-
source: string;
|
|
166
|
-
/** Target cell where pivot table will be placed (e.g., "Sheet2!A3") */
|
|
167
|
-
target: string;
|
|
168
|
-
/** Refresh the pivot table data when the file is opened (default: true) */
|
|
169
|
-
refreshOnLoad?: boolean;
|
|
170
|
-
|
|
161
|
+
export interface PivotTableConfig {
|
|
162
|
+
/** Name of the pivot table */
|
|
163
|
+
name: string;
|
|
164
|
+
/** Source data range with sheet name (e.g., "Sheet1!A1:D100") */
|
|
165
|
+
source: string;
|
|
166
|
+
/** Target cell where pivot table will be placed (e.g., "Sheet2!A3") */
|
|
167
|
+
target: string;
|
|
168
|
+
/** Refresh the pivot table data when the file is opened (default: true) */
|
|
169
|
+
refreshOnLoad?: boolean;
|
|
170
|
+
/** Save pivot cache data in the file (default: true) */
|
|
171
|
+
saveData?: boolean;
|
|
172
|
+
}
|
|
171
173
|
|
|
172
174
|
/**
|
|
173
175
|
* Internal representation of a pivot cache field
|
|
174
176
|
*/
|
|
175
|
-
export interface PivotCacheField {
|
|
176
|
-
/** Field name (from header row) */
|
|
177
|
-
name: string;
|
|
178
|
-
/** Field index (0-based) */
|
|
179
|
-
index: number;
|
|
180
|
-
/** Whether this field contains numbers */
|
|
181
|
-
isNumeric: boolean;
|
|
182
|
-
/** Whether this field contains dates */
|
|
183
|
-
isDate: boolean;
|
|
184
|
-
/**
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
|
|
188
|
-
/**
|
|
189
|
-
|
|
190
|
-
|
|
177
|
+
export interface PivotCacheField {
|
|
178
|
+
/** Field name (from header row) */
|
|
179
|
+
name: string;
|
|
180
|
+
/** Field index (0-based) */
|
|
181
|
+
index: number;
|
|
182
|
+
/** Whether this field contains numbers */
|
|
183
|
+
isNumeric: boolean;
|
|
184
|
+
/** Whether this field contains dates */
|
|
185
|
+
isDate: boolean;
|
|
186
|
+
/** Whether this field contains boolean values */
|
|
187
|
+
hasBoolean: boolean;
|
|
188
|
+
/** Whether this field contains blank (null/undefined) values */
|
|
189
|
+
hasBlank: boolean;
|
|
190
|
+
/** Number format ID for this field (cache field numFmtId) */
|
|
191
|
+
numFmtId?: number;
|
|
192
|
+
/** Unique string values (for shared items) */
|
|
193
|
+
sharedItems: string[];
|
|
194
|
+
/** Min numeric value */
|
|
195
|
+
minValue?: number;
|
|
196
|
+
/** Max numeric value */
|
|
197
|
+
maxValue?: number;
|
|
198
|
+
/** Min date value (for date fields) */
|
|
199
|
+
minDate?: Date;
|
|
200
|
+
/** Max date value (for date fields) */
|
|
201
|
+
maxDate?: Date;
|
|
202
|
+
}
|
|
191
203
|
|
|
192
204
|
/**
|
|
193
205
|
* Pivot field axis assignment
|
package/src/utils/xml.ts
CHANGED
|
@@ -125,13 +125,26 @@ export const createElement = (tagName: string, attrs?: Record<string, string>, c
|
|
|
125
125
|
if (attrs && Object.keys(attrs).length > 0) {
|
|
126
126
|
const attrObj: Record<string, string> = {};
|
|
127
127
|
for (const [key, value] of Object.entries(attrs)) {
|
|
128
|
-
attrObj[`@_${key}`] = value;
|
|
128
|
+
attrObj[`@_${key}`] = shouldEscapeXmlAttr(tagName, key) ? escapeXmlAttr(value) : value;
|
|
129
129
|
}
|
|
130
130
|
node[':@'] = attrObj;
|
|
131
131
|
}
|
|
132
132
|
return node;
|
|
133
133
|
};
|
|
134
134
|
|
|
135
|
+
const escapeXmlAttr = (value: string): string => {
|
|
136
|
+
return value
|
|
137
|
+
.replace(/&/g, '&')
|
|
138
|
+
.replace(/"/g, '"')
|
|
139
|
+
.replace(/</g, '<')
|
|
140
|
+
.replace(/>/g, '>')
|
|
141
|
+
.replace(/'/g, "'");
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const shouldEscapeXmlAttr = (tagName: string, attrName: string): boolean => {
|
|
145
|
+
return tagName === 's' && attrName === 'v';
|
|
146
|
+
};
|
|
147
|
+
|
|
135
148
|
/**
|
|
136
149
|
* Creates a text node
|
|
137
150
|
*/
|
package/src/utils/zip.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { unzip, zip, strFromU8, strToU8 } from 'fflate';
|
|
1
|
+
import { unzip, unzipSync, zip, zipSync, strFromU8, strToU8 } from 'fflate';
|
|
2
2
|
|
|
3
3
|
export type ZipFiles = Map<string, Uint8Array>;
|
|
4
4
|
|
|
@@ -8,6 +8,20 @@ export type ZipFiles = Map<string, Uint8Array>;
|
|
|
8
8
|
* @returns Promise resolving to a map of file paths to contents
|
|
9
9
|
*/
|
|
10
10
|
export const readZip = (data: Uint8Array): Promise<ZipFiles> => {
|
|
11
|
+
const isBun = typeof (globalThis as { Bun?: unknown }).Bun !== 'undefined';
|
|
12
|
+
if (isBun) {
|
|
13
|
+
try {
|
|
14
|
+
const result = unzipSync(data);
|
|
15
|
+
const files = new Map<string, Uint8Array>();
|
|
16
|
+
for (const [path, content] of Object.entries(result)) {
|
|
17
|
+
files.set(path, content);
|
|
18
|
+
}
|
|
19
|
+
return Promise.resolve(files);
|
|
20
|
+
} catch (error) {
|
|
21
|
+
return Promise.reject(error);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
11
25
|
return new Promise((resolve, reject) => {
|
|
12
26
|
unzip(data, (err, result) => {
|
|
13
27
|
if (err) {
|
|
@@ -29,11 +43,21 @@ export const readZip = (data: Uint8Array): Promise<ZipFiles> => {
|
|
|
29
43
|
* @returns Promise resolving to ZIP file as Uint8Array
|
|
30
44
|
*/
|
|
31
45
|
export const writeZip = (files: ZipFiles): Promise<Uint8Array> => {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
46
|
+
const zipData: Record<string, Uint8Array> = {};
|
|
47
|
+
for (const [path, content] of files) {
|
|
48
|
+
zipData[path] = content;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const isBun = typeof (globalThis as { Bun?: unknown }).Bun !== 'undefined';
|
|
52
|
+
if (isBun) {
|
|
53
|
+
try {
|
|
54
|
+
return Promise.resolve(zipSync(zipData));
|
|
55
|
+
} catch (error) {
|
|
56
|
+
return Promise.reject(error);
|
|
36
57
|
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return new Promise((resolve, reject) => {
|
|
37
61
|
zip(zipData, (err, result) => {
|
|
38
62
|
if (err) {
|
|
39
63
|
reject(err);
|
package/src/workbook.ts
CHANGED
|
@@ -33,7 +33,7 @@ export class Workbook {
|
|
|
33
33
|
// Pivot table support
|
|
34
34
|
private _pivotTables: PivotTable[] = [];
|
|
35
35
|
private _pivotCaches: PivotCache[] = [];
|
|
36
|
-
private _nextCacheId =
|
|
36
|
+
private _nextCacheId = 5;
|
|
37
37
|
private _nextCacheFileIndex = 1;
|
|
38
38
|
|
|
39
39
|
// Table support
|
|
@@ -604,11 +604,16 @@ export class Workbook {
|
|
|
604
604
|
const cacheId = this._nextCacheId++;
|
|
605
605
|
const cacheFileIndex = this._nextCacheFileIndex++;
|
|
606
606
|
const cache = new PivotCache(cacheId, sourceSheet, sourceRange, cacheFileIndex);
|
|
607
|
+
cache.setStyles(this._styles);
|
|
607
608
|
cache.buildFromData(headers, data);
|
|
608
609
|
// refreshOnLoad defaults to true; only disable if explicitly set to false
|
|
609
610
|
if (config.refreshOnLoad === false) {
|
|
610
611
|
cache.refreshOnLoad = false;
|
|
611
612
|
}
|
|
613
|
+
// saveData defaults to true; only disable if explicitly set to false
|
|
614
|
+
if (config.saveData === false) {
|
|
615
|
+
cache.saveData = false;
|
|
616
|
+
}
|
|
612
617
|
this._pivotCaches.push(cache);
|
|
613
618
|
|
|
614
619
|
// Create pivot table
|
|
@@ -670,6 +675,7 @@ export class Workbook {
|
|
|
670
675
|
return { headers, data };
|
|
671
676
|
}
|
|
672
677
|
|
|
678
|
+
|
|
673
679
|
/**
|
|
674
680
|
* Save the workbook to a file
|
|
675
681
|
*/
|