@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
@@ -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._comment = cSrc._comment;
83
+ cDst.comment = cSrc.comment;
84
84
  }
85
85
  else if (cDst) {
86
86
  cDst.value = null;
87
87
  cDst.style = {};
88
- cDst._comment = undefined;
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._comment = cSrc._comment;
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._comment = undefined;
112
+ cDst.comment = undefined;
113
113
  }
114
114
  }
115
- eachCell(options, iteratee) {
116
- if (!iteratee) {
117
- iteratee = options;
118
- options = null;
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((cell) => cell && cell.type !== Enums.ValueType.Null);
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
- this._applyStyle("numFmt", value);
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
- this._applyStyle("font", value);
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
- this._applyStyle("alignment", value);
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
- this._applyStyle("protection", value);
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
- this._applyStyle("border", value);
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
- this._applyStyle("fill", value);
290
+ if (value !== undefined) {
291
+ this._applyStyle("fill", value);
292
+ }
277
293
  }
278
294
  get hidden() {
279
295
  return !!this._hidden;
@@ -154,9 +154,7 @@ class Table {
154
154
  // the sheet...
155
155
  const assignStyle = (cell, style) => {
156
156
  if (style) {
157
- Object.keys(style).forEach(key => {
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.name;
375
+ return this.table.style.theme;
378
376
  }
379
377
  set theme(value) {
380
- this.table.style.name = value;
378
+ this.table.style.theme = value;
381
379
  }
382
380
  get showFirstColumn() {
383
381
  return this.table.style.showFirstColumn;
@@ -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 = Object.assign({}, options, {
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(Object.assign({}, image, { type: "image" }));
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._worksheets.find((ws) => ws && ws.name.toLowerCase() === name.toLowerCase())) {
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 && 1) || (cv.headers && cv.headers.length) || 0;
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 rowArguments = [start, count];
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(...rowArguments);
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 = null;
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((row) => {
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((row) => {
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._value.constructor.name === "MergeValue") {
452
- const cellToBeMerged = this.getRow(cell._row._number + nInserts).getCell(colNumber);
453
- const prevMaster = cell._value._master;
454
- const newMaster = this.getRow(prevMaster._row._number + nInserts).getCell(prevMaster._column._number);
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(options, iteratee) {
474
- if (!iteratee) {
475
- iteratee = options;
476
- options = undefined;
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).value = {
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 6 (good balance of speed and size)
35
- const level = options.zip?.zlib?.level ?? options.zip?.compressionOptions?.level ?? 6;
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 fmt = cell.numFmt || "General";
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
- return this._xml.join("");
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 = {