@js-ak/excel-toolbox 1.0.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 (41) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1 -0
  3. package/build/index.d.ts +2 -0
  4. package/build/index.js +1 -0
  5. package/build/lib/index.d.ts +2 -0
  6. package/build/lib/index.d.ts.map +1 -0
  7. package/build/lib/index.js +1 -0
  8. package/build/lib/merge-sheets-to-base-file.d.ts +12 -0
  9. package/build/lib/merge-sheets-to-base-file.d.ts.map +1 -0
  10. package/build/lib/merge-sheets-to-base-file.js +91 -0
  11. package/build/lib/xml/build-merged-sheet.d.ts +17 -0
  12. package/build/lib/xml/build-merged-sheet.d.ts.map +1 -0
  13. package/build/lib/xml/build-merged-sheet.js +32 -0
  14. package/build/lib/xml/extract-rows-from-sheet.d.ts +30 -0
  15. package/build/lib/xml/extract-rows-from-sheet.d.ts.map +1 -0
  16. package/build/lib/xml/extract-rows-from-sheet.js +61 -0
  17. package/build/lib/xml/extract-xml-from-sheet.d.ts +17 -0
  18. package/build/lib/xml/extract-xml-from-sheet.d.ts.map +1 -0
  19. package/build/lib/xml/extract-xml-from-sheet.js +51 -0
  20. package/build/lib/xml/extract-xml-from-system-content.d.ts +18 -0
  21. package/build/lib/xml/extract-xml-from-system-content.d.ts.map +1 -0
  22. package/build/lib/xml/extract-xml-from-system-content.js +49 -0
  23. package/build/lib/xml/shift-row-indices.d.ts +20 -0
  24. package/build/lib/xml/shift-row-indices.d.ts.map +1 -0
  25. package/build/lib/xml/shift-row-indices.js +32 -0
  26. package/build/lib/zip/constants.d.ts +32 -0
  27. package/build/lib/zip/constants.d.ts.map +1 -0
  28. package/build/lib/zip/constants.js +29 -0
  29. package/build/lib/zip/create.d.ts +13 -0
  30. package/build/lib/zip/create.d.ts.map +1 -0
  31. package/build/lib/zip/create.js +80 -0
  32. package/build/lib/zip/index.d.ts +3 -0
  33. package/build/lib/zip/index.d.ts.map +1 -0
  34. package/build/lib/zip/index.js +2 -0
  35. package/build/lib/zip/read.d.ts +13 -0
  36. package/build/lib/zip/read.d.ts.map +1 -0
  37. package/build/lib/zip/read.js +53 -0
  38. package/build/lib/zip/utils.d.ts +61 -0
  39. package/build/lib/zip/utils.d.ts.map +1 -0
  40. package/build/lib/zip/utils.js +152 -0
  41. package/package.json +58 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Anton K.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1 @@
1
+ # excel-toolbox
@@ -0,0 +1,2 @@
1
+ export * from './lib/index.js';
2
+ //# sourceMappingURL=index.d.ts.map
package/build/index.js ADDED
@@ -0,0 +1 @@
1
+ export * from './lib/index.js';
@@ -0,0 +1,2 @@
1
+ export * from './merge-sheets-to-base-file.js';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/lib/index.ts"],"names":[],"mappings":"AAAA,cAAc,gCAAgC,CAAC"}
@@ -0,0 +1 @@
1
+ export * from './merge-sheets-to-base-file.js';
@@ -0,0 +1,12 @@
1
+ /// <reference types="node" />
2
+ /// <reference types="node" />
3
+ export declare function mergeSheetsToBaseFile(data: {
4
+ baseFile: Buffer;
5
+ baseSheetIndex?: number;
6
+ additions: {
7
+ file: Buffer;
8
+ sheetIndex: number;
9
+ }[];
10
+ gap?: number;
11
+ }): Promise<Buffer>;
12
+ //# sourceMappingURL=merge-sheets-to-base-file.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"merge-sheets-to-base-file.d.ts","sourceRoot":"","sources":["../../src/lib/merge-sheets-to-base-file.ts"],"names":[],"mappings":";;AAMA,wBAAsB,qBAAqB,CAAC,IAAI,EAAE;IACjD,QAAQ,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,SAAS,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC;KAAE,EAAE,CAAC;IACnD,GAAG,CAAC,EAAE,MAAM,CAAA;CACZ,mBAgEA"}
@@ -0,0 +1,91 @@
1
+ import { extractRowsFromSheet } from './xml/extract-rows-from-sheet.js';
2
+ import { shiftRowIndices } from './xml/shift-row-indices.js';
3
+ import { buildMergedSheet } from './xml/build-merged-sheet.js';
4
+ import * as Zip from './zip/index.js';
5
+ export async function mergeSheetsToBaseFile(data) {
6
+ const { additions = [], baseFile, baseSheetIndex = 1, gap = 1, } = data;
7
+ const baseFiles = Zip.read(baseFile);
8
+ const basePath = `xl/worksheets/sheet${baseSheetIndex}.xml`;
9
+ if (!baseFiles[basePath]) {
10
+ throw new Error(`Base file does not contain ${basePath}`);
11
+ }
12
+ const { lastRowNumber, mergeCells: baseMergeCells, rows: baseRows, } = extractRowsFromSheet(baseFiles[basePath]);
13
+ const allRows = [...baseRows];
14
+ const allMergeCells = [...(baseMergeCells || [])];
15
+ let currentRowOffset = lastRowNumber + gap;
16
+ for (const { file, sheetIndex } of additions) {
17
+ const files = isSameBuffer(file, baseFile) ? baseFiles : Zip.read(file);
18
+ const sheetPath = `xl/worksheets/sheet${sheetIndex}.xml`;
19
+ if (!files[sheetPath]) {
20
+ throw new Error(`File does not contain ${sheetPath}`);
21
+ }
22
+ const { mergeCells, rows } = extractRowsFromSheet(files[sheetPath]);
23
+ const shiftedRows = shiftRowIndices(rows, currentRowOffset);
24
+ const shiftedMergeCells = (mergeCells || []).map(cell => {
25
+ const [start, end] = cell.ref.split(':');
26
+ if (!start || !end) {
27
+ return cell;
28
+ }
29
+ const shiftedStart = shiftCellRef(start, currentRowOffset);
30
+ const shiftedEnd = shiftCellRef(end, currentRowOffset);
31
+ return { ...cell, ref: `${shiftedStart}:${shiftedEnd}` };
32
+ });
33
+ allRows.push(...shiftedRows);
34
+ allMergeCells.push(...shiftedMergeCells);
35
+ currentRowOffset += getMaxRowNumber(rows) + gap;
36
+ }
37
+ const mergedXml = buildMergedSheet(baseFiles[basePath], allRows, allMergeCells);
38
+ baseFiles[basePath] = mergedXml;
39
+ const zip = Zip.create(baseFiles);
40
+ return zip;
41
+ }
42
+ /**
43
+ * Shifts the row number in a cell reference by the specified number of rows.
44
+ * The function takes a cell reference string in the format "A1" and a row shift value.
45
+ * It returns the shifted cell reference string.
46
+ *
47
+ * @example
48
+ * // Shifts the cell reference "A1" down by 2 rows, resulting in "A3"
49
+ * shiftCellRef('A1', 2);
50
+ * @param {string} cellRef - The cell reference string to be shifted
51
+ * @param {number} rowShift - The number of rows to shift the reference by
52
+ * @returns {string} - The shifted cell reference string
53
+ */
54
+ function shiftCellRef(cellRef, rowShift) {
55
+ const match = cellRef.match(/^([A-Z]+)(\d+)$/);
56
+ if (!match)
57
+ return cellRef;
58
+ const col = match[1];
59
+ if (!match[2])
60
+ return cellRef;
61
+ const row = parseInt(match[2], 10);
62
+ return `${col}${row + rowShift}`;
63
+ }
64
+ /**
65
+ * Checks if two Buffers are the same
66
+ * @param {Buffer} buf1 - the first Buffer
67
+ * @param {Buffer} buf2 - the second Buffer
68
+ * @returns {boolean} - true if the Buffers are the same, false otherwise
69
+ */
70
+ function isSameBuffer(buf1, buf2) {
71
+ return buf1.equals(buf2);
72
+ }
73
+ /**
74
+ * Finds the maximum row number in a list of <row> elements.
75
+ * @param {string[]} rows - An array of strings, each representing a <row> element.
76
+ * @returns {number} - The maximum row number.
77
+ */
78
+ function getMaxRowNumber(rows) {
79
+ let max = 0;
80
+ for (const row of rows) {
81
+ const match = row.match(/<row[^>]* r="(\d+)"/);
82
+ if (match) {
83
+ if (!match[1])
84
+ continue;
85
+ const num = parseInt(match[1], 10);
86
+ if (num > max)
87
+ max = num;
88
+ }
89
+ }
90
+ return max;
91
+ }
@@ -0,0 +1,17 @@
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 sheet data content in the original XML with the merged rows
6
+ * and optionally adds merge cell definitions at the end of the sheet data.
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 declare function buildMergedSheet(originalXml: string, mergedRows: string[], mergeCells?: {
15
+ ref: string;
16
+ }[]): string;
17
+ //# sourceMappingURL=build-merged-sheet.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"build-merged-sheet.d.ts","sourceRoot":"","sources":["../../../src/lib/xml/build-merged-sheet.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,wBAAgB,gBAAgB,CAC/B,WAAW,EAAE,MAAM,EACnB,UAAU,EAAE,MAAM,EAAE,EACpB,UAAU,GAAE;IAAE,GAAG,EAAE,MAAM,CAAA;CAAE,EAAO,GAChC,MAAM,CAwBR"}
@@ -0,0 +1,32 @@
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 sheet data content in the original XML with the merged rows
6
+ * and optionally adds merge cell definitions at the end of the sheet data.
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
+ // Replace the entire sheetData section in the original XML with our merged rows
16
+ // The regex matches:
17
+ // - Opening <sheetData> tag with any attributes
18
+ // - Any content between opening and closing tags (including line breaks)
19
+ // - Closing </sheetData> tag
20
+ let xmlData = originalXml.replace(/<sheetData[^>]*>[\s\S]*?<\/sheetData>/, `<sheetData>\n${mergedRows.join('\n')}\n</sheetData>`);
21
+ // If merge cells were specified, add them after the sheetData section
22
+ if (mergeCells.length > 0) {
23
+ // Create mergeCells XML section:
24
+ // - Includes count attribute with total number of merges
25
+ // - Contains one mergeCell element for each merge definition
26
+ const mergeCellsXml = `<mergeCells count="${mergeCells.length}">${mergeCells.map(mc => `<mergeCell ref="${mc.ref}"/>`).join('')}</mergeCells>`;
27
+ // Insert the mergeCells section immediately after the sheetData closing tag
28
+ xmlData = xmlData.replace('</sheetData>', `</sheetData>${mergeCellsXml}`);
29
+ }
30
+ // Return the fully reconstructed XML
31
+ return xmlData;
32
+ }
@@ -0,0 +1,30 @@
1
+ /// <reference types="node" />
2
+ /// <reference types="node" />
3
+ /**
4
+ * Parses a worksheet (either as Buffer or string) to extract row data,
5
+ * last row number, and merge cell information from Excel XML format.
6
+ *
7
+ * This function is particularly useful for processing Excel files in
8
+ * Open XML Spreadsheet format (.xlsx).
9
+ *
10
+ * @param {Buffer|string} sheet - The worksheet content to parse, either as:
11
+ * - Buffer (binary Excel sheet)
12
+ * - string (raw XML content)
13
+ * @returns {{
14
+ * rows: string[],
15
+ * lastRowNumber: number,
16
+ * mergeCells: {ref: string}[]
17
+ * }} An object containing:
18
+ * - rows: Array of raw XML strings for each <row> element
19
+ * - lastRowNumber: Highest row number found in the sheet (1-based)
20
+ * - mergeCells: Array of merged cell ranges (e.g., [{ref: "A1:B2"}])
21
+ * @throws {Error} If the sheetData section is not found in the XML
22
+ */
23
+ export declare function extractRowsFromSheet(sheet: Buffer | string): {
24
+ rows: string[];
25
+ lastRowNumber: number;
26
+ mergeCells: {
27
+ ref: string;
28
+ }[];
29
+ };
30
+ //# sourceMappingURL=extract-rows-from-sheet.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"extract-rows-from-sheet.d.ts","sourceRoot":"","sources":["../../../src/lib/xml/extract-rows-from-sheet.ts"],"names":[],"mappings":";;AAEA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG;IAC7D,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE;QAAE,GAAG,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CAC9B,CA+CA"}
@@ -0,0 +1,61 @@
1
+ import { extractXmlFromSheet } from './extract-xml-from-sheet.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 extractRowsFromSheet(sheet) {
23
+ // Convert Buffer input to XML string if needed
24
+ const xml = typeof sheet === 'string' ? sheet : extractXmlFromSheet(sheet);
25
+ // Extract the sheetData section containing all rows
26
+ const sheetDataMatch = xml.match(/<sheetData[^>]*>([\s\S]*?)<\/sheetData>/);
27
+ if (!sheetDataMatch) {
28
+ throw new Error('sheetData not found in worksheet XML');
29
+ }
30
+ const sheetDataContent = sheetDataMatch[1] || '';
31
+ // Extract all <row> elements using regex
32
+ const rowMatches = [...sheetDataContent.matchAll(/<row[\s\S]*?<\/row>/g)];
33
+ const rows = rowMatches.map(match => match[0]);
34
+ // Calculate the highest row number present in the sheet
35
+ const lastRowNumber = rowMatches
36
+ .map(match => {
37
+ // Extract row number from r="..." attribute (1-based)
38
+ const rowNumMatch = match[0].match(/r="(\d+)"/);
39
+ return rowNumMatch?.[1] ? parseInt(rowNumMatch[1], 10) : null;
40
+ })
41
+ .filter((row) => row !== null) // Type guard to filter out nulls
42
+ .reduce((max, current) => Math.max(max, current), 0); // Find maximum row number
43
+ // Extract all merged cell ranges from the worksheet
44
+ const mergeCells = [];
45
+ const mergeCellsMatch = xml.match(/<mergeCells[^>]*>([\s\S]*?)<\/mergeCells>/);
46
+ if (mergeCellsMatch) {
47
+ // Find all mergeCell entries with ref attributes
48
+ const mergeCellMatches = mergeCellsMatch[1]?.match(/<mergeCell[^>]+ref="([^"]+)"[^>]*>/g) || [];
49
+ mergeCellMatches.forEach(match => {
50
+ const refMatch = match.match(/ref="([^"]+)"/);
51
+ if (refMatch?.[1]) {
52
+ mergeCells.push({ ref: refMatch[1] }); // Store the cell range (e.g., "A1:B2")
53
+ }
54
+ });
55
+ }
56
+ return {
57
+ rows,
58
+ lastRowNumber,
59
+ mergeCells,
60
+ };
61
+ }
@@ -0,0 +1,17 @@
1
+ /// <reference types="node" />
2
+ /// <reference types="node" />
3
+ /**
4
+ * Extracts and parses XML content from an Excel worksheet file (e.g., xl/worksheets/sheet1.xml).
5
+ * Handles both compressed (raw deflate) and uncompressed (plain XML) formats.
6
+ *
7
+ * This function is designed to work with Excel Open XML (.xlsx) worksheet files,
8
+ * which may be stored in either compressed or uncompressed format within the ZIP container.
9
+ *
10
+ * @param {Buffer} buffer - The file content to process, which may be:
11
+ * - Raw XML text
12
+ * - Deflate-compressed XML data (without zlib headers)
13
+ * @returns {string} - The extracted XML content as a UTF-8 string
14
+ * @throws {Error} - If the buffer is empty or cannot be processed
15
+ */
16
+ export declare function extractXmlFromSheet(buffer: Buffer): string;
17
+ //# sourceMappingURL=extract-xml-from-sheet.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"extract-xml-from-sheet.d.ts","sourceRoot":"","sources":["../../../src/lib/xml/extract-xml-from-sheet.ts"],"names":[],"mappings":";;AAEA;;;;;;;;;;;;GAYG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAwC1D"}
@@ -0,0 +1,51 @@
1
+ import { inflateRaw } from 'pako';
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 extractXmlFromSheet(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 startsWithXml = buffer.subarray(0, 5).toString('utf8').trim().startsWith('<?xml');
22
+ if (startsWithXml) {
23
+ // Case 1: Already uncompressed XML - convert directly to string
24
+ xml = buffer.toString('utf8');
25
+ }
26
+ else {
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
+ }
37
+ }
38
+ catch (e) {
39
+ console.error('Decompression failed:', e);
40
+ // Continue to fallback attempt
41
+ }
42
+ }
43
+ // Fallback: If no XML obtained yet, try direct UTF-8 conversion
44
+ if (!xml) {
45
+ xml = buffer.toString('utf8');
46
+ }
47
+ // Sanitize XML by removing control characters (except tab, newline, carriage return)
48
+ // This handles potential corruption from binary data or encoding issues
49
+ xml = xml.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F]/g, '');
50
+ return xml;
51
+ }
@@ -0,0 +1,18 @@
1
+ /// <reference types="node" />
2
+ /// <reference types="node" />
3
+ /**
4
+ * Extracts and decompresses XML content from Excel system files (e.g., workbook.xml, [Content_Types].xml).
5
+ * Handles both compressed (raw DEFLATE) and uncompressed (plain XML) formats with comprehensive error handling.
6
+ *
7
+ * @param {Buffer} buffer - The file content to process, which may be:
8
+ * - Raw XML text
9
+ * - DEFLATE-compressed XML data (without zlib headers)
10
+ * @param {string} name - The filename being processed (for error reporting)
11
+ * @returns {string} - The extracted XML content as a sanitized UTF-8 string
12
+ * @throws {Error} - With descriptive messages for various failure scenarios:
13
+ * - Empty buffer
14
+ * - Decompression failures
15
+ * - Invalid XML content
16
+ */
17
+ export declare const extractXmlFromSystemContent: (buffer: Buffer, name: string) => string;
18
+ //# sourceMappingURL=extract-xml-from-system-content.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"extract-xml-from-system-content.d.ts","sourceRoot":"","sources":["../../../src/lib/xml/extract-xml-from-system-content.ts"],"names":[],"mappings":";;AAEA;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,2BAA2B,WAAY,MAAM,QAAQ,MAAM,KAAG,MAqC1E,CAAC"}
@@ -0,0 +1,49 @@
1
+ import { inflateRaw } from 'pako';
2
+ /**
3
+ * Extracts and decompresses XML content from Excel system files (e.g., workbook.xml, [Content_Types].xml).
4
+ * Handles both compressed (raw DEFLATE) and uncompressed (plain XML) formats with comprehensive error handling.
5
+ *
6
+ * @param {Buffer} buffer - The file content to process, which may be:
7
+ * - Raw XML text
8
+ * - DEFLATE-compressed XML data (without zlib headers)
9
+ * @param {string} name - The filename being processed (for error reporting)
10
+ * @returns {string} - The extracted XML content as a sanitized UTF-8 string
11
+ * @throws {Error} - With descriptive messages for various failure scenarios:
12
+ * - Empty buffer
13
+ * - Decompression failures
14
+ * - Invalid XML content
15
+ */
16
+ export const extractXmlFromSystemContent = (buffer, name) => {
17
+ // Validate input buffer
18
+ if (!buffer || buffer.length === 0) {
19
+ throw new Error(`Empty data buffer provided for file ${name}`);
20
+ }
21
+ let xml;
22
+ // Check for XML declaration in first 5 bytes (<?xml)
23
+ const startsWithXml = buffer.subarray(0, 5).toString('utf8').trim().startsWith('<?xml');
24
+ if (startsWithXml) {
25
+ // Case 1: Already uncompressed XML - convert directly to string
26
+ xml = buffer.toString('utf8');
27
+ }
28
+ else {
29
+ // Case 2: Attempt DEFLATE decompression
30
+ try {
31
+ const inflated = inflateRaw(buffer, { to: 'string' });
32
+ // Validate decompressed content contains XML declaration
33
+ if (inflated && inflated.includes('<?xml')) {
34
+ xml = inflated;
35
+ }
36
+ else {
37
+ throw new Error(`Decompressed data doesn't contain valid XML in ${name}`);
38
+ }
39
+ }
40
+ catch (error) {
41
+ const message = error instanceof Error ? error.message : 'Unknown error';
42
+ throw new Error(`Failed to decompress ${name}: ${message}`);
43
+ }
44
+ }
45
+ // Sanitize XML by removing illegal control characters (per XML 1.0 spec)
46
+ // Preserves tabs (0x09), newlines (0x0A), and carriage returns (0x0D)
47
+ xml = xml.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F]/g, '');
48
+ return xml;
49
+ };
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Adjusts row indices in Excel XML row elements by a specified offset.
3
+ * Handles both row element attributes and cell references within rows.
4
+ *
5
+ * This function is particularly useful when merging sheets or rearranging
6
+ * worksheet content while maintaining proper Excel XML structure.
7
+ *
8
+ * @param {string[]} rows - Array of XML <row> elements as strings
9
+ * @param {number} offset - Numeric value to adjust row indices by:
10
+ * - Positive values shift rows down
11
+ * - Negative values shift rows up
12
+ * @returns {string[]} - New array with modified row elements containing updated indices
13
+ *
14
+ * @example
15
+ * // Shifts rows down by 2 positions
16
+ * shiftRowIndices([`<row r="1"><c r="A1"/></row>`], 2);
17
+ * // Returns: [`<row r="3"><c r="A3"/></row>`]
18
+ */
19
+ export declare function shiftRowIndices(rows: string[], offset: number): string[];
20
+ //# sourceMappingURL=shift-row-indices.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shift-row-indices.d.ts","sourceRoot":"","sources":["../../../src/lib/xml/shift-row-indices.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAsBxE"}
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Adjusts row indices in Excel XML row elements by a specified offset.
3
+ * Handles both row element attributes and cell references within rows.
4
+ *
5
+ * This function is particularly useful when merging sheets or rearranging
6
+ * worksheet content while maintaining proper Excel XML structure.
7
+ *
8
+ * @param {string[]} rows - Array of XML <row> elements as strings
9
+ * @param {number} offset - Numeric value to adjust row indices by:
10
+ * - Positive values shift rows down
11
+ * - Negative values shift rows up
12
+ * @returns {string[]} - New array with modified row elements containing updated indices
13
+ *
14
+ * @example
15
+ * // Shifts rows down by 2 positions
16
+ * shiftRowIndices([`<row r="1"><c r="A1"/></row>`], 2);
17
+ * // Returns: [`<row r="3"><c r="A3"/></row>`]
18
+ */
19
+ export function shiftRowIndices(rows, offset) {
20
+ return rows.map(row => {
21
+ // Process each row element through two replacement phases:
22
+ // 1. Update the row's own index (r="N" attribute)
23
+ let adjustedRow = row.replace(/(<row[^>]*\br=")(\d+)(")/, (_, prefix, rowIndex, suffix) => {
24
+ return `${prefix}${parseInt(rowIndex) + offset}${suffix}`;
25
+ });
26
+ // 2. Update all cell references within the row (r="AN" attributes)
27
+ adjustedRow = adjustedRow.replace(/(<c[^>]*\br=")([A-Z]+)(\d+)(")/g, (_, prefix, columnLetter, cellRowIndex, suffix) => {
28
+ return `${prefix}${columnLetter}${parseInt(cellRowIndex) + offset}${suffix}`;
29
+ });
30
+ return adjustedRow;
31
+ });
32
+ }
@@ -0,0 +1,32 @@
1
+ /// <reference types="node" />
2
+ /// <reference types="node" />
3
+ import { Buffer } from 'node:buffer';
4
+ /**
5
+ * ZIP file signature constants in Buffer format.
6
+ * These magic numbers identify different sections of a ZIP file,
7
+ * as specified in PKWARE's APPNOTE.TXT (ZIP File Format Specification).
8
+ */
9
+ /**
10
+ * Central Directory Header signature (0x504b0102).
11
+ * Marks an entry in the central directory, which contains metadata
12
+ * about all files in the archive.
13
+ * Format: 'PK\01\02'
14
+ * Found in the central directory that appears at the end of the ZIP file.
15
+ */
16
+ export declare const CENTRAL_DIR_HEADER_SIG: Buffer;
17
+ /**
18
+ * End of Central Directory Record signature (0x504b0506).
19
+ * Marks the end of the central directory and contains global information
20
+ * about the ZIP archive.
21
+ * Format: 'PK\05\06'
22
+ * This is the last record in a valid ZIP file.
23
+ */
24
+ export declare const END_OF_CENTRAL_DIR_SIG: Buffer;
25
+ /**
26
+ * Local File Header signature (0x504b0304).
27
+ * Marks the beginning of a file entry within the ZIP archive.
28
+ * Format: 'PK\03\04' (ASCII letters PK followed by version numbers)
29
+ * Appears before each file's compressed data.
30
+ */
31
+ export declare const LOCAL_FILE_HEADER_SIG: Buffer;
32
+ //# sourceMappingURL=constants.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../../src/lib/zip/constants.ts"],"names":[],"mappings":";;AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAErC;;;;GAIG;AAEH;;;;;;GAMG;AACH,eAAO,MAAM,sBAAsB,QAAiC,CAAC;AAErE;;;;;;GAMG;AACH,eAAO,MAAM,sBAAsB,QAAiC,CAAC;AAErE;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,QAAiC,CAAC"}
@@ -0,0 +1,29 @@
1
+ import { Buffer } from 'node:buffer';
2
+ /**
3
+ * ZIP file signature constants in Buffer format.
4
+ * These magic numbers identify different sections of a ZIP file,
5
+ * as specified in PKWARE's APPNOTE.TXT (ZIP File Format Specification).
6
+ */
7
+ /**
8
+ * Central Directory Header signature (0x504b0102).
9
+ * Marks an entry in the central directory, which contains metadata
10
+ * about all files in the archive.
11
+ * Format: 'PK\01\02'
12
+ * Found in the central directory that appears at the end of the ZIP file.
13
+ */
14
+ export const CENTRAL_DIR_HEADER_SIG = Buffer.from('504b0102', 'hex');
15
+ /**
16
+ * End of Central Directory Record signature (0x504b0506).
17
+ * Marks the end of the central directory and contains global information
18
+ * about the ZIP archive.
19
+ * Format: 'PK\05\06'
20
+ * This is the last record in a valid ZIP file.
21
+ */
22
+ export const END_OF_CENTRAL_DIR_SIG = Buffer.from('504b0506', 'hex');
23
+ /**
24
+ * Local File Header signature (0x504b0304).
25
+ * Marks the beginning of a file entry within the ZIP archive.
26
+ * Format: 'PK\03\04' (ASCII letters PK followed by version numbers)
27
+ * Appears before each file's compressed data.
28
+ */
29
+ export const LOCAL_FILE_HEADER_SIG = Buffer.from('504b0304', 'hex');
@@ -0,0 +1,13 @@
1
+ /// <reference types="node" />
2
+ /// <reference types="node" />
3
+ import { Buffer } from 'node:buffer';
4
+ /**
5
+ * Creates a ZIP archive from a collection of files.
6
+ *
7
+ * @param {Object.<string, Buffer|string>} files - An object with file paths as keys and either Buffer or string content as values.
8
+ * @returns {Buffer} - The ZIP archive as a Buffer.
9
+ */
10
+ export declare function create(files: {
11
+ [path: string]: Buffer | string;
12
+ }): Buffer;
13
+ //# sourceMappingURL=create.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"create.d.ts","sourceRoot":"","sources":["../../../src/lib/zip/create.ts"],"names":[],"mappings":";;AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAWrC;;;;;GAKG;AACH,wBAAgB,MAAM,CAAC,KAAK,EAAE;IAAE,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CAAA;CAAE,GAAG,MAAM,CAkFzE"}
@@ -0,0 +1,80 @@
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';
5
+ /**
6
+ * Creates a ZIP archive from a collection of files.
7
+ *
8
+ * @param {Object.<string, Buffer|string>} files - An object with file paths as keys and either Buffer or string content as values.
9
+ * @returns {Buffer} - The ZIP archive as a Buffer.
10
+ */
11
+ export function create(files) {
12
+ const fileEntries = [];
13
+ const centralDirectory = [];
14
+ let offset = 0;
15
+ for (const [filename, rawContent] of Object.entries(files).sort(([a], [b]) => a.localeCompare(b))) {
16
+ if (filename.includes('..')) {
17
+ throw new Error(`Invalid filename: ${filename}`);
18
+ }
19
+ const content = Buffer.isBuffer(rawContent) ? rawContent : Buffer.from(rawContent);
20
+ const fileNameBuf = Buffer.from(filename, 'utf8');
21
+ const modTime = dosTime(new Date());
22
+ const crc = crc32(content);
23
+ const compressed = deflateRawSync(content);
24
+ const compSize = compressed.length;
25
+ const uncompSize = content.length;
26
+ // Local file header
27
+ const localHeader = Buffer.concat([
28
+ LOCAL_FILE_HEADER_SIG,
29
+ toBytes(20, 2),
30
+ toBytes(0, 2),
31
+ toBytes(8, 2),
32
+ modTime,
33
+ toBytes(crc, 4),
34
+ toBytes(compSize, 4),
35
+ toBytes(uncompSize, 4),
36
+ toBytes(fileNameBuf.length, 2),
37
+ toBytes(0, 2),
38
+ ]);
39
+ const localEntry = Buffer.concat([
40
+ localHeader,
41
+ fileNameBuf,
42
+ compressed,
43
+ ]);
44
+ fileEntries.push(localEntry);
45
+ const centralEntry = Buffer.concat([
46
+ Buffer.from(CENTRAL_DIR_HEADER_SIG),
47
+ Buffer.from(toBytes(20, 2)), // Version made by
48
+ Buffer.from(toBytes(20, 2)), // Version needed
49
+ Buffer.from(toBytes(0, 2)), // Flags
50
+ Buffer.from(toBytes(8, 2)), // Compression
51
+ Buffer.from(modTime),
52
+ Buffer.from(toBytes(crc, 4)),
53
+ Buffer.from(toBytes(compSize, 4)),
54
+ Buffer.from(toBytes(uncompSize, 4)),
55
+ Buffer.from(toBytes(fileNameBuf.length, 2)),
56
+ Buffer.from(toBytes(0, 2)), // Extra field length
57
+ Buffer.from(toBytes(0, 2)), // Comment length
58
+ Buffer.from(toBytes(0, 2)), // Disk start
59
+ Buffer.from(toBytes(0, 2)), // Internal attrs
60
+ Buffer.from(toBytes(0, 4)), // External attrs
61
+ Buffer.from(toBytes(offset, 4)),
62
+ fileNameBuf,
63
+ ]);
64
+ centralDirectory.push(centralEntry);
65
+ offset += localEntry.length;
66
+ }
67
+ const centralDirSize = centralDirectory.reduce((sum, entry) => sum + entry.length, 0);
68
+ const centralDirOffset = offset;
69
+ const endRecord = Buffer.concat([
70
+ Buffer.from(END_OF_CENTRAL_DIR_SIG),
71
+ Buffer.from(toBytes(0, 2)), // Disk #
72
+ Buffer.from(toBytes(0, 2)), // Start disk #
73
+ Buffer.from(toBytes(centralDirectory.length, 2)),
74
+ Buffer.from(toBytes(centralDirectory.length, 2)),
75
+ Buffer.from(toBytes(centralDirSize, 4)),
76
+ Buffer.from(toBytes(centralDirOffset, 4)),
77
+ Buffer.from(toBytes(0, 2)), // Comment length
78
+ ]);
79
+ return Buffer.concat(fileEntries.concat(centralDirectory).concat([endRecord]));
80
+ }
@@ -0,0 +1,3 @@
1
+ export * from './create.js';
2
+ export * from './read.js';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/lib/zip/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,WAAW,CAAC"}
@@ -0,0 +1,2 @@
1
+ export * from './create.js';
2
+ export * from './read.js';
@@ -0,0 +1,13 @@
1
+ /// <reference types="node" />
2
+ /// <reference types="node" />
3
+ /**
4
+ * Parses a ZIP archive from a buffer and extracts the files within.
5
+ *
6
+ * @param {Buffer} buffer - The buffer containing the ZIP archive data.
7
+ * @returns {Object.<string, string>} - An object where keys are file names and values are file contents.
8
+ * @throws {Error} - Throws an error if an unsupported compression method is encountered or if decompression fails.
9
+ */
10
+ export declare function read(buffer: Buffer): {
11
+ [s: string]: string;
12
+ };
13
+ //# sourceMappingURL=read.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"read.d.ts","sourceRoot":"","sources":["../../../src/lib/zip/read.ts"],"names":[],"mappings":";;AAEA;;;;;;GAMG;AAEH,wBAAgB,IAAI,CAAC,MAAM,EAAE,MAAM,GAAG;IAAE,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;CAAE,CA+C7D"}
@@ -0,0 +1,53 @@
1
+ import { inflateRawSync } from 'node:zlib';
2
+ /**
3
+ * Parses a ZIP archive from a buffer and extracts the files within.
4
+ *
5
+ * @param {Buffer} buffer - The buffer containing the ZIP archive data.
6
+ * @returns {Object.<string, string>} - An object where keys are file names and values are file contents.
7
+ * @throws {Error} - Throws an error if an unsupported compression method is encountered or if decompression fails.
8
+ */
9
+ export function read(buffer) {
10
+ const files = {};
11
+ let offset = 0;
12
+ while (offset + 4 <= buffer.length) {
13
+ const signature = buffer.readUInt32LE(offset);
14
+ if (signature !== 0x04034b50)
15
+ break;
16
+ const compressionMethod = buffer.readUInt16LE(offset + 8);
17
+ const fileNameLength = buffer.readUInt16LE(offset + 26);
18
+ const extraLength = buffer.readUInt16LE(offset + 28);
19
+ const fileNameStart = offset + 30;
20
+ const fileNameEnd = fileNameStart + fileNameLength;
21
+ const fileName = buffer.subarray(fileNameStart, fileNameEnd).toString();
22
+ const dataStart = fileNameEnd + extraLength;
23
+ let nextOffset = dataStart;
24
+ while (nextOffset + 4 <= buffer.length) {
25
+ if (buffer.readUInt32LE(nextOffset) === 0x04034b50)
26
+ break;
27
+ nextOffset++;
28
+ }
29
+ if (nextOffset + 4 > buffer.length) {
30
+ nextOffset = buffer.length;
31
+ }
32
+ const compressedData = buffer.subarray(dataStart, nextOffset);
33
+ let content = '';
34
+ try {
35
+ if (compressionMethod === 0) {
36
+ content = compressedData.toString();
37
+ }
38
+ else if (compressionMethod === 8) {
39
+ content = inflateRawSync(new Uint8Array(compressedData)).toString();
40
+ }
41
+ else {
42
+ throw new Error(`Unsupported compression method ${compressionMethod}`);
43
+ }
44
+ }
45
+ catch (error) {
46
+ const message = error instanceof Error ? error.message : 'Unknown error';
47
+ throw new Error(`Error unpacking file ${fileName}: ${message}`);
48
+ }
49
+ files[fileName] = content;
50
+ offset = nextOffset;
51
+ }
52
+ return files;
53
+ }
@@ -0,0 +1,61 @@
1
+ /// <reference types="node" />
2
+ /// <reference types="node" />
3
+ import { Buffer } from 'buffer';
4
+ /**
5
+ * Computes a CRC-32 checksum for the given Buffer using the standard IEEE 802.3 polynomial.
6
+ * This implementation uses a precomputed lookup table for optimal performance.
7
+ *
8
+ * The algorithm follows these characteristics:
9
+ * - Polynomial: 0xEDB88320 (reversed representation of 0x04C11DB7)
10
+ * - Initial value: 0xFFFFFFFF (inverted by ~0)
11
+ * - Final XOR value: 0xFFFFFFFF (achieved by inverting the result)
12
+ * - Input and output reflection: Yes
13
+ *
14
+ * @param {Buffer} buf - The input buffer to calculate checksum for
15
+ * @returns {number} - The 32-bit unsigned CRC-32 checksum (0x00000000 to 0xFFFFFFFF)
16
+ */
17
+ export declare function crc32(buf: Buffer): number;
18
+ /**
19
+ * Converts a JavaScript Date object to a 4-byte Buffer in MS-DOS date/time format
20
+ * as specified in the ZIP file format specification (PKZIP APPNOTE.TXT).
21
+ *
22
+ * The MS-DOS date/time format packs both date and time into 4 bytes (32 bits) with
23
+ * the following bit layout:
24
+ *
25
+ * Time portion (2 bytes/16 bits):
26
+ * - Bits 00-04: Seconds divided by 2 (0-29, representing 0-58 seconds)
27
+ * - Bits 05-10: Minutes (0-59)
28
+ * - Bits 11-15: Hours (0-23)
29
+ *
30
+ * Date portion (2 bytes/16 bits):
31
+ * - Bits 00-04: Day (1-31)
32
+ * - Bits 05-08: Month (1-12)
33
+ * - Bits 09-15: Year offset from 1980 (0-127, representing 1980-2107)
34
+ *
35
+ * @param {Date} date - The JavaScript Date object to convert
36
+ * @returns {Buffer} - 4-byte Buffer containing:
37
+ * - Bytes 0-1: DOS time (hours, minutes, seconds/2)
38
+ * - Bytes 2-3: DOS date (year-1980, month, day)
39
+ * @throws {RangeError} - If the date is before 1980 or after 2107
40
+ */
41
+ export declare function dosTime(date: Date): Buffer;
42
+ /**
43
+ * Converts a numeric value into a fixed-length Buffer representation,
44
+ * storing the value in little-endian format with right-padding of zeros.
45
+ *
46
+ * This is particularly useful for binary protocols or file formats that
47
+ * require fixed-width numeric fields.
48
+ *
49
+ * @param {number} value - The numeric value to convert to bytes.
50
+ * Note: JavaScript numbers are IEEE 754 doubles, but only the
51
+ * integer portion will be used (up to 53-bit precision).
52
+ * @param {number} len - The desired length of the output Buffer in bytes.
53
+ * Must be a positive integer.
54
+ * @returns {Buffer} - A new Buffer of exactly `len` bytes containing:
55
+ * 1. The value's bytes in little-endian order (least significant byte first)
56
+ * 2. Zero padding in any remaining higher-order bytes
57
+ * @throws {RangeError} - If the value requires more bytes than `len` to represent
58
+ * (though this is currently not explicitly checked)
59
+ */
60
+ export declare function toBytes(value: number, len: number): Buffer;
61
+ //# sourceMappingURL=utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../../src/lib/zip/utils.ts"],"names":[],"mappings":";;AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAyChC;;;;;;;;;;;;GAYG;AACH,wBAAgB,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CA2BzC;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,OAAO,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,CAyB1C;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAkB1D"}
@@ -0,0 +1,152 @@
1
+ import { Buffer } from 'buffer';
2
+ /**
3
+ * Precomputed CRC-32 lookup table for optimized checksum calculation.
4
+ * The table is generated using the standard IEEE 802.3 (Ethernet) polynomial:
5
+ * 0xEDB88320 (reversed representation of 0x04C11DB7).
6
+ *
7
+ * The table is immediately invoked and cached as a constant for performance,
8
+ * following the common implementation pattern for CRC algorithms.
9
+ */
10
+ const crcTable = (() => {
11
+ // Create a typed array for better performance with 256 32-bit unsigned integers
12
+ const table = new Uint32Array(256);
13
+ // Generate table entries for all possible byte values (0-255)
14
+ for (let i = 0; i < 256; i++) {
15
+ let crc = i; // Initialize with current byte value
16
+ // Process each bit (8 times)
17
+ for (let j = 0; j < 8; j++) {
18
+ /*
19
+ * CRC division algorithm:
20
+ * 1. If LSB is set (crc & 1), XOR with polynomial
21
+ * 2. Right-shift by 1 (unsigned)
22
+ *
23
+ * The polynomial 0xEDB88320 is:
24
+ * - Bit-reversed version of 0x04C11DB7
25
+ * - Uses reflected input/output algorithm
26
+ */
27
+ crc = crc & 1
28
+ ? 0xedb88320 ^ (crc >>> 1) // XOR with polynomial if LSB is set
29
+ : crc >>> 1; // Just shift right if LSB is not set
30
+ }
31
+ // Store final 32-bit value (>>> 0 ensures unsigned 32-bit representation)
32
+ table[i] = crc >>> 0;
33
+ }
34
+ return table;
35
+ })();
36
+ /**
37
+ * Computes a CRC-32 checksum for the given Buffer using the standard IEEE 802.3 polynomial.
38
+ * This implementation uses a precomputed lookup table for optimal performance.
39
+ *
40
+ * The algorithm follows these characteristics:
41
+ * - Polynomial: 0xEDB88320 (reversed representation of 0x04C11DB7)
42
+ * - Initial value: 0xFFFFFFFF (inverted by ~0)
43
+ * - Final XOR value: 0xFFFFFFFF (achieved by inverting the result)
44
+ * - Input and output reflection: Yes
45
+ *
46
+ * @param {Buffer} buf - The input buffer to calculate checksum for
47
+ * @returns {number} - The 32-bit unsigned CRC-32 checksum (0x00000000 to 0xFFFFFFFF)
48
+ */
49
+ export function crc32(buf) {
50
+ // Initialize CRC with all 1's (0xFFFFFFFF) using bitwise NOT
51
+ let crc = ~0;
52
+ // Process each byte in the buffer
53
+ for (let i = 0; i < buf.length; i++) {
54
+ /*
55
+ * CRC update algorithm steps:
56
+ * 1. XOR current CRC with next byte (lowest 8 bits)
57
+ * 2. Use result as index in precomputed table (0-255)
58
+ * 3. XOR the table value with right-shifted CRC (8 bits)
59
+ *
60
+ * The operation breakdown:
61
+ * - (crc ^ buf[i]) - XOR with next byte
62
+ * - & 0xff - Isolate lowest 8 bits
63
+ * - crc >>> 8 - Shift CRC right by 8 bits (unsigned)
64
+ * - ^ crcTable[...] - XOR with precomputed table value
65
+ */
66
+ crc = (crc >>> 8) ^ crcTable[(crc ^ buf[i]) & 0xff];
67
+ }
68
+ /*
69
+ * Final processing:
70
+ * 1. Invert all bits (~crc) to match standard CRC-32 output
71
+ * 2. Convert to unsigned 32-bit integer (>>> 0)
72
+ */
73
+ return ~crc >>> 0;
74
+ }
75
+ /**
76
+ * Converts a JavaScript Date object to a 4-byte Buffer in MS-DOS date/time format
77
+ * as specified in the ZIP file format specification (PKZIP APPNOTE.TXT).
78
+ *
79
+ * The MS-DOS date/time format packs both date and time into 4 bytes (32 bits) with
80
+ * the following bit layout:
81
+ *
82
+ * Time portion (2 bytes/16 bits):
83
+ * - Bits 00-04: Seconds divided by 2 (0-29, representing 0-58 seconds)
84
+ * - Bits 05-10: Minutes (0-59)
85
+ * - Bits 11-15: Hours (0-23)
86
+ *
87
+ * Date portion (2 bytes/16 bits):
88
+ * - Bits 00-04: Day (1-31)
89
+ * - Bits 05-08: Month (1-12)
90
+ * - Bits 09-15: Year offset from 1980 (0-127, representing 1980-2107)
91
+ *
92
+ * @param {Date} date - The JavaScript Date object to convert
93
+ * @returns {Buffer} - 4-byte Buffer containing:
94
+ * - Bytes 0-1: DOS time (hours, minutes, seconds/2)
95
+ * - Bytes 2-3: DOS date (year-1980, month, day)
96
+ * @throws {RangeError} - If the date is before 1980 or after 2107
97
+ */
98
+ export function dosTime(date) {
99
+ // Pack time components into 2 bytes (16 bits):
100
+ // - Hours (5 bits) shifted left 11 positions (bits 11-15)
101
+ // - Minutes (6 bits) shifted left 5 positions (bits 5-10)
102
+ // - Seconds/2 (5 bits) in least significant bits (bits 0-4)
103
+ const time = (date.getHours() << 11) | // Hours occupy bits 11-15
104
+ (date.getMinutes() << 5) | // Minutes occupy bits 5-10
105
+ (Math.floor(date.getSeconds() / 2)); // Seconds/2 occupy bits 0-4
106
+ // Pack date components into 2 bytes (16 bits):
107
+ // - (Year-1980) (7 bits) shifted left 9 positions (bits 9-15)
108
+ // - Month (4 bits) shifted left 5 positions (bits 5-8)
109
+ // - Day (5 bits) in least significant bits (bits 0-4)
110
+ const day = ((date.getFullYear() - 1980) << 9) | // Years since 1980 (bits 9-15)
111
+ ((date.getMonth() + 1) << 5) | // Month 1-12 (bits 5-8)
112
+ date.getDate(); // Day 1-31 (bits 0-4)
113
+ // Combine both 2-byte values into a single 4-byte Buffer
114
+ // Note: Using little-endian byte order for each 2-byte segment
115
+ return Buffer.from([
116
+ ...toBytes(time, 2), // Convert time to 2 bytes (LSB first)
117
+ ...toBytes(day, 2), // Convert date to 2 bytes (LSB first)
118
+ ]);
119
+ }
120
+ /**
121
+ * Converts a numeric value into a fixed-length Buffer representation,
122
+ * storing the value in little-endian format with right-padding of zeros.
123
+ *
124
+ * This is particularly useful for binary protocols or file formats that
125
+ * require fixed-width numeric fields.
126
+ *
127
+ * @param {number} value - The numeric value to convert to bytes.
128
+ * Note: JavaScript numbers are IEEE 754 doubles, but only the
129
+ * integer portion will be used (up to 53-bit precision).
130
+ * @param {number} len - The desired length of the output Buffer in bytes.
131
+ * Must be a positive integer.
132
+ * @returns {Buffer} - A new Buffer of exactly `len` bytes containing:
133
+ * 1. The value's bytes in little-endian order (least significant byte first)
134
+ * 2. Zero padding in any remaining higher-order bytes
135
+ * @throws {RangeError} - If the value requires more bytes than `len` to represent
136
+ * (though this is currently not explicitly checked)
137
+ */
138
+ export function toBytes(value, len) {
139
+ // Allocate a new Buffer of the requested length, automatically zero-filled
140
+ const buf = Buffer.alloc(len);
141
+ // Process each byte position from least significant to most significant
142
+ for (let i = 0; i < len; i++) {
143
+ // Store the least significant byte of the current value
144
+ buf[i] = value & 0xff; // Mask to get bottom 8 bits
145
+ // Right-shift the value by 8 bits to process the next byte
146
+ // Note: This uses unsigned right shift (>>> would be signed)
147
+ value >>= 8;
148
+ // If the loop completes with value != 0, we've overflowed the buffer length,
149
+ // but this isn't currently checked/handled
150
+ }
151
+ return buf;
152
+ }
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "@js-ak/excel-toolbox",
3
+ "version": "1.0.0",
4
+ "description": "excel-toolbox",
5
+ "publishConfig": {
6
+ "access": "public",
7
+ "registry": "https://registry.npmjs.org/"
8
+ },
9
+ "type": "module",
10
+ "main": "build/index.js",
11
+ "types": "build/index.d.ts",
12
+ "files": [
13
+ "build/lib",
14
+ "build/index.js",
15
+ "build/index.d.ts",
16
+ "LICENSE",
17
+ "README.md",
18
+ "package.json"
19
+ ],
20
+ "scripts": {
21
+ "build": "tsc",
22
+ "lint": "eslint . --ext .ts",
23
+ "test": "npm run build && node ./build/test/index.js"
24
+ },
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "git+https://github.com/JS-AK/excel-toolbox.git"
28
+ },
29
+ "keywords": [
30
+ "excel-toolbox"
31
+ ],
32
+ "author": "JS-AK",
33
+ "license": "MIT",
34
+ "bugs": {
35
+ "url": "https://github.com/JS-AK/excel-toolbox/issues"
36
+ },
37
+ "homepage": "https://github.com/JS-AK/excel-toolbox#readme",
38
+ "devDependencies": {
39
+ "@semantic-release/changelog": "6.0.3",
40
+ "@semantic-release/commit-analyzer": "13.0.0",
41
+ "@semantic-release/git": "10.0.1",
42
+ "@semantic-release/github": "10.0.6",
43
+ "@semantic-release/npm": "12.0.1",
44
+ "@semantic-release/release-notes-generator": "14.0.0",
45
+ "@types/node": "22.14.0",
46
+ "@types/pako": "2.0.3",
47
+ "eslint": "9.24.0",
48
+ "eslint-plugin-sort-destructure-keys": "2.0.0",
49
+ "eslint-plugin-sort-exports": "0.9.1",
50
+ "globals": "16.0.0",
51
+ "semantic-release": "24.0.0",
52
+ "typescript": "5.4.5",
53
+ "typescript-eslint": "8.29.0"
54
+ },
55
+ "dependencies": {
56
+ "pako": "2.1.0"
57
+ }
58
+ }