@cj-tech-master/excelts 1.0.0 → 1.4.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.
Files changed (69) hide show
  1. package/dist/browser/excelts.iife.js +2568 -1188
  2. package/dist/browser/excelts.iife.js.map +1 -1
  3. package/dist/browser/excelts.iife.min.js +21 -19
  4. package/dist/cjs/index.js +1 -0
  5. package/dist/cjs/stream/xlsx/workbook-reader.js +2 -2
  6. package/dist/cjs/stream/xlsx/workbook-writer.js +8 -4
  7. package/dist/cjs/utils/cell-format.js +815 -0
  8. package/dist/cjs/utils/cell-matrix.js +37 -2
  9. package/dist/cjs/utils/parse-sax.js +2 -2
  10. package/dist/cjs/utils/sheet-utils.js +615 -0
  11. package/dist/cjs/utils/stream-buf.js +15 -4
  12. package/dist/cjs/utils/unzip/buffer-stream.js +27 -0
  13. package/dist/cjs/utils/unzip/index.js +23 -0
  14. package/dist/cjs/utils/unzip/noop-stream.js +20 -0
  15. package/dist/cjs/utils/unzip/parse-buffer.js +60 -0
  16. package/dist/cjs/utils/unzip/parse-datetime.js +23 -0
  17. package/dist/cjs/utils/unzip/parse-extra-field.js +52 -0
  18. package/dist/cjs/utils/unzip/parse.js +340 -0
  19. package/dist/cjs/utils/unzip/pull-stream.js +145 -0
  20. package/dist/cjs/utils/utils.js +13 -17
  21. package/dist/cjs/utils/zip-stream.js +29 -33
  22. package/dist/cjs/xlsx/xlsx.js +1 -2
  23. package/dist/esm/index.browser.js +1 -0
  24. package/dist/esm/index.js +1 -0
  25. package/dist/esm/stream/xlsx/workbook-reader.js +2 -2
  26. package/dist/esm/stream/xlsx/workbook-writer.js +9 -5
  27. package/dist/esm/utils/cell-format.js +810 -0
  28. package/dist/esm/utils/cell-matrix.js +37 -2
  29. package/dist/esm/utils/parse-sax.js +1 -1
  30. package/dist/esm/utils/sheet-utils.js +595 -0
  31. package/dist/esm/utils/stream-buf.js +15 -4
  32. package/dist/esm/utils/unzip/buffer-stream.js +24 -0
  33. package/dist/esm/utils/unzip/index.js +12 -0
  34. package/dist/esm/utils/unzip/noop-stream.js +16 -0
  35. package/dist/esm/utils/unzip/parse-buffer.js +57 -0
  36. package/dist/esm/utils/unzip/parse-datetime.js +20 -0
  37. package/dist/esm/utils/unzip/parse-extra-field.js +49 -0
  38. package/dist/esm/utils/unzip/parse.js +332 -0
  39. package/dist/esm/utils/unzip/pull-stream.js +141 -0
  40. package/dist/esm/utils/utils.js +12 -16
  41. package/dist/esm/utils/zip-stream.js +30 -34
  42. package/dist/esm/xlsx/xlsx.js +1 -2
  43. package/dist/types/doc/column.d.ts +1 -1
  44. package/dist/types/doc/worksheet.d.ts +2 -2
  45. package/dist/types/index.browser.d.ts +1 -0
  46. package/dist/types/index.d.ts +1 -0
  47. package/dist/types/stream/xlsx/workbook-writer.d.ts +1 -0
  48. package/dist/types/utils/cell-format.d.ts +32 -0
  49. package/dist/types/utils/sheet-utils.d.ts +203 -0
  50. package/dist/types/utils/unzip/buffer-stream.d.ts +9 -0
  51. package/dist/types/utils/unzip/index.d.ts +12 -0
  52. package/dist/types/utils/unzip/noop-stream.d.ts +13 -0
  53. package/dist/types/utils/unzip/parse-buffer.d.ts +24 -0
  54. package/dist/types/utils/unzip/parse-datetime.d.ts +12 -0
  55. package/dist/types/utils/unzip/parse-extra-field.d.ts +18 -0
  56. package/dist/types/utils/unzip/parse.d.ts +70 -0
  57. package/dist/types/utils/unzip/pull-stream.d.ts +24 -0
  58. package/dist/types/utils/utils.d.ts +5 -2
  59. package/dist/types/utils/zip-stream.d.ts +5 -1
  60. package/package.json +35 -32
  61. package/dist/cjs/utils/browser-buffer-decode.js +0 -13
  62. package/dist/cjs/utils/browser-buffer-encode.js +0 -13
  63. package/dist/cjs/utils/browser.js +0 -6
  64. package/dist/esm/utils/browser-buffer-decode.js +0 -11
  65. package/dist/esm/utils/browser-buffer-encode.js +0 -11
  66. package/dist/esm/utils/browser.js +0 -3
  67. package/dist/types/utils/browser-buffer-decode.d.ts +0 -2
  68. package/dist/types/utils/browser-buffer-encode.d.ts +0 -2
  69. package/dist/types/utils/browser.d.ts +0 -1
@@ -1,4 +1,27 @@
1
1
  import { colCache } from "./col-cache.js";
2
+ // Helper to check for prototype pollution
3
+ function isSafeKey(key) {
4
+ if (typeof key === "number") {
5
+ return true;
6
+ }
7
+ return key !== "__proto__" && key !== "constructor" && key !== "prototype";
8
+ }
9
+ // Safe deep clone that filters out prototype pollution keys
10
+ function safeDeepClone(obj) {
11
+ if (obj === null || typeof obj !== "object") {
12
+ return obj;
13
+ }
14
+ if (Array.isArray(obj)) {
15
+ return obj.map(item => safeDeepClone(item));
16
+ }
17
+ const result = {};
18
+ for (const key of Object.keys(obj)) {
19
+ if (isSafeKey(key)) {
20
+ result[key] = safeDeepClone(obj[key]);
21
+ }
22
+ }
23
+ return result;
24
+ }
2
25
  class CellMatrix {
3
26
  constructor(template) {
4
27
  this.template = template;
@@ -39,6 +62,9 @@ class CellMatrix {
39
62
  return this.findRowCell(row, address, create);
40
63
  }
41
64
  getCellAt(sheetName, rowNumber, colNumber) {
65
+ if (!isSafeKey(sheetName)) {
66
+ throw new Error(`Invalid sheet name: ${sheetName}`);
67
+ }
42
68
  const sheet = this.sheets[sheetName] || (this.sheets[sheetName] = []);
43
69
  const row = sheet[rowNumber] || (sheet[rowNumber] = []);
44
70
  const cell = row[colNumber] ||
@@ -89,7 +115,10 @@ class CellMatrix {
89
115
  }
90
116
  findSheet(address, create) {
91
117
  const name = address.sheetName;
92
- if (this.sheets[name]) {
118
+ if (!isSafeKey(name)) {
119
+ throw new Error(`Invalid sheet name: ${name}`);
120
+ }
121
+ if (Object.prototype.hasOwnProperty.call(this.sheets, name)) {
93
122
  return this.sheets[name];
94
123
  }
95
124
  if (create) {
@@ -99,6 +128,9 @@ class CellMatrix {
99
128
  }
100
129
  findSheetRow(sheet, address, create) {
101
130
  const { row } = address;
131
+ if (!isSafeKey(row)) {
132
+ throw new Error(`Invalid row: ${row}`);
133
+ }
102
134
  if (sheet && sheet[row]) {
103
135
  return sheet[row];
104
136
  }
@@ -109,12 +141,15 @@ class CellMatrix {
109
141
  }
110
142
  findRowCell(row, address, create) {
111
143
  const { col } = address;
144
+ if (!isSafeKey(col)) {
145
+ throw new Error(`Invalid column: ${col}`);
146
+ }
112
147
  if (row && row[col]) {
113
148
  return row[col];
114
149
  }
115
150
  if (create) {
116
151
  return (row[col] = this.template
117
- ? Object.assign(address, JSON.parse(JSON.stringify(this.template)))
152
+ ? { ...address, ...safeDeepClone(this.template) }
118
153
  : address);
119
154
  }
120
155
  return undefined;
@@ -1,5 +1,5 @@
1
1
  import { SaxesParser } from "saxes";
2
- import { bufferToString } from "./browser-buffer-decode.js";
2
+ import { bufferToString } from "./utils.js";
3
3
  async function* parseSax(iterable) {
4
4
  const saxesParser = new SaxesParser({
5
5
  xmlns: false,
@@ -0,0 +1,595 @@
1
+ /**
2
+ * Utility functions for ExcelTS
3
+ * Provides convenient helper functions for common spreadsheet operations
4
+ */
5
+ import { Workbook } from "../doc/workbook.js";
6
+ import { colCache } from "./col-cache.js";
7
+ import { dateToExcel } from "./utils.js";
8
+ import { format as cellFormat } from "./cell-format.js";
9
+ /**
10
+ * Get formatted display text for a cell value
11
+ * Returns the value formatted according to the cell's numFmt
12
+ * This matches Excel's display exactly
13
+ */
14
+ function getCellDisplayText(cell) {
15
+ const value = cell.value;
16
+ const fmt = cell.numFmt || "General";
17
+ // Null/undefined
18
+ if (value == null) {
19
+ return "";
20
+ }
21
+ // Date object - convert to Excel serial number
22
+ if (value instanceof Date) {
23
+ const serial = dateToExcel(value, false);
24
+ return cellFormat(fmt, serial);
25
+ }
26
+ // Number/Boolean/String - let cellFormat handle it
27
+ if (typeof value === "number" || typeof value === "boolean" || typeof value === "string") {
28
+ return cellFormat(fmt, value);
29
+ }
30
+ // Fallback to cell.text for other types (rich text, hyperlink, error, formula, etc.)
31
+ return cell.text;
32
+ }
33
+ // =============================================================================
34
+ // Cell Address Encoding/Decoding
35
+ // =============================================================================
36
+ /**
37
+ * Decode column string to 0-indexed number
38
+ * @example decodeCol("A") => 0, decodeCol("Z") => 25, decodeCol("AA") => 26
39
+ */
40
+ export function decodeCol(colstr) {
41
+ return colCache.l2n(colstr.toUpperCase()) - 1;
42
+ }
43
+ /**
44
+ * Encode 0-indexed column number to string
45
+ * @example encodeCol(0) => "A", encodeCol(25) => "Z", encodeCol(26) => "AA"
46
+ */
47
+ export function encodeCol(col) {
48
+ return colCache.n2l(col + 1);
49
+ }
50
+ /**
51
+ * Decode row string to 0-indexed number
52
+ * @example decodeRow("1") => 0, decodeRow("10") => 9
53
+ */
54
+ export function decodeRow(rowstr) {
55
+ return parseInt(rowstr, 10) - 1;
56
+ }
57
+ /**
58
+ * Encode 0-indexed row number to string
59
+ * @example encodeRow(0) => "1", encodeRow(9) => "10"
60
+ */
61
+ export function encodeRow(row) {
62
+ return String(row + 1);
63
+ }
64
+ /**
65
+ * Decode cell address string to CellAddress object
66
+ * @example decodeCell("A1") => {c: 0, r: 0}, decodeCell("B2") => {c: 1, r: 1}
67
+ */
68
+ export function decodeCell(cstr) {
69
+ const addr = colCache.decodeAddress(cstr.toUpperCase());
70
+ return { c: addr.col - 1, r: addr.row - 1 };
71
+ }
72
+ /**
73
+ * Encode CellAddress object to cell address string
74
+ * @example encodeCell({c: 0, r: 0}) => "A1", encodeCell({c: 1, r: 1}) => "B2"
75
+ */
76
+ export function encodeCell(cell) {
77
+ return colCache.encodeAddress(cell.r + 1, cell.c + 1);
78
+ }
79
+ /**
80
+ * Decode range string to Range object
81
+ * @example decodeRange("A1:B2") => {s: {c: 0, r: 0}, e: {c: 1, r: 1}}
82
+ */
83
+ export function decodeRange(range) {
84
+ const idx = range.indexOf(":");
85
+ if (idx === -1) {
86
+ const cell = decodeCell(range);
87
+ return { s: cell, e: { ...cell } };
88
+ }
89
+ return {
90
+ s: decodeCell(range.slice(0, idx)),
91
+ e: decodeCell(range.slice(idx + 1))
92
+ };
93
+ }
94
+ export function encodeRange(startOrRange, end) {
95
+ if (end === undefined) {
96
+ const range = startOrRange;
97
+ return encodeRange(range.s, range.e);
98
+ }
99
+ const start = startOrRange;
100
+ const startStr = encodeCell(start);
101
+ const endStr = encodeCell(end);
102
+ return startStr === endStr ? startStr : `${startStr}:${endStr}`;
103
+ }
104
+ /**
105
+ * Create a worksheet from an array of objects (xlsx compatible)
106
+ * @example
107
+ * const ws = jsonToSheet([{name: "Alice", age: 30}, {name: "Bob", age: 25}])
108
+ */
109
+ export function jsonToSheet(data, opts) {
110
+ const o = opts || {};
111
+ // Create a temporary workbook to get a worksheet
112
+ const tempWb = new Workbook();
113
+ const worksheet = tempWb.addWorksheet("Sheet1");
114
+ if (data.length === 0) {
115
+ return worksheet;
116
+ }
117
+ // Determine headers - use provided header or Object.keys from first object
118
+ const allKeys = new Set();
119
+ data.forEach(row => Object.keys(row).forEach(k => allKeys.add(k)));
120
+ const headers = o.header ? [...o.header] : [...allKeys];
121
+ // Add any missing keys from data that weren't in header
122
+ if (o.header) {
123
+ allKeys.forEach(k => {
124
+ if (!headers.includes(k)) {
125
+ headers.push(k);
126
+ }
127
+ });
128
+ }
129
+ let rowNum = 1;
130
+ // Write header row
131
+ if (!o.skipHeader) {
132
+ headers.forEach((h, colIdx) => {
133
+ worksheet.getCell(rowNum, colIdx + 1).value = h;
134
+ });
135
+ rowNum++;
136
+ }
137
+ // Write data rows
138
+ for (const row of data) {
139
+ headers.forEach((key, colIdx) => {
140
+ const val = row[key];
141
+ if (val === null && o.nullError) {
142
+ worksheet.getCell(rowNum, colIdx + 1).value = { error: "#NULL!" };
143
+ }
144
+ else if (val !== undefined && val !== null) {
145
+ worksheet.getCell(rowNum, colIdx + 1).value = val;
146
+ }
147
+ });
148
+ rowNum++;
149
+ }
150
+ return worksheet;
151
+ }
152
+ /**
153
+ * Add data from an array of objects to an existing worksheet (xlsx compatible)
154
+ */
155
+ export function sheetAddJson(worksheet, data, opts) {
156
+ const o = opts || {};
157
+ if (data.length === 0) {
158
+ return worksheet;
159
+ }
160
+ // Determine starting position
161
+ let startRow = 1;
162
+ let startCol = 1;
163
+ if (o.origin !== undefined) {
164
+ if (typeof o.origin === "string") {
165
+ const addr = decodeCell(o.origin);
166
+ startRow = addr.r + 1;
167
+ startCol = addr.c + 1;
168
+ }
169
+ else if (typeof o.origin === "number") {
170
+ if (o.origin === -1) {
171
+ // Append to bottom
172
+ startRow = worksheet.rowCount + 1;
173
+ }
174
+ else {
175
+ startRow = o.origin + 1; // 0-indexed row
176
+ }
177
+ }
178
+ else {
179
+ startRow = o.origin.r + 1;
180
+ startCol = o.origin.c + 1;
181
+ }
182
+ }
183
+ // Determine headers
184
+ const allKeys = new Set();
185
+ data.forEach(row => Object.keys(row).forEach(k => allKeys.add(k)));
186
+ const headers = o.header ? [...o.header] : [...allKeys];
187
+ if (o.header) {
188
+ allKeys.forEach(k => {
189
+ if (!headers.includes(k)) {
190
+ headers.push(k);
191
+ }
192
+ });
193
+ }
194
+ let rowNum = startRow;
195
+ // Write header row
196
+ if (!o.skipHeader) {
197
+ headers.forEach((h, colIdx) => {
198
+ worksheet.getCell(rowNum, startCol + colIdx).value = h;
199
+ });
200
+ rowNum++;
201
+ }
202
+ // Write data rows
203
+ for (const row of data) {
204
+ headers.forEach((key, colIdx) => {
205
+ const val = row[key];
206
+ if (val === null && o.nullError) {
207
+ worksheet.getCell(rowNum, startCol + colIdx).value = { error: "#NULL!" };
208
+ }
209
+ else if (val !== undefined && val !== null) {
210
+ worksheet.getCell(rowNum, startCol + colIdx).value = val;
211
+ }
212
+ });
213
+ rowNum++;
214
+ }
215
+ return worksheet;
216
+ }
217
+ /**
218
+ * Convert worksheet to JSON array (xlsx compatible)
219
+ * @example
220
+ * // Default: array of objects with first row as headers
221
+ * const data = sheetToJson(worksheet)
222
+ * // => [{name: "Alice", age: 30}, {name: "Bob", age: 25}]
223
+ *
224
+ * // Array of arrays
225
+ * const aoa = sheetToJson(worksheet, { header: 1 })
226
+ * // => [["name", "age"], ["Alice", 30], ["Bob", 25]]
227
+ *
228
+ * // Column letters as keys
229
+ * const cols = sheetToJson(worksheet, { header: "A" })
230
+ * // => [{A: "name", B: "age"}, {A: "Alice", B: 30}]
231
+ */
232
+ export function sheetToJson(worksheet, opts) {
233
+ const o = opts || {};
234
+ // Determine range
235
+ let startRow = 1;
236
+ let endRow = worksheet.rowCount;
237
+ let startCol = 1;
238
+ let endCol = worksheet.columnCount;
239
+ if (o.range !== undefined) {
240
+ if (typeof o.range === "number") {
241
+ startRow = o.range + 1; // 0-indexed to 1-indexed
242
+ }
243
+ else if (typeof o.range === "string") {
244
+ const r = decodeRange(o.range);
245
+ startRow = r.s.r + 1;
246
+ endRow = r.e.r + 1;
247
+ startCol = r.s.c + 1;
248
+ endCol = r.e.c + 1;
249
+ }
250
+ }
251
+ if (endRow < startRow || endCol < startCol) {
252
+ return [];
253
+ }
254
+ // Handle header option
255
+ const headerOpt = o.header;
256
+ // header: 1 - return array of arrays
257
+ if (headerOpt === 1) {
258
+ const result = [];
259
+ // Default for header:1 is to include blank rows
260
+ const includeBlank = o.blankrows !== false;
261
+ for (let row = startRow; row <= endRow; row++) {
262
+ const rowData = [];
263
+ let isEmpty = true;
264
+ for (let col = startCol; col <= endCol; col++) {
265
+ const cell = worksheet.getCell(row, col);
266
+ const val = o.raw === false ? getCellDisplayText(cell).trim() : cell.value;
267
+ if (val != null && val !== "") {
268
+ rowData[col - startCol] = val;
269
+ isEmpty = false;
270
+ }
271
+ else if (o.defval !== undefined) {
272
+ rowData[col - startCol] = o.defval;
273
+ }
274
+ else {
275
+ rowData[col - startCol] = null;
276
+ }
277
+ }
278
+ if (!isEmpty || includeBlank) {
279
+ result.push(rowData);
280
+ }
281
+ }
282
+ return result;
283
+ }
284
+ // header: "A" - use column letters as keys
285
+ if (headerOpt === "A") {
286
+ const result = [];
287
+ // Default for header:"A" is to skip blank rows
288
+ const includeBlank = o.blankrows === true;
289
+ for (let row = startRow; row <= endRow; row++) {
290
+ const rowData = {};
291
+ let isEmpty = true;
292
+ for (let col = startCol; col <= endCol; col++) {
293
+ const cell = worksheet.getCell(row, col);
294
+ const val = o.raw === false ? getCellDisplayText(cell).trim() : cell.value;
295
+ const key = encodeCol(col - 1); // 0-indexed for encodeCol
296
+ if (val != null && val !== "") {
297
+ rowData[key] = val;
298
+ isEmpty = false;
299
+ }
300
+ else if (o.defval !== undefined) {
301
+ rowData[key] = o.defval;
302
+ }
303
+ }
304
+ if (!isEmpty || includeBlank) {
305
+ result.push(rowData);
306
+ }
307
+ }
308
+ return result;
309
+ }
310
+ // header: string[] - use provided array as keys
311
+ if (Array.isArray(headerOpt)) {
312
+ const result = [];
313
+ const includeBlank = o.blankrows === true;
314
+ for (let row = startRow; row <= endRow; row++) {
315
+ const rowData = {};
316
+ let isEmpty = true;
317
+ for (let col = startCol; col <= endCol; col++) {
318
+ const colIdx = col - startCol;
319
+ const key = headerOpt[colIdx] ?? `__EMPTY_${colIdx}`;
320
+ const cell = worksheet.getCell(row, col);
321
+ const val = o.raw === false ? getCellDisplayText(cell).trim() : cell.value;
322
+ if (val != null && val !== "") {
323
+ rowData[key] = val;
324
+ isEmpty = false;
325
+ }
326
+ else if (o.defval !== undefined) {
327
+ rowData[key] = o.defval;
328
+ }
329
+ }
330
+ if (!isEmpty || includeBlank) {
331
+ result.push(rowData);
332
+ }
333
+ }
334
+ return result;
335
+ }
336
+ // Default: first row as header, disambiguate duplicates
337
+ const headers = [];
338
+ const headerCounts = {};
339
+ for (let col = startCol; col <= endCol; col++) {
340
+ const cell = worksheet.getCell(startRow, col);
341
+ const val = cell.value;
342
+ let header = val != null ? String(val) : `__EMPTY_${col - startCol}`;
343
+ // Disambiguate duplicate headers
344
+ if (headerCounts[header] !== undefined) {
345
+ headerCounts[header]++;
346
+ header = `${header}_${headerCounts[header]}`;
347
+ }
348
+ else {
349
+ headerCounts[header] = 0;
350
+ }
351
+ headers.push(header);
352
+ }
353
+ // Read data rows (skip header row)
354
+ const result = [];
355
+ const dataStartRow = startRow + 1;
356
+ // Default for objects is to skip blank rows
357
+ const includeBlank = o.blankrows === true;
358
+ for (let row = dataStartRow; row <= endRow; row++) {
359
+ const rowData = {};
360
+ let isEmpty = true;
361
+ for (let col = startCol; col <= endCol; col++) {
362
+ const cell = worksheet.getCell(row, col);
363
+ const val = o.raw === false ? getCellDisplayText(cell).trim() : cell.value;
364
+ const key = headers[col - startCol];
365
+ if (val != null && val !== "") {
366
+ rowData[key] = val;
367
+ isEmpty = false;
368
+ }
369
+ else if (o.defval !== undefined) {
370
+ rowData[key] = o.defval;
371
+ }
372
+ }
373
+ if (!isEmpty || includeBlank) {
374
+ result.push(rowData);
375
+ }
376
+ }
377
+ return result;
378
+ }
379
+ /**
380
+ * Convert worksheet to CSV string
381
+ */
382
+ export function sheetToCsv(worksheet, opts) {
383
+ const o = opts || {};
384
+ const FS = o.FS ?? ",";
385
+ const RS = o.RS ?? "\n";
386
+ const rows = [];
387
+ worksheet.eachRow({ includeEmpty: o.blankrows !== false }, (row, rowNumber) => {
388
+ const cells = [];
389
+ let isEmpty = true;
390
+ row.eachCell({ includeEmpty: true }, (cell, colNumber) => {
391
+ let val = "";
392
+ if (cell.value != null) {
393
+ if (cell.value instanceof Date) {
394
+ val = cell.value.toISOString();
395
+ }
396
+ else if (typeof cell.value === "object") {
397
+ // Handle rich text, formula results, etc.
398
+ if ("result" in cell.value) {
399
+ val = String(cell.value.result ?? "");
400
+ }
401
+ else if ("text" in cell.value) {
402
+ val = String(cell.value.text ?? "");
403
+ }
404
+ else if ("richText" in cell.value) {
405
+ val = cell.value.richText.map(r => r.text).join("");
406
+ }
407
+ else {
408
+ val = String(cell.value);
409
+ }
410
+ }
411
+ else {
412
+ val = String(cell.value);
413
+ }
414
+ isEmpty = false;
415
+ }
416
+ // Quote if necessary
417
+ const needsQuote = o.forceQuotes ||
418
+ val.includes(FS) ||
419
+ val.includes('"') ||
420
+ val.includes("\n") ||
421
+ val.includes("\r");
422
+ if (needsQuote) {
423
+ val = `"${val.replace(/"/g, '""')}"`;
424
+ }
425
+ cells.push(val);
426
+ });
427
+ // Pad cells to match column count
428
+ while (cells.length < worksheet.columnCount) {
429
+ cells.push("");
430
+ }
431
+ if (!isEmpty || o.blankrows !== false) {
432
+ rows.push(cells.join(FS));
433
+ }
434
+ });
435
+ return rows.join(RS);
436
+ }
437
+ // =============================================================================
438
+ // Workbook Functions
439
+ // =============================================================================
440
+ /**
441
+ * Create a new workbook
442
+ */
443
+ export function bookNew() {
444
+ return new Workbook();
445
+ }
446
+ /**
447
+ * Append worksheet to workbook (xlsx compatible)
448
+ * @example
449
+ * const wb = bookNew();
450
+ * const ws = jsonToSheet([{a: 1, b: 2}]);
451
+ * bookAppendSheet(wb, ws, "Sheet1");
452
+ */
453
+ export function bookAppendSheet(workbook, worksheet, name) {
454
+ // Copy the worksheet data to a new sheet in the workbook
455
+ const newWs = workbook.addWorksheet(name);
456
+ // Copy all cells from source worksheet
457
+ worksheet.eachRow({ includeEmpty: true }, (row, rowNumber) => {
458
+ row.eachCell({ includeEmpty: true }, (cell, colNumber) => {
459
+ const newCell = newWs.getCell(rowNumber, colNumber);
460
+ newCell.value = cell.value;
461
+ if (cell.style) {
462
+ newCell.style = cell.style;
463
+ }
464
+ });
465
+ });
466
+ // Copy column properties
467
+ worksheet.columns?.forEach((col, idx) => {
468
+ if (col && newWs.columns[idx]) {
469
+ if (col.width) {
470
+ newWs.getColumn(idx + 1).width = col.width;
471
+ }
472
+ }
473
+ });
474
+ }
475
+ /**
476
+ * Create a worksheet from an array of arrays (xlsx compatible)
477
+ * @example
478
+ * const ws = aoaToSheet([["Name", "Age"], ["Alice", 30], ["Bob", 25]])
479
+ */
480
+ export function aoaToSheet(data, opts) {
481
+ const tempWb = new Workbook();
482
+ const worksheet = tempWb.addWorksheet("Sheet1");
483
+ if (data.length === 0) {
484
+ return worksheet;
485
+ }
486
+ // Determine starting position
487
+ let startRow = 1;
488
+ let startCol = 1;
489
+ if (opts?.origin !== undefined) {
490
+ if (typeof opts.origin === "string") {
491
+ const addr = decodeCell(opts.origin);
492
+ startRow = addr.r + 1;
493
+ startCol = addr.c + 1;
494
+ }
495
+ else if (typeof opts.origin === "number") {
496
+ startRow = opts.origin + 1; // 0-indexed row
497
+ }
498
+ else {
499
+ startRow = opts.origin.r + 1;
500
+ startCol = opts.origin.c + 1;
501
+ }
502
+ }
503
+ data.forEach((row, rowIdx) => {
504
+ if (!row) {
505
+ return;
506
+ }
507
+ row.forEach((val, colIdx) => {
508
+ if (val !== undefined && val !== null) {
509
+ worksheet.getCell(startRow + rowIdx, startCol + colIdx).value = val;
510
+ }
511
+ });
512
+ });
513
+ return worksheet;
514
+ }
515
+ /**
516
+ * Add data from an array of arrays to an existing worksheet (xlsx compatible)
517
+ */
518
+ export function sheetAddAoa(worksheet, data, opts) {
519
+ if (data.length === 0) {
520
+ return worksheet;
521
+ }
522
+ // Determine starting position
523
+ let startRow = 1;
524
+ let startCol = 1;
525
+ if (opts?.origin !== undefined) {
526
+ if (typeof opts.origin === "string") {
527
+ const addr = decodeCell(opts.origin);
528
+ startRow = addr.r + 1;
529
+ startCol = addr.c + 1;
530
+ }
531
+ else if (typeof opts.origin === "number") {
532
+ if (opts.origin === -1) {
533
+ // Append to bottom
534
+ startRow = worksheet.rowCount + 1;
535
+ }
536
+ else {
537
+ startRow = opts.origin + 1; // 0-indexed row
538
+ }
539
+ }
540
+ else {
541
+ startRow = opts.origin.r + 1;
542
+ startCol = opts.origin.c + 1;
543
+ }
544
+ }
545
+ data.forEach((row, rowIdx) => {
546
+ if (!row) {
547
+ return;
548
+ }
549
+ row.forEach((val, colIdx) => {
550
+ if (val !== undefined && val !== null) {
551
+ worksheet.getCell(startRow + rowIdx, startCol + colIdx).value = val;
552
+ }
553
+ });
554
+ });
555
+ return worksheet;
556
+ }
557
+ /**
558
+ * Convert worksheet to array of arrays
559
+ */
560
+ export function sheetToAoa(worksheet) {
561
+ const result = [];
562
+ worksheet.eachRow({ includeEmpty: true }, (row, rowNumber) => {
563
+ const rowData = [];
564
+ row.eachCell({ includeEmpty: true }, (cell, colNumber) => {
565
+ rowData[colNumber - 1] = cell.value;
566
+ });
567
+ result[rowNumber - 1] = rowData;
568
+ });
569
+ return result;
570
+ }
571
+ // =============================================================================
572
+ // Export utils object
573
+ // =============================================================================
574
+ export const utils = {
575
+ // Cell encoding/decoding (camelCase)
576
+ decodeCol,
577
+ encodeCol,
578
+ decodeRow,
579
+ encodeRow,
580
+ decodeCell,
581
+ encodeCell,
582
+ decodeRange,
583
+ encodeRange,
584
+ // Sheet/JSON conversion (camelCase)
585
+ jsonToSheet,
586
+ sheetAddJson,
587
+ sheetToJson,
588
+ sheetToCsv,
589
+ aoaToSheet,
590
+ sheetAddAoa,
591
+ sheetToAoa,
592
+ // Workbook functions (camelCase)
593
+ bookNew,
594
+ bookAppendSheet
595
+ };