@js-ak/excel-toolbox 1.4.1 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/README.md +41 -62
  2. package/build/cjs/lib/template/index.js +1 -0
  3. package/build/cjs/lib/template/memory-write-stream.js +17 -0
  4. package/build/cjs/lib/template/template-fs.js +137 -57
  5. package/build/cjs/lib/template/template-memory.js +753 -0
  6. package/build/cjs/lib/template/utils/index.js +25 -0
  7. package/build/cjs/lib/template/utils/prepare-row-to-cells.js +17 -0
  8. package/build/cjs/lib/template/utils/regexp.js +32 -0
  9. package/build/cjs/lib/template/utils/update-dimension.js +15 -0
  10. package/build/cjs/lib/template/utils/validate-worksheet-xml.js +74 -74
  11. package/build/cjs/lib/template/utils/write-rows-to-stream.js +66 -22
  12. package/build/esm/lib/template/index.js +1 -0
  13. package/build/esm/lib/template/memory-write-stream.js +13 -0
  14. package/build/esm/lib/template/template-fs.js +134 -57
  15. package/build/esm/lib/template/template-memory.js +716 -0
  16. package/build/esm/lib/template/utils/index.js +2 -0
  17. package/build/esm/lib/template/utils/prepare-row-to-cells.js +14 -0
  18. package/build/esm/lib/template/utils/regexp.js +28 -0
  19. package/build/esm/lib/template/utils/update-dimension.js +15 -0
  20. package/build/esm/lib/template/utils/validate-worksheet-xml.js +74 -74
  21. package/build/esm/lib/template/utils/write-rows-to-stream.js +66 -22
  22. package/build/types/lib/template/index.d.ts +1 -0
  23. package/build/types/lib/template/memory-write-stream.d.ts +6 -0
  24. package/build/types/lib/template/template-fs.d.ts +2 -0
  25. package/build/types/lib/template/template-memory.d.ts +146 -0
  26. package/build/types/lib/template/utils/index.d.ts +2 -0
  27. package/build/types/lib/template/utils/prepare-row-to-cells.d.ts +5 -0
  28. package/build/types/lib/template/utils/regexp.d.ts +24 -0
  29. package/build/types/lib/template/utils/update-dimension.d.ts +15 -0
  30. package/build/types/lib/template/utils/write-rows-to-stream.d.ts +28 -11
  31. package/package.json +5 -3
@@ -10,10 +10,34 @@ var __createBinding = (this && this.__createBinding) || (Object.create ? (functi
10
10
  if (k2 === undefined) k2 = k;
11
11
  o[k2] = m[k];
12
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
+ })();
13
35
  var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
36
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
37
  };
16
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.Common = void 0;
40
+ exports.Common = __importStar(require("../../utils/index.js"));
17
41
  __exportStar(require("./apply-replacements.js"), exports);
18
42
  __exportStar(require("./check-row.js"), exports);
19
43
  __exportStar(require("./check-rows.js"), exports);
@@ -30,6 +54,7 @@ __exportStar(require("./process-merge-cells.js"), exports);
30
54
  __exportStar(require("./process-merge-finalize.js"), exports);
31
55
  __exportStar(require("./process-rows.js"), exports);
32
56
  __exportStar(require("./process-shared-strings.js"), exports);
57
+ __exportStar(require("./regexp.js"), exports);
33
58
  __exportStar(require("./to-excel-column-object.js"), exports);
34
59
  __exportStar(require("./update-dimension.js"), exports);
35
60
  __exportStar(require("./validate-worksheet-xml.js"), exports);
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.prepareRowToCells = prepareRowToCells;
4
+ const column_index_to_letter_js_1 = require("./column-index-to-letter.js");
5
+ const escape_xml_js_1 = require("./escape-xml.js");
6
+ function prepareRowToCells(row, rowNumber) {
7
+ return row.map((value, colIndex) => {
8
+ const colLetter = (0, column_index_to_letter_js_1.columnIndexToLetter)(colIndex);
9
+ const cellRef = `${colLetter}${rowNumber}`;
10
+ const cellValue = (0, escape_xml_js_1.escapeXml)(String(value ?? ""));
11
+ return {
12
+ cellRef,
13
+ cellValue,
14
+ cellXml: `<c r="${cellRef}" t="inlineStr"><is><t>${cellValue}</t></is></c>`,
15
+ };
16
+ });
17
+ }
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.relationshipMatch = relationshipMatch;
4
+ exports.sheetMatch = sheetMatch;
5
+ /**
6
+ * Creates a regular expression to match a relationship element with a specific ID.
7
+ *
8
+ * @param {string} id - The relationship ID to match (e.g. "rId1")
9
+ * @returns {RegExp} A regular expression that matches a Relationship XML element with the given ID and captures the Target attribute value
10
+ * @example
11
+ * const regex = relationshipMatch("rId1");
12
+ * const xml = '<Relationship Id="rId1" Target="worksheets/sheet1.xml"/>';
13
+ * const match = xml.match(regex);
14
+ * // match[1] === "worksheets/sheet1.xml"
15
+ */
16
+ function relationshipMatch(id) {
17
+ return new RegExp(`<Relationship[^>]+Id="${id}"[^>]+Target="([^"]+)"[^>]*/>`);
18
+ }
19
+ /**
20
+ * Creates a regular expression to match a sheet element with a specific name.
21
+ *
22
+ * @param {string} sheetName - The name of the sheet to match
23
+ * @returns {RegExp} A regular expression that matches a sheet XML element with the given name and captures the r:id attribute value
24
+ * @example
25
+ * const regex = sheetMatch("Sheet1");
26
+ * const xml = '<sheet name="Sheet1" sheetId="1" r:id="rId1"/>';
27
+ * const match = xml.match(regex);
28
+ * // match[1] === "rId1"
29
+ */
30
+ function sheetMatch(sheetName) {
31
+ return new RegExp(`<sheet[^>]+name="${sheetName}"[^>]+r:id="([^"]+)"[^>]*/>`);
32
+ }
@@ -1,6 +1,21 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.updateDimension = updateDimension;
4
+ /**
5
+ * Updates the dimension element in an Excel worksheet XML string based on the actual cell references.
6
+ *
7
+ * This function scans the XML for all cell references and calculates the minimum and maximum
8
+ * column/row values to determine the actual used range in the worksheet. It then updates
9
+ * the dimension element to reflect this range.
10
+ *
11
+ * @param {string} xml - The worksheet XML string to process
12
+ * @returns {string} The XML string with updated dimension element
13
+ * @example
14
+ * // XML with cells from A1 to C3
15
+ * const xml = '....<dimension ref="A1:B2"/>.....<c r="C3">...</c>...';
16
+ * const updated = updateDimension(xml);
17
+ * // Returns XML with dimension updated to ref="A1:C3"
18
+ */
4
19
  function updateDimension(xml) {
5
20
  const cellRefs = [...xml.matchAll(/<c r="([A-Z]+)(\d+)"/g)];
6
21
  if (cellRefs.length === 0)
@@ -33,9 +33,7 @@ function validateWorksheetXml(xml) {
33
33
  const requiredElements = [
34
34
  { name: "sheetViews", tag: "<sheetViews>" },
35
35
  { name: "sheetFormatPr", tag: "<sheetFormatPr" },
36
- { name: "cols", tag: "<cols>" },
37
36
  { name: "sheetData", tag: "<sheetData>" },
38
- { name: "mergeCells", tag: "<mergeCells" },
39
37
  ];
40
38
  for (const { name, tag } of requiredElements) {
41
39
  if (!xml.includes(tag)) {
@@ -100,59 +98,82 @@ function validateWorksheetXml(xml) {
100
98
  }
101
99
  }
102
100
  // 4. Check mergeCells
103
- const mergeCellsStart = xml.indexOf("<mergeCells");
104
- const mergeCellsEnd = xml.indexOf("</mergeCells>");
105
- if (mergeCellsStart === -1 || mergeCellsEnd === -1) {
106
- return createError("Invalid mergeCells structure");
107
- }
108
- const mergeCellsContent = xml.substring(mergeCellsStart, mergeCellsEnd);
109
- const countMatch = mergeCellsContent.match(/count="(\d+)"/);
110
- if (!countMatch) {
111
- return createError("Count attribute not specified for mergeCells");
112
- }
113
- const mergeCellTags = mergeCellsContent.match(/<mergeCell\s+ref="([A-Z]+\d+:[A-Z]+\d+)"\s*\/>/g);
114
- if (!mergeCellTags) {
115
- return createError("No merged cells found");
116
- }
117
- // Check if the number of mergeCells matches the count attribute
118
- if (mergeCellTags.length !== parseInt(countMatch[1])) {
119
- return createError("Mismatch in the number of merged cells", `Expected: ${countMatch[1]}, found: ${mergeCellTags.length}`);
120
- }
121
- // Check for duplicates of mergeCell
122
- const mergeRefs = new Set();
123
- const duplicates = new Set();
124
- for (const mergeTag of mergeCellTags) {
125
- const refMatch = mergeTag.match(/ref="([A-Z]+\d+:[A-Z]+\d+)"/);
126
- if (!refMatch) {
127
- return createError("Invalid merge cell format", `Tag: ${mergeTag}`);
128
- }
129
- const ref = refMatch[1];
130
- if (mergeRefs.has(ref)) {
131
- duplicates.add(ref);
101
+ if (xml.includes("<mergeCells")) {
102
+ const mergeCellsStart = xml.indexOf("<mergeCells");
103
+ const mergeCellsEnd = xml.indexOf("</mergeCells>");
104
+ if (mergeCellsStart === -1 || mergeCellsEnd === -1) {
105
+ return createError("Invalid mergeCells structure");
106
+ }
107
+ const mergeCellsContent = xml.substring(mergeCellsStart, mergeCellsEnd);
108
+ const countMatch = mergeCellsContent.match(/count="(\d+)"/);
109
+ if (!countMatch) {
110
+ return createError("Count attribute not specified for mergeCells");
111
+ }
112
+ const mergeCellTags = mergeCellsContent.match(/<mergeCell\s+ref="([A-Z]+\d+:[A-Z]+\d+)"\s*\/>/g);
113
+ if (!mergeCellTags) {
114
+ return createError("No merged cells found");
115
+ }
116
+ // Check if the number of mergeCells matches the count attribute
117
+ if (mergeCellTags.length !== parseInt(countMatch[1])) {
118
+ return createError("Mismatch in the number of merged cells", `Expected: ${countMatch[1]}, found: ${mergeCellTags.length}`);
119
+ }
120
+ // Check for duplicates of mergeCell
121
+ const mergeRefs = new Set();
122
+ const duplicates = new Set();
123
+ for (const mergeTag of mergeCellTags) {
124
+ const refMatch = mergeTag.match(/ref="([A-Z]+\d+:[A-Z]+\d+)"/);
125
+ if (!refMatch) {
126
+ return createError("Invalid merge cell format", `Tag: ${mergeTag}`);
127
+ }
128
+ const ref = refMatch[1];
129
+ if (mergeRefs.has(ref)) {
130
+ duplicates.add(ref);
131
+ }
132
+ else {
133
+ mergeRefs.add(ref);
134
+ }
132
135
  }
133
- else {
134
- mergeRefs.add(ref);
136
+ if (duplicates.size > 0) {
137
+ return createError("Duplicates of merged cells found", `Duplicates: ${Array.from(duplicates).join(", ")}`);
138
+ }
139
+ // Check for overlapping merge ranges
140
+ const mergedRanges = Array.from(mergeRefs).map(ref => {
141
+ const [start, end] = ref.split(":");
142
+ return {
143
+ endCol: end.match(/[A-Z]+/)?.[0] || "",
144
+ endRow: parseInt(end.match(/\d+/)?.[0] || "0"),
145
+ startCol: start.match(/[A-Z]+/)?.[0] || "",
146
+ startRow: parseInt(start.match(/\d+/)?.[0] || "0"),
147
+ };
148
+ });
149
+ for (let i = 0; i < mergedRanges.length; i++) {
150
+ for (let j = i + 1; j < mergedRanges.length; j++) {
151
+ const a = mergedRanges[i];
152
+ const b = mergedRanges[j];
153
+ if (rangesIntersect(a, b)) {
154
+ return createError("Found intersecting merged cells", `Intersecting: ${getRangeString(a)} and ${getRangeString(b)}`);
155
+ }
156
+ }
135
157
  }
136
- }
137
- if (duplicates.size > 0) {
138
- return createError("Duplicates of merged cells found", `Duplicates: ${Array.from(duplicates).join(", ")}`);
139
- }
140
- // Check for overlapping merge ranges
141
- const mergedRanges = Array.from(mergeRefs).map(ref => {
142
- const [start, end] = ref.split(":");
143
- return {
144
- endCol: end.match(/[A-Z]+/)?.[0] || "",
145
- endRow: parseInt(end.match(/\d+/)?.[0] || "0"),
146
- startCol: start.match(/[A-Z]+/)?.[0] || "",
147
- startRow: parseInt(start.match(/\d+/)?.[0] || "0"),
148
- };
149
- });
150
- for (let i = 0; i < mergedRanges.length; i++) {
151
- for (let j = i + 1; j < mergedRanges.length; j++) {
152
- const a = mergedRanges[i];
153
- const b = mergedRanges[j];
154
- if (rangesIntersect(a, b)) {
155
- return createError("Found intersecting merged cells", `Intersecting: ${getRangeString(a)} and ${getRangeString(b)}`);
158
+ // 6. Additional check: all mergeCell tags refer to existing cells
159
+ for (const mergeTag of mergeCellTags) {
160
+ const refMatch = mergeTag.match(/ref="([A-Z]+\d+:[A-Z]+\d+)"/);
161
+ if (!refMatch) {
162
+ return createError("Invalid merge cell format", `Tag: ${mergeTag}`);
163
+ }
164
+ const [cell1, cell2] = refMatch[1].split(":");
165
+ const cell1Col = cell1.match(/[A-Z]+/)?.[0];
166
+ const cell1Row = parseInt(cell1.match(/\d+/)?.[0] || "0");
167
+ const cell2Col = cell2.match(/[A-Z]+/)?.[0];
168
+ const cell2Row = parseInt(cell2.match(/\d+/)?.[0] || "0");
169
+ if (!cell1Col || !cell2Col || isNaN(cell1Row) || isNaN(cell2Row)) {
170
+ return createError("Invalid merged cell coordinates", `Merged cells: ${refMatch[1]}`);
171
+ }
172
+ // Check if the merged cells exist
173
+ const cell1Exists = allCells.some(c => c.row === cell1Row && c.col === cell1Col);
174
+ const cell2Exists = allCells.some(c => c.row === cell2Row && c.col === cell2Col);
175
+ if (!cell1Exists || !cell2Exists) {
176
+ return createError("Merged cell reference points to non-existent cells", `Merged cells: ${refMatch[1]}, missing: ${!cell1Exists ? `${cell1Col}${cell1Row}` : `${cell2Col}${cell2Row}`}`);
156
177
  }
157
178
  }
158
179
  }
@@ -181,27 +202,6 @@ function validateWorksheetXml(xml) {
181
202
  return createError("Cell is outside the specified area (by column)", `Cell: ${cell.col}${cell.row}, dimension: ${dimensionMatch[1]}`);
182
203
  }
183
204
  }
184
- // 6. Additional check: all mergeCell tags refer to existing cells
185
- for (const mergeTag of mergeCellTags) {
186
- const refMatch = mergeTag.match(/ref="([A-Z]+\d+:[A-Z]+\d+)"/);
187
- if (!refMatch) {
188
- return createError("Invalid merge cell format", `Tag: ${mergeTag}`);
189
- }
190
- const [cell1, cell2] = refMatch[1].split(":");
191
- const cell1Col = cell1.match(/[A-Z]+/)?.[0];
192
- const cell1Row = parseInt(cell1.match(/\d+/)?.[0] || "0");
193
- const cell2Col = cell2.match(/[A-Z]+/)?.[0];
194
- const cell2Row = parseInt(cell2.match(/\d+/)?.[0] || "0");
195
- if (!cell1Col || !cell2Col || isNaN(cell1Row) || isNaN(cell2Row)) {
196
- return createError("Invalid merged cell coordinates", `Merged cells: ${refMatch[1]}`);
197
- }
198
- // Check if the merged cells exist
199
- const cell1Exists = allCells.some(c => c.row === cell1Row && c.col === cell1Col);
200
- const cell2Exists = allCells.some(c => c.row === cell2Row && c.col === cell2Col);
201
- if (!cell1Exists || !cell2Exists) {
202
- return createError("Merged cell reference points to non-existent cells", `Merged cells: ${refMatch[1]}, missing: ${!cell1Exists ? `${cell1Col}${cell1Row}` : `${cell2Col}${cell2Row}`}`);
203
- }
204
- }
205
205
  return { isValid: true };
206
206
  }
207
207
  // A function to check if two ranges intersect
@@ -1,8 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.writeRowsToStream = writeRowsToStream;
4
- const column_index_to_letter_js_1 = require("./column-index-to-letter.js");
5
- const escape_xml_js_1 = require("./escape-xml.js");
4
+ const prepare_row_to_cells_js_1 = require("./prepare-row-to-cells.js");
6
5
  /**
7
6
  * Writes an async iterable of rows to an Excel XML file.
8
7
  *
@@ -14,29 +13,74 @@ const escape_xml_js_1 = require("./escape-xml.js");
14
13
  * for the first row written to the file. Subsequent rows are written
15
14
  * with incrementing row numbers.
16
15
  *
17
- * @param output - A file write stream to write the Excel XML to.
18
- * @param rows - An async iterable of rows, where each row is an array
19
- * of values.
20
- * @param startRowNumber - The starting row number to use for the first
21
- * row written to the file.
22
- *
23
- * @returns An object with a single property `rowNumber`, which is the
24
- * last row number written to the file (i.e., the `startRowNumber`
25
- * plus the number of rows written).
16
+ * @param {WritableLike} output - A file write stream to write the Excel XML to.
17
+ * @param {AsyncIterable<unknown[] | unknown[][]>} rows - An async iterable of rows, where each row is an array
18
+ * of values or an array of arrays of values.
19
+ * @param {number} startRowNumber - The starting row number to use for the first
20
+ * row written to the file.
21
+ * @returns {Promise<{
22
+ * dimension: {
23
+ * maxColumn: string;
24
+ * maxRow: number;
25
+ * minColumn: string;
26
+ * minRow: number;
27
+ * };
28
+ * rowNumber: number;
29
+ * }>} An object containing:
30
+ * - dimension: The boundaries of the written data (min/max columns and rows)
31
+ * - rowNumber: The last row number written to the file
26
32
  */
27
33
  async function writeRowsToStream(output, rows, startRowNumber) {
28
34
  let rowNumber = startRowNumber;
35
+ const dimension = {
36
+ maxColumn: "A",
37
+ maxRow: startRowNumber,
38
+ minColumn: "A",
39
+ minRow: startRowNumber,
40
+ };
41
+ // Функция для сравнения колонок (A < B, AA > Z и т.д.)
42
+ const compareColumns = (a, b) => {
43
+ if (a === b)
44
+ return 0;
45
+ return a.length === b.length ? (a < b ? -1 : 1) : (a.length < b.length ? -1 : 1);
46
+ };
47
+ const processRow = (row, currentRowNumber) => {
48
+ const cells = (0, prepare_row_to_cells_js_1.prepareRowToCells)(row, currentRowNumber);
49
+ if (cells.length === 0)
50
+ return;
51
+ output.write(`<row r="${currentRowNumber}">${cells.map(cell => cell.cellXml).join("")}</row>`);
52
+ // Обновление границ
53
+ const firstCellRef = cells[0]?.cellRef;
54
+ const lastCellRef = cells[cells.length - 1]?.cellRef;
55
+ if (firstCellRef) {
56
+ const colLetters = firstCellRef.match(/[A-Z]+/)?.[0] || "";
57
+ if (compareColumns(colLetters, dimension.minColumn) < 0) {
58
+ dimension.minColumn = colLetters;
59
+ }
60
+ }
61
+ if (lastCellRef) {
62
+ const colLetters = lastCellRef.match(/[A-Z]+/)?.[0] || "";
63
+ if (compareColumns(colLetters, dimension.maxColumn) > 0) {
64
+ dimension.maxColumn = colLetters;
65
+ }
66
+ }
67
+ dimension.maxRow = currentRowNumber;
68
+ };
29
69
  for await (const row of rows) {
30
- // Transform the row into XML
31
- const cells = row.map((value, colIndex) => {
32
- const colLetter = (0, column_index_to_letter_js_1.columnIndexToLetter)(colIndex);
33
- const cellRef = `${colLetter}${rowNumber}`;
34
- const cellValue = (0, escape_xml_js_1.escapeXml)(String(value ?? ""));
35
- return `<c r="${cellRef}" t="inlineStr"><is><t>${cellValue}</t></is></c>`;
36
- });
37
- // Write the row to the file
38
- output.write(`<row r="${rowNumber}">${cells.join("")}</row>`);
39
- rowNumber++;
70
+ if (!row.length)
71
+ continue;
72
+ if (Array.isArray(row[0])) {
73
+ for (const subRow of row) {
74
+ if (!subRow.length)
75
+ continue;
76
+ processRow(subRow, rowNumber);
77
+ rowNumber++;
78
+ }
79
+ }
80
+ else {
81
+ processRow(row, rowNumber);
82
+ rowNumber++;
83
+ }
40
84
  }
41
- return { rowNumber };
85
+ return { dimension, rowNumber };
42
86
  }
@@ -1 +1,2 @@
1
1
  export * from "./template-fs.js";
2
+ export * from "./template-memory.js";
@@ -0,0 +1,13 @@
1
+ export class MemoryWriteStream {
2
+ chunks = [];
3
+ write(chunk) {
4
+ this.chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, "utf-8"));
5
+ return true;
6
+ }
7
+ end() {
8
+ // no-op
9
+ }
10
+ toBuffer() {
11
+ return Buffer.concat(this.chunks);
12
+ }
13
+ }