xlvane 1.1.0 → 1.1.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/README.md CHANGED
@@ -385,10 +385,10 @@ Limits can be changed per call, for example `readWorkbook(bytes, { limits: { max
385
385
  | `maxXmlDepth` | 128 | deeply nested XML |
386
386
  | `maxXmlAttributes` | 256 | elements with huge numbers of attributes |
387
387
  | `maxXmlNameLength` | 1 024 | huge element or attribute names |
388
- | `maxXmlTextLength` | 8 Mi characters | huge text nodes or attribute values |
388
+ | `maxXmlTextLength` | 8 Mi characters | huge text nodes, attribute values, comments or processing instructions |
389
389
  | `maxXmlTreeElements` | 1 000 000 | huge workbook, styles or relationship parts (elements plus attributes) |
390
- | `maxCells` | 5 000 000 | workbooks too large to hold in memory (measured: about 180 bytes per numeric cell, Node.js 26) |
391
- | `maxSharedStringChars` | 512 Mi characters | many cells pointing at one huge shared string (a few MB that become many GB when values are copied) |
390
+ | `maxCells` | 5 000 000 | workbooks too large to hold in memory (measured: about 180 bytes per numeric cell, Node.js 26); also bounds the output of `toCSV`, `readTable`, `toJSON` and `readTableStream` to `max(maxCells, 16 × cells read)` positions |
391
+ | `maxSharedStringChars` | 512 Mi characters | many cells pointing at one huge shared string (a few MB that become many GB when values are copied); counted separately, the same for cells sharing one huge formula |
392
392
 
393
393
  ## Package size
394
394
 
@@ -403,6 +403,7 @@ Limits can be changed per call, for example `readWorkbook(bytes, { limits: { max
403
403
 
404
404
  ## Links
405
405
 
406
+ - Website with the guide and an in-browser demo: [xlvane.github.io](https://xlvane.github.io)
406
407
  - Changes in each version: [CHANGELOG.md](https://github.com/DevXord/xlvane/blob/main/CHANGELOG.md)
407
408
  - Bugs and questions: [GitHub issues](https://github.com/DevXord/xlvane/issues)
408
409
  - Reporting a vulnerability: [SECURITY.md](https://github.com/DevXord/xlvane/blob/main/SECURITY.md)
package/dist/cli.js CHANGED
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
- import { Bt as suggest, En as XlvError, _ as isoDate, b as readWorkbook, g as inspect, ln as parseRange } from "./options-CwSp2pA4.js";
3
- import { a as appendRows, c as fromCSV, d as fromJSON, f as pickSheet, h as toJSON, l as inferValue, n as insertRows, o as editCells, p as readTable, s as diff, t as deleteRows, u as toCSV } from "./rows-DKwlr0rg.js";
4
- import { i as readInput } from "./node-files-B0iqSJIJ.js";
2
+ import { On as XlvError, Vt as suggest, _ as isoDate, b as readWorkbook, dn as parseRange, g as inspect } from "./options-Ba6r4zP1.js";
3
+ import { a as appendRows, c as fromCSV, d as fromJSON, f as pickSheet, h as toJSON, l as inferValue, n as insertRows, o as editCells, p as readTable, s as diff, t as deleteRows, u as toCSV } from "./rows-Biu_ddwz.js";
4
+ import { i as readInput } from "./node-files-CsQVpKgP.js";
5
5
  import { existsSync, readFileSync } from "node:fs";
6
- import { chmod, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
6
+ import { chmod, lstat, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
7
7
  import process from "node:process";
8
8
  import { createRequire } from "node:module";
9
9
  import { basename, dirname, extname, isAbsolute, join, relative, resolve } from "node:path";
@@ -463,8 +463,15 @@ var Output = class {
463
463
  this.color = color;
464
464
  this.width = width;
465
465
  }
466
+ /**
467
+ * Colour codes are written with {@link STYLE_MARK} in place of ESC, and an ESC already in the
468
+ * text (from a file) is escaped, so that the final filter ({@link terminalSafe}) can tell the
469
+ * CLI's own colour codes from the same bytes in a sheet name or cell (D-H01).
470
+ */
466
471
  style(format, text) {
467
- return this.color && text !== "" ? nodeUtil.styleText(format, text, { validateStream: false }) : text;
472
+ if (!this.color || text === "") return text;
473
+ const safe = text.includes("\x1B") ? text.split("\x1B").join("\\x1b") : text;
474
+ return nodeUtil.styleText(format, safe, { validateStream: false }).split("\x1B").join(STYLE_MARK);
468
475
  }
469
476
  bold(text) {
470
477
  return this.style("bold", text);
@@ -519,7 +526,8 @@ function oneLine(text) {
519
526
  * which includes the 8-bit CSI U+009B) written as `\x1b`, and bidirectional formatting characters
520
527
  * written as `\u202e`, so text from a file, such as a cell
521
528
  * holding `_x001B_]0;title_x0007_`, cannot move the cursor, change the window title or hide
522
- * output (docs/decisions.md D-R02). Used for all human-readable output; `--json` is unchanged.
529
+ * output (docs/decisions.md D-R02). Used for all human-readable output; `--json` writes the same
530
+ * characters as `\uXXXX` escapes instead (D-H01).
523
531
  */
524
532
  function escapeControls(text) {
525
533
  return text.replace(/[\u0000-\u0008\u000b-\u001f\u007f-\u009f]/g, (c) => `\\x${c.charCodeAt(0).toString(16).padStart(2, "0")}`).replace(BIDI, (c) => `\\u${c.charCodeAt(0).toString(16).padStart(4, "0")}`);
@@ -531,27 +539,40 @@ function escapeControls(text) {
531
539
  * `\u202e` in human-readable output, like other control characters (D-R08).
532
540
  */
533
541
  const BIDI = /[\u061c\u200e\u200f\u202a-\u202e\u2066-\u2069]/g;
534
- /** The colour sequences `Output` writes (styleText: bold, dim, red, green, yellow, cyan and their resets). */
535
- const OWN_STYLES = /\u001b\[(?:1|2|22|31|32|33|36|39)m/y;
542
+ /**
543
+ * Stands for ESC in the colour codes `Output` writes until {@link terminalSafe} turns it back:
544
+ * NUL and 16 random hexadecimal digits, new for each process, so text from a file cannot contain
545
+ * it by chance or design. Before 1.1.1 the filter kept every ESC sequence that looked like one of
546
+ * the CLI's colours, including ones from a sheet name (D-H01).
547
+ */
548
+ const STYLE_MARK = `\u0000${Array.from(globalThis.crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(8)), (b) => b.toString(16).padStart(2, "0")).join("")}`;
549
+ /** The colour sequences `Output` writes (styleText: bold, dim, red, green, yellow, cyan and their resets), after the mark. */
550
+ const OWN_STYLE = /^\[(?:1|2|22|31|32|33|36|39)m/;
551
+ const MARKED_STYLE = new RegExp(`${STYLE_MARK}\\[[0-9;]*m`, "g");
536
552
  /**
537
553
  * Final filter for human-readable output: escapes every control character (see
538
- * {@link escapeControls}) except line feeds, tabs and, when `keepStyles` is true, the exact
539
- * colour sequences that `Output` itself writes. Applied to everything the CLI prints for people.
554
+ * {@link escapeControls}) except line feeds and tabs. When `keepStyles` is true, the colour codes
555
+ * that `Output` wrote (marked with {@link STYLE_MARK}) are turned into ESC sequences; any ESC in
556
+ * the text itself is escaped. Applied to everything the CLI prints for people.
540
557
  */
541
558
  function terminalSafe(text, keepStyles) {
559
+ if (!text.includes(STYLE_MARK)) return controlsSafe(text);
560
+ const parts = text.split(STYLE_MARK);
561
+ let result = controlsSafe(parts[0] ?? "");
562
+ for (const part of parts.slice(1)) {
563
+ const own = OWN_STYLE.exec(part)?.[0];
564
+ if (own === void 0) result += controlsSafe(part);
565
+ else result += (keepStyles ? `\u001b${own}` : "") + controlsSafe(part.slice(own.length));
566
+ }
567
+ return result;
568
+ }
569
+ /** Escapes the control characters of text (see {@link escapeControls}) except line feeds and tabs. */
570
+ function controlsSafe(text) {
542
571
  let result = "";
543
572
  let start = 0;
544
573
  for (let i = 0; i < text.length; i++) {
545
574
  const code = text.charCodeAt(i);
546
575
  if (!(code < 32 && code !== 10 && code !== 9 || code >= 127 && code <= 159 || code === 1564 || code === 8206 || code === 8207 || code >= 8234 && code <= 8238 || code >= 8294 && code <= 8297)) continue;
547
- if (keepStyles && code === 27) {
548
- OWN_STYLES.lastIndex = i;
549
- const m = OWN_STYLES.exec(text);
550
- if (m !== null) {
551
- i += m[0].length - 1;
552
- continue;
553
- }
554
- }
555
576
  result += text.slice(start, i) + escapeControls(text[i]);
556
577
  start = i + 1;
557
578
  }
@@ -591,6 +612,7 @@ const segmenter = typeof Intl === "object" && typeof Intl.Segmenter === "functio
591
612
  */
592
613
  function displayWidth(text) {
593
614
  if (text.includes("\x1B")) text = text.replace(/\u001b\[[0-9;]*m/g, "");
615
+ if (text.includes(STYLE_MARK)) text = text.replace(MARKED_STYLE, "");
594
616
  let width = 0;
595
617
  for (let i = 0; i < text.length; i++) {
596
618
  const code = text.charCodeAt(i);
@@ -1334,7 +1356,7 @@ const DEFAULT_LIMIT$1 = 50;
1334
1356
  function showCell(c) {
1335
1357
  if (c === null) return "(empty)";
1336
1358
  const value = c.value === null ? "" : typeof c.value === "string" ? JSON.stringify(c.value) : String(c.value);
1337
- return c.formula !== null ? `=${c.formula}${value !== "" ? ` (${value})` : ""}` : value === "" ? "(no value)" : value;
1359
+ return c.formula !== null ? `=${escapeControls(c.formula)}${value !== "" ? ` (${value})` : ""}` : value === "" ? "(no value)" : value;
1338
1360
  }
1339
1361
  function kindLabel(out, kind) {
1340
1362
  const text = kind.padEnd(7);
@@ -1393,14 +1415,14 @@ const diffCommand = {
1393
1415
  result: r,
1394
1416
  exitCode: r.equal ? 0 : 1,
1395
1417
  render: (out) => {
1396
- if (r.equal) return `${out.green("No differences")} between ${a} and ${b}.`;
1418
+ if (r.equal) return `${out.green("No differences")} between ${escapeControls(a)} and ${escapeControls(b)}.`;
1397
1419
  const s = r.summary;
1398
- const lines = [`${out.bold(`${a} → ${b}`)}: ${plural(s.cellsChanged, "cell")} changed, ${String(s.cellsAdded)} added, ${String(s.cellsRemoved)} removed` + (s.sheetsAdded + s.sheetsRemoved > 0 ? `; sheets added: ${String(s.sheetsAdded)}, removed: ${String(s.sheetsRemoved)}` : "")];
1399
- for (const name of r.sheetsAdded) lines.push(`${out.green("added sheet")} "${name}"`);
1400
- for (const name of r.sheetsRemoved) lines.push(`${out.red("removed sheet")} "${name}"`);
1420
+ const lines = [`${out.bold(`${escapeControls(a)} → ${escapeControls(b)}`)}: ${plural(s.cellsChanged, "cell")} changed, ${String(s.cellsAdded)} added, ${String(s.cellsRemoved)} removed` + (s.sheetsAdded + s.sheetsRemoved > 0 ? `; sheets added: ${String(s.sheetsAdded)}, removed: ${String(s.sheetsRemoved)}` : "")];
1421
+ for (const name of r.sheetsAdded) lines.push(`${out.green("added sheet")} "${escapeControls(name)}"`);
1422
+ for (const name of r.sheetsRemoved) lines.push(`${out.red("removed sheet")} "${escapeControls(name)}"`);
1401
1423
  for (const sheet of r.sheets) {
1402
1424
  if (sheet.cells.length === 0 && !sheet.state) continue;
1403
- lines.push("", out.bold(`Sheet "${sheet.name}"`));
1425
+ lines.push("", out.bold(`Sheet "${escapeControls(sheet.name)}"`));
1404
1426
  if (sheet.state) lines.push(` ${out.yellow("changed")} visibility: ${sheet.state.before} → ${sheet.state.after}`);
1405
1427
  for (const c of sheet.cells.slice(0, limit)) {
1406
1428
  const style = c.kind === "changed" && showCell(c.before) === showCell(c.after) ? " (formatting)" : "";
@@ -1757,17 +1779,17 @@ const inspectCommand = {
1757
1779
  }
1758
1780
  };
1759
1781
  function renderReport(out, file, r, verbose) {
1760
- const lines = [`${out.bold(file)}: ${r.format}, ${plural(r.sheetCount, "sheet")}, date system ${r.dateSystem}${r.conformance === "strict" ? ", Strict conformance" : ""}`];
1782
+ const lines = [`${out.bold(escapeControls(file))}: ${r.format}, ${plural(r.sheetCount, "sheet")}, date system ${r.dateSystem}${r.conformance === "strict" ? ", Strict conformance" : ""}`];
1761
1783
  if (r.hasMacros) lines.push("Contains macros (not run).");
1762
1784
  if (r.externalLinkCount > 0) lines.push(`Links to ${plural(r.externalLinkCount, "other workbook")} (not followed).`);
1763
1785
  const props = Object.entries(r.properties);
1764
1786
  if (props.length > 0) {
1765
1787
  const keyWidth = Math.max(...props.map(([k]) => k.length));
1766
- lines.push(out.bold("Properties:"), ...props.map(([k, v]) => ` ${k.padEnd(keyWidth)} ${v}`));
1788
+ lines.push(out.bold("Properties:"), ...props.map(([k, v]) => ` ${k.padEnd(keyWidth)} ${escapeControls(String(v))}`));
1767
1789
  }
1768
1790
  const names = verbose ? r.definedNames : r.definedNames.filter((n) => !(n.hidden && n.name.startsWith("_xlnm.")));
1769
1791
  if (names.length > 0) {
1770
- const shown = names.slice(0, 5).map((n) => `${n.name}${n.sheet === null ? "" : ` (sheet "${n.sheet}")`}${n.hidden ? " (hidden)" : ""} =${n.refersTo}`);
1792
+ const shown = names.slice(0, 5).map((n) => `${escapeControls(n.name)}${n.sheet === null ? "" : ` (sheet "${escapeControls(n.sheet)}")`}${n.hidden ? " (hidden)" : ""} =${escapeControls(n.refersTo)}`);
1771
1793
  const more = names.length > 5 || r.definedNamesTruncated ? `, … (${r.definedNamesTruncated ? "more than " : ""}${String(names.length)} in total; see --json)` : "";
1772
1794
  lines.push(`${out.bold("Defined names:")} ${shown.join(", ")}${more}`);
1773
1795
  }
@@ -1775,16 +1797,16 @@ function renderReport(out, file, r, verbose) {
1775
1797
  lines.push("");
1776
1798
  const state = s.state === "visible" ? "" : ` (${s.state})`;
1777
1799
  if (s.kind !== "worksheet") {
1778
- lines.push(`${out.bold(`Sheet "${s.name}"`)}${state}: ${s.kind}, no cells`);
1800
+ lines.push(`${out.bold(`Sheet "${escapeControls(s.name)}"`)}${state}: ${s.kind}, no cells`);
1779
1801
  continue;
1780
1802
  }
1781
1803
  if (s.usedRange === null) {
1782
- lines.push(`${out.bold(`Sheet "${s.name}"`)}${state}: empty`);
1804
+ lines.push(`${out.bold(`Sheet "${escapeControls(s.name)}"`)}${state}: empty`);
1783
1805
  continue;
1784
1806
  }
1785
- lines.push(`${out.bold(`Sheet "${s.name}"`)}${state}: ${s.usedRange}, ${plural(s.rowCount, "row")} × ${plural(s.columnCount, "column")}, ${plural(s.cellCount, "cell")}, ${plural(s.formulaCount, "formula")}`);
1807
+ lines.push(`${out.bold(`Sheet "${escapeControls(s.name)}"`)}${state}: ${s.usedRange}, ${plural(s.rowCount, "row")} × ${plural(s.columnCount, "column")}, ${plural(s.cellCount, "cell")}, ${plural(s.formulaCount, "formula")}`);
1786
1808
  lines.push(s.headerRow ? ` Header row: ${String(s.headerRow.row)} (${s.headerRow.confidence} confidence)` : " Header row: none detected");
1787
- if (s.mergedRangeCount > 0) lines.push(` Merged: ${s.mergedRanges.slice(0, 10).join(", ")}${s.mergedRangeCount > 10 ? `, … (${String(s.mergedRangeCount)} in total)` : ""}`);
1809
+ if (s.mergedRangeCount > 0) lines.push(` Merged: ${escapeControls(s.mergedRanges.slice(0, 10).join(", "))}${s.mergedRangeCount > 10 ? `, … (${String(s.mergedRangeCount)} in total)` : ""}`);
1788
1810
  if (s.columns.length > 0) {
1789
1811
  lines.push(out.table([
1790
1812
  "Column",
@@ -1799,23 +1821,23 @@ function renderReport(out, file, r, verbose) {
1799
1821
  ]), " "));
1800
1822
  if (s.columnsTruncated) lines.push(` … ${plural(s.columnCount - s.columns.length, "more column")}; use --max-columns ${String(s.columnCount)} to see them`);
1801
1823
  }
1802
- if (s.formulas.length > 0) lines.push(` Formulas: ${s.formulas.slice(0, 3).map((f) => `${f.cell} =${f.formula ?? "(shared)"}`).join(", ")}`);
1824
+ if (s.formulas.length > 0) lines.push(` Formulas: ${s.formulas.slice(0, 3).map((f) => `${f.cell} =${escapeControls(f.formula ?? "(shared)")}`).join(", ")}`);
1803
1825
  if (s.richTextCellCount > 0) lines.push(` Rich text: ${plural(s.richTextCellCount, "cell")} (${s.richTextCells.slice(0, 5).join(", ")}${s.richTextCellCount > 5 ? ", …" : ""})`);
1804
1826
  if (s.styledColumns.length > 0) lines.push(` Column styles: ${s.styledColumns.slice(0, 10).join(", ")}${s.styledColumns.length > 10 ? ", …" : ""}`);
1805
1827
  if (s.hyperlinkCount > 0) {
1806
- const shown = s.hyperlinks.slice(0, 3).map((l) => `${l.ref} → ${l.target ?? ""}${l.location === null ? "" : `${l.target === null ? "" : "#"}${l.location}`}`);
1828
+ const shown = s.hyperlinks.slice(0, 3).map((l) => `${l.ref} → ${escapeControls(l.target ?? "")}${l.location === null ? "" : `${l.target === null ? "" : "#"}${escapeControls(l.location)}`}`);
1807
1829
  lines.push(` Hyperlinks: ${plural(s.hyperlinkCount, "link")} (${shown.join(", ")}${s.hyperlinkCount > 3 ? ", …" : ""})`);
1808
1830
  }
1809
1831
  if (s.imageCount > 0) lines.push(` Images: ${plural(s.imageCount, "picture")} (${s.images.slice(0, 5).map((i) => `${i.cell ?? "absolute"} ${i.format ?? "other"}`).join(", ")}${s.imageCount > 5 ? ", …" : ""})`);
1810
1832
  if (s.noteCount > 0) lines.push(` Notes: ${plural(s.noteCount, "note")} (${s.notes.slice(0, 5).join(", ")}${s.noteCount > 5 ? ", …" : ""})`);
1811
- if (s.tables.length > 0) lines.push(` Tables: ${s.tables.slice(0, 5).map((t) => `${t.name} ${t.ref}`).join(", ")}${s.tables.length > 5 ? ", …" : ""}`);
1833
+ if (s.tables.length > 0) lines.push(` Tables: ${s.tables.slice(0, 5).map((table) => `${escapeControls(table.name)} ${escapeControls(table.ref)}`).join(", ")}${s.tables.length > 5 ? ", …" : ""}`);
1812
1834
  if (s.dataValidationCount > 0 || s.conditionalFormatCount > 0) {
1813
1835
  const parts = [s.dataValidationCount > 0 ? plural(s.dataValidationCount, "data validation") : null, s.conditionalFormatCount > 0 ? plural(s.conditionalFormatCount, "conditional format rule") : null];
1814
1836
  lines.push(` Rules: ${parts.filter((p) => p !== null).join(", ")}`);
1815
1837
  }
1816
1838
  if (s.protected) lines.push(" Protected: yes");
1817
1839
  if (s.printArea !== null || s.pageBreakCount > 0) {
1818
- const parts = [s.printArea === null ? null : `print area ${s.printArea.join(", ")}`, s.pageBreakCount > 0 ? plural(s.pageBreakCount, "page break") : null];
1840
+ const parts = [s.printArea === null ? null : `print area ${escapeControls(s.printArea.join(", "))}`, s.pageBreakCount > 0 ? plural(s.pageBreakCount, "page break") : null];
1819
1841
  lines.push(` Printing: ${parts.filter((p) => p !== null).join(", ")}`);
1820
1842
  }
1821
1843
  }
@@ -1866,10 +1888,11 @@ async function sourceFiles(root) {
1866
1888
  const path = join(dir, name);
1867
1889
  let info;
1868
1890
  try {
1869
- info = await stat(path);
1891
+ info = await lstat(path);
1870
1892
  } catch {
1871
1893
  continue;
1872
1894
  }
1895
+ if (info.isSymbolicLink()) continue;
1873
1896
  if (info.isDirectory()) await walk(path);
1874
1897
  else if (info.isFile() && SOURCE.test(name) && info.size <= MAX_FILE_BYTES) files.push(path);
1875
1898
  }
@@ -1898,6 +1921,75 @@ function scanSource(file, text) {
1898
1921
  });
1899
1922
  return findings;
1900
1923
  }
1924
+ /**
1925
+ * Offsets `[start, end)` of the string value (with its quotes) at `path` in valid JSON text, the
1926
+ * one JSON.parse reads when keys repeat, or `null`. Keys are compared after decoding their
1927
+ * escapes. Values off the path are skipped without recursion.
1928
+ */
1929
+ function stringValueSpan(text, path) {
1930
+ let i = 0;
1931
+ const space = () => {
1932
+ while (i < text.length && " \r\n".includes(text[i])) i++;
1933
+ };
1934
+ /** Skips a string starting at i (on its opening quote); returns its raw text. */
1935
+ const string = () => {
1936
+ const start = i++;
1937
+ while (text[i] !== "\"") i += text[i] === "\\" ? 2 : 1;
1938
+ i++;
1939
+ return text.slice(start, i);
1940
+ };
1941
+ /** Skips any value starting at i, iteratively. */
1942
+ const skip = () => {
1943
+ let depth = 0;
1944
+ do {
1945
+ const c = text[i];
1946
+ if (c === "\"") string();
1947
+ else {
1948
+ if (c === "{" || c === "[") depth++;
1949
+ else if (c === "}" || c === "]") depth--;
1950
+ i++;
1951
+ if (depth === 0 && c !== "{" && c !== "[") while (i < text.length && !",]} \r\n".includes(text[i])) i++;
1952
+ }
1953
+ } while (depth > 0 && i < text.length);
1954
+ };
1955
+ const value = (level) => {
1956
+ space();
1957
+ if (level === path.length) {
1958
+ if (text[i] !== "\"") {
1959
+ skip();
1960
+ return null;
1961
+ }
1962
+ const start = i;
1963
+ string();
1964
+ return [start, i];
1965
+ }
1966
+ if (text[i] !== "{") {
1967
+ skip();
1968
+ return null;
1969
+ }
1970
+ i++;
1971
+ let found = null;
1972
+ space();
1973
+ if (text[i] === "}") {
1974
+ i++;
1975
+ return null;
1976
+ }
1977
+ for (;;) {
1978
+ space();
1979
+ const key = JSON.parse(string());
1980
+ space();
1981
+ i++;
1982
+ if (key === path[level]) found = value(level + 1);
1983
+ else {
1984
+ space();
1985
+ skip();
1986
+ }
1987
+ space();
1988
+ if (text[i++] === "}") return found;
1989
+ }
1990
+ };
1991
+ return value(0);
1992
+ }
1901
1993
  function jsonError(path, cause) {
1902
1994
  return new XlvError("XLV_E_JSON_INVALID", `"${path}" is not a valid package.json.`, {
1903
1995
  hint: "Fix the JSON syntax of package.json and run the command again.",
@@ -1945,11 +2037,14 @@ async function run$1(ctx) {
1945
2037
  }
1946
2038
  let write = null;
1947
2039
  if (changes.length > 0 && flag(ctx.args, "force")) {
1948
- let updated = text;
1949
- for (const c of changes) {
1950
- const escaped = c.from.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1951
- updated = updated.replace(new RegExp(`("exceljs"\\s*:\\s*)"${escaped}"`), `$1"${ALIAS}"`);
2040
+ const spans = changes.map((c) => stringValueSpan(text, [c.section, "exceljs"])).filter((s) => s !== null);
2041
+ let updated = "";
2042
+ let at = 0;
2043
+ for (const [start, end] of spans.sort((a, b) => a[0] - b[0])) {
2044
+ updated += `${text.slice(at, start)}"${ALIAS}"`;
2045
+ at = end;
1952
2046
  }
2047
+ updated += text.slice(at);
1953
2048
  write = await writeOutput(ctx, pkgPath, updated, {
1954
2049
  force: true,
1955
2050
  dryRun: flag(ctx.args, "dry-run")
@@ -2350,14 +2445,14 @@ async function run(argv, io) {
2350
2445
  const err = new Output(useColor(io.env, io.stderrIsTTY), io.stderrIsTTY ? width : 100);
2351
2446
  const stdout = (text) => io.stdout(terminalSafe(text, out.color));
2352
2447
  const stderr = (text) => io.stderr(terminalSafe(text, err.color));
2353
- const toJson = (value) => `${io.stdoutIsTTY ? JSON.stringify(value, null, 2) : JSON.stringify(value)}\n`;
2448
+ const toJson = (value) => `${jsonSafe(io.stdoutIsTTY ? JSON.stringify(value, null, 2) : JSON.stringify(value))}\n`;
2354
2449
  const json = optionArgs(argv).includes("--json");
2355
2450
  const [first, ...rest] = argv;
2356
2451
  let spec;
2357
2452
  const state = { lastFile: null };
2358
2453
  try {
2359
2454
  if (first === void 0 || first === "--json" && rest.length === 0) {
2360
- io.stdout(welcome(out, io.version, COMMANDS));
2455
+ stdout(welcome(out, io.version, COMMANDS));
2361
2456
  return EXIT.ok;
2362
2457
  }
2363
2458
  if (first === "--version" || first === "-v") {
@@ -2367,7 +2462,7 @@ async function run(argv, io) {
2367
2462
  if (first === "help" || first === "--help" || first === "-h") {
2368
2463
  const topic = rest.find((a) => !a.startsWith("-"));
2369
2464
  if (topic === void 0) {
2370
- io.stdout(welcome(out, io.version, COMMANDS));
2465
+ stdout(welcome(out, io.version, COMMANDS));
2371
2466
  return EXIT.ok;
2372
2467
  }
2373
2468
  stdout(commandHelp(out, findCommand(topic)));
@@ -2542,6 +2637,15 @@ function formatError(err, error, tryCommand, verbose = false) {
2542
2637
  ""
2543
2638
  ].join("\n");
2544
2639
  }
2640
+ /**
2641
+ * JSON text with DEL, the C1 controls (which include the 8-bit CSI U+009B), the bidirectional
2642
+ * formatting characters and U+2028/U+2029 written as `\uXXXX` escapes. JSON.stringify already
2643
+ * escapes C0 controls; these characters can occur only inside strings, so the escaped text is
2644
+ * valid JSON with the same value, and a terminal showing it does not act on them (D-H01).
2645
+ */
2646
+ function jsonSafe(json) {
2647
+ return json.replace(/[\u007f-\u009f\u061c\u200e\u200f\u202a-\u202e\u2066-\u2069\u2028\u2029]/g, (c) => `\\u${c.charCodeAt(0).toString(16).padStart(4, "0")}`);
2648
+ }
2545
2649
  // src/cli/bin.ts
2546
2650
  function version() {
2547
2651
  try {
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { $ as InspectOptions, $n as verifyPassword, $t as DataValidationOperator, A as toJSON, An as Hyperlink, At as Workbook, B as FormulaResult, Bn as PrintErrors, Bt as CfValue, C as TableRow, Cn as ImageAnchorInput, Ct as Sheet, D as fromJSON, Dn as SheetImage, Dt as SheetState, E as WriteTableOptions, En as ImagePositionInput, Et as SheetProperties, F as CellInput, Fn as PageOrder, Ft as TableDefinitionColumn, G as SheetPropertiesOptions, Gn as SheetProtectionOptions, Gt as ConditionalFormatRule, H as NewSheet, Hn as PrintTitles, Ht as CfValueType, I as ColumnOptions, In as PageOrientation, It as TableInput, J as WriteWorkbookOptions, Jn as HashPasswordOptions, Jt as DataBarAxisPosition, K as SheetViewOptions, Kn as WorkbookProtection, Kt as ConditionalFormatRuleInput, L as CreateWorkbookOptions, Ln as PageSetup, Lt as TableStyleOptions, M as StandardSchemaV1, Mn as HeaderFooter, Mt as WorkbookView, N as AddSheetOptions, Nn as PageBreaks, Nt as TableColumnInput, O as readTable, On as WorkbookImage, Ot as SheetView, P as CalculationOptions, Pn as PageMargins, Pt as TableDefinition, Q as InspectDefinedName, Qn as legacyPasswordHash, Qt as DataValidationImeMode, R as ERROR_VALUES, Rn as PageSetupOptions, Rt as TotalsRowFunction, S as TableResult, Sn as ImageAnchor, St as RowInfo, T as ToJSONOptions, Tn as ImageOptions, Tt as SheetLayout, U as NewWorkbook, Un as SheetProtection, Ut as ConditionalFormat, V as FreezeOptions, Vn as PrintOptions, Vt as CfValueInput, W as RowOptions, Wn as SheetProtectionFlags, Wt as ConditionalFormatOperator, X as writeWorkbook, Xn as PasswordVerifier, Xt as DataValidation, Y as createWorkbook, Yn as PasswordHash, Yt as DataBarDirection, Z as InspectColumn, Zn as hashPassword, Zt as DataValidationErrorStyle, _ as JsonTable, _n as ReadingOrder, _r as DefinedNameOptions, _t as DateSystem, a as WriteWorkbookStreamOptions, an as BorderEdge, ar as XlvDetails, at as ReadOptions, b as TableColumnSpec, bn as VerticalAlignment, bt as FrozenPanes, c as OpenWorkbookStreamOptions, cn as CellStyle, cr as XlvErrorInit, ct as NoteBox, d as StreamRow, dn as FontScheme, dr as XlvWarning, dt as NoteMargins, en as DataValidationRule, er as ShaAlgorithm, et as InspectReport, f as StreamSheetInfo, fn as FontStyle, fr as XlvWarningCode, ft as Cell, g as readTableStream, gn as ProtectionStyle, gr as DefinedName, gt as Conformance, h as openWorkbookStream, hn as PatternType, hr as ReadLimits, ht as ColumnInfo, i as WriteTableStreamOptions, in as AlignmentStyle, ir as XlvDetailValue, it as inspect, j as writeTable, jn as HyperlinkTarget, jt as WorkbookFormat, k as readTableAsync, kn as imageFormat, kt as SplitPanes, l as ReadTableStreamOptions, ln as Color, lr as XlvErrorJSON, lt as NoteBoxInput, m as WorkbookStream, mn as HorizontalAlignment, mr as DEFAULT_READ_LIMITS, mt as CellValue, n as StreamRowInput, nn as IconSetType, nr as ERROR_CODES, nt as InspectTypeCounts, o as writeTableStream, on as BorderLineStyle, or as XlvError, ot as readWorkbook, p as TableStreamItem, pn as GradientFill, pr as isXlvError, pt as CellType, q as WorkbookViewOptions, qn as WorkbookProtectionOptions, qt as ConditionalFormatType, r as StreamSheetSpec, rn as TimePeriod, rr as WARNING_CODES, rt as InspectValue, s as writeWorkbookStream, sn as BorderStyle, sr as XlvErrorCode, st as Note, t as StreamDefinedName, tn as DataValidationType, tr as WorkbookProperties, tt as InspectSheet, u as STREAM_LIMIT_DEFAULTS, un as FillStyle, ur as XlvLocation, ut as NoteInput, v as ReadTableOptions, vn as UnderlineStyle, vt as Formula, w as TableValue, wn as ImageFormat, wt as SheetKind, x as TableIssue, xn as AnchorMarker, xt as RichTextRun, y as TableColumn, yn as VerticalAlignRun, yt as FormulaKind, z as ErrorValue, zn as PrintCellComments, zt as CfIcon } from "./stream-write-GJXORYQ2.js";
1
+ import { $ as InspectOptions, $n as verifyPassword, $t as DataValidationOperator, A as toJSON, An as Hyperlink, At as Workbook, B as FormulaResult, Bn as PrintErrors, Bt as CfValue, C as TableRow, Cn as ImageAnchorInput, Ct as Sheet, D as fromJSON, Dn as SheetImage, Dt as SheetState, E as WriteTableOptions, En as ImagePositionInput, Et as SheetProperties, F as CellInput, Fn as PageOrder, Ft as TableDefinitionColumn, G as SheetPropertiesOptions, Gn as SheetProtectionOptions, Gt as ConditionalFormatRule, H as NewSheet, Hn as PrintTitles, Ht as CfValueType, I as ColumnOptions, In as PageOrientation, It as TableInput, J as WriteWorkbookOptions, Jn as HashPasswordOptions, Jt as DataBarAxisPosition, K as SheetViewOptions, Kn as WorkbookProtection, Kt as ConditionalFormatRuleInput, L as CreateWorkbookOptions, Ln as PageSetup, Lt as TableStyleOptions, M as StandardSchemaV1, Mn as HeaderFooter, Mt as WorkbookView, N as AddSheetOptions, Nn as PageBreaks, Nt as TableColumnInput, O as readTable, On as WorkbookImage, Ot as SheetView, P as CalculationOptions, Pn as PageMargins, Pt as TableDefinition, Q as InspectDefinedName, Qn as legacyPasswordHash, Qt as DataValidationImeMode, R as ERROR_VALUES, Rn as PageSetupOptions, Rt as TotalsRowFunction, S as TableResult, Sn as ImageAnchor, St as RowInfo, T as ToJSONOptions, Tn as ImageOptions, Tt as SheetLayout, U as NewWorkbook, Un as SheetProtection, Ut as ConditionalFormat, V as FreezeOptions, Vn as PrintOptions, Vt as CfValueInput, W as RowOptions, Wn as SheetProtectionFlags, Wt as ConditionalFormatOperator, X as writeWorkbook, Xn as PasswordVerifier, Xt as DataValidation, Y as createWorkbook, Yn as PasswordHash, Yt as DataBarDirection, Z as InspectColumn, Zn as hashPassword, Zt as DataValidationErrorStyle, _ as JsonTable, _n as ReadingOrder, _r as DefinedNameOptions, _t as DateSystem, a as WriteWorkbookStreamOptions, an as BorderEdge, ar as XlvDetails, at as ReadOptions, b as TableColumnSpec, bn as VerticalAlignment, bt as FrozenPanes, c as OpenWorkbookStreamOptions, cn as CellStyle, cr as XlvErrorInit, ct as NoteBox, d as StreamRow, dn as FontScheme, dr as XlvWarning, dt as NoteMargins, en as DataValidationRule, er as ShaAlgorithm, et as InspectReport, f as StreamSheetInfo, fn as FontStyle, fr as XlvWarningCode, ft as Cell, g as readTableStream, gn as ProtectionStyle, gr as DefinedName, gt as Conformance, h as openWorkbookStream, hn as PatternType, hr as ReadLimits, ht as ColumnInfo, i as WriteTableStreamOptions, in as AlignmentStyle, ir as XlvDetailValue, it as inspect, j as writeTable, jn as HyperlinkTarget, jt as WorkbookFormat, k as readTableAsync, kn as imageFormat, kt as SplitPanes, l as ReadTableStreamOptions, ln as Color, lr as XlvErrorJSON, lt as NoteBoxInput, m as WorkbookStream, mn as HorizontalAlignment, mr as DEFAULT_READ_LIMITS, mt as CellValue, n as StreamRowInput, nn as IconSetType, nr as ERROR_CODES, nt as InspectTypeCounts, o as writeTableStream, on as BorderLineStyle, or as XlvError, ot as readWorkbook, p as TableStreamItem, pn as GradientFill, pr as isXlvError, pt as CellType, q as WorkbookViewOptions, qn as WorkbookProtectionOptions, qt as ConditionalFormatType, r as StreamSheetSpec, rn as TimePeriod, rr as WARNING_CODES, rt as InspectValue, s as writeWorkbookStream, sn as BorderStyle, sr as XlvErrorCode, st as Note, t as StreamDefinedName, tn as DataValidationType, tr as WorkbookProperties, tt as InspectSheet, u as STREAM_LIMIT_DEFAULTS, un as FillStyle, ur as XlvLocation, ut as NoteInput, v as ReadTableOptions, vn as UnderlineStyle, vt as Formula, w as TableValue, wn as ImageFormat, wt as SheetKind, x as TableIssue, xn as AnchorMarker, xt as RichTextRun, y as TableColumn, yn as VerticalAlignRun, yt as FormulaKind, z as ErrorValue, zn as PrintCellComments, zt as CfIcon } from "./stream-write-CaGFWPhB.js";
2
2
  /**
3
3
  * Options for {@link toCSV}.
4
4
  *
@@ -21,9 +21,10 @@ interface ToCSVOptions {
21
21
  /** Start with a UTF-8 byte order mark, which helps Excel detect UTF-8. Default `false`. */
22
22
  readonly bom?: boolean;
23
23
  /**
24
- * `"escape"` (default): text cells that start with `=`, `+`, `-`, `@`, tab, CR, LF or their
25
- * full-width forms get a leading `'` and quotes, so spreadsheet applications show them as
26
- * text instead of running them as formulas (OWASP CSV Injection). `"off"`: write them as is.
24
+ * `"escape"` (default): text cells that start with `=`, `+`, `-`, `@`, their full-width or small
25
+ * forms, or a control character (tab, CR, LF, NUL, …) get a leading `'` and quotes, so
26
+ * spreadsheet applications show them as text instead of running them as formulas (OWASP CSV
27
+ * Injection). `"off"`: write them as is.
27
28
  */
28
29
  readonly formulaInjection?: "escape" | "off";
29
30
  /** Read limits for byte input; see `ReadLimits`. */
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { Dn as isXlvError, En as XlvError, Gt as legacyPasswordHash, Kt as verifyPassword, Rt as DEFAULT_READ_LIMITS, Tn as WARNING_CODES, Wt as hashPassword, Zt as imageFormat, b as readWorkbook, c as NewSheet, f as createWorkbook, g as inspect, l as NewWorkbook, m as writeWorkbook, s as ERROR_VALUES, wn as ERROR_CODES, z as shiftFormula } from "./options-CwSp2pA4.js";
2
- import { a as appendRows, c as fromCSV, d as fromJSON, g as writeTable, h as toJSON, i as adjustRange, m as readTableAsync, n as insertRows, o as editCells, p as readTable, r as adjustFormula, s as diff, t as deleteRows, u as toCSV } from "./rows-DKwlr0rg.js";
3
- import { a as readTableStream, i as openWorkbookStream, n as writeWorkbookStream, r as STREAM_LIMIT_DEFAULTS, t as writeTableStream } from "./stream-write-BKppyMz0.js";
1
+ import { Dn as WARNING_CODES, En as ERROR_CODES, Gt as hashPassword, Kt as legacyPasswordHash, On as XlvError, Qt as imageFormat, Rt as DEFAULT_READ_LIMITS, b as readWorkbook, c as NewSheet, f as createWorkbook, g as inspect, kn as isXlvError, l as NewWorkbook, m as writeWorkbook, qt as verifyPassword, s as ERROR_VALUES, z as shiftFormula } from "./options-Ba6r4zP1.js";
2
+ import { a as appendRows, c as fromCSV, d as fromJSON, g as writeTable, h as toJSON, i as adjustRange, m as readTableAsync, n as insertRows, o as editCells, p as readTable, r as adjustFormula, s as diff, t as deleteRows, u as toCSV } from "./rows-Biu_ddwz.js";
3
+ import { a as readTableStream, i as openWorkbookStream, n as writeWorkbookStream, r as STREAM_LIMIT_DEFAULTS, t as writeTableStream } from "./stream-write-CtWyTd44.js";
4
4
  export { DEFAULT_READ_LIMITS, ERROR_CODES, ERROR_VALUES, NewSheet, NewWorkbook, STREAM_LIMIT_DEFAULTS, WARNING_CODES, XlvError, adjustFormula, adjustRange, appendRows, createWorkbook, deleteRows, diff, editCells, fromCSV, fromJSON, hashPassword, imageFormat, insertRows, inspect, isXlvError, legacyPasswordHash, openWorkbookStream, readTable, readTableAsync, readTableStream, readWorkbook, shiftFormula, toCSV, toJSON, verifyPassword, writeTable, writeTableStream, writeWorkbook, writeWorkbookStream };
@@ -1,4 +1,4 @@
1
- import { En as XlvError, zt as resolveLimits } from "./options-CwSp2pA4.js";
1
+ import { Bt as resolveLimits, On as XlvError } from "./options-Ba6r4zP1.js";
2
2
  import { readFile, stat } from "node:fs/promises";
3
3
  // src/util/node-files.ts
4
4
  /** The path as shown in messages. */
package/dist/node.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { $ as InspectOptions, At as Workbook, J as WriteWorkbookOptions, U as NewWorkbook, a as WriteWorkbookStreamOptions, at as ReadOptions, c as OpenWorkbookStreamOptions, et as InspectReport, m as WorkbookStream } from "./stream-write-GJXORYQ2.js";
1
+ import { $ as InspectOptions, At as Workbook, J as WriteWorkbookOptions, U as NewWorkbook, a as WriteWorkbookStreamOptions, at as ReadOptions, c as OpenWorkbookStreamOptions, et as InspectReport, m as WorkbookStream } from "./stream-write-CaGFWPhB.js";
2
2
  /**
3
3
  * Reads an XLSX file from disk. Node.js only; in other runtimes pass the bytes to
4
4
  * `readWorkbook` from `xlvane`. The file size is checked against `limits.maxInputBytes`
package/dist/node.js CHANGED
@@ -1,6 +1,6 @@
1
- import { En as XlvError, b as readWorkbook, g as inspect, m as writeWorkbook, zt as resolveLimits } from "./options-CwSp2pA4.js";
2
- import { i as readInput, n as describePath, r as fileError, t as checkFile } from "./node-files-B0iqSJIJ.js";
3
- import { i as openWorkbookStream, n as writeWorkbookStream, r as STREAM_LIMIT_DEFAULTS } from "./stream-write-BKppyMz0.js";
1
+ import { Bt as resolveLimits, On as XlvError, b as readWorkbook, g as inspect, m as writeWorkbook } from "./options-Ba6r4zP1.js";
2
+ import { i as readInput, n as describePath, r as fileError, t as checkFile } from "./node-files-CsQVpKgP.js";
3
+ import { i as openWorkbookStream, n as writeWorkbookStream, r as STREAM_LIMIT_DEFAULTS } from "./stream-write-CtWyTd44.js";
4
4
  import { openAsBlob } from "node:fs";
5
5
  import { open, unlink, writeFile } from "node:fs/promises";
6
6
  // src/node.ts