@cj-tech-master/excelts 1.4.3 → 1.4.5

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