@niicojs/excel 0.1.0

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 ADDED
@@ -0,0 +1,2894 @@
1
+ Object.defineProperty(exports, '__esModule', { value: true });
2
+
3
+ var promises = require('fs/promises');
4
+ var fastXmlParser = require('fast-xml-parser');
5
+ var fflate = require('fflate');
6
+
7
+ /**
8
+ * Converts a column index (0-based) to Excel column letters (A, B, ..., Z, AA, AB, ...)
9
+ * @param col - 0-based column index
10
+ * @returns Column letter(s)
11
+ */ const colToLetter = (col)=>{
12
+ let result = '';
13
+ let n = col;
14
+ while(n >= 0){
15
+ result = String.fromCharCode(n % 26 + 65) + result;
16
+ n = Math.floor(n / 26) - 1;
17
+ }
18
+ return result;
19
+ };
20
+ /**
21
+ * Converts Excel column letters to a 0-based column index
22
+ * @param letters - Column letter(s) like 'A', 'B', 'AA'
23
+ * @returns 0-based column index
24
+ */ const letterToCol = (letters)=>{
25
+ const upper = letters.toUpperCase();
26
+ let col = 0;
27
+ for(let i = 0; i < upper.length; i++){
28
+ col = col * 26 + (upper.charCodeAt(i) - 64);
29
+ }
30
+ return col - 1;
31
+ };
32
+ /**
33
+ * Parses an Excel cell address (e.g., 'A1', '$B$2') to row/col indices
34
+ * @param address - Cell address string
35
+ * @returns CellAddress with 0-based row and col
36
+ */ const parseAddress = (address)=>{
37
+ // Remove $ signs for absolute references
38
+ const clean = address.replace(/\$/g, '');
39
+ const match = clean.match(/^([A-Z]+)(\d+)$/i);
40
+ if (!match) {
41
+ throw new Error(`Invalid cell address: ${address}`);
42
+ }
43
+ const col = letterToCol(match[1].toUpperCase());
44
+ const row = parseInt(match[2], 10) - 1; // Convert to 0-based
45
+ return {
46
+ row,
47
+ col
48
+ };
49
+ };
50
+ /**
51
+ * Converts row/col indices to an Excel cell address
52
+ * @param row - 0-based row index
53
+ * @param col - 0-based column index
54
+ * @returns Cell address string like 'A1'
55
+ */ const toAddress = (row, col)=>{
56
+ return `${colToLetter(col)}${row + 1}`;
57
+ };
58
+ /**
59
+ * Parses an Excel range (e.g., 'A1:B10') to start/end addresses
60
+ * @param range - Range string
61
+ * @returns RangeAddress with start and end
62
+ */ const parseRange = (range)=>{
63
+ const parts = range.split(':');
64
+ if (parts.length === 1) {
65
+ // Single cell range
66
+ const addr = parseAddress(parts[0]);
67
+ return {
68
+ start: addr,
69
+ end: addr
70
+ };
71
+ }
72
+ if (parts.length !== 2) {
73
+ throw new Error(`Invalid range: ${range}`);
74
+ }
75
+ return {
76
+ start: parseAddress(parts[0]),
77
+ end: parseAddress(parts[1])
78
+ };
79
+ };
80
+ /**
81
+ * Converts a RangeAddress to a range string
82
+ * @param range - RangeAddress object
83
+ * @returns Range string like 'A1:B10'
84
+ */ const toRange = (range)=>{
85
+ const start = toAddress(range.start.row, range.start.col);
86
+ const end = toAddress(range.end.row, range.end.col);
87
+ if (start === end) {
88
+ return start;
89
+ }
90
+ return `${start}:${end}`;
91
+ };
92
+ /**
93
+ * Normalizes a range so start is always top-left and end is bottom-right
94
+ */ const normalizeRange = (range)=>{
95
+ return {
96
+ start: {
97
+ row: Math.min(range.start.row, range.end.row),
98
+ col: Math.min(range.start.col, range.end.col)
99
+ },
100
+ end: {
101
+ row: Math.max(range.start.row, range.end.row),
102
+ col: Math.max(range.start.col, range.end.col)
103
+ }
104
+ };
105
+ };
106
+
107
+ // Excel epoch: December 30, 1899 (accounting for the 1900 leap year bug)
108
+ const EXCEL_EPOCH = new Date(Date.UTC(1899, 11, 30));
109
+ const MS_PER_DAY = 24 * 60 * 60 * 1000;
110
+ // Excel error types
111
+ const ERROR_TYPES = new Set([
112
+ '#NULL!',
113
+ '#DIV/0!',
114
+ '#VALUE!',
115
+ '#REF!',
116
+ '#NAME?',
117
+ '#NUM!',
118
+ '#N/A',
119
+ '#GETTING_DATA'
120
+ ]);
121
+ /**
122
+ * Represents a single cell in a worksheet
123
+ */ class Cell {
124
+ constructor(worksheet, row, col, data){
125
+ this._dirty = false;
126
+ this._worksheet = worksheet;
127
+ this._row = row;
128
+ this._col = col;
129
+ this._data = data || {};
130
+ }
131
+ /**
132
+ * Get the cell address (e.g., 'A1')
133
+ */ get address() {
134
+ return toAddress(this._row, this._col);
135
+ }
136
+ /**
137
+ * Get the 0-based row index
138
+ */ get row() {
139
+ return this._row;
140
+ }
141
+ /**
142
+ * Get the 0-based column index
143
+ */ get col() {
144
+ return this._col;
145
+ }
146
+ /**
147
+ * Get the cell type
148
+ */ get type() {
149
+ const t = this._data.t;
150
+ if (!t && this._data.v === undefined && !this._data.f) {
151
+ return 'empty';
152
+ }
153
+ switch(t){
154
+ case 'n':
155
+ return 'number';
156
+ case 's':
157
+ case 'str':
158
+ return 'string';
159
+ case 'b':
160
+ return 'boolean';
161
+ case 'e':
162
+ return 'error';
163
+ case 'd':
164
+ return 'date';
165
+ default:
166
+ // If no type but has value, infer from value
167
+ if (typeof this._data.v === 'number') return 'number';
168
+ if (typeof this._data.v === 'string') return 'string';
169
+ if (typeof this._data.v === 'boolean') return 'boolean';
170
+ return 'empty';
171
+ }
172
+ }
173
+ /**
174
+ * Get the cell value
175
+ */ get value() {
176
+ const t = this._data.t;
177
+ const v = this._data.v;
178
+ if (v === undefined && !this._data.f) {
179
+ return null;
180
+ }
181
+ switch(t){
182
+ case 'n':
183
+ return typeof v === 'number' ? v : parseFloat(String(v));
184
+ case 's':
185
+ // Shared string reference
186
+ if (typeof v === 'number') {
187
+ return this._worksheet.workbook.sharedStrings.getString(v) ?? '';
188
+ }
189
+ return String(v);
190
+ case 'str':
191
+ // Inline string
192
+ return String(v);
193
+ case 'b':
194
+ return v === 1 || v === '1' || v === true;
195
+ case 'e':
196
+ return {
197
+ error: String(v)
198
+ };
199
+ case 'd':
200
+ // ISO 8601 date string
201
+ return new Date(String(v));
202
+ default:
203
+ // No type specified - try to infer
204
+ if (typeof v === 'number') {
205
+ // Check if this might be a date based on number format
206
+ if (this._isDateFormat()) {
207
+ return this._excelDateToJs(v);
208
+ }
209
+ return v;
210
+ }
211
+ if (typeof v === 'string') {
212
+ if (ERROR_TYPES.has(v)) {
213
+ return {
214
+ error: v
215
+ };
216
+ }
217
+ return v;
218
+ }
219
+ if (typeof v === 'boolean') return v;
220
+ return null;
221
+ }
222
+ }
223
+ /**
224
+ * Set the cell value
225
+ */ set value(val) {
226
+ this._dirty = true;
227
+ if (val === null || val === undefined) {
228
+ this._data.v = undefined;
229
+ this._data.t = undefined;
230
+ this._data.f = undefined;
231
+ return;
232
+ }
233
+ if (typeof val === 'number') {
234
+ this._data.v = val;
235
+ this._data.t = 'n';
236
+ } else if (typeof val === 'string') {
237
+ // Store as shared string
238
+ const index = this._worksheet.workbook.sharedStrings.addString(val);
239
+ this._data.v = index;
240
+ this._data.t = 's';
241
+ } else if (typeof val === 'boolean') {
242
+ this._data.v = val ? 1 : 0;
243
+ this._data.t = 'b';
244
+ } else if (val instanceof Date) {
245
+ // Store as ISO date string with 'd' type
246
+ this._data.v = val.toISOString();
247
+ this._data.t = 'd';
248
+ } else if ('error' in val) {
249
+ this._data.v = val.error;
250
+ this._data.t = 'e';
251
+ }
252
+ // Clear formula when setting value directly
253
+ this._data.f = undefined;
254
+ }
255
+ /**
256
+ * Write a 2D array of values starting at this cell
257
+ */ set values(data) {
258
+ for(let r = 0; r < data.length; r++){
259
+ const row = data[r];
260
+ for(let c = 0; c < row.length; c++){
261
+ const cell = this._worksheet.cell(this._row + r, this._col + c);
262
+ cell.value = row[c];
263
+ }
264
+ }
265
+ }
266
+ /**
267
+ * Get the formula (without leading '=')
268
+ */ get formula() {
269
+ return this._data.f;
270
+ }
271
+ /**
272
+ * Set the formula (without leading '=')
273
+ */ set formula(f) {
274
+ this._dirty = true;
275
+ if (f === undefined) {
276
+ this._data.f = undefined;
277
+ } else {
278
+ // Remove leading '=' if present
279
+ this._data.f = f.startsWith('=') ? f.slice(1) : f;
280
+ }
281
+ }
282
+ /**
283
+ * Get the formatted text (as displayed in Excel)
284
+ */ get text() {
285
+ if (this._data.w) {
286
+ return this._data.w;
287
+ }
288
+ const val = this.value;
289
+ if (val === null) return '';
290
+ if (typeof val === 'object' && 'error' in val) return val.error;
291
+ if (val instanceof Date) return val.toISOString().split('T')[0];
292
+ return String(val);
293
+ }
294
+ /**
295
+ * Get the style index
296
+ */ get styleIndex() {
297
+ return this._data.s;
298
+ }
299
+ /**
300
+ * Set the style index
301
+ */ set styleIndex(index) {
302
+ this._dirty = true;
303
+ this._data.s = index;
304
+ }
305
+ /**
306
+ * Get the cell style
307
+ */ get style() {
308
+ if (this._data.s === undefined) {
309
+ return {};
310
+ }
311
+ return this._worksheet.workbook.styles.getStyle(this._data.s);
312
+ }
313
+ /**
314
+ * Set the cell style (merges with existing)
315
+ */ set style(style) {
316
+ this._dirty = true;
317
+ const currentStyle = this.style;
318
+ const merged = {
319
+ ...currentStyle,
320
+ ...style
321
+ };
322
+ this._data.s = this._worksheet.workbook.styles.createStyle(merged);
323
+ }
324
+ /**
325
+ * Check if cell has been modified
326
+ */ get dirty() {
327
+ return this._dirty;
328
+ }
329
+ /**
330
+ * Get internal cell data
331
+ */ get data() {
332
+ return this._data;
333
+ }
334
+ /**
335
+ * Check if this cell has a date number format
336
+ */ _isDateFormat() {
337
+ // TODO: Check actual number format from styles
338
+ // For now, return false - dates should be explicitly typed
339
+ return false;
340
+ }
341
+ /**
342
+ * Convert Excel serial date to JavaScript Date
343
+ * Used when reading dates stored as numbers with date formats
344
+ */ _excelDateToJs(serial) {
345
+ // Excel incorrectly considers 1900 a leap year
346
+ // Dates after Feb 28, 1900 need adjustment
347
+ const adjusted = serial > 60 ? serial - 1 : serial;
348
+ const ms = Math.round((adjusted - 1) * MS_PER_DAY);
349
+ return new Date(EXCEL_EPOCH.getTime() + ms);
350
+ }
351
+ /**
352
+ * Convert JavaScript Date to Excel serial date
353
+ * Used when writing dates as numbers for Excel compatibility
354
+ */ _jsDateToExcel(date) {
355
+ const ms = date.getTime() - EXCEL_EPOCH.getTime();
356
+ let serial = ms / MS_PER_DAY + 1;
357
+ // Account for Excel's 1900 leap year bug
358
+ if (serial > 60) {
359
+ serial += 1;
360
+ }
361
+ return serial;
362
+ }
363
+ }
364
+ /**
365
+ * Parse a cell address or row/col to get row and col indices
366
+ */ const parseCellRef = (rowOrAddress, col)=>{
367
+ if (typeof rowOrAddress === 'string') {
368
+ return parseAddress(rowOrAddress);
369
+ }
370
+ if (col === undefined) {
371
+ throw new Error('Column must be provided when row is a number');
372
+ }
373
+ return {
374
+ row: rowOrAddress,
375
+ col
376
+ };
377
+ };
378
+
379
+ var _computedKey;
380
+ _computedKey = Symbol.iterator;
381
+ /**
382
+ * Represents a range of cells in a worksheet
383
+ */ class Range {
384
+ constructor(worksheet, range){
385
+ this._worksheet = worksheet;
386
+ this._range = normalizeRange(range);
387
+ }
388
+ /**
389
+ * Get the range address as a string
390
+ */ get address() {
391
+ const start = toAddress(this._range.start.row, this._range.start.col);
392
+ const end = toAddress(this._range.end.row, this._range.end.col);
393
+ if (start === end) return start;
394
+ return `${start}:${end}`;
395
+ }
396
+ /**
397
+ * Get the number of rows in the range
398
+ */ get rowCount() {
399
+ return this._range.end.row - this._range.start.row + 1;
400
+ }
401
+ /**
402
+ * Get the number of columns in the range
403
+ */ get colCount() {
404
+ return this._range.end.col - this._range.start.col + 1;
405
+ }
406
+ /**
407
+ * Get all values in the range as a 2D array
408
+ */ get values() {
409
+ const result = [];
410
+ for(let r = this._range.start.row; r <= this._range.end.row; r++){
411
+ const row = [];
412
+ for(let c = this._range.start.col; c <= this._range.end.col; c++){
413
+ const cell = this._worksheet.cell(r, c);
414
+ row.push(cell.value);
415
+ }
416
+ result.push(row);
417
+ }
418
+ return result;
419
+ }
420
+ /**
421
+ * Set values in the range from a 2D array
422
+ */ set values(data) {
423
+ for(let r = 0; r < data.length && r < this.rowCount; r++){
424
+ const row = data[r];
425
+ for(let c = 0; c < row.length && c < this.colCount; c++){
426
+ const cell = this._worksheet.cell(this._range.start.row + r, this._range.start.col + c);
427
+ cell.value = row[c];
428
+ }
429
+ }
430
+ }
431
+ /**
432
+ * Get all formulas in the range as a 2D array
433
+ */ get formulas() {
434
+ const result = [];
435
+ for(let r = this._range.start.row; r <= this._range.end.row; r++){
436
+ const row = [];
437
+ for(let c = this._range.start.col; c <= this._range.end.col; c++){
438
+ const cell = this._worksheet.cell(r, c);
439
+ row.push(cell.formula);
440
+ }
441
+ result.push(row);
442
+ }
443
+ return result;
444
+ }
445
+ /**
446
+ * Set formulas in the range from a 2D array
447
+ */ set formulas(data) {
448
+ for(let r = 0; r < data.length && r < this.rowCount; r++){
449
+ const row = data[r];
450
+ for(let c = 0; c < row.length && c < this.colCount; c++){
451
+ const cell = this._worksheet.cell(this._range.start.row + r, this._range.start.col + c);
452
+ cell.formula = row[c];
453
+ }
454
+ }
455
+ }
456
+ /**
457
+ * Get the style of the top-left cell
458
+ */ get style() {
459
+ return this._worksheet.cell(this._range.start.row, this._range.start.col).style;
460
+ }
461
+ /**
462
+ * Set style for all cells in the range
463
+ */ set style(style) {
464
+ for(let r = this._range.start.row; r <= this._range.end.row; r++){
465
+ for(let c = this._range.start.col; c <= this._range.end.col; c++){
466
+ const cell = this._worksheet.cell(r, c);
467
+ cell.style = style;
468
+ }
469
+ }
470
+ }
471
+ /**
472
+ * Iterate over all cells in the range
473
+ */ *[_computedKey]() {
474
+ for(let r = this._range.start.row; r <= this._range.end.row; r++){
475
+ for(let c = this._range.start.col; c <= this._range.end.col; c++){
476
+ yield this._worksheet.cell(r, c);
477
+ }
478
+ }
479
+ }
480
+ /**
481
+ * Iterate over cells row by row
482
+ */ *rows() {
483
+ for(let r = this._range.start.row; r <= this._range.end.row; r++){
484
+ const row = [];
485
+ for(let c = this._range.start.col; c <= this._range.end.col; c++){
486
+ row.push(this._worksheet.cell(r, c));
487
+ }
488
+ yield row;
489
+ }
490
+ }
491
+ }
492
+
493
+ // Parser options that preserve structure and attributes
494
+ const parserOptions = {
495
+ ignoreAttributes: false,
496
+ attributeNamePrefix: '@_',
497
+ textNodeName: '#text',
498
+ preserveOrder: true,
499
+ commentPropName: '#comment',
500
+ cdataPropName: '#cdata',
501
+ trimValues: false,
502
+ parseTagValue: false,
503
+ parseAttributeValue: false
504
+ };
505
+ // Builder options matching parser for round-trip compatibility
506
+ const builderOptions = {
507
+ ignoreAttributes: false,
508
+ attributeNamePrefix: '@_',
509
+ textNodeName: '#text',
510
+ preserveOrder: true,
511
+ commentPropName: '#comment',
512
+ cdataPropName: '#cdata',
513
+ format: false,
514
+ suppressEmptyNode: false,
515
+ suppressBooleanAttributes: false
516
+ };
517
+ const parser = new fastXmlParser.XMLParser(parserOptions);
518
+ const builder = new fastXmlParser.XMLBuilder(builderOptions);
519
+ /**
520
+ * Parses an XML string into a JavaScript object
521
+ * Preserves element order and attributes for round-trip compatibility
522
+ */ const parseXml = (xml)=>{
523
+ return parser.parse(xml);
524
+ };
525
+ /**
526
+ * Converts a JavaScript object back to an XML string
527
+ */ const stringifyXml = (obj)=>{
528
+ return builder.build(obj);
529
+ };
530
+ /**
531
+ * Finds the first element with the given tag name in the XML tree
532
+ */ const findElement = (nodes, tagName)=>{
533
+ for (const node of nodes){
534
+ if (tagName in node) {
535
+ return node;
536
+ }
537
+ }
538
+ return undefined;
539
+ };
540
+ /**
541
+ * Gets the children of an element
542
+ */ const getChildren = (node, tagName)=>{
543
+ const children = node[tagName];
544
+ if (Array.isArray(children)) {
545
+ return children;
546
+ }
547
+ return [];
548
+ };
549
+ /**
550
+ * Gets an attribute value from a node
551
+ */ const getAttr = (node, name)=>{
552
+ const attrs = node[':@'];
553
+ return attrs?.[`@_${name}`];
554
+ };
555
+ /**
556
+ * Creates a new XML element
557
+ */ const createElement = (tagName, attrs, children)=>{
558
+ const node = {
559
+ [tagName]: children || []
560
+ };
561
+ if (attrs && Object.keys(attrs).length > 0) {
562
+ const attrObj = {};
563
+ for (const [key, value] of Object.entries(attrs)){
564
+ attrObj[`@_${key}`] = value;
565
+ }
566
+ node[':@'] = attrObj;
567
+ }
568
+ return node;
569
+ };
570
+ /**
571
+ * Creates a text node
572
+ */ const createText = (text)=>{
573
+ return {
574
+ '#text': text
575
+ };
576
+ };
577
+
578
+ /**
579
+ * Represents a worksheet in a workbook
580
+ */ class Worksheet {
581
+ constructor(workbook, name){
582
+ this._cells = new Map();
583
+ this._xmlNodes = null;
584
+ this._dirty = false;
585
+ this._mergedCells = new Set();
586
+ this._sheetData = [];
587
+ this._workbook = workbook;
588
+ this._name = name;
589
+ }
590
+ /**
591
+ * Get the workbook this sheet belongs to
592
+ */ get workbook() {
593
+ return this._workbook;
594
+ }
595
+ /**
596
+ * Get the sheet name
597
+ */ get name() {
598
+ return this._name;
599
+ }
600
+ /**
601
+ * Set the sheet name
602
+ */ set name(value) {
603
+ this._name = value;
604
+ this._dirty = true;
605
+ }
606
+ /**
607
+ * Parse worksheet XML content
608
+ */ parse(xml) {
609
+ this._xmlNodes = parseXml(xml);
610
+ const worksheet = findElement(this._xmlNodes, 'worksheet');
611
+ if (!worksheet) return;
612
+ const worksheetChildren = getChildren(worksheet, 'worksheet');
613
+ // Parse sheet data (cells)
614
+ const sheetData = findElement(worksheetChildren, 'sheetData');
615
+ if (sheetData) {
616
+ this._sheetData = getChildren(sheetData, 'sheetData');
617
+ this._parseSheetData(this._sheetData);
618
+ }
619
+ // Parse merged cells
620
+ const mergeCells = findElement(worksheetChildren, 'mergeCells');
621
+ if (mergeCells) {
622
+ const mergeChildren = getChildren(mergeCells, 'mergeCells');
623
+ for (const mergeCell of mergeChildren){
624
+ if ('mergeCell' in mergeCell) {
625
+ const ref = getAttr(mergeCell, 'ref');
626
+ if (ref) {
627
+ this._mergedCells.add(ref);
628
+ }
629
+ }
630
+ }
631
+ }
632
+ }
633
+ /**
634
+ * Parse the sheetData element to extract cells
635
+ */ _parseSheetData(rows) {
636
+ for (const rowNode of rows){
637
+ if (!('row' in rowNode)) continue;
638
+ const rowChildren = getChildren(rowNode, 'row');
639
+ for (const cellNode of rowChildren){
640
+ if (!('c' in cellNode)) continue;
641
+ const ref = getAttr(cellNode, 'r');
642
+ if (!ref) continue;
643
+ const { row, col } = parseAddress(ref);
644
+ const cellData = this._parseCellNode(cellNode);
645
+ const cell = new Cell(this, row, col, cellData);
646
+ this._cells.set(ref, cell);
647
+ }
648
+ }
649
+ }
650
+ /**
651
+ * Parse a cell XML node to CellData
652
+ */ _parseCellNode(node) {
653
+ const data = {};
654
+ // Type attribute
655
+ const t = getAttr(node, 't');
656
+ if (t) {
657
+ data.t = t;
658
+ }
659
+ // Style attribute
660
+ const s = getAttr(node, 's');
661
+ if (s) {
662
+ data.s = parseInt(s, 10);
663
+ }
664
+ const children = getChildren(node, 'c');
665
+ // Value element
666
+ const vNode = findElement(children, 'v');
667
+ if (vNode) {
668
+ const vChildren = getChildren(vNode, 'v');
669
+ for (const child of vChildren){
670
+ if ('#text' in child) {
671
+ const text = child['#text'];
672
+ // Parse based on type
673
+ if (data.t === 's') {
674
+ data.v = parseInt(text, 10); // Shared string index
675
+ } else if (data.t === 'b') {
676
+ data.v = text === '1' ? 1 : 0;
677
+ } else if (data.t === 'e' || data.t === 'str') {
678
+ data.v = text;
679
+ } else {
680
+ // Number or default
681
+ data.v = parseFloat(text);
682
+ }
683
+ break;
684
+ }
685
+ }
686
+ }
687
+ // Formula element
688
+ const fNode = findElement(children, 'f');
689
+ if (fNode) {
690
+ const fChildren = getChildren(fNode, 'f');
691
+ for (const child of fChildren){
692
+ if ('#text' in child) {
693
+ data.f = child['#text'];
694
+ break;
695
+ }
696
+ }
697
+ // Check for shared formula
698
+ const si = getAttr(fNode, 'si');
699
+ if (si) {
700
+ data.si = parseInt(si, 10);
701
+ }
702
+ // Check for array formula range
703
+ const ref = getAttr(fNode, 'ref');
704
+ if (ref) {
705
+ data.F = ref;
706
+ }
707
+ }
708
+ // Inline string (is element)
709
+ const isNode = findElement(children, 'is');
710
+ if (isNode) {
711
+ data.t = 'str';
712
+ const isChildren = getChildren(isNode, 'is');
713
+ const tNode = findElement(isChildren, 't');
714
+ if (tNode) {
715
+ const tChildren = getChildren(tNode, 't');
716
+ for (const child of tChildren){
717
+ if ('#text' in child) {
718
+ data.v = child['#text'];
719
+ break;
720
+ }
721
+ }
722
+ }
723
+ }
724
+ return data;
725
+ }
726
+ /**
727
+ * Get a cell by address or row/col
728
+ */ cell(rowOrAddress, col) {
729
+ const { row, col: c } = parseCellRef(rowOrAddress, col);
730
+ const address = toAddress(row, c);
731
+ let cell = this._cells.get(address);
732
+ if (!cell) {
733
+ cell = new Cell(this, row, c);
734
+ this._cells.set(address, cell);
735
+ }
736
+ return cell;
737
+ }
738
+ range(startRowOrRange, startCol, endRow, endCol) {
739
+ let rangeAddr;
740
+ if (typeof startRowOrRange === 'string') {
741
+ rangeAddr = parseRange(startRowOrRange);
742
+ } else {
743
+ if (startCol === undefined || endRow === undefined || endCol === undefined) {
744
+ throw new Error('All range parameters must be provided');
745
+ }
746
+ rangeAddr = {
747
+ start: {
748
+ row: startRowOrRange,
749
+ col: startCol
750
+ },
751
+ end: {
752
+ row: endRow,
753
+ col: endCol
754
+ }
755
+ };
756
+ }
757
+ return new Range(this, rangeAddr);
758
+ }
759
+ /**
760
+ * Merge cells in the given range
761
+ */ mergeCells(rangeOrStart, end) {
762
+ let rangeStr;
763
+ if (end) {
764
+ rangeStr = `${rangeOrStart}:${end}`;
765
+ } else {
766
+ rangeStr = rangeOrStart;
767
+ }
768
+ this._mergedCells.add(rangeStr);
769
+ this._dirty = true;
770
+ }
771
+ /**
772
+ * Unmerge cells in the given range
773
+ */ unmergeCells(rangeStr) {
774
+ this._mergedCells.delete(rangeStr);
775
+ this._dirty = true;
776
+ }
777
+ /**
778
+ * Get all merged cell ranges
779
+ */ get mergedCells() {
780
+ return Array.from(this._mergedCells);
781
+ }
782
+ /**
783
+ * Check if the worksheet has been modified
784
+ */ get dirty() {
785
+ if (this._dirty) return true;
786
+ for (const cell of this._cells.values()){
787
+ if (cell.dirty) return true;
788
+ }
789
+ return false;
790
+ }
791
+ /**
792
+ * Get all cells in the worksheet
793
+ */ get cells() {
794
+ return this._cells;
795
+ }
796
+ /**
797
+ * Generate XML for this worksheet
798
+ */ toXml() {
799
+ // Build sheetData from cells
800
+ const rowMap = new Map();
801
+ for (const cell of this._cells.values()){
802
+ const row = cell.row;
803
+ if (!rowMap.has(row)) {
804
+ rowMap.set(row, []);
805
+ }
806
+ rowMap.get(row).push(cell);
807
+ }
808
+ // Sort rows and cells
809
+ const sortedRows = Array.from(rowMap.entries()).sort((a, b)=>a[0] - b[0]);
810
+ const rowNodes = [];
811
+ for (const [rowIdx, cells] of sortedRows){
812
+ cells.sort((a, b)=>a.col - b.col);
813
+ const cellNodes = [];
814
+ for (const cell of cells){
815
+ const cellNode = this._buildCellNode(cell);
816
+ cellNodes.push(cellNode);
817
+ }
818
+ const rowNode = createElement('row', {
819
+ r: String(rowIdx + 1)
820
+ }, cellNodes);
821
+ rowNodes.push(rowNode);
822
+ }
823
+ const sheetDataNode = createElement('sheetData', {}, rowNodes);
824
+ // Build worksheet structure
825
+ const worksheetChildren = [
826
+ sheetDataNode
827
+ ];
828
+ // Add merged cells if any
829
+ if (this._mergedCells.size > 0) {
830
+ const mergeCellNodes = [];
831
+ for (const ref of this._mergedCells){
832
+ mergeCellNodes.push(createElement('mergeCell', {
833
+ ref
834
+ }, []));
835
+ }
836
+ const mergeCellsNode = createElement('mergeCells', {
837
+ count: String(this._mergedCells.size)
838
+ }, mergeCellNodes);
839
+ worksheetChildren.push(mergeCellsNode);
840
+ }
841
+ const worksheetNode = createElement('worksheet', {
842
+ xmlns: 'http://schemas.openxmlformats.org/spreadsheetml/2006/main',
843
+ 'xmlns:r': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships'
844
+ }, worksheetChildren);
845
+ return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n${stringifyXml([
846
+ worksheetNode
847
+ ])}`;
848
+ }
849
+ /**
850
+ * Build a cell XML node from a Cell object
851
+ */ _buildCellNode(cell) {
852
+ const data = cell.data;
853
+ const attrs = {
854
+ r: cell.address
855
+ };
856
+ if (data.t && data.t !== 'n') {
857
+ attrs.t = data.t;
858
+ }
859
+ if (data.s !== undefined) {
860
+ attrs.s = String(data.s);
861
+ }
862
+ const children = [];
863
+ // Formula
864
+ if (data.f) {
865
+ const fAttrs = {};
866
+ if (data.F) fAttrs.ref = data.F;
867
+ if (data.si !== undefined) fAttrs.si = String(data.si);
868
+ children.push(createElement('f', fAttrs, [
869
+ createText(data.f)
870
+ ]));
871
+ }
872
+ // Value
873
+ if (data.v !== undefined) {
874
+ children.push(createElement('v', {}, [
875
+ createText(String(data.v))
876
+ ]));
877
+ }
878
+ return createElement('c', attrs, children);
879
+ }
880
+ }
881
+
882
+ /**
883
+ * Manages the shared strings table from xl/sharedStrings.xml
884
+ * Excel stores strings in a shared table to reduce file size
885
+ */ class SharedStrings {
886
+ /**
887
+ * Parse shared strings from XML content
888
+ */ static parse(xml) {
889
+ const ss = new SharedStrings();
890
+ const parsed = parseXml(xml);
891
+ const sst = findElement(parsed, 'sst');
892
+ if (!sst) return ss;
893
+ const children = getChildren(sst, 'sst');
894
+ for (const child of children){
895
+ if ('si' in child) {
896
+ const siChildren = getChildren(child, 'si');
897
+ const text = ss.extractText(siChildren);
898
+ ss.strings.push(text);
899
+ ss.stringToIndex.set(text, ss.strings.length - 1);
900
+ }
901
+ }
902
+ return ss;
903
+ }
904
+ /**
905
+ * Extract text from a string item (si element)
906
+ * Handles both simple <t> elements and rich text <r> elements
907
+ */ extractText(nodes) {
908
+ let text = '';
909
+ for (const node of nodes){
910
+ if ('t' in node) {
911
+ // Simple text: <t>value</t>
912
+ const tChildren = getChildren(node, 't');
913
+ for (const child of tChildren){
914
+ if ('#text' in child) {
915
+ text += child['#text'];
916
+ }
917
+ }
918
+ } else if ('r' in node) {
919
+ // Rich text: <r><t>value</t></r>
920
+ const rChildren = getChildren(node, 'r');
921
+ for (const rChild of rChildren){
922
+ if ('t' in rChild) {
923
+ const tChildren = getChildren(rChild, 't');
924
+ for (const child of tChildren){
925
+ if ('#text' in child) {
926
+ text += child['#text'];
927
+ }
928
+ }
929
+ }
930
+ }
931
+ }
932
+ }
933
+ return text;
934
+ }
935
+ /**
936
+ * Get a string by index
937
+ */ getString(index) {
938
+ return this.strings[index];
939
+ }
940
+ /**
941
+ * Add a string and return its index
942
+ * If the string already exists, returns the existing index
943
+ */ addString(str) {
944
+ const existing = this.stringToIndex.get(str);
945
+ if (existing !== undefined) {
946
+ return existing;
947
+ }
948
+ const index = this.strings.length;
949
+ this.strings.push(str);
950
+ this.stringToIndex.set(str, index);
951
+ this._dirty = true;
952
+ return index;
953
+ }
954
+ /**
955
+ * Check if the shared strings table has been modified
956
+ */ get dirty() {
957
+ return this._dirty;
958
+ }
959
+ /**
960
+ * Get the count of strings
961
+ */ get count() {
962
+ return this.strings.length;
963
+ }
964
+ /**
965
+ * Generate XML for the shared strings table
966
+ */ toXml() {
967
+ const siElements = [];
968
+ for (const str of this.strings){
969
+ const tElement = createElement('t', str.startsWith(' ') || str.endsWith(' ') ? {
970
+ 'xml:space': 'preserve'
971
+ } : {}, [
972
+ createText(str)
973
+ ]);
974
+ const siElement = createElement('si', {}, [
975
+ tElement
976
+ ]);
977
+ siElements.push(siElement);
978
+ }
979
+ const sst = createElement('sst', {
980
+ xmlns: 'http://schemas.openxmlformats.org/spreadsheetml/2006/main',
981
+ count: String(this.strings.length),
982
+ uniqueCount: String(this.strings.length)
983
+ }, siElements);
984
+ return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n${stringifyXml([
985
+ sst
986
+ ])}`;
987
+ }
988
+ constructor(){
989
+ this.strings = [];
990
+ this.stringToIndex = new Map();
991
+ this._dirty = false;
992
+ }
993
+ }
994
+
995
+ /**
996
+ * Normalize a color to ARGB format (8 hex chars).
997
+ * Accepts: "#RGB", "#RRGGBB", "RGB", "RRGGBB", "AARRGGBB", "#AARRGGBB"
998
+ */ const normalizeColor = (color)=>{
999
+ let c = color.replace(/^#/, '').toUpperCase();
1000
+ // Handle shorthand 3-char format (e.g., "FFF" -> "FFFFFF")
1001
+ if (c.length === 3) {
1002
+ c = c[0] + c[0] + c[1] + c[1] + c[2] + c[2];
1003
+ }
1004
+ // Add alpha channel if not present (6 chars -> 8 chars)
1005
+ if (c.length === 6) {
1006
+ c = 'FF' + c;
1007
+ }
1008
+ return c;
1009
+ };
1010
+ /**
1011
+ * Manages the styles (xl/styles.xml)
1012
+ */ class Styles {
1013
+ /**
1014
+ * Parse styles from XML content
1015
+ */ static parse(xml) {
1016
+ const styles = new Styles();
1017
+ styles._xmlNodes = parseXml(xml);
1018
+ const styleSheet = findElement(styles._xmlNodes, 'styleSheet');
1019
+ if (!styleSheet) return styles;
1020
+ const children = getChildren(styleSheet, 'styleSheet');
1021
+ // Parse number formats
1022
+ const numFmts = findElement(children, 'numFmts');
1023
+ if (numFmts) {
1024
+ for (const child of getChildren(numFmts, 'numFmts')){
1025
+ if ('numFmt' in child) {
1026
+ const id = parseInt(getAttr(child, 'numFmtId') || '0', 10);
1027
+ const code = getAttr(child, 'formatCode') || '';
1028
+ styles._numFmts.set(id, code);
1029
+ }
1030
+ }
1031
+ }
1032
+ // Parse fonts
1033
+ const fonts = findElement(children, 'fonts');
1034
+ if (fonts) {
1035
+ for (const child of getChildren(fonts, 'fonts')){
1036
+ if ('font' in child) {
1037
+ styles._fonts.push(styles._parseFont(child));
1038
+ }
1039
+ }
1040
+ }
1041
+ // Parse fills
1042
+ const fills = findElement(children, 'fills');
1043
+ if (fills) {
1044
+ for (const child of getChildren(fills, 'fills')){
1045
+ if ('fill' in child) {
1046
+ styles._fills.push(styles._parseFill(child));
1047
+ }
1048
+ }
1049
+ }
1050
+ // Parse borders
1051
+ const borders = findElement(children, 'borders');
1052
+ if (borders) {
1053
+ for (const child of getChildren(borders, 'borders')){
1054
+ if ('border' in child) {
1055
+ styles._borders.push(styles._parseBorder(child));
1056
+ }
1057
+ }
1058
+ }
1059
+ // Parse cellXfs (cell formats)
1060
+ const cellXfs = findElement(children, 'cellXfs');
1061
+ if (cellXfs) {
1062
+ for (const child of getChildren(cellXfs, 'cellXfs')){
1063
+ if ('xf' in child) {
1064
+ styles._cellXfs.push(styles._parseCellXf(child));
1065
+ }
1066
+ }
1067
+ }
1068
+ return styles;
1069
+ }
1070
+ /**
1071
+ * Create an empty styles object with defaults
1072
+ */ static createDefault() {
1073
+ const styles = new Styles();
1074
+ // Default font (Calibri 11)
1075
+ styles._fonts.push({
1076
+ bold: false,
1077
+ italic: false,
1078
+ underline: false,
1079
+ strike: false,
1080
+ size: 11,
1081
+ name: 'Calibri',
1082
+ color: undefined
1083
+ });
1084
+ // Default fills (none and gray125 pattern are required)
1085
+ styles._fills.push({
1086
+ type: 'none'
1087
+ });
1088
+ styles._fills.push({
1089
+ type: 'gray125'
1090
+ });
1091
+ // Default border (none)
1092
+ styles._borders.push({});
1093
+ // Default cell format
1094
+ styles._cellXfs.push({
1095
+ fontId: 0,
1096
+ fillId: 0,
1097
+ borderId: 0,
1098
+ numFmtId: 0
1099
+ });
1100
+ return styles;
1101
+ }
1102
+ _parseFont(node) {
1103
+ const font = {
1104
+ bold: false,
1105
+ italic: false,
1106
+ underline: false,
1107
+ strike: false
1108
+ };
1109
+ const children = getChildren(node, 'font');
1110
+ for (const child of children){
1111
+ if ('b' in child) font.bold = true;
1112
+ if ('i' in child) font.italic = true;
1113
+ if ('u' in child) font.underline = true;
1114
+ if ('strike' in child) font.strike = true;
1115
+ if ('sz' in child) font.size = parseFloat(getAttr(child, 'val') || '11');
1116
+ if ('name' in child) font.name = getAttr(child, 'val');
1117
+ if ('color' in child) {
1118
+ font.color = getAttr(child, 'rgb') || getAttr(child, 'theme');
1119
+ }
1120
+ }
1121
+ return font;
1122
+ }
1123
+ _parseFill(node) {
1124
+ const fill = {
1125
+ type: 'none'
1126
+ };
1127
+ const children = getChildren(node, 'fill');
1128
+ for (const child of children){
1129
+ if ('patternFill' in child) {
1130
+ const pattern = getAttr(child, 'patternType');
1131
+ fill.type = pattern || 'none';
1132
+ const pfChildren = getChildren(child, 'patternFill');
1133
+ for (const pfChild of pfChildren){
1134
+ if ('fgColor' in pfChild) {
1135
+ fill.fgColor = getAttr(pfChild, 'rgb') || getAttr(pfChild, 'theme');
1136
+ }
1137
+ if ('bgColor' in pfChild) {
1138
+ fill.bgColor = getAttr(pfChild, 'rgb') || getAttr(pfChild, 'theme');
1139
+ }
1140
+ }
1141
+ }
1142
+ }
1143
+ return fill;
1144
+ }
1145
+ _parseBorder(node) {
1146
+ const border = {};
1147
+ const children = getChildren(node, 'border');
1148
+ for (const child of children){
1149
+ const style = getAttr(child, 'style');
1150
+ if ('left' in child && style) border.left = style;
1151
+ if ('right' in child && style) border.right = style;
1152
+ if ('top' in child && style) border.top = style;
1153
+ if ('bottom' in child && style) border.bottom = style;
1154
+ }
1155
+ return border;
1156
+ }
1157
+ _parseCellXf(node) {
1158
+ return {
1159
+ fontId: parseInt(getAttr(node, 'fontId') || '0', 10),
1160
+ fillId: parseInt(getAttr(node, 'fillId') || '0', 10),
1161
+ borderId: parseInt(getAttr(node, 'borderId') || '0', 10),
1162
+ numFmtId: parseInt(getAttr(node, 'numFmtId') || '0', 10),
1163
+ alignment: this._parseAlignment(node)
1164
+ };
1165
+ }
1166
+ _parseAlignment(node) {
1167
+ const children = getChildren(node, 'xf');
1168
+ const alignNode = findElement(children, 'alignment');
1169
+ if (!alignNode) return undefined;
1170
+ return {
1171
+ horizontal: getAttr(alignNode, 'horizontal'),
1172
+ vertical: getAttr(alignNode, 'vertical'),
1173
+ wrapText: getAttr(alignNode, 'wrapText') === '1',
1174
+ textRotation: parseInt(getAttr(alignNode, 'textRotation') || '0', 10)
1175
+ };
1176
+ }
1177
+ /**
1178
+ * Get a style by index
1179
+ */ getStyle(index) {
1180
+ const xf = this._cellXfs[index];
1181
+ if (!xf) return {};
1182
+ const font = this._fonts[xf.fontId];
1183
+ const fill = this._fills[xf.fillId];
1184
+ const border = this._borders[xf.borderId];
1185
+ const numFmt = this._numFmts.get(xf.numFmtId);
1186
+ const style = {};
1187
+ if (font) {
1188
+ if (font.bold) style.bold = true;
1189
+ if (font.italic) style.italic = true;
1190
+ if (font.underline) style.underline = true;
1191
+ if (font.strike) style.strike = true;
1192
+ if (font.size) style.fontSize = font.size;
1193
+ if (font.name) style.fontName = font.name;
1194
+ if (font.color) style.fontColor = font.color;
1195
+ }
1196
+ if (fill && fill.fgColor) {
1197
+ style.fill = fill.fgColor;
1198
+ }
1199
+ if (border) {
1200
+ if (border.top || border.bottom || border.left || border.right) {
1201
+ style.border = {
1202
+ top: border.top,
1203
+ bottom: border.bottom,
1204
+ left: border.left,
1205
+ right: border.right
1206
+ };
1207
+ }
1208
+ }
1209
+ if (numFmt) {
1210
+ style.numberFormat = numFmt;
1211
+ }
1212
+ if (xf.alignment) {
1213
+ style.alignment = {
1214
+ horizontal: xf.alignment.horizontal,
1215
+ vertical: xf.alignment.vertical,
1216
+ wrapText: xf.alignment.wrapText,
1217
+ textRotation: xf.alignment.textRotation
1218
+ };
1219
+ }
1220
+ return style;
1221
+ }
1222
+ /**
1223
+ * Create a style and return its index
1224
+ * Uses caching to deduplicate identical styles
1225
+ */ createStyle(style) {
1226
+ const key = JSON.stringify(style);
1227
+ const cached = this._styleCache.get(key);
1228
+ if (cached !== undefined) {
1229
+ return cached;
1230
+ }
1231
+ this._dirty = true;
1232
+ // Create or find font
1233
+ const fontId = this._findOrCreateFont(style);
1234
+ // Create or find fill
1235
+ const fillId = this._findOrCreateFill(style);
1236
+ // Create or find border
1237
+ const borderId = this._findOrCreateBorder(style);
1238
+ // Create or find number format
1239
+ const numFmtId = style.numberFormat ? this._findOrCreateNumFmt(style.numberFormat) : 0;
1240
+ // Create cell format
1241
+ const xf = {
1242
+ fontId,
1243
+ fillId,
1244
+ borderId,
1245
+ numFmtId
1246
+ };
1247
+ if (style.alignment) {
1248
+ xf.alignment = {
1249
+ horizontal: style.alignment.horizontal,
1250
+ vertical: style.alignment.vertical,
1251
+ wrapText: style.alignment.wrapText,
1252
+ textRotation: style.alignment.textRotation
1253
+ };
1254
+ }
1255
+ const index = this._cellXfs.length;
1256
+ this._cellXfs.push(xf);
1257
+ this._styleCache.set(key, index);
1258
+ return index;
1259
+ }
1260
+ _findOrCreateFont(style) {
1261
+ const font = {
1262
+ bold: style.bold || false,
1263
+ italic: style.italic || false,
1264
+ underline: style.underline === true || style.underline === 'single' || style.underline === 'double',
1265
+ strike: style.strike || false,
1266
+ size: style.fontSize,
1267
+ name: style.fontName,
1268
+ color: style.fontColor
1269
+ };
1270
+ // Try to find existing font
1271
+ for(let i = 0; i < this._fonts.length; i++){
1272
+ const f = this._fonts[i];
1273
+ if (f.bold === font.bold && f.italic === font.italic && f.underline === font.underline && f.strike === font.strike && f.size === font.size && f.name === font.name && f.color === font.color) {
1274
+ return i;
1275
+ }
1276
+ }
1277
+ // Create new font
1278
+ this._fonts.push(font);
1279
+ return this._fonts.length - 1;
1280
+ }
1281
+ _findOrCreateFill(style) {
1282
+ if (!style.fill) return 0;
1283
+ // Try to find existing fill
1284
+ for(let i = 0; i < this._fills.length; i++){
1285
+ const f = this._fills[i];
1286
+ if (f.fgColor === style.fill) {
1287
+ return i;
1288
+ }
1289
+ }
1290
+ // Create new fill
1291
+ this._fills.push({
1292
+ type: 'solid',
1293
+ fgColor: style.fill
1294
+ });
1295
+ return this._fills.length - 1;
1296
+ }
1297
+ _findOrCreateBorder(style) {
1298
+ if (!style.border) return 0;
1299
+ const border = {
1300
+ top: style.border.top,
1301
+ bottom: style.border.bottom,
1302
+ left: style.border.left,
1303
+ right: style.border.right
1304
+ };
1305
+ // Try to find existing border
1306
+ for(let i = 0; i < this._borders.length; i++){
1307
+ const b = this._borders[i];
1308
+ if (b.top === border.top && b.bottom === border.bottom && b.left === border.left && b.right === border.right) {
1309
+ return i;
1310
+ }
1311
+ }
1312
+ // Create new border
1313
+ this._borders.push(border);
1314
+ return this._borders.length - 1;
1315
+ }
1316
+ _findOrCreateNumFmt(format) {
1317
+ // Check if already exists
1318
+ for (const [id, code] of this._numFmts){
1319
+ if (code === format) return id;
1320
+ }
1321
+ // Create new (custom formats start at 164)
1322
+ const id = Math.max(164, ...Array.from(this._numFmts.keys())) + 1;
1323
+ this._numFmts.set(id, format);
1324
+ return id;
1325
+ }
1326
+ /**
1327
+ * Check if styles have been modified
1328
+ */ get dirty() {
1329
+ return this._dirty;
1330
+ }
1331
+ /**
1332
+ * Generate XML for styles
1333
+ */ toXml() {
1334
+ const children = [];
1335
+ // Number formats
1336
+ if (this._numFmts.size > 0) {
1337
+ const numFmtNodes = [];
1338
+ for (const [id, code] of this._numFmts){
1339
+ numFmtNodes.push(createElement('numFmt', {
1340
+ numFmtId: String(id),
1341
+ formatCode: code
1342
+ }, []));
1343
+ }
1344
+ children.push(createElement('numFmts', {
1345
+ count: String(numFmtNodes.length)
1346
+ }, numFmtNodes));
1347
+ }
1348
+ // Fonts
1349
+ const fontNodes = this._fonts.map((font)=>this._buildFontNode(font));
1350
+ children.push(createElement('fonts', {
1351
+ count: String(fontNodes.length)
1352
+ }, fontNodes));
1353
+ // Fills
1354
+ const fillNodes = this._fills.map((fill)=>this._buildFillNode(fill));
1355
+ children.push(createElement('fills', {
1356
+ count: String(fillNodes.length)
1357
+ }, fillNodes));
1358
+ // Borders
1359
+ const borderNodes = this._borders.map((border)=>this._buildBorderNode(border));
1360
+ children.push(createElement('borders', {
1361
+ count: String(borderNodes.length)
1362
+ }, borderNodes));
1363
+ // Cell style xfs (required but we just add a default)
1364
+ children.push(createElement('cellStyleXfs', {
1365
+ count: '1'
1366
+ }, [
1367
+ createElement('xf', {
1368
+ numFmtId: '0',
1369
+ fontId: '0',
1370
+ fillId: '0',
1371
+ borderId: '0'
1372
+ }, [])
1373
+ ]));
1374
+ // Cell xfs
1375
+ const xfNodes = this._cellXfs.map((xf)=>this._buildXfNode(xf));
1376
+ children.push(createElement('cellXfs', {
1377
+ count: String(xfNodes.length)
1378
+ }, xfNodes));
1379
+ // Cell styles (required)
1380
+ children.push(createElement('cellStyles', {
1381
+ count: '1'
1382
+ }, [
1383
+ createElement('cellStyle', {
1384
+ name: 'Normal',
1385
+ xfId: '0',
1386
+ builtinId: '0'
1387
+ }, [])
1388
+ ]));
1389
+ const styleSheet = createElement('styleSheet', {
1390
+ xmlns: 'http://schemas.openxmlformats.org/spreadsheetml/2006/main'
1391
+ }, children);
1392
+ return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n${stringifyXml([
1393
+ styleSheet
1394
+ ])}`;
1395
+ }
1396
+ _buildFontNode(font) {
1397
+ const children = [];
1398
+ if (font.bold) children.push(createElement('b', {}, []));
1399
+ if (font.italic) children.push(createElement('i', {}, []));
1400
+ if (font.underline) children.push(createElement('u', {}, []));
1401
+ if (font.strike) children.push(createElement('strike', {}, []));
1402
+ if (font.size) children.push(createElement('sz', {
1403
+ val: String(font.size)
1404
+ }, []));
1405
+ if (font.color) children.push(createElement('color', {
1406
+ rgb: normalizeColor(font.color)
1407
+ }, []));
1408
+ if (font.name) children.push(createElement('name', {
1409
+ val: font.name
1410
+ }, []));
1411
+ return createElement('font', {}, children);
1412
+ }
1413
+ _buildFillNode(fill) {
1414
+ const patternChildren = [];
1415
+ if (fill.fgColor) {
1416
+ const rgb = normalizeColor(fill.fgColor);
1417
+ patternChildren.push(createElement('fgColor', {
1418
+ rgb
1419
+ }, []));
1420
+ // For solid fills, bgColor is required (indexed 64 = system background)
1421
+ if (fill.type === 'solid') {
1422
+ patternChildren.push(createElement('bgColor', {
1423
+ indexed: '64'
1424
+ }, []));
1425
+ }
1426
+ }
1427
+ if (fill.bgColor && fill.type !== 'solid') {
1428
+ const rgb = normalizeColor(fill.bgColor);
1429
+ patternChildren.push(createElement('bgColor', {
1430
+ rgb
1431
+ }, []));
1432
+ }
1433
+ const patternFill = createElement('patternFill', {
1434
+ patternType: fill.type || 'none'
1435
+ }, patternChildren);
1436
+ return createElement('fill', {}, [
1437
+ patternFill
1438
+ ]);
1439
+ }
1440
+ _buildBorderNode(border) {
1441
+ const children = [];
1442
+ if (border.left) children.push(createElement('left', {
1443
+ style: border.left
1444
+ }, []));
1445
+ if (border.right) children.push(createElement('right', {
1446
+ style: border.right
1447
+ }, []));
1448
+ if (border.top) children.push(createElement('top', {
1449
+ style: border.top
1450
+ }, []));
1451
+ if (border.bottom) children.push(createElement('bottom', {
1452
+ style: border.bottom
1453
+ }, []));
1454
+ // Add empty elements if not present (required by Excel)
1455
+ if (!border.left) children.push(createElement('left', {}, []));
1456
+ if (!border.right) children.push(createElement('right', {}, []));
1457
+ if (!border.top) children.push(createElement('top', {}, []));
1458
+ if (!border.bottom) children.push(createElement('bottom', {}, []));
1459
+ children.push(createElement('diagonal', {}, []));
1460
+ return createElement('border', {}, children);
1461
+ }
1462
+ _buildXfNode(xf) {
1463
+ const attrs = {
1464
+ numFmtId: String(xf.numFmtId),
1465
+ fontId: String(xf.fontId),
1466
+ fillId: String(xf.fillId),
1467
+ borderId: String(xf.borderId)
1468
+ };
1469
+ if (xf.fontId > 0) attrs.applyFont = '1';
1470
+ if (xf.fillId > 0) attrs.applyFill = '1';
1471
+ if (xf.borderId > 0) attrs.applyBorder = '1';
1472
+ if (xf.numFmtId > 0) attrs.applyNumberFormat = '1';
1473
+ const children = [];
1474
+ if (xf.alignment) {
1475
+ const alignAttrs = {};
1476
+ if (xf.alignment.horizontal) alignAttrs.horizontal = xf.alignment.horizontal;
1477
+ if (xf.alignment.vertical) alignAttrs.vertical = xf.alignment.vertical;
1478
+ if (xf.alignment.wrapText) alignAttrs.wrapText = '1';
1479
+ if (xf.alignment.textRotation) alignAttrs.textRotation = String(xf.alignment.textRotation);
1480
+ children.push(createElement('alignment', alignAttrs, []));
1481
+ attrs.applyAlignment = '1';
1482
+ }
1483
+ return createElement('xf', attrs, children);
1484
+ }
1485
+ constructor(){
1486
+ this._numFmts = new Map();
1487
+ this._fonts = [];
1488
+ this._fills = [];
1489
+ this._borders = [];
1490
+ this._cellXfs = []; // Cell formats (combined style index)
1491
+ this._xmlNodes = null;
1492
+ this._dirty = false;
1493
+ // Cache for style deduplication
1494
+ this._styleCache = new Map();
1495
+ }
1496
+ }
1497
+
1498
+ /**
1499
+ * Represents an Excel pivot table with a fluent API for configuration.
1500
+ */ class PivotTable {
1501
+ constructor(name, cache, targetSheet, targetCell, targetRow, targetCol, pivotTableIndex){
1502
+ this._rowFields = [];
1503
+ this._columnFields = [];
1504
+ this._valueFields = [];
1505
+ this._filterFields = [];
1506
+ this._name = name;
1507
+ this._cache = cache;
1508
+ this._targetSheet = targetSheet;
1509
+ this._targetCell = targetCell;
1510
+ this._targetRow = targetRow;
1511
+ this._targetCol = targetCol;
1512
+ this._pivotTableIndex = pivotTableIndex;
1513
+ }
1514
+ /**
1515
+ * Get the pivot table name
1516
+ */ get name() {
1517
+ return this._name;
1518
+ }
1519
+ /**
1520
+ * Get the target sheet name
1521
+ */ get targetSheet() {
1522
+ return this._targetSheet;
1523
+ }
1524
+ /**
1525
+ * Get the target cell address
1526
+ */ get targetCell() {
1527
+ return this._targetCell;
1528
+ }
1529
+ /**
1530
+ * Get the pivot cache
1531
+ */ get cache() {
1532
+ return this._cache;
1533
+ }
1534
+ /**
1535
+ * Get the pivot table index (for file naming)
1536
+ */ get index() {
1537
+ return this._pivotTableIndex;
1538
+ }
1539
+ /**
1540
+ * Add a field to the row area
1541
+ * @param fieldName - Name of the source field (column header)
1542
+ */ addRowField(fieldName) {
1543
+ const fieldIndex = this._cache.getFieldIndex(fieldName);
1544
+ if (fieldIndex < 0) {
1545
+ throw new Error(`Field not found in source data: ${fieldName}`);
1546
+ }
1547
+ this._rowFields.push({
1548
+ fieldName,
1549
+ fieldIndex,
1550
+ axis: 'row'
1551
+ });
1552
+ return this;
1553
+ }
1554
+ /**
1555
+ * Add a field to the column area
1556
+ * @param fieldName - Name of the source field (column header)
1557
+ */ addColumnField(fieldName) {
1558
+ const fieldIndex = this._cache.getFieldIndex(fieldName);
1559
+ if (fieldIndex < 0) {
1560
+ throw new Error(`Field not found in source data: ${fieldName}`);
1561
+ }
1562
+ this._columnFields.push({
1563
+ fieldName,
1564
+ fieldIndex,
1565
+ axis: 'column'
1566
+ });
1567
+ return this;
1568
+ }
1569
+ /**
1570
+ * Add a field to the values area with aggregation
1571
+ * @param fieldName - Name of the source field (column header)
1572
+ * @param aggregation - Aggregation function (sum, count, average, min, max)
1573
+ * @param displayName - Optional display name (defaults to "Sum of FieldName")
1574
+ */ addValueField(fieldName, aggregation = 'sum', displayName) {
1575
+ const fieldIndex = this._cache.getFieldIndex(fieldName);
1576
+ if (fieldIndex < 0) {
1577
+ throw new Error(`Field not found in source data: ${fieldName}`);
1578
+ }
1579
+ const defaultName = `${aggregation.charAt(0).toUpperCase() + aggregation.slice(1)} of ${fieldName}`;
1580
+ this._valueFields.push({
1581
+ fieldName,
1582
+ fieldIndex,
1583
+ axis: 'value',
1584
+ aggregation,
1585
+ displayName: displayName || defaultName
1586
+ });
1587
+ return this;
1588
+ }
1589
+ /**
1590
+ * Add a field to the filter (page) area
1591
+ * @param fieldName - Name of the source field (column header)
1592
+ */ addFilterField(fieldName) {
1593
+ const fieldIndex = this._cache.getFieldIndex(fieldName);
1594
+ if (fieldIndex < 0) {
1595
+ throw new Error(`Field not found in source data: ${fieldName}`);
1596
+ }
1597
+ this._filterFields.push({
1598
+ fieldName,
1599
+ fieldIndex,
1600
+ axis: 'filter'
1601
+ });
1602
+ return this;
1603
+ }
1604
+ /**
1605
+ * Generate the pivotTableDefinition XML
1606
+ */ toXml() {
1607
+ const children = [];
1608
+ // Calculate location (estimate based on fields)
1609
+ const locationRef = this._calculateLocationRef();
1610
+ // Calculate first data row/col offsets (1-based, relative to pivot table)
1611
+ // firstHeaderRow: row offset of column headers (usually 1)
1612
+ // firstDataRow: row offset where data starts (after filters and column headers)
1613
+ // firstDataCol: column offset where data starts (after row labels)
1614
+ const filterRowCount = this._filterFields.length > 0 ? this._filterFields.length + 1 : 0;
1615
+ const headerRows = this._columnFields.length > 0 ? 1 : 0;
1616
+ const firstDataRow = filterRowCount + headerRows + 1;
1617
+ const firstDataCol = this._rowFields.length > 0 ? this._rowFields.length : 1;
1618
+ const locationNode = createElement('location', {
1619
+ ref: locationRef,
1620
+ firstHeaderRow: String(filterRowCount + 1),
1621
+ firstDataRow: String(firstDataRow),
1622
+ firstDataCol: String(firstDataCol)
1623
+ }, []);
1624
+ children.push(locationNode);
1625
+ // Build pivotFields (one per source field)
1626
+ const pivotFieldNodes = [];
1627
+ for (const cacheField of this._cache.fields){
1628
+ const fieldNode = this._buildPivotFieldNode(cacheField.index);
1629
+ pivotFieldNodes.push(fieldNode);
1630
+ }
1631
+ children.push(createElement('pivotFields', {
1632
+ count: String(pivotFieldNodes.length)
1633
+ }, pivotFieldNodes));
1634
+ // Row fields
1635
+ if (this._rowFields.length > 0) {
1636
+ const rowFieldNodes = this._rowFields.map((f)=>createElement('field', {
1637
+ x: String(f.fieldIndex)
1638
+ }, []));
1639
+ children.push(createElement('rowFields', {
1640
+ count: String(rowFieldNodes.length)
1641
+ }, rowFieldNodes));
1642
+ // Row items
1643
+ const rowItemNodes = this._buildRowItems();
1644
+ children.push(createElement('rowItems', {
1645
+ count: String(rowItemNodes.length)
1646
+ }, rowItemNodes));
1647
+ }
1648
+ // Column fields
1649
+ if (this._columnFields.length > 0) {
1650
+ const colFieldNodes = this._columnFields.map((f)=>createElement('field', {
1651
+ x: String(f.fieldIndex)
1652
+ }, []));
1653
+ // If we have multiple value fields, add -2 to indicate where "Values" header goes
1654
+ if (this._valueFields.length > 1) {
1655
+ colFieldNodes.push(createElement('field', {
1656
+ x: '-2'
1657
+ }, []));
1658
+ }
1659
+ children.push(createElement('colFields', {
1660
+ count: String(colFieldNodes.length)
1661
+ }, colFieldNodes));
1662
+ // Column items - need to account for multiple value fields
1663
+ const colItemNodes = this._buildColItems();
1664
+ children.push(createElement('colItems', {
1665
+ count: String(colItemNodes.length)
1666
+ }, colItemNodes));
1667
+ } else if (this._valueFields.length > 1) {
1668
+ // If no column fields but we have multiple values, need colFields with -2 (data field indicator)
1669
+ children.push(createElement('colFields', {
1670
+ count: '1'
1671
+ }, [
1672
+ createElement('field', {
1673
+ x: '-2'
1674
+ }, [])
1675
+ ]));
1676
+ // Column items for each value field
1677
+ const colItemNodes = [];
1678
+ for(let i = 0; i < this._valueFields.length; i++){
1679
+ colItemNodes.push(createElement('i', {}, [
1680
+ createElement('x', i === 0 ? {} : {
1681
+ v: String(i)
1682
+ }, [])
1683
+ ]));
1684
+ }
1685
+ children.push(createElement('colItems', {
1686
+ count: String(colItemNodes.length)
1687
+ }, colItemNodes));
1688
+ } else if (this._valueFields.length === 1) {
1689
+ // Single value field - just add a single column item
1690
+ children.push(createElement('colItems', {
1691
+ count: '1'
1692
+ }, [
1693
+ createElement('i', {}, [])
1694
+ ]));
1695
+ }
1696
+ // Page (filter) fields
1697
+ if (this._filterFields.length > 0) {
1698
+ const pageFieldNodes = this._filterFields.map((f)=>createElement('pageField', {
1699
+ fld: String(f.fieldIndex),
1700
+ hier: '-1'
1701
+ }, []));
1702
+ children.push(createElement('pageFields', {
1703
+ count: String(pageFieldNodes.length)
1704
+ }, pageFieldNodes));
1705
+ }
1706
+ // Data fields (values)
1707
+ if (this._valueFields.length > 0) {
1708
+ const dataFieldNodes = this._valueFields.map((f)=>createElement('dataField', {
1709
+ name: f.displayName || f.fieldName,
1710
+ fld: String(f.fieldIndex),
1711
+ baseField: '0',
1712
+ baseItem: '0',
1713
+ subtotal: f.aggregation || 'sum'
1714
+ }, []));
1715
+ children.push(createElement('dataFields', {
1716
+ count: String(dataFieldNodes.length)
1717
+ }, dataFieldNodes));
1718
+ }
1719
+ // Pivot table style
1720
+ children.push(createElement('pivotTableStyleInfo', {
1721
+ name: 'PivotStyleMedium9',
1722
+ showRowHeaders: '1',
1723
+ showColHeaders: '1',
1724
+ showRowStripes: '0',
1725
+ showColStripes: '0',
1726
+ showLastColumn: '1'
1727
+ }, []));
1728
+ const pivotTableNode = createElement('pivotTableDefinition', {
1729
+ xmlns: 'http://schemas.openxmlformats.org/spreadsheetml/2006/main',
1730
+ 'xmlns:r': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships',
1731
+ name: this._name,
1732
+ cacheId: String(this._cache.cacheId),
1733
+ applyNumberFormats: '0',
1734
+ applyBorderFormats: '0',
1735
+ applyFontFormats: '0',
1736
+ applyPatternFormats: '0',
1737
+ applyAlignmentFormats: '0',
1738
+ applyWidthHeightFormats: '1',
1739
+ dataCaption: 'Values',
1740
+ updatedVersion: '8',
1741
+ minRefreshableVersion: '3',
1742
+ useAutoFormatting: '1',
1743
+ rowGrandTotals: '1',
1744
+ colGrandTotals: '1',
1745
+ itemPrintTitles: '1',
1746
+ createdVersion: '8',
1747
+ indent: '0',
1748
+ outline: '1',
1749
+ outlineData: '1',
1750
+ multipleFieldFilters: '0'
1751
+ }, children);
1752
+ return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n${stringifyXml([
1753
+ pivotTableNode
1754
+ ])}`;
1755
+ }
1756
+ /**
1757
+ * Build a pivotField node for a given field index
1758
+ */ _buildPivotFieldNode(fieldIndex) {
1759
+ const attrs = {};
1760
+ const children = [];
1761
+ // Check if this field is assigned to an axis
1762
+ const rowField = this._rowFields.find((f)=>f.fieldIndex === fieldIndex);
1763
+ const colField = this._columnFields.find((f)=>f.fieldIndex === fieldIndex);
1764
+ const filterField = this._filterFields.find((f)=>f.fieldIndex === fieldIndex);
1765
+ const valueField = this._valueFields.find((f)=>f.fieldIndex === fieldIndex);
1766
+ if (rowField) {
1767
+ attrs.axis = 'axisRow';
1768
+ attrs.showAll = '0';
1769
+ // Add items for shared values
1770
+ const cacheField = this._cache.fields[fieldIndex];
1771
+ if (cacheField && cacheField.sharedItems.length > 0) {
1772
+ const itemNodes = [];
1773
+ for(let i = 0; i < cacheField.sharedItems.length; i++){
1774
+ itemNodes.push(createElement('item', {
1775
+ x: String(i)
1776
+ }, []));
1777
+ }
1778
+ // Add default subtotal item
1779
+ itemNodes.push(createElement('item', {
1780
+ t: 'default'
1781
+ }, []));
1782
+ children.push(createElement('items', {
1783
+ count: String(itemNodes.length)
1784
+ }, itemNodes));
1785
+ }
1786
+ } else if (colField) {
1787
+ attrs.axis = 'axisCol';
1788
+ attrs.showAll = '0';
1789
+ const cacheField = this._cache.fields[fieldIndex];
1790
+ if (cacheField && cacheField.sharedItems.length > 0) {
1791
+ const itemNodes = [];
1792
+ for(let i = 0; i < cacheField.sharedItems.length; i++){
1793
+ itemNodes.push(createElement('item', {
1794
+ x: String(i)
1795
+ }, []));
1796
+ }
1797
+ itemNodes.push(createElement('item', {
1798
+ t: 'default'
1799
+ }, []));
1800
+ children.push(createElement('items', {
1801
+ count: String(itemNodes.length)
1802
+ }, itemNodes));
1803
+ }
1804
+ } else if (filterField) {
1805
+ attrs.axis = 'axisPage';
1806
+ attrs.showAll = '0';
1807
+ const cacheField = this._cache.fields[fieldIndex];
1808
+ if (cacheField && cacheField.sharedItems.length > 0) {
1809
+ const itemNodes = [];
1810
+ for(let i = 0; i < cacheField.sharedItems.length; i++){
1811
+ itemNodes.push(createElement('item', {
1812
+ x: String(i)
1813
+ }, []));
1814
+ }
1815
+ itemNodes.push(createElement('item', {
1816
+ t: 'default'
1817
+ }, []));
1818
+ children.push(createElement('items', {
1819
+ count: String(itemNodes.length)
1820
+ }, itemNodes));
1821
+ }
1822
+ } else if (valueField) {
1823
+ attrs.dataField = '1';
1824
+ attrs.showAll = '0';
1825
+ } else {
1826
+ attrs.showAll = '0';
1827
+ }
1828
+ return createElement('pivotField', attrs, children);
1829
+ }
1830
+ /**
1831
+ * Build row items based on unique values in row fields
1832
+ */ _buildRowItems() {
1833
+ const items = [];
1834
+ if (this._rowFields.length === 0) return items;
1835
+ // Get unique values from first row field
1836
+ const firstRowField = this._rowFields[0];
1837
+ const cacheField = this._cache.fields[firstRowField.fieldIndex];
1838
+ if (cacheField && cacheField.sharedItems.length > 0) {
1839
+ for(let i = 0; i < cacheField.sharedItems.length; i++){
1840
+ items.push(createElement('i', {}, [
1841
+ createElement('x', i === 0 ? {} : {
1842
+ v: String(i)
1843
+ }, [])
1844
+ ]));
1845
+ }
1846
+ }
1847
+ // Add grand total row
1848
+ items.push(createElement('i', {
1849
+ t: 'grand'
1850
+ }, [
1851
+ createElement('x', {}, [])
1852
+ ]));
1853
+ return items;
1854
+ }
1855
+ /**
1856
+ * Build column items based on unique values in column fields
1857
+ */ _buildColItems() {
1858
+ const items = [];
1859
+ if (this._columnFields.length === 0) return items;
1860
+ // Get unique values from first column field
1861
+ const firstColField = this._columnFields[0];
1862
+ const cacheField = this._cache.fields[firstColField.fieldIndex];
1863
+ if (cacheField && cacheField.sharedItems.length > 0) {
1864
+ if (this._valueFields.length > 1) {
1865
+ // Multiple value fields - need nested items for each column value + value field combination
1866
+ for(let colIdx = 0; colIdx < cacheField.sharedItems.length; colIdx++){
1867
+ for(let valIdx = 0; valIdx < this._valueFields.length; valIdx++){
1868
+ const xNodes = [
1869
+ createElement('x', colIdx === 0 ? {} : {
1870
+ v: String(colIdx)
1871
+ }, []),
1872
+ createElement('x', valIdx === 0 ? {} : {
1873
+ v: String(valIdx)
1874
+ }, [])
1875
+ ];
1876
+ items.push(createElement('i', {}, xNodes));
1877
+ }
1878
+ }
1879
+ } else {
1880
+ // Single value field - simple column items
1881
+ for(let i = 0; i < cacheField.sharedItems.length; i++){
1882
+ items.push(createElement('i', {}, [
1883
+ createElement('x', i === 0 ? {} : {
1884
+ v: String(i)
1885
+ }, [])
1886
+ ]));
1887
+ }
1888
+ }
1889
+ }
1890
+ // Add grand total column(s)
1891
+ if (this._valueFields.length > 1) {
1892
+ // Grand total for each value field
1893
+ for(let valIdx = 0; valIdx < this._valueFields.length; valIdx++){
1894
+ const xNodes = [
1895
+ createElement('x', {}, []),
1896
+ createElement('x', valIdx === 0 ? {} : {
1897
+ v: String(valIdx)
1898
+ }, [])
1899
+ ];
1900
+ items.push(createElement('i', {
1901
+ t: 'grand'
1902
+ }, xNodes));
1903
+ }
1904
+ } else {
1905
+ items.push(createElement('i', {
1906
+ t: 'grand'
1907
+ }, [
1908
+ createElement('x', {}, [])
1909
+ ]));
1910
+ }
1911
+ return items;
1912
+ }
1913
+ /**
1914
+ * Calculate the location reference for the pivot table output
1915
+ */ _calculateLocationRef() {
1916
+ // Estimate output size based on fields
1917
+ const numRows = this._estimateRowCount();
1918
+ const numCols = this._estimateColCount();
1919
+ const startRow = this._targetRow;
1920
+ const startCol = this._targetCol;
1921
+ const endRow = startRow + numRows - 1;
1922
+ const endCol = startCol + numCols - 1;
1923
+ return `${this._colToLetter(startCol)}${startRow}:${this._colToLetter(endCol)}${endRow}`;
1924
+ }
1925
+ /**
1926
+ * Estimate number of rows in pivot table output
1927
+ */ _estimateRowCount() {
1928
+ let count = 1; // Header row
1929
+ // Add filter area rows
1930
+ count += this._filterFields.length;
1931
+ // Add row labels (unique values in row fields)
1932
+ if (this._rowFields.length > 0) {
1933
+ const firstRowField = this._rowFields[0];
1934
+ const cacheField = this._cache.fields[firstRowField.fieldIndex];
1935
+ count += (cacheField?.sharedItems.length || 1) + 1; // +1 for grand total
1936
+ } else {
1937
+ count += 1; // At least one data row
1938
+ }
1939
+ return Math.max(count, 3);
1940
+ }
1941
+ /**
1942
+ * Estimate number of columns in pivot table output
1943
+ */ _estimateColCount() {
1944
+ let count = 0;
1945
+ // Row label columns
1946
+ count += Math.max(this._rowFields.length, 1);
1947
+ // Column labels (unique values in column fields)
1948
+ if (this._columnFields.length > 0) {
1949
+ const firstColField = this._columnFields[0];
1950
+ const cacheField = this._cache.fields[firstColField.fieldIndex];
1951
+ count += (cacheField?.sharedItems.length || 1) + 1; // +1 for grand total
1952
+ } else {
1953
+ // Value columns
1954
+ count += Math.max(this._valueFields.length, 1);
1955
+ }
1956
+ return Math.max(count, 2);
1957
+ }
1958
+ /**
1959
+ * Convert 0-based column index to letter (A, B, ..., Z, AA, etc.)
1960
+ */ _colToLetter(col) {
1961
+ let result = '';
1962
+ let n = col;
1963
+ while(n >= 0){
1964
+ result = String.fromCharCode(n % 26 + 65) + result;
1965
+ n = Math.floor(n / 26) - 1;
1966
+ }
1967
+ return result;
1968
+ }
1969
+ }
1970
+
1971
+ /**
1972
+ * Manages the pivot cache (definition and records) for a pivot table.
1973
+ * The cache stores source data metadata and cached values.
1974
+ */ class PivotCache {
1975
+ constructor(cacheId, sourceSheet, sourceRange){
1976
+ this._fields = [];
1977
+ this._records = [];
1978
+ this._recordCount = 0;
1979
+ this._refreshOnLoad = true; // Default to true
1980
+ this._cacheId = cacheId;
1981
+ this._sourceSheet = sourceSheet;
1982
+ this._sourceRange = sourceRange;
1983
+ }
1984
+ /**
1985
+ * Get the cache ID
1986
+ */ get cacheId() {
1987
+ return this._cacheId;
1988
+ }
1989
+ /**
1990
+ * Set refreshOnLoad option
1991
+ */ set refreshOnLoad(value) {
1992
+ this._refreshOnLoad = value;
1993
+ }
1994
+ /**
1995
+ * Get refreshOnLoad option
1996
+ */ get refreshOnLoad() {
1997
+ return this._refreshOnLoad;
1998
+ }
1999
+ /**
2000
+ * Get the source sheet name
2001
+ */ get sourceSheet() {
2002
+ return this._sourceSheet;
2003
+ }
2004
+ /**
2005
+ * Get the source range
2006
+ */ get sourceRange() {
2007
+ return this._sourceRange;
2008
+ }
2009
+ /**
2010
+ * Get the full source reference (Sheet!Range)
2011
+ */ get sourceRef() {
2012
+ return `${this._sourceSheet}!${this._sourceRange}`;
2013
+ }
2014
+ /**
2015
+ * Get the fields in this cache
2016
+ */ get fields() {
2017
+ return this._fields;
2018
+ }
2019
+ /**
2020
+ * Get the number of data records
2021
+ */ get recordCount() {
2022
+ return this._recordCount;
2023
+ }
2024
+ /**
2025
+ * Build the cache from source data.
2026
+ * @param headers - Array of column header names
2027
+ * @param data - 2D array of data rows (excluding headers)
2028
+ */ buildFromData(headers, data) {
2029
+ this._recordCount = data.length;
2030
+ // Initialize fields from headers
2031
+ this._fields = headers.map((name, index)=>({
2032
+ name,
2033
+ index,
2034
+ isNumeric: true,
2035
+ isDate: false,
2036
+ sharedItems: [],
2037
+ minValue: undefined,
2038
+ maxValue: undefined
2039
+ }));
2040
+ // Analyze data to determine field types and collect unique values
2041
+ for (const row of data){
2042
+ for(let colIdx = 0; colIdx < row.length && colIdx < this._fields.length; colIdx++){
2043
+ const value = row[colIdx];
2044
+ const field = this._fields[colIdx];
2045
+ if (value === null || value === undefined) {
2046
+ continue;
2047
+ }
2048
+ if (typeof value === 'string') {
2049
+ field.isNumeric = false;
2050
+ if (!field.sharedItems.includes(value)) {
2051
+ field.sharedItems.push(value);
2052
+ }
2053
+ } else if (typeof value === 'number') {
2054
+ if (field.minValue === undefined || value < field.minValue) {
2055
+ field.minValue = value;
2056
+ }
2057
+ if (field.maxValue === undefined || value > field.maxValue) {
2058
+ field.maxValue = value;
2059
+ }
2060
+ } else if (value instanceof Date) {
2061
+ field.isDate = true;
2062
+ field.isNumeric = false;
2063
+ } else if (typeof value === 'boolean') {
2064
+ field.isNumeric = false;
2065
+ }
2066
+ }
2067
+ }
2068
+ // Store records
2069
+ this._records = data;
2070
+ }
2071
+ /**
2072
+ * Get field by name
2073
+ */ getField(name) {
2074
+ return this._fields.find((f)=>f.name === name);
2075
+ }
2076
+ /**
2077
+ * Get field index by name
2078
+ */ getFieldIndex(name) {
2079
+ const field = this._fields.find((f)=>f.name === name);
2080
+ return field ? field.index : -1;
2081
+ }
2082
+ /**
2083
+ * Generate the pivotCacheDefinition XML
2084
+ */ toDefinitionXml(recordsRelId) {
2085
+ const cacheFieldNodes = this._fields.map((field)=>{
2086
+ const sharedItemsAttrs = {};
2087
+ const sharedItemChildren = [];
2088
+ if (field.sharedItems.length > 0) {
2089
+ // String field with shared items - Excel just uses count attribute
2090
+ sharedItemsAttrs.count = String(field.sharedItems.length);
2091
+ for (const item of field.sharedItems){
2092
+ sharedItemChildren.push(createElement('s', {
2093
+ v: item
2094
+ }, []));
2095
+ }
2096
+ } else if (field.isNumeric) {
2097
+ // Numeric field - use "0"/"1" for boolean attributes as Excel expects
2098
+ sharedItemsAttrs.containsSemiMixedTypes = '0';
2099
+ sharedItemsAttrs.containsString = '0';
2100
+ sharedItemsAttrs.containsNumber = '1';
2101
+ // Check if all values are integers
2102
+ if (field.minValue !== undefined && field.maxValue !== undefined) {
2103
+ const isInteger = Number.isInteger(field.minValue) && Number.isInteger(field.maxValue);
2104
+ if (isInteger) {
2105
+ sharedItemsAttrs.containsInteger = '1';
2106
+ }
2107
+ sharedItemsAttrs.minValue = String(field.minValue);
2108
+ sharedItemsAttrs.maxValue = String(field.maxValue);
2109
+ }
2110
+ }
2111
+ const sharedItemsNode = createElement('sharedItems', sharedItemsAttrs, sharedItemChildren);
2112
+ return createElement('cacheField', {
2113
+ name: field.name,
2114
+ numFmtId: '0'
2115
+ }, [
2116
+ sharedItemsNode
2117
+ ]);
2118
+ });
2119
+ const cacheFieldsNode = createElement('cacheFields', {
2120
+ count: String(this._fields.length)
2121
+ }, cacheFieldNodes);
2122
+ const worksheetSourceNode = createElement('worksheetSource', {
2123
+ ref: this._sourceRange,
2124
+ sheet: this._sourceSheet
2125
+ }, []);
2126
+ const cacheSourceNode = createElement('cacheSource', {
2127
+ type: 'worksheet'
2128
+ }, [
2129
+ worksheetSourceNode
2130
+ ]);
2131
+ // Build attributes - refreshOnLoad should come early per OOXML schema
2132
+ const definitionAttrs = {
2133
+ xmlns: 'http://schemas.openxmlformats.org/spreadsheetml/2006/main',
2134
+ 'xmlns:r': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships',
2135
+ 'r:id': recordsRelId
2136
+ };
2137
+ // Add refreshOnLoad early in attributes (default is true)
2138
+ if (this._refreshOnLoad) {
2139
+ definitionAttrs.refreshOnLoad = '1';
2140
+ }
2141
+ // Continue with remaining attributes
2142
+ definitionAttrs.refreshedBy = 'User';
2143
+ definitionAttrs.refreshedVersion = '8';
2144
+ definitionAttrs.minRefreshableVersion = '3';
2145
+ definitionAttrs.createdVersion = '8';
2146
+ definitionAttrs.recordCount = String(this._recordCount);
2147
+ const definitionNode = createElement('pivotCacheDefinition', definitionAttrs, [
2148
+ cacheSourceNode,
2149
+ cacheFieldsNode
2150
+ ]);
2151
+ return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n${stringifyXml([
2152
+ definitionNode
2153
+ ])}`;
2154
+ }
2155
+ /**
2156
+ * Generate the pivotCacheRecords XML
2157
+ */ toRecordsXml() {
2158
+ const recordNodes = [];
2159
+ for (const row of this._records){
2160
+ const fieldNodes = [];
2161
+ for(let colIdx = 0; colIdx < this._fields.length; colIdx++){
2162
+ const field = this._fields[colIdx];
2163
+ const value = colIdx < row.length ? row[colIdx] : null;
2164
+ if (value === null || value === undefined) {
2165
+ // Missing value
2166
+ fieldNodes.push(createElement('m', {}, []));
2167
+ } else if (typeof value === 'string') {
2168
+ // String value - use index into sharedItems
2169
+ const idx = field.sharedItems.indexOf(value);
2170
+ if (idx >= 0) {
2171
+ fieldNodes.push(createElement('x', {
2172
+ v: String(idx)
2173
+ }, []));
2174
+ } else {
2175
+ // Direct string value (shouldn't happen if cache is built correctly)
2176
+ fieldNodes.push(createElement('s', {
2177
+ v: value
2178
+ }, []));
2179
+ }
2180
+ } else if (typeof value === 'number') {
2181
+ fieldNodes.push(createElement('n', {
2182
+ v: String(value)
2183
+ }, []));
2184
+ } else if (typeof value === 'boolean') {
2185
+ fieldNodes.push(createElement('b', {
2186
+ v: value ? '1' : '0'
2187
+ }, []));
2188
+ } else if (value instanceof Date) {
2189
+ fieldNodes.push(createElement('d', {
2190
+ v: value.toISOString()
2191
+ }, []));
2192
+ } else {
2193
+ // Unknown type, treat as missing
2194
+ fieldNodes.push(createElement('m', {}, []));
2195
+ }
2196
+ }
2197
+ recordNodes.push(createElement('r', {}, fieldNodes));
2198
+ }
2199
+ const recordsNode = createElement('pivotCacheRecords', {
2200
+ xmlns: 'http://schemas.openxmlformats.org/spreadsheetml/2006/main',
2201
+ 'xmlns:r': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships',
2202
+ count: String(this._recordCount)
2203
+ }, recordNodes);
2204
+ return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n${stringifyXml([
2205
+ recordsNode
2206
+ ])}`;
2207
+ }
2208
+ }
2209
+
2210
+ /**
2211
+ * Reads a ZIP file and returns a map of path -> content
2212
+ * @param data - ZIP file as Uint8Array
2213
+ * @returns Promise resolving to a map of file paths to contents
2214
+ */ const readZip = (data)=>{
2215
+ return new Promise((resolve, reject)=>{
2216
+ fflate.unzip(data, (err, result)=>{
2217
+ if (err) {
2218
+ reject(err);
2219
+ return;
2220
+ }
2221
+ const files = new Map();
2222
+ for (const [path, content] of Object.entries(result)){
2223
+ files.set(path, content);
2224
+ }
2225
+ resolve(files);
2226
+ });
2227
+ });
2228
+ };
2229
+ /**
2230
+ * Creates a ZIP file from a map of path -> content
2231
+ * @param files - Map of file paths to contents
2232
+ * @returns Promise resolving to ZIP file as Uint8Array
2233
+ */ const writeZip = (files)=>{
2234
+ return new Promise((resolve, reject)=>{
2235
+ const zipData = {};
2236
+ for (const [path, content] of files){
2237
+ zipData[path] = content;
2238
+ }
2239
+ fflate.zip(zipData, (err, result)=>{
2240
+ if (err) {
2241
+ reject(err);
2242
+ return;
2243
+ }
2244
+ resolve(result);
2245
+ });
2246
+ });
2247
+ };
2248
+ /**
2249
+ * Reads a file from the ZIP as a UTF-8 string
2250
+ */ const readZipText = (files, path)=>{
2251
+ const data = files.get(path);
2252
+ if (!data) return undefined;
2253
+ return fflate.strFromU8(data);
2254
+ };
2255
+ /**
2256
+ * Writes a UTF-8 string to the ZIP files map
2257
+ */ const writeZipText = (files, path, content)=>{
2258
+ files.set(path, fflate.strToU8(content));
2259
+ };
2260
+
2261
+ /**
2262
+ * Represents an Excel workbook (.xlsx file)
2263
+ */ class Workbook {
2264
+ constructor(){
2265
+ this._files = new Map();
2266
+ this._sheets = new Map();
2267
+ this._sheetDefs = [];
2268
+ this._relationships = [];
2269
+ this._dirty = false;
2270
+ // Pivot table support
2271
+ this._pivotTables = [];
2272
+ this._pivotCaches = [];
2273
+ this._nextCacheId = 0;
2274
+ this._sharedStrings = new SharedStrings();
2275
+ this._styles = Styles.createDefault();
2276
+ }
2277
+ /**
2278
+ * Load a workbook from a file path
2279
+ */ static async fromFile(path) {
2280
+ const data = await promises.readFile(path);
2281
+ return Workbook.fromBuffer(new Uint8Array(data));
2282
+ }
2283
+ /**
2284
+ * Load a workbook from a buffer
2285
+ */ static async fromBuffer(data) {
2286
+ const workbook = new Workbook();
2287
+ workbook._files = await readZip(data);
2288
+ // Parse workbook.xml for sheet definitions
2289
+ const workbookXml = readZipText(workbook._files, 'xl/workbook.xml');
2290
+ if (workbookXml) {
2291
+ workbook._parseWorkbook(workbookXml);
2292
+ }
2293
+ // Parse relationships
2294
+ const relsXml = readZipText(workbook._files, 'xl/_rels/workbook.xml.rels');
2295
+ if (relsXml) {
2296
+ workbook._parseRelationships(relsXml);
2297
+ }
2298
+ // Parse shared strings
2299
+ const sharedStringsXml = readZipText(workbook._files, 'xl/sharedStrings.xml');
2300
+ if (sharedStringsXml) {
2301
+ workbook._sharedStrings = SharedStrings.parse(sharedStringsXml);
2302
+ }
2303
+ // Parse styles
2304
+ const stylesXml = readZipText(workbook._files, 'xl/styles.xml');
2305
+ if (stylesXml) {
2306
+ workbook._styles = Styles.parse(stylesXml);
2307
+ }
2308
+ return workbook;
2309
+ }
2310
+ /**
2311
+ * Create a new empty workbook
2312
+ */ static create() {
2313
+ const workbook = new Workbook();
2314
+ workbook._dirty = true;
2315
+ // Add default sheet
2316
+ workbook.addSheet('Sheet1');
2317
+ return workbook;
2318
+ }
2319
+ /**
2320
+ * Get sheet names
2321
+ */ get sheetNames() {
2322
+ return this._sheetDefs.map((s)=>s.name);
2323
+ }
2324
+ /**
2325
+ * Get number of sheets
2326
+ */ get sheetCount() {
2327
+ return this._sheetDefs.length;
2328
+ }
2329
+ /**
2330
+ * Get shared strings table
2331
+ */ get sharedStrings() {
2332
+ return this._sharedStrings;
2333
+ }
2334
+ /**
2335
+ * Get styles
2336
+ */ get styles() {
2337
+ return this._styles;
2338
+ }
2339
+ /**
2340
+ * Get a worksheet by name or index
2341
+ */ sheet(nameOrIndex) {
2342
+ let def;
2343
+ if (typeof nameOrIndex === 'number') {
2344
+ def = this._sheetDefs[nameOrIndex];
2345
+ } else {
2346
+ def = this._sheetDefs.find((s)=>s.name === nameOrIndex);
2347
+ }
2348
+ if (!def) {
2349
+ throw new Error(`Sheet not found: ${nameOrIndex}`);
2350
+ }
2351
+ // Return cached worksheet if available
2352
+ if (this._sheets.has(def.name)) {
2353
+ return this._sheets.get(def.name);
2354
+ }
2355
+ // Load worksheet
2356
+ const worksheet = new Worksheet(this, def.name);
2357
+ // Find the relationship to get the file path
2358
+ const rel = this._relationships.find((r)=>r.id === def.rId);
2359
+ if (rel) {
2360
+ const sheetPath = `xl/${rel.target}`;
2361
+ const sheetXml = readZipText(this._files, sheetPath);
2362
+ if (sheetXml) {
2363
+ worksheet.parse(sheetXml);
2364
+ }
2365
+ }
2366
+ this._sheets.set(def.name, worksheet);
2367
+ return worksheet;
2368
+ }
2369
+ /**
2370
+ * Add a new worksheet
2371
+ */ addSheet(name, index) {
2372
+ // Check for duplicate name
2373
+ if (this._sheetDefs.some((s)=>s.name === name)) {
2374
+ throw new Error(`Sheet already exists: ${name}`);
2375
+ }
2376
+ this._dirty = true;
2377
+ // Generate new sheet ID and relationship ID
2378
+ const sheetId = Math.max(0, ...this._sheetDefs.map((s)=>s.sheetId)) + 1;
2379
+ const rId = `rId${Math.max(0, ...this._relationships.map((r)=>parseInt(r.id.replace('rId', ''), 10) || 0)) + 1}`;
2380
+ const def = {
2381
+ name,
2382
+ sheetId,
2383
+ rId
2384
+ };
2385
+ // Add relationship
2386
+ this._relationships.push({
2387
+ id: rId,
2388
+ type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet',
2389
+ target: `worksheets/sheet${sheetId}.xml`
2390
+ });
2391
+ // Insert at index or append
2392
+ if (index !== undefined && index >= 0 && index < this._sheetDefs.length) {
2393
+ this._sheetDefs.splice(index, 0, def);
2394
+ } else {
2395
+ this._sheetDefs.push(def);
2396
+ }
2397
+ // Create worksheet
2398
+ const worksheet = new Worksheet(this, name);
2399
+ this._sheets.set(name, worksheet);
2400
+ return worksheet;
2401
+ }
2402
+ /**
2403
+ * Delete a worksheet by name or index
2404
+ */ deleteSheet(nameOrIndex) {
2405
+ let index;
2406
+ if (typeof nameOrIndex === 'number') {
2407
+ index = nameOrIndex;
2408
+ } else {
2409
+ index = this._sheetDefs.findIndex((s)=>s.name === nameOrIndex);
2410
+ }
2411
+ if (index < 0 || index >= this._sheetDefs.length) {
2412
+ throw new Error(`Sheet not found: ${nameOrIndex}`);
2413
+ }
2414
+ if (this._sheetDefs.length === 1) {
2415
+ throw new Error('Cannot delete the last sheet');
2416
+ }
2417
+ this._dirty = true;
2418
+ const def = this._sheetDefs[index];
2419
+ this._sheetDefs.splice(index, 1);
2420
+ this._sheets.delete(def.name);
2421
+ // Remove relationship
2422
+ const relIndex = this._relationships.findIndex((r)=>r.id === def.rId);
2423
+ if (relIndex >= 0) {
2424
+ this._relationships.splice(relIndex, 1);
2425
+ }
2426
+ }
2427
+ /**
2428
+ * Rename a worksheet
2429
+ */ renameSheet(oldName, newName) {
2430
+ const def = this._sheetDefs.find((s)=>s.name === oldName);
2431
+ if (!def) {
2432
+ throw new Error(`Sheet not found: ${oldName}`);
2433
+ }
2434
+ if (this._sheetDefs.some((s)=>s.name === newName)) {
2435
+ throw new Error(`Sheet already exists: ${newName}`);
2436
+ }
2437
+ this._dirty = true;
2438
+ // Update cached worksheet
2439
+ const worksheet = this._sheets.get(oldName);
2440
+ if (worksheet) {
2441
+ worksheet.name = newName;
2442
+ this._sheets.delete(oldName);
2443
+ this._sheets.set(newName, worksheet);
2444
+ }
2445
+ def.name = newName;
2446
+ }
2447
+ /**
2448
+ * Copy a worksheet
2449
+ */ copySheet(sourceName, newName) {
2450
+ const source = this.sheet(sourceName);
2451
+ const copy = this.addSheet(newName);
2452
+ // Copy all cells
2453
+ for (const [address, cell] of source.cells){
2454
+ const newCell = copy.cell(address);
2455
+ newCell.value = cell.value;
2456
+ if (cell.formula) {
2457
+ newCell.formula = cell.formula;
2458
+ }
2459
+ if (cell.styleIndex !== undefined) {
2460
+ newCell.styleIndex = cell.styleIndex;
2461
+ }
2462
+ }
2463
+ // Copy merged cells
2464
+ for (const mergedRange of source.mergedCells){
2465
+ copy.mergeCells(mergedRange);
2466
+ }
2467
+ return copy;
2468
+ }
2469
+ /**
2470
+ * Create a pivot table from source data.
2471
+ *
2472
+ * @param config - Pivot table configuration
2473
+ * @returns PivotTable instance for fluent configuration
2474
+ *
2475
+ * @example
2476
+ * ```typescript
2477
+ * const pivot = wb.createPivotTable({
2478
+ * name: 'SalesPivot',
2479
+ * source: 'DataSheet!A1:D100',
2480
+ * target: 'PivotSheet!A3',
2481
+ * });
2482
+ *
2483
+ * pivot
2484
+ * .addRowField('Region')
2485
+ * .addColumnField('Product')
2486
+ * .addValueField('Sales', 'sum', 'Total Sales');
2487
+ * ```
2488
+ */ createPivotTable(config) {
2489
+ this._dirty = true;
2490
+ // Parse source reference (Sheet!Range)
2491
+ const { sheetName: sourceSheet, range: sourceRange } = this._parseSheetRef(config.source);
2492
+ // Parse target reference
2493
+ const { sheetName: targetSheet, range: targetCell } = this._parseSheetRef(config.target);
2494
+ // Ensure target sheet exists
2495
+ if (!this._sheetDefs.some((s)=>s.name === targetSheet)) {
2496
+ this.addSheet(targetSheet);
2497
+ }
2498
+ // Parse target cell address
2499
+ const targetAddr = parseAddress(targetCell);
2500
+ // Get source worksheet and extract data
2501
+ const sourceWs = this.sheet(sourceSheet);
2502
+ const { headers, data } = this._extractSourceData(sourceWs, sourceRange);
2503
+ // Create pivot cache
2504
+ const cacheId = this._nextCacheId++;
2505
+ const cache = new PivotCache(cacheId, sourceSheet, sourceRange);
2506
+ cache.buildFromData(headers, data);
2507
+ // refreshOnLoad defaults to true; only disable if explicitly set to false
2508
+ if (config.refreshOnLoad === false) {
2509
+ cache.refreshOnLoad = false;
2510
+ }
2511
+ this._pivotCaches.push(cache);
2512
+ // Create pivot table
2513
+ const pivotTableIndex = this._pivotTables.length + 1;
2514
+ const pivotTable = new PivotTable(config.name, cache, targetSheet, targetCell, targetAddr.row + 1, targetAddr.col, pivotTableIndex);
2515
+ this._pivotTables.push(pivotTable);
2516
+ return pivotTable;
2517
+ }
2518
+ /**
2519
+ * Parse a sheet reference like "Sheet1!A1:D100" into sheet name and range
2520
+ */ _parseSheetRef(ref) {
2521
+ const match = ref.match(/^(.+?)!(.+)$/);
2522
+ if (!match) {
2523
+ throw new Error(`Invalid reference format: ${ref}. Expected "SheetName!Range"`);
2524
+ }
2525
+ return {
2526
+ sheetName: match[1],
2527
+ range: match[2]
2528
+ };
2529
+ }
2530
+ /**
2531
+ * Extract headers and data from a source range
2532
+ */ _extractSourceData(sheet, rangeStr) {
2533
+ const range = parseRange(rangeStr);
2534
+ const headers = [];
2535
+ const data = [];
2536
+ // First row is headers
2537
+ for(let col = range.start.col; col <= range.end.col; col++){
2538
+ const cell = sheet.cell(toAddress(range.start.row, col));
2539
+ headers.push(String(cell.value ?? `Column${col + 1}`));
2540
+ }
2541
+ // Remaining rows are data
2542
+ for(let row = range.start.row + 1; row <= range.end.row; row++){
2543
+ const rowData = [];
2544
+ for(let col = range.start.col; col <= range.end.col; col++){
2545
+ const cell = sheet.cell(toAddress(row, col));
2546
+ rowData.push(cell.value);
2547
+ }
2548
+ data.push(rowData);
2549
+ }
2550
+ return {
2551
+ headers,
2552
+ data
2553
+ };
2554
+ }
2555
+ /**
2556
+ * Save the workbook to a file
2557
+ */ async toFile(path) {
2558
+ const buffer = await this.toBuffer();
2559
+ await promises.writeFile(path, buffer);
2560
+ }
2561
+ /**
2562
+ * Save the workbook to a buffer
2563
+ */ async toBuffer() {
2564
+ // Update files map with modified content
2565
+ this._updateFiles();
2566
+ // Write ZIP
2567
+ return writeZip(this._files);
2568
+ }
2569
+ _parseWorkbook(xml) {
2570
+ const parsed = parseXml(xml);
2571
+ const workbook = findElement(parsed, 'workbook');
2572
+ if (!workbook) return;
2573
+ const children = getChildren(workbook, 'workbook');
2574
+ const sheets = findElement(children, 'sheets');
2575
+ if (!sheets) return;
2576
+ for (const child of getChildren(sheets, 'sheets')){
2577
+ if ('sheet' in child) {
2578
+ const name = getAttr(child, 'name');
2579
+ const sheetId = getAttr(child, 'sheetId');
2580
+ const rId = getAttr(child, 'r:id');
2581
+ if (name && sheetId && rId) {
2582
+ this._sheetDefs.push({
2583
+ name,
2584
+ sheetId: parseInt(sheetId, 10),
2585
+ rId
2586
+ });
2587
+ }
2588
+ }
2589
+ }
2590
+ }
2591
+ _parseRelationships(xml) {
2592
+ const parsed = parseXml(xml);
2593
+ const rels = findElement(parsed, 'Relationships');
2594
+ if (!rels) return;
2595
+ for (const child of getChildren(rels, 'Relationships')){
2596
+ if ('Relationship' in child) {
2597
+ const id = getAttr(child, 'Id');
2598
+ const type = getAttr(child, 'Type');
2599
+ const target = getAttr(child, 'Target');
2600
+ if (id && type && target) {
2601
+ this._relationships.push({
2602
+ id,
2603
+ type,
2604
+ target
2605
+ });
2606
+ }
2607
+ }
2608
+ }
2609
+ }
2610
+ _updateFiles() {
2611
+ // Update workbook.xml
2612
+ this._updateWorkbookXml();
2613
+ // Update relationships
2614
+ this._updateRelationshipsXml();
2615
+ // Update content types
2616
+ this._updateContentTypes();
2617
+ // Update shared strings if modified
2618
+ if (this._sharedStrings.dirty || this._sharedStrings.count > 0) {
2619
+ writeZipText(this._files, 'xl/sharedStrings.xml', this._sharedStrings.toXml());
2620
+ }
2621
+ // Update styles if modified or if file doesn't exist yet
2622
+ if (this._styles.dirty || this._dirty || !this._files.has('xl/styles.xml')) {
2623
+ writeZipText(this._files, 'xl/styles.xml', this._styles.toXml());
2624
+ }
2625
+ // Update worksheets
2626
+ for (const [name, worksheet] of this._sheets){
2627
+ if (worksheet.dirty || this._dirty) {
2628
+ const def = this._sheetDefs.find((s)=>s.name === name);
2629
+ if (def) {
2630
+ const rel = this._relationships.find((r)=>r.id === def.rId);
2631
+ if (rel) {
2632
+ const sheetPath = `xl/${rel.target}`;
2633
+ writeZipText(this._files, sheetPath, worksheet.toXml());
2634
+ }
2635
+ }
2636
+ }
2637
+ }
2638
+ // Update pivot tables
2639
+ if (this._pivotTables.length > 0) {
2640
+ this._updatePivotTableFiles();
2641
+ }
2642
+ }
2643
+ _updateWorkbookXml() {
2644
+ const sheetNodes = this._sheetDefs.map((def)=>createElement('sheet', {
2645
+ name: def.name,
2646
+ sheetId: String(def.sheetId),
2647
+ 'r:id': def.rId
2648
+ }, []));
2649
+ const sheetsNode = createElement('sheets', {}, sheetNodes);
2650
+ const children = [
2651
+ sheetsNode
2652
+ ];
2653
+ // Add pivot caches if any
2654
+ if (this._pivotCaches.length > 0) {
2655
+ const pivotCacheNodes = this._pivotCaches.map((cache, idx)=>{
2656
+ // Cache relationship ID is after sheets, sharedStrings, and styles
2657
+ const cacheRelId = `rId${this._relationships.length + 3 + idx}`;
2658
+ return createElement('pivotCache', {
2659
+ cacheId: String(cache.cacheId),
2660
+ 'r:id': cacheRelId
2661
+ }, []);
2662
+ });
2663
+ children.push(createElement('pivotCaches', {}, pivotCacheNodes));
2664
+ }
2665
+ const workbookNode = createElement('workbook', {
2666
+ xmlns: 'http://schemas.openxmlformats.org/spreadsheetml/2006/main',
2667
+ 'xmlns:r': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships'
2668
+ }, children);
2669
+ const xml = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n${stringifyXml([
2670
+ workbookNode
2671
+ ])}`;
2672
+ writeZipText(this._files, 'xl/workbook.xml', xml);
2673
+ }
2674
+ _updateRelationshipsXml() {
2675
+ const relNodes = this._relationships.map((rel)=>createElement('Relationship', {
2676
+ Id: rel.id,
2677
+ Type: rel.type,
2678
+ Target: rel.target
2679
+ }, []));
2680
+ let nextRelId = this._relationships.length + 1;
2681
+ // Add shared strings relationship if needed
2682
+ if (this._sharedStrings.count > 0) {
2683
+ const hasSharedStrings = this._relationships.some((r)=>r.type === 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings');
2684
+ if (!hasSharedStrings) {
2685
+ relNodes.push(createElement('Relationship', {
2686
+ Id: `rId${nextRelId++}`,
2687
+ Type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings',
2688
+ Target: 'sharedStrings.xml'
2689
+ }, []));
2690
+ }
2691
+ }
2692
+ // Add styles relationship if needed
2693
+ const hasStyles = this._relationships.some((r)=>r.type === 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles');
2694
+ if (!hasStyles) {
2695
+ relNodes.push(createElement('Relationship', {
2696
+ Id: `rId${nextRelId++}`,
2697
+ Type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles',
2698
+ Target: 'styles.xml'
2699
+ }, []));
2700
+ }
2701
+ // Add pivot cache relationships
2702
+ for(let i = 0; i < this._pivotCaches.length; i++){
2703
+ relNodes.push(createElement('Relationship', {
2704
+ Id: `rId${nextRelId++}`,
2705
+ Type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotCacheDefinition',
2706
+ Target: `pivotCache/pivotCacheDefinition${i + 1}.xml`
2707
+ }, []));
2708
+ }
2709
+ const relsNode = createElement('Relationships', {
2710
+ xmlns: 'http://schemas.openxmlformats.org/package/2006/relationships'
2711
+ }, relNodes);
2712
+ const xml = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n${stringifyXml([
2713
+ relsNode
2714
+ ])}`;
2715
+ writeZipText(this._files, 'xl/_rels/workbook.xml.rels', xml);
2716
+ }
2717
+ _updateContentTypes() {
2718
+ const types = [
2719
+ createElement('Default', {
2720
+ Extension: 'rels',
2721
+ ContentType: 'application/vnd.openxmlformats-package.relationships+xml'
2722
+ }, []),
2723
+ createElement('Default', {
2724
+ Extension: 'xml',
2725
+ ContentType: 'application/xml'
2726
+ }, []),
2727
+ createElement('Override', {
2728
+ PartName: '/xl/workbook.xml',
2729
+ ContentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml'
2730
+ }, []),
2731
+ createElement('Override', {
2732
+ PartName: '/xl/styles.xml',
2733
+ ContentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml'
2734
+ }, [])
2735
+ ];
2736
+ // Add shared strings if present
2737
+ if (this._sharedStrings.count > 0) {
2738
+ types.push(createElement('Override', {
2739
+ PartName: '/xl/sharedStrings.xml',
2740
+ ContentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml'
2741
+ }, []));
2742
+ }
2743
+ // Add worksheets
2744
+ for (const def of this._sheetDefs){
2745
+ const rel = this._relationships.find((r)=>r.id === def.rId);
2746
+ if (rel) {
2747
+ types.push(createElement('Override', {
2748
+ PartName: `/xl/${rel.target}`,
2749
+ ContentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml'
2750
+ }, []));
2751
+ }
2752
+ }
2753
+ // Add pivot cache definitions and records
2754
+ for(let i = 0; i < this._pivotCaches.length; i++){
2755
+ types.push(createElement('Override', {
2756
+ PartName: `/xl/pivotCache/pivotCacheDefinition${i + 1}.xml`,
2757
+ ContentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCacheDefinition+xml'
2758
+ }, []));
2759
+ types.push(createElement('Override', {
2760
+ PartName: `/xl/pivotCache/pivotCacheRecords${i + 1}.xml`,
2761
+ ContentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCacheRecords+xml'
2762
+ }, []));
2763
+ }
2764
+ // Add pivot tables
2765
+ for(let i = 0; i < this._pivotTables.length; i++){
2766
+ types.push(createElement('Override', {
2767
+ PartName: `/xl/pivotTables/pivotTable${i + 1}.xml`,
2768
+ ContentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.pivotTable+xml'
2769
+ }, []));
2770
+ }
2771
+ const typesNode = createElement('Types', {
2772
+ xmlns: 'http://schemas.openxmlformats.org/package/2006/content-types'
2773
+ }, types);
2774
+ const xml = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n${stringifyXml([
2775
+ typesNode
2776
+ ])}`;
2777
+ writeZipText(this._files, '[Content_Types].xml', xml);
2778
+ // Also ensure _rels/.rels exists
2779
+ const rootRelsXml = readZipText(this._files, '_rels/.rels');
2780
+ if (!rootRelsXml) {
2781
+ const rootRels = createElement('Relationships', {
2782
+ xmlns: 'http://schemas.openxmlformats.org/package/2006/relationships'
2783
+ }, [
2784
+ createElement('Relationship', {
2785
+ Id: 'rId1',
2786
+ Type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument',
2787
+ Target: 'xl/workbook.xml'
2788
+ }, [])
2789
+ ]);
2790
+ writeZipText(this._files, '_rels/.rels', `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n${stringifyXml([
2791
+ rootRels
2792
+ ])}`);
2793
+ }
2794
+ }
2795
+ /**
2796
+ * Generate all pivot table related files
2797
+ */ _updatePivotTableFiles() {
2798
+ // Track which sheets have pivot tables for their .rels files
2799
+ const sheetPivotTables = new Map();
2800
+ for (const pivotTable of this._pivotTables){
2801
+ const sheetName = pivotTable.targetSheet;
2802
+ if (!sheetPivotTables.has(sheetName)) {
2803
+ sheetPivotTables.set(sheetName, []);
2804
+ }
2805
+ sheetPivotTables.get(sheetName).push(pivotTable);
2806
+ }
2807
+ // Generate pivot cache files
2808
+ for(let i = 0; i < this._pivotCaches.length; i++){
2809
+ const cache = this._pivotCaches[i];
2810
+ const cacheIdx = i + 1;
2811
+ // Pivot cache definition
2812
+ const definitionPath = `xl/pivotCache/pivotCacheDefinition${cacheIdx}.xml`;
2813
+ writeZipText(this._files, definitionPath, cache.toDefinitionXml('rId1'));
2814
+ // Pivot cache records
2815
+ const recordsPath = `xl/pivotCache/pivotCacheRecords${cacheIdx}.xml`;
2816
+ writeZipText(this._files, recordsPath, cache.toRecordsXml());
2817
+ // Pivot cache definition relationships (link to records)
2818
+ const cacheRelsPath = `xl/pivotCache/_rels/pivotCacheDefinition${cacheIdx}.xml.rels`;
2819
+ const cacheRels = createElement('Relationships', {
2820
+ xmlns: 'http://schemas.openxmlformats.org/package/2006/relationships'
2821
+ }, [
2822
+ createElement('Relationship', {
2823
+ Id: 'rId1',
2824
+ Type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotCacheRecords',
2825
+ Target: `pivotCacheRecords${cacheIdx}.xml`
2826
+ }, [])
2827
+ ]);
2828
+ writeZipText(this._files, cacheRelsPath, `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n${stringifyXml([
2829
+ cacheRels
2830
+ ])}`);
2831
+ }
2832
+ // Generate pivot table files
2833
+ for(let i = 0; i < this._pivotTables.length; i++){
2834
+ const pivotTable = this._pivotTables[i];
2835
+ const ptIdx = i + 1;
2836
+ // Pivot table definition
2837
+ const ptPath = `xl/pivotTables/pivotTable${ptIdx}.xml`;
2838
+ writeZipText(this._files, ptPath, pivotTable.toXml());
2839
+ // Pivot table relationships (link to cache definition)
2840
+ const cacheIdx = this._pivotCaches.indexOf(pivotTable.cache) + 1;
2841
+ const ptRelsPath = `xl/pivotTables/_rels/pivotTable${ptIdx}.xml.rels`;
2842
+ const ptRels = createElement('Relationships', {
2843
+ xmlns: 'http://schemas.openxmlformats.org/package/2006/relationships'
2844
+ }, [
2845
+ createElement('Relationship', {
2846
+ Id: 'rId1',
2847
+ Type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotCacheDefinition',
2848
+ Target: `../pivotCache/pivotCacheDefinition${cacheIdx}.xml`
2849
+ }, [])
2850
+ ]);
2851
+ writeZipText(this._files, ptRelsPath, `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n${stringifyXml([
2852
+ ptRels
2853
+ ])}`);
2854
+ }
2855
+ // Generate worksheet relationships for pivot tables
2856
+ for (const [sheetName, pivotTables] of sheetPivotTables){
2857
+ const def = this._sheetDefs.find((s)=>s.name === sheetName);
2858
+ if (!def) continue;
2859
+ const rel = this._relationships.find((r)=>r.id === def.rId);
2860
+ if (!rel) continue;
2861
+ // Extract sheet file name from target path
2862
+ const sheetFileName = rel.target.split('/').pop();
2863
+ const sheetRelsPath = `xl/worksheets/_rels/${sheetFileName}.rels`;
2864
+ const relNodes = [];
2865
+ for(let i = 0; i < pivotTables.length; i++){
2866
+ const pt = pivotTables[i];
2867
+ relNodes.push(createElement('Relationship', {
2868
+ Id: `rId${i + 1}`,
2869
+ Type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotTable',
2870
+ Target: `../pivotTables/pivotTable${pt.index}.xml`
2871
+ }, []));
2872
+ }
2873
+ const sheetRels = createElement('Relationships', {
2874
+ xmlns: 'http://schemas.openxmlformats.org/package/2006/relationships'
2875
+ }, relNodes);
2876
+ writeZipText(this._files, sheetRelsPath, `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n${stringifyXml([
2877
+ sheetRels
2878
+ ])}`);
2879
+ }
2880
+ }
2881
+ }
2882
+
2883
+ exports.Cell = Cell;
2884
+ exports.PivotCache = PivotCache;
2885
+ exports.PivotTable = PivotTable;
2886
+ exports.Range = Range;
2887
+ exports.SharedStrings = SharedStrings;
2888
+ exports.Styles = Styles;
2889
+ exports.Workbook = Workbook;
2890
+ exports.Worksheet = Worksheet;
2891
+ exports.parseAddress = parseAddress;
2892
+ exports.parseRange = parseRange;
2893
+ exports.toAddress = toAddress;
2894
+ exports.toRange = toRange;