@niicojs/excel 0.2.7 → 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 +1455 -152
- package/dist/index.d.cts +359 -5
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +359 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1455 -153
- package/package.json +1 -1
- package/src/index.ts +9 -1
- package/src/pivot-cache.ts +10 -1
- package/src/pivot-table.ts +129 -31
- 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 +70 -0
- 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/src/workbook.ts
CHANGED
|
@@ -7,6 +7,7 @@ import type {
|
|
|
7
7
|
SheetFromDataConfig,
|
|
8
8
|
ColumnConfig,
|
|
9
9
|
RichCellValue,
|
|
10
|
+
DateHandling,
|
|
10
11
|
} from './types';
|
|
11
12
|
import { Worksheet } from './worksheet';
|
|
12
13
|
import { SharedStrings } from './shared-strings';
|
|
@@ -33,6 +34,13 @@ export class Workbook {
|
|
|
33
34
|
private _pivotTables: PivotTable[] = [];
|
|
34
35
|
private _pivotCaches: PivotCache[] = [];
|
|
35
36
|
private _nextCacheId = 0;
|
|
37
|
+
private _nextCacheFileIndex = 1;
|
|
38
|
+
|
|
39
|
+
// Table support
|
|
40
|
+
private _nextTableId = 1;
|
|
41
|
+
|
|
42
|
+
// Date serialization handling
|
|
43
|
+
private _dateHandling: DateHandling = 'jsDate';
|
|
36
44
|
|
|
37
45
|
private constructor() {
|
|
38
46
|
this._sharedStrings = new SharedStrings();
|
|
@@ -119,6 +127,29 @@ export class Workbook {
|
|
|
119
127
|
return this._styles;
|
|
120
128
|
}
|
|
121
129
|
|
|
130
|
+
/**
|
|
131
|
+
* Get the workbook date handling strategy.
|
|
132
|
+
*/
|
|
133
|
+
get dateHandling(): DateHandling {
|
|
134
|
+
return this._dateHandling;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Set the workbook date handling strategy.
|
|
139
|
+
*/
|
|
140
|
+
set dateHandling(value: DateHandling) {
|
|
141
|
+
this._dateHandling = value;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Get the next unique table ID for this workbook.
|
|
146
|
+
* Table IDs must be unique across all worksheets.
|
|
147
|
+
* @internal
|
|
148
|
+
*/
|
|
149
|
+
getNextTableId(): number {
|
|
150
|
+
return this._nextTableId++;
|
|
151
|
+
}
|
|
152
|
+
|
|
122
153
|
/**
|
|
123
154
|
* Get a worksheet by name or index
|
|
124
155
|
*/
|
|
@@ -221,6 +252,12 @@ export class Workbook {
|
|
|
221
252
|
this._sheetDefs.splice(index, 1);
|
|
222
253
|
this._sheets.delete(def.name);
|
|
223
254
|
|
|
255
|
+
const rel = this._relationships.find((r) => r.id === def.rId);
|
|
256
|
+
if (rel) {
|
|
257
|
+
const sheetPath = `xl/${rel.target}`;
|
|
258
|
+
this._files.delete(sheetPath);
|
|
259
|
+
}
|
|
260
|
+
|
|
224
261
|
// Remove relationship
|
|
225
262
|
const relIndex = this._relationships.findIndex((r) => r.id === def.rId);
|
|
226
263
|
if (relIndex >= 0) {
|
|
@@ -273,14 +310,89 @@ export class Workbook {
|
|
|
273
310
|
}
|
|
274
311
|
}
|
|
275
312
|
|
|
313
|
+
// Copy column widths
|
|
314
|
+
for (const [col, width] of source.getColumnWidths()) {
|
|
315
|
+
copy.setColumnWidth(col, width);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Copy row heights
|
|
319
|
+
for (const [row, height] of source.getRowHeights()) {
|
|
320
|
+
copy.setRowHeight(row, height);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Copy frozen panes
|
|
324
|
+
const frozen = source.getFrozenPane();
|
|
325
|
+
if (frozen) {
|
|
326
|
+
copy.freezePane(frozen.row, frozen.col);
|
|
327
|
+
}
|
|
328
|
+
|
|
276
329
|
// Copy merged cells
|
|
277
330
|
for (const mergedRange of source.mergedCells) {
|
|
278
331
|
copy.mergeCells(mergedRange);
|
|
279
332
|
}
|
|
280
333
|
|
|
334
|
+
// Copy tables
|
|
335
|
+
for (const table of source.tables) {
|
|
336
|
+
const tableName = this._createUniqueTableName(table.name, newName);
|
|
337
|
+
const newTable = copy.createTable({
|
|
338
|
+
name: tableName,
|
|
339
|
+
range: table.baseRange,
|
|
340
|
+
totalRow: table.hasTotalRow,
|
|
341
|
+
headerRow: table.hasHeaderRow,
|
|
342
|
+
style: table.style,
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
if (!table.hasAutoFilter) {
|
|
346
|
+
newTable.setAutoFilter(false);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (table.hasTotalRow) {
|
|
350
|
+
for (const columnName of table.columns) {
|
|
351
|
+
const fn = table.getTotalFunction(columnName);
|
|
352
|
+
if (fn) {
|
|
353
|
+
newTable.setTotalFunction(columnName, fn);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
281
359
|
return copy;
|
|
282
360
|
}
|
|
283
361
|
|
|
362
|
+
private _createUniqueTableName(base: string, sheetName: string): string {
|
|
363
|
+
const normalizedSheet = sheetName.replace(/[^A-Za-z0-9_.]/g, '_');
|
|
364
|
+
const sanitizedBase = this._sanitizeTableName(`${base}_${normalizedSheet || 'Sheet'}`);
|
|
365
|
+
let candidate = sanitizedBase;
|
|
366
|
+
let counter = 1;
|
|
367
|
+
|
|
368
|
+
while (this._hasTableName(candidate)) {
|
|
369
|
+
candidate = `${sanitizedBase}_${counter++}`;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
return candidate;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
private _sanitizeTableName(name: string): string {
|
|
376
|
+
let result = name.replace(/[^A-Za-z0-9_.]/g, '_');
|
|
377
|
+
if (!/^[A-Za-z_]/.test(result)) {
|
|
378
|
+
result = `_${result}`;
|
|
379
|
+
}
|
|
380
|
+
if (result.length === 0) {
|
|
381
|
+
result = 'Table';
|
|
382
|
+
}
|
|
383
|
+
return result;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
private _hasTableName(name: string): boolean {
|
|
387
|
+
for (const sheetName of this.sheetNames) {
|
|
388
|
+
const ws = this.sheet(sheetName);
|
|
389
|
+
for (const table of ws.tables) {
|
|
390
|
+
if (table.name === name) return true;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
return false;
|
|
394
|
+
}
|
|
395
|
+
|
|
284
396
|
/**
|
|
285
397
|
* Create a new worksheet from an array of objects.
|
|
286
398
|
*
|
|
@@ -474,7 +586,8 @@ export class Workbook {
|
|
|
474
586
|
|
|
475
587
|
// Create pivot cache
|
|
476
588
|
const cacheId = this._nextCacheId++;
|
|
477
|
-
const
|
|
589
|
+
const cacheFileIndex = this._nextCacheFileIndex++;
|
|
590
|
+
const cache = new PivotCache(cacheId, sourceSheet, sourceRange, cacheFileIndex);
|
|
478
591
|
cache.buildFromData(headers, data);
|
|
479
592
|
// refreshOnLoad defaults to true; only disable if explicitly set to false
|
|
480
593
|
if (config.refreshOnLoad === false) {
|
|
@@ -492,6 +605,7 @@ export class Workbook {
|
|
|
492
605
|
targetAddr.row + 1, // Convert to 1-based
|
|
493
606
|
targetAddr.col,
|
|
494
607
|
pivotTableIndex,
|
|
608
|
+
cacheFileIndex,
|
|
495
609
|
);
|
|
496
610
|
|
|
497
611
|
// Set styles reference for number format resolution
|
|
@@ -604,11 +718,13 @@ export class Workbook {
|
|
|
604
718
|
}
|
|
605
719
|
|
|
606
720
|
private _updateFiles(): void {
|
|
721
|
+
const relationshipInfo = this._buildRelationshipInfo();
|
|
722
|
+
|
|
607
723
|
// Update workbook.xml
|
|
608
|
-
this._updateWorkbookXml();
|
|
724
|
+
this._updateWorkbookXml(relationshipInfo.pivotCacheRelIds);
|
|
609
725
|
|
|
610
726
|
// Update relationships
|
|
611
|
-
this._updateRelationshipsXml();
|
|
727
|
+
this._updateRelationshipsXml(relationshipInfo.relNodes);
|
|
612
728
|
|
|
613
729
|
// Update content types
|
|
614
730
|
this._updateContentTypes();
|
|
@@ -623,9 +739,9 @@ export class Workbook {
|
|
|
623
739
|
writeZipText(this._files, 'xl/styles.xml', this._styles.toXml());
|
|
624
740
|
}
|
|
625
741
|
|
|
626
|
-
// Update worksheets
|
|
742
|
+
// Update worksheets (needed for pivot table targets)
|
|
627
743
|
for (const [name, worksheet] of this._sheets) {
|
|
628
|
-
if (worksheet.dirty || this._dirty) {
|
|
744
|
+
if (worksheet.dirty || this._dirty || worksheet.tables.length > 0) {
|
|
629
745
|
const def = this._sheetDefs.find((s) => s.name === name);
|
|
630
746
|
if (def) {
|
|
631
747
|
const rel = this._relationships.find((r) => r.id === def.rId);
|
|
@@ -641,9 +757,26 @@ export class Workbook {
|
|
|
641
757
|
if (this._pivotTables.length > 0) {
|
|
642
758
|
this._updatePivotTableFiles();
|
|
643
759
|
}
|
|
760
|
+
|
|
761
|
+
// Update tables (sets table rel IDs for tableParts)
|
|
762
|
+
this._updateTableFiles();
|
|
763
|
+
|
|
764
|
+
// Update worksheets to align tableParts with relationship IDs
|
|
765
|
+
for (const [name, worksheet] of this._sheets) {
|
|
766
|
+
if (worksheet.dirty || this._dirty || worksheet.tables.length > 0) {
|
|
767
|
+
const def = this._sheetDefs.find((s) => s.name === name);
|
|
768
|
+
if (def) {
|
|
769
|
+
const rel = this._relationships.find((r) => r.id === def.rId);
|
|
770
|
+
if (rel) {
|
|
771
|
+
const sheetPath = `xl/${rel.target}`;
|
|
772
|
+
writeZipText(this._files, sheetPath, worksheet.toXml());
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
}
|
|
644
777
|
}
|
|
645
778
|
|
|
646
|
-
private _updateWorkbookXml(): void {
|
|
779
|
+
private _updateWorkbookXml(pivotCacheRelIds: Map<number, string>): void {
|
|
647
780
|
const sheetNodes: XmlNode[] = this._sheetDefs.map((def) =>
|
|
648
781
|
createElement('sheet', { name: def.name, sheetId: String(def.sheetId), 'r:id': def.rId }, []),
|
|
649
782
|
);
|
|
@@ -654,9 +787,11 @@ export class Workbook {
|
|
|
654
787
|
|
|
655
788
|
// Add pivot caches if any
|
|
656
789
|
if (this._pivotCaches.length > 0) {
|
|
657
|
-
const pivotCacheNodes: XmlNode[] = this._pivotCaches.map((cache
|
|
658
|
-
|
|
659
|
-
|
|
790
|
+
const pivotCacheNodes: XmlNode[] = this._pivotCaches.map((cache) => {
|
|
791
|
+
const cacheRelId = pivotCacheRelIds.get(cache.cacheId);
|
|
792
|
+
if (!cacheRelId) {
|
|
793
|
+
throw new Error(`Missing pivot cache relationship ID for cache ${cache.cacheId}`);
|
|
794
|
+
}
|
|
660
795
|
return createElement('pivotCache', { cacheId: String(cache.cacheId), 'r:id': cacheRelId }, []);
|
|
661
796
|
});
|
|
662
797
|
children.push(createElement('pivotCaches', {}, pivotCacheNodes));
|
|
@@ -675,14 +810,35 @@ export class Workbook {
|
|
|
675
810
|
writeZipText(this._files, 'xl/workbook.xml', xml);
|
|
676
811
|
}
|
|
677
812
|
|
|
678
|
-
private _updateRelationshipsXml(): void {
|
|
813
|
+
private _updateRelationshipsXml(relNodes: XmlNode[]): void {
|
|
814
|
+
const relsNode = createElement(
|
|
815
|
+
'Relationships',
|
|
816
|
+
{ xmlns: 'http://schemas.openxmlformats.org/package/2006/relationships' },
|
|
817
|
+
relNodes,
|
|
818
|
+
);
|
|
819
|
+
|
|
820
|
+
const xml = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n${stringifyXml([relsNode])}`;
|
|
821
|
+
writeZipText(this._files, 'xl/_rels/workbook.xml.rels', xml);
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
private _buildRelationshipInfo(): { relNodes: XmlNode[]; pivotCacheRelIds: Map<number, string> } {
|
|
679
825
|
const relNodes: XmlNode[] = this._relationships.map((rel) =>
|
|
680
826
|
createElement('Relationship', { Id: rel.id, Type: rel.type, Target: rel.target }, []),
|
|
681
827
|
);
|
|
682
828
|
|
|
683
|
-
|
|
829
|
+
const reservedRelIds = new Set<string>(relNodes.map((node) => getAttr(node, 'Id') || '').filter(Boolean));
|
|
684
830
|
let nextRelId = Math.max(0, ...this._relationships.map((r) => parseInt(r.id.replace('rId', ''), 10) || 0)) + 1;
|
|
685
831
|
|
|
832
|
+
const allocateRelId = (): string => {
|
|
833
|
+
while (reservedRelIds.has(`rId${nextRelId}`)) {
|
|
834
|
+
nextRelId++;
|
|
835
|
+
}
|
|
836
|
+
const id = `rId${nextRelId}`;
|
|
837
|
+
nextRelId++;
|
|
838
|
+
reservedRelIds.add(id);
|
|
839
|
+
return id;
|
|
840
|
+
};
|
|
841
|
+
|
|
686
842
|
// Add shared strings relationship if needed
|
|
687
843
|
if (this._sharedStrings.count > 0) {
|
|
688
844
|
const hasSharedStrings = this._relationships.some(
|
|
@@ -693,7 +849,7 @@ export class Workbook {
|
|
|
693
849
|
createElement(
|
|
694
850
|
'Relationship',
|
|
695
851
|
{
|
|
696
|
-
Id:
|
|
852
|
+
Id: allocateRelId(),
|
|
697
853
|
Type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings',
|
|
698
854
|
Target: 'sharedStrings.xml',
|
|
699
855
|
},
|
|
@@ -712,7 +868,7 @@ export class Workbook {
|
|
|
712
868
|
createElement(
|
|
713
869
|
'Relationship',
|
|
714
870
|
{
|
|
715
|
-
Id:
|
|
871
|
+
Id: allocateRelId(),
|
|
716
872
|
Type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles',
|
|
717
873
|
Target: 'styles.xml',
|
|
718
874
|
},
|
|
@@ -722,28 +878,24 @@ export class Workbook {
|
|
|
722
878
|
}
|
|
723
879
|
|
|
724
880
|
// Add pivot cache relationships
|
|
725
|
-
|
|
881
|
+
const pivotCacheRelIds = new Map<number, string>();
|
|
882
|
+
for (const cache of this._pivotCaches) {
|
|
883
|
+
const id = allocateRelId();
|
|
884
|
+
pivotCacheRelIds.set(cache.cacheId, id);
|
|
726
885
|
relNodes.push(
|
|
727
886
|
createElement(
|
|
728
887
|
'Relationship',
|
|
729
888
|
{
|
|
730
|
-
Id:
|
|
889
|
+
Id: id,
|
|
731
890
|
Type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotCacheDefinition',
|
|
732
|
-
Target: `pivotCache/pivotCacheDefinition${
|
|
891
|
+
Target: `pivotCache/pivotCacheDefinition${cache.fileIndex}.xml`,
|
|
733
892
|
},
|
|
734
893
|
[],
|
|
735
894
|
),
|
|
736
895
|
);
|
|
737
896
|
}
|
|
738
897
|
|
|
739
|
-
|
|
740
|
-
'Relationships',
|
|
741
|
-
{ xmlns: 'http://schemas.openxmlformats.org/package/2006/relationships' },
|
|
742
|
-
relNodes,
|
|
743
|
-
);
|
|
744
|
-
|
|
745
|
-
const xml = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n${stringifyXml([relsNode])}`;
|
|
746
|
-
writeZipText(this._files, 'xl/_rels/workbook.xml.rels', xml);
|
|
898
|
+
return { relNodes, pivotCacheRelIds };
|
|
747
899
|
}
|
|
748
900
|
|
|
749
901
|
private _updateContentTypes(): void {
|
|
@@ -804,12 +956,12 @@ export class Workbook {
|
|
|
804
956
|
}
|
|
805
957
|
|
|
806
958
|
// Add pivot cache definitions and records
|
|
807
|
-
for (
|
|
959
|
+
for (const cache of this._pivotCaches) {
|
|
808
960
|
types.push(
|
|
809
961
|
createElement(
|
|
810
962
|
'Override',
|
|
811
963
|
{
|
|
812
|
-
PartName: `/xl/pivotCache/pivotCacheDefinition${
|
|
964
|
+
PartName: `/xl/pivotCache/pivotCacheDefinition${cache.fileIndex}.xml`,
|
|
813
965
|
ContentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCacheDefinition+xml',
|
|
814
966
|
},
|
|
815
967
|
[],
|
|
@@ -819,7 +971,7 @@ export class Workbook {
|
|
|
819
971
|
createElement(
|
|
820
972
|
'Override',
|
|
821
973
|
{
|
|
822
|
-
PartName: `/xl/pivotCache/pivotCacheRecords${
|
|
974
|
+
PartName: `/xl/pivotCache/pivotCacheRecords${cache.fileIndex}.xml`,
|
|
823
975
|
ContentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCacheRecords+xml',
|
|
824
976
|
},
|
|
825
977
|
[],
|
|
@@ -828,12 +980,12 @@ export class Workbook {
|
|
|
828
980
|
}
|
|
829
981
|
|
|
830
982
|
// Add pivot tables
|
|
831
|
-
for (
|
|
983
|
+
for (const pivotTable of this._pivotTables) {
|
|
832
984
|
types.push(
|
|
833
985
|
createElement(
|
|
834
986
|
'Override',
|
|
835
987
|
{
|
|
836
|
-
PartName: `/xl/pivotTables/pivotTable${
|
|
988
|
+
PartName: `/xl/pivotTables/pivotTable${pivotTable.index}.xml`,
|
|
837
989
|
ContentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.pivotTable+xml',
|
|
838
990
|
},
|
|
839
991
|
[],
|
|
@@ -841,6 +993,65 @@ export class Workbook {
|
|
|
841
993
|
);
|
|
842
994
|
}
|
|
843
995
|
|
|
996
|
+
// Add tables
|
|
997
|
+
let tableIndex = 1;
|
|
998
|
+
for (const def of this._sheetDefs) {
|
|
999
|
+
const worksheet = this._sheets.get(def.name);
|
|
1000
|
+
if (worksheet) {
|
|
1001
|
+
for (let i = 0; i < worksheet.tables.length; i++) {
|
|
1002
|
+
types.push(
|
|
1003
|
+
createElement(
|
|
1004
|
+
'Override',
|
|
1005
|
+
{
|
|
1006
|
+
PartName: `/xl/tables/table${tableIndex}.xml`,
|
|
1007
|
+
ContentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.table+xml',
|
|
1008
|
+
},
|
|
1009
|
+
[],
|
|
1010
|
+
),
|
|
1011
|
+
);
|
|
1012
|
+
tableIndex++;
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
const existingTypesXml = readZipText(this._files, '[Content_Types].xml');
|
|
1018
|
+
const existingKeys = new Set(
|
|
1019
|
+
types
|
|
1020
|
+
.map((t) => {
|
|
1021
|
+
if ('Default' in t) {
|
|
1022
|
+
const a = t[':@'] as Record<string, string> | undefined;
|
|
1023
|
+
return `Default:${a?.['@_Extension'] || ''}`;
|
|
1024
|
+
}
|
|
1025
|
+
if ('Override' in t) {
|
|
1026
|
+
const a = t[':@'] as Record<string, string> | undefined;
|
|
1027
|
+
return `Override:${a?.['@_PartName'] || ''}`;
|
|
1028
|
+
}
|
|
1029
|
+
return '';
|
|
1030
|
+
})
|
|
1031
|
+
.filter(Boolean),
|
|
1032
|
+
);
|
|
1033
|
+
if (existingTypesXml) {
|
|
1034
|
+
const parsed = parseXml(existingTypesXml);
|
|
1035
|
+
const typesElement = findElement(parsed, 'Types');
|
|
1036
|
+
if (typesElement) {
|
|
1037
|
+
const existingNodes = getChildren(typesElement, 'Types');
|
|
1038
|
+
for (const node of existingNodes) {
|
|
1039
|
+
if ('Default' in node || 'Override' in node) {
|
|
1040
|
+
const type = 'Default' in node ? 'Default' : 'Override';
|
|
1041
|
+
const attrs = node[':@'] as Record<string, string> | undefined;
|
|
1042
|
+
const key =
|
|
1043
|
+
type === 'Default'
|
|
1044
|
+
? `Default:${attrs?.['@_Extension'] || ''}`
|
|
1045
|
+
: `Override:${attrs?.['@_PartName'] || ''}`;
|
|
1046
|
+
if (!existingKeys.has(key)) {
|
|
1047
|
+
types.push(node);
|
|
1048
|
+
existingKeys.add(key);
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
|
|
844
1055
|
const typesNode = createElement(
|
|
845
1056
|
'Types',
|
|
846
1057
|
{ xmlns: 'http://schemas.openxmlformats.org/package/2006/content-types' },
|
|
@@ -894,18 +1105,17 @@ export class Workbook {
|
|
|
894
1105
|
// Generate pivot cache files
|
|
895
1106
|
for (let i = 0; i < this._pivotCaches.length; i++) {
|
|
896
1107
|
const cache = this._pivotCaches[i];
|
|
897
|
-
const cacheIdx = i + 1;
|
|
898
1108
|
|
|
899
1109
|
// Pivot cache definition
|
|
900
|
-
const definitionPath = `xl/pivotCache/pivotCacheDefinition${
|
|
1110
|
+
const definitionPath = `xl/pivotCache/pivotCacheDefinition${cache.fileIndex}.xml`;
|
|
901
1111
|
writeZipText(this._files, definitionPath, cache.toDefinitionXml('rId1'));
|
|
902
1112
|
|
|
903
1113
|
// Pivot cache records
|
|
904
|
-
const recordsPath = `xl/pivotCache/pivotCacheRecords${
|
|
1114
|
+
const recordsPath = `xl/pivotCache/pivotCacheRecords${cache.fileIndex}.xml`;
|
|
905
1115
|
writeZipText(this._files, recordsPath, cache.toRecordsXml());
|
|
906
1116
|
|
|
907
1117
|
// Pivot cache definition relationships (link to records)
|
|
908
|
-
const cacheRelsPath = `xl/pivotCache/_rels/pivotCacheDefinition${
|
|
1118
|
+
const cacheRelsPath = `xl/pivotCache/_rels/pivotCacheDefinition${cache.fileIndex}.xml.rels`;
|
|
909
1119
|
const cacheRels = createElement(
|
|
910
1120
|
'Relationships',
|
|
911
1121
|
{ xmlns: 'http://schemas.openxmlformats.org/package/2006/relationships' },
|
|
@@ -915,7 +1125,7 @@ export class Workbook {
|
|
|
915
1125
|
{
|
|
916
1126
|
Id: 'rId1',
|
|
917
1127
|
Type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotCacheRecords',
|
|
918
|
-
Target: `pivotCacheRecords${
|
|
1128
|
+
Target: `pivotCacheRecords${cache.fileIndex}.xml`,
|
|
919
1129
|
},
|
|
920
1130
|
[],
|
|
921
1131
|
),
|
|
@@ -931,14 +1141,14 @@ export class Workbook {
|
|
|
931
1141
|
// Generate pivot table files
|
|
932
1142
|
for (let i = 0; i < this._pivotTables.length; i++) {
|
|
933
1143
|
const pivotTable = this._pivotTables[i];
|
|
934
|
-
const ptIdx =
|
|
1144
|
+
const ptIdx = pivotTable.index;
|
|
935
1145
|
|
|
936
1146
|
// Pivot table definition
|
|
937
1147
|
const ptPath = `xl/pivotTables/pivotTable${ptIdx}.xml`;
|
|
938
1148
|
writeZipText(this._files, ptPath, pivotTable.toXml());
|
|
939
1149
|
|
|
940
1150
|
// Pivot table relationships (link to cache definition)
|
|
941
|
-
const cacheIdx =
|
|
1151
|
+
const cacheIdx = pivotTable.cacheFileIndex;
|
|
942
1152
|
const ptRelsPath = `xl/pivotTables/_rels/pivotTable${ptIdx}.xml.rels`;
|
|
943
1153
|
const ptRels = createElement(
|
|
944
1154
|
'Relationships',
|
|
@@ -974,22 +1184,197 @@ export class Workbook {
|
|
|
974
1184
|
const sheetFileName = rel.target.split('/').pop();
|
|
975
1185
|
const sheetRelsPath = `xl/worksheets/_rels/${sheetFileName}.rels`;
|
|
976
1186
|
|
|
977
|
-
const
|
|
978
|
-
|
|
979
|
-
|
|
1187
|
+
const existingRelsXml = readZipText(this._files, sheetRelsPath);
|
|
1188
|
+
let relNodes: XmlNode[] = [];
|
|
1189
|
+
let nextRelId = 1;
|
|
1190
|
+
const reservedRelIds = new Set<string>();
|
|
1191
|
+
|
|
1192
|
+
if (existingRelsXml) {
|
|
1193
|
+
const parsed = parseXml(existingRelsXml);
|
|
1194
|
+
const relsElement = findElement(parsed, 'Relationships');
|
|
1195
|
+
if (relsElement) {
|
|
1196
|
+
const existingRelNodes = getChildren(relsElement, 'Relationships');
|
|
1197
|
+
for (const relNode of existingRelNodes) {
|
|
1198
|
+
if ('Relationship' in relNode) {
|
|
1199
|
+
relNodes.push(relNode);
|
|
1200
|
+
const id = getAttr(relNode, 'Id');
|
|
1201
|
+
if (id) {
|
|
1202
|
+
reservedRelIds.add(id);
|
|
1203
|
+
const idNum = parseInt(id.replace('rId', ''), 10);
|
|
1204
|
+
if (idNum >= nextRelId) {
|
|
1205
|
+
nextRelId = idNum + 1;
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
const allocateRelId = (): string => {
|
|
1214
|
+
while (reservedRelIds.has(`rId${nextRelId}`)) {
|
|
1215
|
+
nextRelId++;
|
|
1216
|
+
}
|
|
1217
|
+
const id = `rId${nextRelId}`;
|
|
1218
|
+
nextRelId++;
|
|
1219
|
+
reservedRelIds.add(id);
|
|
1220
|
+
return id;
|
|
1221
|
+
};
|
|
1222
|
+
|
|
1223
|
+
for (const pt of pivotTables) {
|
|
1224
|
+
const target = `../pivotTables/pivotTable${pt.index}.xml`;
|
|
1225
|
+
const existing = relNodes.some(
|
|
1226
|
+
(node) =>
|
|
1227
|
+
getAttr(node, 'Type') ===
|
|
1228
|
+
'http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotTable' &&
|
|
1229
|
+
getAttr(node, 'Target') === target,
|
|
1230
|
+
);
|
|
1231
|
+
if (existing) continue;
|
|
980
1232
|
relNodes.push(
|
|
981
1233
|
createElement(
|
|
982
1234
|
'Relationship',
|
|
983
1235
|
{
|
|
984
|
-
Id:
|
|
1236
|
+
Id: allocateRelId(),
|
|
985
1237
|
Type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotTable',
|
|
986
|
-
Target:
|
|
1238
|
+
Target: target,
|
|
1239
|
+
},
|
|
1240
|
+
[],
|
|
1241
|
+
),
|
|
1242
|
+
);
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
const sheetRels = createElement(
|
|
1246
|
+
'Relationships',
|
|
1247
|
+
{ xmlns: 'http://schemas.openxmlformats.org/package/2006/relationships' },
|
|
1248
|
+
relNodes,
|
|
1249
|
+
);
|
|
1250
|
+
writeZipText(
|
|
1251
|
+
this._files,
|
|
1252
|
+
sheetRelsPath,
|
|
1253
|
+
`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n${stringifyXml([sheetRels])}`,
|
|
1254
|
+
);
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
/**
|
|
1259
|
+
* Generate all table related files
|
|
1260
|
+
*/
|
|
1261
|
+
private _updateTableFiles(): void {
|
|
1262
|
+
// Collect all tables with their global indices
|
|
1263
|
+
let globalTableIndex = 1;
|
|
1264
|
+
const sheetTables: Map<string, { table: import('./table').Table; globalIndex: number }[]> = new Map();
|
|
1265
|
+
|
|
1266
|
+
for (const def of this._sheetDefs) {
|
|
1267
|
+
const worksheet = this._sheets.get(def.name);
|
|
1268
|
+
if (!worksheet) continue;
|
|
1269
|
+
|
|
1270
|
+
const tables = worksheet.tables;
|
|
1271
|
+
if (tables.length === 0) continue;
|
|
1272
|
+
|
|
1273
|
+
const tableInfos: { table: import('./table').Table; globalIndex: number }[] = [];
|
|
1274
|
+
for (const table of tables) {
|
|
1275
|
+
tableInfos.push({ table, globalIndex: globalTableIndex });
|
|
1276
|
+
globalTableIndex++;
|
|
1277
|
+
}
|
|
1278
|
+
sheetTables.set(def.name, tableInfos);
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
// Generate table files
|
|
1282
|
+
for (const [, tableInfos] of sheetTables) {
|
|
1283
|
+
for (const { table, globalIndex } of tableInfos) {
|
|
1284
|
+
const tablePath = `xl/tables/table${globalIndex}.xml`;
|
|
1285
|
+
writeZipText(this._files, tablePath, table.toXml());
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
// Generate worksheet relationships for tables
|
|
1290
|
+
for (const [sheetName, tableInfos] of sheetTables) {
|
|
1291
|
+
const def = this._sheetDefs.find((s) => s.name === sheetName);
|
|
1292
|
+
if (!def) continue;
|
|
1293
|
+
|
|
1294
|
+
const rel = this._relationships.find((r) => r.id === def.rId);
|
|
1295
|
+
if (!rel) continue;
|
|
1296
|
+
|
|
1297
|
+
// Extract sheet file name from target path
|
|
1298
|
+
const sheetFileName = rel.target.split('/').pop();
|
|
1299
|
+
const sheetRelsPath = `xl/worksheets/_rels/${sheetFileName}.rels`;
|
|
1300
|
+
|
|
1301
|
+
// Check if there are already pivot table relationships for this sheet
|
|
1302
|
+
const existingRelsXml = readZipText(this._files, sheetRelsPath);
|
|
1303
|
+
let nextRelId = 1;
|
|
1304
|
+
const relNodes: XmlNode[] = [];
|
|
1305
|
+
const reservedRelIds = new Set<string>();
|
|
1306
|
+
|
|
1307
|
+
if (existingRelsXml) {
|
|
1308
|
+
// Parse existing rels and find max rId
|
|
1309
|
+
const parsed = parseXml(existingRelsXml);
|
|
1310
|
+
const relsElement = findElement(parsed, 'Relationships');
|
|
1311
|
+
if (relsElement) {
|
|
1312
|
+
const existingRelNodes = getChildren(relsElement, 'Relationships');
|
|
1313
|
+
for (const relNode of existingRelNodes) {
|
|
1314
|
+
if ('Relationship' in relNode) {
|
|
1315
|
+
relNodes.push(relNode);
|
|
1316
|
+
const id = getAttr(relNode, 'Id');
|
|
1317
|
+
if (id) {
|
|
1318
|
+
reservedRelIds.add(id);
|
|
1319
|
+
const idNum = parseInt(id.replace('rId', ''), 10);
|
|
1320
|
+
if (idNum >= nextRelId) {
|
|
1321
|
+
nextRelId = idNum + 1;
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
const allocateRelId = (): string => {
|
|
1330
|
+
while (reservedRelIds.has(`rId${nextRelId}`)) {
|
|
1331
|
+
nextRelId++;
|
|
1332
|
+
}
|
|
1333
|
+
const id = `rId${nextRelId}`;
|
|
1334
|
+
nextRelId++;
|
|
1335
|
+
reservedRelIds.add(id);
|
|
1336
|
+
return id;
|
|
1337
|
+
};
|
|
1338
|
+
|
|
1339
|
+
// Add table relationships
|
|
1340
|
+
const tableRelIds: string[] = [];
|
|
1341
|
+
for (const { globalIndex } of tableInfos) {
|
|
1342
|
+
const target = `../tables/table${globalIndex}.xml`;
|
|
1343
|
+
const existing = relNodes.some(
|
|
1344
|
+
(node) =>
|
|
1345
|
+
getAttr(node, 'Type') === 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/table' &&
|
|
1346
|
+
getAttr(node, 'Target') === target,
|
|
1347
|
+
);
|
|
1348
|
+
if (existing) {
|
|
1349
|
+
const existingRel = relNodes.find(
|
|
1350
|
+
(node) =>
|
|
1351
|
+
getAttr(node, 'Type') === 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/table' &&
|
|
1352
|
+
getAttr(node, 'Target') === target,
|
|
1353
|
+
);
|
|
1354
|
+
const existingId = existingRel ? getAttr(existingRel, 'Id') : undefined;
|
|
1355
|
+
tableRelIds.push(existingId ?? allocateRelId());
|
|
1356
|
+
continue;
|
|
1357
|
+
}
|
|
1358
|
+
const id = allocateRelId();
|
|
1359
|
+
tableRelIds.push(id);
|
|
1360
|
+
relNodes.push(
|
|
1361
|
+
createElement(
|
|
1362
|
+
'Relationship',
|
|
1363
|
+
{
|
|
1364
|
+
Id: id,
|
|
1365
|
+
Type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/table',
|
|
1366
|
+
Target: target,
|
|
987
1367
|
},
|
|
988
1368
|
[],
|
|
989
1369
|
),
|
|
990
1370
|
);
|
|
991
1371
|
}
|
|
992
1372
|
|
|
1373
|
+
const worksheet = this._sheets.get(sheetName);
|
|
1374
|
+
if (worksheet) {
|
|
1375
|
+
worksheet.setTableRelIds(tableRelIds);
|
|
1376
|
+
}
|
|
1377
|
+
|
|
993
1378
|
const sheetRels = createElement(
|
|
994
1379
|
'Relationships',
|
|
995
1380
|
{ xmlns: 'http://schemas.openxmlformats.org/package/2006/relationships' },
|