xlvane 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- import { $ as fillXml, At as EMPTY_STYLES, B as tokenize, Bt as suggest, Cn as escapeText, Ct as relationshipsPartName, En as XlvError, Ft as normalizeStyle, G as DEFAULT_FONT, H as X14_DV_URI, I as WarningCollector, It as stableKey, J as borderXml, Lt as isDateFormat, M as openZip, Mt as READING_ORDERS, Nt as checkStyleCount, Pt as mergeStyles, Rt as DEFAULT_READ_LIMITS, Sn as escapeAttr, St as openPackage, Tt as zipNameToPartName, V as X14_CF_URI, _ as isoDate, an as columnLetters, at as reusableBuiltinNumFmt, b as readWorkbook, bn as parseXml, bt as parseIsoDateTime, cn as parseCellRef, en as SML_NS, et as fontXml, f as createWorkbook, ft as stringItemXml, gn as parseXmlTree, h as detectHeader, hn as childrenNamed, i as readOptions, in as MAX_ROWS, jt as parseStyles, ln as parseRange, m as writeWorkbook, mn as childNamed, mt as textElement, n as oneOfOption, nn as relationshipTypes, on as formatCellRef, q as attr, r as openInput, rn as MAX_COLUMNS, sn as formatRange, t as booleanOption, tn as classifyRelationship, tt as formatNumber, vn as decodeXml, vt as writeZip, wt as partKey, x as toBytes, xn as encodeXstring, xt as parseSharedStrings, yn as normalizeLineEnds, z as shiftFormula, zt as resolveLimits } from "./options-CwSp2pA4.js";
1
+ import { $ as fillXml, At as EMPTY_STYLES, B as tokenize, Bt as resolveLimits, Cn as encodeXstring, Ct as relationshipsPartName, Ft as normalizeStyle, G as DEFAULT_FONT, H as X14_DV_URI, I as WarningCollector, It as stableKey, J as borderXml, Lt as isDateFormat, M as openZip, Mt as READING_ORDERS, Nt as checkStyleCount, On as XlvError, Pt as mergeStyles, Rt as DEFAULT_READ_LIMITS, Sn as parseXml, St as openPackage, Tn as escapeText, Tt as zipNameToPartName, V as X14_CF_URI, Vt as suggest, _ as isoDate, _n as childrenNamed, an as MAX_COLUMNS, at as reusableBuiltinNumFmt, b as readWorkbook, bn as decodeXml, bt as parseIsoDateTime, cn as formatCellRef, dn as parseRange, et as fontXml, f as createWorkbook, ft as stringItemXml, gn as childNamed, h as detectHeader, i as readOptions, in as relationshipTypes, jt as parseStyles, ln as formatRange, m as writeWorkbook, mt as textElement, n as oneOfOption, nn as SML_NS, on as MAX_ROWS, q as attr, r as openInput, rn as classifyRelationship, sn as columnLetters, t as booleanOption, tn as REL_NS, tt as formatNumber, un as parseCellRef, vn as parseXmlTree, vt as writeZip, wn as escapeAttr, wt as partKey, x as toBytes, xn as normalizeLineEnds, xt as parseSharedStrings, z as shiftFormula, zt as checkOutputPositions } from "./options-Ba1iWD-Z.js";
2
2
  // src/table.ts
3
3
  const READ_OPTIONS = [
4
4
  "sheet",
@@ -106,6 +106,7 @@ function pickTable(workbook, o) {
106
106
  }
107
107
  function prepare(input, options) {
108
108
  const o = readOptions(options, READ_OPTIONS, "readTable options");
109
+ const limits = resolveLimits(o.get("limits"));
109
110
  const workbook = openInput(input, o.get("limits"), o.get("onWarning"));
110
111
  const table = pickTable(workbook, o);
111
112
  const sheet = table?.sheet ?? pickSheet(workbook, o.get("sheet"));
@@ -173,13 +174,16 @@ function prepare(input, options) {
173
174
  }
174
175
  const columns = [];
175
176
  const used = /* @__PURE__ */ new Set();
177
+ const nextSuffix = /* @__PURE__ */ new Map();
176
178
  const headerCells = headerRow === null ? void 0 : byRow.get(headerRow);
177
179
  for (let col = minCol; col <= maxCol; col++) {
178
180
  const letter = columnLetters(col);
179
181
  const header = headerRow === null ? null : headerText(headerCells?.get(col));
180
182
  const base = header === null || header.trim() === "" ? letter : header.trim();
181
183
  let key = base;
182
- for (let n = 2; used.has(key); n++) key = `${base}_${String(n)}`;
184
+ let n = nextSuffix.get(base) ?? 2;
185
+ while (used.has(key)) key = `${base}_${String(n++)}`;
186
+ nextSuffix.set(base, n);
183
187
  used.add(key);
184
188
  columns.push({
185
189
  key,
@@ -192,6 +196,9 @@ function prepare(input, options) {
192
196
  const rowNumbers = [];
193
197
  const first = headerRow === null ? minRow : headerRow + 1;
194
198
  const numbers = skipEmpty ? [...byRow.keys()].filter((r) => r >= first && r <= maxRow).sort((a, b) => a - b) : rangeOf(first, maxRow);
199
+ let cellCount = 0;
200
+ for (const cells of byRow.values()) cellCount += cells.size;
201
+ checkOutputPositions(numbers.length * columns.length, cellCount, limits.maxCells, "The table", sheet.name);
195
202
  for (const r of numbers) {
196
203
  const cells = byRow.get(r);
197
204
  if (skipEmpty && (cells === void 0 || [...cells.values()].every((c) => c.value === null))) continue;
@@ -549,7 +556,11 @@ function fromJSON(input, options) {
549
556
  return writeTable(rows, options);
550
557
  }
551
558
  // src/csv.ts
552
- /** First characters that make spreadsheet applications treat a CSV field as a formula (CSV-10). */
559
+ /**
560
+ * First characters that make spreadsheet applications treat a CSV field as a formula (CSV-10),
561
+ * plus, as a precaution, the small forms of = + - @ (CSV-12, D-H01). Text starting with any other
562
+ * C0 control character is escaped too (see `injectionStart`).
563
+ */
553
564
  const INJECTION_START = /* @__PURE__ */ new Set([
554
565
  "=",
555
566
  "+",
@@ -561,8 +572,14 @@ const INJECTION_START = /* @__PURE__ */ new Set([
561
572
  "=",
562
573
  "+",
563
574
  "-",
564
- "@"
575
+ "@",
576
+ "﹦",
577
+ "﹢",
578
+ "﹣",
579
+ "﹫"
565
580
  ]);
581
+ /** Whether text starting with `c` is escaped: one of INJECTION_START or a C0 control (NUL included). */
582
+ const injectionStart = (c) => INJECTION_START.has(c) || c.charCodeAt(0) < 32;
566
583
  /**
567
584
  * Converts a sheet to CSV text. Fields that contain the delimiter, a quote, CR or LF are
568
585
  * enclosed in quotes, and quotes are doubled (RFC 4180). Numbers are written in JavaScript's
@@ -598,6 +615,7 @@ function toCSV(input, options) {
598
615
  const newline = oneOfOption("newline", o.get("newline"), ["\r\n", "\n"], "\r\n");
599
616
  const bom = booleanOption("bom", o.get("bom"), false);
600
617
  const injection = oneOfOption("formulaInjection", o.get("formulaInjection"), ["escape", "off"], "escape");
618
+ const limits = resolveLimits(o.get("limits"));
601
619
  const sheet = pickSheet(openInput(input, o.get("limits"), o.get("onWarning")), o.get("sheet"));
602
620
  let startRow = 1;
603
621
  let startCol = 1;
@@ -617,6 +635,16 @@ function toCSV(input, options) {
617
635
  if (cell.row > endRow) endRow = cell.row;
618
636
  if (cell.col > endCol) endCol = cell.col;
619
637
  }
638
+ const byRow = /* @__PURE__ */ new Map();
639
+ let cellCount = 0;
640
+ for (const cell of sheet.cells()) {
641
+ if (cell.row < startRow || cell.row > endRow || cell.col < startCol || cell.col > endCol) continue;
642
+ let r = byRow.get(cell.row);
643
+ if (r === void 0) byRow.set(cell.row, r = /* @__PURE__ */ new Map());
644
+ r.set(cell.col, cell.value);
645
+ cellCount++;
646
+ }
647
+ if (endRow >= startRow && endCol >= startCol) checkOutputPositions((endRow - startRow + 1) * (endCol - startCol + 1), cellCount, limits.maxCells, "The CSV text", sheet.name);
620
648
  const needsQuotes = (s) => s.includes(delimiter) || s.includes("\"") || s.includes("\r") || s.includes("\n");
621
649
  const field = (value) => {
622
650
  if (value === null || value === void 0) return "";
@@ -625,7 +653,7 @@ function toCSV(input, options) {
625
653
  if (value instanceof Date) return isoDate(value);
626
654
  let s = String(value);
627
655
  let quote = needsQuotes(s);
628
- if (injection === "escape" && s.length > 0 && INJECTION_START.has(s[0] ?? "")) {
656
+ if (injection === "escape" && s.length > 0 && injectionStart(s[0] ?? "")) {
629
657
  s = `'${s}`;
630
658
  quote = true;
631
659
  }
@@ -634,10 +662,8 @@ function toCSV(input, options) {
634
662
  const lines = [];
635
663
  for (let r = startRow; r <= endRow; r++) {
636
664
  const fields = [];
637
- for (let c = startCol; c <= endCol; c++) {
638
- const cell = sheet.cell(formatCellRef(r, c));
639
- fields.push(field(cell?.value));
640
- }
665
+ const cells = byRow.get(r);
666
+ for (let c = startCol; c <= endCol; c++) fields.push(field(cells?.get(c)));
641
667
  lines.push(fields.join(delimiter));
642
668
  }
643
669
  const body = lines.length === 0 ? "" : lines.join(newline) + newline;
@@ -3255,8 +3281,25 @@ function eachVmlShape(text, change) {
3255
3281
  out.push(text.slice(pos));
3256
3282
  return out.join("");
3257
3283
  }
3284
+ /**
3285
+ * Finds the first `drawing` and `legacyDrawing` elements of the worksheet (SpreadsheetML
3286
+ * namespace, any depth, so also inside `mc:AlternateContent`) with the element scanner, so that
3287
+ * comments are skipped and the time is linear in the part's size (D-H01); the id is the
3288
+ * attribute `id` in the relationships namespace, whatever its prefix.
3289
+ */
3290
+ function drawingIds(text, part, limits) {
3291
+ const { elements } = scan(text, part, limits, (local, depth, _parent, ns) => depth >= 2 && (local === "drawing" || local === "legacyDrawing") && (ns === SML_NS.transitional || ns === SML_NS.strict));
3292
+ const idOf = (local) => {
3293
+ const e = elements.find((x) => x.local === local);
3294
+ return e?.attrs.get(`{${REL_NS.transitional}}id`) ?? e?.attrs.get(`{${REL_NS.strict}}id`);
3295
+ };
3296
+ return {
3297
+ drawing: idOf("drawing"),
3298
+ legacyDrawing: idOf("legacyDrawing")
3299
+ };
3300
+ }
3258
3301
  /** Rewrites the notes of the changed sheet: the Comments part and the note shapes of its VML part. */
3259
- function rewriteNotes(edit, pkg, sheetPart, sheetText, sheet, c) {
3302
+ function rewriteNotes(edit, pkg, sheetPart, ids, sheet, c) {
3260
3303
  const rels = pkg.relationships(sheetPart);
3261
3304
  const comments = rels.find((r) => !r.external && classifyRelationship(r.type)?.name === "comments");
3262
3305
  if (comments !== void 0) {
@@ -3282,7 +3325,7 @@ function rewriteNotes(edit, pkg, sheetPart, sheetText, sheet, c) {
3282
3325
  if (edits.length > 0) edit.write(comments.target, applyEdits(text, edits));
3283
3326
  }
3284
3327
  }
3285
- const id = /<(?:[A-Za-z_][\w.-]*:)?legacyDrawing\b[^>]*?\br:id\s*=\s*["']([^"']*)["']/.exec(sheetText)?.[1];
3328
+ const id = ids.legacyDrawing;
3286
3329
  const vml = id === void 0 ? void 0 : rels.find((r) => r.id === id && !r.external);
3287
3330
  if (vml === void 0) return;
3288
3331
  const text = edit.text(vml.target);
@@ -3318,8 +3361,8 @@ function rewriteNotes(edit, pkg, sheetPart, sheetText, sheet, c) {
3318
3361
  * rows. Drawings with charts, shapes, groups or other objects are refused, and so is a deletion
3319
3362
  * that would cut a picture (a deleted row inside the rows it covers or under its anchor cell).
3320
3363
  */
3321
- function rewriteDrawing(edit, pkg, sheetPart, sheetText, sheet, c) {
3322
- const id = /<(?:[A-Za-z_][\w.-]*:)?drawing\b[^>]*?\br:id\s*=\s*["']([^"']*)["']/.exec(sheetText)?.[1];
3364
+ function rewriteDrawing(edit, pkg, sheetPart, ids, sheet, c) {
3365
+ const id = ids.drawing;
3323
3366
  const rels = pkg.relationships(sheetPart);
3324
3367
  const rel = id === void 0 ? void 0 : rels.find((r) => r.id === id && !r.external && classifyRelationship(r.type)?.name === "drawing");
3325
3368
  if (rel === void 0) return;
@@ -3573,8 +3616,9 @@ function changeRows(input, sheetName, at, count, style, limits) {
3573
3616
  if (out.text !== text) edit.write(s.part, out.text);
3574
3617
  rewriteTables(edit, pkg, s.part, s, c, target);
3575
3618
  if (target) {
3576
- rewriteNotes(edit, pkg, s.part, text, s, c);
3577
- rewriteDrawing(edit, pkg, s.part, text, s, c);
3619
+ const ids = drawingIds(text, s.part, edit.limits);
3620
+ rewriteNotes(edit, pkg, s.part, ids, s, c);
3621
+ rewriteDrawing(edit, pkg, s.part, ids, s, c);
3578
3622
  }
3579
3623
  }
3580
3624
  rewriteNames(edit, workbookPart, c);
@@ -1,4 +1,4 @@
1
- import { $t as resolveImage, A as limitExceeded, C as Bytes, D as TAIL_BYTES, Dt as normalizeTable, E as NotLoaded, En as XlvError, Et as TABLE_CONTENT_TYPE, F as validateLocalHeaders, Ft as normalizeStyle, Ht as printXml, I as WarningCollector, Jt as IMAGE_CONTENT_TYPES, K as SheetRels, L as sheetNotFound, N as readCentralDirectory, O as checkSource$1, Ot as checkDefinedName, P as unsupported, Q as createSharedStrings, Qt as metricsOf, R as createWorksheetParser, Rt as DEFAULT_READ_LIMITS, S as workbookPartsToLoad, St as openPackage, T as HEAD_BYTES, Ut as sheetProtectionXml, Vt as printNames, X as cellXml, Xt as drawingParts, Y as buildStyles, Yt as MediaParts, Z as contentTypesXml, _ as isoDate, _n as createXmlByteStreamParser, _t as FormulaGroups, a as DATE_FORMAT, an as columnLetters, ct as sheetHead, d as convertCellInput, dn as CORE_PROPERTIES_PART, dt as sheetTail, f as createWorkbook, fn as appPropertiesXml, gt as workbookXml, h as detectHeader, ht as workbookRelsXml, i as readOptions, in as MAX_ROWS, it as noteParts, j as locateCentralDirectory, k as corrupt, kt as checkUniqueName, lt as sheetRelsPartName, n as oneOfOption, nt as hyperlinksXml, o as DATE_TIME_FORMAT, on as formatCellRef, ot as rootRelsXml, p as sortedNotes, pn as corePropertiesXml, pt as tableParts, qt as DRAWING_CONTENT_TYPE, rn as MAX_COLUMNS, rt as nameTitles, st as rowStart, tn as classifyRelationship, u as checkBookViews, un as APP_PROPERTIES_PART, ut as sheetRulesXml, v as analyzeWorkbook, w as CORRUPT_HINT, y as readNotes, yt as crc32, zt as resolveLimits } from "./options-CwSp2pA4.js";
1
+ import { $t as metricsOf, A as limitExceeded, Bt as resolveLimits, C as Bytes, D as TAIL_BYTES, Dt as normalizeTable, E as NotLoaded, Et as TABLE_CONTENT_TYPE, F as validateLocalHeaders, Ft as normalizeStyle, Ht as printNames, I as WarningCollector, Jt as DRAWING_CONTENT_TYPE, K as SheetRels, L as sheetNotFound, N as readCentralDirectory, O as checkSource$1, On as XlvError, Ot as checkDefinedName, P as unsupported, Q as createSharedStrings, R as createWorksheetParser, Rt as DEFAULT_READ_LIMITS, S as workbookPartsToLoad, St as openPackage, T as HEAD_BYTES, Ut as printXml, Wt as sheetProtectionXml, X as cellXml, Xt as MediaParts, Y as buildStyles, Yt as IMAGE_CONTENT_TYPES, Z as contentTypesXml, Zt as drawingParts, _ as isoDate, _t as FormulaGroups, a as DATE_FORMAT, an as MAX_COLUMNS, cn as formatCellRef, ct as sheetHead, d as convertCellInput, dt as sheetTail, en as resolveImage, f as createWorkbook, fn as APP_PROPERTIES_PART, gt as workbookXml, h as detectHeader, hn as corePropertiesXml, ht as workbookRelsXml, i as readOptions, it as noteParts, j as locateCentralDirectory, k as corrupt, kt as checkUniqueName, lt as sheetRelsPartName, mn as appPropertiesXml, n as oneOfOption, nt as hyperlinksXml, o as DATE_TIME_FORMAT, on as MAX_ROWS, ot as rootRelsXml, p as sortedNotes, pn as CORE_PROPERTIES_PART, pt as tableParts, rn as classifyRelationship, rt as nameTitles, sn as columnLetters, st as rowStart, u as checkBookViews, ut as sheetRulesXml, v as analyzeWorkbook, w as CORRUPT_HINT, wt as partKey, y as readNotes, yn as createXmlByteStreamParser, yt as crc32, zt as checkOutputPositions } from "./options-Ba1iWD-Z.js";
2
2
  import { Deflate, Inflate } from "fflate";
3
3
  // src/zip/stream.ts
4
4
  /**
@@ -78,7 +78,7 @@ async function openZipSource(source, limits) {
78
78
  const dir = await withLoading(b, src, () => locateCentralDirectory(b, limits));
79
79
  if (!b.has(dir.centralOffset, dir.centralLimit) && dir.centralLimit > dir.centralOffset) b.add(dir.centralOffset, await src.read(dir.centralOffset, dir.centralLimit));
80
80
  const entries = await withLoading(b, src, () => readCentralDirectory(b, dir.centralOffset, dir.centralLimit, dir.entryCount));
81
- for (const entry of entries) {
81
+ for (const entry of [...entries].sort((x, y) => x.headerOffset - y.headerOffset)) {
82
82
  const at = entry.headerOffset;
83
83
  if (at + 30 > dir.centralOffset || b.has(at, at + 30)) continue;
84
84
  const fixed = await src.read(at, at + 30);
@@ -503,9 +503,9 @@ async function openWorkbookStream(source, options) {
503
503
  if (entry.kind !== "worksheet" || entry.part === null) return [];
504
504
  const cached = noteCache.get(entry.part);
505
505
  if (cached !== void 0) return cached;
506
- const parts = pkg.relationships(entry.part).filter((r) => !r.external && ["comments", "vmlDrawing"].includes(classifyRelationship(r.type)?.name ?? "") && pkg.hasPart(r.target));
506
+ const parts = pkg.relationships(entry.part).filter((r) => !r.external && ["comments", "vmlDrawing"].includes(classifyRelationship(r.type)?.name ?? "") && pkg.hasPart(r.target) && !entry.foreignParts.has(partKey(r.target)));
507
507
  if (parts.length > 0) await preloaded.load(parts.map((r) => zipName(r.target)));
508
- const notes = Object.freeze(readNotes(pkg, entry.part, entry.name, limits, warnings));
508
+ const notes = Object.freeze(readNotes(pkg, entry.part, entry.name, limits, warnings, entry.foreignParts));
509
509
  noteCache.set(entry.part, notes);
510
510
  return notes;
511
511
  }
@@ -538,7 +538,8 @@ async function openWorkbookStream(source, options) {
538
538
  conformance: info.conformance,
539
539
  budget: {
540
540
  remaining: Infinity,
541
- sharedText: limits.maxSharedStringChars
541
+ sharedText: limits.maxSharedStringChars,
542
+ formulaText: limits.maxSharedStringChars
542
543
  },
543
544
  formatCache
544
545
  }, (number, cells, rowInfo, styledCells) => ready.push({
@@ -638,6 +639,7 @@ async function* readTableStream(source, options) {
638
639
  }
639
640
  const limits = o.get("limits");
640
641
  const book = await openWorkbookStream(source, limits === void 0 ? void 0 : { limits });
642
+ const outputCells = limits?.maxCells ?? DEFAULT_READ_LIMITS.maxCells;
641
643
  const sheetName = o.get("sheet") ?? (book.sheets.find((s) => s.kind === "worksheet" && s.state === "visible") ?? book.sheets[0])?.name;
642
644
  if (sheetName === void 0) return;
643
645
  const buffered = [];
@@ -659,17 +661,25 @@ async function* readTableStream(source, options) {
659
661
  headerRow = header;
660
662
  }
661
663
  const source = headerRow === null ? nonEmpty.flatMap((r) => r.cells) : nonEmpty.find((r) => r.number === headerRow)?.cells ?? [];
662
- const minCol = Math.min(...source.map((c) => c.col));
663
- const maxCol = Math.max(...source.map((c) => c.col));
664
+ let minCol = Infinity;
665
+ let maxCol = -Infinity;
666
+ for (const c of source) {
667
+ if (c.col < minCol) minCol = c.col;
668
+ if (c.col > maxCol) maxCol = c.col;
669
+ }
670
+ const headerCells = new Map(headerRow === null ? [] : source.map((c) => [c.col, c]));
664
671
  const out = [];
665
672
  const used = /* @__PURE__ */ new Set();
673
+ const nextSuffix = /* @__PURE__ */ new Map();
666
674
  for (let col = minCol; col <= maxCol; col++) {
667
675
  const letter = columnLetters(col);
668
- const cell = headerRow === null ? void 0 : source.find((c) => c.col === col);
676
+ const cell = headerCells.get(col);
669
677
  const text = cell === void 0 || cell.value === null ? null : cell.value instanceof Date ? isoDate(cell.value) : String(cell.value);
670
678
  const base = text === null || text.trim() === "" ? letter : text.trim();
671
679
  let key = base;
672
- for (let n = 2; used.has(key); n++) key = `${base}_${String(n)}`;
680
+ let n = nextSuffix.get(base) ?? 2;
681
+ while (used.has(key)) key = `${base}_${String(n++)}`;
682
+ nextSuffix.set(base, n);
673
683
  used.add(key);
674
684
  out.push({
675
685
  key,
@@ -680,8 +690,13 @@ async function* readTableStream(source, options) {
680
690
  }
681
691
  return out;
682
692
  };
693
+ let positions = 0;
694
+ let cellsRead = 0;
683
695
  const emit = async (r, cols) => {
684
696
  if (headerRow !== null && r.number <= headerRow) return null;
697
+ cellsRead += r.cells.length;
698
+ positions += cols.length;
699
+ checkOutputPositions(positions, cellsRead, outputCells, "The table", sheetName);
685
700
  const byCol = new Map(r.cells.map((c) => [c.col, c]));
686
701
  if (cols.every((c) => byCol.get(c.col)?.value == null)) return null;
687
702
  const row = {};
@@ -77,7 +77,7 @@ interface ReadLimits {
77
77
  readonly maxXmlAttributes: number;
78
78
  /** Maximum length of an XML element or attribute name. Default 1 024 characters. */
79
79
  readonly maxXmlNameLength: number;
80
- /** Maximum length of one XML text node or attribute value. Default 8 Mi characters. */
80
+ /** Maximum length of one XML text node, attribute value, comment or processing instruction. Default 8 Mi characters. */
81
81
  readonly maxXmlTextLength: number;
82
82
  /** Maximum number of elements plus attributes in a part that is parsed into a tree (workbook, styles, relationships). Default 1 000 000. */
83
83
  readonly maxXmlTreeElements: number;
@@ -87,7 +87,8 @@ interface ReadLimits {
87
87
  * Maximum total length, in UTF-16 code units, of the shared strings that cells refer to, each
88
88
  * reference counted again. Protects against a small file that points thousands of cells at
89
89
  * one huge shared string (a few MB that become many GB once values are copied, e.g. to CSV).
90
- * Default 512 Mi.
90
+ * The same limit applies, counted separately, to shared formulas: the length of the formula of
91
+ * each cell that shares one, as moved to that cell. Default 512 Mi.
91
92
  */
92
93
  readonly maxSharedStringChars: number;
93
94
  }
@@ -111,7 +112,7 @@ declare const DEFAULT_READ_LIMITS: Readonly<ReadLimits>;
111
112
  * console.log(ERROR_CODES.includes('XLV_E_SHEET_NOT_FOUND')); // true
112
113
  * ```
113
114
  */
114
- declare const ERROR_CODES: readonly ["XLV_E_INVALID_INPUT", "XLV_E_INVALID_OPTION", "XLV_E_INVALID_ARGUMENT", "XLV_E_LIMIT_EXCEEDED", "XLV_E_NOT_ZIP", "XLV_E_UNSUPPORTED_CONTAINER", "XLV_E_ZIP_CORRUPT", "XLV_E_ZIP_UNSUPPORTED", "XLV_E_XML_MALFORMED", "XLV_E_XML_DTD_FORBIDDEN", "XLV_E_XML_ENTITY_FORBIDDEN", "XLV_E_XML_ENCODING", "XLV_E_PART_MISSING", "XLV_E_PART_INVALID", "XLV_E_INVALID_CELL_REF", "XLV_E_SHEET_NOT_FOUND", "XLV_E_TABLE_NOT_FOUND", "XLV_E_FILE_READ", "XLV_E_INVALID_VALUE", "XLV_E_INVALID_STYLE", "XLV_E_INVALID_SHEET_NAME", "XLV_E_DUPLICATE_SHEET_NAME", "XLV_E_INVALID_DEFINED_NAME", "XLV_E_INVALID_HYPERLINK", "XLV_E_INVALID_TABLE", "XLV_E_INVALID_IMAGE", "XLV_E_OVERLAPPING_MERGE", "XLV_E_FILE_EXISTS", "XLV_E_FILE_WRITE", "XLV_E_HEADER_NOT_FOUND", "XLV_E_SCHEMA_INVALID", "XLV_E_ASYNC_SCHEMA", "XLV_E_INVALID_ROWS", "XLV_E_JSON_INVALID", "XLV_E_CSV_MALFORMED", "XLV_E_NOT_SUPPORTED", "XLV_E_USAGE", "XLV_E_INTERNAL"];
115
+ declare const ERROR_CODES: readonly ["XLV_E_INVALID_INPUT", "XLV_E_INVALID_OPTION", "XLV_E_INVALID_ARGUMENT", "XLV_E_LIMIT_EXCEEDED", "XLV_E_NOT_ZIP", "XLV_E_UNSUPPORTED_CONTAINER", "XLV_E_ZIP_CORRUPT", "XLV_E_ZIP_UNSUPPORTED", "XLV_E_XML_MALFORMED", "XLV_E_XML_DTD_FORBIDDEN", "XLV_E_XML_ENTITY_FORBIDDEN", "XLV_E_XML_ENCODING", "XLV_E_PART_MISSING", "XLV_E_PART_INVALID", "XLV_E_INVALID_CELL_REF", "XLV_E_SHEET_NOT_FOUND", "XLV_E_TABLE_NOT_FOUND", "XLV_E_FILE_READ", "XLV_E_INVALID_VALUE", "XLV_E_INVALID_STYLE", "XLV_E_INVALID_SHEET_NAME", "XLV_E_DUPLICATE_SHEET_NAME", "XLV_E_INVALID_DEFINED_NAME", "XLV_E_INVALID_HYPERLINK", "XLV_E_INVALID_TABLE", "XLV_E_INVALID_IMAGE", "XLV_E_OVERLAPPING_MERGE", "XLV_E_FILE_EXISTS", "XLV_E_FILE_WRITE", "XLV_E_HEADER_NOT_FOUND", "XLV_E_SCHEMA_INVALID", "XLV_E_ASYNC_SCHEMA", "XLV_E_INVALID_ROWS", "XLV_E_JSON_INVALID", "XLV_E_CSV_MALFORMED", "XLV_E_NOT_SUPPORTED", "XLV_E_USAGE", "XLV_E_NOT_INSTALLED", "XLV_E_INTERNAL", "XLV_E_PATH_NOT_ALLOWED"];
115
116
  /**
116
117
  * Every warning code xlvane can report. Warnings never stop reading; they describe something
117
118
  * in the file that the caller may want to know about. Each one is described in docs/errors.md.
@@ -122,7 +123,7 @@ declare const ERROR_CODES: readonly ["XLV_E_INVALID_INPUT", "XLV_E_INVALID_OPTIO
122
123
  * console.log(WARNING_CODES.includes('XLV_W_MACROS_PRESENT')); // true
123
124
  * ```
124
125
  */
125
- declare const WARNING_CODES: readonly ["XLV_W_DIMENSION_MISMATCH", "XLV_W_SHARED_FORMULA_NOT_EXPANDED", "XLV_W_FORMULA_NO_CACHED_VALUE", "XLV_W_VALUES_IN_MERGED_CELLS", "XLV_W_VERY_HIDDEN_SHEET", "XLV_W_MACROS_PRESENT", "XLV_W_EXTERNAL_LINKS", "XLV_W_DUPLICATE_CELL", "XLV_W_INVALID_VALUE", "XLV_W_SHEET_PART_MISSING", "XLV_W_PART_MISSING", "XLV_W_FORMULAS_NOT_RECALCULATED", "XLV_W_INVALID_CELL_REF", "XLV_W_UNUSUAL_HYPERLINK", "XLV_W_THREADED_COMMENTS", "XLV_W_DRAWING_CONTENT_NOT_READ", "XLV_W_MERGED_CELL_WRITE"];
126
+ declare const WARNING_CODES: readonly ["XLV_W_DIMENSION_MISMATCH", "XLV_W_SHARED_FORMULA_NOT_EXPANDED", "XLV_W_FORMULA_NO_CACHED_VALUE", "XLV_W_VALUES_IN_MERGED_CELLS", "XLV_W_VERY_HIDDEN_SHEET", "XLV_W_MACROS_PRESENT", "XLV_W_EXTERNAL_LINKS", "XLV_W_DUPLICATE_CELL", "XLV_W_INVALID_VALUE", "XLV_W_SHEET_PART_MISSING", "XLV_W_SHEET_PART_SHARED", "XLV_W_PART_MISSING", "XLV_W_FORMULAS_NOT_RECALCULATED", "XLV_W_INVALID_CELL_REF", "XLV_W_UNUSUAL_HYPERLINK", "XLV_W_THREADED_COMMENTS", "XLV_W_DRAWING_CONTENT_NOT_READ", "XLV_W_MERGED_CELL_WRITE", "XLV_W_LIMIT_REACHED"];
126
127
  /**
127
128
  * A stable error code, see {@link ERROR_CODES}.
128
129
  *
@@ -2594,7 +2595,7 @@ interface Sheet {
2594
2595
  readonly sheetId: number | null;
2595
2596
  readonly kind: SheetKind;
2596
2597
  readonly state: SheetState;
2597
- /** Part name of the sheet, for example `/xl/worksheets/sheet1.xml`; `null` if it is missing. */
2598
+ /** Part name of the sheet, for example `/xl/worksheets/sheet1.xml`; `null` if it is missing or is the part of an earlier sheet (`XLV_W_SHEET_PART_SHARED`). */
2598
2599
  readonly part: string | null;
2599
2600
  /** The used range declared in the file (`dimension` element), or `null`. */
2600
2601
  readonly dimension: string | null;
@@ -4675,7 +4676,9 @@ declare const STREAM_LIMIT_DEFAULTS: Partial<ReadLimits>;
4675
4676
  */
4676
4677
  declare function openWorkbookStream(source: Uint8Array | ArrayBuffer | Blob, options?: OpenWorkbookStreamOptions): Promise<WorkbookStream>;
4677
4678
  /**
4678
- * Options for {@link readTableStream}.
4679
+ * Options for {@link readTableStream}. As with `readTable`, at most `max(limits.maxCells, 16 ×
4680
+ * cells read)` row properties are produced (`limits.maxCells` defaults to 5 000 000 here,
4681
+ * although it does not limit the cells streamed).
4679
4682
  *
4680
4683
  * @example
4681
4684
  * ```ts
@@ -0,0 +1,43 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "mcp.schema.json",
4
+ "title": "xlvane mcp --json (errors only; the server itself speaks MCP)",
5
+ "oneOf": [
6
+ {
7
+ "type": "object",
8
+ "properties": {
9
+ "xlvane": {
10
+ "const": 1
11
+ },
12
+ "command": {
13
+ "const": "mcp"
14
+ },
15
+ "ok": {
16
+ "const": true
17
+ },
18
+ "result": {
19
+ "type": "object",
20
+ "properties": {
21
+ "exitCode": {
22
+ "type": "integer"
23
+ }
24
+ },
25
+ "required": [
26
+ "exitCode"
27
+ ],
28
+ "additionalProperties": false
29
+ }
30
+ },
31
+ "required": [
32
+ "xlvane",
33
+ "command",
34
+ "ok",
35
+ "result"
36
+ ],
37
+ "additionalProperties": false
38
+ },
39
+ {
40
+ "$ref": "common.schema.json#/$defs/errorEnvelope"
41
+ }
42
+ ]
43
+ }
package/docs/cli-json.md CHANGED
@@ -53,7 +53,7 @@ The error envelope is defined in [cli-json/common.schema.json](cli-json/common.s
53
53
  | Code | Meaning |
54
54
  |---|---|
55
55
  | 0 | Success. |
56
- | 1 | The command ran, and a check found something: `diff` found differences, or `validate` found problems. |
56
+ | 1 | The command ran, and a check found something: `diff` found differences, or `validate` found problems. `mcp` returns the exit code of the server (0, or 2 and 3 as below). |
57
57
  | 2 | Usage error (`XLV_E_USAGE`): unknown command or option, missing or extra argument, invalid option value. |
58
58
  | 3 | Input or file error: the file cannot be read, is not a valid workbook, the sheet does not exist, the output exists without `--force`, and so on. |
59
59
 
@@ -69,3 +69,4 @@ The error envelope is defined in [cli-json/common.schema.json](cli-json/common.s
69
69
  | `diff` | [cli-json/diff.schema.json](cli-json/diff.schema.json) | The `diff()` result: `equal`, `sheetsAdded`, `sheetsRemoved`, `sheets` (cell changes with `before` and `after`), `summary`. Every change is included. |
70
70
  | `validate` | [cli-json/validate.schema.json](cli-json/validate.schema.json) | `file`, `ok`, `sheet`, `rowsChecked`, `problems` (`kind`, `column`, `cell`, `row`, `message`), `warnings`. |
71
71
  | `migrate` | [cli-json/migrate.schema.json](cli-json/migrate.schema.json) | `directory`, `packageJson`, `changes` (`section`, `from`, `to`), `alreadyMigrated`, `enginesNode`, `filesScanned`, `scanLimited`, `findings` (`file`, `line`, `column`, `api`, `status`), `written`. |
72
+ | `mcp` | [cli-json/mcp.schema.json](cli-json/mcp.schema.json) | `--json` is refused with a usage error (exit code 2), printed as this document, because stdout carries the MCP protocol. |
package/docs/errors.md CHANGED
@@ -61,11 +61,13 @@ Each code links to its section, which says what to do. The CLI prints the code a
61
61
  | [`XLV_W_MACROS_PRESENT`](#xlv_w_macros_present) | The workbook contains a VBA project or Excel 4.0 macro sheets. |
62
62
  | [`XLV_W_EXTERNAL_LINKS`](#xlv_w_external_links) | The workbook links to other workbooks. |
63
63
  | [`XLV_W_DUPLICATE_CELL`](#xlv_w_duplicate_cell) | A sheet defines the same cell more than once. |
64
+ | [`XLV_W_LIMIT_REACHED`](#xlv_w_limit_reached) | Reading stopped computing something derived from the file because the work would be far out of proportion to its data. |
64
65
  | [`XLV_W_INVALID_VALUE`](#xlv_w_invalid_value) | A value does not match its declared type and is returned in a safe form, for example a numeric cell whose text is not a number (returned … |
65
66
  | [`XLV_W_UNUSUAL_HYPERLINK`](#xlv_w_unusual_hyperlink) | A sheet has hyperlinks whose external target uses a scheme other than `http`, `https` or `mailto`, for example `file:`, `javascript:` or … |
66
67
  | [`XLV_W_DRAWING_CONTENT_NOT_READ`](#xlv_w_drawing_content_not_read) | The drawing of a sheet holds objects other than pictures: charts, shapes, connectors, group shapes, content parts or alternate content. |
67
68
  | [`XLV_W_THREADED_COMMENTS`](#xlv_w_threaded_comments) | A sheet has threaded comments, the conversations of current Excel versions ([MS-XLSX] §2.3.7). |
68
69
  | [`XLV_W_SHEET_PART_MISSING`](#xlv_w_sheet_part_missing) | A sheet listed in the workbook has no part (the relationship or the part itself is missing). |
70
+ | [`XLV_W_SHEET_PART_SHARED`](#xlv_w_sheet_part_shared) | Two or more sheets listed in the workbook lead to the same worksheet part, or relate to the same comments, VML or table part. |
69
71
  | [`XLV_W_PART_MISSING`](#xlv_w_part_missing) | The workbook points to a styles or shared strings part that does not exist. |
70
72
  | [`XLV_W_FORMULAS_NOT_RECALCULATED`](#xlv_w_formulas_not_recalculated) | Reported by `editCells` and `appendRows` when the workbook contains formulas. |
71
73
  | [`XLV_W_INVALID_CELL_REF`](#xlv_w_invalid_cell_ref) | A worksheet contains a row whose number is not a whole number from 1 to 1048576, a cell whose `r` reference is not a valid A1 reference … |
@@ -89,6 +91,14 @@ An argument of a method of `@xlvane/compat` is not valid, in a case where excelj
89
91
  ### XLV_E_LIMIT_EXCEEDED
90
92
  The file exceeds one of the resource limits (`ReadLimits`): input size, number of ZIP entries, decompressed size of one entry or of all entries, compression ratio, XML nesting depth, attribute count, name or text length, number of elements in a tree-parsed part, or number of cells. `details.limit` names the limit and `details.value` its current value. Sizes are measured on the bytes actually produced by decompression, so a file cannot avoid a limit by declaring false sizes.
91
93
 
94
+ Some limits also bound work derived from a small file (1.1.1, D-H01):
95
+
96
+ - `maxCells` (`details.limit` `maxCells`, `details.value` the budget): `toCSV`, `readTable`, `toJSON`, `readTableStream`, `xlvane read` and `xlvane convert` write at most `max(maxCells, 16 × cells read)` positions (rows × columns of the output), so a few cells far apart cannot make them produce a value for every position between; `@xlvane/compat`'s `xlsx.load` creates at most as many cells for merged ranges, tables and column definitions, and looks up at most 64 Mi rows (or 16 per cell read) for row settings up to the sheets' dimensions. Pass a smaller `range`, or raise `maxCells` for a trusted file.
97
+ - `maxXmlTextLength`: also the length of an XML comment or processing instruction.
98
+ - `maxSharedStringChars`: also the text of shared formulas, counted apart from shared strings: the length of each sharing cell's formula, as moved to that cell.
99
+
100
+ The MCP server (`@xlvane/mcp`) returns it as a tool error for a file larger than `--max-file-size` (`details.limit` `maxFileBytes`) and for a result longer than `--max-response-chars` (`details.limit` `maxResponseChars`; `read_table` instead returns the rows that fit).
101
+
92
102
  `fromCSV` (and `xlvane convert`/`create` with CSV input) throws it for text with more fields than `maxCells` (default 5 000 000, `details.limit` `maxCells`), more than 16 384 fields on a line or more than 1 048 576 lines.
93
103
 
94
104
  `NewSheet.validate` throws it when a sheet would get more than 65534 data validations ([MS-OI29500] §2.1.614 g; `details.limit` `dataValidations`). When reading, the conditional formatting and data validations of one sheet count against `maxXmlTreeElements`.
@@ -162,6 +172,7 @@ A part exists but has the wrong structure: for example the workbook part's root
162
172
  | `target-mode` | `TargetMode` is neither `Internal` nor `External` |
163
173
  | `internal-target` | an internal relationship target cannot be resolved to a part name |
164
174
  | `equivalent-part-names` | two ZIP entries map to equivalent part names (e.g. a raw UTF-8 name and its percent-encoded form) |
175
+
165
176
  **Fix:** check that the file is an Excel workbook (not a Word or PowerPoint document) and re-save it in a spreadsheet application.
166
177
 
167
178
  ### XLV_E_INVALID_CELL_REF
@@ -263,10 +274,18 @@ For `insertRows`/`deleteRows`, insert or delete the rows in a spreadsheet applic
263
274
  The `xlvane` command line was used incorrectly: an unknown command or option, a missing or extra argument, an option without its value, or an option value that is not allowed. `details` names the command and the option or argument; `details.suggestions` lists close matches ("Did you mean ...?"). Exit code 2.
264
275
  **Fix:** run `xlvane help <command>` for the usage and examples of the command.
265
276
 
277
+ ### XLV_E_NOT_INSTALLED
278
+ `xlvane mcp` needs the separate package `@xlvane/mcp`, which could not be loaded. `details.package` names it.
279
+ **Fix:** install it next to xlvane, for example `npm install @xlvane/mcp`.
280
+
266
281
  ### XLV_E_INTERNAL
267
282
  The `xlvane` command line caught an error that is not an `XlvError`. This is a bug in xlvane. `details.name` is the error's name; `--verbose` prints the stack trace.
268
283
  **Fix:** report it with the command and, if possible, the input file (see SECURITY.md for files that must stay private).
269
284
 
285
+ ### XLV_E_PATH_NOT_ALLOWED
286
+ The MCP server (`@xlvane/mcp`) was asked to read or write a path outside the directories it was started with (`--root`), including paths that leave them through `..` or a symbolic link, or to write through a symbolic link. Nothing was read or written. `details.path` is the path as given and `details.roots` lists the allowed directories.
287
+ **Fix:** use a path inside one of the listed directories, or restart the server with another `--root`.
288
+
270
289
  ## Warnings
271
290
 
272
291
  ### XLV_W_DIMENSION_MISMATCH
@@ -289,6 +308,10 @@ Some merged ranges have values in cells other than the top-left one. Spreadsheet
289
308
  `editCells` or `appendRows` wrote a value into a cell of a merged range other than its top-left cell. The value is stored, but spreadsheet applications show only the top-left cell of a merged range, so the value is not visible. `cells` lists the cells written (up to ten); the message names the first one and its merged range.
290
309
  **Fix:** write the value into the top-left cell of the range (named in the message), or unmerge the range first.
291
310
 
311
+ ### XLV_W_LIMIT_REACHED
312
+ Reading stopped computing something derived from the file because the work would be far out of proportion to its data, and the file is read without it. Today: hyperlinks whose ranges cover far more positions than the sheet has cells (more than 16 visits per cell plus 1 000 000); `sheet.hyperlinks` lists every link, but `cell.hyperlink` is not set for the cells of the remaining links (D-H01).
313
+ **Fix:** nothing, if the file comes from an unknown source: this protects the reader. Look up a cell's link in `sheet.hyperlinks` by its range.
314
+
292
315
  ### XLV_W_VERY_HIDDEN_SHEET
293
316
  A sheet has the state `veryHidden`: spreadsheet applications offer no menu command to unhide it, so users usually do not know it exists.
294
317
  **Fix:** nothing, if the sheet is expected; `inspect` lists it with its state, and `workbook.sheets` includes it.
@@ -325,6 +348,10 @@ A sheet has threaded comments, the conversations of current Excel versions ([MS-
325
348
  A sheet listed in the workbook has no part (the relationship or the part itself is missing). The sheet is returned with `part: null` and no cells.
326
349
  **Fix:** re-save the file in the application that made it; the sheet's cells are not in the file.
327
350
 
351
+ ### XLV_W_SHEET_PART_SHARED
352
+ Two or more sheets listed in the workbook lead to the same worksheet part, or relate to the same comments, VML or table part (part names compared without regard to case, as OPC requires). Spreadsheet applications write one such part per sheet; reading a shared part again for every sheet would decompress and parse it many times, outside the limit on decompressed bytes. The part is read for the first of these sheets only: for a worksheet part, the others are returned with `part: null` and no cells; for a comments, VML or table part, the others are read without its notes or table. A sheet that relates to one table part twice gets that table once. The location names the sheet and the part.
353
+ **Fix:** re-save the file in the application that made it, and check whether the sheets returned without cells should hold data.
354
+
328
355
  ### XLV_W_PART_MISSING
329
356
  The workbook points to a styles or shared strings part that does not exist. Reading continues without it: without styles, date-formatted numbers are returned as numbers; without shared strings, shared-string cells are treated as empty.
330
357
  **Fix:** re-save the file in the application that made it; check number formats and text cells of the result.
@@ -38,7 +38,7 @@ Status: **supported** means the same behaviour as exceljs for the cases covered
38
38
  | `xlsx.write(stream)` | supported | | `lib/xlsx/xlsx.js` L643 |
39
39
  | `xlsx.createInputStream()` | supported | Throws the deprecation error, as in exceljs 4. | `lib/xlsx/xlsx.js` L238 |
40
40
  | `csv.read(stream, options)`, `csv.readFile(path, options)` | supported | Adds a worksheet, with exceljs's options (`sheetName`, `dateFormats`, `map`, `parserOptions`: `delimiter`, `quote`, `escape`, `trim`/`ltrim`/`rtrim`, `ignoreEmpty`, `comment`, `skipLines`, `skipRows`, `maxRows`, `headers`, `renameHeaders`, `encoding`) and its default conversion of fields (X4-23), on xlvane's own parser (no fast-csv). Accepts a Node.js readable stream, a Web `ReadableStream` or any (async) iterable of strings or bytes. Differences: text of white space only stays text (exceljs reads `' '` as 0), `-Infinity` stays text (it cannot be stored), a leading byte order mark is dropped (exceljs keeps it in the first field), `headers: true` skips the header line (exceljs throws `data.map is not a function`), a missing file or a malformed line throws `XLV_E_FILE_READ` / `XLV_E_CSV_MALFORMED`. `readFile` needs `node:fs` (Node.js, Bun, Deno). Dates are parsed as dayjs 1.11 customParseFormat parses them in strict mode (D-E06): letters that are no token, such as an unbracketed `T` or `dddd`, are skipped, and each token is searched in the rest of the text. An unclosed quote is reported on the line where it opens; a `quote` or `escape` longer than one character throws `XLV_E_INVALID_OPTION`. | `lib/csv/csv.js` L32–98 |
41
- | `csv.write(stream, options)`, `csv.writeFile(path, options)`, `csv.writeBuffer(options)` | supported | Writes one worksheet with exceljs's options (`sheetName`, `sheetId`, `dateFormat`, `dateUTC`, `map`, `includeEmptyRows`, `encoding`, `formatterOptions`: `delimiter`, `rowDelimiter`, `quote`, `escape`, `quoteColumns`, `headers`, `writeHeaders`, `includeEndRowDelimiter`, `writeBOM`) and fast-csv's quoting (X4-24); the cross-check sample gives the same text as exceljs 4.4.0. Differences: text that starts with `=`, `+`, `-`, `@`, tab, CR or LF is written with a leading `'` (CSV injection, docs/facts/csv.md CSV-10; `formulaInjection: "off"` writes it unchanged, as exceljs); a formula result of 0 or `false` is written (exceljs writes an empty field), a date result is formatted as a date and an error result as its text; one blank line per missing row also before the first row (exceljs writes one too few there); a `sheetName` or `sheetId` that does not exist throws `XLV_E_SHEET_NOT_FOUND` (exceljs writes an empty file); header names in `formatterOptions.headers` are escaped like values. With `headers`, an array row gives one field per header (longer rows cut, shorter ones end in empty fields), as fast-csv writes it; `dateFormat: ''` uses the default format, as dayjs does (D-E06). `writeFile` writes `utf8`, `utf16le` or `latin1` and needs `node:fs`. | `lib/csv/csv.js` L109–186 |
41
+ | `csv.write(stream, options)`, `csv.writeFile(path, options)`, `csv.writeBuffer(options)` | supported | Writes one worksheet with exceljs's options (`sheetName`, `sheetId`, `dateFormat`, `dateUTC`, `map`, `includeEmptyRows`, `encoding`, `formatterOptions`: `delimiter`, `rowDelimiter`, `quote`, `escape`, `quoteColumns`, `headers`, `writeHeaders`, `includeEndRowDelimiter`, `writeBOM`) and fast-csv's quoting (X4-24); the cross-check sample gives the same text as exceljs 4.4.0. Differences: text that starts with `=`, `+`, `-`, `@`, tab, CR, LF or another control character, or with a full-width or small form of these signs, is written with a leading `'` (CSV injection, docs/facts/csv.md CSV-10, CSV-12; `formulaInjection: "off"` writes it unchanged, as exceljs); a formula result of 0 or `false` is written (exceljs writes an empty field), a date result is formatted as a date and an error result as its text; one blank line per missing row also before the first row (exceljs writes one too few there); a `sheetName` or `sheetId` that does not exist throws `XLV_E_SHEET_NOT_FOUND` (exceljs writes an empty file); header names in `formatterOptions.headers` are escaped like values. With `headers`, an array row gives one field per header (longer rows cut, shorter ones end in empty fields), as fast-csv writes it; `dateFormat: ''` uses the default format, as dayjs does (D-E06). `writeFile` writes `utf8`, `utf16le` or `latin1` and needs `node:fs`. | `lib/csv/csv.js` L109–186 |
42
42
  | `csv.createInputStream()` | supported | Throws the deprecation error, as in exceljs 4. | `lib/csv/csv.js` L101 |
43
43
  | `definedNames` (`add`, `addEx`, `remove`, `removeEx`, `removeAllNames`, `forEach`, `getNames`, `getNamesEx`, `getRanges`, `getMatrix`, `model`) | supported | As in exceljs, a name is a set of cells, written as rectangles with absolute references and no sheet scope; on reading, names that are not cell ranges (constants, formulas, `#REF!`) and the sheet scope (`localSheetId`) are dropped. Built-in `_xlnm.` names are not listed: print areas and titles are read into `pageSetup`, as in exceljs, and xlvane writes the autofilter name itself. Names exceljs accepts but Excel does not (for example `A1` or names starting with `_xl`) throw `XLV_E_INVALID_DEFINED_NAME` when the workbook is written. | `lib/doc/defined-names.js`; `lib/xlsx/xform/book/defined-name-xform.js`; `lib/xlsx/xform/book/workbook-xform.js` L187 |
44
44
  | `addImage(image)`, `getImage(id)` | supported | PNG, JPEG and GIF given as `buffer`, `base64` (a data URL is accepted, as in exceljs) or `filename`; the format is recognised from the bytes, `extension` is kept but not used (exceljs writes `image/jpg` for `extension: 'jpg'`). As in exceljs, a `filename` is read when the workbook is written (`xlsx.writeBuffer`/`writeFile`/`write` and the streaming `WorkbookWriter`), so a missing file fails there; outside Node.js, Bun and Deno it gives `XLV_E_NOT_SUPPORTED`. Other formats throw `XLV_E_INVALID_IMAGE`. Read back, each image part is one media entry `{ type, name, extension, buffer }`, `buffer` a `Uint8Array`. Identical images are stored once, with one relationship per drawing (exceljs writes one per use). | `lib/doc/workbook.js` L142–151; `lib/xlsx/xlsx.js` L421–442 (L426–429: `filename` read at write) |