@js-ak/excel-toolbox 1.5.0 → 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.
- package/README.md +41 -62
- package/build/cjs/lib/template/template-fs.js +137 -57
- package/build/cjs/lib/template/template-memory.js +281 -59
- package/build/cjs/lib/template/utils/index.js +25 -0
- package/build/cjs/lib/template/utils/prepare-row-to-cells.js +5 -1
- package/build/cjs/lib/template/utils/regexp.js +32 -0
- package/build/cjs/lib/template/utils/update-dimension.js +15 -0
- package/build/cjs/lib/template/utils/validate-worksheet-xml.js +74 -74
- package/build/cjs/lib/template/utils/write-rows-to-stream.js +57 -17
- package/build/esm/lib/template/template-fs.js +134 -57
- package/build/esm/lib/template/template-memory.js +281 -59
- package/build/esm/lib/template/utils/index.js +2 -0
- package/build/esm/lib/template/utils/prepare-row-to-cells.js +5 -1
- package/build/esm/lib/template/utils/regexp.js +28 -0
- package/build/esm/lib/template/utils/update-dimension.js +15 -0
- package/build/esm/lib/template/utils/validate-worksheet-xml.js +74 -74
- package/build/esm/lib/template/utils/write-rows-to-stream.js +57 -17
- package/build/types/lib/template/template-fs.d.ts +2 -0
- package/build/types/lib/template/template-memory.d.ts +61 -0
- package/build/types/lib/template/utils/index.d.ts +2 -0
- package/build/types/lib/template/utils/prepare-row-to-cells.d.ts +5 -1
- package/build/types/lib/template/utils/regexp.d.ts +24 -0
- package/build/types/lib/template/utils/update-dimension.d.ts +15 -0
- package/build/types/lib/template/utils/write-rows-to-stream.d.ts +22 -9
- package/package.json +1 -1
@@ -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
|
-
|
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
|
+
#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
|
-
*
|
90
|
-
*
|
91
|
-
* @
|
92
|
-
* @
|
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
|
+
#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
|
-
|
95
|
-
//
|
96
|
-
const workbookXml = this.#
|
97
|
-
const sheetMatch = workbookXml.match(
|
98
|
-
if (!sheetMatch)
|
139
|
+
#getSheetPathByName(sheetName) {
|
140
|
+
// Find the sheet
|
141
|
+
const workbookXml = 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.#
|
102
|
-
const relMatch = relsXml.match(
|
103
|
-
if (!relMatch)
|
147
|
+
const relsXml = 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 =
|
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.#
|
200
|
+
sharedStringsContent = this.#extractXmlFromSheet(sharedStringsPath);
|
127
201
|
}
|
202
|
+
const sheetPath = this.#getSheetPathByName(sheetName);
|
128
203
|
if (this.files[sheetPath]) {
|
129
|
-
sheetContent = this.#
|
204
|
+
sheetContent = 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
|
+
#mergeSheets(data) {
|
240
|
+
const { additions, baseSheetIndex = 1, baseSheetName, gap = 0, } = data;
|
241
|
+
let fileKey = "";
|
242
|
+
if (baseSheetName) {
|
243
|
+
fileKey = 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, } = 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(...(additions.sheetIndexes).map(e => this.#getSheetPathById(e)));
|
261
|
+
}
|
262
|
+
if (additions.sheetNames) {
|
263
|
+
sheetPaths.push(...(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 } = 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 =
|
164
|
-
const workbookXml = this.#
|
343
|
+
const workbookXmlPath = this.#excelKeys.workbook;
|
344
|
+
const workbookXml = this.#extractXmlFromSheet(this.#excelKeys.workbook);
|
165
345
|
// Find the source sheet
|
166
|
-
const
|
167
|
-
|
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
|
-
|
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
|
178
|
-
const
|
179
|
-
const
|
180
|
-
const relMatch = relsXml.match(
|
181
|
-
if (!relMatch)
|
182
|
-
throw new Error(`Relationship "${
|
356
|
+
const rId = sheetMatch[1];
|
357
|
+
const relsXmlPath = this.#excelKeys.workbookRels;
|
358
|
+
const relsXml = 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.#
|
400
|
+
const contentTypesXml = 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
|
280
|
-
const
|
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 = this.#getSheetPathByName(sheetName);
|
458
|
+
const sheetXml = 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
|
-
|
340
|
-
const
|
341
|
-
|
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 = this.#getSheetPathByName(sheetName);
|
522
|
+
const sheetXml = 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
|
-
//
|
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
|
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)
|