@niicojs/excel 0.3.4 → 0.3.5
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 +1187 -1265
- 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 +1187 -1266
- 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 +1 -1
- 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/workbook.ts
CHANGED
|
@@ -2,67 +2,70 @@ import { readFile, writeFile } from 'fs/promises';
|
|
|
2
2
|
import type {
|
|
3
3
|
SheetDefinition,
|
|
4
4
|
Relationship,
|
|
5
|
-
PivotTableConfig,
|
|
6
5
|
CellValue,
|
|
7
6
|
SheetFromDataConfig,
|
|
8
7
|
ColumnConfig,
|
|
9
8
|
RichCellValue,
|
|
10
9
|
DateHandling,
|
|
10
|
+
PivotTableConfig,
|
|
11
|
+
RangeAddress,
|
|
12
|
+
WorkbookReadOptions,
|
|
11
13
|
} from './types';
|
|
12
14
|
import { Worksheet } from './worksheet';
|
|
13
15
|
import { SharedStrings } from './shared-strings';
|
|
14
16
|
import { Styles } from './styles';
|
|
15
17
|
import { PivotTable } from './pivot-table';
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import { parseAddress, parseRange, toAddress } from './utils/address';
|
|
18
|
+
import { readZip, writeZip, readZipText, writeZipText, ZipStore, createZipStore } from './utils/zip';
|
|
19
|
+
import { parseAddress, parseSheetAddress, parseSheetRange } from './utils/address';
|
|
19
20
|
import { parseXml, findElement, getChildren, getAttr, XmlNode, stringifyXml, createElement } from './utils/xml';
|
|
20
21
|
|
|
21
22
|
/**
|
|
22
23
|
* Represents an Excel workbook (.xlsx file)
|
|
23
24
|
*/
|
|
24
25
|
export class Workbook {
|
|
25
|
-
private _files:
|
|
26
|
+
private _files: ZipStore = createZipStore();
|
|
26
27
|
private _sheets: Map<string, Worksheet> = new Map();
|
|
27
28
|
private _sheetDefs: SheetDefinition[] = [];
|
|
28
29
|
private _relationships: Relationship[] = [];
|
|
29
|
-
private _sharedStrings: SharedStrings;
|
|
30
|
-
private _styles: Styles;
|
|
30
|
+
private _sharedStrings: SharedStrings | null = null;
|
|
31
|
+
private _styles: Styles | null = null;
|
|
32
|
+
private _sharedStringsXml: string | null = null;
|
|
33
|
+
private _stylesXml: string | null = null;
|
|
34
|
+
private _lazy = true;
|
|
31
35
|
private _dirty = false;
|
|
32
36
|
|
|
33
|
-
// Pivot table support
|
|
34
|
-
private _pivotTables: PivotTable[] = [];
|
|
35
|
-
private _pivotCaches: PivotCache[] = [];
|
|
36
|
-
private _nextCacheId = 5;
|
|
37
|
-
private _nextCacheFileIndex = 1;
|
|
38
|
-
|
|
39
37
|
// Table support
|
|
40
38
|
private _nextTableId = 1;
|
|
41
39
|
|
|
40
|
+
// Pivot table support
|
|
41
|
+
private _pivotTables: PivotTable[] = [];
|
|
42
|
+
private _nextPivotTableId = 1;
|
|
43
|
+
private _nextPivotCacheId = 1;
|
|
44
|
+
|
|
42
45
|
// Date serialization handling
|
|
43
46
|
private _dateHandling: DateHandling = 'jsDate';
|
|
44
47
|
|
|
45
48
|
private _locale = 'fr-FR';
|
|
46
49
|
|
|
47
50
|
private constructor() {
|
|
48
|
-
|
|
49
|
-
this._styles = Styles.createDefault();
|
|
51
|
+
// Lazy init
|
|
50
52
|
}
|
|
51
53
|
|
|
52
54
|
/**
|
|
53
55
|
* Load a workbook from a file path
|
|
54
56
|
*/
|
|
55
|
-
static async fromFile(path: string): Promise<Workbook> {
|
|
57
|
+
static async fromFile(path: string, options: WorkbookReadOptions = {}): Promise<Workbook> {
|
|
56
58
|
const data = await readFile(path);
|
|
57
|
-
return Workbook.fromBuffer(new Uint8Array(data));
|
|
59
|
+
return Workbook.fromBuffer(new Uint8Array(data), options);
|
|
58
60
|
}
|
|
59
61
|
|
|
60
62
|
/**
|
|
61
63
|
* Load a workbook from a buffer
|
|
62
64
|
*/
|
|
63
|
-
static async fromBuffer(data: Uint8Array): Promise<Workbook> {
|
|
65
|
+
static async fromBuffer(data: Uint8Array, options: WorkbookReadOptions = {}): Promise<Workbook> {
|
|
64
66
|
const workbook = new Workbook();
|
|
65
|
-
workbook.
|
|
67
|
+
workbook._lazy = options.lazy ?? true;
|
|
68
|
+
workbook._files = await readZip(data, { lazy: workbook._lazy });
|
|
66
69
|
|
|
67
70
|
// Parse workbook.xml for sheet definitions
|
|
68
71
|
const workbookXml = readZipText(workbook._files, 'xl/workbook.xml');
|
|
@@ -77,16 +80,9 @@ export class Workbook {
|
|
|
77
80
|
}
|
|
78
81
|
|
|
79
82
|
// Parse shared strings
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// Parse styles
|
|
86
|
-
const stylesXml = readZipText(workbook._files, 'xl/styles.xml');
|
|
87
|
-
if (stylesXml) {
|
|
88
|
-
workbook._styles = Styles.parse(stylesXml);
|
|
89
|
-
}
|
|
83
|
+
// Store shared strings/styles XML for lazy parse
|
|
84
|
+
workbook._sharedStringsXml = readZipText(workbook._files, 'xl/sharedStrings.xml') ?? null;
|
|
85
|
+
workbook._stylesXml = readZipText(workbook._files, 'xl/styles.xml') ?? null;
|
|
90
86
|
|
|
91
87
|
return workbook;
|
|
92
88
|
}
|
|
@@ -97,6 +93,9 @@ export class Workbook {
|
|
|
97
93
|
static create(): Workbook {
|
|
98
94
|
const workbook = new Workbook();
|
|
99
95
|
workbook._dirty = true;
|
|
96
|
+
workbook._lazy = false;
|
|
97
|
+
workbook._sharedStrings = new SharedStrings();
|
|
98
|
+
workbook._styles = Styles.createDefault();
|
|
100
99
|
|
|
101
100
|
return workbook;
|
|
102
101
|
}
|
|
@@ -119,6 +118,13 @@ export class Workbook {
|
|
|
119
118
|
* Get shared strings table
|
|
120
119
|
*/
|
|
121
120
|
get sharedStrings(): SharedStrings {
|
|
121
|
+
if (!this._sharedStrings) {
|
|
122
|
+
if (this._sharedStringsXml) {
|
|
123
|
+
this._sharedStrings = SharedStrings.parse(this._sharedStringsXml);
|
|
124
|
+
} else {
|
|
125
|
+
this._sharedStrings = new SharedStrings();
|
|
126
|
+
}
|
|
127
|
+
}
|
|
122
128
|
return this._sharedStrings;
|
|
123
129
|
}
|
|
124
130
|
|
|
@@ -126,6 +132,13 @@ export class Workbook {
|
|
|
126
132
|
* Get styles
|
|
127
133
|
*/
|
|
128
134
|
get styles(): Styles {
|
|
135
|
+
if (!this._styles) {
|
|
136
|
+
if (this._stylesXml) {
|
|
137
|
+
this._styles = Styles.parse(this._stylesXml);
|
|
138
|
+
} else {
|
|
139
|
+
this._styles = Styles.createDefault();
|
|
140
|
+
}
|
|
141
|
+
}
|
|
129
142
|
return this._styles;
|
|
130
143
|
}
|
|
131
144
|
|
|
@@ -166,6 +179,102 @@ export class Workbook {
|
|
|
166
179
|
return this._nextTableId++;
|
|
167
180
|
}
|
|
168
181
|
|
|
182
|
+
/**
|
|
183
|
+
* Get all pivot tables in the workbook.
|
|
184
|
+
*/
|
|
185
|
+
get pivotTables(): PivotTable[] {
|
|
186
|
+
return [...this._pivotTables];
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Create a new pivot table.
|
|
191
|
+
*/
|
|
192
|
+
createPivotTable(config: PivotTableConfig): PivotTable {
|
|
193
|
+
if (!config.name || config.name.trim().length === 0) {
|
|
194
|
+
throw new Error('Pivot table name is required');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (this._pivotTables.some((pivot) => pivot.name === config.name)) {
|
|
198
|
+
throw new Error(`Pivot table name already exists: ${config.name}`);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const sourceRef = parseSheetRange(config.source);
|
|
202
|
+
const targetRef = parseSheetAddress(config.target);
|
|
203
|
+
|
|
204
|
+
const sourceSheet = this.sheet(sourceRef.sheet);
|
|
205
|
+
this.sheet(targetRef.sheet);
|
|
206
|
+
|
|
207
|
+
const sourceRange = this._normalizeRange(sourceRef.range);
|
|
208
|
+
if (sourceRange.start.row >= sourceRange.end.row) {
|
|
209
|
+
throw new Error('Pivot source range must include a header row and at least one data row');
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const fields = this._extractPivotFields(sourceSheet, sourceRange);
|
|
213
|
+
|
|
214
|
+
const cacheId = this._nextPivotCacheId++;
|
|
215
|
+
const pivotId = this._nextPivotTableId++;
|
|
216
|
+
const cachePartIndex = this._pivotTables.length + 1;
|
|
217
|
+
|
|
218
|
+
const pivot = new PivotTable(
|
|
219
|
+
this,
|
|
220
|
+
config,
|
|
221
|
+
sourceRef.sheet,
|
|
222
|
+
sourceSheet,
|
|
223
|
+
sourceRange,
|
|
224
|
+
targetRef.sheet,
|
|
225
|
+
targetRef.address,
|
|
226
|
+
cacheId,
|
|
227
|
+
pivotId,
|
|
228
|
+
cachePartIndex,
|
|
229
|
+
fields,
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
this._pivotTables.push(pivot);
|
|
233
|
+
this._dirty = true;
|
|
234
|
+
|
|
235
|
+
return pivot;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
private _extractPivotFields(
|
|
239
|
+
sourceSheet: Worksheet,
|
|
240
|
+
sourceRange: RangeAddress,
|
|
241
|
+
): { name: string; sourceCol: number }[] {
|
|
242
|
+
const fields: { name: string; sourceCol: number }[] = [];
|
|
243
|
+
const seen = new Set<string>();
|
|
244
|
+
|
|
245
|
+
for (let col = sourceRange.start.col; col <= sourceRange.end.col; col++) {
|
|
246
|
+
const headerCell = sourceSheet.getCellIfExists(sourceRange.start.row, col);
|
|
247
|
+
const rawHeader = headerCell?.value;
|
|
248
|
+
const name = rawHeader == null ? `Column${col - sourceRange.start.col + 1}` : String(rawHeader).trim();
|
|
249
|
+
|
|
250
|
+
if (!name) {
|
|
251
|
+
throw new Error(`Pivot source header is empty at column ${col + 1}`);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (seen.has(name)) {
|
|
255
|
+
throw new Error(`Duplicate pivot source header: ${name}`);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
seen.add(name);
|
|
259
|
+
fields.push({ name, sourceCol: col });
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return fields;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
private _normalizeRange(range: RangeAddress): RangeAddress {
|
|
266
|
+
return {
|
|
267
|
+
start: {
|
|
268
|
+
row: Math.min(range.start.row, range.end.row),
|
|
269
|
+
col: Math.min(range.start.col, range.end.col),
|
|
270
|
+
},
|
|
271
|
+
end: {
|
|
272
|
+
row: Math.max(range.start.row, range.end.row),
|
|
273
|
+
col: Math.max(range.start.col, range.end.col),
|
|
274
|
+
},
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
169
278
|
/**
|
|
170
279
|
* Get a worksheet by name or index
|
|
171
280
|
*/
|
|
@@ -196,7 +305,7 @@ export class Workbook {
|
|
|
196
305
|
const sheetPath = `xl/${rel.target}`;
|
|
197
306
|
const sheetXml = readZipText(this._files, sheetPath);
|
|
198
307
|
if (sheetXml) {
|
|
199
|
-
worksheet.parse(sheetXml);
|
|
308
|
+
worksheet.parse(sheetXml, { lazy: this._lazy });
|
|
200
309
|
}
|
|
201
310
|
}
|
|
202
311
|
|
|
@@ -559,123 +668,6 @@ export class Workbook {
|
|
|
559
668
|
return String(value);
|
|
560
669
|
}
|
|
561
670
|
|
|
562
|
-
/**
|
|
563
|
-
* Create a pivot table from source data.
|
|
564
|
-
*
|
|
565
|
-
* @param config - Pivot table configuration
|
|
566
|
-
* @returns PivotTable instance for fluent configuration
|
|
567
|
-
*
|
|
568
|
-
* @example
|
|
569
|
-
* ```typescript
|
|
570
|
-
* const pivot = wb.createPivotTable({
|
|
571
|
-
* name: 'SalesPivot',
|
|
572
|
-
* source: 'DataSheet!A1:D100',
|
|
573
|
-
* target: 'PivotSheet!A3',
|
|
574
|
-
* });
|
|
575
|
-
*
|
|
576
|
-
* pivot
|
|
577
|
-
* .addRowField('Region')
|
|
578
|
-
* .addColumnField('Product')
|
|
579
|
-
* .addValueField('Sales', 'sum', 'Total Sales');
|
|
580
|
-
* ```
|
|
581
|
-
*/
|
|
582
|
-
createPivotTable(config: PivotTableConfig): PivotTable {
|
|
583
|
-
this._dirty = true;
|
|
584
|
-
|
|
585
|
-
// Parse source reference (Sheet!Range)
|
|
586
|
-
const { sheetName: sourceSheet, range: sourceRange } = this._parseSheetRef(config.source);
|
|
587
|
-
|
|
588
|
-
// Parse target reference
|
|
589
|
-
const { sheetName: targetSheet, range: targetCell } = this._parseSheetRef(config.target);
|
|
590
|
-
|
|
591
|
-
// Ensure target sheet exists
|
|
592
|
-
if (!this._sheetDefs.some((s) => s.name === targetSheet)) {
|
|
593
|
-
this.addSheet(targetSheet);
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
// Parse target cell address
|
|
597
|
-
const targetAddr = parseAddress(targetCell);
|
|
598
|
-
|
|
599
|
-
// Get source worksheet and extract data
|
|
600
|
-
const sourceWs = this.sheet(sourceSheet);
|
|
601
|
-
const { headers, data } = this._extractSourceData(sourceWs, sourceRange);
|
|
602
|
-
|
|
603
|
-
// Create pivot cache
|
|
604
|
-
const cacheId = this._nextCacheId++;
|
|
605
|
-
const cacheFileIndex = this._nextCacheFileIndex++;
|
|
606
|
-
const cache = new PivotCache(cacheId, sourceSheet, sourceRange, cacheFileIndex);
|
|
607
|
-
cache.setStyles(this._styles);
|
|
608
|
-
cache.buildFromData(headers, data);
|
|
609
|
-
// refreshOnLoad defaults to true; only disable if explicitly set to false
|
|
610
|
-
if (config.refreshOnLoad === false) {
|
|
611
|
-
cache.refreshOnLoad = false;
|
|
612
|
-
}
|
|
613
|
-
// saveData defaults to true; only disable if explicitly set to false
|
|
614
|
-
if (config.saveData === false) {
|
|
615
|
-
cache.saveData = false;
|
|
616
|
-
}
|
|
617
|
-
this._pivotCaches.push(cache);
|
|
618
|
-
|
|
619
|
-
// Create pivot table
|
|
620
|
-
const pivotTableIndex = this._pivotTables.length + 1;
|
|
621
|
-
const pivotTable = new PivotTable(
|
|
622
|
-
config.name,
|
|
623
|
-
cache,
|
|
624
|
-
targetSheet,
|
|
625
|
-
targetCell,
|
|
626
|
-
targetAddr.row + 1, // Convert to 1-based
|
|
627
|
-
targetAddr.col,
|
|
628
|
-
pivotTableIndex,
|
|
629
|
-
cacheFileIndex,
|
|
630
|
-
);
|
|
631
|
-
|
|
632
|
-
// Set styles reference for number format resolution
|
|
633
|
-
pivotTable.setStyles(this._styles);
|
|
634
|
-
|
|
635
|
-
this._pivotTables.push(pivotTable);
|
|
636
|
-
|
|
637
|
-
return pivotTable;
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
/**
|
|
641
|
-
* Parse a sheet reference like "Sheet1!A1:D100" into sheet name and range
|
|
642
|
-
*/
|
|
643
|
-
private _parseSheetRef(ref: string): { sheetName: string; range: string } {
|
|
644
|
-
const match = ref.match(/^(.+?)!(.+)$/);
|
|
645
|
-
if (!match) {
|
|
646
|
-
throw new Error(`Invalid reference format: ${ref}. Expected "SheetName!Range"`);
|
|
647
|
-
}
|
|
648
|
-
return { sheetName: match[1], range: match[2] };
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
/**
|
|
652
|
-
* Extract headers and data from a source range
|
|
653
|
-
*/
|
|
654
|
-
private _extractSourceData(sheet: Worksheet, rangeStr: string): { headers: string[]; data: CellValue[][] } {
|
|
655
|
-
const range = parseRange(rangeStr);
|
|
656
|
-
const headers: string[] = [];
|
|
657
|
-
const data: CellValue[][] = [];
|
|
658
|
-
|
|
659
|
-
// First row is headers
|
|
660
|
-
for (let col = range.start.col; col <= range.end.col; col++) {
|
|
661
|
-
const cell = sheet.cell(toAddress(range.start.row, col));
|
|
662
|
-
headers.push(String(cell.value ?? `Column${col + 1}`));
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
// Remaining rows are data
|
|
666
|
-
for (let row = range.start.row + 1; row <= range.end.row; row++) {
|
|
667
|
-
const rowData: CellValue[] = [];
|
|
668
|
-
for (let col = range.start.col; col <= range.end.col; col++) {
|
|
669
|
-
const cell = sheet.cell(toAddress(row, col));
|
|
670
|
-
rowData.push(cell.value);
|
|
671
|
-
}
|
|
672
|
-
data.push(rowData);
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
return { headers, data };
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
|
|
679
671
|
/**
|
|
680
672
|
* Save the workbook to a file
|
|
681
673
|
*/
|
|
@@ -743,7 +735,7 @@ export class Workbook {
|
|
|
743
735
|
const relationshipInfo = this._buildRelationshipInfo();
|
|
744
736
|
|
|
745
737
|
// Update workbook.xml
|
|
746
|
-
this._updateWorkbookXml(relationshipInfo.
|
|
738
|
+
this._updateWorkbookXml(relationshipInfo.pivotCacheRelByTarget);
|
|
747
739
|
|
|
748
740
|
// Update relationships
|
|
749
741
|
this._updateRelationshipsXml(relationshipInfo.relNodes);
|
|
@@ -752,16 +744,24 @@ export class Workbook {
|
|
|
752
744
|
this._updateContentTypes();
|
|
753
745
|
|
|
754
746
|
// Update shared strings if modified
|
|
755
|
-
if (this._sharedStrings
|
|
756
|
-
|
|
747
|
+
if (this._sharedStrings) {
|
|
748
|
+
if (this._sharedStrings.dirty || this._sharedStrings.count > 0) {
|
|
749
|
+
writeZipText(this._files, 'xl/sharedStrings.xml', this._sharedStrings.toXml());
|
|
750
|
+
}
|
|
751
|
+
} else if (this._sharedStringsXml) {
|
|
752
|
+
writeZipText(this._files, 'xl/sharedStrings.xml', this._sharedStringsXml);
|
|
757
753
|
}
|
|
758
754
|
|
|
759
755
|
// Update styles if modified or if file doesn't exist yet
|
|
760
|
-
if (this._styles
|
|
761
|
-
|
|
756
|
+
if (this._styles) {
|
|
757
|
+
if (this._styles.dirty || this._dirty || !this._files.has('xl/styles.xml')) {
|
|
758
|
+
writeZipText(this._files, 'xl/styles.xml', this._styles.toXml());
|
|
759
|
+
}
|
|
760
|
+
} else if (this._stylesXml) {
|
|
761
|
+
writeZipText(this._files, 'xl/styles.xml', this._stylesXml);
|
|
762
762
|
}
|
|
763
763
|
|
|
764
|
-
// Update worksheets
|
|
764
|
+
// Update worksheets
|
|
765
765
|
for (const [name, worksheet] of this._sheets) {
|
|
766
766
|
if (worksheet.dirty || this._dirty || worksheet.tables.length > 0) {
|
|
767
767
|
const def = this._sheetDefs.find((s) => s.name === name);
|
|
@@ -775,17 +775,15 @@ export class Workbook {
|
|
|
775
775
|
}
|
|
776
776
|
}
|
|
777
777
|
|
|
778
|
-
// Update pivot tables
|
|
779
|
-
if (this._pivotTables.length > 0) {
|
|
780
|
-
this._updatePivotTableFiles();
|
|
781
|
-
}
|
|
782
|
-
|
|
783
778
|
// Update tables (sets table rel IDs for tableParts)
|
|
784
779
|
this._updateTableFiles();
|
|
785
780
|
|
|
781
|
+
// Update pivot tables (sets pivot rel IDs for pivotTableParts)
|
|
782
|
+
this._updatePivotFiles();
|
|
783
|
+
|
|
786
784
|
// Update worksheets to align tableParts with relationship IDs
|
|
787
785
|
for (const [name, worksheet] of this._sheets) {
|
|
788
|
-
if (worksheet.dirty || this._dirty || worksheet.tables.length > 0) {
|
|
786
|
+
if (worksheet.dirty || this._dirty || worksheet.tables.length > 0 || this._pivotTables.length > 0) {
|
|
789
787
|
const def = this._sheetDefs.find((s) => s.name === name);
|
|
790
788
|
if (def) {
|
|
791
789
|
const rel = this._relationships.find((r) => r.id === def.rId);
|
|
@@ -798,7 +796,7 @@ export class Workbook {
|
|
|
798
796
|
}
|
|
799
797
|
}
|
|
800
798
|
|
|
801
|
-
private _updateWorkbookXml(
|
|
799
|
+
private _updateWorkbookXml(pivotCacheRelByTarget: Map<string, string>): void {
|
|
802
800
|
const sheetNodes: XmlNode[] = this._sheetDefs.map((def) =>
|
|
803
801
|
createElement('sheet', { name: def.name, sheetId: String(def.sheetId), 'r:id': def.rId }, []),
|
|
804
802
|
);
|
|
@@ -807,16 +805,18 @@ export class Workbook {
|
|
|
807
805
|
|
|
808
806
|
const children: XmlNode[] = [sheetsNode];
|
|
809
807
|
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
const
|
|
813
|
-
const
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
}
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
808
|
+
if (this._pivotTables.length > 0) {
|
|
809
|
+
const pivotCacheNodes: XmlNode[] = [];
|
|
810
|
+
for (const pivot of this._pivotTables) {
|
|
811
|
+
const target = `pivotCache/pivotCacheDefinition${pivot.cachePartIndex}.xml`;
|
|
812
|
+
const relId = pivotCacheRelByTarget.get(target);
|
|
813
|
+
if (!relId) continue;
|
|
814
|
+
pivotCacheNodes.push(createElement('pivotCache', { cacheId: String(pivot.cacheId), 'r:id': relId }, []));
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
if (pivotCacheNodes.length > 0) {
|
|
818
|
+
children.push(createElement('pivotCaches', {}, pivotCacheNodes));
|
|
819
|
+
}
|
|
820
820
|
}
|
|
821
821
|
|
|
822
822
|
const workbookNode = createElement(
|
|
@@ -843,10 +843,11 @@ export class Workbook {
|
|
|
843
843
|
writeZipText(this._files, 'xl/_rels/workbook.xml.rels', xml);
|
|
844
844
|
}
|
|
845
845
|
|
|
846
|
-
private _buildRelationshipInfo(): { relNodes: XmlNode[];
|
|
846
|
+
private _buildRelationshipInfo(): { relNodes: XmlNode[]; pivotCacheRelByTarget: Map<string, string> } {
|
|
847
847
|
const relNodes: XmlNode[] = this._relationships.map((rel) =>
|
|
848
848
|
createElement('Relationship', { Id: rel.id, Type: rel.type, Target: rel.target }, []),
|
|
849
849
|
);
|
|
850
|
+
const pivotCacheRelByTarget = new Map<string, string>();
|
|
850
851
|
|
|
851
852
|
const reservedRelIds = new Set<string>(relNodes.map((node) => getAttr(node, 'Id') || '').filter(Boolean));
|
|
852
853
|
let nextRelId = Math.max(0, ...this._relationships.map((r) => parseInt(r.id.replace('rId', ''), 10) || 0)) + 1;
|
|
@@ -862,7 +863,8 @@ export class Workbook {
|
|
|
862
863
|
};
|
|
863
864
|
|
|
864
865
|
// Add shared strings relationship if needed
|
|
865
|
-
|
|
866
|
+
const shouldIncludeSharedStrings = (this._sharedStrings?.count ?? 0) > 0 || this._sharedStringsXml !== null;
|
|
867
|
+
if (shouldIncludeSharedStrings) {
|
|
866
868
|
const hasSharedStrings = this._relationships.some(
|
|
867
869
|
(r) => r.type === 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings',
|
|
868
870
|
);
|
|
@@ -899,28 +901,48 @@ export class Workbook {
|
|
|
899
901
|
);
|
|
900
902
|
}
|
|
901
903
|
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
'Relationship',
|
|
910
|
-
{
|
|
911
|
-
Id: id,
|
|
912
|
-
Type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotCacheDefinition',
|
|
913
|
-
Target: `pivotCache/pivotCacheDefinition${cache.fileIndex}.xml`,
|
|
914
|
-
},
|
|
915
|
-
[],
|
|
916
|
-
),
|
|
904
|
+
for (const pivot of this._pivotTables) {
|
|
905
|
+
const target = `pivotCache/pivotCacheDefinition${pivot.cachePartIndex}.xml`;
|
|
906
|
+
const hasPivotCacheRel = relNodes.some(
|
|
907
|
+
(node) =>
|
|
908
|
+
getAttr(node, 'Type') ===
|
|
909
|
+
'http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotCacheDefinition' &&
|
|
910
|
+
getAttr(node, 'Target') === target,
|
|
917
911
|
);
|
|
912
|
+
|
|
913
|
+
if (!hasPivotCacheRel) {
|
|
914
|
+
const id = allocateRelId();
|
|
915
|
+
pivotCacheRelByTarget.set(target, id);
|
|
916
|
+
relNodes.push(
|
|
917
|
+
createElement(
|
|
918
|
+
'Relationship',
|
|
919
|
+
{
|
|
920
|
+
Id: id,
|
|
921
|
+
Type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotCacheDefinition',
|
|
922
|
+
Target: target,
|
|
923
|
+
},
|
|
924
|
+
[],
|
|
925
|
+
),
|
|
926
|
+
);
|
|
927
|
+
} else {
|
|
928
|
+
const existing = relNodes.find(
|
|
929
|
+
(node) =>
|
|
930
|
+
getAttr(node, 'Type') ===
|
|
931
|
+
'http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotCacheDefinition' &&
|
|
932
|
+
getAttr(node, 'Target') === target,
|
|
933
|
+
);
|
|
934
|
+
const existingId = existing ? getAttr(existing, 'Id') : undefined;
|
|
935
|
+
if (existingId) {
|
|
936
|
+
pivotCacheRelByTarget.set(target, existingId);
|
|
937
|
+
}
|
|
938
|
+
}
|
|
918
939
|
}
|
|
919
940
|
|
|
920
|
-
return { relNodes,
|
|
941
|
+
return { relNodes, pivotCacheRelByTarget };
|
|
921
942
|
}
|
|
922
943
|
|
|
923
944
|
private _updateContentTypes(): void {
|
|
945
|
+
const shouldIncludeSharedStrings = (this._sharedStrings?.count ?? 0) > 0 || this._sharedStringsXml !== null;
|
|
924
946
|
const types: XmlNode[] = [
|
|
925
947
|
createElement(
|
|
926
948
|
'Default',
|
|
@@ -947,7 +969,7 @@ export class Workbook {
|
|
|
947
969
|
];
|
|
948
970
|
|
|
949
971
|
// Add shared strings if present
|
|
950
|
-
if (
|
|
972
|
+
if (shouldIncludeSharedStrings) {
|
|
951
973
|
types.push(
|
|
952
974
|
createElement(
|
|
953
975
|
'Override',
|
|
@@ -977,37 +999,56 @@ export class Workbook {
|
|
|
977
999
|
}
|
|
978
1000
|
}
|
|
979
1001
|
|
|
980
|
-
// Add
|
|
981
|
-
|
|
1002
|
+
// Add tables
|
|
1003
|
+
let tableIndex = 1;
|
|
1004
|
+
for (const def of this._sheetDefs) {
|
|
1005
|
+
const worksheet = this._sheets.get(def.name);
|
|
1006
|
+
if (worksheet) {
|
|
1007
|
+
for (let i = 0; i < worksheet.tables.length; i++) {
|
|
1008
|
+
types.push(
|
|
1009
|
+
createElement(
|
|
1010
|
+
'Override',
|
|
1011
|
+
{
|
|
1012
|
+
PartName: `/xl/tables/table${tableIndex}.xml`,
|
|
1013
|
+
ContentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.table+xml',
|
|
1014
|
+
},
|
|
1015
|
+
[],
|
|
1016
|
+
),
|
|
1017
|
+
);
|
|
1018
|
+
tableIndex++;
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
// Add pivot caches and pivot tables
|
|
1024
|
+
for (const pivot of this._pivotTables) {
|
|
982
1025
|
types.push(
|
|
983
1026
|
createElement(
|
|
984
1027
|
'Override',
|
|
985
1028
|
{
|
|
986
|
-
PartName: `/xl/pivotCache/pivotCacheDefinition${
|
|
1029
|
+
PartName: `/xl/pivotCache/pivotCacheDefinition${pivot.cachePartIndex}.xml`,
|
|
987
1030
|
ContentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCacheDefinition+xml',
|
|
988
1031
|
},
|
|
989
1032
|
[],
|
|
990
1033
|
),
|
|
991
1034
|
);
|
|
1035
|
+
|
|
992
1036
|
types.push(
|
|
993
1037
|
createElement(
|
|
994
1038
|
'Override',
|
|
995
1039
|
{
|
|
996
|
-
PartName: `/xl/pivotCache/pivotCacheRecords${
|
|
1040
|
+
PartName: `/xl/pivotCache/pivotCacheRecords${pivot.cachePartIndex}.xml`,
|
|
997
1041
|
ContentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCacheRecords+xml',
|
|
998
1042
|
},
|
|
999
1043
|
[],
|
|
1000
1044
|
),
|
|
1001
1045
|
);
|
|
1002
|
-
}
|
|
1003
1046
|
|
|
1004
|
-
// Add pivot tables
|
|
1005
|
-
for (const pivotTable of this._pivotTables) {
|
|
1006
1047
|
types.push(
|
|
1007
1048
|
createElement(
|
|
1008
1049
|
'Override',
|
|
1009
1050
|
{
|
|
1010
|
-
PartName: `/xl/pivotTables/pivotTable${
|
|
1051
|
+
PartName: `/xl/pivotTables/pivotTable${pivot.pivotId}.xml`,
|
|
1011
1052
|
ContentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.pivotTable+xml',
|
|
1012
1053
|
},
|
|
1013
1054
|
[],
|
|
@@ -1015,27 +1056,6 @@ export class Workbook {
|
|
|
1015
1056
|
);
|
|
1016
1057
|
}
|
|
1017
1058
|
|
|
1018
|
-
// Add tables
|
|
1019
|
-
let tableIndex = 1;
|
|
1020
|
-
for (const def of this._sheetDefs) {
|
|
1021
|
-
const worksheet = this._sheets.get(def.name);
|
|
1022
|
-
if (worksheet) {
|
|
1023
|
-
for (let i = 0; i < worksheet.tables.length; i++) {
|
|
1024
|
-
types.push(
|
|
1025
|
-
createElement(
|
|
1026
|
-
'Override',
|
|
1027
|
-
{
|
|
1028
|
-
PartName: `/xl/tables/table${tableIndex}.xml`,
|
|
1029
|
-
ContentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.table+xml',
|
|
1030
|
-
},
|
|
1031
|
-
[],
|
|
1032
|
-
),
|
|
1033
|
-
);
|
|
1034
|
-
tableIndex++;
|
|
1035
|
-
}
|
|
1036
|
-
}
|
|
1037
|
-
}
|
|
1038
|
-
|
|
1039
1059
|
const existingTypesXml = readZipText(this._files, '[Content_Types].xml');
|
|
1040
1060
|
const existingKeys = new Set(
|
|
1041
1061
|
types
|
|
@@ -1110,92 +1130,38 @@ export class Workbook {
|
|
|
1110
1130
|
}
|
|
1111
1131
|
|
|
1112
1132
|
/**
|
|
1113
|
-
* Generate all
|
|
1133
|
+
* Generate all table related files
|
|
1114
1134
|
*/
|
|
1115
|
-
private
|
|
1116
|
-
//
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
for (const pivotTable of this._pivotTables) {
|
|
1120
|
-
const sheetName = pivotTable.targetSheet;
|
|
1121
|
-
if (!sheetPivotTables.has(sheetName)) {
|
|
1122
|
-
sheetPivotTables.set(sheetName, []);
|
|
1123
|
-
}
|
|
1124
|
-
sheetPivotTables.get(sheetName)!.push(pivotTable);
|
|
1125
|
-
}
|
|
1126
|
-
|
|
1127
|
-
// Generate pivot cache files
|
|
1128
|
-
for (let i = 0; i < this._pivotCaches.length; i++) {
|
|
1129
|
-
const cache = this._pivotCaches[i];
|
|
1135
|
+
private _updateTableFiles(): void {
|
|
1136
|
+
// Collect all tables with their global indices
|
|
1137
|
+
let globalTableIndex = 1;
|
|
1138
|
+
const sheetTables: Map<string, { table: import('./table').Table; globalIndex: number }[]> = new Map();
|
|
1130
1139
|
|
|
1131
|
-
|
|
1132
|
-
const
|
|
1133
|
-
|
|
1140
|
+
for (const def of this._sheetDefs) {
|
|
1141
|
+
const worksheet = this._sheets.get(def.name);
|
|
1142
|
+
if (!worksheet) continue;
|
|
1134
1143
|
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
writeZipText(this._files, recordsPath, cache.toRecordsXml());
|
|
1144
|
+
const tables = worksheet.tables;
|
|
1145
|
+
if (tables.length === 0) continue;
|
|
1138
1146
|
|
|
1139
|
-
|
|
1140
|
-
const
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
createElement(
|
|
1146
|
-
'Relationship',
|
|
1147
|
-
{
|
|
1148
|
-
Id: 'rId1',
|
|
1149
|
-
Type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotCacheRecords',
|
|
1150
|
-
Target: `pivotCacheRecords${cache.fileIndex}.xml`,
|
|
1151
|
-
},
|
|
1152
|
-
[],
|
|
1153
|
-
),
|
|
1154
|
-
],
|
|
1155
|
-
);
|
|
1156
|
-
writeZipText(
|
|
1157
|
-
this._files,
|
|
1158
|
-
cacheRelsPath,
|
|
1159
|
-
`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n${stringifyXml([cacheRels])}`,
|
|
1160
|
-
);
|
|
1147
|
+
const tableInfos: { table: import('./table').Table; globalIndex: number }[] = [];
|
|
1148
|
+
for (const table of tables) {
|
|
1149
|
+
tableInfos.push({ table, globalIndex: globalTableIndex });
|
|
1150
|
+
globalTableIndex++;
|
|
1151
|
+
}
|
|
1152
|
+
sheetTables.set(def.name, tableInfos);
|
|
1161
1153
|
}
|
|
1162
1154
|
|
|
1163
|
-
// Generate
|
|
1164
|
-
for (
|
|
1165
|
-
const
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
const ptPath = `xl/pivotTables/pivotTable${ptIdx}.xml`;
|
|
1170
|
-
writeZipText(this._files, ptPath, pivotTable.toXml());
|
|
1171
|
-
|
|
1172
|
-
// Pivot table relationships (link to cache definition)
|
|
1173
|
-
const cacheIdx = pivotTable.cacheFileIndex;
|
|
1174
|
-
const ptRelsPath = `xl/pivotTables/_rels/pivotTable${ptIdx}.xml.rels`;
|
|
1175
|
-
const ptRels = createElement(
|
|
1176
|
-
'Relationships',
|
|
1177
|
-
{ xmlns: 'http://schemas.openxmlformats.org/package/2006/relationships' },
|
|
1178
|
-
[
|
|
1179
|
-
createElement(
|
|
1180
|
-
'Relationship',
|
|
1181
|
-
{
|
|
1182
|
-
Id: 'rId1',
|
|
1183
|
-
Type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotCacheDefinition',
|
|
1184
|
-
Target: `../pivotCache/pivotCacheDefinition${cacheIdx}.xml`,
|
|
1185
|
-
},
|
|
1186
|
-
[],
|
|
1187
|
-
),
|
|
1188
|
-
],
|
|
1189
|
-
);
|
|
1190
|
-
writeZipText(
|
|
1191
|
-
this._files,
|
|
1192
|
-
ptRelsPath,
|
|
1193
|
-
`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n${stringifyXml([ptRels])}`,
|
|
1194
|
-
);
|
|
1155
|
+
// Generate table files
|
|
1156
|
+
for (const [, tableInfos] of sheetTables) {
|
|
1157
|
+
for (const { table, globalIndex } of tableInfos) {
|
|
1158
|
+
const tablePath = `xl/tables/table${globalIndex}.xml`;
|
|
1159
|
+
writeZipText(this._files, tablePath, table.toXml());
|
|
1160
|
+
}
|
|
1195
1161
|
}
|
|
1196
1162
|
|
|
1197
|
-
// Generate worksheet relationships for
|
|
1198
|
-
for (const [sheetName,
|
|
1163
|
+
// Generate worksheet relationships for tables
|
|
1164
|
+
for (const [sheetName, tableInfos] of sheetTables) {
|
|
1199
1165
|
const def = this._sheetDefs.find((s) => s.name === sheetName);
|
|
1200
1166
|
if (!def) continue;
|
|
1201
1167
|
|
|
@@ -1206,12 +1172,14 @@ export class Workbook {
|
|
|
1206
1172
|
const sheetFileName = rel.target.split('/').pop();
|
|
1207
1173
|
const sheetRelsPath = `xl/worksheets/_rels/${sheetFileName}.rels`;
|
|
1208
1174
|
|
|
1175
|
+
// Check if there are already pivot table relationships for this sheet
|
|
1209
1176
|
const existingRelsXml = readZipText(this._files, sheetRelsPath);
|
|
1210
|
-
let relNodes: XmlNode[] = [];
|
|
1211
1177
|
let nextRelId = 1;
|
|
1178
|
+
const relNodes: XmlNode[] = [];
|
|
1212
1179
|
const reservedRelIds = new Set<string>();
|
|
1213
1180
|
|
|
1214
1181
|
if (existingRelsXml) {
|
|
1182
|
+
// Parse existing rels and find max rId
|
|
1215
1183
|
const parsed = parseXml(existingRelsXml);
|
|
1216
1184
|
const relsElement = findElement(parsed, 'Relationships');
|
|
1217
1185
|
if (relsElement) {
|
|
@@ -1242,21 +1210,33 @@ export class Workbook {
|
|
|
1242
1210
|
return id;
|
|
1243
1211
|
};
|
|
1244
1212
|
|
|
1245
|
-
|
|
1246
|
-
|
|
1213
|
+
// Add table relationships
|
|
1214
|
+
const tableRelIds: string[] = [];
|
|
1215
|
+
for (const { globalIndex } of tableInfos) {
|
|
1216
|
+
const target = `../tables/table${globalIndex}.xml`;
|
|
1247
1217
|
const existing = relNodes.some(
|
|
1248
1218
|
(node) =>
|
|
1249
|
-
getAttr(node, 'Type') ===
|
|
1250
|
-
'http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotTable' &&
|
|
1219
|
+
getAttr(node, 'Type') === 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/table' &&
|
|
1251
1220
|
getAttr(node, 'Target') === target,
|
|
1252
1221
|
);
|
|
1253
|
-
if (existing)
|
|
1222
|
+
if (existing) {
|
|
1223
|
+
const existingRel = relNodes.find(
|
|
1224
|
+
(node) =>
|
|
1225
|
+
getAttr(node, 'Type') === 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/table' &&
|
|
1226
|
+
getAttr(node, 'Target') === target,
|
|
1227
|
+
);
|
|
1228
|
+
const existingId = existingRel ? getAttr(existingRel, 'Id') : undefined;
|
|
1229
|
+
tableRelIds.push(existingId ?? allocateRelId());
|
|
1230
|
+
continue;
|
|
1231
|
+
}
|
|
1232
|
+
const id = allocateRelId();
|
|
1233
|
+
tableRelIds.push(id);
|
|
1254
1234
|
relNodes.push(
|
|
1255
1235
|
createElement(
|
|
1256
1236
|
'Relationship',
|
|
1257
1237
|
{
|
|
1258
|
-
Id:
|
|
1259
|
-
Type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/
|
|
1238
|
+
Id: id,
|
|
1239
|
+
Type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/table',
|
|
1260
1240
|
Target: target,
|
|
1261
1241
|
},
|
|
1262
1242
|
[],
|
|
@@ -1264,6 +1244,11 @@ export class Workbook {
|
|
|
1264
1244
|
);
|
|
1265
1245
|
}
|
|
1266
1246
|
|
|
1247
|
+
const worksheet = this._sheets.get(sheetName);
|
|
1248
|
+
if (worksheet) {
|
|
1249
|
+
worksheet.setTableRelIds(tableRelIds);
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1267
1252
|
const sheetRels = createElement(
|
|
1268
1253
|
'Relationships',
|
|
1269
1254
|
{ xmlns: 'http://schemas.openxmlformats.org/package/2006/relationships' },
|
|
@@ -1278,61 +1263,58 @@ export class Workbook {
|
|
|
1278
1263
|
}
|
|
1279
1264
|
|
|
1280
1265
|
/**
|
|
1281
|
-
* Generate
|
|
1266
|
+
* Generate pivot cache/table parts and worksheet relationships.
|
|
1282
1267
|
*/
|
|
1283
|
-
private
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1268
|
+
private _updatePivotFiles(): void {
|
|
1269
|
+
if (this._pivotTables.length === 0) {
|
|
1270
|
+
return;
|
|
1271
|
+
}
|
|
1287
1272
|
|
|
1288
|
-
for (const
|
|
1289
|
-
const
|
|
1290
|
-
if (!worksheet) continue;
|
|
1273
|
+
for (const pivot of this._pivotTables) {
|
|
1274
|
+
const pivotParts = pivot.buildPivotPartsXml();
|
|
1291
1275
|
|
|
1292
|
-
const
|
|
1293
|
-
|
|
1276
|
+
const pivotCachePath = `xl/pivotCache/pivotCacheDefinition${pivot.cachePartIndex}.xml`;
|
|
1277
|
+
writeZipText(this._files, pivotCachePath, pivotParts.cacheDefinitionXml);
|
|
1294
1278
|
|
|
1295
|
-
const
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1279
|
+
const pivotCacheRecordsPath = `xl/pivotCache/pivotCacheRecords${pivot.cachePartIndex}.xml`;
|
|
1280
|
+
writeZipText(this._files, pivotCacheRecordsPath, pivotParts.cacheRecordsXml);
|
|
1281
|
+
|
|
1282
|
+
const pivotCacheRelsPath = `xl/pivotCache/_rels/pivotCacheDefinition${pivot.cachePartIndex}.xml.rels`;
|
|
1283
|
+
writeZipText(this._files, pivotCacheRelsPath, pivotParts.cacheRelsXml);
|
|
1284
|
+
|
|
1285
|
+
const pivotTablePath = `xl/pivotTables/pivotTable${pivot.pivotId}.xml`;
|
|
1286
|
+
writeZipText(this._files, pivotTablePath, pivotParts.pivotTableXml);
|
|
1301
1287
|
}
|
|
1302
1288
|
|
|
1303
|
-
|
|
1304
|
-
for (const
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
}
|
|
1289
|
+
const pivotsBySheet = new Map<string, PivotTable[]>();
|
|
1290
|
+
for (const pivot of this._pivotTables) {
|
|
1291
|
+
const existing = pivotsBySheet.get(pivot.targetSheetName) ?? [];
|
|
1292
|
+
existing.push(pivot);
|
|
1293
|
+
pivotsBySheet.set(pivot.targetSheetName, existing);
|
|
1309
1294
|
}
|
|
1310
1295
|
|
|
1311
|
-
|
|
1312
|
-
for (const [sheetName, tableInfos] of sheetTables) {
|
|
1296
|
+
for (const [sheetName, pivots] of pivotsBySheet) {
|
|
1313
1297
|
const def = this._sheetDefs.find((s) => s.name === sheetName);
|
|
1314
1298
|
if (!def) continue;
|
|
1315
1299
|
|
|
1316
1300
|
const rel = this._relationships.find((r) => r.id === def.rId);
|
|
1317
1301
|
if (!rel) continue;
|
|
1318
1302
|
|
|
1319
|
-
// Extract sheet file name from target path
|
|
1320
1303
|
const sheetFileName = rel.target.split('/').pop();
|
|
1321
|
-
|
|
1304
|
+
if (!sheetFileName) continue;
|
|
1322
1305
|
|
|
1323
|
-
|
|
1306
|
+
const sheetRelsPath = `xl/worksheets/_rels/${sheetFileName}.rels`;
|
|
1324
1307
|
const existingRelsXml = readZipText(this._files, sheetRelsPath);
|
|
1308
|
+
|
|
1325
1309
|
let nextRelId = 1;
|
|
1326
1310
|
const relNodes: XmlNode[] = [];
|
|
1327
1311
|
const reservedRelIds = new Set<string>();
|
|
1328
1312
|
|
|
1329
1313
|
if (existingRelsXml) {
|
|
1330
|
-
// Parse existing rels and find max rId
|
|
1331
1314
|
const parsed = parseXml(existingRelsXml);
|
|
1332
1315
|
const relsElement = findElement(parsed, 'Relationships');
|
|
1333
1316
|
if (relsElement) {
|
|
1334
|
-
const
|
|
1335
|
-
for (const relNode of existingRelNodes) {
|
|
1317
|
+
for (const relNode of getChildren(relsElement, 'Relationships')) {
|
|
1336
1318
|
if ('Relationship' in relNode) {
|
|
1337
1319
|
relNodes.push(relNode);
|
|
1338
1320
|
const id = getAttr(relNode, 'Id');
|
|
@@ -1358,33 +1340,30 @@ export class Workbook {
|
|
|
1358
1340
|
return id;
|
|
1359
1341
|
};
|
|
1360
1342
|
|
|
1361
|
-
|
|
1362
|
-
const
|
|
1363
|
-
|
|
1364
|
-
const
|
|
1365
|
-
const existing = relNodes.some(
|
|
1343
|
+
const pivotRelIds: string[] = [];
|
|
1344
|
+
for (const pivot of pivots) {
|
|
1345
|
+
const target = `../pivotTables/pivotTable${pivot.pivotId}.xml`;
|
|
1346
|
+
const existing = relNodes.find(
|
|
1366
1347
|
(node) =>
|
|
1367
|
-
getAttr(node, 'Type') ===
|
|
1348
|
+
getAttr(node, 'Type') ===
|
|
1349
|
+
'http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotTable' &&
|
|
1368
1350
|
getAttr(node, 'Target') === target,
|
|
1369
1351
|
);
|
|
1352
|
+
|
|
1370
1353
|
if (existing) {
|
|
1371
|
-
const
|
|
1372
|
-
|
|
1373
|
-
getAttr(node, 'Type') === 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/table' &&
|
|
1374
|
-
getAttr(node, 'Target') === target,
|
|
1375
|
-
);
|
|
1376
|
-
const existingId = existingRel ? getAttr(existingRel, 'Id') : undefined;
|
|
1377
|
-
tableRelIds.push(existingId ?? allocateRelId());
|
|
1354
|
+
const existingId = getAttr(existing, 'Id');
|
|
1355
|
+
pivotRelIds.push(existingId ?? allocateRelId());
|
|
1378
1356
|
continue;
|
|
1379
1357
|
}
|
|
1358
|
+
|
|
1380
1359
|
const id = allocateRelId();
|
|
1381
|
-
|
|
1360
|
+
pivotRelIds.push(id);
|
|
1382
1361
|
relNodes.push(
|
|
1383
1362
|
createElement(
|
|
1384
1363
|
'Relationship',
|
|
1385
1364
|
{
|
|
1386
1365
|
Id: id,
|
|
1387
|
-
Type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/
|
|
1366
|
+
Type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotTable',
|
|
1388
1367
|
Target: target,
|
|
1389
1368
|
},
|
|
1390
1369
|
[],
|
|
@@ -1394,7 +1373,7 @@ export class Workbook {
|
|
|
1394
1373
|
|
|
1395
1374
|
const worksheet = this._sheets.get(sheetName);
|
|
1396
1375
|
if (worksheet) {
|
|
1397
|
-
worksheet.
|
|
1376
|
+
worksheet.setPivotTableRelIds(pivotRelIds);
|
|
1398
1377
|
}
|
|
1399
1378
|
|
|
1400
1379
|
const sheetRels = createElement(
|
|
@@ -1402,6 +1381,7 @@ export class Workbook {
|
|
|
1402
1381
|
{ xmlns: 'http://schemas.openxmlformats.org/package/2006/relationships' },
|
|
1403
1382
|
relNodes,
|
|
1404
1383
|
);
|
|
1384
|
+
|
|
1405
1385
|
writeZipText(
|
|
1406
1386
|
this._files,
|
|
1407
1387
|
sheetRelsPath,
|