@js-ak/excel-toolbox 1.0.0 → 1.2.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 (104) hide show
  1. package/README.md +100 -0
  2. package/build/cjs/index.js +17 -0
  3. package/build/cjs/lib/index.js +18 -0
  4. package/build/cjs/lib/merge-sheets-to-base-file-process.js +96 -0
  5. package/build/cjs/lib/merge-sheets-to-base-file-sync.js +68 -0
  6. package/build/cjs/lib/merge-sheets-to-base-file.js +68 -0
  7. package/build/cjs/lib/utils/get-max-row-number.js +23 -0
  8. package/build/cjs/lib/utils/index.js +23 -0
  9. package/build/cjs/lib/utils/is-same-buffer.js +13 -0
  10. package/build/cjs/lib/utils/remove-sheet-by-name.js +45 -0
  11. package/build/cjs/lib/utils/remove-sheet-from-content-types.js +13 -0
  12. package/build/cjs/lib/utils/remove-sheet-from-rels.js +13 -0
  13. package/build/cjs/lib/utils/remove-sheet-from-workbook.js +13 -0
  14. package/build/cjs/lib/utils/shift-cell-ref.js +26 -0
  15. package/build/cjs/lib/xml/build-merged-sheet.js +32 -0
  16. package/build/cjs/lib/xml/extract-rows-from-sheet.js +65 -0
  17. package/build/cjs/lib/xml/extract-xml-from-sheet.js +49 -0
  18. package/build/cjs/lib/xml/extract-xml-from-system-content.js +53 -0
  19. package/build/cjs/lib/xml/index.js +21 -0
  20. package/build/cjs/lib/xml/shift-row-indices.js +36 -0
  21. package/build/cjs/lib/zip/constants.js +32 -0
  22. package/build/cjs/lib/zip/create-sync.js +84 -0
  23. package/build/cjs/lib/zip/create.js +89 -0
  24. package/build/cjs/lib/zip/index.js +20 -0
  25. package/build/cjs/lib/zip/read-sync.js +57 -0
  26. package/build/cjs/lib/zip/read.js +62 -0
  27. package/build/cjs/lib/zip/utils.js +158 -0
  28. package/build/cjs/test/index.js +10 -0
  29. package/build/esm/index.js +1 -0
  30. package/build/esm/lib/index.js +2 -0
  31. package/build/esm/lib/merge-sheets-to-base-file-process.js +69 -0
  32. package/build/esm/lib/merge-sheets-to-base-file-sync.js +41 -0
  33. package/build/esm/lib/merge-sheets-to-base-file.js +41 -0
  34. package/build/esm/lib/utils/get-max-row-number.js +19 -0
  35. package/build/esm/lib/utils/index.js +7 -0
  36. package/build/esm/lib/utils/is-same-buffer.js +9 -0
  37. package/build/esm/lib/utils/remove-sheet-by-name.js +41 -0
  38. package/build/esm/lib/utils/remove-sheet-from-content-types.js +9 -0
  39. package/build/esm/lib/utils/remove-sheet-from-rels.js +9 -0
  40. package/build/esm/lib/utils/remove-sheet-from-workbook.js +9 -0
  41. package/build/esm/lib/utils/shift-cell-ref.js +22 -0
  42. package/build/esm/lib/xml/build-merged-sheet.js +28 -0
  43. package/build/{lib → esm/lib}/xml/extract-rows-from-sheet.js +6 -6
  44. package/build/{lib → esm/lib}/xml/extract-xml-from-sheet.js +12 -18
  45. package/build/{lib → esm/lib}/xml/extract-xml-from-system-content.js +7 -7
  46. package/build/esm/lib/xml/index.js +5 -0
  47. package/build/{lib → esm/lib}/zip/constants.js +4 -4
  48. package/build/{lib/zip/create.js → esm/lib/zip/create-sync.js} +7 -7
  49. package/build/esm/lib/zip/create.js +82 -0
  50. package/build/esm/lib/zip/index.js +4 -0
  51. package/build/{lib/zip/read.js → esm/lib/zip/read-sync.js} +4 -4
  52. package/build/esm/lib/zip/read.js +55 -0
  53. package/build/{lib → esm/lib}/zip/utils.js +1 -1
  54. package/build/esm/test/index.js +5 -0
  55. package/build/types/index.d.ts +1 -0
  56. package/build/types/lib/index.d.ts +2 -0
  57. package/build/types/lib/merge-sheets-to-base-file-process.d.ts +27 -0
  58. package/build/types/lib/merge-sheets-to-base-file-sync.d.ts +29 -0
  59. package/build/types/lib/merge-sheets-to-base-file.d.ts +29 -0
  60. package/build/types/lib/utils/get-max-row-number.d.ts +6 -0
  61. package/build/types/lib/utils/index.d.ts +7 -0
  62. package/build/types/lib/utils/is-same-buffer.d.ts +9 -0
  63. package/build/types/lib/utils/remove-sheet-by-name.d.ts +7 -0
  64. package/build/types/lib/utils/remove-sheet-from-content-types.d.ts +7 -0
  65. package/build/types/lib/utils/remove-sheet-from-rels.d.ts +7 -0
  66. package/build/types/lib/utils/remove-sheet-from-workbook.d.ts +7 -0
  67. package/build/types/lib/utils/shift-cell-ref.d.ts +13 -0
  68. package/build/{lib → types/lib}/xml/build-merged-sheet.d.ts +7 -8
  69. package/build/{lib → types/lib}/xml/extract-rows-from-sheet.d.ts +0 -1
  70. package/build/{lib → types/lib}/xml/extract-xml-from-sheet.d.ts +0 -1
  71. package/build/{lib → types/lib}/xml/extract-xml-from-system-content.d.ts +0 -1
  72. package/build/types/lib/xml/index.d.ts +5 -0
  73. package/build/{lib → types/lib}/xml/shift-row-indices.d.ts +0 -1
  74. package/build/{lib → types/lib}/zip/constants.d.ts +1 -2
  75. package/build/types/lib/zip/create-sync.d.ts +12 -0
  76. package/build/{lib → types/lib}/zip/create.d.ts +2 -3
  77. package/build/types/lib/zip/index.d.ts +4 -0
  78. package/build/types/lib/zip/read-sync.d.ts +10 -0
  79. package/build/{lib → types/lib}/zip/read.d.ts +1 -4
  80. package/build/{lib → types/lib}/zip/utils.d.ts +1 -2
  81. package/build/types/test/index.d.ts +1 -0
  82. package/package.json +16 -8
  83. package/build/index.d.ts +0 -2
  84. package/build/index.js +0 -1
  85. package/build/lib/index.d.ts +0 -2
  86. package/build/lib/index.d.ts.map +0 -1
  87. package/build/lib/index.js +0 -1
  88. package/build/lib/merge-sheets-to-base-file.d.ts +0 -12
  89. package/build/lib/merge-sheets-to-base-file.d.ts.map +0 -1
  90. package/build/lib/merge-sheets-to-base-file.js +0 -91
  91. package/build/lib/xml/build-merged-sheet.d.ts.map +0 -1
  92. package/build/lib/xml/build-merged-sheet.js +0 -32
  93. package/build/lib/xml/extract-rows-from-sheet.d.ts.map +0 -1
  94. package/build/lib/xml/extract-xml-from-sheet.d.ts.map +0 -1
  95. package/build/lib/xml/extract-xml-from-system-content.d.ts.map +0 -1
  96. package/build/lib/xml/shift-row-indices.d.ts.map +0 -1
  97. package/build/lib/zip/constants.d.ts.map +0 -1
  98. package/build/lib/zip/create.d.ts.map +0 -1
  99. package/build/lib/zip/index.d.ts +0 -3
  100. package/build/lib/zip/index.d.ts.map +0 -1
  101. package/build/lib/zip/index.js +0 -2
  102. package/build/lib/zip/read.d.ts.map +0 -1
  103. package/build/lib/zip/utils.d.ts.map +0 -1
  104. /package/build/{lib → esm/lib}/xml/shift-row-indices.js +0 -0
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const node_assert_1 = __importDefault(require("node:assert"));
7
+ const node_test_1 = __importDefault(require("node:test"));
8
+ (0, node_test_1.default)("test", () => {
9
+ node_assert_1.default.equal(true, true);
10
+ });
@@ -0,0 +1 @@
1
+ export * from "./lib/index.js";
@@ -0,0 +1,2 @@
1
+ export * from "./merge-sheets-to-base-file-sync.js";
2
+ export * from "./merge-sheets-to-base-file.js";
@@ -0,0 +1,69 @@
1
+ import * as Utils from "./utils/index.js";
2
+ import * as Xml from "./xml/index.js";
3
+ /**
4
+ * Merges rows from other Excel files into a base Excel file.
5
+ *
6
+ * This function is a process-friendly version of mergeSheetsToBaseFile.
7
+ * It takes a single object with the following properties:
8
+ * - additions: An array of objects with two properties:
9
+ * - files: A dictionary of file paths to their corresponding XML content
10
+ * - sheetIndexes: The 1-based indexes of the sheet to extract rows from
11
+ * - baseFiles: A dictionary of file paths to their corresponding XML content
12
+ * - baseSheetIndex: The 1-based index of the sheet in the base file to add rows to
13
+ * - gap: The number of empty rows to insert between each added section
14
+ * - sheetNamesToRemove: The names of sheets to remove from the output file
15
+ * - sheetsToRemove: The 1-based indices of sheets to remove from the output file
16
+ *
17
+ * The function returns a dictionary of file paths to their corresponding XML content.
18
+ */
19
+ export function mergeSheetsToBaseFileProcess(data) {
20
+ const { additions, baseFiles, baseSheetIndex, gap, sheetNamesToRemove, sheetsToRemove, } = data;
21
+ const basePath = `xl/worksheets/sheet${baseSheetIndex}.xml`;
22
+ if (!baseFiles[basePath]) {
23
+ throw new Error(`Base file does not contain ${basePath}`);
24
+ }
25
+ const { lastRowNumber, mergeCells: baseMergeCells, rows: baseRows, } = Xml.extractRowsFromSheet(baseFiles[basePath]);
26
+ const allRows = [...baseRows];
27
+ const allMergeCells = [...baseMergeCells];
28
+ let currentRowOffset = lastRowNumber + gap;
29
+ for (const { files, sheetIndexes } of additions) {
30
+ for (const sheetIndex of sheetIndexes) {
31
+ const sheetPath = `xl/worksheets/sheet${sheetIndex}.xml`;
32
+ if (!files[sheetPath]) {
33
+ throw new Error(`File does not contain ${sheetPath}`);
34
+ }
35
+ const { mergeCells, rows } = Xml.extractRowsFromSheet(files[sheetPath]);
36
+ const shiftedRows = Xml.shiftRowIndices(rows, currentRowOffset);
37
+ const shiftedMergeCells = mergeCells.map(cell => {
38
+ const [start, end] = cell.ref.split(":");
39
+ if (!start || !end) {
40
+ return cell;
41
+ }
42
+ const shiftedStart = Utils.shiftCellRef(start, currentRowOffset);
43
+ const shiftedEnd = Utils.shiftCellRef(end, currentRowOffset);
44
+ return { ...cell, ref: `${shiftedStart}:${shiftedEnd}` };
45
+ });
46
+ allRows.push(...shiftedRows);
47
+ allMergeCells.push(...shiftedMergeCells);
48
+ currentRowOffset += Utils.getMaxRowNumber(rows) + gap;
49
+ }
50
+ }
51
+ const mergedXml = Xml.buildMergedSheet(baseFiles[basePath], allRows, allMergeCells);
52
+ baseFiles[basePath] = mergedXml;
53
+ for (const sheetIndex of sheetsToRemove) {
54
+ const sheetPath = `xl/worksheets/sheet${sheetIndex}.xml`;
55
+ delete baseFiles[sheetPath];
56
+ if (baseFiles["xl/workbook.xml"]) {
57
+ baseFiles["xl/workbook.xml"] = Utils.removeSheetFromWorkbook(baseFiles["xl/workbook.xml"], sheetIndex);
58
+ }
59
+ if (baseFiles["xl/_rels/workbook.xml.rels"]) {
60
+ baseFiles["xl/_rels/workbook.xml.rels"] = Utils.removeSheetFromRels(baseFiles["xl/_rels/workbook.xml.rels"], sheetIndex);
61
+ }
62
+ if (baseFiles["[Content_Types].xml"]) {
63
+ baseFiles["[Content_Types].xml"] = Utils.removeSheetFromContentTypes(baseFiles["[Content_Types].xml"], sheetIndex);
64
+ }
65
+ }
66
+ for (const sheetName of sheetNamesToRemove) {
67
+ Utils.removeSheetByName(baseFiles, sheetName);
68
+ }
69
+ }
@@ -0,0 +1,41 @@
1
+ import * as Utils from "./utils/index.js";
2
+ import * as Zip from "./zip/index.js";
3
+ import { mergeSheetsToBaseFileProcess } from "./merge-sheets-to-base-file-process.js";
4
+ /**
5
+ * Merge rows from other Excel files into a base Excel file.
6
+ * The output is a new Excel file with the merged content.
7
+ *
8
+ * @param {Object} data
9
+ * @param {Object[]} data.additions
10
+ * @param {Buffer} data.additions.file - The file to extract rows from
11
+ * @param {number[]} data.additions.sheetIndexes - The 1-based indexes of the sheet to extract rows from
12
+ * @param {Buffer} data.baseFile - The base file to add rows to
13
+ * @param {number} [data.baseSheetIndex=1] - The 1-based index of the sheet in the base file to add rows to
14
+ * @param {number} [data.gap=0] - The number of empty rows to insert between each added section
15
+ * @param {string[]} [data.sheetNamesToRemove=[]] - The names of sheets to remove from the output file
16
+ * @param {number[]} [data.sheetsToRemove=[]] - The 1-based indices of sheets to remove from the output file
17
+ * @returns {Buffer} - The merged Excel file
18
+ */
19
+ export function mergeSheetsToBaseFileSync(data) {
20
+ const { additions = [], baseFile, baseSheetIndex = 1, gap = 0, sheetNamesToRemove = [], sheetsToRemove = [], } = data;
21
+ const baseFiles = Zip.readSync(baseFile);
22
+ const additionsUpdated = [];
23
+ for (const { file, isBaseFile, sheetIndexes } of additions) {
24
+ const files = (isBaseFile || Utils.isSameBuffer(file, baseFile))
25
+ ? baseFiles
26
+ : Zip.readSync(file);
27
+ additionsUpdated.push({
28
+ files,
29
+ sheetIndexes,
30
+ });
31
+ }
32
+ mergeSheetsToBaseFileProcess({
33
+ additions: additionsUpdated,
34
+ baseFiles,
35
+ baseSheetIndex,
36
+ gap,
37
+ sheetNamesToRemove,
38
+ sheetsToRemove,
39
+ });
40
+ return Zip.createSync(baseFiles);
41
+ }
@@ -0,0 +1,41 @@
1
+ import * as Utils from "./utils/index.js";
2
+ import * as Zip from "./zip/index.js";
3
+ import { mergeSheetsToBaseFileProcess } from "./merge-sheets-to-base-file-process.js";
4
+ /**
5
+ * Merge rows from other Excel files into a base Excel file.
6
+ * The output is a new Excel file with the merged content.
7
+ *
8
+ * @param {Object} data
9
+ * @param {Object[]} data.additions
10
+ * @param {Buffer} data.additions.file - The file to extract rows from
11
+ * @param {number[]} data.additions.sheetIndexes - The 1-based indexes of the sheet to extract rows from
12
+ * @param {Buffer} data.baseFile - The base file to add rows to
13
+ * @param {number} [data.baseSheetIndex=1] - The 1-based index of the sheet in the base file to add rows to
14
+ * @param {number} [data.gap=0] - The number of empty rows to insert between each added section
15
+ * @param {string[]} [data.sheetNamesToRemove=[]] - The names of sheets to remove from the output file
16
+ * @param {number[]} [data.sheetsToRemove=[]] - The 1-based indices of sheets to remove from the output file
17
+ * @returns {Promise<Buffer>} - The merged Excel file
18
+ */
19
+ export async function mergeSheetsToBaseFile(data) {
20
+ const { additions = [], baseFile, baseSheetIndex = 1, gap = 0, sheetNamesToRemove = [], sheetsToRemove = [], } = data;
21
+ const baseFiles = await Zip.read(baseFile);
22
+ const additionsUpdated = [];
23
+ for (const { file, isBaseFile, sheetIndexes } of additions) {
24
+ const files = (isBaseFile || Utils.isSameBuffer(file, baseFile))
25
+ ? baseFiles
26
+ : await Zip.read(file);
27
+ additionsUpdated.push({
28
+ files,
29
+ sheetIndexes,
30
+ });
31
+ }
32
+ mergeSheetsToBaseFileProcess({
33
+ additions: additionsUpdated,
34
+ baseFiles,
35
+ baseSheetIndex,
36
+ gap,
37
+ sheetNamesToRemove,
38
+ sheetsToRemove,
39
+ });
40
+ return Zip.create(baseFiles);
41
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Finds the maximum row number in a list of <row> elements.
3
+ * @param {string[]} rows - An array of strings, each representing a <row> element.
4
+ * @returns {number} - The maximum row number.
5
+ */
6
+ export function getMaxRowNumber(rows) {
7
+ let max = 0;
8
+ for (const row of rows) {
9
+ const match = row.match(/<row[^>]* r="(\d+)"/);
10
+ if (match) {
11
+ if (!match[1])
12
+ continue;
13
+ const num = parseInt(match[1], 10);
14
+ if (num > max)
15
+ max = num;
16
+ }
17
+ }
18
+ return max;
19
+ }
@@ -0,0 +1,7 @@
1
+ export * from "./get-max-row-number.js";
2
+ export * from "./is-same-buffer.js";
3
+ export * from "./remove-sheet-by-name.js";
4
+ export * from "./remove-sheet-from-content-types.js";
5
+ export * from "./remove-sheet-from-rels.js";
6
+ export * from "./remove-sheet-from-workbook.js";
7
+ export * from "./shift-cell-ref.js";
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Checks if two Buffers are the same
3
+ * @param {Buffer} buf1 - the first Buffer
4
+ * @param {Buffer} buf2 - the second Buffer
5
+ * @returns {boolean} - true if the Buffers are the same, false otherwise
6
+ */
7
+ export function isSameBuffer(buf1, buf2) {
8
+ return buf1.equals(buf2);
9
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Removes a sheet from the Excel workbook by name.
3
+ * @param {Object.<string, string | Buffer>} files - The dictionary of files in the workbook.
4
+ * @param {string} sheetName - The name of the sheet to remove.
5
+ * @returns {void}
6
+ */
7
+ export function removeSheetByName(files, sheetName) {
8
+ const workbookXml = files["xl/workbook.xml"];
9
+ const relsXml = files["xl/_rels/workbook.xml.rels"];
10
+ if (!workbookXml || !relsXml) {
11
+ return;
12
+ }
13
+ const sheetMatch = workbookXml.match(new RegExp(`<sheet[^>]+name=["']${sheetName}["'][^>]*/>`));
14
+ if (!sheetMatch) {
15
+ return;
16
+ }
17
+ const sheetTag = sheetMatch[0];
18
+ const sheetIdMatch = sheetTag.match(/sheetId=["'](\d+)["']/);
19
+ const ridMatch = sheetTag.match(/r:id=["'](rId\d+)["']/);
20
+ if (!sheetIdMatch || !ridMatch) {
21
+ return;
22
+ }
23
+ const relId = ridMatch[1];
24
+ const relMatch = relsXml.match(new RegExp(`<Relationship[^>]+Id=["']${relId}["'][^>]+Target=["']([^"']+)["'][^>]*/>`));
25
+ if (!relMatch) {
26
+ return;
27
+ }
28
+ const relTag = relMatch[0];
29
+ const targetMatch = relTag.match(/Target=["']([^"']+)["']/);
30
+ if (!targetMatch) {
31
+ return;
32
+ }
33
+ const targetPath = `xl/${targetMatch[1]}`.replace(/\\/g, "/");
34
+ delete files[targetPath];
35
+ files["xl/workbook.xml"] = workbookXml.replace(sheetTag, "");
36
+ files["xl/_rels/workbook.xml.rels"] = relsXml.replace(relTag, "");
37
+ const contentTypes = files["[Content_Types].xml"];
38
+ if (contentTypes) {
39
+ files["[Content_Types].xml"] = contentTypes.replace(new RegExp(`<Override[^>]+PartName=["']/${targetPath}["'][^>]*/>`, "g"), "");
40
+ }
41
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Removes the specified sheet from the Content_Types.xml file.
3
+ * @param {string} xml - The Content_Types.xml file contents as a string
4
+ * @param {number} sheetIndex - The 1-based index of the sheet to remove
5
+ * @returns {string} - The modified Content_Types.xml file contents
6
+ */
7
+ export function removeSheetFromContentTypes(xml, sheetIndex) {
8
+ return xml.replace(new RegExp(`<Override[^>]+PartName=["']/xl/worksheets/sheet${sheetIndex}\\.xml["'][^>]*/>`, "g"), "");
9
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Removes the specified sheet from the workbook relationships file (xl/_rels/workbook.xml.rels).
3
+ * @param {string} xml - The workbook relationships file contents as a string
4
+ * @param {number} sheetIndex - The 1-based index of the sheet to remove
5
+ * @returns {string} - The modified workbook relationships file contents
6
+ */
7
+ export function removeSheetFromRels(xml, sheetIndex) {
8
+ return xml.replace(new RegExp(`<Relationship[^>]+Target=["']worksheets/sheet${sheetIndex}\\.xml["'][^>]*/>`, "g"), "");
9
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Removes the specified sheet from the workbook (xl/workbook.xml).
3
+ * @param {string} xml - The workbook file contents as a string
4
+ * @param {number} sheetIndex - The 1-based index of the sheet to remove
5
+ * @returns {string} - The modified workbook file contents
6
+ */
7
+ export function removeSheetFromWorkbook(xml, sheetIndex) {
8
+ return xml.replace(new RegExp(`<sheet[^>]+sheetId=["']${sheetIndex}["'][^>]*/>`, "g"), "");
9
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Shifts the row number in a cell reference by the specified number of rows.
3
+ * The function takes a cell reference string in the format "A1" and a row shift value.
4
+ * It returns the shifted cell reference string.
5
+ *
6
+ * @example
7
+ * // Shifts the cell reference "A1" down by 2 rows, resulting in "A3"
8
+ * shiftCellRef('A1', 2);
9
+ * @param {string} cellRef - The cell reference string to be shifted
10
+ * @param {number} rowShift - The number of rows to shift the reference by
11
+ * @returns {string} - The shifted cell reference string
12
+ */
13
+ export function shiftCellRef(cellRef, rowShift) {
14
+ const match = cellRef.match(/^([A-Z]+)(\d+)$/);
15
+ if (!match)
16
+ return cellRef;
17
+ const col = match[1];
18
+ if (!match[2])
19
+ return cellRef;
20
+ const row = parseInt(match[2], 10);
21
+ return `${col}${row + rowShift}`;
22
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Builds a new XML string for a merged Excel sheet by combining the original XML
3
+ * with merged rows and optional cell merge information.
4
+ *
5
+ * This function replaces the <sheetData> section in the original XML with the provided merged rows,
6
+ * and optionally adds <mergeCells> definitions after the </sheetData> tag.
7
+ *
8
+ * @param {string} originalXml - The original XML string of the Excel worksheet.
9
+ * @param {string[]} mergedRows - Array of XML strings representing each row in the merged sheet.
10
+ * @param {Object[]} [mergeCells] - Optional array of merge cell definitions.
11
+ * Each object should have a 'ref' property specifying the merge range (e.g., "A1:B2").
12
+ * @returns {string} - The reconstructed XML string with merged content.
13
+ */
14
+ export function buildMergedSheet(originalXml, mergedRows, mergeCells = []) {
15
+ // Remove any existing <mergeCells> section from the XML
16
+ let xmlData = originalXml.replace(/<mergeCells[^>]*>[\s\S]*?<\/mergeCells>/g, "");
17
+ // Construct a new <sheetData> section with the provided rows
18
+ const sheetDataXml = `<sheetData>${mergedRows.join("")}</sheetData>`;
19
+ // Replace the existing <sheetData> section with the new one
20
+ xmlData = xmlData.replace(/<sheetData[^>]*>[\s\S]*?<\/sheetData>/, sheetDataXml);
21
+ if (mergeCells.length > 0) {
22
+ // Construct a new <mergeCells> section with the provided merge references
23
+ const mergeCellsXml = `<mergeCells count="${mergeCells.length}">${mergeCells.map(mc => `<mergeCell ref="${mc.ref}"/>`).join("")}</mergeCells>`;
24
+ // Insert <mergeCells> after </sheetData> and before the next XML tag
25
+ xmlData = xmlData.replace(/(<\/sheetData>)(\s*<)/, `$1\n${mergeCellsXml}\n$2`);
26
+ }
27
+ return xmlData;
28
+ }
@@ -1,4 +1,4 @@
1
- import { extractXmlFromSheet } from './extract-xml-from-sheet.js';
1
+ import { extractXmlFromSheet } from "./extract-xml-from-sheet.js";
2
2
  /**
3
3
  * Parses a worksheet (either as Buffer or string) to extract row data,
4
4
  * last row number, and merge cell information from Excel XML format.
@@ -21,15 +21,15 @@ import { extractXmlFromSheet } from './extract-xml-from-sheet.js';
21
21
  */
22
22
  export 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" ? sheet : extractXmlFromSheet(sheet);
25
25
  // Extract the sheetData section containing all rows
26
26
  const sheetDataMatch = xml.match(/<sheetData[^>]*>([\s\S]*?)<\/sheetData>/);
27
27
  if (!sheetDataMatch) {
28
- throw new Error('sheetData not found in worksheet XML');
28
+ throw new Error("sheetData not found in worksheet XML");
29
29
  }
30
- const sheetDataContent = sheetDataMatch[1] || '';
30
+ const sheetDataContent = sheetDataMatch[1] || "";
31
31
  // Extract all <row> elements using regex
32
- const rowMatches = [...sheetDataContent.matchAll(/<row[\s\S]*?<\/row>/g)];
32
+ const rowMatches = [...sheetDataContent.matchAll(/<row\b[^>]*\/>|<row\b[^>]*>[\s\S]*?<\/row>/g)];
33
33
  const rows = rowMatches.map(match => match[0]);
34
34
  // Calculate the highest row number present in the sheet
35
35
  const lastRowNumber = rowMatches
@@ -54,8 +54,8 @@ export function extractRowsFromSheet(sheet) {
54
54
  });
55
55
  }
56
56
  return {
57
- rows,
58
57
  lastRowNumber,
59
58
  mergeCells,
59
+ rows,
60
60
  };
61
61
  }
@@ -1,4 +1,4 @@
1
- import { inflateRaw } from 'pako';
1
+ import { inflateRaw } from "pako";
2
2
  /**
3
3
  * Extracts and parses XML content from an Excel worksheet file (e.g., xl/worksheets/sheet1.xml).
4
4
  * Handles both compressed (raw deflate) and uncompressed (plain XML) formats.
@@ -14,38 +14,32 @@ import { inflateRaw } from 'pako';
14
14
  */
15
15
  export function extractXmlFromSheet(buffer) {
16
16
  if (!buffer || buffer.length === 0) {
17
- throw new Error('Empty buffer provided');
17
+ throw new Error("Empty buffer provided");
18
18
  }
19
19
  let xml;
20
20
  // Check if the buffer starts with an XML declaration (<?xml)
21
- const startsWithXml = buffer.subarray(0, 5).toString('utf8').trim().startsWith('<?xml');
21
+ const startsWithXml = buffer.subarray(0, 5).toString("utf8").trim().startsWith("<?xml");
22
22
  if (startsWithXml) {
23
23
  // Case 1: Already uncompressed XML - convert directly to string
24
- xml = buffer.toString('utf8');
24
+ xml = buffer.toString("utf8");
25
25
  }
26
26
  else {
27
27
  // Case 2: Attempt to decompress as raw deflate data
28
- try {
29
- const inflated = inflateRaw(buffer, { to: 'string' });
30
- // Validate the decompressed content contains worksheet data
31
- if (inflated && inflated.includes('<sheetData')) {
32
- xml = inflated;
33
- }
34
- else {
35
- throw new Error('Decompressed data does not contain sheetData');
36
- }
28
+ const inflated = inflateRaw(buffer, { to: "string" });
29
+ // Validate the decompressed content contains worksheet data
30
+ if (inflated && inflated.includes("<sheetData")) {
31
+ xml = inflated;
37
32
  }
38
- catch (e) {
39
- console.error('Decompression failed:', e);
40
- // Continue to fallback attempt
33
+ else {
34
+ throw new Error("Decompressed data does not contain sheetData");
41
35
  }
42
36
  }
43
37
  // Fallback: If no XML obtained yet, try direct UTF-8 conversion
44
38
  if (!xml) {
45
- xml = buffer.toString('utf8');
39
+ xml = buffer.toString("utf8");
46
40
  }
47
41
  // Sanitize XML by removing control characters (except tab, newline, carriage return)
48
42
  // This handles potential corruption from binary data or encoding issues
49
- xml = xml.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F]/g, '');
43
+ xml = xml.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F]/g, "");
50
44
  return xml;
51
45
  }
@@ -1,4 +1,4 @@
1
- import { inflateRaw } from 'pako';
1
+ import { inflateRaw } from "pako";
2
2
  /**
3
3
  * Extracts and decompresses XML content from Excel system files (e.g., workbook.xml, [Content_Types].xml).
4
4
  * Handles both compressed (raw DEFLATE) and uncompressed (plain XML) formats with comprehensive error handling.
@@ -20,17 +20,17 @@ export const extractXmlFromSystemContent = (buffer, name) => {
20
20
  }
21
21
  let xml;
22
22
  // Check for XML declaration in first 5 bytes (<?xml)
23
- const startsWithXml = buffer.subarray(0, 5).toString('utf8').trim().startsWith('<?xml');
23
+ const startsWithXml = buffer.subarray(0, 5).toString("utf8").trim().startsWith("<?xml");
24
24
  if (startsWithXml) {
25
25
  // Case 1: Already uncompressed XML - convert directly to string
26
- xml = buffer.toString('utf8');
26
+ xml = buffer.toString("utf8");
27
27
  }
28
28
  else {
29
29
  // Case 2: Attempt DEFLATE decompression
30
30
  try {
31
- const inflated = inflateRaw(buffer, { to: 'string' });
31
+ const inflated = inflateRaw(buffer, { to: "string" });
32
32
  // Validate decompressed content contains XML declaration
33
- if (inflated && inflated.includes('<?xml')) {
33
+ if (inflated && inflated.includes("<?xml")) {
34
34
  xml = inflated;
35
35
  }
36
36
  else {
@@ -38,12 +38,12 @@ export const extractXmlFromSystemContent = (buffer, name) => {
38
38
  }
39
39
  }
40
40
  catch (error) {
41
- const message = error instanceof Error ? error.message : 'Unknown error';
41
+ const message = error instanceof Error ? error.message : "Unknown error";
42
42
  throw new Error(`Failed to decompress ${name}: ${message}`);
43
43
  }
44
44
  }
45
45
  // Sanitize XML by removing illegal control characters (per XML 1.0 spec)
46
46
  // Preserves tabs (0x09), newlines (0x0A), and carriage returns (0x0D)
47
- xml = xml.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F]/g, '');
47
+ xml = xml.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F]/g, "");
48
48
  return xml;
49
49
  };
@@ -0,0 +1,5 @@
1
+ export * from "./build-merged-sheet.js";
2
+ export * from "./extract-rows-from-sheet.js";
3
+ export * from "./extract-xml-from-sheet.js";
4
+ export * from "./extract-xml-from-system-content.js";
5
+ export * from "./shift-row-indices.js";
@@ -1,4 +1,4 @@
1
- import { Buffer } from 'node:buffer';
1
+ import { Buffer } from "node:buffer";
2
2
  /**
3
3
  * ZIP file signature constants in Buffer format.
4
4
  * These magic numbers identify different sections of a ZIP file,
@@ -11,7 +11,7 @@ import { Buffer } from 'node:buffer';
11
11
  * Format: 'PK\01\02'
12
12
  * Found in the central directory that appears at the end of the ZIP file.
13
13
  */
14
- export const CENTRAL_DIR_HEADER_SIG = Buffer.from('504b0102', 'hex');
14
+ export const CENTRAL_DIR_HEADER_SIG = Buffer.from("504b0102", "hex");
15
15
  /**
16
16
  * End of Central Directory Record signature (0x504b0506).
17
17
  * Marks the end of the central directory and contains global information
@@ -19,11 +19,11 @@ export const CENTRAL_DIR_HEADER_SIG = Buffer.from('504b0102', 'hex');
19
19
  * Format: 'PK\05\06'
20
20
  * This is the last record in a valid ZIP file.
21
21
  */
22
- export const END_OF_CENTRAL_DIR_SIG = Buffer.from('504b0506', 'hex');
22
+ export const END_OF_CENTRAL_DIR_SIG = Buffer.from("504b0506", "hex");
23
23
  /**
24
24
  * Local File Header signature (0x504b0304).
25
25
  * Marks the beginning of a file entry within the ZIP archive.
26
26
  * Format: 'PK\03\04' (ASCII letters PK followed by version numbers)
27
27
  * Appears before each file's compressed data.
28
28
  */
29
- export const LOCAL_FILE_HEADER_SIG = Buffer.from('504b0304', 'hex');
29
+ export const LOCAL_FILE_HEADER_SIG = Buffer.from("504b0304", "hex");
@@ -1,23 +1,23 @@
1
- import { Buffer } from 'node:buffer';
2
- import { deflateRawSync } from 'node:zlib';
3
- import { toBytes, dosTime, crc32 } from './utils.js';
4
- import { LOCAL_FILE_HEADER_SIG, CENTRAL_DIR_HEADER_SIG, END_OF_CENTRAL_DIR_SIG, } from './constants.js';
1
+ import { Buffer } from "node:buffer";
2
+ import { deflateRawSync } from "node:zlib";
3
+ import { crc32, dosTime, toBytes } from "./utils.js";
4
+ import { CENTRAL_DIR_HEADER_SIG, END_OF_CENTRAL_DIR_SIG, LOCAL_FILE_HEADER_SIG, } from "./constants.js";
5
5
  /**
6
6
  * Creates a ZIP archive from a collection of files.
7
7
  *
8
8
  * @param {Object.<string, Buffer|string>} files - An object with file paths as keys and either Buffer or string content as values.
9
9
  * @returns {Buffer} - The ZIP archive as a Buffer.
10
10
  */
11
- export function create(files) {
11
+ export function createSync(files) {
12
12
  const fileEntries = [];
13
13
  const centralDirectory = [];
14
14
  let offset = 0;
15
15
  for (const [filename, rawContent] of Object.entries(files).sort(([a], [b]) => a.localeCompare(b))) {
16
- if (filename.includes('..')) {
16
+ if (filename.includes("..")) {
17
17
  throw new Error(`Invalid filename: ${filename}`);
18
18
  }
19
19
  const content = Buffer.isBuffer(rawContent) ? rawContent : Buffer.from(rawContent);
20
- const fileNameBuf = Buffer.from(filename, 'utf8');
20
+ const fileNameBuf = Buffer.from(filename, "utf8");
21
21
  const modTime = dosTime(new Date());
22
22
  const crc = crc32(content);
23
23
  const compressed = deflateRawSync(content);
@@ -0,0 +1,82 @@
1
+ import { Buffer } from "node:buffer";
2
+ import util from "node:util";
3
+ import zlib from "node:zlib";
4
+ const deflateRaw = util.promisify(zlib.deflateRaw);
5
+ import { crc32, dosTime, toBytes } from "./utils.js";
6
+ import { CENTRAL_DIR_HEADER_SIG, END_OF_CENTRAL_DIR_SIG, LOCAL_FILE_HEADER_SIG, } from "./constants.js";
7
+ /**
8
+ * Creates a ZIP archive from a collection of files.
9
+ *
10
+ * @param {Object.<string, Buffer|string>} files - An object with file paths as keys and either Buffer or string content as values.
11
+ * @returns {Buffer} - The ZIP archive as a Buffer.
12
+ */
13
+ export async function create(files) {
14
+ const fileEntries = [];
15
+ const centralDirectory = [];
16
+ let offset = 0;
17
+ for (const [filename, rawContent] of Object.entries(files).sort(([a], [b]) => a.localeCompare(b))) {
18
+ if (filename.includes("..")) {
19
+ throw new Error(`Invalid filename: ${filename}`);
20
+ }
21
+ const content = Buffer.isBuffer(rawContent) ? rawContent : Buffer.from(rawContent);
22
+ const fileNameBuf = Buffer.from(filename, "utf8");
23
+ const modTime = dosTime(new Date());
24
+ const crc = crc32(content);
25
+ const compressed = await deflateRaw(content);
26
+ const compSize = compressed.length;
27
+ const uncompSize = content.length;
28
+ // Local file header
29
+ const localHeader = Buffer.concat([
30
+ LOCAL_FILE_HEADER_SIG,
31
+ toBytes(20, 2),
32
+ toBytes(0, 2),
33
+ toBytes(8, 2),
34
+ modTime,
35
+ toBytes(crc, 4),
36
+ toBytes(compSize, 4),
37
+ toBytes(uncompSize, 4),
38
+ toBytes(fileNameBuf.length, 2),
39
+ toBytes(0, 2),
40
+ ]);
41
+ const localEntry = Buffer.concat([
42
+ localHeader,
43
+ fileNameBuf,
44
+ compressed,
45
+ ]);
46
+ fileEntries.push(localEntry);
47
+ const centralEntry = Buffer.concat([
48
+ Buffer.from(CENTRAL_DIR_HEADER_SIG),
49
+ Buffer.from(toBytes(20, 2)), // Version made by
50
+ Buffer.from(toBytes(20, 2)), // Version needed
51
+ Buffer.from(toBytes(0, 2)), // Flags
52
+ Buffer.from(toBytes(8, 2)), // Compression
53
+ Buffer.from(modTime),
54
+ Buffer.from(toBytes(crc, 4)),
55
+ Buffer.from(toBytes(compSize, 4)),
56
+ Buffer.from(toBytes(uncompSize, 4)),
57
+ Buffer.from(toBytes(fileNameBuf.length, 2)),
58
+ Buffer.from(toBytes(0, 2)), // Extra field length
59
+ Buffer.from(toBytes(0, 2)), // Comment length
60
+ Buffer.from(toBytes(0, 2)), // Disk start
61
+ Buffer.from(toBytes(0, 2)), // Internal attrs
62
+ Buffer.from(toBytes(0, 4)), // External attrs
63
+ Buffer.from(toBytes(offset, 4)),
64
+ fileNameBuf,
65
+ ]);
66
+ centralDirectory.push(centralEntry);
67
+ offset += localEntry.length;
68
+ }
69
+ const centralDirSize = centralDirectory.reduce((sum, entry) => sum + entry.length, 0);
70
+ const centralDirOffset = offset;
71
+ const endRecord = Buffer.concat([
72
+ Buffer.from(END_OF_CENTRAL_DIR_SIG),
73
+ Buffer.from(toBytes(0, 2)), // Disk #
74
+ Buffer.from(toBytes(0, 2)), // Start disk #
75
+ Buffer.from(toBytes(centralDirectory.length, 2)),
76
+ Buffer.from(toBytes(centralDirectory.length, 2)),
77
+ Buffer.from(toBytes(centralDirSize, 4)),
78
+ Buffer.from(toBytes(centralDirOffset, 4)),
79
+ Buffer.from(toBytes(0, 2)), // Comment length
80
+ ]);
81
+ return Buffer.concat(fileEntries.concat(centralDirectory).concat([endRecord]));
82
+ }
@@ -0,0 +1,4 @@
1
+ export * from "./create-sync.js";
2
+ export * from "./create.js";
3
+ export * from "./read-sync.js";
4
+ export * from "./read.js";