xlvane 1.0.1 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- import { $t as MAX_COLUMNS, At as mergeStyles, B as tokenize, Dt as parseStyles, Et as EMPTY_STYLES, Ft as resolveLimits, I as WarningCollector, It as suggest, M as openZip, Mt as stableKey, Nt as isDateFormat, Ot as READING_ORDERS, Pt as DEFAULT_READ_LIMITS, Qt as relationshipTypes, Sn as XlvError, U as attr, V as DEFAULT_FONT, W as borderXml, X as fontXml, Xt as SML_NS, Y as fillXml, Z as formatNumber, Zt as classifyRelationship, _ as isoDate, _n as encodeXstring, _t as parseSharedStrings, an as parseRange, b as readWorkbook, bt as partKey, ct as stringItemXml, dn as childrenNamed, en as MAX_ROWS, f as createWorkbook, fn as parseXmlTree, gn as parseXml, gt as parseIsoDateTime, h as detectHeader, hn as normalizeLineEnds, i as readOptions, in as parseCellRef, jt as normalizeStyle, kt as checkStyleCount, m as writeWorkbook, mn as decodeXml, mt as writeZip, n as oneOfOption, nn as formatCellRef, r as openInput, rn as formatRange, t as booleanOption, tn as columnLetters, tt as reusableBuiltinNumFmt, un as childNamed, ut as textElement, vn as escapeAttr, vt as openPackage, x as toBytes, xt as zipNameToPartName, yn as escapeText, yt as relationshipsPartName, z as shiftFormula } from "./options-B7UhudO_.js";
1
+ import { $ as fillXml, At as EMPTY_STYLES, B as tokenize, Bt as resolveLimits, Cn as encodeXstring, Ct as relationshipsPartName, Ft as normalizeStyle, G as DEFAULT_FONT, H as X14_DV_URI, I as WarningCollector, It as stableKey, J as borderXml, Lt as isDateFormat, M as openZip, Mt as READING_ORDERS, Nt as checkStyleCount, On as XlvError, Pt as mergeStyles, Rt as DEFAULT_READ_LIMITS, Sn as parseXml, St as openPackage, Tn as escapeText, Tt as zipNameToPartName, V as X14_CF_URI, Vt as suggest, _ as isoDate, _n as childrenNamed, an as MAX_COLUMNS, at as reusableBuiltinNumFmt, b as readWorkbook, bn as decodeXml, bt as parseIsoDateTime, cn as formatCellRef, dn as parseRange, et as fontXml, f as createWorkbook, ft as stringItemXml, gn as childNamed, h as detectHeader, i as readOptions, in as relationshipTypes, jt as parseStyles, ln as formatRange, m as writeWorkbook, mt as textElement, n as oneOfOption, nn as SML_NS, on as MAX_ROWS, q as attr, r as openInput, rn as classifyRelationship, sn as columnLetters, t as booleanOption, tn as REL_NS, tt as formatNumber, un as parseCellRef, vn as parseXmlTree, vt as writeZip, wn as escapeAttr, wt as partKey, x as toBytes, xn as normalizeLineEnds, xt as parseSharedStrings, z as shiftFormula, zt as checkOutputPositions } from "./options-Ba6r4zP1.js";
2
2
  // src/table.ts
3
3
  const READ_OPTIONS = [
4
4
  "sheet",
@@ -106,6 +106,7 @@ function pickTable(workbook, o) {
106
106
  }
107
107
  function prepare(input, options) {
108
108
  const o = readOptions(options, READ_OPTIONS, "readTable options");
109
+ const limits = resolveLimits(o.get("limits"));
109
110
  const workbook = openInput(input, o.get("limits"), o.get("onWarning"));
110
111
  const table = pickTable(workbook, o);
111
112
  const sheet = table?.sheet ?? pickSheet(workbook, o.get("sheet"));
@@ -173,13 +174,16 @@ function prepare(input, options) {
173
174
  }
174
175
  const columns = [];
175
176
  const used = /* @__PURE__ */ new Set();
177
+ const nextSuffix = /* @__PURE__ */ new Map();
176
178
  const headerCells = headerRow === null ? void 0 : byRow.get(headerRow);
177
179
  for (let col = minCol; col <= maxCol; col++) {
178
180
  const letter = columnLetters(col);
179
181
  const header = headerRow === null ? null : headerText(headerCells?.get(col));
180
182
  const base = header === null || header.trim() === "" ? letter : header.trim();
181
183
  let key = base;
182
- for (let n = 2; used.has(key); n++) key = `${base}_${String(n)}`;
184
+ let n = nextSuffix.get(base) ?? 2;
185
+ while (used.has(key)) key = `${base}_${String(n++)}`;
186
+ nextSuffix.set(base, n);
183
187
  used.add(key);
184
188
  columns.push({
185
189
  key,
@@ -192,6 +196,9 @@ function prepare(input, options) {
192
196
  const rowNumbers = [];
193
197
  const first = headerRow === null ? minRow : headerRow + 1;
194
198
  const numbers = skipEmpty ? [...byRow.keys()].filter((r) => r >= first && r <= maxRow).sort((a, b) => a - b) : rangeOf(first, maxRow);
199
+ let cellCount = 0;
200
+ for (const cells of byRow.values()) cellCount += cells.size;
201
+ checkOutputPositions(numbers.length * columns.length, cellCount, limits.maxCells, "The table", sheet.name);
195
202
  for (const r of numbers) {
196
203
  const cells = byRow.get(r);
197
204
  if (skipEmpty && (cells === void 0 || [...cells.values()].every((c) => c.value === null))) continue;
@@ -549,7 +556,11 @@ function fromJSON(input, options) {
549
556
  return writeTable(rows, options);
550
557
  }
551
558
  // src/csv.ts
552
- /** First characters that make spreadsheet applications treat a CSV field as a formula (CSV-10). */
559
+ /**
560
+ * First characters that make spreadsheet applications treat a CSV field as a formula (CSV-10),
561
+ * plus, as a precaution, the small forms of = + - @ (CSV-12, D-H01). Text starting with any other
562
+ * C0 control character is escaped too (see `injectionStart`).
563
+ */
553
564
  const INJECTION_START = /* @__PURE__ */ new Set([
554
565
  "=",
555
566
  "+",
@@ -561,8 +572,14 @@ const INJECTION_START = /* @__PURE__ */ new Set([
561
572
  "=",
562
573
  "+",
563
574
  "-",
564
- "@"
575
+ "@",
576
+ "﹦",
577
+ "﹢",
578
+ "﹣",
579
+ "﹫"
565
580
  ]);
581
+ /** Whether text starting with `c` is escaped: one of INJECTION_START or a C0 control (NUL included). */
582
+ const injectionStart = (c) => INJECTION_START.has(c) || c.charCodeAt(0) < 32;
566
583
  /**
567
584
  * Converts a sheet to CSV text. Fields that contain the delimiter, a quote, CR or LF are
568
585
  * enclosed in quotes, and quotes are doubled (RFC 4180). Numbers are written in JavaScript's
@@ -598,6 +615,7 @@ function toCSV(input, options) {
598
615
  const newline = oneOfOption("newline", o.get("newline"), ["\r\n", "\n"], "\r\n");
599
616
  const bom = booleanOption("bom", o.get("bom"), false);
600
617
  const injection = oneOfOption("formulaInjection", o.get("formulaInjection"), ["escape", "off"], "escape");
618
+ const limits = resolveLimits(o.get("limits"));
601
619
  const sheet = pickSheet(openInput(input, o.get("limits"), o.get("onWarning")), o.get("sheet"));
602
620
  let startRow = 1;
603
621
  let startCol = 1;
@@ -617,6 +635,16 @@ function toCSV(input, options) {
617
635
  if (cell.row > endRow) endRow = cell.row;
618
636
  if (cell.col > endCol) endCol = cell.col;
619
637
  }
638
+ const byRow = /* @__PURE__ */ new Map();
639
+ let cellCount = 0;
640
+ for (const cell of sheet.cells()) {
641
+ if (cell.row < startRow || cell.row > endRow || cell.col < startCol || cell.col > endCol) continue;
642
+ let r = byRow.get(cell.row);
643
+ if (r === void 0) byRow.set(cell.row, r = /* @__PURE__ */ new Map());
644
+ r.set(cell.col, cell.value);
645
+ cellCount++;
646
+ }
647
+ if (endRow >= startRow && endCol >= startCol) checkOutputPositions((endRow - startRow + 1) * (endCol - startCol + 1), cellCount, limits.maxCells, "The CSV text", sheet.name);
620
648
  const needsQuotes = (s) => s.includes(delimiter) || s.includes("\"") || s.includes("\r") || s.includes("\n");
621
649
  const field = (value) => {
622
650
  if (value === null || value === void 0) return "";
@@ -625,7 +653,7 @@ function toCSV(input, options) {
625
653
  if (value instanceof Date) return isoDate(value);
626
654
  let s = String(value);
627
655
  let quote = needsQuotes(s);
628
- if (injection === "escape" && s.length > 0 && INJECTION_START.has(s[0] ?? "")) {
656
+ if (injection === "escape" && s.length > 0 && injectionStart(s[0] ?? "")) {
629
657
  s = `'${s}`;
630
658
  quote = true;
631
659
  }
@@ -634,10 +662,8 @@ function toCSV(input, options) {
634
662
  const lines = [];
635
663
  for (let r = startRow; r <= endRow; r++) {
636
664
  const fields = [];
637
- for (let c = startCol; c <= endCol; c++) {
638
- const cell = sheet.cell(formatCellRef(r, c));
639
- fields.push(field(cell?.value));
640
- }
665
+ const cells = byRow.get(r);
666
+ for (let c = startCol; c <= endCol; c++) fields.push(field(cells?.get(c)));
641
667
  lines.push(fields.join(delimiter));
642
668
  }
643
669
  const body = lines.length === 0 ? "" : lines.join(newline) + newline;
@@ -1030,7 +1056,7 @@ function withPrefix(xml, prefix) {
1030
1056
  /** Sets (or adds) an attribute in a start tag given as source text. */
1031
1057
  function setAttr(tag, name, value) {
1032
1058
  const pattern = new RegExp(`(\\s${name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*=\\s*)("[^"]*"|'[^']*')`);
1033
- if (pattern.test(tag)) return tag.replace(pattern, `$1"${escapeAttr(value)}"`);
1059
+ if (pattern.test(tag)) return tag.replace(pattern, (_m, head) => `${head}"${escapeAttr(value)}"`);
1034
1060
  const close = tag.endsWith("/>") ? tag.length - 2 : tag.length - 1;
1035
1061
  return `${tag.slice(0, close)} ${name}="${escapeAttr(value)}"${tag.slice(close)}`;
1036
1062
  }
@@ -1078,14 +1104,15 @@ function scan(text, part, limits, keep) {
1078
1104
  const names = [];
1079
1105
  let root = null;
1080
1106
  parseXml(text, {
1081
- start(_ns, local, attrs, span) {
1107
+ start(ns, local, attrs, span) {
1082
1108
  const depth = names.length + 1;
1083
1109
  const parent = names[names.length - 1] ?? "";
1084
1110
  names.push(local);
1085
1111
  let element = null;
1086
- if (depth === 1 || keep(local, depth, parent)) {
1112
+ if (depth === 1 || keep(local, depth, parent, ns)) {
1087
1113
  element = {
1088
1114
  local,
1115
+ ns,
1089
1116
  depth,
1090
1117
  attrs,
1091
1118
  start: span,
@@ -2649,10 +2676,27 @@ function notSupported(message, location, details) {
2649
2676
  function adjustText(raw, c, formulaSheet) {
2650
2677
  if (raw.includes("<")) return null;
2651
2678
  const text = decodeEntities(raw);
2652
- const out = adjustFormula(text, refChange(c, formulaSheet));
2653
- if (out === null) return formulaSheet.toLowerCase() !== c.sheet.toLowerCase() && !text.includes("!") ? raw : null;
2679
+ const out = adjustDecoded(text, c, formulaSheet);
2680
+ if (out === null) return null;
2654
2681
  return out === text ? raw : escapeText(out);
2655
2682
  }
2683
+ /** {@link adjustText} for formula text that is already decoded, such as an attribute value. */
2684
+ function adjustDecoded(text, c, formulaSheet) {
2685
+ const out = adjustFormula(text, refChange(c, formulaSheet));
2686
+ if (out === null) return formulaSheet.toLowerCase() !== c.sheet.toLowerCase() && !text.includes("!") ? text : null;
2687
+ return out;
2688
+ }
2689
+ /** `ext@uri` values of the worksheet extensions whose ranges and formulas are rewritten (SR-40, D-F04). */
2690
+ const KNOWN_EXTENSIONS = new Set([X14_CF_URI, X14_DV_URI].map((u) => u.toLowerCase()));
2691
+ const isKnownExtension = (e) => KNOWN_EXTENSIONS.has((e.attrs.get("uri") ?? "").trim().toLowerCase());
2692
+ /** Start tags named `f` or `sqref`, with any prefix: the formulas and ranges an extension can hold. */
2693
+ const FORMULA_OR_RANGE_TAG = /<(?:[\w.-]+:)?(?:f|sqref)[\s/>]/g;
2694
+ const CFVO_PARENTS = /* @__PURE__ */ new Set([
2695
+ "colorScale",
2696
+ "dataBar",
2697
+ "iconSet"
2698
+ ]);
2699
+ const within = (outer, inner) => inner.start.start >= outer.start.start && inner.end.end <= outer.end.end;
2656
2700
  /**
2657
2701
  * Applies non-overlapping replacements in one pass (linear in the text size, unlike edit.ts
2658
2702
  * `splice`, which matters for sheets with many moved cells). An edit inside an earlier replaced
@@ -2683,7 +2727,7 @@ const KEEP_DEPTH3 = /* @__PURE__ */ new Set([
2683
2727
  ]);
2684
2728
  /** Rewrites one worksheet part: references everywhere, and row positions on the changed sheet. */
2685
2729
  function rewriteSheet(text, part, sheet, c, target, style, limits) {
2686
- const { elements } = scan(text, part, limits, (local, depth, parent) => depth === 2 || depth === 3 && KEEP_DEPTH3.has(local) || depth === 4 && (local === "c" && parent === "row" || local === "formula" && parent === "cfRule" || (local === "formula1" || local === "formula2") && parent === "dataValidation") || depth === 5 && local === "f" && parent === "c");
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"));
2687
2731
  const edits = [];
2688
2732
  const tagOf = (e) => text.slice(e.start.start, e.start.end);
2689
2733
  const replaceTag = (e, tag) => {
@@ -2712,11 +2756,26 @@ function rewriteSheet(text, part, sheet, c, target, style, limits) {
2712
2756
  if (e.depth !== 2) continue;
2713
2757
  const what = REFUSED_ELEMENTS[e.local];
2714
2758
  if (what !== void 0) throw notSupported(`Sheet "${sheet.name}" has ${what}, which xlvane cannot move with its rows yet.`, where(), { element: e.local });
2715
- if (e.local === "extLst" && /sqref|<xm:f|sparkline|slicer/i.test(text.slice(e.start.start, e.end.end))) throw notSupported(`Sheet "${sheet.name}" has extended content (such as data bars, sparklines or slicers) that refers to cells, which xlvane cannot move with its rows yet.`, where(), { element: "extLst" });
2716
2759
  }
2717
- else {
2718
- const ext = elements.find((e) => e.depth === 2 && e.local === "extLst");
2719
- if (ext && /<xm:f|sparkline/i.test(text.slice(ext.start.start, ext.end.end)) && text.slice(ext.start.start, ext.end.end).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" });
2760
+ const knownExts = /* @__PURE__ */ new Set();
2761
+ {
2762
+ let current = null;
2763
+ let found = 0;
2764
+ const close = () => {
2765
+ if (current !== null && (text.slice(current.start.start, current.end.end).match(FORMULA_OR_RANGE_TAG) ?? []).length === found) knownExts.add(current);
2766
+ };
2767
+ for (const e of elements) if (e.depth === 3 && e.local === "ext") {
2768
+ close();
2769
+ current = isKnownExtension(e) ? e : null;
2770
+ found = 0;
2771
+ } else if (current !== null && e.ns === "http://schemas.microsoft.com/office/excel/2006/main" && (e.depth === 6 && e.local === "sqref" || e.depth >= 7 && e.local === "f") && within(current, e)) found++;
2772
+ close();
2773
+ }
2774
+ for (const ext of elements) {
2775
+ if (ext.depth !== 3 || ext.local !== "ext" || knownExts.has(ext)) continue;
2776
+ const body = text.slice(ext.start.start, ext.end.end);
2777
+ 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" });
2720
2779
  }
2721
2780
  const rows = [];
2722
2781
  let current = null;
@@ -2732,64 +2791,83 @@ function rewriteSheet(text, part, sheet, c, target, style, limits) {
2732
2791
  });
2733
2792
  };
2734
2793
  let hasFormulas = false;
2735
- for (const e of elements) if (e.depth === 3 && e.local === "row") {
2736
- const r = e.attrs.get("r");
2737
- const n = r === void 0 ? currentRow + 1 : /^[0-9]{1,7}$/.test(r.trim()) ? Number(r.trim()) : NaN;
2738
- if (!Number.isInteger(n) || n < 1 || n > 1048576) invalidRef(r ?? String(n));
2739
- currentRow = n;
2740
- current = {
2741
- number: n,
2742
- hasR: r !== void 0,
2743
- element: e,
2744
- cells: []
2745
- };
2746
- rows.push(current);
2747
- lastCol = 0;
2748
- } else if (e.depth === 4 && e.local === "c" && current) {
2749
- const r = e.attrs.get("r");
2750
- const address = r === void 0 || r === "" ? null : parseCellRef(r);
2751
- if (r !== void 0 && r !== "" && address === null) invalidRef(r);
2752
- const col = address?.col ?? lastCol + 1;
2753
- lastCol = col;
2754
- lastCell = {
2755
- col,
2756
- hasR: r !== void 0 && r !== "",
2757
- element: e,
2758
- f: null
2759
- };
2760
- current.cells.push(lastCell);
2761
- } else if (e.depth === 5 && e.local === "f" && lastCell && current) {
2762
- lastCell.f = e;
2763
- hasFormulas = true;
2764
- formulas.push({
2765
- cell: lastCell,
2766
- row: current
2767
- });
2768
- } else if (e.depth === 4 && (e.local === "formula" || e.local === "formula1" || e.local === "formula2")) {
2769
- const raw = content(text, e);
2770
- const out = adjustText(raw, c, sheet.name);
2771
- 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) });
2772
- if (out !== raw) edits.push([
2773
- e.start.end,
2774
- e.end.start,
2775
- out
2776
- ]);
2777
- } else if (e.depth === 3 && e.local === "hyperlink") {
2778
- const location = e.attrs.get("location");
2779
- let tag = tagOf(e);
2780
- if (location !== void 0) {
2781
- const out = adjustFormula(location, refChange(c, sheet.name));
2782
- if (out !== null && out !== location) tag = setAttr(tag, "location", out);
2783
- }
2784
- if (target) {
2785
- const ref = adjustRange(e.attrs.get("ref") ?? "", refChange(c, sheet.name));
2786
- if (ref === "") {
2787
- removeElement(e);
2788
- continue;
2794
+ let extension = null;
2795
+ for (const e of elements) {
2796
+ 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")) {
2831
+ const raw = content(text, e);
2832
+ const out = adjustText(raw, c, sheet.name);
2833
+ 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) });
2834
+ if (out !== raw) edits.push([
2835
+ e.start.end,
2836
+ e.end.start,
2837
+ out
2838
+ ]);
2839
+ } else if (e.depth === 5 && e.local === "cfvo") {
2840
+ const val = e.attrs.get("val");
2841
+ if (val === void 0) continue;
2842
+ const out = adjustDecoded(val, c, sheet.name);
2843
+ if (out === null) throw notSupported(`A conditional format threshold of sheet "${sheet.name}" cannot be adjusted safely: "${val.slice(0, 60)}".`, where(), { formula: val.slice(0, 80) });
2844
+ if (out !== val) replaceTag(e, setAttr(tagOf(e), "val", out));
2845
+ } else if (e.ns === "http://schemas.microsoft.com/office/excel/2006/main" && e.local === "f" && extension !== null && within(extension, e)) {
2846
+ const raw = content(text, e);
2847
+ const out = adjustText(raw, c, sheet.name);
2848
+ if (out === null) throw notSupported(`A ${(extension.attrs.get("uri") ?? "").toLowerCase() === "{CCE6A557-97BC-4B89-ADB6-D9C93CAAB3DF}".toLowerCase() ? "data validation" : "conditional format"} formula of sheet "${sheet.name}" cannot be adjusted safely: "${decodeEntities(raw).slice(0, 60)}".`, where(), { formula: decodeEntities(raw).slice(0, 80) });
2849
+ if (out !== raw) edits.push([
2850
+ e.start.end,
2851
+ e.end.start,
2852
+ out
2853
+ ]);
2854
+ } else if (e.depth === 3 && e.local === "hyperlink") {
2855
+ const location = e.attrs.get("location");
2856
+ let tag = tagOf(e);
2857
+ if (location !== void 0) {
2858
+ const out = adjustFormula(location, refChange(c, sheet.name));
2859
+ if (out !== null && out !== location) tag = setAttr(tag, "location", out);
2860
+ }
2861
+ if (target) {
2862
+ const ref = adjustRange(e.attrs.get("ref") ?? "", refChange(c, sheet.name));
2863
+ if (ref === "") {
2864
+ removeElement(e);
2865
+ continue;
2866
+ }
2867
+ if (ref !== null && ref !== e.attrs.get("ref")) tag = setAttr(tag, "ref", ref);
2789
2868
  }
2790
- if (ref !== null && ref !== e.attrs.get("ref")) tag = setAttr(tag, "ref", ref);
2869
+ if (tag !== tagOf(e)) replaceTag(e, tag);
2791
2870
  }
2792
- if (tag !== tagOf(e)) replaceTag(e, tag);
2793
2871
  }
2794
2872
  const masters = /* @__PURE__ */ new Map();
2795
2873
  for (const { cell, row } of formulas) {
@@ -2917,6 +2995,15 @@ function rewriteSheet(text, part, sheet, c, target, style, limits) {
2917
2995
  }
2918
2996
  }
2919
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
+ }
2920
3007
  const containers = /* @__PURE__ */ new Map();
2921
3008
  let parent2 = null;
2922
3009
  for (const e of elements) {
@@ -2934,8 +3021,10 @@ function rewriteSheet(text, part, sheet, c, target, style, limits) {
2934
3021
  const sqref = e.attrs.get("sqref") ?? "";
2935
3022
  const out = adjustRange(sqref, refChange(c, sheet.name));
2936
3023
  if (out === null) throw notSupported(`A conditional format of sheet "${sheet.name}" has a range xlvane cannot adjust: "${sqref.slice(0, 60)}".`, where());
2937
- if (out === "") removeElement(e);
2938
- else if (out !== sqref) replaceTag(e, setAttr(tagOf(e), "sqref", out));
3024
+ if (out === "") {
3025
+ 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));
2939
3028
  }
2940
3029
  continue;
2941
3030
  }
@@ -3007,6 +3096,7 @@ function rewriteSheet(text, part, sheet, c, target, style, limits) {
3007
3096
  if (tag !== tagOf(container)) replaceTag(container, tag);
3008
3097
  }
3009
3098
  }
3099
+ rewriteExtensionRanges(text, elements, knownExts, c, sheet, removedIds, edits, where);
3010
3100
  const result = applyEdits(text, edits);
3011
3101
  return {
3012
3102
  text: /sortState/.test(result) ? rewriteSortStates(result, part, sheet, c, limits) : result,
@@ -3014,6 +3104,90 @@ function rewriteSheet(text, part, sheet, c, target, style, limits) {
3014
3104
  };
3015
3105
  }
3016
3106
  /**
3107
+ * Moves the ranges (`xm:sqref`) of the extension conditional formats and data validations of the
3108
+ * changed sheet (SR-40, SR-44; R-20; D-F04). An item whose cells are all deleted is removed, as is
3109
+ * an extension rule linked to a removed classic rule (SR-41); emptied containers, `ext` and
3110
+ * `extLst` elements are removed, and `x14:dataValidations@count` is updated.
3111
+ */
3112
+ function rewriteExtensionRanges(text, elements, knownExts, c, sheet, removedIds, edits, where) {
3113
+ const roots = [];
3114
+ const stack = [];
3115
+ for (const e of elements) {
3116
+ if (!(e.depth === 2 && e.local === "extLst" || e.depth === 3 && e.local === "ext" || e.ns === "http://schemas.microsoft.com/office/spreadsheetml/2009/9/main" && e.depth >= 4 && e.depth <= 6 || e.ns === "http://schemas.microsoft.com/office/excel/2006/main" && e.depth === 6 && e.local === "sqref")) continue;
3117
+ while (stack.length > 0 && !within(stack[stack.length - 1].e, e)) stack.pop();
3118
+ const node = {
3119
+ e,
3120
+ children: []
3121
+ };
3122
+ (stack.length > 0 ? stack[stack.length - 1].children : roots).push(node);
3123
+ stack.push(node);
3124
+ }
3125
+ const tagOf = (e) => text.slice(e.start.start, e.start.end);
3126
+ const remove = (e) => {
3127
+ edits.push([
3128
+ e.start.start,
3129
+ e.end.end,
3130
+ ""
3131
+ ]);
3132
+ };
3133
+ for (const list of roots) {
3134
+ if (list.e.local !== "extLst") continue;
3135
+ let extsKept = 0;
3136
+ for (const ext of list.children) {
3137
+ if (ext.e.local !== "ext" || !knownExts.has(ext.e)) {
3138
+ extsKept++;
3139
+ continue;
3140
+ }
3141
+ let containersKept = 0;
3142
+ let containersTotal = 0;
3143
+ for (const container of ext.children) {
3144
+ if (container.e.depth !== 4 || container.e.ns !== "http://schemas.microsoft.com/office/spreadsheetml/2009/9/main") continue;
3145
+ containersTotal++;
3146
+ let kept = 0;
3147
+ let total = 0;
3148
+ for (const item of container.children) {
3149
+ if (item.e.depth !== 5 || item.e.ns !== "http://schemas.microsoft.com/office/spreadsheetml/2009/9/main") continue;
3150
+ total++;
3151
+ let gone = false;
3152
+ const sqref = item.children.find((n) => n.e.ns === "http://schemas.microsoft.com/office/excel/2006/main" && n.e.local === "sqref")?.e;
3153
+ if (sqref) {
3154
+ const normal = decodeEntities(content(text, sqref)).trim().split(/\s+/).join(" ");
3155
+ const out = normal === "" ? "" : adjustRange(normal, refChange(c, sheet.name));
3156
+ if (out === null) throw notSupported(`An extension ${item.e.local === "dataValidation" ? "data validation" : "conditional format"} of sheet "${sheet.name}" has a range xlvane cannot adjust: "${normal.slice(0, 60)}".`, where());
3157
+ if (out === "" && normal !== "") gone = true;
3158
+ else if (out !== normal) edits.push([
3159
+ sqref.start.end,
3160
+ sqref.end.start,
3161
+ escapeText(out)
3162
+ ]);
3163
+ }
3164
+ if (!gone && removedIds.size > 0) {
3165
+ const rules = item.children.filter((n) => n.e.ns === "http://schemas.microsoft.com/office/spreadsheetml/2009/9/main" && n.e.local === "cfRule").map((n) => n.e);
3166
+ const linked = rules.filter((r) => removedIds.has((r.attrs.get("id") ?? "").trim().toLowerCase()));
3167
+ if (linked.length > 0 && linked.length === rules.length) gone = true;
3168
+ else for (const r of linked) remove(r);
3169
+ }
3170
+ if (gone) remove(item.e);
3171
+ else kept++;
3172
+ }
3173
+ if (kept === 0 && total > 0) {
3174
+ remove(container.e);
3175
+ continue;
3176
+ }
3177
+ containersKept++;
3178
+ if (kept < total && container.e.attrs.has("count")) edits.push([
3179
+ container.e.start.start,
3180
+ container.e.start.end,
3181
+ setAttr(tagOf(container.e), "count", String(kept))
3182
+ ]);
3183
+ }
3184
+ if (containersTotal > 0 && containersKept === 0) remove(ext.e);
3185
+ else extsKept++;
3186
+ }
3187
+ if (list.children.length > 0 && extsKept === 0) remove(list.e);
3188
+ }
3189
+ }
3190
+ /**
3017
3191
  * Moves the sort ranges of the changed sheet (`sortState` at sheet level or inside `autoFilter`,
3018
3192
  * and their `sortCondition`s) as ranges move (D-B01). A sort range that is deleted entirely is
3019
3193
  * removed; one that cannot be adjusted is refused. Runs on the text after the other edits, so
@@ -3107,8 +3281,25 @@ function eachVmlShape(text, change) {
3107
3281
  out.push(text.slice(pos));
3108
3282
  return out.join("");
3109
3283
  }
3284
+ /**
3285
+ * Finds the first `drawing` and `legacyDrawing` elements of the worksheet (SpreadsheetML
3286
+ * namespace, any depth, so also inside `mc:AlternateContent`) with the element scanner, so that
3287
+ * comments are skipped and the time is linear in the part's size (D-H01); the id is the
3288
+ * attribute `id` in the relationships namespace, whatever its prefix.
3289
+ */
3290
+ function drawingIds(text, part, limits) {
3291
+ const { elements } = scan(text, part, limits, (local, depth, _parent, ns) => depth >= 2 && (local === "drawing" || local === "legacyDrawing") && (ns === SML_NS.transitional || ns === SML_NS.strict));
3292
+ const idOf = (local) => {
3293
+ const e = elements.find((x) => x.local === local);
3294
+ return e?.attrs.get(`{${REL_NS.transitional}}id`) ?? e?.attrs.get(`{${REL_NS.strict}}id`);
3295
+ };
3296
+ return {
3297
+ drawing: idOf("drawing"),
3298
+ legacyDrawing: idOf("legacyDrawing")
3299
+ };
3300
+ }
3110
3301
  /** Rewrites the notes of the changed sheet: the Comments part and the note shapes of its VML part. */
3111
- function rewriteNotes(edit, pkg, sheetPart, sheetText, sheet, c) {
3302
+ function rewriteNotes(edit, pkg, sheetPart, ids, sheet, c) {
3112
3303
  const rels = pkg.relationships(sheetPart);
3113
3304
  const comments = rels.find((r) => !r.external && classifyRelationship(r.type)?.name === "comments");
3114
3305
  if (comments !== void 0) {
@@ -3134,7 +3325,7 @@ function rewriteNotes(edit, pkg, sheetPart, sheetText, sheet, c) {
3134
3325
  if (edits.length > 0) edit.write(comments.target, applyEdits(text, edits));
3135
3326
  }
3136
3327
  }
3137
- const id = /<(?:[A-Za-z_][\w.-]*:)?legacyDrawing\b[^>]*?\br:id\s*=\s*["']([^"']*)["']/.exec(sheetText)?.[1];
3328
+ const id = ids.legacyDrawing;
3138
3329
  const vml = id === void 0 ? void 0 : rels.find((r) => r.id === id && !r.external);
3139
3330
  if (vml === void 0) return;
3140
3331
  const text = edit.text(vml.target);
@@ -3170,8 +3361,8 @@ function rewriteNotes(edit, pkg, sheetPart, sheetText, sheet, c) {
3170
3361
  * rows. Drawings with charts, shapes, groups or other objects are refused, and so is a deletion
3171
3362
  * that would cut a picture (a deleted row inside the rows it covers or under its anchor cell).
3172
3363
  */
3173
- function rewriteDrawing(edit, pkg, sheetPart, sheetText, sheet, c) {
3174
- const id = /<(?:[A-Za-z_][\w.-]*:)?drawing\b[^>]*?\br:id\s*=\s*["']([^"']*)["']/.exec(sheetText)?.[1];
3364
+ function rewriteDrawing(edit, pkg, sheetPart, ids, sheet, c) {
3365
+ const id = ids.drawing;
3175
3366
  const rels = pkg.relationships(sheetPart);
3176
3367
  const rel = id === void 0 ? void 0 : rels.find((r) => r.id === id && !r.external && classifyRelationship(r.type)?.name === "drawing");
3177
3368
  if (rel === void 0) return;
@@ -3425,8 +3616,9 @@ function changeRows(input, sheetName, at, count, style, limits) {
3425
3616
  if (out.text !== text) edit.write(s.part, out.text);
3426
3617
  rewriteTables(edit, pkg, s.part, s, c, target);
3427
3618
  if (target) {
3428
- rewriteNotes(edit, pkg, s.part, text, s, c);
3429
- rewriteDrawing(edit, pkg, s.part, text, s, c);
3619
+ const ids = drawingIds(text, s.part, edit.limits);
3620
+ rewriteNotes(edit, pkg, s.part, ids, s, c);
3621
+ rewriteDrawing(edit, pkg, s.part, ids, s, c);
3430
3622
  }
3431
3623
  }
3432
3624
  rewriteNames(edit, workbookPart, c);
@@ -3449,15 +3641,14 @@ function changeRows(input, sheetName, at, count, style, limits) {
3449
3641
  * Inserts empty rows into a sheet of an existing XLSX file and returns the new file. Rows below
3450
3642
  * move down, and every reference follows them as in a spreadsheet application: formulas on all
3451
3643
  * sheets (ranges that the new rows fall into grow), merged cells, hyperlinks, notes, the
3452
- * autofilter, conditional formats, data validation, page breaks, defined names, tables (which
3453
- * grow when rows are inserted inside them) and pictures placed on the sheet. Every other part of
3454
- * the file stays exactly as it was.
3644
+ * autofilter, conditional formats (also those of the [MS-XLSX] extension, and thresholds that
3645
+ * refer to cells), data validation (also of the extension), page breaks, defined names, tables
3646
+ * (which grow when rows are inserted inside them) and pictures placed on the sheet. Every other
3647
+ * part of the file stays exactly as it was.
3455
3648
  *
3456
3649
  * Refused with `XLV_E_NOT_SUPPORTED`, leaving the file unchanged: a drawing that holds charts,
3457
3650
  * shapes or anything besides pictures; embedded objects, form controls, scenarios, cell watches,
3458
- * custom sheet views, extended content that refers to cells (data bars, sparklines, slicers -
3459
- * including the extended data bar settings and the icon sets `3Stars`, `3Triangles`, `5Boxes`, `NoIcons`
3460
- * that xlvane itself writes) and
3651
+ * custom sheet views, other extended content that refers to cells (sparklines, slicers) and
3461
3652
  * other related parts xlvane does not know; charts or pivot tables anywhere in the workbook that
3462
3653
  * may refer to the sheet; data tables; an array formula the change would split; and formulas it
3463
3654
  * cannot rewrite safely (docs/facts/ref-adjust.md).