xlvane 1.2.3 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
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";
1
+ import { $ as fillXml, An as decodeXml, At as parseStyles, Bt as resolveLimits, C as toBytes, Ct as relationshipsPartName, Dn as childrenNamed, En as childNamed, Fn as escapeAttr, Ft as stableKey, G as DEFAULT_FONT, H as X14_DV_URI, In as escapeText, It as isDateFormat, J as borderXml, Lt as DEFAULT_READ_LIMITS, Mn as parseXml, Mt as checkStyleCount, Nt as mergeStyles, On as parseXmlTree, P as openZip, Pn as encodeXstring, Pt as normalizeStyle, R as WarningCollector, Rt as checkKeyText, S as styledCellCount, St as openPackage, V as X14_CF_URI, Vt as suggest, _n as formatRange, an as relationshipTypes, at as reusableBuiltinNumFmt, bn as partKey, bt as parseIsoDateTime, dn as shiftFormula, et as fontXml, f as createWorkbook, fn as tokenize, ft as stringItemXml, g as detectHeader, gn as formatCellRef, h as RangeIndex, hn as columnLetters, i as readOptions, in as classifyRelationship, jn as normalizeLineEnds, jt as READING_ORDERS, kt as EMPTY_STYLES, m as writeWorkbook, mn as MAX_ROWS, mt as textElement, n as oneOfOption, nn as REL_NS, pn as MAX_COLUMNS, q as attr, r as openInput, rn as SML_NS, t as booleanOption, tt as formatNumber, v as isoDate, vn as parseCellRef, vt as writeZip, x as readWorkbook, xn as zipNameToPartName, xt as parseSharedStrings, yn as parseRange, zn as XlvError, zt as checkOutputPositions } from "./options-BD873QNd.js";
2
2
  // src/table.ts
3
3
  const READ_OPTIONS = [
4
4
  "sheet",
@@ -599,8 +599,10 @@ function fromJSON(input, options) {
599
599
  // src/csv.ts
600
600
  /**
601
601
  * First characters that make spreadsheet applications treat a CSV field as a formula (CSV-10),
602
- * plus, as a precaution, the small forms of = + - @ (CSV-12, D-H01). Text starting with any other
603
- * C0 control character is escaped too (see `injectionStart`).
602
+ * plus, as a precaution, the small forms of = + - @ (CSV-12, D-H01) and the byte order marks
603
+ * U+FEFF and U+FFFE, which an application drops at the start of the file before it looks at the
604
+ * field (CSV-12). Text starting with any other C0 control character is escaped too (see
605
+ * `injectionStart`).
604
606
  */
605
607
  const INJECTION_START = /* @__PURE__ */ new Set([
606
608
  "=",
@@ -617,7 +619,9 @@ const INJECTION_START = /* @__PURE__ */ new Set([
617
619
  "﹦",
618
620
  "﹢",
619
621
  "﹣",
620
- "﹫"
622
+ "﹫",
623
+ "",
624
+ "￾"
621
625
  ]);
622
626
  /** Signs that start a formula: refused as the delimiter while formulas are escaped (D-J01). */
623
627
  const FORMULA_SIGNS = /* @__PURE__ */ new Set([
@@ -637,8 +641,20 @@ const FORMULA_SIGNS = /* @__PURE__ */ new Set([
637
641
  /** Whether text starting with `c` is escaped: one of INJECTION_START or a C0 control (NUL included). */
638
642
  const injectionStart = (c) => INJECTION_START.has(c) || c.charCodeAt(0) < 32;
639
643
  /**
640
- * Converts a sheet to CSV text. Fields that contain the delimiter, a quote, CR or LF are
641
- * enclosed in quotes, and quotes are doubled (RFC 4180). Numbers are written in JavaScript's
644
+ * Separators that common readers use besides the delimiter: a field that holds one of them is
645
+ * quoted whatever the delimiter, so a reader that splits with another separator keeps it whole
646
+ * (CSV-14).
647
+ */
648
+ const SEPARATORS = [
649
+ ",",
650
+ ";",
651
+ " ",
652
+ "|"
653
+ ];
654
+ /**
655
+ * Converts a sheet to CSV text. Fields that contain the delimiter, a quote, CR, LF, or one of
656
+ * the common separators `,`, `;`, tab and `|` (whatever the delimiter, CSV-14) are enclosed in
657
+ * quotes, and quotes are doubled (RFC 4180). Numbers are written in JavaScript's
642
658
  * shortest form, booleans as `TRUE`/`FALSE`, dates as ISO 8601, error cells as their code and
643
659
  * formula cells as their cached value. By default, text that a spreadsheet would run as a
644
660
  * formula is escaped (`formulaInjection: "escape"`).
@@ -705,7 +721,7 @@ function toCSV(input, options) {
705
721
  cellCount++;
706
722
  }
707
723
  if (endRow >= startRow && endCol >= startCol) checkOutputPositions((endRow - startRow + 1) * (endCol - startCol + 1), cellCount, limits.maxCells, "The CSV text", sheet.name);
708
- const needsQuotes = (s) => s.includes(delimiter) || s.includes("\"") || s.includes("\r") || s.includes("\n");
724
+ const needsQuotes = (s) => s.includes(delimiter) || s.includes("\"") || s.includes("\r") || s.includes("\n") || SEPARATORS.some((c) => s.includes(c));
709
725
  const field = (value) => {
710
726
  if (value === null || value === void 0) return "";
711
727
  if (typeof value === "number") return String(value);
@@ -863,7 +879,8 @@ function inferNumber(s) {
863
879
  const value = Number(s);
864
880
  if (!Number.isFinite(value)) return null;
865
881
  const digits = s.slice(intStart, intStart + intDigits + (fracDigits > 0 ? fracDigits + 1 : 0)).replace(".", "").replace(/^0+/, "");
866
- if (value === 0 ? /[1-9]/.test(digits) : Math.abs(value) < MIN_NORMAL) return null;
882
+ if (value === 0 && /[1-9]/.test(digits)) return null;
883
+ if (value !== 0 && Math.abs(value) < MIN_NORMAL && String(value) !== s) return null;
867
884
  if (digits.length > 15) {
868
885
  if (fracDigits === 0 && !/[eE]/.test(s) || String(value) !== s) return null;
869
886
  }
@@ -1123,6 +1140,76 @@ function diff(a, b, options) {
1123
1140
  };
1124
1141
  }
1125
1142
  // src/edit.ts
1143
+ /**
1144
+ * How many cells one call of {@link editCells} or {@link appendRows} writes at most when the
1145
+ * caller gives no `limits.maxCells` (A2). While the file is rebuilt each cell written costs about
1146
+ * 0.5 KB of memory, so the 5 000 000 of `DEFAULT_READ_LIMITS.maxCells` would need about 2.5 GB,
1147
+ * more than the default heap of Node.js (about 2 GB on a 64-bit machine with 16 GB of memory).
1148
+ */
1149
+ const EDIT_MAX_CELLS = 1e6;
1150
+ /** The read limits of an edit, and how many cells it may write. */
1151
+ function editLimits(input) {
1152
+ const limits = resolveLimits(input);
1153
+ return {
1154
+ limits,
1155
+ writable: typeof input === "object" && input !== null && Object.hasOwn(input, "maxCells") ? limits.maxCells : Math.min(limits.maxCells, EDIT_MAX_CELLS)
1156
+ };
1157
+ }
1158
+ /** Refuses an edit that would write more cells than one call may (A2). */
1159
+ function checkWrites(count, writable, what, sheet) {
1160
+ if (count <= writable) return;
1161
+ throw new XlvError("XLV_E_LIMIT_EXCEEDED", `${what} would write ${String(count)} cells, more than the limit of ${String(writable)} cells per call.`, {
1162
+ location: sheet === void 0 ? {} : { sheet },
1163
+ hint: `Each cell written takes about 0.5 KB of memory while the file is rebuilt. Split the cells into several calls of at most ${String(writable)}, or pass { limits: { maxCells: ${String(count)} } } if there is enough memory.`,
1164
+ details: {
1165
+ limit: "maxCells",
1166
+ value: writable
1167
+ }
1168
+ });
1169
+ }
1170
+ /**
1171
+ * Refuses an edit after which the workbook would hold more cells (with a value or only a style)
1172
+ * than `maxCells` (A2), the limit `readWorkbook` applies to the whole workbook, so the result can be
1173
+ * read again with the same limits (D-K04). Checked before anything per cell is built.
1174
+ */
1175
+ function checkCellCount(workbook, added, maxCells) {
1176
+ let total = 0;
1177
+ for (const sheet of workbook.sheets) total += sheet.cellCount + styledCellCount(sheet) + (added.get(sheet) ?? 0);
1178
+ if (total <= maxCells) return;
1179
+ const edited = [...added.keys()];
1180
+ throw new XlvError("XLV_E_LIMIT_EXCEEDED", `After the edit, the workbook would hold ${String(total)} cells, more than the limit of ${String(maxCells)}.`, {
1181
+ location: edited.length === 1 ? { sheet: edited[0].name } : {},
1182
+ hint: `Cells of every sheet count, also cells that hold only a style. Write fewer new cells, or, if you trust this file and there is enough memory, raise the limit, for example { limits: { maxCells: ${String(Math.max(total, maxCells * 2))} } }.`,
1183
+ details: {
1184
+ limit: "maxCells",
1185
+ value: maxCells
1186
+ }
1187
+ });
1188
+ }
1189
+ /** Data properties of an edit, read without running getters (planEdits validates edits). */
1190
+ function dataProperty(o, key) {
1191
+ return Object.getOwnPropertyDescriptor(o, key)?.value;
1192
+ }
1193
+ /** Number of cells per sheet that edits write where the sheet has no cell with a value. */
1194
+ function newCells(workbook, edits) {
1195
+ const byName = new Map(workbook.sheets.map((sheet) => [sheet.name, sheet]));
1196
+ const seen = /* @__PURE__ */ new Map();
1197
+ for (const raw of edits) {
1198
+ if (typeof raw !== "object" || raw === null) continue;
1199
+ const name = dataProperty(raw, "sheet");
1200
+ const ref = dataProperty(raw, "cell");
1201
+ const sheet = typeof name === "string" ? byName.get(name) : void 0;
1202
+ const address = typeof ref === "string" ? parseCellRef(ref) : null;
1203
+ if (sheet === void 0 || address === null) continue;
1204
+ let cells = seen.get(sheet);
1205
+ if (cells === void 0) {
1206
+ cells = /* @__PURE__ */ new Set();
1207
+ seen.set(sheet, cells);
1208
+ }
1209
+ if (sheet.cell(formatCellRef(address.row, address.col)) === void 0) cells.add(address.row * (MAX_COLUMNS + 1) + address.col);
1210
+ }
1211
+ return new Map([...seen].map(([sheet, cells]) => [sheet, cells.size]));
1212
+ }
1126
1213
  /** `"x:"` for a qualified name `x:local`, `""` without a prefix. */
1127
1214
  function prefixOf(qname) {
1128
1215
  const colon = qname.indexOf(":");
@@ -1267,11 +1354,22 @@ var PackageEditor = class {
1267
1354
  if (cached !== void 0) return cached;
1268
1355
  const entry = this.entryOf(partName);
1269
1356
  if (entry === void 0) return void 0;
1270
- return normalizeLineEnds(decodeXml(this.bytesOf(entry), partName));
1357
+ return normalizeLineEnds(decodeXml(this.changed.get(entry.name) ?? this.bytesOf(entry), partName));
1271
1358
  }
1359
+ /**
1360
+ * Writes a part. Text given in pieces is encoded piece by piece and not kept as text, so a sheet
1361
+ * of millions of cells is never one more string of its whole size (A2); reading the part again
1362
+ * decodes the written bytes.
1363
+ */
1272
1364
  write(partName, text) {
1273
- this.texts.set(partKey(partName), text);
1274
- const data = this.encoder.encode(asUtf8(text));
1365
+ let data;
1366
+ if (typeof text === "string") {
1367
+ this.texts.set(partKey(partName), text);
1368
+ data = this.encoder.encode(asUtf8(text));
1369
+ } else {
1370
+ this.texts.delete(partKey(partName));
1371
+ data = this.encodePieces(text);
1372
+ }
1275
1373
  const entry = this.entryOf(partName);
1276
1374
  if (entry) {
1277
1375
  this.changed.set(entry.name, data);
@@ -1285,6 +1383,28 @@ var PackageEditor = class {
1285
1383
  data
1286
1384
  });
1287
1385
  }
1386
+ encodePieces(pieces) {
1387
+ let size = 0;
1388
+ for (const piece of pieces) size += piece.length;
1389
+ let out = new Uint8Array(Math.max(1024, size + (size >> 2)));
1390
+ let used = 0;
1391
+ pieces.forEach((piece, i) => {
1392
+ const text = i === 0 ? asUtf8(piece) : piece;
1393
+ for (let from = 0; from < text.length;) {
1394
+ if (out.length - used < 3 * Math.min(text.length - from, 65536)) {
1395
+ const bigger = new Uint8Array(Math.max(out.length * 2, used + 196608));
1396
+ bigger.set(out.subarray(0, used));
1397
+ out = bigger;
1398
+ }
1399
+ let to = Math.min(text.length, from + 65536);
1400
+ if (to < text.length && (text.charCodeAt(to) & 64512) === 56320) to--;
1401
+ const { written } = this.encoder.encodeInto(text.slice(from, to), out.subarray(used));
1402
+ used += written;
1403
+ from = to;
1404
+ }
1405
+ });
1406
+ return out.slice(0, used);
1407
+ }
1288
1408
  remove(partName) {
1289
1409
  const entry = this.entryOf(partName);
1290
1410
  if (entry) this.removed.add(entry.name);
@@ -1752,10 +1872,178 @@ function newCellXml(ref, value, xf, sst) {
1752
1872
  const r = valueParts(value, sst, false);
1753
1873
  return `<c r="${ref}"${s}${r.t === "" ? "" : attr("t", r.t)}><v>${r.v}</v></c>`;
1754
1874
  }
1755
- /** Inserts `r="…"` after the qualified name of a start tag given as source text. */
1756
- function insertRef(tag, qname, ref) {
1757
- const at = 1 + qname.length;
1758
- return `${tag.slice(0, at)} r="${ref}"${tag.slice(at)}`;
1875
+ /** A growable list of 32-bit integers, so that a sheet's cells cost a few numbers each (A2, D-K04). */
1876
+ var IntList = class {
1877
+ data = /* @__PURE__ */ new Int32Array(1024);
1878
+ length = 0;
1879
+ push(value) {
1880
+ if (this.length === this.data.length) {
1881
+ const bigger = new Int32Array(this.data.length * 2);
1882
+ bigger.set(this.data);
1883
+ this.data = bigger;
1884
+ }
1885
+ this.data[this.length++] = value;
1886
+ }
1887
+ get(index) {
1888
+ return this.data[index];
1889
+ }
1890
+ set(index, value) {
1891
+ this.data[index] = value;
1892
+ }
1893
+ };
1894
+ /** Integers stored per row of {@link SheetScan.rows}. */
1895
+ const ROW_FIELDS$1 = 8;
1896
+ /** Integers stored per cell of {@link SheetScan.cells}. */
1897
+ const CELL_FIELDS$1 = 5;
1898
+ function parseIndex(value, digits) {
1899
+ const v = value?.trim() ?? "";
1900
+ return v.length > 0 && v.length <= digits && /^[0-9]+$/.test(v) ? Number(v) : NaN;
1901
+ }
1902
+ /**
1903
+ * Scans a worksheet part once for {@link editSheetXml} and {@link newCellStyle}. Rows and cells
1904
+ * are numbered as the reader numbers them (D-105); the reader skips rows and cells with invalid
1905
+ * references (D-903), and editing around them could put new cells in the wrong place, so a sheet
1906
+ * that has any is not edited.
1907
+ */
1908
+ function scanSheet(text, part, sheet, limits, styles) {
1909
+ const invalid = (ref) => {
1910
+ throw new XlvError("XLV_E_INVALID_CELL_REF", `Sheet "${sheet}" contains the invalid reference "${ref.slice(0, 20)}", so it cannot be edited safely.`, {
1911
+ location: {
1912
+ part,
1913
+ sheet
1914
+ },
1915
+ hint: "Open and re-save the file in a spreadsheet application, which drops the damaged cells, then edit the re-saved file.",
1916
+ details: { ref: ref.slice(0, 40) }
1917
+ });
1918
+ };
1919
+ const names = [];
1920
+ /** Whether each open element is a recorded row (depth 3) or cell (depth 4). */
1921
+ const recorded = [];
1922
+ let rootQname = null;
1923
+ let sheetDataStart = null;
1924
+ let sheetData = null;
1925
+ let dimension = null;
1926
+ const rows = new IntList();
1927
+ const cells = new IntList();
1928
+ let rowCount = 0;
1929
+ let currentRow = 0;
1930
+ let lastCol = 0;
1931
+ let missingRowRefs = false;
1932
+ const cols = [];
1933
+ const rowsWithFormat = /* @__PURE__ */ new Set();
1934
+ parseXml(text, {
1935
+ start(_ns, local, attrs, span) {
1936
+ const depth = names.length + 1;
1937
+ const parent = names[names.length - 1] ?? "";
1938
+ names.push(local);
1939
+ let keep = false;
1940
+ if (depth === 1) rootQname = span.qname;
1941
+ else if (depth === 2 && local === "sheetData" && sheetDataStart === null) sheetDataStart = span;
1942
+ else if (depth === 2 && local === "dimension" && dimension === null) dimension = {
1943
+ start: span,
1944
+ ref: attrs.get("ref")
1945
+ };
1946
+ else if (depth === 3 && local === "col" && parent === "cols") {
1947
+ const [min, max, style] = [
1948
+ parseIndex(attrs.get("min"), 10),
1949
+ parseIndex(attrs.get("max"), 10),
1950
+ parseIndex(attrs.get("style"), 10)
1951
+ ];
1952
+ if (Number.isInteger(min) && Number.isInteger(max) && Number.isInteger(style) && style !== 0 && styles.hasCellFormat(style)) cols.push({
1953
+ min,
1954
+ max,
1955
+ style
1956
+ });
1957
+ } else if (depth === 3 && local === "row" && parent === "sheetData") {
1958
+ const r = attrs.get("r");
1959
+ const n = r === void 0 ? currentRow + 1 : /^[0-9]{1,7}$/.test(r.trim()) ? Number(r.trim()) : NaN;
1960
+ if (!Number.isInteger(n) || n < 1 || n > 1048576) invalid(r ?? String(n));
1961
+ currentRow = n;
1962
+ lastCol = 0;
1963
+ if (r === void 0) missingRowRefs = true;
1964
+ const custom = attrs.get("customFormat")?.trim();
1965
+ if (attrs.has("s") && (custom === "1" || custom === "true")) rowsWithFormat.add(n);
1966
+ rows.push(n);
1967
+ rows.push(span.start);
1968
+ rows.push(span.end);
1969
+ rows.push(span.end);
1970
+ rows.push(span.end);
1971
+ rows.push((r === void 0 ? 0 : 1) | (span.empty ? 2 : 0) | span.qname.length << 2);
1972
+ rows.push(cells.length / CELL_FIELDS$1);
1973
+ rows.push(0);
1974
+ rowCount++;
1975
+ keep = true;
1976
+ } else if (depth === 4 && local === "c" && parent === "row" && recorded[depth - 2] === true) {
1977
+ const r = attrs.get("r");
1978
+ const address = r === void 0 || r === "" ? null : parseCellRef(r);
1979
+ if (r !== void 0 && r !== "" && address === null) invalid(r);
1980
+ const col = address?.col ?? lastCol + 1;
1981
+ lastCol = col;
1982
+ cells.push(col);
1983
+ cells.push(span.start);
1984
+ cells.push(span.end);
1985
+ cells.push(span.end);
1986
+ cells.push((r === void 0 ? 0 : 1) | span.qname.length << 1);
1987
+ rows.set((rowCount - 1) * ROW_FIELDS$1 + 7, rows.get((rowCount - 1) * ROW_FIELDS$1 + 7) + 1);
1988
+ keep = true;
1989
+ }
1990
+ recorded.push(keep);
1991
+ },
1992
+ end(_ns, local, span) {
1993
+ const depth = names.length;
1994
+ names.pop();
1995
+ const keep = recorded.pop() === true;
1996
+ if (depth === 2 && local === "sheetData" && sheetData === null && sheetDataStart !== null) sheetData = {
1997
+ start: sheetDataStart,
1998
+ end: span
1999
+ };
2000
+ else if (keep && depth === 3) {
2001
+ rows.set((rowCount - 1) * ROW_FIELDS$1 + 3, span.start);
2002
+ rows.set((rowCount - 1) * ROW_FIELDS$1 + 4, span.end);
2003
+ } else if (keep && depth === 4) cells.set(cells.length - 2, span.end);
2004
+ },
2005
+ text() {}
2006
+ }, {
2007
+ part,
2008
+ limits
2009
+ });
2010
+ if (rootQname === null) throw new XlvError("XLV_E_PART_INVALID", "The part has no root element.", {
2011
+ location: { part },
2012
+ hint: "Re-save the file in a spreadsheet application."
2013
+ });
2014
+ const columnStyles = new Int32Array(MAX_COLUMNS + 2);
2015
+ const next = new Int32Array(MAX_COLUMNS + 2);
2016
+ for (let i = 0; i < next.length; i++) next[i] = i;
2017
+ const unset = (c) => {
2018
+ let root = c;
2019
+ while (next[root] !== root) root = next[root];
2020
+ while (next[c] !== root) {
2021
+ const up = next[c];
2022
+ next[c] = root;
2023
+ c = up;
2024
+ }
2025
+ return root;
2026
+ };
2027
+ for (const c of cols) {
2028
+ const min = Math.max(c.min, 1);
2029
+ const max = Math.min(c.max, MAX_COLUMNS);
2030
+ if (min > max) continue;
2031
+ for (let col = unset(min); col <= max; col = unset(col + 1)) {
2032
+ columnStyles[col] = c.style;
2033
+ next[col] = col + 1;
2034
+ }
2035
+ }
2036
+ return {
2037
+ rootQname,
2038
+ sheetData,
2039
+ dimension,
2040
+ rows,
2041
+ rowCount,
2042
+ cells,
2043
+ missingRowRefs,
2044
+ columnStyles,
2045
+ rowsWithFormat
2046
+ };
1759
2047
  }
1760
2048
  /**
1761
2049
  * The cell format a new cell of a sheet starts from: the `style` of the `col` element that covers
@@ -1764,37 +2052,36 @@ function insertRef(tag, qname, ref) {
1764
2052
  * which of the two wins is not stated in the sources read (D-A04). Indexes beyond the cell
1765
2053
  * formats of the file are ignored.
1766
2054
  */
1767
- function newCellStyles(text, part, limits, styles) {
1768
- const { elements } = scan(text, part, limits, (local, depth, parent) => depth === 3 && local === "col" && parent === "cols" || depth === 3 && local === "row" && parent === "sheetData");
1769
- const cols = [];
1770
- const rowsWithFormat = /* @__PURE__ */ new Set();
1771
- let rowNumber = 0;
1772
- for (const e of elements) if (e.local === "col") {
1773
- const [min, max, style] = [
1774
- "min",
1775
- "max",
1776
- "style"
1777
- ].map((k) => {
1778
- const v = e.attrs.get(k)?.trim() ?? "";
1779
- return /^[0-9]{1,10}$/.test(v) ? Number(v) : NaN;
1780
- });
1781
- if (Number.isInteger(min) && Number.isInteger(max) && Number.isInteger(style) && style !== 0 && styles.hasCellFormat(style ?? 0)) cols.push({
1782
- min: min ?? 0,
1783
- max: max ?? 0,
1784
- style: style ?? 0
1785
- });
1786
- } else {
1787
- const r = e.attrs.get("r")?.trim();
1788
- rowNumber = r !== void 0 && /^[0-9]{1,7}$/.test(r) ? Number(r) : rowNumber + 1;
1789
- const custom = e.attrs.get("customFormat")?.trim();
1790
- if (e.attrs.has("s") && (custom === "1" || custom === "true")) rowsWithFormat.add(rowNumber);
2055
+ function newCellStyle(scan, row, col) {
2056
+ return scan.rowsWithFormat.has(row) ? 0 : scan.columnStyles[col] ?? 0;
2057
+ }
2058
+ /** Joins many small strings without keeping one string per piece until the end. */
2059
+ var TextBuilder = class {
2060
+ pieces = [];
2061
+ joined = [];
2062
+ add(text) {
2063
+ this.pieces.push(text);
2064
+ if (this.pieces.length >= 4096) {
2065
+ this.joined.push(this.pieces.join(""));
2066
+ this.pieces.length = 0;
2067
+ }
1791
2068
  }
1792
- return (row, col) => rowsWithFormat.has(row) ? 0 : cols.find((c) => col >= c.min && col <= c.max)?.style ?? 0;
2069
+ /** The text as a list of strings of a few thousand pieces each. */
2070
+ finish() {
2071
+ this.joined.push(this.pieces.join(""));
2072
+ this.pieces.length = 0;
2073
+ return this.joined;
2074
+ }
2075
+ };
2076
+ /** Inserts `r="…"` after the qualified name of a start tag given as source text. */
2077
+ function insertRef(tag, qnameLength, ref) {
2078
+ const at = 1 + qnameLength;
2079
+ return `${tag.slice(0, at)} r="${ref}"${tag.slice(at)}`;
1793
2080
  }
1794
- function editSheetXml(text, part, sheet, planned, limits) {
1795
- const { root, elements } = scan(text, part, limits, (local, depth, parent) => depth === 2 && (local === "sheetData" || local === "dimension") || depth === 3 && local === "row" && parent === "sheetData" || depth === 4 && local === "c" && parent === "row");
1796
- const prefix = prefixOf(root.start.qname);
1797
- const sheetData = elements.find((e) => e.depth === 2 && e.local === "sheetData");
2081
+ /** The edited text of a worksheet part, as pieces for {@link PackageEditor.write}. */
2082
+ function editSheetXml(text, part, sheet, scan, planned) {
2083
+ const prefix = prefixOf(scan.rootQname);
2084
+ const sheetData = scan.sheetData;
1798
2085
  if (!sheetData) throw new XlvError("XLV_E_PART_INVALID", `The worksheet part of sheet "${sheet}" has no sheetData element.`, {
1799
2086
  location: {
1800
2087
  part,
@@ -1802,45 +2089,7 @@ function editSheetXml(text, part, sheet, planned, limits) {
1802
2089
  },
1803
2090
  hint: "The sheet part is damaged. Re-save the file in a spreadsheet application."
1804
2091
  });
1805
- const rows = [];
1806
- let currentRow = 0;
1807
- let current = null;
1808
- let lastCol = 0;
1809
- const invalid = (ref) => {
1810
- throw new XlvError("XLV_E_INVALID_CELL_REF", `Sheet "${sheet}" contains the invalid reference "${ref.slice(0, 20)}", so it cannot be edited safely.`, {
1811
- location: {
1812
- part,
1813
- sheet
1814
- },
1815
- hint: "Open and re-save the file in a spreadsheet application, which drops the damaged cells, then edit the re-saved file.",
1816
- details: { ref: ref.slice(0, 40) }
1817
- });
1818
- };
1819
- for (const e of elements) if (e.depth === 3) {
1820
- const r = e.attrs.get("r");
1821
- const n = r === void 0 ? currentRow + 1 : /^[0-9]{1,7}$/.test(r.trim()) ? Number(r.trim()) : NaN;
1822
- if (!Number.isInteger(n) || n < 1 || n > 1048576) invalid(r ?? String(n));
1823
- currentRow = n;
1824
- current = {
1825
- number: currentRow,
1826
- hasR: r !== void 0,
1827
- element: e,
1828
- cells: []
1829
- };
1830
- rows.push(current);
1831
- lastCol = 0;
1832
- } else if (e.depth === 4 && current) {
1833
- const r = e.attrs.get("r");
1834
- const address = r === void 0 || r === "" ? null : parseCellRef(r);
1835
- if (r !== void 0 && r !== "" && address === null) invalid(r);
1836
- const col = address?.col ?? lastCol + 1;
1837
- lastCol = col;
1838
- current.cells.push({
1839
- col,
1840
- hasR: r !== void 0,
1841
- element: e
1842
- });
1843
- }
2092
+ const { rows, cells, rowCount } = scan;
1844
2093
  const byRow = /* @__PURE__ */ new Map();
1845
2094
  for (const p of planned) {
1846
2095
  const list = byRow.get(p.row) ?? [];
@@ -1848,57 +2097,80 @@ function editSheetXml(text, part, sheet, planned, limits) {
1848
2097
  list.push(p);
1849
2098
  }
1850
2099
  for (const list of byRow.values()) list.sort((a, b) => a.col - b.col);
1851
- const forceRefs = rows.some((r) => !r.hasR);
1852
2100
  const emitted = /* @__PURE__ */ new Set();
1853
- const existingRows = new Set(rows.map((r) => r.number));
2101
+ const existingRows = /* @__PURE__ */ new Set();
2102
+ for (let i = 0; i < rowCount; i++) existingRows.add(rows.get(i * ROW_FIELDS$1));
1854
2103
  const pending = [...byRow.keys()].filter((n) => !existingRows.has(n)).sort((a, b) => a - b);
1855
2104
  let next = 0;
1856
2105
  const p = (xml) => withPrefix(xml, prefix);
2106
+ const out = new TextBuilder();
1857
2107
  const newRow = (n) => {
1858
- const cells = (byRow.get(n) ?? []).filter((c) => c.xml !== null).map((c) => p(c.xml ?? "")).join("");
1859
- return `${p(`<row r="${String(n)}">`)}${cells}</${prefix}row>`;
2108
+ out.add(p(`<row r="${String(n)}">`));
2109
+ for (const c of byRow.get(n) ?? []) if (c.xml !== null) out.add(p(c.xml));
2110
+ out.add(`</${prefix}row>`);
1860
2111
  };
1861
- let inner = "";
1862
- for (const row of rows) {
1863
- while (next < pending.length && (pending[next] ?? 0) < row.number) inner += newRow(pending[next++] ?? 0);
1864
- const edits = byRow.get(row.number);
1865
- const needsRefs = forceRefs || row.cells.some((c) => !c.hasR);
1866
- if (!edits && row.hasR && !needsRefs) {
1867
- inner += text.slice(row.element.start.start, row.element.end.end);
2112
+ for (let i = 0; i < rowCount; i++) {
2113
+ const at = i * ROW_FIELDS$1;
2114
+ const number = rows.get(at);
2115
+ const tagStart = rows.get(at + 1);
2116
+ const tagEnd = rows.get(at + 2);
2117
+ const endStart = rows.get(at + 3);
2118
+ const endEnd = rows.get(at + 4);
2119
+ const flags = rows.get(at + 5);
2120
+ const firstCell = rows.get(at + 6);
2121
+ const cellCount = rows.get(at + 7);
2122
+ const hasR = (flags & 1) === 1;
2123
+ while (next < pending.length && (pending[next] ?? 0) < number) newRow(pending[next++] ?? 0);
2124
+ const edits = byRow.get(number);
2125
+ let needsRefs = scan.missingRowRefs;
2126
+ for (let k = 0; !needsRefs && k < cellCount; k++) needsRefs = (cells.get((firstCell + k) * CELL_FIELDS$1 + 4) & 1) === 0;
2127
+ if (!edits && hasR && !needsRefs) {
2128
+ out.add(text.slice(tagStart, endEnd));
1868
2129
  continue;
1869
2130
  }
1870
- let tag = text.slice(row.element.start.start, row.element.start.end);
1871
- if (!row.hasR) tag = insertRef(tag, row.element.start.qname, String(row.number));
1872
- let body = "";
2131
+ let tag = text.slice(tagStart, tagEnd);
2132
+ if (!hasR) tag = insertRef(tag, flags >>> 2, String(number));
2133
+ const empty = (flags & 2) === 2;
2134
+ out.add(empty ? openTag(tag) : tag);
1873
2135
  const edited = new Set((edits ?? []).map((c) => c.col));
1874
- const toInsert = emitted.has(row.number) ? [] : (edits ?? []).filter((c) => c.xml !== null);
1875
- emitted.add(row.number);
2136
+ const toInsert = emitted.has(number) ? [] : (edits ?? []).filter((c) => c.xml !== null);
2137
+ emitted.add(number);
1876
2138
  let k = 0;
1877
- for (const cell of row.cells) {
1878
- while (k < toInsert.length && (toInsert[k]?.col ?? 0) < cell.col) body += p(toInsert[k++]?.xml ?? "");
1879
- if (edited.has(cell.col)) continue;
1880
- let cellText = text.slice(cell.element.start.start, cell.element.end.end);
1881
- if (!cell.hasR) cellText = insertRef(cellText, cell.element.start.qname, formatCellRef(row.number, cell.col));
1882
- body += cellText;
2139
+ for (let j = 0; j < cellCount; j++) {
2140
+ const c = (firstCell + j) * CELL_FIELDS$1;
2141
+ const col = cells.get(c);
2142
+ while (k < toInsert.length && (toInsert[k]?.col ?? 0) < col) out.add(p(toInsert[k++]?.xml ?? ""));
2143
+ if (edited.has(col)) continue;
2144
+ const cellFlags = cells.get(c + 4);
2145
+ if ((cellFlags & 1) === 1) out.add(text.slice(cells.get(c + 1), cells.get(c + 3)));
2146
+ else {
2147
+ out.add(insertRef(text.slice(cells.get(c + 1), cells.get(c + 2)), cellFlags >>> 1, formatCellRef(number, col)));
2148
+ out.add(text.slice(cells.get(c + 2), cells.get(c + 3)));
2149
+ }
1883
2150
  }
1884
- while (k < toInsert.length) body += p(toInsert[k++]?.xml ?? "");
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)}`;
2151
+ while (k < toInsert.length) out.add(p(toInsert[k++]?.xml ?? ""));
2152
+ out.add(empty ? `</${text.slice(tagStart + 1, tagStart + 1 + (flags >>> 2))}>` : text.slice(endStart, endEnd));
1886
2153
  }
1887
- while (next < pending.length) inner += newRow(pending[next++] ?? 0);
2154
+ while (next < pending.length) newRow(pending[next++] ?? 0);
2155
+ const inner = out.finish();
1888
2156
  const changes = [];
1889
2157
  if (sheetData.start.empty) changes.push([
1890
2158
  sheetData.start.start,
1891
2159
  sheetData.start.end,
1892
- `${openTag(text.slice(sheetData.start.start, sheetData.start.end))}${inner}</${sheetData.start.qname}>`
2160
+ [
2161
+ openTag(text.slice(sheetData.start.start, sheetData.start.end)),
2162
+ ...inner,
2163
+ `</${sheetData.start.qname}>`
2164
+ ]
1893
2165
  ]);
1894
2166
  else changes.push([
1895
2167
  sheetData.start.end,
1896
2168
  sheetData.end.start,
1897
2169
  inner
1898
2170
  ]);
1899
- const dimension = elements.find((e) => e.depth === 2 && e.local === "dimension");
2171
+ const dimension = scan.dimension;
1900
2172
  if (dimension) {
1901
- const old = parseRange(dimension.attrs.get("ref") ?? "");
2173
+ const old = parseRange(dimension.ref ?? "");
1902
2174
  const values = planned.filter((c) => c.xml !== null);
1903
2175
  if (values.length > 0) {
1904
2176
  let minRow = old?.start.row ?? Infinity;
@@ -1915,11 +2187,19 @@ function editSheetXml(text, part, sheet, planned, limits) {
1915
2187
  changes.push([
1916
2188
  dimension.start.start,
1917
2189
  dimension.start.end,
1918
- setAttr(text.slice(dimension.start.start, dimension.start.end), "ref", ref)
2190
+ [setAttr(text.slice(dimension.start.start, dimension.start.end), "ref", ref)]
1919
2191
  ]);
1920
2192
  }
1921
2193
  }
1922
- return splice(text, changes);
2194
+ changes.sort((a, b) => a[0] - b[0]);
2195
+ const pieces = [];
2196
+ let pos = 0;
2197
+ for (const [start, end, replacement] of changes) {
2198
+ pieces.push(text.slice(pos, start), ...replacement);
2199
+ pos = end;
2200
+ }
2201
+ pieces.push(text.slice(pos));
2202
+ return pieces;
1923
2203
  }
1924
2204
  const CALC_PR_LATER = [
1925
2205
  "oleSize",
@@ -1955,6 +2235,30 @@ function requestFullCalculation(edit, workbookPart) {
1955
2235
  created
1956
2236
  ]]));
1957
2237
  }
2238
+ /** Ranges with their text as written, found by a cell they contain (A3). */
2239
+ var RangeLookup = class {
2240
+ index = new RangeIndex();
2241
+ refs = /* @__PURE__ */ new Map();
2242
+ add(range, ref) {
2243
+ this.index.add(range);
2244
+ this.refs.set(range, ref);
2245
+ }
2246
+ /** A range that contains the cell and that `accept` (when given) accepts, with its text. */
2247
+ at(row, col, accept) {
2248
+ const cell = {
2249
+ row,
2250
+ col
2251
+ };
2252
+ const range = this.index.find({
2253
+ start: cell,
2254
+ end: cell
2255
+ }, accept);
2256
+ return range === void 0 ? void 0 : {
2257
+ ref: this.refs.get(range) ?? "",
2258
+ range
2259
+ };
2260
+ }
2261
+ };
1958
2262
  /**
1959
2263
  * editCells writes plain formulas only, and does not change cells whose formula covers other
1960
2264
  * cells: an array formula's range (P1 §18.17.6.3) or a shared formula master with dependents
@@ -1974,17 +2278,14 @@ function checkFormulaGroups(sheet, ref, at, value, cache) {
1974
2278
  if (value !== null && value.kind === "formula" && (value.group !== void 0 || value.master !== void 0)) throw refuse(`editCells writes plain formulas only; the value for ${ref} is an array or shared formula.`, "Write { formula: \"…\" } for each cell, or build the sheet with createWorkbook to write array and shared formulas.");
1975
2279
  let arrays = cache.get(sheet.name);
1976
2280
  if (arrays === void 0) {
1977
- arrays = [];
2281
+ arrays = new RangeLookup();
1978
2282
  for (const c of sheet.cells()) {
1979
2283
  const range = c.formula?.kind === "array" && c.formula.ref !== null ? parseRange(c.formula.ref) : null;
1980
- if (range !== null) arrays.push({
1981
- ref: c.formula.ref,
1982
- range
1983
- });
2284
+ if (range !== null) arrays.add(range, c.formula.ref);
1984
2285
  }
1985
2286
  cache.set(sheet.name, arrays);
1986
2287
  }
1987
- const array = arrays.find((a) => at.row >= a.range.start.row && at.row <= a.range.end.row && at.col >= a.range.start.col && at.col <= a.range.end.col);
2288
+ const array = arrays.at(at.row, at.col);
1988
2289
  if (array !== void 0) throw refuse(`${ref} belongs to the array formula ${array.ref}, which editCells cannot change cell by cell.`, `Change the array formula in a spreadsheet application, or rebuild the sheet with createWorkbook; cells outside ${array.ref} can be edited.`);
1989
2290
  const f = sheet.cell(ref)?.formula;
1990
2291
  const range = f?.kind === "shared" && f.ref !== null ? parseRange(f.ref) : null;
@@ -2145,6 +2446,7 @@ function planEdits(workbook, edits) {
2145
2446
  const scratchSheet = scratch.addSheet("Edit");
2146
2447
  const plans = /* @__PURE__ */ new Map();
2147
2448
  const arrayRanges = /* @__PURE__ */ new Map();
2449
+ const byName = new Map(workbook.sheets.map((sheet) => [sheet.name, sheet]));
2148
2450
  edits.forEach((raw, i) => {
2149
2451
  const o = readOptions(raw, [
2150
2452
  "sheet",
@@ -2157,7 +2459,7 @@ function planEdits(workbook, edits) {
2157
2459
  hint: "Give each edit a sheet, for example { sheet: \"Data\", cell: \"B2\", value: 5 }.",
2158
2460
  details: { option: "sheet" }
2159
2461
  });
2160
- const sheet = workbook.getSheet(sheetName);
2462
+ const sheet = byName.get(sheetName) ?? workbook.getSheet(sheetName);
2161
2463
  if (sheet.kind !== "worksheet" || sheet.part === null) throw new XlvError("XLV_E_INVALID_INPUT", `Sheet "${sheet.name}" is a ${sheet.kind} without editable cells.`, {
2162
2464
  location: { sheet: sheet.name },
2163
2465
  hint: "Edit cells of worksheets only."
@@ -2235,17 +2537,31 @@ function applyEdits$1(bytes, workbook, plans, limits, appended = null) {
2235
2537
  const part = plan.sheet.part ?? "";
2236
2538
  const planned = [];
2237
2539
  const text = edit.text(part);
2238
- const columnStyle = text === void 0 ? () => 0 : newCellStyles(text, part, limits, styles);
2540
+ const sheetScan = text === void 0 ? null : scanSheet(text, part, plan.sheet.name, limits, styles);
2239
2541
  let styled = null;
2240
- const styledCell = (ref) => (styled ??= new Map([...plan.sheet.styledCells()].map((cell) => [cell.ref, cell]))).get(ref);
2241
- const merges = plan.sheet.merges.map((m) => ({
2242
- ref: m,
2243
- range: parseRange(m.trim().toUpperCase())
2244
- }));
2542
+ const styledCell = (ref) => {
2543
+ if (styled === null) {
2544
+ const wanted = /* @__PURE__ */ new Set();
2545
+ for (const r of plan.cells.keys()) if (plan.sheet.cell(r) === void 0) wanted.add(r);
2546
+ styled = /* @__PURE__ */ new Map();
2547
+ if (wanted.size > 0) {
2548
+ for (const cell of plan.sheet.styledCells()) if (wanted.has(cell.ref)) styled.set(cell.ref, cell);
2549
+ }
2550
+ }
2551
+ return styled.get(ref);
2552
+ };
2553
+ let merges = null;
2245
2554
  for (const [ref, c] of plan.cells) {
2246
- if (c.value !== null) {
2247
- const merge = merges.find(({ range: r }) => r !== null && c.row >= r.start.row && c.row <= r.end.row && c.col >= r.start.col && c.col <= r.end.col && (c.row !== r.start.row || c.col !== r.start.col));
2248
- if (merge !== void 0 && merge.range !== null) {
2555
+ if (c.value !== null && plan.sheet.merges.length > 0) {
2556
+ if (merges === null) {
2557
+ merges = new RangeLookup();
2558
+ for (const m of plan.sheet.merges) {
2559
+ const range = parseRange(m.trim().toUpperCase());
2560
+ if (range !== null) merges.add(range, m);
2561
+ }
2562
+ }
2563
+ const merge = merges.at(c.row, c.col, (r) => c.row !== r.start.row || c.col !== r.start.col);
2564
+ if (merge !== void 0) {
2249
2565
  const topLeft = formatCellRef(merge.range.start.row, merge.range.start.col);
2250
2566
  warnings.add("XLV_W_MERGED_CELL_WRITE", `A value was written into a merged range outside its top-left cell (first: ${ref} in ${merge.ref}). Spreadsheet applications show only the top-left cell (${topLeft}), so the value will not be visible. Write it to the top-left cell, or unmerge the range.`, {
2251
2567
  part,
@@ -2265,7 +2581,7 @@ function applyEdits$1(bytes, workbook, plans, limits, appended = null) {
2265
2581
  });
2266
2582
  continue;
2267
2583
  }
2268
- const base = existing?.styleIndex ?? columnStyle(c.row, c.col);
2584
+ const base = existing?.styleIndex ?? (sheetScan === null ? 0 : newCellStyle(sheetScan, c.row, c.col));
2269
2585
  let patch = c.style ?? {};
2270
2586
  const existingIsDate = existing?.numFmt != null && isDateFormat(existing.numFmt);
2271
2587
  if (c.date !== null && patch.numFmt === void 0 && !existingIsDate) patch = {
@@ -2280,13 +2596,21 @@ function applyEdits$1(bytes, workbook, plans, limits, appended = null) {
2280
2596
  xml: newCellXml(ref, c.value, xf, sst)
2281
2597
  });
2282
2598
  }
2283
- if (text === void 0) continue;
2599
+ if (text === void 0 || sheetScan === null) continue;
2284
2600
  syncTables(edit, plan, appended !== null && appended.sheet === plan.sheet.name ? appended : null);
2285
- edit.write(part, editSheetXml(text, part, plan.sheet.name, planned, limits));
2601
+ edit.write(part, editSheetXml(text, part, plan.sheet.name, sheetScan, planned));
2286
2602
  }
2287
2603
  sst.finish();
2288
2604
  styles.finish();
2289
- if (formulasChanged || workbook.sheets.some((s) => [...s.cells()].some((cell) => cell.formula !== null))) {
2605
+ let hasFormulas = formulasChanged;
2606
+ for (const sheet of workbook.sheets) {
2607
+ if (hasFormulas) break;
2608
+ for (const cell of sheet.cells()) if (cell.formula !== null) {
2609
+ hasFormulas = true;
2610
+ break;
2611
+ }
2612
+ }
2613
+ if (hasFormulas) {
2290
2614
  requestFullCalculation(edit, workbookPart);
2291
2615
  warnings.add("XLV_W_FORMULAS_NOT_RECALCULATED", "The workbook contains formulas. xlvane does not recalculate them: cached results may be out of date until the file is opened in a spreadsheet application, which is asked to recalculate on load.", { part: workbookPart });
2292
2616
  }
@@ -2329,19 +2653,25 @@ function applyEdits$1(bytes, workbook, plans, limits, appended = null) {
2329
2653
  function editCells(input, edits, options) {
2330
2654
  const bytes = inputBytes(input);
2331
2655
  if (!Array.isArray(edits)) throw new XlvError("XLV_E_INVALID_INPUT", "editCells expects an array of edits.", { hint: "Pass an array such as [{ sheet: \"Data\", cell: \"A1\", value: 1 }]." });
2332
- const o = readOptions(options, ["limits"], "editCells options");
2333
- const limits = resolveLimits(o.get("limits"));
2656
+ const { limits, writable } = editLimits(readOptions(options, ["limits"], "editCells options").get("limits"));
2657
+ checkWrites(edits.length, writable, `${String(edits.length)} edits`);
2658
+ const copies = edits.map((e) => typeof e === "object" && e !== null && !Array.isArray(e) ? { ...e } : e);
2334
2659
  const workbook = readWorkbook(bytes, { limits });
2335
- return applyEdits$1(bytes, workbook, planEdits(workbook, edits), limits);
2660
+ checkCellCount(workbook, newCells(workbook, copies), limits.maxCells);
2661
+ return applyEdits$1(bytes, workbook, planEdits(workbook, copies), limits);
2336
2662
  }
2337
2663
  /**
2338
2664
  * Appends rows below the last used row of a sheet (the rows of merged ranges count as used) in
2339
2665
  * an existing XLSX file, editing the file surgically like {@link editCells}. Object rows are
2340
2666
  * placed under the matching header columns (unknown keys throw `XLV_E_INVALID_ROWS` with
2341
- * suggestions); array rows start at the first used column. A table whose last row is the sheet's last used row grows to include the new
2342
- * rows (its range and filter); if that table has a totals row the call is refused with
2667
+ * suggestions). Array rows start at the first used column, or, when the new rows extend a table
2668
+ * (one that reaches the last used row), at that table's first column; when several such tables
2669
+ * start in different columns the call is refused with `XLV_E_INVALID_ARGUMENT` (pass object rows
2670
+ * instead). A table whose last row is the sheet's last used row grows to include the new rows
2671
+ * (its range and filter); if that table has a totals row the call is refused with
2343
2672
  * `XLV_E_NOT_SUPPORTED` and a hint to insert the rows above the totals row with `insertRows`.
2344
- * Calculated columns are not filled in the new rows.
2673
+ * Calculated columns are not filled in the new rows. One call writes at most 1 000 000 cells
2674
+ * unless `limits.maxCells` is given (see `EditOptions`).
2345
2675
  *
2346
2676
  * @example
2347
2677
  * ```ts
@@ -2352,15 +2682,26 @@ function editCells(input, edits, options) {
2352
2682
  * { Customer: 'Alice', Total: 120.5 },
2353
2683
  * { Customer: 'Bob', Total: 80 },
2354
2684
  * ]);
2685
+ * // Array rows that extend a table start at the table's first column.
2686
+ * const { bytes: more } = appendRows(updated, 'Orders', [['Carol', 64]]);
2687
+ * console.log(more.length);
2355
2688
  * ```
2356
2689
  */
2357
2690
  function appendRows(input, sheet, rows, options) {
2358
2691
  const bytes = inputBytes(input);
2359
2692
  const o = readOptions(options, ["limits", "header"], "appendRows options");
2360
- const limits = resolveLimits(o.get("limits"));
2693
+ const { limits, writable } = editLimits(o.get("limits"));
2361
2694
  const workbook = readWorkbook(bytes, { limits });
2362
2695
  const target = pickSheet(workbook, sheet);
2363
2696
  if (!Array.isArray(rows)) throw new XlvError("XLV_E_INVALID_ROWS", "appendRows expects an array of rows.", { hint: "Pass an array of objects or arrays." });
2697
+ let values = 0;
2698
+ for (const row of rows) if (Array.isArray(row)) {
2699
+ for (const value of row) if (value !== null && value !== void 0) values++;
2700
+ } else if (typeof row === "object" && row !== null) {
2701
+ for (const key of Object.keys(row)) if (row[key] !== null && row[key] !== void 0) values++;
2702
+ }
2703
+ checkWrites(values, writable, `Appending ${String(rows.length)} rows`, target.name);
2704
+ checkCellCount(workbook, /* @__PURE__ */ new Map([[target, values]]), limits.maxCells);
2364
2705
  let lastRow = 0;
2365
2706
  let firstCol = Infinity;
2366
2707
  for (const cell of target.cells()) {
@@ -2380,6 +2721,27 @@ function appendRows(input, sheet, rows, options) {
2380
2721
  value: MAX_ROWS
2381
2722
  }
2382
2723
  });
2724
+ let arrayCol = firstCol;
2725
+ if (rows.some((row) => Array.isArray(row))) {
2726
+ const joined = target.tables.map((table) => ({
2727
+ table,
2728
+ range: parseRange(table.ref.trim().toUpperCase())
2729
+ })).filter((t) => t.range !== null && lastRow > 0 && t.range.end.row >= lastRow && t.range.start.row <= lastRow);
2730
+ const starts = new Set(joined.map((t) => t.range.start.col));
2731
+ if (starts.size > 1) {
2732
+ const names = joined.map((t) => `"${t.table.displayName}" (${t.table.ref})`).join(", ");
2733
+ throw new XlvError("XLV_E_INVALID_ARGUMENT", `Tables ${names} of sheet "${target.name}" reach the last used row ${String(lastRow)} and start in different columns, so it is not clear in which column array rows start.`, {
2734
+ location: { sheet: target.name },
2735
+ hint: "Pass the rows as objects keyed by header text, for example [{ Customer: \"Dan\", Total: 42 }], so that each value goes under its column (use { header: n } to name the header row).",
2736
+ details: {
2737
+ tables: joined.map((t) => t.table.displayName),
2738
+ row: lastRow
2739
+ }
2740
+ });
2741
+ }
2742
+ const [start] = starts;
2743
+ if (start !== void 0) arrayCol = start;
2744
+ }
2383
2745
  let columns = null;
2384
2746
  const edits = [];
2385
2747
  rows.forEach((row, i) => {
@@ -2388,7 +2750,7 @@ function appendRows(input, sheet, rows, options) {
2388
2750
  row.forEach((value, k) => {
2389
2751
  if (value !== null && value !== void 0) edits.push({
2390
2752
  sheet: target.name,
2391
- cell: formatCellRef(r, firstCol + k),
2753
+ cell: formatCellRef(r, arrayCol + k),
2392
2754
  value
2393
2755
  });
2394
2756
  });
@@ -2685,6 +3047,13 @@ function adjustRange(sqref, change) {
2685
3047
  }
2686
3048
  // src/rows.ts
2687
3049
  /** Relationship types of a worksheet whose parts do not depend on row positions or are handled here. */
3050
+ /**
3051
+ * Media type of pivot cache definition parts, as Microsoft Excel writes it in
3052
+ * `[Content_Types].xml` (Apache POI corpus files poi/ExcelPivotTableSample.xlsx, poi/54436.xlsx,
3053
+ * poi/DataTableCities.xlsx; B6). The relationship from the workbook ends in
3054
+ * `/pivotCacheDefinition` in the same files.
3055
+ */
3056
+ const PIVOT_CACHE_DEFINITION_CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCacheDefinition+xml";
2688
3057
  const SAFE_SHEET_RELATIONSHIPS = /* @__PURE__ */ new Set([
2689
3058
  "hyperlink",
2690
3059
  "comments",
@@ -2726,16 +3095,30 @@ function moveRow(n, c) {
2726
3095
  if (n < c.at) return n;
2727
3096
  return n > end ? n + c.count : null;
2728
3097
  }
3098
+ /** Decodes the predefined entities and character references of XML text; a reference to a code point that does not exist stays as written. */
2729
3099
  function decodeEntities(s) {
2730
- return s.replace(/&(#x[0-9a-fA-F]+|#[0-9]+|amp|lt|gt|quot|apos);/g, (_m, e) => {
3100
+ return s.replace(/&(#x[0-9a-fA-F]+|#[0-9]+|amp|lt|gt|quot|apos);/g, (m, e) => {
2731
3101
  if (e === "amp") return "&";
2732
3102
  if (e === "lt") return "<";
2733
3103
  if (e === "gt") return ">";
2734
3104
  if (e === "quot") return "\"";
2735
3105
  if (e === "apos") return "'";
2736
- return String.fromCodePoint(e.startsWith("#x") ? parseInt(e.slice(2), 16) : parseInt(e.slice(1), 10));
3106
+ const cp = e.startsWith("#x") ? parseInt(e.slice(2), 16) : parseInt(e.slice(1), 10);
3107
+ return cp <= 1114111 ? String.fromCodePoint(cp) : m;
2737
3108
  });
2738
3109
  }
3110
+ /**
3111
+ * Whether the XML text of a part may refer to a sheet: its name, ignoring case, appears in the
3112
+ * text after entity and character references are decoded, as written or in the quoted form of a
3113
+ * formula, where each "'" is doubled (docs/facts/formulas.md F2-11). Searching the raw XML missed names such
3114
+ * as "R&D" (written "R&amp;D") and "O'Brien" (written "'O''Brien'!", B6). It may find the name
3115
+ * where it is not a reference, which only refuses more.
3116
+ */
3117
+ function mentionsSheet(xml, sheet) {
3118
+ const text = decodeEntities(xml).toLowerCase();
3119
+ const name = sheet.toLowerCase();
3120
+ return text.includes(name) || name.includes("'") && text.includes(name.replace(/'/g, "''"));
3121
+ }
2739
3122
  function removeAttr(tag, name) {
2740
3123
  return tag.replace(new RegExp(`\\s${name}\\s*=\\s*("[^"]*"|'[^']*')`), "");
2741
3124
  }
@@ -2797,7 +3180,6 @@ function applyEdits(text, edits) {
2797
3180
  return out.join("");
2798
3181
  }
2799
3182
  const KEEP_DEPTH3 = /* @__PURE__ */ new Set([
2800
- "row",
2801
3183
  "mergeCell",
2802
3184
  "hyperlink",
2803
3185
  "cfRule",
@@ -2806,9 +3188,159 @@ const KEEP_DEPTH3 = /* @__PURE__ */ new Set([
2806
3188
  "ignoredError",
2807
3189
  "protectedRange"
2808
3190
  ]);
3191
+ /** Integers per row of {@link WorksheetScan.rows}: number, start tag start and end, element end, flags, first cell, cell count. */
3192
+ const ROW_FIELDS = 7;
3193
+ /** Integers per cell of {@link WorksheetScan.cells}: column, start tag start and end, element end, flags. */
3194
+ const CELL_FIELDS = 5;
3195
+ /** Integers per formula of {@link WorksheetScan.formulas}: row, column, start tag start and end, end tag start and end, kind. */
3196
+ const FORMULA_FIELDS = 7;
3197
+ /** Row and cell flags. */
3198
+ const HAS_R = 1;
3199
+ const EMPTY = 2;
3200
+ /** Formula kinds (`f@t`); any other value is handled as a normal formula. */
3201
+ const NORMAL = 0;
3202
+ const SHARED = 1;
3203
+ const ARRAY = 2;
3204
+ const DATA_TABLE = 3;
3205
+ function scanWorksheet(text, part, limits) {
3206
+ const keep = (local, depth, parent, ns) => depth === 2 || depth === 3 && (KEEP_DEPTH3.has(local) || local === "ext" && parent === "extLst") || depth === 4 && (local === "formula" && parent === "cfRule" || (local === "formula1" || local === "formula2") && parent === "dataValidation") || depth === 5 && local === "cfvo" && CFVO_PARENTS.has(parent) || ns === "http://schemas.microsoft.com/office/spreadsheetml/2009/9/main" && (depth === 4 && (local === "conditionalFormattings" || local === "dataValidations") || depth === 5 && (local === "conditionalFormatting" || local === "dataValidation") || depth === 6 && (local === "cfRule" || local === "id" && parent === "ext")) || ns === "http://schemas.microsoft.com/office/excel/2006/main" && (depth === 6 && local === "sqref" || depth >= 7 && local === "f");
3207
+ const elements = [];
3208
+ /** Per open element: the kept Element, or a code for a recorded row (1), cell (2) or formula (3), or null. */
3209
+ const open = [];
3210
+ const names = [];
3211
+ let root = false;
3212
+ const rows = new IntList();
3213
+ const cells = new IntList();
3214
+ const formulas = new IntList();
3215
+ let rowCount = 0;
3216
+ let formulaCount = 0;
3217
+ let currentRow = 0;
3218
+ let lastCol = 0;
3219
+ let hasRow = false;
3220
+ let hasCell = false;
3221
+ let missingRefs = false;
3222
+ let invalidRef = null;
3223
+ parseXml(text, {
3224
+ start(ns, local, attrs, span) {
3225
+ const depth = names.length + 1;
3226
+ const parent = names[names.length - 1] ?? "";
3227
+ names.push(local);
3228
+ if (depth === 1) {
3229
+ root = true;
3230
+ open.push(null);
3231
+ return;
3232
+ }
3233
+ if (depth === 3 && local === "row") {
3234
+ const r = attrs.get("r");
3235
+ const n = r === void 0 ? currentRow + 1 : /^[0-9]{1,7}$/.test(r.trim()) ? Number(r.trim()) : NaN;
3236
+ if (!Number.isInteger(n) || n < 1 || n > 1048576) invalidRef ??= r ?? String(n);
3237
+ currentRow = Number.isInteger(n) ? n : currentRow;
3238
+ lastCol = 0;
3239
+ if (r === void 0) missingRefs = true;
3240
+ rows.push(n);
3241
+ rows.push(span.start);
3242
+ rows.push(span.end);
3243
+ rows.push(span.end);
3244
+ rows.push((r === void 0 ? 0 : HAS_R) | (span.empty ? EMPTY : 0));
3245
+ rows.push(cells.length / CELL_FIELDS);
3246
+ rows.push(0);
3247
+ rowCount++;
3248
+ hasRow = true;
3249
+ hasCell = false;
3250
+ open.push(1);
3251
+ return;
3252
+ }
3253
+ if (depth === 4 && local === "c" && parent === "row" && hasRow) {
3254
+ const r = attrs.get("r");
3255
+ const address = r === void 0 || r === "" ? null : parseCellRef(r);
3256
+ if (r !== void 0 && r !== "" && address === null) invalidRef ??= r;
3257
+ const col = address?.col ?? lastCol + 1;
3258
+ lastCol = col;
3259
+ const hasR = r !== void 0 && r !== "";
3260
+ if (!hasR) missingRefs = true;
3261
+ cells.push(col);
3262
+ cells.push(span.start);
3263
+ cells.push(span.end);
3264
+ cells.push(span.end);
3265
+ cells.push((hasR ? HAS_R : 0) | (span.empty ? EMPTY : 0));
3266
+ const count = (rowCount - 1) * ROW_FIELDS + 6;
3267
+ rows.set(count, rows.get(count) + 1);
3268
+ hasCell = true;
3269
+ open.push(2);
3270
+ return;
3271
+ }
3272
+ if (depth === 5 && local === "f" && parent === "c" && hasCell) {
3273
+ const t = attrs.get("t");
3274
+ formulas.push(currentRow);
3275
+ formulas.push(lastCol);
3276
+ formulas.push(span.start);
3277
+ formulas.push(span.end);
3278
+ formulas.push(span.end);
3279
+ formulas.push(span.end);
3280
+ formulas.push(t === "shared" ? SHARED : t === "array" ? ARRAY : t === "dataTable" ? DATA_TABLE : NORMAL);
3281
+ formulaCount++;
3282
+ open.push(3);
3283
+ return;
3284
+ }
3285
+ if (keep(local, depth, parent, ns)) {
3286
+ const element = {
3287
+ local,
3288
+ ns,
3289
+ depth,
3290
+ attrs,
3291
+ start: span,
3292
+ end: span
3293
+ };
3294
+ elements.push(element);
3295
+ open.push(element);
3296
+ } else open.push(null);
3297
+ },
3298
+ end(_ns, _local, span) {
3299
+ names.pop();
3300
+ const what = open.pop();
3301
+ if (what === 1) rows.set((rowCount - 1) * ROW_FIELDS + 3, span.end);
3302
+ else if (what === 2) cells.set(cells.length - 2, span.end);
3303
+ else if (what === 3) {
3304
+ formulas.set(formulas.length - 3, span.start);
3305
+ formulas.set(formulas.length - 2, span.end);
3306
+ } else if (what !== null && what !== void 0) what.end = span;
3307
+ },
3308
+ text() {}
3309
+ }, {
3310
+ part,
3311
+ limits
3312
+ });
3313
+ if (!root) throw new XlvError("XLV_E_PART_INVALID", "The part has no root element.", {
3314
+ location: { part },
3315
+ hint: "Re-save the file in a spreadsheet application."
3316
+ });
3317
+ return {
3318
+ elements,
3319
+ rows,
3320
+ rowCount,
3321
+ cells,
3322
+ formulas,
3323
+ formulaCount,
3324
+ missingRefs,
3325
+ invalidRef
3326
+ };
3327
+ }
3328
+ /**
3329
+ * The attributes of a start tag given as source text (already checked by the XML parser), with
3330
+ * references decoded. Used for the few tags whose attributes are needed again, instead of keeping
3331
+ * the attributes of every cell and formula.
3332
+ */
3333
+ function tagAttributes(tag) {
3334
+ const attrs = /* @__PURE__ */ new Map();
3335
+ const pattern = /\s+([^\s=/>]+)\s*=\s*(?:"([^"]*)"|'([^']*)')/y;
3336
+ pattern.lastIndex = tag.search(/[\s/>]/);
3337
+ for (let m = pattern.exec(tag); m !== null; m = pattern.exec(tag)) attrs.set(m[1] ?? "", decodeEntities((m[2] ?? m[3] ?? "").replace(/[\t\n\r]/g, " ")));
3338
+ return attrs;
3339
+ }
2809
3340
  /** Rewrites one worksheet part: references everywhere, and row positions on the changed sheet. */
2810
- function rewriteSheet(text, part, sheet, c, target, style, limits) {
2811
- const { elements } = scan(text, part, limits, (local, depth, parent, ns) => depth === 2 || depth === 3 && (KEEP_DEPTH3.has(local) || local === "ext" && parent === "extLst") || depth === 4 && (local === "c" && parent === "row" || local === "formula" && parent === "cfRule" || (local === "formula1" || local === "formula2") && parent === "dataValidation") || depth === 5 && (local === "f" && parent === "c" || local === "cfvo" && CFVO_PARENTS.has(parent)) || ns === "http://schemas.microsoft.com/office/spreadsheetml/2009/9/main" && (depth === 4 && (local === "conditionalFormattings" || local === "dataValidations") || depth === 5 && (local === "conditionalFormatting" || local === "dataValidation") || depth === 6 && (local === "cfRule" || local === "id" && parent === "ext")) || ns === "http://schemas.microsoft.com/office/excel/2006/main" && (depth === 6 && local === "sqref" || depth >= 7 && local === "f"));
3341
+ function rewriteSheet(text, part, sheet, c, target, style, limits, workbookCells) {
3342
+ const scanned = scanWorksheet(text, part, limits);
3343
+ const { elements, rows, rowCount, cells, formulas, formulaCount } = scanned;
2812
3344
  const edits = [];
2813
3345
  const tagOf = (e) => text.slice(e.start.start, e.start.end);
2814
3346
  const replaceTag = (e, tag) => {
@@ -2856,59 +3388,21 @@ function rewriteSheet(text, part, sheet, c, target, style, limits) {
2856
3388
  if (ext.depth !== 3 || ext.local !== "ext" || knownExts.has(ext)) continue;
2857
3389
  const body = text.slice(ext.start.start, ext.end.end);
2858
3390
  if (target && /sqref|<xm:f|sparkline|slicer/i.test(body)) throw notSupported(`Sheet "${sheet.name}" has extended content (such as sparklines or slicers) that refers to cells, which xlvane cannot move with its rows yet.`, where(), { element: "extLst" });
2859
- if (!target && /<xm:f|sparkline/i.test(body) && body.toLowerCase().includes(c.sheet.toLowerCase())) throw notSupported(`Sheet "${sheet.name}" has extended content (such as sparklines) that may refer to sheet "${c.sheet}", which xlvane cannot rewrite yet.`, where(), { element: "extLst" });
3391
+ if (!target && /<xm:f|sparkline/i.test(body) && mentionsSheet(body, c.sheet)) throw notSupported(`Sheet "${sheet.name}" has extended content (such as sparklines) that may refer to sheet "${c.sheet}", which xlvane cannot rewrite yet.`, where(), { element: "extLst" });
2860
3392
  }
2861
- const rows = [];
2862
- let current = null;
2863
- let currentRow = 0;
2864
- let lastCol = 0;
2865
- let lastCell = null;
2866
- const formulas = [];
2867
- const invalidRef = (ref) => {
3393
+ if (scanned.invalidRef !== null) {
3394
+ const ref = scanned.invalidRef;
2868
3395
  throw new XlvError("XLV_E_INVALID_CELL_REF", `Sheet "${sheet.name}" contains the invalid reference "${ref.slice(0, 20)}", so its rows cannot be moved safely.`, {
2869
3396
  location: where(),
2870
3397
  hint: "Open and re-save the file in a spreadsheet application, which drops the damaged cells, then try again.",
2871
3398
  details: { ref: ref.slice(0, 40) }
2872
3399
  });
2873
- };
2874
- let hasFormulas = false;
3400
+ }
3401
+ const hasFormulas = formulaCount > 0;
2875
3402
  let extension = null;
2876
3403
  for (const e of elements) {
2877
3404
  if (e.depth === 3 && e.local === "ext") extension = knownExts.has(e) ? e : null;
2878
- if (e.depth === 3 && e.local === "row") {
2879
- const r = e.attrs.get("r");
2880
- const n = r === void 0 ? currentRow + 1 : /^[0-9]{1,7}$/.test(r.trim()) ? Number(r.trim()) : NaN;
2881
- if (!Number.isInteger(n) || n < 1 || n > 1048576) invalidRef(r ?? String(n));
2882
- currentRow = n;
2883
- current = {
2884
- number: n,
2885
- hasR: r !== void 0,
2886
- element: e,
2887
- cells: []
2888
- };
2889
- rows.push(current);
2890
- lastCol = 0;
2891
- } else if (e.depth === 4 && e.local === "c" && current) {
2892
- const r = e.attrs.get("r");
2893
- const address = r === void 0 || r === "" ? null : parseCellRef(r);
2894
- if (r !== void 0 && r !== "" && address === null) invalidRef(r);
2895
- const col = address?.col ?? lastCol + 1;
2896
- lastCol = col;
2897
- lastCell = {
2898
- col,
2899
- hasR: r !== void 0 && r !== "",
2900
- element: e,
2901
- f: null
2902
- };
2903
- current.cells.push(lastCell);
2904
- } else if (e.depth === 5 && e.local === "f" && lastCell && current) {
2905
- lastCell.f = e;
2906
- hasFormulas = true;
2907
- formulas.push({
2908
- cell: lastCell,
2909
- row: current
2910
- });
2911
- } else if (e.depth === 4 && (e.local === "formula" || e.local === "formula1" || e.local === "formula2")) {
3405
+ if (e.depth === 4 && (e.local === "formula" || e.local === "formula1" || e.local === "formula2")) {
2912
3406
  const raw = content(text, e);
2913
3407
  const out = adjustText(raw, c, sheet.name);
2914
3408
  if (out === null) throw notSupported(`A ${e.local === "formula" ? "conditional format" : "data validation"} formula of sheet "${sheet.name}" cannot be adjusted safely: "${decodeEntities(raw).slice(0, 60)}".`, where(), { formula: decodeEntities(raw).slice(0, 80) });
@@ -2951,12 +3445,14 @@ function rewriteSheet(text, part, sheet, c, target, style, limits) {
2951
3445
  }
2952
3446
  }
2953
3447
  const masters = /* @__PURE__ */ new Map();
2954
- for (const { cell, row } of formulas) {
2955
- const f = cell.f;
2956
- if (f.attrs.get("t") === "shared" && f.attrs.has("ref") && f.attrs.has("si")) masters.set(f.attrs.get("si") ?? "", {
2957
- row: row.number,
2958
- col: cell.col,
2959
- text: content(text, f)
3448
+ for (let i = 0; i < formulaCount; i++) {
3449
+ const at = i * FORMULA_FIELDS;
3450
+ if (formulas.get(at + 6) !== SHARED) continue;
3451
+ const attrs = tagAttributes(text.slice(formulas.get(at + 2), formulas.get(at + 3)));
3452
+ if (attrs.get("t") === "shared" && attrs.has("ref") && attrs.has("si")) masters.set(attrs.get("si") ?? "", {
3453
+ row: formulas.get(at),
3454
+ col: formulas.get(at + 1),
3455
+ text: text.slice(formulas.get(at + 3), formulas.get(at + 4))
2960
3456
  });
2961
3457
  }
2962
3458
  const expand = /* @__PURE__ */ new Set();
@@ -2965,65 +3461,77 @@ function rewriteSheet(text, part, sheet, c, target, style, limits) {
2965
3461
  if (target || out === null || out !== m.text) expand.add(si);
2966
3462
  }
2967
3463
  const deletedRow = (n) => c.count < 0 && moveRow(n, c) === null;
2968
- for (const { cell, row } of formulas) {
2969
- const f = cell.f;
2970
- if (target && deletedRow(row.number)) continue;
2971
- const ref = formatCellRef(row.number, cell.col);
2972
- const kind = f.attrs.get("t");
3464
+ /** The changes of formula `i`: [start, end, text] replacements in document order. */
3465
+ const formulaChanges = (i) => {
3466
+ const at = i * FORMULA_FIELDS;
3467
+ const row = formulas.get(at);
3468
+ const col = formulas.get(at + 1);
3469
+ const tagStart = formulas.get(at + 2);
3470
+ const tagEnd = formulas.get(at + 3);
3471
+ const endStart = formulas.get(at + 4);
3472
+ const end = formulas.get(at + 5);
3473
+ const kind = formulas.get(at + 6);
3474
+ if (target && deletedRow(row)) return [];
3475
+ const ref = formatCellRef(row, col);
2973
3476
  const failed = (formula) => {
2974
3477
  throw notSupported(`The formula in ${ref} of sheet "${sheet.name}" cannot be adjusted safely: "${formula.slice(0, 60)}".`, where(ref), { formula: formula.slice(0, 80) });
2975
3478
  };
2976
- if (kind === "shared") {
2977
- const si = f.attrs.get("si") ?? "";
3479
+ const tag = text.slice(tagStart, tagEnd);
3480
+ if (kind === SHARED) {
3481
+ const si = tagAttributes(tag).get("si") ?? "";
2978
3482
  const master = masters.get(si);
2979
- if (master === void 0 || !expand.has(si)) continue;
3483
+ if (master === void 0 || !expand.has(si)) return [];
2980
3484
  const source = decodeEntities(master.text);
2981
- const own = shiftFormula(source, row.number - master.row, cell.col - master.col);
3485
+ const own = shiftFormula(source, row - master.row, col - master.col);
2982
3486
  if (own === null) failed(source);
2983
3487
  const adjusted = adjustFormula(own, refChange(c, sheet.name));
2984
3488
  if (adjusted === null) failed(own);
2985
- let tag = removeAttr(removeAttr(removeAttr(tagOf(f), "t"), "ref"), "si");
2986
- if (tag.endsWith("/>")) tag = `${tag.slice(0, -2).trimEnd()}>`;
2987
- edits.push([
2988
- f.start.start,
2989
- f.end.end,
2990
- `${tag}${escapeText(adjusted)}</${f.start.qname}>`
2991
- ]);
2992
- continue;
3489
+ let plain = removeAttr(removeAttr(removeAttr(tag, "t"), "ref"), "si");
3490
+ if (plain.endsWith("/>")) plain = `${plain.slice(0, -2).trimEnd()}>`;
3491
+ const qname = /^<([^\s/>]+)/.exec(tag)?.[1] ?? "f";
3492
+ return [[
3493
+ tagStart,
3494
+ end,
3495
+ `${plain}${escapeText(adjusted)}</${qname}>`
3496
+ ]];
2993
3497
  }
2994
- if (kind === "dataTable") {
3498
+ if (kind === DATA_TABLE) {
2995
3499
  if (target) throw notSupported(`Cell ${ref} of sheet "${sheet.name}" is part of a data table, which xlvane cannot move.`, where(ref));
2996
- continue;
3500
+ return [];
2997
3501
  }
2998
- const raw = content(text, f);
3502
+ const raw = text.slice(tagEnd, endStart);
2999
3503
  const out = adjustText(raw, c, sheet.name);
3000
3504
  if (out === null) failed(decodeEntities(raw));
3001
- let tag = tagOf(f);
3002
- if (kind === "array" && target) {
3003
- const range = parseRange(f.attrs.get("ref") ?? "");
3505
+ const changes = [];
3506
+ if (kind === ARRAY && target) {
3507
+ const arrayRef = tagAttributes(tag).get("ref") ?? "";
3508
+ const range = parseRange(arrayRef);
3004
3509
  if (range !== null) {
3005
- const end = c.at - c.count - 1;
3006
- if (c.count > 0 ? c.at > range.start.row && c.at <= range.end.row : !(range.start.row >= c.at && range.end.row <= end) && range.end.row >= c.at && range.start.row <= end) throw notSupported(`The array formula in ${f.attrs.get("ref") ?? ref} of sheet "${sheet.name}" would be split by the change.`, where(ref));
3007
- const moved = adjustRange(f.attrs.get("ref") ?? "", refChange(c, sheet.name));
3008
- if (moved !== null && moved !== "" && moved !== f.attrs.get("ref")) tag = setAttr(tag, "ref", moved);
3510
+ const last = c.at - c.count - 1;
3511
+ if (c.count > 0 ? c.at > range.start.row && c.at <= range.end.row : !(range.start.row >= c.at && range.end.row <= last) && range.end.row >= c.at && range.start.row <= last) throw notSupported(`The array formula in ${arrayRef === "" ? ref : arrayRef} of sheet "${sheet.name}" would be split by the change.`, where(ref));
3512
+ const moved = adjustRange(arrayRef, refChange(c, sheet.name));
3513
+ if (moved !== null && moved !== "" && moved !== arrayRef) changes.push([
3514
+ tagStart,
3515
+ tagEnd,
3516
+ setAttr(tag, "ref", moved)
3517
+ ]);
3009
3518
  }
3010
3519
  }
3011
- if (tag !== tagOf(f)) replaceTag(f, tag);
3012
- if (out !== raw) edits.push([
3013
- f.start.end,
3014
- f.end.start,
3520
+ if (out !== raw) changes.push([
3521
+ tagEnd,
3522
+ endStart,
3015
3523
  out
3016
3524
  ]);
3017
- }
3018
- if (!target) return {
3019
- text: edits.length === 0 ? text : applyEdits(text, edits),
3020
- hasFormulas
3525
+ return changes;
3021
3526
  };
3022
- const force = rows.some((r) => !r.hasR || r.cells.some((x) => !x.hasR));
3023
- for (const row of rows) {
3024
- const n = moveRow(row.number, c);
3025
- if (n === null) {
3026
- if (c.count > 0 && row.cells.some((x) => !x.element.start.empty)) throw new XlvError("XLV_E_LIMIT_EXCEEDED", `Inserting ${String(c.count)} rows would push row ${String(row.number)} of sheet "${sheet.name}", which holds data, past the last row of the sheet.`, {
3527
+ let force = false;
3528
+ if (target) {
3529
+ force = scanned.missingRefs;
3530
+ for (let i = 0; i < rowCount && c.count > 0; i++) {
3531
+ const at = i * ROW_FIELDS;
3532
+ const number = rows.get(at);
3533
+ if (moveRow(number, c) !== null) continue;
3534
+ for (let j = 0; j < rows.get(at + 6); j++) if ((cells.get((rows.get(at + 5) + j) * CELL_FIELDS + 4) & EMPTY) === 0) throw new XlvError("XLV_E_LIMIT_EXCEEDED", `Inserting ${String(c.count)} rows would push row ${String(number)} of sheet "${sheet.name}", which holds data, past the last row of the sheet.`, {
3027
3535
  location: where(),
3028
3536
  hint: `A sheet has ${String(MAX_ROWS)} rows. Delete the rows at the end of the sheet first, or insert fewer rows.`,
3029
3537
  details: {
@@ -3031,156 +3539,252 @@ function rewriteSheet(text, part, sheet, c, target, style, limits) {
3031
3539
  value: MAX_ROWS
3032
3540
  }
3033
3541
  });
3034
- removeElement(row.element);
3035
- continue;
3036
3542
  }
3037
- if (n === row.number && !force) continue;
3038
- replaceTag(row.element, setAttr(tagOf(row.element), "r", String(n)));
3039
- for (const cell of row.cells) replaceTag(cell.element, setAttr(tagOf(cell.element), "r", formatCellRef(n, cell.col)));
3040
- }
3041
- if (c.count > 0 && style !== "none") {
3042
- const source = rows.find((r) => r.number === (style === "above" ? c.at - 1 : c.at));
3043
- if (source) {
3044
- const prefix = source.element.start.qname.slice(0, source.element.start.qname.length - 3);
3045
- const keep = [
3046
- "s",
3047
- "customFormat",
3048
- "ht",
3049
- "customHeight"
3050
- ].filter((a) => source.element.attrs.has(a));
3051
- let created = "";
3052
- for (let i = 0; i < c.count; i++) {
3053
- const n = c.at + i;
3054
- const attrs = keep.map((a) => ` ${a}="${escapeAttr(source.element.attrs.get(a) ?? "")}"`).join("");
3055
- const cells = source.cells.filter((x) => (x.element.attrs.get("s") ?? "0") !== "0").map((x) => `<${prefix}c r="${formatCellRef(n, x.col)}" s="${escapeAttr(x.element.attrs.get("s") ?? "0")}"/>`).join("");
3056
- created += cells === "" ? `<${prefix}row r="${String(n)}"${attrs}/>` : `<${prefix}row r="${String(n)}"${attrs}>${cells}</${prefix}row>`;
3057
- }
3058
- const next = rows.find((r) => r.number >= c.at);
3059
- if (next) edits.push([
3060
- next.element.start.start,
3061
- next.element.start.start,
3062
- created
3063
- ]);
3064
- else {
3065
- const sheetData = elements.find((e) => e.depth === 2 && e.local === "sheetData");
3066
- if (sheetData && !sheetData.start.empty) edits.push([
3067
- sheetData.end.start,
3068
- sheetData.end.start,
3069
- created
3070
- ]);
3071
- else if (sheetData) edits.push([
3072
- sheetData.start.start,
3073
- sheetData.start.end,
3074
- `${tagOf(sheetData).slice(0, -2).trimEnd()}>${created}</${sheetData.start.qname}>`
3543
+ const findRow = (test) => {
3544
+ for (let i = 0; i < rowCount; i++) if (test(rows.get(i * ROW_FIELDS))) return i;
3545
+ return -1;
3546
+ };
3547
+ if (c.count > 0 && style !== "none") {
3548
+ const source = findRow((n) => n === (style === "above" ? c.at - 1 : c.at));
3549
+ if (source >= 0) {
3550
+ const at = source * ROW_FIELDS;
3551
+ const rowTag = text.slice(rows.get(at + 1), rows.get(at + 2));
3552
+ const rowAttrs = tagAttributes(rowTag);
3553
+ const rowQname = /^<([^\s/>]+)/.exec(rowTag)?.[1] ?? "row";
3554
+ const prefix = rowQname.slice(0, rowQname.length - 3);
3555
+ const keep = [
3556
+ "s",
3557
+ "customFormat",
3558
+ "ht",
3559
+ "customHeight"
3560
+ ].filter((a) => rowAttrs.has(a));
3561
+ const formats = [];
3562
+ for (let j = 0; j < rows.get(at + 6); j++) {
3563
+ const cell = (rows.get(at + 5) + j) * CELL_FIELDS;
3564
+ const s = tagAttributes(text.slice(cells.get(cell + 1), cells.get(cell + 2))).get("s") ?? "0";
3565
+ if (s !== "0") formats.push({
3566
+ col: cells.get(cell),
3567
+ s
3568
+ });
3569
+ }
3570
+ const added = formats.length * c.count;
3571
+ if (workbookCells + added > limits.maxCells) throw new XlvError("XLV_E_LIMIT_EXCEEDED", `Inserting ${String(c.count)} rows formatted like row ${String(rows.get(at))} of sheet "${sheet.name}" would add ${String(added)} formatted cells, and the workbook would hold more cells than the limit of ${String(limits.maxCells)}.`, {
3572
+ location: where(),
3573
+ hint: `Cells that hold only a style count too. Insert fewer rows, use { style: 'none' } and format the rows afterwards, or, if there is enough memory, raise the limit, for example { limits: { maxCells: ${String(workbookCells + added)} } }.`,
3574
+ details: {
3575
+ limit: "maxCells",
3576
+ value: limits.maxCells
3577
+ }
3578
+ });
3579
+ const attrs = keep.map((a) => ` ${a}="${escapeAttr(rowAttrs.get(a) ?? "")}"`).join("");
3580
+ const created = new TextBuilder();
3581
+ for (let i = 0; i < c.count; i++) {
3582
+ const n = c.at + i;
3583
+ if (formats.length === 0) {
3584
+ created.add(`<${prefix}row r="${String(n)}"${attrs}/>`);
3585
+ continue;
3586
+ }
3587
+ created.add(`<${prefix}row r="${String(n)}"${attrs}>`);
3588
+ for (const f of formats) created.add(`<${prefix}c r="${formatCellRef(n, f.col)}" s="${escapeAttr(f.s)}"/>`);
3589
+ created.add(`</${prefix}row>`);
3590
+ }
3591
+ const createdText = created.finish().join("");
3592
+ const next = findRow((n) => n >= c.at);
3593
+ if (next >= 0) edits.push([
3594
+ rows.get(next * ROW_FIELDS + 1),
3595
+ rows.get(next * ROW_FIELDS + 1),
3596
+ createdText
3075
3597
  ]);
3598
+ else {
3599
+ const sheetData = elements.find((e) => e.depth === 2 && e.local === "sheetData");
3600
+ if (sheetData && !sheetData.start.empty) edits.push([
3601
+ sheetData.end.start,
3602
+ sheetData.end.start,
3603
+ createdText
3604
+ ]);
3605
+ else if (sheetData) edits.push([
3606
+ sheetData.start.start,
3607
+ sheetData.start.end,
3608
+ `${tagOf(sheetData).slice(0, -2).trimEnd()}>${createdText}</${sheetData.start.qname}>`
3609
+ ]);
3610
+ }
3076
3611
  }
3077
3612
  }
3078
- }
3079
- const removedIds = /* @__PURE__ */ new Set();
3080
- const linkIds = /* @__PURE__ */ new Map();
3081
- let format = null;
3082
- for (const e of elements) if (e.depth === 2) format = e.local === "conditionalFormatting" ? e : null;
3083
- else if (format !== null && e.depth === 6 && e.ns === "http://schemas.microsoft.com/office/spreadsheetml/2009/9/main" && e.local === "id") {
3084
- const list = linkIds.get(format) ?? [];
3085
- list.push(content(text, e).trim().toLowerCase());
3086
- linkIds.set(format, list);
3087
- }
3088
- const containers = /* @__PURE__ */ new Map();
3089
- let parent2 = null;
3090
- for (const e of elements) {
3091
- if (e.depth === 2) {
3092
- parent2 = e;
3093
- if (e.local === "dimension" || e.local === "autoFilter") {
3094
- const ref = e.attrs.get("ref");
3095
- if (ref === void 0) continue;
3096
- const out = adjustRange(ref, refChange(c, sheet.name));
3097
- if (out === null || out === ref) continue;
3098
- if (e.local === "dimension") replaceTag(e, setAttr(tagOf(e), "ref", out === "" ? "A1" : out.split(" ")[0] ?? "A1"));
3099
- else if (out === "") removeElement(e);
3100
- else replaceTag(e, setAttr(tagOf(e), "ref", out));
3101
- } else if (e.local === "conditionalFormatting") {
3613
+ const removedIds = /* @__PURE__ */ new Set();
3614
+ const linkIds = /* @__PURE__ */ new Map();
3615
+ let format = null;
3616
+ for (const e of elements) if (e.depth === 2) format = e.local === "conditionalFormatting" ? e : null;
3617
+ else if (format !== null && e.depth === 6 && e.ns === "http://schemas.microsoft.com/office/spreadsheetml/2009/9/main" && e.local === "id") {
3618
+ const list = linkIds.get(format) ?? [];
3619
+ list.push(content(text, e).trim().toLowerCase());
3620
+ linkIds.set(format, list);
3621
+ }
3622
+ const containers = /* @__PURE__ */ new Map();
3623
+ let parent2 = null;
3624
+ for (const e of elements) {
3625
+ if (e.depth === 2) {
3626
+ parent2 = e;
3627
+ if (e.local === "dimension" || e.local === "autoFilter") {
3628
+ const ref = e.attrs.get("ref");
3629
+ if (ref === void 0) continue;
3630
+ const out = adjustRange(ref, refChange(c, sheet.name));
3631
+ if (out === null || out === ref) continue;
3632
+ if (e.local === "dimension") replaceTag(e, setAttr(tagOf(e), "ref", out === "" ? "A1" : out.split(" ")[0] ?? "A1"));
3633
+ else if (out === "") removeElement(e);
3634
+ else replaceTag(e, setAttr(tagOf(e), "ref", out));
3635
+ } else if (e.local === "conditionalFormatting") {
3636
+ const sqref = e.attrs.get("sqref") ?? "";
3637
+ const out = adjustRange(sqref, refChange(c, sheet.name));
3638
+ if (out === null) throw notSupported(`A conditional format of sheet "${sheet.name}" has a range xlvane cannot adjust: "${sqref.slice(0, 60)}".`, where());
3639
+ if (out === "") {
3640
+ removeElement(e);
3641
+ for (const id of linkIds.get(e) ?? []) removedIds.add(id);
3642
+ } else if (out !== sqref) replaceTag(e, setAttr(tagOf(e), "sqref", out));
3643
+ }
3644
+ continue;
3645
+ }
3646
+ if (e.depth !== 3 || parent2 === null) continue;
3647
+ const container = parent2;
3648
+ const count = containers.get(container) ?? {
3649
+ kept: 0,
3650
+ total: 0
3651
+ };
3652
+ containers.set(container, count);
3653
+ if (e.local === "mergeCell") {
3654
+ count.total++;
3655
+ const ref = e.attrs.get("ref") ?? "";
3656
+ const range = parseRange(ref);
3657
+ const out = range === null ? null : c.count < 0 && deletedRow(range.start.row) ? "" : adjustRange(ref, refChange(c, sheet.name));
3658
+ if (out === null) {
3659
+ count.kept++;
3660
+ continue;
3661
+ }
3662
+ const moved = out === "" ? null : parseRange(out);
3663
+ if (moved === null || moved.start.row === moved.end.row && moved.start.col === moved.end.col) {
3664
+ removeElement(e);
3665
+ continue;
3666
+ }
3667
+ count.kept++;
3668
+ if (out !== ref) replaceTag(e, setAttr(tagOf(e), "ref", out));
3669
+ } else if (e.local === "dataValidation" || e.local === "ignoredError" || e.local === "protectedRange") {
3670
+ count.total++;
3102
3671
  const sqref = e.attrs.get("sqref") ?? "";
3103
3672
  const out = adjustRange(sqref, refChange(c, sheet.name));
3104
- if (out === null) throw notSupported(`A conditional format of sheet "${sheet.name}" has a range xlvane cannot adjust: "${sqref.slice(0, 60)}".`, where());
3673
+ if (out === null) throw notSupported(`Sheet "${sheet.name}" has a ${e.local} range xlvane cannot adjust: "${sqref.slice(0, 60)}".`, where());
3105
3674
  if (out === "") {
3106
3675
  removeElement(e);
3107
- for (const id of linkIds.get(e) ?? []) removedIds.add(id);
3108
- } else if (out !== sqref) replaceTag(e, setAttr(tagOf(e), "sqref", out));
3109
- }
3110
- continue;
3111
- }
3112
- if (e.depth !== 3 || parent2 === null) continue;
3113
- const container = parent2;
3114
- const count = containers.get(container) ?? {
3115
- kept: 0,
3116
- total: 0
3117
- };
3118
- containers.set(container, count);
3119
- if (e.local === "mergeCell") {
3120
- count.total++;
3121
- const ref = e.attrs.get("ref") ?? "";
3122
- const range = parseRange(ref);
3123
- const out = range === null ? null : c.count < 0 && deletedRow(range.start.row) ? "" : adjustRange(ref, refChange(c, sheet.name));
3124
- if (out === null) {
3676
+ continue;
3677
+ }
3125
3678
  count.kept++;
3126
- continue;
3127
- }
3128
- const moved = out === "" ? null : parseRange(out);
3129
- if (moved === null || moved.start.row === moved.end.row && moved.start.col === moved.end.col) {
3130
- removeElement(e);
3131
- continue;
3132
- }
3133
- count.kept++;
3134
- if (out !== ref) replaceTag(e, setAttr(tagOf(e), "ref", out));
3135
- } else if (e.local === "dataValidation" || e.local === "ignoredError" || e.local === "protectedRange") {
3136
- count.total++;
3137
- const sqref = e.attrs.get("sqref") ?? "";
3138
- const out = adjustRange(sqref, refChange(c, sheet.name));
3139
- if (out === null) throw notSupported(`Sheet "${sheet.name}" has a ${e.local} range xlvane cannot adjust: "${sqref.slice(0, 60)}".`, where());
3140
- if (out === "") {
3141
- removeElement(e);
3142
- continue;
3143
- }
3144
- count.kept++;
3145
- if (out !== sqref) replaceTag(e, setAttr(tagOf(e), "sqref", out));
3146
- } else if (e.local === "hyperlink") {
3147
- count.total++;
3148
- if (adjustRange(e.attrs.get("ref") ?? "", refChange(c, sheet.name)) !== "") count.kept++;
3149
- } else if (e.local === "brk" && container.local === "rowBreaks") {
3150
- count.total++;
3151
- const id = Number(e.attrs.get("id") ?? "NaN");
3152
- if (!Number.isInteger(id)) {
3679
+ if (out !== sqref) replaceTag(e, setAttr(tagOf(e), "sqref", out));
3680
+ } else if (e.local === "hyperlink") {
3681
+ count.total++;
3682
+ if (adjustRange(e.attrs.get("ref") ?? "", refChange(c, sheet.name)) !== "") count.kept++;
3683
+ } else if (e.local === "brk" && container.local === "rowBreaks") {
3684
+ count.total++;
3685
+ const id = Number(e.attrs.get("id") ?? "NaN");
3686
+ if (!Number.isInteger(id)) {
3687
+ count.kept++;
3688
+ continue;
3689
+ }
3690
+ const n = moveRow(id, c);
3691
+ if (n === null) {
3692
+ removeElement(e);
3693
+ continue;
3694
+ }
3153
3695
  count.kept++;
3154
- continue;
3696
+ if (n !== id) replaceTag(e, setAttr(tagOf(e), "id", String(n)));
3155
3697
  }
3156
- const n = moveRow(id, c);
3157
- if (n === null) {
3158
- removeElement(e);
3159
- continue;
3698
+ }
3699
+ for (const [container, n] of containers) {
3700
+ if (n.total === 0 || n.kept === n.total) continue;
3701
+ const counted = [
3702
+ "mergeCells",
3703
+ "dataValidations",
3704
+ "rowBreaks"
3705
+ ].includes(container.local);
3706
+ if (n.kept === 0) removeElement(container);
3707
+ else if (counted) {
3708
+ let tag = tagOf(container);
3709
+ if (container.attrs.has("count")) tag = setAttr(tag, "count", String(n.kept));
3710
+ if (container.local === "rowBreaks" && container.attrs.has("manualBreakCount")) tag = setAttr(tag, "manualBreakCount", String(n.kept));
3711
+ if (tag !== tagOf(container)) replaceTag(container, tag);
3160
3712
  }
3161
- count.kept++;
3162
- if (n !== id) replaceTag(e, setAttr(tagOf(e), "id", String(n)));
3163
- }
3164
- }
3165
- for (const [container, n] of containers) {
3166
- if (n.total === 0 || n.kept === n.total) continue;
3167
- const counted = [
3168
- "mergeCells",
3169
- "dataValidations",
3170
- "rowBreaks"
3171
- ].includes(container.local);
3172
- if (n.kept === 0) removeElement(container);
3173
- else if (counted) {
3174
- let tag = tagOf(container);
3175
- if (container.attrs.has("count")) tag = setAttr(tag, "count", String(n.kept));
3176
- if (container.local === "rowBreaks" && container.attrs.has("manualBreakCount")) tag = setAttr(tag, "manualBreakCount", String(n.kept));
3177
- if (tag !== tagOf(container)) replaceTag(container, tag);
3178
- }
3179
- }
3180
- rewriteExtensionRanges(text, elements, knownExts, c, sheet, removedIds, edits, where);
3181
- const result = applyEdits(text, edits);
3713
+ }
3714
+ rewriteExtensionRanges(text, elements, knownExts, c, sheet, removedIds, edits, where);
3715
+ } else if (edits.length === 0) {
3716
+ let changed = false;
3717
+ for (let i = 0; i < formulaCount && !changed; i++) changed = formulaChanges(i).length > 0;
3718
+ if (!changed) return {
3719
+ text,
3720
+ hasFormulas
3721
+ };
3722
+ }
3723
+ const general = [...edits].sort((a, b) => a[0] - b[0] || Number(b[0] === b[1]) - Number(a[0] === a[1]) || b[1] - a[1]);
3724
+ const out = new TextBuilder();
3725
+ let pos = 0;
3726
+ let g = 0;
3727
+ const flush = (limit) => {
3728
+ while (g < general.length) {
3729
+ const [start, end, replacement] = general[g];
3730
+ if (!(start < limit || start === limit && end === start)) break;
3731
+ g++;
3732
+ if (start < pos) continue;
3733
+ out.add(text.slice(pos, start));
3734
+ out.add(replacement);
3735
+ pos = end;
3736
+ }
3737
+ };
3738
+ const replace = (start, end, replacement) => {
3739
+ flush(start);
3740
+ if (start < pos) return;
3741
+ out.add(text.slice(pos, start));
3742
+ out.add(replacement);
3743
+ pos = end;
3744
+ };
3745
+ let f = 0;
3746
+ /** Formulas that start before `limit`: adjusted, or skipped when `skip`. */
3747
+ const formulasBefore = (limit, skip) => {
3748
+ for (; f < formulaCount && formulas.get(f * FORMULA_FIELDS + 2) < limit; f++) {
3749
+ if (skip) continue;
3750
+ for (const [start, end, replacement] of formulaChanges(f)) replace(start, end, replacement);
3751
+ }
3752
+ };
3753
+ for (let i = 0; i < rowCount; i++) {
3754
+ const at = i * ROW_FIELDS;
3755
+ const number = rows.get(at);
3756
+ const rowStart = rows.get(at + 1);
3757
+ const rowTagEnd = rows.get(at + 2);
3758
+ const rowEnd = rows.get(at + 3);
3759
+ formulasBefore(rowStart, false);
3760
+ const n = target ? moveRow(number, c) : number;
3761
+ if (n === null) {
3762
+ replace(rowStart, rowEnd, "");
3763
+ formulasBefore(rowEnd, true);
3764
+ continue;
3765
+ }
3766
+ const moved = target && (n !== number || force);
3767
+ if (moved) replace(rowStart, rowTagEnd, setAttr(text.slice(rowStart, rowTagEnd), "r", String(n)));
3768
+ const first = rows.get(at + 5);
3769
+ for (let j = 0; j < rows.get(at + 6); j++) {
3770
+ const cell = (first + j) * CELL_FIELDS;
3771
+ const cellStart = cells.get(cell + 1);
3772
+ formulasBefore(cellStart, false);
3773
+ if (moved) replace(cellStart, cells.get(cell + 2), setAttr(text.slice(cellStart, cells.get(cell + 2)), "r", formatCellRef(n, cells.get(cell))));
3774
+ formulasBefore(cells.get(cell + 3), false);
3775
+ }
3776
+ formulasBefore(rowEnd, false);
3777
+ }
3778
+ formulasBefore(text.length + 1, false);
3779
+ flush(text.length + 1);
3780
+ out.add(text.slice(pos));
3781
+ const pieces = out.finish();
3782
+ if (target && /sortState/.test(text)) return {
3783
+ text: rewriteSortStates(pieces.join(""), part, sheet, c, limits),
3784
+ hasFormulas
3785
+ };
3182
3786
  return {
3183
- text: /sortState/.test(result) ? rewriteSortStates(result, part, sheet, c, limits) : result,
3787
+ text: pieces,
3184
3788
  hasFormulas
3185
3789
  };
3186
3790
  }
@@ -3677,22 +4281,55 @@ function changeRows(input, sheetName, at, count, style, limits) {
3677
4281
  sheet: sheet.name
3678
4282
  }, { type: rel.type });
3679
4283
  }
3680
- const lowerName = sheet.name.toLowerCase();
4284
+ const charts = /* @__PURE__ */ new Set();
4285
+ const drawings = /* @__PURE__ */ new Map();
4286
+ const unmatched = (source, r) => notSupported(`The part ${source} has a relationship to ${r.target}, which is not a part of the package (its name may be written differently in the archive), so xlvane cannot check whether a chart there refers to sheet "${sheet.name}".`, {
4287
+ part: source,
4288
+ sheet: sheet.name
4289
+ }, {
4290
+ type: r.type,
4291
+ target: r.target
4292
+ });
4293
+ for (const name of pkg.partNames) {
4294
+ const type = pkg.contentType(name);
4295
+ if (type === "application/vnd.openxmlformats-officedocument.drawingml.chart+xml" || type === PIVOT_CACHE_DEFINITION_CONTENT_TYPE) charts.add(partKey(name));
4296
+ else if (type === "application/vnd.openxmlformats-officedocument.drawing+xml") drawings.set(partKey(name), name);
4297
+ }
4298
+ for (const s of workbook.sheets) {
4299
+ if (s.part === null) continue;
4300
+ for (const r of pkg.relationships(s.part)) {
4301
+ if (r.external || classifyRelationship(r.type)?.name !== "drawing") continue;
4302
+ if (!pkg.hasPart(r.target)) throw unmatched(s.part, r);
4303
+ drawings.set(partKey(r.target), r.target);
4304
+ }
4305
+ }
4306
+ for (const name of drawings.values()) for (const r of pkg.relationships(name)) {
4307
+ if (r.external || classifyRelationship(r.type)?.name !== "chart") continue;
4308
+ if (!pkg.hasPart(r.target)) throw unmatched(name, r);
4309
+ charts.add(partKey(r.target));
4310
+ }
4311
+ for (const r of pkg.relationships(workbookPart)) {
4312
+ if (r.external || classifyRelationship(r.type)?.name !== "pivotCacheDefinition") continue;
4313
+ if (!pkg.hasPart(r.target)) throw unmatched(workbookPart, r);
4314
+ charts.add(partKey(r.target));
4315
+ }
3681
4316
  for (const name of pkg.partNames) {
3682
- if (!/\/charts\/[^/]+\.xml$|pivotCacheDefinition[^/]*\.xml$/i.test(name)) continue;
4317
+ if (!charts.has(partKey(name)) && !/\/charts\/[^/]+\.xml$|pivotCacheDefinition[^/]*\.xml$/i.test(name)) continue;
3683
4318
  const text = edit.text(name);
3684
- if (text !== void 0 && text.toLowerCase().includes(lowerName)) throw notSupported(`The part ${name} (a chart or pivot table) may refer to sheet "${sheet.name}", and xlvane cannot rewrite it yet.`, {
4319
+ if (text !== void 0 && mentionsSheet(text, sheet.name)) throw notSupported(`The part ${name} (a chart or pivot table) may refer to sheet "${sheet.name}", and xlvane cannot rewrite it yet.`, {
3685
4320
  part: name,
3686
4321
  sheet: sheet.name
3687
4322
  });
3688
4323
  }
3689
4324
  let hasFormulas = false;
4325
+ let workbookCells = 0;
4326
+ for (const s of workbook.sheets) workbookCells += s.cellCount + styledCellCount(s);
3690
4327
  for (const s of workbook.sheets) {
3691
4328
  if (s.kind !== "worksheet" || s.part === null) continue;
3692
4329
  const text = edit.text(s.part);
3693
4330
  if (text === void 0) continue;
3694
4331
  const target = s.part === sheet.part;
3695
- const out = rewriteSheet(text, s.part, s, c, target, style, limits);
4332
+ const out = rewriteSheet(text, s.part, s, c, target, style, limits, workbookCells);
3696
4333
  hasFormulas ||= out.hasFormulas;
3697
4334
  if (out.text !== text) edit.write(s.part, out.text);
3698
4335
  rewriteTables(edit, pkg, s.part, s, c, target);
@@ -3732,7 +4369,9 @@ function changeRows(input, sheetName, at, count, style, limits) {
3732
4369
  * custom sheet views, other extended content that refers to cells (sparklines, slicers) and
3733
4370
  * other related parts xlvane does not know; charts or pivot tables anywhere in the workbook that
3734
4371
  * may refer to the sheet; data tables; an array formula the change would split; and formulas it
3735
- * cannot rewrite safely (docs/facts/ref-adjust.md).
4372
+ * cannot rewrite safely (docs/facts/ref-adjust.md). Rows formatted with `style` add a formatted
4373
+ * cell for each formatted cell of the row they copy; when the workbook would then hold more cells
4374
+ * than `limits.maxCells`, the call throws `XLV_E_LIMIT_EXCEEDED` before building them.
3736
4375
  *
3737
4376
  * @example
3738
4377
  * ```ts