@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
@@ -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. Проверка базовой структуры XML
25
+ // 1. Check for XML declaration
13
26
  if (!xml.startsWith("<?xml")) {
14
- return createError("XML должен начинаться с декларации <?xml>");
27
+ return createError("XML must start with <?xml> declaration");
15
28
  }
16
29
  if (!xml.includes("<worksheet") || !xml.includes("</worksheet>")) {
17
- return createError("Не найден корневой элемент worksheet");
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(`Отсутствует обязательный элемент ${name}`);
42
+ return createError(`Missing required element ${name}`);
30
43
  }
31
44
  }
32
- // 3. Извлечение и проверка sheetData
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("Некорректная структура sheetData");
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("Не найден тег row", `Фрагмент: ${row.substring(0, 50)}...`);
62
+ return createError("Row tag not found", `Fragment: ${row.substring(0, 50)}...`);
50
63
  }
51
64
  if (!row.includes("<c ")) {
52
- return createError("Строка не содержит ячеек", `Строка: ${row.substring(0, 50)}...`);
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("Не указан номер строки (атрибут r)", `Строка: ${row.substring(0, 50)}...`);
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("Найден дубликат номера строки", `Номер строки: ${rowNum}`);
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("Нарушен порядок следования строк", `Текущая строка: ${rowNum}, предыдущая: ${prevRowNum}`);
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("Некорректный формат ячейки", `Ячейка: ${cell}`);
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("Несоответствие номера строки в ячейке", `Ожидалось: ${rowNum}, найдено: ${cellRowNum} в ячейке ${col}${cellRowNum}`);
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. Проверка mergeCells
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("Некорректная структура mergeCells");
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("Не указано количество объединенных ячеек (атрибут count)");
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("Несоответствие количества объединенных ячеек", `Ожидалось: ${countMatch[1]}, найдено: ${mergeCellTags.length}`);
119
+ return createError("Mismatch in the number of merged cells", `Expected: ${countMatch[1]}, found: ${mergeCellTags.length}`);
107
120
  }
108
- // Проверка на дублирующиеся mergeCell
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("Некорректный формат объединения ячеек", `Тег: ${mergeTag}`);
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("Найдены дублирующиеся объединения ячеек", `Дубликаты: ${Array.from(duplicates).join(", ")}`);
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("Найдены пересекающиеся объединения ячеек", `Пересекаются: ${getRangeString(a)} и ${getRangeString(b)}`);
155
+ return createError("Found intersecting merged cells", `Intersecting: ${getRangeString(a)} and ${getRangeString(b)}`);
143
156
  }
144
157
  }
145
158
  }
146
- // 5. Проверка dimension и соответствия с реальными данными
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("Не указана область данных (dimension)");
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("Некорректный формат dimension", `Dimension: ${dimensionMatch[1]}`);
170
+ return createError("Invalid dimension format", `Dimension: ${dimensionMatch[1]}`);
158
171
  }
159
172
  const startColNum = colToNumber(startCol);
160
173
  const endColNum = colToNumber(endCol);
161
- // Проверяем все ячейки на вхождение в dimension
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("Ячейка находится вне указанной области (по строке)", `Ячейка: ${cell.col}${cell.row}, dimension: ${dimensionMatch[1]}`);
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("Ячейка находится вне указанной области (по столбцу)", `Ячейка: ${cell.col}${cell.row}, dimension: ${dimensionMatch[1]}`);
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. Дополнительная проверка: все mergeCell ссылаются на существующие ячейки
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("Некорректный формат объединения ячеек", `Тег: ${mergeTag}`);
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("Некорректные координаты объединения ячеек", `Объединение: ${refMatch[1]}`);
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("Объединение ссылается на несуществующие ячейки", `Объединение: ${refMatch[1]}, отсутствует: ${!cell1Exists ? `${cell1Col}${cell1Row}` : `${cell2Col}${cell2Row}`}`);
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 processBuild({
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+)"[^>]*>/g)];
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
+ }