@cj-tech-master/excelts 1.4.2 → 1.4.4
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.
- package/README.md +3 -3
- package/README_zh.md +3 -3
- package/dist/browser/excelts.iife.js +8135 -2722
- package/dist/browser/excelts.iife.js.map +1 -1
- package/dist/browser/excelts.iife.min.js +86 -23
- package/dist/cjs/stream/xlsx/workbook-writer.js +3 -2
- package/dist/cjs/utils/cell-format.js +13 -9
- package/dist/cjs/utils/sheet-utils.js +125 -15
- package/dist/cjs/utils/unzip/extract.js +166 -0
- package/dist/cjs/utils/unzip/index.js +7 -1
- package/dist/cjs/utils/xml-stream.js +25 -3
- package/dist/cjs/utils/zip/compress.js +261 -0
- package/dist/cjs/utils/zip/crc32.js +154 -0
- package/dist/cjs/utils/zip/index.js +70 -0
- package/dist/cjs/utils/zip/zip-builder.js +378 -0
- package/dist/cjs/utils/zip-stream.js +30 -34
- package/dist/cjs/xlsx/xform/book/defined-name-xform.js +36 -2
- package/dist/cjs/xlsx/xform/list-xform.js +6 -0
- package/dist/cjs/xlsx/xform/sheet/cell-xform.js +6 -1
- package/dist/cjs/xlsx/xform/sheet/row-xform.js +24 -2
- package/dist/cjs/xlsx/xform/table/filter-column-xform.js +4 -0
- package/dist/esm/stream/xlsx/workbook-writer.js +3 -2
- package/dist/esm/utils/cell-format.js +13 -9
- package/dist/esm/utils/sheet-utils.js +125 -15
- package/dist/esm/utils/unzip/extract.js +160 -0
- package/dist/esm/utils/unzip/index.js +2 -0
- package/dist/esm/utils/xml-stream.js +25 -3
- package/dist/esm/utils/zip/compress.js +220 -0
- package/dist/esm/utils/zip/crc32.js +116 -0
- package/dist/esm/utils/zip/index.js +55 -0
- package/dist/esm/utils/zip/zip-builder.js +372 -0
- package/dist/esm/utils/zip-stream.js +30 -34
- package/dist/esm/xlsx/xform/book/defined-name-xform.js +36 -2
- package/dist/esm/xlsx/xform/list-xform.js +6 -0
- package/dist/esm/xlsx/xform/sheet/cell-xform.js +6 -1
- package/dist/esm/xlsx/xform/sheet/row-xform.js +24 -2
- package/dist/esm/xlsx/xform/table/filter-column-xform.js +4 -0
- package/dist/types/utils/sheet-utils.d.ts +8 -2
- package/dist/types/utils/unzip/extract.d.ts +92 -0
- package/dist/types/utils/unzip/index.d.ts +1 -0
- package/dist/types/utils/xml-stream.d.ts +2 -0
- package/dist/types/utils/zip/compress.d.ts +83 -0
- package/dist/types/utils/zip/crc32.d.ts +55 -0
- package/dist/types/utils/zip/index.d.ts +52 -0
- package/dist/types/utils/zip/zip-builder.d.ts +110 -0
- package/dist/types/utils/zip-stream.d.ts +6 -12
- package/dist/types/xlsx/xform/list-xform.d.ts +1 -0
- package/dist/types/xlsx/xform/sheet/row-xform.d.ts +2 -0
- package/package.json +1 -1
|
@@ -4,30 +4,140 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { Workbook } from "../doc/workbook.js";
|
|
6
6
|
import { colCache } from "./col-cache.js";
|
|
7
|
-
import { dateToExcel } from "./utils.js";
|
|
8
7
|
import { format as cellFormat } from "./cell-format.js";
|
|
8
|
+
/**
|
|
9
|
+
* Convert a Date object back to Excel serial number without timezone issues.
|
|
10
|
+
* This reverses the excelToDate conversion exactly.
|
|
11
|
+
* excelToDate uses: new Date(Math.round((v - 25569) * 24 * 3600 * 1000))
|
|
12
|
+
* So we reverse it: (date.getTime() / (24 * 3600 * 1000)) + 25569
|
|
13
|
+
*/
|
|
14
|
+
function dateToExcelSerial(d) {
|
|
15
|
+
return d.getTime() / (24 * 3600 * 1000) + 25569;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Check if format is a pure time format (no date components like y, m for month, d)
|
|
19
|
+
* Time formats only contain: h, m (minutes in time context), s, AM/PM
|
|
20
|
+
* Excludes elapsed time formats like [h]:mm:ss which should keep full serial number
|
|
21
|
+
*/
|
|
22
|
+
function isTimeOnlyFormat(fmt) {
|
|
23
|
+
// Remove quoted strings first
|
|
24
|
+
const cleaned = fmt.replace(/"[^"]*"/g, "");
|
|
25
|
+
// Elapsed time formats [h], [m], [s] should NOT be treated as time-only
|
|
26
|
+
// They need the full serial number to calculate total hours/minutes/seconds
|
|
27
|
+
if (/\[[hms]\]/i.test(cleaned)) {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
// Remove color codes and conditions (but we already checked for [h], [m], [s])
|
|
31
|
+
const withoutBrackets = cleaned.replace(/\[[^\]]*\]/g, "");
|
|
32
|
+
// Check if it has time components (h, s, or AM/PM)
|
|
33
|
+
const hasTimeComponents = /[hs]/i.test(withoutBrackets) || /AM\/PM|A\/P/i.test(withoutBrackets);
|
|
34
|
+
// Check if it has date components (y, d, or m not adjacent to h/s which would make it minutes)
|
|
35
|
+
// In Excel: "m" after "h" or before "s" is minutes, otherwise it's month
|
|
36
|
+
const hasDateComponents = /[yd]/i.test(withoutBrackets);
|
|
37
|
+
// If it has time but no date components, it's a time-only format
|
|
38
|
+
// Also check for standalone 'm' that's not minutes (not near h or s)
|
|
39
|
+
if (hasDateComponents) {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
// Check for month 'm' - if 'm' exists but not in h:m or m:s context, it's a date format
|
|
43
|
+
if (/m/i.test(withoutBrackets) && !hasTimeComponents) {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
return hasTimeComponents;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Check if format is a date format (contains y, d, or month-m)
|
|
50
|
+
* Used to determine if dateFormat override should be applied
|
|
51
|
+
*/
|
|
52
|
+
function isDateFormat(fmt) {
|
|
53
|
+
// Remove quoted strings first
|
|
54
|
+
const cleaned = fmt.replace(/"[^"]*"/g, "");
|
|
55
|
+
// Elapsed time formats [h], [m], [s] are NOT date formats
|
|
56
|
+
if (/\[[hms]\]/i.test(cleaned)) {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
// Remove color codes and conditions
|
|
60
|
+
const withoutBrackets = cleaned.replace(/\[[^\]]*\]/g, "");
|
|
61
|
+
// Check for year or day components
|
|
62
|
+
if (/[yd]/i.test(withoutBrackets)) {
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
// Check for month 'm' - only if it's NOT in time context (not near h or s)
|
|
66
|
+
// In Excel: "m" after "h" or before "s" is minutes, otherwise it's month
|
|
67
|
+
if (/m/i.test(withoutBrackets)) {
|
|
68
|
+
const hasTimeComponents = /[hs]/i.test(withoutBrackets) || /AM\/PM|A\/P/i.test(withoutBrackets);
|
|
69
|
+
// If no time components, 'm' is month
|
|
70
|
+
if (!hasTimeComponents) {
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
// If has time components, need to check if 'm' is month or minutes
|
|
74
|
+
// Simplified: if format has both date-like and time-like patterns, consider it a date format
|
|
75
|
+
// e.g., "m/d/yy h:mm" - has 'm' as month and 'mm' as minutes
|
|
76
|
+
}
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Format a value (Date, number, boolean, string) according to the given format
|
|
81
|
+
* Handles timezone-independent conversion for Date objects
|
|
82
|
+
* @param value - The value to format
|
|
83
|
+
* @param fmt - The format string to use
|
|
84
|
+
* @param dateFormat - Optional override format for date values (not applied to time or elapsed time formats)
|
|
85
|
+
*/
|
|
86
|
+
function formatValue(value, fmt, dateFormat) {
|
|
87
|
+
// Date object - convert back to Excel serial number
|
|
88
|
+
if (value instanceof Date) {
|
|
89
|
+
let serial = dateToExcelSerial(value);
|
|
90
|
+
// For time-only formats, use only the fractional part (time portion)
|
|
91
|
+
if (isTimeOnlyFormat(fmt)) {
|
|
92
|
+
serial = serial % 1;
|
|
93
|
+
if (serial < 0) {
|
|
94
|
+
serial += 1;
|
|
95
|
+
}
|
|
96
|
+
return cellFormat(fmt, serial);
|
|
97
|
+
}
|
|
98
|
+
// Only apply dateFormat override to actual date formats
|
|
99
|
+
// (not elapsed time formats like [h]:mm:ss)
|
|
100
|
+
const actualFmt = dateFormat && isDateFormat(fmt) ? dateFormat : fmt;
|
|
101
|
+
return cellFormat(actualFmt, serial);
|
|
102
|
+
}
|
|
103
|
+
// Number/Boolean/String - let cellFormat handle it
|
|
104
|
+
return cellFormat(fmt, value);
|
|
105
|
+
}
|
|
9
106
|
/**
|
|
10
107
|
* Get formatted display text for a cell value
|
|
11
108
|
* Returns the value formatted according to the cell's numFmt
|
|
12
|
-
* This matches Excel's display exactly
|
|
109
|
+
* This matches Excel's display exactly (timezone-independent)
|
|
110
|
+
* @param cell - The cell to get display text for
|
|
111
|
+
* @param dateFormat - Optional override format for date values
|
|
13
112
|
*/
|
|
14
|
-
function getCellDisplayText(cell) {
|
|
113
|
+
function getCellDisplayText(cell, dateFormat) {
|
|
15
114
|
const value = cell.value;
|
|
16
115
|
const fmt = cell.numFmt || "General";
|
|
17
116
|
// Null/undefined
|
|
18
117
|
if (value == null) {
|
|
19
118
|
return "";
|
|
20
119
|
}
|
|
21
|
-
// Date
|
|
22
|
-
if (value instanceof Date
|
|
23
|
-
|
|
24
|
-
|
|
120
|
+
// Date/Number/Boolean/String - format directly
|
|
121
|
+
if (value instanceof Date ||
|
|
122
|
+
typeof value === "number" ||
|
|
123
|
+
typeof value === "boolean" ||
|
|
124
|
+
typeof value === "string") {
|
|
125
|
+
return formatValue(value, fmt, dateFormat);
|
|
25
126
|
}
|
|
26
|
-
//
|
|
27
|
-
if (typeof value === "
|
|
28
|
-
|
|
127
|
+
// Formula type - use the result value
|
|
128
|
+
if (typeof value === "object" && "formula" in value) {
|
|
129
|
+
const result = value.result;
|
|
130
|
+
if (result == null) {
|
|
131
|
+
return "";
|
|
132
|
+
}
|
|
133
|
+
if (result instanceof Date ||
|
|
134
|
+
typeof result === "number" ||
|
|
135
|
+
typeof result === "boolean" ||
|
|
136
|
+
typeof result === "string") {
|
|
137
|
+
return formatValue(result, fmt, dateFormat);
|
|
138
|
+
}
|
|
29
139
|
}
|
|
30
|
-
// Fallback to cell.text for other types (rich text, hyperlink, error,
|
|
140
|
+
// Fallback to cell.text for other types (rich text, hyperlink, error, etc.)
|
|
31
141
|
return cell.text;
|
|
32
142
|
}
|
|
33
143
|
// =============================================================================
|
|
@@ -263,7 +373,7 @@ export function sheetToJson(worksheet, opts) {
|
|
|
263
373
|
let isEmpty = true;
|
|
264
374
|
for (let col = startCol; col <= endCol; col++) {
|
|
265
375
|
const cell = worksheet.getCell(row, col);
|
|
266
|
-
const val = o.raw === false ? getCellDisplayText(cell).trim() : cell.value;
|
|
376
|
+
const val = o.raw === false ? getCellDisplayText(cell, o.dateFormat).trim() : cell.value;
|
|
267
377
|
if (val != null && val !== "") {
|
|
268
378
|
rowData[col - startCol] = val;
|
|
269
379
|
isEmpty = false;
|
|
@@ -291,7 +401,7 @@ export function sheetToJson(worksheet, opts) {
|
|
|
291
401
|
let isEmpty = true;
|
|
292
402
|
for (let col = startCol; col <= endCol; col++) {
|
|
293
403
|
const cell = worksheet.getCell(row, col);
|
|
294
|
-
const val = o.raw === false ? getCellDisplayText(cell).trim() : cell.value;
|
|
404
|
+
const val = o.raw === false ? getCellDisplayText(cell, o.dateFormat).trim() : cell.value;
|
|
295
405
|
const key = encodeCol(col - 1); // 0-indexed for encodeCol
|
|
296
406
|
if (val != null && val !== "") {
|
|
297
407
|
rowData[key] = val;
|
|
@@ -318,7 +428,7 @@ export function sheetToJson(worksheet, opts) {
|
|
|
318
428
|
const colIdx = col - startCol;
|
|
319
429
|
const key = headerOpt[colIdx] ?? `__EMPTY_${colIdx}`;
|
|
320
430
|
const cell = worksheet.getCell(row, col);
|
|
321
|
-
const val = o.raw === false ? getCellDisplayText(cell).trim() : cell.value;
|
|
431
|
+
const val = o.raw === false ? getCellDisplayText(cell, o.dateFormat).trim() : cell.value;
|
|
322
432
|
if (val != null && val !== "") {
|
|
323
433
|
rowData[key] = val;
|
|
324
434
|
isEmpty = false;
|
|
@@ -360,7 +470,7 @@ export function sheetToJson(worksheet, opts) {
|
|
|
360
470
|
let isEmpty = true;
|
|
361
471
|
for (let col = startCol; col <= endCol; col++) {
|
|
362
472
|
const cell = worksheet.getCell(row, col);
|
|
363
|
-
const val = o.raw === false ? getCellDisplayText(cell).trim() : cell.value;
|
|
473
|
+
const val = o.raw === false ? getCellDisplayText(cell, o.dateFormat).trim() : cell.value;
|
|
364
474
|
const key = headers[col - startCol];
|
|
365
475
|
if (val != null && val !== "") {
|
|
366
476
|
rowData[key] = val;
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simple ZIP extraction utilities
|
|
3
|
+
* Provides easy-to-use Promise-based API for extracting ZIP files
|
|
4
|
+
*/
|
|
5
|
+
import { Readable } from "stream";
|
|
6
|
+
import { createParse } from "./parse.js";
|
|
7
|
+
/**
|
|
8
|
+
* Extract all files from a ZIP buffer
|
|
9
|
+
*
|
|
10
|
+
* @param zipData - ZIP file data as Buffer or Uint8Array
|
|
11
|
+
* @returns Map of file paths to their content
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```ts
|
|
15
|
+
* import { extractAll } from "./utils/unzip/extract.js";
|
|
16
|
+
*
|
|
17
|
+
* const zipData = fs.readFileSync("archive.zip");
|
|
18
|
+
* const files = await extractAll(zipData);
|
|
19
|
+
*
|
|
20
|
+
* for (const [path, file] of files) {
|
|
21
|
+
* console.log(`${path}: ${file.data.length} bytes`);
|
|
22
|
+
* }
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export async function extractAll(zipData) {
|
|
26
|
+
const files = new Map();
|
|
27
|
+
const buffer = Buffer.isBuffer(zipData) ? zipData : Buffer.from(zipData);
|
|
28
|
+
const parse = createParse({ forceStream: true });
|
|
29
|
+
const stream = Readable.from([buffer]);
|
|
30
|
+
stream.pipe(parse);
|
|
31
|
+
for await (const entry of parse) {
|
|
32
|
+
const zipEntry = entry;
|
|
33
|
+
const isDirectory = zipEntry.type === "Directory";
|
|
34
|
+
if (isDirectory) {
|
|
35
|
+
files.set(zipEntry.path, {
|
|
36
|
+
path: zipEntry.path,
|
|
37
|
+
data: Buffer.alloc(0),
|
|
38
|
+
isDirectory: true,
|
|
39
|
+
size: 0
|
|
40
|
+
});
|
|
41
|
+
zipEntry.autodrain();
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
const data = await zipEntry.buffer();
|
|
45
|
+
files.set(zipEntry.path, {
|
|
46
|
+
path: zipEntry.path,
|
|
47
|
+
data,
|
|
48
|
+
isDirectory: false,
|
|
49
|
+
size: data.length
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return files;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Extract a single file from a ZIP buffer
|
|
57
|
+
*
|
|
58
|
+
* @param zipData - ZIP file data as Buffer or Uint8Array
|
|
59
|
+
* @param filePath - Path of the file to extract
|
|
60
|
+
* @returns File content as Buffer, or null if not found
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* ```ts
|
|
64
|
+
* import { extractFile } from "./utils/unzip/extract.js";
|
|
65
|
+
*
|
|
66
|
+
* const zipData = fs.readFileSync("archive.zip");
|
|
67
|
+
* const content = await extractFile(zipData, "readme.txt");
|
|
68
|
+
* if (content) {
|
|
69
|
+
* console.log(content.toString("utf-8"));
|
|
70
|
+
* }
|
|
71
|
+
* ```
|
|
72
|
+
*/
|
|
73
|
+
export async function extractFile(zipData, filePath) {
|
|
74
|
+
const buffer = Buffer.isBuffer(zipData) ? zipData : Buffer.from(zipData);
|
|
75
|
+
const parse = createParse({ forceStream: true });
|
|
76
|
+
const stream = Readable.from([buffer]);
|
|
77
|
+
stream.pipe(parse);
|
|
78
|
+
for await (const entry of parse) {
|
|
79
|
+
const zipEntry = entry;
|
|
80
|
+
if (zipEntry.path === filePath) {
|
|
81
|
+
if (zipEntry.type === "Directory") {
|
|
82
|
+
return Buffer.alloc(0);
|
|
83
|
+
}
|
|
84
|
+
return zipEntry.buffer();
|
|
85
|
+
}
|
|
86
|
+
zipEntry.autodrain();
|
|
87
|
+
}
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* List all file paths in a ZIP buffer (without extracting content)
|
|
92
|
+
*
|
|
93
|
+
* @param zipData - ZIP file data as Buffer or Uint8Array
|
|
94
|
+
* @returns Array of file paths
|
|
95
|
+
*
|
|
96
|
+
* @example
|
|
97
|
+
* ```ts
|
|
98
|
+
* import { listFiles } from "./utils/unzip/extract.js";
|
|
99
|
+
*
|
|
100
|
+
* const zipData = fs.readFileSync("archive.zip");
|
|
101
|
+
* const paths = await listFiles(zipData);
|
|
102
|
+
* console.log(paths); // ["file1.txt", "folder/file2.txt", ...]
|
|
103
|
+
* ```
|
|
104
|
+
*/
|
|
105
|
+
export async function listFiles(zipData) {
|
|
106
|
+
const paths = [];
|
|
107
|
+
const buffer = Buffer.isBuffer(zipData) ? zipData : Buffer.from(zipData);
|
|
108
|
+
const parse = createParse({ forceStream: true });
|
|
109
|
+
const stream = Readable.from([buffer]);
|
|
110
|
+
stream.pipe(parse);
|
|
111
|
+
for await (const entry of parse) {
|
|
112
|
+
const zipEntry = entry;
|
|
113
|
+
paths.push(zipEntry.path);
|
|
114
|
+
zipEntry.autodrain();
|
|
115
|
+
}
|
|
116
|
+
return paths;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Iterate over ZIP entries with a callback (memory efficient for large ZIPs)
|
|
120
|
+
*
|
|
121
|
+
* @param zipData - ZIP file data as Buffer or Uint8Array
|
|
122
|
+
* @param callback - Async callback for each entry, return false to stop iteration
|
|
123
|
+
*
|
|
124
|
+
* @example
|
|
125
|
+
* ```ts
|
|
126
|
+
* import { forEachEntry } from "./utils/unzip/extract.js";
|
|
127
|
+
*
|
|
128
|
+
* await forEachEntry(zipData, async (path, getData) => {
|
|
129
|
+
* if (path.endsWith(".xml")) {
|
|
130
|
+
* const content = await getData();
|
|
131
|
+
* console.log(content.toString("utf-8"));
|
|
132
|
+
* }
|
|
133
|
+
* return true; // continue iteration
|
|
134
|
+
* });
|
|
135
|
+
* ```
|
|
136
|
+
*/
|
|
137
|
+
export async function forEachEntry(zipData, callback) {
|
|
138
|
+
const buffer = Buffer.isBuffer(zipData) ? zipData : Buffer.from(zipData);
|
|
139
|
+
const parse = createParse({ forceStream: true });
|
|
140
|
+
const stream = Readable.from([buffer]);
|
|
141
|
+
stream.pipe(parse);
|
|
142
|
+
for await (const entry of parse) {
|
|
143
|
+
const zipEntry = entry;
|
|
144
|
+
let dataPromise = null;
|
|
145
|
+
const getData = () => {
|
|
146
|
+
if (!dataPromise) {
|
|
147
|
+
dataPromise = zipEntry.buffer();
|
|
148
|
+
}
|
|
149
|
+
return dataPromise;
|
|
150
|
+
};
|
|
151
|
+
const shouldContinue = await callback(zipEntry.path, getData, zipEntry);
|
|
152
|
+
// If callback didn't read data, drain it
|
|
153
|
+
if (!dataPromise) {
|
|
154
|
+
zipEntry.autodrain();
|
|
155
|
+
}
|
|
156
|
+
if (shouldContinue === false) {
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
@@ -10,3 +10,5 @@ export { bufferStream } from "./buffer-stream.js";
|
|
|
10
10
|
export { parse as parseBuffer } from "./parse-buffer.js";
|
|
11
11
|
export { parseDateTime } from "./parse-datetime.js";
|
|
12
12
|
export { parseExtraField } from "./parse-extra-field.js";
|
|
13
|
+
// Simple extraction API
|
|
14
|
+
export { extractAll, extractFile, listFiles, forEachEntry } from "./extract.js";
|
|
@@ -4,6 +4,8 @@ const OPEN_ANGLE = "<";
|
|
|
4
4
|
const CLOSE_ANGLE = ">";
|
|
5
5
|
const OPEN_ANGLE_SLASH = "</";
|
|
6
6
|
const CLOSE_SLASH_ANGLE = "/>";
|
|
7
|
+
// Chunk size for periodic consolidation (reduces final join overhead)
|
|
8
|
+
const CHUNK_SIZE = 10000;
|
|
7
9
|
function pushAttribute(xml, name, value) {
|
|
8
10
|
xml.push(` ${name}="${xmlEncode(value.toString())}"`);
|
|
9
11
|
}
|
|
@@ -21,15 +23,23 @@ function pushAttributes(xml, attributes) {
|
|
|
21
23
|
class XmlStream {
|
|
22
24
|
constructor() {
|
|
23
25
|
this._xml = [];
|
|
26
|
+
this._chunks = [];
|
|
24
27
|
this._stack = [];
|
|
25
28
|
this._rollbacks = [];
|
|
26
29
|
}
|
|
30
|
+
_consolidate() {
|
|
31
|
+
// Periodically join small strings into larger chunks to reduce final join overhead
|
|
32
|
+
if (this._xml.length >= CHUNK_SIZE) {
|
|
33
|
+
this._chunks.push(this._xml.join(""));
|
|
34
|
+
this._xml = [];
|
|
35
|
+
}
|
|
36
|
+
}
|
|
27
37
|
get tos() {
|
|
28
38
|
return this._stack.length ? this._stack[this._stack.length - 1] : undefined;
|
|
29
39
|
}
|
|
30
40
|
get cursor() {
|
|
31
41
|
// handy way to track whether anything has been added
|
|
32
|
-
return this._xml.length;
|
|
42
|
+
return this._chunks.length * CHUNK_SIZE + this._xml.length;
|
|
33
43
|
}
|
|
34
44
|
openXml(docAttributes) {
|
|
35
45
|
const xml = this._xml;
|
|
@@ -96,6 +106,7 @@ class XmlStream {
|
|
|
96
106
|
}
|
|
97
107
|
this.open = false;
|
|
98
108
|
this.leaf = false;
|
|
109
|
+
this._consolidate();
|
|
99
110
|
}
|
|
100
111
|
leafNode(name, attributes, text) {
|
|
101
112
|
this.openNode(name, attributes);
|
|
@@ -115,7 +126,8 @@ class XmlStream {
|
|
|
115
126
|
xml: this._xml.length,
|
|
116
127
|
stack: this._stack.length,
|
|
117
128
|
leaf: this.leaf,
|
|
118
|
-
open: this.open
|
|
129
|
+
open: this.open,
|
|
130
|
+
chunksLength: this._chunks.length
|
|
119
131
|
});
|
|
120
132
|
return this.cursor;
|
|
121
133
|
}
|
|
@@ -130,12 +142,22 @@ class XmlStream {
|
|
|
130
142
|
if (this._stack.length > r.stack) {
|
|
131
143
|
this._stack.splice(r.stack, this._stack.length - r.stack);
|
|
132
144
|
}
|
|
145
|
+
if (this._chunks.length > r.chunksLength) {
|
|
146
|
+
this._chunks.splice(r.chunksLength, this._chunks.length - r.chunksLength);
|
|
147
|
+
}
|
|
133
148
|
this.leaf = r.leaf;
|
|
134
149
|
this.open = r.open;
|
|
135
150
|
}
|
|
136
151
|
get xml() {
|
|
137
152
|
this.closeAll();
|
|
138
|
-
|
|
153
|
+
// Join chunks first, then remaining xml array
|
|
154
|
+
if (this._chunks.length === 0) {
|
|
155
|
+
return this._xml.join("");
|
|
156
|
+
}
|
|
157
|
+
if (this._xml.length > 0) {
|
|
158
|
+
this._chunks.push(this._xml.join(""));
|
|
159
|
+
}
|
|
160
|
+
return this._chunks.join("");
|
|
139
161
|
}
|
|
140
162
|
}
|
|
141
163
|
XmlStream.StdDocAttributes = {
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native compression utilities using platform APIs
|
|
3
|
+
*
|
|
4
|
+
* - Node.js: Uses native zlib module (C++ implementation, fastest)
|
|
5
|
+
* - Browser: Uses CompressionStream API (Chrome 80+, Firefox 113+, Safari 16.4+)
|
|
6
|
+
*
|
|
7
|
+
* Both use "deflate-raw" format which is required for ZIP files
|
|
8
|
+
* (raw DEFLATE without zlib header/trailer)
|
|
9
|
+
*/
|
|
10
|
+
// Detect environment
|
|
11
|
+
const isNode = typeof process !== "undefined" && process.versions?.node;
|
|
12
|
+
// Lazy-loaded zlib module for Node.js
|
|
13
|
+
let _zlib = null;
|
|
14
|
+
let _zlibLoading = null;
|
|
15
|
+
// Auto-initialize zlib in Node.js environment
|
|
16
|
+
if (isNode) {
|
|
17
|
+
_zlibLoading = import("zlib")
|
|
18
|
+
.then(module => {
|
|
19
|
+
_zlib = module.default ?? module;
|
|
20
|
+
return _zlib;
|
|
21
|
+
})
|
|
22
|
+
.catch(() => {
|
|
23
|
+
_zlib = null;
|
|
24
|
+
return null;
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Get zlib module (Node.js only)
|
|
29
|
+
* Returns null if not yet loaded or not in Node.js
|
|
30
|
+
*/
|
|
31
|
+
function getZlib() {
|
|
32
|
+
return _zlib;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Ensure zlib is loaded (Node.js only)
|
|
36
|
+
* Call this before using sync methods if you need to guarantee availability
|
|
37
|
+
*/
|
|
38
|
+
export async function ensureZlib() {
|
|
39
|
+
if (_zlibLoading) {
|
|
40
|
+
return _zlibLoading;
|
|
41
|
+
}
|
|
42
|
+
return _zlib;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Check if native zlib is available (Node.js)
|
|
46
|
+
*/
|
|
47
|
+
export function hasNativeZlib() {
|
|
48
|
+
const zlib = getZlib();
|
|
49
|
+
return zlib !== null && typeof zlib.deflateRawSync === "function";
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Check if CompressionStream is available (Browser/Node.js 17+)
|
|
53
|
+
*/
|
|
54
|
+
export function hasCompressionStream() {
|
|
55
|
+
return typeof CompressionStream !== "undefined";
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Compress data using the best available native method
|
|
59
|
+
*
|
|
60
|
+
* Priority:
|
|
61
|
+
* 1. Node.js zlib (if available) - fastest, supports compression levels
|
|
62
|
+
* 2. CompressionStream (browser/Node.js 17+) - no level support
|
|
63
|
+
* 3. Return uncompressed data (fallback)
|
|
64
|
+
*
|
|
65
|
+
* @param data - Data to compress
|
|
66
|
+
* @param options - Compression options
|
|
67
|
+
* @returns Compressed data
|
|
68
|
+
*
|
|
69
|
+
* @example
|
|
70
|
+
* ```ts
|
|
71
|
+
* const data = new TextEncoder().encode("Hello, World!");
|
|
72
|
+
* const compressed = await compress(data, { level: 6 });
|
|
73
|
+
* ```
|
|
74
|
+
*/
|
|
75
|
+
export async function compress(data, options = {}) {
|
|
76
|
+
const level = options.level ?? 6;
|
|
77
|
+
// Level 0 means no compression
|
|
78
|
+
if (level === 0) {
|
|
79
|
+
return data;
|
|
80
|
+
}
|
|
81
|
+
// Ensure zlib is loaded first
|
|
82
|
+
const zlib = await ensureZlib();
|
|
83
|
+
// Try Node.js zlib first (fastest, supports levels)
|
|
84
|
+
if (zlib && typeof zlib.deflateRawSync === "function") {
|
|
85
|
+
const result = zlib.deflateRawSync(Buffer.from(data), { level });
|
|
86
|
+
return new Uint8Array(result.buffer, result.byteOffset, result.byteLength);
|
|
87
|
+
}
|
|
88
|
+
// Fall back to CompressionStream (browser/Node.js 17+)
|
|
89
|
+
if (typeof CompressionStream !== "undefined") {
|
|
90
|
+
return compressWithCompressionStream(data);
|
|
91
|
+
}
|
|
92
|
+
// No compression available - return original data
|
|
93
|
+
console.warn("No native compression available, returning uncompressed data");
|
|
94
|
+
return data;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Compress data synchronously using Node.js zlib
|
|
98
|
+
* Only available in Node.js environment
|
|
99
|
+
*
|
|
100
|
+
* @param data - Data to compress
|
|
101
|
+
* @param options - Compression options
|
|
102
|
+
* @returns Compressed data
|
|
103
|
+
* @throws Error if not in Node.js environment
|
|
104
|
+
*/
|
|
105
|
+
export function compressSync(data, options = {}) {
|
|
106
|
+
const level = options.level ?? 6;
|
|
107
|
+
if (level === 0) {
|
|
108
|
+
return data;
|
|
109
|
+
}
|
|
110
|
+
const zlib = getZlib();
|
|
111
|
+
if (!zlib || typeof zlib.deflateRawSync !== "function") {
|
|
112
|
+
throw new Error("Synchronous compression is only available in Node.js environment");
|
|
113
|
+
}
|
|
114
|
+
const result = zlib.deflateRawSync(Buffer.from(data), { level });
|
|
115
|
+
return new Uint8Array(result.buffer, result.byteOffset, result.byteLength);
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Compress using browser's native CompressionStream
|
|
119
|
+
* Uses "deflate-raw" format (required for ZIP files)
|
|
120
|
+
*
|
|
121
|
+
* Note: CompressionStream does not support compression level configuration
|
|
122
|
+
*
|
|
123
|
+
* @param data - Data to compress
|
|
124
|
+
* @returns Compressed data
|
|
125
|
+
*/
|
|
126
|
+
async function compressWithCompressionStream(data) {
|
|
127
|
+
const cs = new CompressionStream("deflate-raw");
|
|
128
|
+
const writer = cs.writable.getWriter();
|
|
129
|
+
const reader = cs.readable.getReader();
|
|
130
|
+
// Write data and close
|
|
131
|
+
writer.write(new Uint8Array(data.buffer, data.byteOffset, data.byteLength));
|
|
132
|
+
writer.close();
|
|
133
|
+
// Read all compressed chunks
|
|
134
|
+
const chunks = [];
|
|
135
|
+
let totalLength = 0;
|
|
136
|
+
while (true) {
|
|
137
|
+
const { done, value } = await reader.read();
|
|
138
|
+
if (done) {
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
chunks.push(value);
|
|
142
|
+
totalLength += value.length;
|
|
143
|
+
}
|
|
144
|
+
// Combine chunks into single array
|
|
145
|
+
const result = new Uint8Array(totalLength);
|
|
146
|
+
let offset = 0;
|
|
147
|
+
for (const chunk of chunks) {
|
|
148
|
+
result.set(chunk, offset);
|
|
149
|
+
offset += chunk.length;
|
|
150
|
+
}
|
|
151
|
+
return result;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Decompress data using the best available native method
|
|
155
|
+
*
|
|
156
|
+
* @param data - Compressed data (deflate-raw format)
|
|
157
|
+
* @returns Decompressed data
|
|
158
|
+
*/
|
|
159
|
+
export async function decompress(data) {
|
|
160
|
+
// Ensure zlib is loaded first
|
|
161
|
+
const zlib = await ensureZlib();
|
|
162
|
+
// Try Node.js zlib first
|
|
163
|
+
if (zlib && typeof zlib.inflateRawSync === "function") {
|
|
164
|
+
const result = zlib.inflateRawSync(Buffer.from(data));
|
|
165
|
+
return new Uint8Array(result.buffer, result.byteOffset, result.byteLength);
|
|
166
|
+
}
|
|
167
|
+
// Fall back to DecompressionStream
|
|
168
|
+
if (typeof DecompressionStream !== "undefined") {
|
|
169
|
+
return decompressWithDecompressionStream(data);
|
|
170
|
+
}
|
|
171
|
+
throw new Error("No native decompression available");
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Decompress data synchronously using Node.js zlib
|
|
175
|
+
*
|
|
176
|
+
* @param data - Compressed data (deflate-raw format)
|
|
177
|
+
* @returns Decompressed data
|
|
178
|
+
* @throws Error if not in Node.js environment
|
|
179
|
+
*/
|
|
180
|
+
export function decompressSync(data) {
|
|
181
|
+
const zlib = getZlib();
|
|
182
|
+
if (!zlib || typeof zlib.inflateRawSync !== "function") {
|
|
183
|
+
throw new Error("Synchronous decompression is only available in Node.js environment");
|
|
184
|
+
}
|
|
185
|
+
const result = zlib.inflateRawSync(Buffer.from(data));
|
|
186
|
+
return new Uint8Array(result.buffer, result.byteOffset, result.byteLength);
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Decompress using browser's native DecompressionStream
|
|
190
|
+
*
|
|
191
|
+
* @param data - Compressed data (deflate-raw format)
|
|
192
|
+
* @returns Decompressed data
|
|
193
|
+
*/
|
|
194
|
+
async function decompressWithDecompressionStream(data) {
|
|
195
|
+
const ds = new DecompressionStream("deflate-raw");
|
|
196
|
+
const writer = ds.writable.getWriter();
|
|
197
|
+
const reader = ds.readable.getReader();
|
|
198
|
+
// Write data and close
|
|
199
|
+
writer.write(new Uint8Array(data.buffer, data.byteOffset, data.byteLength));
|
|
200
|
+
writer.close();
|
|
201
|
+
// Read all decompressed chunks
|
|
202
|
+
const chunks = [];
|
|
203
|
+
let totalLength = 0;
|
|
204
|
+
while (true) {
|
|
205
|
+
const { done, value } = await reader.read();
|
|
206
|
+
if (done) {
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
chunks.push(value);
|
|
210
|
+
totalLength += value.length;
|
|
211
|
+
}
|
|
212
|
+
// Combine chunks into single array
|
|
213
|
+
const result = new Uint8Array(totalLength);
|
|
214
|
+
let offset = 0;
|
|
215
|
+
for (const chunk of chunks) {
|
|
216
|
+
result.set(chunk, offset);
|
|
217
|
+
offset += chunk.length;
|
|
218
|
+
}
|
|
219
|
+
return result;
|
|
220
|
+
}
|