@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.
- package/dist/browser/excelts.iife.js +2568 -1188
- package/dist/browser/excelts.iife.js.map +1 -1
- package/dist/browser/excelts.iife.min.js +21 -19
- package/dist/cjs/index.js +1 -0
- package/dist/cjs/stream/xlsx/workbook-reader.js +2 -2
- package/dist/cjs/stream/xlsx/workbook-writer.js +8 -4
- package/dist/cjs/utils/cell-format.js +815 -0
- package/dist/cjs/utils/cell-matrix.js +37 -2
- package/dist/cjs/utils/parse-sax.js +2 -2
- package/dist/cjs/utils/sheet-utils.js +615 -0
- package/dist/cjs/utils/stream-buf.js +15 -4
- package/dist/cjs/utils/unzip/buffer-stream.js +27 -0
- package/dist/cjs/utils/unzip/index.js +23 -0
- package/dist/cjs/utils/unzip/noop-stream.js +20 -0
- package/dist/cjs/utils/unzip/parse-buffer.js +60 -0
- package/dist/cjs/utils/unzip/parse-datetime.js +23 -0
- package/dist/cjs/utils/unzip/parse-extra-field.js +52 -0
- package/dist/cjs/utils/unzip/parse.js +340 -0
- package/dist/cjs/utils/unzip/pull-stream.js +145 -0
- package/dist/cjs/utils/utils.js +13 -17
- package/dist/cjs/utils/zip-stream.js +29 -33
- package/dist/cjs/xlsx/xlsx.js +1 -2
- package/dist/esm/index.browser.js +1 -0
- package/dist/esm/index.js +1 -0
- package/dist/esm/stream/xlsx/workbook-reader.js +2 -2
- package/dist/esm/stream/xlsx/workbook-writer.js +9 -5
- package/dist/esm/utils/cell-format.js +810 -0
- package/dist/esm/utils/cell-matrix.js +37 -2
- package/dist/esm/utils/parse-sax.js +1 -1
- package/dist/esm/utils/sheet-utils.js +595 -0
- package/dist/esm/utils/stream-buf.js +15 -4
- package/dist/esm/utils/unzip/buffer-stream.js +24 -0
- package/dist/esm/utils/unzip/index.js +12 -0
- package/dist/esm/utils/unzip/noop-stream.js +16 -0
- package/dist/esm/utils/unzip/parse-buffer.js +57 -0
- package/dist/esm/utils/unzip/parse-datetime.js +20 -0
- package/dist/esm/utils/unzip/parse-extra-field.js +49 -0
- package/dist/esm/utils/unzip/parse.js +332 -0
- package/dist/esm/utils/unzip/pull-stream.js +141 -0
- package/dist/esm/utils/utils.js +12 -16
- package/dist/esm/utils/zip-stream.js +30 -34
- package/dist/esm/xlsx/xlsx.js +1 -2
- package/dist/types/doc/column.d.ts +1 -1
- package/dist/types/doc/worksheet.d.ts +2 -2
- package/dist/types/index.browser.d.ts +1 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/stream/xlsx/workbook-writer.d.ts +1 -0
- package/dist/types/utils/cell-format.d.ts +32 -0
- package/dist/types/utils/sheet-utils.d.ts +203 -0
- package/dist/types/utils/unzip/buffer-stream.d.ts +9 -0
- package/dist/types/utils/unzip/index.d.ts +12 -0
- package/dist/types/utils/unzip/noop-stream.d.ts +13 -0
- package/dist/types/utils/unzip/parse-buffer.d.ts +24 -0
- package/dist/types/utils/unzip/parse-datetime.d.ts +12 -0
- package/dist/types/utils/unzip/parse-extra-field.d.ts +18 -0
- package/dist/types/utils/unzip/parse.d.ts +70 -0
- package/dist/types/utils/unzip/pull-stream.d.ts +24 -0
- package/dist/types/utils/utils.d.ts +5 -2
- package/dist/types/utils/zip-stream.d.ts +5 -1
- package/package.json +35 -32
- package/dist/cjs/utils/browser-buffer-decode.js +0 -13
- package/dist/cjs/utils/browser-buffer-encode.js +0 -13
- package/dist/cjs/utils/browser.js +0 -6
- package/dist/esm/utils/browser-buffer-decode.js +0 -11
- package/dist/esm/utils/browser-buffer-encode.js +0 -11
- package/dist/esm/utils/browser.js +0 -3
- package/dist/types/utils/browser-buffer-decode.d.ts +0 -2
- package/dist/types/utils/browser-buffer-encode.d.ts +0 -2
- 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 (
|
|
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
|
-
?
|
|
152
|
+
? { ...address, ...safeDeepClone(this.template) }
|
|
118
153
|
: address);
|
|
119
154
|
}
|
|
120
155
|
return undefined;
|
|
@@ -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
|
+
};
|