@js-ak/excel-toolbox 1.6.0 → 1.8.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 (41) hide show
  1. package/build/cjs/lib/merge-sheets-to-base-file-process-sync.js +105 -0
  2. package/build/cjs/lib/merge-sheets-to-base-file-process.js +3 -3
  3. package/build/cjs/lib/merge-sheets-to-base-file-sync.js +2 -2
  4. package/build/cjs/lib/merge-sheets-to-base-file.js +1 -1
  5. package/build/cjs/lib/template/template-fs.js +97 -18
  6. package/build/cjs/lib/template/template-memory.js +110 -43
  7. package/build/cjs/lib/template/utils/compare-columns.js +16 -0
  8. package/build/cjs/lib/template/utils/index.js +1 -0
  9. package/build/cjs/lib/xml/extract-rows-from-sheet-sync.js +67 -0
  10. package/build/cjs/lib/xml/extract-rows-from-sheet.js +4 -2
  11. package/build/cjs/lib/xml/extract-xml-from-sheet-sync.js +43 -0
  12. package/build/cjs/lib/xml/extract-xml-from-sheet.js +15 -15
  13. package/build/cjs/lib/xml/index.js +2 -1
  14. package/build/esm/lib/merge-sheets-to-base-file-process-sync.js +69 -0
  15. package/build/esm/lib/merge-sheets-to-base-file-process.js +3 -3
  16. package/build/esm/lib/merge-sheets-to-base-file-sync.js +2 -2
  17. package/build/esm/lib/merge-sheets-to-base-file.js +1 -1
  18. package/build/esm/lib/template/template-fs.js +97 -18
  19. package/build/esm/lib/template/template-memory.js +110 -43
  20. package/build/esm/lib/template/utils/compare-columns.js +13 -0
  21. package/build/esm/lib/template/utils/index.js +1 -0
  22. package/build/esm/lib/xml/extract-rows-from-sheet-sync.js +64 -0
  23. package/build/esm/lib/xml/extract-rows-from-sheet.js +4 -2
  24. package/build/esm/lib/xml/extract-xml-from-sheet-sync.js +40 -0
  25. package/build/esm/lib/xml/extract-xml-from-sheet.js +12 -15
  26. package/build/esm/lib/xml/index.js +2 -1
  27. package/build/types/lib/merge-sheets-to-base-file-process-sync.d.ts +27 -0
  28. package/build/types/lib/merge-sheets-to-base-file-process.d.ts +1 -1
  29. package/build/types/lib/template/template-fs.d.ts +14 -0
  30. package/build/types/lib/template/template-memory.d.ts +4 -2
  31. package/build/types/lib/template/utils/compare-columns.d.ts +8 -0
  32. package/build/types/lib/template/utils/index.d.ts +1 -0
  33. package/build/types/lib/xml/extract-rows-from-sheet-sync.d.ts +28 -0
  34. package/build/types/lib/xml/extract-rows-from-sheet.d.ts +2 -2
  35. package/build/types/lib/xml/extract-xml-from-sheet-sync.d.ts +14 -0
  36. package/build/types/lib/xml/extract-xml-from-sheet.d.ts +2 -2
  37. package/build/types/lib/xml/index.d.ts +2 -1
  38. package/package.json +1 -5
  39. package/build/cjs/lib/xml/extract-xml-from-system-content.js +0 -53
  40. package/build/esm/lib/xml/extract-xml-from-system-content.js +0 -49
  41. package/build/types/lib/xml/extract-xml-from-system-content.d.ts +0 -15
@@ -16,13 +16,13 @@ import * as Xml from "./xml/index.js";
16
16
  *
17
17
  * The function returns a dictionary of file paths to their corresponding XML content.
18
18
  */
19
- export function mergeSheetsToBaseFileProcess(data) {
19
+ export async function mergeSheetsToBaseFileProcess(data) {
20
20
  const { additions, baseFiles, baseSheetIndex, gap, sheetNamesToRemove, sheetsToRemove, } = data;
21
21
  const basePath = `xl/worksheets/sheet${baseSheetIndex}.xml`;
22
22
  if (!baseFiles[basePath]) {
23
23
  throw new Error(`Base file does not contain ${basePath}`);
24
24
  }
25
- const { lastRowNumber, mergeCells: baseMergeCells, rows: baseRows, xml, } = Xml.extractRowsFromSheet(baseFiles[basePath]);
25
+ const { lastRowNumber, mergeCells: baseMergeCells, rows: baseRows, xml, } = await Xml.extractRowsFromSheet(baseFiles[basePath]);
26
26
  const allRows = [...baseRows];
27
27
  const allMergeCells = [...baseMergeCells];
28
28
  let currentRowOffset = lastRowNumber + gap;
@@ -32,7 +32,7 @@ export function mergeSheetsToBaseFileProcess(data) {
32
32
  if (!files[sheetPath]) {
33
33
  throw new Error(`File does not contain ${sheetPath}`);
34
34
  }
35
- const { mergeCells, rows } = Xml.extractRowsFromSheet(files[sheetPath]);
35
+ const { mergeCells, rows } = await Xml.extractRowsFromSheet(files[sheetPath]);
36
36
  const shiftedRows = Xml.shiftRowIndices(rows, currentRowOffset);
37
37
  const shiftedMergeCells = mergeCells.map(cell => {
38
38
  const [start, end] = cell.ref.split(":");
@@ -1,6 +1,6 @@
1
1
  import * as Utils from "./utils/index.js";
2
2
  import * as Zip from "./zip/index.js";
3
- import { mergeSheetsToBaseFileProcess } from "./merge-sheets-to-base-file-process.js";
3
+ import { mergeSheetsToBaseFileProcessSync } from "./merge-sheets-to-base-file-process-sync.js";
4
4
  /**
5
5
  * Merge rows from other Excel files into a base Excel file.
6
6
  * The output is a new Excel file with the merged content.
@@ -29,7 +29,7 @@ export function mergeSheetsToBaseFileSync(data) {
29
29
  sheetIndexes,
30
30
  });
31
31
  }
32
- mergeSheetsToBaseFileProcess({
32
+ mergeSheetsToBaseFileProcessSync({
33
33
  additions: additionsUpdated,
34
34
  baseFiles,
35
35
  baseSheetIndex,
@@ -29,7 +29,7 @@ export async function mergeSheetsToBaseFile(data) {
29
29
  sheetIndexes,
30
30
  });
31
31
  }
32
- mergeSheetsToBaseFileProcess({
32
+ await mergeSheetsToBaseFileProcess({
33
33
  additions: additionsUpdated,
34
34
  baseFiles,
35
35
  baseSheetIndex,
@@ -53,6 +53,7 @@ export class TemplateFs {
53
53
  this.fileKeys = fileKeys;
54
54
  this.destination = destination;
55
55
  }
56
+ /** Private methods */
56
57
  /**
57
58
  * Removes the temporary directory created by this Template instance.
58
59
  * @private
@@ -126,13 +127,13 @@ export class TemplateFs {
126
127
  */
127
128
  async #getSheetPathByName(sheetName) {
128
129
  // Read XML workbook to find sheet name and path
129
- const workbookXml = Xml.extractXmlFromSheet(await this.#readFile(this.#excelKeys.workbook));
130
+ const workbookXml = await Xml.extractXmlFromSheet(await this.#readFile(this.#excelKeys.workbook));
130
131
  const sheetMatch = workbookXml.match(Utils.sheetMatch(sheetName));
131
132
  if (!sheetMatch || !sheetMatch[1]) {
132
133
  throw new Error(`Sheet "${sheetName}" not found`);
133
134
  }
134
135
  const rId = sheetMatch[1];
135
- const relsXml = Xml.extractXmlFromSheet(await this.#readFile(this.#excelKeys.workbookRels));
136
+ const relsXml = await Xml.extractXmlFromSheet(await this.#readFile(this.#excelKeys.workbookRels));
136
137
  const relMatch = relsXml.match(Utils.relationshipMatch(rId));
137
138
  if (!relMatch || !relMatch[1]) {
138
139
  throw new Error(`Relationship "${rId}" not found`);
@@ -185,16 +186,30 @@ export class TemplateFs {
185
186
  const fullPath = path.join(this.destination, ...key.split("/"));
186
187
  await fs.writeFile(fullPath, Buffer.isBuffer(content) ? content : Buffer.from(content));
187
188
  }
189
+ /**
190
+ * Replaces placeholders in the given sheet with values from the replacements map.
191
+ *
192
+ * The function searches for placeholders in the format `${key}` within the sheet
193
+ * content, where `key` corresponds to a path in the replacements object.
194
+ * If a value is found for the key, it replaces the placeholder with the value.
195
+ * If no value is found, the original placeholder remains unchanged.
196
+ *
197
+ * @param sheetName - The name of the sheet to be replaced.
198
+ * @param replacements - An object where keys represent placeholder paths and values are the replacements.
199
+ * @returns A promise that resolves when the substitution is complete.
200
+ * @throws {Error} If the template instance has been destroyed.
201
+ * @experimental This API is experimental and might change in future versions.
202
+ */
188
203
  async #substitute(sheetName, replacements) {
189
204
  const sharedStringsPath = this.#excelKeys.sharedStrings;
190
205
  const sheetPath = await this.#getSheetPathByName(sheetName);
191
206
  let sharedStringsContent = "";
192
207
  let sheetContent = "";
193
208
  if (this.fileKeys.has(sharedStringsPath)) {
194
- sharedStringsContent = Xml.extractXmlFromSheet(await this.#readFile(sharedStringsPath));
209
+ sharedStringsContent = await Xml.extractXmlFromSheet(await this.#readFile(sharedStringsPath));
195
210
  }
196
211
  if (this.fileKeys.has(sheetPath)) {
197
- sheetContent = Xml.extractXmlFromSheet(await this.#readFile(sheetPath));
212
+ sheetContent = await Xml.extractXmlFromSheet(await this.#readFile(sheetPath));
198
213
  const TABLE_REGEX = /\$\{table:([a-zA-Z0-9_]+)\.([a-zA-Z0-9_]+)\}/g;
199
214
  const hasTablePlaceholders = TABLE_REGEX.test(sharedStringsContent) || TABLE_REGEX.test(sheetContent);
200
215
  if (hasTablePlaceholders) {
@@ -212,6 +227,54 @@ export class TemplateFs {
212
227
  await this.#set(sheetPath, sheetContent);
213
228
  }
214
229
  }
230
+ /**
231
+ * Removes sheets from the workbook.
232
+ *
233
+ * @param {Object} data - The data for sheet removal.
234
+ * @param {number[]} [data.sheetIndexes] - The 1-based indexes of the sheets to remove.
235
+ * @param {string[]} [data.sheetNames] - The names of the sheets to remove.
236
+ * @returns {void}
237
+ *
238
+ * @throws {Error} If the template instance has been destroyed.
239
+ * @throws {Error} If the sheet does not exist.
240
+ * @experimental This API is experimental and might change in future versions.
241
+ */
242
+ async #removeSheets(data) {
243
+ const { sheetIndexes = [], sheetNames = [] } = data;
244
+ // first get index of sheets to remove
245
+ const sheetIndexesToRemove = new Set(sheetIndexes);
246
+ for (const sheetName of sheetNames) {
247
+ const sheetPath = await this.#getSheetPathByName(sheetName);
248
+ const sheetIndexMatch = sheetPath.match(/sheet(\d+)\.xml$/);
249
+ if (!sheetIndexMatch || !sheetIndexMatch[1]) {
250
+ throw new Error(`Sheet "${sheetName}" not found`);
251
+ }
252
+ const sheetIndex = parseInt(sheetIndexMatch[1], 10);
253
+ sheetIndexesToRemove.add(sheetIndex);
254
+ }
255
+ // Remove sheets by index
256
+ for (const sheetIndex of sheetIndexesToRemove.values()) {
257
+ const sheetPath = `xl/worksheets/sheet${sheetIndex}.xml`;
258
+ if (!this.fileKeys.has(sheetPath)) {
259
+ continue;
260
+ }
261
+ // remove sheet file
262
+ await fs.unlink(path.join(this.destination, ...sheetPath.split("/")));
263
+ this.fileKeys.delete(sheetPath);
264
+ // remove sheet from workbook
265
+ if (this.fileKeys.has(this.#excelKeys.workbook)) {
266
+ this.#set(this.#excelKeys.workbook, Buffer.from(Utils.Common.removeSheetFromWorkbook(this.#readFile(this.#excelKeys.workbook).toString(), sheetIndex)));
267
+ }
268
+ // remove sheet from workbook relations
269
+ if (this.fileKeys.has(this.#excelKeys.workbookRels)) {
270
+ this.#set(this.#excelKeys.workbookRels, Buffer.from(Utils.Common.removeSheetFromRels(this.#readFile(this.#excelKeys.workbookRels).toString(), sheetIndex)));
271
+ }
272
+ // remove sheet from content types
273
+ if (this.fileKeys.has(this.#excelKeys.contentTypes)) {
274
+ this.#set(this.#excelKeys.contentTypes, Buffer.from(Utils.Common.removeSheetFromContentTypes(this.#readFile(this.#excelKeys.contentTypes).toString(), sheetIndex)));
275
+ }
276
+ }
277
+ }
215
278
  /**
216
279
  * Validates the template by checking all required files exist.
217
280
  *
@@ -231,6 +294,7 @@ export class TemplateFs {
231
294
  }
232
295
  }
233
296
  }
297
+ /** Public methods */
234
298
  /**
235
299
  * Copies a sheet from the template to a new name.
236
300
  *
@@ -251,7 +315,7 @@ export class TemplateFs {
251
315
  }
252
316
  // Read workbook.xml and find the source sheet
253
317
  const workbookXmlPath = this.#excelKeys.workbook;
254
- const workbookXml = Xml.extractXmlFromSheet(await this.#readFile(workbookXmlPath));
318
+ const workbookXml = await Xml.extractXmlFromSheet(await this.#readFile(workbookXmlPath));
255
319
  // Find the source sheet
256
320
  const sheetMatch = workbookXml.match(Utils.sheetMatch(sourceName));
257
321
  if (!sheetMatch || !sheetMatch[1]) {
@@ -265,7 +329,7 @@ export class TemplateFs {
265
329
  // Find the source sheet path by rId
266
330
  const rId = sheetMatch[1];
267
331
  const relsXmlPath = this.#excelKeys.workbookRels;
268
- const relsXml = Xml.extractXmlFromSheet(await this.#readFile(relsXmlPath));
332
+ const relsXml = await Xml.extractXmlFromSheet(await this.#readFile(relsXmlPath));
269
333
  const relMatch = relsXml.match(Utils.relationshipMatch(rId));
270
334
  if (!relMatch || !relMatch[1]) {
271
335
  throw new Error(`Relationship "${rId}" not found`);
@@ -300,7 +364,7 @@ export class TemplateFs {
300
364
  // Read [Content_Types].xml
301
365
  // Update [Content_Types].xml
302
366
  const contentTypesPath = this.#excelKeys.contentTypes;
303
- const contentTypesXml = Xml.extractXmlFromSheet(await this.#readFile(contentTypesPath));
367
+ const contentTypesXml = await Xml.extractXmlFromSheet(await this.#readFile(contentTypesPath));
304
368
  const overrideTag = `<Override PartName="/xl/worksheets/${newSheetFilename}" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>`;
305
369
  const updatedContentTypesXml = contentTypesXml.replace("</Types>", overrideTag + "</Types>");
306
370
  await this.#set(contentTypesPath, updatedContentTypesXml);
@@ -321,12 +385,12 @@ export class TemplateFs {
321
385
  * @param replacements - An object where keys represent placeholder paths and values are the replacements.
322
386
  * @returns A promise that resolves when the substitution is complete.
323
387
  */
324
- substitute(sheetName, replacements) {
388
+ async substitute(sheetName, replacements) {
325
389
  this.#ensureNotProcessing();
326
390
  this.#ensureNotDestroyed();
327
391
  this.#isProcessing = true;
328
392
  try {
329
- return this.#substitute(sheetName, replacements);
393
+ await this.#substitute(sheetName, replacements);
330
394
  }
331
395
  finally {
332
396
  this.#isProcessing = false;
@@ -359,7 +423,7 @@ export class TemplateFs {
359
423
  // Find the sheet
360
424
  const sheetPath = await this.#getSheetPathByName(sheetName);
361
425
  const sheetXmlRaw = await this.#readFile(sheetPath);
362
- const sheetXml = Xml.extractXmlFromSheet(sheetXmlRaw);
426
+ const sheetXml = await Xml.extractXmlFromSheet(sheetXmlRaw);
363
427
  let nextRow = 0;
364
428
  if (!startRowNumber) {
365
429
  // Find the last row
@@ -488,7 +552,7 @@ export class TemplateFs {
488
552
  }
489
553
  }
490
554
  const { dimension: newDimension, rowNumber: actualRowNumber } = await Utils.writeRowsToStream(output, rows, maxRowNumber);
491
- if (compareColumns(newDimension.maxColumn, dimension.maxColumn) > 0) {
555
+ if (Utils.compareColumns(newDimension.maxColumn, dimension.maxColumn) > 0) {
492
556
  dimension.maxColumn = newDimension.maxColumn;
493
557
  }
494
558
  if (newDimension.maxRow > dimension.maxRow) {
@@ -535,7 +599,7 @@ export class TemplateFs {
535
599
  }
536
600
  // new <row>
537
601
  const { dimension: newDimension, rowNumber: actualRowNumber } = await Utils.writeRowsToStream(output, rows, maxRowNumber);
538
- if (compareColumns(newDimension.maxColumn, dimension.maxColumn) > 0) {
602
+ if (Utils.compareColumns(newDimension.maxColumn, dimension.maxColumn) > 0) {
539
603
  dimension.maxColumn = newDimension.maxColumn;
540
604
  }
541
605
  if (newDimension.maxRow > dimension.maxRow) {
@@ -568,7 +632,7 @@ export class TemplateFs {
568
632
  output.write("<sheetData>");
569
633
  // Prepare the rows
570
634
  const { dimension: newDimension } = await Utils.writeRowsToStream(output, rows, maxRowNumber);
571
- if (compareColumns(newDimension.maxColumn, dimension.maxColumn) > 0) {
635
+ if (Utils.compareColumns(newDimension.maxColumn, dimension.maxColumn) > 0) {
572
636
  dimension.maxColumn = newDimension.maxColumn;
573
637
  }
574
638
  if (newDimension.maxRow > dimension.maxRow) {
@@ -635,6 +699,25 @@ export class TemplateFs {
635
699
  this.#isProcessing = false;
636
700
  }
637
701
  }
702
+ /**
703
+ * Removes sheets from the workbook.
704
+ *
705
+ * @param {Object} data
706
+ * @param {number[]} [data.sheetIndexes] - The 1-based indexes of the sheets to remove.
707
+ * @param {string[]} [data.sheetNames] - The names of the sheets to remove.
708
+ * @returns {void}
709
+ */
710
+ async removeSheets(data) {
711
+ this.#ensureNotProcessing();
712
+ this.#ensureNotDestroyed();
713
+ this.#isProcessing = true;
714
+ try {
715
+ await this.#removeSheets(data);
716
+ }
717
+ finally {
718
+ this.#isProcessing = false;
719
+ }
720
+ }
638
721
  /**
639
722
  * Saves the modified Excel template to a buffer.
640
723
  *
@@ -723,6 +806,7 @@ export class TemplateFs {
723
806
  this.#isProcessing = false;
724
807
  }
725
808
  }
809
+ /** Static methods */
726
810
  /**
727
811
  * Creates a Template instance from an Excel file source.
728
812
  * Removes any existing files in the destination directory.
@@ -760,8 +844,3 @@ export class TemplateFs {
760
844
  return new TemplateFs(new Set(Object.keys(files)), destinationWithUuid);
761
845
  }
762
846
  }
763
- const compareColumns = (a, b) => {
764
- if (a === b)
765
- return 0;
766
- return a.length === b.length ? (a < b ? -1 : 1) : (a.length < b.length ? -1 : 1);
767
- };
@@ -40,6 +40,7 @@ export class TemplateMemory {
40
40
  constructor(files) {
41
41
  this.files = files;
42
42
  }
43
+ /** Private methods */
43
44
  /**
44
45
  * Ensures that this Template instance has not been destroyed.
45
46
  * @private
@@ -104,7 +105,7 @@ export class TemplateMemory {
104
105
  * @throws {Error} If the file key is not found.
105
106
  * @experimental This API is experimental and might change in future versions.
106
107
  */
107
- #extractXmlFromSheet(fileKey) {
108
+ async #extractXmlFromSheet(fileKey) {
108
109
  if (!this.files[fileKey]) {
109
110
  throw new Error(`${fileKey} not found`);
110
111
  }
@@ -122,7 +123,7 @@ export class TemplateMemory {
122
123
  * @throws {Error} If the file key is not found
123
124
  * @experimental This API is experimental and might change in future versions.
124
125
  */
125
- #extractRowsFromSheet(fileKey) {
126
+ async #extractRowsFromSheet(fileKey) {
126
127
  if (!this.files[fileKey]) {
127
128
  throw new Error(`${fileKey} not found`);
128
129
  }
@@ -136,15 +137,15 @@ export class TemplateMemory {
136
137
  * @throws {Error} If the sheet with the given name does not exist.
137
138
  * @experimental This API is experimental and might change in future versions.
138
139
  */
139
- #getSheetPathByName(sheetName) {
140
+ async #getSheetPathByName(sheetName) {
140
141
  // Find the sheet
141
- const workbookXml = this.#extractXmlFromSheet(this.#excelKeys.workbook);
142
+ const workbookXml = await this.#extractXmlFromSheet(this.#excelKeys.workbook);
142
143
  const sheetMatch = workbookXml.match(Utils.sheetMatch(sheetName));
143
144
  if (!sheetMatch || !sheetMatch[1]) {
144
145
  throw new Error(`Sheet "${sheetName}" not found`);
145
146
  }
146
147
  const rId = sheetMatch[1];
147
- const relsXml = this.#extractXmlFromSheet(this.#excelKeys.workbookRels);
148
+ const relsXml = await this.#extractXmlFromSheet(this.#excelKeys.workbookRels);
148
149
  const relMatch = relsXml.match(Utils.relationshipMatch(rId));
149
150
  if (!relMatch || !relMatch[1]) {
150
151
  throw new Error(`Relationship "${rId}" not found`);
@@ -197,11 +198,11 @@ export class TemplateMemory {
197
198
  let sharedStringsContent = "";
198
199
  let sheetContent = "";
199
200
  if (this.files[sharedStringsPath]) {
200
- sharedStringsContent = this.#extractXmlFromSheet(sharedStringsPath);
201
+ sharedStringsContent = await this.#extractXmlFromSheet(sharedStringsPath);
201
202
  }
202
- const sheetPath = this.#getSheetPathByName(sheetName);
203
+ const sheetPath = await this.#getSheetPathByName(sheetName);
203
204
  if (this.files[sheetPath]) {
204
- sheetContent = this.#extractXmlFromSheet(sheetPath);
205
+ sheetContent = await this.#extractXmlFromSheet(sheetPath);
205
206
  const TABLE_REGEX = /\$\{table:([a-zA-Z0-9_]+)\.([a-zA-Z0-9_]+)\}/g;
206
207
  const hasTablePlaceholders = TABLE_REGEX.test(sharedStringsContent) || TABLE_REGEX.test(sheetContent);
207
208
  if (hasTablePlaceholders) {
@@ -236,11 +237,11 @@ export class TemplateMemory {
236
237
  * @throws {Error} If no sheets are found to merge.
237
238
  * @experimental This API is experimental and might change in future versions.
238
239
  */
239
- #mergeSheets(data) {
240
+ async #mergeSheets(data) {
240
241
  const { additions, baseSheetIndex = 1, baseSheetName, gap = 0, } = data;
241
242
  let fileKey = "";
242
243
  if (baseSheetName) {
243
- fileKey = this.#getSheetPathByName(baseSheetName);
244
+ fileKey = await this.#getSheetPathByName(baseSheetName);
244
245
  }
245
246
  if (baseSheetIndex && !fileKey) {
246
247
  if (baseSheetIndex < 1) {
@@ -251,16 +252,16 @@ export class TemplateMemory {
251
252
  if (!fileKey) {
252
253
  throw new Error("Base sheet not found");
253
254
  }
254
- const { lastRowNumber, mergeCells: baseMergeCells, rows: baseRows, xml, } = this.#extractRowsFromSheet(fileKey);
255
+ const { lastRowNumber, mergeCells: baseMergeCells, rows: baseRows, xml, } = await this.#extractRowsFromSheet(fileKey);
255
256
  const allRows = [...baseRows];
256
257
  const allMergeCells = [...baseMergeCells];
257
258
  let currentRowOffset = lastRowNumber + gap;
258
259
  const sheetPaths = [];
259
260
  if (additions.sheetIndexes) {
260
- sheetPaths.push(...(additions.sheetIndexes).map(e => this.#getSheetPathById(e)));
261
+ sheetPaths.push(...(await Promise.all(additions.sheetIndexes.map(e => this.#getSheetPathById(e)))));
261
262
  }
262
263
  if (additions.sheetNames) {
263
- sheetPaths.push(...(additions.sheetNames).map(e => this.#getSheetPathByName(e)));
264
+ sheetPaths.push(...(await Promise.all(additions.sheetNames.map(e => this.#getSheetPathByName(e)))));
264
265
  }
265
266
  if (sheetPaths.length === 0) {
266
267
  throw new Error("No sheets found to merge");
@@ -269,7 +270,7 @@ export class TemplateMemory {
269
270
  if (!this.files[sheetPath]) {
270
271
  throw new Error(`Sheet "${sheetPath}" not found`);
271
272
  }
272
- const { mergeCells, rows } = Xml.extractRowsFromSheet(this.files[sheetPath]);
273
+ const { mergeCells, rows } = await Xml.extractRowsFromSheet(this.files[sheetPath]);
273
274
  const shiftedRows = Xml.shiftRowIndices(rows, currentRowOffset);
274
275
  const shiftedMergeCells = mergeCells.map(cell => {
275
276
  const [start, end] = cell.ref.split(":");
@@ -299,28 +300,45 @@ export class TemplateMemory {
299
300
  * @throws {Error} If the sheet does not exist.
300
301
  * @experimental This API is experimental and might change in future versions.
301
302
  */
302
- #removeSheets(data) {
303
+ async #removeSheets(data) {
303
304
  const { sheetIndexes = [], sheetNames = [] } = data;
304
- for (const sheetIndex of sheetIndexes) {
305
+ // first get index of sheets to remove
306
+ const sheetIndexesToRemove = new Set(sheetIndexes);
307
+ for (const sheetName of sheetNames) {
308
+ const sheetPath = await this.#getSheetPathByName(sheetName);
309
+ const sheetIndexMatch = sheetPath.match(/sheet(\d+)\.xml$/);
310
+ if (!sheetIndexMatch || !sheetIndexMatch[1]) {
311
+ throw new Error(`Sheet "${sheetName}" not found`);
312
+ }
313
+ const sheetIndex = parseInt(sheetIndexMatch[1], 10);
314
+ sheetIndexesToRemove.add(sheetIndex);
315
+ }
316
+ // Remove sheets by index
317
+ for (const sheetIndex of sheetIndexesToRemove.values()) {
305
318
  const sheetPath = `xl/worksheets/sheet${sheetIndex}.xml`;
306
319
  if (!this.files[sheetPath]) {
307
320
  continue;
308
321
  }
322
+ // remove sheet file
309
323
  delete this.files[sheetPath];
310
- if (this.files["xl/workbook.xml"]) {
311
- this.files["xl/workbook.xml"] = Buffer.from(Utils.Common.removeSheetFromWorkbook(this.files["xl/workbook.xml"].toString(), sheetIndex));
324
+ // remove sheet from workbook
325
+ const workbook = this.files[this.#excelKeys.workbook];
326
+ if (workbook) {
327
+ this.files[this.#excelKeys.workbook] = Buffer.from(Utils.Common.removeSheetFromWorkbook(workbook.toString(), sheetIndex));
312
328
  }
313
- if (this.files["xl/_rels/workbook.xml.rels"]) {
314
- this.files["xl/_rels/workbook.xml.rels"] = Buffer.from(Utils.Common.removeSheetFromRels(this.files["xl/_rels/workbook.xml.rels"].toString(), sheetIndex));
329
+ // remove sheet from workbook relations
330
+ const workbookRels = this.files[this.#excelKeys.workbookRels];
331
+ if (workbookRels) {
332
+ this.files[this.#excelKeys.workbookRels] = Buffer.from(Utils.Common.removeSheetFromRels(workbookRels.toString(), sheetIndex));
315
333
  }
316
- if (this.files["[Content_Types].xml"]) {
317
- this.files["[Content_Types].xml"] = Buffer.from(Utils.Common.removeSheetFromContentTypes(this.files["[Content_Types].xml"].toString(), sheetIndex));
334
+ // remove sheet from content types
335
+ const contentTypes = this.files[this.#excelKeys.contentTypes];
336
+ if (contentTypes) {
337
+ this.files[this.#excelKeys.contentTypes] = Buffer.from(Utils.Common.removeSheetFromContentTypes(contentTypes.toString(), sheetIndex));
318
338
  }
319
339
  }
320
- for (const sheetName of sheetNames) {
321
- Utils.Common.removeSheetByName(this.files, sheetName);
322
- }
323
340
  }
341
+ /** Public methods */
324
342
  /**
325
343
  * Copies a sheet from the template to a new name.
326
344
  *
@@ -341,7 +359,7 @@ export class TemplateMemory {
341
359
  }
342
360
  // Read workbook.xml and find the source sheet
343
361
  const workbookXmlPath = this.#excelKeys.workbook;
344
- const workbookXml = this.#extractXmlFromSheet(this.#excelKeys.workbook);
362
+ const workbookXml = await this.#extractXmlFromSheet(this.#excelKeys.workbook);
345
363
  // Find the source sheet
346
364
  const sheetMatch = workbookXml.match(Utils.sheetMatch(sourceName));
347
365
  if (!sheetMatch || !sheetMatch[1]) {
@@ -355,7 +373,7 @@ export class TemplateMemory {
355
373
  // Find the source sheet path by rId
356
374
  const rId = sheetMatch[1];
357
375
  const relsXmlPath = this.#excelKeys.workbookRels;
358
- const relsXml = this.#extractXmlFromSheet(this.#excelKeys.workbookRels);
376
+ const relsXml = await this.#extractXmlFromSheet(this.#excelKeys.workbookRels);
359
377
  const relMatch = relsXml.match(Utils.relationshipMatch(rId));
360
378
  if (!relMatch || !relMatch[1]) {
361
379
  throw new Error(`Relationship "${rId}" not found`);
@@ -397,7 +415,7 @@ export class TemplateMemory {
397
415
  // Read [Content_Types].xml
398
416
  // Update [Content_Types].xml
399
417
  const contentTypesPath = "[Content_Types].xml";
400
- const contentTypesXml = this.#extractXmlFromSheet(contentTypesPath);
418
+ const contentTypesXml = await this.#extractXmlFromSheet(contentTypesPath);
401
419
  const overrideTag = `<Override PartName="/xl/worksheets/${newSheetFilename}" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>`;
402
420
  const updatedContentTypesXml = contentTypesXml.replace("</Types>", overrideTag + "</Types>");
403
421
  await this.#set(contentTypesPath, Buffer.from(updatedContentTypesXml));
@@ -418,12 +436,12 @@ export class TemplateMemory {
418
436
  * @param replacements - An object where keys represent placeholder paths and values are the replacements.
419
437
  * @returns A promise that resolves when the substitution is complete.
420
438
  */
421
- substitute(sheetName, replacements) {
439
+ async substitute(sheetName, replacements) {
422
440
  this.#ensureNotProcessing();
423
441
  this.#ensureNotDestroyed();
424
442
  this.#isProcessing = true;
425
443
  try {
426
- return this.#substitute(sheetName, replacements);
444
+ await this.#substitute(sheetName, replacements);
427
445
  }
428
446
  finally {
429
447
  this.#isProcessing = false;
@@ -454,8 +472,8 @@ export class TemplateMemory {
454
472
  Utils.checkStartRow(startRowNumber);
455
473
  Utils.checkRows(preparedRows);
456
474
  // Find the sheet
457
- const sheetPath = this.#getSheetPathByName(sheetName);
458
- const sheetXml = this.#extractXmlFromSheet(sheetPath);
475
+ const sheetPath = await this.#getSheetPathByName(sheetName);
476
+ const sheetXml = await this.#extractXmlFromSheet(sheetPath);
459
477
  let nextRow = 0;
460
478
  if (!startRowNumber) {
461
479
  // Find the last row
@@ -489,7 +507,7 @@ export class TemplateMemory {
489
507
  else {
490
508
  updatedXml = sheetXml.replace(/<worksheet[^>]*>/, (match) => `${match}<sheetData>${rowsXml}</sheetData>`);
491
509
  }
492
- await this.#set(sheetPath, Buffer.from(updatedXml));
510
+ await this.#set(sheetPath, Buffer.from(Utils.updateDimension(updatedXml)));
493
511
  }
494
512
  finally {
495
513
  this.#isProcessing = false;
@@ -518,10 +536,30 @@ export class TemplateMemory {
518
536
  if (!sheetName)
519
537
  throw new Error("Sheet name is required");
520
538
  // Read XML workbook to find sheet name and path
521
- const sheetPath = this.#getSheetPathByName(sheetName);
522
- const sheetXml = this.#extractXmlFromSheet(sheetPath);
539
+ const sheetPath = await this.#getSheetPathByName(sheetName);
540
+ const sheetXml = await this.#extractXmlFromSheet(sheetPath);
523
541
  const output = new MemoryWriteStream();
524
542
  let inserted = false;
543
+ const initialDimension = sheetXml.match(/<dimension\s+ref="[^"]*"/)?.[0] || "";
544
+ const dimension = {
545
+ maxColumn: "A",
546
+ maxRow: 1,
547
+ minColumn: "A",
548
+ minRow: 1,
549
+ };
550
+ if (initialDimension) {
551
+ const dimensionMatch = initialDimension.match(/<dimension\s+ref="([^"]*)"/);
552
+ if (dimensionMatch) {
553
+ const dimensionRef = dimensionMatch[1];
554
+ if (dimensionRef) {
555
+ const [min, max] = dimensionRef.split(":");
556
+ dimension.minColumn = min.slice(0, 1);
557
+ dimension.minRow = parseInt(min.slice(1));
558
+ dimension.maxColumn = max.slice(0, 1);
559
+ dimension.maxRow = parseInt(max.slice(1));
560
+ }
561
+ }
562
+ }
525
563
  // --- Case 1: <sheetData>...</sheetData> on one line ---
526
564
  const singleLineMatch = sheetXml.match(/(<sheetData[^>]*>)(.*)(<\/sheetData>)/);
527
565
  if (!inserted && singleLineMatch) {
@@ -542,7 +580,13 @@ export class TemplateMemory {
542
580
  output.write(innerRows);
543
581
  }
544
582
  }
545
- const { rowNumber: actualRowNumber } = await Utils.writeRowsToStream(output, rows, maxRowNumber);
583
+ const { dimension: newDimension, rowNumber: actualRowNumber } = await Utils.writeRowsToStream(output, rows, maxRowNumber);
584
+ if (Utils.compareColumns(newDimension.maxColumn, dimension.maxColumn) > 0) {
585
+ dimension.maxColumn = newDimension.maxColumn;
586
+ }
587
+ if (newDimension.maxRow > dimension.maxRow) {
588
+ dimension.maxRow = newDimension.maxRow;
589
+ }
546
590
  if (innerRows) {
547
591
  const filtered = Utils.getRowsAbove(innerRowsMap, actualRowNumber);
548
592
  if (filtered)
@@ -559,7 +603,13 @@ export class TemplateMemory {
559
603
  const matchIndex = match.index;
560
604
  output.write(sheetXml.slice(0, matchIndex));
561
605
  output.write("<sheetData>");
562
- await Utils.writeRowsToStream(output, rows, maxRowNumber);
606
+ const { dimension: newDimension } = await Utils.writeRowsToStream(output, rows, maxRowNumber);
607
+ if (Utils.compareColumns(newDimension.maxColumn, dimension.maxColumn) > 0) {
608
+ dimension.maxColumn = newDimension.maxColumn;
609
+ }
610
+ if (newDimension.maxRow > dimension.maxRow) {
611
+ dimension.maxRow = newDimension.maxRow;
612
+ }
563
613
  output.write("</sheetData>");
564
614
  output.write(sheetXml.slice(matchIndex + match[0].length));
565
615
  inserted = true;
@@ -590,7 +640,13 @@ export class TemplateMemory {
590
640
  output.write(innerRows);
591
641
  }
592
642
  }
593
- const { rowNumber: actualRowNumber } = await Utils.writeRowsToStream(output, rows, Utils.getMaxRowNumber(innerRows));
643
+ const { dimension: newDimension, rowNumber: actualRowNumber } = await Utils.writeRowsToStream(output, rows, Utils.getMaxRowNumber(innerRows));
644
+ if (Utils.compareColumns(newDimension.maxColumn, dimension.maxColumn) > 0) {
645
+ dimension.maxColumn = newDimension.maxColumn;
646
+ }
647
+ if (newDimension.maxRow > dimension.maxRow) {
648
+ dimension.maxRow = newDimension.maxRow;
649
+ }
594
650
  if (innerRows) {
595
651
  const filtered = Utils.getRowsAbove(innerRowsMap, actualRowNumber);
596
652
  if (filtered)
@@ -602,8 +658,18 @@ export class TemplateMemory {
602
658
  }
603
659
  if (!inserted)
604
660
  throw new Error("Failed to locate <sheetData> for insertion");
661
+ let result = output.toBuffer();
662
+ // update dimension
663
+ {
664
+ const target = initialDimension;
665
+ const refRange = `${dimension.minColumn}${dimension.minRow}:${dimension.maxColumn}${dimension.maxRow}`;
666
+ const replacement = `<dimension ref="${refRange}"`;
667
+ if (target) {
668
+ result = Buffer.from(result.toString().replace(target, replacement));
669
+ }
670
+ }
605
671
  // Save the buffer to the sheet
606
- this.files[sheetPath] = output.toBuffer();
672
+ this.files[sheetPath] = result;
607
673
  }
608
674
  finally {
609
675
  this.#isProcessing = false;
@@ -666,12 +732,12 @@ export class TemplateMemory {
666
732
  * @param {number} [data.gap=0] - The number of empty rows to insert between each added section.
667
733
  * @returns {void}
668
734
  */
669
- mergeSheets(data) {
735
+ async mergeSheets(data) {
670
736
  this.#ensureNotProcessing();
671
737
  this.#ensureNotDestroyed();
672
738
  this.#isProcessing = true;
673
739
  try {
674
- this.#mergeSheets(data);
740
+ await this.#mergeSheets(data);
675
741
  }
676
742
  finally {
677
743
  this.#isProcessing = false;
@@ -685,17 +751,18 @@ export class TemplateMemory {
685
751
  * @param {string[]} [data.sheetNames] - The names of the sheets to remove.
686
752
  * @returns {void}
687
753
  */
688
- removeSheets(data) {
754
+ async removeSheets(data) {
689
755
  this.#ensureNotProcessing();
690
756
  this.#ensureNotDestroyed();
691
757
  this.#isProcessing = true;
692
758
  try {
693
- this.#removeSheets(data);
759
+ await this.#removeSheets(data);
694
760
  }
695
761
  finally {
696
762
  this.#isProcessing = false;
697
763
  }
698
764
  }
765
+ /** Static methods */
699
766
  /**
700
767
  * Creates a Template instance from an Excel file source.
701
768
  *
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Compares two column strings and returns a number indicating their relative order.
3
+ *
4
+ * @param a - The first column string to compare.
5
+ * @param b - The second column string to compare.
6
+ * @returns 0 if the columns are equal, -1 if the first column is less than the second, or 1 if the first column is greater than the second.
7
+ */
8
+ export function compareColumns(a, b) {
9
+ if (a === b) {
10
+ return 0;
11
+ }
12
+ return a.length === b.length ? (a < b ? -1 : 1) : (a.length < b.length ? -1 : 1);
13
+ }
@@ -4,6 +4,7 @@ export * from "./check-row.js";
4
4
  export * from "./check-rows.js";
5
5
  export * from "./check-start-row.js";
6
6
  export * from "./column-index-to-letter.js";
7
+ export * from "./compare-columns.js";
7
8
  export * from "./escape-xml.js";
8
9
  export * from "./extract-xml-declaration.js";
9
10
  export * from "./get-by-path.js";