@js-ak/excel-toolbox 1.4.1 → 1.6.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 (31) hide show
  1. package/README.md +41 -62
  2. package/build/cjs/lib/template/index.js +1 -0
  3. package/build/cjs/lib/template/memory-write-stream.js +17 -0
  4. package/build/cjs/lib/template/template-fs.js +137 -57
  5. package/build/cjs/lib/template/template-memory.js +753 -0
  6. package/build/cjs/lib/template/utils/index.js +25 -0
  7. package/build/cjs/lib/template/utils/prepare-row-to-cells.js +17 -0
  8. package/build/cjs/lib/template/utils/regexp.js +32 -0
  9. package/build/cjs/lib/template/utils/update-dimension.js +15 -0
  10. package/build/cjs/lib/template/utils/validate-worksheet-xml.js +74 -74
  11. package/build/cjs/lib/template/utils/write-rows-to-stream.js +66 -22
  12. package/build/esm/lib/template/index.js +1 -0
  13. package/build/esm/lib/template/memory-write-stream.js +13 -0
  14. package/build/esm/lib/template/template-fs.js +134 -57
  15. package/build/esm/lib/template/template-memory.js +716 -0
  16. package/build/esm/lib/template/utils/index.js +2 -0
  17. package/build/esm/lib/template/utils/prepare-row-to-cells.js +14 -0
  18. package/build/esm/lib/template/utils/regexp.js +28 -0
  19. package/build/esm/lib/template/utils/update-dimension.js +15 -0
  20. package/build/esm/lib/template/utils/validate-worksheet-xml.js +74 -74
  21. package/build/esm/lib/template/utils/write-rows-to-stream.js +66 -22
  22. package/build/types/lib/template/index.d.ts +1 -0
  23. package/build/types/lib/template/memory-write-stream.d.ts +6 -0
  24. package/build/types/lib/template/template-fs.d.ts +2 -0
  25. package/build/types/lib/template/template-memory.d.ts +146 -0
  26. package/build/types/lib/template/utils/index.d.ts +2 -0
  27. package/build/types/lib/template/utils/prepare-row-to-cells.d.ts +5 -0
  28. package/build/types/lib/template/utils/regexp.d.ts +24 -0
  29. package/build/types/lib/template/utils/update-dimension.d.ts +15 -0
  30. package/build/types/lib/template/utils/write-rows-to-stream.d.ts +28 -11
  31. package/package.json +5 -3
@@ -0,0 +1,753 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.TemplateMemory = void 0;
37
+ const fs = __importStar(require("node:fs/promises"));
38
+ const Xml = __importStar(require("../xml/index.js"));
39
+ const Zip = __importStar(require("../zip/index.js"));
40
+ const Utils = __importStar(require("./utils/index.js"));
41
+ const memory_write_stream_js_1 = require("./memory-write-stream.js");
42
+ /**
43
+ * A class for manipulating Excel templates by extracting, modifying, and repacking Excel files.
44
+ *
45
+ * @experimental This API is experimental and might change in future versions.
46
+ */
47
+ class TemplateMemory {
48
+ files;
49
+ /**
50
+ * Flag indicating whether this template instance has been destroyed.
51
+ * @type {boolean}
52
+ */
53
+ destroyed = false;
54
+ /**
55
+ * Flag indicating whether this template instance is currently being processed.
56
+ * @type {boolean}
57
+ */
58
+ #isProcessing = false;
59
+ /**
60
+ * The keys for the Excel files in the template.
61
+ */
62
+ #excelKeys = {
63
+ contentTypes: "[Content_Types].xml",
64
+ sharedStrings: "xl/sharedStrings.xml",
65
+ styles: "xl/styles.xml",
66
+ workbook: "xl/workbook.xml",
67
+ workbookRels: "xl/_rels/workbook.xml.rels",
68
+ };
69
+ /**
70
+ * Creates a Template instance from a map of file paths to buffers.
71
+ *
72
+ * @param {Object<string, Buffer>} files - The files to create the template from.
73
+ * @throws {Error} If reading or writing files fails.
74
+ * @experimental This API is experimental and might change in future versions.
75
+ */
76
+ constructor(files) {
77
+ this.files = files;
78
+ }
79
+ /**
80
+ * Ensures that this Template instance has not been destroyed.
81
+ * @private
82
+ * @throws {Error} If the template instance has already been destroyed.
83
+ * @experimental This API is experimental and might change in future versions.
84
+ */
85
+ #ensureNotDestroyed() {
86
+ if (this.destroyed) {
87
+ throw new Error("This Template instance has already been saved and destroyed.");
88
+ }
89
+ }
90
+ /**
91
+ * Ensures that this Template instance is not currently being processed.
92
+ * @throws {Error} If the template instance is currently being processed.
93
+ * @experimental This API is experimental and might change in future versions.
94
+ */
95
+ #ensureNotProcessing() {
96
+ if (this.#isProcessing) {
97
+ throw new Error("This Template instance is currently being processed.");
98
+ }
99
+ }
100
+ /**
101
+ * Expand table rows in the given sheet and shared strings XML.
102
+ *
103
+ * @param {string} sheetXml - The XML content of the sheet.
104
+ * @param {string} sharedStringsXml - The XML content of the shared strings.
105
+ * @param {Record<string, unknown>} replacements - An object containing replacement values.
106
+ *
107
+ * @returns {Object} An object with two properties:
108
+ * - sheet: The expanded sheet XML.
109
+ * - shared: The expanded shared strings XML.
110
+ * @experimental This API is experimental and might change in future versions.
111
+ */
112
+ #expandTableRows(sheetXml, sharedStringsXml, replacements) {
113
+ const { initialMergeCells, mergeCellMatches, modifiedXml, } = Utils.processMergeCells(sheetXml);
114
+ const { sharedIndexMap, sharedStrings, sharedStringsHeader, sheetMergeCells, } = Utils.processSharedStrings(sharedStringsXml);
115
+ const { lastIndex, resultRows, rowShift } = Utils.processRows({
116
+ mergeCellMatches,
117
+ replacements,
118
+ sharedIndexMap,
119
+ sharedStrings,
120
+ sheetMergeCells,
121
+ sheetXml: modifiedXml,
122
+ });
123
+ return Utils.processMergeFinalize({
124
+ initialMergeCells,
125
+ lastIndex,
126
+ mergeCellMatches,
127
+ resultRows,
128
+ rowShift,
129
+ sharedStrings,
130
+ sharedStringsHeader,
131
+ sheetMergeCells,
132
+ sheetXml: modifiedXml,
133
+ });
134
+ }
135
+ /**
136
+ * Extracts the XML content from an Excel sheet file.
137
+ *
138
+ * @param {string} fileKey - The file key of the sheet to extract.
139
+ * @returns {string} The XML content of the sheet.
140
+ * @throws {Error} If the file key is not found.
141
+ * @experimental This API is experimental and might change in future versions.
142
+ */
143
+ #extractXmlFromSheet(fileKey) {
144
+ if (!this.files[fileKey]) {
145
+ throw new Error(`${fileKey} not found`);
146
+ }
147
+ return Xml.extractXmlFromSheet(this.files[fileKey]);
148
+ }
149
+ /**
150
+ * Extracts row data from an Excel sheet file.
151
+ *
152
+ * @param {string} fileKey - The file key of the sheet to extract.
153
+ * @returns {Object} An object containing:
154
+ * - rows: Array of raw XML strings for each <row> element
155
+ * - lastRowNumber: Highest row number found in the sheet (1-based)
156
+ * - mergeCells: Array of merged cell ranges (e.g., [{ref: "A1:B2"}])
157
+ * - xml: The XML content of the sheet
158
+ * @throws {Error} If the file key is not found
159
+ * @experimental This API is experimental and might change in future versions.
160
+ */
161
+ #extractRowsFromSheet(fileKey) {
162
+ if (!this.files[fileKey]) {
163
+ throw new Error(`${fileKey} not found`);
164
+ }
165
+ return Xml.extractRowsFromSheet(this.files[fileKey]);
166
+ }
167
+ /**
168
+ * Returns the Excel path of the sheet with the given name.
169
+ *
170
+ * @param sheetName - The name of the sheet to find.
171
+ * @returns The Excel path of the sheet.
172
+ * @throws {Error} If the sheet with the given name does not exist.
173
+ * @experimental This API is experimental and might change in future versions.
174
+ */
175
+ #getSheetPathByName(sheetName) {
176
+ // Find the sheet
177
+ const workbookXml = this.#extractXmlFromSheet(this.#excelKeys.workbook);
178
+ const sheetMatch = workbookXml.match(Utils.sheetMatch(sheetName));
179
+ if (!sheetMatch || !sheetMatch[1]) {
180
+ throw new Error(`Sheet "${sheetName}" not found`);
181
+ }
182
+ const rId = sheetMatch[1];
183
+ const relsXml = this.#extractXmlFromSheet(this.#excelKeys.workbookRels);
184
+ const relMatch = relsXml.match(Utils.relationshipMatch(rId));
185
+ if (!relMatch || !relMatch[1]) {
186
+ throw new Error(`Relationship "${rId}" not found`);
187
+ }
188
+ return "xl/" + relMatch[1].replace(/^\/?xl\//, "");
189
+ }
190
+ /**
191
+ * Returns the Excel path of the sheet with the given ID.
192
+ *
193
+ * @param {number} id - The 1-based index of the sheet to find.
194
+ * @returns {string} The Excel path of the sheet.
195
+ * @throws {Error} If the sheet index is less than 1.
196
+ * @experimental This API is experimental and might change in future versions.
197
+ */
198
+ #getSheetPathById(id) {
199
+ if (id < 1) {
200
+ throw new Error("Sheet index must be greater than 0");
201
+ }
202
+ return `xl/worksheets/sheet${id}.xml`;
203
+ }
204
+ /**
205
+ * Replaces the contents of a file in the template.
206
+ *
207
+ * @param {string} key - The Excel path of the file to replace.
208
+ * @param {Buffer|string} content - The new content.
209
+ * @returns {Promise<void>}
210
+ * @throws {Error} If the template instance has been destroyed.
211
+ * @throws {Error} If the file does not exist in the template.
212
+ * @experimental This API is experimental and might change in future versions.
213
+ */
214
+ async #set(key, content) {
215
+ this.files[key] = content;
216
+ }
217
+ /**
218
+ * Replaces placeholders in the given sheet with values from the replacements map.
219
+ *
220
+ * The function searches for placeholders in the format `${key}` within the sheet
221
+ * content, where `key` corresponds to a path in the replacements object.
222
+ * If a value is found for the key, it replaces the placeholder with the value.
223
+ * If no value is found, the original placeholder remains unchanged.
224
+ *
225
+ * @param sheetName - The name of the sheet to be replaced.
226
+ * @param replacements - An object where keys represent placeholder paths and values are the replacements.
227
+ * @returns A promise that resolves when the substitution is complete.
228
+ * @throws {Error} If the template instance has been destroyed.
229
+ * @experimental This API is experimental and might change in future versions.
230
+ */
231
+ async #substitute(sheetName, replacements) {
232
+ const sharedStringsPath = this.#excelKeys.sharedStrings;
233
+ let sharedStringsContent = "";
234
+ let sheetContent = "";
235
+ if (this.files[sharedStringsPath]) {
236
+ sharedStringsContent = this.#extractXmlFromSheet(sharedStringsPath);
237
+ }
238
+ const sheetPath = this.#getSheetPathByName(sheetName);
239
+ if (this.files[sheetPath]) {
240
+ sheetContent = this.#extractXmlFromSheet(sheetPath);
241
+ const TABLE_REGEX = /\$\{table:([a-zA-Z0-9_]+)\.([a-zA-Z0-9_]+)\}/g;
242
+ const hasTablePlaceholders = TABLE_REGEX.test(sharedStringsContent) || TABLE_REGEX.test(sheetContent);
243
+ if (hasTablePlaceholders) {
244
+ const result = this.#expandTableRows(sheetContent, sharedStringsContent, replacements);
245
+ sheetContent = result.sheet;
246
+ sharedStringsContent = result.shared;
247
+ }
248
+ }
249
+ if (this.files[sharedStringsPath]) {
250
+ sharedStringsContent = Utils.applyReplacements(sharedStringsContent, replacements);
251
+ await this.#set(sharedStringsPath, Buffer.from(sharedStringsContent));
252
+ }
253
+ if (this.files[sheetPath]) {
254
+ sheetContent = Utils.applyReplacements(sheetContent, replacements);
255
+ await this.#set(sheetPath, Buffer.from(sheetContent));
256
+ }
257
+ }
258
+ /**
259
+ * Merges rows from other sheets into a base sheet.
260
+ *
261
+ * @param {Object} data
262
+ * @param {Object} data.additions
263
+ * @param {number[]} [data.additions.sheetIndexes] - The 1-based indexes of the sheets to extract rows from.
264
+ * @param {string[]} [data.additions.sheetNames] - The names of the sheets to extract rows from.
265
+ * @param {number} [data.baseSheetIndex=1] - The 1-based index of the sheet in the workbook to add rows to.
266
+ * @param {string} [data.baseSheetName] - The name of the sheet in the workbook to add rows to.
267
+ * @param {number} [data.gap=0] - The number of empty rows to insert between each added section.
268
+ * @throws {Error} If the base sheet index is less than 1.
269
+ * @throws {Error} If the base sheet name is not found.
270
+ * @throws {Error} If the sheet index is less than 1.
271
+ * @throws {Error} If the sheet name is not found.
272
+ * @throws {Error} If no sheets are found to merge.
273
+ * @experimental This API is experimental and might change in future versions.
274
+ */
275
+ #mergeSheets(data) {
276
+ const { additions, baseSheetIndex = 1, baseSheetName, gap = 0, } = data;
277
+ let fileKey = "";
278
+ if (baseSheetName) {
279
+ fileKey = this.#getSheetPathByName(baseSheetName);
280
+ }
281
+ if (baseSheetIndex && !fileKey) {
282
+ if (baseSheetIndex < 1) {
283
+ throw new Error("Base sheet index must be greater than 0");
284
+ }
285
+ fileKey = `xl/worksheets/sheet${baseSheetIndex}.xml`;
286
+ }
287
+ if (!fileKey) {
288
+ throw new Error("Base sheet not found");
289
+ }
290
+ const { lastRowNumber, mergeCells: baseMergeCells, rows: baseRows, xml, } = this.#extractRowsFromSheet(fileKey);
291
+ const allRows = [...baseRows];
292
+ const allMergeCells = [...baseMergeCells];
293
+ let currentRowOffset = lastRowNumber + gap;
294
+ const sheetPaths = [];
295
+ if (additions.sheetIndexes) {
296
+ sheetPaths.push(...(additions.sheetIndexes).map(e => this.#getSheetPathById(e)));
297
+ }
298
+ if (additions.sheetNames) {
299
+ sheetPaths.push(...(additions.sheetNames).map(e => this.#getSheetPathByName(e)));
300
+ }
301
+ if (sheetPaths.length === 0) {
302
+ throw new Error("No sheets found to merge");
303
+ }
304
+ for (const sheetPath of sheetPaths) {
305
+ if (!this.files[sheetPath]) {
306
+ throw new Error(`Sheet "${sheetPath}" not found`);
307
+ }
308
+ const { mergeCells, rows } = Xml.extractRowsFromSheet(this.files[sheetPath]);
309
+ const shiftedRows = Xml.shiftRowIndices(rows, currentRowOffset);
310
+ const shiftedMergeCells = mergeCells.map(cell => {
311
+ const [start, end] = cell.ref.split(":");
312
+ if (!start || !end) {
313
+ return cell;
314
+ }
315
+ const shiftedStart = Utils.Common.shiftCellRef(start, currentRowOffset);
316
+ const shiftedEnd = Utils.Common.shiftCellRef(end, currentRowOffset);
317
+ return { ...cell, ref: `${shiftedStart}:${shiftedEnd}` };
318
+ });
319
+ allRows.push(...shiftedRows);
320
+ allMergeCells.push(...shiftedMergeCells);
321
+ currentRowOffset += Utils.Common.getMaxRowNumber(rows) + gap;
322
+ }
323
+ const mergedXml = Xml.buildMergedSheet(xml, allRows, allMergeCells);
324
+ this.#set(fileKey, mergedXml);
325
+ }
326
+ /**
327
+ * Removes sheets from the workbook.
328
+ *
329
+ * @param {Object} data - The data for sheet removal.
330
+ * @param {number[]} [data.sheetIndexes] - The 1-based indexes of the sheets to remove.
331
+ * @param {string[]} [data.sheetNames] - The names of the sheets to remove.
332
+ * @returns {void}
333
+ *
334
+ * @throws {Error} If the template instance has been destroyed.
335
+ * @throws {Error} If the sheet does not exist.
336
+ * @experimental This API is experimental and might change in future versions.
337
+ */
338
+ #removeSheets(data) {
339
+ const { sheetIndexes = [], sheetNames = [] } = data;
340
+ for (const sheetIndex of sheetIndexes) {
341
+ const sheetPath = `xl/worksheets/sheet${sheetIndex}.xml`;
342
+ if (!this.files[sheetPath]) {
343
+ continue;
344
+ }
345
+ 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));
348
+ }
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));
351
+ }
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));
354
+ }
355
+ }
356
+ for (const sheetName of sheetNames) {
357
+ Utils.Common.removeSheetByName(this.files, sheetName);
358
+ }
359
+ }
360
+ /**
361
+ * Copies a sheet from the template to a new name.
362
+ *
363
+ * @param {string} sourceName - The name of the sheet to copy.
364
+ * @param {string} newName - The new name for the sheet.
365
+ * @returns {Promise<void>}
366
+ * @throws {Error} If the sheet with the source name does not exist.
367
+ * @throws {Error} If a sheet with the new name already exists.
368
+ * @experimental This API is experimental and might change in future versions.
369
+ */
370
+ async copySheet(sourceName, newName) {
371
+ this.#ensureNotProcessing();
372
+ this.#ensureNotDestroyed();
373
+ this.#isProcessing = true;
374
+ try {
375
+ if (sourceName === newName) {
376
+ throw new Error("Source and new sheet names cannot be the same");
377
+ }
378
+ // Read workbook.xml and find the source sheet
379
+ const workbookXmlPath = this.#excelKeys.workbook;
380
+ const workbookXml = this.#extractXmlFromSheet(this.#excelKeys.workbook);
381
+ // Find the source sheet
382
+ const sheetMatch = workbookXml.match(Utils.sheetMatch(sourceName));
383
+ if (!sheetMatch || !sheetMatch[1]) {
384
+ throw new Error(`Sheet "${sourceName}" not found`);
385
+ }
386
+ // Check if a sheet with the new name already exists
387
+ if (new RegExp(`<sheet[^>]+name="${newName}"`).test(workbookXml)) {
388
+ throw new Error(`Sheet "${newName}" already exists`);
389
+ }
390
+ // Read workbook.rels
391
+ // Find the source sheet path by rId
392
+ const rId = sheetMatch[1];
393
+ const relsXmlPath = this.#excelKeys.workbookRels;
394
+ const relsXml = this.#extractXmlFromSheet(this.#excelKeys.workbookRels);
395
+ const relMatch = relsXml.match(Utils.relationshipMatch(rId));
396
+ if (!relMatch || !relMatch[1]) {
397
+ throw new Error(`Relationship "${rId}" not found`);
398
+ }
399
+ const sourceTarget = relMatch[1]; // sheetN.xml
400
+ const sourceSheetPath = "xl/" + sourceTarget.replace(/^\/?.*xl\//, "");
401
+ // Get the index of the new sheet
402
+ const sheetNumbers = Array.from(Object.keys(this.files))
403
+ .map((key) => key.match(/^xl\/worksheets\/sheet(\d+)\.xml$/))
404
+ .filter(Boolean)
405
+ .map((match) => parseInt(match[1], 10));
406
+ const nextSheetIndex = sheetNumbers.length > 0 ? Math.max(...sheetNumbers) + 1 : 1;
407
+ const newSheetFilename = `sheet${nextSheetIndex}.xml`;
408
+ const newSheetPath = `xl/worksheets/${newSheetFilename}`;
409
+ const newTarget = `worksheets/${newSheetFilename}`;
410
+ // Generate a unique rId
411
+ const usedRIds = [...relsXml.matchAll(/Id="(rId\d+)"/g)].map(m => m[1]);
412
+ let nextRIdNum = 1;
413
+ while (usedRIds.includes(`rId${nextRIdNum}`))
414
+ nextRIdNum++;
415
+ const newRId = `rId${nextRIdNum}`;
416
+ // Copy the source sheet file
417
+ const sheetContent = this.files[sourceSheetPath];
418
+ if (!sheetContent) {
419
+ throw new Error(`Sheet "${sourceSheetPath}" not found`);
420
+ }
421
+ function copyBuffer(source) {
422
+ const target = Buffer.alloc(source.length);
423
+ source.copy(target);
424
+ return target;
425
+ }
426
+ await this.#set(newSheetPath, copyBuffer(sheetContent));
427
+ // Update workbook.xml
428
+ const updatedWorkbookXml = workbookXml.replace("</sheets>", `<sheet name="${newName}" sheetId="${nextSheetIndex}" r:id="${newRId}"/></sheets>`);
429
+ await this.#set(workbookXmlPath, Buffer.from(updatedWorkbookXml));
430
+ // Update workbook.xml.rels
431
+ const updatedRelsXml = relsXml.replace("</Relationships>", `<Relationship Id="${newRId}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="${newTarget}"/></Relationships>`);
432
+ await this.#set(relsXmlPath, Buffer.from(updatedRelsXml));
433
+ // Read [Content_Types].xml
434
+ // Update [Content_Types].xml
435
+ const contentTypesPath = "[Content_Types].xml";
436
+ const contentTypesXml = this.#extractXmlFromSheet(contentTypesPath);
437
+ const overrideTag = `<Override PartName="/xl/worksheets/${newSheetFilename}" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>`;
438
+ const updatedContentTypesXml = contentTypesXml.replace("</Types>", overrideTag + "</Types>");
439
+ await this.#set(contentTypesPath, Buffer.from(updatedContentTypesXml));
440
+ }
441
+ finally {
442
+ this.#isProcessing = false;
443
+ }
444
+ }
445
+ /**
446
+ * Replaces placeholders in the given sheet with values from the replacements map.
447
+ *
448
+ * The function searches for placeholders in the format `${key}` within the sheet
449
+ * content, where `key` corresponds to a path in the replacements object.
450
+ * If a value is found for the key, it replaces the placeholder with the value.
451
+ * If no value is found, the original placeholder remains unchanged.
452
+ *
453
+ * @param sheetName - The name of the sheet to be replaced.
454
+ * @param replacements - An object where keys represent placeholder paths and values are the replacements.
455
+ * @returns A promise that resolves when the substitution is complete.
456
+ */
457
+ substitute(sheetName, replacements) {
458
+ this.#ensureNotProcessing();
459
+ this.#ensureNotDestroyed();
460
+ this.#isProcessing = true;
461
+ try {
462
+ return this.#substitute(sheetName, replacements);
463
+ }
464
+ finally {
465
+ this.#isProcessing = false;
466
+ }
467
+ }
468
+ /**
469
+ * Inserts rows into a specific sheet in the template.
470
+ *
471
+ * @param {Object} data - The data for row insertion.
472
+ * @param {string} data.sheetName - The name of the sheet to insert rows into.
473
+ * @param {number} [data.startRowNumber] - The row number to start inserting from.
474
+ * @param {unknown[][]} data.rows - The rows to insert.
475
+ * @returns {Promise<void>}
476
+ * @throws {Error} If the template instance has been destroyed.
477
+ * @throws {Error} If the sheet does not exist.
478
+ * @throws {Error} If the row number is out of range.
479
+ * @throws {Error} If a column is out of range.
480
+ * @experimental This API is experimental and might change in future versions.
481
+ */
482
+ async insertRows(data) {
483
+ this.#ensureNotProcessing();
484
+ this.#ensureNotDestroyed();
485
+ this.#isProcessing = true;
486
+ try {
487
+ const { rows, sheetName, startRowNumber } = data;
488
+ const preparedRows = rows.map(row => Utils.toExcelColumnObject(row));
489
+ // Validation
490
+ Utils.checkStartRow(startRowNumber);
491
+ Utils.checkRows(preparedRows);
492
+ // Find the sheet
493
+ const sheetPath = this.#getSheetPathByName(sheetName);
494
+ const sheetXml = this.#extractXmlFromSheet(sheetPath);
495
+ let nextRow = 0;
496
+ if (!startRowNumber) {
497
+ // Find the last row
498
+ let lastRowNumber = 0;
499
+ const rowMatches = [...sheetXml.matchAll(/<row[^>]+r="(\d+)"[^>]*>/g)];
500
+ if (rowMatches.length > 0) {
501
+ lastRowNumber = Math.max(...rowMatches.map((m) => parseInt(m[1], 10)));
502
+ }
503
+ nextRow = lastRowNumber + 1;
504
+ }
505
+ else {
506
+ nextRow = startRowNumber;
507
+ }
508
+ // Generate XML for all rows
509
+ const rowsXml = preparedRows.map((cells, i) => {
510
+ const rowNumber = nextRow + i;
511
+ const cellTags = Object.entries(cells).map(([col, value]) => {
512
+ const colUpper = col.toUpperCase();
513
+ const ref = `${colUpper}${rowNumber}`;
514
+ return `<c r="${ref}" t="inlineStr"><is><t>${Utils.escapeXml(value)}</t></is></c>`;
515
+ }).join("");
516
+ return `<row r="${rowNumber}">${cellTags}</row>`;
517
+ }).join("");
518
+ let updatedXml;
519
+ if (/<sheetData\s*\/>/.test(sheetXml)) {
520
+ updatedXml = sheetXml.replace(/<sheetData\s*\/>/, `<sheetData>${rowsXml}</sheetData>`);
521
+ }
522
+ else if (/<sheetData>([\s\S]*?)<\/sheetData>/.test(sheetXml)) {
523
+ updatedXml = sheetXml.replace(/<\/sheetData>/, `${rowsXml}</sheetData>`);
524
+ }
525
+ else {
526
+ updatedXml = sheetXml.replace(/<worksheet[^>]*>/, (match) => `${match}<sheetData>${rowsXml}</sheetData>`);
527
+ }
528
+ await this.#set(sheetPath, Buffer.from(updatedXml));
529
+ }
530
+ finally {
531
+ this.#isProcessing = false;
532
+ }
533
+ }
534
+ /**
535
+ * Inserts rows into a specific sheet in the template using an async stream.
536
+ *
537
+ * @param {Object} data - The data for row insertion.
538
+ * @param {string} data.sheetName - The name of the sheet to insert rows into.
539
+ * @param {number} [data.startRowNumber] - The row number to start inserting from.
540
+ * @param {AsyncIterable<unknown[]>} data.rows - Async iterable of rows to insert.
541
+ * @returns {Promise<void>}
542
+ * @throws {Error} If the template instance has been destroyed.
543
+ * @throws {Error} If the sheet does not exist.
544
+ * @throws {Error} If the row number is out of range.
545
+ * @throws {Error} If a column is out of range.
546
+ * @experimental This API is experimental and might change in future versions.
547
+ */
548
+ async insertRowsStream(data) {
549
+ this.#ensureNotProcessing();
550
+ this.#ensureNotDestroyed();
551
+ this.#isProcessing = true;
552
+ try {
553
+ const { rows, sheetName, startRowNumber } = data;
554
+ if (!sheetName)
555
+ throw new Error("Sheet name is required");
556
+ // Read XML workbook to find sheet name and path
557
+ const sheetPath = this.#getSheetPathByName(sheetName);
558
+ const sheetXml = this.#extractXmlFromSheet(sheetPath);
559
+ const output = new memory_write_stream_js_1.MemoryWriteStream();
560
+ let inserted = false;
561
+ // --- Case 1: <sheetData>...</sheetData> on one line ---
562
+ const singleLineMatch = sheetXml.match(/(<sheetData[^>]*>)(.*)(<\/sheetData>)/);
563
+ if (!inserted && singleLineMatch) {
564
+ const maxRowNumber = startRowNumber ?? Utils.getMaxRowNumber(sheetXml);
565
+ const openTag = singleLineMatch[1];
566
+ const innerRows = singleLineMatch[2].trim();
567
+ const closeTag = singleLineMatch[3];
568
+ const innerRowsMap = Utils.parseRows(innerRows);
569
+ output.write(sheetXml.slice(0, singleLineMatch.index));
570
+ output.write(openTag);
571
+ if (innerRows) {
572
+ if (startRowNumber) {
573
+ const filtered = Utils.getRowsBelow(innerRowsMap, startRowNumber);
574
+ if (filtered)
575
+ output.write(filtered);
576
+ }
577
+ else {
578
+ output.write(innerRows);
579
+ }
580
+ }
581
+ const { rowNumber: actualRowNumber } = await Utils.writeRowsToStream(output, rows, maxRowNumber);
582
+ if (innerRows) {
583
+ const filtered = Utils.getRowsAbove(innerRowsMap, actualRowNumber);
584
+ if (filtered)
585
+ output.write(filtered);
586
+ }
587
+ output.write(closeTag);
588
+ output.write(sheetXml.slice(singleLineMatch.index + singleLineMatch[0].length));
589
+ inserted = true;
590
+ }
591
+ // --- Case 2: <sheetData/> ---
592
+ if (!inserted && /<sheetData\s*\/>/.test(sheetXml)) {
593
+ const maxRowNumber = startRowNumber ?? Utils.getMaxRowNumber(sheetXml);
594
+ const match = sheetXml.match(/<sheetData\s*\/>/);
595
+ const matchIndex = match.index;
596
+ output.write(sheetXml.slice(0, matchIndex));
597
+ output.write("<sheetData>");
598
+ await Utils.writeRowsToStream(output, rows, maxRowNumber);
599
+ output.write("</sheetData>");
600
+ output.write(sheetXml.slice(matchIndex + match[0].length));
601
+ inserted = true;
602
+ }
603
+ // --- Case 3: Multiline <sheetData> ---
604
+ if (!inserted && sheetXml.includes("<sheetData")) {
605
+ const openTagMatch = sheetXml.match(/<sheetData[^>]*>/);
606
+ const closeTag = "</sheetData>";
607
+ if (!openTagMatch)
608
+ throw new Error("Invalid sheetData structure");
609
+ const openTag = openTagMatch[0];
610
+ const openIdx = sheetXml.indexOf(openTag);
611
+ const closeIdx = sheetXml.lastIndexOf(closeTag);
612
+ if (closeIdx === -1)
613
+ throw new Error("Missing </sheetData>");
614
+ const beforeRows = sheetXml.slice(0, openIdx + openTag.length);
615
+ const innerRows = sheetXml.slice(openIdx + openTag.length, closeIdx).trim();
616
+ const afterRows = sheetXml.slice(closeIdx + closeTag.length);
617
+ const innerRowsMap = Utils.parseRows(innerRows);
618
+ output.write(beforeRows);
619
+ if (innerRows) {
620
+ if (startRowNumber) {
621
+ const filtered = Utils.getRowsBelow(innerRowsMap, startRowNumber);
622
+ if (filtered)
623
+ output.write(filtered);
624
+ }
625
+ else {
626
+ output.write(innerRows);
627
+ }
628
+ }
629
+ const { rowNumber: actualRowNumber } = await Utils.writeRowsToStream(output, rows, Utils.getMaxRowNumber(innerRows));
630
+ if (innerRows) {
631
+ const filtered = Utils.getRowsAbove(innerRowsMap, actualRowNumber);
632
+ if (filtered)
633
+ output.write(filtered);
634
+ }
635
+ output.write(closeTag);
636
+ output.write(afterRows);
637
+ inserted = true;
638
+ }
639
+ if (!inserted)
640
+ throw new Error("Failed to locate <sheetData> for insertion");
641
+ // Save the buffer to the sheet
642
+ this.files[sheetPath] = output.toBuffer();
643
+ }
644
+ finally {
645
+ this.#isProcessing = false;
646
+ }
647
+ }
648
+ /**
649
+ * Saves the modified Excel template to a buffer.
650
+ *
651
+ * @returns {Promise<Buffer>} The modified Excel template as a buffer.
652
+ * @throws {Error} If the template instance has been destroyed.
653
+ * @experimental This API is experimental and might change in future versions.
654
+ */
655
+ async save() {
656
+ this.#ensureNotProcessing();
657
+ this.#ensureNotDestroyed();
658
+ this.#isProcessing = true;
659
+ try {
660
+ const zipBuffer = await Zip.create(this.files);
661
+ this.destroyed = true;
662
+ // Clear all buffers
663
+ for (const key in this.files) {
664
+ if (this.files.hasOwnProperty(key)) {
665
+ this.files[key] = Buffer.alloc(0); // Clear the buffer
666
+ }
667
+ }
668
+ return zipBuffer;
669
+ }
670
+ finally {
671
+ this.#isProcessing = false;
672
+ }
673
+ }
674
+ /**
675
+ * Replaces the contents of a file in the template.
676
+ *
677
+ * @param {string} key - The Excel path of the file to replace.
678
+ * @param {Buffer|string} content - The new content.
679
+ * @returns {Promise<void>}
680
+ * @throws {Error} If the template instance has been destroyed.
681
+ * @throws {Error} If the file does not exist in the template.
682
+ * @experimental This API is experimental and might change in future versions.
683
+ */
684
+ async set(key, content) {
685
+ this.#ensureNotProcessing();
686
+ this.#ensureNotDestroyed();
687
+ this.#isProcessing = true;
688
+ try {
689
+ await this.#set(key, content);
690
+ }
691
+ finally {
692
+ this.#isProcessing = false;
693
+ }
694
+ }
695
+ /**
696
+ * Merges sheets into a base sheet.
697
+ *
698
+ * @param {Object} data
699
+ * @param {{ sheetIndexes?: number[]; sheetNames?: string[] }} data.additions - The sheets to merge.
700
+ * @param {number} [data.baseSheetIndex=1] - The 1-based index of the sheet to merge into.
701
+ * @param {string} [data.baseSheetName] - The name of the sheet to merge into.
702
+ * @param {number} [data.gap=0] - The number of empty rows to insert between each added section.
703
+ * @returns {void}
704
+ */
705
+ mergeSheets(data) {
706
+ this.#ensureNotProcessing();
707
+ this.#ensureNotDestroyed();
708
+ this.#isProcessing = true;
709
+ try {
710
+ this.#mergeSheets(data);
711
+ }
712
+ finally {
713
+ this.#isProcessing = false;
714
+ }
715
+ }
716
+ /**
717
+ * Removes sheets from the workbook.
718
+ *
719
+ * @param {Object} data
720
+ * @param {number[]} [data.sheetIndexes] - The 1-based indexes of the sheets to remove.
721
+ * @param {string[]} [data.sheetNames] - The names of the sheets to remove.
722
+ * @returns {void}
723
+ */
724
+ removeSheets(data) {
725
+ this.#ensureNotProcessing();
726
+ this.#ensureNotDestroyed();
727
+ this.#isProcessing = true;
728
+ try {
729
+ this.#removeSheets(data);
730
+ }
731
+ finally {
732
+ this.#isProcessing = false;
733
+ }
734
+ }
735
+ /**
736
+ * Creates a Template instance from an Excel file source.
737
+ *
738
+ * @param {Object} data - The data to create the template from.
739
+ * @param {string | Buffer} data.source - The path or buffer of the Excel file.
740
+ * @returns {Promise<TemplateMemory>} A new Template instance.
741
+ * @throws {Error} If reading the file fails.
742
+ * @experimental This API is experimental and might change in future versions.
743
+ */
744
+ static async from(data) {
745
+ const { source } = data;
746
+ const buffer = typeof source === "string"
747
+ ? await fs.readFile(source)
748
+ : source;
749
+ const files = await Zip.read(buffer);
750
+ return new TemplateMemory(files);
751
+ }
752
+ }
753
+ exports.TemplateMemory = TemplateMemory;