@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.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 = styleIn(s);
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
- if (row.height !== void 0 || row.hidden || row.outlineLevel) sheet.row(row.number, {
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) at(ws.name, cell.address, () => writeCell(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 = useStyles ? cellStyleIn(cell, styleOf(cell.style)) : isDateCell(cell) ? { numFmt: 14 } : {};
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["formula"] || value["sharedFormula"]) return ValueType.Formula;
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
- const column = _row.worksheet.findColumn(_col);
2959
- this.style = {
2960
- ...column?.style,
2961
- ...withoutEmpty(_row.style)
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
- if (typeof v["sharedFormula"] === "string") {
3040
- const masterRef = v["sharedFormula"].replace(/\$/g, "");
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
- const text = master?.formula;
3043
- if (!master || text === void 0 || master === this) return void 0;
3044
- return shiftFormula(text, this.row - master.row, this.col - master.col) ?? void 0;
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
- /** `FormulaType.Shared` for a `{ sharedFormula }` value, `Master` for other formulas, otherwise `None` (lib/doc/cell.js L807). */
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 FormulaType.None;
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 : FormulaType.None;
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
- const cell = includeEmpty ? row.getCell(this.number) : row.findCell(this.number);
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, except those for which `takeMasterStyle` returns true (cells inserted inside the range),
4183
- * which get a copy of the master's style; style objects are never shared.
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 = { ...master.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)?._unmerge();
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
- /** Splits CSV text into rows of fields as fast-csv 4.3 does with the given parser options. */
4989
- function parseCsv(text, o) {
4990
- const rows = [];
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
- fields.push(value);
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
- fields.push(text.slice(at, i));
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() === "" ? [""] : fields;
5054
- rows.push(row.map((f) => o.trim ? f.trim() : o.ltrim ? f.trimStart() : o.rtrim ? f.trimEnd() : f));
5055
- }
5056
- let out = rows;
5057
- if (o.ignoreEmpty) out = out.filter((r) => r.some((f) => f.trim() !== ""));
5058
- if (o.skipHeader) out = out.slice(1);
5059
- if (o.skipRows > 0) out = out.slice(o.skipRows);
5060
- if (o.maxRows > 0) out = out.slice(0, o.maxRows);
5061
- return out;
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: o["quoteColumns"] ?? false,
5095
- headers: Array.isArray(o["headers"]) && o["writeHeaders"] !== false ? o["headers"].map(String) : null,
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
- /** One field as fast-csv FieldFormatter.format writes it. */
5102
- function formatField(value, index, f, forceQuote) {
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["formula"] || v["sharedFormula"] || "result" in 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, latin1 = false) {
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
- /** A formatted field, refused if a standard reader would take its leading quote as quoting. */
5174
- const field = (out, index, force) => {
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 first = out.replace(/\0/g, "")[0];
5192
- return first !== void 0 && (INJECTION_START.has(first) || first.charCodeAt(0) < 32) ? [`'${out}`, f.quote !== ""] : [out, false];
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
- let lastRow = 0;
5898
+ const rows = [];
5200
5899
  sheet.eachRow((row, rowNumber) => {
5201
- if (includeEmptyRows) while (++lastRow < rowNumber) lines.push("");
5202
- let values = row.values.slice(1);
5203
- if (f.headers) values = f.headers.map((_h, i) => values[i]);
5204
- const fields = [];
5205
- for (let i = 0; i < values.length; i++) {
5206
- if (!(i in values)) {
5207
- fields.push("");
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
- lines.push(fields.join(f.delimiter));
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
- const label = e === "utf8" ? "utf-8" : e === "ucs2" || e === "ucs-2" || e === "utf16le" ? "utf-16le" : e === "binary" ? "latin1" : e;
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 rows = parseCsv(await readText(stream, parser.encoding), parser);
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
- for (const row of rows) worksheet.addRow(row.map((datum, i) => map(datum, i)));
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 e = (options.encoding ?? "").toLowerCase();
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, latin1 = false) {
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, latin1);
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 = styleOut(c.style, typeof c.style.numFmt === "string" ? c.style.numFmt : null);
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 = styleOut(cell.style, cell.numFmt);
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.text !== null) value = formulaOut(cell.formula, plain, cell.ref, masters);
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 = styleOut(cell.style, cell.numFmt);
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.mergeCells(m);
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["formula"] === "string" && r["shareType"] !== "array" && typeof r["ref"] === "string") {
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
- target.value = cell.formula && cell.formula.text !== null ? plain === null ? { formula: cell.formula.text } : {
7237
- formula: cell.formula.text,
7238
- result: plain
7239
- } : cell.richText ? richTextOut(cell.richText) : plain;
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) {