@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
@@ -33,9 +33,7 @@ function validateWorksheetXml(xml) {
|
|
33
33
|
const requiredElements = [
|
34
34
|
{ name: "sheetViews", tag: "<sheetViews>" },
|
35
35
|
{ name: "sheetFormatPr", tag: "<sheetFormatPr" },
|
36
|
-
{ name: "cols", tag: "<cols>" },
|
37
36
|
{ name: "sheetData", tag: "<sheetData>" },
|
38
|
-
{ name: "mergeCells", tag: "<mergeCells" },
|
39
37
|
];
|
40
38
|
for (const { name, tag } of requiredElements) {
|
41
39
|
if (!xml.includes(tag)) {
|
@@ -100,59 +98,82 @@ function validateWorksheetXml(xml) {
|
|
100
98
|
}
|
101
99
|
}
|
102
100
|
// 4. Check mergeCells
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
const
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
101
|
+
if (xml.includes("<mergeCells")) {
|
102
|
+
const mergeCellsStart = xml.indexOf("<mergeCells");
|
103
|
+
const mergeCellsEnd = xml.indexOf("</mergeCells>");
|
104
|
+
if (mergeCellsStart === -1 || mergeCellsEnd === -1) {
|
105
|
+
return createError("Invalid mergeCells structure");
|
106
|
+
}
|
107
|
+
const mergeCellsContent = xml.substring(mergeCellsStart, mergeCellsEnd);
|
108
|
+
const countMatch = mergeCellsContent.match(/count="(\d+)"/);
|
109
|
+
if (!countMatch) {
|
110
|
+
return createError("Count attribute not specified for mergeCells");
|
111
|
+
}
|
112
|
+
const mergeCellTags = mergeCellsContent.match(/<mergeCell\s+ref="([A-Z]+\d+:[A-Z]+\d+)"\s*\/>/g);
|
113
|
+
if (!mergeCellTags) {
|
114
|
+
return createError("No merged cells found");
|
115
|
+
}
|
116
|
+
// Check if the number of mergeCells matches the count attribute
|
117
|
+
if (mergeCellTags.length !== parseInt(countMatch[1])) {
|
118
|
+
return createError("Mismatch in the number of merged cells", `Expected: ${countMatch[1]}, found: ${mergeCellTags.length}`);
|
119
|
+
}
|
120
|
+
// Check for duplicates of mergeCell
|
121
|
+
const mergeRefs = new Set();
|
122
|
+
const duplicates = new Set();
|
123
|
+
for (const mergeTag of mergeCellTags) {
|
124
|
+
const refMatch = mergeTag.match(/ref="([A-Z]+\d+:[A-Z]+\d+)"/);
|
125
|
+
if (!refMatch) {
|
126
|
+
return createError("Invalid merge cell format", `Tag: ${mergeTag}`);
|
127
|
+
}
|
128
|
+
const ref = refMatch[1];
|
129
|
+
if (mergeRefs.has(ref)) {
|
130
|
+
duplicates.add(ref);
|
131
|
+
}
|
132
|
+
else {
|
133
|
+
mergeRefs.add(ref);
|
134
|
+
}
|
132
135
|
}
|
133
|
-
|
134
|
-
|
136
|
+
if (duplicates.size > 0) {
|
137
|
+
return createError("Duplicates of merged cells found", `Duplicates: ${Array.from(duplicates).join(", ")}`);
|
138
|
+
}
|
139
|
+
// Check for overlapping merge ranges
|
140
|
+
const mergedRanges = Array.from(mergeRefs).map(ref => {
|
141
|
+
const [start, end] = ref.split(":");
|
142
|
+
return {
|
143
|
+
endCol: end.match(/[A-Z]+/)?.[0] || "",
|
144
|
+
endRow: parseInt(end.match(/\d+/)?.[0] || "0"),
|
145
|
+
startCol: start.match(/[A-Z]+/)?.[0] || "",
|
146
|
+
startRow: parseInt(start.match(/\d+/)?.[0] || "0"),
|
147
|
+
};
|
148
|
+
});
|
149
|
+
for (let i = 0; i < mergedRanges.length; i++) {
|
150
|
+
for (let j = i + 1; j < mergedRanges.length; j++) {
|
151
|
+
const a = mergedRanges[i];
|
152
|
+
const b = mergedRanges[j];
|
153
|
+
if (rangesIntersect(a, b)) {
|
154
|
+
return createError("Found intersecting merged cells", `Intersecting: ${getRangeString(a)} and ${getRangeString(b)}`);
|
155
|
+
}
|
156
|
+
}
|
135
157
|
}
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
const
|
153
|
-
|
154
|
-
|
155
|
-
return createError("Found intersecting merged cells", `Intersecting: ${getRangeString(a)} and ${getRangeString(b)}`);
|
158
|
+
// 6. Additional check: all mergeCell tags refer to existing cells
|
159
|
+
for (const mergeTag of mergeCellTags) {
|
160
|
+
const refMatch = mergeTag.match(/ref="([A-Z]+\d+:[A-Z]+\d+)"/);
|
161
|
+
if (!refMatch) {
|
162
|
+
return createError("Invalid merge cell format", `Tag: ${mergeTag}`);
|
163
|
+
}
|
164
|
+
const [cell1, cell2] = refMatch[1].split(":");
|
165
|
+
const cell1Col = cell1.match(/[A-Z]+/)?.[0];
|
166
|
+
const cell1Row = parseInt(cell1.match(/\d+/)?.[0] || "0");
|
167
|
+
const cell2Col = cell2.match(/[A-Z]+/)?.[0];
|
168
|
+
const cell2Row = parseInt(cell2.match(/\d+/)?.[0] || "0");
|
169
|
+
if (!cell1Col || !cell2Col || isNaN(cell1Row) || isNaN(cell2Row)) {
|
170
|
+
return createError("Invalid merged cell coordinates", `Merged cells: ${refMatch[1]}`);
|
171
|
+
}
|
172
|
+
// Check if the merged cells exist
|
173
|
+
const cell1Exists = allCells.some(c => c.row === cell1Row && c.col === cell1Col);
|
174
|
+
const cell2Exists = allCells.some(c => c.row === cell2Row && c.col === cell2Col);
|
175
|
+
if (!cell1Exists || !cell2Exists) {
|
176
|
+
return createError("Merged cell reference points to non-existent cells", `Merged cells: ${refMatch[1]}, missing: ${!cell1Exists ? `${cell1Col}${cell1Row}` : `${cell2Col}${cell2Row}`}`);
|
156
177
|
}
|
157
178
|
}
|
158
179
|
}
|
@@ -181,27 +202,6 @@ function validateWorksheetXml(xml) {
|
|
181
202
|
return createError("Cell is outside the specified area (by column)", `Cell: ${cell.col}${cell.row}, dimension: ${dimensionMatch[1]}`);
|
182
203
|
}
|
183
204
|
}
|
184
|
-
// 6. Additional check: all mergeCell tags refer to existing cells
|
185
|
-
for (const mergeTag of mergeCellTags) {
|
186
|
-
const refMatch = mergeTag.match(/ref="([A-Z]+\d+:[A-Z]+\d+)"/);
|
187
|
-
if (!refMatch) {
|
188
|
-
return createError("Invalid merge cell format", `Tag: ${mergeTag}`);
|
189
|
-
}
|
190
|
-
const [cell1, cell2] = refMatch[1].split(":");
|
191
|
-
const cell1Col = cell1.match(/[A-Z]+/)?.[0];
|
192
|
-
const cell1Row = parseInt(cell1.match(/\d+/)?.[0] || "0");
|
193
|
-
const cell2Col = cell2.match(/[A-Z]+/)?.[0];
|
194
|
-
const cell2Row = parseInt(cell2.match(/\d+/)?.[0] || "0");
|
195
|
-
if (!cell1Col || !cell2Col || isNaN(cell1Row) || isNaN(cell2Row)) {
|
196
|
-
return createError("Invalid merged cell coordinates", `Merged cells: ${refMatch[1]}`);
|
197
|
-
}
|
198
|
-
// Check if the merged cells exist
|
199
|
-
const cell1Exists = allCells.some(c => c.row === cell1Row && c.col === cell1Col);
|
200
|
-
const cell2Exists = allCells.some(c => c.row === cell2Row && c.col === cell2Col);
|
201
|
-
if (!cell1Exists || !cell2Exists) {
|
202
|
-
return createError("Merged cell reference points to non-existent cells", `Merged cells: ${refMatch[1]}, missing: ${!cell1Exists ? `${cell1Col}${cell1Row}` : `${cell2Col}${cell2Row}`}`);
|
203
|
-
}
|
204
|
-
}
|
205
205
|
return { isValid: true };
|
206
206
|
}
|
207
207
|
// A function to check if two ranges intersect
|
@@ -13,34 +13,74 @@ const prepare_row_to_cells_js_1 = require("./prepare-row-to-cells.js");
|
|
13
13
|
* for the first row written to the file. Subsequent rows are written
|
14
14
|
* with incrementing row numbers.
|
15
15
|
*
|
16
|
-
* @param output - A file write stream to write the Excel XML to.
|
17
|
-
* @param rows - An async iterable of rows, where each row is an array
|
18
|
-
*
|
19
|
-
* @param startRowNumber - The starting row number to use for the first
|
20
|
-
*
|
21
|
-
*
|
22
|
-
*
|
23
|
-
*
|
24
|
-
*
|
16
|
+
* @param {WritableLike} output - A file write stream to write the Excel XML to.
|
17
|
+
* @param {AsyncIterable<unknown[] | unknown[][]>} rows - An async iterable of rows, where each row is an array
|
18
|
+
* of values or an array of arrays of values.
|
19
|
+
* @param {number} startRowNumber - The starting row number to use for the first
|
20
|
+
* row written to the file.
|
21
|
+
* @returns {Promise<{
|
22
|
+
* dimension: {
|
23
|
+
* maxColumn: string;
|
24
|
+
* maxRow: number;
|
25
|
+
* minColumn: string;
|
26
|
+
* minRow: number;
|
27
|
+
* };
|
28
|
+
* rowNumber: number;
|
29
|
+
* }>} An object containing:
|
30
|
+
* - dimension: The boundaries of the written data (min/max columns and rows)
|
31
|
+
* - rowNumber: The last row number written to the file
|
25
32
|
*/
|
26
33
|
async function writeRowsToStream(output, rows, startRowNumber) {
|
27
34
|
let rowNumber = startRowNumber;
|
35
|
+
const dimension = {
|
36
|
+
maxColumn: "A",
|
37
|
+
maxRow: startRowNumber,
|
38
|
+
minColumn: "A",
|
39
|
+
minRow: startRowNumber,
|
40
|
+
};
|
41
|
+
// Функция для сравнения колонок (A < B, AA > Z и т.д.)
|
42
|
+
const compareColumns = (a, b) => {
|
43
|
+
if (a === b)
|
44
|
+
return 0;
|
45
|
+
return a.length === b.length ? (a < b ? -1 : 1) : (a.length < b.length ? -1 : 1);
|
46
|
+
};
|
47
|
+
const processRow = (row, currentRowNumber) => {
|
48
|
+
const cells = (0, prepare_row_to_cells_js_1.prepareRowToCells)(row, currentRowNumber);
|
49
|
+
if (cells.length === 0)
|
50
|
+
return;
|
51
|
+
output.write(`<row r="${currentRowNumber}">${cells.map(cell => cell.cellXml).join("")}</row>`);
|
52
|
+
// Обновление границ
|
53
|
+
const firstCellRef = cells[0]?.cellRef;
|
54
|
+
const lastCellRef = cells[cells.length - 1]?.cellRef;
|
55
|
+
if (firstCellRef) {
|
56
|
+
const colLetters = firstCellRef.match(/[A-Z]+/)?.[0] || "";
|
57
|
+
if (compareColumns(colLetters, dimension.minColumn) < 0) {
|
58
|
+
dimension.minColumn = colLetters;
|
59
|
+
}
|
60
|
+
}
|
61
|
+
if (lastCellRef) {
|
62
|
+
const colLetters = lastCellRef.match(/[A-Z]+/)?.[0] || "";
|
63
|
+
if (compareColumns(colLetters, dimension.maxColumn) > 0) {
|
64
|
+
dimension.maxColumn = colLetters;
|
65
|
+
}
|
66
|
+
}
|
67
|
+
dimension.maxRow = currentRowNumber;
|
68
|
+
};
|
28
69
|
for await (const row of rows) {
|
29
|
-
|
70
|
+
if (!row.length)
|
71
|
+
continue;
|
30
72
|
if (Array.isArray(row[0])) {
|
31
73
|
for (const subRow of row) {
|
32
|
-
|
33
|
-
|
34
|
-
|
74
|
+
if (!subRow.length)
|
75
|
+
continue;
|
76
|
+
processRow(subRow, rowNumber);
|
35
77
|
rowNumber++;
|
36
78
|
}
|
37
79
|
}
|
38
80
|
else {
|
39
|
-
|
40
|
-
// Write the row to the file
|
41
|
-
output.write(`<row r="${rowNumber}">${cells.join("")}</row>`);
|
81
|
+
processRow(row, rowNumber);
|
42
82
|
rowNumber++;
|
43
83
|
}
|
44
84
|
}
|
45
|
-
return { rowNumber };
|
85
|
+
return { dimension, rowNumber };
|
46
86
|
}
|
@@ -2,6 +2,7 @@ import * as fs from "node:fs/promises";
|
|
2
2
|
import * as fsSync from "node:fs";
|
3
3
|
import * as path from "node:path";
|
4
4
|
import * as readline from "node:readline";
|
5
|
+
import crypto from "node:crypto";
|
5
6
|
import * as Xml from "../xml/index.js";
|
6
7
|
import * as Zip from "../zip/index.js";
|
7
8
|
import * as Utils from "./utils/index.js";
|
@@ -31,6 +32,16 @@ export class TemplateFs {
|
|
31
32
|
* @type {boolean}
|
32
33
|
*/
|
33
34
|
#isProcessing = false;
|
35
|
+
/**
|
36
|
+
* The keys for the Excel files in the template.
|
37
|
+
*/
|
38
|
+
#excelKeys = {
|
39
|
+
contentTypes: "[Content_Types].xml",
|
40
|
+
sharedStrings: "xl/sharedStrings.xml",
|
41
|
+
styles: "xl/styles.xml",
|
42
|
+
workbook: "xl/workbook.xml",
|
43
|
+
workbookRels: "xl/_rels/workbook.xml.rels",
|
44
|
+
};
|
34
45
|
/**
|
35
46
|
* Creates a Template instance.
|
36
47
|
*
|
@@ -113,17 +124,19 @@ export class TemplateFs {
|
|
113
124
|
* @returns The path of the sheet inside the workbook.
|
114
125
|
* @throws {Error} If the sheet is not found.
|
115
126
|
*/
|
116
|
-
async #
|
127
|
+
async #getSheetPathByName(sheetName) {
|
117
128
|
// Read XML workbook to find sheet name and path
|
118
|
-
const workbookXml = Xml.extractXmlFromSheet(await this.#readFile(
|
119
|
-
const sheetMatch = workbookXml.match(
|
120
|
-
if (!sheetMatch)
|
129
|
+
const workbookXml = Xml.extractXmlFromSheet(await this.#readFile(this.#excelKeys.workbook));
|
130
|
+
const sheetMatch = workbookXml.match(Utils.sheetMatch(sheetName));
|
131
|
+
if (!sheetMatch || !sheetMatch[1]) {
|
121
132
|
throw new Error(`Sheet "${sheetName}" not found`);
|
133
|
+
}
|
122
134
|
const rId = sheetMatch[1];
|
123
|
-
const relsXml = Xml.extractXmlFromSheet(await this.#readFile(
|
124
|
-
const relMatch = relsXml.match(
|
125
|
-
if (!relMatch)
|
135
|
+
const relsXml = Xml.extractXmlFromSheet(await this.#readFile(this.#excelKeys.workbookRels));
|
136
|
+
const relMatch = relsXml.match(Utils.relationshipMatch(rId));
|
137
|
+
if (!relMatch || !relMatch[1]) {
|
126
138
|
throw new Error(`Relationship "${rId}" not found`);
|
139
|
+
}
|
127
140
|
return "xl/" + relMatch[1].replace(/^\/?xl\//, "");
|
128
141
|
}
|
129
142
|
/**
|
@@ -173,8 +186,8 @@ export class TemplateFs {
|
|
173
186
|
await fs.writeFile(fullPath, Buffer.isBuffer(content) ? content : Buffer.from(content));
|
174
187
|
}
|
175
188
|
async #substitute(sheetName, replacements) {
|
176
|
-
const sharedStringsPath =
|
177
|
-
const sheetPath = await this.#
|
189
|
+
const sharedStringsPath = this.#excelKeys.sharedStrings;
|
190
|
+
const sheetPath = await this.#getSheetPathByName(sheetName);
|
178
191
|
let sharedStringsContent = "";
|
179
192
|
let sheetContent = "";
|
180
193
|
if (this.fileKeys.has(sharedStringsPath)) {
|
@@ -233,30 +246,31 @@ export class TemplateFs {
|
|
233
246
|
this.#ensureNotDestroyed();
|
234
247
|
this.#isProcessing = true;
|
235
248
|
try {
|
249
|
+
if (sourceName === newName) {
|
250
|
+
throw new Error("Source and new sheet names cannot be the same");
|
251
|
+
}
|
236
252
|
// Read workbook.xml and find the source sheet
|
237
|
-
const workbookXmlPath =
|
253
|
+
const workbookXmlPath = this.#excelKeys.workbook;
|
238
254
|
const workbookXml = Xml.extractXmlFromSheet(await this.#readFile(workbookXmlPath));
|
239
255
|
// Find the source sheet
|
240
|
-
const
|
241
|
-
|
242
|
-
if (!sheetMatch)
|
256
|
+
const sheetMatch = workbookXml.match(Utils.sheetMatch(sourceName));
|
257
|
+
if (!sheetMatch || !sheetMatch[1]) {
|
243
258
|
throw new Error(`Sheet "${sourceName}" not found`);
|
244
|
-
|
259
|
+
}
|
245
260
|
// Check if a sheet with the new name already exists
|
246
261
|
if (new RegExp(`<sheet[^>]+name="${newName}"`).test(workbookXml)) {
|
247
262
|
throw new Error(`Sheet "${newName}" already exists`);
|
248
263
|
}
|
249
264
|
// Read workbook.rels
|
250
265
|
// Find the source sheet path by rId
|
251
|
-
const
|
266
|
+
const rId = sheetMatch[1];
|
267
|
+
const relsXmlPath = this.#excelKeys.workbookRels;
|
252
268
|
const relsXml = Xml.extractXmlFromSheet(await this.#readFile(relsXmlPath));
|
253
|
-
const
|
254
|
-
|
255
|
-
|
256
|
-
|
269
|
+
const relMatch = relsXml.match(Utils.relationshipMatch(rId));
|
270
|
+
if (!relMatch || !relMatch[1]) {
|
271
|
+
throw new Error(`Relationship "${rId}" not found`);
|
272
|
+
}
|
257
273
|
const sourceTarget = relMatch[1]; // sheetN.xml
|
258
|
-
if (!sourceTarget)
|
259
|
-
throw new Error(`Relationship "${sourceRId}" not found`);
|
260
274
|
const sourceSheetPath = "xl/" + sourceTarget.replace(/^\/?.*xl\//, "");
|
261
275
|
// Get the index of the new sheet
|
262
276
|
const sheetNumbers = [...this.fileKeys]
|
@@ -285,7 +299,7 @@ export class TemplateFs {
|
|
285
299
|
await this.#set(relsXmlPath, updatedRelsXml);
|
286
300
|
// Read [Content_Types].xml
|
287
301
|
// Update [Content_Types].xml
|
288
|
-
const contentTypesPath =
|
302
|
+
const contentTypesPath = this.#excelKeys.contentTypes;
|
289
303
|
const contentTypesXml = Xml.extractXmlFromSheet(await this.#readFile(contentTypesPath));
|
290
304
|
const overrideTag = `<Override PartName="/xl/worksheets/${newSheetFilename}" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>`;
|
291
305
|
const updatedContentTypesXml = contentTypesXml.replace("</Types>", overrideTag + "</Types>");
|
@@ -343,18 +357,7 @@ export class TemplateFs {
|
|
343
357
|
Utils.checkStartRow(startRowNumber);
|
344
358
|
Utils.checkRows(preparedRows);
|
345
359
|
// Find the sheet
|
346
|
-
const
|
347
|
-
const sheetMatch = workbookXml.match(new RegExp(`<sheet[^>]+name="${sheetName}"[^>]+r:id="([^"]+)"[^>]*/>`));
|
348
|
-
if (!sheetMatch || !sheetMatch[1]) {
|
349
|
-
throw new Error(`Sheet "${sheetName}" not found`);
|
350
|
-
}
|
351
|
-
const rId = sheetMatch[1];
|
352
|
-
const relsXml = Xml.extractXmlFromSheet(await this.#readFile("xl/_rels/workbook.xml.rels"));
|
353
|
-
const relMatch = relsXml.match(new RegExp(`<Relationship[^>]+Id="${rId}"[^>]+Target="([^"]+)"[^>]*/>`));
|
354
|
-
if (!relMatch || !relMatch[1]) {
|
355
|
-
throw new Error(`Relationship "${rId}" not found`);
|
356
|
-
}
|
357
|
-
const sheetPath = "xl/" + relMatch[1].replace(/^\/?xl\//, "");
|
360
|
+
const sheetPath = await this.#getSheetPathByName(sheetName);
|
358
361
|
const sheetXmlRaw = await this.#readFile(sheetPath);
|
359
362
|
const sheetXml = Xml.extractXmlFromSheet(sheetXmlRaw);
|
360
363
|
let nextRow = 0;
|
@@ -390,7 +393,7 @@ export class TemplateFs {
|
|
390
393
|
else {
|
391
394
|
updatedXml = sheetXml.replace(/<worksheet[^>]*>/, (match) => `${match}<sheetData>${rowsXml}</sheetData>`);
|
392
395
|
}
|
393
|
-
await this.#set(sheetPath, updatedXml);
|
396
|
+
await this.#set(sheetPath, Utils.updateDimension(updatedXml));
|
394
397
|
}
|
395
398
|
finally {
|
396
399
|
this.#isProcessing = false;
|
@@ -418,18 +421,8 @@ export class TemplateFs {
|
|
418
421
|
const { rows, sheetName, startRowNumber } = data;
|
419
422
|
if (!sheetName)
|
420
423
|
throw new Error("Sheet name is required");
|
421
|
-
//
|
422
|
-
const
|
423
|
-
const sheetMatch = workbookXml.match(new RegExp(`<sheet[^>]+name="${sheetName}"[^>]+r:id="([^"]+)"[^>]*/>`));
|
424
|
-
if (!sheetMatch)
|
425
|
-
throw new Error(`Sheet "${sheetName}" not found`);
|
426
|
-
const rId = sheetMatch[1];
|
427
|
-
const relsXml = Xml.extractXmlFromSheet(await this.#readFile("xl/_rels/workbook.xml.rels"));
|
428
|
-
const relMatch = relsXml.match(new RegExp(`<Relationship[^>]+Id="${rId}"[^>]+Target="([^"]+)"[^>]*/>`));
|
429
|
-
if (!relMatch)
|
430
|
-
throw new Error(`Relationship "${rId}" not found`);
|
431
|
-
// Path to the desired sheet (sheet1.xml)
|
432
|
-
const sheetPath = "xl/" + relMatch[1].replace(/^\/?xl\//, "");
|
424
|
+
// Get the path to the sheet
|
425
|
+
const sheetPath = await this.#getSheetPathByName(sheetName);
|
433
426
|
// The temporary file for writing
|
434
427
|
const fullPath = path.join(this.destination, ...sheetPath.split("/"));
|
435
428
|
const tempPath = fullPath + ".tmp";
|
@@ -438,6 +431,13 @@ export class TemplateFs {
|
|
438
431
|
const output = fsSync.createWriteStream(tempPath, { encoding: "utf-8" });
|
439
432
|
// Inserted rows flag
|
440
433
|
let inserted = false;
|
434
|
+
let initialDimension = "";
|
435
|
+
const dimension = {
|
436
|
+
maxColumn: "A",
|
437
|
+
maxRow: 1,
|
438
|
+
minColumn: "A",
|
439
|
+
minRow: 1,
|
440
|
+
};
|
441
441
|
const rl = readline.createInterface({
|
442
442
|
// Process all line breaks
|
443
443
|
crlfDelay: Infinity,
|
@@ -446,6 +446,21 @@ export class TemplateFs {
|
|
446
446
|
let isCollecting = false;
|
447
447
|
let collection = "";
|
448
448
|
for await (const line of rl) {
|
449
|
+
// Process <dimension>
|
450
|
+
if (!initialDimension && /<dimension\s+ref="[^"]*"/.test(line)) {
|
451
|
+
const dimensionMatch = line.match(/<dimension\s+ref="([^"]*)"/);
|
452
|
+
if (dimensionMatch) {
|
453
|
+
const dimensionRef = dimensionMatch[1];
|
454
|
+
if (dimensionRef) {
|
455
|
+
const [min, max] = dimensionRef.split(":");
|
456
|
+
dimension.minColumn = min.slice(0, 1);
|
457
|
+
dimension.minRow = parseInt(min.slice(1));
|
458
|
+
dimension.maxColumn = max.slice(0, 1);
|
459
|
+
dimension.maxRow = parseInt(max.slice(1));
|
460
|
+
}
|
461
|
+
initialDimension = line.match(/<dimension\s+ref="[^"]*"/)?.[0] || "";
|
462
|
+
}
|
463
|
+
}
|
449
464
|
// Collect lines between <sheetData> and </sheetData>
|
450
465
|
if (!inserted && isCollecting) {
|
451
466
|
collection += line;
|
@@ -472,7 +487,13 @@ export class TemplateFs {
|
|
472
487
|
output.write(innerRows);
|
473
488
|
}
|
474
489
|
}
|
475
|
-
const { rowNumber: actualRowNumber } = await Utils.writeRowsToStream(output, rows, maxRowNumber);
|
490
|
+
const { dimension: newDimension, rowNumber: actualRowNumber } = await Utils.writeRowsToStream(output, rows, maxRowNumber);
|
491
|
+
if (compareColumns(newDimension.maxColumn, dimension.maxColumn) > 0) {
|
492
|
+
dimension.maxColumn = newDimension.maxColumn;
|
493
|
+
}
|
494
|
+
if (newDimension.maxRow > dimension.maxRow) {
|
495
|
+
dimension.maxRow = newDimension.maxRow;
|
496
|
+
}
|
476
497
|
if (innerRows) {
|
477
498
|
const filteredRows = Utils.getRowsAbove(innerRowsMap, actualRowNumber);
|
478
499
|
if (filteredRows)
|
@@ -513,7 +534,13 @@ export class TemplateFs {
|
|
513
534
|
}
|
514
535
|
}
|
515
536
|
// new <row>
|
516
|
-
const { rowNumber: actualRowNumber } = await Utils.writeRowsToStream(output, rows, maxRowNumber);
|
537
|
+
const { dimension: newDimension, rowNumber: actualRowNumber } = await Utils.writeRowsToStream(output, rows, maxRowNumber);
|
538
|
+
if (compareColumns(newDimension.maxColumn, dimension.maxColumn) > 0) {
|
539
|
+
dimension.maxColumn = newDimension.maxColumn;
|
540
|
+
}
|
541
|
+
if (newDimension.maxRow > dimension.maxRow) {
|
542
|
+
dimension.maxRow = newDimension.maxRow;
|
543
|
+
}
|
517
544
|
if (innerRows) {
|
518
545
|
const filteredRows = Utils.getRowsAbove(innerRowsMap, actualRowNumber);
|
519
546
|
if (filteredRows) {
|
@@ -540,7 +567,13 @@ export class TemplateFs {
|
|
540
567
|
// Insert opening tag
|
541
568
|
output.write("<sheetData>");
|
542
569
|
// Prepare the rows
|
543
|
-
await Utils.writeRowsToStream(output, rows, maxRowNumber);
|
570
|
+
const { dimension: newDimension } = await Utils.writeRowsToStream(output, rows, maxRowNumber);
|
571
|
+
if (compareColumns(newDimension.maxColumn, dimension.maxColumn) > 0) {
|
572
|
+
dimension.maxColumn = newDimension.maxColumn;
|
573
|
+
}
|
574
|
+
if (newDimension.maxRow > dimension.maxRow) {
|
575
|
+
dimension.maxRow = newDimension.maxRow;
|
576
|
+
}
|
544
577
|
// Insert closing tag
|
545
578
|
output.write("</sheetData>");
|
546
579
|
if (after) {
|
@@ -561,8 +594,42 @@ export class TemplateFs {
|
|
561
594
|
// Close the streams
|
562
595
|
rl.close();
|
563
596
|
output.end();
|
564
|
-
//
|
565
|
-
|
597
|
+
// update dimension
|
598
|
+
{
|
599
|
+
const target = initialDimension;
|
600
|
+
const refRange = `${dimension.minColumn}${dimension.minRow}:${dimension.maxColumn}${dimension.maxRow}`;
|
601
|
+
const replacement = `<dimension ref="${refRange}"`;
|
602
|
+
let buffer = "";
|
603
|
+
let replaced = false;
|
604
|
+
const input = fsSync.createReadStream(tempPath, { encoding: "utf8" });
|
605
|
+
const output = fsSync.createWriteStream(fullPath);
|
606
|
+
await new Promise((resolve, reject) => {
|
607
|
+
input.on("data", chunk => {
|
608
|
+
buffer += chunk;
|
609
|
+
if (!replaced) {
|
610
|
+
const index = buffer.indexOf(target);
|
611
|
+
if (index !== -1) {
|
612
|
+
// Заменяем только первое вхождение
|
613
|
+
buffer = buffer.replace(target, replacement);
|
614
|
+
replaced = true;
|
615
|
+
}
|
616
|
+
}
|
617
|
+
output.write(buffer);
|
618
|
+
buffer = ""; // очищаем, т.к. мы уже записали
|
619
|
+
});
|
620
|
+
input.on("error", reject);
|
621
|
+
output.on("error", reject);
|
622
|
+
input.on("end", () => {
|
623
|
+
// на всякий случай дописываем остаток
|
624
|
+
if (buffer) {
|
625
|
+
output.write(buffer);
|
626
|
+
}
|
627
|
+
output.end();
|
628
|
+
resolve(true);
|
629
|
+
});
|
630
|
+
});
|
631
|
+
}
|
632
|
+
await fs.unlink(tempPath);
|
566
633
|
}
|
567
634
|
finally {
|
568
635
|
this.#isProcessing = false;
|
@@ -663,28 +730,38 @@ export class TemplateFs {
|
|
663
730
|
* @param {Object} data - The data to create the template from.
|
664
731
|
* @param {string} data.source - The path or buffer of the Excel file.
|
665
732
|
* @param {string} data.destination - The path to save the template to.
|
733
|
+
* @param {boolean} data.isUniqueDestination - Whether to add a random UUID to the destination path.
|
666
734
|
* @returns {Promise<Template>} A new Template instance.
|
667
735
|
* @throws {Error} If reading or writing files fails.
|
668
736
|
* @experimental This API is experimental and might change in future versions.
|
669
737
|
*/
|
670
738
|
static async from(data) {
|
671
|
-
const { destination, source } = data;
|
739
|
+
const { destination, isUniqueDestination = true, source } = data;
|
672
740
|
if (!destination) {
|
673
741
|
throw new Error("Destination is required");
|
674
742
|
}
|
743
|
+
// add random uuid to destination
|
744
|
+
const destinationWithUuid = isUniqueDestination
|
745
|
+
? path.join(destination, crypto.randomUUID())
|
746
|
+
: destination;
|
675
747
|
const buffer = typeof source === "string"
|
676
748
|
? await fs.readFile(source)
|
677
749
|
: source;
|
678
750
|
const files = await Zip.read(buffer);
|
679
751
|
// if destination exists, remove it
|
680
|
-
await fs.rm(
|
752
|
+
await fs.rm(destinationWithUuid, { force: true, recursive: true });
|
681
753
|
// Write all files to the file system, preserving exact paths
|
682
|
-
await fs.mkdir(
|
754
|
+
await fs.mkdir(destinationWithUuid, { recursive: true });
|
683
755
|
await Promise.all(Object.entries(files).map(async ([filePath, content]) => {
|
684
|
-
const fullPath = path.join(
|
756
|
+
const fullPath = path.join(destinationWithUuid, ...filePath.split("/"));
|
685
757
|
await fs.mkdir(path.dirname(fullPath), { recursive: true });
|
686
758
|
await fs.writeFile(fullPath, content);
|
687
759
|
}));
|
688
|
-
return new TemplateFs(new Set(Object.keys(files)),
|
760
|
+
return new TemplateFs(new Set(Object.keys(files)), destinationWithUuid);
|
689
761
|
}
|
690
762
|
}
|
763
|
+
const compareColumns = (a, b) => {
|
764
|
+
if (a === b)
|
765
|
+
return 0;
|
766
|
+
return a.length === b.length ? (a < b ? -1 : 1) : (a.length < b.length ? -1 : 1);
|
767
|
+
};
|