@niicojs/excel 0.3.4 → 0.3.6
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/LICENSE +20 -20
- package/README.md +8 -2
- package/dist/index.cjs +1191 -1266
- package/dist/index.d.cts +171 -324
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +171 -324
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1191 -1267
- package/package.json +4 -4
- package/src/index.ts +8 -10
- package/src/pivot-table.ts +619 -524
- package/src/shared-strings.ts +33 -9
- package/src/styles.ts +38 -9
- package/src/types.ts +295 -323
- package/src/utils/address.ts +48 -0
- package/src/utils/format.ts +8 -7
- package/src/utils/xml.ts +7 -4
- package/src/utils/zip.ts +153 -11
- package/src/workbook.ts +330 -350
- package/src/worksheet.ts +1003 -935
- package/src/pivot-cache.ts +0 -449
package/src/worksheet.ts
CHANGED
|
@@ -1,951 +1,1019 @@
|
|
|
1
|
-
import type { CellData, RangeAddress, SheetToJsonConfig, CellValue, DateHandling, TableConfig } from './types';
|
|
2
|
-
import type { Workbook } from './workbook';
|
|
3
|
-
import { Cell, parseCellRef } from './cell';
|
|
4
|
-
import { Range } from './range';
|
|
5
|
-
import { Table } from './table';
|
|
6
|
-
import { parseRange, toAddress, parseAddress, letterToCol } from './utils/address';
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
private
|
|
23
|
-
private
|
|
24
|
-
private
|
|
25
|
-
private
|
|
26
|
-
private
|
|
27
|
-
private
|
|
28
|
-
private
|
|
29
|
-
private
|
|
30
|
-
private
|
|
31
|
-
private
|
|
32
|
-
private
|
|
33
|
-
private
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
if (
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
const
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
const
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
if (
|
|
384
|
-
throw new Error(
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
this.
|
|
388
|
-
this._colsDirty = true;
|
|
389
|
-
this._dirty = true;
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
/**
|
|
393
|
-
* Get a
|
|
394
|
-
*/
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
if (
|
|
407
|
-
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
*
|
|
461
|
-
* @
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
*
|
|
470
|
-
*
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
*
|
|
479
|
-
*
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
1
|
+
import type { CellData, RangeAddress, SheetToJsonConfig, CellValue, DateHandling, TableConfig } from './types';
|
|
2
|
+
import type { Workbook } from './workbook';
|
|
3
|
+
import { Cell, parseCellRef } from './cell';
|
|
4
|
+
import { Range } from './range';
|
|
5
|
+
import { Table } from './table';
|
|
6
|
+
import { parseRange, toAddress, parseAddress, letterToCol } from './utils/address';
|
|
7
|
+
import { findElement, getChildren, getAttr, XmlNode, stringifyXml, createElement, createText, parseXml } from './utils/xml';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Represents a worksheet in a workbook
|
|
11
|
+
*/
|
|
12
|
+
export class Worksheet {
|
|
13
|
+
private _name: string;
|
|
14
|
+
private _workbook: Workbook;
|
|
15
|
+
private _cells: Map<string, Cell> = new Map();
|
|
16
|
+
private _xmlNodes: XmlNode[] | null = null;
|
|
17
|
+
private _dirty = false;
|
|
18
|
+
private _mergedCells: Set<string> = new Set();
|
|
19
|
+
private _columnWidths: Map<number, number> = new Map();
|
|
20
|
+
private _rowHeights: Map<number, number> = new Map();
|
|
21
|
+
private _frozenPane: { row: number; col: number } | null = null;
|
|
22
|
+
private _dataBoundsCache: { minRow: number; maxRow: number; minCol: number; maxCol: number } | null = null;
|
|
23
|
+
private _boundsDirty = true;
|
|
24
|
+
private _tables: Table[] = [];
|
|
25
|
+
private _preserveXml = false;
|
|
26
|
+
private _rawXml: string | null = null;
|
|
27
|
+
private _lazyParse = false;
|
|
28
|
+
private _tableRelIds: string[] | null = null;
|
|
29
|
+
private _pivotTableRelIds: string[] | null = null;
|
|
30
|
+
private _sheetViewsDirty = false;
|
|
31
|
+
private _colsDirty = false;
|
|
32
|
+
private _tablePartsDirty = false;
|
|
33
|
+
private _pivotTablePartsDirty = false;
|
|
34
|
+
|
|
35
|
+
constructor(workbook: Workbook, name: string) {
|
|
36
|
+
this._workbook = workbook;
|
|
37
|
+
this._name = name;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Get the workbook this sheet belongs to
|
|
42
|
+
*/
|
|
43
|
+
get workbook(): Workbook {
|
|
44
|
+
return this._workbook;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Get the sheet name
|
|
49
|
+
*/
|
|
50
|
+
get name(): string {
|
|
51
|
+
return this._name;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Set the sheet name
|
|
56
|
+
*/
|
|
57
|
+
set name(value: string) {
|
|
58
|
+
this._name = value;
|
|
59
|
+
this._dirty = true;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Parse worksheet XML content
|
|
64
|
+
*/
|
|
65
|
+
parse(xml: string, options: { lazy?: boolean } = {}): void {
|
|
66
|
+
this._rawXml = xml;
|
|
67
|
+
this._xmlNodes = null;
|
|
68
|
+
this._preserveXml = true;
|
|
69
|
+
this._lazyParse = options.lazy ?? true;
|
|
70
|
+
if (!this._lazyParse) {
|
|
71
|
+
this._ensureParsed();
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private _ensureParsed(): void {
|
|
76
|
+
if (!this._lazyParse) return;
|
|
77
|
+
if (!this._rawXml) {
|
|
78
|
+
this._lazyParse = false;
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
this._xmlNodes = parseXml(this._rawXml);
|
|
83
|
+
this._preserveXml = true;
|
|
84
|
+
const worksheet = findElement(this._xmlNodes, 'worksheet');
|
|
85
|
+
if (!worksheet) {
|
|
86
|
+
this._lazyParse = false;
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const worksheetChildren = getChildren(worksheet, 'worksheet');
|
|
91
|
+
|
|
92
|
+
// Parse sheet views (freeze panes)
|
|
93
|
+
const sheetViews = findElement(worksheetChildren, 'sheetViews');
|
|
94
|
+
if (sheetViews) {
|
|
95
|
+
const viewChildren = getChildren(sheetViews, 'sheetViews');
|
|
96
|
+
const sheetView = findElement(viewChildren, 'sheetView');
|
|
97
|
+
if (sheetView) {
|
|
98
|
+
const sheetViewChildren = getChildren(sheetView, 'sheetView');
|
|
99
|
+
const pane = findElement(sheetViewChildren, 'pane');
|
|
100
|
+
if (pane && getAttr(pane, 'state') === 'frozen') {
|
|
101
|
+
const xSplit = parseInt(getAttr(pane, 'xSplit') || '0', 10);
|
|
102
|
+
const ySplit = parseInt(getAttr(pane, 'ySplit') || '0', 10);
|
|
103
|
+
if (xSplit > 0 || ySplit > 0) {
|
|
104
|
+
this._frozenPane = { row: ySplit, col: xSplit };
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Parse sheet data (cells)
|
|
111
|
+
const sheetData = findElement(worksheetChildren, 'sheetData');
|
|
112
|
+
if (sheetData) {
|
|
113
|
+
const rows = getChildren(sheetData, 'sheetData');
|
|
114
|
+
this._parseSheetData(rows);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Parse column widths
|
|
118
|
+
const cols = findElement(worksheetChildren, 'cols');
|
|
119
|
+
if (cols) {
|
|
120
|
+
const colChildren = getChildren(cols, 'cols');
|
|
121
|
+
for (const col of colChildren) {
|
|
122
|
+
if (!('col' in col)) continue;
|
|
123
|
+
const min = parseInt(getAttr(col, 'min') || '0', 10);
|
|
124
|
+
const max = parseInt(getAttr(col, 'max') || '0', 10);
|
|
125
|
+
const width = parseFloat(getAttr(col, 'width') || '0');
|
|
126
|
+
if (!Number.isFinite(width) || width <= 0) continue;
|
|
127
|
+
if (min > 0 && max > 0) {
|
|
128
|
+
for (let idx = min; idx <= max; idx++) {
|
|
129
|
+
this._columnWidths.set(idx - 1, width);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Parse merged cells
|
|
136
|
+
const mergeCells = findElement(worksheetChildren, 'mergeCells');
|
|
137
|
+
if (mergeCells) {
|
|
138
|
+
const mergeChildren = getChildren(mergeCells, 'mergeCells');
|
|
139
|
+
for (const mergeCell of mergeChildren) {
|
|
140
|
+
if ('mergeCell' in mergeCell) {
|
|
141
|
+
const ref = getAttr(mergeCell, 'ref');
|
|
142
|
+
if (ref) {
|
|
143
|
+
this._mergedCells.add(ref);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
this._lazyParse = false;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Parse the sheetData element to extract cells
|
|
154
|
+
*/
|
|
155
|
+
private _parseSheetData(rows: XmlNode[]): void {
|
|
156
|
+
for (const rowNode of rows) {
|
|
157
|
+
if (!('row' in rowNode)) continue;
|
|
158
|
+
|
|
159
|
+
const rowIndex = parseInt(getAttr(rowNode, 'r') || '0', 10) - 1;
|
|
160
|
+
const rowHeight = parseFloat(getAttr(rowNode, 'ht') || '0');
|
|
161
|
+
if (rowIndex >= 0 && Number.isFinite(rowHeight) && rowHeight > 0) {
|
|
162
|
+
this._rowHeights.set(rowIndex, rowHeight);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const rowChildren = getChildren(rowNode, 'row');
|
|
166
|
+
for (const cellNode of rowChildren) {
|
|
167
|
+
if (!('c' in cellNode)) continue;
|
|
168
|
+
|
|
169
|
+
const ref = getAttr(cellNode, 'r');
|
|
170
|
+
if (!ref) continue;
|
|
171
|
+
|
|
172
|
+
const { row, col } = parseAddress(ref);
|
|
173
|
+
const cellData = this._parseCellNode(cellNode);
|
|
174
|
+
const cell = new Cell(this, row, col, cellData);
|
|
175
|
+
this._cells.set(ref, cell);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
this._boundsDirty = true;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Parse a cell XML node to CellData
|
|
184
|
+
*/
|
|
185
|
+
private _parseCellNode(node: XmlNode): CellData {
|
|
186
|
+
const data: CellData = {};
|
|
187
|
+
|
|
188
|
+
// Type attribute
|
|
189
|
+
const t = getAttr(node, 't');
|
|
190
|
+
if (t) {
|
|
191
|
+
data.t = t as CellData['t'];
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Style attribute
|
|
195
|
+
const s = getAttr(node, 's');
|
|
196
|
+
if (s) {
|
|
197
|
+
data.s = parseInt(s, 10);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const children = getChildren(node, 'c');
|
|
201
|
+
|
|
202
|
+
// Value element
|
|
203
|
+
const vNode = findElement(children, 'v');
|
|
204
|
+
if (vNode) {
|
|
205
|
+
const vChildren = getChildren(vNode, 'v');
|
|
206
|
+
for (const child of vChildren) {
|
|
207
|
+
if ('#text' in child) {
|
|
208
|
+
const text = child['#text'] as string;
|
|
209
|
+
// Parse based on type
|
|
210
|
+
if (data.t === 's') {
|
|
211
|
+
data.v = parseInt(text, 10); // Shared string index
|
|
212
|
+
} else if (data.t === 'b') {
|
|
213
|
+
data.v = text === '1' ? 1 : 0;
|
|
214
|
+
} else if (data.t === 'e' || data.t === 'str') {
|
|
215
|
+
data.v = text;
|
|
216
|
+
} else {
|
|
217
|
+
// Number or default
|
|
218
|
+
data.v = parseFloat(text);
|
|
219
|
+
}
|
|
220
|
+
break;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Formula element
|
|
226
|
+
const fNode = findElement(children, 'f');
|
|
227
|
+
if (fNode) {
|
|
228
|
+
const fChildren = getChildren(fNode, 'f');
|
|
229
|
+
for (const child of fChildren) {
|
|
230
|
+
if ('#text' in child) {
|
|
231
|
+
data.f = child['#text'] as string;
|
|
232
|
+
break;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Check for shared formula
|
|
237
|
+
const si = getAttr(fNode, 'si');
|
|
238
|
+
if (si) {
|
|
239
|
+
data.si = parseInt(si, 10);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Check for array formula range
|
|
243
|
+
const ref = getAttr(fNode, 'ref');
|
|
244
|
+
if (ref) {
|
|
245
|
+
data.F = ref;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Inline string (is element)
|
|
250
|
+
const isNode = findElement(children, 'is');
|
|
251
|
+
if (isNode) {
|
|
252
|
+
data.t = 'str';
|
|
253
|
+
const isChildren = getChildren(isNode, 'is');
|
|
254
|
+
const tNode = findElement(isChildren, 't');
|
|
255
|
+
if (tNode) {
|
|
256
|
+
const tChildren = getChildren(tNode, 't');
|
|
257
|
+
for (const child of tChildren) {
|
|
258
|
+
if ('#text' in child) {
|
|
259
|
+
data.v = child['#text'] as string;
|
|
260
|
+
break;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return data;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Get a cell by address or row/col
|
|
271
|
+
*/
|
|
272
|
+
cell(rowOrAddress: number | string, col?: number): Cell {
|
|
273
|
+
this._ensureParsed();
|
|
274
|
+
const { row, col: c } = parseCellRef(rowOrAddress, col);
|
|
275
|
+
const address = toAddress(row, c);
|
|
276
|
+
|
|
277
|
+
let cell = this._cells.get(address);
|
|
278
|
+
if (!cell) {
|
|
279
|
+
cell = new Cell(this, row, c);
|
|
280
|
+
this._cells.set(address, cell);
|
|
281
|
+
this._boundsDirty = true;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return cell;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Get an existing cell without creating it.
|
|
289
|
+
*/
|
|
290
|
+
getCellIfExists(rowOrAddress: number | string, col?: number): Cell | undefined {
|
|
291
|
+
this._ensureParsed();
|
|
292
|
+
const { row, col: c } = parseCellRef(rowOrAddress, col);
|
|
293
|
+
const address = toAddress(row, c);
|
|
294
|
+
return this._cells.get(address);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Get a range of cells
|
|
299
|
+
*/
|
|
300
|
+
range(rangeStr: string): Range;
|
|
301
|
+
range(startRow: number, startCol: number, endRow: number, endCol: number): Range;
|
|
302
|
+
range(startRowOrRange: number | string, startCol?: number, endRow?: number, endCol?: number): Range {
|
|
303
|
+
this._ensureParsed();
|
|
304
|
+
let rangeAddr: RangeAddress;
|
|
305
|
+
|
|
306
|
+
if (typeof startRowOrRange === 'string') {
|
|
307
|
+
rangeAddr = parseRange(startRowOrRange);
|
|
308
|
+
} else {
|
|
309
|
+
if (startCol === undefined || endRow === undefined || endCol === undefined) {
|
|
310
|
+
throw new Error('All range parameters must be provided');
|
|
311
|
+
}
|
|
312
|
+
rangeAddr = {
|
|
313
|
+
start: { row: startRowOrRange, col: startCol },
|
|
314
|
+
end: { row: endRow, col: endCol },
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return new Range(this, rangeAddr);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Merge cells in the given range
|
|
323
|
+
*/
|
|
324
|
+
mergeCells(rangeOrStart: string, end?: string): void {
|
|
325
|
+
this._ensureParsed();
|
|
326
|
+
let rangeStr: string;
|
|
327
|
+
if (end) {
|
|
328
|
+
rangeStr = `${rangeOrStart}:${end}`;
|
|
329
|
+
} else {
|
|
330
|
+
rangeStr = rangeOrStart;
|
|
331
|
+
}
|
|
332
|
+
this._mergedCells.add(rangeStr);
|
|
333
|
+
this._dirty = true;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Unmerge cells in the given range
|
|
338
|
+
*/
|
|
339
|
+
unmergeCells(rangeStr: string): void {
|
|
340
|
+
this._ensureParsed();
|
|
341
|
+
this._mergedCells.delete(rangeStr);
|
|
342
|
+
this._dirty = true;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Get all merged cell ranges
|
|
347
|
+
*/
|
|
348
|
+
get mergedCells(): string[] {
|
|
349
|
+
this._ensureParsed();
|
|
350
|
+
return Array.from(this._mergedCells);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Check if the worksheet has been modified
|
|
355
|
+
*/
|
|
356
|
+
get dirty(): boolean {
|
|
357
|
+
this._ensureParsed();
|
|
358
|
+
if (this._dirty) return true;
|
|
359
|
+
for (const cell of this._cells.values()) {
|
|
360
|
+
if (cell.dirty) return true;
|
|
361
|
+
}
|
|
362
|
+
return false;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Get all cells in the worksheet
|
|
367
|
+
*/
|
|
368
|
+
get cells(): Map<string, Cell> {
|
|
369
|
+
this._ensureParsed();
|
|
370
|
+
return this._cells;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Set a column width (0-based index or column letter)
|
|
375
|
+
*/
|
|
376
|
+
setColumnWidth(col: number | string, width: number): void {
|
|
377
|
+
this._ensureParsed();
|
|
378
|
+
if (!Number.isFinite(width) || width <= 0) {
|
|
379
|
+
throw new Error('Column width must be a positive number');
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const colIndex = typeof col === 'number' ? col : letterToCol(col);
|
|
383
|
+
if (colIndex < 0) {
|
|
384
|
+
throw new Error(`Invalid column: ${col}`);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
this._columnWidths.set(colIndex, width);
|
|
388
|
+
this._colsDirty = true;
|
|
389
|
+
this._dirty = true;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Get a column width if set
|
|
394
|
+
*/
|
|
395
|
+
getColumnWidth(col: number | string): number | undefined {
|
|
396
|
+
this._ensureParsed();
|
|
397
|
+
const colIndex = typeof col === 'number' ? col : letterToCol(col);
|
|
398
|
+
return this._columnWidths.get(colIndex);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Set a row height (0-based index)
|
|
403
|
+
*/
|
|
404
|
+
setRowHeight(row: number, height: number): void {
|
|
405
|
+
this._ensureParsed();
|
|
406
|
+
if (!Number.isFinite(height) || height <= 0) {
|
|
407
|
+
throw new Error('Row height must be a positive number');
|
|
408
|
+
}
|
|
409
|
+
if (row < 0) {
|
|
410
|
+
throw new Error('Row index must be >= 0');
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
this._rowHeights.set(row, height);
|
|
414
|
+
this._colsDirty = true;
|
|
415
|
+
this._dirty = true;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Get a row height if set
|
|
420
|
+
*/
|
|
421
|
+
getRowHeight(row: number): number | undefined {
|
|
422
|
+
this._ensureParsed();
|
|
423
|
+
return this._rowHeights.get(row);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Freeze panes at a given row/column split (counts from top-left)
|
|
428
|
+
*/
|
|
429
|
+
freezePane(rowSplit: number, colSplit: number): void {
|
|
430
|
+
this._ensureParsed();
|
|
431
|
+
if (rowSplit < 0 || colSplit < 0) {
|
|
432
|
+
throw new Error('Freeze pane splits must be >= 0');
|
|
433
|
+
}
|
|
434
|
+
if (rowSplit === 0 && colSplit === 0) {
|
|
435
|
+
this._frozenPane = null;
|
|
436
|
+
} else {
|
|
437
|
+
this._frozenPane = { row: rowSplit, col: colSplit };
|
|
438
|
+
}
|
|
439
|
+
this._sheetViewsDirty = true;
|
|
440
|
+
this._dirty = true;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Get current frozen pane configuration
|
|
445
|
+
*/
|
|
446
|
+
getFrozenPane(): { row: number; col: number } | null {
|
|
447
|
+
this._ensureParsed();
|
|
448
|
+
return this._frozenPane ? { ...this._frozenPane } : null;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Get all tables in the worksheet
|
|
453
|
+
*/
|
|
454
|
+
get tables(): Table[] {
|
|
455
|
+
this._ensureParsed();
|
|
456
|
+
return [...this._tables];
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Get column width entries
|
|
461
|
+
* @internal
|
|
462
|
+
*/
|
|
463
|
+
getColumnWidths(): Map<number, number> {
|
|
464
|
+
this._ensureParsed();
|
|
465
|
+
return new Map(this._columnWidths);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Get row height entries
|
|
470
|
+
* @internal
|
|
471
|
+
*/
|
|
472
|
+
getRowHeights(): Map<number, number> {
|
|
473
|
+
this._ensureParsed();
|
|
474
|
+
return new Map(this._rowHeights);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Set table relationship IDs for tableParts generation.
|
|
479
|
+
* @internal
|
|
480
|
+
*/
|
|
481
|
+
setTableRelIds(ids: string[] | null): void {
|
|
482
|
+
this._ensureParsed();
|
|
483
|
+
this._tableRelIds = ids ? [...ids] : null;
|
|
484
|
+
this._tablePartsDirty = true;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* Set pivot table relationship IDs for pivotTableParts generation.
|
|
489
|
+
* @internal
|
|
490
|
+
*/
|
|
491
|
+
setPivotTableRelIds(ids: string[] | null): void {
|
|
492
|
+
this._ensureParsed();
|
|
493
|
+
this._pivotTableRelIds = ids ? [...ids] : null;
|
|
494
|
+
this._pivotTablePartsDirty = true;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Create an Excel Table (ListObject) from a data range.
|
|
499
|
+
*
|
|
500
|
+
* Tables provide structured data features like auto-filter, banded styling,
|
|
501
|
+
* and total row with aggregation functions.
|
|
502
|
+
*
|
|
503
|
+
* @param config - Table configuration
|
|
504
|
+
* @returns Table instance for method chaining
|
|
505
|
+
*
|
|
506
|
+
* @example
|
|
507
|
+
* ```typescript
|
|
508
|
+
* // Create a table with default styling
|
|
509
|
+
* const table = sheet.createTable({
|
|
510
|
+
* name: 'SalesData',
|
|
511
|
+
* range: 'A1:D10',
|
|
512
|
+
* });
|
|
513
|
+
*
|
|
514
|
+
* // Create a table with total row
|
|
515
|
+
* const table = sheet.createTable({
|
|
516
|
+
* name: 'SalesData',
|
|
517
|
+
* range: 'A1:D10',
|
|
518
|
+
* totalRow: true,
|
|
519
|
+
* style: { name: 'TableStyleMedium2' }
|
|
520
|
+
* });
|
|
521
|
+
*
|
|
522
|
+
* table.setTotalFunction('Sales', 'sum');
|
|
523
|
+
* ```
|
|
524
|
+
*/
|
|
525
|
+
createTable(config: TableConfig): Table {
|
|
526
|
+
this._ensureParsed();
|
|
527
|
+
// Validate table name is unique within the workbook
|
|
528
|
+
for (const sheet of this._workbook.sheetNames) {
|
|
529
|
+
const ws = this._workbook.sheet(sheet);
|
|
530
|
+
for (const table of ws._tables) {
|
|
531
|
+
if (table.name === config.name) {
|
|
532
|
+
throw new Error(`Table name already exists: ${config.name}`);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// Validate table name format (Excel rules: no spaces at start/end, alphanumeric + underscore)
|
|
538
|
+
if (!config.name || !/^[A-Za-z_\\][A-Za-z0-9_.\\]*$/.test(config.name)) {
|
|
539
|
+
throw new Error(
|
|
540
|
+
`Invalid table name: ${config.name}. Names must start with a letter or underscore and contain only alphanumeric characters, underscores, or periods.`,
|
|
541
|
+
);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// Create the table with a unique ID from the workbook
|
|
545
|
+
const tableId = this._workbook.getNextTableId();
|
|
546
|
+
const table = new Table(this, config, tableId);
|
|
547
|
+
|
|
548
|
+
this._tables.push(table);
|
|
549
|
+
this._tablePartsDirty = true;
|
|
550
|
+
this._dirty = true;
|
|
551
|
+
|
|
552
|
+
return table;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/**
|
|
512
556
|
* Convert sheet data to an array of JSON objects.
|
|
513
|
-
*
|
|
557
|
+
*
|
|
514
558
|
* @param config - Configuration options
|
|
515
559
|
* @returns Array of objects where keys are field names and values are cell values
|
|
516
560
|
*
|
|
517
561
|
* @example
|
|
518
|
-
* ```typescript
|
|
519
|
-
* // Using first row as headers
|
|
520
|
-
* const data = sheet.toJson();
|
|
521
|
-
*
|
|
522
|
-
* // Using custom field names
|
|
523
|
-
* const data = sheet.toJson({ fields: ['name', 'age', 'city'] });
|
|
524
|
-
*
|
|
525
|
-
* // Starting from a specific row/column
|
|
526
|
-
* const data = sheet.toJson({ startRow: 2, startCol: 1 });
|
|
527
|
-
* ```
|
|
528
|
-
*/
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
562
|
+
* ```typescript
|
|
563
|
+
* // Using first row as headers
|
|
564
|
+
* const data = sheet.toJson();
|
|
565
|
+
*
|
|
566
|
+
* // Using custom field names
|
|
567
|
+
* const data = sheet.toJson({ fields: ['name', 'age', 'city'] });
|
|
568
|
+
*
|
|
569
|
+
* // Starting from a specific row/column
|
|
570
|
+
* const data = sheet.toJson({ startRow: 2, startCol: 1 });
|
|
571
|
+
* ```
|
|
572
|
+
*/
|
|
573
|
+
toJson<T = Record<string, CellValue>>(config: SheetToJsonConfig = {}): T[] {
|
|
574
|
+
this._ensureParsed();
|
|
575
|
+
const {
|
|
576
|
+
fields,
|
|
577
|
+
startRow = 0,
|
|
578
|
+
startCol = 0,
|
|
579
|
+
endRow,
|
|
580
|
+
endCol,
|
|
536
581
|
stopOnEmptyRow = true,
|
|
537
582
|
dateHandling = this._workbook.dateHandling,
|
|
538
583
|
asText = false,
|
|
539
584
|
locale,
|
|
540
585
|
} = config;
|
|
541
|
-
|
|
542
|
-
// Get the bounds of data in the sheet
|
|
543
|
-
const bounds = this._getDataBounds();
|
|
544
|
-
if (!bounds) {
|
|
545
|
-
return [];
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
const effectiveEndRow = endRow ?? bounds.maxRow;
|
|
549
|
-
const effectiveEndCol = endCol ?? bounds.maxCol;
|
|
550
|
-
|
|
551
|
-
// Determine field names
|
|
552
|
-
let fieldNames: string[];
|
|
553
|
-
let dataStartRow: number;
|
|
554
|
-
|
|
555
|
-
if (fields) {
|
|
556
|
-
// Use provided field names, data starts at startRow
|
|
557
|
-
fieldNames = fields;
|
|
558
|
-
dataStartRow = startRow;
|
|
559
|
-
} else {
|
|
560
|
-
// Use first row as headers
|
|
561
|
-
fieldNames = [];
|
|
562
|
-
for (let col = startCol; col <= effectiveEndCol; col++) {
|
|
563
|
-
const cell = this._cells.get(toAddress(startRow, col));
|
|
564
|
-
const value = cell?.value;
|
|
565
|
-
fieldNames.push(value != null ? String(value) : `column${col}`);
|
|
566
|
-
}
|
|
567
|
-
dataStartRow = startRow + 1;
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
// Read data rows
|
|
571
|
-
const result: T[] = [];
|
|
572
|
-
|
|
573
|
-
for (let row = dataStartRow; row <= effectiveEndRow; row++) {
|
|
574
|
-
const obj: Record<string, CellValue | string> = {};
|
|
575
|
-
let hasData = false;
|
|
576
|
-
|
|
577
|
-
for (let colOffset = 0; colOffset < fieldNames.length; colOffset++) {
|
|
578
|
-
const col = startCol + colOffset;
|
|
579
|
-
const cell = this._cells.get(toAddress(row, col));
|
|
580
|
-
|
|
581
|
-
let value: CellValue | string;
|
|
582
|
-
|
|
586
|
+
|
|
587
|
+
// Get the bounds of data in the sheet
|
|
588
|
+
const bounds = this._getDataBounds();
|
|
589
|
+
if (!bounds) {
|
|
590
|
+
return [];
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
const effectiveEndRow = endRow ?? bounds.maxRow;
|
|
594
|
+
const effectiveEndCol = endCol ?? bounds.maxCol;
|
|
595
|
+
|
|
596
|
+
// Determine field names
|
|
597
|
+
let fieldNames: string[];
|
|
598
|
+
let dataStartRow: number;
|
|
599
|
+
|
|
600
|
+
if (fields) {
|
|
601
|
+
// Use provided field names, data starts at startRow
|
|
602
|
+
fieldNames = fields;
|
|
603
|
+
dataStartRow = startRow;
|
|
604
|
+
} else {
|
|
605
|
+
// Use first row as headers
|
|
606
|
+
fieldNames = [];
|
|
607
|
+
for (let col = startCol; col <= effectiveEndCol; col++) {
|
|
608
|
+
const cell = this._cells.get(toAddress(startRow, col));
|
|
609
|
+
const value = cell?.value;
|
|
610
|
+
fieldNames.push(value != null ? String(value) : `column${col}`);
|
|
611
|
+
}
|
|
612
|
+
dataStartRow = startRow + 1;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// Read data rows
|
|
616
|
+
const result: T[] = [];
|
|
617
|
+
|
|
618
|
+
for (let row = dataStartRow; row <= effectiveEndRow; row++) {
|
|
619
|
+
const obj: Record<string, CellValue | string> = {};
|
|
620
|
+
let hasData = false;
|
|
621
|
+
|
|
622
|
+
for (let colOffset = 0; colOffset < fieldNames.length; colOffset++) {
|
|
623
|
+
const col = startCol + colOffset;
|
|
624
|
+
const cell = this._cells.get(toAddress(row, col));
|
|
625
|
+
|
|
626
|
+
let value: CellValue | string;
|
|
627
|
+
|
|
583
628
|
if (asText) {
|
|
584
629
|
// Return formatted text instead of raw value
|
|
585
630
|
value = cell?.textWithLocale(locale) ?? '';
|
|
586
631
|
if (value !== '') {
|
|
587
632
|
hasData = true;
|
|
588
633
|
}
|
|
589
|
-
} else {
|
|
590
|
-
value = cell?.value ?? null;
|
|
591
|
-
if (value instanceof Date) {
|
|
592
|
-
value = this._serializeDate(value, dateHandling, cell);
|
|
593
|
-
}
|
|
594
|
-
if (value !== null) {
|
|
595
|
-
hasData = true;
|
|
596
|
-
}
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
const fieldName = fieldNames[colOffset];
|
|
600
|
-
if (fieldName) {
|
|
601
|
-
obj[fieldName] = value;
|
|
602
|
-
}
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
// Stop on empty row if configured
|
|
606
|
-
if (stopOnEmptyRow && !hasData) {
|
|
607
|
-
break;
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
result.push(obj as T);
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
return result;
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
private _serializeDate(value: Date, dateHandling: DateHandling, cell?: Cell | null): CellValue | number | string {
|
|
617
|
-
if (dateHandling === 'excelSerial') {
|
|
618
|
-
return cell?._jsDateToExcel(value) ?? value;
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
if (dateHandling === 'isoString') {
|
|
622
|
-
return value.toISOString();
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
return value;
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
/**
|
|
629
|
-
* Get the bounds of data in the sheet (min/max row and column with data)
|
|
630
|
-
*/
|
|
631
|
-
private _getDataBounds(): { minRow: number; maxRow: number; minCol: number; maxCol: number } | null {
|
|
632
|
-
if (!this._boundsDirty && this._dataBoundsCache) {
|
|
633
|
-
return this._dataBoundsCache;
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
if (this._cells.size === 0) {
|
|
637
|
-
this._dataBoundsCache = null;
|
|
638
|
-
this._boundsDirty = false;
|
|
639
|
-
return null;
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
let minRow = Infinity;
|
|
643
|
-
let maxRow = -Infinity;
|
|
644
|
-
let minCol = Infinity;
|
|
645
|
-
let maxCol = -Infinity;
|
|
646
|
-
|
|
647
|
-
for (const cell of this._cells.values()) {
|
|
648
|
-
if (cell.value !== null) {
|
|
649
|
-
minRow = Math.min(minRow, cell.row);
|
|
650
|
-
maxRow = Math.max(maxRow, cell.row);
|
|
651
|
-
minCol = Math.min(minCol, cell.col);
|
|
652
|
-
maxCol = Math.max(maxCol, cell.col);
|
|
653
|
-
}
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
if (minRow === Infinity) {
|
|
657
|
-
this._dataBoundsCache = null;
|
|
658
|
-
this._boundsDirty = false;
|
|
659
|
-
return null;
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
this._dataBoundsCache = { minRow, maxRow, minCol, maxCol };
|
|
663
|
-
this._boundsDirty = false;
|
|
664
|
-
return this._dataBoundsCache;
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
/**
|
|
668
|
-
* Generate XML for this worksheet
|
|
669
|
-
*/
|
|
670
|
-
toXml(): string {
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
const
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
if (this._frozenPane.row > 0
|
|
692
|
-
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
);
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
const
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
}
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
)
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
}
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
const
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
const
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
if (
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
634
|
+
} else {
|
|
635
|
+
value = cell?.value ?? null;
|
|
636
|
+
if (value instanceof Date) {
|
|
637
|
+
value = this._serializeDate(value, dateHandling, cell);
|
|
638
|
+
}
|
|
639
|
+
if (value !== null) {
|
|
640
|
+
hasData = true;
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
const fieldName = fieldNames[colOffset];
|
|
645
|
+
if (fieldName) {
|
|
646
|
+
obj[fieldName] = value;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// Stop on empty row if configured
|
|
651
|
+
if (stopOnEmptyRow && !hasData) {
|
|
652
|
+
break;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
result.push(obj as T);
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
return result;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
private _serializeDate(value: Date, dateHandling: DateHandling, cell?: Cell | null): CellValue | number | string {
|
|
662
|
+
if (dateHandling === 'excelSerial') {
|
|
663
|
+
return cell?._jsDateToExcel(value) ?? value;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
if (dateHandling === 'isoString') {
|
|
667
|
+
return value.toISOString();
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
return value;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
/**
|
|
674
|
+
* Get the bounds of data in the sheet (min/max row and column with data)
|
|
675
|
+
*/
|
|
676
|
+
private _getDataBounds(): { minRow: number; maxRow: number; minCol: number; maxCol: number } | null {
|
|
677
|
+
if (!this._boundsDirty && this._dataBoundsCache) {
|
|
678
|
+
return this._dataBoundsCache;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
if (this._cells.size === 0) {
|
|
682
|
+
this._dataBoundsCache = null;
|
|
683
|
+
this._boundsDirty = false;
|
|
684
|
+
return null;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
let minRow = Infinity;
|
|
688
|
+
let maxRow = -Infinity;
|
|
689
|
+
let minCol = Infinity;
|
|
690
|
+
let maxCol = -Infinity;
|
|
691
|
+
|
|
692
|
+
for (const cell of this._cells.values()) {
|
|
693
|
+
if (cell.value !== null) {
|
|
694
|
+
minRow = Math.min(minRow, cell.row);
|
|
695
|
+
maxRow = Math.max(maxRow, cell.row);
|
|
696
|
+
minCol = Math.min(minCol, cell.col);
|
|
697
|
+
maxCol = Math.max(maxCol, cell.col);
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
if (minRow === Infinity) {
|
|
702
|
+
this._dataBoundsCache = null;
|
|
703
|
+
this._boundsDirty = false;
|
|
704
|
+
return null;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
this._dataBoundsCache = { minRow, maxRow, minCol, maxCol };
|
|
708
|
+
this._boundsDirty = false;
|
|
709
|
+
return this._dataBoundsCache;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
/**
|
|
713
|
+
* Generate XML for this worksheet
|
|
714
|
+
*/
|
|
715
|
+
toXml(): string {
|
|
716
|
+
if (this._lazyParse && !this._dirty && this._rawXml) {
|
|
717
|
+
return this._rawXml;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
this._ensureParsed();
|
|
721
|
+
const preserved = this._preserveXml && this._xmlNodes ? this._buildPreservedWorksheet() : null;
|
|
722
|
+
// Build sheetData from cells
|
|
723
|
+
const sheetDataNode = this._buildSheetDataNode();
|
|
724
|
+
|
|
725
|
+
// Build worksheet structure
|
|
726
|
+
const worksheetChildren: XmlNode[] = [];
|
|
727
|
+
|
|
728
|
+
// Sheet views (freeze panes)
|
|
729
|
+
if (this._frozenPane) {
|
|
730
|
+
const paneAttrs: Record<string, string> = { state: 'frozen' };
|
|
731
|
+
const topLeftCell = toAddress(this._frozenPane.row, this._frozenPane.col);
|
|
732
|
+
paneAttrs.topLeftCell = topLeftCell;
|
|
733
|
+
if (this._frozenPane.col > 0) {
|
|
734
|
+
paneAttrs.xSplit = String(this._frozenPane.col);
|
|
735
|
+
}
|
|
736
|
+
if (this._frozenPane.row > 0) {
|
|
737
|
+
paneAttrs.ySplit = String(this._frozenPane.row);
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
let activePane = 'bottomRight';
|
|
741
|
+
if (this._frozenPane.row > 0 && this._frozenPane.col === 0) {
|
|
742
|
+
activePane = 'bottomLeft';
|
|
743
|
+
} else if (this._frozenPane.row === 0 && this._frozenPane.col > 0) {
|
|
744
|
+
activePane = 'topRight';
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
paneAttrs.activePane = activePane;
|
|
748
|
+
const paneNode = createElement('pane', paneAttrs, []);
|
|
749
|
+
const selectionNode = createElement(
|
|
750
|
+
'selection',
|
|
751
|
+
{ pane: activePane, activeCell: topLeftCell, sqref: topLeftCell },
|
|
752
|
+
[],
|
|
753
|
+
);
|
|
754
|
+
|
|
755
|
+
const sheetViewNode = createElement('sheetView', { workbookViewId: '0' }, [paneNode, selectionNode]);
|
|
756
|
+
worksheetChildren.push(createElement('sheetViews', {}, [sheetViewNode]));
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
// Column widths
|
|
760
|
+
if (this._columnWidths.size > 0) {
|
|
761
|
+
const colNodes: XmlNode[] = [];
|
|
762
|
+
const entries = Array.from(this._columnWidths.entries()).sort((a, b) => a[0] - b[0]);
|
|
763
|
+
for (const [colIndex, width] of entries) {
|
|
764
|
+
colNodes.push(
|
|
765
|
+
createElement(
|
|
766
|
+
'col',
|
|
767
|
+
{
|
|
768
|
+
min: String(colIndex + 1),
|
|
769
|
+
max: String(colIndex + 1),
|
|
770
|
+
width: String(width),
|
|
771
|
+
customWidth: '1',
|
|
772
|
+
},
|
|
773
|
+
[],
|
|
774
|
+
),
|
|
775
|
+
);
|
|
776
|
+
}
|
|
777
|
+
worksheetChildren.push(createElement('cols', {}, colNodes));
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
worksheetChildren.push(sheetDataNode);
|
|
781
|
+
|
|
782
|
+
// Add merged cells if any
|
|
783
|
+
if (this._mergedCells.size > 0) {
|
|
784
|
+
const mergeCellNodes: XmlNode[] = [];
|
|
785
|
+
for (const ref of this._mergedCells) {
|
|
786
|
+
mergeCellNodes.push(createElement('mergeCell', { ref }, []));
|
|
787
|
+
}
|
|
788
|
+
const mergeCellsNode = createElement('mergeCells', { count: String(this._mergedCells.size) }, mergeCellNodes);
|
|
789
|
+
worksheetChildren.push(mergeCellsNode);
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
// Add table parts if any tables exist
|
|
793
|
+
const tablePartsNode = this._buildTablePartsNode();
|
|
794
|
+
if (tablePartsNode) {
|
|
795
|
+
worksheetChildren.push(tablePartsNode);
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
const pivotTablePartsNode = this._buildPivotTablePartsNode();
|
|
799
|
+
if (pivotTablePartsNode) {
|
|
800
|
+
worksheetChildren.push(pivotTablePartsNode);
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
const worksheetNode = createElement(
|
|
804
|
+
'worksheet',
|
|
805
|
+
{
|
|
806
|
+
xmlns: 'http://schemas.openxmlformats.org/spreadsheetml/2006/main',
|
|
807
|
+
'xmlns:r': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships',
|
|
808
|
+
},
|
|
809
|
+
worksheetChildren,
|
|
810
|
+
);
|
|
811
|
+
|
|
812
|
+
if (preserved) {
|
|
813
|
+
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n${stringifyXml([preserved])}`;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n${stringifyXml([worksheetNode])}`;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
private _buildSheetDataNode(): XmlNode {
|
|
820
|
+
const rowMap = new Map<number, Cell[]>();
|
|
821
|
+
for (const cell of this._cells.values()) {
|
|
822
|
+
const row = cell.row;
|
|
823
|
+
if (!rowMap.has(row)) {
|
|
824
|
+
rowMap.set(row, []);
|
|
825
|
+
}
|
|
826
|
+
rowMap.get(row)!.push(cell);
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
for (const rowIdx of this._rowHeights.keys()) {
|
|
830
|
+
if (!rowMap.has(rowIdx)) {
|
|
831
|
+
rowMap.set(rowIdx, []);
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
const sortedRows = Array.from(rowMap.entries()).sort((a, b) => a[0] - b[0]);
|
|
836
|
+
const rowNodes: XmlNode[] = [];
|
|
837
|
+
for (const [rowIdx, cells] of sortedRows) {
|
|
838
|
+
cells.sort((a, b) => a.col - b.col);
|
|
839
|
+
|
|
840
|
+
const cellNodes: XmlNode[] = [];
|
|
841
|
+
for (const cell of cells) {
|
|
842
|
+
const cellNode = this._buildCellNode(cell);
|
|
843
|
+
cellNodes.push(cellNode);
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
const rowAttrs: Record<string, string> = { r: String(rowIdx + 1) };
|
|
847
|
+
const rowHeight = this._rowHeights.get(rowIdx);
|
|
848
|
+
if (rowHeight !== undefined) {
|
|
849
|
+
rowAttrs.ht = String(rowHeight);
|
|
850
|
+
rowAttrs.customHeight = '1';
|
|
851
|
+
}
|
|
852
|
+
const rowNode = createElement('row', rowAttrs, cellNodes);
|
|
853
|
+
rowNodes.push(rowNode);
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
return createElement('sheetData', {}, rowNodes);
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
private _buildSheetViewsNode(): XmlNode | null {
|
|
860
|
+
if (!this._frozenPane) return null;
|
|
861
|
+
const paneAttrs: Record<string, string> = { state: 'frozen' };
|
|
862
|
+
const topLeftCell = toAddress(this._frozenPane.row, this._frozenPane.col);
|
|
863
|
+
paneAttrs.topLeftCell = topLeftCell;
|
|
864
|
+
if (this._frozenPane.col > 0) {
|
|
865
|
+
paneAttrs.xSplit = String(this._frozenPane.col);
|
|
866
|
+
}
|
|
867
|
+
if (this._frozenPane.row > 0) {
|
|
868
|
+
paneAttrs.ySplit = String(this._frozenPane.row);
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
let activePane = 'bottomRight';
|
|
872
|
+
if (this._frozenPane.row > 0 && this._frozenPane.col === 0) {
|
|
873
|
+
activePane = 'bottomLeft';
|
|
874
|
+
} else if (this._frozenPane.row === 0 && this._frozenPane.col > 0) {
|
|
875
|
+
activePane = 'topRight';
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
paneAttrs.activePane = activePane;
|
|
879
|
+
const paneNode = createElement('pane', paneAttrs, []);
|
|
880
|
+
const selectionNode = createElement(
|
|
881
|
+
'selection',
|
|
882
|
+
{ pane: activePane, activeCell: topLeftCell, sqref: topLeftCell },
|
|
883
|
+
[],
|
|
884
|
+
);
|
|
885
|
+
|
|
886
|
+
const sheetViewNode = createElement('sheetView', { workbookViewId: '0' }, [paneNode, selectionNode]);
|
|
887
|
+
return createElement('sheetViews', {}, [sheetViewNode]);
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
private _buildColsNode(): XmlNode | null {
|
|
891
|
+
if (this._columnWidths.size === 0) return null;
|
|
892
|
+
const colNodes: XmlNode[] = [];
|
|
893
|
+
const entries = Array.from(this._columnWidths.entries()).sort((a, b) => a[0] - b[0]);
|
|
894
|
+
for (const [colIndex, width] of entries) {
|
|
895
|
+
colNodes.push(
|
|
896
|
+
createElement(
|
|
897
|
+
'col',
|
|
898
|
+
{
|
|
899
|
+
min: String(colIndex + 1),
|
|
900
|
+
max: String(colIndex + 1),
|
|
901
|
+
width: String(width),
|
|
902
|
+
customWidth: '1',
|
|
903
|
+
},
|
|
904
|
+
[],
|
|
905
|
+
),
|
|
906
|
+
);
|
|
907
|
+
}
|
|
908
|
+
return createElement('cols', {}, colNodes);
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
private _buildMergeCellsNode(): XmlNode | null {
|
|
912
|
+
if (this._mergedCells.size === 0) return null;
|
|
913
|
+
const mergeCellNodes: XmlNode[] = [];
|
|
914
|
+
for (const ref of this._mergedCells) {
|
|
915
|
+
mergeCellNodes.push(createElement('mergeCell', { ref }, []));
|
|
916
|
+
}
|
|
917
|
+
return createElement('mergeCells', { count: String(this._mergedCells.size) }, mergeCellNodes);
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
private _buildTablePartsNode(): XmlNode | null {
|
|
921
|
+
if (this._tables.length === 0) return null;
|
|
922
|
+
const tablePartNodes: XmlNode[] = [];
|
|
923
|
+
for (let i = 0; i < this._tables.length; i++) {
|
|
924
|
+
const relId =
|
|
925
|
+
this._tableRelIds && this._tableRelIds.length === this._tables.length ? this._tableRelIds[i] : `rId${i + 1}`;
|
|
926
|
+
tablePartNodes.push(createElement('tablePart', { 'r:id': relId }, []));
|
|
927
|
+
}
|
|
928
|
+
return createElement('tableParts', { count: String(this._tables.length) }, tablePartNodes);
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
private _buildPivotTablePartsNode(): XmlNode | null {
|
|
932
|
+
if (!this._pivotTableRelIds || this._pivotTableRelIds.length === 0) return null;
|
|
933
|
+
const pivotPartNodes: XmlNode[] = this._pivotTableRelIds.map((relId) =>
|
|
934
|
+
createElement('pivotTablePart', { 'r:id': relId }, []),
|
|
935
|
+
);
|
|
936
|
+
return createElement('pivotTableParts', { count: String(pivotPartNodes.length) }, pivotPartNodes);
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
private _buildPreservedWorksheet(): XmlNode | null {
|
|
940
|
+
if (!this._xmlNodes) return null;
|
|
941
|
+
const worksheet = findElement(this._xmlNodes, 'worksheet');
|
|
942
|
+
if (!worksheet) return null;
|
|
943
|
+
|
|
944
|
+
const children = getChildren(worksheet, 'worksheet');
|
|
945
|
+
|
|
946
|
+
const upsertChild = (tag: string, node: XmlNode | null) => {
|
|
947
|
+
const existingIndex = children.findIndex((child) => tag in child);
|
|
948
|
+
if (node) {
|
|
949
|
+
if (existingIndex >= 0) {
|
|
950
|
+
children[existingIndex] = node;
|
|
951
|
+
} else {
|
|
952
|
+
children.push(node);
|
|
953
|
+
}
|
|
954
|
+
} else if (existingIndex >= 0) {
|
|
955
|
+
children.splice(existingIndex, 1);
|
|
956
|
+
}
|
|
957
|
+
};
|
|
958
|
+
|
|
959
|
+
if (this._sheetViewsDirty) {
|
|
960
|
+
const sheetViewsNode = this._buildSheetViewsNode();
|
|
961
|
+
upsertChild('sheetViews', sheetViewsNode);
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
if (this._colsDirty) {
|
|
965
|
+
const colsNode = this._buildColsNode();
|
|
966
|
+
upsertChild('cols', colsNode);
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
const sheetDataNode = this._buildSheetDataNode();
|
|
970
|
+
upsertChild('sheetData', sheetDataNode);
|
|
971
|
+
|
|
972
|
+
const mergeCellsNode = this._buildMergeCellsNode();
|
|
973
|
+
upsertChild('mergeCells', mergeCellsNode);
|
|
974
|
+
|
|
975
|
+
if (this._tablePartsDirty) {
|
|
976
|
+
const tablePartsNode = this._buildTablePartsNode();
|
|
977
|
+
upsertChild('tableParts', tablePartsNode);
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
if (this._pivotTablePartsDirty) {
|
|
981
|
+
const pivotTablePartsNode = this._buildPivotTablePartsNode();
|
|
982
|
+
upsertChild('pivotTableParts', pivotTablePartsNode);
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
return worksheet;
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
/**
|
|
989
|
+
* Build a cell XML node from a Cell object
|
|
990
|
+
*/
|
|
991
|
+
private _buildCellNode(cell: Cell): XmlNode {
|
|
992
|
+
const data = cell.data;
|
|
993
|
+
const attrs: Record<string, string> = { r: cell.address };
|
|
994
|
+
|
|
995
|
+
if (data.t && data.t !== 'n') {
|
|
996
|
+
attrs.t = data.t;
|
|
997
|
+
}
|
|
998
|
+
if (data.s !== undefined) {
|
|
999
|
+
attrs.s = String(data.s);
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
const children: XmlNode[] = [];
|
|
1003
|
+
|
|
1004
|
+
// Formula
|
|
1005
|
+
if (data.f) {
|
|
1006
|
+
const fAttrs: Record<string, string> = {};
|
|
1007
|
+
if (data.F) fAttrs.ref = data.F;
|
|
1008
|
+
if (data.si !== undefined) fAttrs.si = String(data.si);
|
|
1009
|
+
children.push(createElement('f', fAttrs, [createText(data.f)]));
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
// Value
|
|
1013
|
+
if (data.v !== undefined) {
|
|
1014
|
+
children.push(createElement('v', {}, [createText(String(data.v))]));
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
return createElement('c', attrs, children);
|
|
1018
|
+
}
|
|
1019
|
+
}
|