@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.
- package/build/cjs/lib/template/template-fs.js +458 -196
- package/build/cjs/lib/template/utils/apply-replacements.js +26 -0
- package/build/cjs/lib/template/utils/check-row.js +6 -11
- package/build/cjs/lib/template/utils/column-index-to-letter.js +14 -3
- package/build/cjs/lib/template/utils/extract-xml-declaration.js +22 -0
- package/build/cjs/lib/template/utils/get-by-path.js +18 -0
- package/build/cjs/lib/template/utils/get-max-row-number.js +1 -1
- package/build/cjs/lib/template/utils/index.js +9 -0
- package/build/cjs/lib/template/utils/process-merge-cells.js +40 -0
- package/build/cjs/lib/template/utils/process-merge-finalize.js +51 -0
- package/build/cjs/lib/template/utils/process-rows.js +160 -0
- package/build/cjs/lib/template/utils/process-shared-strings.js +45 -0
- package/build/cjs/lib/template/utils/to-excel-column-object.js +2 -10
- package/build/cjs/lib/template/utils/update-dimension.js +40 -0
- package/build/cjs/lib/template/utils/validate-worksheet-xml.js +231 -0
- package/build/cjs/lib/template/utils/write-rows-to-stream.js +2 -1
- package/build/cjs/lib/zip/create-with-stream.js +2 -2
- package/build/esm/lib/template/template-fs.js +458 -196
- package/build/esm/lib/template/utils/apply-replacements.js +22 -0
- package/build/esm/lib/template/utils/check-row.js +6 -11
- package/build/esm/lib/template/utils/column-index-to-letter.js +14 -3
- package/build/esm/lib/template/utils/extract-xml-declaration.js +19 -0
- package/build/esm/lib/template/utils/get-by-path.js +15 -0
- package/build/esm/lib/template/utils/get-max-row-number.js +1 -1
- package/build/esm/lib/template/utils/index.js +9 -0
- package/build/esm/lib/template/utils/process-merge-cells.js +37 -0
- package/build/esm/lib/template/utils/process-merge-finalize.js +48 -0
- package/build/esm/lib/template/utils/process-rows.js +157 -0
- package/build/esm/lib/template/utils/process-shared-strings.js +42 -0
- package/build/esm/lib/template/utils/to-excel-column-object.js +2 -10
- package/build/esm/lib/template/utils/update-dimension.js +37 -0
- package/build/esm/lib/template/utils/validate-worksheet-xml.js +228 -0
- package/build/esm/lib/template/utils/write-rows-to-stream.js +2 -1
- package/build/esm/lib/zip/create-with-stream.js +2 -2
- package/build/types/lib/template/template-fs.d.ts +24 -0
- package/build/types/lib/template/utils/apply-replacements.d.ts +13 -0
- package/build/types/lib/template/utils/check-row.d.ts +5 -10
- package/build/types/lib/template/utils/column-index-to-letter.d.ts +11 -3
- package/build/types/lib/template/utils/extract-xml-declaration.d.ts +14 -0
- package/build/types/lib/template/utils/get-by-path.d.ts +8 -0
- package/build/types/lib/template/utils/index.d.ts +9 -0
- package/build/types/lib/template/utils/process-merge-cells.d.ts +20 -0
- package/build/types/lib/template/utils/process-merge-finalize.d.ts +38 -0
- package/build/types/lib/template/utils/process-rows.d.ts +31 -0
- package/build/types/lib/template/utils/process-shared-strings.d.ts +20 -0
- package/build/types/lib/template/utils/update-dimension.d.ts +1 -0
- package/build/types/lib/template/utils/validate-worksheet-xml.d.ts +25 -0
- 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
|
-
//
|
94
|
+
// Count compressed bytes
|
95
95
|
compSize += chunk.length;
|
96
|
-
//
|
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
|