@js-ak/excel-toolbox 1.5.0 → 1.7.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.
Files changed (53) hide show
  1. package/README.md +41 -62
  2. package/build/cjs/lib/merge-sheets-to-base-file-process-sync.js +105 -0
  3. package/build/cjs/lib/merge-sheets-to-base-file-process.js +3 -3
  4. package/build/cjs/lib/merge-sheets-to-base-file-sync.js +2 -2
  5. package/build/cjs/lib/merge-sheets-to-base-file.js +1 -1
  6. package/build/cjs/lib/template/template-fs.js +143 -63
  7. package/build/cjs/lib/template/template-memory.js +281 -59
  8. package/build/cjs/lib/template/utils/index.js +25 -0
  9. package/build/cjs/lib/template/utils/prepare-row-to-cells.js +5 -1
  10. package/build/cjs/lib/template/utils/regexp.js +32 -0
  11. package/build/cjs/lib/template/utils/update-dimension.js +15 -0
  12. package/build/cjs/lib/template/utils/validate-worksheet-xml.js +74 -74
  13. package/build/cjs/lib/template/utils/write-rows-to-stream.js +57 -17
  14. package/build/cjs/lib/xml/extract-rows-from-sheet-sync.js +67 -0
  15. package/build/cjs/lib/xml/extract-rows-from-sheet.js +4 -2
  16. package/build/cjs/lib/xml/extract-xml-from-sheet-sync.js +43 -0
  17. package/build/cjs/lib/xml/extract-xml-from-sheet.js +15 -15
  18. package/build/cjs/lib/xml/index.js +2 -1
  19. package/build/esm/lib/merge-sheets-to-base-file-process-sync.js +69 -0
  20. package/build/esm/lib/merge-sheets-to-base-file-process.js +3 -3
  21. package/build/esm/lib/merge-sheets-to-base-file-sync.js +2 -2
  22. package/build/esm/lib/merge-sheets-to-base-file.js +1 -1
  23. package/build/esm/lib/template/template-fs.js +140 -63
  24. package/build/esm/lib/template/template-memory.js +281 -59
  25. package/build/esm/lib/template/utils/index.js +2 -0
  26. package/build/esm/lib/template/utils/prepare-row-to-cells.js +5 -1
  27. package/build/esm/lib/template/utils/regexp.js +28 -0
  28. package/build/esm/lib/template/utils/update-dimension.js +15 -0
  29. package/build/esm/lib/template/utils/validate-worksheet-xml.js +74 -74
  30. package/build/esm/lib/template/utils/write-rows-to-stream.js +57 -17
  31. package/build/esm/lib/xml/extract-rows-from-sheet-sync.js +64 -0
  32. package/build/esm/lib/xml/extract-rows-from-sheet.js +4 -2
  33. package/build/esm/lib/xml/extract-xml-from-sheet-sync.js +40 -0
  34. package/build/esm/lib/xml/extract-xml-from-sheet.js +12 -15
  35. package/build/esm/lib/xml/index.js +2 -1
  36. package/build/types/lib/merge-sheets-to-base-file-process-sync.d.ts +27 -0
  37. package/build/types/lib/merge-sheets-to-base-file-process.d.ts +1 -1
  38. package/build/types/lib/template/template-fs.d.ts +2 -0
  39. package/build/types/lib/template/template-memory.d.ts +61 -0
  40. package/build/types/lib/template/utils/index.d.ts +2 -0
  41. package/build/types/lib/template/utils/prepare-row-to-cells.d.ts +5 -1
  42. package/build/types/lib/template/utils/regexp.d.ts +24 -0
  43. package/build/types/lib/template/utils/update-dimension.d.ts +15 -0
  44. package/build/types/lib/template/utils/write-rows-to-stream.d.ts +22 -9
  45. package/build/types/lib/xml/extract-rows-from-sheet-sync.d.ts +28 -0
  46. package/build/types/lib/xml/extract-rows-from-sheet.d.ts +2 -2
  47. package/build/types/lib/xml/extract-xml-from-sheet-sync.d.ts +14 -0
  48. package/build/types/lib/xml/extract-xml-from-sheet.d.ts +2 -2
  49. package/build/types/lib/xml/index.d.ts +2 -1
  50. package/package.json +1 -5
  51. package/build/cjs/lib/xml/extract-xml-from-system-content.js +0 -53
  52. package/build/esm/lib/xml/extract-xml-from-system-content.js +0 -49
  53. package/build/types/lib/xml/extract-xml-from-system-content.d.ts +0 -15
@@ -30,9 +30,7 @@ export function validateWorksheetXml(xml) {
30
30
  const requiredElements = [
31
31
  { name: "sheetViews", tag: "<sheetViews>" },
32
32
  { name: "sheetFormatPr", tag: "<sheetFormatPr" },
33
- { name: "cols", tag: "<cols>" },
34
33
  { name: "sheetData", tag: "<sheetData>" },
35
- { name: "mergeCells", tag: "<mergeCells" },
36
34
  ];
37
35
  for (const { name, tag } of requiredElements) {
38
36
  if (!xml.includes(tag)) {
@@ -97,59 +95,82 @@ export function validateWorksheetXml(xml) {
97
95
  }
98
96
  }
99
97
  // 4. Check mergeCells
100
- const mergeCellsStart = xml.indexOf("<mergeCells");
101
- const mergeCellsEnd = xml.indexOf("</mergeCells>");
102
- if (mergeCellsStart === -1 || mergeCellsEnd === -1) {
103
- return createError("Invalid mergeCells structure");
104
- }
105
- const mergeCellsContent = xml.substring(mergeCellsStart, mergeCellsEnd);
106
- const countMatch = mergeCellsContent.match(/count="(\d+)"/);
107
- if (!countMatch) {
108
- return createError("Count attribute not specified for mergeCells");
109
- }
110
- const mergeCellTags = mergeCellsContent.match(/<mergeCell\s+ref="([A-Z]+\d+:[A-Z]+\d+)"\s*\/>/g);
111
- if (!mergeCellTags) {
112
- return createError("No merged cells found");
113
- }
114
- // Check if the number of mergeCells matches the count attribute
115
- if (mergeCellTags.length !== parseInt(countMatch[1])) {
116
- return createError("Mismatch in the number of merged cells", `Expected: ${countMatch[1]}, found: ${mergeCellTags.length}`);
117
- }
118
- // Check for duplicates of mergeCell
119
- const mergeRefs = new Set();
120
- const duplicates = new Set();
121
- for (const mergeTag of mergeCellTags) {
122
- const refMatch = mergeTag.match(/ref="([A-Z]+\d+:[A-Z]+\d+)"/);
123
- if (!refMatch) {
124
- return createError("Invalid merge cell format", `Tag: ${mergeTag}`);
125
- }
126
- const ref = refMatch[1];
127
- if (mergeRefs.has(ref)) {
128
- duplicates.add(ref);
98
+ if (xml.includes("<mergeCells")) {
99
+ const mergeCellsStart = xml.indexOf("<mergeCells");
100
+ const mergeCellsEnd = xml.indexOf("</mergeCells>");
101
+ if (mergeCellsStart === -1 || mergeCellsEnd === -1) {
102
+ return createError("Invalid mergeCells structure");
103
+ }
104
+ const mergeCellsContent = xml.substring(mergeCellsStart, mergeCellsEnd);
105
+ const countMatch = mergeCellsContent.match(/count="(\d+)"/);
106
+ if (!countMatch) {
107
+ return createError("Count attribute not specified for mergeCells");
108
+ }
109
+ const mergeCellTags = mergeCellsContent.match(/<mergeCell\s+ref="([A-Z]+\d+:[A-Z]+\d+)"\s*\/>/g);
110
+ if (!mergeCellTags) {
111
+ return createError("No merged cells found");
112
+ }
113
+ // Check if the number of mergeCells matches the count attribute
114
+ if (mergeCellTags.length !== parseInt(countMatch[1])) {
115
+ return createError("Mismatch in the number of merged cells", `Expected: ${countMatch[1]}, found: ${mergeCellTags.length}`);
116
+ }
117
+ // Check for duplicates of mergeCell
118
+ const mergeRefs = new Set();
119
+ const duplicates = new Set();
120
+ for (const mergeTag of mergeCellTags) {
121
+ const refMatch = mergeTag.match(/ref="([A-Z]+\d+:[A-Z]+\d+)"/);
122
+ if (!refMatch) {
123
+ return createError("Invalid merge cell format", `Tag: ${mergeTag}`);
124
+ }
125
+ const ref = refMatch[1];
126
+ if (mergeRefs.has(ref)) {
127
+ duplicates.add(ref);
128
+ }
129
+ else {
130
+ mergeRefs.add(ref);
131
+ }
129
132
  }
130
- else {
131
- mergeRefs.add(ref);
133
+ if (duplicates.size > 0) {
134
+ return createError("Duplicates of merged cells found", `Duplicates: ${Array.from(duplicates).join(", ")}`);
135
+ }
136
+ // Check for overlapping merge ranges
137
+ const mergedRanges = Array.from(mergeRefs).map(ref => {
138
+ const [start, end] = ref.split(":");
139
+ return {
140
+ endCol: end.match(/[A-Z]+/)?.[0] || "",
141
+ endRow: parseInt(end.match(/\d+/)?.[0] || "0"),
142
+ startCol: start.match(/[A-Z]+/)?.[0] || "",
143
+ startRow: parseInt(start.match(/\d+/)?.[0] || "0"),
144
+ };
145
+ });
146
+ for (let i = 0; i < mergedRanges.length; i++) {
147
+ for (let j = i + 1; j < mergedRanges.length; j++) {
148
+ const a = mergedRanges[i];
149
+ const b = mergedRanges[j];
150
+ if (rangesIntersect(a, b)) {
151
+ return createError("Found intersecting merged cells", `Intersecting: ${getRangeString(a)} and ${getRangeString(b)}`);
152
+ }
153
+ }
132
154
  }
133
- }
134
- if (duplicates.size > 0) {
135
- return createError("Duplicates of merged cells found", `Duplicates: ${Array.from(duplicates).join(", ")}`);
136
- }
137
- // Check for overlapping merge ranges
138
- const mergedRanges = Array.from(mergeRefs).map(ref => {
139
- const [start, end] = ref.split(":");
140
- return {
141
- endCol: end.match(/[A-Z]+/)?.[0] || "",
142
- endRow: parseInt(end.match(/\d+/)?.[0] || "0"),
143
- startCol: start.match(/[A-Z]+/)?.[0] || "",
144
- startRow: parseInt(start.match(/\d+/)?.[0] || "0"),
145
- };
146
- });
147
- for (let i = 0; i < mergedRanges.length; i++) {
148
- for (let j = i + 1; j < mergedRanges.length; j++) {
149
- const a = mergedRanges[i];
150
- const b = mergedRanges[j];
151
- if (rangesIntersect(a, b)) {
152
- return createError("Found intersecting merged cells", `Intersecting: ${getRangeString(a)} and ${getRangeString(b)}`);
155
+ // 6. Additional check: all mergeCell tags refer to existing cells
156
+ for (const mergeTag of mergeCellTags) {
157
+ const refMatch = mergeTag.match(/ref="([A-Z]+\d+:[A-Z]+\d+)"/);
158
+ if (!refMatch) {
159
+ return createError("Invalid merge cell format", `Tag: ${mergeTag}`);
160
+ }
161
+ const [cell1, cell2] = refMatch[1].split(":");
162
+ const cell1Col = cell1.match(/[A-Z]+/)?.[0];
163
+ const cell1Row = parseInt(cell1.match(/\d+/)?.[0] || "0");
164
+ const cell2Col = cell2.match(/[A-Z]+/)?.[0];
165
+ const cell2Row = parseInt(cell2.match(/\d+/)?.[0] || "0");
166
+ if (!cell1Col || !cell2Col || isNaN(cell1Row) || isNaN(cell2Row)) {
167
+ return createError("Invalid merged cell coordinates", `Merged cells: ${refMatch[1]}`);
168
+ }
169
+ // Check if the merged cells exist
170
+ const cell1Exists = allCells.some(c => c.row === cell1Row && c.col === cell1Col);
171
+ const cell2Exists = allCells.some(c => c.row === cell2Row && c.col === cell2Col);
172
+ if (!cell1Exists || !cell2Exists) {
173
+ return createError("Merged cell reference points to non-existent cells", `Merged cells: ${refMatch[1]}, missing: ${!cell1Exists ? `${cell1Col}${cell1Row}` : `${cell2Col}${cell2Row}`}`);
153
174
  }
154
175
  }
155
176
  }
@@ -178,27 +199,6 @@ export function validateWorksheetXml(xml) {
178
199
  return createError("Cell is outside the specified area (by column)", `Cell: ${cell.col}${cell.row}, dimension: ${dimensionMatch[1]}`);
179
200
  }
180
201
  }
181
- // 6. Additional check: all mergeCell tags refer to existing cells
182
- for (const mergeTag of mergeCellTags) {
183
- const refMatch = mergeTag.match(/ref="([A-Z]+\d+:[A-Z]+\d+)"/);
184
- if (!refMatch) {
185
- return createError("Invalid merge cell format", `Tag: ${mergeTag}`);
186
- }
187
- const [cell1, cell2] = refMatch[1].split(":");
188
- const cell1Col = cell1.match(/[A-Z]+/)?.[0];
189
- const cell1Row = parseInt(cell1.match(/\d+/)?.[0] || "0");
190
- const cell2Col = cell2.match(/[A-Z]+/)?.[0];
191
- const cell2Row = parseInt(cell2.match(/\d+/)?.[0] || "0");
192
- if (!cell1Col || !cell2Col || isNaN(cell1Row) || isNaN(cell2Row)) {
193
- return createError("Invalid merged cell coordinates", `Merged cells: ${refMatch[1]}`);
194
- }
195
- // Check if the merged cells exist
196
- const cell1Exists = allCells.some(c => c.row === cell1Row && c.col === cell1Col);
197
- const cell2Exists = allCells.some(c => c.row === cell2Row && c.col === cell2Col);
198
- if (!cell1Exists || !cell2Exists) {
199
- return createError("Merged cell reference points to non-existent cells", `Merged cells: ${refMatch[1]}, missing: ${!cell1Exists ? `${cell1Col}${cell1Row}` : `${cell2Col}${cell2Row}`}`);
200
- }
201
- }
202
202
  return { isValid: true };
203
203
  }
204
204
  // A function to check if two ranges intersect
@@ -10,34 +10,74 @@ import { prepareRowToCells } from "./prepare-row-to-cells.js";
10
10
  * for the first row written to the file. Subsequent rows are written
11
11
  * with incrementing row numbers.
12
12
  *
13
- * @param output - A file write stream to write the Excel XML to.
14
- * @param rows - An async iterable of rows, where each row is an array
15
- * of values.
16
- * @param startRowNumber - The starting row number to use for the first
17
- * row written to the file.
18
- *
19
- * @returns An object with a single property `rowNumber`, which is the
20
- * last row number written to the file (i.e., the `startRowNumber`
21
- * plus the number of rows written).
13
+ * @param {WritableLike} output - A file write stream to write the Excel XML to.
14
+ * @param {AsyncIterable<unknown[] | unknown[][]>} rows - An async iterable of rows, where each row is an array
15
+ * of values or an array of arrays of values.
16
+ * @param {number} startRowNumber - The starting row number to use for the first
17
+ * row written to the file.
18
+ * @returns {Promise<{
19
+ * dimension: {
20
+ * maxColumn: string;
21
+ * maxRow: number;
22
+ * minColumn: string;
23
+ * minRow: number;
24
+ * };
25
+ * rowNumber: number;
26
+ * }>} An object containing:
27
+ * - dimension: The boundaries of the written data (min/max columns and rows)
28
+ * - rowNumber: The last row number written to the file
22
29
  */
23
30
  export async function writeRowsToStream(output, rows, startRowNumber) {
24
31
  let rowNumber = startRowNumber;
32
+ const dimension = {
33
+ maxColumn: "A",
34
+ maxRow: startRowNumber,
35
+ minColumn: "A",
36
+ minRow: startRowNumber,
37
+ };
38
+ // Функция для сравнения колонок (A < B, AA > Z и т.д.)
39
+ const compareColumns = (a, b) => {
40
+ if (a === b)
41
+ return 0;
42
+ return a.length === b.length ? (a < b ? -1 : 1) : (a.length < b.length ? -1 : 1);
43
+ };
44
+ const processRow = (row, currentRowNumber) => {
45
+ const cells = prepareRowToCells(row, currentRowNumber);
46
+ if (cells.length === 0)
47
+ return;
48
+ output.write(`<row r="${currentRowNumber}">${cells.map(cell => cell.cellXml).join("")}</row>`);
49
+ // Обновление границ
50
+ const firstCellRef = cells[0]?.cellRef;
51
+ const lastCellRef = cells[cells.length - 1]?.cellRef;
52
+ if (firstCellRef) {
53
+ const colLetters = firstCellRef.match(/[A-Z]+/)?.[0] || "";
54
+ if (compareColumns(colLetters, dimension.minColumn) < 0) {
55
+ dimension.minColumn = colLetters;
56
+ }
57
+ }
58
+ if (lastCellRef) {
59
+ const colLetters = lastCellRef.match(/[A-Z]+/)?.[0] || "";
60
+ if (compareColumns(colLetters, dimension.maxColumn) > 0) {
61
+ dimension.maxColumn = colLetters;
62
+ }
63
+ }
64
+ dimension.maxRow = currentRowNumber;
65
+ };
25
66
  for await (const row of rows) {
26
- // Transform the row into XML
67
+ if (!row.length)
68
+ continue;
27
69
  if (Array.isArray(row[0])) {
28
70
  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>`);
71
+ if (!subRow.length)
72
+ continue;
73
+ processRow(subRow, rowNumber);
32
74
  rowNumber++;
33
75
  }
34
76
  }
35
77
  else {
36
- const cells = prepareRowToCells(row, rowNumber);
37
- // Write the row to the file
38
- output.write(`<row r="${rowNumber}">${cells.join("")}</row>`);
78
+ processRow(row, rowNumber);
39
79
  rowNumber++;
40
80
  }
41
81
  }
42
- return { rowNumber };
82
+ return { dimension, rowNumber };
43
83
  }
@@ -0,0 +1,64 @@
1
+ import { extractXmlFromSheetSync } from "./extract-xml-from-sheet-sync.js";
2
+ /**
3
+ * Parses a worksheet (either as Buffer or string) to extract row data,
4
+ * last row number, and merge cell information from Excel XML format.
5
+ *
6
+ * This function is particularly useful for processing Excel files in
7
+ * Open XML Spreadsheet format (.xlsx).
8
+ *
9
+ * @param {Buffer|string} sheet - The worksheet content to parse, either as:
10
+ * - Buffer (binary Excel sheet)
11
+ * - string (raw XML content)
12
+ * @returns {{
13
+ * rows: string[],
14
+ * lastRowNumber: number,
15
+ * mergeCells: {ref: string}[]
16
+ * }} An object containing:
17
+ * - rows: Array of raw XML strings for each <row> element
18
+ * - lastRowNumber: Highest row number found in the sheet (1-based)
19
+ * - mergeCells: Array of merged cell ranges (e.g., [{ref: "A1:B2"}])
20
+ * @throws {Error} If the sheetData section is not found in the XML
21
+ */
22
+ export function extractRowsFromSheetSync(sheet) {
23
+ // Convert Buffer input to XML string if needed
24
+ const xml = typeof sheet === "string"
25
+ ? sheet
26
+ : extractXmlFromSheetSync(sheet);
27
+ // Extract the sheetData section containing all rows
28
+ const sheetDataMatch = xml.match(/<sheetData[^>]*>([\s\S]*?)<\/sheetData>/);
29
+ if (!sheetDataMatch) {
30
+ throw new Error("sheetData not found in worksheet XML");
31
+ }
32
+ const sheetDataContent = sheetDataMatch[1] || "";
33
+ // Extract all <row> elements using regex
34
+ const rowMatches = [...sheetDataContent.matchAll(/<row\b[^>]*\/>|<row\b[^>]*>[\s\S]*?<\/row>/g)];
35
+ const rows = rowMatches.map(match => match[0]);
36
+ // Calculate the highest row number present in the sheet
37
+ const lastRowNumber = rowMatches
38
+ .map(match => {
39
+ // Extract row number from r="..." attribute (1-based)
40
+ const rowNumMatch = match[0].match(/r="(\d+)"/);
41
+ return rowNumMatch?.[1] ? parseInt(rowNumMatch[1], 10) : null;
42
+ })
43
+ .filter((row) => row !== null) // Type guard to filter out nulls
44
+ .reduce((max, current) => Math.max(max, current), 0); // Find maximum row number
45
+ // Extract all merged cell ranges from the worksheet
46
+ const mergeCells = [];
47
+ const mergeCellsMatch = xml.match(/<mergeCells[^>]*>([\s\S]*?)<\/mergeCells>/);
48
+ if (mergeCellsMatch) {
49
+ // Find all mergeCell entries with ref attributes
50
+ const mergeCellMatches = mergeCellsMatch[1]?.match(/<mergeCell[^>]+ref="([^"]+)"[^>]*>/g) || [];
51
+ mergeCellMatches.forEach(match => {
52
+ const refMatch = match.match(/ref="([^"]+)"/);
53
+ if (refMatch?.[1]) {
54
+ mergeCells.push({ ref: refMatch[1] }); // Store the cell range (e.g., "A1:B2")
55
+ }
56
+ });
57
+ }
58
+ return {
59
+ lastRowNumber,
60
+ mergeCells,
61
+ rows,
62
+ xml,
63
+ };
64
+ }
@@ -19,9 +19,11 @@ import { extractXmlFromSheet } from "./extract-xml-from-sheet.js";
19
19
  * - mergeCells: Array of merged cell ranges (e.g., [{ref: "A1:B2"}])
20
20
  * @throws {Error} If the sheetData section is not found in the XML
21
21
  */
22
- export function extractRowsFromSheet(sheet) {
22
+ export async function extractRowsFromSheet(sheet) {
23
23
  // Convert Buffer input to XML string if needed
24
- const xml = typeof sheet === "string" ? sheet : extractXmlFromSheet(sheet);
24
+ const xml = typeof sheet === "string"
25
+ ? sheet
26
+ : await extractXmlFromSheet(sheet);
25
27
  // Extract the sheetData section containing all rows
26
28
  const sheetDataMatch = xml.match(/<sheetData[^>]*>([\s\S]*?)<\/sheetData>/);
27
29
  if (!sheetDataMatch) {
@@ -0,0 +1,40 @@
1
+ import { inflateRawSync } from "node:zlib";
2
+ /**
3
+ * Extracts and parses XML content from an Excel worksheet file (e.g., xl/worksheets/sheet1.xml).
4
+ * Handles both compressed (raw deflate) and uncompressed (plain XML) formats.
5
+ *
6
+ * This function is designed to work with Excel Open XML (.xlsx) worksheet files,
7
+ * which may be stored in either compressed or uncompressed format within the ZIP container.
8
+ *
9
+ * @param {Buffer} buffer - The file content to process, which may be:
10
+ * - Raw XML text
11
+ * - Deflate-compressed XML data (without zlib headers)
12
+ * @returns {string} - The extracted XML content as a UTF-8 string
13
+ * @throws {Error} - If the buffer is empty or cannot be processed
14
+ */
15
+ export function extractXmlFromSheetSync(buffer) {
16
+ if (!buffer || buffer.length === 0) {
17
+ throw new Error("Empty buffer provided");
18
+ }
19
+ let xml;
20
+ // Check if the buffer starts with an XML declaration (<?xml)
21
+ const head = buffer.subarray(0, 1024).toString("utf8").replace(/[\x00-\x08\x0B\x0C\x0E-\x1F]/g, "").trim();
22
+ const isXml = /^<\?xml[\s\S]+<\w+[\s>]/.test(head);
23
+ if (isXml) {
24
+ // Case 1: Already uncompressed XML - convert directly to string
25
+ xml = buffer.toString("utf8");
26
+ }
27
+ else {
28
+ // Case 2: Attempt to decompress as raw deflate data
29
+ try {
30
+ xml = inflateRawSync(buffer).toString("utf8");
31
+ }
32
+ catch (err) {
33
+ throw new Error("Failed to decompress sheet XML: " + (err instanceof Error ? err.message : String(err)));
34
+ }
35
+ }
36
+ // Sanitize XML by removing control characters (except tab, newline, carriage return)
37
+ // This handles potential corruption from binary data or encoding issues
38
+ xml = xml.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F]/g, "");
39
+ return xml;
40
+ }
@@ -1,4 +1,6 @@
1
- import { inflateRaw } from "pako";
1
+ import util from "node:util";
2
+ import zlib from "node:zlib";
3
+ const inflateRaw = util.promisify(zlib.inflateRaw);
2
4
  /**
3
5
  * Extracts and parses XML content from an Excel worksheet file (e.g., xl/worksheets/sheet1.xml).
4
6
  * Handles both compressed (raw deflate) and uncompressed (plain XML) formats.
@@ -9,35 +11,30 @@ import { inflateRaw } from "pako";
9
11
  * @param {Buffer} buffer - The file content to process, which may be:
10
12
  * - Raw XML text
11
13
  * - Deflate-compressed XML data (without zlib headers)
12
- * @returns {string} - The extracted XML content as a UTF-8 string
14
+ * @returns {Promise<string>} - The extracted XML content as a UTF-8 string
13
15
  * @throws {Error} - If the buffer is empty or cannot be processed
14
16
  */
15
- export function extractXmlFromSheet(buffer) {
17
+ export async function extractXmlFromSheet(buffer) {
16
18
  if (!buffer || buffer.length === 0) {
17
19
  throw new Error("Empty buffer provided");
18
20
  }
19
21
  let xml;
20
22
  // Check if the buffer starts with an XML declaration (<?xml)
21
- const startsWithXml = buffer.subarray(0, 5).toString("utf8").trim().startsWith("<?xml");
22
- if (startsWithXml) {
23
+ const head = buffer.subarray(0, 1024).toString("utf8").replace(/[\x00-\x08\x0B\x0C\x0E-\x1F]/g, "").trim();
24
+ const isXml = /^<\?xml[\s\S]+<\w+[\s>]/.test(head);
25
+ if (isXml) {
23
26
  // Case 1: Already uncompressed XML - convert directly to string
24
27
  xml = buffer.toString("utf8");
25
28
  }
26
29
  else {
27
30
  // Case 2: Attempt to decompress as raw deflate data
28
- const inflated = inflateRaw(buffer, { to: "string" });
29
- // Validate the decompressed content contains worksheet data
30
- if (inflated && inflated.includes("<sheetData")) {
31
- xml = inflated;
31
+ try {
32
+ xml = (await inflateRaw(buffer)).toString("utf8");
32
33
  }
33
- else {
34
- throw new Error("Decompressed data does not contain sheetData");
34
+ catch (err) {
35
+ throw new Error("Failed to decompress sheet XML: " + (err instanceof Error ? err.message : String(err)));
35
36
  }
36
37
  }
37
- // Fallback: If no XML obtained yet, try direct UTF-8 conversion
38
- if (!xml) {
39
- xml = buffer.toString("utf8");
40
- }
41
38
  // Sanitize XML by removing control characters (except tab, newline, carriage return)
42
39
  // This handles potential corruption from binary data or encoding issues
43
40
  xml = xml.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F]/g, "");
@@ -1,5 +1,6 @@
1
1
  export * from "./build-merged-sheet.js";
2
+ export * from "./extract-rows-from-sheet-sync.js";
2
3
  export * from "./extract-rows-from-sheet.js";
4
+ export * from "./extract-xml-from-sheet-sync.js";
3
5
  export * from "./extract-xml-from-sheet.js";
4
- export * from "./extract-xml-from-system-content.js";
5
6
  export * from "./shift-row-indices.js";
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Merges rows from other Excel files into a base Excel file.
3
+ *
4
+ * This function is a process-friendly version of mergeSheetsToBaseFile.
5
+ * It takes a single object with the following properties:
6
+ * - additions: An array of objects with two properties:
7
+ * - files: A dictionary of file paths to their corresponding XML content
8
+ * - sheetIndexes: The 1-based indexes of the sheet to extract rows from
9
+ * - baseFiles: A dictionary of file paths to their corresponding XML content
10
+ * - baseSheetIndex: The 1-based index of the sheet in the base file to add rows to
11
+ * - gap: The number of empty rows to insert between each added section
12
+ * - sheetNamesToRemove: The names of sheets to remove from the output file
13
+ * - sheetsToRemove: The 1-based indices of sheets to remove from the output file
14
+ *
15
+ * The function returns a dictionary of file paths to their corresponding XML content.
16
+ */
17
+ export declare function mergeSheetsToBaseFileProcessSync(data: {
18
+ additions: {
19
+ files: Record<string, Buffer>;
20
+ sheetIndexes: number[];
21
+ }[];
22
+ baseFiles: Record<string, Buffer>;
23
+ baseSheetIndex: number;
24
+ gap: number;
25
+ sheetNamesToRemove: string[];
26
+ sheetsToRemove: number[];
27
+ }): void;
@@ -24,4 +24,4 @@ export declare function mergeSheetsToBaseFileProcess(data: {
24
24
  gap: number;
25
25
  sheetNamesToRemove: string[];
26
26
  sheetsToRemove: number[];
27
- }): void;
27
+ }): Promise<void>;
@@ -135,6 +135,7 @@ export declare class TemplateFs {
135
135
  * @param {Object} data - The data to create the template from.
136
136
  * @param {string} data.source - The path or buffer of the Excel file.
137
137
  * @param {string} data.destination - The path to save the template to.
138
+ * @param {boolean} data.isUniqueDestination - Whether to add a random UUID to the destination path.
138
139
  * @returns {Promise<Template>} A new Template instance.
139
140
  * @throws {Error} If reading or writing files fails.
140
141
  * @experimental This API is experimental and might change in future versions.
@@ -142,5 +143,6 @@ export declare class TemplateFs {
142
143
  static from(data: {
143
144
  destination: string;
144
145
  source: string | Buffer;
146
+ isUniqueDestination?: boolean;
145
147
  }): Promise<TemplateFs>;
146
148
  }
@@ -11,6 +11,13 @@ export declare class TemplateMemory {
11
11
  * @type {boolean}
12
12
  */
13
13
  destroyed: boolean;
14
+ /**
15
+ * Creates a Template instance from a map of file paths to buffers.
16
+ *
17
+ * @param {Object<string, Buffer>} files - The files to create the template from.
18
+ * @throws {Error} If reading or writing files fails.
19
+ * @experimental This API is experimental and might change in future versions.
20
+ */
14
21
  constructor(files: Record<string, Buffer>);
15
22
  /**
16
23
  * Copies a sheet from the template to a new name.
@@ -55,6 +62,20 @@ export declare class TemplateMemory {
55
62
  startRowNumber?: number;
56
63
  rows: unknown[][];
57
64
  }): Promise<void>;
65
+ /**
66
+ * Inserts rows into a specific sheet in the template using an async stream.
67
+ *
68
+ * @param {Object} data - The data for row insertion.
69
+ * @param {string} data.sheetName - The name of the sheet to insert rows into.
70
+ * @param {number} [data.startRowNumber] - The row number to start inserting from.
71
+ * @param {AsyncIterable<unknown[]>} data.rows - Async iterable of rows to insert.
72
+ * @returns {Promise<void>}
73
+ * @throws {Error} If the template instance has been destroyed.
74
+ * @throws {Error} If the sheet does not exist.
75
+ * @throws {Error} If the row number is out of range.
76
+ * @throws {Error} If a column is out of range.
77
+ * @experimental This API is experimental and might change in future versions.
78
+ */
58
79
  insertRowsStream(data: {
59
80
  sheetName: string;
60
81
  startRowNumber?: number;
@@ -79,6 +100,46 @@ export declare class TemplateMemory {
79
100
  * @experimental This API is experimental and might change in future versions.
80
101
  */
81
102
  set(key: string, content: Buffer): Promise<void>;
103
+ /**
104
+ * Merges sheets into a base sheet.
105
+ *
106
+ * @param {Object} data
107
+ * @param {{ sheetIndexes?: number[]; sheetNames?: string[] }} data.additions - The sheets to merge.
108
+ * @param {number} [data.baseSheetIndex=1] - The 1-based index of the sheet to merge into.
109
+ * @param {string} [data.baseSheetName] - The name of the sheet to merge into.
110
+ * @param {number} [data.gap=0] - The number of empty rows to insert between each added section.
111
+ * @returns {void}
112
+ */
113
+ mergeSheets(data: {
114
+ additions: {
115
+ sheetIndexes?: number[];
116
+ sheetNames?: string[];
117
+ };
118
+ baseSheetIndex?: number;
119
+ baseSheetName?: string;
120
+ gap?: number;
121
+ }): void;
122
+ /**
123
+ * Removes sheets from the workbook.
124
+ *
125
+ * @param {Object} data
126
+ * @param {number[]} [data.sheetIndexes] - The 1-based indexes of the sheets to remove.
127
+ * @param {string[]} [data.sheetNames] - The names of the sheets to remove.
128
+ * @returns {void}
129
+ */
130
+ removeSheets(data: {
131
+ sheetNames?: string[];
132
+ sheetIndexes?: number[];
133
+ }): void;
134
+ /**
135
+ * Creates a Template instance from an Excel file source.
136
+ *
137
+ * @param {Object} data - The data to create the template from.
138
+ * @param {string | Buffer} data.source - The path or buffer of the Excel file.
139
+ * @returns {Promise<TemplateMemory>} A new Template instance.
140
+ * @throws {Error} If reading the file fails.
141
+ * @experimental This API is experimental and might change in future versions.
142
+ */
82
143
  static from(data: {
83
144
  source: string | Buffer;
84
145
  }): Promise<TemplateMemory>;
@@ -1,3 +1,4 @@
1
+ export * as Common from "../../utils/index.js";
1
2
  export * from "./apply-replacements.js";
2
3
  export * from "./check-row.js";
3
4
  export * from "./check-rows.js";
@@ -14,6 +15,7 @@ export * from "./process-merge-cells.js";
14
15
  export * from "./process-merge-finalize.js";
15
16
  export * from "./process-rows.js";
16
17
  export * from "./process-shared-strings.js";
18
+ export * from "./regexp.js";
17
19
  export * from "./to-excel-column-object.js";
18
20
  export * from "./update-dimension.js";
19
21
  export * from "./validate-worksheet-xml.js";
@@ -1 +1,5 @@
1
- export declare function prepareRowToCells(row: unknown[], rowNumber: number): string[];
1
+ export declare function prepareRowToCells(row: unknown[], rowNumber: number): {
2
+ cellRef: string;
3
+ cellValue: string;
4
+ cellXml: string;
5
+ }[];
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Creates a regular expression to match a relationship element with a specific ID.
3
+ *
4
+ * @param {string} id - The relationship ID to match (e.g. "rId1")
5
+ * @returns {RegExp} A regular expression that matches a Relationship XML element with the given ID and captures the Target attribute value
6
+ * @example
7
+ * const regex = relationshipMatch("rId1");
8
+ * const xml = '<Relationship Id="rId1" Target="worksheets/sheet1.xml"/>';
9
+ * const match = xml.match(regex);
10
+ * // match[1] === "worksheets/sheet1.xml"
11
+ */
12
+ export declare function relationshipMatch(id: string): RegExp;
13
+ /**
14
+ * Creates a regular expression to match a sheet element with a specific name.
15
+ *
16
+ * @param {string} sheetName - The name of the sheet to match
17
+ * @returns {RegExp} A regular expression that matches a sheet XML element with the given name and captures the r:id attribute value
18
+ * @example
19
+ * const regex = sheetMatch("Sheet1");
20
+ * const xml = '<sheet name="Sheet1" sheetId="1" r:id="rId1"/>';
21
+ * const match = xml.match(regex);
22
+ * // match[1] === "rId1"
23
+ */
24
+ export declare function sheetMatch(sheetName: string): RegExp;
@@ -1 +1,16 @@
1
+ /**
2
+ * Updates the dimension element in an Excel worksheet XML string based on the actual cell references.
3
+ *
4
+ * This function scans the XML for all cell references and calculates the minimum and maximum
5
+ * column/row values to determine the actual used range in the worksheet. It then updates
6
+ * the dimension element to reflect this range.
7
+ *
8
+ * @param {string} xml - The worksheet XML string to process
9
+ * @returns {string} The XML string with updated dimension element
10
+ * @example
11
+ * // XML with cells from A1 to C3
12
+ * const xml = '....<dimension ref="A1:B2"/>.....<c r="C3">...</c>...';
13
+ * const updated = updateDimension(xml);
14
+ * // Returns XML with dimension updated to ref="A1:C3"
15
+ */
1
16
  export declare function updateDimension(xml: string): string;