@js-ak/excel-toolbox 1.7.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.
@@ -92,6 +92,7 @@ class TemplateFs {
92
92
  this.fileKeys = fileKeys;
93
93
  this.destination = destination;
94
94
  }
95
+ /** Private methods */
95
96
  /**
96
97
  * Removes the temporary directory created by this Template instance.
97
98
  * @private
@@ -224,6 +225,20 @@ class TemplateFs {
224
225
  const fullPath = path.join(this.destination, ...key.split("/"));
225
226
  await fs.writeFile(fullPath, Buffer.isBuffer(content) ? content : Buffer.from(content));
226
227
  }
228
+ /**
229
+ * Replaces placeholders in the given sheet with values from the replacements map.
230
+ *
231
+ * The function searches for placeholders in the format `${key}` within the sheet
232
+ * content, where `key` corresponds to a path in the replacements object.
233
+ * If a value is found for the key, it replaces the placeholder with the value.
234
+ * If no value is found, the original placeholder remains unchanged.
235
+ *
236
+ * @param sheetName - The name of the sheet to be replaced.
237
+ * @param replacements - An object where keys represent placeholder paths and values are the replacements.
238
+ * @returns A promise that resolves when the substitution is complete.
239
+ * @throws {Error} If the template instance has been destroyed.
240
+ * @experimental This API is experimental and might change in future versions.
241
+ */
227
242
  async #substitute(sheetName, replacements) {
228
243
  const sharedStringsPath = this.#excelKeys.sharedStrings;
229
244
  const sheetPath = await this.#getSheetPathByName(sheetName);
@@ -251,6 +266,54 @@ class TemplateFs {
251
266
  await this.#set(sheetPath, sheetContent);
252
267
  }
253
268
  }
269
+ /**
270
+ * Removes sheets from the workbook.
271
+ *
272
+ * @param {Object} data - The data for sheet removal.
273
+ * @param {number[]} [data.sheetIndexes] - The 1-based indexes of the sheets to remove.
274
+ * @param {string[]} [data.sheetNames] - The names of the sheets to remove.
275
+ * @returns {void}
276
+ *
277
+ * @throws {Error} If the template instance has been destroyed.
278
+ * @throws {Error} If the sheet does not exist.
279
+ * @experimental This API is experimental and might change in future versions.
280
+ */
281
+ async #removeSheets(data) {
282
+ const { sheetIndexes = [], sheetNames = [] } = data;
283
+ // first get index of sheets to remove
284
+ const sheetIndexesToRemove = new Set(sheetIndexes);
285
+ for (const sheetName of sheetNames) {
286
+ const sheetPath = await this.#getSheetPathByName(sheetName);
287
+ const sheetIndexMatch = sheetPath.match(/sheet(\d+)\.xml$/);
288
+ if (!sheetIndexMatch || !sheetIndexMatch[1]) {
289
+ throw new Error(`Sheet "${sheetName}" not found`);
290
+ }
291
+ const sheetIndex = parseInt(sheetIndexMatch[1], 10);
292
+ sheetIndexesToRemove.add(sheetIndex);
293
+ }
294
+ // Remove sheets by index
295
+ for (const sheetIndex of sheetIndexesToRemove.values()) {
296
+ const sheetPath = `xl/worksheets/sheet${sheetIndex}.xml`;
297
+ if (!this.fileKeys.has(sheetPath)) {
298
+ continue;
299
+ }
300
+ // remove sheet file
301
+ await fs.unlink(path.join(this.destination, ...sheetPath.split("/")));
302
+ this.fileKeys.delete(sheetPath);
303
+ // remove sheet from workbook
304
+ if (this.fileKeys.has(this.#excelKeys.workbook)) {
305
+ this.#set(this.#excelKeys.workbook, Buffer.from(Utils.Common.removeSheetFromWorkbook(this.#readFile(this.#excelKeys.workbook).toString(), sheetIndex)));
306
+ }
307
+ // remove sheet from workbook relations
308
+ if (this.fileKeys.has(this.#excelKeys.workbookRels)) {
309
+ this.#set(this.#excelKeys.workbookRels, Buffer.from(Utils.Common.removeSheetFromRels(this.#readFile(this.#excelKeys.workbookRels).toString(), sheetIndex)));
310
+ }
311
+ // remove sheet from content types
312
+ if (this.fileKeys.has(this.#excelKeys.contentTypes)) {
313
+ this.#set(this.#excelKeys.contentTypes, Buffer.from(Utils.Common.removeSheetFromContentTypes(this.#readFile(this.#excelKeys.contentTypes).toString(), sheetIndex)));
314
+ }
315
+ }
316
+ }
254
317
  /**
255
318
  * Validates the template by checking all required files exist.
256
319
  *
@@ -270,6 +333,7 @@ class TemplateFs {
270
333
  }
271
334
  }
272
335
  }
336
+ /** Public methods */
273
337
  /**
274
338
  * Copies a sheet from the template to a new name.
275
339
  *
@@ -360,12 +424,12 @@ class TemplateFs {
360
424
  * @param replacements - An object where keys represent placeholder paths and values are the replacements.
361
425
  * @returns A promise that resolves when the substitution is complete.
362
426
  */
363
- substitute(sheetName, replacements) {
427
+ async substitute(sheetName, replacements) {
364
428
  this.#ensureNotProcessing();
365
429
  this.#ensureNotDestroyed();
366
430
  this.#isProcessing = true;
367
431
  try {
368
- return this.#substitute(sheetName, replacements);
432
+ await this.#substitute(sheetName, replacements);
369
433
  }
370
434
  finally {
371
435
  this.#isProcessing = false;
@@ -527,7 +591,7 @@ class TemplateFs {
527
591
  }
528
592
  }
529
593
  const { dimension: newDimension, rowNumber: actualRowNumber } = await Utils.writeRowsToStream(output, rows, maxRowNumber);
530
- if (compareColumns(newDimension.maxColumn, dimension.maxColumn) > 0) {
594
+ if (Utils.compareColumns(newDimension.maxColumn, dimension.maxColumn) > 0) {
531
595
  dimension.maxColumn = newDimension.maxColumn;
532
596
  }
533
597
  if (newDimension.maxRow > dimension.maxRow) {
@@ -574,7 +638,7 @@ class TemplateFs {
574
638
  }
575
639
  // new <row>
576
640
  const { dimension: newDimension, rowNumber: actualRowNumber } = await Utils.writeRowsToStream(output, rows, maxRowNumber);
577
- if (compareColumns(newDimension.maxColumn, dimension.maxColumn) > 0) {
641
+ if (Utils.compareColumns(newDimension.maxColumn, dimension.maxColumn) > 0) {
578
642
  dimension.maxColumn = newDimension.maxColumn;
579
643
  }
580
644
  if (newDimension.maxRow > dimension.maxRow) {
@@ -607,7 +671,7 @@ class TemplateFs {
607
671
  output.write("<sheetData>");
608
672
  // Prepare the rows
609
673
  const { dimension: newDimension } = await Utils.writeRowsToStream(output, rows, maxRowNumber);
610
- if (compareColumns(newDimension.maxColumn, dimension.maxColumn) > 0) {
674
+ if (Utils.compareColumns(newDimension.maxColumn, dimension.maxColumn) > 0) {
611
675
  dimension.maxColumn = newDimension.maxColumn;
612
676
  }
613
677
  if (newDimension.maxRow > dimension.maxRow) {
@@ -674,6 +738,25 @@ class TemplateFs {
674
738
  this.#isProcessing = false;
675
739
  }
676
740
  }
741
+ /**
742
+ * Removes sheets from the workbook.
743
+ *
744
+ * @param {Object} data
745
+ * @param {number[]} [data.sheetIndexes] - The 1-based indexes of the sheets to remove.
746
+ * @param {string[]} [data.sheetNames] - The names of the sheets to remove.
747
+ * @returns {void}
748
+ */
749
+ async removeSheets(data) {
750
+ this.#ensureNotProcessing();
751
+ this.#ensureNotDestroyed();
752
+ this.#isProcessing = true;
753
+ try {
754
+ await this.#removeSheets(data);
755
+ }
756
+ finally {
757
+ this.#isProcessing = false;
758
+ }
759
+ }
677
760
  /**
678
761
  * Saves the modified Excel template to a buffer.
679
762
  *
@@ -762,6 +845,7 @@ class TemplateFs {
762
845
  this.#isProcessing = false;
763
846
  }
764
847
  }
848
+ /** Static methods */
765
849
  /**
766
850
  * Creates a Template instance from an Excel file source.
767
851
  * Removes any existing files in the destination directory.
@@ -800,8 +884,3 @@ class TemplateFs {
800
884
  }
801
885
  }
802
886
  exports.TemplateFs = TemplateFs;
803
- const compareColumns = (a, b) => {
804
- if (a === b)
805
- return 0;
806
- return a.length === b.length ? (a < b ? -1 : 1) : (a.length < b.length ? -1 : 1);
807
- };
@@ -76,6 +76,7 @@ class TemplateMemory {
76
76
  constructor(files) {
77
77
  this.files = files;
78
78
  }
79
+ /** Private methods */
79
80
  /**
80
81
  * Ensures that this Template instance has not been destroyed.
81
82
  * @private
@@ -335,28 +336,45 @@ class TemplateMemory {
335
336
  * @throws {Error} If the sheet does not exist.
336
337
  * @experimental This API is experimental and might change in future versions.
337
338
  */
338
- #removeSheets(data) {
339
+ async #removeSheets(data) {
339
340
  const { sheetIndexes = [], sheetNames = [] } = data;
340
- for (const sheetIndex of sheetIndexes) {
341
+ // first get index of sheets to remove
342
+ const sheetIndexesToRemove = new Set(sheetIndexes);
343
+ for (const sheetName of sheetNames) {
344
+ const sheetPath = await this.#getSheetPathByName(sheetName);
345
+ const sheetIndexMatch = sheetPath.match(/sheet(\d+)\.xml$/);
346
+ if (!sheetIndexMatch || !sheetIndexMatch[1]) {
347
+ throw new Error(`Sheet "${sheetName}" not found`);
348
+ }
349
+ const sheetIndex = parseInt(sheetIndexMatch[1], 10);
350
+ sheetIndexesToRemove.add(sheetIndex);
351
+ }
352
+ // Remove sheets by index
353
+ for (const sheetIndex of sheetIndexesToRemove.values()) {
341
354
  const sheetPath = `xl/worksheets/sheet${sheetIndex}.xml`;
342
355
  if (!this.files[sheetPath]) {
343
356
  continue;
344
357
  }
358
+ // remove sheet file
345
359
  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));
360
+ // remove sheet from workbook
361
+ const workbook = this.files[this.#excelKeys.workbook];
362
+ if (workbook) {
363
+ this.files[this.#excelKeys.workbook] = Buffer.from(Utils.Common.removeSheetFromWorkbook(workbook.toString(), sheetIndex));
348
364
  }
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));
365
+ // remove sheet from workbook relations
366
+ const workbookRels = this.files[this.#excelKeys.workbookRels];
367
+ if (workbookRels) {
368
+ this.files[this.#excelKeys.workbookRels] = Buffer.from(Utils.Common.removeSheetFromRels(workbookRels.toString(), sheetIndex));
351
369
  }
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));
370
+ // remove sheet from content types
371
+ const contentTypes = this.files[this.#excelKeys.contentTypes];
372
+ if (contentTypes) {
373
+ this.files[this.#excelKeys.contentTypes] = Buffer.from(Utils.Common.removeSheetFromContentTypes(contentTypes.toString(), sheetIndex));
354
374
  }
355
375
  }
356
- for (const sheetName of sheetNames) {
357
- Utils.Common.removeSheetByName(this.files, sheetName);
358
- }
359
376
  }
377
+ /** Public methods */
360
378
  /**
361
379
  * Copies a sheet from the template to a new name.
362
380
  *
@@ -454,12 +472,12 @@ class TemplateMemory {
454
472
  * @param replacements - An object where keys represent placeholder paths and values are the replacements.
455
473
  * @returns A promise that resolves when the substitution is complete.
456
474
  */
457
- substitute(sheetName, replacements) {
475
+ async substitute(sheetName, replacements) {
458
476
  this.#ensureNotProcessing();
459
477
  this.#ensureNotDestroyed();
460
478
  this.#isProcessing = true;
461
479
  try {
462
- return this.#substitute(sheetName, replacements);
480
+ await this.#substitute(sheetName, replacements);
463
481
  }
464
482
  finally {
465
483
  this.#isProcessing = false;
@@ -525,7 +543,7 @@ class TemplateMemory {
525
543
  else {
526
544
  updatedXml = sheetXml.replace(/<worksheet[^>]*>/, (match) => `${match}<sheetData>${rowsXml}</sheetData>`);
527
545
  }
528
- await this.#set(sheetPath, Buffer.from(updatedXml));
546
+ await this.#set(sheetPath, Buffer.from(Utils.updateDimension(updatedXml)));
529
547
  }
530
548
  finally {
531
549
  this.#isProcessing = false;
@@ -558,6 +576,26 @@ class TemplateMemory {
558
576
  const sheetXml = await this.#extractXmlFromSheet(sheetPath);
559
577
  const output = new memory_write_stream_js_1.MemoryWriteStream();
560
578
  let inserted = false;
579
+ const initialDimension = sheetXml.match(/<dimension\s+ref="[^"]*"/)?.[0] || "";
580
+ const dimension = {
581
+ maxColumn: "A",
582
+ maxRow: 1,
583
+ minColumn: "A",
584
+ minRow: 1,
585
+ };
586
+ if (initialDimension) {
587
+ const dimensionMatch = initialDimension.match(/<dimension\s+ref="([^"]*)"/);
588
+ if (dimensionMatch) {
589
+ const dimensionRef = dimensionMatch[1];
590
+ if (dimensionRef) {
591
+ const [min, max] = dimensionRef.split(":");
592
+ dimension.minColumn = min.slice(0, 1);
593
+ dimension.minRow = parseInt(min.slice(1));
594
+ dimension.maxColumn = max.slice(0, 1);
595
+ dimension.maxRow = parseInt(max.slice(1));
596
+ }
597
+ }
598
+ }
561
599
  // --- Case 1: <sheetData>...</sheetData> on one line ---
562
600
  const singleLineMatch = sheetXml.match(/(<sheetData[^>]*>)(.*)(<\/sheetData>)/);
563
601
  if (!inserted && singleLineMatch) {
@@ -578,7 +616,13 @@ class TemplateMemory {
578
616
  output.write(innerRows);
579
617
  }
580
618
  }
581
- const { rowNumber: actualRowNumber } = await Utils.writeRowsToStream(output, rows, maxRowNumber);
619
+ const { dimension: newDimension, rowNumber: actualRowNumber } = await Utils.writeRowsToStream(output, rows, maxRowNumber);
620
+ if (Utils.compareColumns(newDimension.maxColumn, dimension.maxColumn) > 0) {
621
+ dimension.maxColumn = newDimension.maxColumn;
622
+ }
623
+ if (newDimension.maxRow > dimension.maxRow) {
624
+ dimension.maxRow = newDimension.maxRow;
625
+ }
582
626
  if (innerRows) {
583
627
  const filtered = Utils.getRowsAbove(innerRowsMap, actualRowNumber);
584
628
  if (filtered)
@@ -595,7 +639,13 @@ class TemplateMemory {
595
639
  const matchIndex = match.index;
596
640
  output.write(sheetXml.slice(0, matchIndex));
597
641
  output.write("<sheetData>");
598
- await Utils.writeRowsToStream(output, rows, maxRowNumber);
642
+ const { dimension: newDimension } = await Utils.writeRowsToStream(output, rows, maxRowNumber);
643
+ if (Utils.compareColumns(newDimension.maxColumn, dimension.maxColumn) > 0) {
644
+ dimension.maxColumn = newDimension.maxColumn;
645
+ }
646
+ if (newDimension.maxRow > dimension.maxRow) {
647
+ dimension.maxRow = newDimension.maxRow;
648
+ }
599
649
  output.write("</sheetData>");
600
650
  output.write(sheetXml.slice(matchIndex + match[0].length));
601
651
  inserted = true;
@@ -626,7 +676,13 @@ class TemplateMemory {
626
676
  output.write(innerRows);
627
677
  }
628
678
  }
629
- const { rowNumber: actualRowNumber } = await Utils.writeRowsToStream(output, rows, Utils.getMaxRowNumber(innerRows));
679
+ const { dimension: newDimension, rowNumber: actualRowNumber } = await Utils.writeRowsToStream(output, rows, Utils.getMaxRowNumber(innerRows));
680
+ if (Utils.compareColumns(newDimension.maxColumn, dimension.maxColumn) > 0) {
681
+ dimension.maxColumn = newDimension.maxColumn;
682
+ }
683
+ if (newDimension.maxRow > dimension.maxRow) {
684
+ dimension.maxRow = newDimension.maxRow;
685
+ }
630
686
  if (innerRows) {
631
687
  const filtered = Utils.getRowsAbove(innerRowsMap, actualRowNumber);
632
688
  if (filtered)
@@ -638,8 +694,18 @@ class TemplateMemory {
638
694
  }
639
695
  if (!inserted)
640
696
  throw new Error("Failed to locate <sheetData> for insertion");
697
+ let result = output.toBuffer();
698
+ // update dimension
699
+ {
700
+ const target = initialDimension;
701
+ const refRange = `${dimension.minColumn}${dimension.minRow}:${dimension.maxColumn}${dimension.maxRow}`;
702
+ const replacement = `<dimension ref="${refRange}"`;
703
+ if (target) {
704
+ result = Buffer.from(result.toString().replace(target, replacement));
705
+ }
706
+ }
641
707
  // Save the buffer to the sheet
642
- this.files[sheetPath] = output.toBuffer();
708
+ this.files[sheetPath] = result;
643
709
  }
644
710
  finally {
645
711
  this.#isProcessing = false;
@@ -702,12 +768,12 @@ class TemplateMemory {
702
768
  * @param {number} [data.gap=0] - The number of empty rows to insert between each added section.
703
769
  * @returns {void}
704
770
  */
705
- mergeSheets(data) {
771
+ async mergeSheets(data) {
706
772
  this.#ensureNotProcessing();
707
773
  this.#ensureNotDestroyed();
708
774
  this.#isProcessing = true;
709
775
  try {
710
- this.#mergeSheets(data);
776
+ await this.#mergeSheets(data);
711
777
  }
712
778
  finally {
713
779
  this.#isProcessing = false;
@@ -721,17 +787,18 @@ class TemplateMemory {
721
787
  * @param {string[]} [data.sheetNames] - The names of the sheets to remove.
722
788
  * @returns {void}
723
789
  */
724
- removeSheets(data) {
790
+ async removeSheets(data) {
725
791
  this.#ensureNotProcessing();
726
792
  this.#ensureNotDestroyed();
727
793
  this.#isProcessing = true;
728
794
  try {
729
- this.#removeSheets(data);
795
+ await this.#removeSheets(data);
730
796
  }
731
797
  finally {
732
798
  this.#isProcessing = false;
733
799
  }
734
800
  }
801
+ /** Static methods */
735
802
  /**
736
803
  * Creates a Template instance from an Excel file source.
737
804
  *
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.compareColumns = compareColumns;
4
+ /**
5
+ * Compares two column strings and returns a number indicating their relative order.
6
+ *
7
+ * @param a - The first column string to compare.
8
+ * @param b - The second column string to compare.
9
+ * @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.
10
+ */
11
+ function compareColumns(a, b) {
12
+ if (a === b) {
13
+ return 0;
14
+ }
15
+ return a.length === b.length ? (a < b ? -1 : 1) : (a.length < b.length ? -1 : 1);
16
+ }
@@ -43,6 +43,7 @@ __exportStar(require("./check-row.js"), exports);
43
43
  __exportStar(require("./check-rows.js"), exports);
44
44
  __exportStar(require("./check-start-row.js"), exports);
45
45
  __exportStar(require("./column-index-to-letter.js"), exports);
46
+ __exportStar(require("./compare-columns.js"), exports);
46
47
  __exportStar(require("./escape-xml.js"), exports);
47
48
  __exportStar(require("./extract-xml-declaration.js"), exports);
48
49
  __exportStar(require("./get-by-path.js"), exports);
@@ -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
@@ -185,6 +186,20 @@ 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);
@@ -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
  *
@@ -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;
@@ -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
@@ -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
  *
@@ -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;
@@ -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;
@@ -522,6 +540,26 @@ export class TemplateMemory {
522
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";
@@ -29,6 +29,7 @@ export declare class TemplateFs {
29
29
  * @experimental This API is experimental and might change in future versions.
30
30
  */
31
31
  constructor(fileKeys: Set<string>, destination: string);
32
+ /** Public methods */
32
33
  /**
33
34
  * Copies a sheet from the template to a new name.
34
35
  *
@@ -91,6 +92,18 @@ export declare class TemplateFs {
91
92
  startRowNumber?: number;
92
93
  rows: AsyncIterable<unknown[]>;
93
94
  }): Promise<void>;
95
+ /**
96
+ * Removes sheets from the workbook.
97
+ *
98
+ * @param {Object} data
99
+ * @param {number[]} [data.sheetIndexes] - The 1-based indexes of the sheets to remove.
100
+ * @param {string[]} [data.sheetNames] - The names of the sheets to remove.
101
+ * @returns {void}
102
+ */
103
+ removeSheets(data: {
104
+ sheetNames?: string[];
105
+ sheetIndexes?: number[];
106
+ }): Promise<void>;
94
107
  /**
95
108
  * Saves the modified Excel template to a buffer.
96
109
  *
@@ -128,6 +141,7 @@ export declare class TemplateFs {
128
141
  * @experimental This API is experimental and might change in future versions.
129
142
  */
130
143
  validate(): Promise<void>;
144
+ /** Static methods */
131
145
  /**
132
146
  * Creates a Template instance from an Excel file source.
133
147
  * Removes any existing files in the destination directory.
@@ -19,6 +19,7 @@ export declare class TemplateMemory {
19
19
  * @experimental This API is experimental and might change in future versions.
20
20
  */
21
21
  constructor(files: Record<string, Buffer>);
22
+ /** Public methods */
22
23
  /**
23
24
  * Copies a sheet from the template to a new name.
24
25
  *
@@ -118,7 +119,7 @@ export declare class TemplateMemory {
118
119
  baseSheetIndex?: number;
119
120
  baseSheetName?: string;
120
121
  gap?: number;
121
- }): void;
122
+ }): Promise<void>;
122
123
  /**
123
124
  * Removes sheets from the workbook.
124
125
  *
@@ -130,7 +131,8 @@ export declare class TemplateMemory {
130
131
  removeSheets(data: {
131
132
  sheetNames?: string[];
132
133
  sheetIndexes?: number[];
133
- }): void;
134
+ }): Promise<void>;
135
+ /** Static methods */
134
136
  /**
135
137
  * Creates a Template instance from an Excel file source.
136
138
  *
@@ -0,0 +1,8 @@
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 declare function compareColumns(a: string, b: string): number;
@@ -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";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@js-ak/excel-toolbox",
3
- "version": "1.7.0",
3
+ "version": "1.8.0",
4
4
  "description": "excel-toolbox",
5
5
  "publishConfig": {
6
6
  "access": "public",