@js-ak/excel-toolbox 1.5.0 → 1.7.0

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 (53) hide show
  1. package/README.md +41 -62
  2. package/build/cjs/lib/merge-sheets-to-base-file-process-sync.js +105 -0
  3. package/build/cjs/lib/merge-sheets-to-base-file-process.js +3 -3
  4. package/build/cjs/lib/merge-sheets-to-base-file-sync.js +2 -2
  5. package/build/cjs/lib/merge-sheets-to-base-file.js +1 -1
  6. package/build/cjs/lib/template/template-fs.js +143 -63
  7. package/build/cjs/lib/template/template-memory.js +281 -59
  8. package/build/cjs/lib/template/utils/index.js +25 -0
  9. package/build/cjs/lib/template/utils/prepare-row-to-cells.js +5 -1
  10. package/build/cjs/lib/template/utils/regexp.js +32 -0
  11. package/build/cjs/lib/template/utils/update-dimension.js +15 -0
  12. package/build/cjs/lib/template/utils/validate-worksheet-xml.js +74 -74
  13. package/build/cjs/lib/template/utils/write-rows-to-stream.js +57 -17
  14. package/build/cjs/lib/xml/extract-rows-from-sheet-sync.js +67 -0
  15. package/build/cjs/lib/xml/extract-rows-from-sheet.js +4 -2
  16. package/build/cjs/lib/xml/extract-xml-from-sheet-sync.js +43 -0
  17. package/build/cjs/lib/xml/extract-xml-from-sheet.js +15 -15
  18. package/build/cjs/lib/xml/index.js +2 -1
  19. package/build/esm/lib/merge-sheets-to-base-file-process-sync.js +69 -0
  20. package/build/esm/lib/merge-sheets-to-base-file-process.js +3 -3
  21. package/build/esm/lib/merge-sheets-to-base-file-sync.js +2 -2
  22. package/build/esm/lib/merge-sheets-to-base-file.js +1 -1
  23. package/build/esm/lib/template/template-fs.js +140 -63
  24. package/build/esm/lib/template/template-memory.js +281 -59
  25. package/build/esm/lib/template/utils/index.js +2 -0
  26. package/build/esm/lib/template/utils/prepare-row-to-cells.js +5 -1
  27. package/build/esm/lib/template/utils/regexp.js +28 -0
  28. package/build/esm/lib/template/utils/update-dimension.js +15 -0
  29. package/build/esm/lib/template/utils/validate-worksheet-xml.js +74 -74
  30. package/build/esm/lib/template/utils/write-rows-to-stream.js +57 -17
  31. package/build/esm/lib/xml/extract-rows-from-sheet-sync.js +64 -0
  32. package/build/esm/lib/xml/extract-rows-from-sheet.js +4 -2
  33. package/build/esm/lib/xml/extract-xml-from-sheet-sync.js +40 -0
  34. package/build/esm/lib/xml/extract-xml-from-sheet.js +12 -15
  35. package/build/esm/lib/xml/index.js +2 -1
  36. package/build/types/lib/merge-sheets-to-base-file-process-sync.d.ts +27 -0
  37. package/build/types/lib/merge-sheets-to-base-file-process.d.ts +1 -1
  38. package/build/types/lib/template/template-fs.d.ts +2 -0
  39. package/build/types/lib/template/template-memory.d.ts +61 -0
  40. package/build/types/lib/template/utils/index.d.ts +2 -0
  41. package/build/types/lib/template/utils/prepare-row-to-cells.d.ts +5 -1
  42. package/build/types/lib/template/utils/regexp.d.ts +24 -0
  43. package/build/types/lib/template/utils/update-dimension.d.ts +15 -0
  44. package/build/types/lib/template/utils/write-rows-to-stream.d.ts +22 -9
  45. package/build/types/lib/xml/extract-rows-from-sheet-sync.d.ts +28 -0
  46. package/build/types/lib/xml/extract-rows-from-sheet.d.ts +2 -2
  47. package/build/types/lib/xml/extract-xml-from-sheet-sync.d.ts +14 -0
  48. package/build/types/lib/xml/extract-xml-from-sheet.d.ts +2 -2
  49. package/build/types/lib/xml/index.d.ts +2 -1
  50. package/package.json +1 -5
  51. package/build/cjs/lib/xml/extract-xml-from-system-content.js +0 -53
  52. package/build/esm/lib/xml/extract-xml-from-system-content.js +0 -49
  53. package/build/types/lib/xml/extract-xml-from-system-content.d.ts +0 -15
@@ -56,6 +56,23 @@ class TemplateMemory {
56
56
  * @type {boolean}
57
57
  */
58
58
  #isProcessing = false;
59
+ /**
60
+ * The keys for the Excel files in the template.
61
+ */
62
+ #excelKeys = {
63
+ contentTypes: "[Content_Types].xml",
64
+ sharedStrings: "xl/sharedStrings.xml",
65
+ styles: "xl/styles.xml",
66
+ workbook: "xl/workbook.xml",
67
+ workbookRels: "xl/_rels/workbook.xml.rels",
68
+ };
69
+ /**
70
+ * Creates a Template instance from a map of file paths to buffers.
71
+ *
72
+ * @param {Object<string, Buffer>} files - The files to create the template from.
73
+ * @throws {Error} If reading or writing files fails.
74
+ * @experimental This API is experimental and might change in future versions.
75
+ */
59
76
  constructor(files) {
60
77
  this.files = files;
61
78
  }
@@ -115,31 +132,75 @@ class TemplateMemory {
115
132
  sheetXml: modifiedXml,
116
133
  });
117
134
  }
118
- #getXml(fileKey) {
135
+ /**
136
+ * Extracts the XML content from an Excel sheet file.
137
+ *
138
+ * @param {string} fileKey - The file key of the sheet to extract.
139
+ * @returns {string} The XML content of the sheet.
140
+ * @throws {Error} If the file key is not found.
141
+ * @experimental This API is experimental and might change in future versions.
142
+ */
143
+ async #extractXmlFromSheet(fileKey) {
119
144
  if (!this.files[fileKey]) {
120
145
  throw new Error(`${fileKey} not found`);
121
146
  }
122
147
  return Xml.extractXmlFromSheet(this.files[fileKey]);
123
148
  }
124
149
  /**
125
- * Get the path of the sheet with the given name inside the workbook.
126
- * @param sheetName The name of the sheet to find.
127
- * @returns The path of the sheet inside the workbook.
128
- * @throws {Error} If the sheet is not found.
150
+ * Extracts row data from an Excel sheet file.
151
+ *
152
+ * @param {string} fileKey - The file key of the sheet to extract.
153
+ * @returns {Object} An object containing:
154
+ * - rows: Array of raw XML strings for each <row> element
155
+ * - lastRowNumber: Highest row number found in the sheet (1-based)
156
+ * - mergeCells: Array of merged cell ranges (e.g., [{ref: "A1:B2"}])
157
+ * - xml: The XML content of the sheet
158
+ * @throws {Error} If the file key is not found
159
+ * @experimental This API is experimental and might change in future versions.
160
+ */
161
+ async #extractRowsFromSheet(fileKey) {
162
+ if (!this.files[fileKey]) {
163
+ throw new Error(`${fileKey} not found`);
164
+ }
165
+ return Xml.extractRowsFromSheet(this.files[fileKey]);
166
+ }
167
+ /**
168
+ * Returns the Excel path of the sheet with the given name.
169
+ *
170
+ * @param sheetName - The name of the sheet to find.
171
+ * @returns The Excel path of the sheet.
172
+ * @throws {Error} If the sheet with the given name does not exist.
173
+ * @experimental This API is experimental and might change in future versions.
129
174
  */
130
- async #getSheetPath(sheetName) {
131
- // Read XML workbook to find sheet name and path
132
- const workbookXml = this.#getXml("xl/workbook.xml");
133
- const sheetMatch = workbookXml.match(new RegExp(`<sheet[^>]+name="${sheetName}"[^>]+r:id="([^"]+)"[^>]*/>`));
134
- if (!sheetMatch)
175
+ async #getSheetPathByName(sheetName) {
176
+ // Find the sheet
177
+ const workbookXml = await this.#extractXmlFromSheet(this.#excelKeys.workbook);
178
+ const sheetMatch = workbookXml.match(Utils.sheetMatch(sheetName));
179
+ if (!sheetMatch || !sheetMatch[1]) {
135
180
  throw new Error(`Sheet "${sheetName}" not found`);
181
+ }
136
182
  const rId = sheetMatch[1];
137
- const relsXml = this.#getXml("xl/_rels/workbook.xml.rels");
138
- const relMatch = relsXml.match(new RegExp(`<Relationship[^>]+Id="${rId}"[^>]+Target="([^"]+)"[^>]*/>`));
139
- if (!relMatch)
183
+ const relsXml = await this.#extractXmlFromSheet(this.#excelKeys.workbookRels);
184
+ const relMatch = relsXml.match(Utils.relationshipMatch(rId));
185
+ if (!relMatch || !relMatch[1]) {
140
186
  throw new Error(`Relationship "${rId}" not found`);
187
+ }
141
188
  return "xl/" + relMatch[1].replace(/^\/?xl\//, "");
142
189
  }
190
+ /**
191
+ * Returns the Excel path of the sheet with the given ID.
192
+ *
193
+ * @param {number} id - The 1-based index of the sheet to find.
194
+ * @returns {string} The Excel path of the sheet.
195
+ * @throws {Error} If the sheet index is less than 1.
196
+ * @experimental This API is experimental and might change in future versions.
197
+ */
198
+ #getSheetPathById(id) {
199
+ if (id < 1) {
200
+ throw new Error("Sheet index must be greater than 0");
201
+ }
202
+ return `xl/worksheets/sheet${id}.xml`;
203
+ }
143
204
  /**
144
205
  * Replaces the contents of a file in the template.
145
206
  *
@@ -153,16 +214,30 @@ class TemplateMemory {
153
214
  async #set(key, content) {
154
215
  this.files[key] = content;
155
216
  }
217
+ /**
218
+ * Replaces placeholders in the given sheet with values from the replacements map.
219
+ *
220
+ * The function searches for placeholders in the format `${key}` within the sheet
221
+ * content, where `key` corresponds to a path in the replacements object.
222
+ * If a value is found for the key, it replaces the placeholder with the value.
223
+ * If no value is found, the original placeholder remains unchanged.
224
+ *
225
+ * @param sheetName - The name of the sheet to be replaced.
226
+ * @param replacements - An object where keys represent placeholder paths and values are the replacements.
227
+ * @returns A promise that resolves when the substitution is complete.
228
+ * @throws {Error} If the template instance has been destroyed.
229
+ * @experimental This API is experimental and might change in future versions.
230
+ */
156
231
  async #substitute(sheetName, replacements) {
157
- const sharedStringsPath = "xl/sharedStrings.xml";
158
- const sheetPath = await this.#getSheetPath(sheetName);
232
+ const sharedStringsPath = this.#excelKeys.sharedStrings;
159
233
  let sharedStringsContent = "";
160
234
  let sheetContent = "";
161
235
  if (this.files[sharedStringsPath]) {
162
- sharedStringsContent = this.#getXml(sharedStringsPath);
236
+ sharedStringsContent = await this.#extractXmlFromSheet(sharedStringsPath);
163
237
  }
238
+ const sheetPath = await this.#getSheetPathByName(sheetName);
164
239
  if (this.files[sheetPath]) {
165
- sheetContent = this.#getXml(sheetPath);
240
+ sheetContent = await this.#extractXmlFromSheet(sheetPath);
166
241
  const TABLE_REGEX = /\$\{table:([a-zA-Z0-9_]+)\.([a-zA-Z0-9_]+)\}/g;
167
242
  const hasTablePlaceholders = TABLE_REGEX.test(sharedStringsContent) || TABLE_REGEX.test(sheetContent);
168
243
  if (hasTablePlaceholders) {
@@ -180,6 +255,108 @@ class TemplateMemory {
180
255
  await this.#set(sheetPath, Buffer.from(sheetContent));
181
256
  }
182
257
  }
258
+ /**
259
+ * Merges rows from other sheets into a base sheet.
260
+ *
261
+ * @param {Object} data
262
+ * @param {Object} data.additions
263
+ * @param {number[]} [data.additions.sheetIndexes] - The 1-based indexes of the sheets to extract rows from.
264
+ * @param {string[]} [data.additions.sheetNames] - The names of the sheets to extract rows from.
265
+ * @param {number} [data.baseSheetIndex=1] - The 1-based index of the sheet in the workbook to add rows to.
266
+ * @param {string} [data.baseSheetName] - The name of the sheet in the workbook to add rows to.
267
+ * @param {number} [data.gap=0] - The number of empty rows to insert between each added section.
268
+ * @throws {Error} If the base sheet index is less than 1.
269
+ * @throws {Error} If the base sheet name is not found.
270
+ * @throws {Error} If the sheet index is less than 1.
271
+ * @throws {Error} If the sheet name is not found.
272
+ * @throws {Error} If no sheets are found to merge.
273
+ * @experimental This API is experimental and might change in future versions.
274
+ */
275
+ async #mergeSheets(data) {
276
+ const { additions, baseSheetIndex = 1, baseSheetName, gap = 0, } = data;
277
+ let fileKey = "";
278
+ if (baseSheetName) {
279
+ fileKey = await this.#getSheetPathByName(baseSheetName);
280
+ }
281
+ if (baseSheetIndex && !fileKey) {
282
+ if (baseSheetIndex < 1) {
283
+ throw new Error("Base sheet index must be greater than 0");
284
+ }
285
+ fileKey = `xl/worksheets/sheet${baseSheetIndex}.xml`;
286
+ }
287
+ if (!fileKey) {
288
+ throw new Error("Base sheet not found");
289
+ }
290
+ const { lastRowNumber, mergeCells: baseMergeCells, rows: baseRows, xml, } = await this.#extractRowsFromSheet(fileKey);
291
+ const allRows = [...baseRows];
292
+ const allMergeCells = [...baseMergeCells];
293
+ let currentRowOffset = lastRowNumber + gap;
294
+ const sheetPaths = [];
295
+ if (additions.sheetIndexes) {
296
+ sheetPaths.push(...(await Promise.all(additions.sheetIndexes.map(e => this.#getSheetPathById(e)))));
297
+ }
298
+ if (additions.sheetNames) {
299
+ sheetPaths.push(...(await Promise.all(additions.sheetNames.map(e => this.#getSheetPathByName(e)))));
300
+ }
301
+ if (sheetPaths.length === 0) {
302
+ throw new Error("No sheets found to merge");
303
+ }
304
+ for (const sheetPath of sheetPaths) {
305
+ if (!this.files[sheetPath]) {
306
+ throw new Error(`Sheet "${sheetPath}" not found`);
307
+ }
308
+ const { mergeCells, rows } = await Xml.extractRowsFromSheet(this.files[sheetPath]);
309
+ const shiftedRows = Xml.shiftRowIndices(rows, currentRowOffset);
310
+ const shiftedMergeCells = mergeCells.map(cell => {
311
+ const [start, end] = cell.ref.split(":");
312
+ if (!start || !end) {
313
+ return cell;
314
+ }
315
+ const shiftedStart = Utils.Common.shiftCellRef(start, currentRowOffset);
316
+ const shiftedEnd = Utils.Common.shiftCellRef(end, currentRowOffset);
317
+ return { ...cell, ref: `${shiftedStart}:${shiftedEnd}` };
318
+ });
319
+ allRows.push(...shiftedRows);
320
+ allMergeCells.push(...shiftedMergeCells);
321
+ currentRowOffset += Utils.Common.getMaxRowNumber(rows) + gap;
322
+ }
323
+ const mergedXml = Xml.buildMergedSheet(xml, allRows, allMergeCells);
324
+ this.#set(fileKey, mergedXml);
325
+ }
326
+ /**
327
+ * Removes sheets from the workbook.
328
+ *
329
+ * @param {Object} data - The data for sheet removal.
330
+ * @param {number[]} [data.sheetIndexes] - The 1-based indexes of the sheets to remove.
331
+ * @param {string[]} [data.sheetNames] - The names of the sheets to remove.
332
+ * @returns {void}
333
+ *
334
+ * @throws {Error} If the template instance has been destroyed.
335
+ * @throws {Error} If the sheet does not exist.
336
+ * @experimental This API is experimental and might change in future versions.
337
+ */
338
+ #removeSheets(data) {
339
+ const { sheetIndexes = [], sheetNames = [] } = data;
340
+ for (const sheetIndex of sheetIndexes) {
341
+ const sheetPath = `xl/worksheets/sheet${sheetIndex}.xml`;
342
+ if (!this.files[sheetPath]) {
343
+ continue;
344
+ }
345
+ delete this.files[sheetPath];
346
+ if (this.files["xl/workbook.xml"]) {
347
+ this.files["xl/workbook.xml"] = Buffer.from(Utils.Common.removeSheetFromWorkbook(this.files["xl/workbook.xml"].toString(), sheetIndex));
348
+ }
349
+ if (this.files["xl/_rels/workbook.xml.rels"]) {
350
+ this.files["xl/_rels/workbook.xml.rels"] = Buffer.from(Utils.Common.removeSheetFromRels(this.files["xl/_rels/workbook.xml.rels"].toString(), sheetIndex));
351
+ }
352
+ if (this.files["[Content_Types].xml"]) {
353
+ this.files["[Content_Types].xml"] = Buffer.from(Utils.Common.removeSheetFromContentTypes(this.files["[Content_Types].xml"].toString(), sheetIndex));
354
+ }
355
+ }
356
+ for (const sheetName of sheetNames) {
357
+ Utils.Common.removeSheetByName(this.files, sheetName);
358
+ }
359
+ }
183
360
  /**
184
361
  * Copies a sheet from the template to a new name.
185
362
  *
@@ -195,30 +372,31 @@ class TemplateMemory {
195
372
  this.#ensureNotDestroyed();
196
373
  this.#isProcessing = true;
197
374
  try {
375
+ if (sourceName === newName) {
376
+ throw new Error("Source and new sheet names cannot be the same");
377
+ }
198
378
  // Read workbook.xml and find the source sheet
199
- const workbookXmlPath = "xl/workbook.xml";
200
- const workbookXml = this.#getXml(workbookXmlPath);
379
+ const workbookXmlPath = this.#excelKeys.workbook;
380
+ const workbookXml = await this.#extractXmlFromSheet(this.#excelKeys.workbook);
201
381
  // Find the source sheet
202
- const sheetRegex = new RegExp(`<sheet[^>]+name="${sourceName}"[^>]+r:id="([^"]+)"[^>]*/>`);
203
- const sheetMatch = workbookXml.match(sheetRegex);
204
- if (!sheetMatch)
382
+ const sheetMatch = workbookXml.match(Utils.sheetMatch(sourceName));
383
+ if (!sheetMatch || !sheetMatch[1]) {
205
384
  throw new Error(`Sheet "${sourceName}" not found`);
206
- const sourceRId = sheetMatch[1];
385
+ }
207
386
  // Check if a sheet with the new name already exists
208
387
  if (new RegExp(`<sheet[^>]+name="${newName}"`).test(workbookXml)) {
209
388
  throw new Error(`Sheet "${newName}" already exists`);
210
389
  }
211
390
  // Read workbook.rels
212
391
  // Find the source sheet path by rId
213
- const relsXmlPath = "xl/_rels/workbook.xml.rels";
214
- const relsXml = this.#getXml(relsXmlPath);
215
- const relRegex = new RegExp(`<Relationship[^>]+Id="${sourceRId}"[^>]+Target="([^"]+)"[^>]*/>`);
216
- const relMatch = relsXml.match(relRegex);
217
- if (!relMatch)
218
- throw new Error(`Relationship "${sourceRId}" not found`);
392
+ const rId = sheetMatch[1];
393
+ const relsXmlPath = this.#excelKeys.workbookRels;
394
+ const relsXml = await this.#extractXmlFromSheet(this.#excelKeys.workbookRels);
395
+ const relMatch = relsXml.match(Utils.relationshipMatch(rId));
396
+ if (!relMatch || !relMatch[1]) {
397
+ throw new Error(`Relationship "${rId}" not found`);
398
+ }
219
399
  const sourceTarget = relMatch[1]; // sheetN.xml
220
- if (!sourceTarget)
221
- throw new Error(`Relationship "${sourceRId}" not found`);
222
400
  const sourceSheetPath = "xl/" + sourceTarget.replace(/^\/?.*xl\//, "");
223
401
  // Get the index of the new sheet
224
402
  const sheetNumbers = Array.from(Object.keys(this.files))
@@ -255,7 +433,7 @@ class TemplateMemory {
255
433
  // Read [Content_Types].xml
256
434
  // Update [Content_Types].xml
257
435
  const contentTypesPath = "[Content_Types].xml";
258
- const contentTypesXml = this.#getXml(contentTypesPath);
436
+ const contentTypesXml = await this.#extractXmlFromSheet(contentTypesPath);
259
437
  const overrideTag = `<Override PartName="/xl/worksheets/${newSheetFilename}" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>`;
260
438
  const updatedContentTypesXml = contentTypesXml.replace("</Types>", overrideTag + "</Types>");
261
439
  await this.#set(contentTypesPath, Buffer.from(updatedContentTypesXml));
@@ -312,19 +490,8 @@ class TemplateMemory {
312
490
  Utils.checkStartRow(startRowNumber);
313
491
  Utils.checkRows(preparedRows);
314
492
  // Find the sheet
315
- const workbookXml = this.#getXml("xl/workbook.xml");
316
- const sheetMatch = workbookXml.match(new RegExp(`<sheet[^>]+name="${sheetName}"[^>]+r:id="([^"]+)"[^>]*/>`));
317
- if (!sheetMatch || !sheetMatch[1]) {
318
- throw new Error(`Sheet "${sheetName}" not found`);
319
- }
320
- const rId = sheetMatch[1];
321
- const relsXml = this.#getXml("xl/_rels/workbook.xml.rels");
322
- const relMatch = relsXml.match(new RegExp(`<Relationship[^>]+Id="${rId}"[^>]+Target="([^"]+)"[^>]*/>`));
323
- if (!relMatch || !relMatch[1]) {
324
- throw new Error(`Relationship "${rId}" not found`);
325
- }
326
- const sheetPath = "xl/" + relMatch[1].replace(/^\/?xl\//, "");
327
- const sheetXml = this.#getXml(sheetPath);
493
+ const sheetPath = await this.#getSheetPathByName(sheetName);
494
+ const sheetXml = await this.#extractXmlFromSheet(sheetPath);
328
495
  let nextRow = 0;
329
496
  if (!startRowNumber) {
330
497
  // Find the last row
@@ -364,6 +531,20 @@ class TemplateMemory {
364
531
  this.#isProcessing = false;
365
532
  }
366
533
  }
534
+ /**
535
+ * Inserts rows into a specific sheet in the template using an async stream.
536
+ *
537
+ * @param {Object} data - The data for row insertion.
538
+ * @param {string} data.sheetName - The name of the sheet to insert rows into.
539
+ * @param {number} [data.startRowNumber] - The row number to start inserting from.
540
+ * @param {AsyncIterable<unknown[]>} data.rows - Async iterable of rows to insert.
541
+ * @returns {Promise<void>}
542
+ * @throws {Error} If the template instance has been destroyed.
543
+ * @throws {Error} If the sheet does not exist.
544
+ * @throws {Error} If the row number is out of range.
545
+ * @throws {Error} If a column is out of range.
546
+ * @experimental This API is experimental and might change in future versions.
547
+ */
367
548
  async insertRowsStream(data) {
368
549
  this.#ensureNotProcessing();
369
550
  this.#ensureNotDestroyed();
@@ -372,17 +553,9 @@ class TemplateMemory {
372
553
  const { rows, sheetName, startRowNumber } = data;
373
554
  if (!sheetName)
374
555
  throw new Error("Sheet name is required");
375
- const workbookXml = this.#getXml("xl/workbook.xml");
376
- const sheetMatch = workbookXml.match(new RegExp(`<sheet[^>]+name="${sheetName}"[^>]+r:id="([^"]+)"[^>]*/>`));
377
- if (!sheetMatch)
378
- throw new Error(`Sheet "${sheetName}" not found`);
379
- const rId = sheetMatch[1];
380
- const relsXml = this.#getXml("xl/_rels/workbook.xml.rels");
381
- const relMatch = relsXml.match(new RegExp(`<Relationship[^>]+Id="${rId}"[^>]+Target="([^"]+)"[^>]*/>`));
382
- if (!relMatch || !relMatch[1])
383
- throw new Error(`Relationship "${rId}" not found`);
384
- const sheetPath = "xl/" + relMatch[1].replace(/^\/?xl\//, "");
385
- const sheetXml = this.#getXml(sheetPath);
556
+ // Read XML workbook to find sheet name and path
557
+ const sheetPath = await this.#getSheetPathByName(sheetName);
558
+ const sheetXml = await this.#extractXmlFromSheet(sheetPath);
386
559
  const output = new memory_write_stream_js_1.MemoryWriteStream();
387
560
  let inserted = false;
388
561
  // --- Case 1: <sheetData>...</sheetData> on one line ---
@@ -465,7 +638,7 @@ class TemplateMemory {
465
638
  }
466
639
  if (!inserted)
467
640
  throw new Error("Failed to locate <sheetData> for insertion");
468
- // теперь мы не собираем строку, а собираем Buffer
641
+ // Save the buffer to the sheet
469
642
  this.files[sheetPath] = output.toBuffer();
470
643
  }
471
644
  finally {
@@ -486,10 +659,10 @@ class TemplateMemory {
486
659
  try {
487
660
  const zipBuffer = await Zip.create(this.files);
488
661
  this.destroyed = true;
489
- // Очистка всех буферов
662
+ // Clear all buffers
490
663
  for (const key in this.files) {
491
664
  if (this.files.hasOwnProperty(key)) {
492
- this.files[key] = Buffer.alloc(0); // Заменяем на пустой буфер
665
+ this.files[key] = Buffer.alloc(0); // Clear the buffer
493
666
  }
494
667
  }
495
668
  return zipBuffer;
@@ -519,6 +692,55 @@ class TemplateMemory {
519
692
  this.#isProcessing = false;
520
693
  }
521
694
  }
695
+ /**
696
+ * Merges sheets into a base sheet.
697
+ *
698
+ * @param {Object} data
699
+ * @param {{ sheetIndexes?: number[]; sheetNames?: string[] }} data.additions - The sheets to merge.
700
+ * @param {number} [data.baseSheetIndex=1] - The 1-based index of the sheet to merge into.
701
+ * @param {string} [data.baseSheetName] - The name of the sheet to merge into.
702
+ * @param {number} [data.gap=0] - The number of empty rows to insert between each added section.
703
+ * @returns {void}
704
+ */
705
+ mergeSheets(data) {
706
+ this.#ensureNotProcessing();
707
+ this.#ensureNotDestroyed();
708
+ this.#isProcessing = true;
709
+ try {
710
+ this.#mergeSheets(data);
711
+ }
712
+ finally {
713
+ this.#isProcessing = false;
714
+ }
715
+ }
716
+ /**
717
+ * Removes sheets from the workbook.
718
+ *
719
+ * @param {Object} data
720
+ * @param {number[]} [data.sheetIndexes] - The 1-based indexes of the sheets to remove.
721
+ * @param {string[]} [data.sheetNames] - The names of the sheets to remove.
722
+ * @returns {void}
723
+ */
724
+ removeSheets(data) {
725
+ this.#ensureNotProcessing();
726
+ this.#ensureNotDestroyed();
727
+ this.#isProcessing = true;
728
+ try {
729
+ this.#removeSheets(data);
730
+ }
731
+ finally {
732
+ this.#isProcessing = false;
733
+ }
734
+ }
735
+ /**
736
+ * Creates a Template instance from an Excel file source.
737
+ *
738
+ * @param {Object} data - The data to create the template from.
739
+ * @param {string | Buffer} data.source - The path or buffer of the Excel file.
740
+ * @returns {Promise<TemplateMemory>} A new Template instance.
741
+ * @throws {Error} If reading the file fails.
742
+ * @experimental This API is experimental and might change in future versions.
743
+ */
522
744
  static async from(data) {
523
745
  const { source } = data;
524
746
  const buffer = typeof source === "string"
@@ -10,10 +10,34 @@ var __createBinding = (this && this.__createBinding) || (Object.create ? (functi
10
10
  if (k2 === undefined) k2 = k;
11
11
  o[k2] = m[k];
12
12
  }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
13
35
  var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
36
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
37
  };
16
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.Common = void 0;
40
+ exports.Common = __importStar(require("../../utils/index.js"));
17
41
  __exportStar(require("./apply-replacements.js"), exports);
18
42
  __exportStar(require("./check-row.js"), exports);
19
43
  __exportStar(require("./check-rows.js"), exports);
@@ -30,6 +54,7 @@ __exportStar(require("./process-merge-cells.js"), exports);
30
54
  __exportStar(require("./process-merge-finalize.js"), exports);
31
55
  __exportStar(require("./process-rows.js"), exports);
32
56
  __exportStar(require("./process-shared-strings.js"), exports);
57
+ __exportStar(require("./regexp.js"), exports);
33
58
  __exportStar(require("./to-excel-column-object.js"), exports);
34
59
  __exportStar(require("./update-dimension.js"), exports);
35
60
  __exportStar(require("./validate-worksheet-xml.js"), exports);
@@ -8,6 +8,10 @@ function prepareRowToCells(row, rowNumber) {
8
8
  const colLetter = (0, column_index_to_letter_js_1.columnIndexToLetter)(colIndex);
9
9
  const cellRef = `${colLetter}${rowNumber}`;
10
10
  const cellValue = (0, escape_xml_js_1.escapeXml)(String(value ?? ""));
11
- return `<c r="${cellRef}" t="inlineStr"><is><t>${cellValue}</t></is></c>`;
11
+ return {
12
+ cellRef,
13
+ cellValue,
14
+ cellXml: `<c r="${cellRef}" t="inlineStr"><is><t>${cellValue}</t></is></c>`,
15
+ };
12
16
  });
13
17
  }
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.relationshipMatch = relationshipMatch;
4
+ exports.sheetMatch = sheetMatch;
5
+ /**
6
+ * Creates a regular expression to match a relationship element with a specific ID.
7
+ *
8
+ * @param {string} id - The relationship ID to match (e.g. "rId1")
9
+ * @returns {RegExp} A regular expression that matches a Relationship XML element with the given ID and captures the Target attribute value
10
+ * @example
11
+ * const regex = relationshipMatch("rId1");
12
+ * const xml = '<Relationship Id="rId1" Target="worksheets/sheet1.xml"/>';
13
+ * const match = xml.match(regex);
14
+ * // match[1] === "worksheets/sheet1.xml"
15
+ */
16
+ function relationshipMatch(id) {
17
+ return new RegExp(`<Relationship[^>]+Id="${id}"[^>]+Target="([^"]+)"[^>]*/>`);
18
+ }
19
+ /**
20
+ * Creates a regular expression to match a sheet element with a specific name.
21
+ *
22
+ * @param {string} sheetName - The name of the sheet to match
23
+ * @returns {RegExp} A regular expression that matches a sheet XML element with the given name and captures the r:id attribute value
24
+ * @example
25
+ * const regex = sheetMatch("Sheet1");
26
+ * const xml = '<sheet name="Sheet1" sheetId="1" r:id="rId1"/>';
27
+ * const match = xml.match(regex);
28
+ * // match[1] === "rId1"
29
+ */
30
+ function sheetMatch(sheetName) {
31
+ return new RegExp(`<sheet[^>]+name="${sheetName}"[^>]+r:id="([^"]+)"[^>]*/>`);
32
+ }
@@ -1,6 +1,21 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.updateDimension = updateDimension;
4
+ /**
5
+ * Updates the dimension element in an Excel worksheet XML string based on the actual cell references.
6
+ *
7
+ * This function scans the XML for all cell references and calculates the minimum and maximum
8
+ * column/row values to determine the actual used range in the worksheet. It then updates
9
+ * the dimension element to reflect this range.
10
+ *
11
+ * @param {string} xml - The worksheet XML string to process
12
+ * @returns {string} The XML string with updated dimension element
13
+ * @example
14
+ * // XML with cells from A1 to C3
15
+ * const xml = '....<dimension ref="A1:B2"/>.....<c r="C3">...</c>...';
16
+ * const updated = updateDimension(xml);
17
+ * // Returns XML with dimension updated to ref="A1:C3"
18
+ */
4
19
  function updateDimension(xml) {
5
20
  const cellRefs = [...xml.matchAll(/<c r="([A-Z]+)(\d+)"/g)];
6
21
  if (cellRefs.length === 0)