@cj-tech-master/excelts 1.4.3 → 1.4.5-canary.20251212053535.13d32d8
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/README.md +3 -3
- package/README_zh.md +3 -3
- package/dist/browser/excelts.iife.js +13026 -7610
- package/dist/browser/excelts.iife.js.map +1 -1
- package/dist/browser/excelts.iife.min.js +87 -24
- package/dist/cjs/doc/anchor.js +25 -11
- package/dist/cjs/doc/cell.js +75 -43
- package/dist/cjs/doc/column.js +39 -16
- package/dist/cjs/doc/defined-names.js +53 -7
- package/dist/cjs/doc/image.js +11 -8
- package/dist/cjs/doc/range.js +64 -28
- package/dist/cjs/doc/row.js +33 -17
- package/dist/cjs/doc/table.js +3 -5
- package/dist/cjs/doc/workbook.js +5 -4
- package/dist/cjs/doc/worksheet.js +24 -20
- package/dist/cjs/stream/xlsx/workbook-writer.js +3 -2
- package/dist/cjs/utils/sheet-utils.js +3 -1
- package/dist/cjs/utils/unzip/extract.js +166 -0
- package/dist/cjs/utils/unzip/index.js +7 -1
- package/dist/cjs/utils/xml-stream.js +25 -3
- package/dist/cjs/utils/zip/compress.js +261 -0
- package/dist/cjs/utils/zip/crc32.js +154 -0
- package/dist/cjs/utils/zip/index.js +70 -0
- package/dist/cjs/utils/zip/zip-builder.js +378 -0
- package/dist/cjs/utils/zip-stream.js +30 -34
- package/dist/cjs/xlsx/xform/book/defined-name-xform.js +36 -2
- package/dist/cjs/xlsx/xform/list-xform.js +6 -0
- package/dist/cjs/xlsx/xform/sheet/cell-xform.js +6 -1
- package/dist/cjs/xlsx/xform/sheet/row-xform.js +24 -2
- package/dist/cjs/xlsx/xform/table/filter-column-xform.js +4 -0
- package/dist/esm/doc/anchor.js +25 -11
- package/dist/esm/doc/cell.js +75 -43
- package/dist/esm/doc/column.js +39 -16
- package/dist/esm/doc/defined-names.js +53 -7
- package/dist/esm/doc/image.js +11 -8
- package/dist/esm/doc/range.js +64 -28
- package/dist/esm/doc/row.js +33 -17
- package/dist/esm/doc/table.js +3 -5
- package/dist/esm/doc/workbook.js +5 -4
- package/dist/esm/doc/worksheet.js +24 -20
- package/dist/esm/stream/xlsx/workbook-writer.js +3 -2
- package/dist/esm/utils/sheet-utils.js +3 -1
- package/dist/esm/utils/unzip/extract.js +160 -0
- package/dist/esm/utils/unzip/index.js +2 -0
- package/dist/esm/utils/xml-stream.js +25 -3
- package/dist/esm/utils/zip/compress.js +220 -0
- package/dist/esm/utils/zip/crc32.js +116 -0
- package/dist/esm/utils/zip/index.js +55 -0
- package/dist/esm/utils/zip/zip-builder.js +372 -0
- package/dist/esm/utils/zip-stream.js +30 -34
- package/dist/esm/xlsx/xform/book/defined-name-xform.js +36 -2
- package/dist/esm/xlsx/xform/list-xform.js +6 -0
- package/dist/esm/xlsx/xform/sheet/cell-xform.js +6 -1
- package/dist/esm/xlsx/xform/sheet/row-xform.js +24 -2
- package/dist/esm/xlsx/xform/table/filter-column-xform.js +4 -0
- package/dist/types/doc/anchor.d.ts +14 -7
- package/dist/types/doc/cell.d.ts +85 -40
- package/dist/types/doc/column.d.ts +39 -34
- package/dist/types/doc/defined-names.d.ts +11 -8
- package/dist/types/doc/image.d.ts +29 -12
- package/dist/types/doc/pivot-table.d.ts +1 -1
- package/dist/types/doc/range.d.ts +15 -4
- package/dist/types/doc/row.d.ts +34 -40
- package/dist/types/doc/table.d.ts +21 -36
- package/dist/types/doc/workbook.d.ts +30 -33
- package/dist/types/doc/worksheet.d.ts +105 -80
- package/dist/types/stream/xlsx/worksheet-reader.d.ts +3 -5
- package/dist/types/types.d.ts +86 -26
- package/dist/types/utils/col-cache.d.ts +11 -8
- package/dist/types/utils/unzip/extract.d.ts +92 -0
- package/dist/types/utils/unzip/index.d.ts +1 -0
- package/dist/types/utils/xml-stream.d.ts +2 -0
- package/dist/types/utils/zip/compress.d.ts +83 -0
- package/dist/types/utils/zip/crc32.d.ts +55 -0
- package/dist/types/utils/zip/index.d.ts +52 -0
- package/dist/types/utils/zip/zip-builder.d.ts +110 -0
- package/dist/types/utils/zip-stream.d.ts +6 -12
- package/dist/types/xlsx/xform/list-xform.d.ts +1 -0
- package/dist/types/xlsx/xform/sheet/row-xform.d.ts +2 -0
- package/package.json +8 -8
package/dist/esm/doc/row.js
CHANGED
|
@@ -80,12 +80,12 @@ class Row {
|
|
|
80
80
|
cDst = this.getCell(i);
|
|
81
81
|
cDst.value = cSrc.value;
|
|
82
82
|
cDst.style = cSrc.style;
|
|
83
|
-
cDst.
|
|
83
|
+
cDst.comment = cSrc.comment;
|
|
84
84
|
}
|
|
85
85
|
else if (cDst) {
|
|
86
86
|
cDst.value = null;
|
|
87
87
|
cDst.style = {};
|
|
88
|
-
cDst.
|
|
88
|
+
cDst.comment = undefined;
|
|
89
89
|
}
|
|
90
90
|
}
|
|
91
91
|
}
|
|
@@ -97,7 +97,7 @@ class Row {
|
|
|
97
97
|
cDst = this.getCell(i + nExpand);
|
|
98
98
|
cDst.value = cSrc.value;
|
|
99
99
|
cDst.style = cSrc.style;
|
|
100
|
-
cDst.
|
|
100
|
+
cDst.comment = cSrc.comment;
|
|
101
101
|
}
|
|
102
102
|
else {
|
|
103
103
|
this._cells[i + nExpand - 1] = undefined;
|
|
@@ -109,13 +109,18 @@ class Row {
|
|
|
109
109
|
cDst = this.getCell(start + i);
|
|
110
110
|
cDst.value = inserts[i];
|
|
111
111
|
cDst.style = {};
|
|
112
|
-
cDst.
|
|
112
|
+
cDst.comment = undefined;
|
|
113
113
|
}
|
|
114
114
|
}
|
|
115
|
-
eachCell(
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
115
|
+
eachCell(optionsOrIteratee, maybeIteratee) {
|
|
116
|
+
let options = null;
|
|
117
|
+
let iteratee;
|
|
118
|
+
if (typeof optionsOrIteratee === "function") {
|
|
119
|
+
iteratee = optionsOrIteratee;
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
options = optionsOrIteratee;
|
|
123
|
+
iteratee = maybeIteratee;
|
|
119
124
|
}
|
|
120
125
|
if (options && options.includeEmpty) {
|
|
121
126
|
const n = this._cells.length;
|
|
@@ -195,7 +200,7 @@ class Row {
|
|
|
195
200
|
}
|
|
196
201
|
// returns true if the row includes at least one cell with a value
|
|
197
202
|
get hasValues() {
|
|
198
|
-
return this._cells.some(
|
|
203
|
+
return this._cells.some(cell => cell && cell.type !== Enums.ValueType.Null);
|
|
199
204
|
}
|
|
200
205
|
get cellCount() {
|
|
201
206
|
return this._cells.length;
|
|
@@ -234,46 +239,57 @@ class Row {
|
|
|
234
239
|
this.style[name] = value;
|
|
235
240
|
this._cells.forEach(cell => {
|
|
236
241
|
if (cell) {
|
|
237
|
-
cell[name] = value;
|
|
242
|
+
cell.style[name] = value;
|
|
238
243
|
}
|
|
239
244
|
});
|
|
240
|
-
return value;
|
|
241
245
|
}
|
|
242
246
|
get numFmt() {
|
|
243
247
|
return this.style.numFmt;
|
|
244
248
|
}
|
|
245
249
|
set numFmt(value) {
|
|
246
|
-
|
|
250
|
+
if (value !== undefined) {
|
|
251
|
+
this._applyStyle("numFmt", value);
|
|
252
|
+
}
|
|
247
253
|
}
|
|
248
254
|
get font() {
|
|
249
255
|
return this.style.font;
|
|
250
256
|
}
|
|
251
257
|
set font(value) {
|
|
252
|
-
|
|
258
|
+
if (value !== undefined) {
|
|
259
|
+
this._applyStyle("font", value);
|
|
260
|
+
}
|
|
253
261
|
}
|
|
254
262
|
get alignment() {
|
|
255
263
|
return this.style.alignment;
|
|
256
264
|
}
|
|
257
265
|
set alignment(value) {
|
|
258
|
-
|
|
266
|
+
if (value !== undefined) {
|
|
267
|
+
this._applyStyle("alignment", value);
|
|
268
|
+
}
|
|
259
269
|
}
|
|
260
270
|
get protection() {
|
|
261
271
|
return this.style.protection;
|
|
262
272
|
}
|
|
263
273
|
set protection(value) {
|
|
264
|
-
|
|
274
|
+
if (value !== undefined) {
|
|
275
|
+
this._applyStyle("protection", value);
|
|
276
|
+
}
|
|
265
277
|
}
|
|
266
278
|
get border() {
|
|
267
279
|
return this.style.border;
|
|
268
280
|
}
|
|
269
281
|
set border(value) {
|
|
270
|
-
|
|
282
|
+
if (value !== undefined) {
|
|
283
|
+
this._applyStyle("border", value);
|
|
284
|
+
}
|
|
271
285
|
}
|
|
272
286
|
get fill() {
|
|
273
287
|
return this.style.fill;
|
|
274
288
|
}
|
|
275
289
|
set fill(value) {
|
|
276
|
-
|
|
290
|
+
if (value !== undefined) {
|
|
291
|
+
this._applyStyle("fill", value);
|
|
292
|
+
}
|
|
277
293
|
}
|
|
278
294
|
get hidden() {
|
|
279
295
|
return !!this._hidden;
|
package/dist/esm/doc/table.js
CHANGED
|
@@ -154,9 +154,7 @@ class Table {
|
|
|
154
154
|
// the sheet...
|
|
155
155
|
const assignStyle = (cell, style) => {
|
|
156
156
|
if (style) {
|
|
157
|
-
Object.
|
|
158
|
-
cell.style[key] = style[key];
|
|
159
|
-
});
|
|
157
|
+
Object.assign(cell.style, style);
|
|
160
158
|
}
|
|
161
159
|
};
|
|
162
160
|
const { worksheet, table } = this;
|
|
@@ -374,10 +372,10 @@ class Table {
|
|
|
374
372
|
this._assign(this.table, "totalsRow", value);
|
|
375
373
|
}
|
|
376
374
|
get theme() {
|
|
377
|
-
return this.table.style.
|
|
375
|
+
return this.table.style.theme;
|
|
378
376
|
}
|
|
379
377
|
set theme(value) {
|
|
380
|
-
this.table.style.
|
|
378
|
+
this.table.style.theme = value;
|
|
381
379
|
}
|
|
382
380
|
get showFirstColumn() {
|
|
383
381
|
return this.table.style.showFirstColumn;
|
package/dist/esm/doc/workbook.js
CHANGED
|
@@ -50,12 +50,13 @@ class Workbook {
|
|
|
50
50
|
addWorksheet(name, options) {
|
|
51
51
|
const id = this.nextId;
|
|
52
52
|
const lastOrderNo = this._worksheets.reduce((acc, ws) => ((ws && ws.orderNo) > acc ? ws.orderNo : acc), 0);
|
|
53
|
-
const worksheetOptions =
|
|
53
|
+
const worksheetOptions = {
|
|
54
|
+
...options,
|
|
54
55
|
id,
|
|
55
56
|
name,
|
|
56
57
|
orderNo: lastOrderNo + 1,
|
|
57
58
|
workbook: this
|
|
58
|
-
}
|
|
59
|
+
};
|
|
59
60
|
const worksheet = new Worksheet(worksheetOptions);
|
|
60
61
|
this._worksheets[id] = worksheet;
|
|
61
62
|
return worksheet;
|
|
@@ -103,11 +104,11 @@ class Workbook {
|
|
|
103
104
|
addImage(image) {
|
|
104
105
|
// TODO: validation?
|
|
105
106
|
const id = this.media.length;
|
|
106
|
-
this.media.push(
|
|
107
|
+
this.media.push({ ...image, type: "image" });
|
|
107
108
|
return id;
|
|
108
109
|
}
|
|
109
110
|
getImage(id) {
|
|
110
|
-
return this.media[id];
|
|
111
|
+
return this.media[Number(id)];
|
|
111
112
|
}
|
|
112
113
|
get model() {
|
|
113
114
|
return {
|
|
@@ -125,7 +125,7 @@ class Worksheet {
|
|
|
125
125
|
}
|
|
126
126
|
name = name.substring(0, 31);
|
|
127
127
|
}
|
|
128
|
-
if (this._workbook.
|
|
128
|
+
if (this._workbook.worksheets.find(ws => ws && ws.name.toLowerCase() === name.toLowerCase())) {
|
|
129
129
|
throw new Error(`Worksheet name already exists: ${name}`);
|
|
130
130
|
}
|
|
131
131
|
this._name = name;
|
|
@@ -161,7 +161,7 @@ class Worksheet {
|
|
|
161
161
|
set columns(value) {
|
|
162
162
|
// calculate max header row count
|
|
163
163
|
this._headerRowCount = value.reduce((pv, cv) => {
|
|
164
|
-
const headerCount = (cv.header
|
|
164
|
+
const headerCount = Array.isArray(cv.header) ? cv.header.length : cv.header ? 1 : 0;
|
|
165
165
|
return Math.max(pv, headerCount);
|
|
166
166
|
}, 0);
|
|
167
167
|
// construct Column objects
|
|
@@ -217,12 +217,9 @@ class Worksheet {
|
|
|
217
217
|
if (inserts.length > 0) {
|
|
218
218
|
// must iterate over all rows whether they exist yet or not
|
|
219
219
|
for (let i = 0; i < nRows; i++) {
|
|
220
|
-
const
|
|
221
|
-
inserts.forEach(insert => {
|
|
222
|
-
rowArguments.push(insert[i] || null);
|
|
223
|
-
});
|
|
220
|
+
const insertValues = inserts.map(insert => insert[i] || null);
|
|
224
221
|
const row = this.getRow(i + 1);
|
|
225
|
-
row.splice(...
|
|
222
|
+
row.splice(start, count, ...insertValues);
|
|
226
223
|
}
|
|
227
224
|
}
|
|
228
225
|
else {
|
|
@@ -248,7 +245,7 @@ class Worksheet {
|
|
|
248
245
|
}
|
|
249
246
|
}
|
|
250
247
|
for (let i = start; i < start + inserts.length; i++) {
|
|
251
|
-
this.getColumn(i).defn =
|
|
248
|
+
this.getColumn(i).defn = undefined;
|
|
252
249
|
}
|
|
253
250
|
// account for defined names
|
|
254
251
|
this.workbook.definedNames.spliceColumns(this.name, start, count, inserts.length);
|
|
@@ -258,7 +255,7 @@ class Worksheet {
|
|
|
258
255
|
}
|
|
259
256
|
get columnCount() {
|
|
260
257
|
let maxCount = 0;
|
|
261
|
-
this.eachRow(
|
|
258
|
+
this.eachRow(row => {
|
|
262
259
|
maxCount = Math.max(maxCount, row.cellCount);
|
|
263
260
|
});
|
|
264
261
|
return maxCount;
|
|
@@ -267,7 +264,7 @@ class Worksheet {
|
|
|
267
264
|
// performance nightmare - for each row, counts all the columns used
|
|
268
265
|
const counts = [];
|
|
269
266
|
let count = 0;
|
|
270
|
-
this.eachRow(
|
|
267
|
+
this.eachRow(row => {
|
|
271
268
|
row.eachCell(({ col }) => {
|
|
272
269
|
if (!counts[col]) {
|
|
273
270
|
counts[col] = true;
|
|
@@ -448,10 +445,10 @@ class Worksheet {
|
|
|
448
445
|
rSrc.eachCell({ includeEmpty: true }, (cell, colNumber) => {
|
|
449
446
|
rDst.getCell(colNumber).style = cell.style;
|
|
450
447
|
// remerge cells accounting for insert offset
|
|
451
|
-
if (cell.
|
|
452
|
-
const cellToBeMerged = this.getRow(cell.
|
|
453
|
-
const prevMaster = cell.
|
|
454
|
-
const newMaster = this.getRow(prevMaster.
|
|
448
|
+
if (cell.type === Enums.ValueType.Merge) {
|
|
449
|
+
const cellToBeMerged = this.getRow(cell.row + nInserts).getCell(colNumber);
|
|
450
|
+
const prevMaster = cell.master;
|
|
451
|
+
const newMaster = this.getRow(prevMaster.row + nInserts).getCell(prevMaster.col);
|
|
455
452
|
cellToBeMerged.merge(newMaster);
|
|
456
453
|
}
|
|
457
454
|
});
|
|
@@ -470,10 +467,15 @@ class Worksheet {
|
|
|
470
467
|
// account for defined names
|
|
471
468
|
this.workbook.definedNames.spliceRows(this.name, start, count, nInserts);
|
|
472
469
|
}
|
|
473
|
-
eachRow(
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
470
|
+
eachRow(optionsOrIteratee, maybeIteratee) {
|
|
471
|
+
let options;
|
|
472
|
+
let iteratee;
|
|
473
|
+
if (typeof optionsOrIteratee === "function") {
|
|
474
|
+
iteratee = optionsOrIteratee;
|
|
475
|
+
}
|
|
476
|
+
else {
|
|
477
|
+
options = optionsOrIteratee;
|
|
478
|
+
iteratee = maybeIteratee;
|
|
477
479
|
}
|
|
478
480
|
if (options && options.includeEmpty) {
|
|
479
481
|
const n = this._rows.length;
|
|
@@ -611,12 +613,14 @@ class Worksheet {
|
|
|
611
613
|
for (let r = top; r <= bottom; r++) {
|
|
612
614
|
for (let c = left; c <= right; c++) {
|
|
613
615
|
if (first) {
|
|
614
|
-
this.getCell(r, c)
|
|
616
|
+
const cell = this.getCell(r, c);
|
|
617
|
+
const formulaValue = {
|
|
615
618
|
shareType,
|
|
616
619
|
formula,
|
|
617
620
|
ref: range,
|
|
618
621
|
result: getResult(r, c)
|
|
619
622
|
};
|
|
623
|
+
cell.value = formulaValue;
|
|
620
624
|
first = false;
|
|
621
625
|
}
|
|
622
626
|
else {
|
|
@@ -752,7 +756,7 @@ Please leave feedback at https://github.com/excelts/excelts/discussions/2575`);
|
|
|
752
756
|
};
|
|
753
757
|
// =================================================
|
|
754
758
|
// columns
|
|
755
|
-
model.cols = Column.toModel(this.columns);
|
|
759
|
+
model.cols = Column.toModel(this.columns || []);
|
|
756
760
|
// ==========================================================
|
|
757
761
|
// Rows
|
|
758
762
|
const rows = (model.rows = []);
|
|
@@ -31,8 +31,9 @@ class WorkbookWriter {
|
|
|
31
31
|
this.views = [];
|
|
32
32
|
this.zipOptions = options.zip;
|
|
33
33
|
// Extract compression level from zip options (supports both zlib.level and compressionOptions.level)
|
|
34
|
-
// Default compression level is
|
|
35
|
-
|
|
34
|
+
// Default compression level is 1 (fast compression with good ratio)
|
|
35
|
+
// Level 1 is ~2x faster than level 6 with only ~7% larger files
|
|
36
|
+
const level = options.zip?.zlib?.level ?? options.zip?.compressionOptions?.level ?? 1;
|
|
36
37
|
this.compressionLevel = Math.max(0, Math.min(9, level));
|
|
37
38
|
this.media = [];
|
|
38
39
|
this.commentRefs = [];
|
|
@@ -112,7 +112,9 @@ function formatValue(value, fmt, dateFormat) {
|
|
|
112
112
|
*/
|
|
113
113
|
function getCellDisplayText(cell, dateFormat) {
|
|
114
114
|
const value = cell.value;
|
|
115
|
-
const
|
|
115
|
+
const numFmt = cell.numFmt;
|
|
116
|
+
// Extract format code string from numFmt (which can be string or NumFmt object)
|
|
117
|
+
const fmt = typeof numFmt === "string" ? numFmt : (numFmt?.formatCode ?? "General");
|
|
116
118
|
// Null/undefined
|
|
117
119
|
if (value == null) {
|
|
118
120
|
return "";
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simple ZIP extraction utilities
|
|
3
|
+
* Provides easy-to-use Promise-based API for extracting ZIP files
|
|
4
|
+
*/
|
|
5
|
+
import { Readable } from "stream";
|
|
6
|
+
import { createParse } from "./parse.js";
|
|
7
|
+
/**
|
|
8
|
+
* Extract all files from a ZIP buffer
|
|
9
|
+
*
|
|
10
|
+
* @param zipData - ZIP file data as Buffer or Uint8Array
|
|
11
|
+
* @returns Map of file paths to their content
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```ts
|
|
15
|
+
* import { extractAll } from "./utils/unzip/extract.js";
|
|
16
|
+
*
|
|
17
|
+
* const zipData = fs.readFileSync("archive.zip");
|
|
18
|
+
* const files = await extractAll(zipData);
|
|
19
|
+
*
|
|
20
|
+
* for (const [path, file] of files) {
|
|
21
|
+
* console.log(`${path}: ${file.data.length} bytes`);
|
|
22
|
+
* }
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export async function extractAll(zipData) {
|
|
26
|
+
const files = new Map();
|
|
27
|
+
const buffer = Buffer.isBuffer(zipData) ? zipData : Buffer.from(zipData);
|
|
28
|
+
const parse = createParse({ forceStream: true });
|
|
29
|
+
const stream = Readable.from([buffer]);
|
|
30
|
+
stream.pipe(parse);
|
|
31
|
+
for await (const entry of parse) {
|
|
32
|
+
const zipEntry = entry;
|
|
33
|
+
const isDirectory = zipEntry.type === "Directory";
|
|
34
|
+
if (isDirectory) {
|
|
35
|
+
files.set(zipEntry.path, {
|
|
36
|
+
path: zipEntry.path,
|
|
37
|
+
data: Buffer.alloc(0),
|
|
38
|
+
isDirectory: true,
|
|
39
|
+
size: 0
|
|
40
|
+
});
|
|
41
|
+
zipEntry.autodrain();
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
const data = await zipEntry.buffer();
|
|
45
|
+
files.set(zipEntry.path, {
|
|
46
|
+
path: zipEntry.path,
|
|
47
|
+
data,
|
|
48
|
+
isDirectory: false,
|
|
49
|
+
size: data.length
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return files;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Extract a single file from a ZIP buffer
|
|
57
|
+
*
|
|
58
|
+
* @param zipData - ZIP file data as Buffer or Uint8Array
|
|
59
|
+
* @param filePath - Path of the file to extract
|
|
60
|
+
* @returns File content as Buffer, or null if not found
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* ```ts
|
|
64
|
+
* import { extractFile } from "./utils/unzip/extract.js";
|
|
65
|
+
*
|
|
66
|
+
* const zipData = fs.readFileSync("archive.zip");
|
|
67
|
+
* const content = await extractFile(zipData, "readme.txt");
|
|
68
|
+
* if (content) {
|
|
69
|
+
* console.log(content.toString("utf-8"));
|
|
70
|
+
* }
|
|
71
|
+
* ```
|
|
72
|
+
*/
|
|
73
|
+
export async function extractFile(zipData, filePath) {
|
|
74
|
+
const buffer = Buffer.isBuffer(zipData) ? zipData : Buffer.from(zipData);
|
|
75
|
+
const parse = createParse({ forceStream: true });
|
|
76
|
+
const stream = Readable.from([buffer]);
|
|
77
|
+
stream.pipe(parse);
|
|
78
|
+
for await (const entry of parse) {
|
|
79
|
+
const zipEntry = entry;
|
|
80
|
+
if (zipEntry.path === filePath) {
|
|
81
|
+
if (zipEntry.type === "Directory") {
|
|
82
|
+
return Buffer.alloc(0);
|
|
83
|
+
}
|
|
84
|
+
return zipEntry.buffer();
|
|
85
|
+
}
|
|
86
|
+
zipEntry.autodrain();
|
|
87
|
+
}
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* List all file paths in a ZIP buffer (without extracting content)
|
|
92
|
+
*
|
|
93
|
+
* @param zipData - ZIP file data as Buffer or Uint8Array
|
|
94
|
+
* @returns Array of file paths
|
|
95
|
+
*
|
|
96
|
+
* @example
|
|
97
|
+
* ```ts
|
|
98
|
+
* import { listFiles } from "./utils/unzip/extract.js";
|
|
99
|
+
*
|
|
100
|
+
* const zipData = fs.readFileSync("archive.zip");
|
|
101
|
+
* const paths = await listFiles(zipData);
|
|
102
|
+
* console.log(paths); // ["file1.txt", "folder/file2.txt", ...]
|
|
103
|
+
* ```
|
|
104
|
+
*/
|
|
105
|
+
export async function listFiles(zipData) {
|
|
106
|
+
const paths = [];
|
|
107
|
+
const buffer = Buffer.isBuffer(zipData) ? zipData : Buffer.from(zipData);
|
|
108
|
+
const parse = createParse({ forceStream: true });
|
|
109
|
+
const stream = Readable.from([buffer]);
|
|
110
|
+
stream.pipe(parse);
|
|
111
|
+
for await (const entry of parse) {
|
|
112
|
+
const zipEntry = entry;
|
|
113
|
+
paths.push(zipEntry.path);
|
|
114
|
+
zipEntry.autodrain();
|
|
115
|
+
}
|
|
116
|
+
return paths;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Iterate over ZIP entries with a callback (memory efficient for large ZIPs)
|
|
120
|
+
*
|
|
121
|
+
* @param zipData - ZIP file data as Buffer or Uint8Array
|
|
122
|
+
* @param callback - Async callback for each entry, return false to stop iteration
|
|
123
|
+
*
|
|
124
|
+
* @example
|
|
125
|
+
* ```ts
|
|
126
|
+
* import { forEachEntry } from "./utils/unzip/extract.js";
|
|
127
|
+
*
|
|
128
|
+
* await forEachEntry(zipData, async (path, getData) => {
|
|
129
|
+
* if (path.endsWith(".xml")) {
|
|
130
|
+
* const content = await getData();
|
|
131
|
+
* console.log(content.toString("utf-8"));
|
|
132
|
+
* }
|
|
133
|
+
* return true; // continue iteration
|
|
134
|
+
* });
|
|
135
|
+
* ```
|
|
136
|
+
*/
|
|
137
|
+
export async function forEachEntry(zipData, callback) {
|
|
138
|
+
const buffer = Buffer.isBuffer(zipData) ? zipData : Buffer.from(zipData);
|
|
139
|
+
const parse = createParse({ forceStream: true });
|
|
140
|
+
const stream = Readable.from([buffer]);
|
|
141
|
+
stream.pipe(parse);
|
|
142
|
+
for await (const entry of parse) {
|
|
143
|
+
const zipEntry = entry;
|
|
144
|
+
let dataPromise = null;
|
|
145
|
+
const getData = () => {
|
|
146
|
+
if (!dataPromise) {
|
|
147
|
+
dataPromise = zipEntry.buffer();
|
|
148
|
+
}
|
|
149
|
+
return dataPromise;
|
|
150
|
+
};
|
|
151
|
+
const shouldContinue = await callback(zipEntry.path, getData, zipEntry);
|
|
152
|
+
// If callback didn't read data, drain it
|
|
153
|
+
if (!dataPromise) {
|
|
154
|
+
zipEntry.autodrain();
|
|
155
|
+
}
|
|
156
|
+
if (shouldContinue === false) {
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
@@ -10,3 +10,5 @@ export { bufferStream } from "./buffer-stream.js";
|
|
|
10
10
|
export { parse as parseBuffer } from "./parse-buffer.js";
|
|
11
11
|
export { parseDateTime } from "./parse-datetime.js";
|
|
12
12
|
export { parseExtraField } from "./parse-extra-field.js";
|
|
13
|
+
// Simple extraction API
|
|
14
|
+
export { extractAll, extractFile, listFiles, forEachEntry } from "./extract.js";
|
|
@@ -4,6 +4,8 @@ const OPEN_ANGLE = "<";
|
|
|
4
4
|
const CLOSE_ANGLE = ">";
|
|
5
5
|
const OPEN_ANGLE_SLASH = "</";
|
|
6
6
|
const CLOSE_SLASH_ANGLE = "/>";
|
|
7
|
+
// Chunk size for periodic consolidation (reduces final join overhead)
|
|
8
|
+
const CHUNK_SIZE = 10000;
|
|
7
9
|
function pushAttribute(xml, name, value) {
|
|
8
10
|
xml.push(` ${name}="${xmlEncode(value.toString())}"`);
|
|
9
11
|
}
|
|
@@ -21,15 +23,23 @@ function pushAttributes(xml, attributes) {
|
|
|
21
23
|
class XmlStream {
|
|
22
24
|
constructor() {
|
|
23
25
|
this._xml = [];
|
|
26
|
+
this._chunks = [];
|
|
24
27
|
this._stack = [];
|
|
25
28
|
this._rollbacks = [];
|
|
26
29
|
}
|
|
30
|
+
_consolidate() {
|
|
31
|
+
// Periodically join small strings into larger chunks to reduce final join overhead
|
|
32
|
+
if (this._xml.length >= CHUNK_SIZE) {
|
|
33
|
+
this._chunks.push(this._xml.join(""));
|
|
34
|
+
this._xml = [];
|
|
35
|
+
}
|
|
36
|
+
}
|
|
27
37
|
get tos() {
|
|
28
38
|
return this._stack.length ? this._stack[this._stack.length - 1] : undefined;
|
|
29
39
|
}
|
|
30
40
|
get cursor() {
|
|
31
41
|
// handy way to track whether anything has been added
|
|
32
|
-
return this._xml.length;
|
|
42
|
+
return this._chunks.length * CHUNK_SIZE + this._xml.length;
|
|
33
43
|
}
|
|
34
44
|
openXml(docAttributes) {
|
|
35
45
|
const xml = this._xml;
|
|
@@ -96,6 +106,7 @@ class XmlStream {
|
|
|
96
106
|
}
|
|
97
107
|
this.open = false;
|
|
98
108
|
this.leaf = false;
|
|
109
|
+
this._consolidate();
|
|
99
110
|
}
|
|
100
111
|
leafNode(name, attributes, text) {
|
|
101
112
|
this.openNode(name, attributes);
|
|
@@ -115,7 +126,8 @@ class XmlStream {
|
|
|
115
126
|
xml: this._xml.length,
|
|
116
127
|
stack: this._stack.length,
|
|
117
128
|
leaf: this.leaf,
|
|
118
|
-
open: this.open
|
|
129
|
+
open: this.open,
|
|
130
|
+
chunksLength: this._chunks.length
|
|
119
131
|
});
|
|
120
132
|
return this.cursor;
|
|
121
133
|
}
|
|
@@ -130,12 +142,22 @@ class XmlStream {
|
|
|
130
142
|
if (this._stack.length > r.stack) {
|
|
131
143
|
this._stack.splice(r.stack, this._stack.length - r.stack);
|
|
132
144
|
}
|
|
145
|
+
if (this._chunks.length > r.chunksLength) {
|
|
146
|
+
this._chunks.splice(r.chunksLength, this._chunks.length - r.chunksLength);
|
|
147
|
+
}
|
|
133
148
|
this.leaf = r.leaf;
|
|
134
149
|
this.open = r.open;
|
|
135
150
|
}
|
|
136
151
|
get xml() {
|
|
137
152
|
this.closeAll();
|
|
138
|
-
|
|
153
|
+
// Join chunks first, then remaining xml array
|
|
154
|
+
if (this._chunks.length === 0) {
|
|
155
|
+
return this._xml.join("");
|
|
156
|
+
}
|
|
157
|
+
if (this._xml.length > 0) {
|
|
158
|
+
this._chunks.push(this._xml.join(""));
|
|
159
|
+
}
|
|
160
|
+
return this._chunks.join("");
|
|
139
161
|
}
|
|
140
162
|
}
|
|
141
163
|
XmlStream.StdDocAttributes = {
|