@js-ak/excel-toolbox 1.5.0 → 1.7.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 (53) hide show
  1. package/README.md +41 -62
  2. package/build/cjs/lib/merge-sheets-to-base-file-process-sync.js +105 -0
  3. package/build/cjs/lib/merge-sheets-to-base-file-process.js +3 -3
  4. package/build/cjs/lib/merge-sheets-to-base-file-sync.js +2 -2
  5. package/build/cjs/lib/merge-sheets-to-base-file.js +1 -1
  6. package/build/cjs/lib/template/template-fs.js +143 -63
  7. package/build/cjs/lib/template/template-memory.js +281 -59
  8. package/build/cjs/lib/template/utils/index.js +25 -0
  9. package/build/cjs/lib/template/utils/prepare-row-to-cells.js +5 -1
  10. package/build/cjs/lib/template/utils/regexp.js +32 -0
  11. package/build/cjs/lib/template/utils/update-dimension.js +15 -0
  12. package/build/cjs/lib/template/utils/validate-worksheet-xml.js +74 -74
  13. package/build/cjs/lib/template/utils/write-rows-to-stream.js +57 -17
  14. package/build/cjs/lib/xml/extract-rows-from-sheet-sync.js +67 -0
  15. package/build/cjs/lib/xml/extract-rows-from-sheet.js +4 -2
  16. package/build/cjs/lib/xml/extract-xml-from-sheet-sync.js +43 -0
  17. package/build/cjs/lib/xml/extract-xml-from-sheet.js +15 -15
  18. package/build/cjs/lib/xml/index.js +2 -1
  19. package/build/esm/lib/merge-sheets-to-base-file-process-sync.js +69 -0
  20. package/build/esm/lib/merge-sheets-to-base-file-process.js +3 -3
  21. package/build/esm/lib/merge-sheets-to-base-file-sync.js +2 -2
  22. package/build/esm/lib/merge-sheets-to-base-file.js +1 -1
  23. package/build/esm/lib/template/template-fs.js +140 -63
  24. package/build/esm/lib/template/template-memory.js +281 -59
  25. package/build/esm/lib/template/utils/index.js +2 -0
  26. package/build/esm/lib/template/utils/prepare-row-to-cells.js +5 -1
  27. package/build/esm/lib/template/utils/regexp.js +28 -0
  28. package/build/esm/lib/template/utils/update-dimension.js +15 -0
  29. package/build/esm/lib/template/utils/validate-worksheet-xml.js +74 -74
  30. package/build/esm/lib/template/utils/write-rows-to-stream.js +57 -17
  31. package/build/esm/lib/xml/extract-rows-from-sheet-sync.js +64 -0
  32. package/build/esm/lib/xml/extract-rows-from-sheet.js +4 -2
  33. package/build/esm/lib/xml/extract-xml-from-sheet-sync.js +40 -0
  34. package/build/esm/lib/xml/extract-xml-from-sheet.js +12 -15
  35. package/build/esm/lib/xml/index.js +2 -1
  36. package/build/types/lib/merge-sheets-to-base-file-process-sync.d.ts +27 -0
  37. package/build/types/lib/merge-sheets-to-base-file-process.d.ts +1 -1
  38. package/build/types/lib/template/template-fs.d.ts +2 -0
  39. package/build/types/lib/template/template-memory.d.ts +61 -0
  40. package/build/types/lib/template/utils/index.d.ts +2 -0
  41. package/build/types/lib/template/utils/prepare-row-to-cells.d.ts +5 -1
  42. package/build/types/lib/template/utils/regexp.d.ts +24 -0
  43. package/build/types/lib/template/utils/update-dimension.d.ts +15 -0
  44. package/build/types/lib/template/utils/write-rows-to-stream.d.ts +22 -9
  45. package/build/types/lib/xml/extract-rows-from-sheet-sync.d.ts +28 -0
  46. package/build/types/lib/xml/extract-rows-from-sheet.d.ts +2 -2
  47. package/build/types/lib/xml/extract-xml-from-sheet-sync.d.ts +14 -0
  48. package/build/types/lib/xml/extract-xml-from-sheet.d.ts +2 -2
  49. package/build/types/lib/xml/index.d.ts +2 -1
  50. package/package.json +1 -5
  51. package/build/cjs/lib/xml/extract-xml-from-system-content.js +0 -53
  52. package/build/esm/lib/xml/extract-xml-from-system-content.js +0 -49
  53. package/build/types/lib/xml/extract-xml-from-system-content.d.ts +0 -15
@@ -20,6 +20,23 @@ export class TemplateMemory {
20
20
  * @type {boolean}
21
21
  */
22
22
  #isProcessing = false;
23
+ /**
24
+ * The keys for the Excel files in the template.
25
+ */
26
+ #excelKeys = {
27
+ contentTypes: "[Content_Types].xml",
28
+ sharedStrings: "xl/sharedStrings.xml",
29
+ styles: "xl/styles.xml",
30
+ workbook: "xl/workbook.xml",
31
+ workbookRels: "xl/_rels/workbook.xml.rels",
32
+ };
33
+ /**
34
+ * Creates a Template instance from a map of file paths to buffers.
35
+ *
36
+ * @param {Object<string, Buffer>} files - The files to create the template from.
37
+ * @throws {Error} If reading or writing files fails.
38
+ * @experimental This API is experimental and might change in future versions.
39
+ */
23
40
  constructor(files) {
24
41
  this.files = files;
25
42
  }
@@ -79,31 +96,75 @@ export class TemplateMemory {
79
96
  sheetXml: modifiedXml,
80
97
  });
81
98
  }
82
- #getXml(fileKey) {
99
+ /**
100
+ * Extracts the XML content from an Excel sheet file.
101
+ *
102
+ * @param {string} fileKey - The file key of the sheet to extract.
103
+ * @returns {string} The XML content of the sheet.
104
+ * @throws {Error} If the file key is not found.
105
+ * @experimental This API is experimental and might change in future versions.
106
+ */
107
+ async #extractXmlFromSheet(fileKey) {
83
108
  if (!this.files[fileKey]) {
84
109
  throw new Error(`${fileKey} not found`);
85
110
  }
86
111
  return Xml.extractXmlFromSheet(this.files[fileKey]);
87
112
  }
88
113
  /**
89
- * Get the path of the sheet with the given name inside the workbook.
90
- * @param sheetName The name of the sheet to find.
91
- * @returns The path of the sheet inside the workbook.
92
- * @throws {Error} If the sheet is not found.
114
+ * Extracts row data from an Excel sheet file.
115
+ *
116
+ * @param {string} fileKey - The file key of the sheet to extract.
117
+ * @returns {Object} An object containing:
118
+ * - rows: Array of raw XML strings for each <row> element
119
+ * - lastRowNumber: Highest row number found in the sheet (1-based)
120
+ * - mergeCells: Array of merged cell ranges (e.g., [{ref: "A1:B2"}])
121
+ * - xml: The XML content of the sheet
122
+ * @throws {Error} If the file key is not found
123
+ * @experimental This API is experimental and might change in future versions.
124
+ */
125
+ async #extractRowsFromSheet(fileKey) {
126
+ if (!this.files[fileKey]) {
127
+ throw new Error(`${fileKey} not found`);
128
+ }
129
+ return Xml.extractRowsFromSheet(this.files[fileKey]);
130
+ }
131
+ /**
132
+ * Returns the Excel path of the sheet with the given name.
133
+ *
134
+ * @param sheetName - The name of the sheet to find.
135
+ * @returns The Excel path of the sheet.
136
+ * @throws {Error} If the sheet with the given name does not exist.
137
+ * @experimental This API is experimental and might change in future versions.
93
138
  */
94
- async #getSheetPath(sheetName) {
95
- // Read XML workbook to find sheet name and path
96
- const workbookXml = this.#getXml("xl/workbook.xml");
97
- const sheetMatch = workbookXml.match(new RegExp(`<sheet[^>]+name="${sheetName}"[^>]+r:id="([^"]+)"[^>]*/>`));
98
- if (!sheetMatch)
139
+ async #getSheetPathByName(sheetName) {
140
+ // Find the sheet
141
+ const workbookXml = await this.#extractXmlFromSheet(this.#excelKeys.workbook);
142
+ const sheetMatch = workbookXml.match(Utils.sheetMatch(sheetName));
143
+ if (!sheetMatch || !sheetMatch[1]) {
99
144
  throw new Error(`Sheet "${sheetName}" not found`);
145
+ }
100
146
  const rId = sheetMatch[1];
101
- const relsXml = this.#getXml("xl/_rels/workbook.xml.rels");
102
- const relMatch = relsXml.match(new RegExp(`<Relationship[^>]+Id="${rId}"[^>]+Target="([^"]+)"[^>]*/>`));
103
- if (!relMatch)
147
+ const relsXml = await this.#extractXmlFromSheet(this.#excelKeys.workbookRels);
148
+ const relMatch = relsXml.match(Utils.relationshipMatch(rId));
149
+ if (!relMatch || !relMatch[1]) {
104
150
  throw new Error(`Relationship "${rId}" not found`);
151
+ }
105
152
  return "xl/" + relMatch[1].replace(/^\/?xl\//, "");
106
153
  }
154
+ /**
155
+ * Returns the Excel path of the sheet with the given ID.
156
+ *
157
+ * @param {number} id - The 1-based index of the sheet to find.
158
+ * @returns {string} The Excel path of the sheet.
159
+ * @throws {Error} If the sheet index is less than 1.
160
+ * @experimental This API is experimental and might change in future versions.
161
+ */
162
+ #getSheetPathById(id) {
163
+ if (id < 1) {
164
+ throw new Error("Sheet index must be greater than 0");
165
+ }
166
+ return `xl/worksheets/sheet${id}.xml`;
167
+ }
107
168
  /**
108
169
  * Replaces the contents of a file in the template.
109
170
  *
@@ -117,16 +178,30 @@ export class TemplateMemory {
117
178
  async #set(key, content) {
118
179
  this.files[key] = content;
119
180
  }
181
+ /**
182
+ * Replaces placeholders in the given sheet with values from the replacements map.
183
+ *
184
+ * The function searches for placeholders in the format `${key}` within the sheet
185
+ * content, where `key` corresponds to a path in the replacements object.
186
+ * If a value is found for the key, it replaces the placeholder with the value.
187
+ * If no value is found, the original placeholder remains unchanged.
188
+ *
189
+ * @param sheetName - The name of the sheet to be replaced.
190
+ * @param replacements - An object where keys represent placeholder paths and values are the replacements.
191
+ * @returns A promise that resolves when the substitution is complete.
192
+ * @throws {Error} If the template instance has been destroyed.
193
+ * @experimental This API is experimental and might change in future versions.
194
+ */
120
195
  async #substitute(sheetName, replacements) {
121
- const sharedStringsPath = "xl/sharedStrings.xml";
122
- const sheetPath = await this.#getSheetPath(sheetName);
196
+ const sharedStringsPath = this.#excelKeys.sharedStrings;
123
197
  let sharedStringsContent = "";
124
198
  let sheetContent = "";
125
199
  if (this.files[sharedStringsPath]) {
126
- sharedStringsContent = this.#getXml(sharedStringsPath);
200
+ sharedStringsContent = await this.#extractXmlFromSheet(sharedStringsPath);
127
201
  }
202
+ const sheetPath = await this.#getSheetPathByName(sheetName);
128
203
  if (this.files[sheetPath]) {
129
- sheetContent = this.#getXml(sheetPath);
204
+ sheetContent = await this.#extractXmlFromSheet(sheetPath);
130
205
  const TABLE_REGEX = /\$\{table:([a-zA-Z0-9_]+)\.([a-zA-Z0-9_]+)\}/g;
131
206
  const hasTablePlaceholders = TABLE_REGEX.test(sharedStringsContent) || TABLE_REGEX.test(sheetContent);
132
207
  if (hasTablePlaceholders) {
@@ -144,6 +219,108 @@ export class TemplateMemory {
144
219
  await this.#set(sheetPath, Buffer.from(sheetContent));
145
220
  }
146
221
  }
222
+ /**
223
+ * Merges rows from other sheets into a base sheet.
224
+ *
225
+ * @param {Object} data
226
+ * @param {Object} data.additions
227
+ * @param {number[]} [data.additions.sheetIndexes] - The 1-based indexes of the sheets to extract rows from.
228
+ * @param {string[]} [data.additions.sheetNames] - The names of the sheets to extract rows from.
229
+ * @param {number} [data.baseSheetIndex=1] - The 1-based index of the sheet in the workbook to add rows to.
230
+ * @param {string} [data.baseSheetName] - The name of the sheet in the workbook to add rows to.
231
+ * @param {number} [data.gap=0] - The number of empty rows to insert between each added section.
232
+ * @throws {Error} If the base sheet index is less than 1.
233
+ * @throws {Error} If the base sheet name is not found.
234
+ * @throws {Error} If the sheet index is less than 1.
235
+ * @throws {Error} If the sheet name is not found.
236
+ * @throws {Error} If no sheets are found to merge.
237
+ * @experimental This API is experimental and might change in future versions.
238
+ */
239
+ async #mergeSheets(data) {
240
+ const { additions, baseSheetIndex = 1, baseSheetName, gap = 0, } = data;
241
+ let fileKey = "";
242
+ if (baseSheetName) {
243
+ fileKey = await this.#getSheetPathByName(baseSheetName);
244
+ }
245
+ if (baseSheetIndex && !fileKey) {
246
+ if (baseSheetIndex < 1) {
247
+ throw new Error("Base sheet index must be greater than 0");
248
+ }
249
+ fileKey = `xl/worksheets/sheet${baseSheetIndex}.xml`;
250
+ }
251
+ if (!fileKey) {
252
+ throw new Error("Base sheet not found");
253
+ }
254
+ const { lastRowNumber, mergeCells: baseMergeCells, rows: baseRows, xml, } = await this.#extractRowsFromSheet(fileKey);
255
+ const allRows = [...baseRows];
256
+ const allMergeCells = [...baseMergeCells];
257
+ let currentRowOffset = lastRowNumber + gap;
258
+ const sheetPaths = [];
259
+ if (additions.sheetIndexes) {
260
+ sheetPaths.push(...(await Promise.all(additions.sheetIndexes.map(e => this.#getSheetPathById(e)))));
261
+ }
262
+ if (additions.sheetNames) {
263
+ sheetPaths.push(...(await Promise.all(additions.sheetNames.map(e => this.#getSheetPathByName(e)))));
264
+ }
265
+ if (sheetPaths.length === 0) {
266
+ throw new Error("No sheets found to merge");
267
+ }
268
+ for (const sheetPath of sheetPaths) {
269
+ if (!this.files[sheetPath]) {
270
+ throw new Error(`Sheet "${sheetPath}" not found`);
271
+ }
272
+ const { mergeCells, rows } = await Xml.extractRowsFromSheet(this.files[sheetPath]);
273
+ const shiftedRows = Xml.shiftRowIndices(rows, currentRowOffset);
274
+ const shiftedMergeCells = mergeCells.map(cell => {
275
+ const [start, end] = cell.ref.split(":");
276
+ if (!start || !end) {
277
+ return cell;
278
+ }
279
+ const shiftedStart = Utils.Common.shiftCellRef(start, currentRowOffset);
280
+ const shiftedEnd = Utils.Common.shiftCellRef(end, currentRowOffset);
281
+ return { ...cell, ref: `${shiftedStart}:${shiftedEnd}` };
282
+ });
283
+ allRows.push(...shiftedRows);
284
+ allMergeCells.push(...shiftedMergeCells);
285
+ currentRowOffset += Utils.Common.getMaxRowNumber(rows) + gap;
286
+ }
287
+ const mergedXml = Xml.buildMergedSheet(xml, allRows, allMergeCells);
288
+ this.#set(fileKey, mergedXml);
289
+ }
290
+ /**
291
+ * Removes sheets from the workbook.
292
+ *
293
+ * @param {Object} data - The data for sheet removal.
294
+ * @param {number[]} [data.sheetIndexes] - The 1-based indexes of the sheets to remove.
295
+ * @param {string[]} [data.sheetNames] - The names of the sheets to remove.
296
+ * @returns {void}
297
+ *
298
+ * @throws {Error} If the template instance has been destroyed.
299
+ * @throws {Error} If the sheet does not exist.
300
+ * @experimental This API is experimental and might change in future versions.
301
+ */
302
+ #removeSheets(data) {
303
+ const { sheetIndexes = [], sheetNames = [] } = data;
304
+ for (const sheetIndex of sheetIndexes) {
305
+ const sheetPath = `xl/worksheets/sheet${sheetIndex}.xml`;
306
+ if (!this.files[sheetPath]) {
307
+ continue;
308
+ }
309
+ delete this.files[sheetPath];
310
+ if (this.files["xl/workbook.xml"]) {
311
+ this.files["xl/workbook.xml"] = Buffer.from(Utils.Common.removeSheetFromWorkbook(this.files["xl/workbook.xml"].toString(), sheetIndex));
312
+ }
313
+ if (this.files["xl/_rels/workbook.xml.rels"]) {
314
+ this.files["xl/_rels/workbook.xml.rels"] = Buffer.from(Utils.Common.removeSheetFromRels(this.files["xl/_rels/workbook.xml.rels"].toString(), sheetIndex));
315
+ }
316
+ if (this.files["[Content_Types].xml"]) {
317
+ this.files["[Content_Types].xml"] = Buffer.from(Utils.Common.removeSheetFromContentTypes(this.files["[Content_Types].xml"].toString(), sheetIndex));
318
+ }
319
+ }
320
+ for (const sheetName of sheetNames) {
321
+ Utils.Common.removeSheetByName(this.files, sheetName);
322
+ }
323
+ }
147
324
  /**
148
325
  * Copies a sheet from the template to a new name.
149
326
  *
@@ -159,30 +336,31 @@ export class TemplateMemory {
159
336
  this.#ensureNotDestroyed();
160
337
  this.#isProcessing = true;
161
338
  try {
339
+ if (sourceName === newName) {
340
+ throw new Error("Source and new sheet names cannot be the same");
341
+ }
162
342
  // Read workbook.xml and find the source sheet
163
- const workbookXmlPath = "xl/workbook.xml";
164
- const workbookXml = this.#getXml(workbookXmlPath);
343
+ const workbookXmlPath = this.#excelKeys.workbook;
344
+ const workbookXml = await this.#extractXmlFromSheet(this.#excelKeys.workbook);
165
345
  // Find the source sheet
166
- const sheetRegex = new RegExp(`<sheet[^>]+name="${sourceName}"[^>]+r:id="([^"]+)"[^>]*/>`);
167
- const sheetMatch = workbookXml.match(sheetRegex);
168
- if (!sheetMatch)
346
+ const sheetMatch = workbookXml.match(Utils.sheetMatch(sourceName));
347
+ if (!sheetMatch || !sheetMatch[1]) {
169
348
  throw new Error(`Sheet "${sourceName}" not found`);
170
- const sourceRId = sheetMatch[1];
349
+ }
171
350
  // Check if a sheet with the new name already exists
172
351
  if (new RegExp(`<sheet[^>]+name="${newName}"`).test(workbookXml)) {
173
352
  throw new Error(`Sheet "${newName}" already exists`);
174
353
  }
175
354
  // Read workbook.rels
176
355
  // Find the source sheet path by rId
177
- const relsXmlPath = "xl/_rels/workbook.xml.rels";
178
- const relsXml = this.#getXml(relsXmlPath);
179
- const relRegex = new RegExp(`<Relationship[^>]+Id="${sourceRId}"[^>]+Target="([^"]+)"[^>]*/>`);
180
- const relMatch = relsXml.match(relRegex);
181
- if (!relMatch)
182
- throw new Error(`Relationship "${sourceRId}" not found`);
356
+ const rId = sheetMatch[1];
357
+ const relsXmlPath = this.#excelKeys.workbookRels;
358
+ const relsXml = await this.#extractXmlFromSheet(this.#excelKeys.workbookRels);
359
+ const relMatch = relsXml.match(Utils.relationshipMatch(rId));
360
+ if (!relMatch || !relMatch[1]) {
361
+ throw new Error(`Relationship "${rId}" not found`);
362
+ }
183
363
  const sourceTarget = relMatch[1]; // sheetN.xml
184
- if (!sourceTarget)
185
- throw new Error(`Relationship "${sourceRId}" not found`);
186
364
  const sourceSheetPath = "xl/" + sourceTarget.replace(/^\/?.*xl\//, "");
187
365
  // Get the index of the new sheet
188
366
  const sheetNumbers = Array.from(Object.keys(this.files))
@@ -219,7 +397,7 @@ export class TemplateMemory {
219
397
  // Read [Content_Types].xml
220
398
  // Update [Content_Types].xml
221
399
  const contentTypesPath = "[Content_Types].xml";
222
- const contentTypesXml = this.#getXml(contentTypesPath);
400
+ const contentTypesXml = await this.#extractXmlFromSheet(contentTypesPath);
223
401
  const overrideTag = `<Override PartName="/xl/worksheets/${newSheetFilename}" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>`;
224
402
  const updatedContentTypesXml = contentTypesXml.replace("</Types>", overrideTag + "</Types>");
225
403
  await this.#set(contentTypesPath, Buffer.from(updatedContentTypesXml));
@@ -276,19 +454,8 @@ export class TemplateMemory {
276
454
  Utils.checkStartRow(startRowNumber);
277
455
  Utils.checkRows(preparedRows);
278
456
  // Find the sheet
279
- const workbookXml = this.#getXml("xl/workbook.xml");
280
- const sheetMatch = workbookXml.match(new RegExp(`<sheet[^>]+name="${sheetName}"[^>]+r:id="([^"]+)"[^>]*/>`));
281
- if (!sheetMatch || !sheetMatch[1]) {
282
- throw new Error(`Sheet "${sheetName}" not found`);
283
- }
284
- const rId = sheetMatch[1];
285
- const relsXml = this.#getXml("xl/_rels/workbook.xml.rels");
286
- const relMatch = relsXml.match(new RegExp(`<Relationship[^>]+Id="${rId}"[^>]+Target="([^"]+)"[^>]*/>`));
287
- if (!relMatch || !relMatch[1]) {
288
- throw new Error(`Relationship "${rId}" not found`);
289
- }
290
- const sheetPath = "xl/" + relMatch[1].replace(/^\/?xl\//, "");
291
- const sheetXml = this.#getXml(sheetPath);
457
+ const sheetPath = await this.#getSheetPathByName(sheetName);
458
+ const sheetXml = await this.#extractXmlFromSheet(sheetPath);
292
459
  let nextRow = 0;
293
460
  if (!startRowNumber) {
294
461
  // Find the last row
@@ -328,6 +495,20 @@ export class TemplateMemory {
328
495
  this.#isProcessing = false;
329
496
  }
330
497
  }
498
+ /**
499
+ * Inserts rows into a specific sheet in the template using an async stream.
500
+ *
501
+ * @param {Object} data - The data for row insertion.
502
+ * @param {string} data.sheetName - The name of the sheet to insert rows into.
503
+ * @param {number} [data.startRowNumber] - The row number to start inserting from.
504
+ * @param {AsyncIterable<unknown[]>} data.rows - Async iterable of rows to insert.
505
+ * @returns {Promise<void>}
506
+ * @throws {Error} If the template instance has been destroyed.
507
+ * @throws {Error} If the sheet does not exist.
508
+ * @throws {Error} If the row number is out of range.
509
+ * @throws {Error} If a column is out of range.
510
+ * @experimental This API is experimental and might change in future versions.
511
+ */
331
512
  async insertRowsStream(data) {
332
513
  this.#ensureNotProcessing();
333
514
  this.#ensureNotDestroyed();
@@ -336,17 +517,9 @@ export class TemplateMemory {
336
517
  const { rows, sheetName, startRowNumber } = data;
337
518
  if (!sheetName)
338
519
  throw new Error("Sheet name is required");
339
- const workbookXml = this.#getXml("xl/workbook.xml");
340
- const sheetMatch = workbookXml.match(new RegExp(`<sheet[^>]+name="${sheetName}"[^>]+r:id="([^"]+)"[^>]*/>`));
341
- if (!sheetMatch)
342
- throw new Error(`Sheet "${sheetName}" not found`);
343
- const rId = sheetMatch[1];
344
- const relsXml = this.#getXml("xl/_rels/workbook.xml.rels");
345
- const relMatch = relsXml.match(new RegExp(`<Relationship[^>]+Id="${rId}"[^>]+Target="([^"]+)"[^>]*/>`));
346
- if (!relMatch || !relMatch[1])
347
- throw new Error(`Relationship "${rId}" not found`);
348
- const sheetPath = "xl/" + relMatch[1].replace(/^\/?xl\//, "");
349
- const sheetXml = this.#getXml(sheetPath);
520
+ // Read XML workbook to find sheet name and path
521
+ const sheetPath = await this.#getSheetPathByName(sheetName);
522
+ const sheetXml = await this.#extractXmlFromSheet(sheetPath);
350
523
  const output = new MemoryWriteStream();
351
524
  let inserted = false;
352
525
  // --- Case 1: <sheetData>...</sheetData> on one line ---
@@ -429,7 +602,7 @@ export class TemplateMemory {
429
602
  }
430
603
  if (!inserted)
431
604
  throw new Error("Failed to locate <sheetData> for insertion");
432
- // теперь мы не собираем строку, а собираем Buffer
605
+ // Save the buffer to the sheet
433
606
  this.files[sheetPath] = output.toBuffer();
434
607
  }
435
608
  finally {
@@ -450,10 +623,10 @@ export class TemplateMemory {
450
623
  try {
451
624
  const zipBuffer = await Zip.create(this.files);
452
625
  this.destroyed = true;
453
- // Очистка всех буферов
626
+ // Clear all buffers
454
627
  for (const key in this.files) {
455
628
  if (this.files.hasOwnProperty(key)) {
456
- this.files[key] = Buffer.alloc(0); // Заменяем на пустой буфер
629
+ this.files[key] = Buffer.alloc(0); // Clear the buffer
457
630
  }
458
631
  }
459
632
  return zipBuffer;
@@ -483,6 +656,55 @@ export class TemplateMemory {
483
656
  this.#isProcessing = false;
484
657
  }
485
658
  }
659
+ /**
660
+ * Merges sheets into a base sheet.
661
+ *
662
+ * @param {Object} data
663
+ * @param {{ sheetIndexes?: number[]; sheetNames?: string[] }} data.additions - The sheets to merge.
664
+ * @param {number} [data.baseSheetIndex=1] - The 1-based index of the sheet to merge into.
665
+ * @param {string} [data.baseSheetName] - The name of the sheet to merge into.
666
+ * @param {number} [data.gap=0] - The number of empty rows to insert between each added section.
667
+ * @returns {void}
668
+ */
669
+ mergeSheets(data) {
670
+ this.#ensureNotProcessing();
671
+ this.#ensureNotDestroyed();
672
+ this.#isProcessing = true;
673
+ try {
674
+ this.#mergeSheets(data);
675
+ }
676
+ finally {
677
+ this.#isProcessing = false;
678
+ }
679
+ }
680
+ /**
681
+ * Removes sheets from the workbook.
682
+ *
683
+ * @param {Object} data
684
+ * @param {number[]} [data.sheetIndexes] - The 1-based indexes of the sheets to remove.
685
+ * @param {string[]} [data.sheetNames] - The names of the sheets to remove.
686
+ * @returns {void}
687
+ */
688
+ removeSheets(data) {
689
+ this.#ensureNotProcessing();
690
+ this.#ensureNotDestroyed();
691
+ this.#isProcessing = true;
692
+ try {
693
+ this.#removeSheets(data);
694
+ }
695
+ finally {
696
+ this.#isProcessing = false;
697
+ }
698
+ }
699
+ /**
700
+ * Creates a Template instance from an Excel file source.
701
+ *
702
+ * @param {Object} data - The data to create the template from.
703
+ * @param {string | Buffer} data.source - The path or buffer of the Excel file.
704
+ * @returns {Promise<TemplateMemory>} A new Template instance.
705
+ * @throws {Error} If reading the file fails.
706
+ * @experimental This API is experimental and might change in future versions.
707
+ */
486
708
  static async from(data) {
487
709
  const { source } = data;
488
710
  const buffer = typeof source === "string"
@@ -1,3 +1,4 @@
1
+ export * as Common from "../../utils/index.js";
1
2
  export * from "./apply-replacements.js";
2
3
  export * from "./check-row.js";
3
4
  export * from "./check-rows.js";
@@ -14,6 +15,7 @@ export * from "./process-merge-cells.js";
14
15
  export * from "./process-merge-finalize.js";
15
16
  export * from "./process-rows.js";
16
17
  export * from "./process-shared-strings.js";
18
+ export * from "./regexp.js";
17
19
  export * from "./to-excel-column-object.js";
18
20
  export * from "./update-dimension.js";
19
21
  export * from "./validate-worksheet-xml.js";
@@ -5,6 +5,10 @@ export function prepareRowToCells(row, rowNumber) {
5
5
  const colLetter = columnIndexToLetter(colIndex);
6
6
  const cellRef = `${colLetter}${rowNumber}`;
7
7
  const cellValue = escapeXml(String(value ?? ""));
8
- return `<c r="${cellRef}" t="inlineStr"><is><t>${cellValue}</t></is></c>`;
8
+ return {
9
+ cellRef,
10
+ cellValue,
11
+ cellXml: `<c r="${cellRef}" t="inlineStr"><is><t>${cellValue}</t></is></c>`,
12
+ };
9
13
  });
10
14
  }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Creates a regular expression to match a relationship element with a specific ID.
3
+ *
4
+ * @param {string} id - The relationship ID to match (e.g. "rId1")
5
+ * @returns {RegExp} A regular expression that matches a Relationship XML element with the given ID and captures the Target attribute value
6
+ * @example
7
+ * const regex = relationshipMatch("rId1");
8
+ * const xml = '<Relationship Id="rId1" Target="worksheets/sheet1.xml"/>';
9
+ * const match = xml.match(regex);
10
+ * // match[1] === "worksheets/sheet1.xml"
11
+ */
12
+ export function relationshipMatch(id) {
13
+ return new RegExp(`<Relationship[^>]+Id="${id}"[^>]+Target="([^"]+)"[^>]*/>`);
14
+ }
15
+ /**
16
+ * Creates a regular expression to match a sheet element with a specific name.
17
+ *
18
+ * @param {string} sheetName - The name of the sheet to match
19
+ * @returns {RegExp} A regular expression that matches a sheet XML element with the given name and captures the r:id attribute value
20
+ * @example
21
+ * const regex = sheetMatch("Sheet1");
22
+ * const xml = '<sheet name="Sheet1" sheetId="1" r:id="rId1"/>';
23
+ * const match = xml.match(regex);
24
+ * // match[1] === "rId1"
25
+ */
26
+ export function sheetMatch(sheetName) {
27
+ return new RegExp(`<sheet[^>]+name="${sheetName}"[^>]+r:id="([^"]+)"[^>]*/>`);
28
+ }
@@ -1,3 +1,18 @@
1
+ /**
2
+ * Updates the dimension element in an Excel worksheet XML string based on the actual cell references.
3
+ *
4
+ * This function scans the XML for all cell references and calculates the minimum and maximum
5
+ * column/row values to determine the actual used range in the worksheet. It then updates
6
+ * the dimension element to reflect this range.
7
+ *
8
+ * @param {string} xml - The worksheet XML string to process
9
+ * @returns {string} The XML string with updated dimension element
10
+ * @example
11
+ * // XML with cells from A1 to C3
12
+ * const xml = '....<dimension ref="A1:B2"/>.....<c r="C3">...</c>...';
13
+ * const updated = updateDimension(xml);
14
+ * // Returns XML with dimension updated to ref="A1:C3"
15
+ */
1
16
  export function updateDimension(xml) {
2
17
  const cellRefs = [...xml.matchAll(/<c r="([A-Z]+)(\d+)"/g)];
3
18
  if (cellRefs.length === 0)