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