@xlvane/compat 1.2.3 → 1.3.1
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 +37 -9
- package/dist/index.js +940 -112
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -773,6 +773,67 @@ const BUILTIN_CODES = /* @__PURE__ */ new Map([
|
|
|
773
773
|
["##0.0E+0", 48],
|
|
774
774
|
["@", 49]
|
|
775
775
|
]);
|
|
776
|
+
const DAY_MS = 864e5;
|
|
777
|
+
/** exceljs's serial 0 in the 1900 date system (lib/utils/utils.js L55-61: 25569 days before 1970). */
|
|
778
|
+
const EPOCH_1899_12_30 = Date.UTC(1899, 11, 30);
|
|
779
|
+
/** Serial 0 of the core's Transitional 1900 system for 1900-01-01 to 1900-02-28 (D-112). */
|
|
780
|
+
const EPOCH_1899_12_31 = Date.UTC(1899, 11, 31);
|
|
781
|
+
const EPOCH_1904_01_01 = Date.UTC(1904, 0, 1);
|
|
782
|
+
const TIME_1900_01_01 = Date.UTC(1900, 0, 1);
|
|
783
|
+
const MIN_DATE_TIME = (/* @__PURE__ */ new Date(0)).setUTCFullYear(1, 0, 1);
|
|
784
|
+
const MAX_DATE_TIME = Date.UTC(9999, 11, 31, 23, 59, 59, 999);
|
|
785
|
+
/**
|
|
786
|
+
* The serial exceljs writes for a Date from 1899-12-30 to the end of 1899 in the 1900 date system
|
|
787
|
+
* (time-only values such as `new Date(Date.UTC(1899, 11, 30, 13, 20))`): `25569 + ms / 86400000`
|
|
788
|
+
* (lib/utils/utils.js L55-57 `dateToExcel`). The core refuses these dates, so they are written
|
|
789
|
+
* as numbers with the cell's date format. `undefined` for other values.
|
|
790
|
+
*/
|
|
791
|
+
function earlyDateSerial(v, date1904) {
|
|
792
|
+
if (date1904 || !(v instanceof Date)) return void 0;
|
|
793
|
+
const t = v.getTime();
|
|
794
|
+
return t >= EPOCH_1899_12_30 && t < TIME_1900_01_01 ? (t - EPOCH_1899_12_30) / DAY_MS : void 0;
|
|
795
|
+
}
|
|
796
|
+
/**
|
|
797
|
+
* exceljs's test for a date format (lib/utils/utils.js L140-151 `isDateFmt`): after removing the
|
|
798
|
+
* parts in brackets and in double quotes, the format has one of the letters y, m, d, h, M, s, b.
|
|
799
|
+
* It also takes upper-case formats such as `DD.MM.YYYY`, which the core does not.
|
|
800
|
+
*/
|
|
801
|
+
function isExcelJsDateFormat(fmt) {
|
|
802
|
+
if (!fmt) return false;
|
|
803
|
+
return /[ymdhMsb]/.test(fmt.replace(/\[[^\]]*]/g, "").replace(/"[^"]*"/g, ""));
|
|
804
|
+
}
|
|
805
|
+
/**
|
|
806
|
+
* A serial as a Date, as the core converts it (D-112), except that a serial below 1 in the
|
|
807
|
+
* Transitional 1900 system counts from 1899-12-30 as in exceljs (F6). `null` where the core has no
|
|
808
|
+
* Date either (serial 60, negative serials, outside 0001-9999).
|
|
809
|
+
*/
|
|
810
|
+
function serialToDate(serial, dates) {
|
|
811
|
+
if (!Number.isFinite(serial) || Math.abs(serial) > 3e6) return null;
|
|
812
|
+
const ms = Math.round(serial * DAY_MS);
|
|
813
|
+
let time;
|
|
814
|
+
if (dates.date1904) time = EPOCH_1904_01_01 + ms;
|
|
815
|
+
else if (dates.strict) time = EPOCH_1899_12_30 + ms;
|
|
816
|
+
else if (ms < 0) return null;
|
|
817
|
+
else if (ms < DAY_MS) time = EPOCH_1899_12_30 + ms;
|
|
818
|
+
else if (ms < 60 * DAY_MS) time = EPOCH_1899_12_31 + ms;
|
|
819
|
+
else if (ms < 61 * DAY_MS) return null;
|
|
820
|
+
else time = EPOCH_1899_12_30 + ms;
|
|
821
|
+
return time < MIN_DATE_TIME || time > MAX_DATE_TIME ? null : new Date(time);
|
|
822
|
+
}
|
|
823
|
+
/**
|
|
824
|
+
* A plain value read by the core as exceljs gives it: a time-only Date of the Transitional 1900
|
|
825
|
+
* system (which the core places on 1899-12-31, D-112) on 1899-12-30, as exceljs's `excelToDate`
|
|
826
|
+
* (lib/utils/utils.js L58-61) does (F6); and, when `dateFormat` is set, a number whose format
|
|
827
|
+
* exceljs takes for a date (`isExcelJsDateFormat`) as a Date (F7).
|
|
828
|
+
*/
|
|
829
|
+
function readValueOut(value, numFmt, dates, dateFormat = true) {
|
|
830
|
+
if (value instanceof Date) {
|
|
831
|
+
const t = value.getTime();
|
|
832
|
+
return !dates.date1904 && !dates.strict && t < TIME_1900_01_01 ? /* @__PURE__ */ new Date(t - DAY_MS) : value;
|
|
833
|
+
}
|
|
834
|
+
if (dateFormat && typeof value === "number" && isExcelJsDateFormat(numFmt)) return serialToDate(value, dates) ?? value;
|
|
835
|
+
return value;
|
|
836
|
+
}
|
|
776
837
|
function colorIn(c, where) {
|
|
777
838
|
if (c === void 0 || c === null) return void 0;
|
|
778
839
|
if (typeof c.argb === "string") return c.argb;
|
|
@@ -973,14 +1034,16 @@ function isRecord$1(v) {
|
|
|
973
1034
|
return typeof v === "object" && v !== null && !(v instanceof Date) && !Array.isArray(v);
|
|
974
1035
|
}
|
|
975
1036
|
/** The `result` part of a formula value for the writer. */
|
|
976
|
-
function resultIn(r) {
|
|
1037
|
+
function resultIn(r, date1904 = false) {
|
|
977
1038
|
if (r === void 0 || r === null) return {};
|
|
1039
|
+
const serial = earlyDateSerial(r, date1904);
|
|
1040
|
+
if (serial !== void 0) return { result: serial };
|
|
978
1041
|
if (isRecord$1(r) && typeof r["error"] === "string") return { result: { error: r["error"] } };
|
|
979
1042
|
return { result: r };
|
|
980
1043
|
}
|
|
981
1044
|
/** The cached result of a formula as a plain value (`null` without one). */
|
|
982
|
-
function resultValue(r) {
|
|
983
|
-
return resultIn(r).result ?? null;
|
|
1045
|
+
function resultValue(r, date1904) {
|
|
1046
|
+
return resultIn(r, date1904).result ?? null;
|
|
984
1047
|
}
|
|
985
1048
|
/**
|
|
986
1049
|
* The range of each shared formula of a worksheet, by master address, as exceljs computes it on
|
|
@@ -1095,9 +1158,11 @@ function overlapCounts(queries, set) {
|
|
|
1095
1158
|
return counts;
|
|
1096
1159
|
}
|
|
1097
1160
|
/** Converts a cell value for the writer; a `{ text, hyperlink }` value gives its text (the link is {@link linkIn}). */
|
|
1098
|
-
function valueIn(cell, sharedRanges) {
|
|
1161
|
+
function valueIn(cell, sharedRanges, date1904 = false) {
|
|
1099
1162
|
const v = cell.value;
|
|
1100
1163
|
if (v === null || v === void 0) return null;
|
|
1164
|
+
const serial = earlyDateSerial(v, date1904);
|
|
1165
|
+
if (serial !== void 0) return serial;
|
|
1101
1166
|
if (typeof v === "string" || typeof v === "number" || typeof v === "boolean" || v instanceof Date) return v;
|
|
1102
1167
|
if (isRecord$1(v)) {
|
|
1103
1168
|
if (cell.type === ValueType.Hyperlink) {
|
|
@@ -1109,19 +1174,19 @@ function valueIn(cell, sharedRanges) {
|
|
|
1109
1174
|
const master = v["sharedFormula"].replace(/\$/g, "").toUpperCase();
|
|
1110
1175
|
if (sharedRanges === void 0 || sharedRanges.has(master)) return {
|
|
1111
1176
|
sharedFormula: master,
|
|
1112
|
-
...resultIn(v["result"])
|
|
1177
|
+
...resultIn(v["result"], date1904)
|
|
1113
1178
|
};
|
|
1114
1179
|
const text = cell.formula;
|
|
1115
1180
|
if (text !== void 0) return {
|
|
1116
1181
|
formula: text,
|
|
1117
|
-
...resultIn(v["result"])
|
|
1182
|
+
...resultIn(v["result"], date1904)
|
|
1118
1183
|
};
|
|
1119
|
-
return resultValue(v["result"]);
|
|
1184
|
+
return resultValue(v["result"], date1904);
|
|
1120
1185
|
}
|
|
1121
1186
|
if (typeof v["formula"] === "string") {
|
|
1122
1187
|
const base = {
|
|
1123
1188
|
formula: v["formula"],
|
|
1124
|
-
...resultIn(v["result"])
|
|
1189
|
+
...resultIn(v["result"], date1904)
|
|
1125
1190
|
};
|
|
1126
1191
|
if (v["shareType"] === "array" && typeof v["ref"] === "string") return {
|
|
1127
1192
|
...base,
|
|
@@ -1379,7 +1444,8 @@ function sameBytes(a, b) {
|
|
|
1379
1444
|
function toNewWorkbook(sheets, date1904, meta = {}) {
|
|
1380
1445
|
const book = createWorkbook({
|
|
1381
1446
|
...date1904 ? { dateSystem: "1904" } : {},
|
|
1382
|
-
...meta.properties ? { properties: meta.properties } : {}
|
|
1447
|
+
...meta.properties ? { properties: meta.properties } : {},
|
|
1448
|
+
...meta.defaultFont ? { defaultFont: meta.defaultFont } : {}
|
|
1383
1449
|
});
|
|
1384
1450
|
if (sheets.length === 0) notSupported("Writing a workbook without worksheets");
|
|
1385
1451
|
const coreImages = /* @__PURE__ */ new Map();
|
|
@@ -1426,31 +1492,76 @@ function toNewWorkbook(sheets, date1904, meta = {}) {
|
|
|
1426
1492
|
if (options) sheet.column(column.number, options);
|
|
1427
1493
|
});
|
|
1428
1494
|
const styleCache = /* @__PURE__ */ new Map();
|
|
1495
|
+
const partsCache = /* @__PURE__ */ new Map();
|
|
1496
|
+
const byParts = (s) => {
|
|
1497
|
+
const parts = [
|
|
1498
|
+
s.numFmt,
|
|
1499
|
+
s.font,
|
|
1500
|
+
s.fill,
|
|
1501
|
+
s.border,
|
|
1502
|
+
s.alignment,
|
|
1503
|
+
s.protection
|
|
1504
|
+
];
|
|
1505
|
+
let level = partsCache;
|
|
1506
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
1507
|
+
let next = level.get(parts[i]);
|
|
1508
|
+
if (!next) level.set(parts[i], next = /* @__PURE__ */ new Map());
|
|
1509
|
+
level = next;
|
|
1510
|
+
}
|
|
1511
|
+
let c = level.get(parts[parts.length - 1]);
|
|
1512
|
+
if (!c) level.set(parts[parts.length - 1], c = styleIn(s));
|
|
1513
|
+
return c;
|
|
1514
|
+
};
|
|
1429
1515
|
const styleOf = (s) => {
|
|
1430
1516
|
let c = styleCache.get(s);
|
|
1431
1517
|
if (!c) {
|
|
1432
|
-
c =
|
|
1518
|
+
c = byParts(s);
|
|
1433
1519
|
styleCache.set(s, c);
|
|
1434
1520
|
}
|
|
1435
1521
|
return c;
|
|
1436
1522
|
};
|
|
1523
|
+
const styleFor = (cell) => {
|
|
1524
|
+
if (!useStyles) return isDateCell(cell) ? { numFmt: 14 } : {};
|
|
1525
|
+
const master = cell._master;
|
|
1526
|
+
const style = master !== null && cell.style !== master.style && sameStyle(cell.style, master.style) ? master.style : cell.style;
|
|
1527
|
+
return cellStyleIn(cell, styleOf(style));
|
|
1528
|
+
};
|
|
1529
|
+
const blocks = mergeBlocks(ws);
|
|
1437
1530
|
ws._rows.forEach((row) => {
|
|
1438
1531
|
if (!row) return;
|
|
1439
|
-
|
|
1532
|
+
const style = rowStyleIn(row, useStyles, styleOf);
|
|
1533
|
+
if (row.height !== void 0 || row.hidden || row.outlineLevel || style) sheet.row(row.number, {
|
|
1534
|
+
...style ? { style } : {},
|
|
1440
1535
|
...row.height === void 0 ? {} : { height: row.height },
|
|
1441
1536
|
...row.hidden ? { hidden: true } : {},
|
|
1442
1537
|
...row.outlineLevel ? { outlineLevel: row.outlineLevel } : {},
|
|
1443
1538
|
...row.collapsed ? { collapsed: true } : {}
|
|
1444
1539
|
});
|
|
1445
1540
|
row._cells.forEach((cell) => {
|
|
1446
|
-
if (cell)
|
|
1541
|
+
if (!cell) return;
|
|
1542
|
+
if (cell._master !== null && blocks.covers(cell)) {
|
|
1543
|
+
if (cell.note !== void 0) at(ws.name, cell.address, () => {
|
|
1544
|
+
const note = noteIn(cell);
|
|
1545
|
+
if (note) sheet.note(cell.address, note);
|
|
1546
|
+
});
|
|
1547
|
+
} else at(ws.name, cell.address, () => writeCell(cell));
|
|
1447
1548
|
});
|
|
1448
1549
|
});
|
|
1550
|
+
for (const m of blocks.blocks) {
|
|
1551
|
+
const style = at(ws.name, m.cell.address, () => styleFor(m.cell));
|
|
1552
|
+
if (Object.keys(style).length === 0) continue;
|
|
1553
|
+
const width = m.right - m.left + 1;
|
|
1554
|
+
const step = Math.max(1, Math.floor(MERGE_STYLE_BLOCK / width));
|
|
1555
|
+
for (let top = m.top; top <= m.bottom; top += step) {
|
|
1556
|
+
const bottom = Math.min(m.bottom, top + step - 1);
|
|
1557
|
+
sheet.style(`${columnLetter(m.left)}${String(top)}:${columnLetter(m.right)}${String(bottom)}`, style);
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1449
1560
|
function writeCell(cell) {
|
|
1450
1561
|
const note = noteIn(cell);
|
|
1451
1562
|
if (note) sheet.note(cell.address, note);
|
|
1452
|
-
const style =
|
|
1453
|
-
const value = cell.type === ValueType.Merge ? null : valueIn(cell, sharedRanges);
|
|
1563
|
+
const style = styleFor(cell);
|
|
1564
|
+
const value = cell.type === ValueType.Merge ? null : valueIn(cell, sharedRanges, date1904);
|
|
1454
1565
|
if (value === null && Object.keys(style).length === 0) return;
|
|
1455
1566
|
sheet.set(cell.address, value, style);
|
|
1456
1567
|
const link = cell.type === ValueType.Merge ? null : linkIn(cell);
|
|
@@ -1486,6 +1597,127 @@ function toNewWorkbook(sheets, date1904, meta = {}) {
|
|
|
1486
1597
|
return book;
|
|
1487
1598
|
}
|
|
1488
1599
|
/**
|
|
1600
|
+
* The part of a file's default font (font 0) that the writer accepts as `defaultFont`: parts it
|
|
1601
|
+
* refuses (a name of more than 31 characters or an empty one, a size outside 1 to 409.55 points)
|
|
1602
|
+
* are left out, so that a loaded workbook can always be written again, as with 1.3.0 and exceljs,
|
|
1603
|
+
* which writes its own Calibri 11 as font 0. `null` when nothing of it is accepted.
|
|
1604
|
+
*/
|
|
1605
|
+
function writableDefaultFont(font) {
|
|
1606
|
+
if (!font) return null;
|
|
1607
|
+
const out = { ...font };
|
|
1608
|
+
for (let tries = Object.keys(out).length; tries >= 0; tries--) {
|
|
1609
|
+
if (Object.keys(out).length === 0) return null;
|
|
1610
|
+
try {
|
|
1611
|
+
createWorkbook({ defaultFont: out });
|
|
1612
|
+
return out;
|
|
1613
|
+
} catch (error) {
|
|
1614
|
+
const property = error instanceof XlvError ? error.details["property"] : void 0;
|
|
1615
|
+
const key = typeof property === "string" && property.startsWith("font.") ? property.slice(5).split(".")[0] : void 0;
|
|
1616
|
+
if (error instanceof XlvError && error.code === "XLV_E_INVALID_STYLE" && key !== void 0 && key in out) delete out[key];
|
|
1617
|
+
else return null;
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
return null;
|
|
1621
|
+
}
|
|
1622
|
+
/**
|
|
1623
|
+
* The row style to write for a row (F3), as exceljs writes it: row@s with customFormat="1"
|
|
1624
|
+
* (row-xform.js L19-23, L44-47), with styles only, and only for a row that exceljs writes at all,
|
|
1625
|
+
* one with a height or cells (lib/doc/row.js L333-364). `undefined` when there is none.
|
|
1626
|
+
*/
|
|
1627
|
+
function rowStyleIn(row, useStyles, convert = styleIn) {
|
|
1628
|
+
if (!useStyles || row.height === void 0 && !row._cells.some((c) => c !== void 0)) return void 0;
|
|
1629
|
+
const style = convert(row.style);
|
|
1630
|
+
return Object.keys(style).length > 0 ? style : void 0;
|
|
1631
|
+
}
|
|
1632
|
+
/** Cells styled by one `sheet.style` call for a merged range: at most what the core styles at once. */
|
|
1633
|
+
const MERGE_STYLE_BLOCK = 1048576;
|
|
1634
|
+
/**
|
|
1635
|
+
* Whether two exceljs styles are equal: the same object (as `mergeCells` shares the master's),
|
|
1636
|
+
* parts that are the same objects (a column style given to new cells), or equal plain data (styles
|
|
1637
|
+
* read from a file). Parts that are `undefined` count as missing.
|
|
1638
|
+
*/
|
|
1639
|
+
function sameStyle(a, b) {
|
|
1640
|
+
if (a === b) return true;
|
|
1641
|
+
const x = a;
|
|
1642
|
+
const y = b;
|
|
1643
|
+
for (const k in x) if (!samePlain(x[k], y[k], 0)) return false;
|
|
1644
|
+
for (const k in y) if (x[k] === void 0 && y[k] !== void 0) return false;
|
|
1645
|
+
return true;
|
|
1646
|
+
}
|
|
1647
|
+
/** Whether two styles read by the core are the same data (F3, F4: a row or column style against xf 0). */
|
|
1648
|
+
function sameStyleData(a, b) {
|
|
1649
|
+
return samePlain(a, b, 0);
|
|
1650
|
+
}
|
|
1651
|
+
/** Deep equality of plain style data (objects, arrays and primitives), to a bounded depth. */
|
|
1652
|
+
function samePlain(a, b, depth) {
|
|
1653
|
+
if (a === b) return true;
|
|
1654
|
+
if (depth > 8 || a === null || b === null || typeof a !== "object" || typeof b !== "object") return false;
|
|
1655
|
+
if (Array.isArray(a) !== Array.isArray(b) || a instanceof Date || b instanceof Date) return false;
|
|
1656
|
+
const x = a;
|
|
1657
|
+
const y = b;
|
|
1658
|
+
const keys = Object.keys(x);
|
|
1659
|
+
if (keys.length !== Object.keys(y).length) return false;
|
|
1660
|
+
for (const k of keys) if (!Object.prototype.hasOwnProperty.call(y, k) || !samePlain(x[k], y[k], depth + 1)) return false;
|
|
1661
|
+
return true;
|
|
1662
|
+
}
|
|
1663
|
+
/**
|
|
1664
|
+
* The other cells of the merged ranges, styled in blocks instead of one by one (K2, F2): in each
|
|
1665
|
+
* column of a range, runs of rows whose cells exist, belong to the range's master and have equal
|
|
1666
|
+
* styles (the object `mergeCells` shares with them, as in exceljs, a column's or row's style
|
|
1667
|
+
* parts, or equal data read from a file), joined across columns while the runs are the same
|
|
1668
|
+
* (review of 1.3.1: cells of a styled column or row that differ from the master were written one
|
|
1669
|
+
* by one). The master itself is written as a cell. `covers` tells whether a cell is styled by a
|
|
1670
|
+
* block; only its note is then written on its own (K3).
|
|
1671
|
+
*/
|
|
1672
|
+
function mergeBlocks(ws) {
|
|
1673
|
+
const blocks = [];
|
|
1674
|
+
const masters = /* @__PURE__ */ new Set();
|
|
1675
|
+
for (const m of ws._merges.values()) {
|
|
1676
|
+
const master = ws.findCell(m.top, m.left);
|
|
1677
|
+
if (!master) continue;
|
|
1678
|
+
masters.add(master);
|
|
1679
|
+
let open = [];
|
|
1680
|
+
const close = (right) => {
|
|
1681
|
+
for (const o of open) blocks.push({
|
|
1682
|
+
cell: o.run.cell,
|
|
1683
|
+
top: o.run.top,
|
|
1684
|
+
bottom: o.run.bottom,
|
|
1685
|
+
left: o.left,
|
|
1686
|
+
right
|
|
1687
|
+
});
|
|
1688
|
+
};
|
|
1689
|
+
for (let c = m.left; c <= m.right; c++) {
|
|
1690
|
+
const runs = [];
|
|
1691
|
+
let run = null;
|
|
1692
|
+
for (let r = m.top; r <= m.bottom; r++) {
|
|
1693
|
+
const cell = r === m.top && c === m.left ? void 0 : ws.findRow(r)?.findCell(c);
|
|
1694
|
+
if (!cell || cell._master !== master) {
|
|
1695
|
+
run = null;
|
|
1696
|
+
continue;
|
|
1697
|
+
}
|
|
1698
|
+
if (run && sameStyle(cell.style, run.cell.style)) run.bottom = r;
|
|
1699
|
+
else runs.push(run = {
|
|
1700
|
+
top: r,
|
|
1701
|
+
bottom: r,
|
|
1702
|
+
cell
|
|
1703
|
+
});
|
|
1704
|
+
}
|
|
1705
|
+
if (!(open.length === runs.length && runs.every((x, i) => x.top === open[i].run.top && x.bottom === open[i].run.bottom && sameStyle(x.cell.style, open[i].run.cell.style)))) {
|
|
1706
|
+
close(c - 1);
|
|
1707
|
+
open = runs.map((x) => ({
|
|
1708
|
+
run: x,
|
|
1709
|
+
left: c
|
|
1710
|
+
}));
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
close(m.right);
|
|
1714
|
+
}
|
|
1715
|
+
return {
|
|
1716
|
+
blocks,
|
|
1717
|
+
covers: (cell) => cell._master !== null && masters.has(cell._master)
|
|
1718
|
+
};
|
|
1719
|
+
}
|
|
1720
|
+
/**
|
|
1489
1721
|
* The validations, conditional formats, protection and printing settings of a worksheet in the
|
|
1490
1722
|
* form of the core API (D-C06). exceljs always writes page margins and page setup
|
|
1491
1723
|
* (worksheet-xform.js L337–339), so `pageSetup` is always given.
|
|
@@ -1598,6 +1830,18 @@ function viewOut(view, freeze) {
|
|
|
1598
1830
|
};
|
|
1599
1831
|
}
|
|
1600
1832
|
/**
|
|
1833
|
+
* Whether a formula read by xlvane has text, without making the core build it: the core moves a
|
|
1834
|
+
* shared master's text to a dependent only when the dependent's `text` is read (a getter), and
|
|
1835
|
+
* gives `null` (a plain value) when it cannot move it. Reading the text of every dependent made a
|
|
1836
|
+
* small file with a huge master and many dependents exhaust memory (K2), and exceljs reads a
|
|
1837
|
+
* dependent as `{ sharedFormula }` without its text anyway.
|
|
1838
|
+
*/
|
|
1839
|
+
function hasFormulaText(f) {
|
|
1840
|
+
const d = Object.getOwnPropertyDescriptor(f, "text");
|
|
1841
|
+
if (d !== void 0 && d.get !== void 0) return true;
|
|
1842
|
+
return f.text !== null;
|
|
1843
|
+
}
|
|
1844
|
+
/**
|
|
1601
1845
|
* A formula read by xlvane in exceljs's shape (cell-xform.js L460–470; X4-29): a shared-formula
|
|
1602
1846
|
* master `{ formula, result, ref, shareType: 'shared' }`, a dependent `{ sharedFormula: master,
|
|
1603
1847
|
* result }` (its `cell.formula` is translated), an array formula `{ formula, result, ref,
|
|
@@ -2858,6 +3102,56 @@ function notSupported(feature) {
|
|
|
2858
3102
|
details: { feature }
|
|
2859
3103
|
});
|
|
2860
3104
|
}
|
|
3105
|
+
/**
|
|
3106
|
+
* The shared-formula text moved per workbook (K4 S1, D-K04). A move costs time in proportion to the
|
|
3107
|
+
* master's length, and the load-time budget is spent once, so a 4 KB file whose 1 000 000-character
|
|
3108
|
+
* master is shared with 250 cells took about 47 s to read every dependent's formula once. Two
|
|
3109
|
+
* budgets, counting the master's text read plus the moved text built for each `cell.formula` that
|
|
3110
|
+
* is not kept yet:
|
|
3111
|
+
* - every move: 512 Mi characters, the default `maxSharedStringChars` with which xlvane's
|
|
3112
|
+
* `readWorkbook` expands shared formulas, so a workbook the core reads is read here too;
|
|
3113
|
+
* - moves of masters longer than 8192 characters, the longest formula Office writes ([MS-OI29500]
|
|
3114
|
+
* §2.1.1085 a): 32 Mi characters, so such a master stops after a few moves.
|
|
3115
|
+
*/
|
|
3116
|
+
const SHARED_FORMULA_TEXT = 536870912;
|
|
3117
|
+
const LONG_SHARED_FORMULA_TEXT = 33554432;
|
|
3118
|
+
const LONG_FORMULA = 8192;
|
|
3119
|
+
/** Characters moved in all and for long masters, per workbook (a `Worksheet.workbook`). */
|
|
3120
|
+
const sharedFormulaSpent = /* @__PURE__ */ new WeakMap();
|
|
3121
|
+
/**
|
|
3122
|
+
* @internal Moves the text of a shared-formula master at `master` to `row`/`col` of `sheet` with
|
|
3123
|
+
* `shiftFormula`, counted against the workbook's shared-formula budgets; `undefined` when the
|
|
3124
|
+
* formula cannot be moved. Throws `XLV_E_LIMIT_EXCEEDED` before a move that would pass one.
|
|
3125
|
+
*/
|
|
3126
|
+
function moveSharedFormula(workbook, sheet, text, master, row, col) {
|
|
3127
|
+
let spent = sharedFormulaSpent.get(workbook);
|
|
3128
|
+
if (spent === void 0) sharedFormulaSpent.set(workbook, spent = {
|
|
3129
|
+
chars: 0,
|
|
3130
|
+
long: 0
|
|
3131
|
+
});
|
|
3132
|
+
const cost = 2 * text.length;
|
|
3133
|
+
const long = text.length > LONG_FORMULA;
|
|
3134
|
+
const limit = long && spent.long + cost > LONG_SHARED_FORMULA_TEXT ? LONG_SHARED_FORMULA_TEXT : spent.chars + cost > SHARED_FORMULA_TEXT ? SHARED_FORMULA_TEXT : null;
|
|
3135
|
+
if (limit !== null) {
|
|
3136
|
+
const cell = `${columnLetter(col)}${String(row)}`;
|
|
3137
|
+
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.`, {
|
|
3138
|
+
location: {
|
|
3139
|
+
sheet,
|
|
3140
|
+
cell
|
|
3141
|
+
},
|
|
3142
|
+
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.",
|
|
3143
|
+
details: {
|
|
3144
|
+
limit: "sharedFormulaText",
|
|
3145
|
+
value: limit
|
|
3146
|
+
}
|
|
3147
|
+
});
|
|
3148
|
+
}
|
|
3149
|
+
const moved = shiftFormula(text, row - master.row, col - master.col);
|
|
3150
|
+
const used = text.length + (moved?.length ?? 0);
|
|
3151
|
+
spent.chars += used;
|
|
3152
|
+
if (long) spent.long += used;
|
|
3153
|
+
return moved ?? void 0;
|
|
3154
|
+
}
|
|
2861
3155
|
/** Column letters for a 1-based number (A … XFD). */
|
|
2862
3156
|
function columnLetter(n) {
|
|
2863
3157
|
let s = "";
|
|
@@ -2902,7 +3196,7 @@ function typeOf(value) {
|
|
|
2902
3196
|
if (value instanceof Date) return ValueType.Date;
|
|
2903
3197
|
if (isRecord(value)) {
|
|
2904
3198
|
if (value["text"] && value["hyperlink"]) return ValueType.Hyperlink;
|
|
2905
|
-
if (value["
|
|
3199
|
+
if (value["sharedFormula"] || value["formula"]) return ValueType.Formula;
|
|
2906
3200
|
if (value["richText"]) return ValueType.RichText;
|
|
2907
3201
|
if (value["sharedString"]) return ValueType.SharedString;
|
|
2908
3202
|
if (value["error"]) return ValueType.Error;
|
|
@@ -2951,14 +3245,19 @@ var Cell = class {
|
|
|
2951
3245
|
_master = null;
|
|
2952
3246
|
/** @internal Number of merged ranges this cell is the master of. */
|
|
2953
3247
|
_mergeCount = 0;
|
|
3248
|
+
/** The moved formula of a `{ sharedFormula }` value, and what it was moved from (see `formula`). */
|
|
3249
|
+
_moved = void 0;
|
|
2954
3250
|
/** @internal Use `worksheet.getCell()`. */
|
|
2955
3251
|
constructor(_row, _col) {
|
|
2956
3252
|
this._row = _row;
|
|
2957
3253
|
this._col = _col;
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
3254
|
+
this.style = this._rowColumnStyle();
|
|
3255
|
+
}
|
|
3256
|
+
/** The style a new cell takes: row style wins over column style, part by part (lib/doc/cell.js L99-119 `_mergeStyle`). */
|
|
3257
|
+
_rowColumnStyle() {
|
|
3258
|
+
return {
|
|
3259
|
+
...this._row.worksheet.findColumn(this._col)?.style,
|
|
3260
|
+
...withoutEmpty(this._row.style)
|
|
2962
3261
|
};
|
|
2963
3262
|
}
|
|
2964
3263
|
get worksheet() {
|
|
@@ -3036,20 +3335,52 @@ var Cell = class {
|
|
|
3036
3335
|
const v = this.value;
|
|
3037
3336
|
if (!isRecord(v)) return void 0;
|
|
3038
3337
|
if (typeof v["formula"] === "string") return v["formula"];
|
|
3039
|
-
|
|
3040
|
-
|
|
3338
|
+
const shared = v["sharedFormula"];
|
|
3339
|
+
if (typeof shared === "string") {
|
|
3340
|
+
const masterRef = shared.replace(/\$/g, "");
|
|
3041
3341
|
const master = this.worksheet.findCell(masterRef);
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3342
|
+
let text;
|
|
3343
|
+
let at;
|
|
3344
|
+
if (!master) {
|
|
3345
|
+
text = this.worksheet._sharedMasters?.get(masterRef.toUpperCase());
|
|
3346
|
+
if (text === void 0) return void 0;
|
|
3347
|
+
at = decodeAddress(masterRef);
|
|
3348
|
+
} else {
|
|
3349
|
+
if (master === this) return void 0;
|
|
3350
|
+
text = master.formula;
|
|
3351
|
+
if (text === void 0) return void 0;
|
|
3352
|
+
at = {
|
|
3353
|
+
row: master.row,
|
|
3354
|
+
col: master.col
|
|
3355
|
+
};
|
|
3356
|
+
}
|
|
3357
|
+
const kept = this._moved;
|
|
3358
|
+
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;
|
|
3359
|
+
const moved = moveSharedFormula(this.worksheet.workbook, this.worksheet.name, text, at, this.row, this.col);
|
|
3360
|
+
this._moved = {
|
|
3361
|
+
value: v,
|
|
3362
|
+
shared,
|
|
3363
|
+
master: text,
|
|
3364
|
+
masterRow: at.row,
|
|
3365
|
+
masterCol: at.col,
|
|
3366
|
+
row: this.row,
|
|
3367
|
+
col: this.col,
|
|
3368
|
+
text: moved
|
|
3369
|
+
};
|
|
3370
|
+
return moved;
|
|
3045
3371
|
}
|
|
3046
3372
|
}
|
|
3047
|
-
/**
|
|
3373
|
+
/**
|
|
3374
|
+
* `FormulaType.Shared` for a `{ sharedFormula }` value, `Master` for other formulas, and
|
|
3375
|
+
* `undefined` for a cell without a formula and for a merged cell other than the top-left one,
|
|
3376
|
+
* as in exceljs, where only the formula value has a `formulaType` (lib/doc/cell.js L264-266,
|
|
3377
|
+
* L807-815; exceljs's declaration says `FormulaType` all the same).
|
|
3378
|
+
*/
|
|
3048
3379
|
get formulaType() {
|
|
3049
|
-
const v = this.value;
|
|
3050
|
-
if (!isRecord(v)) return
|
|
3380
|
+
const v = this._master ? null : this.value;
|
|
3381
|
+
if (!isRecord(v)) return void 0;
|
|
3051
3382
|
if (typeof v["sharedFormula"] === "string") return FormulaType.Shared;
|
|
3052
|
-
return typeof v["formula"] === "string" ? FormulaType.Master :
|
|
3383
|
+
return typeof v["formula"] === "string" ? FormulaType.Master : void 0;
|
|
3053
3384
|
}
|
|
3054
3385
|
get result() {
|
|
3055
3386
|
const v = this.value;
|
|
@@ -3224,6 +3555,15 @@ var Cell = class {
|
|
|
3224
3555
|
_unmerge() {
|
|
3225
3556
|
this._master = null;
|
|
3226
3557
|
}
|
|
3558
|
+
/**
|
|
3559
|
+
* @internal As exceljs `cell.unmerge()` (lib/doc/cell.js L177-183): a cell merged into another
|
|
3560
|
+
* one gets the row and column style again, instead of the master's.
|
|
3561
|
+
*/
|
|
3562
|
+
_unmergeResetStyle() {
|
|
3563
|
+
if (this._master === null) return;
|
|
3564
|
+
this._master = null;
|
|
3565
|
+
this.style = this._rowColumnStyle();
|
|
3566
|
+
}
|
|
3227
3567
|
};
|
|
3228
3568
|
function withoutEmpty(style) {
|
|
3229
3569
|
const out = {};
|
|
@@ -3601,11 +3941,11 @@ var Column = class {
|
|
|
3601
3941
|
this._hidden = !!value.hidden;
|
|
3602
3942
|
this._outlineLevel = value.outlineLevel ?? 0;
|
|
3603
3943
|
}
|
|
3604
|
-
/** Cell values of this column by row number (index 0 unused). */
|
|
3944
|
+
/** Cell values of this column by row number (index 0 unused); empty cells are left out (lib/doc/column.js L179-187). */
|
|
3605
3945
|
get values() {
|
|
3606
3946
|
const out = [];
|
|
3607
3947
|
this.eachCell((cell, r) => {
|
|
3608
|
-
out[r] = cell.value;
|
|
3948
|
+
if (cell.type !== ValueType.Null) out[r] = cell.value;
|
|
3609
3949
|
});
|
|
3610
3950
|
return out;
|
|
3611
3951
|
}
|
|
@@ -3620,8 +3960,7 @@ var Column = class {
|
|
|
3620
3960
|
const includeEmpty = typeof options === "object" && options !== null && options.includeEmpty === true;
|
|
3621
3961
|
if (!cb) return;
|
|
3622
3962
|
this.worksheet.eachRow({ includeEmpty }, (row, r) => {
|
|
3623
|
-
|
|
3624
|
-
if (cell && (includeEmpty || cell.type !== ValueType.Null)) cb(cell, r);
|
|
3963
|
+
cb(row.getCell(this.number), r);
|
|
3625
3964
|
});
|
|
3626
3965
|
}
|
|
3627
3966
|
applyStyle(name, value) {
|
|
@@ -3782,6 +4121,11 @@ var Worksheet = class {
|
|
|
3782
4121
|
_columns = [];
|
|
3783
4122
|
/** @internal */
|
|
3784
4123
|
_merges = new MergeMap();
|
|
4124
|
+
/**
|
|
4125
|
+
* @internal Formulas of shared-formula masters by address, for the dependents of a worksheet
|
|
4126
|
+
* whose rows are not kept (stream.xlsx.WorkbookReader), so `cell.formula` can move them (K2).
|
|
4127
|
+
*/
|
|
4128
|
+
_sharedMasters = void 0;
|
|
3785
4129
|
/** The cells covered by `_merges` (row × 16 385 + column), valid while `_mergedVersion` equals `_merges.version`. */
|
|
3786
4130
|
_mergedCells = /* @__PURE__ */ new Set();
|
|
3787
4131
|
_mergedVersion = -1;
|
|
@@ -4179,8 +4523,9 @@ var Worksheet = class {
|
|
|
4179
4523
|
}
|
|
4180
4524
|
/**
|
|
4181
4525
|
* @internal Merges a range without the overlap check of `mergeCells`. Cells keep their own
|
|
4182
|
-
* style
|
|
4183
|
-
*
|
|
4526
|
+
* style object (the master's, when `mergeCells` shared it with them), except those for which
|
|
4527
|
+
* `takeMasterStyle` returns true (cells inserted inside the range), which get a copy of the
|
|
4528
|
+
* master's style.
|
|
4184
4529
|
*/
|
|
4185
4530
|
_mergeRange(range, takeMasterStyle = () => false) {
|
|
4186
4531
|
const master = this.getCell(range.top, range.left);
|
|
@@ -4492,7 +4837,7 @@ var Worksheet = class {
|
|
|
4492
4837
|
if (r === range.top && c === range.left) continue;
|
|
4493
4838
|
const cell = this.getCell(r, c);
|
|
4494
4839
|
cell._merge(master);
|
|
4495
|
-
if (!this._mergeKeepStyle) cell.style =
|
|
4840
|
+
if (!this._mergeKeepStyle) cell.style = master.style;
|
|
4496
4841
|
}
|
|
4497
4842
|
master._mergeCount++;
|
|
4498
4843
|
this._merges.set(`${columnLetter(range.left)}${String(range.top)}`, range);
|
|
@@ -4511,7 +4856,7 @@ var Worksheet = class {
|
|
|
4511
4856
|
unMergeCells(...args) {
|
|
4512
4857
|
const range = parseRangeArgs(args);
|
|
4513
4858
|
for (const [key, m] of this._merges) if (range.top <= m.bottom && m.top <= range.bottom && range.left <= m.right && m.left <= range.right) {
|
|
4514
|
-
for (let r = m.top; r <= m.bottom; r++) for (let c = m.left; c <= m.right; c++) this.findCell(r, c)?.
|
|
4859
|
+
for (let r = m.top; r <= m.bottom; r++) for (let c = m.left; c <= m.right; c++) this.findCell(r, c)?._unmergeResetStyle();
|
|
4515
4860
|
const master = this.findCell(m.top, m.left);
|
|
4516
4861
|
if (master) master._mergeCount = Math.max(0, master._mergeCount - 1);
|
|
4517
4862
|
this._merges.delete(key);
|
|
@@ -4698,8 +5043,12 @@ const INJECTION_START = /* @__PURE__ */ new Set([
|
|
|
4698
5043
|
"﹦",
|
|
4699
5044
|
"﹢",
|
|
4700
5045
|
"﹣",
|
|
4701
|
-
"﹫"
|
|
5046
|
+
"﹫",
|
|
5047
|
+
"",
|
|
5048
|
+
""
|
|
4702
5049
|
]);
|
|
5050
|
+
/** Characters a field is quoted for while formulas are escaped, whatever the delimiter: other applications may split on them (K2). */
|
|
5051
|
+
const COMMON_DELIMITERS = ",; |";
|
|
4703
5052
|
function invalid(name, message, hint) {
|
|
4704
5053
|
return new XlvError("XLV_E_INVALID_OPTION", message, {
|
|
4705
5054
|
hint,
|
|
@@ -4985,9 +5334,41 @@ function malformed(line, message) {
|
|
|
4985
5334
|
details: { line }
|
|
4986
5335
|
});
|
|
4987
5336
|
}
|
|
4988
|
-
/**
|
|
4989
|
-
|
|
4990
|
-
|
|
5337
|
+
/** Rows and columns of a worksheet (Part 1 §18.3.1.73, ST_CellRef up to XFD1048576). */
|
|
5338
|
+
const MAX_SHEET_ROWS = 1048576;
|
|
5339
|
+
const MAX_SHEET_COLUMNS = 16384;
|
|
5340
|
+
/** Fields added to the worksheet in total, as the core's `maxCells` read limit (K3). */
|
|
5341
|
+
const MAX_CSV_CELLS = DEFAULT_READ_LIMITS.maxCells;
|
|
5342
|
+
function tooMany(what, line) {
|
|
5343
|
+
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.`, {
|
|
5344
|
+
location: { line },
|
|
5345
|
+
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.",
|
|
5346
|
+
details: {
|
|
5347
|
+
limit: "maxCells",
|
|
5348
|
+
value: MAX_CSV_CELLS,
|
|
5349
|
+
line
|
|
5350
|
+
}
|
|
5351
|
+
});
|
|
5352
|
+
const rows = what === "rows";
|
|
5353
|
+
const max = rows ? MAX_SHEET_ROWS : MAX_SHEET_COLUMNS;
|
|
5354
|
+
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.`, {
|
|
5355
|
+
location: { line },
|
|
5356
|
+
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).",
|
|
5357
|
+
details: {
|
|
5358
|
+
limit: rows ? "maxRows" : "maxColumns",
|
|
5359
|
+
value: max,
|
|
5360
|
+
line
|
|
5361
|
+
}
|
|
5362
|
+
});
|
|
5363
|
+
}
|
|
5364
|
+
/**
|
|
5365
|
+
* The rows of CSV text, split as fast-csv 4.3 does with the given parser options, one at a time:
|
|
5366
|
+
* the rows that `ignoreEmpty`, the header, `skipRows` and `maxRows` drop are left out. More than
|
|
5367
|
+
* 1 048 576 rows, or a row of more than 16 384 fields, throw `XLV_E_LIMIT_EXCEEDED` as soon as
|
|
5368
|
+
* they are reached, because a worksheet cannot hold them (K2); so do more than 5 000 000 fields
|
|
5369
|
+
* in the rows given (`DEFAULT_READ_LIMITS.maxCells`, K3).
|
|
5370
|
+
*/
|
|
5371
|
+
function* csvRows(text, o) {
|
|
4991
5372
|
let at = 0;
|
|
4992
5373
|
let line = 1;
|
|
4993
5374
|
const n = text.length;
|
|
@@ -4996,6 +5377,10 @@ function parseCsv(text, o) {
|
|
|
4996
5377
|
if (text[i] === "\r" && text[i + 1] === "\n") return i + 2;
|
|
4997
5378
|
return i < n ? i + 1 : i;
|
|
4998
5379
|
};
|
|
5380
|
+
const trim = (f) => o.trim ? f.trim() : o.ltrim ? f.trimStart() : o.rtrim ? f.trimEnd() : f;
|
|
5381
|
+
let kept = 0;
|
|
5382
|
+
let emitted = 0;
|
|
5383
|
+
let cells = 0;
|
|
4999
5384
|
for (let k = 0; k < o.skipLines && at < n; k++) {
|
|
5000
5385
|
while (!atLineEnd(at)) at++;
|
|
5001
5386
|
at = skipLineEnd(at);
|
|
@@ -5008,38 +5393,52 @@ function parseCsv(text, o) {
|
|
|
5008
5393
|
line++;
|
|
5009
5394
|
continue;
|
|
5010
5395
|
}
|
|
5396
|
+
const rowLine = line;
|
|
5011
5397
|
const fields = [];
|
|
5398
|
+
let quoted = false;
|
|
5399
|
+
let extra = 0;
|
|
5400
|
+
let extraText = false;
|
|
5401
|
+
const push = (field) => {
|
|
5402
|
+
if (fields.length < MAX_SHEET_COLUMNS) fields.push(field);
|
|
5403
|
+
else {
|
|
5404
|
+
extra++;
|
|
5405
|
+
if (field.trim() !== "") extraText = true;
|
|
5406
|
+
}
|
|
5407
|
+
};
|
|
5012
5408
|
for (;;) {
|
|
5013
5409
|
let start = at;
|
|
5014
5410
|
while (start < n && (text[start] === " " || text[start] === " ") && text[start] !== o.delimiter) start++;
|
|
5015
5411
|
if (o.quote !== "" && text[start] === o.quote) {
|
|
5412
|
+
quoted = true;
|
|
5016
5413
|
let value = "";
|
|
5017
5414
|
let i = start + 1;
|
|
5415
|
+
let from = i;
|
|
5018
5416
|
const opened = line;
|
|
5019
5417
|
for (;;) {
|
|
5020
5418
|
if (i >= n) throw malformed(opened, `missing closing ${o.quote} for the field that starts here.`);
|
|
5021
5419
|
const ch = text[i];
|
|
5022
5420
|
if (ch === o.escape && text[i + 1] === o.quote) {
|
|
5023
|
-
value += o.quote;
|
|
5421
|
+
value += text.slice(from, i) + o.quote;
|
|
5024
5422
|
i += 2;
|
|
5423
|
+
from = i;
|
|
5025
5424
|
continue;
|
|
5026
5425
|
}
|
|
5027
5426
|
if (ch === o.quote) {
|
|
5427
|
+
value += text.slice(from, i);
|
|
5028
5428
|
i++;
|
|
5029
5429
|
break;
|
|
5030
5430
|
}
|
|
5031
5431
|
if (ch === "\n") line++;
|
|
5032
|
-
value += ch;
|
|
5033
5432
|
i++;
|
|
5034
5433
|
}
|
|
5035
5434
|
while (i < n && (text[i] === " " || text[i] === " ") && text[i] !== o.delimiter) i++;
|
|
5036
5435
|
if (i < n && text[i] !== o.delimiter && text[i] !== "\n" && text[i] !== "\r") throw malformed(line, `unexpected text after the closing ${o.quote}.`);
|
|
5037
|
-
|
|
5436
|
+
push(value);
|
|
5038
5437
|
at = i;
|
|
5039
5438
|
} else {
|
|
5040
5439
|
let i = at;
|
|
5041
5440
|
while (i < n && text[i] !== o.delimiter && text[i] !== "\n" && text[i] !== "\r") i++;
|
|
5042
|
-
|
|
5441
|
+
push(text.slice(at, i));
|
|
5043
5442
|
at = i;
|
|
5044
5443
|
}
|
|
5045
5444
|
if (at < n && text[at] === o.delimiter) {
|
|
@@ -5050,15 +5449,31 @@ function parseCsv(text, o) {
|
|
|
5050
5449
|
}
|
|
5051
5450
|
at = skipLineEnd(at);
|
|
5052
5451
|
line++;
|
|
5053
|
-
const row = fields.length === 1 && fields[0].trim() === "" ? [
|
|
5054
|
-
|
|
5055
|
-
|
|
5056
|
-
|
|
5057
|
-
|
|
5058
|
-
|
|
5059
|
-
|
|
5060
|
-
|
|
5061
|
-
|
|
5452
|
+
const row = fields.length === 1 && !quoted && fields[0].trim() === "" ? [] : fields;
|
|
5453
|
+
if (o.ignoreEmpty && !extraText && row.every((f) => f.trim() === "")) continue;
|
|
5454
|
+
kept++;
|
|
5455
|
+
if (o.skipHeader && kept === 1) continue;
|
|
5456
|
+
if (kept - (o.skipHeader ? 1 : 0) <= o.skipRows) continue;
|
|
5457
|
+
if (o.maxRows > 0 && emitted >= o.maxRows) continue;
|
|
5458
|
+
if (extra > 0) throw tooMany("columns", rowLine);
|
|
5459
|
+
if (++emitted > MAX_SHEET_ROWS) throw tooMany("rows", rowLine);
|
|
5460
|
+
cells += row.length;
|
|
5461
|
+
if (cells > MAX_CSV_CELLS) throw tooMany("cells", rowLine);
|
|
5462
|
+
yield {
|
|
5463
|
+
fields: row.map(trim),
|
|
5464
|
+
line: rowLine
|
|
5465
|
+
};
|
|
5466
|
+
}
|
|
5467
|
+
}
|
|
5468
|
+
/** The delimiters and line-break characters in `text`: at least the number of fields less one. */
|
|
5469
|
+
function fieldEnds(text, delimiter) {
|
|
5470
|
+
const d = delimiter.charCodeAt(0);
|
|
5471
|
+
let n = 0;
|
|
5472
|
+
for (let i = 0; i < text.length; i++) {
|
|
5473
|
+
const c = text.charCodeAt(i);
|
|
5474
|
+
if (c === d || c === 10 || c === 13) n++;
|
|
5475
|
+
}
|
|
5476
|
+
return n;
|
|
5062
5477
|
}
|
|
5063
5478
|
/**
|
|
5064
5479
|
* exceljs's default conversion of a CSV field (lib/csv/csv.js L57–83), with two fixes: text of
|
|
@@ -5078,7 +5493,7 @@ function csvValue(datum, dateFormats) {
|
|
|
5078
5493
|
if (special !== void 0) return typeof special === "object" ? { ...special } : special;
|
|
5079
5494
|
return datum;
|
|
5080
5495
|
}
|
|
5081
|
-
function formatterOptions(raw) {
|
|
5496
|
+
function formatterOptions(raw, escapeFormulas) {
|
|
5082
5497
|
const o = raw ?? {};
|
|
5083
5498
|
const delimiter = typeof o["delimiter"] === "string" ? o["delimiter"] : ",";
|
|
5084
5499
|
const rowDelimiter = typeof o["rowDelimiter"] === "string" ? o["rowDelimiter"] : "\n";
|
|
@@ -5086,20 +5501,49 @@ function formatterOptions(raw) {
|
|
|
5086
5501
|
const quote = q === false ? "" : q === true || q === void 0 ? "\"" : String(q);
|
|
5087
5502
|
const escape = typeof o["escape"] === "string" ? o["escape"] : quote;
|
|
5088
5503
|
const escapeRe = (s) => s.replace(/[\\^$.*+?()[\]{}|-]/g, "\\$&");
|
|
5504
|
+
const transform = o["transform"];
|
|
5505
|
+
if (transform && typeof transform !== "function") throw invalid("formatterOptions.transform", "The option \"transform\" must be a function.", "Pass a function that takes a row and returns the row to write.");
|
|
5506
|
+
if (o["BOM"] !== void 0 && o["BOM"] !== "") throw new XlvError("XLV_E_NOT_SUPPORTED", "formatterOptions.BOM other than \"\\uFEFF\" is not supported.", {
|
|
5507
|
+
hint: "Leave BOM out; writeBOM: true writes the byte order mark U+FEFF.",
|
|
5508
|
+
details: { option: "BOM" }
|
|
5509
|
+
});
|
|
5510
|
+
const quoteColumns = o["quoteColumns"] ?? false;
|
|
5089
5511
|
return {
|
|
5090
5512
|
delimiter,
|
|
5091
5513
|
rowDelimiter,
|
|
5092
5514
|
quote,
|
|
5093
5515
|
escapedQuote: `${escape}${quote}`,
|
|
5094
|
-
quoteColumns
|
|
5095
|
-
|
|
5516
|
+
quoteColumns,
|
|
5517
|
+
quoteHeaders: o["quoteHeaders"] === void 0 ? quoteColumns : o["quoteHeaders"],
|
|
5518
|
+
headers: Array.isArray(o["headers"]) ? o["headers"].map(String) : null,
|
|
5519
|
+
shouldWriteHeaders: !!o["headers"] && !!(o["writeHeaders"] ?? true),
|
|
5520
|
+
alwaysWriteHeaders: o["alwaysWriteHeaders"] === true,
|
|
5521
|
+
transform: typeof transform === "function" ? transform : void 0,
|
|
5096
5522
|
includeEndRowDelimiter: o["includeEndRowDelimiter"] === true,
|
|
5097
5523
|
writeBOM: o["writeBOM"] === true,
|
|
5098
|
-
escapeChars: new RegExp(`[${escapeRe(delimiter)}${escapeRe(rowDelimiter)}|\\r|\\n]`)
|
|
5524
|
+
escapeChars: new RegExp(`[${escapeRe(delimiter)}${escapeRe(rowDelimiter)}|\\r|\\n${escapeFormulas ? escapeRe(COMMON_DELIMITERS) : ""}]`),
|
|
5525
|
+
strictQuoting: escapeFormulas
|
|
5099
5526
|
};
|
|
5100
5527
|
}
|
|
5101
|
-
/**
|
|
5102
|
-
|
|
5528
|
+
/**
|
|
5529
|
+
* Whether an application that splits `text` at a comma, ";", tab or "|" would get a field that
|
|
5530
|
+
* starts like a formula: one of those characters followed by a formula sign, a control character
|
|
5531
|
+
* or a byte order mark, after any spaces or tabs and an opening double quote (K2).
|
|
5532
|
+
*/
|
|
5533
|
+
function splitStartsFormula(text) {
|
|
5534
|
+
for (let i = 0; i < text.length; i++) {
|
|
5535
|
+
if (!COMMON_DELIMITERS.includes(text[i])) continue;
|
|
5536
|
+
let j = i + 1;
|
|
5537
|
+
while (text[j] === " " || text[j] === " ") j++;
|
|
5538
|
+
if (text[j] === "\"") j++;
|
|
5539
|
+
while (text[j] === " ") j++;
|
|
5540
|
+
const c = text[j];
|
|
5541
|
+
if (c !== void 0 && (INJECTION_START.has(c) || c.charCodeAt(0) < 32)) return true;
|
|
5542
|
+
}
|
|
5543
|
+
return false;
|
|
5544
|
+
}
|
|
5545
|
+
/** One field as fast-csv FieldFormatter.format writes it; `quoteThis`: quoteColumns or quoteHeaders asks for quotes, or the escape. */
|
|
5546
|
+
function formatField(value, index, f, quoteThis) {
|
|
5103
5547
|
const text = `${value === null || value === void 0 ? "" : String(value)}`.replace(/\0/g, "");
|
|
5104
5548
|
if (f.quote === "" && (/[\r\n]/.test(text) || f.delimiter !== "" && text.includes(f.delimiter))) throw new XlvError("XLV_E_INVALID_OPTION", `Field ${String(index + 1)} contains a line break or the delimiter, which cannot be written with quote: false.`, {
|
|
5105
5549
|
hint: "Leave formatterOptions.quote at its default (\") so such fields are quoted, or remove the line breaks from the value.",
|
|
@@ -5108,9 +5552,14 @@ function formatField(value, index, f, forceQuote) {
|
|
|
5108
5552
|
field: index + 1
|
|
5109
5553
|
}
|
|
5110
5554
|
});
|
|
5555
|
+
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.`, {
|
|
5556
|
+
hint: "Leave formatterOptions.quote at its default (\") so such fields are quoted, or pass formulaInjection: \"off\" for a trusted consumer.",
|
|
5557
|
+
details: {
|
|
5558
|
+
option: "quote",
|
|
5559
|
+
field: index + 1
|
|
5560
|
+
}
|
|
5561
|
+
});
|
|
5111
5562
|
if (f.quote !== "" && text.includes(f.quote)) return `${f.quote}${text.split(f.quote).join(f.escapedQuote)}${f.quote}`;
|
|
5112
|
-
const q = f.quoteColumns;
|
|
5113
|
-
const quoteThis = forceQuote || q === true || Array.isArray(q) && q[index] === true;
|
|
5114
5563
|
if (text.search(f.escapeChars) !== -1 || quoteThis) return `${f.quote}${text}${f.quote}`;
|
|
5115
5564
|
return text;
|
|
5116
5565
|
}
|
|
@@ -5127,7 +5576,7 @@ function defaultWriteMap(dateFormat, dateUTC) {
|
|
|
5127
5576
|
if (typeof value === "object") {
|
|
5128
5577
|
const v = value;
|
|
5129
5578
|
if (v["text"] || v["hyperlink"]) return v["hyperlink"] || v["text"] || "";
|
|
5130
|
-
if (v["
|
|
5579
|
+
if (v["sharedFormula"] || v["formula"] || "result" in v) {
|
|
5131
5580
|
const result = v["result"];
|
|
5132
5581
|
if (result === void 0 || result === null) return "";
|
|
5133
5582
|
if (result instanceof Date) return date(result);
|
|
@@ -5140,15 +5589,165 @@ function defaultWriteMap(dateFormat, dateUTC) {
|
|
|
5140
5589
|
return value;
|
|
5141
5590
|
};
|
|
5142
5591
|
}
|
|
5592
|
+
/** The output encoding for the `encoding` option (an unsupported one is refused when encoding). */
|
|
5593
|
+
function outputEncoding(encoding) {
|
|
5594
|
+
const e = (encoding ?? "").toLowerCase();
|
|
5595
|
+
if (e === "latin1" || e === "binary") return "latin1";
|
|
5596
|
+
if (e === "utf16le" || e === "utf-16le" || e === "ucs2" || e === "ucs-2") return "utf16le";
|
|
5597
|
+
return "utf8";
|
|
5598
|
+
}
|
|
5599
|
+
/** Leading UTF-8 sequences of U+FF1D, U+FF0B, U+FF0D, U+FF20, U+FE66, U+FE62, U+FE63, U+FE6B. */
|
|
5600
|
+
const UTF8_FORMULA_SIGNS = [
|
|
5601
|
+
[
|
|
5602
|
+
239,
|
|
5603
|
+
188,
|
|
5604
|
+
157
|
|
5605
|
+
],
|
|
5606
|
+
[
|
|
5607
|
+
239,
|
|
5608
|
+
188,
|
|
5609
|
+
139
|
|
5610
|
+
],
|
|
5611
|
+
[
|
|
5612
|
+
239,
|
|
5613
|
+
188,
|
|
5614
|
+
141
|
|
5615
|
+
],
|
|
5616
|
+
[
|
|
5617
|
+
239,
|
|
5618
|
+
188,
|
|
5619
|
+
160
|
|
5620
|
+
],
|
|
5621
|
+
[
|
|
5622
|
+
239,
|
|
5623
|
+
185,
|
|
5624
|
+
166
|
|
5625
|
+
],
|
|
5626
|
+
[
|
|
5627
|
+
239,
|
|
5628
|
+
185,
|
|
5629
|
+
162
|
|
5630
|
+
],
|
|
5631
|
+
[
|
|
5632
|
+
239,
|
|
5633
|
+
185,
|
|
5634
|
+
163
|
|
5635
|
+
],
|
|
5636
|
+
[
|
|
5637
|
+
239,
|
|
5638
|
+
185,
|
|
5639
|
+
171
|
|
5640
|
+
]
|
|
5641
|
+
];
|
|
5642
|
+
/**
|
|
5643
|
+
* Whether the first bytes of a field, as latin1 or UTF-16LE writes it, are a byte order mark
|
|
5644
|
+
* (EF BB BF, FF FE, FE FF), which makes a reader decode the rest of the file in that encoding
|
|
5645
|
+
* and drop the mark, or the UTF-8 form of a full-width or small formula sign, which a reader that
|
|
5646
|
+
* guesses UTF-8 shows as one (K2). `prefix` holds bytes written before the field (latin1 writes
|
|
5647
|
+
* the `writeBOM` mark U+FEFF as the single byte FF). UTF-16LE bytes are taken without their NUL
|
|
5648
|
+
* bytes, as an 8-bit reader that drops them reads them (K3); a sequence that holds no NUL byte is
|
|
5649
|
+
* the same either way.
|
|
5650
|
+
*/
|
|
5651
|
+
function encodedStartIsUnsafe(text, encoding, prefix) {
|
|
5652
|
+
if (encoding === "utf8") return false;
|
|
5653
|
+
const bytes = [...prefix];
|
|
5654
|
+
for (let i = 0; bytes.length < 3 && i < text.length; i++) {
|
|
5655
|
+
const c = text.charCodeAt(i);
|
|
5656
|
+
if (encoding === "latin1") bytes.push(c & 255);
|
|
5657
|
+
else for (const b of [c & 255, c >> 8]) if (b !== 0) bytes.push(b);
|
|
5658
|
+
}
|
|
5659
|
+
const starts = (lead) => lead.every((b, i) => bytes[i] === b);
|
|
5660
|
+
return starts([
|
|
5661
|
+
239,
|
|
5662
|
+
187,
|
|
5663
|
+
191
|
|
5664
|
+
]) || starts([255, 254]) || starts([254, 255]) || UTF8_FORMULA_SIGNS.some(starts);
|
|
5665
|
+
}
|
|
5666
|
+
/** Bytes that end a field or a row for a reader of the 8-bit text, whatever its delimiter. */
|
|
5667
|
+
const BYTE_BREAKS = /* @__PURE__ */ new Set([
|
|
5668
|
+
44,
|
|
5669
|
+
59,
|
|
5670
|
+
9,
|
|
5671
|
+
124,
|
|
5672
|
+
13,
|
|
5673
|
+
10
|
|
5674
|
+
]);
|
|
5675
|
+
/** Whether the 8-bit text `view` has a formula start at `i`: "=", or spaces or tabs, a quote and "=". */
|
|
5676
|
+
function viewFormulaAt(view, i) {
|
|
5677
|
+
if (view[i] === "=") return true;
|
|
5678
|
+
let j = i;
|
|
5679
|
+
while (view[j] === " " || view[j] === " ") j++;
|
|
5680
|
+
return view[j] === "\"" && view[j + 1] === "=";
|
|
5681
|
+
}
|
|
5682
|
+
/**
|
|
5683
|
+
* Whether the UTF-16LE bytes of `text` without NULs start a formula (see {@link viewFormulaAt})
|
|
5684
|
+
* because of a character above U+00FF, such as U+3D00 (00 3D) or U+4E3D (3D 4E). Starts made only
|
|
5685
|
+
* of characters up to U+00FF are the same text in both readings and are checked as text.
|
|
5686
|
+
*/
|
|
5687
|
+
function utf16StartsFormula(text) {
|
|
5688
|
+
let view = "";
|
|
5689
|
+
let foreign = false;
|
|
5690
|
+
let lead = 0;
|
|
5691
|
+
let blank = true;
|
|
5692
|
+
for (let i = 0; i < text.length && view.length - lead < 2; i++) {
|
|
5693
|
+
const c = text.charCodeAt(i);
|
|
5694
|
+
if (c > 255) foreign = true;
|
|
5695
|
+
for (const b of c > 255 ? [c & 255, c >> 8] : [c]) {
|
|
5696
|
+
if (b === 0) continue;
|
|
5697
|
+
view += String.fromCharCode(b);
|
|
5698
|
+
if (blank && (b === 32 || b === 9)) lead++;
|
|
5699
|
+
else blank = false;
|
|
5700
|
+
}
|
|
5701
|
+
}
|
|
5702
|
+
return foreign && viewFormulaAt(view, 0);
|
|
5703
|
+
}
|
|
5704
|
+
/** The UTF-16LE bytes of a written field without NULs, and whether characters above U+00FF give a break or a quote among them. */
|
|
5705
|
+
function utf16View(written, delimiter) {
|
|
5706
|
+
let view = "";
|
|
5707
|
+
let foreignBreak = false;
|
|
5708
|
+
let foreignQuote = false;
|
|
5709
|
+
for (let i = 0; i < written.length; i++) {
|
|
5710
|
+
const c = written.charCodeAt(i);
|
|
5711
|
+
for (const b of c > 255 ? [c & 255, c >> 8] : [c]) {
|
|
5712
|
+
if (b === 0) continue;
|
|
5713
|
+
if (c > 255) {
|
|
5714
|
+
if (b === 34) foreignQuote = true;
|
|
5715
|
+
else if (BYTE_BREAKS.has(b) || delimiter.includes(String.fromCharCode(b))) foreignBreak = true;
|
|
5716
|
+
}
|
|
5717
|
+
view += String.fromCharCode(b);
|
|
5718
|
+
}
|
|
5719
|
+
}
|
|
5720
|
+
return {
|
|
5721
|
+
view,
|
|
5722
|
+
foreignBreak,
|
|
5723
|
+
foreignQuote
|
|
5724
|
+
};
|
|
5725
|
+
}
|
|
5726
|
+
/** Whether the 8-bit text `view` has a delimiter (`delimiter` or a common one) or a line break followed by a formula start. */
|
|
5727
|
+
function viewSplitStartsFormula(view, delimiter) {
|
|
5728
|
+
for (let i = 0; i < view.length; i++) {
|
|
5729
|
+
if (BYTE_BREAKS.has(view.charCodeAt(i)) && viewFormulaAt(view, i + 1)) return true;
|
|
5730
|
+
if (view.startsWith(delimiter, i) && viewFormulaAt(view, i + delimiter.length)) return true;
|
|
5731
|
+
}
|
|
5732
|
+
return false;
|
|
5733
|
+
}
|
|
5734
|
+
/** Column letters of a 1-based column number. */
|
|
5735
|
+
function columnName(n) {
|
|
5736
|
+
let s = "";
|
|
5737
|
+
for (let c = n; c > 0; c = Math.floor((c - 1) / 26)) s = String.fromCharCode(65 + (c - 1) % 26) + s;
|
|
5738
|
+
return s;
|
|
5739
|
+
}
|
|
5143
5740
|
/**
|
|
5144
|
-
* The CSV text of a worksheet, as exceljs `csv.write` gives it
|
|
5741
|
+
* The CSV text of a worksheet, as exceljs `csv.write` gives it, for output in `encoding`.
|
|
5145
5742
|
*
|
|
5146
5743
|
* @internal
|
|
5147
5744
|
*/
|
|
5148
|
-
function worksheetCsv(sheet, options,
|
|
5149
|
-
const f = formatterOptions(options.formatterOptions);
|
|
5745
|
+
async function worksheetCsv(sheet, options, encoding = "utf8") {
|
|
5150
5746
|
const injection = options.formulaInjection ?? "escape";
|
|
5151
5747
|
if (injection !== "escape" && injection !== "off") throw invalid("formulaInjection", "The option \"formulaInjection\" must be \"escape\" or \"off\".", "Use \"escape\" (default) or \"off\".");
|
|
5748
|
+
const f = formatterOptions(options.formatterOptions, injection === "escape");
|
|
5749
|
+
const latin1 = encoding === "latin1";
|
|
5750
|
+
const sheetName = sheet?.name ?? "";
|
|
5152
5751
|
if (latin1) {
|
|
5153
5752
|
for (const [name, value] of [
|
|
5154
5753
|
["delimiter", f.delimiter],
|
|
@@ -5159,6 +5758,10 @@ function worksheetCsv(sheet, options, latin1 = false) {
|
|
|
5159
5758
|
}
|
|
5160
5759
|
if (injection === "escape") {
|
|
5161
5760
|
const off = "or pass formulaInjection: \"off\" for a trusted consumer.";
|
|
5761
|
+
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}`);
|
|
5762
|
+
if (encoding !== "utf8") {
|
|
5763
|
+
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}`);
|
|
5764
|
+
}
|
|
5162
5765
|
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
5766
|
if (![
|
|
5164
5767
|
"\n",
|
|
@@ -5170,8 +5773,34 @@ function worksheetCsv(sheet, options, latin1 = false) {
|
|
|
5170
5773
|
}
|
|
5171
5774
|
/** The text latin1 will write: characters above U+00FF as "?". */
|
|
5172
5775
|
const narrow = (out) => latin1 && typeof out === "string" ? out.replace(/[^\u0000-\u00ff]/g, "?") : out;
|
|
5173
|
-
|
|
5174
|
-
const
|
|
5776
|
+
const utf16 = encoding === "utf16le" && injection === "escape";
|
|
5777
|
+
const marked = injection === "escape" && (utf16 || latin1 && f.writeBOM);
|
|
5778
|
+
/** A quote may be taken differently by a reader of the 8-bit text from here on (K3). */
|
|
5779
|
+
let quoteShifted = false;
|
|
5780
|
+
const unsafeField = (index, row, reason, hint) => {
|
|
5781
|
+
const cell = `${columnName(index + 1)}${String(row)}`;
|
|
5782
|
+
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}`, {
|
|
5783
|
+
location: row === 0 ? { sheet: sheetName } : {
|
|
5784
|
+
sheet: sheetName,
|
|
5785
|
+
cell
|
|
5786
|
+
},
|
|
5787
|
+
hint,
|
|
5788
|
+
details: {
|
|
5789
|
+
option: "encoding",
|
|
5790
|
+
field: index + 1,
|
|
5791
|
+
row
|
|
5792
|
+
}
|
|
5793
|
+
});
|
|
5794
|
+
};
|
|
5795
|
+
/**
|
|
5796
|
+
* A formatted field, refused if a standard reader would take its leading quote as quoting, or
|
|
5797
|
+
* if a reader of the output as 8-bit text that drops NUL bytes (LibreOffice) would split it
|
|
5798
|
+
* before a formula start: in utf16le output, when characters above U+00FF give a delimiter,
|
|
5799
|
+
* line break or quote there, when an earlier quote of that kind may shift the quoting, or for
|
|
5800
|
+
* the first field after a mark that reader keeps (K3). `row` is the row number, 0 for the header
|
|
5801
|
+
* line; `atStart`: the field is the first one of the file.
|
|
5802
|
+
*/
|
|
5803
|
+
const field = (out, index, force, row, atStart) => {
|
|
5175
5804
|
const text = formatField(out, index, f, force);
|
|
5176
5805
|
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
5806
|
hint: "Leave formatterOptions.quote at its default (\"), or pass formulaInjection: \"off\" for a trusted consumer.",
|
|
@@ -5180,43 +5809,114 @@ function worksheetCsv(sheet, options, latin1 = false) {
|
|
|
5180
5809
|
field: index + 1
|
|
5181
5810
|
}
|
|
5182
5811
|
});
|
|
5812
|
+
const first = marked && atStart;
|
|
5813
|
+
if (first || quoteShifted || utf16 && /[^\u0000-\u00ff]/.test(text)) {
|
|
5814
|
+
const { view, foreignBreak, foreignQuote } = utf16 ? utf16View(text, f.delimiter) : {
|
|
5815
|
+
view: text,
|
|
5816
|
+
foreignBreak: false,
|
|
5817
|
+
foreignQuote: false
|
|
5818
|
+
};
|
|
5819
|
+
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.");
|
|
5820
|
+
if (foreignQuote || first && ([...view].some((c) => BYTE_BREAKS.has(c.charCodeAt(0))) || view.includes(f.delimiter))) quoteShifted = true;
|
|
5821
|
+
}
|
|
5183
5822
|
return text;
|
|
5184
5823
|
};
|
|
5185
5824
|
const map = options.map ?? defaultWriteMap(options.dateFormat, options.dateUTC === true);
|
|
5186
5825
|
const includeEmptyRows = options.includeEmptyRows === void 0 || options.includeEmptyRows;
|
|
5187
5826
|
const lines = [];
|
|
5188
|
-
const escape = (raw) => {
|
|
5189
|
-
const out = narrow(raw);
|
|
5827
|
+
const escape = (raw, atStart) => {
|
|
5828
|
+
const out = narrow(raw === null || raw === void 0 || typeof raw === "number" || typeof raw === "boolean" || typeof raw === "bigint" || typeof raw === "string" ? raw : String(raw));
|
|
5190
5829
|
if (injection !== "escape" || typeof out !== "string") return [out, false];
|
|
5191
|
-
const
|
|
5192
|
-
|
|
5830
|
+
const text = out.replace(/\0/g, "");
|
|
5831
|
+
const first = text[0];
|
|
5832
|
+
if (first === void 0) return [out, false];
|
|
5833
|
+
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];
|
|
5834
|
+
};
|
|
5835
|
+
/** RowFormatter `headers`: given, or taken from the first row written (L109-124). */
|
|
5836
|
+
let headers = f.headers;
|
|
5837
|
+
/** FieldFormatter `_headers`, by which an object of quoteColumns or quoteHeaders is looked up. */
|
|
5838
|
+
let fieldHeaders = f.headers;
|
|
5839
|
+
let hasWrittenHeaders = false;
|
|
5840
|
+
const own = (o, key) => Object.prototype.hasOwnProperty.call(o, key);
|
|
5841
|
+
const shouldQuote = (index, isHeader) => {
|
|
5842
|
+
const config = isHeader ? f.quoteHeaders : f.quoteColumns;
|
|
5843
|
+
if (typeof config === "boolean") return config;
|
|
5844
|
+
if (Array.isArray(config)) return Boolean(config[index]);
|
|
5845
|
+
if (fieldHeaders !== null && config !== null && typeof config === "object") {
|
|
5846
|
+
const key = String(fieldHeaders[index]);
|
|
5847
|
+
return own(config, key) && Boolean(config[key]);
|
|
5848
|
+
}
|
|
5849
|
+
return false;
|
|
5850
|
+
};
|
|
5851
|
+
const formatColumns = (columns, isHeader, rowNumber) => {
|
|
5852
|
+
const fields = [];
|
|
5853
|
+
for (let i = 0; i < columns.length; i++) {
|
|
5854
|
+
if (!(i in columns)) {
|
|
5855
|
+
fields.push("");
|
|
5856
|
+
continue;
|
|
5857
|
+
}
|
|
5858
|
+
const atStart = lines.length === 0 && i === 0;
|
|
5859
|
+
const [out, force] = escape(columns[i], atStart);
|
|
5860
|
+
fields.push(field(out, i, force || shouldQuote(i, isHeader), isHeader ? 0 : rowNumber, atStart));
|
|
5861
|
+
}
|
|
5862
|
+
lines.push(fields.join(f.delimiter));
|
|
5863
|
+
};
|
|
5864
|
+
const isHashArray = (row) => Array.isArray(row) && Array.isArray(row[0]) && row[0].length === 2;
|
|
5865
|
+
const gatherHeaders = (row) => isHashArray(row) ? row.map((it) => it[0]) : Array.isArray(row) ? row : Object.keys(Object(row));
|
|
5866
|
+
const gatherColumns = (row, names) => {
|
|
5867
|
+
if (!Array.isArray(row)) {
|
|
5868
|
+
const o = Object(row);
|
|
5869
|
+
return names.map((h) => own(o, String(h)) ? o[String(h)] : void 0);
|
|
5870
|
+
}
|
|
5871
|
+
if (isHashArray(row)) return names.map((_h, i) => row[i] ? row[i][1] : "");
|
|
5872
|
+
if (!f.shouldWriteHeaders) return row;
|
|
5873
|
+
return names.map((_h, i) => row[i]);
|
|
5874
|
+
};
|
|
5875
|
+
const formatRow = (row, rowNumber) => {
|
|
5876
|
+
if (!row) return;
|
|
5877
|
+
let formatRowColumns = true;
|
|
5878
|
+
if (headers === null) {
|
|
5879
|
+
headers = gatherHeaders(row);
|
|
5880
|
+
fieldHeaders = headers;
|
|
5881
|
+
if (f.shouldWriteHeaders) formatRowColumns = !(Array.isArray(row) && !isHashArray(row));
|
|
5882
|
+
}
|
|
5883
|
+
if (f.shouldWriteHeaders && !hasWrittenHeaders) {
|
|
5884
|
+
formatColumns(headers, true, 0);
|
|
5885
|
+
hasWrittenHeaders = true;
|
|
5886
|
+
}
|
|
5887
|
+
if (formatRowColumns) formatColumns(gatherColumns(row, headers), false, rowNumber);
|
|
5888
|
+
};
|
|
5889
|
+
const transform = f.transform;
|
|
5890
|
+
const transformed = (row) => {
|
|
5891
|
+
if (!transform) return row;
|
|
5892
|
+
if (transform.length === 1) return transform(row);
|
|
5893
|
+
return new Promise((resolve, reject) => {
|
|
5894
|
+
transform(row, (error, out) => error ? reject(error instanceof Error ? error : new Error(String(error))) : resolve(out));
|
|
5895
|
+
});
|
|
5193
5896
|
};
|
|
5194
|
-
if (f.headers) lines.push(f.headers.map((h, i) => {
|
|
5195
|
-
const [out, force] = escape(h);
|
|
5196
|
-
return field(out, i, force);
|
|
5197
|
-
}).join(f.delimiter));
|
|
5198
5897
|
if (sheet) {
|
|
5199
|
-
|
|
5898
|
+
const rows = [];
|
|
5200
5899
|
sheet.eachRow((row, rowNumber) => {
|
|
5201
|
-
|
|
5202
|
-
|
|
5203
|
-
|
|
5204
|
-
|
|
5205
|
-
|
|
5206
|
-
|
|
5207
|
-
|
|
5208
|
-
continue;
|
|
5209
|
-
}
|
|
5210
|
-
const [out, force] = escape(map(values[i], i));
|
|
5211
|
-
fields.push(field(out, i, force));
|
|
5900
|
+
rows.push([rowNumber, row]);
|
|
5901
|
+
});
|
|
5902
|
+
let lastRow = f.shouldWriteHeaders && f.headers === null && rows.length > 0 ? rows[0][0] - 1 : 0;
|
|
5903
|
+
for (const [rowNumber, row] of rows) {
|
|
5904
|
+
if (includeEmptyRows) while (++lastRow < rowNumber) {
|
|
5905
|
+
const out = transformed([]);
|
|
5906
|
+
formatRow(out instanceof Promise ? await out : out, lastRow);
|
|
5212
5907
|
}
|
|
5213
|
-
|
|
5908
|
+
const out = transformed(row.values.slice(1).map((v, i) => map(v, i)));
|
|
5909
|
+
formatRow(out instanceof Promise ? await out : out, rowNumber);
|
|
5214
5910
|
lastRow = rowNumber;
|
|
5215
|
-
}
|
|
5911
|
+
}
|
|
5912
|
+
}
|
|
5913
|
+
if (f.alwaysWriteHeaders && lines.length === 0) {
|
|
5914
|
+
if (!headers) throw invalid("formatterOptions.alwaysWriteHeaders", "alwaysWriteHeaders is set, but no header names are given in formatterOptions.headers.", "Pass formatterOptions.headers as an array of names, or leave alwaysWriteHeaders out.");
|
|
5915
|
+
formatColumns(headers, true, 0);
|
|
5216
5916
|
}
|
|
5217
5917
|
let text = lines.join(f.rowDelimiter);
|
|
5218
5918
|
if (f.includeEndRowDelimiter) text += f.rowDelimiter;
|
|
5219
|
-
return (f.writeBOM ? "" : "") + text;
|
|
5919
|
+
return (f.writeBOM || utf16 ? "" : "") + text;
|
|
5220
5920
|
}
|
|
5221
5921
|
function encode(text, encoding) {
|
|
5222
5922
|
const e = (encoding ?? "utf8").toLowerCase();
|
|
@@ -5237,9 +5937,22 @@ function encode(text, encoding) {
|
|
|
5237
5937
|
}
|
|
5238
5938
|
throw invalid("encoding", `The encoding "${encoding ?? ""}" is not supported.`, "Use \"utf8\" (default), \"utf16le\" or \"latin1\".");
|
|
5239
5939
|
}
|
|
5940
|
+
/**
|
|
5941
|
+
* Node.js's 'latin1' (alias 'binary'), which fast-csv's StringDecoder uses
|
|
5942
|
+
* (`@fast-csv/parse/build/src/CsvParserStream.js` L20): each byte is the code point of the same
|
|
5943
|
+
* value. The WHATWG label 'latin1' of `TextDecoder` is windows-1252 instead (bytes 80-9F become
|
|
5944
|
+
* other characters, for example 80 the euro sign), so it is not used (F10).
|
|
5945
|
+
*/
|
|
5946
|
+
const latin1Decoder = { decode(input) {
|
|
5947
|
+
if (!input) return "";
|
|
5948
|
+
let out = "";
|
|
5949
|
+
for (let i = 0; i < input.length; i += 8192) out += String.fromCharCode(...input.subarray(i, i + 8192));
|
|
5950
|
+
return out;
|
|
5951
|
+
} };
|
|
5240
5952
|
function decoderFor(encoding) {
|
|
5241
5953
|
const e = encoding.toLowerCase();
|
|
5242
|
-
|
|
5954
|
+
if (e === "latin1" || e === "binary") return latin1Decoder;
|
|
5955
|
+
const label = e === "utf8" ? "utf-8" : e === "ucs2" || e === "ucs-2" || e === "utf16le" ? "utf-16le" : e;
|
|
5243
5956
|
try {
|
|
5244
5957
|
return new TextDecoder(label);
|
|
5245
5958
|
} catch {
|
|
@@ -5358,10 +6071,16 @@ var CsvIO = class {
|
|
|
5358
6071
|
async read(stream, options = {}) {
|
|
5359
6072
|
const worksheet = this.workbook.addWorksheet(options.sheetName);
|
|
5360
6073
|
const parser = parserOptions(options.parserOptions);
|
|
5361
|
-
const
|
|
6074
|
+
const text = await readText(stream, parser.encoding);
|
|
5362
6075
|
const formats = options.dateFormats ?? DEFAULT_DATE_FORMATS;
|
|
5363
6076
|
const map = options.map ?? ((datum) => csvValue(datum, formats));
|
|
5364
|
-
|
|
6077
|
+
if (text.length >= MAX_CSV_CELLS && fieldEnds(text, parser.delimiter) >= MAX_CSV_CELLS) try {
|
|
6078
|
+
const rows = csvRows(text, parser);
|
|
6079
|
+
while (rows.next().done !== true);
|
|
6080
|
+
} catch (error) {
|
|
6081
|
+
if (error instanceof XlvError && error.code === "XLV_E_LIMIT_EXCEEDED" && error.details?.limit === "maxCells") throw error;
|
|
6082
|
+
}
|
|
6083
|
+
for (const row of csvRows(text, parser)) worksheet.addRow(row.fields.map((datum, i) => map(datum, i)));
|
|
5365
6084
|
return worksheet;
|
|
5366
6085
|
}
|
|
5367
6086
|
/** Reads a CSV file (Node.js, Bun, Deno) into a new worksheet. */
|
|
@@ -5385,12 +6104,11 @@ var CsvIO = class {
|
|
|
5385
6104
|
}
|
|
5386
6105
|
/** Writes a worksheet as CSV to a stream and ends it. */
|
|
5387
6106
|
async write(stream, options = {}) {
|
|
5388
|
-
await writeTo(stream, encode(this.text(options), "utf8"));
|
|
6107
|
+
await writeTo(stream, encode(await this.text(options), "utf8"));
|
|
5389
6108
|
}
|
|
5390
6109
|
/** Writes a worksheet to a CSV file (Node.js, Bun, Deno), replacing it. */
|
|
5391
6110
|
async writeFile(path, options = {}) {
|
|
5392
|
-
const
|
|
5393
|
-
const bytes = encode(this.text(options, e === "latin1" || e === "binary"), options.encoding);
|
|
6111
|
+
const bytes = encode(await this.text(options, outputEncoding(options.encoding)), options.encoding);
|
|
5394
6112
|
const fs = await nodeFs("writeFile");
|
|
5395
6113
|
try {
|
|
5396
6114
|
await fs.writeFile(path, bytes);
|
|
@@ -5408,13 +6126,13 @@ var CsvIO = class {
|
|
|
5408
6126
|
}
|
|
5409
6127
|
/** The CSV of a worksheet as bytes (a Buffer in Node.js), UTF-8 encoded. */
|
|
5410
6128
|
async writeBuffer(options = {}) {
|
|
5411
|
-
return asBuffer$1(encode(this.text(options), "utf8"));
|
|
6129
|
+
return asBuffer$1(encode(await this.text(options), "utf8"));
|
|
5412
6130
|
}
|
|
5413
6131
|
/** Removed in exceljs 4 (it throws there too); use `read`. */
|
|
5414
6132
|
createInputStream() {
|
|
5415
6133
|
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
6134
|
}
|
|
5417
|
-
text(options,
|
|
6135
|
+
async text(options, encoding = "utf8") {
|
|
5418
6136
|
const key = options.sheetName || options.sheetId;
|
|
5419
6137
|
const sheet = this.workbook.getWorksheet(key);
|
|
5420
6138
|
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 +6140,7 @@ var CsvIO = class {
|
|
|
5422
6140
|
hint: `Available worksheets: ${this.workbook.worksheets.map((w) => `"${w.name}"`).join(", ") || "none"}.`,
|
|
5423
6141
|
details: { available: this.workbook.worksheets.map((w) => w.name) }
|
|
5424
6142
|
});
|
|
5425
|
-
return worksheetCsv(sheet, options,
|
|
6143
|
+
return worksheetCsv(sheet, options, encoding);
|
|
5426
6144
|
}
|
|
5427
6145
|
};
|
|
5428
6146
|
// src/defined-names.ts
|
|
@@ -5951,9 +6669,11 @@ var XlsxIO = class {
|
|
|
5951
6669
|
}
|
|
5952
6670
|
book(options) {
|
|
5953
6671
|
const useStyles = options?.useStyles !== false;
|
|
6672
|
+
const font = this.workbook._defaultFont;
|
|
5954
6673
|
return toNewWorkbook(this.workbook.worksheets, this.workbook._date1904, {
|
|
5955
6674
|
...this.workbook._meta(),
|
|
5956
|
-
useStyles
|
|
6675
|
+
useStyles,
|
|
6676
|
+
...font ? { defaultFont: font } : {}
|
|
5957
6677
|
});
|
|
5958
6678
|
}
|
|
5959
6679
|
bytes(options) {
|
|
@@ -6044,6 +6764,8 @@ var Workbook = class {
|
|
|
6044
6764
|
calcProperties: this.calcProperties
|
|
6045
6765
|
};
|
|
6046
6766
|
}
|
|
6767
|
+
/** @internal The default font (font 0) of the file this workbook was loaded from, written again (F4); `null` for a workbook made in code. */
|
|
6768
|
+
_defaultFont = null;
|
|
6047
6769
|
/** @internal */
|
|
6048
6770
|
get _date1904() {
|
|
6049
6771
|
return this.properties.date1904 === true;
|
|
@@ -6271,6 +6993,10 @@ var Workbook = class {
|
|
|
6271
6993
|
const use = (node) => !ignore.has(node);
|
|
6272
6994
|
let cellsRead = 0;
|
|
6273
6995
|
for (const s of read.sheets) cellsRead += s.cellCount;
|
|
6996
|
+
const dates = {
|
|
6997
|
+
date1904: read.dateSystem === "1904",
|
|
6998
|
+
strict: read.conformance === "strict"
|
|
6999
|
+
};
|
|
6274
7000
|
const budget = Math.max(DEFAULT_READ_LIMITS.maxCells, 16 * cellsRead);
|
|
6275
7001
|
let spent = 0;
|
|
6276
7002
|
const spend = (positions, what, sheet) => {
|
|
@@ -6286,6 +7012,23 @@ var Workbook = class {
|
|
|
6286
7012
|
}
|
|
6287
7013
|
});
|
|
6288
7014
|
};
|
|
7015
|
+
const mergeBudget = Math.max(MERGED_CELLS_BUDGET, cellsRead);
|
|
7016
|
+
let merged = 0;
|
|
7017
|
+
const spendMerge = (positions, ref, sheet) => {
|
|
7018
|
+
merged += positions;
|
|
7019
|
+
if (merged <= mergeBudget) return;
|
|
7020
|
+
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.`, {
|
|
7021
|
+
location: {
|
|
7022
|
+
sheet,
|
|
7023
|
+
cell: ref
|
|
7024
|
+
},
|
|
7025
|
+
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.",
|
|
7026
|
+
details: {
|
|
7027
|
+
limit: "mergedCells",
|
|
7028
|
+
value: mergeBudget
|
|
7029
|
+
}
|
|
7030
|
+
});
|
|
7031
|
+
};
|
|
6289
7032
|
const rowBudget = Math.max(67108864, 16 * cellsRead);
|
|
6290
7033
|
const validations = validationBudget();
|
|
6291
7034
|
let rowsLooked = 0;
|
|
@@ -6310,6 +7053,27 @@ var Workbook = class {
|
|
|
6310
7053
|
}
|
|
6311
7054
|
}
|
|
6312
7055
|
this._clearSheets();
|
|
7056
|
+
const font0 = writableDefaultFont(read.defaultFont);
|
|
7057
|
+
this._defaultFont = font0;
|
|
7058
|
+
const fileStyle = (style, numFmt, styled = Object.keys(style).length > 0) => styleOut(styled && font0 && !style.font ? {
|
|
7059
|
+
...style,
|
|
7060
|
+
font: font0
|
|
7061
|
+
} : style, numFmt);
|
|
7062
|
+
const lineFormat = (style) => typeof style.numFmt === "string" ? style.numFmt : null;
|
|
7063
|
+
let xf0;
|
|
7064
|
+
const defaultXf = () => {
|
|
7065
|
+
if (xf0 !== void 0) return xf0;
|
|
7066
|
+
xf0 = null;
|
|
7067
|
+
for (const sheet of read.sheets) {
|
|
7068
|
+
if (sheet.kind !== "worksheet") continue;
|
|
7069
|
+
for (const cell of sheet.cells()) if (cell.styleIndex === 0) return xf0 = cell.style;
|
|
7070
|
+
}
|
|
7071
|
+
return xf0;
|
|
7072
|
+
};
|
|
7073
|
+
const lineStyle = (style) => {
|
|
7074
|
+
const zero = defaultXf();
|
|
7075
|
+
return zero && sameStyleData(style, zero) ? {} : fileStyle(style, lineFormat(style));
|
|
7076
|
+
};
|
|
6313
7077
|
if (read.dateSystem === "1904") this.properties.date1904 = true;
|
|
6314
7078
|
else delete this.properties.date1904;
|
|
6315
7079
|
const p = read.properties;
|
|
@@ -6371,7 +7135,7 @@ var Workbook = class {
|
|
|
6371
7135
|
if (c.width !== null) column.width = c.width;
|
|
6372
7136
|
if (c.hidden) column.hidden = true;
|
|
6373
7137
|
if (c.outlineLevel) column.outlineLevel = c.outlineLevel;
|
|
6374
|
-
if (c.style) column.style =
|
|
7138
|
+
if (c.style) column.style = lineStyle(c.style);
|
|
6375
7139
|
}
|
|
6376
7140
|
}
|
|
6377
7141
|
if (use("sheetFormatPr") || use("sheetPr")) {
|
|
@@ -6391,14 +7155,14 @@ var Workbook = class {
|
|
|
6391
7155
|
}
|
|
6392
7156
|
let lastRow = 0;
|
|
6393
7157
|
for (const cell of use("sheetData") ? s.styledCells() : []) {
|
|
6394
|
-
ws.getCell(cell.row, cell.col).style =
|
|
7158
|
+
ws.getCell(cell.row, cell.col).style = fileStyle(cell.style, cell.numFmt, cell.styleIndex > 0);
|
|
6395
7159
|
if (cell.row > lastRow) lastRow = cell.row;
|
|
6396
7160
|
}
|
|
6397
7161
|
for (const cell of use("sheetData") ? s.cells() : []) {
|
|
6398
7162
|
const target = ws.getCell(cell.row, cell.col);
|
|
6399
7163
|
let value;
|
|
6400
|
-
const plain = cell.type === "error" ? { error: cell.value } : cell.value;
|
|
6401
|
-
if (cell.formula && cell.formula
|
|
7164
|
+
const plain = cell.type === "error" ? { error: cell.value } : readValueOut(cell.value, cell.numFmt, dates);
|
|
7165
|
+
if (cell.formula && hasFormulaText(cell.formula)) value = formulaOut(cell.formula, plain, cell.ref, masters);
|
|
6402
7166
|
else value = cell.richText ? richTextOut(cell.richText) : plain;
|
|
6403
7167
|
const h = cell.hyperlink;
|
|
6404
7168
|
if (h && use("hyperlinks") && (h.target !== null || h.location !== null && h.location !== "")) {
|
|
@@ -6414,7 +7178,7 @@ var Workbook = class {
|
|
|
6414
7178
|
};
|
|
6415
7179
|
}
|
|
6416
7180
|
target.value = value;
|
|
6417
|
-
target.style =
|
|
7181
|
+
target.style = fileStyle(cell.style, cell.numFmt, cell.styleIndex > 0);
|
|
6418
7182
|
if (cell.row > lastRow) lastRow = cell.row;
|
|
6419
7183
|
}
|
|
6420
7184
|
const dim = /(\d+)$/.exec(s.dimension ?? "");
|
|
@@ -6432,6 +7196,7 @@ var Workbook = class {
|
|
|
6432
7196
|
const info = s.rowInfo(r);
|
|
6433
7197
|
if (!info) continue;
|
|
6434
7198
|
const row = ws.getRow(r);
|
|
7199
|
+
if (info.style && use("sheetData")) row.style = lineStyle(info.style);
|
|
6435
7200
|
if (info.height !== null) row.height = Math.min(info.height, 409.5);
|
|
6436
7201
|
if (info.hidden) row.hidden = true;
|
|
6437
7202
|
if (info.outlineLevel) row.outlineLevel = info.outlineLevel;
|
|
@@ -6440,8 +7205,9 @@ var Workbook = class {
|
|
|
6440
7205
|
for (const m of use("mergeCells") ? s.merges : []) {
|
|
6441
7206
|
if (!m.includes(":")) continue;
|
|
6442
7207
|
spend(areaOf(m), "A merged range", s.name);
|
|
7208
|
+
spendMerge(areaOf(m), m, s.name);
|
|
6443
7209
|
try {
|
|
6444
|
-
ws.
|
|
7210
|
+
ws.mergeCellsWithoutStyle(m);
|
|
6445
7211
|
} catch (error) {
|
|
6446
7212
|
if (error instanceof XlvError || !(error instanceof Error) || error.message !== "Cannot merge already merged cells") throw error;
|
|
6447
7213
|
const other = mergeOverlapping(ws, m);
|
|
@@ -6483,6 +7249,8 @@ var Workbook = class {
|
|
|
6483
7249
|
}
|
|
6484
7250
|
}
|
|
6485
7251
|
};
|
|
7252
|
+
/** Merged cells `xlsx.load` creates at most for a file with fewer cells (one whole column; K2). */
|
|
7253
|
+
const MERGED_CELLS_BUDGET = 1048576;
|
|
6486
7254
|
/**
|
|
6487
7255
|
* @internal The chunks of an input stream, read until it ends. Reading stops with
|
|
6488
7256
|
* XLV_E_LIMIT_EXCEEDED as soon as the chunks pass maxInputBytes, the size the core reads at most,
|
|
@@ -6571,7 +7339,7 @@ function streamValueIn(cell, masters) {
|
|
|
6571
7339
|
const v = cell.value;
|
|
6572
7340
|
if (v !== null && typeof v === "object" && !Array.isArray(v) && !(v instanceof Date)) {
|
|
6573
7341
|
const r = v;
|
|
6574
|
-
if (typeof r["
|
|
7342
|
+
if (typeof r["ref"] === "string" && r["shareType"] !== "array" && typeof r["formula"] === "string") {
|
|
6575
7343
|
masters.set(cell.address, {
|
|
6576
7344
|
formula: r["formula"],
|
|
6577
7345
|
row: cell.row,
|
|
@@ -6805,8 +7573,10 @@ var WorksheetWriter = class extends Worksheet {
|
|
|
6805
7573
|
if (styles[i] !== void 0) styled = true;
|
|
6806
7574
|
});
|
|
6807
7575
|
for (let i = 0; i < values.length; i++) if (values[i] === void 0) values[i] = null;
|
|
7576
|
+
const rowStyle = rowStyleIn(row, this.writer._useStyles);
|
|
6808
7577
|
this.rowsOut.push({
|
|
6809
7578
|
values,
|
|
7579
|
+
...rowStyle ? { rowStyle } : {},
|
|
6810
7580
|
...styled ? { styles } : {},
|
|
6811
7581
|
...links.length > 0 ? { hyperlinks: links } : {},
|
|
6812
7582
|
...notes.length > 0 ? { notes } : {},
|
|
@@ -7178,6 +7948,44 @@ var WorksheetReader = class extends Emitter {
|
|
|
7178
7948
|
}
|
|
7179
7949
|
};
|
|
7180
7950
|
/**
|
|
7951
|
+
* The value of a shared-formula dependent read by the streaming reader: `{ sharedFormula: master,
|
|
7952
|
+
* result }`, as exceljs's xlsx.load gives it, with a non-enumerable `formula` that moves the
|
|
7953
|
+
* master's text (`masters`, by address) to `row`/`col` when it is first read. The master's row is
|
|
7954
|
+
* not kept, so a sheet the value is copied into (stream.xlsx.WorkbookWriter, whose committed rows
|
|
7955
|
+
* are dropped) can write this formula instead of looking the master up (K3); reading the rows
|
|
7956
|
+
* alone builds no moved texts (K2). Setting `formula` replaces it with an ordinary property. A move
|
|
7957
|
+
* counts against the shared-formula budget of `workbook`, as `cell.formula` does (K4 S1).
|
|
7958
|
+
*/
|
|
7959
|
+
function sharedDependent(master, result, masters, row, col, workbook, sheet) {
|
|
7960
|
+
const value = {
|
|
7961
|
+
sharedFormula: master,
|
|
7962
|
+
...result
|
|
7963
|
+
};
|
|
7964
|
+
let moved;
|
|
7965
|
+
let built = false;
|
|
7966
|
+
Object.defineProperty(value, "formula", {
|
|
7967
|
+
configurable: true,
|
|
7968
|
+
enumerable: false,
|
|
7969
|
+
get() {
|
|
7970
|
+
if (!built) {
|
|
7971
|
+
const text = masters.get(master);
|
|
7972
|
+
moved = text === void 0 ? void 0 : moveSharedFormula(workbook, sheet, text, decodeAddress(master), row, col);
|
|
7973
|
+
built = true;
|
|
7974
|
+
}
|
|
7975
|
+
return moved;
|
|
7976
|
+
},
|
|
7977
|
+
set(formula) {
|
|
7978
|
+
Object.defineProperty(this, "formula", {
|
|
7979
|
+
value: formula,
|
|
7980
|
+
writable: true,
|
|
7981
|
+
enumerable: true,
|
|
7982
|
+
configurable: true
|
|
7983
|
+
});
|
|
7984
|
+
}
|
|
7985
|
+
});
|
|
7986
|
+
return value;
|
|
7987
|
+
}
|
|
7988
|
+
/**
|
|
7181
7989
|
* The streaming reader of exceljs (`ExcelJS.stream.xlsx.WorkbookReader`): iterate worksheets
|
|
7182
7990
|
* and their rows with `for await`, or call `read()` and listen to `worksheet`, `end`,
|
|
7183
7991
|
* `finished` and `error` events (and `entry` with `entries: 'emit'`). Rows are not kept.
|
|
@@ -7213,6 +8021,10 @@ var WorkbookReader = class extends Emitter {
|
|
|
7213
8021
|
async *parse(input, options) {
|
|
7214
8022
|
if (options) this.options = readerOptions(options);
|
|
7215
8023
|
const book = await this.open(input ?? this.input);
|
|
8024
|
+
const dates = {
|
|
8025
|
+
date1904: book.dateSystem === "1904",
|
|
8026
|
+
strict: book.conformance === "strict"
|
|
8027
|
+
};
|
|
7216
8028
|
if (this.options.entries === "emit") this.emit("entry", { type: "workbook" });
|
|
7217
8029
|
if (this.options.worksheets === "ignore") return;
|
|
7218
8030
|
const host = new Workbook();
|
|
@@ -7227,16 +8039,32 @@ var WorkbookReader = class extends Emitter {
|
|
|
7227
8039
|
});
|
|
7228
8040
|
const sheet = host._addReadWorksheet(host.nextId, info.name, { state: info.state });
|
|
7229
8041
|
const name = info.name;
|
|
8042
|
+
const masters = /* @__PURE__ */ new Map();
|
|
8043
|
+
const kept = /* @__PURE__ */ new Map();
|
|
8044
|
+
sheet._sharedMasters = kept;
|
|
7230
8045
|
const rows = async function* () {
|
|
7231
8046
|
for await (const r of book.rows(name)) {
|
|
7232
8047
|
const row = sheet.getRow(r.number);
|
|
8048
|
+
if (styles && r.style) row.style = styleOut(r.style, typeof r.style.numFmt === "string" ? r.style.numFmt : null);
|
|
7233
8049
|
for (const cell of r.cells) {
|
|
7234
|
-
const plain = cell.type === "error" ? { error: cell.value } : cell.value;
|
|
8050
|
+
const plain = cell.type === "error" ? { error: cell.value } : readValueOut(cell.value, cell.numFmt, dates, !cell.formula);
|
|
7235
8051
|
const target = row.getCell(cell.col);
|
|
7236
|
-
|
|
7237
|
-
|
|
7238
|
-
|
|
7239
|
-
|
|
8052
|
+
const f = cell.formula;
|
|
8053
|
+
const result = plain === null ? {} : { result: plain };
|
|
8054
|
+
let value;
|
|
8055
|
+
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);
|
|
8056
|
+
else if (f && hasFormulaText(f)) {
|
|
8057
|
+
const text = f.text ?? "";
|
|
8058
|
+
if (f.kind === "shared" && f.sharedIndex !== null && f.ref !== null) {
|
|
8059
|
+
masters.set(f.sharedIndex, cell.ref);
|
|
8060
|
+
kept.set(cell.ref, text);
|
|
8061
|
+
}
|
|
8062
|
+
value = {
|
|
8063
|
+
formula: text,
|
|
8064
|
+
...result
|
|
8065
|
+
};
|
|
8066
|
+
} else value = cell.richText ? richTextOut(cell.richText) : plain;
|
|
8067
|
+
target.value = value;
|
|
7240
8068
|
if (styles) target.style = styleOut(cell.style, cell.numFmt);
|
|
7241
8069
|
}
|
|
7242
8070
|
for (const cell of r.styledCells) {
|