xlvane 1.2.2 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- import { $ as fillXml, At as EMPTY_STYLES, B as tokenize, Bt as 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, 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-Cxt-LJE4.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);
@@ -558,8 +599,10 @@ function fromJSON(input, options) {
558
599
  // src/csv.ts
559
600
  /**
560
601
  * First characters that make spreadsheet applications treat a CSV field as a formula (CSV-10),
561
- * plus, as a precaution, the small forms of = + - @ (CSV-12, D-H01). Text starting with any other
562
- * C0 control character is escaped too (see `injectionStart`).
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`).
563
606
  */
564
607
  const INJECTION_START = /* @__PURE__ */ new Set([
565
608
  "=",
@@ -576,13 +619,42 @@ const INJECTION_START = /* @__PURE__ */ new Set([
576
619
  "﹦",
577
620
  "﹢",
578
621
  "﹣",
622
+ "﹫",
623
+ "",
624
+ "￾"
625
+ ]);
626
+ /** Signs that start a formula: refused as the delimiter while formulas are escaped (D-J01). */
627
+ const FORMULA_SIGNS = /* @__PURE__ */ new Set([
628
+ "=",
629
+ "+",
630
+ "-",
631
+ "@",
632
+ "=",
633
+ "+",
634
+ "-",
635
+ "@",
636
+ "﹦",
637
+ "﹢",
638
+ "﹣",
579
639
  "﹫"
580
640
  ]);
581
641
  /** Whether text starting with `c` is escaped: one of INJECTION_START or a C0 control (NUL included). */
582
642
  const injectionStart = (c) => INJECTION_START.has(c) || c.charCodeAt(0) < 32;
583
643
  /**
584
- * Converts a sheet to CSV text. Fields that contain the delimiter, a quote, CR or LF are
585
- * 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
586
658
  * shortest form, booleans as `TRUE`/`FALSE`, dates as ISO 8601, error cells as their code and
587
659
  * formula cells as their cached value. By default, text that a spreadsheet would run as a
588
660
  * formula is escaped (`formulaInjection: "escape"`).
@@ -615,6 +687,10 @@ function toCSV(input, options) {
615
687
  const newline = oneOfOption("newline", o.get("newline"), ["\r\n", "\n"], "\r\n");
616
688
  const bom = booleanOption("bom", o.get("bom"), false);
617
689
  const injection = oneOfOption("formulaInjection", o.get("formulaInjection"), ["escape", "off"], "escape");
690
+ 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.`, {
691
+ hint: "Use \",\" (default), \";\", \"|\" or \"\\t\", or pass formulaInjection: \"off\" for a trusted consumer.",
692
+ details: { option: "delimiter" }
693
+ });
618
694
  const limits = resolveLimits(o.get("limits"));
619
695
  const sheet = pickSheet(openInput(input, o.get("limits"), o.get("onWarning")), o.get("sheet"));
620
696
  let startRow = 1;
@@ -645,7 +721,7 @@ function toCSV(input, options) {
645
721
  cellCount++;
646
722
  }
647
723
  if (endRow >= startRow && endCol >= startCol) checkOutputPositions((endRow - startRow + 1) * (endCol - startCol + 1), cellCount, limits.maxCells, "The CSV text", sheet.name);
648
- 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));
649
725
  const field = (value) => {
650
726
  if (value === null || value === void 0) return "";
651
727
  if (typeof value === "number") return String(value);
@@ -803,7 +879,8 @@ function inferNumber(s) {
803
879
  const value = Number(s);
804
880
  if (!Number.isFinite(value)) return null;
805
881
  const digits = s.slice(intStart, intStart + intDigits + (fracDigits > 0 ? fracDigits + 1 : 0)).replace(".", "").replace(/^0+/, "");
806
- 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;
807
884
  if (digits.length > 15) {
808
885
  if (fracDigits === 0 && !/[eE]/.test(s) || String(value) !== s) return null;
809
886
  }
@@ -913,6 +990,7 @@ function fromCSV(input, options) {
913
990
  const workbook = createWorkbook(dateSystem === void 0 ? void 0 : { dateSystem });
914
991
  const tableOptions = /* @__PURE__ */ new Map([["sheet", o.get("sheet") ?? "Sheet1"], ["preset", oneOfOption("preset", o.get("preset"), ["plain", "report"], "plain")]]);
915
992
  if (columns) tableOptions.set("columns", columns);
993
+ tableOptions.set("maxCells", maxCells);
916
994
  addTableSheet(workbook, rows, tableOptions);
917
995
  return writeWorkbook(workbook);
918
996
  }
@@ -928,13 +1006,30 @@ function toDiffCell(cell, styles) {
928
1006
  if (styles) out.style = cell.style;
929
1007
  return out;
930
1008
  }
931
- function sameCell(a, b, styles) {
1009
+ /**
1010
+ * Compares styles by their stable key, computed once per style object: the reader shares one
1011
+ * frozen object per style, so a sheet of a million cells needs a few keys, not two per cell.
1012
+ */
1013
+ function styleComparer() {
1014
+ const keys = /* @__PURE__ */ new Map();
1015
+ const keyOf = (style) => {
1016
+ if (typeof style !== "object" || style === null) return stableKey(style);
1017
+ let key = keys.get(style);
1018
+ if (key === void 0) {
1019
+ key = stableKey(style);
1020
+ keys.set(style, key);
1021
+ }
1022
+ return key;
1023
+ };
1024
+ return (a, b) => a === b || keyOf(a) === keyOf(b);
1025
+ }
1026
+ function sameCell(a, b, sameStyle) {
932
1027
  if (a.type !== b.type) return false;
933
1028
  const av = a.value instanceof Date ? a.value.getTime() : a.value;
934
1029
  const bv = b.value instanceof Date ? b.value.getTime() : b.value;
935
1030
  if (!Object.is(av, bv)) return false;
936
1031
  if ((a.formula?.text ?? null) !== (b.formula?.text ?? null)) return false;
937
- return !styles || stableKey(a.style) === stableKey(b.style);
1032
+ return sameStyle === null || sameStyle(a.style, b.style);
938
1033
  }
939
1034
  /**
940
1035
  * Compares two workbooks: added and removed sheets (by name), visibility changes, and cells
@@ -983,44 +1078,45 @@ function diff(a, b, options) {
983
1078
  summary.sheetsAdded = sheetsAdded.length;
984
1079
  summary.sheetsRemoved = sheetsRemoved.length;
985
1080
  const sheets = [];
1081
+ const sameStyle = styles ? styleComparer() : null;
986
1082
  for (const after of second.sheets) {
987
1083
  const before = firstNames.get(after.name);
988
1084
  if (!before) continue;
989
1085
  const fromFirst = [];
990
1086
  const fromSecond = [];
991
1087
  let count = 0;
992
- const record = (list, row, col, change) => {
1088
+ const record = (list, row, col, kind, change) => {
993
1089
  count++;
994
- if (change.kind === "added") summary.cellsAdded++;
995
- else if (change.kind === "removed") summary.cellsRemoved++;
1090
+ if (kind === "added") summary.cellsAdded++;
1091
+ else if (kind === "removed") summary.cellsRemoved++;
996
1092
  else summary.cellsChanged++;
997
1093
  if (list.length < maxCells) list.push({
998
1094
  row,
999
1095
  col,
1000
- change
1096
+ change: change()
1001
1097
  });
1002
1098
  };
1003
1099
  for (const cell of before.cells()) {
1004
1100
  const other = after.cell(cell.ref);
1005
- if (other === void 0) record(fromFirst, cell.row, cell.col, {
1101
+ if (other === void 0) record(fromFirst, cell.row, cell.col, "removed", () => ({
1006
1102
  ref: cell.ref,
1007
1103
  kind: "removed",
1008
1104
  before: toDiffCell(cell, styles),
1009
1105
  after: null
1010
- });
1011
- else if (!sameCell(cell, other, styles)) record(fromFirst, cell.row, cell.col, {
1106
+ }));
1107
+ else if (!sameCell(cell, other, sameStyle)) record(fromFirst, cell.row, cell.col, "changed", () => ({
1012
1108
  ref: cell.ref,
1013
1109
  kind: "changed",
1014
1110
  before: toDiffCell(cell, styles),
1015
1111
  after: toDiffCell(other, styles)
1016
- });
1112
+ }));
1017
1113
  }
1018
- for (const cell of after.cells()) if (before.cell(cell.ref) === void 0) record(fromSecond, cell.row, cell.col, {
1114
+ for (const cell of after.cells()) if (before.cell(cell.ref) === void 0) record(fromSecond, cell.row, cell.col, "added", () => ({
1019
1115
  ref: cell.ref,
1020
1116
  kind: "added",
1021
1117
  before: null,
1022
1118
  after: toDiffCell(cell, styles)
1023
- });
1119
+ }));
1024
1120
  const stateChanged = before.state !== after.state;
1025
1121
  if (count === 0 && !stateChanged) continue;
1026
1122
  summary.sheetsChanged++;
@@ -1044,6 +1140,76 @@ function diff(a, b, options) {
1044
1140
  };
1045
1141
  }
1046
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
+ }
1047
1213
  /** `"x:"` for a qualified name `x:local`, `""` without a prefix. */
1048
1214
  function prefixOf(qname) {
1049
1215
  const colon = qname.indexOf(":");
@@ -1188,11 +1354,22 @@ var PackageEditor = class {
1188
1354
  if (cached !== void 0) return cached;
1189
1355
  const entry = this.entryOf(partName);
1190
1356
  if (entry === void 0) return void 0;
1191
- return normalizeLineEnds(decodeXml(this.bytesOf(entry), partName));
1357
+ return normalizeLineEnds(decodeXml(this.changed.get(entry.name) ?? this.bytesOf(entry), partName));
1192
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
+ */
1193
1364
  write(partName, text) {
1194
- this.texts.set(partKey(partName), text);
1195
- 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
+ }
1196
1373
  const entry = this.entryOf(partName);
1197
1374
  if (entry) {
1198
1375
  this.changed.set(entry.name, data);
@@ -1206,6 +1383,28 @@ var PackageEditor = class {
1206
1383
  data
1207
1384
  });
1208
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
+ }
1209
1408
  remove(partName) {
1210
1409
  const entry = this.entryOf(partName);
1211
1410
  if (entry) this.removed.add(entry.name);
@@ -1673,10 +1872,178 @@ function newCellXml(ref, value, xf, sst) {
1673
1872
  const r = valueParts(value, sst, false);
1674
1873
  return `<c r="${ref}"${s}${r.t === "" ? "" : attr("t", r.t)}><v>${r.v}</v></c>`;
1675
1874
  }
1676
- /** Inserts `r="…"` after the qualified name of a start tag given as source text. */
1677
- function insertRef(tag, qname, ref) {
1678
- const at = 1 + qname.length;
1679
- 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
+ };
1680
2047
  }
1681
2048
  /**
1682
2049
  * The cell format a new cell of a sheet starts from: the `style` of the `col` element that covers
@@ -1685,37 +2052,36 @@ function insertRef(tag, qname, ref) {
1685
2052
  * which of the two wins is not stated in the sources read (D-A04). Indexes beyond the cell
1686
2053
  * formats of the file are ignored.
1687
2054
  */
1688
- function newCellStyles(text, part, limits, styles) {
1689
- const { elements } = scan(text, part, limits, (local, depth, parent) => depth === 3 && local === "col" && parent === "cols" || depth === 3 && local === "row" && parent === "sheetData");
1690
- const cols = [];
1691
- const rowsWithFormat = /* @__PURE__ */ new Set();
1692
- let rowNumber = 0;
1693
- for (const e of elements) if (e.local === "col") {
1694
- const [min, max, style] = [
1695
- "min",
1696
- "max",
1697
- "style"
1698
- ].map((k) => {
1699
- const v = e.attrs.get(k)?.trim() ?? "";
1700
- return /^[0-9]{1,10}$/.test(v) ? Number(v) : NaN;
1701
- });
1702
- if (Number.isInteger(min) && Number.isInteger(max) && Number.isInteger(style) && style !== 0 && styles.hasCellFormat(style ?? 0)) cols.push({
1703
- min: min ?? 0,
1704
- max: max ?? 0,
1705
- style: style ?? 0
1706
- });
1707
- } else {
1708
- const r = e.attrs.get("r")?.trim();
1709
- rowNumber = r !== void 0 && /^[0-9]{1,7}$/.test(r) ? Number(r) : rowNumber + 1;
1710
- const custom = e.attrs.get("customFormat")?.trim();
1711
- 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
+ }
2068
+ }
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;
1712
2074
  }
1713
- return (row, col) => rowsWithFormat.has(row) ? 0 : cols.find((c) => col >= c.min && col <= c.max)?.style ?? 0;
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)}`;
1714
2080
  }
1715
- function editSheetXml(text, part, sheet, planned, limits) {
1716
- 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");
1717
- const prefix = prefixOf(root.start.qname);
1718
- 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;
1719
2085
  if (!sheetData) throw new XlvError("XLV_E_PART_INVALID", `The worksheet part of sheet "${sheet}" has no sheetData element.`, {
1720
2086
  location: {
1721
2087
  part,
@@ -1723,45 +2089,7 @@ function editSheetXml(text, part, sheet, planned, limits) {
1723
2089
  },
1724
2090
  hint: "The sheet part is damaged. Re-save the file in a spreadsheet application."
1725
2091
  });
1726
- const rows = [];
1727
- let currentRow = 0;
1728
- let current = null;
1729
- let lastCol = 0;
1730
- const invalid = (ref) => {
1731
- throw new XlvError("XLV_E_INVALID_CELL_REF", `Sheet "${sheet}" contains the invalid reference "${ref.slice(0, 20)}", so it cannot be edited safely.`, {
1732
- location: {
1733
- part,
1734
- sheet
1735
- },
1736
- hint: "Open and re-save the file in a spreadsheet application, which drops the damaged cells, then edit the re-saved file.",
1737
- details: { ref: ref.slice(0, 40) }
1738
- });
1739
- };
1740
- for (const e of elements) if (e.depth === 3) {
1741
- const r = e.attrs.get("r");
1742
- const n = r === void 0 ? currentRow + 1 : /^[0-9]{1,7}$/.test(r.trim()) ? Number(r.trim()) : NaN;
1743
- if (!Number.isInteger(n) || n < 1 || n > 1048576) invalid(r ?? String(n));
1744
- currentRow = n;
1745
- current = {
1746
- number: currentRow,
1747
- hasR: r !== void 0,
1748
- element: e,
1749
- cells: []
1750
- };
1751
- rows.push(current);
1752
- lastCol = 0;
1753
- } else if (e.depth === 4 && current) {
1754
- const r = e.attrs.get("r");
1755
- const address = r === void 0 || r === "" ? null : parseCellRef(r);
1756
- if (r !== void 0 && r !== "" && address === null) invalid(r);
1757
- const col = address?.col ?? lastCol + 1;
1758
- lastCol = col;
1759
- current.cells.push({
1760
- col,
1761
- hasR: r !== void 0,
1762
- element: e
1763
- });
1764
- }
2092
+ const { rows, cells, rowCount } = scan;
1765
2093
  const byRow = /* @__PURE__ */ new Map();
1766
2094
  for (const p of planned) {
1767
2095
  const list = byRow.get(p.row) ?? [];
@@ -1769,55 +2097,80 @@ function editSheetXml(text, part, sheet, planned, limits) {
1769
2097
  list.push(p);
1770
2098
  }
1771
2099
  for (const list of byRow.values()) list.sort((a, b) => a.col - b.col);
1772
- const forceRefs = rows.some((r) => !r.hasR);
1773
2100
  const emitted = /* @__PURE__ */ new Set();
1774
- const pending = [...byRow.keys()].filter((n) => !rows.some((r) => r.number === n)).sort((a, b) => a - b);
2101
+ const existingRows = /* @__PURE__ */ new Set();
2102
+ for (let i = 0; i < rowCount; i++) existingRows.add(rows.get(i * ROW_FIELDS$1));
2103
+ const pending = [...byRow.keys()].filter((n) => !existingRows.has(n)).sort((a, b) => a - b);
2104
+ let next = 0;
1775
2105
  const p = (xml) => withPrefix(xml, prefix);
2106
+ const out = new TextBuilder();
1776
2107
  const newRow = (n) => {
1777
- const cells = (byRow.get(n) ?? []).filter((c) => c.xml !== null).map((c) => p(c.xml ?? "")).join("");
1778
- 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>`);
1779
2111
  };
1780
- let inner = "";
1781
- for (const row of rows) {
1782
- while (pending.length > 0 && (pending[0] ?? 0) < row.number) inner += newRow(pending.shift() ?? 0);
1783
- const edits = byRow.get(row.number);
1784
- const needsRefs = forceRefs || row.cells.some((c) => !c.hasR);
1785
- if (!edits && row.hasR && !needsRefs) {
1786
- 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));
1787
2129
  continue;
1788
2130
  }
1789
- let tag = text.slice(row.element.start.start, row.element.start.end);
1790
- if (!row.hasR) tag = insertRef(tag, row.element.start.qname, String(row.number));
1791
- 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);
1792
2135
  const edited = new Set((edits ?? []).map((c) => c.col));
1793
- const toInsert = emitted.has(row.number) ? [] : (edits ?? []).filter((c) => c.xml !== null);
1794
- emitted.add(row.number);
2136
+ const toInsert = emitted.has(number) ? [] : (edits ?? []).filter((c) => c.xml !== null);
2137
+ emitted.add(number);
1795
2138
  let k = 0;
1796
- for (const cell of row.cells) {
1797
- while (k < toInsert.length && (toInsert[k]?.col ?? 0) < cell.col) body += p(toInsert[k++]?.xml ?? "");
1798
- if (edited.has(cell.col)) continue;
1799
- let cellText = text.slice(cell.element.start.start, cell.element.end.end);
1800
- if (!cell.hasR) cellText = insertRef(cellText, cell.element.start.qname, formatCellRef(row.number, cell.col));
1801
- 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
+ }
1802
2150
  }
1803
- while (k < toInsert.length) body += p(toInsert[k++]?.xml ?? "");
1804
- 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));
1805
2153
  }
1806
- while (pending.length > 0) inner += newRow(pending.shift() ?? 0);
2154
+ while (next < pending.length) newRow(pending[next++] ?? 0);
2155
+ const inner = out.finish();
1807
2156
  const changes = [];
1808
2157
  if (sheetData.start.empty) changes.push([
1809
2158
  sheetData.start.start,
1810
2159
  sheetData.start.end,
1811
- `${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
+ ]
1812
2165
  ]);
1813
2166
  else changes.push([
1814
2167
  sheetData.start.end,
1815
2168
  sheetData.end.start,
1816
2169
  inner
1817
2170
  ]);
1818
- const dimension = elements.find((e) => e.depth === 2 && e.local === "dimension");
2171
+ const dimension = scan.dimension;
1819
2172
  if (dimension) {
1820
- const old = parseRange(dimension.attrs.get("ref") ?? "");
2173
+ const old = parseRange(dimension.ref ?? "");
1821
2174
  const values = planned.filter((c) => c.xml !== null);
1822
2175
  if (values.length > 0) {
1823
2176
  let minRow = old?.start.row ?? Infinity;
@@ -1834,11 +2187,19 @@ function editSheetXml(text, part, sheet, planned, limits) {
1834
2187
  changes.push([
1835
2188
  dimension.start.start,
1836
2189
  dimension.start.end,
1837
- setAttr(text.slice(dimension.start.start, dimension.start.end), "ref", ref)
2190
+ [setAttr(text.slice(dimension.start.start, dimension.start.end), "ref", ref)]
1838
2191
  ]);
1839
2192
  }
1840
2193
  }
1841
- 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;
1842
2203
  }
1843
2204
  const CALC_PR_LATER = [
1844
2205
  "oleSize",
@@ -1874,6 +2235,30 @@ function requestFullCalculation(edit, workbookPart) {
1874
2235
  created
1875
2236
  ]]));
1876
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
+ };
1877
2262
  /**
1878
2263
  * editCells writes plain formulas only, and does not change cells whose formula covers other
1879
2264
  * cells: an array formula's range (P1 §18.17.6.3) or a shared formula master with dependents
@@ -1893,17 +2278,14 @@ function checkFormulaGroups(sheet, ref, at, value, cache) {
1893
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.");
1894
2279
  let arrays = cache.get(sheet.name);
1895
2280
  if (arrays === void 0) {
1896
- arrays = [];
2281
+ arrays = new RangeLookup();
1897
2282
  for (const c of sheet.cells()) {
1898
2283
  const range = c.formula?.kind === "array" && c.formula.ref !== null ? parseRange(c.formula.ref) : null;
1899
- if (range !== null) arrays.push({
1900
- ref: c.formula.ref,
1901
- range
1902
- });
2284
+ if (range !== null) arrays.add(range, c.formula.ref);
1903
2285
  }
1904
2286
  cache.set(sheet.name, arrays);
1905
2287
  }
1906
- 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);
1907
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.`);
1908
2290
  const f = sheet.cell(ref)?.formula;
1909
2291
  const range = f?.kind === "shared" && f.ref !== null ? parseRange(f.ref) : null;
@@ -2064,6 +2446,7 @@ function planEdits(workbook, edits) {
2064
2446
  const scratchSheet = scratch.addSheet("Edit");
2065
2447
  const plans = /* @__PURE__ */ new Map();
2066
2448
  const arrayRanges = /* @__PURE__ */ new Map();
2449
+ const byName = new Map(workbook.sheets.map((sheet) => [sheet.name, sheet]));
2067
2450
  edits.forEach((raw, i) => {
2068
2451
  const o = readOptions(raw, [
2069
2452
  "sheet",
@@ -2076,7 +2459,7 @@ function planEdits(workbook, edits) {
2076
2459
  hint: "Give each edit a sheet, for example { sheet: \"Data\", cell: \"B2\", value: 5 }.",
2077
2460
  details: { option: "sheet" }
2078
2461
  });
2079
- const sheet = workbook.getSheet(sheetName);
2462
+ const sheet = byName.get(sheetName) ?? workbook.getSheet(sheetName);
2080
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.`, {
2081
2464
  location: { sheet: sheet.name },
2082
2465
  hint: "Edit cells of worksheets only."
@@ -2154,17 +2537,31 @@ function applyEdits$1(bytes, workbook, plans, limits, appended = null) {
2154
2537
  const part = plan.sheet.part ?? "";
2155
2538
  const planned = [];
2156
2539
  const text = edit.text(part);
2157
- 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);
2158
2541
  let styled = null;
2159
- const styledCell = (ref) => (styled ??= new Map([...plan.sheet.styledCells()].map((cell) => [cell.ref, cell]))).get(ref);
2160
- const merges = plan.sheet.merges.map((m) => ({
2161
- ref: m,
2162
- range: parseRange(m.trim().toUpperCase())
2163
- }));
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;
2164
2554
  for (const [ref, c] of plan.cells) {
2165
- if (c.value !== null) {
2166
- 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));
2167
- 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) {
2168
2565
  const topLeft = formatCellRef(merge.range.start.row, merge.range.start.col);
2169
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.`, {
2170
2567
  part,
@@ -2184,7 +2581,7 @@ function applyEdits$1(bytes, workbook, plans, limits, appended = null) {
2184
2581
  });
2185
2582
  continue;
2186
2583
  }
2187
- const base = existing?.styleIndex ?? columnStyle(c.row, c.col);
2584
+ const base = existing?.styleIndex ?? (sheetScan === null ? 0 : newCellStyle(sheetScan, c.row, c.col));
2188
2585
  let patch = c.style ?? {};
2189
2586
  const existingIsDate = existing?.numFmt != null && isDateFormat(existing.numFmt);
2190
2587
  if (c.date !== null && patch.numFmt === void 0 && !existingIsDate) patch = {
@@ -2199,13 +2596,21 @@ function applyEdits$1(bytes, workbook, plans, limits, appended = null) {
2199
2596
  xml: newCellXml(ref, c.value, xf, sst)
2200
2597
  });
2201
2598
  }
2202
- if (text === void 0) continue;
2599
+ if (text === void 0 || sheetScan === null) continue;
2203
2600
  syncTables(edit, plan, appended !== null && appended.sheet === plan.sheet.name ? appended : null);
2204
- edit.write(part, editSheetXml(text, part, plan.sheet.name, planned, limits));
2601
+ edit.write(part, editSheetXml(text, part, plan.sheet.name, sheetScan, planned));
2205
2602
  }
2206
2603
  sst.finish();
2207
2604
  styles.finish();
2208
- 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) {
2209
2614
  requestFullCalculation(edit, workbookPart);
2210
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 });
2211
2616
  }
@@ -2248,19 +2653,25 @@ function applyEdits$1(bytes, workbook, plans, limits, appended = null) {
2248
2653
  function editCells(input, edits, options) {
2249
2654
  const bytes = inputBytes(input);
2250
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 }]." });
2251
- const o = readOptions(options, ["limits"], "editCells options");
2252
- 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);
2253
2659
  const workbook = readWorkbook(bytes, { limits });
2254
- 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);
2255
2662
  }
2256
2663
  /**
2257
2664
  * Appends rows below the last used row of a sheet (the rows of merged ranges count as used) in
2258
2665
  * an existing XLSX file, editing the file surgically like {@link editCells}. Object rows are
2259
2666
  * placed under the matching header columns (unknown keys throw `XLV_E_INVALID_ROWS` with
2260
- * 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
2261
- * 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
2262
2672
  * `XLV_E_NOT_SUPPORTED` and a hint to insert the rows above the totals row with `insertRows`.
2263
- * 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`).
2264
2675
  *
2265
2676
  * @example
2266
2677
  * ```ts
@@ -2271,15 +2682,26 @@ function editCells(input, edits, options) {
2271
2682
  * { Customer: 'Alice', Total: 120.5 },
2272
2683
  * { Customer: 'Bob', Total: 80 },
2273
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);
2274
2688
  * ```
2275
2689
  */
2276
2690
  function appendRows(input, sheet, rows, options) {
2277
2691
  const bytes = inputBytes(input);
2278
2692
  const o = readOptions(options, ["limits", "header"], "appendRows options");
2279
- const limits = resolveLimits(o.get("limits"));
2693
+ const { limits, writable } = editLimits(o.get("limits"));
2280
2694
  const workbook = readWorkbook(bytes, { limits });
2281
2695
  const target = pickSheet(workbook, sheet);
2282
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);
2283
2705
  let lastRow = 0;
2284
2706
  let firstCol = Infinity;
2285
2707
  for (const cell of target.cells()) {
@@ -2299,6 +2721,27 @@ function appendRows(input, sheet, rows, options) {
2299
2721
  value: MAX_ROWS
2300
2722
  }
2301
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
+ }
2302
2745
  let columns = null;
2303
2746
  const edits = [];
2304
2747
  rows.forEach((row, i) => {
@@ -2307,7 +2750,7 @@ function appendRows(input, sheet, rows, options) {
2307
2750
  row.forEach((value, k) => {
2308
2751
  if (value !== null && value !== void 0) edits.push({
2309
2752
  sheet: target.name,
2310
- cell: formatCellRef(r, firstCol + k),
2753
+ cell: formatCellRef(r, arrayCol + k),
2311
2754
  value
2312
2755
  });
2313
2756
  });
@@ -2604,6 +3047,13 @@ function adjustRange(sqref, change) {
2604
3047
  }
2605
3048
  // src/rows.ts
2606
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";
2607
3057
  const SAFE_SHEET_RELATIONSHIPS = /* @__PURE__ */ new Set([
2608
3058
  "hyperlink",
2609
3059
  "comments",
@@ -2645,16 +3095,30 @@ function moveRow(n, c) {
2645
3095
  if (n < c.at) return n;
2646
3096
  return n > end ? n + c.count : null;
2647
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. */
2648
3099
  function decodeEntities(s) {
2649
- 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) => {
2650
3101
  if (e === "amp") return "&";
2651
3102
  if (e === "lt") return "<";
2652
3103
  if (e === "gt") return ">";
2653
3104
  if (e === "quot") return "\"";
2654
3105
  if (e === "apos") return "'";
2655
- 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;
2656
3108
  });
2657
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
+ }
2658
3122
  function removeAttr(tag, name) {
2659
3123
  return tag.replace(new RegExp(`\\s${name}\\s*=\\s*("[^"]*"|'[^']*')`), "");
2660
3124
  }
@@ -2716,7 +3180,6 @@ function applyEdits(text, edits) {
2716
3180
  return out.join("");
2717
3181
  }
2718
3182
  const KEEP_DEPTH3 = /* @__PURE__ */ new Set([
2719
- "row",
2720
3183
  "mergeCell",
2721
3184
  "hyperlink",
2722
3185
  "cfRule",
@@ -2725,9 +3188,159 @@ const KEEP_DEPTH3 = /* @__PURE__ */ new Set([
2725
3188
  "ignoredError",
2726
3189
  "protectedRange"
2727
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
+ }
2728
3340
  /** Rewrites one worksheet part: references everywhere, and row positions on the changed sheet. */
2729
- function rewriteSheet(text, part, sheet, c, target, style, limits) {
2730
- 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;
2731
3344
  const edits = [];
2732
3345
  const tagOf = (e) => text.slice(e.start.start, e.start.end);
2733
3346
  const replaceTag = (e, tag) => {
@@ -2775,59 +3388,21 @@ function rewriteSheet(text, part, sheet, c, target, style, limits) {
2775
3388
  if (ext.depth !== 3 || ext.local !== "ext" || knownExts.has(ext)) continue;
2776
3389
  const body = text.slice(ext.start.start, ext.end.end);
2777
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" });
2778
- 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" });
2779
3392
  }
2780
- const rows = [];
2781
- let current = null;
2782
- let currentRow = 0;
2783
- let lastCol = 0;
2784
- let lastCell = null;
2785
- const formulas = [];
2786
- const invalidRef = (ref) => {
3393
+ if (scanned.invalidRef !== null) {
3394
+ const ref = scanned.invalidRef;
2787
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.`, {
2788
3396
  location: where(),
2789
3397
  hint: "Open and re-save the file in a spreadsheet application, which drops the damaged cells, then try again.",
2790
3398
  details: { ref: ref.slice(0, 40) }
2791
3399
  });
2792
- };
2793
- let hasFormulas = false;
3400
+ }
3401
+ const hasFormulas = formulaCount > 0;
2794
3402
  let extension = null;
2795
3403
  for (const e of elements) {
2796
3404
  if (e.depth === 3 && e.local === "ext") extension = knownExts.has(e) ? e : null;
2797
- if (e.depth === 3 && e.local === "row") {
2798
- const r = e.attrs.get("r");
2799
- const n = r === void 0 ? currentRow + 1 : /^[0-9]{1,7}$/.test(r.trim()) ? Number(r.trim()) : NaN;
2800
- if (!Number.isInteger(n) || n < 1 || n > 1048576) invalidRef(r ?? String(n));
2801
- currentRow = n;
2802
- current = {
2803
- number: n,
2804
- hasR: r !== void 0,
2805
- element: e,
2806
- cells: []
2807
- };
2808
- rows.push(current);
2809
- lastCol = 0;
2810
- } else if (e.depth === 4 && e.local === "c" && current) {
2811
- const r = e.attrs.get("r");
2812
- const address = r === void 0 || r === "" ? null : parseCellRef(r);
2813
- if (r !== void 0 && r !== "" && address === null) invalidRef(r);
2814
- const col = address?.col ?? lastCol + 1;
2815
- lastCol = col;
2816
- lastCell = {
2817
- col,
2818
- hasR: r !== void 0 && r !== "",
2819
- element: e,
2820
- f: null
2821
- };
2822
- current.cells.push(lastCell);
2823
- } else if (e.depth === 5 && e.local === "f" && lastCell && current) {
2824
- lastCell.f = e;
2825
- hasFormulas = true;
2826
- formulas.push({
2827
- cell: lastCell,
2828
- row: current
2829
- });
2830
- } 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")) {
2831
3406
  const raw = content(text, e);
2832
3407
  const out = adjustText(raw, c, sheet.name);
2833
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) });
@@ -2870,12 +3445,14 @@ function rewriteSheet(text, part, sheet, c, target, style, limits) {
2870
3445
  }
2871
3446
  }
2872
3447
  const masters = /* @__PURE__ */ new Map();
2873
- for (const { cell, row } of formulas) {
2874
- const f = cell.f;
2875
- if (f.attrs.get("t") === "shared" && f.attrs.has("ref") && f.attrs.has("si")) masters.set(f.attrs.get("si") ?? "", {
2876
- row: row.number,
2877
- col: cell.col,
2878
- 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))
2879
3456
  });
2880
3457
  }
2881
3458
  const expand = /* @__PURE__ */ new Set();
@@ -2884,65 +3461,77 @@ function rewriteSheet(text, part, sheet, c, target, style, limits) {
2884
3461
  if (target || out === null || out !== m.text) expand.add(si);
2885
3462
  }
2886
3463
  const deletedRow = (n) => c.count < 0 && moveRow(n, c) === null;
2887
- for (const { cell, row } of formulas) {
2888
- const f = cell.f;
2889
- if (target && deletedRow(row.number)) continue;
2890
- const ref = formatCellRef(row.number, cell.col);
2891
- 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);
2892
3476
  const failed = (formula) => {
2893
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) });
2894
3478
  };
2895
- if (kind === "shared") {
2896
- const si = f.attrs.get("si") ?? "";
3479
+ const tag = text.slice(tagStart, tagEnd);
3480
+ if (kind === SHARED) {
3481
+ const si = tagAttributes(tag).get("si") ?? "";
2897
3482
  const master = masters.get(si);
2898
- if (master === void 0 || !expand.has(si)) continue;
3483
+ if (master === void 0 || !expand.has(si)) return [];
2899
3484
  const source = decodeEntities(master.text);
2900
- const own = shiftFormula(source, row.number - master.row, cell.col - master.col);
3485
+ const own = shiftFormula(source, row - master.row, col - master.col);
2901
3486
  if (own === null) failed(source);
2902
3487
  const adjusted = adjustFormula(own, refChange(c, sheet.name));
2903
3488
  if (adjusted === null) failed(own);
2904
- let tag = removeAttr(removeAttr(removeAttr(tagOf(f), "t"), "ref"), "si");
2905
- if (tag.endsWith("/>")) tag = `${tag.slice(0, -2).trimEnd()}>`;
2906
- edits.push([
2907
- f.start.start,
2908
- f.end.end,
2909
- `${tag}${escapeText(adjusted)}</${f.start.qname}>`
2910
- ]);
2911
- 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
+ ]];
2912
3497
  }
2913
- if (kind === "dataTable") {
3498
+ if (kind === DATA_TABLE) {
2914
3499
  if (target) throw notSupported(`Cell ${ref} of sheet "${sheet.name}" is part of a data table, which xlvane cannot move.`, where(ref));
2915
- continue;
3500
+ return [];
2916
3501
  }
2917
- const raw = content(text, f);
3502
+ const raw = text.slice(tagEnd, endStart);
2918
3503
  const out = adjustText(raw, c, sheet.name);
2919
3504
  if (out === null) failed(decodeEntities(raw));
2920
- let tag = tagOf(f);
2921
- if (kind === "array" && target) {
2922
- 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);
2923
3509
  if (range !== null) {
2924
- const end = c.at - c.count - 1;
2925
- 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));
2926
- const moved = adjustRange(f.attrs.get("ref") ?? "", refChange(c, sheet.name));
2927
- 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
+ ]);
2928
3518
  }
2929
3519
  }
2930
- if (tag !== tagOf(f)) replaceTag(f, tag);
2931
- if (out !== raw) edits.push([
2932
- f.start.end,
2933
- f.end.start,
3520
+ if (out !== raw) changes.push([
3521
+ tagEnd,
3522
+ endStart,
2934
3523
  out
2935
3524
  ]);
2936
- }
2937
- if (!target) return {
2938
- text: edits.length === 0 ? text : applyEdits(text, edits),
2939
- hasFormulas
3525
+ return changes;
2940
3526
  };
2941
- const force = rows.some((r) => !r.hasR || r.cells.some((x) => !x.hasR));
2942
- for (const row of rows) {
2943
- const n = moveRow(row.number, c);
2944
- if (n === null) {
2945
- 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.`, {
2946
3535
  location: where(),
2947
3536
  hint: `A sheet has ${String(MAX_ROWS)} rows. Delete the rows at the end of the sheet first, or insert fewer rows.`,
2948
3537
  details: {
@@ -2950,156 +3539,252 @@ function rewriteSheet(text, part, sheet, c, target, style, limits) {
2950
3539
  value: MAX_ROWS
2951
3540
  }
2952
3541
  });
2953
- removeElement(row.element);
2954
- continue;
2955
3542
  }
2956
- if (n === row.number && !force) continue;
2957
- replaceTag(row.element, setAttr(tagOf(row.element), "r", String(n)));
2958
- for (const cell of row.cells) replaceTag(cell.element, setAttr(tagOf(cell.element), "r", formatCellRef(n, cell.col)));
2959
- }
2960
- if (c.count > 0 && style !== "none") {
2961
- const source = rows.find((r) => r.number === (style === "above" ? c.at - 1 : c.at));
2962
- if (source) {
2963
- const prefix = source.element.start.qname.slice(0, source.element.start.qname.length - 3);
2964
- const keep = [
2965
- "s",
2966
- "customFormat",
2967
- "ht",
2968
- "customHeight"
2969
- ].filter((a) => source.element.attrs.has(a));
2970
- let created = "";
2971
- for (let i = 0; i < c.count; i++) {
2972
- const n = c.at + i;
2973
- const attrs = keep.map((a) => ` ${a}="${escapeAttr(source.element.attrs.get(a) ?? "")}"`).join("");
2974
- 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("");
2975
- created += cells === "" ? `<${prefix}row r="${String(n)}"${attrs}/>` : `<${prefix}row r="${String(n)}"${attrs}>${cells}</${prefix}row>`;
2976
- }
2977
- const next = rows.find((r) => r.number >= c.at);
2978
- if (next) edits.push([
2979
- next.element.start.start,
2980
- next.element.start.start,
2981
- created
2982
- ]);
2983
- else {
2984
- const sheetData = elements.find((e) => e.depth === 2 && e.local === "sheetData");
2985
- if (sheetData && !sheetData.start.empty) edits.push([
2986
- sheetData.end.start,
2987
- sheetData.end.start,
2988
- created
2989
- ]);
2990
- else if (sheetData) edits.push([
2991
- sheetData.start.start,
2992
- sheetData.start.end,
2993
- `${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
2994
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
+ }
2995
3611
  }
2996
3612
  }
2997
- }
2998
- const removedIds = /* @__PURE__ */ new Set();
2999
- const linkIds = /* @__PURE__ */ new Map();
3000
- let format = null;
3001
- for (const e of elements) if (e.depth === 2) format = e.local === "conditionalFormatting" ? e : null;
3002
- else if (format !== null && e.depth === 6 && e.ns === "http://schemas.microsoft.com/office/spreadsheetml/2009/9/main" && e.local === "id") {
3003
- const list = linkIds.get(format) ?? [];
3004
- list.push(content(text, e).trim().toLowerCase());
3005
- linkIds.set(format, list);
3006
- }
3007
- const containers = /* @__PURE__ */ new Map();
3008
- let parent2 = null;
3009
- for (const e of elements) {
3010
- if (e.depth === 2) {
3011
- parent2 = e;
3012
- if (e.local === "dimension" || e.local === "autoFilter") {
3013
- const ref = e.attrs.get("ref");
3014
- if (ref === void 0) continue;
3015
- const out = adjustRange(ref, refChange(c, sheet.name));
3016
- if (out === null || out === ref) continue;
3017
- if (e.local === "dimension") replaceTag(e, setAttr(tagOf(e), "ref", out === "" ? "A1" : out.split(" ")[0] ?? "A1"));
3018
- else if (out === "") removeElement(e);
3019
- else replaceTag(e, setAttr(tagOf(e), "ref", out));
3020
- } 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++;
3021
3671
  const sqref = e.attrs.get("sqref") ?? "";
3022
3672
  const out = adjustRange(sqref, refChange(c, sheet.name));
3023
- 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());
3024
3674
  if (out === "") {
3025
3675
  removeElement(e);
3026
- for (const id of linkIds.get(e) ?? []) removedIds.add(id);
3027
- } else if (out !== sqref) replaceTag(e, setAttr(tagOf(e), "sqref", out));
3028
- }
3029
- continue;
3030
- }
3031
- if (e.depth !== 3 || parent2 === null) continue;
3032
- const container = parent2;
3033
- const count = containers.get(container) ?? {
3034
- kept: 0,
3035
- total: 0
3036
- };
3037
- containers.set(container, count);
3038
- if (e.local === "mergeCell") {
3039
- count.total++;
3040
- const ref = e.attrs.get("ref") ?? "";
3041
- const range = parseRange(ref);
3042
- const out = range === null ? null : c.count < 0 && deletedRow(range.start.row) ? "" : adjustRange(ref, refChange(c, sheet.name));
3043
- if (out === null) {
3676
+ continue;
3677
+ }
3044
3678
  count.kept++;
3045
- continue;
3046
- }
3047
- const moved = out === "" ? null : parseRange(out);
3048
- if (moved === null || moved.start.row === moved.end.row && moved.start.col === moved.end.col) {
3049
- removeElement(e);
3050
- continue;
3051
- }
3052
- count.kept++;
3053
- if (out !== ref) replaceTag(e, setAttr(tagOf(e), "ref", out));
3054
- } else if (e.local === "dataValidation" || e.local === "ignoredError" || e.local === "protectedRange") {
3055
- count.total++;
3056
- const sqref = e.attrs.get("sqref") ?? "";
3057
- const out = adjustRange(sqref, refChange(c, sheet.name));
3058
- if (out === null) throw notSupported(`Sheet "${sheet.name}" has a ${e.local} range xlvane cannot adjust: "${sqref.slice(0, 60)}".`, where());
3059
- if (out === "") {
3060
- removeElement(e);
3061
- continue;
3062
- }
3063
- count.kept++;
3064
- if (out !== sqref) replaceTag(e, setAttr(tagOf(e), "sqref", out));
3065
- } else if (e.local === "hyperlink") {
3066
- count.total++;
3067
- if (adjustRange(e.attrs.get("ref") ?? "", refChange(c, sheet.name)) !== "") count.kept++;
3068
- } else if (e.local === "brk" && container.local === "rowBreaks") {
3069
- count.total++;
3070
- const id = Number(e.attrs.get("id") ?? "NaN");
3071
- 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
+ }
3072
3695
  count.kept++;
3073
- continue;
3696
+ if (n !== id) replaceTag(e, setAttr(tagOf(e), "id", String(n)));
3074
3697
  }
3075
- const n = moveRow(id, c);
3076
- if (n === null) {
3077
- removeElement(e);
3078
- 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);
3079
3712
  }
3080
- count.kept++;
3081
- if (n !== id) replaceTag(e, setAttr(tagOf(e), "id", String(n)));
3082
- }
3083
- }
3084
- for (const [container, n] of containers) {
3085
- if (n.total === 0 || n.kept === n.total) continue;
3086
- const counted = [
3087
- "mergeCells",
3088
- "dataValidations",
3089
- "rowBreaks"
3090
- ].includes(container.local);
3091
- if (n.kept === 0) removeElement(container);
3092
- else if (counted) {
3093
- let tag = tagOf(container);
3094
- if (container.attrs.has("count")) tag = setAttr(tag, "count", String(n.kept));
3095
- if (container.local === "rowBreaks" && container.attrs.has("manualBreakCount")) tag = setAttr(tag, "manualBreakCount", String(n.kept));
3096
- if (tag !== tagOf(container)) replaceTag(container, tag);
3097
- }
3098
- }
3099
- rewriteExtensionRanges(text, elements, knownExts, c, sheet, removedIds, edits, where);
3100
- 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
+ };
3101
3786
  return {
3102
- text: /sortState/.test(result) ? rewriteSortStates(result, part, sheet, c, limits) : result,
3787
+ text: pieces,
3103
3788
  hasFormulas
3104
3789
  };
3105
3790
  }
@@ -3596,22 +4281,55 @@ function changeRows(input, sheetName, at, count, style, limits) {
3596
4281
  sheet: sheet.name
3597
4282
  }, { type: rel.type });
3598
4283
  }
3599
- 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
+ }
3600
4316
  for (const name of pkg.partNames) {
3601
- if (!/\/charts\/[^/]+\.xml$|pivotCacheDefinition[^/]*\.xml$/i.test(name)) continue;
4317
+ if (!charts.has(partKey(name)) && !/\/charts\/[^/]+\.xml$|pivotCacheDefinition[^/]*\.xml$/i.test(name)) continue;
3602
4318
  const text = edit.text(name);
3603
- 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.`, {
3604
4320
  part: name,
3605
4321
  sheet: sheet.name
3606
4322
  });
3607
4323
  }
3608
4324
  let hasFormulas = false;
4325
+ let workbookCells = 0;
4326
+ for (const s of workbook.sheets) workbookCells += s.cellCount + styledCellCount(s);
3609
4327
  for (const s of workbook.sheets) {
3610
4328
  if (s.kind !== "worksheet" || s.part === null) continue;
3611
4329
  const text = edit.text(s.part);
3612
4330
  if (text === void 0) continue;
3613
4331
  const target = s.part === sheet.part;
3614
- const out = rewriteSheet(text, s.part, s, c, target, style, limits);
4332
+ const out = rewriteSheet(text, s.part, s, c, target, style, limits, workbookCells);
3615
4333
  hasFormulas ||= out.hasFormulas;
3616
4334
  if (out.text !== text) edit.write(s.part, out.text);
3617
4335
  rewriteTables(edit, pkg, s.part, s, c, target);
@@ -3651,7 +4369,9 @@ function changeRows(input, sheetName, at, count, style, limits) {
3651
4369
  * custom sheet views, other extended content that refers to cells (sparklines, slicers) and
3652
4370
  * other related parts xlvane does not know; charts or pivot tables anywhere in the workbook that
3653
4371
  * may refer to the sheet; data tables; an array formula the change would split; and formulas it
3654
- * 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.
3655
4375
  *
3656
4376
  * @example
3657
4377
  * ```ts