@niicojs/excel 0.2.6 → 0.3.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/LICENSE +20 -20
- package/README.md +241 -8
- package/dist/index.cjs +1485 -167
- package/dist/index.d.cts +376 -12
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +376 -12
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1485 -168
- package/package.json +1 -1
- package/src/index.ts +9 -1
- package/src/pivot-cache.ts +10 -1
- package/src/pivot-table.ts +176 -40
- package/src/range.ts +15 -2
- package/src/shared-strings.ts +65 -16
- package/src/styles.ts +192 -21
- package/src/table.ts +386 -0
- package/src/types.ts +74 -2
- package/src/utils/address.ts +4 -1
- package/src/utils/xml.ts +0 -7
- package/src/workbook.ts +426 -41
- package/src/worksheet.ts +484 -27
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -7,6 +7,8 @@ export { SharedStrings } from './shared-strings';
|
|
|
7
7
|
export { Styles } from './styles';
|
|
8
8
|
export { PivotTable } from './pivot-table';
|
|
9
9
|
export { PivotCache } from './pivot-cache';
|
|
10
|
+
export { Table } from './table';
|
|
11
|
+
export { parseAddress, toAddress, parseRange, toRange } from './utils/address';
|
|
10
12
|
|
|
11
13
|
// Type exports
|
|
12
14
|
export type {
|
|
@@ -20,11 +22,18 @@ export type {
|
|
|
20
22
|
BorderStyle,
|
|
21
23
|
BorderType,
|
|
22
24
|
Alignment,
|
|
25
|
+
DateHandling,
|
|
23
26
|
// Pivot table types
|
|
24
27
|
PivotTableConfig,
|
|
25
28
|
PivotValueConfig,
|
|
26
29
|
AggregationType,
|
|
27
30
|
PivotFieldAxis,
|
|
31
|
+
PivotSortOrder,
|
|
32
|
+
PivotFieldFilter,
|
|
33
|
+
// Table types
|
|
34
|
+
TableConfig,
|
|
35
|
+
TableStyleConfig,
|
|
36
|
+
TableTotalFunction,
|
|
28
37
|
// Sheet from data types
|
|
29
38
|
SheetFromDataConfig,
|
|
30
39
|
ColumnConfig,
|
|
@@ -34,4 +43,3 @@ export type {
|
|
|
34
43
|
} from './types';
|
|
35
44
|
|
|
36
45
|
// Utility exports
|
|
37
|
-
export { parseAddress, toAddress, parseRange, toRange } from './utils/address';
|
package/src/pivot-cache.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { createElement, stringifyXml, XmlNode } from './utils/xml';
|
|
|
7
7
|
*/
|
|
8
8
|
export class PivotCache {
|
|
9
9
|
private _cacheId: number;
|
|
10
|
+
private _fileIndex: number;
|
|
10
11
|
private _sourceSheet: string;
|
|
11
12
|
private _sourceRange: string;
|
|
12
13
|
private _fields: PivotCacheField[] = [];
|
|
@@ -16,8 +17,9 @@ export class PivotCache {
|
|
|
16
17
|
// Optimized lookup: Map<fieldIndex, Map<stringValue, sharedItemsIndex>>
|
|
17
18
|
private _sharedItemsIndexMap: Map<number, Map<string, number>> = new Map();
|
|
18
19
|
|
|
19
|
-
constructor(cacheId: number, sourceSheet: string, sourceRange: string) {
|
|
20
|
+
constructor(cacheId: number, sourceSheet: string, sourceRange: string, fileIndex: number) {
|
|
20
21
|
this._cacheId = cacheId;
|
|
22
|
+
this._fileIndex = fileIndex;
|
|
21
23
|
this._sourceSheet = sourceSheet;
|
|
22
24
|
this._sourceRange = sourceRange;
|
|
23
25
|
}
|
|
@@ -29,6 +31,13 @@ export class PivotCache {
|
|
|
29
31
|
return this._cacheId;
|
|
30
32
|
}
|
|
31
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Get the file index for this cache (used for file naming).
|
|
36
|
+
*/
|
|
37
|
+
get fileIndex(): number {
|
|
38
|
+
return this._fileIndex;
|
|
39
|
+
}
|
|
40
|
+
|
|
32
41
|
/**
|
|
33
42
|
* Set refreshOnLoad option
|
|
34
43
|
*/
|
package/src/pivot-table.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AggregationType, PivotFieldAxis } from './types';
|
|
1
|
+
import type { AggregationType, PivotFieldAxis, PivotFieldFilter, PivotSortOrder, PivotValueConfig } from './types';
|
|
2
2
|
import type { Styles } from './styles';
|
|
3
3
|
import { PivotCache } from './pivot-cache';
|
|
4
4
|
import { createElement, stringifyXml, XmlNode } from './utils/xml';
|
|
@@ -12,7 +12,9 @@ interface FieldAssignment {
|
|
|
12
12
|
axis: PivotFieldAxis;
|
|
13
13
|
aggregation?: AggregationType;
|
|
14
14
|
displayName?: string;
|
|
15
|
-
|
|
15
|
+
numFmtId?: number;
|
|
16
|
+
sortOrder?: PivotSortOrder;
|
|
17
|
+
filter?: PivotFieldFilter;
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
/**
|
|
@@ -30,8 +32,10 @@ export class PivotTable {
|
|
|
30
32
|
private _columnFields: FieldAssignment[] = [];
|
|
31
33
|
private _valueFields: FieldAssignment[] = [];
|
|
32
34
|
private _filterFields: FieldAssignment[] = [];
|
|
35
|
+
private _fieldAssignments: Map<number, FieldAssignment> = new Map();
|
|
33
36
|
|
|
34
37
|
private _pivotTableIndex: number;
|
|
38
|
+
private _cacheFileIndex: number;
|
|
35
39
|
private _styles: Styles | null = null;
|
|
36
40
|
|
|
37
41
|
constructor(
|
|
@@ -42,6 +46,7 @@ export class PivotTable {
|
|
|
42
46
|
targetRow: number,
|
|
43
47
|
targetCol: number,
|
|
44
48
|
pivotTableIndex: number,
|
|
49
|
+
cacheFileIndex: number,
|
|
45
50
|
) {
|
|
46
51
|
this._name = name;
|
|
47
52
|
this._cache = cache;
|
|
@@ -50,6 +55,7 @@ export class PivotTable {
|
|
|
50
55
|
this._targetRow = targetRow;
|
|
51
56
|
this._targetCol = targetCol;
|
|
52
57
|
this._pivotTableIndex = pivotTableIndex;
|
|
58
|
+
this._cacheFileIndex = cacheFileIndex;
|
|
53
59
|
}
|
|
54
60
|
|
|
55
61
|
/**
|
|
@@ -87,6 +93,14 @@ export class PivotTable {
|
|
|
87
93
|
return this._pivotTableIndex;
|
|
88
94
|
}
|
|
89
95
|
|
|
96
|
+
/**
|
|
97
|
+
* Get the pivot cache file index used for rels.
|
|
98
|
+
* @internal
|
|
99
|
+
*/
|
|
100
|
+
get cacheFileIndex(): number {
|
|
101
|
+
return this._cacheFileIndex;
|
|
102
|
+
}
|
|
103
|
+
|
|
90
104
|
/**
|
|
91
105
|
* Set the styles reference for number format resolution
|
|
92
106
|
* @internal
|
|
@@ -106,11 +120,13 @@ export class PivotTable {
|
|
|
106
120
|
throw new Error(`Field not found in source data: ${fieldName}`);
|
|
107
121
|
}
|
|
108
122
|
|
|
109
|
-
|
|
123
|
+
const assignment: FieldAssignment = {
|
|
110
124
|
fieldName,
|
|
111
125
|
fieldIndex,
|
|
112
126
|
axis: 'row',
|
|
113
|
-
}
|
|
127
|
+
};
|
|
128
|
+
this._rowFields.push(assignment);
|
|
129
|
+
this._fieldAssignments.set(fieldIndex, assignment);
|
|
114
130
|
|
|
115
131
|
return this;
|
|
116
132
|
}
|
|
@@ -125,43 +141,80 @@ export class PivotTable {
|
|
|
125
141
|
throw new Error(`Field not found in source data: ${fieldName}`);
|
|
126
142
|
}
|
|
127
143
|
|
|
128
|
-
|
|
144
|
+
const assignment: FieldAssignment = {
|
|
129
145
|
fieldName,
|
|
130
146
|
fieldIndex,
|
|
131
147
|
axis: 'column',
|
|
132
|
-
}
|
|
148
|
+
};
|
|
149
|
+
this._columnFields.push(assignment);
|
|
150
|
+
this._fieldAssignments.set(fieldIndex, assignment);
|
|
133
151
|
|
|
134
152
|
return this;
|
|
135
153
|
}
|
|
136
154
|
|
|
137
155
|
/**
|
|
138
|
-
* Add a field to the values area with aggregation
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
156
|
+
* Add a field to the values area with aggregation.
|
|
157
|
+
*
|
|
158
|
+
* Supports two call signatures:
|
|
159
|
+
* - Positional: `addValueField(fieldName, aggregation?, displayName?, numberFormat?)`
|
|
160
|
+
* - Object: `addValueField({ field, aggregation?, name?, numberFormat? })`
|
|
161
|
+
*
|
|
162
|
+
* @example
|
|
163
|
+
* // Positional arguments
|
|
164
|
+
* pivot.addValueField('Sales', 'sum', 'Total Sales', '$#,##0.00');
|
|
165
|
+
*
|
|
166
|
+
* // Object form
|
|
167
|
+
* pivot.addValueField({ field: 'Sales', aggregation: 'sum', name: 'Total Sales', numberFormat: '$#,##0.00' });
|
|
143
168
|
*/
|
|
169
|
+
addValueField(config: PivotValueConfig): this;
|
|
170
|
+
addValueField(fieldName: string, aggregation?: AggregationType, displayName?: string, numberFormat?: string): this;
|
|
144
171
|
addValueField(
|
|
145
|
-
|
|
172
|
+
fieldNameOrConfig: string | PivotValueConfig,
|
|
146
173
|
aggregation: AggregationType = 'sum',
|
|
147
174
|
displayName?: string,
|
|
148
175
|
numberFormat?: string,
|
|
149
176
|
): this {
|
|
177
|
+
// Normalize arguments to a common form
|
|
178
|
+
let fieldName: string;
|
|
179
|
+
let agg: AggregationType;
|
|
180
|
+
let name: string | undefined;
|
|
181
|
+
let format: string | undefined;
|
|
182
|
+
|
|
183
|
+
if (typeof fieldNameOrConfig === 'object') {
|
|
184
|
+
fieldName = fieldNameOrConfig.field;
|
|
185
|
+
agg = fieldNameOrConfig.aggregation ?? 'sum';
|
|
186
|
+
name = fieldNameOrConfig.name;
|
|
187
|
+
format = fieldNameOrConfig.numberFormat;
|
|
188
|
+
} else {
|
|
189
|
+
fieldName = fieldNameOrConfig;
|
|
190
|
+
agg = aggregation;
|
|
191
|
+
name = displayName;
|
|
192
|
+
format = numberFormat;
|
|
193
|
+
}
|
|
194
|
+
|
|
150
195
|
const fieldIndex = this._cache.getFieldIndex(fieldName);
|
|
151
196
|
if (fieldIndex < 0) {
|
|
152
197
|
throw new Error(`Field not found in source data: ${fieldName}`);
|
|
153
198
|
}
|
|
154
199
|
|
|
155
|
-
const defaultName = `${
|
|
200
|
+
const defaultName = `${agg.charAt(0).toUpperCase() + agg.slice(1)} of ${fieldName}`;
|
|
201
|
+
|
|
202
|
+
// Resolve numFmtId immediately if format is provided and styles are available
|
|
203
|
+
let numFmtId: number | undefined;
|
|
204
|
+
if (format && this._styles) {
|
|
205
|
+
numFmtId = this._styles.getOrCreateNumFmtId(format);
|
|
206
|
+
}
|
|
156
207
|
|
|
157
|
-
|
|
208
|
+
const assignment: FieldAssignment = {
|
|
158
209
|
fieldName,
|
|
159
210
|
fieldIndex,
|
|
160
211
|
axis: 'value',
|
|
161
|
-
aggregation,
|
|
162
|
-
displayName:
|
|
163
|
-
|
|
164
|
-
}
|
|
212
|
+
aggregation: agg,
|
|
213
|
+
displayName: name || defaultName,
|
|
214
|
+
numFmtId,
|
|
215
|
+
};
|
|
216
|
+
this._valueFields.push(assignment);
|
|
217
|
+
this._fieldAssignments.set(fieldIndex, assignment);
|
|
165
218
|
|
|
166
219
|
return this;
|
|
167
220
|
}
|
|
@@ -176,12 +229,61 @@ export class PivotTable {
|
|
|
176
229
|
throw new Error(`Field not found in source data: ${fieldName}`);
|
|
177
230
|
}
|
|
178
231
|
|
|
179
|
-
|
|
232
|
+
const assignment: FieldAssignment = {
|
|
180
233
|
fieldName,
|
|
181
234
|
fieldIndex,
|
|
182
235
|
axis: 'filter',
|
|
183
|
-
}
|
|
236
|
+
};
|
|
237
|
+
this._filterFields.push(assignment);
|
|
238
|
+
this._fieldAssignments.set(fieldIndex, assignment);
|
|
239
|
+
|
|
240
|
+
return this;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Set a sort order for a row or column field
|
|
245
|
+
* @param fieldName - Name of the field to sort
|
|
246
|
+
* @param order - Sort order ('asc' or 'desc')
|
|
247
|
+
*/
|
|
248
|
+
sortField(fieldName: string, order: PivotSortOrder): this {
|
|
249
|
+
const fieldIndex = this._cache.getFieldIndex(fieldName);
|
|
250
|
+
if (fieldIndex < 0) {
|
|
251
|
+
throw new Error(`Field not found in source data: ${fieldName}`);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const assignment = this._fieldAssignments.get(fieldIndex);
|
|
255
|
+
if (!assignment) {
|
|
256
|
+
throw new Error(`Field is not assigned to pivot table: ${fieldName}`);
|
|
257
|
+
}
|
|
258
|
+
if (assignment.axis !== 'row' && assignment.axis !== 'column') {
|
|
259
|
+
throw new Error(`Sort is only supported for row or column fields: ${fieldName}`);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
assignment.sortOrder = order;
|
|
263
|
+
return this;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Filter items for a row, column, or filter field
|
|
268
|
+
* @param fieldName - Name of the field to filter
|
|
269
|
+
* @param filter - Filter configuration with include or exclude list
|
|
270
|
+
*/
|
|
271
|
+
filterField(fieldName: string, filter: PivotFieldFilter): this {
|
|
272
|
+
const fieldIndex = this._cache.getFieldIndex(fieldName);
|
|
273
|
+
if (fieldIndex < 0) {
|
|
274
|
+
throw new Error(`Field not found in source data: ${fieldName}`);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const assignment = this._fieldAssignments.get(fieldIndex);
|
|
278
|
+
if (!assignment) {
|
|
279
|
+
throw new Error(`Field is not assigned to pivot table: ${fieldName}`);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (filter.include && filter.exclude) {
|
|
283
|
+
throw new Error('Cannot use both include and exclude in the same filter');
|
|
284
|
+
}
|
|
184
285
|
|
|
286
|
+
assignment.filter = filter;
|
|
185
287
|
return this;
|
|
186
288
|
}
|
|
187
289
|
|
|
@@ -279,9 +381,9 @@ export class PivotTable {
|
|
|
279
381
|
subtotal: f.aggregation || 'sum',
|
|
280
382
|
};
|
|
281
383
|
|
|
282
|
-
// Add numFmtId if
|
|
283
|
-
if (f.
|
|
284
|
-
attrs.numFmtId = String(
|
|
384
|
+
// Add numFmtId if it was resolved during addValueField
|
|
385
|
+
if (f.numFmtId !== undefined) {
|
|
386
|
+
attrs.numFmtId = String(f.numFmtId);
|
|
285
387
|
}
|
|
286
388
|
|
|
287
389
|
return createElement('dataField', attrs, []);
|
|
@@ -290,7 +392,7 @@ export class PivotTable {
|
|
|
290
392
|
}
|
|
291
393
|
|
|
292
394
|
// Check if any value field has a number format
|
|
293
|
-
const hasNumberFormats = this._valueFields.some((f) => f.
|
|
395
|
+
const hasNumberFormats = this._valueFields.some((f) => f.numFmtId !== undefined);
|
|
294
396
|
|
|
295
397
|
// Pivot table style
|
|
296
398
|
children.push(
|
|
@@ -353,30 +455,36 @@ export class PivotTable {
|
|
|
353
455
|
const filterField = this._filterFields.find((f) => f.fieldIndex === fieldIndex);
|
|
354
456
|
const valueField = this._valueFields.find((f) => f.fieldIndex === fieldIndex);
|
|
355
457
|
|
|
458
|
+
// Get the assignment to check for sort/filter options
|
|
459
|
+
const assignment = rowField || colField || filterField;
|
|
460
|
+
|
|
356
461
|
if (rowField) {
|
|
357
462
|
attrs.axis = 'axisRow';
|
|
358
463
|
attrs.showAll = '0';
|
|
464
|
+
|
|
465
|
+
// Add sort order if specified
|
|
466
|
+
if (rowField.sortOrder) {
|
|
467
|
+
attrs.sortType = rowField.sortOrder === 'asc' ? 'ascending' : 'descending';
|
|
468
|
+
}
|
|
469
|
+
|
|
359
470
|
// Add items for shared values
|
|
360
471
|
const cacheField = this._cache.fields[fieldIndex];
|
|
361
472
|
if (cacheField && cacheField.sharedItems.length > 0) {
|
|
362
|
-
const itemNodes
|
|
363
|
-
for (let i = 0; i < cacheField.sharedItems.length; i++) {
|
|
364
|
-
itemNodes.push(createElement('item', { x: String(i) }, []));
|
|
365
|
-
}
|
|
366
|
-
// Add default subtotal item
|
|
367
|
-
itemNodes.push(createElement('item', { t: 'default' }, []));
|
|
473
|
+
const itemNodes = this._buildItemNodes(cacheField.sharedItems, assignment?.filter);
|
|
368
474
|
children.push(createElement('items', { count: String(itemNodes.length) }, itemNodes));
|
|
369
475
|
}
|
|
370
476
|
} else if (colField) {
|
|
371
477
|
attrs.axis = 'axisCol';
|
|
372
478
|
attrs.showAll = '0';
|
|
479
|
+
|
|
480
|
+
// Add sort order if specified
|
|
481
|
+
if (colField.sortOrder) {
|
|
482
|
+
attrs.sortType = colField.sortOrder === 'asc' ? 'ascending' : 'descending';
|
|
483
|
+
}
|
|
484
|
+
|
|
373
485
|
const cacheField = this._cache.fields[fieldIndex];
|
|
374
486
|
if (cacheField && cacheField.sharedItems.length > 0) {
|
|
375
|
-
const itemNodes
|
|
376
|
-
for (let i = 0; i < cacheField.sharedItems.length; i++) {
|
|
377
|
-
itemNodes.push(createElement('item', { x: String(i) }, []));
|
|
378
|
-
}
|
|
379
|
-
itemNodes.push(createElement('item', { t: 'default' }, []));
|
|
487
|
+
const itemNodes = this._buildItemNodes(cacheField.sharedItems, assignment?.filter);
|
|
380
488
|
children.push(createElement('items', { count: String(itemNodes.length) }, itemNodes));
|
|
381
489
|
}
|
|
382
490
|
} else if (filterField) {
|
|
@@ -384,11 +492,7 @@ export class PivotTable {
|
|
|
384
492
|
attrs.showAll = '0';
|
|
385
493
|
const cacheField = this._cache.fields[fieldIndex];
|
|
386
494
|
if (cacheField && cacheField.sharedItems.length > 0) {
|
|
387
|
-
const itemNodes
|
|
388
|
-
for (let i = 0; i < cacheField.sharedItems.length; i++) {
|
|
389
|
-
itemNodes.push(createElement('item', { x: String(i) }, []));
|
|
390
|
-
}
|
|
391
|
-
itemNodes.push(createElement('item', { t: 'default' }, []));
|
|
495
|
+
const itemNodes = this._buildItemNodes(cacheField.sharedItems, assignment?.filter);
|
|
392
496
|
children.push(createElement('items', { count: String(itemNodes.length) }, itemNodes));
|
|
393
497
|
}
|
|
394
498
|
} else if (valueField) {
|
|
@@ -401,6 +505,38 @@ export class PivotTable {
|
|
|
401
505
|
return createElement('pivotField', attrs, children);
|
|
402
506
|
}
|
|
403
507
|
|
|
508
|
+
/**
|
|
509
|
+
* Build item nodes for a pivot field, with optional filtering
|
|
510
|
+
*/
|
|
511
|
+
private _buildItemNodes(sharedItems: string[], filter?: PivotFieldFilter): XmlNode[] {
|
|
512
|
+
const itemNodes: XmlNode[] = [];
|
|
513
|
+
|
|
514
|
+
for (let i = 0; i < sharedItems.length; i++) {
|
|
515
|
+
const itemValue = sharedItems[i];
|
|
516
|
+
const itemAttrs: Record<string, string> = { x: String(i) };
|
|
517
|
+
|
|
518
|
+
// Check if this item should be hidden
|
|
519
|
+
if (filter) {
|
|
520
|
+
let hidden = false;
|
|
521
|
+
if (filter.exclude && filter.exclude.includes(itemValue)) {
|
|
522
|
+
hidden = true;
|
|
523
|
+
} else if (filter.include && !filter.include.includes(itemValue)) {
|
|
524
|
+
hidden = true;
|
|
525
|
+
}
|
|
526
|
+
if (hidden) {
|
|
527
|
+
itemAttrs.h = '1';
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
itemNodes.push(createElement('item', itemAttrs, []));
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// Add default subtotal item
|
|
535
|
+
itemNodes.push(createElement('item', { t: 'default' }, []));
|
|
536
|
+
|
|
537
|
+
return itemNodes;
|
|
538
|
+
}
|
|
539
|
+
|
|
404
540
|
/**
|
|
405
541
|
* Build row items based on unique values in row fields
|
|
406
542
|
*/
|
package/src/range.ts
CHANGED
|
@@ -42,12 +42,25 @@ export class Range {
|
|
|
42
42
|
* Get all values in the range as a 2D array
|
|
43
43
|
*/
|
|
44
44
|
get values(): CellValue[][] {
|
|
45
|
+
return this.getValues();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Get all values in the range as a 2D array with options
|
|
50
|
+
*/
|
|
51
|
+
getValues(options: { createMissing?: boolean } = {}): CellValue[][] {
|
|
52
|
+
const { createMissing = true } = options;
|
|
45
53
|
const result: CellValue[][] = [];
|
|
46
54
|
for (let r = this._range.start.row; r <= this._range.end.row; r++) {
|
|
47
55
|
const row: CellValue[] = [];
|
|
48
56
|
for (let c = this._range.start.col; c <= this._range.end.col; c++) {
|
|
49
|
-
|
|
50
|
-
|
|
57
|
+
if (createMissing) {
|
|
58
|
+
const cell = this._worksheet.cell(r, c);
|
|
59
|
+
row.push(cell.value);
|
|
60
|
+
} else {
|
|
61
|
+
const cell = this._worksheet.getCellIfExists(r, c);
|
|
62
|
+
row.push(cell?.value ?? null);
|
|
63
|
+
}
|
|
51
64
|
}
|
|
52
65
|
result.push(row);
|
|
53
66
|
}
|
package/src/shared-strings.ts
CHANGED
|
@@ -1,13 +1,23 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
parseXml,
|
|
3
|
+
findElement,
|
|
4
|
+
getChildren,
|
|
5
|
+
getAttr,
|
|
6
|
+
XmlNode,
|
|
7
|
+
stringifyXml,
|
|
8
|
+
createElement,
|
|
9
|
+
createText,
|
|
10
|
+
} from './utils/xml';
|
|
2
11
|
|
|
3
12
|
/**
|
|
4
13
|
* Manages the shared strings table from xl/sharedStrings.xml
|
|
5
14
|
* Excel stores strings in a shared table to reduce file size
|
|
6
15
|
*/
|
|
7
16
|
export class SharedStrings {
|
|
8
|
-
private
|
|
17
|
+
private entries: SharedStringEntry[] = [];
|
|
9
18
|
private stringToIndex: Map<string, number> = new Map();
|
|
10
19
|
private _dirty = false;
|
|
20
|
+
private _totalCount = 0;
|
|
11
21
|
|
|
12
22
|
/**
|
|
13
23
|
* Parse shared strings from XML content
|
|
@@ -18,16 +28,28 @@ export class SharedStrings {
|
|
|
18
28
|
const sst = findElement(parsed, 'sst');
|
|
19
29
|
if (!sst) return ss;
|
|
20
30
|
|
|
31
|
+
const countAttr = getAttr(sst, 'count');
|
|
32
|
+
if (countAttr) {
|
|
33
|
+
const total = parseInt(countAttr, 10);
|
|
34
|
+
if (Number.isFinite(total) && total >= 0) {
|
|
35
|
+
ss._totalCount = total;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
21
39
|
const children = getChildren(sst, 'sst');
|
|
22
40
|
for (const child of children) {
|
|
23
41
|
if ('si' in child) {
|
|
24
42
|
const siChildren = getChildren(child, 'si');
|
|
25
43
|
const text = ss.extractText(siChildren);
|
|
26
|
-
ss.
|
|
27
|
-
ss.stringToIndex.set(text, ss.
|
|
44
|
+
ss.entries.push({ text, node: child });
|
|
45
|
+
ss.stringToIndex.set(text, ss.entries.length - 1);
|
|
28
46
|
}
|
|
29
47
|
}
|
|
30
48
|
|
|
49
|
+
if (ss._totalCount === 0 && ss.entries.length > 0) {
|
|
50
|
+
ss._totalCount = ss.entries.length;
|
|
51
|
+
}
|
|
52
|
+
|
|
31
53
|
return ss;
|
|
32
54
|
}
|
|
33
55
|
|
|
@@ -68,7 +90,7 @@ export class SharedStrings {
|
|
|
68
90
|
* Get a string by index
|
|
69
91
|
*/
|
|
70
92
|
getString(index: number): string | undefined {
|
|
71
|
-
return this.
|
|
93
|
+
return this.entries[index]?.text;
|
|
72
94
|
}
|
|
73
95
|
|
|
74
96
|
/**
|
|
@@ -78,11 +100,18 @@ export class SharedStrings {
|
|
|
78
100
|
addString(str: string): number {
|
|
79
101
|
const existing = this.stringToIndex.get(str);
|
|
80
102
|
if (existing !== undefined) {
|
|
103
|
+
this._totalCount++;
|
|
104
|
+
this._dirty = true;
|
|
81
105
|
return existing;
|
|
82
106
|
}
|
|
83
|
-
const index = this.
|
|
84
|
-
|
|
107
|
+
const index = this.entries.length;
|
|
108
|
+
const tElement = createElement('t', str.startsWith(' ') || str.endsWith(' ') ? { 'xml:space': 'preserve' } : {}, [
|
|
109
|
+
createText(str),
|
|
110
|
+
]);
|
|
111
|
+
const siElement = createElement('si', {}, [tElement]);
|
|
112
|
+
this.entries.push({ text: str, node: siElement });
|
|
85
113
|
this.stringToIndex.set(str, index);
|
|
114
|
+
this._totalCount++;
|
|
86
115
|
this._dirty = true;
|
|
87
116
|
return index;
|
|
88
117
|
}
|
|
@@ -98,7 +127,14 @@ export class SharedStrings {
|
|
|
98
127
|
* Get the count of strings
|
|
99
128
|
*/
|
|
100
129
|
get count(): number {
|
|
101
|
-
return this.
|
|
130
|
+
return this.entries.length;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Get total usage count of shared strings
|
|
135
|
+
*/
|
|
136
|
+
get totalCount(): number {
|
|
137
|
+
return Math.max(this._totalCount, this.entries.length);
|
|
102
138
|
}
|
|
103
139
|
|
|
104
140
|
/**
|
|
@@ -106,20 +142,28 @@ export class SharedStrings {
|
|
|
106
142
|
*/
|
|
107
143
|
toXml(): string {
|
|
108
144
|
const siElements: XmlNode[] = [];
|
|
109
|
-
for (const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
145
|
+
for (const entry of this.entries) {
|
|
146
|
+
if (entry.node) {
|
|
147
|
+
siElements.push(entry.node);
|
|
148
|
+
} else {
|
|
149
|
+
const str = entry.text;
|
|
150
|
+
const tElement = createElement(
|
|
151
|
+
't',
|
|
152
|
+
str.startsWith(' ') || str.endsWith(' ') ? { 'xml:space': 'preserve' } : {},
|
|
153
|
+
[createText(str)],
|
|
154
|
+
);
|
|
155
|
+
const siElement = createElement('si', {}, [tElement]);
|
|
156
|
+
siElements.push(siElement);
|
|
157
|
+
}
|
|
115
158
|
}
|
|
116
159
|
|
|
160
|
+
const totalCount = Math.max(this._totalCount, this.entries.length);
|
|
117
161
|
const sst = createElement(
|
|
118
162
|
'sst',
|
|
119
163
|
{
|
|
120
164
|
xmlns: 'http://schemas.openxmlformats.org/spreadsheetml/2006/main',
|
|
121
|
-
count: String(
|
|
122
|
-
uniqueCount: String(this.
|
|
165
|
+
count: String(totalCount),
|
|
166
|
+
uniqueCount: String(this.entries.length),
|
|
123
167
|
},
|
|
124
168
|
siElements,
|
|
125
169
|
);
|
|
@@ -127,3 +171,8 @@ export class SharedStrings {
|
|
|
127
171
|
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n${stringifyXml([sst])}`;
|
|
128
172
|
}
|
|
129
173
|
}
|
|
174
|
+
|
|
175
|
+
interface SharedStringEntry {
|
|
176
|
+
text: string;
|
|
177
|
+
node?: XmlNode;
|
|
178
|
+
}
|