@js-ak/excel-toolbox 1.4.0 → 1.5.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/build/cjs/lib/template/index.js +1 -0
- package/build/cjs/lib/template/memory-write-stream.js +17 -0
- package/build/cjs/lib/template/template-fs.js +4 -223
- package/build/cjs/lib/template/template-memory.js +531 -0
- 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/prepare-row-to-cells.js +13 -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 +15 -10
- package/build/esm/lib/template/index.js +1 -0
- package/build/esm/lib/template/memory-write-stream.js +13 -0
- package/build/esm/lib/template/template-fs.js +4 -223
- package/build/esm/lib/template/template-memory.js +494 -0
- 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/prepare-row-to-cells.js +10 -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 +15 -10
- package/build/types/lib/template/index.d.ts +1 -0
- package/build/types/lib/template/memory-write-stream.d.ts +6 -0
- package/build/types/lib/template/template-memory.d.ts +85 -0
- package/build/types/lib/template/utils/index.d.ts +4 -0
- package/build/types/lib/template/utils/prepare-row-to-cells.d.ts +1 -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/build/types/lib/template/utils/write-rows-to-stream.d.ts +6 -2
- package/package.json +5 -3
@@ -0,0 +1,157 @@
|
|
1
|
+
/**
|
2
|
+
* Processes a sheet XML by replacing table placeholders with real data and adjusting row numbers accordingly.
|
3
|
+
*
|
4
|
+
* @param data - An object containing the following properties:
|
5
|
+
* - `replacements`: An object where keys are table names and values are arrays of objects with table data.
|
6
|
+
* - `sharedIndexMap`: A Map of shared string indexes by their text content.
|
7
|
+
* - `mergeCellMatches`: An array of objects with `from` and `to` properties, describing the merge cells.
|
8
|
+
* - `sharedStrings`: An array of shared strings.
|
9
|
+
* - `sheetMergeCells`: An array of merge cell XML strings.
|
10
|
+
* - `sheetXml`: The sheet XML string.
|
11
|
+
*
|
12
|
+
* @returns An object with the following properties:
|
13
|
+
* - `lastIndex`: The last processed position in the sheet XML.
|
14
|
+
* - `resultRows`: An array of processed XML rows.
|
15
|
+
* - `rowShift`: The total row shift.
|
16
|
+
*/
|
17
|
+
export function processRows(data) {
|
18
|
+
const { mergeCellMatches, replacements, sharedIndexMap, sharedStrings, sheetMergeCells, sheetXml, } = data;
|
19
|
+
const TABLE_REGEX = /\$\{table:([a-zA-Z0-9_]+)\.([a-zA-Z0-9_]+)\}/g;
|
20
|
+
// Array for storing resulting XML rows
|
21
|
+
const resultRows = [];
|
22
|
+
// Previous position of processed part of XML
|
23
|
+
let lastIndex = 0;
|
24
|
+
// Shift for row numbers
|
25
|
+
let rowShift = 0;
|
26
|
+
// Regular expression for finding <row> elements
|
27
|
+
const rowRegex = /<row[^>]*?>[\s\S]*?<\/row>/g;
|
28
|
+
// Process each <row> element
|
29
|
+
for (const match of sheetXml.matchAll(rowRegex)) {
|
30
|
+
// Full XML row
|
31
|
+
const fullRow = match[0];
|
32
|
+
// Start position of the row in XML
|
33
|
+
const matchStart = match.index;
|
34
|
+
// End position of the row in XML
|
35
|
+
const matchEnd = matchStart + fullRow.length;
|
36
|
+
// Add the intermediate XML chunk (if any) between the previous and the current row
|
37
|
+
if (lastIndex !== matchStart) {
|
38
|
+
resultRows.push(sheetXml.slice(lastIndex, matchStart));
|
39
|
+
}
|
40
|
+
lastIndex = matchEnd;
|
41
|
+
// Get row number from r attribute
|
42
|
+
const originalRowNumber = parseInt(fullRow.match(/<row[^>]* r="(\d+)"/)?.[1] ?? "1", 10);
|
43
|
+
// Update row number based on rowShift
|
44
|
+
const shiftedRowNumber = originalRowNumber + rowShift;
|
45
|
+
// Find shared string indexes in cells of the current row
|
46
|
+
const sharedValueIndexes = [];
|
47
|
+
// Regular expression for finding a cell
|
48
|
+
const cellRegex = /<c[^>]*?r="([A-Z]+\d+)"[^>]*?>([\s\S]*?)<\/c>/g;
|
49
|
+
for (const cell of fullRow.matchAll(cellRegex)) {
|
50
|
+
const cellTag = cell[0];
|
51
|
+
// Check if the cell is a shared string
|
52
|
+
const isShared = /t="s"/.test(cellTag);
|
53
|
+
const valueMatch = cellTag.match(/<v>(\d+)<\/v>/);
|
54
|
+
if (isShared && valueMatch) {
|
55
|
+
sharedValueIndexes.push(parseInt(valueMatch[1], 10));
|
56
|
+
}
|
57
|
+
}
|
58
|
+
// Get the text content of shared strings by their indexes
|
59
|
+
const sharedTexts = sharedValueIndexes.map(i => sharedStrings[i]?.replace(/<\/?si>/g, "") ?? "");
|
60
|
+
// Find table placeholders in shared strings
|
61
|
+
const tablePlaceholders = sharedTexts.flatMap(e => [...e.matchAll(TABLE_REGEX)]);
|
62
|
+
// If there are no table placeholders, just shift the row
|
63
|
+
if (tablePlaceholders.length === 0) {
|
64
|
+
const updatedRow = fullRow
|
65
|
+
.replace(/(<row[^>]* r=")(\d+)(")/, `$1${shiftedRowNumber}$3`)
|
66
|
+
.replace(/<c r="([A-Z]+)(\d+)"/g, (_, col) => `<c r="${col}${shiftedRowNumber}"`);
|
67
|
+
resultRows.push(updatedRow);
|
68
|
+
// Update mergeCells for regular row with rowShift
|
69
|
+
const calculatedRowNumber = originalRowNumber + rowShift;
|
70
|
+
for (const { from, to } of mergeCellMatches) {
|
71
|
+
const [, fromCol, fromRow] = from.match(/^([A-Z]+)(\d+)$/);
|
72
|
+
const [, toCol] = to.match(/^([A-Z]+)(\d+)$/);
|
73
|
+
if (Number(fromRow) === calculatedRowNumber) {
|
74
|
+
const newFrom = `${fromCol}${shiftedRowNumber}`;
|
75
|
+
const newTo = `${toCol}${shiftedRowNumber}`;
|
76
|
+
sheetMergeCells.push(`<mergeCell ref="${newFrom}:${newTo}"/>`);
|
77
|
+
}
|
78
|
+
}
|
79
|
+
continue;
|
80
|
+
}
|
81
|
+
// Get the table name from the first placeholder
|
82
|
+
const firstMatch = tablePlaceholders[0];
|
83
|
+
const tableName = firstMatch?.[1];
|
84
|
+
if (!tableName)
|
85
|
+
throw new Error("Table name not found");
|
86
|
+
// Get data for replacement from replacements
|
87
|
+
const array = replacements[tableName];
|
88
|
+
if (!array)
|
89
|
+
continue;
|
90
|
+
if (!Array.isArray(array))
|
91
|
+
throw new Error("Table data is not an array");
|
92
|
+
const tableRowStart = shiftedRowNumber;
|
93
|
+
// Find mergeCells to duplicate (mergeCells that start with the current row)
|
94
|
+
const mergeCellsToDuplicate = mergeCellMatches.filter(({ from }) => {
|
95
|
+
const match = from.match(/^([A-Z]+)(\d+)$/);
|
96
|
+
if (!match)
|
97
|
+
return false;
|
98
|
+
// Row number of the merge cell start position is in the second group
|
99
|
+
const rowNumber = match[2];
|
100
|
+
return Number(rowNumber) === tableRowStart;
|
101
|
+
});
|
102
|
+
// Change the current row to multiple rows from the data array
|
103
|
+
for (let i = 0; i < array.length; i++) {
|
104
|
+
const rowData = array[i];
|
105
|
+
let newRow = fullRow;
|
106
|
+
// Replace placeholders in shared strings with real data
|
107
|
+
sharedValueIndexes.forEach((originalIdx, idx) => {
|
108
|
+
const originalText = sharedTexts[idx];
|
109
|
+
if (!originalText)
|
110
|
+
throw new Error("Shared value not found");
|
111
|
+
// Replace placeholders ${tableName.field} with real data from array data
|
112
|
+
const replacedText = originalText.replace(TABLE_REGEX, (_, tbl, field) => tbl === tableName ? String(rowData?.[field] ?? "") : "");
|
113
|
+
// Add new text to shared strings if it doesn't exist
|
114
|
+
let newIndex;
|
115
|
+
if (sharedIndexMap.has(replacedText)) {
|
116
|
+
newIndex = sharedIndexMap.get(replacedText);
|
117
|
+
}
|
118
|
+
else {
|
119
|
+
newIndex = sharedStrings.length;
|
120
|
+
sharedIndexMap.set(replacedText, newIndex);
|
121
|
+
sharedStrings.push(`<si>${replacedText}</si>`);
|
122
|
+
}
|
123
|
+
// Replace the shared string index in the cell
|
124
|
+
newRow = newRow.replace(`<v>${originalIdx}</v>`, `<v>${newIndex}</v>`);
|
125
|
+
});
|
126
|
+
// Update row number and cell references
|
127
|
+
const newRowNum = shiftedRowNumber + i;
|
128
|
+
newRow = newRow
|
129
|
+
.replace(/<row[^>]* r="\d+"/, rowTag => rowTag.replace(/r="\d+"/, `r="${newRowNum}"`))
|
130
|
+
.replace(/<c r="([A-Z]+)\d+"/g, (_, col) => `<c r="${col}${newRowNum}"`);
|
131
|
+
resultRows.push(newRow);
|
132
|
+
// Add duplicate mergeCells for new rows
|
133
|
+
for (const { from, to } of mergeCellsToDuplicate) {
|
134
|
+
const [, colFrom, rowFrom] = from.match(/^([A-Z]+)(\d+)$/);
|
135
|
+
const [, colTo, rowTo] = to.match(/^([A-Z]+)(\d+)$/);
|
136
|
+
const newFrom = `${colFrom}${Number(rowFrom) + i}`;
|
137
|
+
const newTo = `${colTo}${Number(rowTo) + i}`;
|
138
|
+
sheetMergeCells.push(`<mergeCell ref="${newFrom}:${newTo}"/>`);
|
139
|
+
}
|
140
|
+
}
|
141
|
+
// It increases the row shift by the number of added rows minus one replaced
|
142
|
+
rowShift += array.length - 1;
|
143
|
+
const delta = array.length - 1;
|
144
|
+
const calculatedRowNumber = originalRowNumber + rowShift - array.length + 1;
|
145
|
+
if (delta > 0) {
|
146
|
+
for (const merge of mergeCellMatches) {
|
147
|
+
const fromRow = parseInt(merge.from.match(/\d+$/)[0], 10);
|
148
|
+
if (fromRow > calculatedRowNumber) {
|
149
|
+
merge.from = merge.from.replace(/\d+$/, r => `${parseInt(r) + delta}`);
|
150
|
+
merge.to = merge.to.replace(/\d+$/, r => `${parseInt(r) + delta}`);
|
151
|
+
}
|
152
|
+
}
|
153
|
+
}
|
154
|
+
}
|
155
|
+
return { lastIndex, resultRows, rowShift };
|
156
|
+
}
|
157
|
+
;
|
@@ -0,0 +1,42 @@
|
|
1
|
+
import { extractXmlDeclaration } from "./extract-xml-declaration.js";
|
2
|
+
/**
|
3
|
+
* Processes the shared strings XML by extracting the XML declaration,
|
4
|
+
* extracting individual <si> elements, and storing them in an array.
|
5
|
+
*
|
6
|
+
* The function returns an object with four properties:
|
7
|
+
* - sharedIndexMap: A map of shared string content to their corresponding index
|
8
|
+
* - sharedStrings: An array of shared strings
|
9
|
+
* - sharedStringsHeader: The XML declaration of the shared strings
|
10
|
+
* - sheetMergeCells: An empty array, which is only used for type compatibility
|
11
|
+
* with the return type of processBuild.
|
12
|
+
*
|
13
|
+
* @param sharedStringsXml - The XML string of the shared strings
|
14
|
+
* @returns An object with the four properties above
|
15
|
+
*/
|
16
|
+
export function processSharedStrings(sharedStringsXml) {
|
17
|
+
// Final list of merged cells with all changes
|
18
|
+
const sheetMergeCells = [];
|
19
|
+
// Array for storing shared strings
|
20
|
+
const sharedStrings = [];
|
21
|
+
const sharedStringsHeader = extractXmlDeclaration(sharedStringsXml);
|
22
|
+
// Map for fast lookup of shared string index by content
|
23
|
+
const sharedIndexMap = new Map();
|
24
|
+
// Regular expression for finding <si> elements (shared string items)
|
25
|
+
const siRegex = /<si>([\s\S]*?)<\/si>/g;
|
26
|
+
// Parse sharedStringsXml and fill sharedStrings and sharedIndexMap
|
27
|
+
for (const match of sharedStringsXml.matchAll(siRegex)) {
|
28
|
+
const content = match[1];
|
29
|
+
if (!content)
|
30
|
+
continue;
|
31
|
+
const fullSi = `<si>${content}</si>`;
|
32
|
+
sharedIndexMap.set(content, sharedStrings.length);
|
33
|
+
sharedStrings.push(fullSi);
|
34
|
+
}
|
35
|
+
return {
|
36
|
+
sharedIndexMap,
|
37
|
+
sharedStrings,
|
38
|
+
sharedStringsHeader,
|
39
|
+
sheetMergeCells,
|
40
|
+
};
|
41
|
+
}
|
42
|
+
;
|
@@ -1,3 +1,4 @@
|
|
1
|
+
import { columnIndexToLetter } from "./column-index-to-letter.js";
|
1
2
|
/**
|
2
3
|
* Converts an array of values into a Record<string, string> with Excel column names as keys.
|
3
4
|
*
|
@@ -8,18 +9,9 @@
|
|
8
9
|
* @returns The resulting Record<string, string>
|
9
10
|
*/
|
10
11
|
export function toExcelColumnObject(values) {
|
11
|
-
const toExcelColumn = (index) => {
|
12
|
-
let column = "";
|
13
|
-
let i = index;
|
14
|
-
while (i >= 0) {
|
15
|
-
column = String.fromCharCode((i % 26) + 65) + column;
|
16
|
-
i = Math.floor(i / 26) - 1;
|
17
|
-
}
|
18
|
-
return column;
|
19
|
-
};
|
20
12
|
const result = {};
|
21
13
|
for (let i = 0; i < values.length; i++) {
|
22
|
-
const key =
|
14
|
+
const key = columnIndexToLetter(i);
|
23
15
|
result[key] = String(values[i]);
|
24
16
|
}
|
25
17
|
return result;
|
@@ -1,19 +1,32 @@
|
|
1
|
+
/**
|
2
|
+
* Validates an Excel worksheet XML against the expected structure and rules.
|
3
|
+
*
|
4
|
+
* Checks the following:
|
5
|
+
* 1. XML starts with <?xml declaration
|
6
|
+
* 2. Root element is worksheet
|
7
|
+
* 3. Required elements are present
|
8
|
+
* 4. row numbers are in ascending order
|
9
|
+
* 5. No duplicate row numbers
|
10
|
+
* 6. No overlapping merge ranges
|
11
|
+
* 7. All cells are within the specified dimension
|
12
|
+
* 8. All mergeCell tags refer to existing cells
|
13
|
+
*
|
14
|
+
* @param xml The raw XML content of the worksheet
|
15
|
+
* @returns A ValidationResult object indicating if the XML is valid, and an error message if it's not
|
16
|
+
*/
|
1
17
|
export function validateWorksheetXml(xml) {
|
2
18
|
const createError = (message, details) => ({
|
3
|
-
error: {
|
4
|
-
details,
|
5
|
-
message,
|
6
|
-
},
|
19
|
+
error: { details, message },
|
7
20
|
isValid: false,
|
8
21
|
});
|
9
|
-
// 1.
|
22
|
+
// 1. Check for XML declaration
|
10
23
|
if (!xml.startsWith("<?xml")) {
|
11
|
-
return createError("XML
|
24
|
+
return createError("XML must start with <?xml> declaration");
|
12
25
|
}
|
13
26
|
if (!xml.includes("<worksheet") || !xml.includes("</worksheet>")) {
|
14
|
-
return createError("
|
27
|
+
return createError("Root element worksheet not found");
|
15
28
|
}
|
16
|
-
// 2.
|
29
|
+
// 2. Check for required elements
|
17
30
|
const requiredElements = [
|
18
31
|
{ name: "sheetViews", tag: "<sheetViews>" },
|
19
32
|
{ name: "sheetFormatPr", tag: "<sheetFormatPr" },
|
@@ -23,59 +36,59 @@ export function validateWorksheetXml(xml) {
|
|
23
36
|
];
|
24
37
|
for (const { name, tag } of requiredElements) {
|
25
38
|
if (!xml.includes(tag)) {
|
26
|
-
return createError(
|
39
|
+
return createError(`Missing required element ${name}`);
|
27
40
|
}
|
28
41
|
}
|
29
|
-
// 3.
|
42
|
+
// 3. Extract and validate sheetData
|
30
43
|
const sheetDataStart = xml.indexOf("<sheetData>");
|
31
44
|
const sheetDataEnd = xml.indexOf("</sheetData>");
|
32
45
|
if (sheetDataStart === -1 || sheetDataEnd === -1) {
|
33
|
-
return createError("
|
46
|
+
return createError("Invalid sheetData structure");
|
34
47
|
}
|
35
48
|
const sheetDataContent = xml.substring(sheetDataStart + 10, sheetDataEnd);
|
36
49
|
const rows = sheetDataContent.split("</row>");
|
37
50
|
if (rows.length < 2) {
|
38
|
-
return createError("SheetData
|
51
|
+
return createError("SheetData should contain at least one row");
|
39
52
|
}
|
40
|
-
//
|
53
|
+
// Collect information about all rows and cells
|
41
54
|
const allRows = [];
|
42
55
|
const allCells = [];
|
43
56
|
let prevRowNum = 0;
|
44
57
|
for (const row of rows.slice(0, -1)) {
|
45
58
|
if (!row.includes("<row ")) {
|
46
|
-
return createError("
|
59
|
+
return createError("Row tag not found", `Fragment: ${row.substring(0, 50)}...`);
|
47
60
|
}
|
48
61
|
if (!row.includes("<c ")) {
|
49
|
-
return createError("
|
62
|
+
return createError("Row does not contain any cells", `Row: ${row.substring(0, 50)}...`);
|
50
63
|
}
|
51
|
-
//
|
64
|
+
// Extract row number
|
52
65
|
const rowNumMatch = row.match(/<row\s+r="(\d+)"/);
|
53
66
|
if (!rowNumMatch) {
|
54
|
-
return createError("
|
67
|
+
return createError("Row number (attribute r) not specified", `Row: ${row.substring(0, 50)}...`);
|
55
68
|
}
|
56
69
|
const rowNum = parseInt(rowNumMatch[1]);
|
57
|
-
//
|
70
|
+
// Check for duplicate row numbers
|
58
71
|
if (allRows.includes(rowNum)) {
|
59
|
-
return createError("
|
72
|
+
return createError("Duplicate row number found", `Row number: ${rowNum}`);
|
60
73
|
}
|
61
74
|
allRows.push(rowNum);
|
62
|
-
//
|
75
|
+
// Check row number order (should be in ascending order)
|
63
76
|
if (rowNum <= prevRowNum) {
|
64
|
-
return createError("
|
77
|
+
return createError("Row order is broken", `Current row: ${rowNum}, previous: ${prevRowNum}`);
|
65
78
|
}
|
66
79
|
prevRowNum = rowNum;
|
67
|
-
//
|
80
|
+
// Extract all cells in the row
|
68
81
|
const cells = row.match(/<c\s+r="([A-Z]+)(\d+)"/g) || [];
|
69
82
|
for (const cell of cells) {
|
70
83
|
const match = cell.match(/<c\s+r="([A-Z]+)(\d+)"/);
|
71
84
|
if (!match) {
|
72
|
-
return createError("
|
85
|
+
return createError("Invalid cell format", `Cell: ${cell}`);
|
73
86
|
}
|
74
87
|
const col = match[1];
|
75
88
|
const cellRowNum = parseInt(match[2]);
|
76
|
-
//
|
89
|
+
// Check row number match for each cell
|
77
90
|
if (cellRowNum !== rowNum) {
|
78
|
-
return createError("
|
91
|
+
return createError("Row number mismatch in cell", `Expected: ${rowNum}, found: ${cellRowNum} in cell ${col}${cellRowNum}`);
|
79
92
|
}
|
80
93
|
allCells.push({
|
81
94
|
col,
|
@@ -83,32 +96,32 @@ export function validateWorksheetXml(xml) {
|
|
83
96
|
});
|
84
97
|
}
|
85
98
|
}
|
86
|
-
// 4.
|
99
|
+
// 4. Check mergeCells
|
87
100
|
const mergeCellsStart = xml.indexOf("<mergeCells");
|
88
101
|
const mergeCellsEnd = xml.indexOf("</mergeCells>");
|
89
102
|
if (mergeCellsStart === -1 || mergeCellsEnd === -1) {
|
90
|
-
return createError("
|
103
|
+
return createError("Invalid mergeCells structure");
|
91
104
|
}
|
92
105
|
const mergeCellsContent = xml.substring(mergeCellsStart, mergeCellsEnd);
|
93
106
|
const countMatch = mergeCellsContent.match(/count="(\d+)"/);
|
94
107
|
if (!countMatch) {
|
95
|
-
return createError("
|
108
|
+
return createError("Count attribute not specified for mergeCells");
|
96
109
|
}
|
97
110
|
const mergeCellTags = mergeCellsContent.match(/<mergeCell\s+ref="([A-Z]+\d+:[A-Z]+\d+)"\s*\/>/g);
|
98
111
|
if (!mergeCellTags) {
|
99
|
-
return createError("
|
112
|
+
return createError("No merged cells found");
|
100
113
|
}
|
101
|
-
//
|
114
|
+
// Check if the number of mergeCells matches the count attribute
|
102
115
|
if (mergeCellTags.length !== parseInt(countMatch[1])) {
|
103
|
-
return createError("
|
116
|
+
return createError("Mismatch in the number of merged cells", `Expected: ${countMatch[1]}, found: ${mergeCellTags.length}`);
|
104
117
|
}
|
105
|
-
//
|
118
|
+
// Check for duplicates of mergeCell
|
106
119
|
const mergeRefs = new Set();
|
107
120
|
const duplicates = new Set();
|
108
121
|
for (const mergeTag of mergeCellTags) {
|
109
122
|
const refMatch = mergeTag.match(/ref="([A-Z]+\d+:[A-Z]+\d+)"/);
|
110
123
|
if (!refMatch) {
|
111
|
-
return createError("
|
124
|
+
return createError("Invalid merge cell format", `Tag: ${mergeTag}`);
|
112
125
|
}
|
113
126
|
const ref = refMatch[1];
|
114
127
|
if (mergeRefs.has(ref)) {
|
@@ -119,9 +132,9 @@ export function validateWorksheetXml(xml) {
|
|
119
132
|
}
|
120
133
|
}
|
121
134
|
if (duplicates.size > 0) {
|
122
|
-
return createError("
|
135
|
+
return createError("Duplicates of merged cells found", `Duplicates: ${Array.from(duplicates).join(", ")}`);
|
123
136
|
}
|
124
|
-
//
|
137
|
+
// Check for overlapping merge ranges
|
125
138
|
const mergedRanges = Array.from(mergeRefs).map(ref => {
|
126
139
|
const [start, end] = ref.split(":");
|
127
140
|
return {
|
@@ -136,14 +149,14 @@ export function validateWorksheetXml(xml) {
|
|
136
149
|
const a = mergedRanges[i];
|
137
150
|
const b = mergedRanges[j];
|
138
151
|
if (rangesIntersect(a, b)) {
|
139
|
-
return createError("
|
152
|
+
return createError("Found intersecting merged cells", `Intersecting: ${getRangeString(a)} and ${getRangeString(b)}`);
|
140
153
|
}
|
141
154
|
}
|
142
155
|
}
|
143
|
-
// 5.
|
156
|
+
// 5. Check dimension and match with real data
|
144
157
|
const dimensionMatch = xml.match(/<dimension\s+ref="([A-Z]+\d+:[A-Z]+\d+)"\s*\/>/);
|
145
158
|
if (!dimensionMatch) {
|
146
|
-
return createError("
|
159
|
+
return createError("Data range (dimension) is not specified");
|
147
160
|
}
|
148
161
|
const [startCell, endCell] = dimensionMatch[1].split(":");
|
149
162
|
const startCol = startCell.match(/[A-Z]+/)?.[0];
|
@@ -151,25 +164,25 @@ export function validateWorksheetXml(xml) {
|
|
151
164
|
const endCol = endCell.match(/[A-Z]+/)?.[0];
|
152
165
|
const endRow = parseInt(endCell.match(/\d+/)?.[0] || "0");
|
153
166
|
if (!startCol || !endCol || isNaN(startRow) || isNaN(endRow)) {
|
154
|
-
return createError("
|
167
|
+
return createError("Invalid dimension format", `Dimension: ${dimensionMatch[1]}`);
|
155
168
|
}
|
156
169
|
const startColNum = colToNumber(startCol);
|
157
170
|
const endColNum = colToNumber(endCol);
|
158
|
-
//
|
171
|
+
// Check if all cells are within the dimension
|
159
172
|
for (const cell of allCells) {
|
160
173
|
const colNum = colToNumber(cell.col);
|
161
174
|
if (cell.row < startRow || cell.row > endRow) {
|
162
|
-
return createError("
|
175
|
+
return createError("Cell is outside the specified area (by row)", `Cell: ${cell.col}${cell.row}, dimension: ${dimensionMatch[1]}`);
|
163
176
|
}
|
164
177
|
if (colNum < startColNum || colNum > endColNum) {
|
165
|
-
return createError("
|
178
|
+
return createError("Cell is outside the specified area (by column)", `Cell: ${cell.col}${cell.row}, dimension: ${dimensionMatch[1]}`);
|
166
179
|
}
|
167
180
|
}
|
168
|
-
// 6.
|
181
|
+
// 6. Additional check: all mergeCell tags refer to existing cells
|
169
182
|
for (const mergeTag of mergeCellTags) {
|
170
183
|
const refMatch = mergeTag.match(/ref="([A-Z]+\d+:[A-Z]+\d+)"/);
|
171
184
|
if (!refMatch) {
|
172
|
-
return createError("
|
185
|
+
return createError("Invalid merge cell format", `Tag: ${mergeTag}`);
|
173
186
|
}
|
174
187
|
const [cell1, cell2] = refMatch[1].split(":");
|
175
188
|
const cell1Col = cell1.match(/[A-Z]+/)?.[0];
|
@@ -177,33 +190,34 @@ export function validateWorksheetXml(xml) {
|
|
177
190
|
const cell2Col = cell2.match(/[A-Z]+/)?.[0];
|
178
191
|
const cell2Row = parseInt(cell2.match(/\d+/)?.[0] || "0");
|
179
192
|
if (!cell1Col || !cell2Col || isNaN(cell1Row) || isNaN(cell2Row)) {
|
180
|
-
return createError("
|
193
|
+
return createError("Invalid merged cell coordinates", `Merged cells: ${refMatch[1]}`);
|
181
194
|
}
|
182
|
-
//
|
195
|
+
// Check if the merged cells exist
|
183
196
|
const cell1Exists = allCells.some(c => c.row === cell1Row && c.col === cell1Col);
|
184
197
|
const cell2Exists = allCells.some(c => c.row === cell2Row && c.col === cell2Col);
|
185
198
|
if (!cell1Exists || !cell2Exists) {
|
186
|
-
return createError("
|
199
|
+
return createError("Merged cell reference points to non-existent cells", `Merged cells: ${refMatch[1]}, missing: ${!cell1Exists ? `${cell1Col}${cell1Row}` : `${cell2Col}${cell2Row}`}`);
|
187
200
|
}
|
188
201
|
}
|
189
202
|
return { isValid: true };
|
190
203
|
}
|
191
|
-
//
|
204
|
+
// A function to check if two ranges intersect
|
192
205
|
function rangesIntersect(a, b) {
|
193
206
|
const aStartColNum = colToNumber(a.startCol);
|
194
207
|
const aEndColNum = colToNumber(a.endCol);
|
195
208
|
const bStartColNum = colToNumber(b.startCol);
|
196
209
|
const bEndColNum = colToNumber(b.endCol);
|
197
|
-
//
|
210
|
+
// Check if the rows intersect
|
198
211
|
const rowsIntersect = !(a.endRow < b.startRow || a.startRow > b.endRow);
|
199
|
-
//
|
212
|
+
// Check if the columns intersect
|
200
213
|
const colsIntersect = !(aEndColNum < bStartColNum || aStartColNum > bEndColNum);
|
201
214
|
return rowsIntersect && colsIntersect;
|
202
215
|
}
|
216
|
+
// Function to get the range string1
|
203
217
|
function getRangeString(range) {
|
204
218
|
return `${range.startCol}${range.startRow}:${range.endCol}${range.endRow}`;
|
205
219
|
}
|
206
|
-
//
|
220
|
+
// Function to convert column letters to numbers
|
207
221
|
function colToNumber(col) {
|
208
222
|
let num = 0;
|
209
223
|
for (let i = 0; i < col.length; i++) {
|
@@ -1,4 +1,4 @@
|
|
1
|
-
import {
|
1
|
+
import { prepareRowToCells } from "./prepare-row-to-cells.js";
|
2
2
|
/**
|
3
3
|
* Writes an async iterable of rows to an Excel XML file.
|
4
4
|
*
|
@@ -24,15 +24,20 @@ export async function writeRowsToStream(output, rows, startRowNumber) {
|
|
24
24
|
let rowNumber = startRowNumber;
|
25
25
|
for await (const row of rows) {
|
26
26
|
// Transform the row into XML
|
27
|
-
|
28
|
-
const
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
27
|
+
if (Array.isArray(row[0])) {
|
28
|
+
for (const subRow of row) {
|
29
|
+
const cells = prepareRowToCells(subRow, rowNumber);
|
30
|
+
// Write the row to the file
|
31
|
+
output.write(`<row r="${rowNumber}">${cells.join("")}</row>`);
|
32
|
+
rowNumber++;
|
33
|
+
}
|
34
|
+
}
|
35
|
+
else {
|
36
|
+
const cells = prepareRowToCells(row, rowNumber);
|
37
|
+
// Write the row to the file
|
38
|
+
output.write(`<row r="${rowNumber}">${cells.join("")}</row>`);
|
39
|
+
rowNumber++;
|
40
|
+
}
|
36
41
|
}
|
37
42
|
return { rowNumber };
|
38
43
|
}
|
@@ -0,0 +1,85 @@
|
|
1
|
+
/**
|
2
|
+
* A class for manipulating Excel templates by extracting, modifying, and repacking Excel files.
|
3
|
+
*
|
4
|
+
* @experimental This API is experimental and might change in future versions.
|
5
|
+
*/
|
6
|
+
export declare class TemplateMemory {
|
7
|
+
#private;
|
8
|
+
files: Record<string, Buffer>;
|
9
|
+
/**
|
10
|
+
* Flag indicating whether this template instance has been destroyed.
|
11
|
+
* @type {boolean}
|
12
|
+
*/
|
13
|
+
destroyed: boolean;
|
14
|
+
constructor(files: Record<string, Buffer>);
|
15
|
+
/**
|
16
|
+
* Copies a sheet from the template to a new name.
|
17
|
+
*
|
18
|
+
* @param {string} sourceName - The name of the sheet to copy.
|
19
|
+
* @param {string} newName - The new name for the sheet.
|
20
|
+
* @returns {Promise<void>}
|
21
|
+
* @throws {Error} If the sheet with the source name does not exist.
|
22
|
+
* @throws {Error} If a sheet with the new name already exists.
|
23
|
+
* @experimental This API is experimental and might change in future versions.
|
24
|
+
*/
|
25
|
+
copySheet(sourceName: string, newName: string): Promise<void>;
|
26
|
+
/**
|
27
|
+
* Replaces placeholders in the given sheet with values from the replacements map.
|
28
|
+
*
|
29
|
+
* The function searches for placeholders in the format `${key}` within the sheet
|
30
|
+
* content, where `key` corresponds to a path in the replacements object.
|
31
|
+
* If a value is found for the key, it replaces the placeholder with the value.
|
32
|
+
* If no value is found, the original placeholder remains unchanged.
|
33
|
+
*
|
34
|
+
* @param sheetName - The name of the sheet to be replaced.
|
35
|
+
* @param replacements - An object where keys represent placeholder paths and values are the replacements.
|
36
|
+
* @returns A promise that resolves when the substitution is complete.
|
37
|
+
*/
|
38
|
+
substitute(sheetName: string, replacements: Record<string, unknown>): Promise<void>;
|
39
|
+
/**
|
40
|
+
* Inserts rows into a specific sheet in the template.
|
41
|
+
*
|
42
|
+
* @param {Object} data - The data for row insertion.
|
43
|
+
* @param {string} data.sheetName - The name of the sheet to insert rows into.
|
44
|
+
* @param {number} [data.startRowNumber] - The row number to start inserting from.
|
45
|
+
* @param {unknown[][]} data.rows - The rows to insert.
|
46
|
+
* @returns {Promise<void>}
|
47
|
+
* @throws {Error} If the template instance has been destroyed.
|
48
|
+
* @throws {Error} If the sheet does not exist.
|
49
|
+
* @throws {Error} If the row number is out of range.
|
50
|
+
* @throws {Error} If a column is out of range.
|
51
|
+
* @experimental This API is experimental and might change in future versions.
|
52
|
+
*/
|
53
|
+
insertRows(data: {
|
54
|
+
sheetName: string;
|
55
|
+
startRowNumber?: number;
|
56
|
+
rows: unknown[][];
|
57
|
+
}): Promise<void>;
|
58
|
+
insertRowsStream(data: {
|
59
|
+
sheetName: string;
|
60
|
+
startRowNumber?: number;
|
61
|
+
rows: AsyncIterable<unknown[]>;
|
62
|
+
}): Promise<void>;
|
63
|
+
/**
|
64
|
+
* Saves the modified Excel template to a buffer.
|
65
|
+
*
|
66
|
+
* @returns {Promise<Buffer>} The modified Excel template as a buffer.
|
67
|
+
* @throws {Error} If the template instance has been destroyed.
|
68
|
+
* @experimental This API is experimental and might change in future versions.
|
69
|
+
*/
|
70
|
+
save(): Promise<Buffer>;
|
71
|
+
/**
|
72
|
+
* Replaces the contents of a file in the template.
|
73
|
+
*
|
74
|
+
* @param {string} key - The Excel path of the file to replace.
|
75
|
+
* @param {Buffer|string} content - The new content.
|
76
|
+
* @returns {Promise<void>}
|
77
|
+
* @throws {Error} If the template instance has been destroyed.
|
78
|
+
* @throws {Error} If the file does not exist in the template.
|
79
|
+
* @experimental This API is experimental and might change in future versions.
|
80
|
+
*/
|
81
|
+
set(key: string, content: Buffer): Promise<void>;
|
82
|
+
static from(data: {
|
83
|
+
source: string | Buffer;
|
84
|
+
}): Promise<TemplateMemory>;
|
85
|
+
}
|
@@ -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 @@
|
|
1
|
+
export declare function prepareRowToCells(row: unknown[], rowNumber: number): string[];
|