@js-ak/excel-toolbox 1.3.1 → 1.4.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/template/template-fs.js +678 -197
- package/build/cjs/lib/template/utils/apply-replacements.js +26 -0
- package/build/cjs/lib/template/utils/check-row.js +6 -11
- package/build/cjs/lib/template/utils/column-index-to-letter.js +14 -3
- package/build/cjs/lib/template/utils/extract-xml-declaration.js +22 -0
- package/build/cjs/lib/template/utils/get-by-path.js +18 -0
- package/build/cjs/lib/template/utils/index.js +5 -0
- package/build/cjs/lib/template/utils/update-dimension.js +40 -0
- package/build/cjs/lib/template/utils/validate-worksheet-xml.js +217 -0
- package/build/cjs/lib/zip/create-with-stream.js +67 -107
- package/build/esm/lib/template/template-fs.js +678 -197
- package/build/esm/lib/template/utils/apply-replacements.js +22 -0
- package/build/esm/lib/template/utils/check-row.js +6 -11
- package/build/esm/lib/template/utils/column-index-to-letter.js +14 -3
- package/build/esm/lib/template/utils/extract-xml-declaration.js +19 -0
- package/build/esm/lib/template/utils/get-by-path.js +15 -0
- package/build/esm/lib/template/utils/index.js +5 -0
- package/build/esm/lib/template/utils/update-dimension.js +37 -0
- package/build/esm/lib/template/utils/validate-worksheet-xml.js +214 -0
- package/build/esm/lib/zip/create-with-stream.js +68 -108
- package/build/types/lib/template/template-fs.d.ts +24 -0
- package/build/types/lib/template/utils/apply-replacements.d.ts +13 -0
- package/build/types/lib/template/utils/check-row.d.ts +5 -10
- package/build/types/lib/template/utils/column-index-to-letter.d.ts +11 -3
- package/build/types/lib/template/utils/extract-xml-declaration.d.ts +14 -0
- package/build/types/lib/template/utils/get-by-path.d.ts +8 -0
- package/build/types/lib/template/utils/index.d.ts +5 -0
- package/build/types/lib/template/utils/update-dimension.d.ts +1 -0
- package/build/types/lib/template/utils/validate-worksheet-xml.d.ts +9 -0
- package/package.json +6 -4
@@ -62,6 +62,11 @@ class TemplateFs {
|
|
62
62
|
* @type {boolean}
|
63
63
|
*/
|
64
64
|
destroyed = false;
|
65
|
+
/**
|
66
|
+
* Flag indicating whether this template instance is currently being processed.
|
67
|
+
* @type {boolean}
|
68
|
+
*/
|
69
|
+
#isProcessing = false;
|
65
70
|
/**
|
66
71
|
* Creates a Template instance.
|
67
72
|
*
|
@@ -93,6 +98,70 @@ class TemplateFs {
|
|
93
98
|
throw new Error("This Template instance has already been saved and destroyed.");
|
94
99
|
}
|
95
100
|
}
|
101
|
+
/**
|
102
|
+
* Ensures that this Template instance is not currently being processed.
|
103
|
+
* @throws {Error} If the template instance is currently being processed.
|
104
|
+
* @experimental This API is experimental and might change in future versions.
|
105
|
+
*/
|
106
|
+
#ensureNotProcessing() {
|
107
|
+
if (this.#isProcessing) {
|
108
|
+
throw new Error("This Template instance is currently being processed.");
|
109
|
+
}
|
110
|
+
}
|
111
|
+
/**
|
112
|
+
* Expand table rows in the given sheet and shared strings XML.
|
113
|
+
*
|
114
|
+
* @param {string} sheetXml - The XML content of the sheet.
|
115
|
+
* @param {string} sharedStringsXml - The XML content of the shared strings.
|
116
|
+
* @param {Record<string, unknown>} replacements - An object containing replacement values.
|
117
|
+
*
|
118
|
+
* @returns {Object} An object with two properties:
|
119
|
+
* - sheet: The expanded sheet XML.
|
120
|
+
* - shared: The expanded shared strings XML.
|
121
|
+
* @experimental This API is experimental and might change in future versions.
|
122
|
+
*/
|
123
|
+
#expandTableRows(sheetXml, sharedStringsXml, replacements) {
|
124
|
+
const { initialMergeCells, mergeCellMatches, modifiedXml, } = processMergeCells(sheetXml);
|
125
|
+
const { sharedIndexMap, sharedStrings, sharedStringsHeader, sheetMergeCells, } = processSharedStrings(sharedStringsXml);
|
126
|
+
const { lastIndex, resultRows, rowShift } = processRows({
|
127
|
+
mergeCellMatches,
|
128
|
+
replacements,
|
129
|
+
sharedIndexMap,
|
130
|
+
sharedStrings,
|
131
|
+
sheetMergeCells,
|
132
|
+
sheetXml: modifiedXml,
|
133
|
+
});
|
134
|
+
return processBuild({
|
135
|
+
initialMergeCells,
|
136
|
+
lastIndex,
|
137
|
+
mergeCellMatches,
|
138
|
+
resultRows,
|
139
|
+
rowShift,
|
140
|
+
sharedStrings,
|
141
|
+
sharedStringsHeader,
|
142
|
+
sheetMergeCells,
|
143
|
+
sheetXml: modifiedXml,
|
144
|
+
});
|
145
|
+
}
|
146
|
+
/**
|
147
|
+
* Get the path of the sheet with the given name inside the workbook.
|
148
|
+
* @param sheetName The name of the sheet to find.
|
149
|
+
* @returns The path of the sheet inside the workbook.
|
150
|
+
* @throws {Error} If the sheet is not found.
|
151
|
+
*/
|
152
|
+
async #getSheetPath(sheetName) {
|
153
|
+
// Read XML workbook to find sheet name and path
|
154
|
+
const workbookXml = Xml.extractXmlFromSheet(await this.#readFile("xl/workbook.xml"));
|
155
|
+
const sheetMatch = workbookXml.match(new RegExp(`<sheet[^>]+name="${sheetName}"[^>]+r:id="([^"]+)"[^>]*/>`));
|
156
|
+
if (!sheetMatch)
|
157
|
+
throw new Error(`Sheet "${sheetName}" not found`);
|
158
|
+
const rId = sheetMatch[1];
|
159
|
+
const relsXml = Xml.extractXmlFromSheet(await this.#readFile("xl/_rels/workbook.xml.rels"));
|
160
|
+
const relMatch = relsXml.match(new RegExp(`<Relationship[^>]+Id="${rId}"[^>]+Target="([^"]+)"[^>]*/>`));
|
161
|
+
if (!relMatch)
|
162
|
+
throw new Error(`Relationship "${rId}" not found`);
|
163
|
+
return "xl/" + relMatch[1].replace(/^\/?xl\//, "");
|
164
|
+
}
|
96
165
|
/**
|
97
166
|
* Reads all files from the destination directory.
|
98
167
|
* @private
|
@@ -122,6 +191,169 @@ class TemplateFs {
|
|
122
191
|
const fullPath = path.join(this.destination, ...pathKey.split("/"));
|
123
192
|
return await fs.readFile(fullPath);
|
124
193
|
}
|
194
|
+
/**
|
195
|
+
* Replaces the contents of a file in the template.
|
196
|
+
*
|
197
|
+
* @param {string} key - The Excel path of the file to replace.
|
198
|
+
* @param {Buffer|string} content - The new content.
|
199
|
+
* @returns {Promise<void>}
|
200
|
+
* @throws {Error} If the template instance has been destroyed.
|
201
|
+
* @throws {Error} If the file does not exist in the template.
|
202
|
+
* @experimental This API is experimental and might change in future versions.
|
203
|
+
*/
|
204
|
+
async #set(key, content) {
|
205
|
+
if (!this.fileKeys.has(key)) {
|
206
|
+
throw new Error(`File "${key}" is not part of the original template.`);
|
207
|
+
}
|
208
|
+
const fullPath = path.join(this.destination, ...key.split("/"));
|
209
|
+
await fs.writeFile(fullPath, Buffer.isBuffer(content) ? content : Buffer.from(content));
|
210
|
+
}
|
211
|
+
async #substitute(sheetName, replacements) {
|
212
|
+
const sharedStringsPath = "xl/sharedStrings.xml";
|
213
|
+
const sheetPath = await this.#getSheetPath(sheetName);
|
214
|
+
let sharedStringsContent = "";
|
215
|
+
let sheetContent = "";
|
216
|
+
if (this.fileKeys.has(sharedStringsPath)) {
|
217
|
+
sharedStringsContent = Xml.extractXmlFromSheet(await this.#readFile(sharedStringsPath));
|
218
|
+
}
|
219
|
+
if (this.fileKeys.has(sheetPath)) {
|
220
|
+
sheetContent = Xml.extractXmlFromSheet(await this.#readFile(sheetPath));
|
221
|
+
const TABLE_REGEX = /\$\{table:([a-zA-Z0-9_]+)\.([a-zA-Z0-9_]+)\}/g;
|
222
|
+
const hasTablePlaceholders = TABLE_REGEX.test(sharedStringsContent) || TABLE_REGEX.test(sheetContent);
|
223
|
+
if (hasTablePlaceholders) {
|
224
|
+
const result = this.#expandTableRows(sheetContent, sharedStringsContent, replacements);
|
225
|
+
sheetContent = result.sheet;
|
226
|
+
sharedStringsContent = result.shared;
|
227
|
+
}
|
228
|
+
}
|
229
|
+
if (this.fileKeys.has(sharedStringsPath)) {
|
230
|
+
sharedStringsContent = Utils.applyReplacements(sharedStringsContent, replacements);
|
231
|
+
await this.#set(sharedStringsPath, sharedStringsContent);
|
232
|
+
}
|
233
|
+
if (this.fileKeys.has(sheetPath)) {
|
234
|
+
sheetContent = Utils.applyReplacements(sheetContent, replacements);
|
235
|
+
await this.#set(sheetPath, sheetContent);
|
236
|
+
}
|
237
|
+
}
|
238
|
+
/**
|
239
|
+
* Validates the template by checking all required files exist.
|
240
|
+
*
|
241
|
+
* @returns {Promise<void>}
|
242
|
+
* @throws {Error} If the template instance has been destroyed.
|
243
|
+
* @throws {Error} If any required files are missing.
|
244
|
+
* @experimental This API is experimental and might change in future versions.
|
245
|
+
*/
|
246
|
+
async #validate() {
|
247
|
+
for (const key of this.fileKeys) {
|
248
|
+
const fullPath = path.join(this.destination, ...key.split("/"));
|
249
|
+
try {
|
250
|
+
await fs.access(fullPath);
|
251
|
+
}
|
252
|
+
catch {
|
253
|
+
throw new Error(`Missing file in template directory: ${key}`);
|
254
|
+
}
|
255
|
+
}
|
256
|
+
}
|
257
|
+
/**
|
258
|
+
* Copies a sheet from the template to a new name.
|
259
|
+
*
|
260
|
+
* @param {string} sourceName - The name of the sheet to copy.
|
261
|
+
* @param {string} newName - The new name for the sheet.
|
262
|
+
* @returns {Promise<void>}
|
263
|
+
* @throws {Error} If the sheet with the source name does not exist.
|
264
|
+
* @throws {Error} If a sheet with the new name already exists.
|
265
|
+
* @experimental This API is experimental and might change in future versions.
|
266
|
+
*/
|
267
|
+
async copySheet(sourceName, newName) {
|
268
|
+
this.#ensureNotProcessing();
|
269
|
+
this.#ensureNotDestroyed();
|
270
|
+
this.#isProcessing = true;
|
271
|
+
try {
|
272
|
+
// Read workbook.xml and find the source sheet
|
273
|
+
const workbookXmlPath = "xl/workbook.xml";
|
274
|
+
const workbookXml = Xml.extractXmlFromSheet(await this.#readFile(workbookXmlPath));
|
275
|
+
// Find the source sheet
|
276
|
+
const sheetRegex = new RegExp(`<sheet[^>]+name="${sourceName}"[^>]+r:id="([^"]+)"[^>]*/>`);
|
277
|
+
const sheetMatch = workbookXml.match(sheetRegex);
|
278
|
+
if (!sheetMatch)
|
279
|
+
throw new Error(`Sheet "${sourceName}" not found`);
|
280
|
+
const sourceRId = sheetMatch[1];
|
281
|
+
// Check if a sheet with the new name already exists
|
282
|
+
if (new RegExp(`<sheet[^>]+name="${newName}"`).test(workbookXml)) {
|
283
|
+
throw new Error(`Sheet "${newName}" already exists`);
|
284
|
+
}
|
285
|
+
// Read workbook.rels
|
286
|
+
// Find the source sheet path by rId
|
287
|
+
const relsXmlPath = "xl/_rels/workbook.xml.rels";
|
288
|
+
const relsXml = Xml.extractXmlFromSheet(await this.#readFile(relsXmlPath));
|
289
|
+
const relRegex = new RegExp(`<Relationship[^>]+Id="${sourceRId}"[^>]+Target="([^"]+)"[^>]*/>`);
|
290
|
+
const relMatch = relsXml.match(relRegex);
|
291
|
+
if (!relMatch)
|
292
|
+
throw new Error(`Relationship "${sourceRId}" not found`);
|
293
|
+
const sourceTarget = relMatch[1]; // sheetN.xml
|
294
|
+
if (!sourceTarget)
|
295
|
+
throw new Error(`Relationship "${sourceRId}" not found`);
|
296
|
+
const sourceSheetPath = "xl/" + sourceTarget.replace(/^\/?.*xl\//, "");
|
297
|
+
// Get the index of the new sheet
|
298
|
+
const sheetNumbers = [...this.fileKeys]
|
299
|
+
.map((key) => key.match(/^xl\/worksheets\/sheet(\d+)\.xml$/))
|
300
|
+
.filter(Boolean)
|
301
|
+
.map((match) => parseInt(match[1], 10));
|
302
|
+
const nextSheetIndex = sheetNumbers.length > 0 ? Math.max(...sheetNumbers) + 1 : 1;
|
303
|
+
const newSheetFilename = `sheet${nextSheetIndex}.xml`;
|
304
|
+
const newSheetPath = `xl/worksheets/${newSheetFilename}`;
|
305
|
+
const newTarget = `worksheets/${newSheetFilename}`;
|
306
|
+
// Generate a unique rId
|
307
|
+
const usedRIds = [...relsXml.matchAll(/Id="(rId\d+)"/g)].map(m => m[1]);
|
308
|
+
let nextRIdNum = 1;
|
309
|
+
while (usedRIds.includes(`rId${nextRIdNum}`))
|
310
|
+
nextRIdNum++;
|
311
|
+
const newRId = `rId${nextRIdNum}`;
|
312
|
+
// Copy the source sheet file
|
313
|
+
const sheetContent = await this.#readFile(sourceSheetPath);
|
314
|
+
await fs.writeFile(path.join(this.destination, ...newSheetPath.split("/")), sheetContent);
|
315
|
+
this.fileKeys.add(newSheetPath);
|
316
|
+
// Update workbook.xml
|
317
|
+
const updatedWorkbookXml = workbookXml.replace("</sheets>", `<sheet name="${newName}" sheetId="${nextSheetIndex}" r:id="${newRId}"/></sheets>`);
|
318
|
+
await this.#set(workbookXmlPath, updatedWorkbookXml);
|
319
|
+
// Update workbook.xml.rels
|
320
|
+
const updatedRelsXml = relsXml.replace("</Relationships>", `<Relationship Id="${newRId}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="${newTarget}"/></Relationships>`);
|
321
|
+
await this.#set(relsXmlPath, updatedRelsXml);
|
322
|
+
// Read [Content_Types].xml
|
323
|
+
// Update [Content_Types].xml
|
324
|
+
const contentTypesPath = "[Content_Types].xml";
|
325
|
+
const contentTypesXml = Xml.extractXmlFromSheet(await this.#readFile(contentTypesPath));
|
326
|
+
const overrideTag = `<Override PartName="/xl/worksheets/${newSheetFilename}" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>`;
|
327
|
+
const updatedContentTypesXml = contentTypesXml.replace("</Types>", overrideTag + "</Types>");
|
328
|
+
await this.#set(contentTypesPath, updatedContentTypesXml);
|
329
|
+
}
|
330
|
+
finally {
|
331
|
+
this.#isProcessing = false;
|
332
|
+
}
|
333
|
+
}
|
334
|
+
/**
|
335
|
+
* Replaces placeholders in the given sheet with values from the replacements map.
|
336
|
+
*
|
337
|
+
* The function searches for placeholders in the format `${key}` within the sheet
|
338
|
+
* content, where `key` corresponds to a path in the replacements object.
|
339
|
+
* If a value is found for the key, it replaces the placeholder with the value.
|
340
|
+
* If no value is found, the original placeholder remains unchanged.
|
341
|
+
*
|
342
|
+
* @param sheetName - The name of the sheet to be replaced.
|
343
|
+
* @param replacements - An object where keys represent placeholder paths and values are the replacements.
|
344
|
+
* @returns A promise that resolves when the substitution is complete.
|
345
|
+
*/
|
346
|
+
substitute(sheetName, replacements) {
|
347
|
+
this.#ensureNotProcessing();
|
348
|
+
this.#ensureNotDestroyed();
|
349
|
+
this.#isProcessing = true;
|
350
|
+
try {
|
351
|
+
return this.#substitute(sheetName, replacements);
|
352
|
+
}
|
353
|
+
finally {
|
354
|
+
this.#isProcessing = false;
|
355
|
+
}
|
356
|
+
}
|
125
357
|
/**
|
126
358
|
* Inserts rows into a specific sheet in the template.
|
127
359
|
*
|
@@ -137,61 +369,68 @@ class TemplateFs {
|
|
137
369
|
* @experimental This API is experimental and might change in future versions.
|
138
370
|
*/
|
139
371
|
async insertRows(data) {
|
372
|
+
this.#ensureNotProcessing();
|
140
373
|
this.#ensureNotDestroyed();
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
374
|
+
this.#isProcessing = true;
|
375
|
+
try {
|
376
|
+
const { rows, sheetName, startRowNumber } = data;
|
377
|
+
const preparedRows = rows.map(row => Utils.toExcelColumnObject(row));
|
378
|
+
// Validation
|
379
|
+
Utils.checkStartRow(startRowNumber);
|
380
|
+
Utils.checkRows(preparedRows);
|
381
|
+
// Find the sheet
|
382
|
+
const workbookXml = Xml.extractXmlFromSheet(await this.#readFile("xl/workbook.xml"));
|
383
|
+
const sheetMatch = workbookXml.match(new RegExp(`<sheet[^>]+name="${sheetName}"[^>]+r:id="([^"]+)"[^>]*/>`));
|
384
|
+
if (!sheetMatch || !sheetMatch[1]) {
|
385
|
+
throw new Error(`Sheet "${sheetName}" not found`);
|
386
|
+
}
|
387
|
+
const rId = sheetMatch[1];
|
388
|
+
const relsXml = Xml.extractXmlFromSheet(await this.#readFile("xl/_rels/workbook.xml.rels"));
|
389
|
+
const relMatch = relsXml.match(new RegExp(`<Relationship[^>]+Id="${rId}"[^>]+Target="([^"]+)"[^>]*/>`));
|
390
|
+
if (!relMatch || !relMatch[1]) {
|
391
|
+
throw new Error(`Relationship "${rId}" not found`);
|
392
|
+
}
|
393
|
+
const sheetPath = "xl/" + relMatch[1].replace(/^\/?xl\//, "");
|
394
|
+
const sheetXmlRaw = await this.#readFile(sheetPath);
|
395
|
+
const sheetXml = Xml.extractXmlFromSheet(sheetXmlRaw);
|
396
|
+
let nextRow = 0;
|
397
|
+
if (!startRowNumber) {
|
398
|
+
// Find the last row
|
399
|
+
let lastRowNumber = 0;
|
400
|
+
const rowMatches = [...sheetXml.matchAll(/<row[^>]+r="(\d+)"[^>]*>/g)];
|
401
|
+
if (rowMatches.length > 0) {
|
402
|
+
lastRowNumber = Math.max(...rowMatches.map((m) => parseInt(m[1], 10)));
|
403
|
+
}
|
404
|
+
nextRow = lastRowNumber + 1;
|
168
405
|
}
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
return `<
|
406
|
+
else {
|
407
|
+
nextRow = startRowNumber;
|
408
|
+
}
|
409
|
+
// Generate XML for all rows
|
410
|
+
const rowsXml = preparedRows.map((cells, i) => {
|
411
|
+
const rowNumber = nextRow + i;
|
412
|
+
const cellTags = Object.entries(cells).map(([col, value]) => {
|
413
|
+
const colUpper = col.toUpperCase();
|
414
|
+
const ref = `${colUpper}${rowNumber}`;
|
415
|
+
return `<c r="${ref}" t="inlineStr"><is><t>${Utils.escapeXml(value)}</t></is></c>`;
|
416
|
+
}).join("");
|
417
|
+
return `<row r="${rowNumber}">${cellTags}</row>`;
|
181
418
|
}).join("");
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
419
|
+
let updatedXml;
|
420
|
+
if (/<sheetData\s*\/>/.test(sheetXml)) {
|
421
|
+
updatedXml = sheetXml.replace(/<sheetData\s*\/>/, `<sheetData>${rowsXml}</sheetData>`);
|
422
|
+
}
|
423
|
+
else if (/<sheetData>([\s\S]*?)<\/sheetData>/.test(sheetXml)) {
|
424
|
+
updatedXml = sheetXml.replace(/<\/sheetData>/, `${rowsXml}</sheetData>`);
|
425
|
+
}
|
426
|
+
else {
|
427
|
+
updatedXml = sheetXml.replace(/<worksheet[^>]*>/, (match) => `${match}<sheetData>${rowsXml}</sheetData>`);
|
428
|
+
}
|
429
|
+
await this.#set(sheetPath, updatedXml);
|
190
430
|
}
|
191
|
-
|
192
|
-
|
431
|
+
finally {
|
432
|
+
this.#isProcessing = false;
|
193
433
|
}
|
194
|
-
await this.set(sheetPath, updatedXml);
|
195
434
|
}
|
196
435
|
/**
|
197
436
|
* Inserts rows into a specific sheet in the template using an async stream.
|
@@ -208,155 +447,162 @@ class TemplateFs {
|
|
208
447
|
* @experimental This API is experimental and might change in future versions.
|
209
448
|
*/
|
210
449
|
async insertRowsStream(data) {
|
450
|
+
this.#ensureNotProcessing();
|
211
451
|
this.#ensureNotDestroyed();
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
if (
|
452
|
+
this.#isProcessing = true;
|
453
|
+
try {
|
454
|
+
const { rows, sheetName, startRowNumber } = data;
|
455
|
+
if (!sheetName)
|
456
|
+
throw new Error("Sheet name is required");
|
457
|
+
// Read XML workbook to find sheet name and path
|
458
|
+
const workbookXml = Xml.extractXmlFromSheet(await this.#readFile("xl/workbook.xml"));
|
459
|
+
const sheetMatch = workbookXml.match(new RegExp(`<sheet[^>]+name="${sheetName}"[^>]+r:id="([^"]+)"[^>]*/>`));
|
460
|
+
if (!sheetMatch)
|
461
|
+
throw new Error(`Sheet "${sheetName}" not found`);
|
462
|
+
const rId = sheetMatch[1];
|
463
|
+
const relsXml = Xml.extractXmlFromSheet(await this.#readFile("xl/_rels/workbook.xml.rels"));
|
464
|
+
const relMatch = relsXml.match(new RegExp(`<Relationship[^>]+Id="${rId}"[^>]+Target="([^"]+)"[^>]*/>`));
|
465
|
+
if (!relMatch)
|
466
|
+
throw new Error(`Relationship "${rId}" not found`);
|
467
|
+
// Path to the desired sheet (sheet1.xml)
|
468
|
+
const sheetPath = "xl/" + relMatch[1].replace(/^\/?xl\//, "");
|
469
|
+
// The temporary file for writing
|
470
|
+
const fullPath = path.join(this.destination, ...sheetPath.split("/"));
|
471
|
+
const tempPath = fullPath + ".tmp";
|
472
|
+
// Streams for reading and writing
|
473
|
+
const input = fsSync.createReadStream(fullPath, { encoding: "utf-8" });
|
474
|
+
const output = fsSync.createWriteStream(tempPath, { encoding: "utf-8" });
|
475
|
+
// Inserted rows flag
|
476
|
+
let inserted = false;
|
477
|
+
const rl = readline.createInterface({
|
478
|
+
// Process all line breaks
|
479
|
+
crlfDelay: Infinity,
|
480
|
+
input,
|
481
|
+
});
|
482
|
+
let isCollecting = false;
|
483
|
+
let collection = "";
|
484
|
+
for await (const line of rl) {
|
485
|
+
// Collect lines between <sheetData> and </sheetData>
|
486
|
+
if (!inserted && isCollecting) {
|
487
|
+
collection += line;
|
488
|
+
if (line.includes("</sheetData>")) {
|
489
|
+
const maxRowNumber = startRowNumber ?? Utils.getMaxRowNumber(line);
|
490
|
+
isCollecting = false;
|
491
|
+
inserted = true;
|
492
|
+
const openTag = collection.match(/<sheetData[^>]*>/)?.[0] ?? "<sheetData>";
|
493
|
+
const closeTag = "</sheetData>";
|
494
|
+
const openIdx = collection.indexOf(openTag);
|
495
|
+
const closeIdx = collection.lastIndexOf(closeTag);
|
496
|
+
const beforeRows = collection.slice(0, openIdx + openTag.length);
|
497
|
+
const innerRows = collection.slice(openIdx + openTag.length, closeIdx).trim();
|
498
|
+
const afterRows = collection.slice(closeIdx);
|
499
|
+
output.write(beforeRows);
|
500
|
+
const innerRowsMap = Utils.parseRows(innerRows);
|
501
|
+
if (innerRows) {
|
502
|
+
if (startRowNumber) {
|
503
|
+
const filteredRows = Utils.getRowsBelow(innerRowsMap, startRowNumber);
|
504
|
+
if (filteredRows)
|
505
|
+
output.write(filteredRows);
|
506
|
+
}
|
507
|
+
else {
|
508
|
+
output.write(innerRows);
|
509
|
+
}
|
510
|
+
}
|
511
|
+
const { rowNumber: actualRowNumber } = await Utils.writeRowsToStream(output, rows, maxRowNumber);
|
512
|
+
if (innerRows) {
|
513
|
+
const filteredRows = Utils.getRowsAbove(innerRowsMap, actualRowNumber);
|
514
|
+
if (filteredRows)
|
515
|
+
output.write(filteredRows);
|
516
|
+
}
|
517
|
+
output.write(afterRows);
|
518
|
+
}
|
519
|
+
continue;
|
520
|
+
}
|
521
|
+
// Case 1: <sheetData> and </sheetData> on one line
|
522
|
+
const singleLineMatch = line.match(/(<sheetData[^>]*>)(.*)(<\/sheetData>)/);
|
523
|
+
if (!inserted && singleLineMatch) {
|
247
524
|
const maxRowNumber = startRowNumber ?? Utils.getMaxRowNumber(line);
|
248
|
-
|
249
|
-
|
250
|
-
const
|
525
|
+
const fullMatch = singleLineMatch[0];
|
526
|
+
const before = line.slice(0, singleLineMatch.index);
|
527
|
+
const after = line.slice(singleLineMatch.index + fullMatch.length);
|
528
|
+
const openTag = "<sheetData>";
|
251
529
|
const closeTag = "</sheetData>";
|
252
|
-
const openIdx =
|
253
|
-
const closeIdx =
|
254
|
-
const beforeRows =
|
255
|
-
const innerRows =
|
256
|
-
const afterRows =
|
530
|
+
const openIdx = fullMatch.indexOf(openTag);
|
531
|
+
const closeIdx = fullMatch.indexOf(closeTag);
|
532
|
+
const beforeRows = fullMatch.slice(0, openIdx + openTag.length);
|
533
|
+
const innerRows = fullMatch.slice(openIdx + openTag.length, closeIdx).trim();
|
534
|
+
const afterRows = fullMatch.slice(closeIdx);
|
535
|
+
if (before) {
|
536
|
+
output.write(before);
|
537
|
+
}
|
257
538
|
output.write(beforeRows);
|
258
539
|
const innerRowsMap = Utils.parseRows(innerRows);
|
259
540
|
if (innerRows) {
|
260
541
|
if (startRowNumber) {
|
261
542
|
const filteredRows = Utils.getRowsBelow(innerRowsMap, startRowNumber);
|
262
|
-
if (filteredRows)
|
543
|
+
if (filteredRows) {
|
263
544
|
output.write(filteredRows);
|
545
|
+
}
|
264
546
|
}
|
265
547
|
else {
|
266
548
|
output.write(innerRows);
|
267
549
|
}
|
268
550
|
}
|
551
|
+
// new <row>
|
269
552
|
const { rowNumber: actualRowNumber } = await Utils.writeRowsToStream(output, rows, maxRowNumber);
|
270
553
|
if (innerRows) {
|
271
554
|
const filteredRows = Utils.getRowsAbove(innerRowsMap, actualRowNumber);
|
272
|
-
if (filteredRows)
|
273
|
-
output.write(filteredRows);
|
274
|
-
}
|
275
|
-
output.write(afterRows);
|
276
|
-
}
|
277
|
-
continue;
|
278
|
-
}
|
279
|
-
// Case 1: <sheetData> and </sheetData> on one line
|
280
|
-
const singleLineMatch = line.match(/(<sheetData[^>]*>)(.*)(<\/sheetData>)/);
|
281
|
-
if (!inserted && singleLineMatch) {
|
282
|
-
const maxRowNumber = startRowNumber ?? Utils.getMaxRowNumber(line);
|
283
|
-
const fullMatch = singleLineMatch[0];
|
284
|
-
const before = line.slice(0, singleLineMatch.index);
|
285
|
-
const after = line.slice(singleLineMatch.index + fullMatch.length);
|
286
|
-
const openTag = "<sheetData>";
|
287
|
-
const closeTag = "</sheetData>";
|
288
|
-
const openIdx = fullMatch.indexOf(openTag);
|
289
|
-
const closeIdx = fullMatch.indexOf(closeTag);
|
290
|
-
const beforeRows = fullMatch.slice(0, openIdx + openTag.length);
|
291
|
-
const innerRows = fullMatch.slice(openIdx + openTag.length, closeIdx).trim();
|
292
|
-
const afterRows = fullMatch.slice(closeIdx);
|
293
|
-
if (before) {
|
294
|
-
output.write(before);
|
295
|
-
}
|
296
|
-
output.write(beforeRows);
|
297
|
-
const innerRowsMap = Utils.parseRows(innerRows);
|
298
|
-
if (innerRows) {
|
299
|
-
if (startRowNumber) {
|
300
|
-
const filteredRows = Utils.getRowsBelow(innerRowsMap, startRowNumber);
|
301
555
|
if (filteredRows) {
|
302
556
|
output.write(filteredRows);
|
303
557
|
}
|
304
558
|
}
|
305
|
-
|
306
|
-
|
559
|
+
output.write(afterRows);
|
560
|
+
if (after) {
|
561
|
+
output.write(after);
|
307
562
|
}
|
563
|
+
inserted = true;
|
564
|
+
continue;
|
308
565
|
}
|
309
|
-
//
|
310
|
-
|
311
|
-
|
312
|
-
const
|
313
|
-
|
314
|
-
|
566
|
+
// Case 2: <sheetData/>
|
567
|
+
if (!inserted && /<sheetData\s*\/>/.test(line)) {
|
568
|
+
const maxRowNumber = startRowNumber ?? Utils.getMaxRowNumber(line);
|
569
|
+
const fullMatch = line.match(/<sheetData\s*\/>/)?.[0] || "";
|
570
|
+
const matchIndex = line.indexOf(fullMatch);
|
571
|
+
const before = line.slice(0, matchIndex);
|
572
|
+
const after = line.slice(matchIndex + fullMatch.length);
|
573
|
+
if (before) {
|
574
|
+
output.write(before);
|
315
575
|
}
|
576
|
+
// Insert opening tag
|
577
|
+
output.write("<sheetData>");
|
578
|
+
// Prepare the rows
|
579
|
+
await Utils.writeRowsToStream(output, rows, maxRowNumber);
|
580
|
+
// Insert closing tag
|
581
|
+
output.write("</sheetData>");
|
582
|
+
if (after) {
|
583
|
+
output.write(after);
|
584
|
+
}
|
585
|
+
inserted = true;
|
586
|
+
continue;
|
316
587
|
}
|
317
|
-
|
318
|
-
if (
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
continue;
|
323
|
-
}
|
324
|
-
// Case 2: <sheetData/>
|
325
|
-
if (!inserted && /<sheetData\s*\/>/.test(line)) {
|
326
|
-
const maxRowNumber = startRowNumber ?? Utils.getMaxRowNumber(line);
|
327
|
-
const fullMatch = line.match(/<sheetData\s*\/>/)?.[0] || "";
|
328
|
-
const matchIndex = line.indexOf(fullMatch);
|
329
|
-
const before = line.slice(0, matchIndex);
|
330
|
-
const after = line.slice(matchIndex + fullMatch.length);
|
331
|
-
if (before) {
|
332
|
-
output.write(before);
|
333
|
-
}
|
334
|
-
// Insert opening tag
|
335
|
-
output.write("<sheetData>");
|
336
|
-
// Prepare the rows
|
337
|
-
await Utils.writeRowsToStream(output, rows, maxRowNumber);
|
338
|
-
// Insert closing tag
|
339
|
-
output.write("</sheetData>");
|
340
|
-
if (after) {
|
341
|
-
output.write(after);
|
588
|
+
// Case 3: <sheetData>
|
589
|
+
if (!inserted && /<sheetData[^>]*>/.test(line)) {
|
590
|
+
isCollecting = true;
|
591
|
+
collection += line;
|
592
|
+
continue;
|
342
593
|
}
|
343
|
-
|
344
|
-
|
345
|
-
}
|
346
|
-
// Case 3: <sheetData>
|
347
|
-
if (!inserted && /<sheetData[^>]*>/.test(line)) {
|
348
|
-
isCollecting = true;
|
349
|
-
collection += line;
|
350
|
-
continue;
|
594
|
+
// After inserting rows, just copy the remaining lines
|
595
|
+
output.write(line);
|
351
596
|
}
|
352
|
-
//
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
597
|
+
// Close the streams
|
598
|
+
rl.close();
|
599
|
+
output.end();
|
600
|
+
// Move the temporary file to the original location
|
601
|
+
await fs.rename(tempPath, fullPath);
|
602
|
+
}
|
603
|
+
finally {
|
604
|
+
this.#isProcessing = false;
|
605
|
+
}
|
360
606
|
}
|
361
607
|
/**
|
362
608
|
* Saves the modified Excel template to a buffer.
|
@@ -366,15 +612,22 @@ class TemplateFs {
|
|
366
612
|
* @experimental This API is experimental and might change in future versions.
|
367
613
|
*/
|
368
614
|
async save() {
|
615
|
+
this.#ensureNotProcessing();
|
369
616
|
this.#ensureNotDestroyed();
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
617
|
+
this.#isProcessing = true;
|
618
|
+
try {
|
619
|
+
// Guarantee integrity
|
620
|
+
await this.#validate();
|
621
|
+
// Read the current files from the directory(in case they were changed manually)
|
622
|
+
const updatedFiles = await this.#readAllFromDestination();
|
623
|
+
const zipBuffer = await Zip.create(updatedFiles);
|
624
|
+
await this.#cleanup();
|
625
|
+
this.destroyed = true;
|
626
|
+
return zipBuffer;
|
627
|
+
}
|
628
|
+
finally {
|
629
|
+
this.#isProcessing = false;
|
630
|
+
}
|
378
631
|
}
|
379
632
|
/**
|
380
633
|
* Writes the modified Excel template to a writable stream.
|
@@ -385,12 +638,19 @@ class TemplateFs {
|
|
385
638
|
* @experimental This API is experimental and might change in future versions.
|
386
639
|
*/
|
387
640
|
async saveStream(output) {
|
641
|
+
this.#ensureNotProcessing();
|
388
642
|
this.#ensureNotDestroyed();
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
643
|
+
this.#isProcessing = true;
|
644
|
+
try {
|
645
|
+
// Guarantee integrity
|
646
|
+
await this.#validate();
|
647
|
+
await Zip.createWithStream(Array.from(this.fileKeys), this.destination, output);
|
648
|
+
await this.#cleanup();
|
649
|
+
this.destroyed = true;
|
650
|
+
}
|
651
|
+
finally {
|
652
|
+
this.#isProcessing = false;
|
653
|
+
}
|
394
654
|
}
|
395
655
|
/**
|
396
656
|
* Replaces the contents of a file in the template.
|
@@ -403,12 +663,15 @@ class TemplateFs {
|
|
403
663
|
* @experimental This API is experimental and might change in future versions.
|
404
664
|
*/
|
405
665
|
async set(key, content) {
|
666
|
+
this.#ensureNotProcessing();
|
406
667
|
this.#ensureNotDestroyed();
|
407
|
-
|
408
|
-
|
668
|
+
this.#isProcessing = true;
|
669
|
+
try {
|
670
|
+
await this.#set(key, content);
|
671
|
+
}
|
672
|
+
finally {
|
673
|
+
this.#isProcessing = false;
|
409
674
|
}
|
410
|
-
const fullPath = path.join(this.destination, ...key.split("/"));
|
411
|
-
await fs.writeFile(fullPath, Buffer.isBuffer(content) ? content : Buffer.from(content));
|
412
675
|
}
|
413
676
|
/**
|
414
677
|
* Validates the template by checking all required files exist.
|
@@ -419,15 +682,14 @@ class TemplateFs {
|
|
419
682
|
* @experimental This API is experimental and might change in future versions.
|
420
683
|
*/
|
421
684
|
async validate() {
|
685
|
+
this.#ensureNotProcessing();
|
422
686
|
this.#ensureNotDestroyed();
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
throw new Error(`Missing file in template directory: ${key}`);
|
430
|
-
}
|
687
|
+
this.#isProcessing = true;
|
688
|
+
try {
|
689
|
+
await this.#validate();
|
690
|
+
}
|
691
|
+
finally {
|
692
|
+
this.#isProcessing = false;
|
431
693
|
}
|
432
694
|
}
|
433
695
|
/**
|
@@ -463,3 +725,222 @@ class TemplateFs {
|
|
463
725
|
}
|
464
726
|
}
|
465
727
|
exports.TemplateFs = TemplateFs;
|
728
|
+
function processMergeCells(sheetXml) {
|
729
|
+
// Regular expression for finding <mergeCells> block
|
730
|
+
const mergeCellsBlockRegex = /<mergeCells[^>]*>[\s\S]*?<\/mergeCells>/;
|
731
|
+
// Find the first <mergeCells> block (if there are multiple, in xlsx usually there is only one)
|
732
|
+
const mergeCellsBlockMatch = sheetXml.match(mergeCellsBlockRegex);
|
733
|
+
const initialMergeCells = [];
|
734
|
+
const mergeCellMatches = [];
|
735
|
+
if (mergeCellsBlockMatch) {
|
736
|
+
const mergeCellsBlock = mergeCellsBlockMatch[0];
|
737
|
+
initialMergeCells.push(mergeCellsBlock);
|
738
|
+
// Extract <mergeCell ref="A1:B2"/> from this block
|
739
|
+
const mergeCellRegex = /<mergeCell ref="([A-Z]+\d+):([A-Z]+\d+)"\/>/g;
|
740
|
+
for (const match of mergeCellsBlock.matchAll(mergeCellRegex)) {
|
741
|
+
mergeCellMatches.push({ from: match[1], to: match[2] });
|
742
|
+
}
|
743
|
+
}
|
744
|
+
// Remove the <mergeCells> block from the XML
|
745
|
+
const modifiedXml = sheetXml.replace(mergeCellsBlockRegex, "");
|
746
|
+
return {
|
747
|
+
initialMergeCells,
|
748
|
+
mergeCellMatches,
|
749
|
+
modifiedXml,
|
750
|
+
};
|
751
|
+
}
|
752
|
+
;
|
753
|
+
function processSharedStrings(sharedStringsXml) {
|
754
|
+
// Final list of merged cells with all changes
|
755
|
+
const sheetMergeCells = [];
|
756
|
+
// Array for storing shared strings
|
757
|
+
const sharedStrings = [];
|
758
|
+
const sharedStringsHeader = Utils.extractXmlDeclaration(sharedStringsXml);
|
759
|
+
// Map for fast lookup of shared string index by content
|
760
|
+
const sharedIndexMap = new Map();
|
761
|
+
// Regular expression for finding <si> elements (shared string items)
|
762
|
+
const siRegex = /<si>([\s\S]*?)<\/si>/g;
|
763
|
+
// Parse sharedStringsXml and fill sharedStrings and sharedIndexMap
|
764
|
+
for (const match of sharedStringsXml.matchAll(siRegex)) {
|
765
|
+
const content = match[1];
|
766
|
+
if (!content)
|
767
|
+
throw new Error("Shared index not found");
|
768
|
+
const fullSi = `<si>${content}</si>`;
|
769
|
+
sharedIndexMap.set(content, sharedStrings.length);
|
770
|
+
sharedStrings.push(fullSi);
|
771
|
+
}
|
772
|
+
return {
|
773
|
+
sharedIndexMap,
|
774
|
+
sharedStrings,
|
775
|
+
sharedStringsHeader,
|
776
|
+
sheetMergeCells,
|
777
|
+
};
|
778
|
+
}
|
779
|
+
;
|
780
|
+
function processRows(data) {
|
781
|
+
const { mergeCellMatches, replacements, sharedIndexMap, sharedStrings, sheetMergeCells, sheetXml, } = data;
|
782
|
+
const TABLE_REGEX = /\$\{table:([a-zA-Z0-9_]+)\.([a-zA-Z0-9_]+)\}/g;
|
783
|
+
// Array for storing resulting XML rows
|
784
|
+
const resultRows = [];
|
785
|
+
// Previous position of processed part of XML
|
786
|
+
let lastIndex = 0;
|
787
|
+
// Shift for row numbers
|
788
|
+
let rowShift = 0;
|
789
|
+
// Regular expression for finding <row> elements
|
790
|
+
const rowRegex = /<row[^>]*?>[\s\S]*?<\/row>/g;
|
791
|
+
// Process each <row> element
|
792
|
+
for (const match of sheetXml.matchAll(rowRegex)) {
|
793
|
+
// Full XML row
|
794
|
+
const fullRow = match[0];
|
795
|
+
// Start position of the row in XML
|
796
|
+
const matchStart = match.index;
|
797
|
+
// End position of the row in XML
|
798
|
+
const matchEnd = matchStart + fullRow.length;
|
799
|
+
// Add the intermediate XML chunk (if any) between the previous and the current row
|
800
|
+
if (lastIndex !== matchStart) {
|
801
|
+
resultRows.push(sheetXml.slice(lastIndex, matchStart));
|
802
|
+
}
|
803
|
+
lastIndex = matchEnd;
|
804
|
+
// Get row number from r attribute
|
805
|
+
const originalRowNumber = parseInt(fullRow.match(/<row[^>]* r="(\d+)"/)?.[1] ?? "1", 10);
|
806
|
+
// Update row number based on rowShift
|
807
|
+
const shiftedRowNumber = originalRowNumber + rowShift;
|
808
|
+
// Find shared string indexes in cells of the current row
|
809
|
+
const sharedValueIndexes = [];
|
810
|
+
// Regular expression for finding a cell
|
811
|
+
const cellRegex = /<c[^>]*?r="([A-Z]+\d+)"[^>]*?>([\s\S]*?)<\/c>/g;
|
812
|
+
for (const cell of fullRow.matchAll(cellRegex)) {
|
813
|
+
const cellTag = cell[0];
|
814
|
+
// Check if the cell is a shared string
|
815
|
+
const isShared = /t="s"/.test(cellTag);
|
816
|
+
const valueMatch = cellTag.match(/<v>(\d+)<\/v>/);
|
817
|
+
if (isShared && valueMatch) {
|
818
|
+
sharedValueIndexes.push(parseInt(valueMatch[1], 10));
|
819
|
+
}
|
820
|
+
}
|
821
|
+
// Get the text content of shared strings by their indexes
|
822
|
+
const sharedTexts = sharedValueIndexes.map(i => sharedStrings[i]?.replace(/<\/?si>/g, "") ?? "");
|
823
|
+
// Find table placeholders in shared strings
|
824
|
+
const tablePlaceholders = sharedTexts.flatMap(e => [...e.matchAll(TABLE_REGEX)]);
|
825
|
+
// If there are no table placeholders, just shift the row
|
826
|
+
if (tablePlaceholders.length === 0) {
|
827
|
+
const updatedRow = fullRow
|
828
|
+
.replace(/(<row[^>]* r=")(\d+)(")/, `$1${shiftedRowNumber}$3`)
|
829
|
+
.replace(/<c r="([A-Z]+)(\d+)"/g, (_, col) => `<c r="${col}${shiftedRowNumber}"`);
|
830
|
+
resultRows.push(updatedRow);
|
831
|
+
// Update mergeCells for regular row with rowShift
|
832
|
+
const calculatedRowNumber = originalRowNumber + rowShift;
|
833
|
+
for (const { from, to } of mergeCellMatches) {
|
834
|
+
const [, fromCol, fromRow] = from.match(/^([A-Z]+)(\d+)$/);
|
835
|
+
const [, toCol] = to.match(/^([A-Z]+)(\d+)$/);
|
836
|
+
if (Number(fromRow) === calculatedRowNumber) {
|
837
|
+
const newFrom = `${fromCol}${shiftedRowNumber}`;
|
838
|
+
const newTo = `${toCol}${shiftedRowNumber}`;
|
839
|
+
sheetMergeCells.push(`<mergeCell ref="${newFrom}:${newTo}"/>`);
|
840
|
+
}
|
841
|
+
}
|
842
|
+
continue;
|
843
|
+
}
|
844
|
+
// Get the table name from the first placeholder
|
845
|
+
const firstMatch = tablePlaceholders[0];
|
846
|
+
const tableName = firstMatch?.[1];
|
847
|
+
if (!tableName)
|
848
|
+
throw new Error("Table name not found");
|
849
|
+
// Get data for replacement from replacements
|
850
|
+
const array = replacements[tableName];
|
851
|
+
if (!array)
|
852
|
+
continue;
|
853
|
+
if (!Array.isArray(array))
|
854
|
+
throw new Error("Table data is not an array");
|
855
|
+
const tableRowStart = shiftedRowNumber;
|
856
|
+
// Find mergeCells to duplicate (mergeCells that start with the current row)
|
857
|
+
const mergeCellsToDuplicate = mergeCellMatches.filter(({ from }) => {
|
858
|
+
const match = from.match(/^([A-Z]+)(\d+)$/);
|
859
|
+
if (!match)
|
860
|
+
return false;
|
861
|
+
// Row number of the merge cell start position is in the second group
|
862
|
+
const rowNumber = match[2];
|
863
|
+
return Number(rowNumber) === tableRowStart;
|
864
|
+
});
|
865
|
+
// Change the current row to multiple rows from the data array
|
866
|
+
for (let i = 0; i < array.length; i++) {
|
867
|
+
const rowData = array[i];
|
868
|
+
let newRow = fullRow;
|
869
|
+
// Replace placeholders in shared strings with real data
|
870
|
+
sharedValueIndexes.forEach((originalIdx, idx) => {
|
871
|
+
const originalText = sharedTexts[idx];
|
872
|
+
if (!originalText)
|
873
|
+
throw new Error("Shared value not found");
|
874
|
+
// Replace placeholders ${tableName.field} with real data from array data
|
875
|
+
const replacedText = originalText.replace(TABLE_REGEX, (_, tbl, field) => tbl === tableName ? String(rowData?.[field] ?? "") : "");
|
876
|
+
// Add new text to shared strings if it doesn't exist
|
877
|
+
let newIndex;
|
878
|
+
if (sharedIndexMap.has(replacedText)) {
|
879
|
+
newIndex = sharedIndexMap.get(replacedText);
|
880
|
+
}
|
881
|
+
else {
|
882
|
+
newIndex = sharedStrings.length;
|
883
|
+
sharedIndexMap.set(replacedText, newIndex);
|
884
|
+
sharedStrings.push(`<si>${replacedText}</si>`);
|
885
|
+
}
|
886
|
+
// Replace the shared string index in the cell
|
887
|
+
newRow = newRow.replace(`<v>${originalIdx}</v>`, `<v>${newIndex}</v>`);
|
888
|
+
});
|
889
|
+
// Update row number and cell references
|
890
|
+
const newRowNum = shiftedRowNumber + i;
|
891
|
+
newRow = newRow
|
892
|
+
.replace(/<row[^>]* r="\d+"/, rowTag => rowTag.replace(/r="\d+"/, `r="${newRowNum}"`))
|
893
|
+
.replace(/<c r="([A-Z]+)\d+"/g, (_, col) => `<c r="${col}${newRowNum}"`);
|
894
|
+
resultRows.push(newRow);
|
895
|
+
// Add duplicate mergeCells for new rows
|
896
|
+
for (const { from, to } of mergeCellsToDuplicate) {
|
897
|
+
const [, colFrom, rowFrom] = from.match(/^([A-Z]+)(\d+)$/);
|
898
|
+
const [, colTo, rowTo] = to.match(/^([A-Z]+)(\d+)$/);
|
899
|
+
const newFrom = `${colFrom}${Number(rowFrom) + i}`;
|
900
|
+
const newTo = `${colTo}${Number(rowTo) + i}`;
|
901
|
+
sheetMergeCells.push(`<mergeCell ref="${newFrom}:${newTo}"/>`);
|
902
|
+
}
|
903
|
+
}
|
904
|
+
// It increases the row shift by the number of added rows minus one replaced
|
905
|
+
rowShift += array.length - 1;
|
906
|
+
const delta = array.length - 1;
|
907
|
+
const calculatedRowNumber = originalRowNumber + rowShift - array.length + 1;
|
908
|
+
if (delta > 0) {
|
909
|
+
for (const merge of mergeCellMatches) {
|
910
|
+
const fromRow = parseInt(merge.from.match(/\d+$/)[0], 10);
|
911
|
+
if (fromRow > calculatedRowNumber) {
|
912
|
+
merge.from = merge.from.replace(/\d+$/, r => `${parseInt(r) + delta}`);
|
913
|
+
merge.to = merge.to.replace(/\d+$/, r => `${parseInt(r) + delta}`);
|
914
|
+
}
|
915
|
+
}
|
916
|
+
}
|
917
|
+
}
|
918
|
+
return { lastIndex, resultRows, rowShift };
|
919
|
+
}
|
920
|
+
;
|
921
|
+
function processBuild(data) {
|
922
|
+
const { initialMergeCells, lastIndex, mergeCellMatches, resultRows, rowShift, sharedStrings, sharedStringsHeader, sheetMergeCells, sheetXml, } = data;
|
923
|
+
for (const { from, to } of mergeCellMatches) {
|
924
|
+
const [, fromCol, fromRow] = from.match(/^([A-Z]+)(\d+)$/);
|
925
|
+
const [, toCol, toRow] = to.match(/^([A-Z]+)(\d+)$/);
|
926
|
+
const fromRowNum = Number(fromRow);
|
927
|
+
// These rows have already been processed, don't add duplicates
|
928
|
+
if (fromRowNum <= lastIndex)
|
929
|
+
continue;
|
930
|
+
const newFrom = `${fromCol}${fromRowNum + rowShift}`;
|
931
|
+
const newTo = `${toCol}${Number(toRow) + rowShift}`;
|
932
|
+
sheetMergeCells.push(`<mergeCell ref="${newFrom}:${newTo}"/>`);
|
933
|
+
}
|
934
|
+
resultRows.push(sheetXml.slice(lastIndex));
|
935
|
+
// Form XML for mergeCells if there are any
|
936
|
+
const mergeXml = sheetMergeCells.length
|
937
|
+
? `<mergeCells count="${sheetMergeCells.length}">${sheetMergeCells.join("")}</mergeCells>`
|
938
|
+
: initialMergeCells;
|
939
|
+
// Insert mergeCells before the closing sheetData tag
|
940
|
+
const sheetWithMerge = resultRows.join("").replace(/<\/sheetData>/, `</sheetData>${mergeXml}`);
|
941
|
+
// Return modified sheet XML and shared strings
|
942
|
+
return {
|
943
|
+
shared: `${sharedStringsHeader}\n<sst xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" count="${sharedStrings.length}" uniqueCount="${sharedStrings.length}">${sharedStrings.join("")}</sst>`,
|
944
|
+
sheet: Utils.updateDimension(sheetWithMerge),
|
945
|
+
};
|
946
|
+
}
|