xlvane 1.2.1 → 1.2.3

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 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";
1
+ import { $ as fillXml, B as tokenize, Bt as checkKeyText, Cn as normalizeLineEnds, Ct as relationshipsPartName, Dn as escapeAttr, En as encodeXstring, Ft as mergeStyles, G as DEFAULT_FONT, H as X14_DV_URI, Ht as resolveLimits, I as WarningCollector, It as normalizeStyle, J as borderXml, Lt as stableKey, M as openZip, Mt as parseStyles, Nt as READING_ORDERS, On as escapeText, Pt as checkStyleCount, Rt as isDateFormat, Sn as decodeXml, St as openPackage, Tt as zipNameToPartName, Ut as suggest, V as X14_CF_URI, Vt as checkOutputPositions, _ as isoDate, an as classifyRelationship, at as reusableBuiltinNumFmt, b as readWorkbook, bn as parseXmlTree, bt as parseIsoDateTime, cn as MAX_ROWS, dn as formatRange, et as fontXml, f as createWorkbook, fn as parseCellRef, ft as stringItemXml, h as detectHeader, i as readOptions, in as SML_NS, jn as XlvError, jt as EMPTY_STYLES, ln as columnLetters, m as writeWorkbook, mt as textElement, n as oneOfOption, on as relationshipTypes, pn as parseRange, q as attr, r as openInput, rn as REL_NS, sn as MAX_COLUMNS, t as booleanOption, tt as formatNumber, un as formatCellRef, vn as childNamed, vt as writeZip, wn as parseXml, wt as partKey, x as toBytes, xt as parseSharedStrings, yn as childrenNamed, z as shiftFormula, zt as DEFAULT_READ_LIMITS } from "./options-Cw7OraxE.js";
2
2
  // src/table.ts
3
3
  const READ_OPTIONS = [
4
4
  "sheet",
@@ -199,6 +199,7 @@ function prepare(input, options) {
199
199
  let cellCount = 0;
200
200
  for (const cells of byRow.values()) cellCount += cells.size;
201
201
  checkOutputPositions(numbers.length * columns.length, cellCount, limits.maxCells, "The table", sheet.name);
202
+ checkKeyText(columns, numbers.length, limits.maxSharedStringChars, sheet.name);
202
203
  for (const r of numbers) {
203
204
  const cells = byRow.get(r);
204
205
  if (skipEmpty && (cells === void 0 || [...cells.values()].every((c) => c.value === null))) continue;
@@ -241,7 +242,7 @@ function toIssue(issue, row, columns) {
241
242
  return typeof key === "number" ? key : String(key);
242
243
  });
243
244
  const first = path[0];
244
- const column = typeof first === "string" ? columns.find((c) => c.key === first) : void 0;
245
+ const column = typeof first === "string" ? columns.get(first) : void 0;
245
246
  return {
246
247
  row,
247
248
  cell: column === void 0 ? null : formatCellRef(row, column.col),
@@ -254,9 +255,11 @@ function collect(prepared, results) {
254
255
  const rows = [];
255
256
  const rowNumbers = [];
256
257
  const issues = [];
258
+ const byKey = /* @__PURE__ */ new Map();
259
+ for (const c of prepared.columns) if (!byKey.has(c.key)) byKey.set(c.key, c);
257
260
  results.forEach((result, i) => {
258
261
  const row = prepared.rowNumbers[i] ?? 0;
259
- if (result.issues) for (const issue of result.issues) issues.push(toIssue(issue, row, prepared.columns));
262
+ if (result.issues) for (const issue of result.issues) issues.push(toIssue(issue, row, byKey));
260
263
  else {
261
264
  rows.push(result.value);
262
265
  rowNumbers.push(row);
@@ -359,7 +362,8 @@ const WRITE_OPTIONS = [
359
362
  "columns",
360
363
  "sheet",
361
364
  "preset",
362
- "dateSystem"
365
+ "dateSystem",
366
+ "maxCells"
363
367
  ];
364
368
  /** Header style of the `report` preset (D-304). */
365
369
  const REPORT_HEADER = {
@@ -478,11 +482,22 @@ function addTableSheet(workbook, rows, options) {
478
482
  columns.forEach((column, c) => {
479
483
  if (hasHeader && column.header !== "") sheet.set(formatCellRef(1, c + 1), column.header, preset === "report" ? REPORT_HEADER : void 0);
480
484
  });
485
+ const formatted = [];
486
+ columns.forEach((column, c) => {
487
+ if (column.numFmt !== void 0) formatted.push(c);
488
+ });
489
+ const columnsOfKey = /* @__PURE__ */ new Map();
490
+ if (!arrays) columns.forEach((column, c) => {
491
+ const list = columnsOfKey.get(column.key);
492
+ if (list === void 0) columnsOfKey.set(column.key, [c]);
493
+ else list.push(c);
494
+ });
495
+ let cells = hasHeader ? columns.filter((c) => c.header !== "").length : 0;
496
+ const hasOwn = (o, key) => Object.prototype.hasOwnProperty.call(o, key);
481
497
  rows.forEach((row, i) => {
482
498
  const r = i + 1 + offset;
483
- columns.forEach((column, c) => {
484
- const record = row;
485
- const raw = arrays ? row[c] : Object.prototype.hasOwnProperty.call(record, column.key) ? record[column.key] : void 0;
499
+ const put = (c, raw) => {
500
+ const column = columns[c];
486
501
  const value = raw === void 0 ? null : raw;
487
502
  if (!(value === null || typeof value === "number" || typeof value === "string" || typeof value === "boolean" || value instanceof Date)) throw tableError(`Row ${String(i + 1)}, column "${column.key}" holds ${Array.isArray(value) ? "an array" : typeof value}, which is not a cell value.`, "Cell values are numbers, strings, booleans, Dates or null. Convert nested data first, for example with JSON.stringify.", {
488
503
  row: r,
@@ -491,8 +506,29 @@ function addTableSheet(workbook, rows, options) {
491
506
  if (value === null && column.numFmt === void 0) return;
492
507
  sheet.set(formatCellRef(r, c + 1), value, column.numFmt === void 0 ? void 0 : { numFmt: column.numFmt });
493
508
  widths[c] = Math.max(widths[c] ?? 0, displayLength(value));
494
- });
509
+ cells++;
510
+ };
511
+ if (arrays) {
512
+ const values = row;
513
+ const n = Math.min(values.length, columns.length);
514
+ for (let c = 0; c < n; c++) put(c, values[c]);
515
+ for (const c of formatted) if (c >= n) put(c, void 0);
516
+ return;
517
+ }
518
+ const record = row;
519
+ const own = Object.getOwnPropertyNames(record);
520
+ if (own.length >= columns.length) {
521
+ for (let c = 0; c < columns.length; c++) put(c, hasOwn(record, columns[c].key) ? record[columns[c].key] : void 0);
522
+ return;
523
+ }
524
+ const visit = [];
525
+ for (const key of own) for (const c of columnsOfKey.get(key) ?? []) visit.push(c);
526
+ for (const c of formatted) if (!hasOwn(record, columns[c].key)) visit.push(c);
527
+ visit.sort((x, y) => x - y);
528
+ for (const c of visit) put(c, hasOwn(record, columns[c].key) ? record[columns[c].key] : void 0);
495
529
  });
530
+ const maxCells = options.get("maxCells");
531
+ checkOutputPositions((rows.length + offset) * columns.length, cells, typeof maxCells === "number" ? maxCells : DEFAULT_READ_LIMITS.maxCells, "The table", sheet.name, "The rows are spread over far more columns than they fill, for example after one row with many keys or a long first CSV line. Write fewer columns (the \"columns\" option), split the data over several tables, or raise the \"maxCells\" option for a trusted sparse table.");
496
532
  columns.forEach((column, c) => {
497
533
  if (column.width !== void 0) sheet.column(c + 1, { width: column.width });
498
534
  else if (preset === "report") sheet.column(c + 1, { width: Math.min(MAX_FITTED_WIDTH, Math.max(MIN_WIDTH, (widths[c] ?? 0) + 2)) });
@@ -524,6 +560,11 @@ function addTableSheet(workbook, rows, options) {
524
560
  */
525
561
  function writeTable(rows, options) {
526
562
  const o = readOptions(options, WRITE_OPTIONS, "writeTable options");
563
+ const maxCells = o.get("maxCells");
564
+ if (maxCells !== void 0 && (typeof maxCells !== "number" || !(maxCells > 0))) throw new XlvError("XLV_E_INVALID_OPTION", "The option \"maxCells\" must be a positive number.", {
565
+ hint: "For example { maxCells: 20_000_000 }, or Infinity.",
566
+ details: { option: "maxCells" }
567
+ });
527
568
  const dateSystem = o.get("dateSystem");
528
569
  const workbook = createWorkbook(dateSystem === void 0 ? void 0 : { dateSystem });
529
570
  addTableSheet(workbook, rows, o);
@@ -578,6 +619,21 @@ const INJECTION_START = /* @__PURE__ */ new Set([
578
619
  "﹣",
579
620
  "﹫"
580
621
  ]);
622
+ /** Signs that start a formula: refused as the delimiter while formulas are escaped (D-J01). */
623
+ const FORMULA_SIGNS = /* @__PURE__ */ new Set([
624
+ "=",
625
+ "+",
626
+ "-",
627
+ "@",
628
+ "=",
629
+ "+",
630
+ "-",
631
+ "@",
632
+ "﹦",
633
+ "﹢",
634
+ "﹣",
635
+ "﹫"
636
+ ]);
581
637
  /** Whether text starting with `c` is escaped: one of INJECTION_START or a C0 control (NUL included). */
582
638
  const injectionStart = (c) => INJECTION_START.has(c) || c.charCodeAt(0) < 32;
583
639
  /**
@@ -615,6 +671,10 @@ function toCSV(input, options) {
615
671
  const newline = oneOfOption("newline", o.get("newline"), ["\r\n", "\n"], "\r\n");
616
672
  const bom = booleanOption("bom", o.get("bom"), false);
617
673
  const injection = oneOfOption("formulaInjection", o.get("formulaInjection"), ["escape", "off"], "escape");
674
+ if (injection === "escape" && FORMULA_SIGNS.has(delimiter)) throw new XlvError("XLV_E_INVALID_OPTION", `The delimiter "${delimiter}" starts a formula, so lines with an empty first field would run as formulas.`, {
675
+ hint: "Use \",\" (default), \";\", \"|\" or \"\\t\", or pass formulaInjection: \"off\" for a trusted consumer.",
676
+ details: { option: "delimiter" }
677
+ });
618
678
  const limits = resolveLimits(o.get("limits"));
619
679
  const sheet = pickSheet(openInput(input, o.get("limits"), o.get("onWarning")), o.get("sheet"));
620
680
  let startRow = 1;
@@ -913,6 +973,7 @@ function fromCSV(input, options) {
913
973
  const workbook = createWorkbook(dateSystem === void 0 ? void 0 : { dateSystem });
914
974
  const tableOptions = /* @__PURE__ */ new Map([["sheet", o.get("sheet") ?? "Sheet1"], ["preset", oneOfOption("preset", o.get("preset"), ["plain", "report"], "plain")]]);
915
975
  if (columns) tableOptions.set("columns", columns);
976
+ tableOptions.set("maxCells", maxCells);
916
977
  addTableSheet(workbook, rows, tableOptions);
917
978
  return writeWorkbook(workbook);
918
979
  }
@@ -928,13 +989,30 @@ function toDiffCell(cell, styles) {
928
989
  if (styles) out.style = cell.style;
929
990
  return out;
930
991
  }
931
- function sameCell(a, b, styles) {
992
+ /**
993
+ * Compares styles by their stable key, computed once per style object: the reader shares one
994
+ * frozen object per style, so a sheet of a million cells needs a few keys, not two per cell.
995
+ */
996
+ function styleComparer() {
997
+ const keys = /* @__PURE__ */ new Map();
998
+ const keyOf = (style) => {
999
+ if (typeof style !== "object" || style === null) return stableKey(style);
1000
+ let key = keys.get(style);
1001
+ if (key === void 0) {
1002
+ key = stableKey(style);
1003
+ keys.set(style, key);
1004
+ }
1005
+ return key;
1006
+ };
1007
+ return (a, b) => a === b || keyOf(a) === keyOf(b);
1008
+ }
1009
+ function sameCell(a, b, sameStyle) {
932
1010
  if (a.type !== b.type) return false;
933
1011
  const av = a.value instanceof Date ? a.value.getTime() : a.value;
934
1012
  const bv = b.value instanceof Date ? b.value.getTime() : b.value;
935
1013
  if (!Object.is(av, bv)) return false;
936
1014
  if ((a.formula?.text ?? null) !== (b.formula?.text ?? null)) return false;
937
- return !styles || stableKey(a.style) === stableKey(b.style);
1015
+ return sameStyle === null || sameStyle(a.style, b.style);
938
1016
  }
939
1017
  /**
940
1018
  * Compares two workbooks: added and removed sheets (by name), visibility changes, and cells
@@ -983,44 +1061,45 @@ function diff(a, b, options) {
983
1061
  summary.sheetsAdded = sheetsAdded.length;
984
1062
  summary.sheetsRemoved = sheetsRemoved.length;
985
1063
  const sheets = [];
1064
+ const sameStyle = styles ? styleComparer() : null;
986
1065
  for (const after of second.sheets) {
987
1066
  const before = firstNames.get(after.name);
988
1067
  if (!before) continue;
989
1068
  const fromFirst = [];
990
1069
  const fromSecond = [];
991
1070
  let count = 0;
992
- const record = (list, row, col, change) => {
1071
+ const record = (list, row, col, kind, change) => {
993
1072
  count++;
994
- if (change.kind === "added") summary.cellsAdded++;
995
- else if (change.kind === "removed") summary.cellsRemoved++;
1073
+ if (kind === "added") summary.cellsAdded++;
1074
+ else if (kind === "removed") summary.cellsRemoved++;
996
1075
  else summary.cellsChanged++;
997
1076
  if (list.length < maxCells) list.push({
998
1077
  row,
999
1078
  col,
1000
- change
1079
+ change: change()
1001
1080
  });
1002
1081
  };
1003
1082
  for (const cell of before.cells()) {
1004
1083
  const other = after.cell(cell.ref);
1005
- if (other === void 0) record(fromFirst, cell.row, cell.col, {
1084
+ if (other === void 0) record(fromFirst, cell.row, cell.col, "removed", () => ({
1006
1085
  ref: cell.ref,
1007
1086
  kind: "removed",
1008
1087
  before: toDiffCell(cell, styles),
1009
1088
  after: null
1010
- });
1011
- else if (!sameCell(cell, other, styles)) record(fromFirst, cell.row, cell.col, {
1089
+ }));
1090
+ else if (!sameCell(cell, other, sameStyle)) record(fromFirst, cell.row, cell.col, "changed", () => ({
1012
1091
  ref: cell.ref,
1013
1092
  kind: "changed",
1014
1093
  before: toDiffCell(cell, styles),
1015
1094
  after: toDiffCell(other, styles)
1016
- });
1095
+ }));
1017
1096
  }
1018
- for (const cell of after.cells()) if (before.cell(cell.ref) === void 0) record(fromSecond, cell.row, cell.col, {
1097
+ for (const cell of after.cells()) if (before.cell(cell.ref) === void 0) record(fromSecond, cell.row, cell.col, "added", () => ({
1019
1098
  ref: cell.ref,
1020
1099
  kind: "added",
1021
1100
  before: null,
1022
1101
  after: toDiffCell(cell, styles)
1023
- });
1102
+ }));
1024
1103
  const stateChanged = before.state !== after.state;
1025
1104
  if (count === 0 && !stateChanged) continue;
1026
1105
  summary.sheetsChanged++;
@@ -1771,7 +1850,9 @@ function editSheetXml(text, part, sheet, planned, limits) {
1771
1850
  for (const list of byRow.values()) list.sort((a, b) => a.col - b.col);
1772
1851
  const forceRefs = rows.some((r) => !r.hasR);
1773
1852
  const emitted = /* @__PURE__ */ new Set();
1774
- const pending = [...byRow.keys()].filter((n) => !rows.some((r) => r.number === n)).sort((a, b) => a - b);
1853
+ const existingRows = new Set(rows.map((r) => r.number));
1854
+ const pending = [...byRow.keys()].filter((n) => !existingRows.has(n)).sort((a, b) => a - b);
1855
+ let next = 0;
1775
1856
  const p = (xml) => withPrefix(xml, prefix);
1776
1857
  const newRow = (n) => {
1777
1858
  const cells = (byRow.get(n) ?? []).filter((c) => c.xml !== null).map((c) => p(c.xml ?? "")).join("");
@@ -1779,7 +1860,7 @@ function editSheetXml(text, part, sheet, planned, limits) {
1779
1860
  };
1780
1861
  let inner = "";
1781
1862
  for (const row of rows) {
1782
- while (pending.length > 0 && (pending[0] ?? 0) < row.number) inner += newRow(pending.shift() ?? 0);
1863
+ while (next < pending.length && (pending[next] ?? 0) < row.number) inner += newRow(pending[next++] ?? 0);
1783
1864
  const edits = byRow.get(row.number);
1784
1865
  const needsRefs = forceRefs || row.cells.some((c) => !c.hasR);
1785
1866
  if (!edits && row.hasR && !needsRefs) {
@@ -1803,7 +1884,7 @@ function editSheetXml(text, part, sheet, planned, limits) {
1803
1884
  while (k < toInsert.length) body += p(toInsert[k++]?.xml ?? "");
1804
1885
  inner += row.element.start.empty ? `${openTag(tag)}${body}</${row.element.start.qname}>` : `${tag}${body}${text.slice(row.element.end.start, row.element.end.end)}`;
1805
1886
  }
1806
- while (pending.length > 0) inner += newRow(pending.shift() ?? 0);
1887
+ while (next < pending.length) inner += newRow(pending[next++] ?? 0);
1807
1888
  const changes = [];
1808
1889
  if (sheetData.start.empty) changes.push([
1809
1890
  sheetData.start.start,
@@ -1,4 +1,4 @@
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";
1
+ import { $t as drawingParts, A as limitExceeded, At as nameScopeKey, Bt as checkKeyText, C as Bytes, Ct as relationshipsPartName, D as TAIL_BYTES, Dt as normalizeTable, E as NotLoaded, Et as TABLE_CONTENT_TYPE, F as validateLocalHeaders, Gt as printXml, Ht as resolveLimits, I as WarningCollector, It as normalizeStyle, K as SheetRels, Kt as sheetProtectionXml, L as sheetNotFound, N as readCentralDirectory, O as checkSource$1, Ot as checkDefinedName, P as unsupported, Q as createSharedStrings, Qt as MediaParts, R as createWorksheetParser, S as workbookPartsToLoad, St as openPackage, T as HEAD_BYTES, Tn as clip, Tt as zipNameToPartName, Vt as checkOutputPositions, Wt as printNames, X as cellXml, Xt as DRAWING_CONTENT_TYPE, Y as buildStyles, Z as contentTypesXml, Zt as IMAGE_CONTENT_TYPES, _ as isoDate, _n as corePropertiesXml, _t as FormulaGroups, a as DATE_FORMAT, an as classifyRelationship, cn as MAX_ROWS, ct as sheetHead, d as convertCellInput, dt as sheetTail, f as createWorkbook, gn as appPropertiesXml, gt as workbookXml, h as detectHeader, hn as CORE_PROPERTIES_PART, ht as workbookRelsXml, i as readOptions, it as noteParts, j as locateCentralDirectory, jn as XlvError, k as corrupt, kt as checkUniqueName, ln as columnLetters, lt as sheetRelsPartName, mn as APP_PROPERTIES_PART, n as oneOfOption, nn as resolveImage, nt as hyperlinksXml, o as DATE_TIME_FORMAT, ot as rootRelsXml, p as sortedNotes, pt as tableParts, rt as nameTitles, sn as MAX_COLUMNS, st as rowStart, tn as metricsOf, u as checkBookViews, un as formatCellRef, ut as sheetRulesXml, v as analyzeWorkbook, w as CORRUPT_HINT, wt as partKey, xn as createXmlByteStreamParser, y as readNotes, yt as crc32, zt as DEFAULT_READ_LIMITS } from "./options-Cw7OraxE.js";
2
2
  import { Deflate, Inflate } from "fflate";
3
3
  // src/zip/stream.ts
4
4
  /**
@@ -396,7 +396,8 @@ function createZipStream(entries, level = 6) {
396
396
  // src/stream-read.ts
397
397
  const GiB = 1073741824;
398
398
  /**
399
- * Limits that differ in streaming mode (D-401): `maxCells` does not apply, and single parts may
399
+ * Limits that differ in streaming mode (D-401): `maxCells` does not apply (but one `<row>` element
400
+ * may hold at most 16 384 cells, D-J01), and single parts may
400
401
  * decompress to 4 GiB (8 GiB in total). All other `DEFAULT_READ_LIMITS` stay.
401
402
  *
402
403
  * @example
@@ -423,32 +424,69 @@ var PreloadedZip = class {
423
424
  entries;
424
425
  bytesRead = 0;
425
426
  data = /* @__PURE__ */ new Map();
427
+ /**
428
+ * ZIP entries by the part name they map to, as openPackage maps them: percent-encoded
429
+ * non-ASCII characters of an item name are decoded (Part 2 §7.3.5; F-OPC-12).
430
+ */
431
+ byPart = /* @__PURE__ */ new Map();
426
432
  constructor(zip, maxBufferedBytes) {
427
433
  this.zip = zip;
428
434
  this.maxBufferedBytes = maxBufferedBytes;
429
435
  this.entries = zip.entries;
436
+ for (const entry of zip.entries) {
437
+ if (entry.name.endsWith("/")) continue;
438
+ const key = partKey(zipNameToPartName(entry.name));
439
+ if (!this.byPart.has(key)) this.byPart.set(key, entry);
440
+ }
430
441
  }
431
- async load(names) {
432
- for (const name of names) {
433
- const entry = this.zip.find(name);
442
+ /** The ZIP entry that stores a part, or undefined. */
443
+ entryOf(partName) {
444
+ return this.byPart.get(partKey(partName));
445
+ }
446
+ async load(entries) {
447
+ for (const entry of entries) {
434
448
  if (entry === void 0 || this.data.has(entry.name)) continue;
435
449
  const bytes = await this.zip.read(entry, this.maxBufferedBytes);
436
450
  this.data.set(entry.name, bytes);
437
451
  this.bytesRead += bytes.length;
438
452
  }
439
453
  }
454
+ /** Loads the parts with these names that exist. */
455
+ async loadParts(partNames) {
456
+ await this.load([...partNames].map((name) => this.entryOf(name)));
457
+ }
458
+ /** Loads the Relationships parts of these sources (null: the package) that exist. */
459
+ async loadRelationships(sources) {
460
+ await this.loadParts([...sources].map(relationshipsPartName));
461
+ }
440
462
  find(name) {
441
463
  return this.zip.find(name);
442
464
  }
443
465
  read(entry) {
444
466
  const name = typeof entry === "string" ? this.zip.find(entry)?.name ?? entry : entry.name;
445
467
  const bytes = this.data.get(name);
446
- if (bytes === void 0) throw new Error(`internal: ZIP entry "${name}" was not preloaded`);
468
+ if (bytes === void 0) throw new XlvError("XLV_E_INTERNAL", `The ZIP entry "${clip(name)}" was read before it was loaded.`, {
469
+ location: { part: `/${clip(name)}` },
470
+ hint: "This is a bug in xlvane. Please report it, with the file if you can share it; readWorkbook reads the file meanwhile."
471
+ });
447
472
  return bytes;
448
473
  }
449
474
  };
450
- function zipName(partName) {
451
- return partName.slice(1);
475
+ /**
476
+ * The internal targets of the workbook part's relationships: the sheets among them, whose
477
+ * Relationships parts analyzeWorkbook reads. Only these, and not every .rels entry of the ZIP,
478
+ * are loaded (D-H01).
479
+ */
480
+ function workbookTargets(pkg, workbookPart) {
481
+ const seen = /* @__PURE__ */ new Set();
482
+ const out = [];
483
+ for (const rel of pkg.relationships(workbookPart)) {
484
+ const key = partKey(rel.target);
485
+ if (rel.external || seen.has(key) || !pkg.hasPart(rel.target)) continue;
486
+ seen.add(key);
487
+ out.push(rel.target);
488
+ }
489
+ return out;
452
490
  }
453
491
  function checkSource(source) {
454
492
  if (source instanceof Uint8Array || source instanceof ArrayBuffer) return source;
@@ -486,9 +524,15 @@ async function openWorkbookStream(source, options) {
486
524
  });
487
525
  const zip = await openZipSource(input, limits);
488
526
  const preloaded = new PreloadedZip(zip, userLimits?.maxEntryBytes ?? DEFAULT_READ_LIMITS.maxEntryBytes);
489
- await preloaded.load(zip.entries.filter((e) => e.name.toLowerCase() === "[content_types].xml" || e.name.toLowerCase().endsWith(".rels")).map((e) => e.name));
527
+ await preloaded.load(zip.entries.filter((e) => e.name.toLowerCase() === "[content_types].xml"));
528
+ await preloaded.loadRelationships([null]);
490
529
  const pkg = openPackage(preloaded, limits);
491
- await preloaded.load(workbookPartsToLoad(pkg).map(zipName));
530
+ const workbookPart = pkg.relationships(null).find((r) => !r.external && classifyRelationship(r.type)?.name === "officeDocument")?.target;
531
+ if (workbookPart !== void 0) {
532
+ await preloaded.loadRelationships([workbookPart]);
533
+ await preloaded.loadParts(workbookPartsToLoad(pkg));
534
+ await preloaded.loadRelationships(workbookTargets(pkg, workbookPart));
535
+ }
492
536
  const warnings = new WarningCollector();
493
537
  const info = analyzeWorkbook(pkg, limits, warnings);
494
538
  const formatCache = /* @__PURE__ */ new Map();
@@ -504,7 +548,7 @@ async function openWorkbookStream(source, options) {
504
548
  const cached = noteCache.get(entry.part);
505
549
  if (cached !== void 0) return cached;
506
550
  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
- if (parts.length > 0) await preloaded.load(parts.map((r) => zipName(r.target)));
551
+ if (parts.length > 0) await preloaded.loadParts(parts.map((r) => r.target));
508
552
  const notes = Object.freeze(readNotes(pkg, entry.part, entry.name, limits, warnings, entry.foreignParts));
509
553
  noteCache.set(entry.part, notes);
510
554
  return notes;
@@ -553,7 +597,15 @@ async function openWorkbookStream(source, options) {
553
597
  part,
554
598
  limits
555
599
  });
556
- const reader = zip.stream(zipName(part)).getReader();
600
+ const stored = preloaded.entryOf(part);
601
+ if (stored === void 0) throw new XlvError("XLV_E_PART_MISSING", `The part ${clip(part)} of sheet "${entry.name}" is not in the archive.`, {
602
+ location: {
603
+ part: clip(part),
604
+ sheet: entry.name
605
+ },
606
+ hint: "The workbook is incomplete. Re-save it in a spreadsheet application."
607
+ });
608
+ const reader = zip.stream(stored).getReader();
557
609
  try {
558
610
  for (;;) {
559
611
  const { done, value } = await reader.read();
@@ -640,6 +692,7 @@ async function* readTableStream(source, options) {
640
692
  const limits = o.get("limits");
641
693
  const book = await openWorkbookStream(source, limits === void 0 ? void 0 : { limits });
642
694
  const outputCells = limits?.maxCells ?? DEFAULT_READ_LIMITS.maxCells;
695
+ const keyTextLimit = limits?.maxSharedStringChars ?? DEFAULT_READ_LIMITS.maxSharedStringChars;
643
696
  const sheetName = o.get("sheet") ?? (book.sheets.find((s) => s.kind === "worksheet" && s.state === "visible") ?? book.sheets[0])?.name;
644
697
  if (sheetName === void 0) return;
645
698
  const buffered = [];
@@ -692,6 +745,16 @@ async function* readTableStream(source, options) {
692
745
  };
693
746
  let positions = 0;
694
747
  let cellsRead = 0;
748
+ let byKey = null;
749
+ let byKeyOf = null;
750
+ const columnOf = (cols, key) => {
751
+ if (byKey === null || byKeyOf !== cols) {
752
+ byKey = /* @__PURE__ */ new Map();
753
+ for (const c of cols) if (!byKey.has(c.key)) byKey.set(c.key, c);
754
+ byKeyOf = cols;
755
+ }
756
+ return byKey.get(key);
757
+ };
695
758
  const emit = async (r, cols) => {
696
759
  if (headerRow !== null && r.number <= headerRow) return null;
697
760
  cellsRead += r.cells.length;
@@ -699,6 +762,7 @@ async function* readTableStream(source, options) {
699
762
  checkOutputPositions(positions, cellsRead, outputCells, "The table", sheetName);
700
763
  const byCol = new Map(r.cells.map((c) => [c.col, c]));
701
764
  if (cols.every((c) => byCol.get(c.col)?.value == null)) return null;
765
+ checkKeyText(cols, 1, keyTextLimit, sheetName);
702
766
  const row = {};
703
767
  for (const c of cols) setOwn(row, c.key, value(byCol.get(c.col)));
704
768
  if (schema === void 0) return {
@@ -717,7 +781,7 @@ async function* readTableStream(source, options) {
717
781
  const key = typeof seg === "object" && seg !== null ? seg.key : seg;
718
782
  return typeof key === "number" ? key : String(key);
719
783
  });
720
- const column = typeof path[0] === "string" ? cols.find((c) => c.key === path[0]) : void 0;
784
+ const column = typeof path[0] === "string" ? columnOf(cols, path[0]) : void 0;
721
785
  return {
722
786
  row: r.number,
723
787
  cell: column ? formatCellRef(r.number, column.col) : null,
@@ -833,6 +897,7 @@ function writeWorkbookStream(options) {
833
897
  details: { option: "definedNames" }
834
898
  });
835
899
  const names = [];
900
+ const nameKeys = /* @__PURE__ */ new Map();
836
901
  rawNames.forEach((raw, i) => {
837
902
  const r = readOptions(raw, [
838
903
  "name",
@@ -843,8 +908,9 @@ function writeWorkbookStream(options) {
843
908
  ], `definedNames[${String(i)}]`);
844
909
  const { name, refersTo, ...rest } = Object.fromEntries(r);
845
910
  const n = checkDefinedName(name, refersTo, rest, null);
846
- checkUniqueName(n, names);
911
+ checkUniqueName(n, nameKeys);
847
912
  names.push(n);
913
+ nameKeys.set(nameScopeKey(n), n);
848
914
  });
849
915
  const addNames = () => {
850
916
  for (const n of names) book.defineName(n.name, n.refersTo, {
@@ -942,7 +1008,7 @@ function writeWorkbookStream(options) {
942
1008
  for (const v of listOf("tables", "{ name: \"Sales\", ref: \"A1:B10\", columns: [{ name: \"Region\" }, { name: \"Revenue\" }] }")) {
943
1009
  const t = normalizeTable(v, sheet.name, true);
944
1010
  book.checkTable(t, sheet);
945
- sheet.tableList.push(t);
1011
+ sheet.pushTable(t);
946
1012
  }
947
1013
  const merges = s.get("merges");
948
1014
  if (merges !== void 0) {
@@ -3587,9 +3587,13 @@ declare class NewSheet {
3587
3587
  private readonly priorities;
3588
3588
  /** @internal Protection, page setup, headers and footers, page breaks, print area and titles. */
3589
3589
  print: SheetPrintData;
3590
- /** @internal Tables in the order they were added (TD-01). */
3590
+ /** @internal Tables in the order they were added (TD-01); add them with {@link NewSheet.pushTable}. */
3591
3591
  readonly tableList: NormalizedTable[];
3592
3592
  private count;
3593
+ private readonly mergeIndex;
3594
+ private mergesIndexed;
3595
+ private readonly tableIndex;
3596
+ private tablesIndexed;
3593
3597
  /** @internal Use {@link NewWorkbook.addSheet}. */
3594
3598
  constructor(name: string, state: SheetState, book: NewWorkbook, sheetId?: number | null);
3595
3599
  /** Number of cells that have a value or a style. */
@@ -3682,6 +3686,12 @@ declare class NewSheet {
3682
3686
  * ```
3683
3687
  */
3684
3688
  merge(range: string): this;
3689
+ /** @internal Whether `range` shares a cell with a merged range of the sheet. */
3690
+ overlapsMerge(range: CellRange): boolean;
3691
+ /** @internal Whether `range` shares a cell with a table of the sheet. */
3692
+ overlapsTable(range: CellRange): boolean;
3693
+ /** @internal Adds a table checked by {@link NewWorkbook.checkTable} to the sheet. */
3694
+ pushTable(t: NormalizedTable): void;
3685
3695
  /**
3686
3696
  * Freezes the top rows and left columns so that they stay visible when scrolling. Pass
3687
3697
  * `{ rows: 0, cols: 0 }` to remove the frozen panes.
@@ -3969,6 +3979,18 @@ declare class NewWorkbook {
3969
3979
  readonly properties: WorkbookProperties;
3970
3980
  private readonly list;
3971
3981
  private readonly names;
3982
+ /** Sheets by lower-case name. */
3983
+ private readonly sheetsByName;
3984
+ /** Sheet names as given, in sheet order. */
3985
+ private readonly sheetNames;
3986
+ /** Sheets by the sheetId given to addSheet. */
3987
+ private readonly sheetsById;
3988
+ /** Defined names by nameScopeKey. */
3989
+ private readonly namesByScope;
3990
+ /** Lower-case defined names, of any scope. */
3991
+ private readonly lowerNames;
3992
+ /** Lower-case names and display names of the tables of all sheets. */
3993
+ private readonly tableNames;
3972
3994
  private readonly styleIds;
3973
3995
  private readonly styleList;
3974
3996
  /** @internal `workbookProtection`, set by {@link NewWorkbook.protect}. */
@@ -4068,6 +4090,8 @@ declare class NewWorkbook {
4068
4090
  * ```
4069
4091
  */
4070
4092
  defineName(name: string, refersTo: string, options?: DefinedNameOptions): this;
4093
+ /** @internal Records the names of a table added to a sheet ({@link NewSheet.pushTable}). */
4094
+ tableAdded(t: NormalizedTable): void;
4071
4095
  /**
4072
4096
  * @internal Checks that a table's names are unique in the workbook (tables and defined names,
4073
4097
  * ignoring case; Part 1 §18.5.1.2) and that it overlaps no table or merged range of its sheet.
@@ -4499,6 +4523,12 @@ interface WriteTableOptions {
4499
4523
  readonly preset?: "report" | "plain";
4500
4524
  /** Date system of the new workbook. Default `"1900"`. */
4501
4525
  readonly dateSystem?: DateSystem;
4526
+ /**
4527
+ * Bounds the sheet: at most `max(maxCells, 16 × cells written)` positions (rows × columns), so
4528
+ * a few values spread over many rows and columns are refused with `XLV_E_LIMIT_EXCEEDED`.
4529
+ * Default 5 000 000, the `maxCells` read limit; raise it for a large sparse table.
4530
+ */
4531
+ readonly maxCells?: number;
4502
4532
  }
4503
4533
  /**
4504
4534
  * Writes rows as a formatted XLSX table and returns the file's bytes. Rows are objects (keys
@@ -4635,14 +4665,15 @@ interface WorkbookStream {
4635
4665
  interface OpenWorkbookStreamOptions {
4636
4666
  /**
4637
4667
  * Read limits. Streaming defaults differ from `DEFAULT_READ_LIMITS`: `maxCells` does not apply
4638
- * (memory does not grow with cells), `maxEntryBytes` is 4 GiB for worksheets (inflated piece by
4668
+ * (memory does not grow with cells; one `<row>` element may hold at most 16 384 cells), `maxEntryBytes` is 4 GiB for worksheets (inflated piece by
4639
4669
  * piece) and `maxTotalBytes` 8 GiB. Parts held in memory whole (shared strings, styles, the
4640
4670
  * workbook, relationships, notes) keep the batch bound of 256 MiB unless `maxEntryBytes` is given.
4641
4671
  */
4642
4672
  readonly limits?: Partial<ReadLimits>;
4643
4673
  }
4644
4674
  /**
4645
- * Limits that differ in streaming mode (D-401): `maxCells` does not apply, and single parts may
4675
+ * Limits that differ in streaming mode (D-401): `maxCells` does not apply (but one `<row>` element
4676
+ * may hold at most 16 384 cells, D-J01), and single parts may
4646
4677
  * decompress to 4 GiB (8 GiB in total). All other `DEFAULT_READ_LIMITS` stay.
4647
4678
  *
4648
4679
  * @example
package/docs/errors.md CHANGED
@@ -95,9 +95,13 @@ Some limits also bound work derived from a small file (1.1.1, D-H01):
95
95
 
96
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
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.
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. From 1.2.3 (D-J01) also the header names that `readTable`, `toJSON` and `readTableStream` repeat in every row object: the sum of the key lengths times the number of rows (a long header over many rows would otherwise make JSON text far larger than the file); `readTableStream` counts one row at a time.
99
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).
100
+ From 1.2.3 (D-J01):
101
+
102
+ - `maxCells`: `fromCSV`, `fromJSON` and `writeTable` write at most `max(maxCells, 16 × cells written)` positions (rows × columns), so one very wide row followed by many short ones cannot make them visit every position; each takes a `maxCells` option (default 5 000 000). While streaming, one `<row>` element may hold at most 16 384 cells (`details.value` 16384), the most a row can have, because its cells are held until the element ends. `@xlvane/compat` `xlsx.load` also counts the cells of defined names against its budget, and its `xlsx.read`, `csv.read` and `stream.xlsx.WorkbookReader` stop collecting a stream after `maxInputBytes` (256 MiB; `details.limit` `maxInputBytes`).
103
+
104
+ The MCP server (`@xlvane/mcp`) returns it as a tool error for a file larger than `--max-file-size` (`details.limit` `maxFileBytes`; `convert` to JSON stops building the text at that size), for a workbook with more cells than `--max-cells` (`details.limit` `maxCells`) and for a result longer than `--max-response-chars` (`details.limit` `maxResponseChars`; `read_table` instead returns the rows that fit). `xlvane convert` to JSON returns it when the text would be longer than a JavaScript string can be.
101
105
 
102
106
  `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.
103
107
 
@@ -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, 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 |
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`; with `latin1` characters above U+00FF are written as `?` (Node.js, and so exceljs, keeps only their low byte, which can turn U+013D into `=`), and formatter options with such characters throw `XLV_E_INVALID_OPTION`. While formulas are escaped (the default), a `delimiter` holding a formula sign (`=`, `+`, `-`, `@`, their full-width or small forms) or a control character (a lone tab is allowed), a `rowDelimiter` other than `\n`, `\r\n` or `\r`, a `quote` other than `"` or `false`, an `escape` other than the quote, and with `quote: false` a field that starts with `"` throw `XLV_E_INVALID_OPTION`, because a standard CSV reader would otherwise get a field starting with a formula (docs/facts/csv.md CSV-13); `formulaInjection: "off"` writes them as exceljs does. | `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) |
@@ -179,7 +179,7 @@ A workbook loaded with `xlsx.load()` is written again by `xlsx.writeBuffer()` (`
179
179
  - `row.destroy()` and `cell.destroy()` do nothing; the row or cell stays usable (exceljs deletes its internals).
180
180
  - Errors found while writing name the worksheet and, for cell values and styles, the cell (`error.location`).
181
181
  - `worksheet.tables` and `dataValidations.model` have no prototype: keys such as `constructor` or `__proto__` are plain entries.
182
- - CSV writing: a field whose first character after any NUL characters is a formula character is escaped (NULs are removed when writing); with `formatterOptions.quote: false`, a field with CR, LF or the delimiter throws `XLV_E_INVALID_OPTION` (fast-csv writes it and the CSV then has other fields or rows). CSV reading: fields such as `constructor` or `__proto__` stay text.
182
+ - CSV writing: a field whose first character after any NUL characters is a formula character is escaped (NULs are removed when writing); options that would get around the escape are refused (CSV-13); `latin1` output writes characters above U+00FF as `?`; `csv.read` stops collecting input after 256 MiB (`XLV_E_LIMIT_EXCEEDED`, `maxInputBytes`); with `formatterOptions.quote: false`, a field with CR, LF or the delimiter throws `XLV_E_INVALID_OPTION` (fast-csv writes it and the CSV then has other fields or rows). CSV reading: fields such as `constructor` or `__proto__` stay text.
183
183
 
184
184
  ## Known differences found by the cross-check
185
185