@js-ak/excel-toolbox 1.3.2 → 1.4.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 (48) hide show
  1. package/build/cjs/lib/template/template-fs.js +458 -196
  2. package/build/cjs/lib/template/utils/apply-replacements.js +26 -0
  3. package/build/cjs/lib/template/utils/check-row.js +6 -11
  4. package/build/cjs/lib/template/utils/column-index-to-letter.js +14 -3
  5. package/build/cjs/lib/template/utils/extract-xml-declaration.js +22 -0
  6. package/build/cjs/lib/template/utils/get-by-path.js +18 -0
  7. package/build/cjs/lib/template/utils/get-max-row-number.js +1 -1
  8. package/build/cjs/lib/template/utils/index.js +9 -0
  9. package/build/cjs/lib/template/utils/process-merge-cells.js +40 -0
  10. package/build/cjs/lib/template/utils/process-merge-finalize.js +51 -0
  11. package/build/cjs/lib/template/utils/process-rows.js +160 -0
  12. package/build/cjs/lib/template/utils/process-shared-strings.js +45 -0
  13. package/build/cjs/lib/template/utils/to-excel-column-object.js +2 -10
  14. package/build/cjs/lib/template/utils/update-dimension.js +40 -0
  15. package/build/cjs/lib/template/utils/validate-worksheet-xml.js +231 -0
  16. package/build/cjs/lib/template/utils/write-rows-to-stream.js +2 -1
  17. package/build/cjs/lib/zip/create-with-stream.js +2 -2
  18. package/build/esm/lib/template/template-fs.js +458 -196
  19. package/build/esm/lib/template/utils/apply-replacements.js +22 -0
  20. package/build/esm/lib/template/utils/check-row.js +6 -11
  21. package/build/esm/lib/template/utils/column-index-to-letter.js +14 -3
  22. package/build/esm/lib/template/utils/extract-xml-declaration.js +19 -0
  23. package/build/esm/lib/template/utils/get-by-path.js +15 -0
  24. package/build/esm/lib/template/utils/get-max-row-number.js +1 -1
  25. package/build/esm/lib/template/utils/index.js +9 -0
  26. package/build/esm/lib/template/utils/process-merge-cells.js +37 -0
  27. package/build/esm/lib/template/utils/process-merge-finalize.js +48 -0
  28. package/build/esm/lib/template/utils/process-rows.js +157 -0
  29. package/build/esm/lib/template/utils/process-shared-strings.js +42 -0
  30. package/build/esm/lib/template/utils/to-excel-column-object.js +2 -10
  31. package/build/esm/lib/template/utils/update-dimension.js +37 -0
  32. package/build/esm/lib/template/utils/validate-worksheet-xml.js +228 -0
  33. package/build/esm/lib/template/utils/write-rows-to-stream.js +2 -1
  34. package/build/esm/lib/zip/create-with-stream.js +2 -2
  35. package/build/types/lib/template/template-fs.d.ts +24 -0
  36. package/build/types/lib/template/utils/apply-replacements.d.ts +13 -0
  37. package/build/types/lib/template/utils/check-row.d.ts +5 -10
  38. package/build/types/lib/template/utils/column-index-to-letter.d.ts +11 -3
  39. package/build/types/lib/template/utils/extract-xml-declaration.d.ts +14 -0
  40. package/build/types/lib/template/utils/get-by-path.d.ts +8 -0
  41. package/build/types/lib/template/utils/index.d.ts +9 -0
  42. package/build/types/lib/template/utils/process-merge-cells.d.ts +20 -0
  43. package/build/types/lib/template/utils/process-merge-finalize.d.ts +38 -0
  44. package/build/types/lib/template/utils/process-rows.d.ts +31 -0
  45. package/build/types/lib/template/utils/process-shared-strings.d.ts +20 -0
  46. package/build/types/lib/template/utils/update-dimension.d.ts +1 -0
  47. package/build/types/lib/template/utils/validate-worksheet-xml.d.ts +25 -0
  48. package/package.json +6 -4
@@ -0,0 +1,231 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.validateWorksheetXml = validateWorksheetXml;
4
+ /**
5
+ * Validates an Excel worksheet XML against the expected structure and rules.
6
+ *
7
+ * Checks the following:
8
+ * 1. XML starts with <?xml declaration
9
+ * 2. Root element is worksheet
10
+ * 3. Required elements are present
11
+ * 4. row numbers are in ascending order
12
+ * 5. No duplicate row numbers
13
+ * 6. No overlapping merge ranges
14
+ * 7. All cells are within the specified dimension
15
+ * 8. All mergeCell tags refer to existing cells
16
+ *
17
+ * @param xml The raw XML content of the worksheet
18
+ * @returns A ValidationResult object indicating if the XML is valid, and an error message if it's not
19
+ */
20
+ function validateWorksheetXml(xml) {
21
+ const createError = (message, details) => ({
22
+ error: { details, message },
23
+ isValid: false,
24
+ });
25
+ // 1. Check for XML declaration
26
+ if (!xml.startsWith("<?xml")) {
27
+ return createError("XML must start with <?xml> declaration");
28
+ }
29
+ if (!xml.includes("<worksheet") || !xml.includes("</worksheet>")) {
30
+ return createError("Root element worksheet not found");
31
+ }
32
+ // 2. Check for required elements
33
+ const requiredElements = [
34
+ { name: "sheetViews", tag: "<sheetViews>" },
35
+ { name: "sheetFormatPr", tag: "<sheetFormatPr" },
36
+ { name: "cols", tag: "<cols>" },
37
+ { name: "sheetData", tag: "<sheetData>" },
38
+ { name: "mergeCells", tag: "<mergeCells" },
39
+ ];
40
+ for (const { name, tag } of requiredElements) {
41
+ if (!xml.includes(tag)) {
42
+ return createError(`Missing required element ${name}`);
43
+ }
44
+ }
45
+ // 3. Extract and validate sheetData
46
+ const sheetDataStart = xml.indexOf("<sheetData>");
47
+ const sheetDataEnd = xml.indexOf("</sheetData>");
48
+ if (sheetDataStart === -1 || sheetDataEnd === -1) {
49
+ return createError("Invalid sheetData structure");
50
+ }
51
+ const sheetDataContent = xml.substring(sheetDataStart + 10, sheetDataEnd);
52
+ const rows = sheetDataContent.split("</row>");
53
+ if (rows.length < 2) {
54
+ return createError("SheetData should contain at least one row");
55
+ }
56
+ // Collect information about all rows and cells
57
+ const allRows = [];
58
+ const allCells = [];
59
+ let prevRowNum = 0;
60
+ for (const row of rows.slice(0, -1)) {
61
+ if (!row.includes("<row ")) {
62
+ return createError("Row tag not found", `Fragment: ${row.substring(0, 50)}...`);
63
+ }
64
+ if (!row.includes("<c ")) {
65
+ return createError("Row does not contain any cells", `Row: ${row.substring(0, 50)}...`);
66
+ }
67
+ // Extract row number
68
+ const rowNumMatch = row.match(/<row\s+r="(\d+)"/);
69
+ if (!rowNumMatch) {
70
+ return createError("Row number (attribute r) not specified", `Row: ${row.substring(0, 50)}...`);
71
+ }
72
+ const rowNum = parseInt(rowNumMatch[1]);
73
+ // Check for duplicate row numbers
74
+ if (allRows.includes(rowNum)) {
75
+ return createError("Duplicate row number found", `Row number: ${rowNum}`);
76
+ }
77
+ allRows.push(rowNum);
78
+ // Check row number order (should be in ascending order)
79
+ if (rowNum <= prevRowNum) {
80
+ return createError("Row order is broken", `Current row: ${rowNum}, previous: ${prevRowNum}`);
81
+ }
82
+ prevRowNum = rowNum;
83
+ // Extract all cells in the row
84
+ const cells = row.match(/<c\s+r="([A-Z]+)(\d+)"/g) || [];
85
+ for (const cell of cells) {
86
+ const match = cell.match(/<c\s+r="([A-Z]+)(\d+)"/);
87
+ if (!match) {
88
+ return createError("Invalid cell format", `Cell: ${cell}`);
89
+ }
90
+ const col = match[1];
91
+ const cellRowNum = parseInt(match[2]);
92
+ // Check row number match for each cell
93
+ if (cellRowNum !== rowNum) {
94
+ return createError("Row number mismatch in cell", `Expected: ${rowNum}, found: ${cellRowNum} in cell ${col}${cellRowNum}`);
95
+ }
96
+ allCells.push({
97
+ col,
98
+ row: rowNum,
99
+ });
100
+ }
101
+ }
102
+ // 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);
132
+ }
133
+ else {
134
+ mergeRefs.add(ref);
135
+ }
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)}`);
156
+ }
157
+ }
158
+ }
159
+ // 5. Check dimension and match with real data
160
+ const dimensionMatch = xml.match(/<dimension\s+ref="([A-Z]+\d+:[A-Z]+\d+)"\s*\/>/);
161
+ if (!dimensionMatch) {
162
+ return createError("Data range (dimension) is not specified");
163
+ }
164
+ const [startCell, endCell] = dimensionMatch[1].split(":");
165
+ const startCol = startCell.match(/[A-Z]+/)?.[0];
166
+ const startRow = parseInt(startCell.match(/\d+/)?.[0] || "0");
167
+ const endCol = endCell.match(/[A-Z]+/)?.[0];
168
+ const endRow = parseInt(endCell.match(/\d+/)?.[0] || "0");
169
+ if (!startCol || !endCol || isNaN(startRow) || isNaN(endRow)) {
170
+ return createError("Invalid dimension format", `Dimension: ${dimensionMatch[1]}`);
171
+ }
172
+ const startColNum = colToNumber(startCol);
173
+ const endColNum = colToNumber(endCol);
174
+ // Check if all cells are within the dimension
175
+ for (const cell of allCells) {
176
+ const colNum = colToNumber(cell.col);
177
+ if (cell.row < startRow || cell.row > endRow) {
178
+ return createError("Cell is outside the specified area (by row)", `Cell: ${cell.col}${cell.row}, dimension: ${dimensionMatch[1]}`);
179
+ }
180
+ if (colNum < startColNum || colNum > endColNum) {
181
+ return createError("Cell is outside the specified area (by column)", `Cell: ${cell.col}${cell.row}, dimension: ${dimensionMatch[1]}`);
182
+ }
183
+ }
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
+ return { isValid: true };
206
+ }
207
+ // A function to check if two ranges intersect
208
+ function rangesIntersect(a, b) {
209
+ const aStartColNum = colToNumber(a.startCol);
210
+ const aEndColNum = colToNumber(a.endCol);
211
+ const bStartColNum = colToNumber(b.startCol);
212
+ const bEndColNum = colToNumber(b.endCol);
213
+ // Check if the rows intersect
214
+ const rowsIntersect = !(a.endRow < b.startRow || a.startRow > b.endRow);
215
+ // Check if the columns intersect
216
+ const colsIntersect = !(aEndColNum < bStartColNum || aStartColNum > bEndColNum);
217
+ return rowsIntersect && colsIntersect;
218
+ }
219
+ // Function to get the range string1
220
+ function getRangeString(range) {
221
+ return `${range.startCol}${range.startRow}:${range.endCol}${range.endRow}`;
222
+ }
223
+ // Function to convert column letters to numbers
224
+ function colToNumber(col) {
225
+ let num = 0;
226
+ for (let i = 0; i < col.length; i++) {
227
+ num = num * 26 + (col.charCodeAt(i) - 64);
228
+ }
229
+ return num;
230
+ }
231
+ ;
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.writeRowsToStream = writeRowsToStream;
4
4
  const column_index_to_letter_js_1 = require("./column-index-to-letter.js");
5
+ const escape_xml_js_1 = require("./escape-xml.js");
5
6
  /**
6
7
  * Writes an async iterable of rows to an Excel XML file.
7
8
  *
@@ -30,7 +31,7 @@ async function writeRowsToStream(output, rows, startRowNumber) {
30
31
  const cells = row.map((value, colIndex) => {
31
32
  const colLetter = (0, column_index_to_letter_js_1.columnIndexToLetter)(colIndex);
32
33
  const cellRef = `${colLetter}${rowNumber}`;
33
- const cellValue = String(value ?? "");
34
+ const cellValue = (0, escape_xml_js_1.escapeXml)(String(value ?? ""));
34
35
  return `<c r="${cellRef}" t="inlineStr"><is><t>${cellValue}</t></is></c>`;
35
36
  });
36
37
  // Write the row to the file
@@ -91,9 +91,9 @@ async function createWithStream(fileKeys, destination, output) {
91
91
  });
92
92
  const collectCompressed = new node_stream_1.PassThrough();
93
93
  collectCompressed.on("data", chunk => {
94
- // // Count compressed bytes
94
+ // Count compressed bytes
95
95
  compSize += chunk.length;
96
- // // Save compressed chunk
96
+ // Save compressed chunk
97
97
  compressedChunks.push(chunk);
98
98
  });
99
99
  // Run all transforms in pipeline: read -> count size -> CRC -> deflate -> collect compressed