@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.
Files changed (65) hide show
  1. package/build/cjs/lib/index.js +1 -0
  2. package/build/cjs/lib/template/index.js +17 -0
  3. package/build/cjs/lib/template/template-fs.js +465 -0
  4. package/build/cjs/lib/template/utils/check-row.js +23 -0
  5. package/build/cjs/lib/template/utils/check-rows.js +19 -0
  6. package/build/cjs/lib/template/utils/check-start-row.js +18 -0
  7. package/build/cjs/lib/template/utils/column-index-to-letter.js +17 -0
  8. package/build/cjs/lib/template/utils/escape-xml.js +24 -0
  9. package/build/cjs/lib/template/utils/get-max-row-number.js +20 -0
  10. package/build/cjs/lib/template/utils/get-rows-above.js +23 -0
  11. package/build/cjs/lib/template/utils/get-rows-below.js +23 -0
  12. package/build/cjs/lib/template/utils/index.js +27 -0
  13. package/build/cjs/lib/template/utils/parse-rows.js +30 -0
  14. package/build/cjs/lib/template/utils/to-excel-column-object.js +29 -0
  15. package/build/cjs/lib/template/utils/write-rows-to-stream.js +41 -0
  16. package/build/cjs/lib/xml/build-merged-sheet.js +1 -1
  17. package/build/cjs/lib/zip/constants.js +16 -1
  18. package/build/cjs/lib/zip/create-with-stream.js +214 -0
  19. package/build/cjs/lib/zip/index.js +1 -0
  20. package/build/cjs/lib/zip/utils/crc-32-stream.js +42 -0
  21. package/build/cjs/lib/zip/utils/crc-32.js +2 -35
  22. package/build/cjs/lib/zip/utils/index.js +1 -0
  23. package/build/esm/lib/index.js +1 -0
  24. package/build/esm/lib/template/index.js +1 -0
  25. package/build/esm/lib/template/template-fs.js +428 -0
  26. package/build/esm/lib/template/utils/check-row.js +20 -0
  27. package/build/esm/lib/template/utils/check-rows.js +16 -0
  28. package/build/esm/lib/template/utils/check-start-row.js +15 -0
  29. package/build/esm/lib/template/utils/column-index-to-letter.js +14 -0
  30. package/build/esm/lib/template/utils/escape-xml.js +21 -0
  31. package/build/esm/lib/template/utils/get-max-row-number.js +17 -0
  32. package/build/esm/lib/template/utils/get-rows-above.js +20 -0
  33. package/build/esm/lib/template/utils/get-rows-below.js +20 -0
  34. package/build/esm/lib/template/utils/index.js +11 -0
  35. package/build/esm/lib/template/utils/parse-rows.js +27 -0
  36. package/build/esm/lib/template/utils/to-excel-column-object.js +26 -0
  37. package/build/esm/lib/template/utils/write-rows-to-stream.js +38 -0
  38. package/build/esm/lib/xml/build-merged-sheet.js +1 -1
  39. package/build/esm/lib/zip/constants.js +15 -0
  40. package/build/esm/lib/zip/create-with-stream.js +175 -0
  41. package/build/esm/lib/zip/index.js +1 -0
  42. package/build/esm/lib/zip/utils/crc-32-stream.js +39 -0
  43. package/build/esm/lib/zip/utils/crc-32.js +2 -35
  44. package/build/esm/lib/zip/utils/index.js +1 -0
  45. package/build/types/lib/index.d.ts +1 -0
  46. package/build/types/lib/template/index.d.ts +1 -0
  47. package/build/types/lib/template/template-fs.d.ts +122 -0
  48. package/build/types/lib/template/utils/check-row.d.ts +14 -0
  49. package/build/types/lib/template/utils/check-rows.d.ts +11 -0
  50. package/build/types/lib/template/utils/check-start-row.d.ts +8 -0
  51. package/build/types/lib/template/utils/column-index-to-letter.d.ts +7 -0
  52. package/build/types/lib/template/utils/escape-xml.d.ts +14 -0
  53. package/build/types/lib/template/utils/get-max-row-number.d.ts +7 -0
  54. package/build/types/lib/template/utils/get-rows-above.d.ts +12 -0
  55. package/build/types/lib/template/utils/get-rows-below.d.ts +12 -0
  56. package/build/types/lib/template/utils/index.d.ts +11 -0
  57. package/build/types/lib/template/utils/parse-rows.d.ts +1 -0
  58. package/build/types/lib/template/utils/to-excel-column-object.d.ts +10 -0
  59. package/build/types/lib/template/utils/write-rows-to-stream.d.ts +25 -0
  60. package/build/types/lib/zip/constants.d.ts +9 -0
  61. package/build/types/lib/zip/create-with-stream.d.ts +13 -0
  62. package/build/types/lib/zip/index.d.ts +1 -0
  63. package/build/types/lib/zip/utils/crc-32-stream.d.ts +11 -0
  64. package/build/types/lib/zip/utils/index.d.ts +1 -0
  65. package/package.json +1 -1
@@ -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 `&amp;`
9
+ * - `<` with `&lt;`
10
+ * - `>` with `&gt;`
11
+ * - `"` with `&quot;`
12
+ * - `'` with `&apos;`
13
+ *
14
+ * @param str - The string to escape.
15
+ * @returns The escaped string.
16
+ */
17
+ function escapeXml(str) {
18
+ return str
19
+ .replace(/&/g, "&amp;")
20
+ .replace(/</g, "&lt;")
21
+ .replace(/>/g, "&gt;")
22
+ .replace(/"/g, "&quot;")
23
+ .replace(/'/g, "&apos;");
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
+ }