@js-ak/excel-toolbox 1.2.7 → 1.3.1
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/index.js +1 -0
- package/build/cjs/lib/template/index.js +17 -0
- package/build/cjs/lib/template/template-fs.js +465 -0
- package/build/cjs/lib/template/utils/check-row.js +23 -0
- package/build/cjs/lib/template/utils/check-rows.js +19 -0
- package/build/cjs/lib/template/utils/check-start-row.js +18 -0
- package/build/cjs/lib/template/utils/column-index-to-letter.js +17 -0
- package/build/cjs/lib/template/utils/escape-xml.js +24 -0
- package/build/cjs/lib/template/utils/get-max-row-number.js +20 -0
- package/build/cjs/lib/template/utils/get-rows-above.js +23 -0
- package/build/cjs/lib/template/utils/get-rows-below.js +23 -0
- package/build/cjs/lib/template/utils/index.js +27 -0
- package/build/cjs/lib/template/utils/parse-rows.js +30 -0
- package/build/cjs/lib/template/utils/to-excel-column-object.js +29 -0
- package/build/cjs/lib/template/utils/write-rows-to-stream.js +41 -0
- package/build/cjs/lib/xml/build-merged-sheet.js +1 -1
- package/build/cjs/lib/zip/constants.js +16 -1
- package/build/cjs/lib/zip/create-with-stream.js +214 -0
- package/build/cjs/lib/zip/index.js +1 -0
- package/build/cjs/lib/zip/utils/crc-32-stream.js +42 -0
- package/build/cjs/lib/zip/utils/crc-32.js +2 -35
- package/build/cjs/lib/zip/utils/index.js +1 -0
- package/build/esm/lib/index.js +1 -0
- package/build/esm/lib/template/index.js +1 -0
- package/build/esm/lib/template/template-fs.js +428 -0
- package/build/esm/lib/template/utils/check-row.js +20 -0
- package/build/esm/lib/template/utils/check-rows.js +16 -0
- package/build/esm/lib/template/utils/check-start-row.js +15 -0
- package/build/esm/lib/template/utils/column-index-to-letter.js +14 -0
- package/build/esm/lib/template/utils/escape-xml.js +21 -0
- package/build/esm/lib/template/utils/get-max-row-number.js +17 -0
- package/build/esm/lib/template/utils/get-rows-above.js +20 -0
- package/build/esm/lib/template/utils/get-rows-below.js +20 -0
- package/build/esm/lib/template/utils/index.js +11 -0
- package/build/esm/lib/template/utils/parse-rows.js +27 -0
- package/build/esm/lib/template/utils/to-excel-column-object.js +26 -0
- package/build/esm/lib/template/utils/write-rows-to-stream.js +38 -0
- package/build/esm/lib/xml/build-merged-sheet.js +1 -1
- package/build/esm/lib/zip/constants.js +15 -0
- package/build/esm/lib/zip/create-with-stream.js +175 -0
- package/build/esm/lib/zip/index.js +1 -0
- package/build/esm/lib/zip/utils/crc-32-stream.js +39 -0
- package/build/esm/lib/zip/utils/crc-32.js +2 -35
- package/build/esm/lib/zip/utils/index.js +1 -0
- package/build/types/lib/index.d.ts +1 -0
- package/build/types/lib/template/index.d.ts +1 -0
- package/build/types/lib/template/template-fs.d.ts +122 -0
- package/build/types/lib/template/utils/check-row.d.ts +14 -0
- package/build/types/lib/template/utils/check-rows.d.ts +11 -0
- package/build/types/lib/template/utils/check-start-row.d.ts +8 -0
- package/build/types/lib/template/utils/column-index-to-letter.d.ts +7 -0
- package/build/types/lib/template/utils/escape-xml.d.ts +14 -0
- package/build/types/lib/template/utils/get-max-row-number.d.ts +7 -0
- package/build/types/lib/template/utils/get-rows-above.d.ts +12 -0
- package/build/types/lib/template/utils/get-rows-below.d.ts +12 -0
- package/build/types/lib/template/utils/index.d.ts +11 -0
- package/build/types/lib/template/utils/parse-rows.d.ts +1 -0
- package/build/types/lib/template/utils/to-excel-column-object.d.ts +10 -0
- package/build/types/lib/template/utils/write-rows-to-stream.d.ts +25 -0
- package/build/types/lib/zip/constants.d.ts +9 -0
- package/build/types/lib/zip/create-with-stream.d.ts +13 -0
- package/build/types/lib/zip/index.d.ts +1 -0
- package/build/types/lib/zip/utils/crc-32-stream.d.ts +11 -0
- package/build/types/lib/zip/utils/index.d.ts +1 -0
- package/package.json +1 -1
package/build/cjs/lib/index.js
CHANGED
@@ -16,3 +16,4 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
17
17
|
__exportStar(require("./merge-sheets-to-base-file-sync.js"), exports);
|
18
18
|
__exportStar(require("./merge-sheets-to-base-file.js"), exports);
|
19
|
+
__exportStar(require("./template/index.js"), exports);
|
@@ -0,0 +1,17 @@
|
|
1
|
+
"use strict";
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
3
|
+
if (k2 === undefined) k2 = k;
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
7
|
+
}
|
8
|
+
Object.defineProperty(o, k2, desc);
|
9
|
+
}) : (function(o, m, k, k2) {
|
10
|
+
if (k2 === undefined) k2 = k;
|
11
|
+
o[k2] = m[k];
|
12
|
+
}));
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
15
|
+
};
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
17
|
+
__exportStar(require("./template-fs.js"), exports);
|
@@ -0,0 +1,465 @@
|
|
1
|
+
"use strict";
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
3
|
+
if (k2 === undefined) k2 = k;
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
7
|
+
}
|
8
|
+
Object.defineProperty(o, k2, desc);
|
9
|
+
}) : (function(o, m, k, k2) {
|
10
|
+
if (k2 === undefined) k2 = k;
|
11
|
+
o[k2] = m[k];
|
12
|
+
}));
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
15
|
+
}) : function(o, v) {
|
16
|
+
o["default"] = v;
|
17
|
+
});
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
19
|
+
var ownKeys = function(o) {
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
21
|
+
var ar = [];
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
23
|
+
return ar;
|
24
|
+
};
|
25
|
+
return ownKeys(o);
|
26
|
+
};
|
27
|
+
return function (mod) {
|
28
|
+
if (mod && mod.__esModule) return mod;
|
29
|
+
var result = {};
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
31
|
+
__setModuleDefault(result, mod);
|
32
|
+
return result;
|
33
|
+
};
|
34
|
+
})();
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
36
|
+
exports.TemplateFs = void 0;
|
37
|
+
const fs = __importStar(require("node:fs/promises"));
|
38
|
+
const fsSync = __importStar(require("node:fs"));
|
39
|
+
const path = __importStar(require("node:path"));
|
40
|
+
const readline = __importStar(require("node:readline"));
|
41
|
+
const Xml = __importStar(require("../xml/index.js"));
|
42
|
+
const Zip = __importStar(require("../zip/index.js"));
|
43
|
+
const Utils = __importStar(require("./utils/index.js"));
|
44
|
+
/**
|
45
|
+
* A class for manipulating Excel templates by extracting, modifying, and repacking Excel files.
|
46
|
+
*
|
47
|
+
* @experimental This API is experimental and might change in future versions.
|
48
|
+
*/
|
49
|
+
class TemplateFs {
|
50
|
+
/**
|
51
|
+
* Set of file paths (relative to the template) that will be used to create a new workbook.
|
52
|
+
* @type {Set<string>}
|
53
|
+
*/
|
54
|
+
fileKeys;
|
55
|
+
/**
|
56
|
+
* The path where the template will be expanded.
|
57
|
+
* @type {string}
|
58
|
+
*/
|
59
|
+
destination;
|
60
|
+
/**
|
61
|
+
* Flag indicating whether this template instance has been destroyed.
|
62
|
+
* @type {boolean}
|
63
|
+
*/
|
64
|
+
destroyed = false;
|
65
|
+
/**
|
66
|
+
* Creates a Template instance.
|
67
|
+
*
|
68
|
+
* @param {Set<string>} fileKeys - Set of file paths (relative to the template) that will be used to create a new workbook.
|
69
|
+
* @param {string} destination - The path where the template will be expanded.
|
70
|
+
* @experimental This API is experimental and might change in future versions.
|
71
|
+
*/
|
72
|
+
constructor(fileKeys, destination) {
|
73
|
+
this.fileKeys = fileKeys;
|
74
|
+
this.destination = destination;
|
75
|
+
}
|
76
|
+
/**
|
77
|
+
* Removes the temporary directory created by this Template instance.
|
78
|
+
* @private
|
79
|
+
* @returns {Promise<void>}
|
80
|
+
* @experimental This API is experimental and might change in future versions.
|
81
|
+
*/
|
82
|
+
async #cleanup() {
|
83
|
+
await fs.rm(this.destination, { force: true, recursive: true });
|
84
|
+
}
|
85
|
+
/**
|
86
|
+
* Ensures that this Template instance has not been destroyed.
|
87
|
+
* @private
|
88
|
+
* @throws {Error} If the template instance has already been destroyed.
|
89
|
+
* @experimental This API is experimental and might change in future versions.
|
90
|
+
*/
|
91
|
+
#ensureNotDestroyed() {
|
92
|
+
if (this.destroyed) {
|
93
|
+
throw new Error("This Template instance has already been saved and destroyed.");
|
94
|
+
}
|
95
|
+
}
|
96
|
+
/**
|
97
|
+
* Reads all files from the destination directory.
|
98
|
+
* @private
|
99
|
+
* @returns {Promise<Record<string, Buffer>>} An object with file keys and their contents as Buffers.
|
100
|
+
* @experimental This API is experimental and might change in future versions.
|
101
|
+
*/
|
102
|
+
async #readAllFromDestination() {
|
103
|
+
const result = {};
|
104
|
+
for (const key of this.fileKeys) {
|
105
|
+
const fullPath = path.join(this.destination, ...key.split("/"));
|
106
|
+
result[key] = await fs.readFile(fullPath);
|
107
|
+
}
|
108
|
+
return result;
|
109
|
+
}
|
110
|
+
/**
|
111
|
+
* Reads a single file from the destination directory.
|
112
|
+
* @private
|
113
|
+
* @param {string} pathKey - The Excel path of the file to read.
|
114
|
+
* @returns {Promise<Buffer>} The contents of the file as a Buffer.
|
115
|
+
* @throws {Error} If the file does not exist in the template.
|
116
|
+
* @experimental This API is experimental and might change in future versions.
|
117
|
+
*/
|
118
|
+
async #readFile(pathKey) {
|
119
|
+
if (!this.fileKeys.has(pathKey)) {
|
120
|
+
throw new Error(`File "${pathKey}" not found in template.`);
|
121
|
+
}
|
122
|
+
const fullPath = path.join(this.destination, ...pathKey.split("/"));
|
123
|
+
return await fs.readFile(fullPath);
|
124
|
+
}
|
125
|
+
/**
|
126
|
+
* Inserts rows into a specific sheet in the template.
|
127
|
+
*
|
128
|
+
* @param {Object} data - The data for row insertion.
|
129
|
+
* @param {string} data.sheetName - The name of the sheet to insert rows into.
|
130
|
+
* @param {number} [data.startRowNumber] - The row number to start inserting from.
|
131
|
+
* @param {unknown[][]} data.rows - The rows to insert.
|
132
|
+
* @returns {Promise<void>}
|
133
|
+
* @throws {Error} If the template instance has been destroyed.
|
134
|
+
* @throws {Error} If the sheet does not exist.
|
135
|
+
* @throws {Error} If the row number is out of range.
|
136
|
+
* @throws {Error} If a column is out of range.
|
137
|
+
* @experimental This API is experimental and might change in future versions.
|
138
|
+
*/
|
139
|
+
async insertRows(data) {
|
140
|
+
this.#ensureNotDestroyed();
|
141
|
+
const { rows, sheetName, startRowNumber } = data;
|
142
|
+
const preparedRows = rows.map(row => Utils.toExcelColumnObject(row));
|
143
|
+
// Validation
|
144
|
+
Utils.checkStartRow(startRowNumber);
|
145
|
+
Utils.checkRows(preparedRows);
|
146
|
+
// Find the sheet
|
147
|
+
const workbookXml = Xml.extractXmlFromSheet(await this.#readFile("xl/workbook.xml"));
|
148
|
+
const sheetMatch = workbookXml.match(new RegExp(`<sheet[^>]+name="${sheetName}"[^>]+r:id="([^"]+)"[^>]*/>`));
|
149
|
+
if (!sheetMatch || !sheetMatch[1]) {
|
150
|
+
throw new Error(`Sheet "${sheetName}" not found`);
|
151
|
+
}
|
152
|
+
const rId = sheetMatch[1];
|
153
|
+
const relsXml = Xml.extractXmlFromSheet(await this.#readFile("xl/_rels/workbook.xml.rels"));
|
154
|
+
const relMatch = relsXml.match(new RegExp(`<Relationship[^>]+Id="${rId}"[^>]+Target="([^"]+)"[^>]*/>`));
|
155
|
+
if (!relMatch || !relMatch[1]) {
|
156
|
+
throw new Error(`Relationship "${rId}" not found`);
|
157
|
+
}
|
158
|
+
const sheetPath = "xl/" + relMatch[1].replace(/^\/?xl\//, "");
|
159
|
+
const sheetXmlRaw = await this.#readFile(sheetPath);
|
160
|
+
const sheetXml = Xml.extractXmlFromSheet(sheetXmlRaw);
|
161
|
+
let nextRow = 0;
|
162
|
+
if (!startRowNumber) {
|
163
|
+
// Find the last row
|
164
|
+
let lastRowNumber = 0;
|
165
|
+
const rowMatches = [...sheetXml.matchAll(/<row[^>]+r="(\d+)"[^>]*>/g)];
|
166
|
+
if (rowMatches.length > 0) {
|
167
|
+
lastRowNumber = Math.max(...rowMatches.map((m) => parseInt(m[1], 10)));
|
168
|
+
}
|
169
|
+
nextRow = lastRowNumber + 1;
|
170
|
+
}
|
171
|
+
else {
|
172
|
+
nextRow = startRowNumber;
|
173
|
+
}
|
174
|
+
// Generate XML for all rows
|
175
|
+
const rowsXml = preparedRows.map((cells, i) => {
|
176
|
+
const rowNumber = nextRow + i;
|
177
|
+
const cellTags = Object.entries(cells).map(([col, value]) => {
|
178
|
+
const colUpper = col.toUpperCase();
|
179
|
+
const ref = `${colUpper}${rowNumber}`;
|
180
|
+
return `<c r="${ref}" t="inlineStr"><is><t>${Utils.escapeXml(value)}</t></is></c>`;
|
181
|
+
}).join("");
|
182
|
+
return `<row r="${rowNumber}">${cellTags}</row>`;
|
183
|
+
}).join("");
|
184
|
+
let updatedXml;
|
185
|
+
if (/<sheetData\s*\/>/.test(sheetXml)) {
|
186
|
+
updatedXml = sheetXml.replace(/<sheetData\s*\/>/, `<sheetData>${rowsXml}</sheetData>`);
|
187
|
+
}
|
188
|
+
else if (/<sheetData>([\s\S]*?)<\/sheetData>/.test(sheetXml)) {
|
189
|
+
updatedXml = sheetXml.replace(/<\/sheetData>/, `${rowsXml}</sheetData>`);
|
190
|
+
}
|
191
|
+
else {
|
192
|
+
updatedXml = sheetXml.replace(/<worksheet[^>]*>/, (match) => `${match}<sheetData>${rowsXml}</sheetData>`);
|
193
|
+
}
|
194
|
+
await this.set(sheetPath, updatedXml);
|
195
|
+
}
|
196
|
+
/**
|
197
|
+
* Inserts rows into a specific sheet in the template using an async stream.
|
198
|
+
*
|
199
|
+
* @param {Object} data - The data for row insertion.
|
200
|
+
* @param {string} data.sheetName - The name of the sheet to insert rows into.
|
201
|
+
* @param {number} [data.startRowNumber] - The row number to start inserting from.
|
202
|
+
* @param {AsyncIterable<unknown[]>} data.rows - Async iterable of rows to insert.
|
203
|
+
* @returns {Promise<void>}
|
204
|
+
* @throws {Error} If the template instance has been destroyed.
|
205
|
+
* @throws {Error} If the sheet does not exist.
|
206
|
+
* @throws {Error} If the row number is out of range.
|
207
|
+
* @throws {Error} If a column is out of range.
|
208
|
+
* @experimental This API is experimental and might change in future versions.
|
209
|
+
*/
|
210
|
+
async insertRowsStream(data) {
|
211
|
+
this.#ensureNotDestroyed();
|
212
|
+
const { rows, sheetName, startRowNumber } = data;
|
213
|
+
if (!sheetName)
|
214
|
+
throw new Error("Sheet name is required");
|
215
|
+
// Read XML workbook to find sheet name and path
|
216
|
+
const workbookXml = Xml.extractXmlFromSheet(await this.#readFile("xl/workbook.xml"));
|
217
|
+
const sheetMatch = workbookXml.match(new RegExp(`<sheet[^>]+name="${sheetName}"[^>]+r:id="([^"]+)"[^>]*/>`));
|
218
|
+
if (!sheetMatch)
|
219
|
+
throw new Error(`Sheet "${sheetName}" not found`);
|
220
|
+
const rId = sheetMatch[1];
|
221
|
+
const relsXml = Xml.extractXmlFromSheet(await this.#readFile("xl/_rels/workbook.xml.rels"));
|
222
|
+
const relMatch = relsXml.match(new RegExp(`<Relationship[^>]+Id="${rId}"[^>]+Target="([^"]+)"[^>]*/>`));
|
223
|
+
if (!relMatch)
|
224
|
+
throw new Error(`Relationship "${rId}" not found`);
|
225
|
+
// Path to the desired sheet (sheet1.xml)
|
226
|
+
const sheetPath = "xl/" + relMatch[1].replace(/^\/?xl\//, "");
|
227
|
+
// The temporary file for writing
|
228
|
+
const fullPath = path.join(this.destination, ...sheetPath.split("/"));
|
229
|
+
const tempPath = fullPath + ".tmp";
|
230
|
+
// Streams for reading and writing
|
231
|
+
const input = fsSync.createReadStream(fullPath, { encoding: "utf-8" });
|
232
|
+
const output = fsSync.createWriteStream(tempPath, { encoding: "utf-8" });
|
233
|
+
// Inserted rows flag
|
234
|
+
let inserted = false;
|
235
|
+
const rl = readline.createInterface({
|
236
|
+
// Process all line breaks
|
237
|
+
crlfDelay: Infinity,
|
238
|
+
input,
|
239
|
+
});
|
240
|
+
let isCollecting = false;
|
241
|
+
let collection = "";
|
242
|
+
for await (const line of rl) {
|
243
|
+
// Collect lines between <sheetData> and </sheetData>
|
244
|
+
if (!inserted && isCollecting) {
|
245
|
+
collection += line;
|
246
|
+
if (line.includes("</sheetData>")) {
|
247
|
+
const maxRowNumber = startRowNumber ?? Utils.getMaxRowNumber(line);
|
248
|
+
isCollecting = false;
|
249
|
+
inserted = true;
|
250
|
+
const openTag = collection.match(/<sheetData[^>]*>/)?.[0] ?? "<sheetData>";
|
251
|
+
const closeTag = "</sheetData>";
|
252
|
+
const openIdx = collection.indexOf(openTag);
|
253
|
+
const closeIdx = collection.lastIndexOf(closeTag);
|
254
|
+
const beforeRows = collection.slice(0, openIdx + openTag.length);
|
255
|
+
const innerRows = collection.slice(openIdx + openTag.length, closeIdx).trim();
|
256
|
+
const afterRows = collection.slice(closeIdx);
|
257
|
+
output.write(beforeRows);
|
258
|
+
const innerRowsMap = Utils.parseRows(innerRows);
|
259
|
+
if (innerRows) {
|
260
|
+
if (startRowNumber) {
|
261
|
+
const filteredRows = Utils.getRowsBelow(innerRowsMap, startRowNumber);
|
262
|
+
if (filteredRows)
|
263
|
+
output.write(filteredRows);
|
264
|
+
}
|
265
|
+
else {
|
266
|
+
output.write(innerRows);
|
267
|
+
}
|
268
|
+
}
|
269
|
+
const { rowNumber: actualRowNumber } = await Utils.writeRowsToStream(output, rows, maxRowNumber);
|
270
|
+
if (innerRows) {
|
271
|
+
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
|
+
if (filteredRows) {
|
302
|
+
output.write(filteredRows);
|
303
|
+
}
|
304
|
+
}
|
305
|
+
else {
|
306
|
+
output.write(innerRows);
|
307
|
+
}
|
308
|
+
}
|
309
|
+
// new <row>
|
310
|
+
const { rowNumber: actualRowNumber } = await Utils.writeRowsToStream(output, rows, maxRowNumber);
|
311
|
+
if (innerRows) {
|
312
|
+
const filteredRows = Utils.getRowsAbove(innerRowsMap, actualRowNumber);
|
313
|
+
if (filteredRows) {
|
314
|
+
output.write(filteredRows);
|
315
|
+
}
|
316
|
+
}
|
317
|
+
output.write(afterRows);
|
318
|
+
if (after) {
|
319
|
+
output.write(after);
|
320
|
+
}
|
321
|
+
inserted = true;
|
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);
|
342
|
+
}
|
343
|
+
inserted = true;
|
344
|
+
continue;
|
345
|
+
}
|
346
|
+
// Case 3: <sheetData>
|
347
|
+
if (!inserted && /<sheetData[^>]*>/.test(line)) {
|
348
|
+
isCollecting = true;
|
349
|
+
collection += line;
|
350
|
+
continue;
|
351
|
+
}
|
352
|
+
// After inserting rows, just copy the remaining lines
|
353
|
+
output.write(line);
|
354
|
+
}
|
355
|
+
// Close the streams
|
356
|
+
rl.close();
|
357
|
+
output.end();
|
358
|
+
// Move the temporary file to the original location
|
359
|
+
await fs.rename(tempPath, fullPath);
|
360
|
+
}
|
361
|
+
/**
|
362
|
+
* Saves the modified Excel template to a buffer.
|
363
|
+
*
|
364
|
+
* @returns {Promise<Buffer>} The modified Excel template as a buffer.
|
365
|
+
* @throws {Error} If the template instance has been destroyed.
|
366
|
+
* @experimental This API is experimental and might change in future versions.
|
367
|
+
*/
|
368
|
+
async save() {
|
369
|
+
this.#ensureNotDestroyed();
|
370
|
+
// Guarantee integrity
|
371
|
+
await this.validate();
|
372
|
+
// Read the current files from the directory(in case they were changed manually)
|
373
|
+
const updatedFiles = await this.#readAllFromDestination();
|
374
|
+
const zipBuffer = await Zip.create(updatedFiles);
|
375
|
+
await this.#cleanup();
|
376
|
+
this.destroyed = true;
|
377
|
+
return zipBuffer;
|
378
|
+
}
|
379
|
+
/**
|
380
|
+
* Writes the modified Excel template to a writable stream.
|
381
|
+
*
|
382
|
+
* @param {Writable} output - The writable stream to write to.
|
383
|
+
* @returns {Promise<void>}
|
384
|
+
* @throws {Error} If the template instance has been destroyed.
|
385
|
+
* @experimental This API is experimental and might change in future versions.
|
386
|
+
*/
|
387
|
+
async saveStream(output) {
|
388
|
+
this.#ensureNotDestroyed();
|
389
|
+
// Guarantee integrity
|
390
|
+
await this.validate();
|
391
|
+
await Zip.createWithStream(Array.from(this.fileKeys), this.destination, output);
|
392
|
+
await this.#cleanup();
|
393
|
+
this.destroyed = true;
|
394
|
+
}
|
395
|
+
/**
|
396
|
+
* Replaces the contents of a file in the template.
|
397
|
+
*
|
398
|
+
* @param {string} key - The Excel path of the file to replace.
|
399
|
+
* @param {Buffer|string} content - The new content.
|
400
|
+
* @returns {Promise<void>}
|
401
|
+
* @throws {Error} If the template instance has been destroyed.
|
402
|
+
* @throws {Error} If the file does not exist in the template.
|
403
|
+
* @experimental This API is experimental and might change in future versions.
|
404
|
+
*/
|
405
|
+
async set(key, content) {
|
406
|
+
this.#ensureNotDestroyed();
|
407
|
+
if (!this.fileKeys.has(key)) {
|
408
|
+
throw new Error(`File "${key}" is not part of the original template.`);
|
409
|
+
}
|
410
|
+
const fullPath = path.join(this.destination, ...key.split("/"));
|
411
|
+
await fs.writeFile(fullPath, Buffer.isBuffer(content) ? content : Buffer.from(content));
|
412
|
+
}
|
413
|
+
/**
|
414
|
+
* Validates the template by checking all required files exist.
|
415
|
+
*
|
416
|
+
* @returns {Promise<void>}
|
417
|
+
* @throws {Error} If the template instance has been destroyed.
|
418
|
+
* @throws {Error} If any required files are missing.
|
419
|
+
* @experimental This API is experimental and might change in future versions.
|
420
|
+
*/
|
421
|
+
async validate() {
|
422
|
+
this.#ensureNotDestroyed();
|
423
|
+
for (const key of this.fileKeys) {
|
424
|
+
const fullPath = path.join(this.destination, ...key.split("/"));
|
425
|
+
try {
|
426
|
+
await fs.access(fullPath);
|
427
|
+
}
|
428
|
+
catch {
|
429
|
+
throw new Error(`Missing file in template directory: ${key}`);
|
430
|
+
}
|
431
|
+
}
|
432
|
+
}
|
433
|
+
/**
|
434
|
+
* Creates a Template instance from an Excel file source.
|
435
|
+
* Removes any existing files in the destination directory.
|
436
|
+
*
|
437
|
+
* @param {Object} data - The data to create the template from.
|
438
|
+
* @param {string} data.source - The path or buffer of the Excel file.
|
439
|
+
* @param {string} data.destination - The path to save the template to.
|
440
|
+
* @returns {Promise<Template>} A new Template instance.
|
441
|
+
* @throws {Error} If reading or writing files fails.
|
442
|
+
* @experimental This API is experimental and might change in future versions.
|
443
|
+
*/
|
444
|
+
static async from(data) {
|
445
|
+
const { destination, source } = data;
|
446
|
+
if (!destination) {
|
447
|
+
throw new Error("Destination is required");
|
448
|
+
}
|
449
|
+
const buffer = typeof source === "string"
|
450
|
+
? await fs.readFile(source)
|
451
|
+
: source;
|
452
|
+
const files = await Zip.read(buffer);
|
453
|
+
// if destination exists, remove it
|
454
|
+
await fs.rm(destination, { force: true, recursive: true });
|
455
|
+
// Write all files to the file system, preserving exact paths
|
456
|
+
await fs.mkdir(destination, { recursive: true });
|
457
|
+
await Promise.all(Object.entries(files).map(async ([filePath, content]) => {
|
458
|
+
const fullPath = path.join(destination, ...filePath.split("/"));
|
459
|
+
await fs.mkdir(path.dirname(fullPath), { recursive: true });
|
460
|
+
await fs.writeFile(fullPath, content);
|
461
|
+
}));
|
462
|
+
return new TemplateFs(new Set(Object.keys(files)), destination);
|
463
|
+
}
|
464
|
+
}
|
465
|
+
exports.TemplateFs = TemplateFs;
|
@@ -0,0 +1,23 @@
|
|
1
|
+
"use strict";
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
exports.checkRow = checkRow;
|
4
|
+
/**
|
5
|
+
* Validates that each key in the given row object is a valid cell reference.
|
6
|
+
*
|
7
|
+
* This function checks that all keys in the provided row object are composed
|
8
|
+
* only of column letters (A-Z, case insensitive). If a key is found that does
|
9
|
+
* not match this pattern, an error is thrown with a message indicating the
|
10
|
+
* invalid cell reference.
|
11
|
+
*
|
12
|
+
* @param row - An object representing a row of data, where keys are cell
|
13
|
+
* references and values are strings.
|
14
|
+
*
|
15
|
+
* @throws {Error} If any key in the row is not a valid column letter.
|
16
|
+
*/
|
17
|
+
function checkRow(row) {
|
18
|
+
for (const key of Object.keys(row)) {
|
19
|
+
if (!/^[A-Z]+$/i.test(key)) {
|
20
|
+
throw new Error(`Invalid cell reference "${key}" in row. Only column letters (like "A", "B", "C") are allowed.`);
|
21
|
+
}
|
22
|
+
}
|
23
|
+
}
|
@@ -0,0 +1,19 @@
|
|
1
|
+
"use strict";
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
exports.checkRows = checkRows;
|
4
|
+
const check_row_js_1 = require("./check-row.js");
|
5
|
+
/**
|
6
|
+
* Validates an array of row objects to ensure each cell reference is valid.
|
7
|
+
* Each row object is checked to ensure that its keys (cell references) are
|
8
|
+
* composed of valid column letters (e.g., "A", "B", "C").
|
9
|
+
*
|
10
|
+
* @param rows An array of row objects, where each object represents a row
|
11
|
+
* of data with cell references as keys and cell values as strings.
|
12
|
+
*
|
13
|
+
* @throws {Error} If any cell reference in the rows is invalid.
|
14
|
+
*/
|
15
|
+
function checkRows(rows) {
|
16
|
+
for (const row of rows) {
|
17
|
+
(0, check_row_js_1.checkRow)(row);
|
18
|
+
}
|
19
|
+
}
|
@@ -0,0 +1,18 @@
|
|
1
|
+
"use strict";
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
exports.checkStartRow = checkStartRow;
|
4
|
+
/**
|
5
|
+
* Checks if startRow is a positive integer.
|
6
|
+
*
|
7
|
+
* @param startRow The start row to check.
|
8
|
+
*
|
9
|
+
* @throws {Error} If startRow is not a positive integer.
|
10
|
+
*/
|
11
|
+
function checkStartRow(startRow) {
|
12
|
+
if (startRow === undefined) {
|
13
|
+
return;
|
14
|
+
}
|
15
|
+
if (!Number.isInteger(startRow) || startRow < 1) {
|
16
|
+
throw new Error(`Invalid startRow "${startRow}". Must be a positive integer.`);
|
17
|
+
}
|
18
|
+
}
|
@@ -0,0 +1,17 @@
|
|
1
|
+
"use strict";
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
exports.columnIndexToLetter = columnIndexToLetter;
|
4
|
+
/**
|
5
|
+
* Converts a 0-based column index to an Excel-style letter (A, B, ..., Z, AA, AB, ...).
|
6
|
+
*
|
7
|
+
* @param index - The 0-based column index.
|
8
|
+
* @returns The Excel-style letter for the given column index.
|
9
|
+
*/
|
10
|
+
function columnIndexToLetter(index) {
|
11
|
+
let letters = "";
|
12
|
+
while (index >= 0) {
|
13
|
+
letters = String.fromCharCode((index % 26) + 65) + letters;
|
14
|
+
index = Math.floor(index / 26) - 1;
|
15
|
+
}
|
16
|
+
return letters;
|
17
|
+
}
|
@@ -0,0 +1,24 @@
|
|
1
|
+
"use strict";
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
exports.escapeXml = escapeXml;
|
4
|
+
/**
|
5
|
+
* Escapes special characters in a string for use in an XML document.
|
6
|
+
*
|
7
|
+
* Replaces:
|
8
|
+
* - `&` with `&`
|
9
|
+
* - `<` with `<`
|
10
|
+
* - `>` with `>`
|
11
|
+
* - `"` with `"`
|
12
|
+
* - `'` with `'`
|
13
|
+
*
|
14
|
+
* @param str - The string to escape.
|
15
|
+
* @returns The escaped string.
|
16
|
+
*/
|
17
|
+
function escapeXml(str) {
|
18
|
+
return str
|
19
|
+
.replace(/&/g, "&")
|
20
|
+
.replace(/</g, "<")
|
21
|
+
.replace(/>/g, ">")
|
22
|
+
.replace(/"/g, """)
|
23
|
+
.replace(/'/g, "'");
|
24
|
+
}
|
@@ -0,0 +1,20 @@
|
|
1
|
+
"use strict";
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
exports.getMaxRowNumber = getMaxRowNumber;
|
4
|
+
/**
|
5
|
+
* Finds the maximum row number in a list of <row> elements
|
6
|
+
* and returns the maximum row number + 1.
|
7
|
+
* @param {string} line - The line of XML to parse.
|
8
|
+
* @returns {number} - The maximum row number found + 1.
|
9
|
+
*/
|
10
|
+
function getMaxRowNumber(line) {
|
11
|
+
let result = 1;
|
12
|
+
const rowMatches = [...line.matchAll(/<row[^>]+r="(\d+)"[^>]*>/g)];
|
13
|
+
for (const match of rowMatches) {
|
14
|
+
const rowNum = parseInt(match[1], 10);
|
15
|
+
if (rowNum >= result) {
|
16
|
+
result = rowNum + 1;
|
17
|
+
}
|
18
|
+
}
|
19
|
+
return result;
|
20
|
+
}
|
@@ -0,0 +1,23 @@
|
|
1
|
+
"use strict";
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
exports.getRowsAbove = getRowsAbove;
|
4
|
+
/**
|
5
|
+
* Filters out all rows in the given map that have a row number
|
6
|
+
* lower than or equal to the given minRow, and returns the
|
7
|
+
* filtered rows as a single string. Useful for removing rows
|
8
|
+
* from a template that are positioned above a certain row
|
9
|
+
* number.
|
10
|
+
*
|
11
|
+
* @param {Map<number, string>} map - The map of row numbers to row content
|
12
|
+
* @param {number} minRow - The minimum row number to include in the output
|
13
|
+
* @returns {string} The filtered rows, concatenated into a single string
|
14
|
+
*/
|
15
|
+
function getRowsAbove(map, minRow) {
|
16
|
+
const filteredRows = [];
|
17
|
+
for (const [key, value] of map.entries()) {
|
18
|
+
if (key > minRow) {
|
19
|
+
filteredRows.push(value);
|
20
|
+
}
|
21
|
+
}
|
22
|
+
return filteredRows.join("");
|
23
|
+
}
|
@@ -0,0 +1,23 @@
|
|
1
|
+
"use strict";
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
3
|
+
exports.getRowsBelow = getRowsBelow;
|
4
|
+
/**
|
5
|
+
* Filters out all rows in the given map that have a row number
|
6
|
+
* greater than or equal to the given maxRow, and returns the
|
7
|
+
* filtered rows as a single string. Useful for removing rows
|
8
|
+
* from a template that are positioned below a certain row
|
9
|
+
* number.
|
10
|
+
*
|
11
|
+
* @param {Map<number, string>} map - The map of row numbers to row content
|
12
|
+
* @param {number} maxRow - The maximum row number to include in the output
|
13
|
+
* @returns {string} The filtered rows, concatenated into a single string
|
14
|
+
*/
|
15
|
+
function getRowsBelow(map, maxRow) {
|
16
|
+
const filteredRows = [];
|
17
|
+
for (const [key, value] of map.entries()) {
|
18
|
+
if (key < maxRow) {
|
19
|
+
filteredRows.push(value);
|
20
|
+
}
|
21
|
+
}
|
22
|
+
return filteredRows.join("");
|
23
|
+
}
|