@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
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* ZIP file format builder
|
|
4
|
+
*
|
|
5
|
+
* Implements ZIP file structure according to PKWARE's APPNOTE.TXT specification
|
|
6
|
+
* https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT
|
|
7
|
+
*
|
|
8
|
+
* ZIP file structure:
|
|
9
|
+
* ┌──────────────────────────┐
|
|
10
|
+
* │ Local File Header 1 │
|
|
11
|
+
* │ File Data 1 │
|
|
12
|
+
* ├──────────────────────────┤
|
|
13
|
+
* │ Local File Header 2 │
|
|
14
|
+
* │ File Data 2 │
|
|
15
|
+
* ├──────────────────────────┤
|
|
16
|
+
* │ ... │
|
|
17
|
+
* ├──────────────────────────┤
|
|
18
|
+
* │ Central Directory 1 │
|
|
19
|
+
* │ Central Directory 2 │
|
|
20
|
+
* │ ... │
|
|
21
|
+
* ├──────────────────────────┤
|
|
22
|
+
* │ End of Central Directory │
|
|
23
|
+
* └──────────────────────────┘
|
|
24
|
+
*/
|
|
25
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
+
exports.ZipBuilder = void 0;
|
|
27
|
+
exports.createZip = createZip;
|
|
28
|
+
exports.createZipSync = createZipSync;
|
|
29
|
+
const crc32_js_1 = require("./crc32");
|
|
30
|
+
const compress_js_1 = require("./compress");
|
|
31
|
+
// ZIP signature constants
|
|
32
|
+
const LOCAL_FILE_HEADER_SIG = 0x04034b50;
|
|
33
|
+
const CENTRAL_DIR_HEADER_SIG = 0x02014b50;
|
|
34
|
+
const END_OF_CENTRAL_DIR_SIG = 0x06054b50;
|
|
35
|
+
// ZIP version constants
|
|
36
|
+
const VERSION_NEEDED = 20; // 2.0 - supports DEFLATE
|
|
37
|
+
const VERSION_MADE_BY = 20; // 2.0
|
|
38
|
+
// Compression methods
|
|
39
|
+
const COMPRESSION_STORE = 0;
|
|
40
|
+
const COMPRESSION_DEFLATE = 8;
|
|
41
|
+
/**
|
|
42
|
+
* Convert Date to DOS time format
|
|
43
|
+
* @param date - Date to convert
|
|
44
|
+
* @returns [dosTime, dosDate]
|
|
45
|
+
*/
|
|
46
|
+
function dateToDos(date) {
|
|
47
|
+
const dosTime = ((date.getHours() & 0x1f) << 11) |
|
|
48
|
+
((date.getMinutes() & 0x3f) << 5) |
|
|
49
|
+
((date.getSeconds() >> 1) & 0x1f);
|
|
50
|
+
const dosDate = (((date.getFullYear() - 1980) & 0x7f) << 9) |
|
|
51
|
+
(((date.getMonth() + 1) & 0x0f) << 5) |
|
|
52
|
+
(date.getDate() & 0x1f);
|
|
53
|
+
return [dosTime, dosDate];
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Encode string to UTF-8 bytes
|
|
57
|
+
*/
|
|
58
|
+
const encoder = new TextEncoder();
|
|
59
|
+
function encodeString(str) {
|
|
60
|
+
return encoder.encode(str);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Build Local File Header (30 bytes + filename + extra)
|
|
64
|
+
*/
|
|
65
|
+
function buildLocalFileHeader(entry) {
|
|
66
|
+
const header = new Uint8Array(30 + entry.name.length);
|
|
67
|
+
const view = new DataView(header.buffer);
|
|
68
|
+
view.setUint32(0, LOCAL_FILE_HEADER_SIG, true); // Signature
|
|
69
|
+
view.setUint16(4, VERSION_NEEDED, true); // Version needed to extract
|
|
70
|
+
view.setUint16(6, 0x0800, true); // General purpose bit flag (UTF-8 names)
|
|
71
|
+
view.setUint16(8, entry.compressionMethod, true); // Compression method
|
|
72
|
+
view.setUint16(10, entry.modTime, true); // Last mod time
|
|
73
|
+
view.setUint16(12, entry.modDate, true); // Last mod date
|
|
74
|
+
view.setUint32(14, entry.crc, true); // CRC-32
|
|
75
|
+
view.setUint32(18, entry.compressedData.length, true); // Compressed size
|
|
76
|
+
view.setUint32(22, entry.data.length, true); // Uncompressed size
|
|
77
|
+
view.setUint16(26, entry.name.length, true); // Filename length
|
|
78
|
+
view.setUint16(28, 0, true); // Extra field length
|
|
79
|
+
header.set(entry.name, 30);
|
|
80
|
+
return header;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Build Central Directory Header (46 bytes + filename + extra + comment)
|
|
84
|
+
*/
|
|
85
|
+
function buildCentralDirHeader(entry) {
|
|
86
|
+
const header = new Uint8Array(46 + entry.name.length + entry.comment.length);
|
|
87
|
+
const view = new DataView(header.buffer);
|
|
88
|
+
view.setUint32(0, CENTRAL_DIR_HEADER_SIG, true); // Signature
|
|
89
|
+
view.setUint16(4, VERSION_MADE_BY, true); // Version made by
|
|
90
|
+
view.setUint16(6, VERSION_NEEDED, true); // Version needed to extract
|
|
91
|
+
view.setUint16(8, 0x0800, true); // General purpose bit flag (UTF-8 names)
|
|
92
|
+
view.setUint16(10, entry.compressionMethod, true); // Compression method
|
|
93
|
+
view.setUint16(12, entry.modTime, true); // Last mod time
|
|
94
|
+
view.setUint16(14, entry.modDate, true); // Last mod date
|
|
95
|
+
view.setUint32(16, entry.crc, true); // CRC-32
|
|
96
|
+
view.setUint32(20, entry.compressedData.length, true); // Compressed size
|
|
97
|
+
view.setUint32(24, entry.data.length, true); // Uncompressed size
|
|
98
|
+
view.setUint16(28, entry.name.length, true); // Filename length
|
|
99
|
+
view.setUint16(30, 0, true); // Extra field length
|
|
100
|
+
view.setUint16(32, entry.comment.length, true); // Comment length
|
|
101
|
+
view.setUint16(34, 0, true); // Disk number start
|
|
102
|
+
view.setUint16(36, 0, true); // Internal file attributes
|
|
103
|
+
view.setUint32(38, 0, true); // External file attributes
|
|
104
|
+
view.setUint32(42, entry.offset, true); // Relative offset of local header
|
|
105
|
+
header.set(entry.name, 46);
|
|
106
|
+
if (entry.comment.length > 0) {
|
|
107
|
+
header.set(entry.comment, 46 + entry.name.length);
|
|
108
|
+
}
|
|
109
|
+
return header;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Build End of Central Directory Record (22 bytes + comment)
|
|
113
|
+
*/
|
|
114
|
+
function buildEndOfCentralDir(entryCount, centralDirSize, centralDirOffset, comment) {
|
|
115
|
+
const record = new Uint8Array(22 + comment.length);
|
|
116
|
+
const view = new DataView(record.buffer);
|
|
117
|
+
view.setUint32(0, END_OF_CENTRAL_DIR_SIG, true); // Signature
|
|
118
|
+
view.setUint16(4, 0, true); // Number of this disk
|
|
119
|
+
view.setUint16(6, 0, true); // Disk where central dir starts
|
|
120
|
+
view.setUint16(8, entryCount, true); // Number of entries on this disk
|
|
121
|
+
view.setUint16(10, entryCount, true); // Total number of entries
|
|
122
|
+
view.setUint32(12, centralDirSize, true); // Size of central directory
|
|
123
|
+
view.setUint32(16, centralDirOffset, true); // Offset of central directory
|
|
124
|
+
view.setUint16(20, comment.length, true); // Comment length
|
|
125
|
+
if (comment.length > 0) {
|
|
126
|
+
record.set(comment, 22);
|
|
127
|
+
}
|
|
128
|
+
return record;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Create a ZIP file from entries (async)
|
|
132
|
+
*
|
|
133
|
+
* @param entries - Files to include in ZIP
|
|
134
|
+
* @param options - ZIP options
|
|
135
|
+
* @returns ZIP file as Uint8Array
|
|
136
|
+
*
|
|
137
|
+
* @example
|
|
138
|
+
* ```ts
|
|
139
|
+
* const zip = await createZip([
|
|
140
|
+
* { name: "hello.txt", data: new TextEncoder().encode("Hello!") },
|
|
141
|
+
* { name: "folder/file.txt", data: new TextEncoder().encode("Nested!") }
|
|
142
|
+
* ], { level: 6 });
|
|
143
|
+
* ```
|
|
144
|
+
*/
|
|
145
|
+
async function createZip(entries, options = {}) {
|
|
146
|
+
const level = options.level ?? 6;
|
|
147
|
+
const zipComment = encodeString(options.comment ?? "");
|
|
148
|
+
const now = new Date();
|
|
149
|
+
// Process entries
|
|
150
|
+
const processedEntries = [];
|
|
151
|
+
let currentOffset = 0;
|
|
152
|
+
for (const entry of entries) {
|
|
153
|
+
const nameBytes = encodeString(entry.name);
|
|
154
|
+
const commentBytes = encodeString(entry.comment ?? "");
|
|
155
|
+
const modDate = entry.modTime ?? now;
|
|
156
|
+
const [dosTime, dosDate] = dateToDos(modDate);
|
|
157
|
+
// Compress data
|
|
158
|
+
const isCompressed = level > 0 && entry.data.length > 0;
|
|
159
|
+
const compressedData = isCompressed ? await (0, compress_js_1.compress)(entry.data, { level }) : entry.data;
|
|
160
|
+
const processedEntry = {
|
|
161
|
+
name: nameBytes,
|
|
162
|
+
data: entry.data,
|
|
163
|
+
compressedData,
|
|
164
|
+
crc: (0, crc32_js_1.crc32)(entry.data),
|
|
165
|
+
compressionMethod: isCompressed ? COMPRESSION_DEFLATE : COMPRESSION_STORE,
|
|
166
|
+
modTime: dosTime,
|
|
167
|
+
modDate: dosDate,
|
|
168
|
+
comment: commentBytes,
|
|
169
|
+
offset: currentOffset
|
|
170
|
+
};
|
|
171
|
+
// Calculate offset for next entry
|
|
172
|
+
currentOffset += 30 + nameBytes.length + compressedData.length;
|
|
173
|
+
processedEntries.push(processedEntry);
|
|
174
|
+
}
|
|
175
|
+
// Build ZIP structure
|
|
176
|
+
const chunks = [];
|
|
177
|
+
// Local file headers and data
|
|
178
|
+
for (const entry of processedEntries) {
|
|
179
|
+
chunks.push(buildLocalFileHeader(entry));
|
|
180
|
+
chunks.push(entry.compressedData);
|
|
181
|
+
}
|
|
182
|
+
const centralDirOffset = currentOffset;
|
|
183
|
+
// Central directory
|
|
184
|
+
const centralDirChunks = [];
|
|
185
|
+
for (const entry of processedEntries) {
|
|
186
|
+
centralDirChunks.push(buildCentralDirHeader(entry));
|
|
187
|
+
}
|
|
188
|
+
chunks.push(...centralDirChunks);
|
|
189
|
+
const centralDirSize = centralDirChunks.reduce((sum, c) => sum + c.length, 0);
|
|
190
|
+
// End of central directory
|
|
191
|
+
chunks.push(buildEndOfCentralDir(processedEntries.length, centralDirSize, centralDirOffset, zipComment));
|
|
192
|
+
// Combine all chunks
|
|
193
|
+
const totalSize = chunks.reduce((sum, c) => sum + c.length, 0);
|
|
194
|
+
const result = new Uint8Array(totalSize);
|
|
195
|
+
let offset = 0;
|
|
196
|
+
for (const chunk of chunks) {
|
|
197
|
+
result.set(chunk, offset);
|
|
198
|
+
offset += chunk.length;
|
|
199
|
+
}
|
|
200
|
+
return result;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Create a ZIP file from entries (sync, Node.js only)
|
|
204
|
+
*
|
|
205
|
+
* @param entries - Files to include in ZIP
|
|
206
|
+
* @param options - ZIP options
|
|
207
|
+
* @returns ZIP file as Uint8Array
|
|
208
|
+
* @throws Error if not in Node.js environment
|
|
209
|
+
*/
|
|
210
|
+
function createZipSync(entries, options = {}) {
|
|
211
|
+
const level = options.level ?? 6;
|
|
212
|
+
const zipComment = encodeString(options.comment ?? "");
|
|
213
|
+
const now = new Date();
|
|
214
|
+
// Process entries
|
|
215
|
+
const processedEntries = [];
|
|
216
|
+
let currentOffset = 0;
|
|
217
|
+
for (const entry of entries) {
|
|
218
|
+
const nameBytes = encodeString(entry.name);
|
|
219
|
+
const commentBytes = encodeString(entry.comment ?? "");
|
|
220
|
+
const modDate = entry.modTime ?? now;
|
|
221
|
+
const [dosTime, dosDate] = dateToDos(modDate);
|
|
222
|
+
// Compress data
|
|
223
|
+
const isCompressed = level > 0 && entry.data.length > 0;
|
|
224
|
+
const compressedData = isCompressed ? (0, compress_js_1.compressSync)(entry.data, { level }) : entry.data;
|
|
225
|
+
const processedEntry = {
|
|
226
|
+
name: nameBytes,
|
|
227
|
+
data: entry.data,
|
|
228
|
+
compressedData,
|
|
229
|
+
crc: (0, crc32_js_1.crc32)(entry.data),
|
|
230
|
+
compressionMethod: isCompressed ? COMPRESSION_DEFLATE : COMPRESSION_STORE,
|
|
231
|
+
modTime: dosTime,
|
|
232
|
+
modDate: dosDate,
|
|
233
|
+
comment: commentBytes,
|
|
234
|
+
offset: currentOffset
|
|
235
|
+
};
|
|
236
|
+
currentOffset += 30 + nameBytes.length + compressedData.length;
|
|
237
|
+
processedEntries.push(processedEntry);
|
|
238
|
+
}
|
|
239
|
+
// Build ZIP structure
|
|
240
|
+
const chunks = [];
|
|
241
|
+
// Local file headers and data
|
|
242
|
+
for (const entry of processedEntries) {
|
|
243
|
+
chunks.push(buildLocalFileHeader(entry));
|
|
244
|
+
chunks.push(entry.compressedData);
|
|
245
|
+
}
|
|
246
|
+
const centralDirOffset = currentOffset;
|
|
247
|
+
// Central directory
|
|
248
|
+
const centralDirChunks = [];
|
|
249
|
+
for (const entry of processedEntries) {
|
|
250
|
+
centralDirChunks.push(buildCentralDirHeader(entry));
|
|
251
|
+
}
|
|
252
|
+
chunks.push(...centralDirChunks);
|
|
253
|
+
const centralDirSize = centralDirChunks.reduce((sum, c) => sum + c.length, 0);
|
|
254
|
+
// End of central directory
|
|
255
|
+
chunks.push(buildEndOfCentralDir(processedEntries.length, centralDirSize, centralDirOffset, zipComment));
|
|
256
|
+
// Combine all chunks
|
|
257
|
+
const totalSize = chunks.reduce((sum, c) => sum + c.length, 0);
|
|
258
|
+
const result = new Uint8Array(totalSize);
|
|
259
|
+
let offset = 0;
|
|
260
|
+
for (const chunk of chunks) {
|
|
261
|
+
result.set(chunk, offset);
|
|
262
|
+
offset += chunk.length;
|
|
263
|
+
}
|
|
264
|
+
return result;
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Streaming ZIP builder for large files
|
|
268
|
+
* Writes chunks to a callback as they are generated
|
|
269
|
+
*/
|
|
270
|
+
class ZipBuilder {
|
|
271
|
+
/**
|
|
272
|
+
* Create a new ZIP builder
|
|
273
|
+
* @param options - ZIP options
|
|
274
|
+
*/
|
|
275
|
+
constructor(options = {}) {
|
|
276
|
+
this.entries = [];
|
|
277
|
+
this.currentOffset = 0;
|
|
278
|
+
this.finalized = false;
|
|
279
|
+
this.level = options.level ?? 6;
|
|
280
|
+
this.zipComment = encodeString(options.comment ?? "");
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Add a file to the ZIP (async)
|
|
284
|
+
* @param entry - File entry
|
|
285
|
+
* @returns Local file header and compressed data chunks
|
|
286
|
+
*/
|
|
287
|
+
async addFile(entry) {
|
|
288
|
+
if (this.finalized) {
|
|
289
|
+
throw new Error("Cannot add files after finalizing");
|
|
290
|
+
}
|
|
291
|
+
const nameBytes = encodeString(entry.name);
|
|
292
|
+
const commentBytes = encodeString(entry.comment ?? "");
|
|
293
|
+
const [dosTime, dosDate] = dateToDos(entry.modTime ?? new Date());
|
|
294
|
+
// Compress data
|
|
295
|
+
const isCompressed = this.level > 0 && entry.data.length > 0;
|
|
296
|
+
const compressedData = isCompressed
|
|
297
|
+
? await (0, compress_js_1.compress)(entry.data, { level: this.level })
|
|
298
|
+
: entry.data;
|
|
299
|
+
const processedEntry = {
|
|
300
|
+
name: nameBytes,
|
|
301
|
+
data: entry.data,
|
|
302
|
+
compressedData,
|
|
303
|
+
crc: (0, crc32_js_1.crc32)(entry.data),
|
|
304
|
+
compressionMethod: isCompressed ? COMPRESSION_DEFLATE : COMPRESSION_STORE,
|
|
305
|
+
modTime: dosTime,
|
|
306
|
+
modDate: dosDate,
|
|
307
|
+
comment: commentBytes,
|
|
308
|
+
offset: this.currentOffset
|
|
309
|
+
};
|
|
310
|
+
this.entries.push(processedEntry);
|
|
311
|
+
this.currentOffset += 30 + nameBytes.length + compressedData.length;
|
|
312
|
+
return [buildLocalFileHeader(processedEntry), compressedData];
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Add a file to the ZIP (sync, Node.js only)
|
|
316
|
+
* @param entry - File entry
|
|
317
|
+
* @returns Local file header and compressed data chunks
|
|
318
|
+
*/
|
|
319
|
+
addFileSync(entry) {
|
|
320
|
+
if (this.finalized) {
|
|
321
|
+
throw new Error("Cannot add files after finalizing");
|
|
322
|
+
}
|
|
323
|
+
const nameBytes = encodeString(entry.name);
|
|
324
|
+
const commentBytes = encodeString(entry.comment ?? "");
|
|
325
|
+
const [dosTime, dosDate] = dateToDos(entry.modTime ?? new Date());
|
|
326
|
+
// Compress data
|
|
327
|
+
const isCompressed = this.level > 0 && entry.data.length > 0;
|
|
328
|
+
const compressedData = isCompressed
|
|
329
|
+
? (0, compress_js_1.compressSync)(entry.data, { level: this.level })
|
|
330
|
+
: entry.data;
|
|
331
|
+
const processedEntry = {
|
|
332
|
+
name: nameBytes,
|
|
333
|
+
data: entry.data,
|
|
334
|
+
compressedData,
|
|
335
|
+
crc: (0, crc32_js_1.crc32)(entry.data),
|
|
336
|
+
compressionMethod: isCompressed ? COMPRESSION_DEFLATE : COMPRESSION_STORE,
|
|
337
|
+
modTime: dosTime,
|
|
338
|
+
modDate: dosDate,
|
|
339
|
+
comment: commentBytes,
|
|
340
|
+
offset: this.currentOffset
|
|
341
|
+
};
|
|
342
|
+
this.entries.push(processedEntry);
|
|
343
|
+
this.currentOffset += 30 + nameBytes.length + compressedData.length;
|
|
344
|
+
return [buildLocalFileHeader(processedEntry), compressedData];
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Finalize the ZIP and return central directory + end record
|
|
348
|
+
* @returns Central directory and end of central directory chunks
|
|
349
|
+
*/
|
|
350
|
+
finalize() {
|
|
351
|
+
if (this.finalized) {
|
|
352
|
+
throw new Error("ZIP already finalized");
|
|
353
|
+
}
|
|
354
|
+
this.finalized = true;
|
|
355
|
+
const chunks = [];
|
|
356
|
+
// Central directory
|
|
357
|
+
for (const entry of this.entries) {
|
|
358
|
+
chunks.push(buildCentralDirHeader(entry));
|
|
359
|
+
}
|
|
360
|
+
const centralDirSize = chunks.reduce((sum, c) => sum + c.length, 0);
|
|
361
|
+
// End of central directory
|
|
362
|
+
chunks.push(buildEndOfCentralDir(this.entries.length, centralDirSize, this.currentOffset, this.zipComment));
|
|
363
|
+
return chunks;
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* Get current number of entries
|
|
367
|
+
*/
|
|
368
|
+
get entryCount() {
|
|
369
|
+
return this.entries.length;
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Get current ZIP data size (without central directory)
|
|
373
|
+
*/
|
|
374
|
+
get dataSize() {
|
|
375
|
+
return this.currentOffset;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
exports.ZipBuilder = ZipBuilder;
|
|
@@ -5,37 +5,25 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.ZipWriter = void 0;
|
|
7
7
|
const events_1 = __importDefault(require("events"));
|
|
8
|
-
const
|
|
8
|
+
const index_js_1 = require("./zip/index");
|
|
9
9
|
const stream_buf_js_1 = require("./stream-buf");
|
|
10
10
|
// =============================================================================
|
|
11
11
|
// The ZipWriter class
|
|
12
12
|
// Packs streamed data into an output zip stream
|
|
13
|
+
// Uses native zlib (Node.js) or CompressionStream (browser) for best performance
|
|
13
14
|
class ZipWriter extends events_1.default.EventEmitter {
|
|
14
15
|
constructor(options) {
|
|
15
16
|
super();
|
|
16
|
-
this.options = Object.assign({
|
|
17
|
-
type: "nodebuffer",
|
|
18
|
-
compression: "DEFLATE"
|
|
19
|
-
}, options);
|
|
20
|
-
// Default compression level is 6 (good balance of speed and size)
|
|
21
|
-
// 0 = no compression, 9 = best compression
|
|
22
|
-
const level = this.options.compressionOptions?.level ?? 6;
|
|
23
|
-
this.compressionLevel = Math.max(0, Math.min(9, level));
|
|
24
|
-
this.files = {};
|
|
25
|
-
this.stream = new stream_buf_js_1.StreamBuf();
|
|
26
17
|
this.finalized = false;
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
});
|
|
18
|
+
this.pendingWrites = [];
|
|
19
|
+
// Determine compression level:
|
|
20
|
+
// - STORE mode = 0 (no compression)
|
|
21
|
+
// - DEFLATE mode = user level or default 1 (fast compression)
|
|
22
|
+
const level = options?.compression === "STORE"
|
|
23
|
+
? 0
|
|
24
|
+
: Math.max(0, Math.min(9, options?.compressionOptions?.level ?? 1));
|
|
25
|
+
this.stream = new stream_buf_js_1.StreamBuf();
|
|
26
|
+
this.zipBuilder = new index_js_1.ZipBuilder({ level });
|
|
39
27
|
}
|
|
40
28
|
append(data, options) {
|
|
41
29
|
let buffer;
|
|
@@ -49,7 +37,7 @@ class ZipWriter extends events_1.default.EventEmitter {
|
|
|
49
37
|
buffer = Buffer.from(data, "utf8");
|
|
50
38
|
}
|
|
51
39
|
else if (Buffer.isBuffer(data)) {
|
|
52
|
-
// Buffer extends Uint8Array,
|
|
40
|
+
// Buffer extends Uint8Array, can use it directly - no copy needed
|
|
53
41
|
buffer = data;
|
|
54
42
|
}
|
|
55
43
|
else if (ArrayBuffer.isView(data)) {
|
|
@@ -64,14 +52,16 @@ class ZipWriter extends events_1.default.EventEmitter {
|
|
|
64
52
|
// Assume it's already a Uint8Array or compatible type
|
|
65
53
|
buffer = data;
|
|
66
54
|
}
|
|
67
|
-
// Add file to zip using
|
|
68
|
-
//
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
55
|
+
// Add file to zip using native compression
|
|
56
|
+
// addFile returns chunks that we write to stream immediately
|
|
57
|
+
const writePromise = this.zipBuilder
|
|
58
|
+
.addFile({ name: options.name, data: buffer })
|
|
59
|
+
.then(chunks => {
|
|
60
|
+
for (const chunk of chunks) {
|
|
61
|
+
this.stream.write(Buffer.from(chunk));
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
this.pendingWrites.push(writePromise);
|
|
75
65
|
}
|
|
76
66
|
push(chunk) {
|
|
77
67
|
return this.stream.push(chunk);
|
|
@@ -81,8 +71,14 @@ class ZipWriter extends events_1.default.EventEmitter {
|
|
|
81
71
|
return;
|
|
82
72
|
}
|
|
83
73
|
this.finalized = true;
|
|
84
|
-
//
|
|
85
|
-
this.
|
|
74
|
+
// Wait for all pending writes to complete
|
|
75
|
+
await Promise.all(this.pendingWrites);
|
|
76
|
+
// Finalize the zip and write central directory
|
|
77
|
+
const finalChunks = this.zipBuilder.finalize();
|
|
78
|
+
for (const chunk of finalChunks) {
|
|
79
|
+
this.stream.write(Buffer.from(chunk));
|
|
80
|
+
}
|
|
81
|
+
this.stream.end();
|
|
86
82
|
this.emit("finish");
|
|
87
83
|
}
|
|
88
84
|
// ==========================================================================
|
|
@@ -46,16 +46,50 @@ class DefinedNamesXform extends base_xform_js_1.BaseXform {
|
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
48
|
exports.DefinedNamesXform = DefinedNamesXform;
|
|
49
|
+
// Regex to validate cell range format:
|
|
50
|
+
// - Cell: $A$1 or A1
|
|
51
|
+
// - Range: $A$1:$B$10 or A1:B10
|
|
52
|
+
// - Row range: $1:$2 (for print titles)
|
|
53
|
+
// - Column range: $A:$B (for print titles)
|
|
54
|
+
const cellRangeRegexp = /^[$]?[A-Za-z]{1,3}[$]?\d+(:[$]?[A-Za-z]{1,3}[$]?\d+)?$/;
|
|
55
|
+
const rowRangeRegexp = /^[$]?\d+:[$]?\d+$/;
|
|
56
|
+
const colRangeRegexp = /^[$]?[A-Za-z]{1,3}:[$]?[A-Za-z]{1,3}$/;
|
|
49
57
|
function isValidRange(range) {
|
|
58
|
+
// Skip array constants wrapped in {} - these are not valid cell ranges
|
|
59
|
+
// e.g., {"'Sheet1'!$A$1:$B$10"} or {#N/A,#N/A,FALSE,"text"}
|
|
60
|
+
if (range.startsWith("{") || range.endsWith("}")) {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
// Extract the cell reference part (after the sheet name if present)
|
|
64
|
+
const cellRef = range.split("!").pop() || "";
|
|
65
|
+
// Must match one of the valid patterns
|
|
66
|
+
if (!cellRangeRegexp.test(cellRef) &&
|
|
67
|
+
!rowRangeRegexp.test(cellRef) &&
|
|
68
|
+
!colRangeRegexp.test(cellRef)) {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
50
71
|
try {
|
|
51
|
-
col_cache_js_1.colCache.decodeEx(range);
|
|
52
|
-
|
|
72
|
+
const decoded = col_cache_js_1.colCache.decodeEx(range);
|
|
73
|
+
// For cell ranges: row/col or top/bottom/left/right should be valid numbers
|
|
74
|
+
// For row ranges ($1:$2): top/bottom are numbers, left/right are null
|
|
75
|
+
// For column ranges ($A:$B): left/right are numbers, top/bottom are null
|
|
76
|
+
if (("row" in decoded && typeof decoded.row === "number") ||
|
|
77
|
+
("top" in decoded && typeof decoded.top === "number") ||
|
|
78
|
+
("left" in decoded && typeof decoded.left === "number")) {
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
return false;
|
|
53
82
|
}
|
|
54
83
|
catch {
|
|
55
84
|
return false;
|
|
56
85
|
}
|
|
57
86
|
}
|
|
58
87
|
function extractRanges(parsedText) {
|
|
88
|
+
// Skip if the entire text is wrapped in {} (array constant)
|
|
89
|
+
const trimmed = parsedText.trim();
|
|
90
|
+
if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
|
|
91
|
+
return [];
|
|
92
|
+
}
|
|
59
93
|
const ranges = [];
|
|
60
94
|
let quotesOpened = false;
|
|
61
95
|
let last = "";
|
|
@@ -410,7 +410,12 @@ class CellXform extends base_xform_js_1.BaseXform {
|
|
|
410
410
|
}
|
|
411
411
|
break;
|
|
412
412
|
case enums_js_1.Enums.ValueType.Formula:
|
|
413
|
-
|
|
413
|
+
// Only convert formula result to date if the result is a number
|
|
414
|
+
// String results (t="str") should not be converted even if the cell has a date format
|
|
415
|
+
if (model.result !== undefined &&
|
|
416
|
+
typeof model.result === "number" &&
|
|
417
|
+
style &&
|
|
418
|
+
(0, utils_js_1.isDateFmt)(style.numFmt)) {
|
|
414
419
|
model.result = (0, utils_js_1.excelToDate)(model.result, options.date1904);
|
|
415
420
|
}
|
|
416
421
|
if (model.shareType === "shared") {
|
|
@@ -4,6 +4,7 @@ exports.RowXform = void 0;
|
|
|
4
4
|
const base_xform_js_1 = require("../base-xform");
|
|
5
5
|
const cell_xform_js_1 = require("./cell-xform");
|
|
6
6
|
const utils_js_1 = require("../../../utils/utils");
|
|
7
|
+
const col_cache_js_1 = require("../../../utils/col-cache");
|
|
7
8
|
class RowXform extends base_xform_js_1.BaseXform {
|
|
8
9
|
constructor(options) {
|
|
9
10
|
super();
|
|
@@ -15,6 +16,11 @@ class RowXform extends base_xform_js_1.BaseXform {
|
|
|
15
16
|
get tag() {
|
|
16
17
|
return "row";
|
|
17
18
|
}
|
|
19
|
+
reset() {
|
|
20
|
+
super.reset();
|
|
21
|
+
this.numRowsSeen = 0;
|
|
22
|
+
this.lastCellCol = 0;
|
|
23
|
+
}
|
|
18
24
|
prepare(model, options) {
|
|
19
25
|
const styleId = options.styles.addStyleModel(model.style);
|
|
20
26
|
if (styleId) {
|
|
@@ -65,11 +71,15 @@ class RowXform extends base_xform_js_1.BaseXform {
|
|
|
65
71
|
}
|
|
66
72
|
if (node.name === "row") {
|
|
67
73
|
this.numRowsSeen += 1;
|
|
74
|
+
// Reset lastCellCol for each new row
|
|
75
|
+
this.lastCellCol = 0;
|
|
68
76
|
const spans = node.attributes.spans
|
|
69
77
|
? node.attributes.spans.split(":").map((span) => parseInt(span, 10))
|
|
70
78
|
: [undefined, undefined];
|
|
79
|
+
// If r attribute is missing, use numRowsSeen as the row number
|
|
80
|
+
const rowNumber = node.attributes.r ? parseInt(node.attributes.r, 10) : this.numRowsSeen;
|
|
71
81
|
const model = (this.model = {
|
|
72
|
-
number:
|
|
82
|
+
number: rowNumber,
|
|
73
83
|
min: spans[0],
|
|
74
84
|
max: spans[1],
|
|
75
85
|
cells: []
|
|
@@ -109,7 +119,19 @@ class RowXform extends base_xform_js_1.BaseXform {
|
|
|
109
119
|
parseClose(name) {
|
|
110
120
|
if (this.parser) {
|
|
111
121
|
if (!this.parser.parseClose(name)) {
|
|
112
|
-
this.
|
|
122
|
+
const cellModel = this.parser.model;
|
|
123
|
+
// If cell has address, extract column number from it
|
|
124
|
+
// Otherwise, calculate address based on position
|
|
125
|
+
if (cellModel.address) {
|
|
126
|
+
const decoded = col_cache_js_1.colCache.decodeAddress(cellModel.address);
|
|
127
|
+
this.lastCellCol = decoded.col;
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
// No r attribute, calculate address from position
|
|
131
|
+
this.lastCellCol += 1;
|
|
132
|
+
cellModel.address = col_cache_js_1.colCache.encodeAddress(this.model.number, this.lastCellCol);
|
|
133
|
+
}
|
|
134
|
+
this.model.cells.push(cellModel);
|
|
113
135
|
if (this.maxItems && this.model.cells.length > this.maxItems) {
|
|
114
136
|
throw new Error(`Max column count (${this.maxItems}) exceeded`);
|
|
115
137
|
}
|
|
@@ -57,6 +57,10 @@ class FilterColumnXform extends base_xform_js_1.BaseXform {
|
|
|
57
57
|
filterButton: attributes.hiddenButton === "0"
|
|
58
58
|
};
|
|
59
59
|
return true;
|
|
60
|
+
case "dynamicFilter":
|
|
61
|
+
// Ignore dynamicFilter nodes - we don't need to preserve them for reading
|
|
62
|
+
// See: https://github.com/exceljs/exceljs/issues/2972
|
|
63
|
+
return true;
|
|
60
64
|
default:
|
|
61
65
|
this.parser = this.map[node.name];
|
|
62
66
|
if (this.parser) {
|