@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.
@@ -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
+ }