@niicojs/excel 0.1.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 +21 -0
- package/README.md +208 -0
- package/dist/index.cjs +2894 -0
- package/dist/index.d.cts +745 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.ts +745 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2881 -0
- package/package.json +61 -0
- package/src/cell.ts +318 -0
- package/src/index.ts +31 -0
- package/src/pivot-cache.ts +268 -0
- package/src/pivot-table.ts +523 -0
- package/src/range.ts +141 -0
- package/src/shared-strings.ts +129 -0
- package/src/styles.ts +588 -0
- package/src/types.ts +165 -0
- package/src/utils/address.ts +118 -0
- package/src/utils/xml.ts +147 -0
- package/src/utils/zip.ts +61 -0
- package/src/workbook.ts +845 -0
- package/src/worksheet.ts +372 -0
package/src/workbook.ts
ADDED
|
@@ -0,0 +1,845 @@
|
|
|
1
|
+
import { readFile, writeFile } from 'fs/promises';
|
|
2
|
+
import type { SheetDefinition, Relationship, PivotTableConfig, CellValue } from './types';
|
|
3
|
+
import { Worksheet } from './worksheet';
|
|
4
|
+
import { SharedStrings } from './shared-strings';
|
|
5
|
+
import { Styles } from './styles';
|
|
6
|
+
import { PivotTable } from './pivot-table';
|
|
7
|
+
import { PivotCache } from './pivot-cache';
|
|
8
|
+
import { readZip, writeZip, readZipText, writeZipText, ZipFiles } from './utils/zip';
|
|
9
|
+
import { parseAddress, parseRange, toAddress } from './utils/address';
|
|
10
|
+
import { parseXml, findElement, getChildren, getAttr, XmlNode, stringifyXml, createElement } from './utils/xml';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Represents an Excel workbook (.xlsx file)
|
|
14
|
+
*/
|
|
15
|
+
export class Workbook {
|
|
16
|
+
private _files: ZipFiles = new Map();
|
|
17
|
+
private _sheets: Map<string, Worksheet> = new Map();
|
|
18
|
+
private _sheetDefs: SheetDefinition[] = [];
|
|
19
|
+
private _relationships: Relationship[] = [];
|
|
20
|
+
private _sharedStrings: SharedStrings;
|
|
21
|
+
private _styles: Styles;
|
|
22
|
+
private _dirty = false;
|
|
23
|
+
|
|
24
|
+
// Pivot table support
|
|
25
|
+
private _pivotTables: PivotTable[] = [];
|
|
26
|
+
private _pivotCaches: PivotCache[] = [];
|
|
27
|
+
private _nextCacheId = 0;
|
|
28
|
+
|
|
29
|
+
private constructor() {
|
|
30
|
+
this._sharedStrings = new SharedStrings();
|
|
31
|
+
this._styles = Styles.createDefault();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Load a workbook from a file path
|
|
36
|
+
*/
|
|
37
|
+
static async fromFile(path: string): Promise<Workbook> {
|
|
38
|
+
const data = await readFile(path);
|
|
39
|
+
return Workbook.fromBuffer(new Uint8Array(data));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Load a workbook from a buffer
|
|
44
|
+
*/
|
|
45
|
+
static async fromBuffer(data: Uint8Array): Promise<Workbook> {
|
|
46
|
+
const workbook = new Workbook();
|
|
47
|
+
workbook._files = await readZip(data);
|
|
48
|
+
|
|
49
|
+
// Parse workbook.xml for sheet definitions
|
|
50
|
+
const workbookXml = readZipText(workbook._files, 'xl/workbook.xml');
|
|
51
|
+
if (workbookXml) {
|
|
52
|
+
workbook._parseWorkbook(workbookXml);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Parse relationships
|
|
56
|
+
const relsXml = readZipText(workbook._files, 'xl/_rels/workbook.xml.rels');
|
|
57
|
+
if (relsXml) {
|
|
58
|
+
workbook._parseRelationships(relsXml);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Parse shared strings
|
|
62
|
+
const sharedStringsXml = readZipText(workbook._files, 'xl/sharedStrings.xml');
|
|
63
|
+
if (sharedStringsXml) {
|
|
64
|
+
workbook._sharedStrings = SharedStrings.parse(sharedStringsXml);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Parse styles
|
|
68
|
+
const stylesXml = readZipText(workbook._files, 'xl/styles.xml');
|
|
69
|
+
if (stylesXml) {
|
|
70
|
+
workbook._styles = Styles.parse(stylesXml);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return workbook;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Create a new empty workbook
|
|
78
|
+
*/
|
|
79
|
+
static create(): Workbook {
|
|
80
|
+
const workbook = new Workbook();
|
|
81
|
+
workbook._dirty = true;
|
|
82
|
+
|
|
83
|
+
// Add default sheet
|
|
84
|
+
workbook.addSheet('Sheet1');
|
|
85
|
+
|
|
86
|
+
return workbook;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Get sheet names
|
|
91
|
+
*/
|
|
92
|
+
get sheetNames(): string[] {
|
|
93
|
+
return this._sheetDefs.map((s) => s.name);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Get number of sheets
|
|
98
|
+
*/
|
|
99
|
+
get sheetCount(): number {
|
|
100
|
+
return this._sheetDefs.length;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Get shared strings table
|
|
105
|
+
*/
|
|
106
|
+
get sharedStrings(): SharedStrings {
|
|
107
|
+
return this._sharedStrings;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Get styles
|
|
112
|
+
*/
|
|
113
|
+
get styles(): Styles {
|
|
114
|
+
return this._styles;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Get a worksheet by name or index
|
|
119
|
+
*/
|
|
120
|
+
sheet(nameOrIndex: string | number): Worksheet {
|
|
121
|
+
let def: SheetDefinition | undefined;
|
|
122
|
+
|
|
123
|
+
if (typeof nameOrIndex === 'number') {
|
|
124
|
+
def = this._sheetDefs[nameOrIndex];
|
|
125
|
+
} else {
|
|
126
|
+
def = this._sheetDefs.find((s) => s.name === nameOrIndex);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (!def) {
|
|
130
|
+
throw new Error(`Sheet not found: ${nameOrIndex}`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Return cached worksheet if available
|
|
134
|
+
if (this._sheets.has(def.name)) {
|
|
135
|
+
return this._sheets.get(def.name)!;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Load worksheet
|
|
139
|
+
const worksheet = new Worksheet(this, def.name);
|
|
140
|
+
|
|
141
|
+
// Find the relationship to get the file path
|
|
142
|
+
const rel = this._relationships.find((r) => r.id === def.rId);
|
|
143
|
+
if (rel) {
|
|
144
|
+
const sheetPath = `xl/${rel.target}`;
|
|
145
|
+
const sheetXml = readZipText(this._files, sheetPath);
|
|
146
|
+
if (sheetXml) {
|
|
147
|
+
worksheet.parse(sheetXml);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
this._sheets.set(def.name, worksheet);
|
|
152
|
+
return worksheet;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Add a new worksheet
|
|
157
|
+
*/
|
|
158
|
+
addSheet(name: string, index?: number): Worksheet {
|
|
159
|
+
// Check for duplicate name
|
|
160
|
+
if (this._sheetDefs.some((s) => s.name === name)) {
|
|
161
|
+
throw new Error(`Sheet already exists: ${name}`);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
this._dirty = true;
|
|
165
|
+
|
|
166
|
+
// Generate new sheet ID and relationship ID
|
|
167
|
+
const sheetId = Math.max(0, ...this._sheetDefs.map((s) => s.sheetId)) + 1;
|
|
168
|
+
const rId = `rId${Math.max(0, ...this._relationships.map((r) => parseInt(r.id.replace('rId', ''), 10) || 0)) + 1}`;
|
|
169
|
+
|
|
170
|
+
const def: SheetDefinition = { name, sheetId, rId };
|
|
171
|
+
|
|
172
|
+
// Add relationship
|
|
173
|
+
this._relationships.push({
|
|
174
|
+
id: rId,
|
|
175
|
+
type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet',
|
|
176
|
+
target: `worksheets/sheet${sheetId}.xml`,
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// Insert at index or append
|
|
180
|
+
if (index !== undefined && index >= 0 && index < this._sheetDefs.length) {
|
|
181
|
+
this._sheetDefs.splice(index, 0, def);
|
|
182
|
+
} else {
|
|
183
|
+
this._sheetDefs.push(def);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Create worksheet
|
|
187
|
+
const worksheet = new Worksheet(this, name);
|
|
188
|
+
this._sheets.set(name, worksheet);
|
|
189
|
+
|
|
190
|
+
return worksheet;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Delete a worksheet by name or index
|
|
195
|
+
*/
|
|
196
|
+
deleteSheet(nameOrIndex: string | number): void {
|
|
197
|
+
let index: number;
|
|
198
|
+
|
|
199
|
+
if (typeof nameOrIndex === 'number') {
|
|
200
|
+
index = nameOrIndex;
|
|
201
|
+
} else {
|
|
202
|
+
index = this._sheetDefs.findIndex((s) => s.name === nameOrIndex);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (index < 0 || index >= this._sheetDefs.length) {
|
|
206
|
+
throw new Error(`Sheet not found: ${nameOrIndex}`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (this._sheetDefs.length === 1) {
|
|
210
|
+
throw new Error('Cannot delete the last sheet');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
this._dirty = true;
|
|
214
|
+
|
|
215
|
+
const def = this._sheetDefs[index];
|
|
216
|
+
this._sheetDefs.splice(index, 1);
|
|
217
|
+
this._sheets.delete(def.name);
|
|
218
|
+
|
|
219
|
+
// Remove relationship
|
|
220
|
+
const relIndex = this._relationships.findIndex((r) => r.id === def.rId);
|
|
221
|
+
if (relIndex >= 0) {
|
|
222
|
+
this._relationships.splice(relIndex, 1);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Rename a worksheet
|
|
228
|
+
*/
|
|
229
|
+
renameSheet(oldName: string, newName: string): void {
|
|
230
|
+
const def = this._sheetDefs.find((s) => s.name === oldName);
|
|
231
|
+
if (!def) {
|
|
232
|
+
throw new Error(`Sheet not found: ${oldName}`);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (this._sheetDefs.some((s) => s.name === newName)) {
|
|
236
|
+
throw new Error(`Sheet already exists: ${newName}`);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
this._dirty = true;
|
|
240
|
+
|
|
241
|
+
// Update cached worksheet
|
|
242
|
+
const worksheet = this._sheets.get(oldName);
|
|
243
|
+
if (worksheet) {
|
|
244
|
+
worksheet.name = newName;
|
|
245
|
+
this._sheets.delete(oldName);
|
|
246
|
+
this._sheets.set(newName, worksheet);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
def.name = newName;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Copy a worksheet
|
|
254
|
+
*/
|
|
255
|
+
copySheet(sourceName: string, newName: string): Worksheet {
|
|
256
|
+
const source = this.sheet(sourceName);
|
|
257
|
+
const copy = this.addSheet(newName);
|
|
258
|
+
|
|
259
|
+
// Copy all cells
|
|
260
|
+
for (const [address, cell] of source.cells) {
|
|
261
|
+
const newCell = copy.cell(address);
|
|
262
|
+
newCell.value = cell.value;
|
|
263
|
+
if (cell.formula) {
|
|
264
|
+
newCell.formula = cell.formula;
|
|
265
|
+
}
|
|
266
|
+
if (cell.styleIndex !== undefined) {
|
|
267
|
+
newCell.styleIndex = cell.styleIndex;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Copy merged cells
|
|
272
|
+
for (const mergedRange of source.mergedCells) {
|
|
273
|
+
copy.mergeCells(mergedRange);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return copy;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Create a pivot table from source data.
|
|
281
|
+
*
|
|
282
|
+
* @param config - Pivot table configuration
|
|
283
|
+
* @returns PivotTable instance for fluent configuration
|
|
284
|
+
*
|
|
285
|
+
* @example
|
|
286
|
+
* ```typescript
|
|
287
|
+
* const pivot = wb.createPivotTable({
|
|
288
|
+
* name: 'SalesPivot',
|
|
289
|
+
* source: 'DataSheet!A1:D100',
|
|
290
|
+
* target: 'PivotSheet!A3',
|
|
291
|
+
* });
|
|
292
|
+
*
|
|
293
|
+
* pivot
|
|
294
|
+
* .addRowField('Region')
|
|
295
|
+
* .addColumnField('Product')
|
|
296
|
+
* .addValueField('Sales', 'sum', 'Total Sales');
|
|
297
|
+
* ```
|
|
298
|
+
*/
|
|
299
|
+
createPivotTable(config: PivotTableConfig): PivotTable {
|
|
300
|
+
this._dirty = true;
|
|
301
|
+
|
|
302
|
+
// Parse source reference (Sheet!Range)
|
|
303
|
+
const { sheetName: sourceSheet, range: sourceRange } = this._parseSheetRef(config.source);
|
|
304
|
+
|
|
305
|
+
// Parse target reference
|
|
306
|
+
const { sheetName: targetSheet, range: targetCell } = this._parseSheetRef(config.target);
|
|
307
|
+
|
|
308
|
+
// Ensure target sheet exists
|
|
309
|
+
if (!this._sheetDefs.some((s) => s.name === targetSheet)) {
|
|
310
|
+
this.addSheet(targetSheet);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Parse target cell address
|
|
314
|
+
const targetAddr = parseAddress(targetCell);
|
|
315
|
+
|
|
316
|
+
// Get source worksheet and extract data
|
|
317
|
+
const sourceWs = this.sheet(sourceSheet);
|
|
318
|
+
const { headers, data } = this._extractSourceData(sourceWs, sourceRange);
|
|
319
|
+
|
|
320
|
+
// Create pivot cache
|
|
321
|
+
const cacheId = this._nextCacheId++;
|
|
322
|
+
const cache = new PivotCache(cacheId, sourceSheet, sourceRange);
|
|
323
|
+
cache.buildFromData(headers, data);
|
|
324
|
+
// refreshOnLoad defaults to true; only disable if explicitly set to false
|
|
325
|
+
if (config.refreshOnLoad === false) {
|
|
326
|
+
cache.refreshOnLoad = false;
|
|
327
|
+
}
|
|
328
|
+
this._pivotCaches.push(cache);
|
|
329
|
+
|
|
330
|
+
// Create pivot table
|
|
331
|
+
const pivotTableIndex = this._pivotTables.length + 1;
|
|
332
|
+
const pivotTable = new PivotTable(
|
|
333
|
+
config.name,
|
|
334
|
+
cache,
|
|
335
|
+
targetSheet,
|
|
336
|
+
targetCell,
|
|
337
|
+
targetAddr.row + 1, // Convert to 1-based
|
|
338
|
+
targetAddr.col,
|
|
339
|
+
pivotTableIndex,
|
|
340
|
+
);
|
|
341
|
+
this._pivotTables.push(pivotTable);
|
|
342
|
+
|
|
343
|
+
return pivotTable;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Parse a sheet reference like "Sheet1!A1:D100" into sheet name and range
|
|
348
|
+
*/
|
|
349
|
+
private _parseSheetRef(ref: string): { sheetName: string; range: string } {
|
|
350
|
+
const match = ref.match(/^(.+?)!(.+)$/);
|
|
351
|
+
if (!match) {
|
|
352
|
+
throw new Error(`Invalid reference format: ${ref}. Expected "SheetName!Range"`);
|
|
353
|
+
}
|
|
354
|
+
return { sheetName: match[1], range: match[2] };
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Extract headers and data from a source range
|
|
359
|
+
*/
|
|
360
|
+
private _extractSourceData(sheet: Worksheet, rangeStr: string): { headers: string[]; data: CellValue[][] } {
|
|
361
|
+
const range = parseRange(rangeStr);
|
|
362
|
+
const headers: string[] = [];
|
|
363
|
+
const data: CellValue[][] = [];
|
|
364
|
+
|
|
365
|
+
// First row is headers
|
|
366
|
+
for (let col = range.start.col; col <= range.end.col; col++) {
|
|
367
|
+
const cell = sheet.cell(toAddress(range.start.row, col));
|
|
368
|
+
headers.push(String(cell.value ?? `Column${col + 1}`));
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Remaining rows are data
|
|
372
|
+
for (let row = range.start.row + 1; row <= range.end.row; row++) {
|
|
373
|
+
const rowData: CellValue[] = [];
|
|
374
|
+
for (let col = range.start.col; col <= range.end.col; col++) {
|
|
375
|
+
const cell = sheet.cell(toAddress(row, col));
|
|
376
|
+
rowData.push(cell.value);
|
|
377
|
+
}
|
|
378
|
+
data.push(rowData);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
return { headers, data };
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Save the workbook to a file
|
|
386
|
+
*/
|
|
387
|
+
async toFile(path: string): Promise<void> {
|
|
388
|
+
const buffer = await this.toBuffer();
|
|
389
|
+
await writeFile(path, buffer);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Save the workbook to a buffer
|
|
394
|
+
*/
|
|
395
|
+
async toBuffer(): Promise<Uint8Array> {
|
|
396
|
+
// Update files map with modified content
|
|
397
|
+
this._updateFiles();
|
|
398
|
+
|
|
399
|
+
// Write ZIP
|
|
400
|
+
return writeZip(this._files);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
private _parseWorkbook(xml: string): void {
|
|
404
|
+
const parsed = parseXml(xml);
|
|
405
|
+
const workbook = findElement(parsed, 'workbook');
|
|
406
|
+
if (!workbook) return;
|
|
407
|
+
|
|
408
|
+
const children = getChildren(workbook, 'workbook');
|
|
409
|
+
const sheets = findElement(children, 'sheets');
|
|
410
|
+
if (!sheets) return;
|
|
411
|
+
|
|
412
|
+
for (const child of getChildren(sheets, 'sheets')) {
|
|
413
|
+
if ('sheet' in child) {
|
|
414
|
+
const name = getAttr(child, 'name');
|
|
415
|
+
const sheetId = getAttr(child, 'sheetId');
|
|
416
|
+
const rId = getAttr(child, 'r:id');
|
|
417
|
+
|
|
418
|
+
if (name && sheetId && rId) {
|
|
419
|
+
this._sheetDefs.push({
|
|
420
|
+
name,
|
|
421
|
+
sheetId: parseInt(sheetId, 10),
|
|
422
|
+
rId,
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
private _parseRelationships(xml: string): void {
|
|
430
|
+
const parsed = parseXml(xml);
|
|
431
|
+
const rels = findElement(parsed, 'Relationships');
|
|
432
|
+
if (!rels) return;
|
|
433
|
+
|
|
434
|
+
for (const child of getChildren(rels, 'Relationships')) {
|
|
435
|
+
if ('Relationship' in child) {
|
|
436
|
+
const id = getAttr(child, 'Id');
|
|
437
|
+
const type = getAttr(child, 'Type');
|
|
438
|
+
const target = getAttr(child, 'Target');
|
|
439
|
+
|
|
440
|
+
if (id && type && target) {
|
|
441
|
+
this._relationships.push({ id, type, target });
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
private _updateFiles(): void {
|
|
448
|
+
// Update workbook.xml
|
|
449
|
+
this._updateWorkbookXml();
|
|
450
|
+
|
|
451
|
+
// Update relationships
|
|
452
|
+
this._updateRelationshipsXml();
|
|
453
|
+
|
|
454
|
+
// Update content types
|
|
455
|
+
this._updateContentTypes();
|
|
456
|
+
|
|
457
|
+
// Update shared strings if modified
|
|
458
|
+
if (this._sharedStrings.dirty || this._sharedStrings.count > 0) {
|
|
459
|
+
writeZipText(this._files, 'xl/sharedStrings.xml', this._sharedStrings.toXml());
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Update styles if modified or if file doesn't exist yet
|
|
463
|
+
if (this._styles.dirty || this._dirty || !this._files.has('xl/styles.xml')) {
|
|
464
|
+
writeZipText(this._files, 'xl/styles.xml', this._styles.toXml());
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// Update worksheets
|
|
468
|
+
for (const [name, worksheet] of this._sheets) {
|
|
469
|
+
if (worksheet.dirty || this._dirty) {
|
|
470
|
+
const def = this._sheetDefs.find((s) => s.name === name);
|
|
471
|
+
if (def) {
|
|
472
|
+
const rel = this._relationships.find((r) => r.id === def.rId);
|
|
473
|
+
if (rel) {
|
|
474
|
+
const sheetPath = `xl/${rel.target}`;
|
|
475
|
+
writeZipText(this._files, sheetPath, worksheet.toXml());
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Update pivot tables
|
|
482
|
+
if (this._pivotTables.length > 0) {
|
|
483
|
+
this._updatePivotTableFiles();
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
private _updateWorkbookXml(): void {
|
|
488
|
+
const sheetNodes: XmlNode[] = this._sheetDefs.map((def) =>
|
|
489
|
+
createElement('sheet', { name: def.name, sheetId: String(def.sheetId), 'r:id': def.rId }, []),
|
|
490
|
+
);
|
|
491
|
+
|
|
492
|
+
const sheetsNode = createElement('sheets', {}, sheetNodes);
|
|
493
|
+
|
|
494
|
+
const children: XmlNode[] = [sheetsNode];
|
|
495
|
+
|
|
496
|
+
// Add pivot caches if any
|
|
497
|
+
if (this._pivotCaches.length > 0) {
|
|
498
|
+
const pivotCacheNodes: XmlNode[] = this._pivotCaches.map((cache, idx) => {
|
|
499
|
+
// Cache relationship ID is after sheets, sharedStrings, and styles
|
|
500
|
+
const cacheRelId = `rId${this._relationships.length + 3 + idx}`;
|
|
501
|
+
return createElement('pivotCache', { cacheId: String(cache.cacheId), 'r:id': cacheRelId }, []);
|
|
502
|
+
});
|
|
503
|
+
children.push(createElement('pivotCaches', {}, pivotCacheNodes));
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
const workbookNode = createElement(
|
|
507
|
+
'workbook',
|
|
508
|
+
{
|
|
509
|
+
xmlns: 'http://schemas.openxmlformats.org/spreadsheetml/2006/main',
|
|
510
|
+
'xmlns:r': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships',
|
|
511
|
+
},
|
|
512
|
+
children,
|
|
513
|
+
);
|
|
514
|
+
|
|
515
|
+
const xml = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n${stringifyXml([workbookNode])}`;
|
|
516
|
+
writeZipText(this._files, 'xl/workbook.xml', xml);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
private _updateRelationshipsXml(): void {
|
|
520
|
+
const relNodes: XmlNode[] = this._relationships.map((rel) =>
|
|
521
|
+
createElement('Relationship', { Id: rel.id, Type: rel.type, Target: rel.target }, []),
|
|
522
|
+
);
|
|
523
|
+
|
|
524
|
+
let nextRelId = this._relationships.length + 1;
|
|
525
|
+
|
|
526
|
+
// Add shared strings relationship if needed
|
|
527
|
+
if (this._sharedStrings.count > 0) {
|
|
528
|
+
const hasSharedStrings = this._relationships.some(
|
|
529
|
+
(r) => r.type === 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings',
|
|
530
|
+
);
|
|
531
|
+
if (!hasSharedStrings) {
|
|
532
|
+
relNodes.push(
|
|
533
|
+
createElement(
|
|
534
|
+
'Relationship',
|
|
535
|
+
{
|
|
536
|
+
Id: `rId${nextRelId++}`,
|
|
537
|
+
Type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings',
|
|
538
|
+
Target: 'sharedStrings.xml',
|
|
539
|
+
},
|
|
540
|
+
[],
|
|
541
|
+
),
|
|
542
|
+
);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// Add styles relationship if needed
|
|
547
|
+
const hasStyles = this._relationships.some(
|
|
548
|
+
(r) => r.type === 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles',
|
|
549
|
+
);
|
|
550
|
+
if (!hasStyles) {
|
|
551
|
+
relNodes.push(
|
|
552
|
+
createElement(
|
|
553
|
+
'Relationship',
|
|
554
|
+
{
|
|
555
|
+
Id: `rId${nextRelId++}`,
|
|
556
|
+
Type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles',
|
|
557
|
+
Target: 'styles.xml',
|
|
558
|
+
},
|
|
559
|
+
[],
|
|
560
|
+
),
|
|
561
|
+
);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// Add pivot cache relationships
|
|
565
|
+
for (let i = 0; i < this._pivotCaches.length; i++) {
|
|
566
|
+
relNodes.push(
|
|
567
|
+
createElement(
|
|
568
|
+
'Relationship',
|
|
569
|
+
{
|
|
570
|
+
Id: `rId${nextRelId++}`,
|
|
571
|
+
Type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotCacheDefinition',
|
|
572
|
+
Target: `pivotCache/pivotCacheDefinition${i + 1}.xml`,
|
|
573
|
+
},
|
|
574
|
+
[],
|
|
575
|
+
),
|
|
576
|
+
);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
const relsNode = createElement(
|
|
580
|
+
'Relationships',
|
|
581
|
+
{ xmlns: 'http://schemas.openxmlformats.org/package/2006/relationships' },
|
|
582
|
+
relNodes,
|
|
583
|
+
);
|
|
584
|
+
|
|
585
|
+
const xml = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n${stringifyXml([relsNode])}`;
|
|
586
|
+
writeZipText(this._files, 'xl/_rels/workbook.xml.rels', xml);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
private _updateContentTypes(): void {
|
|
590
|
+
const types: XmlNode[] = [
|
|
591
|
+
createElement(
|
|
592
|
+
'Default',
|
|
593
|
+
{ Extension: 'rels', ContentType: 'application/vnd.openxmlformats-package.relationships+xml' },
|
|
594
|
+
[],
|
|
595
|
+
),
|
|
596
|
+
createElement('Default', { Extension: 'xml', ContentType: 'application/xml' }, []),
|
|
597
|
+
createElement(
|
|
598
|
+
'Override',
|
|
599
|
+
{
|
|
600
|
+
PartName: '/xl/workbook.xml',
|
|
601
|
+
ContentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml',
|
|
602
|
+
},
|
|
603
|
+
[],
|
|
604
|
+
),
|
|
605
|
+
createElement(
|
|
606
|
+
'Override',
|
|
607
|
+
{
|
|
608
|
+
PartName: '/xl/styles.xml',
|
|
609
|
+
ContentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml',
|
|
610
|
+
},
|
|
611
|
+
[],
|
|
612
|
+
),
|
|
613
|
+
];
|
|
614
|
+
|
|
615
|
+
// Add shared strings if present
|
|
616
|
+
if (this._sharedStrings.count > 0) {
|
|
617
|
+
types.push(
|
|
618
|
+
createElement(
|
|
619
|
+
'Override',
|
|
620
|
+
{
|
|
621
|
+
PartName: '/xl/sharedStrings.xml',
|
|
622
|
+
ContentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml',
|
|
623
|
+
},
|
|
624
|
+
[],
|
|
625
|
+
),
|
|
626
|
+
);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// Add worksheets
|
|
630
|
+
for (const def of this._sheetDefs) {
|
|
631
|
+
const rel = this._relationships.find((r) => r.id === def.rId);
|
|
632
|
+
if (rel) {
|
|
633
|
+
types.push(
|
|
634
|
+
createElement(
|
|
635
|
+
'Override',
|
|
636
|
+
{
|
|
637
|
+
PartName: `/xl/${rel.target}`,
|
|
638
|
+
ContentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml',
|
|
639
|
+
},
|
|
640
|
+
[],
|
|
641
|
+
),
|
|
642
|
+
);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// Add pivot cache definitions and records
|
|
647
|
+
for (let i = 0; i < this._pivotCaches.length; i++) {
|
|
648
|
+
types.push(
|
|
649
|
+
createElement(
|
|
650
|
+
'Override',
|
|
651
|
+
{
|
|
652
|
+
PartName: `/xl/pivotCache/pivotCacheDefinition${i + 1}.xml`,
|
|
653
|
+
ContentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCacheDefinition+xml',
|
|
654
|
+
},
|
|
655
|
+
[],
|
|
656
|
+
),
|
|
657
|
+
);
|
|
658
|
+
types.push(
|
|
659
|
+
createElement(
|
|
660
|
+
'Override',
|
|
661
|
+
{
|
|
662
|
+
PartName: `/xl/pivotCache/pivotCacheRecords${i + 1}.xml`,
|
|
663
|
+
ContentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCacheRecords+xml',
|
|
664
|
+
},
|
|
665
|
+
[],
|
|
666
|
+
),
|
|
667
|
+
);
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// Add pivot tables
|
|
671
|
+
for (let i = 0; i < this._pivotTables.length; i++) {
|
|
672
|
+
types.push(
|
|
673
|
+
createElement(
|
|
674
|
+
'Override',
|
|
675
|
+
{
|
|
676
|
+
PartName: `/xl/pivotTables/pivotTable${i + 1}.xml`,
|
|
677
|
+
ContentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.pivotTable+xml',
|
|
678
|
+
},
|
|
679
|
+
[],
|
|
680
|
+
),
|
|
681
|
+
);
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
const typesNode = createElement(
|
|
685
|
+
'Types',
|
|
686
|
+
{ xmlns: 'http://schemas.openxmlformats.org/package/2006/content-types' },
|
|
687
|
+
types,
|
|
688
|
+
);
|
|
689
|
+
|
|
690
|
+
const xml = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n${stringifyXml([typesNode])}`;
|
|
691
|
+
writeZipText(this._files, '[Content_Types].xml', xml);
|
|
692
|
+
|
|
693
|
+
// Also ensure _rels/.rels exists
|
|
694
|
+
const rootRelsXml = readZipText(this._files, '_rels/.rels');
|
|
695
|
+
if (!rootRelsXml) {
|
|
696
|
+
const rootRels = createElement(
|
|
697
|
+
'Relationships',
|
|
698
|
+
{ xmlns: 'http://schemas.openxmlformats.org/package/2006/relationships' },
|
|
699
|
+
[
|
|
700
|
+
createElement(
|
|
701
|
+
'Relationship',
|
|
702
|
+
{
|
|
703
|
+
Id: 'rId1',
|
|
704
|
+
Type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument',
|
|
705
|
+
Target: 'xl/workbook.xml',
|
|
706
|
+
},
|
|
707
|
+
[],
|
|
708
|
+
),
|
|
709
|
+
],
|
|
710
|
+
);
|
|
711
|
+
writeZipText(
|
|
712
|
+
this._files,
|
|
713
|
+
'_rels/.rels',
|
|
714
|
+
`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n${stringifyXml([rootRels])}`,
|
|
715
|
+
);
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
/**
|
|
720
|
+
* Generate all pivot table related files
|
|
721
|
+
*/
|
|
722
|
+
private _updatePivotTableFiles(): void {
|
|
723
|
+
// Track which sheets have pivot tables for their .rels files
|
|
724
|
+
const sheetPivotTables: Map<string, PivotTable[]> = new Map();
|
|
725
|
+
|
|
726
|
+
for (const pivotTable of this._pivotTables) {
|
|
727
|
+
const sheetName = pivotTable.targetSheet;
|
|
728
|
+
if (!sheetPivotTables.has(sheetName)) {
|
|
729
|
+
sheetPivotTables.set(sheetName, []);
|
|
730
|
+
}
|
|
731
|
+
sheetPivotTables.get(sheetName)!.push(pivotTable);
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// Generate pivot cache files
|
|
735
|
+
for (let i = 0; i < this._pivotCaches.length; i++) {
|
|
736
|
+
const cache = this._pivotCaches[i];
|
|
737
|
+
const cacheIdx = i + 1;
|
|
738
|
+
|
|
739
|
+
// Pivot cache definition
|
|
740
|
+
const definitionPath = `xl/pivotCache/pivotCacheDefinition${cacheIdx}.xml`;
|
|
741
|
+
writeZipText(this._files, definitionPath, cache.toDefinitionXml('rId1'));
|
|
742
|
+
|
|
743
|
+
// Pivot cache records
|
|
744
|
+
const recordsPath = `xl/pivotCache/pivotCacheRecords${cacheIdx}.xml`;
|
|
745
|
+
writeZipText(this._files, recordsPath, cache.toRecordsXml());
|
|
746
|
+
|
|
747
|
+
// Pivot cache definition relationships (link to records)
|
|
748
|
+
const cacheRelsPath = `xl/pivotCache/_rels/pivotCacheDefinition${cacheIdx}.xml.rels`;
|
|
749
|
+
const cacheRels = createElement(
|
|
750
|
+
'Relationships',
|
|
751
|
+
{ xmlns: 'http://schemas.openxmlformats.org/package/2006/relationships' },
|
|
752
|
+
[
|
|
753
|
+
createElement(
|
|
754
|
+
'Relationship',
|
|
755
|
+
{
|
|
756
|
+
Id: 'rId1',
|
|
757
|
+
Type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotCacheRecords',
|
|
758
|
+
Target: `pivotCacheRecords${cacheIdx}.xml`,
|
|
759
|
+
},
|
|
760
|
+
[],
|
|
761
|
+
),
|
|
762
|
+
],
|
|
763
|
+
);
|
|
764
|
+
writeZipText(
|
|
765
|
+
this._files,
|
|
766
|
+
cacheRelsPath,
|
|
767
|
+
`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n${stringifyXml([cacheRels])}`,
|
|
768
|
+
);
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
// Generate pivot table files
|
|
772
|
+
for (let i = 0; i < this._pivotTables.length; i++) {
|
|
773
|
+
const pivotTable = this._pivotTables[i];
|
|
774
|
+
const ptIdx = i + 1;
|
|
775
|
+
|
|
776
|
+
// Pivot table definition
|
|
777
|
+
const ptPath = `xl/pivotTables/pivotTable${ptIdx}.xml`;
|
|
778
|
+
writeZipText(this._files, ptPath, pivotTable.toXml());
|
|
779
|
+
|
|
780
|
+
// Pivot table relationships (link to cache definition)
|
|
781
|
+
const cacheIdx = this._pivotCaches.indexOf(pivotTable.cache) + 1;
|
|
782
|
+
const ptRelsPath = `xl/pivotTables/_rels/pivotTable${ptIdx}.xml.rels`;
|
|
783
|
+
const ptRels = createElement(
|
|
784
|
+
'Relationships',
|
|
785
|
+
{ xmlns: 'http://schemas.openxmlformats.org/package/2006/relationships' },
|
|
786
|
+
[
|
|
787
|
+
createElement(
|
|
788
|
+
'Relationship',
|
|
789
|
+
{
|
|
790
|
+
Id: 'rId1',
|
|
791
|
+
Type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotCacheDefinition',
|
|
792
|
+
Target: `../pivotCache/pivotCacheDefinition${cacheIdx}.xml`,
|
|
793
|
+
},
|
|
794
|
+
[],
|
|
795
|
+
),
|
|
796
|
+
],
|
|
797
|
+
);
|
|
798
|
+
writeZipText(
|
|
799
|
+
this._files,
|
|
800
|
+
ptRelsPath,
|
|
801
|
+
`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n${stringifyXml([ptRels])}`,
|
|
802
|
+
);
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
// Generate worksheet relationships for pivot tables
|
|
806
|
+
for (const [sheetName, pivotTables] of sheetPivotTables) {
|
|
807
|
+
const def = this._sheetDefs.find((s) => s.name === sheetName);
|
|
808
|
+
if (!def) continue;
|
|
809
|
+
|
|
810
|
+
const rel = this._relationships.find((r) => r.id === def.rId);
|
|
811
|
+
if (!rel) continue;
|
|
812
|
+
|
|
813
|
+
// Extract sheet file name from target path
|
|
814
|
+
const sheetFileName = rel.target.split('/').pop();
|
|
815
|
+
const sheetRelsPath = `xl/worksheets/_rels/${sheetFileName}.rels`;
|
|
816
|
+
|
|
817
|
+
const relNodes: XmlNode[] = [];
|
|
818
|
+
for (let i = 0; i < pivotTables.length; i++) {
|
|
819
|
+
const pt = pivotTables[i];
|
|
820
|
+
relNodes.push(
|
|
821
|
+
createElement(
|
|
822
|
+
'Relationship',
|
|
823
|
+
{
|
|
824
|
+
Id: `rId${i + 1}`,
|
|
825
|
+
Type: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotTable',
|
|
826
|
+
Target: `../pivotTables/pivotTable${pt.index}.xml`,
|
|
827
|
+
},
|
|
828
|
+
[],
|
|
829
|
+
),
|
|
830
|
+
);
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
const sheetRels = createElement(
|
|
834
|
+
'Relationships',
|
|
835
|
+
{ xmlns: 'http://schemas.openxmlformats.org/package/2006/relationships' },
|
|
836
|
+
relNodes,
|
|
837
|
+
);
|
|
838
|
+
writeZipText(
|
|
839
|
+
this._files,
|
|
840
|
+
sheetRelsPath,
|
|
841
|
+
`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n${stringifyXml([sheetRels])}`,
|
|
842
|
+
);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
}
|