@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
@@ -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
@@ -140,7 +141,7 @@ class TemplateMemory {
140
141
  * @throws {Error} If the file key is not found.
141
142
  * @experimental This API is experimental and might change in future versions.
142
143
  */
143
- #extractXmlFromSheet(fileKey) {
144
+ async #extractXmlFromSheet(fileKey) {
144
145
  if (!this.files[fileKey]) {
145
146
  throw new Error(`${fileKey} not found`);
146
147
  }
@@ -158,7 +159,7 @@ class TemplateMemory {
158
159
  * @throws {Error} If the file key is not found
159
160
  * @experimental This API is experimental and might change in future versions.
160
161
  */
161
- #extractRowsFromSheet(fileKey) {
162
+ async #extractRowsFromSheet(fileKey) {
162
163
  if (!this.files[fileKey]) {
163
164
  throw new Error(`${fileKey} not found`);
164
165
  }
@@ -172,15 +173,15 @@ class TemplateMemory {
172
173
  * @throws {Error} If the sheet with the given name does not exist.
173
174
  * @experimental This API is experimental and might change in future versions.
174
175
  */
175
- #getSheetPathByName(sheetName) {
176
+ async #getSheetPathByName(sheetName) {
176
177
  // Find the sheet
177
- const workbookXml = this.#extractXmlFromSheet(this.#excelKeys.workbook);
178
+ const workbookXml = await this.#extractXmlFromSheet(this.#excelKeys.workbook);
178
179
  const sheetMatch = workbookXml.match(Utils.sheetMatch(sheetName));
179
180
  if (!sheetMatch || !sheetMatch[1]) {
180
181
  throw new Error(`Sheet "${sheetName}" not found`);
181
182
  }
182
183
  const rId = sheetMatch[1];
183
- const relsXml = this.#extractXmlFromSheet(this.#excelKeys.workbookRels);
184
+ const relsXml = await this.#extractXmlFromSheet(this.#excelKeys.workbookRels);
184
185
  const relMatch = relsXml.match(Utils.relationshipMatch(rId));
185
186
  if (!relMatch || !relMatch[1]) {
186
187
  throw new Error(`Relationship "${rId}" not found`);
@@ -233,11 +234,11 @@ class TemplateMemory {
233
234
  let sharedStringsContent = "";
234
235
  let sheetContent = "";
235
236
  if (this.files[sharedStringsPath]) {
236
- sharedStringsContent = this.#extractXmlFromSheet(sharedStringsPath);
237
+ sharedStringsContent = await this.#extractXmlFromSheet(sharedStringsPath);
237
238
  }
238
- const sheetPath = this.#getSheetPathByName(sheetName);
239
+ const sheetPath = await this.#getSheetPathByName(sheetName);
239
240
  if (this.files[sheetPath]) {
240
- sheetContent = this.#extractXmlFromSheet(sheetPath);
241
+ sheetContent = await this.#extractXmlFromSheet(sheetPath);
241
242
  const TABLE_REGEX = /\$\{table:([a-zA-Z0-9_]+)\.([a-zA-Z0-9_]+)\}/g;
242
243
  const hasTablePlaceholders = TABLE_REGEX.test(sharedStringsContent) || TABLE_REGEX.test(sheetContent);
243
244
  if (hasTablePlaceholders) {
@@ -272,11 +273,11 @@ class TemplateMemory {
272
273
  * @throws {Error} If no sheets are found to merge.
273
274
  * @experimental This API is experimental and might change in future versions.
274
275
  */
275
- #mergeSheets(data) {
276
+ async #mergeSheets(data) {
276
277
  const { additions, baseSheetIndex = 1, baseSheetName, gap = 0, } = data;
277
278
  let fileKey = "";
278
279
  if (baseSheetName) {
279
- fileKey = this.#getSheetPathByName(baseSheetName);
280
+ fileKey = await this.#getSheetPathByName(baseSheetName);
280
281
  }
281
282
  if (baseSheetIndex && !fileKey) {
282
283
  if (baseSheetIndex < 1) {
@@ -287,16 +288,16 @@ class TemplateMemory {
287
288
  if (!fileKey) {
288
289
  throw new Error("Base sheet not found");
289
290
  }
290
- const { lastRowNumber, mergeCells: baseMergeCells, rows: baseRows, xml, } = this.#extractRowsFromSheet(fileKey);
291
+ const { lastRowNumber, mergeCells: baseMergeCells, rows: baseRows, xml, } = await this.#extractRowsFromSheet(fileKey);
291
292
  const allRows = [...baseRows];
292
293
  const allMergeCells = [...baseMergeCells];
293
294
  let currentRowOffset = lastRowNumber + gap;
294
295
  const sheetPaths = [];
295
296
  if (additions.sheetIndexes) {
296
- sheetPaths.push(...(additions.sheetIndexes).map(e => this.#getSheetPathById(e)));
297
+ sheetPaths.push(...(await Promise.all(additions.sheetIndexes.map(e => this.#getSheetPathById(e)))));
297
298
  }
298
299
  if (additions.sheetNames) {
299
- sheetPaths.push(...(additions.sheetNames).map(e => this.#getSheetPathByName(e)));
300
+ sheetPaths.push(...(await Promise.all(additions.sheetNames.map(e => this.#getSheetPathByName(e)))));
300
301
  }
301
302
  if (sheetPaths.length === 0) {
302
303
  throw new Error("No sheets found to merge");
@@ -305,7 +306,7 @@ class TemplateMemory {
305
306
  if (!this.files[sheetPath]) {
306
307
  throw new Error(`Sheet "${sheetPath}" not found`);
307
308
  }
308
- const { mergeCells, rows } = Xml.extractRowsFromSheet(this.files[sheetPath]);
309
+ const { mergeCells, rows } = await Xml.extractRowsFromSheet(this.files[sheetPath]);
309
310
  const shiftedRows = Xml.shiftRowIndices(rows, currentRowOffset);
310
311
  const shiftedMergeCells = mergeCells.map(cell => {
311
312
  const [start, end] = cell.ref.split(":");
@@ -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
  *
@@ -377,7 +395,7 @@ class TemplateMemory {
377
395
  }
378
396
  // Read workbook.xml and find the source sheet
379
397
  const workbookXmlPath = this.#excelKeys.workbook;
380
- const workbookXml = this.#extractXmlFromSheet(this.#excelKeys.workbook);
398
+ const workbookXml = await this.#extractXmlFromSheet(this.#excelKeys.workbook);
381
399
  // Find the source sheet
382
400
  const sheetMatch = workbookXml.match(Utils.sheetMatch(sourceName));
383
401
  if (!sheetMatch || !sheetMatch[1]) {
@@ -391,7 +409,7 @@ class TemplateMemory {
391
409
  // Find the source sheet path by rId
392
410
  const rId = sheetMatch[1];
393
411
  const relsXmlPath = this.#excelKeys.workbookRels;
394
- const relsXml = this.#extractXmlFromSheet(this.#excelKeys.workbookRels);
412
+ const relsXml = await this.#extractXmlFromSheet(this.#excelKeys.workbookRels);
395
413
  const relMatch = relsXml.match(Utils.relationshipMatch(rId));
396
414
  if (!relMatch || !relMatch[1]) {
397
415
  throw new Error(`Relationship "${rId}" not found`);
@@ -433,7 +451,7 @@ class TemplateMemory {
433
451
  // Read [Content_Types].xml
434
452
  // Update [Content_Types].xml
435
453
  const contentTypesPath = "[Content_Types].xml";
436
- const contentTypesXml = this.#extractXmlFromSheet(contentTypesPath);
454
+ const contentTypesXml = await this.#extractXmlFromSheet(contentTypesPath);
437
455
  const overrideTag = `<Override PartName="/xl/worksheets/${newSheetFilename}" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>`;
438
456
  const updatedContentTypesXml = contentTypesXml.replace("</Types>", overrideTag + "</Types>");
439
457
  await this.#set(contentTypesPath, Buffer.from(updatedContentTypesXml));
@@ -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;
@@ -490,8 +508,8 @@ class TemplateMemory {
490
508
  Utils.checkStartRow(startRowNumber);
491
509
  Utils.checkRows(preparedRows);
492
510
  // Find the sheet
493
- const sheetPath = this.#getSheetPathByName(sheetName);
494
- const sheetXml = this.#extractXmlFromSheet(sheetPath);
511
+ const sheetPath = await this.#getSheetPathByName(sheetName);
512
+ const sheetXml = await this.#extractXmlFromSheet(sheetPath);
495
513
  let nextRow = 0;
496
514
  if (!startRowNumber) {
497
515
  // Find the last row
@@ -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;
@@ -554,10 +572,30 @@ class TemplateMemory {
554
572
  if (!sheetName)
555
573
  throw new Error("Sheet name is required");
556
574
  // Read XML workbook to find sheet name and path
557
- const sheetPath = this.#getSheetPathByName(sheetName);
558
- const sheetXml = this.#extractXmlFromSheet(sheetPath);
575
+ const sheetPath = await this.#getSheetPathByName(sheetName);
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);
@@ -0,0 +1,67 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.extractRowsFromSheetSync = extractRowsFromSheetSync;
4
+ const extract_xml_from_sheet_sync_js_1 = require("./extract-xml-from-sheet-sync.js");
5
+ /**
6
+ * Parses a worksheet (either as Buffer or string) to extract row data,
7
+ * last row number, and merge cell information from Excel XML format.
8
+ *
9
+ * This function is particularly useful for processing Excel files in
10
+ * Open XML Spreadsheet format (.xlsx).
11
+ *
12
+ * @param {Buffer|string} sheet - The worksheet content to parse, either as:
13
+ * - Buffer (binary Excel sheet)
14
+ * - string (raw XML content)
15
+ * @returns {{
16
+ * rows: string[],
17
+ * lastRowNumber: number,
18
+ * mergeCells: {ref: string}[]
19
+ * }} An object containing:
20
+ * - rows: Array of raw XML strings for each <row> element
21
+ * - lastRowNumber: Highest row number found in the sheet (1-based)
22
+ * - mergeCells: Array of merged cell ranges (e.g., [{ref: "A1:B2"}])
23
+ * @throws {Error} If the sheetData section is not found in the XML
24
+ */
25
+ function extractRowsFromSheetSync(sheet) {
26
+ // Convert Buffer input to XML string if needed
27
+ const xml = typeof sheet === "string"
28
+ ? sheet
29
+ : (0, extract_xml_from_sheet_sync_js_1.extractXmlFromSheetSync)(sheet);
30
+ // Extract the sheetData section containing all rows
31
+ const sheetDataMatch = xml.match(/<sheetData[^>]*>([\s\S]*?)<\/sheetData>/);
32
+ if (!sheetDataMatch) {
33
+ throw new Error("sheetData not found in worksheet XML");
34
+ }
35
+ const sheetDataContent = sheetDataMatch[1] || "";
36
+ // Extract all <row> elements using regex
37
+ const rowMatches = [...sheetDataContent.matchAll(/<row\b[^>]*\/>|<row\b[^>]*>[\s\S]*?<\/row>/g)];
38
+ const rows = rowMatches.map(match => match[0]);
39
+ // Calculate the highest row number present in the sheet
40
+ const lastRowNumber = rowMatches
41
+ .map(match => {
42
+ // Extract row number from r="..." attribute (1-based)
43
+ const rowNumMatch = match[0].match(/r="(\d+)"/);
44
+ return rowNumMatch?.[1] ? parseInt(rowNumMatch[1], 10) : null;
45
+ })
46
+ .filter((row) => row !== null) // Type guard to filter out nulls
47
+ .reduce((max, current) => Math.max(max, current), 0); // Find maximum row number
48
+ // Extract all merged cell ranges from the worksheet
49
+ const mergeCells = [];
50
+ const mergeCellsMatch = xml.match(/<mergeCells[^>]*>([\s\S]*?)<\/mergeCells>/);
51
+ if (mergeCellsMatch) {
52
+ // Find all mergeCell entries with ref attributes
53
+ const mergeCellMatches = mergeCellsMatch[1]?.match(/<mergeCell[^>]+ref="([^"]+)"[^>]*>/g) || [];
54
+ mergeCellMatches.forEach(match => {
55
+ const refMatch = match.match(/ref="([^"]+)"/);
56
+ if (refMatch?.[1]) {
57
+ mergeCells.push({ ref: refMatch[1] }); // Store the cell range (e.g., "A1:B2")
58
+ }
59
+ });
60
+ }
61
+ return {
62
+ lastRowNumber,
63
+ mergeCells,
64
+ rows,
65
+ xml,
66
+ };
67
+ }
@@ -22,9 +22,11 @@ const extract_xml_from_sheet_js_1 = require("./extract-xml-from-sheet.js");
22
22
  * - mergeCells: Array of merged cell ranges (e.g., [{ref: "A1:B2"}])
23
23
  * @throws {Error} If the sheetData section is not found in the XML
24
24
  */
25
- function extractRowsFromSheet(sheet) {
25
+ async function extractRowsFromSheet(sheet) {
26
26
  // Convert Buffer input to XML string if needed
27
- const xml = typeof sheet === "string" ? sheet : (0, extract_xml_from_sheet_js_1.extractXmlFromSheet)(sheet);
27
+ const xml = typeof sheet === "string"
28
+ ? sheet
29
+ : await (0, extract_xml_from_sheet_js_1.extractXmlFromSheet)(sheet);
28
30
  // Extract the sheetData section containing all rows
29
31
  const sheetDataMatch = xml.match(/<sheetData[^>]*>([\s\S]*?)<\/sheetData>/);
30
32
  if (!sheetDataMatch) {
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.extractXmlFromSheetSync = extractXmlFromSheetSync;
4
+ const node_zlib_1 = require("node:zlib");
5
+ /**
6
+ * Extracts and parses XML content from an Excel worksheet file (e.g., xl/worksheets/sheet1.xml).
7
+ * Handles both compressed (raw deflate) and uncompressed (plain XML) formats.
8
+ *
9
+ * This function is designed to work with Excel Open XML (.xlsx) worksheet files,
10
+ * which may be stored in either compressed or uncompressed format within the ZIP container.
11
+ *
12
+ * @param {Buffer} buffer - The file content to process, which may be:
13
+ * - Raw XML text
14
+ * - Deflate-compressed XML data (without zlib headers)
15
+ * @returns {string} - The extracted XML content as a UTF-8 string
16
+ * @throws {Error} - If the buffer is empty or cannot be processed
17
+ */
18
+ function extractXmlFromSheetSync(buffer) {
19
+ if (!buffer || buffer.length === 0) {
20
+ throw new Error("Empty buffer provided");
21
+ }
22
+ let xml;
23
+ // Check if the buffer starts with an XML declaration (<?xml)
24
+ const head = buffer.subarray(0, 1024).toString("utf8").replace(/[\x00-\x08\x0B\x0C\x0E-\x1F]/g, "").trim();
25
+ const isXml = /^<\?xml[\s\S]+<\w+[\s>]/.test(head);
26
+ if (isXml) {
27
+ // Case 1: Already uncompressed XML - convert directly to string
28
+ xml = buffer.toString("utf8");
29
+ }
30
+ else {
31
+ // Case 2: Attempt to decompress as raw deflate data
32
+ try {
33
+ xml = (0, node_zlib_1.inflateRawSync)(buffer).toString("utf8");
34
+ }
35
+ catch (err) {
36
+ throw new Error("Failed to decompress sheet XML: " + (err instanceof Error ? err.message : String(err)));
37
+ }
38
+ }
39
+ // Sanitize XML by removing control characters (except tab, newline, carriage return)
40
+ // This handles potential corruption from binary data or encoding issues
41
+ xml = xml.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F]/g, "");
42
+ return xml;
43
+ }
@@ -1,7 +1,12 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  exports.extractXmlFromSheet = extractXmlFromSheet;
4
- const pako_1 = require("pako");
7
+ const node_util_1 = __importDefault(require("node:util"));
8
+ const node_zlib_1 = __importDefault(require("node:zlib"));
9
+ const inflateRaw = node_util_1.default.promisify(node_zlib_1.default.inflateRaw);
5
10
  /**
6
11
  * Extracts and parses XML content from an Excel worksheet file (e.g., xl/worksheets/sheet1.xml).
7
12
  * Handles both compressed (raw deflate) and uncompressed (plain XML) formats.
@@ -12,35 +17,30 @@ const pako_1 = require("pako");
12
17
  * @param {Buffer} buffer - The file content to process, which may be:
13
18
  * - Raw XML text
14
19
  * - Deflate-compressed XML data (without zlib headers)
15
- * @returns {string} - The extracted XML content as a UTF-8 string
20
+ * @returns {Promise<string>} - The extracted XML content as a UTF-8 string
16
21
  * @throws {Error} - If the buffer is empty or cannot be processed
17
22
  */
18
- function extractXmlFromSheet(buffer) {
23
+ async function extractXmlFromSheet(buffer) {
19
24
  if (!buffer || buffer.length === 0) {
20
25
  throw new Error("Empty buffer provided");
21
26
  }
22
27
  let xml;
23
28
  // Check if the buffer starts with an XML declaration (<?xml)
24
- const startsWithXml = buffer.subarray(0, 5).toString("utf8").trim().startsWith("<?xml");
25
- if (startsWithXml) {
29
+ const head = buffer.subarray(0, 1024).toString("utf8").replace(/[\x00-\x08\x0B\x0C\x0E-\x1F]/g, "").trim();
30
+ const isXml = /^<\?xml[\s\S]+<\w+[\s>]/.test(head);
31
+ if (isXml) {
26
32
  // Case 1: Already uncompressed XML - convert directly to string
27
33
  xml = buffer.toString("utf8");
28
34
  }
29
35
  else {
30
36
  // Case 2: Attempt to decompress as raw deflate data
31
- const inflated = (0, pako_1.inflateRaw)(buffer, { to: "string" });
32
- // Validate the decompressed content contains worksheet data
33
- if (inflated && inflated.includes("<sheetData")) {
34
- xml = inflated;
37
+ try {
38
+ xml = (await inflateRaw(buffer)).toString("utf8");
35
39
  }
36
- else {
37
- throw new Error("Decompressed data does not contain sheetData");
40
+ catch (err) {
41
+ throw new Error("Failed to decompress sheet XML: " + (err instanceof Error ? err.message : String(err)));
38
42
  }
39
43
  }
40
- // Fallback: If no XML obtained yet, try direct UTF-8 conversion
41
- if (!xml) {
42
- xml = buffer.toString("utf8");
43
- }
44
44
  // Sanitize XML by removing control characters (except tab, newline, carriage return)
45
45
  // This handles potential corruption from binary data or encoding issues
46
46
  xml = xml.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F]/g, "");
@@ -15,7 +15,8 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
17
  __exportStar(require("./build-merged-sheet.js"), exports);
18
+ __exportStar(require("./extract-rows-from-sheet-sync.js"), exports);
18
19
  __exportStar(require("./extract-rows-from-sheet.js"), exports);
20
+ __exportStar(require("./extract-xml-from-sheet-sync.js"), exports);
19
21
  __exportStar(require("./extract-xml-from-sheet.js"), exports);
20
- __exportStar(require("./extract-xml-from-system-content.js"), exports);
21
22
  __exportStar(require("./shift-row-indices.js"), exports);
@@ -0,0 +1,69 @@
1
+ import * as Utils from "./utils/index.js";
2
+ import * as Xml from "./xml/index.js";
3
+ /**
4
+ * Merges rows from other Excel files into a base Excel file.
5
+ *
6
+ * This function is a process-friendly version of mergeSheetsToBaseFile.
7
+ * It takes a single object with the following properties:
8
+ * - additions: An array of objects with two properties:
9
+ * - files: A dictionary of file paths to their corresponding XML content
10
+ * - sheetIndexes: The 1-based indexes of the sheet to extract rows from
11
+ * - baseFiles: A dictionary of file paths to their corresponding XML content
12
+ * - baseSheetIndex: The 1-based index of the sheet in the base file to add rows to
13
+ * - gap: The number of empty rows to insert between each added section
14
+ * - sheetNamesToRemove: The names of sheets to remove from the output file
15
+ * - sheetsToRemove: The 1-based indices of sheets to remove from the output file
16
+ *
17
+ * The function returns a dictionary of file paths to their corresponding XML content.
18
+ */
19
+ export function mergeSheetsToBaseFileProcessSync(data) {
20
+ const { additions, baseFiles, baseSheetIndex, gap, sheetNamesToRemove, sheetsToRemove, } = data;
21
+ const basePath = `xl/worksheets/sheet${baseSheetIndex}.xml`;
22
+ if (!baseFiles[basePath]) {
23
+ throw new Error(`Base file does not contain ${basePath}`);
24
+ }
25
+ const { lastRowNumber, mergeCells: baseMergeCells, rows: baseRows, xml, } = Xml.extractRowsFromSheetSync(baseFiles[basePath]);
26
+ const allRows = [...baseRows];
27
+ const allMergeCells = [...baseMergeCells];
28
+ let currentRowOffset = lastRowNumber + gap;
29
+ for (const { files, sheetIndexes } of additions) {
30
+ for (const sheetIndex of sheetIndexes) {
31
+ const sheetPath = `xl/worksheets/sheet${sheetIndex}.xml`;
32
+ if (!files[sheetPath]) {
33
+ throw new Error(`File does not contain ${sheetPath}`);
34
+ }
35
+ const { mergeCells, rows } = Xml.extractRowsFromSheetSync(files[sheetPath]);
36
+ const shiftedRows = Xml.shiftRowIndices(rows, currentRowOffset);
37
+ const shiftedMergeCells = mergeCells.map(cell => {
38
+ const [start, end] = cell.ref.split(":");
39
+ if (!start || !end) {
40
+ return cell;
41
+ }
42
+ const shiftedStart = Utils.shiftCellRef(start, currentRowOffset);
43
+ const shiftedEnd = Utils.shiftCellRef(end, currentRowOffset);
44
+ return { ...cell, ref: `${shiftedStart}:${shiftedEnd}` };
45
+ });
46
+ allRows.push(...shiftedRows);
47
+ allMergeCells.push(...shiftedMergeCells);
48
+ currentRowOffset += Utils.getMaxRowNumber(rows) + gap;
49
+ }
50
+ }
51
+ const mergedXml = Xml.buildMergedSheet(xml, allRows, allMergeCells);
52
+ baseFiles[basePath] = mergedXml;
53
+ for (const sheetIndex of sheetsToRemove) {
54
+ const sheetPath = `xl/worksheets/sheet${sheetIndex}.xml`;
55
+ delete baseFiles[sheetPath];
56
+ if (baseFiles["xl/workbook.xml"]) {
57
+ baseFiles["xl/workbook.xml"] = Buffer.from(Utils.removeSheetFromWorkbook(baseFiles["xl/workbook.xml"].toString(), sheetIndex));
58
+ }
59
+ if (baseFiles["xl/_rels/workbook.xml.rels"]) {
60
+ baseFiles["xl/_rels/workbook.xml.rels"] = Buffer.from(Utils.removeSheetFromRels(baseFiles["xl/_rels/workbook.xml.rels"].toString(), sheetIndex));
61
+ }
62
+ if (baseFiles["[Content_Types].xml"]) {
63
+ baseFiles["[Content_Types].xml"] = Buffer.from(Utils.removeSheetFromContentTypes(baseFiles["[Content_Types].xml"].toString(), sheetIndex));
64
+ }
65
+ }
66
+ for (const sheetName of sheetNamesToRemove) {
67
+ Utils.removeSheetByName(baseFiles, sheetName);
68
+ }
69
+ }