xlvane 1.2.2 → 1.2.3
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/dist/cli.js +71 -24
- package/dist/index.d.ts +6 -2
- package/dist/index.js +3 -3
- package/dist/{node-files-CDb6TyZb.js → node-files-Bxzv7THG.js} +1 -1
- package/dist/node.d.ts +1 -1
- package/dist/node.js +3 -3
- package/dist/{options-Ba1iWD-Z.js → options-Cw7OraxE.js} +274 -71
- package/dist/{rows-IWKIxHt9.js → rows-DnlfBgme.js} +104 -23
- package/dist/{stream-write-BGuUjlXo.js → stream-write-DWD8Pflj.js} +81 -15
- package/dist/{stream-write-BVaFkgwO.d.ts → stream-write-y52KUDW8.d.ts} +34 -3
- package/docs/errors.md +6 -2
- package/docs/exceljs-compat.md +2 -2
- package/llms-full.txt +12 -8
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as fillXml,
|
|
1
|
+
import { $ as fillXml, B as tokenize, Bt as checkKeyText, Cn as normalizeLineEnds, Ct as relationshipsPartName, Dn as escapeAttr, En as encodeXstring, Ft as mergeStyles, G as DEFAULT_FONT, H as X14_DV_URI, Ht as resolveLimits, I as WarningCollector, It as normalizeStyle, J as borderXml, Lt as stableKey, M as openZip, Mt as parseStyles, Nt as READING_ORDERS, On as escapeText, Pt as checkStyleCount, Rt as isDateFormat, Sn as decodeXml, St as openPackage, Tt as zipNameToPartName, Ut as suggest, V as X14_CF_URI, Vt as checkOutputPositions, _ as isoDate, an as classifyRelationship, at as reusableBuiltinNumFmt, b as readWorkbook, bn as parseXmlTree, bt as parseIsoDateTime, cn as MAX_ROWS, dn as formatRange, et as fontXml, f as createWorkbook, fn as parseCellRef, ft as stringItemXml, h as detectHeader, i as readOptions, in as SML_NS, jn as XlvError, jt as EMPTY_STYLES, ln as columnLetters, m as writeWorkbook, mt as textElement, n as oneOfOption, on as relationshipTypes, pn as parseRange, q as attr, r as openInput, rn as REL_NS, sn as MAX_COLUMNS, t as booleanOption, tt as formatNumber, un as formatCellRef, vn as childNamed, vt as writeZip, wn as parseXml, wt as partKey, x as toBytes, xt as parseSharedStrings, yn as childrenNamed, z as shiftFormula, zt as DEFAULT_READ_LIMITS } from "./options-Cw7OraxE.js";
|
|
2
2
|
// src/table.ts
|
|
3
3
|
const READ_OPTIONS = [
|
|
4
4
|
"sheet",
|
|
@@ -199,6 +199,7 @@ function prepare(input, options) {
|
|
|
199
199
|
let cellCount = 0;
|
|
200
200
|
for (const cells of byRow.values()) cellCount += cells.size;
|
|
201
201
|
checkOutputPositions(numbers.length * columns.length, cellCount, limits.maxCells, "The table", sheet.name);
|
|
202
|
+
checkKeyText(columns, numbers.length, limits.maxSharedStringChars, sheet.name);
|
|
202
203
|
for (const r of numbers) {
|
|
203
204
|
const cells = byRow.get(r);
|
|
204
205
|
if (skipEmpty && (cells === void 0 || [...cells.values()].every((c) => c.value === null))) continue;
|
|
@@ -241,7 +242,7 @@ function toIssue(issue, row, columns) {
|
|
|
241
242
|
return typeof key === "number" ? key : String(key);
|
|
242
243
|
});
|
|
243
244
|
const first = path[0];
|
|
244
|
-
const column = typeof first === "string" ? columns.
|
|
245
|
+
const column = typeof first === "string" ? columns.get(first) : void 0;
|
|
245
246
|
return {
|
|
246
247
|
row,
|
|
247
248
|
cell: column === void 0 ? null : formatCellRef(row, column.col),
|
|
@@ -254,9 +255,11 @@ function collect(prepared, results) {
|
|
|
254
255
|
const rows = [];
|
|
255
256
|
const rowNumbers = [];
|
|
256
257
|
const issues = [];
|
|
258
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
259
|
+
for (const c of prepared.columns) if (!byKey.has(c.key)) byKey.set(c.key, c);
|
|
257
260
|
results.forEach((result, i) => {
|
|
258
261
|
const row = prepared.rowNumbers[i] ?? 0;
|
|
259
|
-
if (result.issues) for (const issue of result.issues) issues.push(toIssue(issue, row,
|
|
262
|
+
if (result.issues) for (const issue of result.issues) issues.push(toIssue(issue, row, byKey));
|
|
260
263
|
else {
|
|
261
264
|
rows.push(result.value);
|
|
262
265
|
rowNumbers.push(row);
|
|
@@ -359,7 +362,8 @@ const WRITE_OPTIONS = [
|
|
|
359
362
|
"columns",
|
|
360
363
|
"sheet",
|
|
361
364
|
"preset",
|
|
362
|
-
"dateSystem"
|
|
365
|
+
"dateSystem",
|
|
366
|
+
"maxCells"
|
|
363
367
|
];
|
|
364
368
|
/** Header style of the `report` preset (D-304). */
|
|
365
369
|
const REPORT_HEADER = {
|
|
@@ -478,11 +482,22 @@ function addTableSheet(workbook, rows, options) {
|
|
|
478
482
|
columns.forEach((column, c) => {
|
|
479
483
|
if (hasHeader && column.header !== "") sheet.set(formatCellRef(1, c + 1), column.header, preset === "report" ? REPORT_HEADER : void 0);
|
|
480
484
|
});
|
|
485
|
+
const formatted = [];
|
|
486
|
+
columns.forEach((column, c) => {
|
|
487
|
+
if (column.numFmt !== void 0) formatted.push(c);
|
|
488
|
+
});
|
|
489
|
+
const columnsOfKey = /* @__PURE__ */ new Map();
|
|
490
|
+
if (!arrays) columns.forEach((column, c) => {
|
|
491
|
+
const list = columnsOfKey.get(column.key);
|
|
492
|
+
if (list === void 0) columnsOfKey.set(column.key, [c]);
|
|
493
|
+
else list.push(c);
|
|
494
|
+
});
|
|
495
|
+
let cells = hasHeader ? columns.filter((c) => c.header !== "").length : 0;
|
|
496
|
+
const hasOwn = (o, key) => Object.prototype.hasOwnProperty.call(o, key);
|
|
481
497
|
rows.forEach((row, i) => {
|
|
482
498
|
const r = i + 1 + offset;
|
|
483
|
-
|
|
484
|
-
const
|
|
485
|
-
const raw = arrays ? row[c] : Object.prototype.hasOwnProperty.call(record, column.key) ? record[column.key] : void 0;
|
|
499
|
+
const put = (c, raw) => {
|
|
500
|
+
const column = columns[c];
|
|
486
501
|
const value = raw === void 0 ? null : raw;
|
|
487
502
|
if (!(value === null || typeof value === "number" || typeof value === "string" || typeof value === "boolean" || value instanceof Date)) throw tableError(`Row ${String(i + 1)}, column "${column.key}" holds ${Array.isArray(value) ? "an array" : typeof value}, which is not a cell value.`, "Cell values are numbers, strings, booleans, Dates or null. Convert nested data first, for example with JSON.stringify.", {
|
|
488
503
|
row: r,
|
|
@@ -491,8 +506,29 @@ function addTableSheet(workbook, rows, options) {
|
|
|
491
506
|
if (value === null && column.numFmt === void 0) return;
|
|
492
507
|
sheet.set(formatCellRef(r, c + 1), value, column.numFmt === void 0 ? void 0 : { numFmt: column.numFmt });
|
|
493
508
|
widths[c] = Math.max(widths[c] ?? 0, displayLength(value));
|
|
494
|
-
|
|
509
|
+
cells++;
|
|
510
|
+
};
|
|
511
|
+
if (arrays) {
|
|
512
|
+
const values = row;
|
|
513
|
+
const n = Math.min(values.length, columns.length);
|
|
514
|
+
for (let c = 0; c < n; c++) put(c, values[c]);
|
|
515
|
+
for (const c of formatted) if (c >= n) put(c, void 0);
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
const record = row;
|
|
519
|
+
const own = Object.getOwnPropertyNames(record);
|
|
520
|
+
if (own.length >= columns.length) {
|
|
521
|
+
for (let c = 0; c < columns.length; c++) put(c, hasOwn(record, columns[c].key) ? record[columns[c].key] : void 0);
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
const visit = [];
|
|
525
|
+
for (const key of own) for (const c of columnsOfKey.get(key) ?? []) visit.push(c);
|
|
526
|
+
for (const c of formatted) if (!hasOwn(record, columns[c].key)) visit.push(c);
|
|
527
|
+
visit.sort((x, y) => x - y);
|
|
528
|
+
for (const c of visit) put(c, hasOwn(record, columns[c].key) ? record[columns[c].key] : void 0);
|
|
495
529
|
});
|
|
530
|
+
const maxCells = options.get("maxCells");
|
|
531
|
+
checkOutputPositions((rows.length + offset) * columns.length, cells, typeof maxCells === "number" ? maxCells : DEFAULT_READ_LIMITS.maxCells, "The table", sheet.name, "The rows are spread over far more columns than they fill, for example after one row with many keys or a long first CSV line. Write fewer columns (the \"columns\" option), split the data over several tables, or raise the \"maxCells\" option for a trusted sparse table.");
|
|
496
532
|
columns.forEach((column, c) => {
|
|
497
533
|
if (column.width !== void 0) sheet.column(c + 1, { width: column.width });
|
|
498
534
|
else if (preset === "report") sheet.column(c + 1, { width: Math.min(MAX_FITTED_WIDTH, Math.max(MIN_WIDTH, (widths[c] ?? 0) + 2)) });
|
|
@@ -524,6 +560,11 @@ function addTableSheet(workbook, rows, options) {
|
|
|
524
560
|
*/
|
|
525
561
|
function writeTable(rows, options) {
|
|
526
562
|
const o = readOptions(options, WRITE_OPTIONS, "writeTable options");
|
|
563
|
+
const maxCells = o.get("maxCells");
|
|
564
|
+
if (maxCells !== void 0 && (typeof maxCells !== "number" || !(maxCells > 0))) throw new XlvError("XLV_E_INVALID_OPTION", "The option \"maxCells\" must be a positive number.", {
|
|
565
|
+
hint: "For example { maxCells: 20_000_000 }, or Infinity.",
|
|
566
|
+
details: { option: "maxCells" }
|
|
567
|
+
});
|
|
527
568
|
const dateSystem = o.get("dateSystem");
|
|
528
569
|
const workbook = createWorkbook(dateSystem === void 0 ? void 0 : { dateSystem });
|
|
529
570
|
addTableSheet(workbook, rows, o);
|
|
@@ -578,6 +619,21 @@ const INJECTION_START = /* @__PURE__ */ new Set([
|
|
|
578
619
|
"﹣",
|
|
579
620
|
"﹫"
|
|
580
621
|
]);
|
|
622
|
+
/** Signs that start a formula: refused as the delimiter while formulas are escaped (D-J01). */
|
|
623
|
+
const FORMULA_SIGNS = /* @__PURE__ */ new Set([
|
|
624
|
+
"=",
|
|
625
|
+
"+",
|
|
626
|
+
"-",
|
|
627
|
+
"@",
|
|
628
|
+
"=",
|
|
629
|
+
"+",
|
|
630
|
+
"-",
|
|
631
|
+
"@",
|
|
632
|
+
"﹦",
|
|
633
|
+
"﹢",
|
|
634
|
+
"﹣",
|
|
635
|
+
"﹫"
|
|
636
|
+
]);
|
|
581
637
|
/** Whether text starting with `c` is escaped: one of INJECTION_START or a C0 control (NUL included). */
|
|
582
638
|
const injectionStart = (c) => INJECTION_START.has(c) || c.charCodeAt(0) < 32;
|
|
583
639
|
/**
|
|
@@ -615,6 +671,10 @@ function toCSV(input, options) {
|
|
|
615
671
|
const newline = oneOfOption("newline", o.get("newline"), ["\r\n", "\n"], "\r\n");
|
|
616
672
|
const bom = booleanOption("bom", o.get("bom"), false);
|
|
617
673
|
const injection = oneOfOption("formulaInjection", o.get("formulaInjection"), ["escape", "off"], "escape");
|
|
674
|
+
if (injection === "escape" && FORMULA_SIGNS.has(delimiter)) throw new XlvError("XLV_E_INVALID_OPTION", `The delimiter "${delimiter}" starts a formula, so lines with an empty first field would run as formulas.`, {
|
|
675
|
+
hint: "Use \",\" (default), \";\", \"|\" or \"\\t\", or pass formulaInjection: \"off\" for a trusted consumer.",
|
|
676
|
+
details: { option: "delimiter" }
|
|
677
|
+
});
|
|
618
678
|
const limits = resolveLimits(o.get("limits"));
|
|
619
679
|
const sheet = pickSheet(openInput(input, o.get("limits"), o.get("onWarning")), o.get("sheet"));
|
|
620
680
|
let startRow = 1;
|
|
@@ -913,6 +973,7 @@ function fromCSV(input, options) {
|
|
|
913
973
|
const workbook = createWorkbook(dateSystem === void 0 ? void 0 : { dateSystem });
|
|
914
974
|
const tableOptions = /* @__PURE__ */ new Map([["sheet", o.get("sheet") ?? "Sheet1"], ["preset", oneOfOption("preset", o.get("preset"), ["plain", "report"], "plain")]]);
|
|
915
975
|
if (columns) tableOptions.set("columns", columns);
|
|
976
|
+
tableOptions.set("maxCells", maxCells);
|
|
916
977
|
addTableSheet(workbook, rows, tableOptions);
|
|
917
978
|
return writeWorkbook(workbook);
|
|
918
979
|
}
|
|
@@ -928,13 +989,30 @@ function toDiffCell(cell, styles) {
|
|
|
928
989
|
if (styles) out.style = cell.style;
|
|
929
990
|
return out;
|
|
930
991
|
}
|
|
931
|
-
|
|
992
|
+
/**
|
|
993
|
+
* Compares styles by their stable key, computed once per style object: the reader shares one
|
|
994
|
+
* frozen object per style, so a sheet of a million cells needs a few keys, not two per cell.
|
|
995
|
+
*/
|
|
996
|
+
function styleComparer() {
|
|
997
|
+
const keys = /* @__PURE__ */ new Map();
|
|
998
|
+
const keyOf = (style) => {
|
|
999
|
+
if (typeof style !== "object" || style === null) return stableKey(style);
|
|
1000
|
+
let key = keys.get(style);
|
|
1001
|
+
if (key === void 0) {
|
|
1002
|
+
key = stableKey(style);
|
|
1003
|
+
keys.set(style, key);
|
|
1004
|
+
}
|
|
1005
|
+
return key;
|
|
1006
|
+
};
|
|
1007
|
+
return (a, b) => a === b || keyOf(a) === keyOf(b);
|
|
1008
|
+
}
|
|
1009
|
+
function sameCell(a, b, sameStyle) {
|
|
932
1010
|
if (a.type !== b.type) return false;
|
|
933
1011
|
const av = a.value instanceof Date ? a.value.getTime() : a.value;
|
|
934
1012
|
const bv = b.value instanceof Date ? b.value.getTime() : b.value;
|
|
935
1013
|
if (!Object.is(av, bv)) return false;
|
|
936
1014
|
if ((a.formula?.text ?? null) !== (b.formula?.text ?? null)) return false;
|
|
937
|
-
return
|
|
1015
|
+
return sameStyle === null || sameStyle(a.style, b.style);
|
|
938
1016
|
}
|
|
939
1017
|
/**
|
|
940
1018
|
* Compares two workbooks: added and removed sheets (by name), visibility changes, and cells
|
|
@@ -983,44 +1061,45 @@ function diff(a, b, options) {
|
|
|
983
1061
|
summary.sheetsAdded = sheetsAdded.length;
|
|
984
1062
|
summary.sheetsRemoved = sheetsRemoved.length;
|
|
985
1063
|
const sheets = [];
|
|
1064
|
+
const sameStyle = styles ? styleComparer() : null;
|
|
986
1065
|
for (const after of second.sheets) {
|
|
987
1066
|
const before = firstNames.get(after.name);
|
|
988
1067
|
if (!before) continue;
|
|
989
1068
|
const fromFirst = [];
|
|
990
1069
|
const fromSecond = [];
|
|
991
1070
|
let count = 0;
|
|
992
|
-
const record = (list, row, col, change) => {
|
|
1071
|
+
const record = (list, row, col, kind, change) => {
|
|
993
1072
|
count++;
|
|
994
|
-
if (
|
|
995
|
-
else if (
|
|
1073
|
+
if (kind === "added") summary.cellsAdded++;
|
|
1074
|
+
else if (kind === "removed") summary.cellsRemoved++;
|
|
996
1075
|
else summary.cellsChanged++;
|
|
997
1076
|
if (list.length < maxCells) list.push({
|
|
998
1077
|
row,
|
|
999
1078
|
col,
|
|
1000
|
-
change
|
|
1079
|
+
change: change()
|
|
1001
1080
|
});
|
|
1002
1081
|
};
|
|
1003
1082
|
for (const cell of before.cells()) {
|
|
1004
1083
|
const other = after.cell(cell.ref);
|
|
1005
|
-
if (other === void 0) record(fromFirst, cell.row, cell.col, {
|
|
1084
|
+
if (other === void 0) record(fromFirst, cell.row, cell.col, "removed", () => ({
|
|
1006
1085
|
ref: cell.ref,
|
|
1007
1086
|
kind: "removed",
|
|
1008
1087
|
before: toDiffCell(cell, styles),
|
|
1009
1088
|
after: null
|
|
1010
|
-
});
|
|
1011
|
-
else if (!sameCell(cell, other,
|
|
1089
|
+
}));
|
|
1090
|
+
else if (!sameCell(cell, other, sameStyle)) record(fromFirst, cell.row, cell.col, "changed", () => ({
|
|
1012
1091
|
ref: cell.ref,
|
|
1013
1092
|
kind: "changed",
|
|
1014
1093
|
before: toDiffCell(cell, styles),
|
|
1015
1094
|
after: toDiffCell(other, styles)
|
|
1016
|
-
});
|
|
1095
|
+
}));
|
|
1017
1096
|
}
|
|
1018
|
-
for (const cell of after.cells()) if (before.cell(cell.ref) === void 0) record(fromSecond, cell.row, cell.col, {
|
|
1097
|
+
for (const cell of after.cells()) if (before.cell(cell.ref) === void 0) record(fromSecond, cell.row, cell.col, "added", () => ({
|
|
1019
1098
|
ref: cell.ref,
|
|
1020
1099
|
kind: "added",
|
|
1021
1100
|
before: null,
|
|
1022
1101
|
after: toDiffCell(cell, styles)
|
|
1023
|
-
});
|
|
1102
|
+
}));
|
|
1024
1103
|
const stateChanged = before.state !== after.state;
|
|
1025
1104
|
if (count === 0 && !stateChanged) continue;
|
|
1026
1105
|
summary.sheetsChanged++;
|
|
@@ -1771,7 +1850,9 @@ function editSheetXml(text, part, sheet, planned, limits) {
|
|
|
1771
1850
|
for (const list of byRow.values()) list.sort((a, b) => a.col - b.col);
|
|
1772
1851
|
const forceRefs = rows.some((r) => !r.hasR);
|
|
1773
1852
|
const emitted = /* @__PURE__ */ new Set();
|
|
1774
|
-
const
|
|
1853
|
+
const existingRows = new Set(rows.map((r) => r.number));
|
|
1854
|
+
const pending = [...byRow.keys()].filter((n) => !existingRows.has(n)).sort((a, b) => a - b);
|
|
1855
|
+
let next = 0;
|
|
1775
1856
|
const p = (xml) => withPrefix(xml, prefix);
|
|
1776
1857
|
const newRow = (n) => {
|
|
1777
1858
|
const cells = (byRow.get(n) ?? []).filter((c) => c.xml !== null).map((c) => p(c.xml ?? "")).join("");
|
|
@@ -1779,7 +1860,7 @@ function editSheetXml(text, part, sheet, planned, limits) {
|
|
|
1779
1860
|
};
|
|
1780
1861
|
let inner = "";
|
|
1781
1862
|
for (const row of rows) {
|
|
1782
|
-
while (pending.length
|
|
1863
|
+
while (next < pending.length && (pending[next] ?? 0) < row.number) inner += newRow(pending[next++] ?? 0);
|
|
1783
1864
|
const edits = byRow.get(row.number);
|
|
1784
1865
|
const needsRefs = forceRefs || row.cells.some((c) => !c.hasR);
|
|
1785
1866
|
if (!edits && row.hasR && !needsRefs) {
|
|
@@ -1803,7 +1884,7 @@ function editSheetXml(text, part, sheet, planned, limits) {
|
|
|
1803
1884
|
while (k < toInsert.length) body += p(toInsert[k++]?.xml ?? "");
|
|
1804
1885
|
inner += row.element.start.empty ? `${openTag(tag)}${body}</${row.element.start.qname}>` : `${tag}${body}${text.slice(row.element.end.start, row.element.end.end)}`;
|
|
1805
1886
|
}
|
|
1806
|
-
while (pending.length
|
|
1887
|
+
while (next < pending.length) inner += newRow(pending[next++] ?? 0);
|
|
1807
1888
|
const changes = [];
|
|
1808
1889
|
if (sheetData.start.empty) changes.push([
|
|
1809
1890
|
sheetData.start.start,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $t as
|
|
1
|
+
import { $t as drawingParts, A as limitExceeded, At as nameScopeKey, Bt as checkKeyText, C as Bytes, Ct as relationshipsPartName, D as TAIL_BYTES, Dt as normalizeTable, E as NotLoaded, Et as TABLE_CONTENT_TYPE, F as validateLocalHeaders, Gt as printXml, Ht as resolveLimits, I as WarningCollector, It as normalizeStyle, K as SheetRels, Kt as sheetProtectionXml, L as sheetNotFound, N as readCentralDirectory, O as checkSource$1, Ot as checkDefinedName, P as unsupported, Q as createSharedStrings, Qt as MediaParts, R as createWorksheetParser, S as workbookPartsToLoad, St as openPackage, T as HEAD_BYTES, Tn as clip, Tt as zipNameToPartName, Vt as checkOutputPositions, Wt as printNames, X as cellXml, Xt as DRAWING_CONTENT_TYPE, Y as buildStyles, Z as contentTypesXml, Zt as IMAGE_CONTENT_TYPES, _ as isoDate, _n as corePropertiesXml, _t as FormulaGroups, a as DATE_FORMAT, an as classifyRelationship, cn as MAX_ROWS, ct as sheetHead, d as convertCellInput, dt as sheetTail, f as createWorkbook, gn as appPropertiesXml, gt as workbookXml, h as detectHeader, hn as CORE_PROPERTIES_PART, ht as workbookRelsXml, i as readOptions, it as noteParts, j as locateCentralDirectory, jn as XlvError, k as corrupt, kt as checkUniqueName, ln as columnLetters, lt as sheetRelsPartName, mn as APP_PROPERTIES_PART, n as oneOfOption, nn as resolveImage, nt as hyperlinksXml, o as DATE_TIME_FORMAT, ot as rootRelsXml, p as sortedNotes, pt as tableParts, rt as nameTitles, sn as MAX_COLUMNS, st as rowStart, tn as metricsOf, u as checkBookViews, un as formatCellRef, ut as sheetRulesXml, v as analyzeWorkbook, w as CORRUPT_HINT, wt as partKey, xn as createXmlByteStreamParser, y as readNotes, yt as crc32, zt as DEFAULT_READ_LIMITS } from "./options-Cw7OraxE.js";
|
|
2
2
|
import { Deflate, Inflate } from "fflate";
|
|
3
3
|
// src/zip/stream.ts
|
|
4
4
|
/**
|
|
@@ -396,7 +396,8 @@ function createZipStream(entries, level = 6) {
|
|
|
396
396
|
// src/stream-read.ts
|
|
397
397
|
const GiB = 1073741824;
|
|
398
398
|
/**
|
|
399
|
-
* Limits that differ in streaming mode (D-401): `maxCells` does not apply
|
|
399
|
+
* Limits that differ in streaming mode (D-401): `maxCells` does not apply (but one `<row>` element
|
|
400
|
+
* may hold at most 16 384 cells, D-J01), and single parts may
|
|
400
401
|
* decompress to 4 GiB (8 GiB in total). All other `DEFAULT_READ_LIMITS` stay.
|
|
401
402
|
*
|
|
402
403
|
* @example
|
|
@@ -423,32 +424,69 @@ var PreloadedZip = class {
|
|
|
423
424
|
entries;
|
|
424
425
|
bytesRead = 0;
|
|
425
426
|
data = /* @__PURE__ */ new Map();
|
|
427
|
+
/**
|
|
428
|
+
* ZIP entries by the part name they map to, as openPackage maps them: percent-encoded
|
|
429
|
+
* non-ASCII characters of an item name are decoded (Part 2 §7.3.5; F-OPC-12).
|
|
430
|
+
*/
|
|
431
|
+
byPart = /* @__PURE__ */ new Map();
|
|
426
432
|
constructor(zip, maxBufferedBytes) {
|
|
427
433
|
this.zip = zip;
|
|
428
434
|
this.maxBufferedBytes = maxBufferedBytes;
|
|
429
435
|
this.entries = zip.entries;
|
|
436
|
+
for (const entry of zip.entries) {
|
|
437
|
+
if (entry.name.endsWith("/")) continue;
|
|
438
|
+
const key = partKey(zipNameToPartName(entry.name));
|
|
439
|
+
if (!this.byPart.has(key)) this.byPart.set(key, entry);
|
|
440
|
+
}
|
|
430
441
|
}
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
442
|
+
/** The ZIP entry that stores a part, or undefined. */
|
|
443
|
+
entryOf(partName) {
|
|
444
|
+
return this.byPart.get(partKey(partName));
|
|
445
|
+
}
|
|
446
|
+
async load(entries) {
|
|
447
|
+
for (const entry of entries) {
|
|
434
448
|
if (entry === void 0 || this.data.has(entry.name)) continue;
|
|
435
449
|
const bytes = await this.zip.read(entry, this.maxBufferedBytes);
|
|
436
450
|
this.data.set(entry.name, bytes);
|
|
437
451
|
this.bytesRead += bytes.length;
|
|
438
452
|
}
|
|
439
453
|
}
|
|
454
|
+
/** Loads the parts with these names that exist. */
|
|
455
|
+
async loadParts(partNames) {
|
|
456
|
+
await this.load([...partNames].map((name) => this.entryOf(name)));
|
|
457
|
+
}
|
|
458
|
+
/** Loads the Relationships parts of these sources (null: the package) that exist. */
|
|
459
|
+
async loadRelationships(sources) {
|
|
460
|
+
await this.loadParts([...sources].map(relationshipsPartName));
|
|
461
|
+
}
|
|
440
462
|
find(name) {
|
|
441
463
|
return this.zip.find(name);
|
|
442
464
|
}
|
|
443
465
|
read(entry) {
|
|
444
466
|
const name = typeof entry === "string" ? this.zip.find(entry)?.name ?? entry : entry.name;
|
|
445
467
|
const bytes = this.data.get(name);
|
|
446
|
-
if (bytes === void 0) throw new
|
|
468
|
+
if (bytes === void 0) throw new XlvError("XLV_E_INTERNAL", `The ZIP entry "${clip(name)}" was read before it was loaded.`, {
|
|
469
|
+
location: { part: `/${clip(name)}` },
|
|
470
|
+
hint: "This is a bug in xlvane. Please report it, with the file if you can share it; readWorkbook reads the file meanwhile."
|
|
471
|
+
});
|
|
447
472
|
return bytes;
|
|
448
473
|
}
|
|
449
474
|
};
|
|
450
|
-
|
|
451
|
-
|
|
475
|
+
/**
|
|
476
|
+
* The internal targets of the workbook part's relationships: the sheets among them, whose
|
|
477
|
+
* Relationships parts analyzeWorkbook reads. Only these, and not every .rels entry of the ZIP,
|
|
478
|
+
* are loaded (D-H01).
|
|
479
|
+
*/
|
|
480
|
+
function workbookTargets(pkg, workbookPart) {
|
|
481
|
+
const seen = /* @__PURE__ */ new Set();
|
|
482
|
+
const out = [];
|
|
483
|
+
for (const rel of pkg.relationships(workbookPart)) {
|
|
484
|
+
const key = partKey(rel.target);
|
|
485
|
+
if (rel.external || seen.has(key) || !pkg.hasPart(rel.target)) continue;
|
|
486
|
+
seen.add(key);
|
|
487
|
+
out.push(rel.target);
|
|
488
|
+
}
|
|
489
|
+
return out;
|
|
452
490
|
}
|
|
453
491
|
function checkSource(source) {
|
|
454
492
|
if (source instanceof Uint8Array || source instanceof ArrayBuffer) return source;
|
|
@@ -486,9 +524,15 @@ async function openWorkbookStream(source, options) {
|
|
|
486
524
|
});
|
|
487
525
|
const zip = await openZipSource(input, limits);
|
|
488
526
|
const preloaded = new PreloadedZip(zip, userLimits?.maxEntryBytes ?? DEFAULT_READ_LIMITS.maxEntryBytes);
|
|
489
|
-
await preloaded.load(zip.entries.filter((e) => e.name.toLowerCase() === "[content_types].xml"
|
|
527
|
+
await preloaded.load(zip.entries.filter((e) => e.name.toLowerCase() === "[content_types].xml"));
|
|
528
|
+
await preloaded.loadRelationships([null]);
|
|
490
529
|
const pkg = openPackage(preloaded, limits);
|
|
491
|
-
|
|
530
|
+
const workbookPart = pkg.relationships(null).find((r) => !r.external && classifyRelationship(r.type)?.name === "officeDocument")?.target;
|
|
531
|
+
if (workbookPart !== void 0) {
|
|
532
|
+
await preloaded.loadRelationships([workbookPart]);
|
|
533
|
+
await preloaded.loadParts(workbookPartsToLoad(pkg));
|
|
534
|
+
await preloaded.loadRelationships(workbookTargets(pkg, workbookPart));
|
|
535
|
+
}
|
|
492
536
|
const warnings = new WarningCollector();
|
|
493
537
|
const info = analyzeWorkbook(pkg, limits, warnings);
|
|
494
538
|
const formatCache = /* @__PURE__ */ new Map();
|
|
@@ -504,7 +548,7 @@ async function openWorkbookStream(source, options) {
|
|
|
504
548
|
const cached = noteCache.get(entry.part);
|
|
505
549
|
if (cached !== void 0) return cached;
|
|
506
550
|
const parts = pkg.relationships(entry.part).filter((r) => !r.external && ["comments", "vmlDrawing"].includes(classifyRelationship(r.type)?.name ?? "") && pkg.hasPart(r.target) && !entry.foreignParts.has(partKey(r.target)));
|
|
507
|
-
if (parts.length > 0) await preloaded.
|
|
551
|
+
if (parts.length > 0) await preloaded.loadParts(parts.map((r) => r.target));
|
|
508
552
|
const notes = Object.freeze(readNotes(pkg, entry.part, entry.name, limits, warnings, entry.foreignParts));
|
|
509
553
|
noteCache.set(entry.part, notes);
|
|
510
554
|
return notes;
|
|
@@ -553,7 +597,15 @@ async function openWorkbookStream(source, options) {
|
|
|
553
597
|
part,
|
|
554
598
|
limits
|
|
555
599
|
});
|
|
556
|
-
const
|
|
600
|
+
const stored = preloaded.entryOf(part);
|
|
601
|
+
if (stored === void 0) throw new XlvError("XLV_E_PART_MISSING", `The part ${clip(part)} of sheet "${entry.name}" is not in the archive.`, {
|
|
602
|
+
location: {
|
|
603
|
+
part: clip(part),
|
|
604
|
+
sheet: entry.name
|
|
605
|
+
},
|
|
606
|
+
hint: "The workbook is incomplete. Re-save it in a spreadsheet application."
|
|
607
|
+
});
|
|
608
|
+
const reader = zip.stream(stored).getReader();
|
|
557
609
|
try {
|
|
558
610
|
for (;;) {
|
|
559
611
|
const { done, value } = await reader.read();
|
|
@@ -640,6 +692,7 @@ async function* readTableStream(source, options) {
|
|
|
640
692
|
const limits = o.get("limits");
|
|
641
693
|
const book = await openWorkbookStream(source, limits === void 0 ? void 0 : { limits });
|
|
642
694
|
const outputCells = limits?.maxCells ?? DEFAULT_READ_LIMITS.maxCells;
|
|
695
|
+
const keyTextLimit = limits?.maxSharedStringChars ?? DEFAULT_READ_LIMITS.maxSharedStringChars;
|
|
643
696
|
const sheetName = o.get("sheet") ?? (book.sheets.find((s) => s.kind === "worksheet" && s.state === "visible") ?? book.sheets[0])?.name;
|
|
644
697
|
if (sheetName === void 0) return;
|
|
645
698
|
const buffered = [];
|
|
@@ -692,6 +745,16 @@ async function* readTableStream(source, options) {
|
|
|
692
745
|
};
|
|
693
746
|
let positions = 0;
|
|
694
747
|
let cellsRead = 0;
|
|
748
|
+
let byKey = null;
|
|
749
|
+
let byKeyOf = null;
|
|
750
|
+
const columnOf = (cols, key) => {
|
|
751
|
+
if (byKey === null || byKeyOf !== cols) {
|
|
752
|
+
byKey = /* @__PURE__ */ new Map();
|
|
753
|
+
for (const c of cols) if (!byKey.has(c.key)) byKey.set(c.key, c);
|
|
754
|
+
byKeyOf = cols;
|
|
755
|
+
}
|
|
756
|
+
return byKey.get(key);
|
|
757
|
+
};
|
|
695
758
|
const emit = async (r, cols) => {
|
|
696
759
|
if (headerRow !== null && r.number <= headerRow) return null;
|
|
697
760
|
cellsRead += r.cells.length;
|
|
@@ -699,6 +762,7 @@ async function* readTableStream(source, options) {
|
|
|
699
762
|
checkOutputPositions(positions, cellsRead, outputCells, "The table", sheetName);
|
|
700
763
|
const byCol = new Map(r.cells.map((c) => [c.col, c]));
|
|
701
764
|
if (cols.every((c) => byCol.get(c.col)?.value == null)) return null;
|
|
765
|
+
checkKeyText(cols, 1, keyTextLimit, sheetName);
|
|
702
766
|
const row = {};
|
|
703
767
|
for (const c of cols) setOwn(row, c.key, value(byCol.get(c.col)));
|
|
704
768
|
if (schema === void 0) return {
|
|
@@ -717,7 +781,7 @@ async function* readTableStream(source, options) {
|
|
|
717
781
|
const key = typeof seg === "object" && seg !== null ? seg.key : seg;
|
|
718
782
|
return typeof key === "number" ? key : String(key);
|
|
719
783
|
});
|
|
720
|
-
const column = typeof path[0] === "string" ? cols
|
|
784
|
+
const column = typeof path[0] === "string" ? columnOf(cols, path[0]) : void 0;
|
|
721
785
|
return {
|
|
722
786
|
row: r.number,
|
|
723
787
|
cell: column ? formatCellRef(r.number, column.col) : null,
|
|
@@ -833,6 +897,7 @@ function writeWorkbookStream(options) {
|
|
|
833
897
|
details: { option: "definedNames" }
|
|
834
898
|
});
|
|
835
899
|
const names = [];
|
|
900
|
+
const nameKeys = /* @__PURE__ */ new Map();
|
|
836
901
|
rawNames.forEach((raw, i) => {
|
|
837
902
|
const r = readOptions(raw, [
|
|
838
903
|
"name",
|
|
@@ -843,8 +908,9 @@ function writeWorkbookStream(options) {
|
|
|
843
908
|
], `definedNames[${String(i)}]`);
|
|
844
909
|
const { name, refersTo, ...rest } = Object.fromEntries(r);
|
|
845
910
|
const n = checkDefinedName(name, refersTo, rest, null);
|
|
846
|
-
checkUniqueName(n,
|
|
911
|
+
checkUniqueName(n, nameKeys);
|
|
847
912
|
names.push(n);
|
|
913
|
+
nameKeys.set(nameScopeKey(n), n);
|
|
848
914
|
});
|
|
849
915
|
const addNames = () => {
|
|
850
916
|
for (const n of names) book.defineName(n.name, n.refersTo, {
|
|
@@ -942,7 +1008,7 @@ function writeWorkbookStream(options) {
|
|
|
942
1008
|
for (const v of listOf("tables", "{ name: \"Sales\", ref: \"A1:B10\", columns: [{ name: \"Region\" }, { name: \"Revenue\" }] }")) {
|
|
943
1009
|
const t = normalizeTable(v, sheet.name, true);
|
|
944
1010
|
book.checkTable(t, sheet);
|
|
945
|
-
sheet.
|
|
1011
|
+
sheet.pushTable(t);
|
|
946
1012
|
}
|
|
947
1013
|
const merges = s.get("merges");
|
|
948
1014
|
if (merges !== void 0) {
|
|
@@ -3587,9 +3587,13 @@ declare class NewSheet {
|
|
|
3587
3587
|
private readonly priorities;
|
|
3588
3588
|
/** @internal Protection, page setup, headers and footers, page breaks, print area and titles. */
|
|
3589
3589
|
print: SheetPrintData;
|
|
3590
|
-
/** @internal Tables in the order they were added (TD-01). */
|
|
3590
|
+
/** @internal Tables in the order they were added (TD-01); add them with {@link NewSheet.pushTable}. */
|
|
3591
3591
|
readonly tableList: NormalizedTable[];
|
|
3592
3592
|
private count;
|
|
3593
|
+
private readonly mergeIndex;
|
|
3594
|
+
private mergesIndexed;
|
|
3595
|
+
private readonly tableIndex;
|
|
3596
|
+
private tablesIndexed;
|
|
3593
3597
|
/** @internal Use {@link NewWorkbook.addSheet}. */
|
|
3594
3598
|
constructor(name: string, state: SheetState, book: NewWorkbook, sheetId?: number | null);
|
|
3595
3599
|
/** Number of cells that have a value or a style. */
|
|
@@ -3682,6 +3686,12 @@ declare class NewSheet {
|
|
|
3682
3686
|
* ```
|
|
3683
3687
|
*/
|
|
3684
3688
|
merge(range: string): this;
|
|
3689
|
+
/** @internal Whether `range` shares a cell with a merged range of the sheet. */
|
|
3690
|
+
overlapsMerge(range: CellRange): boolean;
|
|
3691
|
+
/** @internal Whether `range` shares a cell with a table of the sheet. */
|
|
3692
|
+
overlapsTable(range: CellRange): boolean;
|
|
3693
|
+
/** @internal Adds a table checked by {@link NewWorkbook.checkTable} to the sheet. */
|
|
3694
|
+
pushTable(t: NormalizedTable): void;
|
|
3685
3695
|
/**
|
|
3686
3696
|
* Freezes the top rows and left columns so that they stay visible when scrolling. Pass
|
|
3687
3697
|
* `{ rows: 0, cols: 0 }` to remove the frozen panes.
|
|
@@ -3969,6 +3979,18 @@ declare class NewWorkbook {
|
|
|
3969
3979
|
readonly properties: WorkbookProperties;
|
|
3970
3980
|
private readonly list;
|
|
3971
3981
|
private readonly names;
|
|
3982
|
+
/** Sheets by lower-case name. */
|
|
3983
|
+
private readonly sheetsByName;
|
|
3984
|
+
/** Sheet names as given, in sheet order. */
|
|
3985
|
+
private readonly sheetNames;
|
|
3986
|
+
/** Sheets by the sheetId given to addSheet. */
|
|
3987
|
+
private readonly sheetsById;
|
|
3988
|
+
/** Defined names by nameScopeKey. */
|
|
3989
|
+
private readonly namesByScope;
|
|
3990
|
+
/** Lower-case defined names, of any scope. */
|
|
3991
|
+
private readonly lowerNames;
|
|
3992
|
+
/** Lower-case names and display names of the tables of all sheets. */
|
|
3993
|
+
private readonly tableNames;
|
|
3972
3994
|
private readonly styleIds;
|
|
3973
3995
|
private readonly styleList;
|
|
3974
3996
|
/** @internal `workbookProtection`, set by {@link NewWorkbook.protect}. */
|
|
@@ -4068,6 +4090,8 @@ declare class NewWorkbook {
|
|
|
4068
4090
|
* ```
|
|
4069
4091
|
*/
|
|
4070
4092
|
defineName(name: string, refersTo: string, options?: DefinedNameOptions): this;
|
|
4093
|
+
/** @internal Records the names of a table added to a sheet ({@link NewSheet.pushTable}). */
|
|
4094
|
+
tableAdded(t: NormalizedTable): void;
|
|
4071
4095
|
/**
|
|
4072
4096
|
* @internal Checks that a table's names are unique in the workbook (tables and defined names,
|
|
4073
4097
|
* ignoring case; Part 1 §18.5.1.2) and that it overlaps no table or merged range of its sheet.
|
|
@@ -4499,6 +4523,12 @@ interface WriteTableOptions {
|
|
|
4499
4523
|
readonly preset?: "report" | "plain";
|
|
4500
4524
|
/** Date system of the new workbook. Default `"1900"`. */
|
|
4501
4525
|
readonly dateSystem?: DateSystem;
|
|
4526
|
+
/**
|
|
4527
|
+
* Bounds the sheet: at most `max(maxCells, 16 × cells written)` positions (rows × columns), so
|
|
4528
|
+
* a few values spread over many rows and columns are refused with `XLV_E_LIMIT_EXCEEDED`.
|
|
4529
|
+
* Default 5 000 000, the `maxCells` read limit; raise it for a large sparse table.
|
|
4530
|
+
*/
|
|
4531
|
+
readonly maxCells?: number;
|
|
4502
4532
|
}
|
|
4503
4533
|
/**
|
|
4504
4534
|
* Writes rows as a formatted XLSX table and returns the file's bytes. Rows are objects (keys
|
|
@@ -4635,14 +4665,15 @@ interface WorkbookStream {
|
|
|
4635
4665
|
interface OpenWorkbookStreamOptions {
|
|
4636
4666
|
/**
|
|
4637
4667
|
* Read limits. Streaming defaults differ from `DEFAULT_READ_LIMITS`: `maxCells` does not apply
|
|
4638
|
-
* (memory does not grow with cells), `maxEntryBytes` is 4 GiB for worksheets (inflated piece by
|
|
4668
|
+
* (memory does not grow with cells; one `<row>` element may hold at most 16 384 cells), `maxEntryBytes` is 4 GiB for worksheets (inflated piece by
|
|
4639
4669
|
* piece) and `maxTotalBytes` 8 GiB. Parts held in memory whole (shared strings, styles, the
|
|
4640
4670
|
* workbook, relationships, notes) keep the batch bound of 256 MiB unless `maxEntryBytes` is given.
|
|
4641
4671
|
*/
|
|
4642
4672
|
readonly limits?: Partial<ReadLimits>;
|
|
4643
4673
|
}
|
|
4644
4674
|
/**
|
|
4645
|
-
* Limits that differ in streaming mode (D-401): `maxCells` does not apply
|
|
4675
|
+
* Limits that differ in streaming mode (D-401): `maxCells` does not apply (but one `<row>` element
|
|
4676
|
+
* may hold at most 16 384 cells, D-J01), and single parts may
|
|
4646
4677
|
* decompress to 4 GiB (8 GiB in total). All other `DEFAULT_READ_LIMITS` stay.
|
|
4647
4678
|
*
|
|
4648
4679
|
* @example
|
package/docs/errors.md
CHANGED
|
@@ -95,9 +95,13 @@ Some limits also bound work derived from a small file (1.1.1, D-H01):
|
|
|
95
95
|
|
|
96
96
|
- `maxCells` (`details.limit` `maxCells`, `details.value` the budget): `toCSV`, `readTable`, `toJSON`, `readTableStream`, `xlvane read` and `xlvane convert` write at most `max(maxCells, 16 × cells read)` positions (rows × columns of the output), so a few cells far apart cannot make them produce a value for every position between; `@xlvane/compat`'s `xlsx.load` creates at most as many cells for merged ranges, tables and column definitions, and looks up at most 64 Mi rows (or 16 per cell read) for row settings up to the sheets' dimensions. Pass a smaller `range`, or raise `maxCells` for a trusted file.
|
|
97
97
|
- `maxXmlTextLength`: also the length of an XML comment or processing instruction.
|
|
98
|
-
- `maxSharedStringChars`: also the text of shared formulas, counted apart from shared strings: the length of each sharing cell's formula, as moved to that cell.
|
|
98
|
+
- `maxSharedStringChars`: also the text of shared formulas, counted apart from shared strings: the length of each sharing cell's formula, as moved to that cell. From 1.2.3 (D-J01) also the header names that `readTable`, `toJSON` and `readTableStream` repeat in every row object: the sum of the key lengths times the number of rows (a long header over many rows would otherwise make JSON text far larger than the file); `readTableStream` counts one row at a time.
|
|
99
99
|
|
|
100
|
-
|
|
100
|
+
From 1.2.3 (D-J01):
|
|
101
|
+
|
|
102
|
+
- `maxCells`: `fromCSV`, `fromJSON` and `writeTable` write at most `max(maxCells, 16 × cells written)` positions (rows × columns), so one very wide row followed by many short ones cannot make them visit every position; each takes a `maxCells` option (default 5 000 000). While streaming, one `<row>` element may hold at most 16 384 cells (`details.value` 16384), the most a row can have, because its cells are held until the element ends. `@xlvane/compat` `xlsx.load` also counts the cells of defined names against its budget, and its `xlsx.read`, `csv.read` and `stream.xlsx.WorkbookReader` stop collecting a stream after `maxInputBytes` (256 MiB; `details.limit` `maxInputBytes`).
|
|
103
|
+
|
|
104
|
+
The MCP server (`@xlvane/mcp`) returns it as a tool error for a file larger than `--max-file-size` (`details.limit` `maxFileBytes`; `convert` to JSON stops building the text at that size), for a workbook with more cells than `--max-cells` (`details.limit` `maxCells`) and for a result longer than `--max-response-chars` (`details.limit` `maxResponseChars`; `read_table` instead returns the rows that fit). `xlvane convert` to JSON returns it when the text would be longer than a JavaScript string can be.
|
|
101
105
|
|
|
102
106
|
`fromCSV` (and `xlvane convert`/`create` with CSV input) throws it for text with more fields than `maxCells` (default 5 000 000, `details.limit` `maxCells`), more than 16 384 fields on a line or more than 1 048 576 lines.
|
|
103
107
|
|
package/docs/exceljs-compat.md
CHANGED
|
@@ -38,7 +38,7 @@ Status: **supported** means the same behaviour as exceljs for the cases covered
|
|
|
38
38
|
| `xlsx.write(stream)` | supported | | `lib/xlsx/xlsx.js` L643 |
|
|
39
39
|
| `xlsx.createInputStream()` | supported | Throws the deprecation error, as in exceljs 4. | `lib/xlsx/xlsx.js` L238 |
|
|
40
40
|
| `csv.read(stream, options)`, `csv.readFile(path, options)` | supported | Adds a worksheet, with exceljs's options (`sheetName`, `dateFormats`, `map`, `parserOptions`: `delimiter`, `quote`, `escape`, `trim`/`ltrim`/`rtrim`, `ignoreEmpty`, `comment`, `skipLines`, `skipRows`, `maxRows`, `headers`, `renameHeaders`, `encoding`) and its default conversion of fields (X4-23), on xlvane's own parser (no fast-csv). Accepts a Node.js readable stream, a Web `ReadableStream` or any (async) iterable of strings or bytes. Differences: text of white space only stays text (exceljs reads `' '` as 0), `-Infinity` stays text (it cannot be stored), a leading byte order mark is dropped (exceljs keeps it in the first field), `headers: true` skips the header line (exceljs throws `data.map is not a function`), a missing file or a malformed line throws `XLV_E_FILE_READ` / `XLV_E_CSV_MALFORMED`. `readFile` needs `node:fs` (Node.js, Bun, Deno). Dates are parsed as dayjs 1.11 customParseFormat parses them in strict mode (D-E06): letters that are no token, such as an unbracketed `T` or `dddd`, are skipped, and each token is searched in the rest of the text. An unclosed quote is reported on the line where it opens; a `quote` or `escape` longer than one character throws `XLV_E_INVALID_OPTION`. | `lib/csv/csv.js` L32–98 |
|
|
41
|
-
| `csv.write(stream, options)`, `csv.writeFile(path, options)`, `csv.writeBuffer(options)` | supported | Writes one worksheet with exceljs's options (`sheetName`, `sheetId`, `dateFormat`, `dateUTC`, `map`, `includeEmptyRows`, `encoding`, `formatterOptions`: `delimiter`, `rowDelimiter`, `quote`, `escape`, `quoteColumns`, `headers`, `writeHeaders`, `includeEndRowDelimiter`, `writeBOM`) and fast-csv's quoting (X4-24); the cross-check sample gives the same text as exceljs 4.4.0. Differences: text that starts with `=`, `+`, `-`, `@`, tab, CR, LF or another control character, or with a full-width or small form of these signs, is written with a leading `'` (CSV injection, docs/facts/csv.md CSV-10, CSV-12; `formulaInjection: "off"` writes it unchanged, as exceljs); a formula result of 0 or `false` is written (exceljs writes an empty field), a date result is formatted as a date and an error result as its text; one blank line per missing row also before the first row (exceljs writes one too few there); a `sheetName` or `sheetId` that does not exist throws `XLV_E_SHEET_NOT_FOUND` (exceljs writes an empty file); header names in `formatterOptions.headers` are escaped like values. With `headers`, an array row gives one field per header (longer rows cut, shorter ones end in empty fields), as fast-csv writes it; `dateFormat: ''` uses the default format, as dayjs does (D-E06). `writeFile` writes `utf8`, `utf16le` or `latin1` and needs `node:fs`. | `lib/csv/csv.js` L109–186 |
|
|
41
|
+
| `csv.write(stream, options)`, `csv.writeFile(path, options)`, `csv.writeBuffer(options)` | supported | Writes one worksheet with exceljs's options (`sheetName`, `sheetId`, `dateFormat`, `dateUTC`, `map`, `includeEmptyRows`, `encoding`, `formatterOptions`: `delimiter`, `rowDelimiter`, `quote`, `escape`, `quoteColumns`, `headers`, `writeHeaders`, `includeEndRowDelimiter`, `writeBOM`) and fast-csv's quoting (X4-24); the cross-check sample gives the same text as exceljs 4.4.0. Differences: text that starts with `=`, `+`, `-`, `@`, tab, CR, LF or another control character, or with a full-width or small form of these signs, is written with a leading `'` (CSV injection, docs/facts/csv.md CSV-10, CSV-12; `formulaInjection: "off"` writes it unchanged, as exceljs); a formula result of 0 or `false` is written (exceljs writes an empty field), a date result is formatted as a date and an error result as its text; one blank line per missing row also before the first row (exceljs writes one too few there); a `sheetName` or `sheetId` that does not exist throws `XLV_E_SHEET_NOT_FOUND` (exceljs writes an empty file); header names in `formatterOptions.headers` are escaped like values. With `headers`, an array row gives one field per header (longer rows cut, shorter ones end in empty fields), as fast-csv writes it; `dateFormat: ''` uses the default format, as dayjs does (D-E06). `writeFile` writes `utf8`, `utf16le` or `latin1` and needs `node:fs`; with `latin1` characters above U+00FF are written as `?` (Node.js, and so exceljs, keeps only their low byte, which can turn U+013D into `=`), and formatter options with such characters throw `XLV_E_INVALID_OPTION`. While formulas are escaped (the default), a `delimiter` holding a formula sign (`=`, `+`, `-`, `@`, their full-width or small forms) or a control character (a lone tab is allowed), a `rowDelimiter` other than `\n`, `\r\n` or `\r`, a `quote` other than `"` or `false`, an `escape` other than the quote, and with `quote: false` a field that starts with `"` throw `XLV_E_INVALID_OPTION`, because a standard CSV reader would otherwise get a field starting with a formula (docs/facts/csv.md CSV-13); `formulaInjection: "off"` writes them as exceljs does. | `lib/csv/csv.js` L109–186 |
|
|
42
42
|
| `csv.createInputStream()` | supported | Throws the deprecation error, as in exceljs 4. | `lib/csv/csv.js` L101 |
|
|
43
43
|
| `definedNames` (`add`, `addEx`, `remove`, `removeEx`, `removeAllNames`, `forEach`, `getNames`, `getNamesEx`, `getRanges`, `getMatrix`, `model`) | supported | As in exceljs, a name is a set of cells, written as rectangles with absolute references and no sheet scope; on reading, names that are not cell ranges (constants, formulas, `#REF!`) and the sheet scope (`localSheetId`) are dropped. Built-in `_xlnm.` names are not listed: print areas and titles are read into `pageSetup`, as in exceljs, and xlvane writes the autofilter name itself. Names exceljs accepts but Excel does not (for example `A1` or names starting with `_xl`) throw `XLV_E_INVALID_DEFINED_NAME` when the workbook is written. | `lib/doc/defined-names.js`; `lib/xlsx/xform/book/defined-name-xform.js`; `lib/xlsx/xform/book/workbook-xform.js` L187 |
|
|
44
44
|
| `addImage(image)`, `getImage(id)` | supported | PNG, JPEG and GIF given as `buffer`, `base64` (a data URL is accepted, as in exceljs) or `filename`; the format is recognised from the bytes, `extension` is kept but not used (exceljs writes `image/jpg` for `extension: 'jpg'`). As in exceljs, a `filename` is read when the workbook is written (`xlsx.writeBuffer`/`writeFile`/`write` and the streaming `WorkbookWriter`), so a missing file fails there; outside Node.js, Bun and Deno it gives `XLV_E_NOT_SUPPORTED`. Other formats throw `XLV_E_INVALID_IMAGE`. Read back, each image part is one media entry `{ type, name, extension, buffer }`, `buffer` a `Uint8Array`. Identical images are stored once, with one relationship per drawing (exceljs writes one per use). | `lib/doc/workbook.js` L142–151; `lib/xlsx/xlsx.js` L421–442 (L426–429: `filename` read at write) |
|
|
@@ -179,7 +179,7 @@ A workbook loaded with `xlsx.load()` is written again by `xlsx.writeBuffer()` (`
|
|
|
179
179
|
- `row.destroy()` and `cell.destroy()` do nothing; the row or cell stays usable (exceljs deletes its internals).
|
|
180
180
|
- Errors found while writing name the worksheet and, for cell values and styles, the cell (`error.location`).
|
|
181
181
|
- `worksheet.tables` and `dataValidations.model` have no prototype: keys such as `constructor` or `__proto__` are plain entries.
|
|
182
|
-
- CSV writing: a field whose first character after any NUL characters is a formula character is escaped (NULs are removed when writing); with `formatterOptions.quote: false`, a field with CR, LF or the delimiter throws `XLV_E_INVALID_OPTION` (fast-csv writes it and the CSV then has other fields or rows). CSV reading: fields such as `constructor` or `__proto__` stay text.
|
|
182
|
+
- CSV writing: a field whose first character after any NUL characters is a formula character is escaped (NULs are removed when writing); options that would get around the escape are refused (CSV-13); `latin1` output writes characters above U+00FF as `?`; `csv.read` stops collecting input after 256 MiB (`XLV_E_LIMIT_EXCEEDED`, `maxInputBytes`); with `formatterOptions.quote: false`, a field with CR, LF or the delimiter throws `XLV_E_INVALID_OPTION` (fast-csv writes it and the CSV then has other fields or rows). CSV reading: fields such as `constructor` or `__proto__` stay text.
|
|
183
183
|
|
|
184
184
|
## Known differences found by the cross-check
|
|
185
185
|
|