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