@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/worksheet.ts
ADDED
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
import type { CellData, RangeAddress } from './types';
|
|
2
|
+
import type { Workbook } from './workbook';
|
|
3
|
+
import { Cell, parseCellRef } from './cell';
|
|
4
|
+
import { Range } from './range';
|
|
5
|
+
import { parseRange, toAddress, parseAddress } from './utils/address';
|
|
6
|
+
import {
|
|
7
|
+
parseXml,
|
|
8
|
+
findElement,
|
|
9
|
+
getChildren,
|
|
10
|
+
getAttr,
|
|
11
|
+
XmlNode,
|
|
12
|
+
stringifyXml,
|
|
13
|
+
createElement,
|
|
14
|
+
createText,
|
|
15
|
+
} from './utils/xml';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Represents a worksheet in a workbook
|
|
19
|
+
*/
|
|
20
|
+
export class Worksheet {
|
|
21
|
+
private _name: string;
|
|
22
|
+
private _workbook: Workbook;
|
|
23
|
+
private _cells: Map<string, Cell> = new Map();
|
|
24
|
+
private _xmlNodes: XmlNode[] | null = null;
|
|
25
|
+
private _dirty = false;
|
|
26
|
+
private _mergedCells: Set<string> = new Set();
|
|
27
|
+
private _sheetData: XmlNode[] = [];
|
|
28
|
+
|
|
29
|
+
constructor(workbook: Workbook, name: string) {
|
|
30
|
+
this._workbook = workbook;
|
|
31
|
+
this._name = name;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Get the workbook this sheet belongs to
|
|
36
|
+
*/
|
|
37
|
+
get workbook(): Workbook {
|
|
38
|
+
return this._workbook;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Get the sheet name
|
|
43
|
+
*/
|
|
44
|
+
get name(): string {
|
|
45
|
+
return this._name;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Set the sheet name
|
|
50
|
+
*/
|
|
51
|
+
set name(value: string) {
|
|
52
|
+
this._name = value;
|
|
53
|
+
this._dirty = true;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Parse worksheet XML content
|
|
58
|
+
*/
|
|
59
|
+
parse(xml: string): void {
|
|
60
|
+
this._xmlNodes = parseXml(xml);
|
|
61
|
+
const worksheet = findElement(this._xmlNodes, 'worksheet');
|
|
62
|
+
if (!worksheet) return;
|
|
63
|
+
|
|
64
|
+
const worksheetChildren = getChildren(worksheet, 'worksheet');
|
|
65
|
+
|
|
66
|
+
// Parse sheet data (cells)
|
|
67
|
+
const sheetData = findElement(worksheetChildren, 'sheetData');
|
|
68
|
+
if (sheetData) {
|
|
69
|
+
this._sheetData = getChildren(sheetData, 'sheetData');
|
|
70
|
+
this._parseSheetData(this._sheetData);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Parse merged cells
|
|
74
|
+
const mergeCells = findElement(worksheetChildren, 'mergeCells');
|
|
75
|
+
if (mergeCells) {
|
|
76
|
+
const mergeChildren = getChildren(mergeCells, 'mergeCells');
|
|
77
|
+
for (const mergeCell of mergeChildren) {
|
|
78
|
+
if ('mergeCell' in mergeCell) {
|
|
79
|
+
const ref = getAttr(mergeCell, 'ref');
|
|
80
|
+
if (ref) {
|
|
81
|
+
this._mergedCells.add(ref);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Parse the sheetData element to extract cells
|
|
90
|
+
*/
|
|
91
|
+
private _parseSheetData(rows: XmlNode[]): void {
|
|
92
|
+
for (const rowNode of rows) {
|
|
93
|
+
if (!('row' in rowNode)) continue;
|
|
94
|
+
|
|
95
|
+
const rowChildren = getChildren(rowNode, 'row');
|
|
96
|
+
for (const cellNode of rowChildren) {
|
|
97
|
+
if (!('c' in cellNode)) continue;
|
|
98
|
+
|
|
99
|
+
const ref = getAttr(cellNode, 'r');
|
|
100
|
+
if (!ref) continue;
|
|
101
|
+
|
|
102
|
+
const { row, col } = parseAddress(ref);
|
|
103
|
+
const cellData = this._parseCellNode(cellNode);
|
|
104
|
+
const cell = new Cell(this, row, col, cellData);
|
|
105
|
+
this._cells.set(ref, cell);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Parse a cell XML node to CellData
|
|
112
|
+
*/
|
|
113
|
+
private _parseCellNode(node: XmlNode): CellData {
|
|
114
|
+
const data: CellData = {};
|
|
115
|
+
|
|
116
|
+
// Type attribute
|
|
117
|
+
const t = getAttr(node, 't');
|
|
118
|
+
if (t) {
|
|
119
|
+
data.t = t as CellData['t'];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Style attribute
|
|
123
|
+
const s = getAttr(node, 's');
|
|
124
|
+
if (s) {
|
|
125
|
+
data.s = parseInt(s, 10);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const children = getChildren(node, 'c');
|
|
129
|
+
|
|
130
|
+
// Value element
|
|
131
|
+
const vNode = findElement(children, 'v');
|
|
132
|
+
if (vNode) {
|
|
133
|
+
const vChildren = getChildren(vNode, 'v');
|
|
134
|
+
for (const child of vChildren) {
|
|
135
|
+
if ('#text' in child) {
|
|
136
|
+
const text = child['#text'] as string;
|
|
137
|
+
// Parse based on type
|
|
138
|
+
if (data.t === 's') {
|
|
139
|
+
data.v = parseInt(text, 10); // Shared string index
|
|
140
|
+
} else if (data.t === 'b') {
|
|
141
|
+
data.v = text === '1' ? 1 : 0;
|
|
142
|
+
} else if (data.t === 'e' || data.t === 'str') {
|
|
143
|
+
data.v = text;
|
|
144
|
+
} else {
|
|
145
|
+
// Number or default
|
|
146
|
+
data.v = parseFloat(text);
|
|
147
|
+
}
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Formula element
|
|
154
|
+
const fNode = findElement(children, 'f');
|
|
155
|
+
if (fNode) {
|
|
156
|
+
const fChildren = getChildren(fNode, 'f');
|
|
157
|
+
for (const child of fChildren) {
|
|
158
|
+
if ('#text' in child) {
|
|
159
|
+
data.f = child['#text'] as string;
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Check for shared formula
|
|
165
|
+
const si = getAttr(fNode, 'si');
|
|
166
|
+
if (si) {
|
|
167
|
+
data.si = parseInt(si, 10);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Check for array formula range
|
|
171
|
+
const ref = getAttr(fNode, 'ref');
|
|
172
|
+
if (ref) {
|
|
173
|
+
data.F = ref;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Inline string (is element)
|
|
178
|
+
const isNode = findElement(children, 'is');
|
|
179
|
+
if (isNode) {
|
|
180
|
+
data.t = 'str';
|
|
181
|
+
const isChildren = getChildren(isNode, 'is');
|
|
182
|
+
const tNode = findElement(isChildren, 't');
|
|
183
|
+
if (tNode) {
|
|
184
|
+
const tChildren = getChildren(tNode, 't');
|
|
185
|
+
for (const child of tChildren) {
|
|
186
|
+
if ('#text' in child) {
|
|
187
|
+
data.v = child['#text'] as string;
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return data;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Get a cell by address or row/col
|
|
199
|
+
*/
|
|
200
|
+
cell(rowOrAddress: number | string, col?: number): Cell {
|
|
201
|
+
const { row, col: c } = parseCellRef(rowOrAddress, col);
|
|
202
|
+
const address = toAddress(row, c);
|
|
203
|
+
|
|
204
|
+
let cell = this._cells.get(address);
|
|
205
|
+
if (!cell) {
|
|
206
|
+
cell = new Cell(this, row, c);
|
|
207
|
+
this._cells.set(address, cell);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return cell;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Get a range of cells
|
|
215
|
+
*/
|
|
216
|
+
range(rangeStr: string): Range;
|
|
217
|
+
range(startRow: number, startCol: number, endRow: number, endCol: number): Range;
|
|
218
|
+
range(startRowOrRange: number | string, startCol?: number, endRow?: number, endCol?: number): Range {
|
|
219
|
+
let rangeAddr: RangeAddress;
|
|
220
|
+
|
|
221
|
+
if (typeof startRowOrRange === 'string') {
|
|
222
|
+
rangeAddr = parseRange(startRowOrRange);
|
|
223
|
+
} else {
|
|
224
|
+
if (startCol === undefined || endRow === undefined || endCol === undefined) {
|
|
225
|
+
throw new Error('All range parameters must be provided');
|
|
226
|
+
}
|
|
227
|
+
rangeAddr = {
|
|
228
|
+
start: { row: startRowOrRange, col: startCol },
|
|
229
|
+
end: { row: endRow, col: endCol },
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return new Range(this, rangeAddr);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Merge cells in the given range
|
|
238
|
+
*/
|
|
239
|
+
mergeCells(rangeOrStart: string, end?: string): void {
|
|
240
|
+
let rangeStr: string;
|
|
241
|
+
if (end) {
|
|
242
|
+
rangeStr = `${rangeOrStart}:${end}`;
|
|
243
|
+
} else {
|
|
244
|
+
rangeStr = rangeOrStart;
|
|
245
|
+
}
|
|
246
|
+
this._mergedCells.add(rangeStr);
|
|
247
|
+
this._dirty = true;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Unmerge cells in the given range
|
|
252
|
+
*/
|
|
253
|
+
unmergeCells(rangeStr: string): void {
|
|
254
|
+
this._mergedCells.delete(rangeStr);
|
|
255
|
+
this._dirty = true;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Get all merged cell ranges
|
|
260
|
+
*/
|
|
261
|
+
get mergedCells(): string[] {
|
|
262
|
+
return Array.from(this._mergedCells);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Check if the worksheet has been modified
|
|
267
|
+
*/
|
|
268
|
+
get dirty(): boolean {
|
|
269
|
+
if (this._dirty) return true;
|
|
270
|
+
for (const cell of this._cells.values()) {
|
|
271
|
+
if (cell.dirty) return true;
|
|
272
|
+
}
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Get all cells in the worksheet
|
|
278
|
+
*/
|
|
279
|
+
get cells(): Map<string, Cell> {
|
|
280
|
+
return this._cells;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Generate XML for this worksheet
|
|
285
|
+
*/
|
|
286
|
+
toXml(): string {
|
|
287
|
+
// Build sheetData from cells
|
|
288
|
+
const rowMap = new Map<number, Cell[]>();
|
|
289
|
+
for (const cell of this._cells.values()) {
|
|
290
|
+
const row = cell.row;
|
|
291
|
+
if (!rowMap.has(row)) {
|
|
292
|
+
rowMap.set(row, []);
|
|
293
|
+
}
|
|
294
|
+
rowMap.get(row)!.push(cell);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Sort rows and cells
|
|
298
|
+
const sortedRows = Array.from(rowMap.entries()).sort((a, b) => a[0] - b[0]);
|
|
299
|
+
|
|
300
|
+
const rowNodes: XmlNode[] = [];
|
|
301
|
+
for (const [rowIdx, cells] of sortedRows) {
|
|
302
|
+
cells.sort((a, b) => a.col - b.col);
|
|
303
|
+
|
|
304
|
+
const cellNodes: XmlNode[] = [];
|
|
305
|
+
for (const cell of cells) {
|
|
306
|
+
const cellNode = this._buildCellNode(cell);
|
|
307
|
+
cellNodes.push(cellNode);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const rowNode = createElement('row', { r: String(rowIdx + 1) }, cellNodes);
|
|
311
|
+
rowNodes.push(rowNode);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const sheetDataNode = createElement('sheetData', {}, rowNodes);
|
|
315
|
+
|
|
316
|
+
// Build worksheet structure
|
|
317
|
+
const worksheetChildren: XmlNode[] = [sheetDataNode];
|
|
318
|
+
|
|
319
|
+
// Add merged cells if any
|
|
320
|
+
if (this._mergedCells.size > 0) {
|
|
321
|
+
const mergeCellNodes: XmlNode[] = [];
|
|
322
|
+
for (const ref of this._mergedCells) {
|
|
323
|
+
mergeCellNodes.push(createElement('mergeCell', { ref }, []));
|
|
324
|
+
}
|
|
325
|
+
const mergeCellsNode = createElement('mergeCells', { count: String(this._mergedCells.size) }, mergeCellNodes);
|
|
326
|
+
worksheetChildren.push(mergeCellsNode);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const worksheetNode = createElement(
|
|
330
|
+
'worksheet',
|
|
331
|
+
{
|
|
332
|
+
xmlns: 'http://schemas.openxmlformats.org/spreadsheetml/2006/main',
|
|
333
|
+
'xmlns:r': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships',
|
|
334
|
+
},
|
|
335
|
+
worksheetChildren,
|
|
336
|
+
);
|
|
337
|
+
|
|
338
|
+
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n${stringifyXml([worksheetNode])}`;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Build a cell XML node from a Cell object
|
|
343
|
+
*/
|
|
344
|
+
private _buildCellNode(cell: Cell): XmlNode {
|
|
345
|
+
const data = cell.data;
|
|
346
|
+
const attrs: Record<string, string> = { r: cell.address };
|
|
347
|
+
|
|
348
|
+
if (data.t && data.t !== 'n') {
|
|
349
|
+
attrs.t = data.t;
|
|
350
|
+
}
|
|
351
|
+
if (data.s !== undefined) {
|
|
352
|
+
attrs.s = String(data.s);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const children: XmlNode[] = [];
|
|
356
|
+
|
|
357
|
+
// Formula
|
|
358
|
+
if (data.f) {
|
|
359
|
+
const fAttrs: Record<string, string> = {};
|
|
360
|
+
if (data.F) fAttrs.ref = data.F;
|
|
361
|
+
if (data.si !== undefined) fAttrs.si = String(data.si);
|
|
362
|
+
children.push(createElement('f', fAttrs, [createText(data.f)]));
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Value
|
|
366
|
+
if (data.v !== undefined) {
|
|
367
|
+
children.push(createElement('v', {}, [createText(String(data.v))]));
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
return createElement('c', attrs, children);
|
|
371
|
+
}
|
|
372
|
+
}
|