@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.
Files changed (29) hide show
  1. package/build/cjs/lib/template/template-fs.js +4 -223
  2. package/build/cjs/lib/template/utils/extract-xml-declaration.js +1 -1
  3. package/build/cjs/lib/template/utils/get-max-row-number.js +1 -1
  4. package/build/cjs/lib/template/utils/index.js +4 -0
  5. package/build/cjs/lib/template/utils/process-merge-cells.js +40 -0
  6. package/build/cjs/lib/template/utils/process-merge-finalize.js +51 -0
  7. package/build/cjs/lib/template/utils/process-rows.js +160 -0
  8. package/build/cjs/lib/template/utils/process-shared-strings.js +45 -0
  9. package/build/cjs/lib/template/utils/to-excel-column-object.js +2 -10
  10. package/build/cjs/lib/template/utils/validate-worksheet-xml.js +65 -51
  11. package/build/cjs/lib/template/utils/write-rows-to-stream.js +2 -1
  12. package/build/esm/lib/template/template-fs.js +4 -223
  13. package/build/esm/lib/template/utils/extract-xml-declaration.js +1 -1
  14. package/build/esm/lib/template/utils/get-max-row-number.js +1 -1
  15. package/build/esm/lib/template/utils/index.js +4 -0
  16. package/build/esm/lib/template/utils/process-merge-cells.js +37 -0
  17. package/build/esm/lib/template/utils/process-merge-finalize.js +48 -0
  18. package/build/esm/lib/template/utils/process-rows.js +157 -0
  19. package/build/esm/lib/template/utils/process-shared-strings.js +42 -0
  20. package/build/esm/lib/template/utils/to-excel-column-object.js +2 -10
  21. package/build/esm/lib/template/utils/validate-worksheet-xml.js +65 -51
  22. package/build/esm/lib/template/utils/write-rows-to-stream.js +2 -1
  23. package/build/types/lib/template/utils/index.d.ts +4 -0
  24. package/build/types/lib/template/utils/process-merge-cells.d.ts +20 -0
  25. package/build/types/lib/template/utils/process-merge-finalize.d.ts +38 -0
  26. package/build/types/lib/template/utils/process-rows.d.ts +31 -0
  27. package/build/types/lib/template/utils/process-shared-strings.d.ts +20 -0
  28. package/build/types/lib/template/utils/validate-worksheet-xml.d.ts +16 -0
  29. package/package.json +1 -1
@@ -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 = toExcelColumn(i);
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. Проверка базовой структуры XML
22
+ // 1. Check for XML declaration
10
23
  if (!xml.startsWith("<?xml")) {
11
- return createError("XML должен начинаться с декларации <?xml>");
24
+ return createError("XML must start with <?xml> declaration");
12
25
  }
13
26
  if (!xml.includes("<worksheet") || !xml.includes("</worksheet>")) {
14
- return createError("Не найден корневой элемент worksheet");
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(`Отсутствует обязательный элемент ${name}`);
39
+ return createError(`Missing required element ${name}`);
27
40
  }
28
41
  }
29
- // 3. Извлечение и проверка sheetData
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("Некорректная структура sheetData");
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("Не найден тег row", `Фрагмент: ${row.substring(0, 50)}...`);
59
+ return createError("Row tag not found", `Fragment: ${row.substring(0, 50)}...`);
47
60
  }
48
61
  if (!row.includes("<c ")) {
49
- return createError("Строка не содержит ячеек", `Строка: ${row.substring(0, 50)}...`);
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("Не указан номер строки (атрибут r)", `Строка: ${row.substring(0, 50)}...`);
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("Найден дубликат номера строки", `Номер строки: ${rowNum}`);
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("Нарушен порядок следования строк", `Текущая строка: ${rowNum}, предыдущая: ${prevRowNum}`);
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("Некорректный формат ячейки", `Ячейка: ${cell}`);
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("Несоответствие номера строки в ячейке", `Ожидалось: ${rowNum}, найдено: ${cellRowNum} в ячейке ${col}${cellRowNum}`);
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. Проверка mergeCells
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("Некорректная структура mergeCells");
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("Не указано количество объединенных ячеек (атрибут count)");
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("Несоответствие количества объединенных ячеек", `Ожидалось: ${countMatch[1]}, найдено: ${mergeCellTags.length}`);
116
+ return createError("Mismatch in the number of merged cells", `Expected: ${countMatch[1]}, found: ${mergeCellTags.length}`);
104
117
  }
105
- // Проверка на дублирующиеся mergeCell
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("Некорректный формат объединения ячеек", `Тег: ${mergeTag}`);
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("Найдены дублирующиеся объединения ячеек", `Дубликаты: ${Array.from(duplicates).join(", ")}`);
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("Найдены пересекающиеся объединения ячеек", `Пересекаются: ${getRangeString(a)} и ${getRangeString(b)}`);
152
+ return createError("Found intersecting merged cells", `Intersecting: ${getRangeString(a)} and ${getRangeString(b)}`);
140
153
  }
141
154
  }
142
155
  }
143
- // 5. Проверка dimension и соответствия с реальными данными
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("Не указана область данных (dimension)");
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("Некорректный формат dimension", `Dimension: ${dimensionMatch[1]}`);
167
+ return createError("Invalid dimension format", `Dimension: ${dimensionMatch[1]}`);
155
168
  }
156
169
  const startColNum = colToNumber(startCol);
157
170
  const endColNum = colToNumber(endCol);
158
- // Проверяем все ячейки на вхождение в dimension
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("Ячейка находится вне указанной области (по строке)", `Ячейка: ${cell.col}${cell.row}, dimension: ${dimensionMatch[1]}`);
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("Ячейка находится вне указанной области (по столбцу)", `Ячейка: ${cell.col}${cell.row}, dimension: ${dimensionMatch[1]}`);
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. Дополнительная проверка: все mergeCell ссылаются на существующие ячейки
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("Некорректный формат объединения ячеек", `Тег: ${mergeTag}`);
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("Некорректные координаты объединения ячеек", `Объединение: ${refMatch[1]}`);
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("Объединение ссылается на несуществующие ячейки", `Объединение: ${refMatch[1]}, отсутствует: ${!cell1Exists ? `${cell1Col}${cell1Row}` : `${cell2Col}${cell2Row}`}`);
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,5 @@
1
1
  import { columnIndexToLetter } from "./column-index-to-letter.js";
2
+ import { escapeXml } from "./escape-xml.js";
2
3
  /**
3
4
  * Writes an async iterable of rows to an Excel XML file.
4
5
  *
@@ -27,7 +28,7 @@ export async function writeRowsToStream(output, rows, startRowNumber) {
27
28
  const cells = row.map((value, colIndex) => {
28
29
  const colLetter = columnIndexToLetter(colIndex);
29
30
  const cellRef = `${colLetter}${rowNumber}`;
30
- const cellValue = String(value ?? "");
31
+ const cellValue = escapeXml(String(value ?? ""));
31
32
  return `<c r="${cellRef}" t="inlineStr"><is><t>${cellValue}</t></is></c>`;
32
33
  });
33
34
  // Write the row to the file
@@ -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,20 @@
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 declare function processMergeCells(sheetXml: string): {
14
+ initialMergeCells: string[];
15
+ mergeCellMatches: {
16
+ from: string;
17
+ to: string;
18
+ }[];
19
+ modifiedXml: string;
20
+ };
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Finalizes the processing of the merged sheet by updating the merge cells and
3
+ * inserting them into the sheet XML. It also returns the modified sheet XML and
4
+ * shared strings.
5
+ *
6
+ * @param {object} data - An object containing the following properties:
7
+ * - `initialMergeCells`: The initial merge cells from the original sheet.
8
+ * - `lastIndex`: The last processed position in the sheet XML.
9
+ * - `mergeCellMatches`: An array of objects with `from` and `to` properties,
10
+ * describing the merge cells.
11
+ * - `resultRows`: An array of processed XML rows.
12
+ * - `rowShift`: The total row shift.
13
+ * - `sharedStrings`: An array of shared strings.
14
+ * - `sharedStringsHeader`: The XML declaration of the shared strings.
15
+ * - `sheetMergeCells`: An array of merge cell XML strings.
16
+ * - `sheetXml`: The original sheet XML string.
17
+ *
18
+ * @returns An object with two properties:
19
+ * - `shared`: The modified shared strings XML string.
20
+ * - `sheet`: The modified sheet XML string with updated merge cells.
21
+ */
22
+ export declare function processMergeFinalize(data: {
23
+ initialMergeCells: string[];
24
+ lastIndex: number;
25
+ mergeCellMatches: {
26
+ from: string;
27
+ to: string;
28
+ }[];
29
+ resultRows: string[];
30
+ rowShift: number;
31
+ sharedStrings: string[];
32
+ sharedStringsHeader: string | null;
33
+ sheetMergeCells: string[];
34
+ sheetXml: string;
35
+ }): {
36
+ shared: string;
37
+ sheet: string;
38
+ };
@@ -0,0 +1,31 @@
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 declare function processRows(data: {
18
+ replacements: Record<string, unknown>;
19
+ sharedIndexMap: Map<string, number>;
20
+ mergeCellMatches: {
21
+ from: string;
22
+ to: string;
23
+ }[];
24
+ sharedStrings: string[];
25
+ sheetMergeCells: string[];
26
+ sheetXml: string;
27
+ }): {
28
+ lastIndex: number;
29
+ resultRows: string[];
30
+ rowShift: number;
31
+ };
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Processes the shared strings XML by extracting the XML declaration,
3
+ * extracting individual <si> elements, and storing them in an array.
4
+ *
5
+ * The function returns an object with four properties:
6
+ * - sharedIndexMap: A map of shared string content to their corresponding index
7
+ * - sharedStrings: An array of shared strings
8
+ * - sharedStringsHeader: The XML declaration of the shared strings
9
+ * - sheetMergeCells: An empty array, which is only used for type compatibility
10
+ * with the return type of processBuild.
11
+ *
12
+ * @param sharedStringsXml - The XML string of the shared strings
13
+ * @returns An object with the four properties above
14
+ */
15
+ export declare function processSharedStrings(sharedStringsXml: string): {
16
+ sharedIndexMap: Map<string, number>;
17
+ sharedStrings: string[];
18
+ sharedStringsHeader: string | null;
19
+ sheetMergeCells: string[];
20
+ };