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.
- package/README.md +8 -7
- package/dist/cli.js +474 -51
- package/dist/index.d.ts +10 -10
- package/dist/index.js +3 -3
- package/dist/{node-files-uazELnqk.js → node-files-CsQVpKgP.js} +1 -1
- package/dist/node.d.ts +1 -1
- package/dist/node.js +3 -3
- package/dist/{options-B7UhudO_.js → options-Ba6r4zP1.js} +399 -85
- package/dist/{rows-Cwte1t8m.js → rows-Biu_ddwz.js} +280 -89
- package/dist/{stream-write-Q-sef804.d.ts → stream-write-CaGFWPhB.d.ts} +45 -12
- package/dist/{stream-write-CTgWff2P.js → stream-write-CtWyTd44.js} +32 -10
- package/docs/cli-json/inspect.schema.json +8 -0
- package/docs/errors.md +18 -1
- package/docs/exceljs-compat.md +3 -3
- package/docs/inspect-json.md +2 -0
- package/llms-full.txt +41 -13
- package/package.json +1 -1
- package/skills/xlvane/SKILL.md +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $
|
|
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
|
-
|
|
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
|
-
/**
|
|
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 &&
|
|
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
|
-
|
|
638
|
-
|
|
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, `$
|
|
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(
|
|
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 =
|
|
2653
|
-
if (out === null) return
|
|
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
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
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
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
if (
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
if (
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
col
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
if (
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
e.
|
|
2775
|
-
out
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
if (
|
|
2781
|
-
const
|
|
2782
|
-
if (
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
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 (
|
|
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 === "")
|
|
2938
|
-
|
|
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,
|
|
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 =
|
|
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,
|
|
3174
|
-
const id =
|
|
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
|
-
|
|
3429
|
-
|
|
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
|
|
3453
|
-
*
|
|
3454
|
-
*
|
|
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 (
|
|
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).
|