@xlvane/compat 1.2.3 → 1.3.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.
- package/dist/index.d.ts +14 -3
- package/dist/index.js +581 -52
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -408,9 +408,13 @@ interface CsvWriteOptions {
|
|
|
408
408
|
/** fast-csv formatter options: `delimiter`, `rowDelimiter`, `quote`, `escape`, `quoteColumns`, `headers`, `writeHeaders`, `includeEndRowDelimiter`, `writeBOM`. */
|
|
409
409
|
formatterOptions?: Record<string, unknown>;
|
|
410
410
|
/**
|
|
411
|
-
* `"escape"` (default): text that starts with `=`, `+`, `-`, `@`, tab, CR
|
|
412
|
-
* full-width forms)
|
|
413
|
-
*
|
|
411
|
+
* `"escape"` (default): text that starts with `=`, `+`, `-`, `@`, tab, CR, LF or another
|
|
412
|
+
* control character (or the full-width forms of the signs), or with a byte order mark, is
|
|
413
|
+
* written with a leading `'`, so spreadsheet applications show it as text (OWASP CSV
|
|
414
|
+
* Injection); so is a `latin1` or `utf16le` field whose first bytes are a byte order mark, and
|
|
415
|
+
* a `utf16le` field whose bytes without NULs start with `=`. `utf16le` output starts with the
|
|
416
|
+
* UTF-16LE byte order mark. A field holding a comma, `;`, tab or `|` is quoted whatever the
|
|
417
|
+
* delimiter. `"off"`: written unchanged, as exceljs does. An xlvane option.
|
|
414
418
|
*/
|
|
415
419
|
formulaInjection?: "escape" | "off";
|
|
416
420
|
}
|
|
@@ -2452,6 +2456,8 @@ export declare class Cell {
|
|
|
2452
2456
|
_master: Cell | null;
|
|
2453
2457
|
/** @internal Number of merged ranges this cell is the master of. */
|
|
2454
2458
|
_mergeCount: number;
|
|
2459
|
+
/** The moved formula of a `{ sharedFormula }` value, and what it was moved from (see `formula`). */
|
|
2460
|
+
private _moved;
|
|
2455
2461
|
/** @internal Use `worksheet.getCell()`. */
|
|
2456
2462
|
constructor(_row: Row, _col: number);
|
|
2457
2463
|
get worksheet(): Worksheet;
|
|
@@ -2808,6 +2814,11 @@ export declare class Worksheet {
|
|
|
2808
2814
|
_columns: Column[];
|
|
2809
2815
|
/** @internal */
|
|
2810
2816
|
_merges: MergeMap;
|
|
2817
|
+
/**
|
|
2818
|
+
* @internal Formulas of shared-formula masters by address, for the dependents of a worksheet
|
|
2819
|
+
* whose rows are not kept (stream.xlsx.WorkbookReader), so `cell.formula` can move them (K2).
|
|
2820
|
+
*/
|
|
2821
|
+
_sharedMasters: Map<string, string> | undefined;
|
|
2811
2822
|
/** The cells covered by `_merges` (row × 16 385 + column), valid while `_mergedVersion` equals `_merges.version`. */
|
|
2812
2823
|
private _mergedCells;
|
|
2813
2824
|
private _mergedVersion;
|
package/dist/index.js
CHANGED
|
@@ -1434,6 +1434,8 @@ function toNewWorkbook(sheets, date1904, meta = {}) {
|
|
|
1434
1434
|
}
|
|
1435
1435
|
return c;
|
|
1436
1436
|
};
|
|
1437
|
+
const styleFor = (cell) => useStyles ? cellStyleIn(cell, styleOf(cell.style)) : isDateCell(cell) ? { numFmt: 14 } : {};
|
|
1438
|
+
const uniform = uniformMerges(ws);
|
|
1437
1439
|
ws._rows.forEach((row) => {
|
|
1438
1440
|
if (!row) return;
|
|
1439
1441
|
if (row.height !== void 0 || row.hidden || row.outlineLevel) sheet.row(row.number, {
|
|
@@ -1443,13 +1445,31 @@ function toNewWorkbook(sheets, date1904, meta = {}) {
|
|
|
1443
1445
|
...row.collapsed ? { collapsed: true } : {}
|
|
1444
1446
|
});
|
|
1445
1447
|
row._cells.forEach((cell) => {
|
|
1446
|
-
if (cell)
|
|
1448
|
+
if (!cell) return;
|
|
1449
|
+
if (cell._master !== null && uniform.masters.has(cell._master)) {
|
|
1450
|
+
if (cell.note !== void 0) at(ws.name, cell.address, () => {
|
|
1451
|
+
const note = noteIn(cell);
|
|
1452
|
+
if (note) sheet.note(cell.address, note);
|
|
1453
|
+
});
|
|
1454
|
+
} else at(ws.name, cell.address, () => writeCell(cell));
|
|
1447
1455
|
});
|
|
1448
1456
|
});
|
|
1457
|
+
for (const m of uniform.ranges) {
|
|
1458
|
+
const master = ws.findCell(m.top, m.left);
|
|
1459
|
+
if (!master) continue;
|
|
1460
|
+
const style = styleFor(master);
|
|
1461
|
+
if (Object.keys(style).length === 0) continue;
|
|
1462
|
+
const width = m.right - m.left + 1;
|
|
1463
|
+
const step = Math.max(1, Math.floor(MERGE_STYLE_BLOCK / width));
|
|
1464
|
+
for (let top = m.top; top <= m.bottom; top += step) {
|
|
1465
|
+
const bottom = Math.min(m.bottom, top + step - 1);
|
|
1466
|
+
sheet.style(`${columnLetter(m.left)}${String(top)}:${columnLetter(m.right)}${String(bottom)}`, style);
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1449
1469
|
function writeCell(cell) {
|
|
1450
1470
|
const note = noteIn(cell);
|
|
1451
1471
|
if (note) sheet.note(cell.address, note);
|
|
1452
|
-
const style =
|
|
1472
|
+
const style = styleFor(cell);
|
|
1453
1473
|
const value = cell.type === ValueType.Merge ? null : valueIn(cell, sharedRanges);
|
|
1454
1474
|
if (value === null && Object.keys(style).length === 0) return;
|
|
1455
1475
|
sheet.set(cell.address, value, style);
|
|
@@ -1485,6 +1505,55 @@ function toNewWorkbook(sheets, date1904, meta = {}) {
|
|
|
1485
1505
|
for (const d of meta.definedNames ?? []) book.defineName(d.name, d.ranges.join(","));
|
|
1486
1506
|
return book;
|
|
1487
1507
|
}
|
|
1508
|
+
/** Cells styled by one `sheet.style` call for a merged range: at most what the core styles at once. */
|
|
1509
|
+
const MERGE_STYLE_BLOCK = 1048576;
|
|
1510
|
+
/** Whether two exceljs styles have the same parts (the same objects, as a merge's copies have). */
|
|
1511
|
+
function sameStyleParts(a, b) {
|
|
1512
|
+
if (a === b) return true;
|
|
1513
|
+
let n = 0;
|
|
1514
|
+
for (const k in a) {
|
|
1515
|
+
if (a[k] !== b[k]) return false;
|
|
1516
|
+
n++;
|
|
1517
|
+
}
|
|
1518
|
+
for (const _ in b) n--;
|
|
1519
|
+
return n === 0;
|
|
1520
|
+
}
|
|
1521
|
+
/**
|
|
1522
|
+
* The merged ranges of a worksheet whose other cells all exist, belong to the range's master and
|
|
1523
|
+
* carry the master's style (the copy `mergeCells` gives them, as in exceljs), and the masters all
|
|
1524
|
+
* of whose ranges are such. Their cells are written as the master's style over the range (K2),
|
|
1525
|
+
* and their notes one by one (K3).
|
|
1526
|
+
*/
|
|
1527
|
+
function uniformMerges(ws) {
|
|
1528
|
+
const ranges = [];
|
|
1529
|
+
const masters = /* @__PURE__ */ new Set();
|
|
1530
|
+
const mixed = /* @__PURE__ */ new Set();
|
|
1531
|
+
for (const m of ws._merges.values()) {
|
|
1532
|
+
const master = ws.findCell(m.top, m.left);
|
|
1533
|
+
if (!master) continue;
|
|
1534
|
+
let ok = true;
|
|
1535
|
+
for (let r = m.top; ok && r <= m.bottom; r++) {
|
|
1536
|
+
const row = ws.findRow(r);
|
|
1537
|
+
for (let c = m.left; c <= m.right; c++) {
|
|
1538
|
+
if (r === m.top && c === m.left) continue;
|
|
1539
|
+
const cell = row?.findCell(c);
|
|
1540
|
+
if (!cell || cell._master !== master || !sameStyleParts(cell.style, master.style)) {
|
|
1541
|
+
ok = false;
|
|
1542
|
+
break;
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
if (ok) {
|
|
1547
|
+
ranges.push(m);
|
|
1548
|
+
masters.add(master);
|
|
1549
|
+
} else mixed.add(master);
|
|
1550
|
+
}
|
|
1551
|
+
for (const master of mixed) masters.delete(master);
|
|
1552
|
+
return {
|
|
1553
|
+
ranges: ranges.filter((m) => masters.has(ws.findCell(m.top, m.left))),
|
|
1554
|
+
masters
|
|
1555
|
+
};
|
|
1556
|
+
}
|
|
1488
1557
|
/**
|
|
1489
1558
|
* The validations, conditional formats, protection and printing settings of a worksheet in the
|
|
1490
1559
|
* form of the core API (D-C06). exceljs always writes page margins and page setup
|
|
@@ -1598,6 +1667,18 @@ function viewOut(view, freeze) {
|
|
|
1598
1667
|
};
|
|
1599
1668
|
}
|
|
1600
1669
|
/**
|
|
1670
|
+
* Whether a formula read by xlvane has text, without making the core build it: the core moves a
|
|
1671
|
+
* shared master's text to a dependent only when the dependent's `text` is read (a getter), and
|
|
1672
|
+
* gives `null` (a plain value) when it cannot move it. Reading the text of every dependent made a
|
|
1673
|
+
* small file with a huge master and many dependents exhaust memory (K2), and exceljs reads a
|
|
1674
|
+
* dependent as `{ sharedFormula }` without its text anyway.
|
|
1675
|
+
*/
|
|
1676
|
+
function hasFormulaText(f) {
|
|
1677
|
+
const d = Object.getOwnPropertyDescriptor(f, "text");
|
|
1678
|
+
if (d !== void 0 && d.get !== void 0) return true;
|
|
1679
|
+
return f.text !== null;
|
|
1680
|
+
}
|
|
1681
|
+
/**
|
|
1601
1682
|
* A formula read by xlvane in exceljs's shape (cell-xform.js L460–470; X4-29): a shared-formula
|
|
1602
1683
|
* master `{ formula, result, ref, shareType: 'shared' }`, a dependent `{ sharedFormula: master,
|
|
1603
1684
|
* result }` (its `cell.formula` is translated), an array formula `{ formula, result, ref,
|
|
@@ -2858,6 +2939,56 @@ function notSupported(feature) {
|
|
|
2858
2939
|
details: { feature }
|
|
2859
2940
|
});
|
|
2860
2941
|
}
|
|
2942
|
+
/**
|
|
2943
|
+
* The shared-formula text moved per workbook (K4 S1, D-K04). A move costs time in proportion to the
|
|
2944
|
+
* master's length, and the load-time budget is spent once, so a 4 KB file whose 1 000 000-character
|
|
2945
|
+
* master is shared with 250 cells took about 47 s to read every dependent's formula once. Two
|
|
2946
|
+
* budgets, counting the master's text read plus the moved text built for each `cell.formula` that
|
|
2947
|
+
* is not kept yet:
|
|
2948
|
+
* - every move: 512 Mi characters, the default `maxSharedStringChars` with which xlvane's
|
|
2949
|
+
* `readWorkbook` expands shared formulas, so a workbook the core reads is read here too;
|
|
2950
|
+
* - moves of masters longer than 8192 characters, the longest formula Office writes ([MS-OI29500]
|
|
2951
|
+
* §2.1.1085 a): 32 Mi characters, so such a master stops after a few moves.
|
|
2952
|
+
*/
|
|
2953
|
+
const SHARED_FORMULA_TEXT = 536870912;
|
|
2954
|
+
const LONG_SHARED_FORMULA_TEXT = 33554432;
|
|
2955
|
+
const LONG_FORMULA = 8192;
|
|
2956
|
+
/** Characters moved in all and for long masters, per workbook (a `Worksheet.workbook`). */
|
|
2957
|
+
const sharedFormulaSpent = /* @__PURE__ */ new WeakMap();
|
|
2958
|
+
/**
|
|
2959
|
+
* @internal Moves the text of a shared-formula master at `master` to `row`/`col` of `sheet` with
|
|
2960
|
+
* `shiftFormula`, counted against the workbook's shared-formula budgets; `undefined` when the
|
|
2961
|
+
* formula cannot be moved. Throws `XLV_E_LIMIT_EXCEEDED` before a move that would pass one.
|
|
2962
|
+
*/
|
|
2963
|
+
function moveSharedFormula(workbook, sheet, text, master, row, col) {
|
|
2964
|
+
let spent = sharedFormulaSpent.get(workbook);
|
|
2965
|
+
if (spent === void 0) sharedFormulaSpent.set(workbook, spent = {
|
|
2966
|
+
chars: 0,
|
|
2967
|
+
long: 0
|
|
2968
|
+
});
|
|
2969
|
+
const cost = 2 * text.length;
|
|
2970
|
+
const long = text.length > LONG_FORMULA;
|
|
2971
|
+
const limit = long && spent.long + cost > LONG_SHARED_FORMULA_TEXT ? LONG_SHARED_FORMULA_TEXT : spent.chars + cost > SHARED_FORMULA_TEXT ? SHARED_FORMULA_TEXT : null;
|
|
2972
|
+
if (limit !== null) {
|
|
2973
|
+
const cell = `${columnLetter(col)}${String(row)}`;
|
|
2974
|
+
throw new XlvError("XLV_E_LIMIT_EXCEEDED", `Moving the shared formula of ${columnLetter(master.col)}${String(master.row)} (${String(text.length)} characters) to ${cell} of worksheet "${sheet}" would bring the shared-formula text moved in this workbook past ${String(limit)} characters.`, {
|
|
2975
|
+
location: {
|
|
2976
|
+
sheet,
|
|
2977
|
+
cell
|
|
2978
|
+
},
|
|
2979
|
+
hint: "The workbook shares very long formulas with many cells. Read the dependents' cached result (cell.result) instead of cell.formula, or read the file with xlvane's readWorkbook, whose shared-formula text is limited by limits.maxSharedStringChars.",
|
|
2980
|
+
details: {
|
|
2981
|
+
limit: "sharedFormulaText",
|
|
2982
|
+
value: limit
|
|
2983
|
+
}
|
|
2984
|
+
});
|
|
2985
|
+
}
|
|
2986
|
+
const moved = shiftFormula(text, row - master.row, col - master.col);
|
|
2987
|
+
const used = text.length + (moved?.length ?? 0);
|
|
2988
|
+
spent.chars += used;
|
|
2989
|
+
if (long) spent.long += used;
|
|
2990
|
+
return moved ?? void 0;
|
|
2991
|
+
}
|
|
2861
2992
|
/** Column letters for a 1-based number (A … XFD). */
|
|
2862
2993
|
function columnLetter(n) {
|
|
2863
2994
|
let s = "";
|
|
@@ -2902,7 +3033,7 @@ function typeOf(value) {
|
|
|
2902
3033
|
if (value instanceof Date) return ValueType.Date;
|
|
2903
3034
|
if (isRecord(value)) {
|
|
2904
3035
|
if (value["text"] && value["hyperlink"]) return ValueType.Hyperlink;
|
|
2905
|
-
if (value["
|
|
3036
|
+
if (value["sharedFormula"] || value["formula"]) return ValueType.Formula;
|
|
2906
3037
|
if (value["richText"]) return ValueType.RichText;
|
|
2907
3038
|
if (value["sharedString"]) return ValueType.SharedString;
|
|
2908
3039
|
if (value["error"]) return ValueType.Error;
|
|
@@ -2951,6 +3082,8 @@ var Cell = class {
|
|
|
2951
3082
|
_master = null;
|
|
2952
3083
|
/** @internal Number of merged ranges this cell is the master of. */
|
|
2953
3084
|
_mergeCount = 0;
|
|
3085
|
+
/** The moved formula of a `{ sharedFormula }` value, and what it was moved from (see `formula`). */
|
|
3086
|
+
_moved = void 0;
|
|
2954
3087
|
/** @internal Use `worksheet.getCell()`. */
|
|
2955
3088
|
constructor(_row, _col) {
|
|
2956
3089
|
this._row = _row;
|
|
@@ -3036,12 +3169,39 @@ var Cell = class {
|
|
|
3036
3169
|
const v = this.value;
|
|
3037
3170
|
if (!isRecord(v)) return void 0;
|
|
3038
3171
|
if (typeof v["formula"] === "string") return v["formula"];
|
|
3039
|
-
|
|
3040
|
-
|
|
3172
|
+
const shared = v["sharedFormula"];
|
|
3173
|
+
if (typeof shared === "string") {
|
|
3174
|
+
const masterRef = shared.replace(/\$/g, "");
|
|
3041
3175
|
const master = this.worksheet.findCell(masterRef);
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3176
|
+
let text;
|
|
3177
|
+
let at;
|
|
3178
|
+
if (!master) {
|
|
3179
|
+
text = this.worksheet._sharedMasters?.get(masterRef.toUpperCase());
|
|
3180
|
+
if (text === void 0) return void 0;
|
|
3181
|
+
at = decodeAddress(masterRef);
|
|
3182
|
+
} else {
|
|
3183
|
+
if (master === this) return void 0;
|
|
3184
|
+
text = master.formula;
|
|
3185
|
+
if (text === void 0) return void 0;
|
|
3186
|
+
at = {
|
|
3187
|
+
row: master.row,
|
|
3188
|
+
col: master.col
|
|
3189
|
+
};
|
|
3190
|
+
}
|
|
3191
|
+
const kept = this._moved;
|
|
3192
|
+
if (kept !== void 0 && kept.value === v && kept.shared === shared && kept.master === text && kept.masterRow === at.row && kept.masterCol === at.col && kept.row === this.row && kept.col === this.col) return kept.text;
|
|
3193
|
+
const moved = moveSharedFormula(this.worksheet.workbook, this.worksheet.name, text, at, this.row, this.col);
|
|
3194
|
+
this._moved = {
|
|
3195
|
+
value: v,
|
|
3196
|
+
shared,
|
|
3197
|
+
master: text,
|
|
3198
|
+
masterRow: at.row,
|
|
3199
|
+
masterCol: at.col,
|
|
3200
|
+
row: this.row,
|
|
3201
|
+
col: this.col,
|
|
3202
|
+
text: moved
|
|
3203
|
+
};
|
|
3204
|
+
return moved;
|
|
3045
3205
|
}
|
|
3046
3206
|
}
|
|
3047
3207
|
/** `FormulaType.Shared` for a `{ sharedFormula }` value, `Master` for other formulas, otherwise `None` (lib/doc/cell.js L807). */
|
|
@@ -3782,6 +3942,11 @@ var Worksheet = class {
|
|
|
3782
3942
|
_columns = [];
|
|
3783
3943
|
/** @internal */
|
|
3784
3944
|
_merges = new MergeMap();
|
|
3945
|
+
/**
|
|
3946
|
+
* @internal Formulas of shared-formula masters by address, for the dependents of a worksheet
|
|
3947
|
+
* whose rows are not kept (stream.xlsx.WorkbookReader), so `cell.formula` can move them (K2).
|
|
3948
|
+
*/
|
|
3949
|
+
_sharedMasters = void 0;
|
|
3785
3950
|
/** The cells covered by `_merges` (row × 16 385 + column), valid while `_mergedVersion` equals `_merges.version`. */
|
|
3786
3951
|
_mergedCells = /* @__PURE__ */ new Set();
|
|
3787
3952
|
_mergedVersion = -1;
|
|
@@ -4698,8 +4863,12 @@ const INJECTION_START = /* @__PURE__ */ new Set([
|
|
|
4698
4863
|
"﹦",
|
|
4699
4864
|
"﹢",
|
|
4700
4865
|
"﹣",
|
|
4701
|
-
"﹫"
|
|
4866
|
+
"﹫",
|
|
4867
|
+
"",
|
|
4868
|
+
""
|
|
4702
4869
|
]);
|
|
4870
|
+
/** Characters a field is quoted for while formulas are escaped, whatever the delimiter: other applications may split on them (K2). */
|
|
4871
|
+
const COMMON_DELIMITERS = ",; |";
|
|
4703
4872
|
function invalid(name, message, hint) {
|
|
4704
4873
|
return new XlvError("XLV_E_INVALID_OPTION", message, {
|
|
4705
4874
|
hint,
|
|
@@ -4985,9 +5154,41 @@ function malformed(line, message) {
|
|
|
4985
5154
|
details: { line }
|
|
4986
5155
|
});
|
|
4987
5156
|
}
|
|
4988
|
-
/**
|
|
4989
|
-
|
|
4990
|
-
|
|
5157
|
+
/** Rows and columns of a worksheet (Part 1 §18.3.1.73, ST_CellRef up to XFD1048576). */
|
|
5158
|
+
const MAX_SHEET_ROWS = 1048576;
|
|
5159
|
+
const MAX_SHEET_COLUMNS = 16384;
|
|
5160
|
+
/** Fields added to the worksheet in total, as the core's `maxCells` read limit (K3). */
|
|
5161
|
+
const MAX_CSV_CELLS = DEFAULT_READ_LIMITS.maxCells;
|
|
5162
|
+
function tooMany(what, line) {
|
|
5163
|
+
if (what === "cells") return new XlvError("XLV_E_LIMIT_EXCEEDED", `CSV line ${String(line)}: the CSV has more than ${String(MAX_CSV_CELLS)} fields in the rows read, the most csv.read adds to a worksheet.`, {
|
|
5164
|
+
location: { line },
|
|
5165
|
+
hint: "Split the CSV file into smaller parts, read only part of it with parserOptions.maxRows or skipRows, or read it with the core fromCSV, whose maxCells option can be raised for a trusted file.",
|
|
5166
|
+
details: {
|
|
5167
|
+
limit: "maxCells",
|
|
5168
|
+
value: MAX_CSV_CELLS,
|
|
5169
|
+
line
|
|
5170
|
+
}
|
|
5171
|
+
});
|
|
5172
|
+
const rows = what === "rows";
|
|
5173
|
+
const max = rows ? MAX_SHEET_ROWS : MAX_SHEET_COLUMNS;
|
|
5174
|
+
return new XlvError("XLV_E_LIMIT_EXCEEDED", `CSV line ${String(line)}: ${rows ? `the CSV has more than ${String(max)} rows` : `the row has more than ${String(max)} fields`}, more than a worksheet can hold.`, {
|
|
5175
|
+
location: { line },
|
|
5176
|
+
hint: rows ? "Split the CSV file into parts of at most 1048576 rows, or read only part of it with parserOptions.maxRows." : "Split the CSV file into parts of at most 16384 columns, or check the delimiter (parserOptions.delimiter).",
|
|
5177
|
+
details: {
|
|
5178
|
+
limit: rows ? "maxRows" : "maxColumns",
|
|
5179
|
+
value: max,
|
|
5180
|
+
line
|
|
5181
|
+
}
|
|
5182
|
+
});
|
|
5183
|
+
}
|
|
5184
|
+
/**
|
|
5185
|
+
* The rows of CSV text, split as fast-csv 4.3 does with the given parser options, one at a time:
|
|
5186
|
+
* the rows that `ignoreEmpty`, the header, `skipRows` and `maxRows` drop are left out. More than
|
|
5187
|
+
* 1 048 576 rows, or a row of more than 16 384 fields, throw `XLV_E_LIMIT_EXCEEDED` as soon as
|
|
5188
|
+
* they are reached, because a worksheet cannot hold them (K2); so do more than 5 000 000 fields
|
|
5189
|
+
* in the rows given (`DEFAULT_READ_LIMITS.maxCells`, K3).
|
|
5190
|
+
*/
|
|
5191
|
+
function* csvRows(text, o) {
|
|
4991
5192
|
let at = 0;
|
|
4992
5193
|
let line = 1;
|
|
4993
5194
|
const n = text.length;
|
|
@@ -4996,6 +5197,10 @@ function parseCsv(text, o) {
|
|
|
4996
5197
|
if (text[i] === "\r" && text[i + 1] === "\n") return i + 2;
|
|
4997
5198
|
return i < n ? i + 1 : i;
|
|
4998
5199
|
};
|
|
5200
|
+
const trim = (f) => o.trim ? f.trim() : o.ltrim ? f.trimStart() : o.rtrim ? f.trimEnd() : f;
|
|
5201
|
+
let kept = 0;
|
|
5202
|
+
let emitted = 0;
|
|
5203
|
+
let cells = 0;
|
|
4999
5204
|
for (let k = 0; k < o.skipLines && at < n; k++) {
|
|
5000
5205
|
while (!atLineEnd(at)) at++;
|
|
5001
5206
|
at = skipLineEnd(at);
|
|
@@ -5008,38 +5213,50 @@ function parseCsv(text, o) {
|
|
|
5008
5213
|
line++;
|
|
5009
5214
|
continue;
|
|
5010
5215
|
}
|
|
5216
|
+
const rowLine = line;
|
|
5011
5217
|
const fields = [];
|
|
5218
|
+
let extra = 0;
|
|
5219
|
+
let extraText = false;
|
|
5220
|
+
const push = (field) => {
|
|
5221
|
+
if (fields.length < MAX_SHEET_COLUMNS) fields.push(field);
|
|
5222
|
+
else {
|
|
5223
|
+
extra++;
|
|
5224
|
+
if (field.trim() !== "") extraText = true;
|
|
5225
|
+
}
|
|
5226
|
+
};
|
|
5012
5227
|
for (;;) {
|
|
5013
5228
|
let start = at;
|
|
5014
5229
|
while (start < n && (text[start] === " " || text[start] === " ") && text[start] !== o.delimiter) start++;
|
|
5015
5230
|
if (o.quote !== "" && text[start] === o.quote) {
|
|
5016
5231
|
let value = "";
|
|
5017
5232
|
let i = start + 1;
|
|
5233
|
+
let from = i;
|
|
5018
5234
|
const opened = line;
|
|
5019
5235
|
for (;;) {
|
|
5020
5236
|
if (i >= n) throw malformed(opened, `missing closing ${o.quote} for the field that starts here.`);
|
|
5021
5237
|
const ch = text[i];
|
|
5022
5238
|
if (ch === o.escape && text[i + 1] === o.quote) {
|
|
5023
|
-
value += o.quote;
|
|
5239
|
+
value += text.slice(from, i) + o.quote;
|
|
5024
5240
|
i += 2;
|
|
5241
|
+
from = i;
|
|
5025
5242
|
continue;
|
|
5026
5243
|
}
|
|
5027
5244
|
if (ch === o.quote) {
|
|
5245
|
+
value += text.slice(from, i);
|
|
5028
5246
|
i++;
|
|
5029
5247
|
break;
|
|
5030
5248
|
}
|
|
5031
5249
|
if (ch === "\n") line++;
|
|
5032
|
-
value += ch;
|
|
5033
5250
|
i++;
|
|
5034
5251
|
}
|
|
5035
5252
|
while (i < n && (text[i] === " " || text[i] === " ") && text[i] !== o.delimiter) i++;
|
|
5036
5253
|
if (i < n && text[i] !== o.delimiter && text[i] !== "\n" && text[i] !== "\r") throw malformed(line, `unexpected text after the closing ${o.quote}.`);
|
|
5037
|
-
|
|
5254
|
+
push(value);
|
|
5038
5255
|
at = i;
|
|
5039
5256
|
} else {
|
|
5040
5257
|
let i = at;
|
|
5041
5258
|
while (i < n && text[i] !== o.delimiter && text[i] !== "\n" && text[i] !== "\r") i++;
|
|
5042
|
-
|
|
5259
|
+
push(text.slice(at, i));
|
|
5043
5260
|
at = i;
|
|
5044
5261
|
}
|
|
5045
5262
|
if (at < n && text[at] === o.delimiter) {
|
|
@@ -5051,14 +5268,30 @@ function parseCsv(text, o) {
|
|
|
5051
5268
|
at = skipLineEnd(at);
|
|
5052
5269
|
line++;
|
|
5053
5270
|
const row = fields.length === 1 && fields[0].trim() === "" ? [""] : fields;
|
|
5054
|
-
|
|
5271
|
+
if (o.ignoreEmpty && !extraText && row.every((f) => f.trim() === "")) continue;
|
|
5272
|
+
kept++;
|
|
5273
|
+
if (o.skipHeader && kept === 1) continue;
|
|
5274
|
+
if (kept - (o.skipHeader ? 1 : 0) <= o.skipRows) continue;
|
|
5275
|
+
if (o.maxRows > 0 && emitted >= o.maxRows) continue;
|
|
5276
|
+
if (extra > 0) throw tooMany("columns", rowLine);
|
|
5277
|
+
if (++emitted > MAX_SHEET_ROWS) throw tooMany("rows", rowLine);
|
|
5278
|
+
cells += row.length;
|
|
5279
|
+
if (cells > MAX_CSV_CELLS) throw tooMany("cells", rowLine);
|
|
5280
|
+
yield {
|
|
5281
|
+
fields: row.map(trim),
|
|
5282
|
+
line: rowLine
|
|
5283
|
+
};
|
|
5055
5284
|
}
|
|
5056
|
-
|
|
5057
|
-
|
|
5058
|
-
|
|
5059
|
-
|
|
5060
|
-
|
|
5061
|
-
|
|
5285
|
+
}
|
|
5286
|
+
/** The delimiters and line-break characters in `text`: at least the number of fields less one. */
|
|
5287
|
+
function fieldEnds(text, delimiter) {
|
|
5288
|
+
const d = delimiter.charCodeAt(0);
|
|
5289
|
+
let n = 0;
|
|
5290
|
+
for (let i = 0; i < text.length; i++) {
|
|
5291
|
+
const c = text.charCodeAt(i);
|
|
5292
|
+
if (c === d || c === 10 || c === 13) n++;
|
|
5293
|
+
}
|
|
5294
|
+
return n;
|
|
5062
5295
|
}
|
|
5063
5296
|
/**
|
|
5064
5297
|
* exceljs's default conversion of a CSV field (lib/csv/csv.js L57–83), with two fixes: text of
|
|
@@ -5078,7 +5311,7 @@ function csvValue(datum, dateFormats) {
|
|
|
5078
5311
|
if (special !== void 0) return typeof special === "object" ? { ...special } : special;
|
|
5079
5312
|
return datum;
|
|
5080
5313
|
}
|
|
5081
|
-
function formatterOptions(raw) {
|
|
5314
|
+
function formatterOptions(raw, escapeFormulas) {
|
|
5082
5315
|
const o = raw ?? {};
|
|
5083
5316
|
const delimiter = typeof o["delimiter"] === "string" ? o["delimiter"] : ",";
|
|
5084
5317
|
const rowDelimiter = typeof o["rowDelimiter"] === "string" ? o["rowDelimiter"] : "\n";
|
|
@@ -5095,9 +5328,27 @@ function formatterOptions(raw) {
|
|
|
5095
5328
|
headers: Array.isArray(o["headers"]) && o["writeHeaders"] !== false ? o["headers"].map(String) : null,
|
|
5096
5329
|
includeEndRowDelimiter: o["includeEndRowDelimiter"] === true,
|
|
5097
5330
|
writeBOM: o["writeBOM"] === true,
|
|
5098
|
-
escapeChars: new RegExp(`[${escapeRe(delimiter)}${escapeRe(rowDelimiter)}|\\r|\\n]`)
|
|
5331
|
+
escapeChars: new RegExp(`[${escapeRe(delimiter)}${escapeRe(rowDelimiter)}|\\r|\\n${escapeFormulas ? escapeRe(COMMON_DELIMITERS) : ""}]`),
|
|
5332
|
+
strictQuoting: escapeFormulas
|
|
5099
5333
|
};
|
|
5100
5334
|
}
|
|
5335
|
+
/**
|
|
5336
|
+
* Whether an application that splits `text` at a comma, ";", tab or "|" would get a field that
|
|
5337
|
+
* starts like a formula: one of those characters followed by a formula sign, a control character
|
|
5338
|
+
* or a byte order mark, after any spaces or tabs and an opening double quote (K2).
|
|
5339
|
+
*/
|
|
5340
|
+
function splitStartsFormula(text) {
|
|
5341
|
+
for (let i = 0; i < text.length; i++) {
|
|
5342
|
+
if (!COMMON_DELIMITERS.includes(text[i])) continue;
|
|
5343
|
+
let j = i + 1;
|
|
5344
|
+
while (text[j] === " " || text[j] === " ") j++;
|
|
5345
|
+
if (text[j] === "\"") j++;
|
|
5346
|
+
while (text[j] === " ") j++;
|
|
5347
|
+
const c = text[j];
|
|
5348
|
+
if (c !== void 0 && (INJECTION_START.has(c) || c.charCodeAt(0) < 32)) return true;
|
|
5349
|
+
}
|
|
5350
|
+
return false;
|
|
5351
|
+
}
|
|
5101
5352
|
/** One field as fast-csv FieldFormatter.format writes it. */
|
|
5102
5353
|
function formatField(value, index, f, forceQuote) {
|
|
5103
5354
|
const text = `${value === null || value === void 0 ? "" : String(value)}`.replace(/\0/g, "");
|
|
@@ -5108,6 +5359,13 @@ function formatField(value, index, f, forceQuote) {
|
|
|
5108
5359
|
field: index + 1
|
|
5109
5360
|
}
|
|
5110
5361
|
});
|
|
5362
|
+
if (f.quote === "" && f.strictQuoting && splitStartsFormula(text)) throw new XlvError("XLV_E_INVALID_OPTION", `Field ${String(index + 1)} has a comma, ";", tab or "|" followed by a formula sign, which an application splitting on that character reads as a formula; it cannot be written with quote: false.`, {
|
|
5363
|
+
hint: "Leave formatterOptions.quote at its default (\") so such fields are quoted, or pass formulaInjection: \"off\" for a trusted consumer.",
|
|
5364
|
+
details: {
|
|
5365
|
+
option: "quote",
|
|
5366
|
+
field: index + 1
|
|
5367
|
+
}
|
|
5368
|
+
});
|
|
5111
5369
|
if (f.quote !== "" && text.includes(f.quote)) return `${f.quote}${text.split(f.quote).join(f.escapedQuote)}${f.quote}`;
|
|
5112
5370
|
const q = f.quoteColumns;
|
|
5113
5371
|
const quoteThis = forceQuote || q === true || Array.isArray(q) && q[index] === true;
|
|
@@ -5127,7 +5385,7 @@ function defaultWriteMap(dateFormat, dateUTC) {
|
|
|
5127
5385
|
if (typeof value === "object") {
|
|
5128
5386
|
const v = value;
|
|
5129
5387
|
if (v["text"] || v["hyperlink"]) return v["hyperlink"] || v["text"] || "";
|
|
5130
|
-
if (v["
|
|
5388
|
+
if (v["sharedFormula"] || v["formula"] || "result" in v) {
|
|
5131
5389
|
const result = v["result"];
|
|
5132
5390
|
if (result === void 0 || result === null) return "";
|
|
5133
5391
|
if (result instanceof Date) return date(result);
|
|
@@ -5140,15 +5398,165 @@ function defaultWriteMap(dateFormat, dateUTC) {
|
|
|
5140
5398
|
return value;
|
|
5141
5399
|
};
|
|
5142
5400
|
}
|
|
5401
|
+
/** The output encoding for the `encoding` option (an unsupported one is refused when encoding). */
|
|
5402
|
+
function outputEncoding(encoding) {
|
|
5403
|
+
const e = (encoding ?? "").toLowerCase();
|
|
5404
|
+
if (e === "latin1" || e === "binary") return "latin1";
|
|
5405
|
+
if (e === "utf16le" || e === "utf-16le" || e === "ucs2" || e === "ucs-2") return "utf16le";
|
|
5406
|
+
return "utf8";
|
|
5407
|
+
}
|
|
5408
|
+
/** Leading UTF-8 sequences of U+FF1D, U+FF0B, U+FF0D, U+FF20, U+FE66, U+FE62, U+FE63, U+FE6B. */
|
|
5409
|
+
const UTF8_FORMULA_SIGNS = [
|
|
5410
|
+
[
|
|
5411
|
+
239,
|
|
5412
|
+
188,
|
|
5413
|
+
157
|
|
5414
|
+
],
|
|
5415
|
+
[
|
|
5416
|
+
239,
|
|
5417
|
+
188,
|
|
5418
|
+
139
|
|
5419
|
+
],
|
|
5420
|
+
[
|
|
5421
|
+
239,
|
|
5422
|
+
188,
|
|
5423
|
+
141
|
|
5424
|
+
],
|
|
5425
|
+
[
|
|
5426
|
+
239,
|
|
5427
|
+
188,
|
|
5428
|
+
160
|
|
5429
|
+
],
|
|
5430
|
+
[
|
|
5431
|
+
239,
|
|
5432
|
+
185,
|
|
5433
|
+
166
|
|
5434
|
+
],
|
|
5435
|
+
[
|
|
5436
|
+
239,
|
|
5437
|
+
185,
|
|
5438
|
+
162
|
|
5439
|
+
],
|
|
5440
|
+
[
|
|
5441
|
+
239,
|
|
5442
|
+
185,
|
|
5443
|
+
163
|
|
5444
|
+
],
|
|
5445
|
+
[
|
|
5446
|
+
239,
|
|
5447
|
+
185,
|
|
5448
|
+
171
|
|
5449
|
+
]
|
|
5450
|
+
];
|
|
5451
|
+
/**
|
|
5452
|
+
* Whether the first bytes of a field, as latin1 or UTF-16LE writes it, are a byte order mark
|
|
5453
|
+
* (EF BB BF, FF FE, FE FF), which makes a reader decode the rest of the file in that encoding
|
|
5454
|
+
* and drop the mark, or the UTF-8 form of a full-width or small formula sign, which a reader that
|
|
5455
|
+
* guesses UTF-8 shows as one (K2). `prefix` holds bytes written before the field (latin1 writes
|
|
5456
|
+
* the `writeBOM` mark U+FEFF as the single byte FF). UTF-16LE bytes are taken without their NUL
|
|
5457
|
+
* bytes, as an 8-bit reader that drops them reads them (K3); a sequence that holds no NUL byte is
|
|
5458
|
+
* the same either way.
|
|
5459
|
+
*/
|
|
5460
|
+
function encodedStartIsUnsafe(text, encoding, prefix) {
|
|
5461
|
+
if (encoding === "utf8") return false;
|
|
5462
|
+
const bytes = [...prefix];
|
|
5463
|
+
for (let i = 0; bytes.length < 3 && i < text.length; i++) {
|
|
5464
|
+
const c = text.charCodeAt(i);
|
|
5465
|
+
if (encoding === "latin1") bytes.push(c & 255);
|
|
5466
|
+
else for (const b of [c & 255, c >> 8]) if (b !== 0) bytes.push(b);
|
|
5467
|
+
}
|
|
5468
|
+
const starts = (lead) => lead.every((b, i) => bytes[i] === b);
|
|
5469
|
+
return starts([
|
|
5470
|
+
239,
|
|
5471
|
+
187,
|
|
5472
|
+
191
|
|
5473
|
+
]) || starts([255, 254]) || starts([254, 255]) || UTF8_FORMULA_SIGNS.some(starts);
|
|
5474
|
+
}
|
|
5475
|
+
/** Bytes that end a field or a row for a reader of the 8-bit text, whatever its delimiter. */
|
|
5476
|
+
const BYTE_BREAKS = /* @__PURE__ */ new Set([
|
|
5477
|
+
44,
|
|
5478
|
+
59,
|
|
5479
|
+
9,
|
|
5480
|
+
124,
|
|
5481
|
+
13,
|
|
5482
|
+
10
|
|
5483
|
+
]);
|
|
5484
|
+
/** Whether the 8-bit text `view` has a formula start at `i`: "=", or spaces or tabs, a quote and "=". */
|
|
5485
|
+
function viewFormulaAt(view, i) {
|
|
5486
|
+
if (view[i] === "=") return true;
|
|
5487
|
+
let j = i;
|
|
5488
|
+
while (view[j] === " " || view[j] === " ") j++;
|
|
5489
|
+
return view[j] === "\"" && view[j + 1] === "=";
|
|
5490
|
+
}
|
|
5143
5491
|
/**
|
|
5144
|
-
*
|
|
5492
|
+
* Whether the UTF-16LE bytes of `text` without NULs start a formula (see {@link viewFormulaAt})
|
|
5493
|
+
* because of a character above U+00FF, such as U+3D00 (00 3D) or U+4E3D (3D 4E). Starts made only
|
|
5494
|
+
* of characters up to U+00FF are the same text in both readings and are checked as text.
|
|
5495
|
+
*/
|
|
5496
|
+
function utf16StartsFormula(text) {
|
|
5497
|
+
let view = "";
|
|
5498
|
+
let foreign = false;
|
|
5499
|
+
let lead = 0;
|
|
5500
|
+
let blank = true;
|
|
5501
|
+
for (let i = 0; i < text.length && view.length - lead < 2; i++) {
|
|
5502
|
+
const c = text.charCodeAt(i);
|
|
5503
|
+
if (c > 255) foreign = true;
|
|
5504
|
+
for (const b of c > 255 ? [c & 255, c >> 8] : [c]) {
|
|
5505
|
+
if (b === 0) continue;
|
|
5506
|
+
view += String.fromCharCode(b);
|
|
5507
|
+
if (blank && (b === 32 || b === 9)) lead++;
|
|
5508
|
+
else blank = false;
|
|
5509
|
+
}
|
|
5510
|
+
}
|
|
5511
|
+
return foreign && viewFormulaAt(view, 0);
|
|
5512
|
+
}
|
|
5513
|
+
/** The UTF-16LE bytes of a written field without NULs, and whether characters above U+00FF give a break or a quote among them. */
|
|
5514
|
+
function utf16View(written, delimiter) {
|
|
5515
|
+
let view = "";
|
|
5516
|
+
let foreignBreak = false;
|
|
5517
|
+
let foreignQuote = false;
|
|
5518
|
+
for (let i = 0; i < written.length; i++) {
|
|
5519
|
+
const c = written.charCodeAt(i);
|
|
5520
|
+
for (const b of c > 255 ? [c & 255, c >> 8] : [c]) {
|
|
5521
|
+
if (b === 0) continue;
|
|
5522
|
+
if (c > 255) {
|
|
5523
|
+
if (b === 34) foreignQuote = true;
|
|
5524
|
+
else if (BYTE_BREAKS.has(b) || delimiter.includes(String.fromCharCode(b))) foreignBreak = true;
|
|
5525
|
+
}
|
|
5526
|
+
view += String.fromCharCode(b);
|
|
5527
|
+
}
|
|
5528
|
+
}
|
|
5529
|
+
return {
|
|
5530
|
+
view,
|
|
5531
|
+
foreignBreak,
|
|
5532
|
+
foreignQuote
|
|
5533
|
+
};
|
|
5534
|
+
}
|
|
5535
|
+
/** Whether the 8-bit text `view` has a delimiter (`delimiter` or a common one) or a line break followed by a formula start. */
|
|
5536
|
+
function viewSplitStartsFormula(view, delimiter) {
|
|
5537
|
+
for (let i = 0; i < view.length; i++) {
|
|
5538
|
+
if (BYTE_BREAKS.has(view.charCodeAt(i)) && viewFormulaAt(view, i + 1)) return true;
|
|
5539
|
+
if (view.startsWith(delimiter, i) && viewFormulaAt(view, i + delimiter.length)) return true;
|
|
5540
|
+
}
|
|
5541
|
+
return false;
|
|
5542
|
+
}
|
|
5543
|
+
/** Column letters of a 1-based column number. */
|
|
5544
|
+
function columnName(n) {
|
|
5545
|
+
let s = "";
|
|
5546
|
+
for (let c = n; c > 0; c = Math.floor((c - 1) / 26)) s = String.fromCharCode(65 + (c - 1) % 26) + s;
|
|
5547
|
+
return s;
|
|
5548
|
+
}
|
|
5549
|
+
/**
|
|
5550
|
+
* The CSV text of a worksheet, as exceljs `csv.write` gives it, for output in `encoding`.
|
|
5145
5551
|
*
|
|
5146
5552
|
* @internal
|
|
5147
5553
|
*/
|
|
5148
|
-
function worksheetCsv(sheet, options,
|
|
5149
|
-
const f = formatterOptions(options.formatterOptions);
|
|
5554
|
+
function worksheetCsv(sheet, options, encoding = "utf8") {
|
|
5150
5555
|
const injection = options.formulaInjection ?? "escape";
|
|
5151
5556
|
if (injection !== "escape" && injection !== "off") throw invalid("formulaInjection", "The option \"formulaInjection\" must be \"escape\" or \"off\".", "Use \"escape\" (default) or \"off\".");
|
|
5557
|
+
const f = formatterOptions(options.formatterOptions, injection === "escape");
|
|
5558
|
+
const latin1 = encoding === "latin1";
|
|
5559
|
+
const sheetName = sheet?.name ?? "";
|
|
5152
5560
|
if (latin1) {
|
|
5153
5561
|
for (const [name, value] of [
|
|
5154
5562
|
["delimiter", f.delimiter],
|
|
@@ -5159,6 +5567,10 @@ function worksheetCsv(sheet, options, latin1 = false) {
|
|
|
5159
5567
|
}
|
|
5160
5568
|
if (injection === "escape") {
|
|
5161
5569
|
const off = "or pass formulaInjection: \"off\" for a trusted consumer.";
|
|
5570
|
+
if (f.delimiter === "") throw invalid("delimiter", "The delimiter is empty, so the fields of a row run together and the text of one field could complete a formula or a byte order mark begun by the one before.", `Use "," (default), ";", "|" or "\\t", ${off}`);
|
|
5571
|
+
if (encoding !== "utf8") {
|
|
5572
|
+
for (const [name, value] of [["delimiter", f.delimiter], ["rowDelimiter", f.rowDelimiter]]) if (/[^\u0000-\u007f]/.test(value)) throw invalid(name, `The option "${name}" (${JSON.stringify(value)}) has characters other than ASCII, whose ${encoding} bytes could join the bytes of the fields around them into a byte order mark or a formula sign.`, `Use ASCII characters with encoding "${encoding}" (for example "," or ";"), write UTF-8 (the default encoding), ${off}`);
|
|
5573
|
+
}
|
|
5162
5574
|
if (f.delimiter !== " " && [...f.delimiter].some((c) => INJECTION_START.has(c) || c.charCodeAt(0) < 32)) throw invalid("delimiter", `The delimiter ${JSON.stringify(f.delimiter)} holds a formula sign or a control character, so a line could start with a formula.`, `Use "," (default), ";", "|" or "\\t", ${off}`);
|
|
5163
5575
|
if (![
|
|
5164
5576
|
"\n",
|
|
@@ -5170,8 +5582,34 @@ function worksheetCsv(sheet, options, latin1 = false) {
|
|
|
5170
5582
|
}
|
|
5171
5583
|
/** The text latin1 will write: characters above U+00FF as "?". */
|
|
5172
5584
|
const narrow = (out) => latin1 && typeof out === "string" ? out.replace(/[^\u0000-\u00ff]/g, "?") : out;
|
|
5173
|
-
|
|
5174
|
-
const
|
|
5585
|
+
const utf16 = encoding === "utf16le" && injection === "escape";
|
|
5586
|
+
const marked = injection === "escape" && (utf16 || latin1 && f.writeBOM);
|
|
5587
|
+
/** A quote may be taken differently by a reader of the 8-bit text from here on (K3). */
|
|
5588
|
+
let quoteShifted = false;
|
|
5589
|
+
const unsafeField = (index, row, reason, hint) => {
|
|
5590
|
+
const cell = `${columnName(index + 1)}${String(row)}`;
|
|
5591
|
+
return new XlvError("XLV_E_INVALID_OPTION", `The text of ${row === 0 ? `header ${String(index + 1)}` : `cell ${cell}`} cannot be written safely as ${encoding}: ${reason}`, {
|
|
5592
|
+
location: row === 0 ? { sheet: sheetName } : {
|
|
5593
|
+
sheet: sheetName,
|
|
5594
|
+
cell
|
|
5595
|
+
},
|
|
5596
|
+
hint,
|
|
5597
|
+
details: {
|
|
5598
|
+
option: "encoding",
|
|
5599
|
+
field: index + 1,
|
|
5600
|
+
row
|
|
5601
|
+
}
|
|
5602
|
+
});
|
|
5603
|
+
};
|
|
5604
|
+
/**
|
|
5605
|
+
* A formatted field, refused if a standard reader would take its leading quote as quoting, or
|
|
5606
|
+
* if a reader of the output as 8-bit text that drops NUL bytes (LibreOffice) would split it
|
|
5607
|
+
* before a formula start: in utf16le output, when characters above U+00FF give a delimiter,
|
|
5608
|
+
* line break or quote there, when an earlier quote of that kind may shift the quoting, or for
|
|
5609
|
+
* the first field after a mark that reader keeps (K3). `row` is the row number, 0 for the header
|
|
5610
|
+
* line; `atStart`: the field is the first one of the file.
|
|
5611
|
+
*/
|
|
5612
|
+
const field = (out, index, force, row, atStart) => {
|
|
5175
5613
|
const text = formatField(out, index, f, force);
|
|
5176
5614
|
if (injection === "escape" && f.quote !== "\"" && text.startsWith("\"")) throw new XlvError("XLV_E_INVALID_OPTION", `Field ${String(index + 1)} starts with a double quote, which a standard CSV reader takes as quoting when formatterOptions.quote is not '"'.`, {
|
|
5177
5615
|
hint: "Leave formatterOptions.quote at its default (\"), or pass formulaInjection: \"off\" for a trusted consumer.",
|
|
@@ -5180,20 +5618,32 @@ function worksheetCsv(sheet, options, latin1 = false) {
|
|
|
5180
5618
|
field: index + 1
|
|
5181
5619
|
}
|
|
5182
5620
|
});
|
|
5621
|
+
const first = marked && atStart;
|
|
5622
|
+
if (first || quoteShifted || utf16 && /[^\u0000-\u00ff]/.test(text)) {
|
|
5623
|
+
const { view, foreignBreak, foreignQuote } = utf16 ? utf16View(text, f.delimiter) : {
|
|
5624
|
+
view: text,
|
|
5625
|
+
foreignBreak: false,
|
|
5626
|
+
foreignQuote: false
|
|
5627
|
+
};
|
|
5628
|
+
if ((first || foreignBreak || foreignQuote || quoteShifted) && viewSplitStartsFormula(view, f.delimiter)) throw first ? unsafeField(index, row, `the file starts with ${utf16 ? "the UTF-16LE byte order mark (FF FE)" : "the byte FF (writeBOM)"}, which LibreOffice keeps when it reads the file as 8-bit text, so the opening quote of this first field is not taken as quoting and its delimiter or line break followed by "=" starts a formula.`, utf16 ? "Write UTF-8 (the default encoding), move this text out of the first cell, or pass formulaInjection: \"off\" for a trusted consumer." : "Leave formatterOptions.writeBOM off with latin1, write UTF-8, or pass formulaInjection: \"off\" for a trusted consumer.") : utf16 ? unsafeField(index, row, "read as 8-bit text without its NUL bytes, as LibreOffice reads UTF-16LE CSV files, its bytes hold a delimiter or a line break followed by \"=\", which starts a formula there.", "Write UTF-8 (the default encoding), or pass formulaInjection: \"off\" for a trusted consumer.") : unsafeField(index, row, "the first field, after the byte FF of writeBOM, is split by LibreOffice, which may then take the quotes after it differently, and this field holds a delimiter or a line break followed by \"=\".", "Leave formatterOptions.writeBOM off with latin1, write UTF-8, or pass formulaInjection: \"off\" for a trusted consumer.");
|
|
5629
|
+
if (foreignQuote || first && ([...view].some((c) => BYTE_BREAKS.has(c.charCodeAt(0))) || view.includes(f.delimiter))) quoteShifted = true;
|
|
5630
|
+
}
|
|
5183
5631
|
return text;
|
|
5184
5632
|
};
|
|
5185
5633
|
const map = options.map ?? defaultWriteMap(options.dateFormat, options.dateUTC === true);
|
|
5186
5634
|
const includeEmptyRows = options.includeEmptyRows === void 0 || options.includeEmptyRows;
|
|
5187
5635
|
const lines = [];
|
|
5188
|
-
const escape = (raw) => {
|
|
5189
|
-
const out = narrow(raw);
|
|
5636
|
+
const escape = (raw, atStart) => {
|
|
5637
|
+
const out = narrow(raw === null || raw === void 0 || typeof raw === "number" || typeof raw === "boolean" || typeof raw === "bigint" || typeof raw === "string" ? raw : String(raw));
|
|
5190
5638
|
if (injection !== "escape" || typeof out !== "string") return [out, false];
|
|
5191
|
-
const
|
|
5192
|
-
|
|
5639
|
+
const text = out.replace(/\0/g, "");
|
|
5640
|
+
const first = text[0];
|
|
5641
|
+
if (first === void 0) return [out, false];
|
|
5642
|
+
return INJECTION_START.has(first) || first.charCodeAt(0) < 32 || encodedStartIsUnsafe(text, encoding, atStart && f.writeBOM && latin1 ? [255] : []) || utf16 && utf16StartsFormula(text) ? [`'${out}`, f.quote !== ""] : [out, false];
|
|
5193
5643
|
};
|
|
5194
5644
|
if (f.headers) lines.push(f.headers.map((h, i) => {
|
|
5195
|
-
const [out, force] = escape(h);
|
|
5196
|
-
return field(out, i, force);
|
|
5645
|
+
const [out, force] = escape(h, i === 0);
|
|
5646
|
+
return field(out, i, force, 0, i === 0);
|
|
5197
5647
|
}).join(f.delimiter));
|
|
5198
5648
|
if (sheet) {
|
|
5199
5649
|
let lastRow = 0;
|
|
@@ -5207,8 +5657,9 @@ function worksheetCsv(sheet, options, latin1 = false) {
|
|
|
5207
5657
|
fields.push("");
|
|
5208
5658
|
continue;
|
|
5209
5659
|
}
|
|
5210
|
-
const
|
|
5211
|
-
|
|
5660
|
+
const atStart = lines.length === 0 && i === 0;
|
|
5661
|
+
const [out, force] = escape(map(values[i], i), atStart);
|
|
5662
|
+
fields.push(field(out, i, force, rowNumber, atStart));
|
|
5212
5663
|
}
|
|
5213
5664
|
lines.push(fields.join(f.delimiter));
|
|
5214
5665
|
lastRow = rowNumber;
|
|
@@ -5216,7 +5667,7 @@ function worksheetCsv(sheet, options, latin1 = false) {
|
|
|
5216
5667
|
}
|
|
5217
5668
|
let text = lines.join(f.rowDelimiter);
|
|
5218
5669
|
if (f.includeEndRowDelimiter) text += f.rowDelimiter;
|
|
5219
|
-
return (f.writeBOM ? "" : "") + text;
|
|
5670
|
+
return (f.writeBOM || utf16 ? "" : "") + text;
|
|
5220
5671
|
}
|
|
5221
5672
|
function encode(text, encoding) {
|
|
5222
5673
|
const e = (encoding ?? "utf8").toLowerCase();
|
|
@@ -5358,10 +5809,16 @@ var CsvIO = class {
|
|
|
5358
5809
|
async read(stream, options = {}) {
|
|
5359
5810
|
const worksheet = this.workbook.addWorksheet(options.sheetName);
|
|
5360
5811
|
const parser = parserOptions(options.parserOptions);
|
|
5361
|
-
const
|
|
5812
|
+
const text = await readText(stream, parser.encoding);
|
|
5362
5813
|
const formats = options.dateFormats ?? DEFAULT_DATE_FORMATS;
|
|
5363
5814
|
const map = options.map ?? ((datum) => csvValue(datum, formats));
|
|
5364
|
-
|
|
5815
|
+
if (text.length >= MAX_CSV_CELLS && fieldEnds(text, parser.delimiter) >= MAX_CSV_CELLS) try {
|
|
5816
|
+
const rows = csvRows(text, parser);
|
|
5817
|
+
while (rows.next().done !== true);
|
|
5818
|
+
} catch (error) {
|
|
5819
|
+
if (error instanceof XlvError && error.code === "XLV_E_LIMIT_EXCEEDED" && error.details?.limit === "maxCells") throw error;
|
|
5820
|
+
}
|
|
5821
|
+
for (const row of csvRows(text, parser)) worksheet.addRow(row.fields.map((datum, i) => map(datum, i)));
|
|
5365
5822
|
return worksheet;
|
|
5366
5823
|
}
|
|
5367
5824
|
/** Reads a CSV file (Node.js, Bun, Deno) into a new worksheet. */
|
|
@@ -5389,8 +5846,7 @@ var CsvIO = class {
|
|
|
5389
5846
|
}
|
|
5390
5847
|
/** Writes a worksheet to a CSV file (Node.js, Bun, Deno), replacing it. */
|
|
5391
5848
|
async writeFile(path, options = {}) {
|
|
5392
|
-
const
|
|
5393
|
-
const bytes = encode(this.text(options, e === "latin1" || e === "binary"), options.encoding);
|
|
5849
|
+
const bytes = encode(this.text(options, outputEncoding(options.encoding)), options.encoding);
|
|
5394
5850
|
const fs = await nodeFs("writeFile");
|
|
5395
5851
|
try {
|
|
5396
5852
|
await fs.writeFile(path, bytes);
|
|
@@ -5414,7 +5870,7 @@ var CsvIO = class {
|
|
|
5414
5870
|
createInputStream() {
|
|
5415
5871
|
throw new Error("`CSV#createInputStream` is deprecated. You should use `CSV#read` instead. This method will be removed in version 5.0. Please follow upgrade instruction: https://github.com/exceljs/exceljs/blob/master/UPGRADE-4.0.md");
|
|
5416
5872
|
}
|
|
5417
|
-
text(options,
|
|
5873
|
+
text(options, encoding = "utf8") {
|
|
5418
5874
|
const key = options.sheetName || options.sheetId;
|
|
5419
5875
|
const sheet = this.workbook.getWorksheet(key);
|
|
5420
5876
|
if (key !== void 0 && !sheet) throw new XlvError("XLV_E_SHEET_NOT_FOUND", `The workbook has no worksheet ${typeof key === "string" ? `named "${key}"` : `with id ${String(key)}`}.`, {
|
|
@@ -5422,7 +5878,7 @@ var CsvIO = class {
|
|
|
5422
5878
|
hint: `Available worksheets: ${this.workbook.worksheets.map((w) => `"${w.name}"`).join(", ") || "none"}.`,
|
|
5423
5879
|
details: { available: this.workbook.worksheets.map((w) => w.name) }
|
|
5424
5880
|
});
|
|
5425
|
-
return worksheetCsv(sheet, options,
|
|
5881
|
+
return worksheetCsv(sheet, options, encoding);
|
|
5426
5882
|
}
|
|
5427
5883
|
};
|
|
5428
5884
|
// src/defined-names.ts
|
|
@@ -6286,6 +6742,23 @@ var Workbook = class {
|
|
|
6286
6742
|
}
|
|
6287
6743
|
});
|
|
6288
6744
|
};
|
|
6745
|
+
const mergeBudget = Math.max(MERGED_CELLS_BUDGET, cellsRead);
|
|
6746
|
+
let merged = 0;
|
|
6747
|
+
const spendMerge = (positions, ref, sheet) => {
|
|
6748
|
+
merged += positions;
|
|
6749
|
+
if (merged <= mergeBudget) return;
|
|
6750
|
+
throw new XlvError("XLV_E_LIMIT_EXCEEDED", `The merged range ${ref} of worksheet "${sheet}" would bring the merged cells past ${String(mergeBudget)} for ${String(cellsRead)} cells in the file.`, {
|
|
6751
|
+
location: {
|
|
6752
|
+
sheet,
|
|
6753
|
+
cell: ref
|
|
6754
|
+
},
|
|
6755
|
+
hint: "The file merges far more cells than it holds. Load it with ignoreNodes: [\"mergeCells\"] to skip merged ranges, or read it with xlvane's readWorkbook, which does not create these cells.",
|
|
6756
|
+
details: {
|
|
6757
|
+
limit: "mergedCells",
|
|
6758
|
+
value: mergeBudget
|
|
6759
|
+
}
|
|
6760
|
+
});
|
|
6761
|
+
};
|
|
6289
6762
|
const rowBudget = Math.max(67108864, 16 * cellsRead);
|
|
6290
6763
|
const validations = validationBudget();
|
|
6291
6764
|
let rowsLooked = 0;
|
|
@@ -6398,7 +6871,7 @@ var Workbook = class {
|
|
|
6398
6871
|
const target = ws.getCell(cell.row, cell.col);
|
|
6399
6872
|
let value;
|
|
6400
6873
|
const plain = cell.type === "error" ? { error: cell.value } : cell.value;
|
|
6401
|
-
if (cell.formula && cell.formula
|
|
6874
|
+
if (cell.formula && hasFormulaText(cell.formula)) value = formulaOut(cell.formula, plain, cell.ref, masters);
|
|
6402
6875
|
else value = cell.richText ? richTextOut(cell.richText) : plain;
|
|
6403
6876
|
const h = cell.hyperlink;
|
|
6404
6877
|
if (h && use("hyperlinks") && (h.target !== null || h.location !== null && h.location !== "")) {
|
|
@@ -6440,6 +6913,7 @@ var Workbook = class {
|
|
|
6440
6913
|
for (const m of use("mergeCells") ? s.merges : []) {
|
|
6441
6914
|
if (!m.includes(":")) continue;
|
|
6442
6915
|
spend(areaOf(m), "A merged range", s.name);
|
|
6916
|
+
spendMerge(areaOf(m), m, s.name);
|
|
6443
6917
|
try {
|
|
6444
6918
|
ws.mergeCells(m);
|
|
6445
6919
|
} catch (error) {
|
|
@@ -6483,6 +6957,8 @@ var Workbook = class {
|
|
|
6483
6957
|
}
|
|
6484
6958
|
}
|
|
6485
6959
|
};
|
|
6960
|
+
/** Merged cells `xlsx.load` creates at most for a file with fewer cells (one whole column; K2). */
|
|
6961
|
+
const MERGED_CELLS_BUDGET = 1048576;
|
|
6486
6962
|
/**
|
|
6487
6963
|
* @internal The chunks of an input stream, read until it ends. Reading stops with
|
|
6488
6964
|
* XLV_E_LIMIT_EXCEEDED as soon as the chunks pass maxInputBytes, the size the core reads at most,
|
|
@@ -6571,7 +7047,7 @@ function streamValueIn(cell, masters) {
|
|
|
6571
7047
|
const v = cell.value;
|
|
6572
7048
|
if (v !== null && typeof v === "object" && !Array.isArray(v) && !(v instanceof Date)) {
|
|
6573
7049
|
const r = v;
|
|
6574
|
-
if (typeof r["
|
|
7050
|
+
if (typeof r["ref"] === "string" && r["shareType"] !== "array" && typeof r["formula"] === "string") {
|
|
6575
7051
|
masters.set(cell.address, {
|
|
6576
7052
|
formula: r["formula"],
|
|
6577
7053
|
row: cell.row,
|
|
@@ -7178,6 +7654,44 @@ var WorksheetReader = class extends Emitter {
|
|
|
7178
7654
|
}
|
|
7179
7655
|
};
|
|
7180
7656
|
/**
|
|
7657
|
+
* The value of a shared-formula dependent read by the streaming reader: `{ sharedFormula: master,
|
|
7658
|
+
* result }`, as exceljs's xlsx.load gives it, with a non-enumerable `formula` that moves the
|
|
7659
|
+
* master's text (`masters`, by address) to `row`/`col` when it is first read. The master's row is
|
|
7660
|
+
* not kept, so a sheet the value is copied into (stream.xlsx.WorkbookWriter, whose committed rows
|
|
7661
|
+
* are dropped) can write this formula instead of looking the master up (K3); reading the rows
|
|
7662
|
+
* alone builds no moved texts (K2). Setting `formula` replaces it with an ordinary property. A move
|
|
7663
|
+
* counts against the shared-formula budget of `workbook`, as `cell.formula` does (K4 S1).
|
|
7664
|
+
*/
|
|
7665
|
+
function sharedDependent(master, result, masters, row, col, workbook, sheet) {
|
|
7666
|
+
const value = {
|
|
7667
|
+
sharedFormula: master,
|
|
7668
|
+
...result
|
|
7669
|
+
};
|
|
7670
|
+
let moved;
|
|
7671
|
+
let built = false;
|
|
7672
|
+
Object.defineProperty(value, "formula", {
|
|
7673
|
+
configurable: true,
|
|
7674
|
+
enumerable: false,
|
|
7675
|
+
get() {
|
|
7676
|
+
if (!built) {
|
|
7677
|
+
const text = masters.get(master);
|
|
7678
|
+
moved = text === void 0 ? void 0 : moveSharedFormula(workbook, sheet, text, decodeAddress(master), row, col);
|
|
7679
|
+
built = true;
|
|
7680
|
+
}
|
|
7681
|
+
return moved;
|
|
7682
|
+
},
|
|
7683
|
+
set(formula) {
|
|
7684
|
+
Object.defineProperty(this, "formula", {
|
|
7685
|
+
value: formula,
|
|
7686
|
+
writable: true,
|
|
7687
|
+
enumerable: true,
|
|
7688
|
+
configurable: true
|
|
7689
|
+
});
|
|
7690
|
+
}
|
|
7691
|
+
});
|
|
7692
|
+
return value;
|
|
7693
|
+
}
|
|
7694
|
+
/**
|
|
7181
7695
|
* The streaming reader of exceljs (`ExcelJS.stream.xlsx.WorkbookReader`): iterate worksheets
|
|
7182
7696
|
* and their rows with `for await`, or call `read()` and listen to `worksheet`, `end`,
|
|
7183
7697
|
* `finished` and `error` events (and `entry` with `entries: 'emit'`). Rows are not kept.
|
|
@@ -7227,16 +7741,31 @@ var WorkbookReader = class extends Emitter {
|
|
|
7227
7741
|
});
|
|
7228
7742
|
const sheet = host._addReadWorksheet(host.nextId, info.name, { state: info.state });
|
|
7229
7743
|
const name = info.name;
|
|
7744
|
+
const masters = /* @__PURE__ */ new Map();
|
|
7745
|
+
const kept = /* @__PURE__ */ new Map();
|
|
7746
|
+
sheet._sharedMasters = kept;
|
|
7230
7747
|
const rows = async function* () {
|
|
7231
7748
|
for await (const r of book.rows(name)) {
|
|
7232
7749
|
const row = sheet.getRow(r.number);
|
|
7233
7750
|
for (const cell of r.cells) {
|
|
7234
7751
|
const plain = cell.type === "error" ? { error: cell.value } : cell.value;
|
|
7235
7752
|
const target = row.getCell(cell.col);
|
|
7236
|
-
|
|
7237
|
-
|
|
7238
|
-
|
|
7239
|
-
|
|
7753
|
+
const f = cell.formula;
|
|
7754
|
+
const result = plain === null ? {} : { result: plain };
|
|
7755
|
+
let value;
|
|
7756
|
+
if (f && f.kind === "shared" && f.sharedIndex !== null && f.ref === null && masters.has(f.sharedIndex) && hasFormulaText(f)) value = sharedDependent(masters.get(f.sharedIndex), result, kept, r.number, cell.col, host, name);
|
|
7757
|
+
else if (f && hasFormulaText(f)) {
|
|
7758
|
+
const text = f.text ?? "";
|
|
7759
|
+
if (f.kind === "shared" && f.sharedIndex !== null && f.ref !== null) {
|
|
7760
|
+
masters.set(f.sharedIndex, cell.ref);
|
|
7761
|
+
kept.set(cell.ref, text);
|
|
7762
|
+
}
|
|
7763
|
+
value = {
|
|
7764
|
+
formula: text,
|
|
7765
|
+
...result
|
|
7766
|
+
};
|
|
7767
|
+
} else value = cell.richText ? richTextOut(cell.richText) : plain;
|
|
7768
|
+
target.value = value;
|
|
7240
7769
|
if (styles) target.style = styleOut(cell.style, cell.numFmt);
|
|
7241
7770
|
}
|
|
7242
7771
|
for (const cell of r.styledCells) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xlvane/compat",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "The commonly used exceljs 4.4.0 API on top of xlvane, for switching with one line in package.json.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"xlsx",
|
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
"build": "tsdown"
|
|
73
73
|
},
|
|
74
74
|
"dependencies": {
|
|
75
|
-
"xlvane": "^1.
|
|
75
|
+
"xlvane": "^1.3.0"
|
|
76
76
|
},
|
|
77
77
|
"publishConfig": {
|
|
78
78
|
"access": "public",
|