@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.
- package/build/cjs/lib/merge-sheets-to-base-file-process-sync.js +105 -0
- package/build/cjs/lib/merge-sheets-to-base-file-process.js +3 -3
- package/build/cjs/lib/merge-sheets-to-base-file-sync.js +2 -2
- package/build/cjs/lib/merge-sheets-to-base-file.js +1 -1
- package/build/cjs/lib/template/template-fs.js +97 -18
- package/build/cjs/lib/template/template-memory.js +110 -43
- package/build/cjs/lib/template/utils/compare-columns.js +16 -0
- package/build/cjs/lib/template/utils/index.js +1 -0
- package/build/cjs/lib/xml/extract-rows-from-sheet-sync.js +67 -0
- package/build/cjs/lib/xml/extract-rows-from-sheet.js +4 -2
- package/build/cjs/lib/xml/extract-xml-from-sheet-sync.js +43 -0
- package/build/cjs/lib/xml/extract-xml-from-sheet.js +15 -15
- package/build/cjs/lib/xml/index.js +2 -1
- package/build/esm/lib/merge-sheets-to-base-file-process-sync.js +69 -0
- package/build/esm/lib/merge-sheets-to-base-file-process.js +3 -3
- package/build/esm/lib/merge-sheets-to-base-file-sync.js +2 -2
- package/build/esm/lib/merge-sheets-to-base-file.js +1 -1
- package/build/esm/lib/template/template-fs.js +97 -18
- package/build/esm/lib/template/template-memory.js +110 -43
- package/build/esm/lib/template/utils/compare-columns.js +13 -0
- package/build/esm/lib/template/utils/index.js +1 -0
- package/build/esm/lib/xml/extract-rows-from-sheet-sync.js +64 -0
- package/build/esm/lib/xml/extract-rows-from-sheet.js +4 -2
- package/build/esm/lib/xml/extract-xml-from-sheet-sync.js +40 -0
- package/build/esm/lib/xml/extract-xml-from-sheet.js +12 -15
- package/build/esm/lib/xml/index.js +2 -1
- package/build/types/lib/merge-sheets-to-base-file-process-sync.d.ts +27 -0
- package/build/types/lib/merge-sheets-to-base-file-process.d.ts +1 -1
- package/build/types/lib/template/template-fs.d.ts +14 -0
- package/build/types/lib/template/template-memory.d.ts +4 -2
- package/build/types/lib/template/utils/compare-columns.d.ts +8 -0
- package/build/types/lib/template/utils/index.d.ts +1 -0
- package/build/types/lib/xml/extract-rows-from-sheet-sync.d.ts +28 -0
- package/build/types/lib/xml/extract-rows-from-sheet.d.ts +2 -2
- package/build/types/lib/xml/extract-xml-from-sheet-sync.d.ts +14 -0
- package/build/types/lib/xml/extract-xml-from-sheet.d.ts +2 -2
- package/build/types/lib/xml/index.d.ts +2 -1
- package/package.json +1 -5
- package/build/cjs/lib/xml/extract-xml-from-system-content.js +0 -53
- package/build/esm/lib/xml/extract-xml-from-system-content.js +0 -49
- 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
|
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
|
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
|
-
|
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
|
-
|
347
|
-
|
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
|
-
|
350
|
-
|
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
|
-
|
353
|
-
|
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
|
-
|
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] =
|
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"
|
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
|
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
|
25
|
-
|
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
|
-
|
32
|
-
|
33
|
-
if (inflated && inflated.includes("<sheetData")) {
|
34
|
-
xml = inflated;
|
37
|
+
try {
|
38
|
+
xml = (await inflateRaw(buffer)).toString("utf8");
|
35
39
|
}
|
36
|
-
|
37
|
-
throw new Error("
|
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
|
+
}
|