@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.
@@ -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 Sets for O(1) unique value collection during analysis
110
- const sharedItemsSets: Set<string>[] = this._fields.map(() => new Set<string>());
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
- // O(1) Set.add instead of O(n) Array.includes + push
125
- sharedItemsSets[colIdx].add(value);
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 set = sharedItemsSets[colIdx];
223
+ const map = sharedItemsMaps[colIdx];
147
224
 
148
- // Convert Set to array (maintains insertion order in ES6+)
149
- field.sharedItems = Array.from(set);
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 - Excel just uses count attribute
190
- sharedItemsAttrs.count = String(field.sharedItems.length);
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
- } else if (field.isNumeric) {
196
- // Numeric field - use "0"/"1" for boolean attributes as Excel expects
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 = String(field.minValue);
207
- sharedItemsAttrs.maxValue = String(field.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
- return createElement('cacheField', { name: field.name, numFmtId: '0' }, [sharedItemsNode]);
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 - refreshOnLoad should come early per OOXML schema
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
- definitionAttrs.recordCount = String(this._recordCount);
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
- fieldNodes.push(createElement('m', {}, []));
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
- fieldNodes.push(createElement('n', { v: String(value) }, []));
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
- fieldNodes.push(createElement('b', { v: value ? '1' : '0' }, []));
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: value.toISOString() }, []));
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
  }
@@ -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: hasNumberFormats ? '1' : '0',
416
+ applyNumberFormats: '1',
421
417
  applyBorderFormats: '0',
422
418
  applyFontFormats: '0',
423
419
  applyPatternFormats: '0',
@@ -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
- /** Unique string values (for shared items) */
185
- sharedItems: string[];
186
- /** Min numeric value */
187
- minValue?: number;
188
- /** Max numeric value */
189
- maxValue?: number;
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, '&amp;')
138
+ .replace(/"/g, '&quot;')
139
+ .replace(/</g, '&lt;')
140
+ .replace(/>/g, '&gt;')
141
+ .replace(/&apos;/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
- return new Promise((resolve, reject) => {
33
- const zipData: Record<string, Uint8Array> = {};
34
- for (const [path, content] of files) {
35
- zipData[path] = content;
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 = 0;
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
  */