@js-ak/excel-toolbox 1.5.0 → 1.7.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/merge-sheets-to-base-file-process-sync.js +105 -0
- package/build/cjs/lib/merge-sheets-to-base-file-process.js +3 -3
- package/build/cjs/lib/merge-sheets-to-base-file-sync.js +2 -2
- package/build/cjs/lib/merge-sheets-to-base-file.js +1 -1
- package/build/cjs/lib/template/template-fs.js +143 -63
- 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/cjs/lib/xml/extract-rows-from-sheet-sync.js +67 -0
- package/build/cjs/lib/xml/extract-rows-from-sheet.js +4 -2
- package/build/cjs/lib/xml/extract-xml-from-sheet-sync.js +43 -0
- package/build/cjs/lib/xml/extract-xml-from-sheet.js +15 -15
- package/build/cjs/lib/xml/index.js +2 -1
- package/build/esm/lib/merge-sheets-to-base-file-process-sync.js +69 -0
- package/build/esm/lib/merge-sheets-to-base-file-process.js +3 -3
- package/build/esm/lib/merge-sheets-to-base-file-sync.js +2 -2
- package/build/esm/lib/merge-sheets-to-base-file.js +1 -1
- package/build/esm/lib/template/template-fs.js +140 -63
- 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/esm/lib/xml/extract-rows-from-sheet-sync.js +64 -0
- package/build/esm/lib/xml/extract-rows-from-sheet.js +4 -2
- package/build/esm/lib/xml/extract-xml-from-sheet-sync.js +40 -0
- package/build/esm/lib/xml/extract-xml-from-sheet.js +12 -15
- package/build/esm/lib/xml/index.js +2 -1
- package/build/types/lib/merge-sheets-to-base-file-process-sync.d.ts +27 -0
- package/build/types/lib/merge-sheets-to-base-file-process.d.ts +1 -1
- 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/build/types/lib/xml/extract-rows-from-sheet-sync.d.ts +28 -0
- package/build/types/lib/xml/extract-rows-from-sheet.d.ts +2 -2
- package/build/types/lib/xml/extract-xml-from-sheet-sync.d.ts +14 -0
- package/build/types/lib/xml/extract-xml-from-sheet.d.ts +2 -2
- package/build/types/lib/xml/index.d.ts +2 -1
- package/package.json +1 -5
- package/build/cjs/lib/xml/extract-xml-from-system-content.js +0 -53
- package/build/esm/lib/xml/extract-xml-from-system-content.js +0 -49
- package/build/types/lib/xml/extract-xml-from-system-content.d.ts +0 -15
package/README.md
CHANGED
@@ -2,100 +2,79 @@
|
|
2
2
|
|
3
3
|

|
4
4
|
|
5
|
-
|
5
|
+
📘 **Docs:** [js-ak.github.io/excel-toolbox](https://js-ak.github.io/excel-toolbox/)
|
6
6
|
|
7
|
-
|
7
|
+
A lightweight toolkit for working with `.xlsx` Excel files — modify templates, merge sheets, and handle massive datasets without dependencies.
|
8
8
|
|
9
|
-
|
9
|
+
## Installation
|
10
10
|
|
11
11
|
```bash
|
12
12
|
npm install @js-ak/excel-toolbox
|
13
13
|
```
|
14
14
|
|
15
|
-
##
|
16
|
-
|
17
|
-
To merge rows from multiple Excel files into one:
|
15
|
+
## Features
|
18
16
|
|
19
|
-
|
20
|
-
|
21
|
-
|
17
|
+
- ✨ Work with templates using `TemplateFs` (filesystem) or `TemplateMemory` (in-memory)
|
18
|
+
- 📥 Insert and stream rows into Excel files
|
19
|
+
- 🧩 Merge sheets from multiple `.xlsx` files
|
20
|
+
- 🧼 Remove sheets by name or index
|
21
|
+
- 💎 Preserve styles, merges, and shared strings
|
22
22
|
|
23
|
-
|
24
|
-
const otherFile = fs.readFileSync("data.xlsx");
|
23
|
+
## Template API
|
25
24
|
|
26
|
-
|
27
|
-
baseFile,
|
28
|
-
additions: [
|
29
|
-
{ file: otherFile, sheetIndexes: [1] }
|
30
|
-
],
|
31
|
-
gap: 2,
|
32
|
-
});
|
25
|
+
### `TemplateFs` and `TemplateMemory`
|
33
26
|
|
34
|
-
|
35
|
-
```
|
27
|
+
Both classes provide the same API for modifying Excel templates.
|
36
28
|
|
37
|
-
|
29
|
+
#### Common Features
|
38
30
|
|
39
|
-
-
|
40
|
-
-
|
41
|
-
-
|
42
|
-
-
|
31
|
+
- `substitute()` — replace placeholders like `${name}` or `${table:name}`
|
32
|
+
- `insertRows()` / `insertRowsStream()` — insert rows statically or via stream
|
33
|
+
- `copySheet()` — duplicate existing sheets
|
34
|
+
- `validate()` and `save()` / `saveStream()` — output the result
|
43
35
|
|
44
|
-
|
36
|
+
```ts
|
37
|
+
import { TemplateFs } from "@js-ak/excel-toolbox";
|
45
38
|
|
46
|
-
|
39
|
+
const template = await TemplateFs.from({
|
40
|
+
destination: "/tmp/template",
|
41
|
+
source: fs.readFileSync("template.xlsx"),
|
42
|
+
});
|
47
43
|
|
48
|
-
|
44
|
+
await template.substitute("Sheet1", { name: "Alice" });
|
45
|
+
await template.insertRows({ sheetName: "Sheet1", rows: [["Data"]] });
|
46
|
+
const buffer = await template.save();
|
47
|
+
fs.writeFileSync("output.xlsx", buffer);
|
48
|
+
```
|
49
49
|
|
50
|
-
|
51
|
-
|-----------------------|--------------------------------------------------------------------|------------------------------------------------|
|
52
|
-
| `baseFile` | `Buffer` | The base Excel file. |
|
53
|
-
| `additions` | `{ file: Buffer; sheetIndexes: number[]; isBaseFile?: boolean }[]` | Files and sheet indices to merge. |
|
54
|
-
| `baseSheetIndex` | `number` (default: `1`) | The sheet index in the base file to append to. |
|
55
|
-
| `gap` | `number` (default: `0`) | Empty rows inserted between merged blocks. |
|
56
|
-
| `sheetNamesToRemove` | `string[]` (default: `[]`) | Sheets to remove by name. |
|
57
|
-
| `sheetsToRemove` | `number[]` (default: `[]`) | Sheets to remove by index (1-based). |
|
50
|
+
## Sheet Merging API
|
58
51
|
|
59
|
-
|
52
|
+
### `mergeSheetsToBaseFileSync(options): Buffer`
|
60
53
|
|
61
|
-
|
54
|
+
Synchronously merges sheets into a base file.
|
62
55
|
|
63
56
|
### `mergeSheetsToBaseFile(options): Promise<Buffer>`
|
64
57
|
|
65
|
-
|
66
|
-
|
67
|
-
#### Parameters
|
68
|
-
|
69
|
-
Same as [`mergeSheetsToBaseFileSync`](#mergesheetstobasefilesyncoptions).
|
70
|
-
|
71
|
-
#### Returns
|
72
|
-
|
73
|
-
`Promise<Buffer>` — resolves with the merged Excel file.
|
58
|
+
Async version of the above.
|
74
59
|
|
75
60
|
#### Example
|
76
61
|
|
77
62
|
```ts
|
78
|
-
import fs from "node:fs
|
79
|
-
import {
|
63
|
+
import fs from "node:fs";
|
64
|
+
import { mergeSheetsToBaseFileSync } from "@js-ak/excel-toolbox";
|
80
65
|
|
81
|
-
const baseFile =
|
82
|
-
const
|
66
|
+
const baseFile = fs.readFileSync("base.xlsx");
|
67
|
+
const dataFile = fs.readFileSync("data.xlsx");
|
83
68
|
|
84
|
-
const
|
69
|
+
const result = mergeSheetsToBaseFileSync({
|
85
70
|
baseFile,
|
86
|
-
additions: [
|
87
|
-
|
88
|
-
],
|
89
|
-
gap: 1,
|
71
|
+
additions: [{ file: dataFile, sheetIndexes: [1] }],
|
72
|
+
gap: 2,
|
90
73
|
});
|
91
74
|
|
92
|
-
|
75
|
+
fs.writeFileSync("output.xlsx", result);
|
93
76
|
```
|
94
77
|
|
95
|
-
## Contributing
|
96
|
-
|
97
|
-
Contributions are welcome! Feel free to open an issue or submit a pull request if you have ideas or encounter bugs.
|
98
|
-
|
99
78
|
## License
|
100
79
|
|
101
|
-
MIT — see [LICENSE](./LICENSE)
|
80
|
+
MIT — see [LICENSE](./LICENSE)
|
@@ -0,0 +1,105 @@
|
|
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.mergeSheetsToBaseFileProcessSync = mergeSheetsToBaseFileProcessSync;
|
37
|
+
const Utils = __importStar(require("./utils/index.js"));
|
38
|
+
const Xml = __importStar(require("./xml/index.js"));
|
39
|
+
/**
|
40
|
+
* Merges rows from other Excel files into a base Excel file.
|
41
|
+
*
|
42
|
+
* This function is a process-friendly version of mergeSheetsToBaseFile.
|
43
|
+
* It takes a single object with the following properties:
|
44
|
+
* - additions: An array of objects with two properties:
|
45
|
+
* - files: A dictionary of file paths to their corresponding XML content
|
46
|
+
* - sheetIndexes: The 1-based indexes of the sheet to extract rows from
|
47
|
+
* - baseFiles: A dictionary of file paths to their corresponding XML content
|
48
|
+
* - baseSheetIndex: The 1-based index of the sheet in the base file to add rows to
|
49
|
+
* - gap: The number of empty rows to insert between each added section
|
50
|
+
* - sheetNamesToRemove: The names of sheets to remove from the output file
|
51
|
+
* - sheetsToRemove: The 1-based indices of sheets to remove from the output file
|
52
|
+
*
|
53
|
+
* The function returns a dictionary of file paths to their corresponding XML content.
|
54
|
+
*/
|
55
|
+
function mergeSheetsToBaseFileProcessSync(data) {
|
56
|
+
const { additions, baseFiles, baseSheetIndex, gap, sheetNamesToRemove, sheetsToRemove, } = data;
|
57
|
+
const basePath = `xl/worksheets/sheet${baseSheetIndex}.xml`;
|
58
|
+
if (!baseFiles[basePath]) {
|
59
|
+
throw new Error(`Base file does not contain ${basePath}`);
|
60
|
+
}
|
61
|
+
const { lastRowNumber, mergeCells: baseMergeCells, rows: baseRows, xml, } = Xml.extractRowsFromSheetSync(baseFiles[basePath]);
|
62
|
+
const allRows = [...baseRows];
|
63
|
+
const allMergeCells = [...baseMergeCells];
|
64
|
+
let currentRowOffset = lastRowNumber + gap;
|
65
|
+
for (const { files, sheetIndexes } of additions) {
|
66
|
+
for (const sheetIndex of sheetIndexes) {
|
67
|
+
const sheetPath = `xl/worksheets/sheet${sheetIndex}.xml`;
|
68
|
+
if (!files[sheetPath]) {
|
69
|
+
throw new Error(`File does not contain ${sheetPath}`);
|
70
|
+
}
|
71
|
+
const { mergeCells, rows } = Xml.extractRowsFromSheetSync(files[sheetPath]);
|
72
|
+
const shiftedRows = Xml.shiftRowIndices(rows, currentRowOffset);
|
73
|
+
const shiftedMergeCells = mergeCells.map(cell => {
|
74
|
+
const [start, end] = cell.ref.split(":");
|
75
|
+
if (!start || !end) {
|
76
|
+
return cell;
|
77
|
+
}
|
78
|
+
const shiftedStart = Utils.shiftCellRef(start, currentRowOffset);
|
79
|
+
const shiftedEnd = Utils.shiftCellRef(end, currentRowOffset);
|
80
|
+
return { ...cell, ref: `${shiftedStart}:${shiftedEnd}` };
|
81
|
+
});
|
82
|
+
allRows.push(...shiftedRows);
|
83
|
+
allMergeCells.push(...shiftedMergeCells);
|
84
|
+
currentRowOffset += Utils.getMaxRowNumber(rows) + gap;
|
85
|
+
}
|
86
|
+
}
|
87
|
+
const mergedXml = Xml.buildMergedSheet(xml, allRows, allMergeCells);
|
88
|
+
baseFiles[basePath] = mergedXml;
|
89
|
+
for (const sheetIndex of sheetsToRemove) {
|
90
|
+
const sheetPath = `xl/worksheets/sheet${sheetIndex}.xml`;
|
91
|
+
delete baseFiles[sheetPath];
|
92
|
+
if (baseFiles["xl/workbook.xml"]) {
|
93
|
+
baseFiles["xl/workbook.xml"] = Buffer.from(Utils.removeSheetFromWorkbook(baseFiles["xl/workbook.xml"].toString(), sheetIndex));
|
94
|
+
}
|
95
|
+
if (baseFiles["xl/_rels/workbook.xml.rels"]) {
|
96
|
+
baseFiles["xl/_rels/workbook.xml.rels"] = Buffer.from(Utils.removeSheetFromRels(baseFiles["xl/_rels/workbook.xml.rels"].toString(), sheetIndex));
|
97
|
+
}
|
98
|
+
if (baseFiles["[Content_Types].xml"]) {
|
99
|
+
baseFiles["[Content_Types].xml"] = Buffer.from(Utils.removeSheetFromContentTypes(baseFiles["[Content_Types].xml"].toString(), sheetIndex));
|
100
|
+
}
|
101
|
+
}
|
102
|
+
for (const sheetName of sheetNamesToRemove) {
|
103
|
+
Utils.removeSheetByName(baseFiles, sheetName);
|
104
|
+
}
|
105
|
+
}
|
@@ -52,13 +52,13 @@ const Xml = __importStar(require("./xml/index.js"));
|
|
52
52
|
*
|
53
53
|
* The function returns a dictionary of file paths to their corresponding XML content.
|
54
54
|
*/
|
55
|
-
function mergeSheetsToBaseFileProcess(data) {
|
55
|
+
async function mergeSheetsToBaseFileProcess(data) {
|
56
56
|
const { additions, baseFiles, baseSheetIndex, gap, sheetNamesToRemove, sheetsToRemove, } = data;
|
57
57
|
const basePath = `xl/worksheets/sheet${baseSheetIndex}.xml`;
|
58
58
|
if (!baseFiles[basePath]) {
|
59
59
|
throw new Error(`Base file does not contain ${basePath}`);
|
60
60
|
}
|
61
|
-
const { lastRowNumber, mergeCells: baseMergeCells, rows: baseRows, xml, } = Xml.extractRowsFromSheet(baseFiles[basePath]);
|
61
|
+
const { lastRowNumber, mergeCells: baseMergeCells, rows: baseRows, xml, } = await Xml.extractRowsFromSheet(baseFiles[basePath]);
|
62
62
|
const allRows = [...baseRows];
|
63
63
|
const allMergeCells = [...baseMergeCells];
|
64
64
|
let currentRowOffset = lastRowNumber + gap;
|
@@ -68,7 +68,7 @@ function mergeSheetsToBaseFileProcess(data) {
|
|
68
68
|
if (!files[sheetPath]) {
|
69
69
|
throw new Error(`File does not contain ${sheetPath}`);
|
70
70
|
}
|
71
|
-
const { mergeCells, rows } = Xml.extractRowsFromSheet(files[sheetPath]);
|
71
|
+
const { mergeCells, rows } = await Xml.extractRowsFromSheet(files[sheetPath]);
|
72
72
|
const shiftedRows = Xml.shiftRowIndices(rows, currentRowOffset);
|
73
73
|
const shiftedMergeCells = mergeCells.map(cell => {
|
74
74
|
const [start, end] = cell.ref.split(":");
|
@@ -36,7 +36,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.mergeSheetsToBaseFileSync = mergeSheetsToBaseFileSync;
|
37
37
|
const Utils = __importStar(require("./utils/index.js"));
|
38
38
|
const Zip = __importStar(require("./zip/index.js"));
|
39
|
-
const
|
39
|
+
const merge_sheets_to_base_file_process_sync_js_1 = require("./merge-sheets-to-base-file-process-sync.js");
|
40
40
|
/**
|
41
41
|
* Merge rows from other Excel files into a base Excel file.
|
42
42
|
* The output is a new Excel file with the merged content.
|
@@ -65,7 +65,7 @@ function mergeSheetsToBaseFileSync(data) {
|
|
65
65
|
sheetIndexes,
|
66
66
|
});
|
67
67
|
}
|
68
|
-
(0,
|
68
|
+
(0, merge_sheets_to_base_file_process_sync_js_1.mergeSheetsToBaseFileProcessSync)({
|
69
69
|
additions: additionsUpdated,
|
70
70
|
baseFiles,
|
71
71
|
baseSheetIndex,
|
@@ -65,7 +65,7 @@ async function mergeSheetsToBaseFile(data) {
|
|
65
65
|
sheetIndexes,
|
66
66
|
});
|
67
67
|
}
|
68
|
-
(0, merge_sheets_to_base_file_process_js_1.mergeSheetsToBaseFileProcess)({
|
68
|
+
await (0, merge_sheets_to_base_file_process_js_1.mergeSheetsToBaseFileProcess)({
|
69
69
|
additions: additionsUpdated,
|
70
70
|
baseFiles,
|
71
71
|
baseSheetIndex,
|
@@ -32,12 +32,16 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
32
32
|
return result;
|
33
33
|
};
|
34
34
|
})();
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
37
|
+
};
|
35
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
36
39
|
exports.TemplateFs = void 0;
|
37
40
|
const fs = __importStar(require("node:fs/promises"));
|
38
41
|
const fsSync = __importStar(require("node:fs"));
|
39
42
|
const path = __importStar(require("node:path"));
|
40
43
|
const readline = __importStar(require("node:readline"));
|
44
|
+
const node_crypto_1 = __importDefault(require("node:crypto"));
|
41
45
|
const Xml = __importStar(require("../xml/index.js"));
|
42
46
|
const Zip = __importStar(require("../zip/index.js"));
|
43
47
|
const Utils = __importStar(require("./utils/index.js"));
|
@@ -67,6 +71,16 @@ class TemplateFs {
|
|
67
71
|
* @type {boolean}
|
68
72
|
*/
|
69
73
|
#isProcessing = false;
|
74
|
+
/**
|
75
|
+
* The keys for the Excel files in the template.
|
76
|
+
*/
|
77
|
+
#excelKeys = {
|
78
|
+
contentTypes: "[Content_Types].xml",
|
79
|
+
sharedStrings: "xl/sharedStrings.xml",
|
80
|
+
styles: "xl/styles.xml",
|
81
|
+
workbook: "xl/workbook.xml",
|
82
|
+
workbookRels: "xl/_rels/workbook.xml.rels",
|
83
|
+
};
|
70
84
|
/**
|
71
85
|
* Creates a Template instance.
|
72
86
|
*
|
@@ -149,17 +163,19 @@ class TemplateFs {
|
|
149
163
|
* @returns The path of the sheet inside the workbook.
|
150
164
|
* @throws {Error} If the sheet is not found.
|
151
165
|
*/
|
152
|
-
async #
|
166
|
+
async #getSheetPathByName(sheetName) {
|
153
167
|
// Read XML workbook to find sheet name and path
|
154
|
-
const workbookXml = Xml.extractXmlFromSheet(await this.#readFile(
|
155
|
-
const sheetMatch = workbookXml.match(
|
156
|
-
if (!sheetMatch)
|
168
|
+
const workbookXml = await Xml.extractXmlFromSheet(await this.#readFile(this.#excelKeys.workbook));
|
169
|
+
const sheetMatch = workbookXml.match(Utils.sheetMatch(sheetName));
|
170
|
+
if (!sheetMatch || !sheetMatch[1]) {
|
157
171
|
throw new Error(`Sheet "${sheetName}" not found`);
|
172
|
+
}
|
158
173
|
const rId = sheetMatch[1];
|
159
|
-
const relsXml = Xml.extractXmlFromSheet(await this.#readFile(
|
160
|
-
const relMatch = relsXml.match(
|
161
|
-
if (!relMatch)
|
174
|
+
const relsXml = await Xml.extractXmlFromSheet(await this.#readFile(this.#excelKeys.workbookRels));
|
175
|
+
const relMatch = relsXml.match(Utils.relationshipMatch(rId));
|
176
|
+
if (!relMatch || !relMatch[1]) {
|
162
177
|
throw new Error(`Relationship "${rId}" not found`);
|
178
|
+
}
|
163
179
|
return "xl/" + relMatch[1].replace(/^\/?xl\//, "");
|
164
180
|
}
|
165
181
|
/**
|
@@ -209,15 +225,15 @@ class TemplateFs {
|
|
209
225
|
await fs.writeFile(fullPath, Buffer.isBuffer(content) ? content : Buffer.from(content));
|
210
226
|
}
|
211
227
|
async #substitute(sheetName, replacements) {
|
212
|
-
const sharedStringsPath =
|
213
|
-
const sheetPath = await this.#
|
228
|
+
const sharedStringsPath = this.#excelKeys.sharedStrings;
|
229
|
+
const sheetPath = await this.#getSheetPathByName(sheetName);
|
214
230
|
let sharedStringsContent = "";
|
215
231
|
let sheetContent = "";
|
216
232
|
if (this.fileKeys.has(sharedStringsPath)) {
|
217
|
-
sharedStringsContent = Xml.extractXmlFromSheet(await this.#readFile(sharedStringsPath));
|
233
|
+
sharedStringsContent = await Xml.extractXmlFromSheet(await this.#readFile(sharedStringsPath));
|
218
234
|
}
|
219
235
|
if (this.fileKeys.has(sheetPath)) {
|
220
|
-
sheetContent = Xml.extractXmlFromSheet(await this.#readFile(sheetPath));
|
236
|
+
sheetContent = await Xml.extractXmlFromSheet(await this.#readFile(sheetPath));
|
221
237
|
const TABLE_REGEX = /\$\{table:([a-zA-Z0-9_]+)\.([a-zA-Z0-9_]+)\}/g;
|
222
238
|
const hasTablePlaceholders = TABLE_REGEX.test(sharedStringsContent) || TABLE_REGEX.test(sheetContent);
|
223
239
|
if (hasTablePlaceholders) {
|
@@ -269,30 +285,31 @@ class TemplateFs {
|
|
269
285
|
this.#ensureNotDestroyed();
|
270
286
|
this.#isProcessing = true;
|
271
287
|
try {
|
288
|
+
if (sourceName === newName) {
|
289
|
+
throw new Error("Source and new sheet names cannot be the same");
|
290
|
+
}
|
272
291
|
// Read workbook.xml and find the source sheet
|
273
|
-
const workbookXmlPath =
|
274
|
-
const workbookXml = Xml.extractXmlFromSheet(await this.#readFile(workbookXmlPath));
|
292
|
+
const workbookXmlPath = this.#excelKeys.workbook;
|
293
|
+
const workbookXml = await Xml.extractXmlFromSheet(await this.#readFile(workbookXmlPath));
|
275
294
|
// Find the source sheet
|
276
|
-
const
|
277
|
-
|
278
|
-
if (!sheetMatch)
|
295
|
+
const sheetMatch = workbookXml.match(Utils.sheetMatch(sourceName));
|
296
|
+
if (!sheetMatch || !sheetMatch[1]) {
|
279
297
|
throw new Error(`Sheet "${sourceName}" not found`);
|
280
|
-
|
298
|
+
}
|
281
299
|
// Check if a sheet with the new name already exists
|
282
300
|
if (new RegExp(`<sheet[^>]+name="${newName}"`).test(workbookXml)) {
|
283
301
|
throw new Error(`Sheet "${newName}" already exists`);
|
284
302
|
}
|
285
303
|
// Read workbook.rels
|
286
304
|
// Find the source sheet path by rId
|
287
|
-
const
|
288
|
-
const
|
289
|
-
const
|
290
|
-
const relMatch = relsXml.match(
|
291
|
-
if (!relMatch)
|
292
|
-
throw new Error(`Relationship "${
|
305
|
+
const rId = sheetMatch[1];
|
306
|
+
const relsXmlPath = this.#excelKeys.workbookRels;
|
307
|
+
const relsXml = await Xml.extractXmlFromSheet(await this.#readFile(relsXmlPath));
|
308
|
+
const relMatch = relsXml.match(Utils.relationshipMatch(rId));
|
309
|
+
if (!relMatch || !relMatch[1]) {
|
310
|
+
throw new Error(`Relationship "${rId}" not found`);
|
311
|
+
}
|
293
312
|
const sourceTarget = relMatch[1]; // sheetN.xml
|
294
|
-
if (!sourceTarget)
|
295
|
-
throw new Error(`Relationship "${sourceRId}" not found`);
|
296
313
|
const sourceSheetPath = "xl/" + sourceTarget.replace(/^\/?.*xl\//, "");
|
297
314
|
// Get the index of the new sheet
|
298
315
|
const sheetNumbers = [...this.fileKeys]
|
@@ -321,8 +338,8 @@ class TemplateFs {
|
|
321
338
|
await this.#set(relsXmlPath, updatedRelsXml);
|
322
339
|
// Read [Content_Types].xml
|
323
340
|
// Update [Content_Types].xml
|
324
|
-
const contentTypesPath =
|
325
|
-
const contentTypesXml = Xml.extractXmlFromSheet(await this.#readFile(contentTypesPath));
|
341
|
+
const contentTypesPath = this.#excelKeys.contentTypes;
|
342
|
+
const contentTypesXml = await Xml.extractXmlFromSheet(await this.#readFile(contentTypesPath));
|
326
343
|
const overrideTag = `<Override PartName="/xl/worksheets/${newSheetFilename}" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>`;
|
327
344
|
const updatedContentTypesXml = contentTypesXml.replace("</Types>", overrideTag + "</Types>");
|
328
345
|
await this.#set(contentTypesPath, updatedContentTypesXml);
|
@@ -379,20 +396,9 @@ class TemplateFs {
|
|
379
396
|
Utils.checkStartRow(startRowNumber);
|
380
397
|
Utils.checkRows(preparedRows);
|
381
398
|
// Find the sheet
|
382
|
-
const
|
383
|
-
const sheetMatch = workbookXml.match(new RegExp(`<sheet[^>]+name="${sheetName}"[^>]+r:id="([^"]+)"[^>]*/>`));
|
384
|
-
if (!sheetMatch || !sheetMatch[1]) {
|
385
|
-
throw new Error(`Sheet "${sheetName}" not found`);
|
386
|
-
}
|
387
|
-
const rId = sheetMatch[1];
|
388
|
-
const relsXml = Xml.extractXmlFromSheet(await this.#readFile("xl/_rels/workbook.xml.rels"));
|
389
|
-
const relMatch = relsXml.match(new RegExp(`<Relationship[^>]+Id="${rId}"[^>]+Target="([^"]+)"[^>]*/>`));
|
390
|
-
if (!relMatch || !relMatch[1]) {
|
391
|
-
throw new Error(`Relationship "${rId}" not found`);
|
392
|
-
}
|
393
|
-
const sheetPath = "xl/" + relMatch[1].replace(/^\/?xl\//, "");
|
399
|
+
const sheetPath = await this.#getSheetPathByName(sheetName);
|
394
400
|
const sheetXmlRaw = await this.#readFile(sheetPath);
|
395
|
-
const sheetXml = Xml.extractXmlFromSheet(sheetXmlRaw);
|
401
|
+
const sheetXml = await Xml.extractXmlFromSheet(sheetXmlRaw);
|
396
402
|
let nextRow = 0;
|
397
403
|
if (!startRowNumber) {
|
398
404
|
// Find the last row
|
@@ -426,7 +432,7 @@ class TemplateFs {
|
|
426
432
|
else {
|
427
433
|
updatedXml = sheetXml.replace(/<worksheet[^>]*>/, (match) => `${match}<sheetData>${rowsXml}</sheetData>`);
|
428
434
|
}
|
429
|
-
await this.#set(sheetPath, updatedXml);
|
435
|
+
await this.#set(sheetPath, Utils.updateDimension(updatedXml));
|
430
436
|
}
|
431
437
|
finally {
|
432
438
|
this.#isProcessing = false;
|
@@ -454,18 +460,8 @@ class TemplateFs {
|
|
454
460
|
const { rows, sheetName, startRowNumber } = data;
|
455
461
|
if (!sheetName)
|
456
462
|
throw new Error("Sheet name is required");
|
457
|
-
//
|
458
|
-
const
|
459
|
-
const sheetMatch = workbookXml.match(new RegExp(`<sheet[^>]+name="${sheetName}"[^>]+r:id="([^"]+)"[^>]*/>`));
|
460
|
-
if (!sheetMatch)
|
461
|
-
throw new Error(`Sheet "${sheetName}" not found`);
|
462
|
-
const rId = sheetMatch[1];
|
463
|
-
const relsXml = Xml.extractXmlFromSheet(await this.#readFile("xl/_rels/workbook.xml.rels"));
|
464
|
-
const relMatch = relsXml.match(new RegExp(`<Relationship[^>]+Id="${rId}"[^>]+Target="([^"]+)"[^>]*/>`));
|
465
|
-
if (!relMatch)
|
466
|
-
throw new Error(`Relationship "${rId}" not found`);
|
467
|
-
// Path to the desired sheet (sheet1.xml)
|
468
|
-
const sheetPath = "xl/" + relMatch[1].replace(/^\/?xl\//, "");
|
463
|
+
// Get the path to the sheet
|
464
|
+
const sheetPath = await this.#getSheetPathByName(sheetName);
|
469
465
|
// The temporary file for writing
|
470
466
|
const fullPath = path.join(this.destination, ...sheetPath.split("/"));
|
471
467
|
const tempPath = fullPath + ".tmp";
|
@@ -474,6 +470,13 @@ class TemplateFs {
|
|
474
470
|
const output = fsSync.createWriteStream(tempPath, { encoding: "utf-8" });
|
475
471
|
// Inserted rows flag
|
476
472
|
let inserted = false;
|
473
|
+
let initialDimension = "";
|
474
|
+
const dimension = {
|
475
|
+
maxColumn: "A",
|
476
|
+
maxRow: 1,
|
477
|
+
minColumn: "A",
|
478
|
+
minRow: 1,
|
479
|
+
};
|
477
480
|
const rl = readline.createInterface({
|
478
481
|
// Process all line breaks
|
479
482
|
crlfDelay: Infinity,
|
@@ -482,6 +485,21 @@ class TemplateFs {
|
|
482
485
|
let isCollecting = false;
|
483
486
|
let collection = "";
|
484
487
|
for await (const line of rl) {
|
488
|
+
// Process <dimension>
|
489
|
+
if (!initialDimension && /<dimension\s+ref="[^"]*"/.test(line)) {
|
490
|
+
const dimensionMatch = line.match(/<dimension\s+ref="([^"]*)"/);
|
491
|
+
if (dimensionMatch) {
|
492
|
+
const dimensionRef = dimensionMatch[1];
|
493
|
+
if (dimensionRef) {
|
494
|
+
const [min, max] = dimensionRef.split(":");
|
495
|
+
dimension.minColumn = min.slice(0, 1);
|
496
|
+
dimension.minRow = parseInt(min.slice(1));
|
497
|
+
dimension.maxColumn = max.slice(0, 1);
|
498
|
+
dimension.maxRow = parseInt(max.slice(1));
|
499
|
+
}
|
500
|
+
initialDimension = line.match(/<dimension\s+ref="[^"]*"/)?.[0] || "";
|
501
|
+
}
|
502
|
+
}
|
485
503
|
// Collect lines between <sheetData> and </sheetData>
|
486
504
|
if (!inserted && isCollecting) {
|
487
505
|
collection += line;
|
@@ -508,7 +526,13 @@ class TemplateFs {
|
|
508
526
|
output.write(innerRows);
|
509
527
|
}
|
510
528
|
}
|
511
|
-
const { rowNumber: actualRowNumber } = await Utils.writeRowsToStream(output, rows, maxRowNumber);
|
529
|
+
const { dimension: newDimension, rowNumber: actualRowNumber } = await Utils.writeRowsToStream(output, rows, maxRowNumber);
|
530
|
+
if (compareColumns(newDimension.maxColumn, dimension.maxColumn) > 0) {
|
531
|
+
dimension.maxColumn = newDimension.maxColumn;
|
532
|
+
}
|
533
|
+
if (newDimension.maxRow > dimension.maxRow) {
|
534
|
+
dimension.maxRow = newDimension.maxRow;
|
535
|
+
}
|
512
536
|
if (innerRows) {
|
513
537
|
const filteredRows = Utils.getRowsAbove(innerRowsMap, actualRowNumber);
|
514
538
|
if (filteredRows)
|
@@ -549,7 +573,13 @@ class TemplateFs {
|
|
549
573
|
}
|
550
574
|
}
|
551
575
|
// new <row>
|
552
|
-
const { rowNumber: actualRowNumber } = await Utils.writeRowsToStream(output, rows, maxRowNumber);
|
576
|
+
const { dimension: newDimension, rowNumber: actualRowNumber } = await Utils.writeRowsToStream(output, rows, maxRowNumber);
|
577
|
+
if (compareColumns(newDimension.maxColumn, dimension.maxColumn) > 0) {
|
578
|
+
dimension.maxColumn = newDimension.maxColumn;
|
579
|
+
}
|
580
|
+
if (newDimension.maxRow > dimension.maxRow) {
|
581
|
+
dimension.maxRow = newDimension.maxRow;
|
582
|
+
}
|
553
583
|
if (innerRows) {
|
554
584
|
const filteredRows = Utils.getRowsAbove(innerRowsMap, actualRowNumber);
|
555
585
|
if (filteredRows) {
|
@@ -576,7 +606,13 @@ class TemplateFs {
|
|
576
606
|
// Insert opening tag
|
577
607
|
output.write("<sheetData>");
|
578
608
|
// Prepare the rows
|
579
|
-
await Utils.writeRowsToStream(output, rows, maxRowNumber);
|
609
|
+
const { dimension: newDimension } = await Utils.writeRowsToStream(output, rows, maxRowNumber);
|
610
|
+
if (compareColumns(newDimension.maxColumn, dimension.maxColumn) > 0) {
|
611
|
+
dimension.maxColumn = newDimension.maxColumn;
|
612
|
+
}
|
613
|
+
if (newDimension.maxRow > dimension.maxRow) {
|
614
|
+
dimension.maxRow = newDimension.maxRow;
|
615
|
+
}
|
580
616
|
// Insert closing tag
|
581
617
|
output.write("</sheetData>");
|
582
618
|
if (after) {
|
@@ -597,8 +633,42 @@ class TemplateFs {
|
|
597
633
|
// Close the streams
|
598
634
|
rl.close();
|
599
635
|
output.end();
|
600
|
-
//
|
601
|
-
|
636
|
+
// update dimension
|
637
|
+
{
|
638
|
+
const target = initialDimension;
|
639
|
+
const refRange = `${dimension.minColumn}${dimension.minRow}:${dimension.maxColumn}${dimension.maxRow}`;
|
640
|
+
const replacement = `<dimension ref="${refRange}"`;
|
641
|
+
let buffer = "";
|
642
|
+
let replaced = false;
|
643
|
+
const input = fsSync.createReadStream(tempPath, { encoding: "utf8" });
|
644
|
+
const output = fsSync.createWriteStream(fullPath);
|
645
|
+
await new Promise((resolve, reject) => {
|
646
|
+
input.on("data", chunk => {
|
647
|
+
buffer += chunk;
|
648
|
+
if (!replaced) {
|
649
|
+
const index = buffer.indexOf(target);
|
650
|
+
if (index !== -1) {
|
651
|
+
// Заменяем только первое вхождение
|
652
|
+
buffer = buffer.replace(target, replacement);
|
653
|
+
replaced = true;
|
654
|
+
}
|
655
|
+
}
|
656
|
+
output.write(buffer);
|
657
|
+
buffer = ""; // очищаем, т.к. мы уже записали
|
658
|
+
});
|
659
|
+
input.on("error", reject);
|
660
|
+
output.on("error", reject);
|
661
|
+
input.on("end", () => {
|
662
|
+
// на всякий случай дописываем остаток
|
663
|
+
if (buffer) {
|
664
|
+
output.write(buffer);
|
665
|
+
}
|
666
|
+
output.end();
|
667
|
+
resolve(true);
|
668
|
+
});
|
669
|
+
});
|
670
|
+
}
|
671
|
+
await fs.unlink(tempPath);
|
602
672
|
}
|
603
673
|
finally {
|
604
674
|
this.#isProcessing = false;
|
@@ -699,29 +769,39 @@ class TemplateFs {
|
|
699
769
|
* @param {Object} data - The data to create the template from.
|
700
770
|
* @param {string} data.source - The path or buffer of the Excel file.
|
701
771
|
* @param {string} data.destination - The path to save the template to.
|
772
|
+
* @param {boolean} data.isUniqueDestination - Whether to add a random UUID to the destination path.
|
702
773
|
* @returns {Promise<Template>} A new Template instance.
|
703
774
|
* @throws {Error} If reading or writing files fails.
|
704
775
|
* @experimental This API is experimental and might change in future versions.
|
705
776
|
*/
|
706
777
|
static async from(data) {
|
707
|
-
const { destination, source } = data;
|
778
|
+
const { destination, isUniqueDestination = true, source } = data;
|
708
779
|
if (!destination) {
|
709
780
|
throw new Error("Destination is required");
|
710
781
|
}
|
782
|
+
// add random uuid to destination
|
783
|
+
const destinationWithUuid = isUniqueDestination
|
784
|
+
? path.join(destination, node_crypto_1.default.randomUUID())
|
785
|
+
: destination;
|
711
786
|
const buffer = typeof source === "string"
|
712
787
|
? await fs.readFile(source)
|
713
788
|
: source;
|
714
789
|
const files = await Zip.read(buffer);
|
715
790
|
// if destination exists, remove it
|
716
|
-
await fs.rm(
|
791
|
+
await fs.rm(destinationWithUuid, { force: true, recursive: true });
|
717
792
|
// Write all files to the file system, preserving exact paths
|
718
|
-
await fs.mkdir(
|
793
|
+
await fs.mkdir(destinationWithUuid, { recursive: true });
|
719
794
|
await Promise.all(Object.entries(files).map(async ([filePath, content]) => {
|
720
|
-
const fullPath = path.join(
|
795
|
+
const fullPath = path.join(destinationWithUuid, ...filePath.split("/"));
|
721
796
|
await fs.mkdir(path.dirname(fullPath), { recursive: true });
|
722
797
|
await fs.writeFile(fullPath, content);
|
723
798
|
}));
|
724
|
-
return new TemplateFs(new Set(Object.keys(files)),
|
799
|
+
return new TemplateFs(new Set(Object.keys(files)), destinationWithUuid);
|
725
800
|
}
|
726
801
|
}
|
727
802
|
exports.TemplateFs = TemplateFs;
|
803
|
+
const compareColumns = (a, b) => {
|
804
|
+
if (a === b)
|
805
|
+
return 0;
|
806
|
+
return a.length === b.length ? (a < b ? -1 : 1) : (a.length < b.length ? -1 : 1);
|
807
|
+
};
|