@js-ak/excel-toolbox 1.4.0 → 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 +4 -223
- package/build/cjs/lib/template/utils/extract-xml-declaration.js +1 -1
- package/build/cjs/lib/template/utils/get-max-row-number.js +1 -1
- package/build/cjs/lib/template/utils/index.js +4 -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/validate-worksheet-xml.js +65 -51
- package/build/cjs/lib/template/utils/write-rows-to-stream.js +2 -1
- package/build/esm/lib/template/template-fs.js +4 -223
- package/build/esm/lib/template/utils/extract-xml-declaration.js +1 -1
- package/build/esm/lib/template/utils/get-max-row-number.js +1 -1
- package/build/esm/lib/template/utils/index.js +4 -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/validate-worksheet-xml.js +65 -51
- package/build/esm/lib/template/utils/write-rows-to-stream.js +2 -1
- package/build/types/lib/template/utils/index.d.ts +4 -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/validate-worksheet-xml.d.ts +16 -0
- package/package.json +1 -1
@@ -1,22 +1,35 @@
|
|
1
1
|
"use strict";
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
3
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
|
+
*/
|
4
20
|
function validateWorksheetXml(xml) {
|
5
21
|
const createError = (message, details) => ({
|
6
|
-
error: {
|
7
|
-
details,
|
8
|
-
message,
|
9
|
-
},
|
22
|
+
error: { details, message },
|
10
23
|
isValid: false,
|
11
24
|
});
|
12
|
-
// 1.
|
25
|
+
// 1. Check for XML declaration
|
13
26
|
if (!xml.startsWith("<?xml")) {
|
14
|
-
return createError("XML
|
27
|
+
return createError("XML must start with <?xml> declaration");
|
15
28
|
}
|
16
29
|
if (!xml.includes("<worksheet") || !xml.includes("</worksheet>")) {
|
17
|
-
return createError("
|
30
|
+
return createError("Root element worksheet not found");
|
18
31
|
}
|
19
|
-
// 2.
|
32
|
+
// 2. Check for required elements
|
20
33
|
const requiredElements = [
|
21
34
|
{ name: "sheetViews", tag: "<sheetViews>" },
|
22
35
|
{ name: "sheetFormatPr", tag: "<sheetFormatPr" },
|
@@ -26,59 +39,59 @@ function validateWorksheetXml(xml) {
|
|
26
39
|
];
|
27
40
|
for (const { name, tag } of requiredElements) {
|
28
41
|
if (!xml.includes(tag)) {
|
29
|
-
return createError(
|
42
|
+
return createError(`Missing required element ${name}`);
|
30
43
|
}
|
31
44
|
}
|
32
|
-
// 3.
|
45
|
+
// 3. Extract and validate sheetData
|
33
46
|
const sheetDataStart = xml.indexOf("<sheetData>");
|
34
47
|
const sheetDataEnd = xml.indexOf("</sheetData>");
|
35
48
|
if (sheetDataStart === -1 || sheetDataEnd === -1) {
|
36
|
-
return createError("
|
49
|
+
return createError("Invalid sheetData structure");
|
37
50
|
}
|
38
51
|
const sheetDataContent = xml.substring(sheetDataStart + 10, sheetDataEnd);
|
39
52
|
const rows = sheetDataContent.split("</row>");
|
40
53
|
if (rows.length < 2) {
|
41
|
-
return createError("SheetData
|
54
|
+
return createError("SheetData should contain at least one row");
|
42
55
|
}
|
43
|
-
//
|
56
|
+
// Collect information about all rows and cells
|
44
57
|
const allRows = [];
|
45
58
|
const allCells = [];
|
46
59
|
let prevRowNum = 0;
|
47
60
|
for (const row of rows.slice(0, -1)) {
|
48
61
|
if (!row.includes("<row ")) {
|
49
|
-
return createError("
|
62
|
+
return createError("Row tag not found", `Fragment: ${row.substring(0, 50)}...`);
|
50
63
|
}
|
51
64
|
if (!row.includes("<c ")) {
|
52
|
-
return createError("
|
65
|
+
return createError("Row does not contain any cells", `Row: ${row.substring(0, 50)}...`);
|
53
66
|
}
|
54
|
-
//
|
67
|
+
// Extract row number
|
55
68
|
const rowNumMatch = row.match(/<row\s+r="(\d+)"/);
|
56
69
|
if (!rowNumMatch) {
|
57
|
-
return createError("
|
70
|
+
return createError("Row number (attribute r) not specified", `Row: ${row.substring(0, 50)}...`);
|
58
71
|
}
|
59
72
|
const rowNum = parseInt(rowNumMatch[1]);
|
60
|
-
//
|
73
|
+
// Check for duplicate row numbers
|
61
74
|
if (allRows.includes(rowNum)) {
|
62
|
-
return createError("
|
75
|
+
return createError("Duplicate row number found", `Row number: ${rowNum}`);
|
63
76
|
}
|
64
77
|
allRows.push(rowNum);
|
65
|
-
//
|
78
|
+
// Check row number order (should be in ascending order)
|
66
79
|
if (rowNum <= prevRowNum) {
|
67
|
-
return createError("
|
80
|
+
return createError("Row order is broken", `Current row: ${rowNum}, previous: ${prevRowNum}`);
|
68
81
|
}
|
69
82
|
prevRowNum = rowNum;
|
70
|
-
//
|
83
|
+
// Extract all cells in the row
|
71
84
|
const cells = row.match(/<c\s+r="([A-Z]+)(\d+)"/g) || [];
|
72
85
|
for (const cell of cells) {
|
73
86
|
const match = cell.match(/<c\s+r="([A-Z]+)(\d+)"/);
|
74
87
|
if (!match) {
|
75
|
-
return createError("
|
88
|
+
return createError("Invalid cell format", `Cell: ${cell}`);
|
76
89
|
}
|
77
90
|
const col = match[1];
|
78
91
|
const cellRowNum = parseInt(match[2]);
|
79
|
-
//
|
92
|
+
// Check row number match for each cell
|
80
93
|
if (cellRowNum !== rowNum) {
|
81
|
-
return createError("
|
94
|
+
return createError("Row number mismatch in cell", `Expected: ${rowNum}, found: ${cellRowNum} in cell ${col}${cellRowNum}`);
|
82
95
|
}
|
83
96
|
allCells.push({
|
84
97
|
col,
|
@@ -86,32 +99,32 @@ function validateWorksheetXml(xml) {
|
|
86
99
|
});
|
87
100
|
}
|
88
101
|
}
|
89
|
-
// 4.
|
102
|
+
// 4. Check mergeCells
|
90
103
|
const mergeCellsStart = xml.indexOf("<mergeCells");
|
91
104
|
const mergeCellsEnd = xml.indexOf("</mergeCells>");
|
92
105
|
if (mergeCellsStart === -1 || mergeCellsEnd === -1) {
|
93
|
-
return createError("
|
106
|
+
return createError("Invalid mergeCells structure");
|
94
107
|
}
|
95
108
|
const mergeCellsContent = xml.substring(mergeCellsStart, mergeCellsEnd);
|
96
109
|
const countMatch = mergeCellsContent.match(/count="(\d+)"/);
|
97
110
|
if (!countMatch) {
|
98
|
-
return createError("
|
111
|
+
return createError("Count attribute not specified for mergeCells");
|
99
112
|
}
|
100
113
|
const mergeCellTags = mergeCellsContent.match(/<mergeCell\s+ref="([A-Z]+\d+:[A-Z]+\d+)"\s*\/>/g);
|
101
114
|
if (!mergeCellTags) {
|
102
|
-
return createError("
|
115
|
+
return createError("No merged cells found");
|
103
116
|
}
|
104
|
-
//
|
117
|
+
// Check if the number of mergeCells matches the count attribute
|
105
118
|
if (mergeCellTags.length !== parseInt(countMatch[1])) {
|
106
|
-
return createError("
|
119
|
+
return createError("Mismatch in the number of merged cells", `Expected: ${countMatch[1]}, found: ${mergeCellTags.length}`);
|
107
120
|
}
|
108
|
-
//
|
121
|
+
// Check for duplicates of mergeCell
|
109
122
|
const mergeRefs = new Set();
|
110
123
|
const duplicates = new Set();
|
111
124
|
for (const mergeTag of mergeCellTags) {
|
112
125
|
const refMatch = mergeTag.match(/ref="([A-Z]+\d+:[A-Z]+\d+)"/);
|
113
126
|
if (!refMatch) {
|
114
|
-
return createError("
|
127
|
+
return createError("Invalid merge cell format", `Tag: ${mergeTag}`);
|
115
128
|
}
|
116
129
|
const ref = refMatch[1];
|
117
130
|
if (mergeRefs.has(ref)) {
|
@@ -122,9 +135,9 @@ function validateWorksheetXml(xml) {
|
|
122
135
|
}
|
123
136
|
}
|
124
137
|
if (duplicates.size > 0) {
|
125
|
-
return createError("
|
138
|
+
return createError("Duplicates of merged cells found", `Duplicates: ${Array.from(duplicates).join(", ")}`);
|
126
139
|
}
|
127
|
-
//
|
140
|
+
// Check for overlapping merge ranges
|
128
141
|
const mergedRanges = Array.from(mergeRefs).map(ref => {
|
129
142
|
const [start, end] = ref.split(":");
|
130
143
|
return {
|
@@ -139,14 +152,14 @@ function validateWorksheetXml(xml) {
|
|
139
152
|
const a = mergedRanges[i];
|
140
153
|
const b = mergedRanges[j];
|
141
154
|
if (rangesIntersect(a, b)) {
|
142
|
-
return createError("
|
155
|
+
return createError("Found intersecting merged cells", `Intersecting: ${getRangeString(a)} and ${getRangeString(b)}`);
|
143
156
|
}
|
144
157
|
}
|
145
158
|
}
|
146
|
-
// 5.
|
159
|
+
// 5. Check dimension and match with real data
|
147
160
|
const dimensionMatch = xml.match(/<dimension\s+ref="([A-Z]+\d+:[A-Z]+\d+)"\s*\/>/);
|
148
161
|
if (!dimensionMatch) {
|
149
|
-
return createError("
|
162
|
+
return createError("Data range (dimension) is not specified");
|
150
163
|
}
|
151
164
|
const [startCell, endCell] = dimensionMatch[1].split(":");
|
152
165
|
const startCol = startCell.match(/[A-Z]+/)?.[0];
|
@@ -154,25 +167,25 @@ function validateWorksheetXml(xml) {
|
|
154
167
|
const endCol = endCell.match(/[A-Z]+/)?.[0];
|
155
168
|
const endRow = parseInt(endCell.match(/\d+/)?.[0] || "0");
|
156
169
|
if (!startCol || !endCol || isNaN(startRow) || isNaN(endRow)) {
|
157
|
-
return createError("
|
170
|
+
return createError("Invalid dimension format", `Dimension: ${dimensionMatch[1]}`);
|
158
171
|
}
|
159
172
|
const startColNum = colToNumber(startCol);
|
160
173
|
const endColNum = colToNumber(endCol);
|
161
|
-
//
|
174
|
+
// Check if all cells are within the dimension
|
162
175
|
for (const cell of allCells) {
|
163
176
|
const colNum = colToNumber(cell.col);
|
164
177
|
if (cell.row < startRow || cell.row > endRow) {
|
165
|
-
return createError("
|
178
|
+
return createError("Cell is outside the specified area (by row)", `Cell: ${cell.col}${cell.row}, dimension: ${dimensionMatch[1]}`);
|
166
179
|
}
|
167
180
|
if (colNum < startColNum || colNum > endColNum) {
|
168
|
-
return createError("
|
181
|
+
return createError("Cell is outside the specified area (by column)", `Cell: ${cell.col}${cell.row}, dimension: ${dimensionMatch[1]}`);
|
169
182
|
}
|
170
183
|
}
|
171
|
-
// 6.
|
184
|
+
// 6. Additional check: all mergeCell tags refer to existing cells
|
172
185
|
for (const mergeTag of mergeCellTags) {
|
173
186
|
const refMatch = mergeTag.match(/ref="([A-Z]+\d+:[A-Z]+\d+)"/);
|
174
187
|
if (!refMatch) {
|
175
|
-
return createError("
|
188
|
+
return createError("Invalid merge cell format", `Tag: ${mergeTag}`);
|
176
189
|
}
|
177
190
|
const [cell1, cell2] = refMatch[1].split(":");
|
178
191
|
const cell1Col = cell1.match(/[A-Z]+/)?.[0];
|
@@ -180,33 +193,34 @@ function validateWorksheetXml(xml) {
|
|
180
193
|
const cell2Col = cell2.match(/[A-Z]+/)?.[0];
|
181
194
|
const cell2Row = parseInt(cell2.match(/\d+/)?.[0] || "0");
|
182
195
|
if (!cell1Col || !cell2Col || isNaN(cell1Row) || isNaN(cell2Row)) {
|
183
|
-
return createError("
|
196
|
+
return createError("Invalid merged cell coordinates", `Merged cells: ${refMatch[1]}`);
|
184
197
|
}
|
185
|
-
//
|
198
|
+
// Check if the merged cells exist
|
186
199
|
const cell1Exists = allCells.some(c => c.row === cell1Row && c.col === cell1Col);
|
187
200
|
const cell2Exists = allCells.some(c => c.row === cell2Row && c.col === cell2Col);
|
188
201
|
if (!cell1Exists || !cell2Exists) {
|
189
|
-
return createError("
|
202
|
+
return createError("Merged cell reference points to non-existent cells", `Merged cells: ${refMatch[1]}, missing: ${!cell1Exists ? `${cell1Col}${cell1Row}` : `${cell2Col}${cell2Row}`}`);
|
190
203
|
}
|
191
204
|
}
|
192
205
|
return { isValid: true };
|
193
206
|
}
|
194
|
-
//
|
207
|
+
// A function to check if two ranges intersect
|
195
208
|
function rangesIntersect(a, b) {
|
196
209
|
const aStartColNum = colToNumber(a.startCol);
|
197
210
|
const aEndColNum = colToNumber(a.endCol);
|
198
211
|
const bStartColNum = colToNumber(b.startCol);
|
199
212
|
const bEndColNum = colToNumber(b.endCol);
|
200
|
-
//
|
213
|
+
// Check if the rows intersect
|
201
214
|
const rowsIntersect = !(a.endRow < b.startRow || a.startRow > b.endRow);
|
202
|
-
//
|
215
|
+
// Check if the columns intersect
|
203
216
|
const colsIntersect = !(aEndColNum < bStartColNum || aStartColNum > bEndColNum);
|
204
217
|
return rowsIntersect && colsIntersect;
|
205
218
|
}
|
219
|
+
// Function to get the range string1
|
206
220
|
function getRangeString(range) {
|
207
221
|
return `${range.startCol}${range.startRow}:${range.endCol}${range.endRow}`;
|
208
222
|
}
|
209
|
-
//
|
223
|
+
// Function to convert column letters to numbers
|
210
224
|
function colToNumber(col) {
|
211
225
|
let num = 0;
|
212
226
|
for (let i = 0; i < col.length; i++) {
|
@@ -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
|
@@ -85,9 +85,9 @@ export class TemplateFs {
|
|
85
85
|
* @experimental This API is experimental and might change in future versions.
|
86
86
|
*/
|
87
87
|
#expandTableRows(sheetXml, sharedStringsXml, replacements) {
|
88
|
-
const { initialMergeCells, mergeCellMatches, modifiedXml, } = processMergeCells(sheetXml);
|
89
|
-
const { sharedIndexMap, sharedStrings, sharedStringsHeader, sheetMergeCells, } = processSharedStrings(sharedStringsXml);
|
90
|
-
const { lastIndex, resultRows, rowShift } = processRows({
|
88
|
+
const { initialMergeCells, mergeCellMatches, modifiedXml, } = Utils.processMergeCells(sheetXml);
|
89
|
+
const { sharedIndexMap, sharedStrings, sharedStringsHeader, sheetMergeCells, } = Utils.processSharedStrings(sharedStringsXml);
|
90
|
+
const { lastIndex, resultRows, rowShift } = Utils.processRows({
|
91
91
|
mergeCellMatches,
|
92
92
|
replacements,
|
93
93
|
sharedIndexMap,
|
@@ -95,7 +95,7 @@ export class TemplateFs {
|
|
95
95
|
sheetMergeCells,
|
96
96
|
sheetXml: modifiedXml,
|
97
97
|
});
|
98
|
-
return
|
98
|
+
return Utils.processMergeFinalize({
|
99
99
|
initialMergeCells,
|
100
100
|
lastIndex,
|
101
101
|
mergeCellMatches,
|
@@ -688,222 +688,3 @@ export class TemplateFs {
|
|
688
688
|
return new TemplateFs(new Set(Object.keys(files)), destination);
|
689
689
|
}
|
690
690
|
}
|
691
|
-
function processMergeCells(sheetXml) {
|
692
|
-
// Regular expression for finding <mergeCells> block
|
693
|
-
const mergeCellsBlockRegex = /<mergeCells[^>]*>[\s\S]*?<\/mergeCells>/;
|
694
|
-
// Find the first <mergeCells> block (if there are multiple, in xlsx usually there is only one)
|
695
|
-
const mergeCellsBlockMatch = sheetXml.match(mergeCellsBlockRegex);
|
696
|
-
const initialMergeCells = [];
|
697
|
-
const mergeCellMatches = [];
|
698
|
-
if (mergeCellsBlockMatch) {
|
699
|
-
const mergeCellsBlock = mergeCellsBlockMatch[0];
|
700
|
-
initialMergeCells.push(mergeCellsBlock);
|
701
|
-
// Extract <mergeCell ref="A1:B2"/> from this block
|
702
|
-
const mergeCellRegex = /<mergeCell ref="([A-Z]+\d+):([A-Z]+\d+)"\/>/g;
|
703
|
-
for (const match of mergeCellsBlock.matchAll(mergeCellRegex)) {
|
704
|
-
mergeCellMatches.push({ from: match[1], to: match[2] });
|
705
|
-
}
|
706
|
-
}
|
707
|
-
// Remove the <mergeCells> block from the XML
|
708
|
-
const modifiedXml = sheetXml.replace(mergeCellsBlockRegex, "");
|
709
|
-
return {
|
710
|
-
initialMergeCells,
|
711
|
-
mergeCellMatches,
|
712
|
-
modifiedXml,
|
713
|
-
};
|
714
|
-
}
|
715
|
-
;
|
716
|
-
function processSharedStrings(sharedStringsXml) {
|
717
|
-
// Final list of merged cells with all changes
|
718
|
-
const sheetMergeCells = [];
|
719
|
-
// Array for storing shared strings
|
720
|
-
const sharedStrings = [];
|
721
|
-
const sharedStringsHeader = Utils.extractXmlDeclaration(sharedStringsXml);
|
722
|
-
// Map for fast lookup of shared string index by content
|
723
|
-
const sharedIndexMap = new Map();
|
724
|
-
// Regular expression for finding <si> elements (shared string items)
|
725
|
-
const siRegex = /<si>([\s\S]*?)<\/si>/g;
|
726
|
-
// Parse sharedStringsXml and fill sharedStrings and sharedIndexMap
|
727
|
-
for (const match of sharedStringsXml.matchAll(siRegex)) {
|
728
|
-
const content = match[1];
|
729
|
-
if (!content)
|
730
|
-
throw new Error("Shared index not found");
|
731
|
-
const fullSi = `<si>${content}</si>`;
|
732
|
-
sharedIndexMap.set(content, sharedStrings.length);
|
733
|
-
sharedStrings.push(fullSi);
|
734
|
-
}
|
735
|
-
return {
|
736
|
-
sharedIndexMap,
|
737
|
-
sharedStrings,
|
738
|
-
sharedStringsHeader,
|
739
|
-
sheetMergeCells,
|
740
|
-
};
|
741
|
-
}
|
742
|
-
;
|
743
|
-
function processRows(data) {
|
744
|
-
const { mergeCellMatches, replacements, sharedIndexMap, sharedStrings, sheetMergeCells, sheetXml, } = data;
|
745
|
-
const TABLE_REGEX = /\$\{table:([a-zA-Z0-9_]+)\.([a-zA-Z0-9_]+)\}/g;
|
746
|
-
// Array for storing resulting XML rows
|
747
|
-
const resultRows = [];
|
748
|
-
// Previous position of processed part of XML
|
749
|
-
let lastIndex = 0;
|
750
|
-
// Shift for row numbers
|
751
|
-
let rowShift = 0;
|
752
|
-
// Regular expression for finding <row> elements
|
753
|
-
const rowRegex = /<row[^>]*?>[\s\S]*?<\/row>/g;
|
754
|
-
// Process each <row> element
|
755
|
-
for (const match of sheetXml.matchAll(rowRegex)) {
|
756
|
-
// Full XML row
|
757
|
-
const fullRow = match[0];
|
758
|
-
// Start position of the row in XML
|
759
|
-
const matchStart = match.index;
|
760
|
-
// End position of the row in XML
|
761
|
-
const matchEnd = matchStart + fullRow.length;
|
762
|
-
// Add the intermediate XML chunk (if any) between the previous and the current row
|
763
|
-
if (lastIndex !== matchStart) {
|
764
|
-
resultRows.push(sheetXml.slice(lastIndex, matchStart));
|
765
|
-
}
|
766
|
-
lastIndex = matchEnd;
|
767
|
-
// Get row number from r attribute
|
768
|
-
const originalRowNumber = parseInt(fullRow.match(/<row[^>]* r="(\d+)"/)?.[1] ?? "1", 10);
|
769
|
-
// Update row number based on rowShift
|
770
|
-
const shiftedRowNumber = originalRowNumber + rowShift;
|
771
|
-
// Find shared string indexes in cells of the current row
|
772
|
-
const sharedValueIndexes = [];
|
773
|
-
// Regular expression for finding a cell
|
774
|
-
const cellRegex = /<c[^>]*?r="([A-Z]+\d+)"[^>]*?>([\s\S]*?)<\/c>/g;
|
775
|
-
for (const cell of fullRow.matchAll(cellRegex)) {
|
776
|
-
const cellTag = cell[0];
|
777
|
-
// Check if the cell is a shared string
|
778
|
-
const isShared = /t="s"/.test(cellTag);
|
779
|
-
const valueMatch = cellTag.match(/<v>(\d+)<\/v>/);
|
780
|
-
if (isShared && valueMatch) {
|
781
|
-
sharedValueIndexes.push(parseInt(valueMatch[1], 10));
|
782
|
-
}
|
783
|
-
}
|
784
|
-
// Get the text content of shared strings by their indexes
|
785
|
-
const sharedTexts = sharedValueIndexes.map(i => sharedStrings[i]?.replace(/<\/?si>/g, "") ?? "");
|
786
|
-
// Find table placeholders in shared strings
|
787
|
-
const tablePlaceholders = sharedTexts.flatMap(e => [...e.matchAll(TABLE_REGEX)]);
|
788
|
-
// If there are no table placeholders, just shift the row
|
789
|
-
if (tablePlaceholders.length === 0) {
|
790
|
-
const updatedRow = fullRow
|
791
|
-
.replace(/(<row[^>]* r=")(\d+)(")/, `$1${shiftedRowNumber}$3`)
|
792
|
-
.replace(/<c r="([A-Z]+)(\d+)"/g, (_, col) => `<c r="${col}${shiftedRowNumber}"`);
|
793
|
-
resultRows.push(updatedRow);
|
794
|
-
// Update mergeCells for regular row with rowShift
|
795
|
-
const calculatedRowNumber = originalRowNumber + rowShift;
|
796
|
-
for (const { from, to } of mergeCellMatches) {
|
797
|
-
const [, fromCol, fromRow] = from.match(/^([A-Z]+)(\d+)$/);
|
798
|
-
const [, toCol] = to.match(/^([A-Z]+)(\d+)$/);
|
799
|
-
if (Number(fromRow) === calculatedRowNumber) {
|
800
|
-
const newFrom = `${fromCol}${shiftedRowNumber}`;
|
801
|
-
const newTo = `${toCol}${shiftedRowNumber}`;
|
802
|
-
sheetMergeCells.push(`<mergeCell ref="${newFrom}:${newTo}"/>`);
|
803
|
-
}
|
804
|
-
}
|
805
|
-
continue;
|
806
|
-
}
|
807
|
-
// Get the table name from the first placeholder
|
808
|
-
const firstMatch = tablePlaceholders[0];
|
809
|
-
const tableName = firstMatch?.[1];
|
810
|
-
if (!tableName)
|
811
|
-
throw new Error("Table name not found");
|
812
|
-
// Get data for replacement from replacements
|
813
|
-
const array = replacements[tableName];
|
814
|
-
if (!array)
|
815
|
-
continue;
|
816
|
-
if (!Array.isArray(array))
|
817
|
-
throw new Error("Table data is not an array");
|
818
|
-
const tableRowStart = shiftedRowNumber;
|
819
|
-
// Find mergeCells to duplicate (mergeCells that start with the current row)
|
820
|
-
const mergeCellsToDuplicate = mergeCellMatches.filter(({ from }) => {
|
821
|
-
const match = from.match(/^([A-Z]+)(\d+)$/);
|
822
|
-
if (!match)
|
823
|
-
return false;
|
824
|
-
// Row number of the merge cell start position is in the second group
|
825
|
-
const rowNumber = match[2];
|
826
|
-
return Number(rowNumber) === tableRowStart;
|
827
|
-
});
|
828
|
-
// Change the current row to multiple rows from the data array
|
829
|
-
for (let i = 0; i < array.length; i++) {
|
830
|
-
const rowData = array[i];
|
831
|
-
let newRow = fullRow;
|
832
|
-
// Replace placeholders in shared strings with real data
|
833
|
-
sharedValueIndexes.forEach((originalIdx, idx) => {
|
834
|
-
const originalText = sharedTexts[idx];
|
835
|
-
if (!originalText)
|
836
|
-
throw new Error("Shared value not found");
|
837
|
-
// Replace placeholders ${tableName.field} with real data from array data
|
838
|
-
const replacedText = originalText.replace(TABLE_REGEX, (_, tbl, field) => tbl === tableName ? String(rowData?.[field] ?? "") : "");
|
839
|
-
// Add new text to shared strings if it doesn't exist
|
840
|
-
let newIndex;
|
841
|
-
if (sharedIndexMap.has(replacedText)) {
|
842
|
-
newIndex = sharedIndexMap.get(replacedText);
|
843
|
-
}
|
844
|
-
else {
|
845
|
-
newIndex = sharedStrings.length;
|
846
|
-
sharedIndexMap.set(replacedText, newIndex);
|
847
|
-
sharedStrings.push(`<si>${replacedText}</si>`);
|
848
|
-
}
|
849
|
-
// Replace the shared string index in the cell
|
850
|
-
newRow = newRow.replace(`<v>${originalIdx}</v>`, `<v>${newIndex}</v>`);
|
851
|
-
});
|
852
|
-
// Update row number and cell references
|
853
|
-
const newRowNum = shiftedRowNumber + i;
|
854
|
-
newRow = newRow
|
855
|
-
.replace(/<row[^>]* r="\d+"/, rowTag => rowTag.replace(/r="\d+"/, `r="${newRowNum}"`))
|
856
|
-
.replace(/<c r="([A-Z]+)\d+"/g, (_, col) => `<c r="${col}${newRowNum}"`);
|
857
|
-
resultRows.push(newRow);
|
858
|
-
// Add duplicate mergeCells for new rows
|
859
|
-
for (const { from, to } of mergeCellsToDuplicate) {
|
860
|
-
const [, colFrom, rowFrom] = from.match(/^([A-Z]+)(\d+)$/);
|
861
|
-
const [, colTo, rowTo] = to.match(/^([A-Z]+)(\d+)$/);
|
862
|
-
const newFrom = `${colFrom}${Number(rowFrom) + i}`;
|
863
|
-
const newTo = `${colTo}${Number(rowTo) + i}`;
|
864
|
-
sheetMergeCells.push(`<mergeCell ref="${newFrom}:${newTo}"/>`);
|
865
|
-
}
|
866
|
-
}
|
867
|
-
// It increases the row shift by the number of added rows minus one replaced
|
868
|
-
rowShift += array.length - 1;
|
869
|
-
const delta = array.length - 1;
|
870
|
-
const calculatedRowNumber = originalRowNumber + rowShift - array.length + 1;
|
871
|
-
if (delta > 0) {
|
872
|
-
for (const merge of mergeCellMatches) {
|
873
|
-
const fromRow = parseInt(merge.from.match(/\d+$/)[0], 10);
|
874
|
-
if (fromRow > calculatedRowNumber) {
|
875
|
-
merge.from = merge.from.replace(/\d+$/, r => `${parseInt(r) + delta}`);
|
876
|
-
merge.to = merge.to.replace(/\d+$/, r => `${parseInt(r) + delta}`);
|
877
|
-
}
|
878
|
-
}
|
879
|
-
}
|
880
|
-
}
|
881
|
-
return { lastIndex, resultRows, rowShift };
|
882
|
-
}
|
883
|
-
;
|
884
|
-
function processBuild(data) {
|
885
|
-
const { initialMergeCells, lastIndex, mergeCellMatches, resultRows, rowShift, sharedStrings, sharedStringsHeader, sheetMergeCells, sheetXml, } = data;
|
886
|
-
for (const { from, to } of mergeCellMatches) {
|
887
|
-
const [, fromCol, fromRow] = from.match(/^([A-Z]+)(\d+)$/);
|
888
|
-
const [, toCol, toRow] = to.match(/^([A-Z]+)(\d+)$/);
|
889
|
-
const fromRowNum = Number(fromRow);
|
890
|
-
// These rows have already been processed, don't add duplicates
|
891
|
-
if (fromRowNum <= lastIndex)
|
892
|
-
continue;
|
893
|
-
const newFrom = `${fromCol}${fromRowNum + rowShift}`;
|
894
|
-
const newTo = `${toCol}${Number(toRow) + rowShift}`;
|
895
|
-
sheetMergeCells.push(`<mergeCell ref="${newFrom}:${newTo}"/>`);
|
896
|
-
}
|
897
|
-
resultRows.push(sheetXml.slice(lastIndex));
|
898
|
-
// Form XML for mergeCells if there are any
|
899
|
-
const mergeXml = sheetMergeCells.length
|
900
|
-
? `<mergeCells count="${sheetMergeCells.length}">${sheetMergeCells.join("")}</mergeCells>`
|
901
|
-
: initialMergeCells;
|
902
|
-
// Insert mergeCells before the closing sheetData tag
|
903
|
-
const sheetWithMerge = resultRows.join("").replace(/<\/sheetData>/, `</sheetData>${mergeXml}`);
|
904
|
-
// Return modified sheet XML and shared strings
|
905
|
-
return {
|
906
|
-
shared: `${sharedStringsHeader}\n<sst xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" count="${sharedStrings.length}" uniqueCount="${sharedStrings.length}">${sharedStrings.join("")}</sst>`,
|
907
|
-
sheet: Utils.updateDimension(sheetWithMerge),
|
908
|
-
};
|
909
|
-
}
|
@@ -14,6 +14,6 @@
|
|
14
14
|
export function extractXmlDeclaration(xmlString) {
|
15
15
|
// const declarationRegex = /^<\?xml\s+[^?]+\?>/;
|
16
16
|
const declarationRegex = /^<\?xml\s+version\s*=\s*["'][^"']+["'](\s+(encoding|standalone)\s*=\s*["'][^"']+["'])*\s*\?>/;
|
17
|
-
const match = xmlString.match(declarationRegex);
|
17
|
+
const match = xmlString.trim().match(declarationRegex);
|
18
18
|
return match ? match[0] : null;
|
19
19
|
}
|
@@ -6,7 +6,7 @@
|
|
6
6
|
*/
|
7
7
|
export function getMaxRowNumber(line) {
|
8
8
|
let result = 1;
|
9
|
-
const rowMatches = [...line.matchAll(/<row[^>]+r="(\d+)"[^>]*>/
|
9
|
+
const rowMatches = [...line.matchAll(/<row[^>]+r="(\d+)"[^>]*>/gi)];
|
10
10
|
for (const match of rowMatches) {
|
11
11
|
const rowNum = parseInt(match[1], 10);
|
12
12
|
if (rowNum >= result) {
|
@@ -10,6 +10,10 @@ export * from "./get-max-row-number.js";
|
|
10
10
|
export * from "./get-rows-above.js";
|
11
11
|
export * from "./get-rows-below.js";
|
12
12
|
export * from "./parse-rows.js";
|
13
|
+
export * from "./process-merge-cells.js";
|
14
|
+
export * from "./process-merge-finalize.js";
|
15
|
+
export * from "./process-rows.js";
|
16
|
+
export * from "./process-shared-strings.js";
|
13
17
|
export * from "./to-excel-column-object.js";
|
14
18
|
export * from "./update-dimension.js";
|
15
19
|
export * from "./validate-worksheet-xml.js";
|
@@ -0,0 +1,37 @@
|
|
1
|
+
/**
|
2
|
+
* Processes the sheet XML by extracting the initial <mergeCells> block and
|
3
|
+
* extracting all merge cell references. The function returns an object with
|
4
|
+
* three properties:
|
5
|
+
* - `initialMergeCells`: The initial <mergeCells> block as a string array.
|
6
|
+
* - `mergeCellMatches`: An array of objects with `from` and `to` properties,
|
7
|
+
* representing the merge cell references.
|
8
|
+
* - `modifiedXml`: The modified sheet XML with the <mergeCells> block removed.
|
9
|
+
*
|
10
|
+
* @param sheetXml - The sheet XML string.
|
11
|
+
* @returns An object with the above three properties.
|
12
|
+
*/
|
13
|
+
export function processMergeCells(sheetXml) {
|
14
|
+
// Regular expression for finding <mergeCells> block
|
15
|
+
const mergeCellsBlockRegex = /<mergeCells[^>]*>[\s\S]*?<\/mergeCells>/;
|
16
|
+
// Find the first <mergeCells> block (if there are multiple, in xlsx usually there is only one)
|
17
|
+
const mergeCellsBlockMatch = sheetXml.match(mergeCellsBlockRegex);
|
18
|
+
const initialMergeCells = [];
|
19
|
+
const mergeCellMatches = [];
|
20
|
+
if (mergeCellsBlockMatch) {
|
21
|
+
const mergeCellsBlock = mergeCellsBlockMatch[0];
|
22
|
+
initialMergeCells.push(mergeCellsBlock);
|
23
|
+
// Extract <mergeCell ref="A1:B2"/> from this block
|
24
|
+
const mergeCellRegex = /<mergeCell ref="([A-Z]+\d+):([A-Z]+\d+)"\/>/g;
|
25
|
+
for (const match of mergeCellsBlock.matchAll(mergeCellRegex)) {
|
26
|
+
mergeCellMatches.push({ from: match[1], to: match[2] });
|
27
|
+
}
|
28
|
+
}
|
29
|
+
// Remove the <mergeCells> block from the XML
|
30
|
+
const modifiedXml = sheetXml.replace(mergeCellsBlockRegex, "");
|
31
|
+
return {
|
32
|
+
initialMergeCells,
|
33
|
+
mergeCellMatches,
|
34
|
+
modifiedXml,
|
35
|
+
};
|
36
|
+
}
|
37
|
+
;
|
@@ -0,0 +1,48 @@
|
|
1
|
+
import { updateDimension } from "./update-dimension.js";
|
2
|
+
/**
|
3
|
+
* Finalizes the processing of the merged sheet by updating the merge cells and
|
4
|
+
* inserting them into the sheet XML. It also returns the modified sheet XML and
|
5
|
+
* shared strings.
|
6
|
+
*
|
7
|
+
* @param {object} data - An object containing the following properties:
|
8
|
+
* - `initialMergeCells`: The initial merge cells from the original sheet.
|
9
|
+
* - `lastIndex`: The last processed position in the sheet XML.
|
10
|
+
* - `mergeCellMatches`: An array of objects with `from` and `to` properties,
|
11
|
+
* describing the merge cells.
|
12
|
+
* - `resultRows`: An array of processed XML rows.
|
13
|
+
* - `rowShift`: The total row shift.
|
14
|
+
* - `sharedStrings`: An array of shared strings.
|
15
|
+
* - `sharedStringsHeader`: The XML declaration of the shared strings.
|
16
|
+
* - `sheetMergeCells`: An array of merge cell XML strings.
|
17
|
+
* - `sheetXml`: The original sheet XML string.
|
18
|
+
*
|
19
|
+
* @returns An object with two properties:
|
20
|
+
* - `shared`: The modified shared strings XML string.
|
21
|
+
* - `sheet`: The modified sheet XML string with updated merge cells.
|
22
|
+
*/
|
23
|
+
export function processMergeFinalize(data) {
|
24
|
+
const { initialMergeCells, lastIndex, mergeCellMatches, resultRows, rowShift, sharedStrings, sharedStringsHeader, sheetMergeCells, sheetXml, } = data;
|
25
|
+
for (const { from, to } of mergeCellMatches) {
|
26
|
+
const [, fromCol, fromRow] = from.match(/^([A-Z]+)(\d+)$/);
|
27
|
+
const [, toCol, toRow] = to.match(/^([A-Z]+)(\d+)$/);
|
28
|
+
const fromRowNum = Number(fromRow);
|
29
|
+
// These rows have already been processed, don't add duplicates
|
30
|
+
if (fromRowNum <= lastIndex)
|
31
|
+
continue;
|
32
|
+
const newFrom = `${fromCol}${fromRowNum + rowShift}`;
|
33
|
+
const newTo = `${toCol}${Number(toRow) + rowShift}`;
|
34
|
+
sheetMergeCells.push(`<mergeCell ref="${newFrom}:${newTo}"/>`);
|
35
|
+
}
|
36
|
+
resultRows.push(sheetXml.slice(lastIndex));
|
37
|
+
// Form XML for mergeCells if there are any
|
38
|
+
const mergeXml = sheetMergeCells.length
|
39
|
+
? `<mergeCells count="${sheetMergeCells.length}">${sheetMergeCells.join("")}</mergeCells>`
|
40
|
+
: initialMergeCells;
|
41
|
+
// Insert mergeCells before the closing sheetData tag
|
42
|
+
const sheetWithMerge = resultRows.join("").replace(/<\/sheetData>/, `</sheetData>${mergeXml}`);
|
43
|
+
// Return modified sheet XML and shared strings
|
44
|
+
return {
|
45
|
+
shared: `${sharedStringsHeader}\n<sst xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" count="${sharedStrings.length}" uniqueCount="${sharedStrings.length}">${sharedStrings.join("")}</sst>`,
|
46
|
+
sheet: updateDimension(sheetWithMerge),
|
47
|
+
};
|
48
|
+
}
|