@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.
Files changed (80) hide show
  1. package/README.md +3 -3
  2. package/README_zh.md +3 -3
  3. package/dist/browser/excelts.iife.js +13026 -7610
  4. package/dist/browser/excelts.iife.js.map +1 -1
  5. package/dist/browser/excelts.iife.min.js +87 -24
  6. package/dist/cjs/doc/anchor.js +25 -11
  7. package/dist/cjs/doc/cell.js +75 -43
  8. package/dist/cjs/doc/column.js +39 -16
  9. package/dist/cjs/doc/defined-names.js +53 -7
  10. package/dist/cjs/doc/image.js +11 -8
  11. package/dist/cjs/doc/range.js +64 -28
  12. package/dist/cjs/doc/row.js +33 -17
  13. package/dist/cjs/doc/table.js +3 -5
  14. package/dist/cjs/doc/workbook.js +5 -4
  15. package/dist/cjs/doc/worksheet.js +24 -20
  16. package/dist/cjs/stream/xlsx/workbook-writer.js +3 -2
  17. package/dist/cjs/utils/sheet-utils.js +3 -1
  18. package/dist/cjs/utils/unzip/extract.js +166 -0
  19. package/dist/cjs/utils/unzip/index.js +7 -1
  20. package/dist/cjs/utils/xml-stream.js +25 -3
  21. package/dist/cjs/utils/zip/compress.js +261 -0
  22. package/dist/cjs/utils/zip/crc32.js +154 -0
  23. package/dist/cjs/utils/zip/index.js +70 -0
  24. package/dist/cjs/utils/zip/zip-builder.js +378 -0
  25. package/dist/cjs/utils/zip-stream.js +30 -34
  26. package/dist/cjs/xlsx/xform/book/defined-name-xform.js +36 -2
  27. package/dist/cjs/xlsx/xform/list-xform.js +6 -0
  28. package/dist/cjs/xlsx/xform/sheet/cell-xform.js +6 -1
  29. package/dist/cjs/xlsx/xform/sheet/row-xform.js +24 -2
  30. package/dist/cjs/xlsx/xform/table/filter-column-xform.js +4 -0
  31. package/dist/esm/doc/anchor.js +25 -11
  32. package/dist/esm/doc/cell.js +75 -43
  33. package/dist/esm/doc/column.js +39 -16
  34. package/dist/esm/doc/defined-names.js +53 -7
  35. package/dist/esm/doc/image.js +11 -8
  36. package/dist/esm/doc/range.js +64 -28
  37. package/dist/esm/doc/row.js +33 -17
  38. package/dist/esm/doc/table.js +3 -5
  39. package/dist/esm/doc/workbook.js +5 -4
  40. package/dist/esm/doc/worksheet.js +24 -20
  41. package/dist/esm/stream/xlsx/workbook-writer.js +3 -2
  42. package/dist/esm/utils/sheet-utils.js +3 -1
  43. package/dist/esm/utils/unzip/extract.js +160 -0
  44. package/dist/esm/utils/unzip/index.js +2 -0
  45. package/dist/esm/utils/xml-stream.js +25 -3
  46. package/dist/esm/utils/zip/compress.js +220 -0
  47. package/dist/esm/utils/zip/crc32.js +116 -0
  48. package/dist/esm/utils/zip/index.js +55 -0
  49. package/dist/esm/utils/zip/zip-builder.js +372 -0
  50. package/dist/esm/utils/zip-stream.js +30 -34
  51. package/dist/esm/xlsx/xform/book/defined-name-xform.js +36 -2
  52. package/dist/esm/xlsx/xform/list-xform.js +6 -0
  53. package/dist/esm/xlsx/xform/sheet/cell-xform.js +6 -1
  54. package/dist/esm/xlsx/xform/sheet/row-xform.js +24 -2
  55. package/dist/esm/xlsx/xform/table/filter-column-xform.js +4 -0
  56. package/dist/types/doc/anchor.d.ts +14 -7
  57. package/dist/types/doc/cell.d.ts +85 -40
  58. package/dist/types/doc/column.d.ts +39 -34
  59. package/dist/types/doc/defined-names.d.ts +11 -8
  60. package/dist/types/doc/image.d.ts +29 -12
  61. package/dist/types/doc/pivot-table.d.ts +1 -1
  62. package/dist/types/doc/range.d.ts +15 -4
  63. package/dist/types/doc/row.d.ts +34 -40
  64. package/dist/types/doc/table.d.ts +21 -36
  65. package/dist/types/doc/workbook.d.ts +30 -33
  66. package/dist/types/doc/worksheet.d.ts +105 -80
  67. package/dist/types/stream/xlsx/worksheet-reader.d.ts +3 -5
  68. package/dist/types/types.d.ts +86 -26
  69. package/dist/types/utils/col-cache.d.ts +11 -8
  70. package/dist/types/utils/unzip/extract.d.ts +92 -0
  71. package/dist/types/utils/unzip/index.d.ts +1 -0
  72. package/dist/types/utils/xml-stream.d.ts +2 -0
  73. package/dist/types/utils/zip/compress.d.ts +83 -0
  74. package/dist/types/utils/zip/crc32.d.ts +55 -0
  75. package/dist/types/utils/zip/index.d.ts +52 -0
  76. package/dist/types/utils/zip/zip-builder.d.ts +110 -0
  77. package/dist/types/utils/zip-stream.d.ts +6 -12
  78. package/dist/types/xlsx/xform/list-xform.d.ts +1 -0
  79. package/dist/types/xlsx/xform/sheet/row-xform.d.ts +2 -0
  80. 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 fflate_1 = require("fflate");
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
- // Create fflate Zip instance for streaming compression
28
- this.zip = new fflate_1.Zip((err, data, final) => {
29
- if (err) {
30
- this.stream.emit("error", err);
31
- }
32
- else {
33
- this.stream.write(Buffer.from(data));
34
- if (final) {
35
- this.stream.end();
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, fflate can use it directly - no copy needed
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 streaming API
68
- // Use ZipDeflate for compression or ZipPassThrough for no compression
69
- const useCompression = this.options.compression !== "STORE";
70
- const zipFile = useCompression
71
- ? new fflate_1.ZipDeflate(options.name, { level: this.compressionLevel })
72
- : new fflate_1.ZipPassThrough(options.name);
73
- this.zip.add(zipFile);
74
- zipFile.push(buffer, true); // true = final chunk
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
- // End the zip stream
85
- this.zip.end();
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
- return true;
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 = "";
@@ -82,5 +82,11 @@ class ListXform extends base_xform_js_1.BaseXform {
82
82
  });
83
83
  }
84
84
  }
85
+ reset() {
86
+ super.reset();
87
+ if (this.childXform) {
88
+ this.childXform.reset();
89
+ }
90
+ }
85
91
  }
86
92
  exports.ListXform = ListXform;
@@ -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
- if (model.result !== undefined && style && (0, utils_js_1.isDateFmt)(style.numFmt)) {
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: parseInt(node.attributes.r, 10),
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.model.cells.push(this.parser.model);
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) {