@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/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 cache = new PivotCache(cacheId, sourceSheet, sourceRange);
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, idx) => {
658
- // Cache relationship ID is after sheets, sharedStrings, and styles
659
- const cacheRelId = `rId${this._relationships.length + 3 + idx}`;
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
- // Calculate next available relationship ID based on existing max ID
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: `rId${nextRelId++}`,
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: `rId${nextRelId++}`,
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
- for (let i = 0; i < this._pivotCaches.length; i++) {
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: `rId${nextRelId++}`,
889
+ Id: id,
731
890
  Type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotCacheDefinition',
732
- Target: `pivotCache/pivotCacheDefinition${i + 1}.xml`,
891
+ Target: `pivotCache/pivotCacheDefinition${cache.fileIndex}.xml`,
733
892
  },
734
893
  [],
735
894
  ),
736
895
  );
737
896
  }
738
897
 
739
- const relsNode = createElement(
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 (let i = 0; i < this._pivotCaches.length; i++) {
959
+ for (const cache of this._pivotCaches) {
808
960
  types.push(
809
961
  createElement(
810
962
  'Override',
811
963
  {
812
- PartName: `/xl/pivotCache/pivotCacheDefinition${i + 1}.xml`,
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${i + 1}.xml`,
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 (let i = 0; i < this._pivotTables.length; i++) {
983
+ for (const pivotTable of this._pivotTables) {
832
984
  types.push(
833
985
  createElement(
834
986
  'Override',
835
987
  {
836
- PartName: `/xl/pivotTables/pivotTable${i + 1}.xml`,
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${cacheIdx}.xml`;
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${cacheIdx}.xml`;
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${cacheIdx}.xml.rels`;
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${cacheIdx}.xml`,
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 = i + 1;
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 = this._pivotCaches.indexOf(pivotTable.cache) + 1;
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 relNodes: XmlNode[] = [];
978
- for (let i = 0; i < pivotTables.length; i++) {
979
- const pt = pivotTables[i];
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: `rId${i + 1}`,
1236
+ Id: allocateRelId(),
985
1237
  Type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotTable',
986
- Target: `../pivotTables/pivotTable${pt.index}.xml`,
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' },