xlvane 1.2.2 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -6
- package/dist/cli.js +125 -45
- package/dist/index.d.ts +33 -11
- package/dist/index.js +3 -3
- package/dist/{node-files-CDb6TyZb.js → node-files-SVg17pky.js} +2 -2
- package/dist/node.d.ts +1 -1
- package/dist/node.js +3 -3
- package/dist/{options-Ba1iWD-Z.js → options-Cxt-LJE4.js} +8107 -6923
- package/dist/{rows-IWKIxHt9.js → rows-DltZ-hiW.js} +1116 -396
- package/dist/{stream-write-BGuUjlXo.js → stream-write-D573ewpr.js} +112 -16
- package/dist/{stream-write-BVaFkgwO.d.ts → stream-write-XnCyLdOy.d.ts} +354 -8
- package/docs/cli-json/inspect.schema.json +76 -0
- package/docs/errors.md +33 -12
- package/docs/exceljs-compat.md +32 -12
- 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 +422 -32
- 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-Cxt-LJE4.js";
|
|
2
2
|
// src/table.ts
|
|
3
3
|
const READ_OPTIONS = [
|
|
4
4
|
"sheet",
|
|
@@ -199,6 +199,7 @@ function prepare(input, options) {
|
|
|
199
199
|
let cellCount = 0;
|
|
200
200
|
for (const cells of byRow.values()) cellCount += cells.size;
|
|
201
201
|
checkOutputPositions(numbers.length * columns.length, cellCount, limits.maxCells, "The table", sheet.name);
|
|
202
|
+
checkKeyText(columns, numbers.length, limits.maxSharedStringChars, sheet.name);
|
|
202
203
|
for (const r of numbers) {
|
|
203
204
|
const cells = byRow.get(r);
|
|
204
205
|
if (skipEmpty && (cells === void 0 || [...cells.values()].every((c) => c.value === null))) continue;
|
|
@@ -241,7 +242,7 @@ function toIssue(issue, row, columns) {
|
|
|
241
242
|
return typeof key === "number" ? key : String(key);
|
|
242
243
|
});
|
|
243
244
|
const first = path[0];
|
|
244
|
-
const column = typeof first === "string" ? columns.
|
|
245
|
+
const column = typeof first === "string" ? columns.get(first) : void 0;
|
|
245
246
|
return {
|
|
246
247
|
row,
|
|
247
248
|
cell: column === void 0 ? null : formatCellRef(row, column.col),
|
|
@@ -254,9 +255,11 @@ function collect(prepared, results) {
|
|
|
254
255
|
const rows = [];
|
|
255
256
|
const rowNumbers = [];
|
|
256
257
|
const issues = [];
|
|
258
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
259
|
+
for (const c of prepared.columns) if (!byKey.has(c.key)) byKey.set(c.key, c);
|
|
257
260
|
results.forEach((result, i) => {
|
|
258
261
|
const row = prepared.rowNumbers[i] ?? 0;
|
|
259
|
-
if (result.issues) for (const issue of result.issues) issues.push(toIssue(issue, row,
|
|
262
|
+
if (result.issues) for (const issue of result.issues) issues.push(toIssue(issue, row, byKey));
|
|
260
263
|
else {
|
|
261
264
|
rows.push(result.value);
|
|
262
265
|
rowNumbers.push(row);
|
|
@@ -359,7 +362,8 @@ const WRITE_OPTIONS = [
|
|
|
359
362
|
"columns",
|
|
360
363
|
"sheet",
|
|
361
364
|
"preset",
|
|
362
|
-
"dateSystem"
|
|
365
|
+
"dateSystem",
|
|
366
|
+
"maxCells"
|
|
363
367
|
];
|
|
364
368
|
/** Header style of the `report` preset (D-304). */
|
|
365
369
|
const REPORT_HEADER = {
|
|
@@ -478,11 +482,22 @@ function addTableSheet(workbook, rows, options) {
|
|
|
478
482
|
columns.forEach((column, c) => {
|
|
479
483
|
if (hasHeader && column.header !== "") sheet.set(formatCellRef(1, c + 1), column.header, preset === "report" ? REPORT_HEADER : void 0);
|
|
480
484
|
});
|
|
485
|
+
const formatted = [];
|
|
486
|
+
columns.forEach((column, c) => {
|
|
487
|
+
if (column.numFmt !== void 0) formatted.push(c);
|
|
488
|
+
});
|
|
489
|
+
const columnsOfKey = /* @__PURE__ */ new Map();
|
|
490
|
+
if (!arrays) columns.forEach((column, c) => {
|
|
491
|
+
const list = columnsOfKey.get(column.key);
|
|
492
|
+
if (list === void 0) columnsOfKey.set(column.key, [c]);
|
|
493
|
+
else list.push(c);
|
|
494
|
+
});
|
|
495
|
+
let cells = hasHeader ? columns.filter((c) => c.header !== "").length : 0;
|
|
496
|
+
const hasOwn = (o, key) => Object.prototype.hasOwnProperty.call(o, key);
|
|
481
497
|
rows.forEach((row, i) => {
|
|
482
498
|
const r = i + 1 + offset;
|
|
483
|
-
|
|
484
|
-
const
|
|
485
|
-
const raw = arrays ? row[c] : Object.prototype.hasOwnProperty.call(record, column.key) ? record[column.key] : void 0;
|
|
499
|
+
const put = (c, raw) => {
|
|
500
|
+
const column = columns[c];
|
|
486
501
|
const value = raw === void 0 ? null : raw;
|
|
487
502
|
if (!(value === null || typeof value === "number" || typeof value === "string" || typeof value === "boolean" || value instanceof Date)) throw tableError(`Row ${String(i + 1)}, column "${column.key}" holds ${Array.isArray(value) ? "an array" : typeof value}, which is not a cell value.`, "Cell values are numbers, strings, booleans, Dates or null. Convert nested data first, for example with JSON.stringify.", {
|
|
488
503
|
row: r,
|
|
@@ -491,8 +506,29 @@ function addTableSheet(workbook, rows, options) {
|
|
|
491
506
|
if (value === null && column.numFmt === void 0) return;
|
|
492
507
|
sheet.set(formatCellRef(r, c + 1), value, column.numFmt === void 0 ? void 0 : { numFmt: column.numFmt });
|
|
493
508
|
widths[c] = Math.max(widths[c] ?? 0, displayLength(value));
|
|
494
|
-
|
|
509
|
+
cells++;
|
|
510
|
+
};
|
|
511
|
+
if (arrays) {
|
|
512
|
+
const values = row;
|
|
513
|
+
const n = Math.min(values.length, columns.length);
|
|
514
|
+
for (let c = 0; c < n; c++) put(c, values[c]);
|
|
515
|
+
for (const c of formatted) if (c >= n) put(c, void 0);
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
const record = row;
|
|
519
|
+
const own = Object.getOwnPropertyNames(record);
|
|
520
|
+
if (own.length >= columns.length) {
|
|
521
|
+
for (let c = 0; c < columns.length; c++) put(c, hasOwn(record, columns[c].key) ? record[columns[c].key] : void 0);
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
const visit = [];
|
|
525
|
+
for (const key of own) for (const c of columnsOfKey.get(key) ?? []) visit.push(c);
|
|
526
|
+
for (const c of formatted) if (!hasOwn(record, columns[c].key)) visit.push(c);
|
|
527
|
+
visit.sort((x, y) => x - y);
|
|
528
|
+
for (const c of visit) put(c, hasOwn(record, columns[c].key) ? record[columns[c].key] : void 0);
|
|
495
529
|
});
|
|
530
|
+
const maxCells = options.get("maxCells");
|
|
531
|
+
checkOutputPositions((rows.length + offset) * columns.length, cells, typeof maxCells === "number" ? maxCells : DEFAULT_READ_LIMITS.maxCells, "The table", sheet.name, "The rows are spread over far more columns than they fill, for example after one row with many keys or a long first CSV line. Write fewer columns (the \"columns\" option), split the data over several tables, or raise the \"maxCells\" option for a trusted sparse table.");
|
|
496
532
|
columns.forEach((column, c) => {
|
|
497
533
|
if (column.width !== void 0) sheet.column(c + 1, { width: column.width });
|
|
498
534
|
else if (preset === "report") sheet.column(c + 1, { width: Math.min(MAX_FITTED_WIDTH, Math.max(MIN_WIDTH, (widths[c] ?? 0) + 2)) });
|
|
@@ -524,6 +560,11 @@ function addTableSheet(workbook, rows, options) {
|
|
|
524
560
|
*/
|
|
525
561
|
function writeTable(rows, options) {
|
|
526
562
|
const o = readOptions(options, WRITE_OPTIONS, "writeTable options");
|
|
563
|
+
const maxCells = o.get("maxCells");
|
|
564
|
+
if (maxCells !== void 0 && (typeof maxCells !== "number" || !(maxCells > 0))) throw new XlvError("XLV_E_INVALID_OPTION", "The option \"maxCells\" must be a positive number.", {
|
|
565
|
+
hint: "For example { maxCells: 20_000_000 }, or Infinity.",
|
|
566
|
+
details: { option: "maxCells" }
|
|
567
|
+
});
|
|
527
568
|
const dateSystem = o.get("dateSystem");
|
|
528
569
|
const workbook = createWorkbook(dateSystem === void 0 ? void 0 : { dateSystem });
|
|
529
570
|
addTableSheet(workbook, rows, o);
|
|
@@ -558,8 +599,10 @@ function fromJSON(input, options) {
|
|
|
558
599
|
// src/csv.ts
|
|
559
600
|
/**
|
|
560
601
|
* First characters that make spreadsheet applications treat a CSV field as a formula (CSV-10),
|
|
561
|
-
* plus, as a precaution, the small forms of = + - @ (CSV-12, D-H01)
|
|
562
|
-
*
|
|
602
|
+
* plus, as a precaution, the small forms of = + - @ (CSV-12, D-H01) and the byte order marks
|
|
603
|
+
* U+FEFF and U+FFFE, which an application drops at the start of the file before it looks at the
|
|
604
|
+
* field (CSV-12). Text starting with any other C0 control character is escaped too (see
|
|
605
|
+
* `injectionStart`).
|
|
563
606
|
*/
|
|
564
607
|
const INJECTION_START = /* @__PURE__ */ new Set([
|
|
565
608
|
"=",
|
|
@@ -576,13 +619,42 @@ const INJECTION_START = /* @__PURE__ */ new Set([
|
|
|
576
619
|
"﹦",
|
|
577
620
|
"﹢",
|
|
578
621
|
"﹣",
|
|
622
|
+
"﹫",
|
|
623
|
+
"",
|
|
624
|
+
""
|
|
625
|
+
]);
|
|
626
|
+
/** Signs that start a formula: refused as the delimiter while formulas are escaped (D-J01). */
|
|
627
|
+
const FORMULA_SIGNS = /* @__PURE__ */ new Set([
|
|
628
|
+
"=",
|
|
629
|
+
"+",
|
|
630
|
+
"-",
|
|
631
|
+
"@",
|
|
632
|
+
"=",
|
|
633
|
+
"+",
|
|
634
|
+
"-",
|
|
635
|
+
"@",
|
|
636
|
+
"﹦",
|
|
637
|
+
"﹢",
|
|
638
|
+
"﹣",
|
|
579
639
|
"﹫"
|
|
580
640
|
]);
|
|
581
641
|
/** Whether text starting with `c` is escaped: one of INJECTION_START or a C0 control (NUL included). */
|
|
582
642
|
const injectionStart = (c) => INJECTION_START.has(c) || c.charCodeAt(0) < 32;
|
|
583
643
|
/**
|
|
584
|
-
*
|
|
585
|
-
*
|
|
644
|
+
* Separators that common readers use besides the delimiter: a field that holds one of them is
|
|
645
|
+
* quoted whatever the delimiter, so a reader that splits with another separator keeps it whole
|
|
646
|
+
* (CSV-14).
|
|
647
|
+
*/
|
|
648
|
+
const SEPARATORS = [
|
|
649
|
+
",",
|
|
650
|
+
";",
|
|
651
|
+
" ",
|
|
652
|
+
"|"
|
|
653
|
+
];
|
|
654
|
+
/**
|
|
655
|
+
* Converts a sheet to CSV text. Fields that contain the delimiter, a quote, CR, LF, or one of
|
|
656
|
+
* the common separators `,`, `;`, tab and `|` (whatever the delimiter, CSV-14) are enclosed in
|
|
657
|
+
* quotes, and quotes are doubled (RFC 4180). Numbers are written in JavaScript's
|
|
586
658
|
* shortest form, booleans as `TRUE`/`FALSE`, dates as ISO 8601, error cells as their code and
|
|
587
659
|
* formula cells as their cached value. By default, text that a spreadsheet would run as a
|
|
588
660
|
* formula is escaped (`formulaInjection: "escape"`).
|
|
@@ -615,6 +687,10 @@ function toCSV(input, options) {
|
|
|
615
687
|
const newline = oneOfOption("newline", o.get("newline"), ["\r\n", "\n"], "\r\n");
|
|
616
688
|
const bom = booleanOption("bom", o.get("bom"), false);
|
|
617
689
|
const injection = oneOfOption("formulaInjection", o.get("formulaInjection"), ["escape", "off"], "escape");
|
|
690
|
+
if (injection === "escape" && FORMULA_SIGNS.has(delimiter)) throw new XlvError("XLV_E_INVALID_OPTION", `The delimiter "${delimiter}" starts a formula, so lines with an empty first field would run as formulas.`, {
|
|
691
|
+
hint: "Use \",\" (default), \";\", \"|\" or \"\\t\", or pass formulaInjection: \"off\" for a trusted consumer.",
|
|
692
|
+
details: { option: "delimiter" }
|
|
693
|
+
});
|
|
618
694
|
const limits = resolveLimits(o.get("limits"));
|
|
619
695
|
const sheet = pickSheet(openInput(input, o.get("limits"), o.get("onWarning")), o.get("sheet"));
|
|
620
696
|
let startRow = 1;
|
|
@@ -645,7 +721,7 @@ function toCSV(input, options) {
|
|
|
645
721
|
cellCount++;
|
|
646
722
|
}
|
|
647
723
|
if (endRow >= startRow && endCol >= startCol) checkOutputPositions((endRow - startRow + 1) * (endCol - startCol + 1), cellCount, limits.maxCells, "The CSV text", sheet.name);
|
|
648
|
-
const needsQuotes = (s) => s.includes(delimiter) || s.includes("\"") || s.includes("\r") || s.includes("\n");
|
|
724
|
+
const needsQuotes = (s) => s.includes(delimiter) || s.includes("\"") || s.includes("\r") || s.includes("\n") || SEPARATORS.some((c) => s.includes(c));
|
|
649
725
|
const field = (value) => {
|
|
650
726
|
if (value === null || value === void 0) return "";
|
|
651
727
|
if (typeof value === "number") return String(value);
|
|
@@ -803,7 +879,8 @@ function inferNumber(s) {
|
|
|
803
879
|
const value = Number(s);
|
|
804
880
|
if (!Number.isFinite(value)) return null;
|
|
805
881
|
const digits = s.slice(intStart, intStart + intDigits + (fracDigits > 0 ? fracDigits + 1 : 0)).replace(".", "").replace(/^0+/, "");
|
|
806
|
-
if (value === 0
|
|
882
|
+
if (value === 0 && /[1-9]/.test(digits)) return null;
|
|
883
|
+
if (value !== 0 && Math.abs(value) < MIN_NORMAL && String(value) !== s) return null;
|
|
807
884
|
if (digits.length > 15) {
|
|
808
885
|
if (fracDigits === 0 && !/[eE]/.test(s) || String(value) !== s) return null;
|
|
809
886
|
}
|
|
@@ -913,6 +990,7 @@ function fromCSV(input, options) {
|
|
|
913
990
|
const workbook = createWorkbook(dateSystem === void 0 ? void 0 : { dateSystem });
|
|
914
991
|
const tableOptions = /* @__PURE__ */ new Map([["sheet", o.get("sheet") ?? "Sheet1"], ["preset", oneOfOption("preset", o.get("preset"), ["plain", "report"], "plain")]]);
|
|
915
992
|
if (columns) tableOptions.set("columns", columns);
|
|
993
|
+
tableOptions.set("maxCells", maxCells);
|
|
916
994
|
addTableSheet(workbook, rows, tableOptions);
|
|
917
995
|
return writeWorkbook(workbook);
|
|
918
996
|
}
|
|
@@ -928,13 +1006,30 @@ function toDiffCell(cell, styles) {
|
|
|
928
1006
|
if (styles) out.style = cell.style;
|
|
929
1007
|
return out;
|
|
930
1008
|
}
|
|
931
|
-
|
|
1009
|
+
/**
|
|
1010
|
+
* Compares styles by their stable key, computed once per style object: the reader shares one
|
|
1011
|
+
* frozen object per style, so a sheet of a million cells needs a few keys, not two per cell.
|
|
1012
|
+
*/
|
|
1013
|
+
function styleComparer() {
|
|
1014
|
+
const keys = /* @__PURE__ */ new Map();
|
|
1015
|
+
const keyOf = (style) => {
|
|
1016
|
+
if (typeof style !== "object" || style === null) return stableKey(style);
|
|
1017
|
+
let key = keys.get(style);
|
|
1018
|
+
if (key === void 0) {
|
|
1019
|
+
key = stableKey(style);
|
|
1020
|
+
keys.set(style, key);
|
|
1021
|
+
}
|
|
1022
|
+
return key;
|
|
1023
|
+
};
|
|
1024
|
+
return (a, b) => a === b || keyOf(a) === keyOf(b);
|
|
1025
|
+
}
|
|
1026
|
+
function sameCell(a, b, sameStyle) {
|
|
932
1027
|
if (a.type !== b.type) return false;
|
|
933
1028
|
const av = a.value instanceof Date ? a.value.getTime() : a.value;
|
|
934
1029
|
const bv = b.value instanceof Date ? b.value.getTime() : b.value;
|
|
935
1030
|
if (!Object.is(av, bv)) return false;
|
|
936
1031
|
if ((a.formula?.text ?? null) !== (b.formula?.text ?? null)) return false;
|
|
937
|
-
return
|
|
1032
|
+
return sameStyle === null || sameStyle(a.style, b.style);
|
|
938
1033
|
}
|
|
939
1034
|
/**
|
|
940
1035
|
* Compares two workbooks: added and removed sheets (by name), visibility changes, and cells
|
|
@@ -983,44 +1078,45 @@ function diff(a, b, options) {
|
|
|
983
1078
|
summary.sheetsAdded = sheetsAdded.length;
|
|
984
1079
|
summary.sheetsRemoved = sheetsRemoved.length;
|
|
985
1080
|
const sheets = [];
|
|
1081
|
+
const sameStyle = styles ? styleComparer() : null;
|
|
986
1082
|
for (const after of second.sheets) {
|
|
987
1083
|
const before = firstNames.get(after.name);
|
|
988
1084
|
if (!before) continue;
|
|
989
1085
|
const fromFirst = [];
|
|
990
1086
|
const fromSecond = [];
|
|
991
1087
|
let count = 0;
|
|
992
|
-
const record = (list, row, col, change) => {
|
|
1088
|
+
const record = (list, row, col, kind, change) => {
|
|
993
1089
|
count++;
|
|
994
|
-
if (
|
|
995
|
-
else if (
|
|
1090
|
+
if (kind === "added") summary.cellsAdded++;
|
|
1091
|
+
else if (kind === "removed") summary.cellsRemoved++;
|
|
996
1092
|
else summary.cellsChanged++;
|
|
997
1093
|
if (list.length < maxCells) list.push({
|
|
998
1094
|
row,
|
|
999
1095
|
col,
|
|
1000
|
-
change
|
|
1096
|
+
change: change()
|
|
1001
1097
|
});
|
|
1002
1098
|
};
|
|
1003
1099
|
for (const cell of before.cells()) {
|
|
1004
1100
|
const other = after.cell(cell.ref);
|
|
1005
|
-
if (other === void 0) record(fromFirst, cell.row, cell.col, {
|
|
1101
|
+
if (other === void 0) record(fromFirst, cell.row, cell.col, "removed", () => ({
|
|
1006
1102
|
ref: cell.ref,
|
|
1007
1103
|
kind: "removed",
|
|
1008
1104
|
before: toDiffCell(cell, styles),
|
|
1009
1105
|
after: null
|
|
1010
|
-
});
|
|
1011
|
-
else if (!sameCell(cell, other,
|
|
1106
|
+
}));
|
|
1107
|
+
else if (!sameCell(cell, other, sameStyle)) record(fromFirst, cell.row, cell.col, "changed", () => ({
|
|
1012
1108
|
ref: cell.ref,
|
|
1013
1109
|
kind: "changed",
|
|
1014
1110
|
before: toDiffCell(cell, styles),
|
|
1015
1111
|
after: toDiffCell(other, styles)
|
|
1016
|
-
});
|
|
1112
|
+
}));
|
|
1017
1113
|
}
|
|
1018
|
-
for (const cell of after.cells()) if (before.cell(cell.ref) === void 0) record(fromSecond, cell.row, cell.col, {
|
|
1114
|
+
for (const cell of after.cells()) if (before.cell(cell.ref) === void 0) record(fromSecond, cell.row, cell.col, "added", () => ({
|
|
1019
1115
|
ref: cell.ref,
|
|
1020
1116
|
kind: "added",
|
|
1021
1117
|
before: null,
|
|
1022
1118
|
after: toDiffCell(cell, styles)
|
|
1023
|
-
});
|
|
1119
|
+
}));
|
|
1024
1120
|
const stateChanged = before.state !== after.state;
|
|
1025
1121
|
if (count === 0 && !stateChanged) continue;
|
|
1026
1122
|
summary.sheetsChanged++;
|
|
@@ -1044,6 +1140,76 @@ function diff(a, b, options) {
|
|
|
1044
1140
|
};
|
|
1045
1141
|
}
|
|
1046
1142
|
// src/edit.ts
|
|
1143
|
+
/**
|
|
1144
|
+
* How many cells one call of {@link editCells} or {@link appendRows} writes at most when the
|
|
1145
|
+
* caller gives no `limits.maxCells` (A2). While the file is rebuilt each cell written costs about
|
|
1146
|
+
* 0.5 KB of memory, so the 5 000 000 of `DEFAULT_READ_LIMITS.maxCells` would need about 2.5 GB,
|
|
1147
|
+
* more than the default heap of Node.js (about 2 GB on a 64-bit machine with 16 GB of memory).
|
|
1148
|
+
*/
|
|
1149
|
+
const EDIT_MAX_CELLS = 1e6;
|
|
1150
|
+
/** The read limits of an edit, and how many cells it may write. */
|
|
1151
|
+
function editLimits(input) {
|
|
1152
|
+
const limits = resolveLimits(input);
|
|
1153
|
+
return {
|
|
1154
|
+
limits,
|
|
1155
|
+
writable: typeof input === "object" && input !== null && Object.hasOwn(input, "maxCells") ? limits.maxCells : Math.min(limits.maxCells, EDIT_MAX_CELLS)
|
|
1156
|
+
};
|
|
1157
|
+
}
|
|
1158
|
+
/** Refuses an edit that would write more cells than one call may (A2). */
|
|
1159
|
+
function checkWrites(count, writable, what, sheet) {
|
|
1160
|
+
if (count <= writable) return;
|
|
1161
|
+
throw new XlvError("XLV_E_LIMIT_EXCEEDED", `${what} would write ${String(count)} cells, more than the limit of ${String(writable)} cells per call.`, {
|
|
1162
|
+
location: sheet === void 0 ? {} : { sheet },
|
|
1163
|
+
hint: `Each cell written takes about 0.5 KB of memory while the file is rebuilt. Split the cells into several calls of at most ${String(writable)}, or pass { limits: { maxCells: ${String(count)} } } if there is enough memory.`,
|
|
1164
|
+
details: {
|
|
1165
|
+
limit: "maxCells",
|
|
1166
|
+
value: writable
|
|
1167
|
+
}
|
|
1168
|
+
});
|
|
1169
|
+
}
|
|
1170
|
+
/**
|
|
1171
|
+
* Refuses an edit after which the workbook would hold more cells (with a value or only a style)
|
|
1172
|
+
* than `maxCells` (A2), the limit `readWorkbook` applies to the whole workbook, so the result can be
|
|
1173
|
+
* read again with the same limits (D-K04). Checked before anything per cell is built.
|
|
1174
|
+
*/
|
|
1175
|
+
function checkCellCount(workbook, added, maxCells) {
|
|
1176
|
+
let total = 0;
|
|
1177
|
+
for (const sheet of workbook.sheets) total += sheet.cellCount + styledCellCount(sheet) + (added.get(sheet) ?? 0);
|
|
1178
|
+
if (total <= maxCells) return;
|
|
1179
|
+
const edited = [...added.keys()];
|
|
1180
|
+
throw new XlvError("XLV_E_LIMIT_EXCEEDED", `After the edit, the workbook would hold ${String(total)} cells, more than the limit of ${String(maxCells)}.`, {
|
|
1181
|
+
location: edited.length === 1 ? { sheet: edited[0].name } : {},
|
|
1182
|
+
hint: `Cells of every sheet count, also cells that hold only a style. Write fewer new cells, or, if you trust this file and there is enough memory, raise the limit, for example { limits: { maxCells: ${String(Math.max(total, maxCells * 2))} } }.`,
|
|
1183
|
+
details: {
|
|
1184
|
+
limit: "maxCells",
|
|
1185
|
+
value: maxCells
|
|
1186
|
+
}
|
|
1187
|
+
});
|
|
1188
|
+
}
|
|
1189
|
+
/** Data properties of an edit, read without running getters (planEdits validates edits). */
|
|
1190
|
+
function dataProperty(o, key) {
|
|
1191
|
+
return Object.getOwnPropertyDescriptor(o, key)?.value;
|
|
1192
|
+
}
|
|
1193
|
+
/** Number of cells per sheet that edits write where the sheet has no cell with a value. */
|
|
1194
|
+
function newCells(workbook, edits) {
|
|
1195
|
+
const byName = new Map(workbook.sheets.map((sheet) => [sheet.name, sheet]));
|
|
1196
|
+
const seen = /* @__PURE__ */ new Map();
|
|
1197
|
+
for (const raw of edits) {
|
|
1198
|
+
if (typeof raw !== "object" || raw === null) continue;
|
|
1199
|
+
const name = dataProperty(raw, "sheet");
|
|
1200
|
+
const ref = dataProperty(raw, "cell");
|
|
1201
|
+
const sheet = typeof name === "string" ? byName.get(name) : void 0;
|
|
1202
|
+
const address = typeof ref === "string" ? parseCellRef(ref) : null;
|
|
1203
|
+
if (sheet === void 0 || address === null) continue;
|
|
1204
|
+
let cells = seen.get(sheet);
|
|
1205
|
+
if (cells === void 0) {
|
|
1206
|
+
cells = /* @__PURE__ */ new Set();
|
|
1207
|
+
seen.set(sheet, cells);
|
|
1208
|
+
}
|
|
1209
|
+
if (sheet.cell(formatCellRef(address.row, address.col)) === void 0) cells.add(address.row * (MAX_COLUMNS + 1) + address.col);
|
|
1210
|
+
}
|
|
1211
|
+
return new Map([...seen].map(([sheet, cells]) => [sheet, cells.size]));
|
|
1212
|
+
}
|
|
1047
1213
|
/** `"x:"` for a qualified name `x:local`, `""` without a prefix. */
|
|
1048
1214
|
function prefixOf(qname) {
|
|
1049
1215
|
const colon = qname.indexOf(":");
|
|
@@ -1188,11 +1354,22 @@ var PackageEditor = class {
|
|
|
1188
1354
|
if (cached !== void 0) return cached;
|
|
1189
1355
|
const entry = this.entryOf(partName);
|
|
1190
1356
|
if (entry === void 0) return void 0;
|
|
1191
|
-
return normalizeLineEnds(decodeXml(this.bytesOf(entry), partName));
|
|
1357
|
+
return normalizeLineEnds(decodeXml(this.changed.get(entry.name) ?? this.bytesOf(entry), partName));
|
|
1192
1358
|
}
|
|
1359
|
+
/**
|
|
1360
|
+
* Writes a part. Text given in pieces is encoded piece by piece and not kept as text, so a sheet
|
|
1361
|
+
* of millions of cells is never one more string of its whole size (A2); reading the part again
|
|
1362
|
+
* decodes the written bytes.
|
|
1363
|
+
*/
|
|
1193
1364
|
write(partName, text) {
|
|
1194
|
-
|
|
1195
|
-
|
|
1365
|
+
let data;
|
|
1366
|
+
if (typeof text === "string") {
|
|
1367
|
+
this.texts.set(partKey(partName), text);
|
|
1368
|
+
data = this.encoder.encode(asUtf8(text));
|
|
1369
|
+
} else {
|
|
1370
|
+
this.texts.delete(partKey(partName));
|
|
1371
|
+
data = this.encodePieces(text);
|
|
1372
|
+
}
|
|
1196
1373
|
const entry = this.entryOf(partName);
|
|
1197
1374
|
if (entry) {
|
|
1198
1375
|
this.changed.set(entry.name, data);
|
|
@@ -1206,6 +1383,28 @@ var PackageEditor = class {
|
|
|
1206
1383
|
data
|
|
1207
1384
|
});
|
|
1208
1385
|
}
|
|
1386
|
+
encodePieces(pieces) {
|
|
1387
|
+
let size = 0;
|
|
1388
|
+
for (const piece of pieces) size += piece.length;
|
|
1389
|
+
let out = new Uint8Array(Math.max(1024, size + (size >> 2)));
|
|
1390
|
+
let used = 0;
|
|
1391
|
+
pieces.forEach((piece, i) => {
|
|
1392
|
+
const text = i === 0 ? asUtf8(piece) : piece;
|
|
1393
|
+
for (let from = 0; from < text.length;) {
|
|
1394
|
+
if (out.length - used < 3 * Math.min(text.length - from, 65536)) {
|
|
1395
|
+
const bigger = new Uint8Array(Math.max(out.length * 2, used + 196608));
|
|
1396
|
+
bigger.set(out.subarray(0, used));
|
|
1397
|
+
out = bigger;
|
|
1398
|
+
}
|
|
1399
|
+
let to = Math.min(text.length, from + 65536);
|
|
1400
|
+
if (to < text.length && (text.charCodeAt(to) & 64512) === 56320) to--;
|
|
1401
|
+
const { written } = this.encoder.encodeInto(text.slice(from, to), out.subarray(used));
|
|
1402
|
+
used += written;
|
|
1403
|
+
from = to;
|
|
1404
|
+
}
|
|
1405
|
+
});
|
|
1406
|
+
return out.slice(0, used);
|
|
1407
|
+
}
|
|
1209
1408
|
remove(partName) {
|
|
1210
1409
|
const entry = this.entryOf(partName);
|
|
1211
1410
|
if (entry) this.removed.add(entry.name);
|
|
@@ -1673,10 +1872,178 @@ function newCellXml(ref, value, xf, sst) {
|
|
|
1673
1872
|
const r = valueParts(value, sst, false);
|
|
1674
1873
|
return `<c r="${ref}"${s}${r.t === "" ? "" : attr("t", r.t)}><v>${r.v}</v></c>`;
|
|
1675
1874
|
}
|
|
1676
|
-
/**
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1875
|
+
/** A growable list of 32-bit integers, so that a sheet's cells cost a few numbers each (A2, D-K04). */
|
|
1876
|
+
var IntList = class {
|
|
1877
|
+
data = /* @__PURE__ */ new Int32Array(1024);
|
|
1878
|
+
length = 0;
|
|
1879
|
+
push(value) {
|
|
1880
|
+
if (this.length === this.data.length) {
|
|
1881
|
+
const bigger = new Int32Array(this.data.length * 2);
|
|
1882
|
+
bigger.set(this.data);
|
|
1883
|
+
this.data = bigger;
|
|
1884
|
+
}
|
|
1885
|
+
this.data[this.length++] = value;
|
|
1886
|
+
}
|
|
1887
|
+
get(index) {
|
|
1888
|
+
return this.data[index];
|
|
1889
|
+
}
|
|
1890
|
+
set(index, value) {
|
|
1891
|
+
this.data[index] = value;
|
|
1892
|
+
}
|
|
1893
|
+
};
|
|
1894
|
+
/** Integers stored per row of {@link SheetScan.rows}. */
|
|
1895
|
+
const ROW_FIELDS$1 = 8;
|
|
1896
|
+
/** Integers stored per cell of {@link SheetScan.cells}. */
|
|
1897
|
+
const CELL_FIELDS$1 = 5;
|
|
1898
|
+
function parseIndex(value, digits) {
|
|
1899
|
+
const v = value?.trim() ?? "";
|
|
1900
|
+
return v.length > 0 && v.length <= digits && /^[0-9]+$/.test(v) ? Number(v) : NaN;
|
|
1901
|
+
}
|
|
1902
|
+
/**
|
|
1903
|
+
* Scans a worksheet part once for {@link editSheetXml} and {@link newCellStyle}. Rows and cells
|
|
1904
|
+
* are numbered as the reader numbers them (D-105); the reader skips rows and cells with invalid
|
|
1905
|
+
* references (D-903), and editing around them could put new cells in the wrong place, so a sheet
|
|
1906
|
+
* that has any is not edited.
|
|
1907
|
+
*/
|
|
1908
|
+
function scanSheet(text, part, sheet, limits, styles) {
|
|
1909
|
+
const invalid = (ref) => {
|
|
1910
|
+
throw new XlvError("XLV_E_INVALID_CELL_REF", `Sheet "${sheet}" contains the invalid reference "${ref.slice(0, 20)}", so it cannot be edited safely.`, {
|
|
1911
|
+
location: {
|
|
1912
|
+
part,
|
|
1913
|
+
sheet
|
|
1914
|
+
},
|
|
1915
|
+
hint: "Open and re-save the file in a spreadsheet application, which drops the damaged cells, then edit the re-saved file.",
|
|
1916
|
+
details: { ref: ref.slice(0, 40) }
|
|
1917
|
+
});
|
|
1918
|
+
};
|
|
1919
|
+
const names = [];
|
|
1920
|
+
/** Whether each open element is a recorded row (depth 3) or cell (depth 4). */
|
|
1921
|
+
const recorded = [];
|
|
1922
|
+
let rootQname = null;
|
|
1923
|
+
let sheetDataStart = null;
|
|
1924
|
+
let sheetData = null;
|
|
1925
|
+
let dimension = null;
|
|
1926
|
+
const rows = new IntList();
|
|
1927
|
+
const cells = new IntList();
|
|
1928
|
+
let rowCount = 0;
|
|
1929
|
+
let currentRow = 0;
|
|
1930
|
+
let lastCol = 0;
|
|
1931
|
+
let missingRowRefs = false;
|
|
1932
|
+
const cols = [];
|
|
1933
|
+
const rowsWithFormat = /* @__PURE__ */ new Set();
|
|
1934
|
+
parseXml(text, {
|
|
1935
|
+
start(_ns, local, attrs, span) {
|
|
1936
|
+
const depth = names.length + 1;
|
|
1937
|
+
const parent = names[names.length - 1] ?? "";
|
|
1938
|
+
names.push(local);
|
|
1939
|
+
let keep = false;
|
|
1940
|
+
if (depth === 1) rootQname = span.qname;
|
|
1941
|
+
else if (depth === 2 && local === "sheetData" && sheetDataStart === null) sheetDataStart = span;
|
|
1942
|
+
else if (depth === 2 && local === "dimension" && dimension === null) dimension = {
|
|
1943
|
+
start: span,
|
|
1944
|
+
ref: attrs.get("ref")
|
|
1945
|
+
};
|
|
1946
|
+
else if (depth === 3 && local === "col" && parent === "cols") {
|
|
1947
|
+
const [min, max, style] = [
|
|
1948
|
+
parseIndex(attrs.get("min"), 10),
|
|
1949
|
+
parseIndex(attrs.get("max"), 10),
|
|
1950
|
+
parseIndex(attrs.get("style"), 10)
|
|
1951
|
+
];
|
|
1952
|
+
if (Number.isInteger(min) && Number.isInteger(max) && Number.isInteger(style) && style !== 0 && styles.hasCellFormat(style)) cols.push({
|
|
1953
|
+
min,
|
|
1954
|
+
max,
|
|
1955
|
+
style
|
|
1956
|
+
});
|
|
1957
|
+
} else if (depth === 3 && local === "row" && parent === "sheetData") {
|
|
1958
|
+
const r = attrs.get("r");
|
|
1959
|
+
const n = r === void 0 ? currentRow + 1 : /^[0-9]{1,7}$/.test(r.trim()) ? Number(r.trim()) : NaN;
|
|
1960
|
+
if (!Number.isInteger(n) || n < 1 || n > 1048576) invalid(r ?? String(n));
|
|
1961
|
+
currentRow = n;
|
|
1962
|
+
lastCol = 0;
|
|
1963
|
+
if (r === void 0) missingRowRefs = true;
|
|
1964
|
+
const custom = attrs.get("customFormat")?.trim();
|
|
1965
|
+
if (attrs.has("s") && (custom === "1" || custom === "true")) rowsWithFormat.add(n);
|
|
1966
|
+
rows.push(n);
|
|
1967
|
+
rows.push(span.start);
|
|
1968
|
+
rows.push(span.end);
|
|
1969
|
+
rows.push(span.end);
|
|
1970
|
+
rows.push(span.end);
|
|
1971
|
+
rows.push((r === void 0 ? 0 : 1) | (span.empty ? 2 : 0) | span.qname.length << 2);
|
|
1972
|
+
rows.push(cells.length / CELL_FIELDS$1);
|
|
1973
|
+
rows.push(0);
|
|
1974
|
+
rowCount++;
|
|
1975
|
+
keep = true;
|
|
1976
|
+
} else if (depth === 4 && local === "c" && parent === "row" && recorded[depth - 2] === true) {
|
|
1977
|
+
const r = attrs.get("r");
|
|
1978
|
+
const address = r === void 0 || r === "" ? null : parseCellRef(r);
|
|
1979
|
+
if (r !== void 0 && r !== "" && address === null) invalid(r);
|
|
1980
|
+
const col = address?.col ?? lastCol + 1;
|
|
1981
|
+
lastCol = col;
|
|
1982
|
+
cells.push(col);
|
|
1983
|
+
cells.push(span.start);
|
|
1984
|
+
cells.push(span.end);
|
|
1985
|
+
cells.push(span.end);
|
|
1986
|
+
cells.push((r === void 0 ? 0 : 1) | span.qname.length << 1);
|
|
1987
|
+
rows.set((rowCount - 1) * ROW_FIELDS$1 + 7, rows.get((rowCount - 1) * ROW_FIELDS$1 + 7) + 1);
|
|
1988
|
+
keep = true;
|
|
1989
|
+
}
|
|
1990
|
+
recorded.push(keep);
|
|
1991
|
+
},
|
|
1992
|
+
end(_ns, local, span) {
|
|
1993
|
+
const depth = names.length;
|
|
1994
|
+
names.pop();
|
|
1995
|
+
const keep = recorded.pop() === true;
|
|
1996
|
+
if (depth === 2 && local === "sheetData" && sheetData === null && sheetDataStart !== null) sheetData = {
|
|
1997
|
+
start: sheetDataStart,
|
|
1998
|
+
end: span
|
|
1999
|
+
};
|
|
2000
|
+
else if (keep && depth === 3) {
|
|
2001
|
+
rows.set((rowCount - 1) * ROW_FIELDS$1 + 3, span.start);
|
|
2002
|
+
rows.set((rowCount - 1) * ROW_FIELDS$1 + 4, span.end);
|
|
2003
|
+
} else if (keep && depth === 4) cells.set(cells.length - 2, span.end);
|
|
2004
|
+
},
|
|
2005
|
+
text() {}
|
|
2006
|
+
}, {
|
|
2007
|
+
part,
|
|
2008
|
+
limits
|
|
2009
|
+
});
|
|
2010
|
+
if (rootQname === null) throw new XlvError("XLV_E_PART_INVALID", "The part has no root element.", {
|
|
2011
|
+
location: { part },
|
|
2012
|
+
hint: "Re-save the file in a spreadsheet application."
|
|
2013
|
+
});
|
|
2014
|
+
const columnStyles = new Int32Array(MAX_COLUMNS + 2);
|
|
2015
|
+
const next = new Int32Array(MAX_COLUMNS + 2);
|
|
2016
|
+
for (let i = 0; i < next.length; i++) next[i] = i;
|
|
2017
|
+
const unset = (c) => {
|
|
2018
|
+
let root = c;
|
|
2019
|
+
while (next[root] !== root) root = next[root];
|
|
2020
|
+
while (next[c] !== root) {
|
|
2021
|
+
const up = next[c];
|
|
2022
|
+
next[c] = root;
|
|
2023
|
+
c = up;
|
|
2024
|
+
}
|
|
2025
|
+
return root;
|
|
2026
|
+
};
|
|
2027
|
+
for (const c of cols) {
|
|
2028
|
+
const min = Math.max(c.min, 1);
|
|
2029
|
+
const max = Math.min(c.max, MAX_COLUMNS);
|
|
2030
|
+
if (min > max) continue;
|
|
2031
|
+
for (let col = unset(min); col <= max; col = unset(col + 1)) {
|
|
2032
|
+
columnStyles[col] = c.style;
|
|
2033
|
+
next[col] = col + 1;
|
|
2034
|
+
}
|
|
2035
|
+
}
|
|
2036
|
+
return {
|
|
2037
|
+
rootQname,
|
|
2038
|
+
sheetData,
|
|
2039
|
+
dimension,
|
|
2040
|
+
rows,
|
|
2041
|
+
rowCount,
|
|
2042
|
+
cells,
|
|
2043
|
+
missingRowRefs,
|
|
2044
|
+
columnStyles,
|
|
2045
|
+
rowsWithFormat
|
|
2046
|
+
};
|
|
1680
2047
|
}
|
|
1681
2048
|
/**
|
|
1682
2049
|
* The cell format a new cell of a sheet starts from: the `style` of the `col` element that covers
|
|
@@ -1685,37 +2052,36 @@ function insertRef(tag, qname, ref) {
|
|
|
1685
2052
|
* which of the two wins is not stated in the sources read (D-A04). Indexes beyond the cell
|
|
1686
2053
|
* formats of the file are ignored.
|
|
1687
2054
|
*/
|
|
1688
|
-
function
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
} else {
|
|
1708
|
-
const r = e.attrs.get("r")?.trim();
|
|
1709
|
-
rowNumber = r !== void 0 && /^[0-9]{1,7}$/.test(r) ? Number(r) : rowNumber + 1;
|
|
1710
|
-
const custom = e.attrs.get("customFormat")?.trim();
|
|
1711
|
-
if (e.attrs.has("s") && (custom === "1" || custom === "true")) rowsWithFormat.add(rowNumber);
|
|
2055
|
+
function newCellStyle(scan, row, col) {
|
|
2056
|
+
return scan.rowsWithFormat.has(row) ? 0 : scan.columnStyles[col] ?? 0;
|
|
2057
|
+
}
|
|
2058
|
+
/** Joins many small strings without keeping one string per piece until the end. */
|
|
2059
|
+
var TextBuilder = class {
|
|
2060
|
+
pieces = [];
|
|
2061
|
+
joined = [];
|
|
2062
|
+
add(text) {
|
|
2063
|
+
this.pieces.push(text);
|
|
2064
|
+
if (this.pieces.length >= 4096) {
|
|
2065
|
+
this.joined.push(this.pieces.join(""));
|
|
2066
|
+
this.pieces.length = 0;
|
|
2067
|
+
}
|
|
2068
|
+
}
|
|
2069
|
+
/** The text as a list of strings of a few thousand pieces each. */
|
|
2070
|
+
finish() {
|
|
2071
|
+
this.joined.push(this.pieces.join(""));
|
|
2072
|
+
this.pieces.length = 0;
|
|
2073
|
+
return this.joined;
|
|
1712
2074
|
}
|
|
1713
|
-
|
|
2075
|
+
};
|
|
2076
|
+
/** Inserts `r="…"` after the qualified name of a start tag given as source text. */
|
|
2077
|
+
function insertRef(tag, qnameLength, ref) {
|
|
2078
|
+
const at = 1 + qnameLength;
|
|
2079
|
+
return `${tag.slice(0, at)} r="${ref}"${tag.slice(at)}`;
|
|
1714
2080
|
}
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
const prefix = prefixOf(
|
|
1718
|
-
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;
|
|
1719
2085
|
if (!sheetData) throw new XlvError("XLV_E_PART_INVALID", `The worksheet part of sheet "${sheet}" has no sheetData element.`, {
|
|
1720
2086
|
location: {
|
|
1721
2087
|
part,
|
|
@@ -1723,45 +2089,7 @@ function editSheetXml(text, part, sheet, planned, limits) {
|
|
|
1723
2089
|
},
|
|
1724
2090
|
hint: "The sheet part is damaged. Re-save the file in a spreadsheet application."
|
|
1725
2091
|
});
|
|
1726
|
-
const rows =
|
|
1727
|
-
let currentRow = 0;
|
|
1728
|
-
let current = null;
|
|
1729
|
-
let lastCol = 0;
|
|
1730
|
-
const invalid = (ref) => {
|
|
1731
|
-
throw new XlvError("XLV_E_INVALID_CELL_REF", `Sheet "${sheet}" contains the invalid reference "${ref.slice(0, 20)}", so it cannot be edited safely.`, {
|
|
1732
|
-
location: {
|
|
1733
|
-
part,
|
|
1734
|
-
sheet
|
|
1735
|
-
},
|
|
1736
|
-
hint: "Open and re-save the file in a spreadsheet application, which drops the damaged cells, then edit the re-saved file.",
|
|
1737
|
-
details: { ref: ref.slice(0, 40) }
|
|
1738
|
-
});
|
|
1739
|
-
};
|
|
1740
|
-
for (const e of elements) if (e.depth === 3) {
|
|
1741
|
-
const r = e.attrs.get("r");
|
|
1742
|
-
const n = r === void 0 ? currentRow + 1 : /^[0-9]{1,7}$/.test(r.trim()) ? Number(r.trim()) : NaN;
|
|
1743
|
-
if (!Number.isInteger(n) || n < 1 || n > 1048576) invalid(r ?? String(n));
|
|
1744
|
-
currentRow = n;
|
|
1745
|
-
current = {
|
|
1746
|
-
number: currentRow,
|
|
1747
|
-
hasR: r !== void 0,
|
|
1748
|
-
element: e,
|
|
1749
|
-
cells: []
|
|
1750
|
-
};
|
|
1751
|
-
rows.push(current);
|
|
1752
|
-
lastCol = 0;
|
|
1753
|
-
} else if (e.depth === 4 && current) {
|
|
1754
|
-
const r = e.attrs.get("r");
|
|
1755
|
-
const address = r === void 0 || r === "" ? null : parseCellRef(r);
|
|
1756
|
-
if (r !== void 0 && r !== "" && address === null) invalid(r);
|
|
1757
|
-
const col = address?.col ?? lastCol + 1;
|
|
1758
|
-
lastCol = col;
|
|
1759
|
-
current.cells.push({
|
|
1760
|
-
col,
|
|
1761
|
-
hasR: r !== void 0,
|
|
1762
|
-
element: e
|
|
1763
|
-
});
|
|
1764
|
-
}
|
|
2092
|
+
const { rows, cells, rowCount } = scan;
|
|
1765
2093
|
const byRow = /* @__PURE__ */ new Map();
|
|
1766
2094
|
for (const p of planned) {
|
|
1767
2095
|
const list = byRow.get(p.row) ?? [];
|
|
@@ -1769,55 +2097,80 @@ function editSheetXml(text, part, sheet, planned, limits) {
|
|
|
1769
2097
|
list.push(p);
|
|
1770
2098
|
}
|
|
1771
2099
|
for (const list of byRow.values()) list.sort((a, b) => a.col - b.col);
|
|
1772
|
-
const forceRefs = rows.some((r) => !r.hasR);
|
|
1773
2100
|
const emitted = /* @__PURE__ */ new Set();
|
|
1774
|
-
const
|
|
2101
|
+
const existingRows = /* @__PURE__ */ new Set();
|
|
2102
|
+
for (let i = 0; i < rowCount; i++) existingRows.add(rows.get(i * ROW_FIELDS$1));
|
|
2103
|
+
const pending = [...byRow.keys()].filter((n) => !existingRows.has(n)).sort((a, b) => a - b);
|
|
2104
|
+
let next = 0;
|
|
1775
2105
|
const p = (xml) => withPrefix(xml, prefix);
|
|
2106
|
+
const out = new TextBuilder();
|
|
1776
2107
|
const newRow = (n) => {
|
|
1777
|
-
|
|
1778
|
-
|
|
2108
|
+
out.add(p(`<row r="${String(n)}">`));
|
|
2109
|
+
for (const c of byRow.get(n) ?? []) if (c.xml !== null) out.add(p(c.xml));
|
|
2110
|
+
out.add(`</${prefix}row>`);
|
|
1779
2111
|
};
|
|
1780
|
-
let
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
const
|
|
1784
|
-
const
|
|
1785
|
-
|
|
1786
|
-
|
|
2112
|
+
for (let i = 0; i < rowCount; i++) {
|
|
2113
|
+
const at = i * ROW_FIELDS$1;
|
|
2114
|
+
const number = rows.get(at);
|
|
2115
|
+
const tagStart = rows.get(at + 1);
|
|
2116
|
+
const tagEnd = rows.get(at + 2);
|
|
2117
|
+
const endStart = rows.get(at + 3);
|
|
2118
|
+
const endEnd = rows.get(at + 4);
|
|
2119
|
+
const flags = rows.get(at + 5);
|
|
2120
|
+
const firstCell = rows.get(at + 6);
|
|
2121
|
+
const cellCount = rows.get(at + 7);
|
|
2122
|
+
const hasR = (flags & 1) === 1;
|
|
2123
|
+
while (next < pending.length && (pending[next] ?? 0) < number) newRow(pending[next++] ?? 0);
|
|
2124
|
+
const edits = byRow.get(number);
|
|
2125
|
+
let needsRefs = scan.missingRowRefs;
|
|
2126
|
+
for (let k = 0; !needsRefs && k < cellCount; k++) needsRefs = (cells.get((firstCell + k) * CELL_FIELDS$1 + 4) & 1) === 0;
|
|
2127
|
+
if (!edits && hasR && !needsRefs) {
|
|
2128
|
+
out.add(text.slice(tagStart, endEnd));
|
|
1787
2129
|
continue;
|
|
1788
2130
|
}
|
|
1789
|
-
let tag = text.slice(
|
|
1790
|
-
if (!
|
|
1791
|
-
|
|
2131
|
+
let tag = text.slice(tagStart, tagEnd);
|
|
2132
|
+
if (!hasR) tag = insertRef(tag, flags >>> 2, String(number));
|
|
2133
|
+
const empty = (flags & 2) === 2;
|
|
2134
|
+
out.add(empty ? openTag(tag) : tag);
|
|
1792
2135
|
const edited = new Set((edits ?? []).map((c) => c.col));
|
|
1793
|
-
const toInsert = emitted.has(
|
|
1794
|
-
emitted.add(
|
|
2136
|
+
const toInsert = emitted.has(number) ? [] : (edits ?? []).filter((c) => c.xml !== null);
|
|
2137
|
+
emitted.add(number);
|
|
1795
2138
|
let k = 0;
|
|
1796
|
-
for (
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
if (
|
|
1801
|
-
|
|
2139
|
+
for (let j = 0; j < cellCount; j++) {
|
|
2140
|
+
const c = (firstCell + j) * CELL_FIELDS$1;
|
|
2141
|
+
const col = cells.get(c);
|
|
2142
|
+
while (k < toInsert.length && (toInsert[k]?.col ?? 0) < col) out.add(p(toInsert[k++]?.xml ?? ""));
|
|
2143
|
+
if (edited.has(col)) continue;
|
|
2144
|
+
const cellFlags = cells.get(c + 4);
|
|
2145
|
+
if ((cellFlags & 1) === 1) out.add(text.slice(cells.get(c + 1), cells.get(c + 3)));
|
|
2146
|
+
else {
|
|
2147
|
+
out.add(insertRef(text.slice(cells.get(c + 1), cells.get(c + 2)), cellFlags >>> 1, formatCellRef(number, col)));
|
|
2148
|
+
out.add(text.slice(cells.get(c + 2), cells.get(c + 3)));
|
|
2149
|
+
}
|
|
1802
2150
|
}
|
|
1803
|
-
while (k < toInsert.length)
|
|
1804
|
-
|
|
2151
|
+
while (k < toInsert.length) out.add(p(toInsert[k++]?.xml ?? ""));
|
|
2152
|
+
out.add(empty ? `</${text.slice(tagStart + 1, tagStart + 1 + (flags >>> 2))}>` : text.slice(endStart, endEnd));
|
|
1805
2153
|
}
|
|
1806
|
-
while (pending.length
|
|
2154
|
+
while (next < pending.length) newRow(pending[next++] ?? 0);
|
|
2155
|
+
const inner = out.finish();
|
|
1807
2156
|
const changes = [];
|
|
1808
2157
|
if (sheetData.start.empty) changes.push([
|
|
1809
2158
|
sheetData.start.start,
|
|
1810
2159
|
sheetData.start.end,
|
|
1811
|
-
|
|
2160
|
+
[
|
|
2161
|
+
openTag(text.slice(sheetData.start.start, sheetData.start.end)),
|
|
2162
|
+
...inner,
|
|
2163
|
+
`</${sheetData.start.qname}>`
|
|
2164
|
+
]
|
|
1812
2165
|
]);
|
|
1813
2166
|
else changes.push([
|
|
1814
2167
|
sheetData.start.end,
|
|
1815
2168
|
sheetData.end.start,
|
|
1816
2169
|
inner
|
|
1817
2170
|
]);
|
|
1818
|
-
const dimension =
|
|
2171
|
+
const dimension = scan.dimension;
|
|
1819
2172
|
if (dimension) {
|
|
1820
|
-
const old = parseRange(dimension.
|
|
2173
|
+
const old = parseRange(dimension.ref ?? "");
|
|
1821
2174
|
const values = planned.filter((c) => c.xml !== null);
|
|
1822
2175
|
if (values.length > 0) {
|
|
1823
2176
|
let minRow = old?.start.row ?? Infinity;
|
|
@@ -1834,11 +2187,19 @@ function editSheetXml(text, part, sheet, planned, limits) {
|
|
|
1834
2187
|
changes.push([
|
|
1835
2188
|
dimension.start.start,
|
|
1836
2189
|
dimension.start.end,
|
|
1837
|
-
setAttr(text.slice(dimension.start.start, dimension.start.end), "ref", ref)
|
|
2190
|
+
[setAttr(text.slice(dimension.start.start, dimension.start.end), "ref", ref)]
|
|
1838
2191
|
]);
|
|
1839
2192
|
}
|
|
1840
2193
|
}
|
|
1841
|
-
|
|
2194
|
+
changes.sort((a, b) => a[0] - b[0]);
|
|
2195
|
+
const pieces = [];
|
|
2196
|
+
let pos = 0;
|
|
2197
|
+
for (const [start, end, replacement] of changes) {
|
|
2198
|
+
pieces.push(text.slice(pos, start), ...replacement);
|
|
2199
|
+
pos = end;
|
|
2200
|
+
}
|
|
2201
|
+
pieces.push(text.slice(pos));
|
|
2202
|
+
return pieces;
|
|
1842
2203
|
}
|
|
1843
2204
|
const CALC_PR_LATER = [
|
|
1844
2205
|
"oleSize",
|
|
@@ -1874,6 +2235,30 @@ function requestFullCalculation(edit, workbookPart) {
|
|
|
1874
2235
|
created
|
|
1875
2236
|
]]));
|
|
1876
2237
|
}
|
|
2238
|
+
/** Ranges with their text as written, found by a cell they contain (A3). */
|
|
2239
|
+
var RangeLookup = class {
|
|
2240
|
+
index = new RangeIndex();
|
|
2241
|
+
refs = /* @__PURE__ */ new Map();
|
|
2242
|
+
add(range, ref) {
|
|
2243
|
+
this.index.add(range);
|
|
2244
|
+
this.refs.set(range, ref);
|
|
2245
|
+
}
|
|
2246
|
+
/** A range that contains the cell and that `accept` (when given) accepts, with its text. */
|
|
2247
|
+
at(row, col, accept) {
|
|
2248
|
+
const cell = {
|
|
2249
|
+
row,
|
|
2250
|
+
col
|
|
2251
|
+
};
|
|
2252
|
+
const range = this.index.find({
|
|
2253
|
+
start: cell,
|
|
2254
|
+
end: cell
|
|
2255
|
+
}, accept);
|
|
2256
|
+
return range === void 0 ? void 0 : {
|
|
2257
|
+
ref: this.refs.get(range) ?? "",
|
|
2258
|
+
range
|
|
2259
|
+
};
|
|
2260
|
+
}
|
|
2261
|
+
};
|
|
1877
2262
|
/**
|
|
1878
2263
|
* editCells writes plain formulas only, and does not change cells whose formula covers other
|
|
1879
2264
|
* cells: an array formula's range (P1 §18.17.6.3) or a shared formula master with dependents
|
|
@@ -1893,17 +2278,14 @@ function checkFormulaGroups(sheet, ref, at, value, cache) {
|
|
|
1893
2278
|
if (value !== null && value.kind === "formula" && (value.group !== void 0 || value.master !== void 0)) throw refuse(`editCells writes plain formulas only; the value for ${ref} is an array or shared formula.`, "Write { formula: \"…\" } for each cell, or build the sheet with createWorkbook to write array and shared formulas.");
|
|
1894
2279
|
let arrays = cache.get(sheet.name);
|
|
1895
2280
|
if (arrays === void 0) {
|
|
1896
|
-
arrays =
|
|
2281
|
+
arrays = new RangeLookup();
|
|
1897
2282
|
for (const c of sheet.cells()) {
|
|
1898
2283
|
const range = c.formula?.kind === "array" && c.formula.ref !== null ? parseRange(c.formula.ref) : null;
|
|
1899
|
-
if (range !== null) arrays.
|
|
1900
|
-
ref: c.formula.ref,
|
|
1901
|
-
range
|
|
1902
|
-
});
|
|
2284
|
+
if (range !== null) arrays.add(range, c.formula.ref);
|
|
1903
2285
|
}
|
|
1904
2286
|
cache.set(sheet.name, arrays);
|
|
1905
2287
|
}
|
|
1906
|
-
const array = arrays.
|
|
2288
|
+
const array = arrays.at(at.row, at.col);
|
|
1907
2289
|
if (array !== void 0) throw refuse(`${ref} belongs to the array formula ${array.ref}, which editCells cannot change cell by cell.`, `Change the array formula in a spreadsheet application, or rebuild the sheet with createWorkbook; cells outside ${array.ref} can be edited.`);
|
|
1908
2290
|
const f = sheet.cell(ref)?.formula;
|
|
1909
2291
|
const range = f?.kind === "shared" && f.ref !== null ? parseRange(f.ref) : null;
|
|
@@ -2064,6 +2446,7 @@ function planEdits(workbook, edits) {
|
|
|
2064
2446
|
const scratchSheet = scratch.addSheet("Edit");
|
|
2065
2447
|
const plans = /* @__PURE__ */ new Map();
|
|
2066
2448
|
const arrayRanges = /* @__PURE__ */ new Map();
|
|
2449
|
+
const byName = new Map(workbook.sheets.map((sheet) => [sheet.name, sheet]));
|
|
2067
2450
|
edits.forEach((raw, i) => {
|
|
2068
2451
|
const o = readOptions(raw, [
|
|
2069
2452
|
"sheet",
|
|
@@ -2076,7 +2459,7 @@ function planEdits(workbook, edits) {
|
|
|
2076
2459
|
hint: "Give each edit a sheet, for example { sheet: \"Data\", cell: \"B2\", value: 5 }.",
|
|
2077
2460
|
details: { option: "sheet" }
|
|
2078
2461
|
});
|
|
2079
|
-
const sheet = workbook.getSheet(sheetName);
|
|
2462
|
+
const sheet = byName.get(sheetName) ?? workbook.getSheet(sheetName);
|
|
2080
2463
|
if (sheet.kind !== "worksheet" || sheet.part === null) throw new XlvError("XLV_E_INVALID_INPUT", `Sheet "${sheet.name}" is a ${sheet.kind} without editable cells.`, {
|
|
2081
2464
|
location: { sheet: sheet.name },
|
|
2082
2465
|
hint: "Edit cells of worksheets only."
|
|
@@ -2154,17 +2537,31 @@ function applyEdits$1(bytes, workbook, plans, limits, appended = null) {
|
|
|
2154
2537
|
const part = plan.sheet.part ?? "";
|
|
2155
2538
|
const planned = [];
|
|
2156
2539
|
const text = edit.text(part);
|
|
2157
|
-
const
|
|
2540
|
+
const sheetScan = text === void 0 ? null : scanSheet(text, part, plan.sheet.name, limits, styles);
|
|
2158
2541
|
let styled = null;
|
|
2159
|
-
const styledCell = (ref) =>
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2542
|
+
const styledCell = (ref) => {
|
|
2543
|
+
if (styled === null) {
|
|
2544
|
+
const wanted = /* @__PURE__ */ new Set();
|
|
2545
|
+
for (const r of plan.cells.keys()) if (plan.sheet.cell(r) === void 0) wanted.add(r);
|
|
2546
|
+
styled = /* @__PURE__ */ new Map();
|
|
2547
|
+
if (wanted.size > 0) {
|
|
2548
|
+
for (const cell of plan.sheet.styledCells()) if (wanted.has(cell.ref)) styled.set(cell.ref, cell);
|
|
2549
|
+
}
|
|
2550
|
+
}
|
|
2551
|
+
return styled.get(ref);
|
|
2552
|
+
};
|
|
2553
|
+
let merges = null;
|
|
2164
2554
|
for (const [ref, c] of plan.cells) {
|
|
2165
|
-
if (c.value !== null) {
|
|
2166
|
-
|
|
2167
|
-
|
|
2555
|
+
if (c.value !== null && plan.sheet.merges.length > 0) {
|
|
2556
|
+
if (merges === null) {
|
|
2557
|
+
merges = new RangeLookup();
|
|
2558
|
+
for (const m of plan.sheet.merges) {
|
|
2559
|
+
const range = parseRange(m.trim().toUpperCase());
|
|
2560
|
+
if (range !== null) merges.add(range, m);
|
|
2561
|
+
}
|
|
2562
|
+
}
|
|
2563
|
+
const merge = merges.at(c.row, c.col, (r) => c.row !== r.start.row || c.col !== r.start.col);
|
|
2564
|
+
if (merge !== void 0) {
|
|
2168
2565
|
const topLeft = formatCellRef(merge.range.start.row, merge.range.start.col);
|
|
2169
2566
|
warnings.add("XLV_W_MERGED_CELL_WRITE", `A value was written into a merged range outside its top-left cell (first: ${ref} in ${merge.ref}). Spreadsheet applications show only the top-left cell (${topLeft}), so the value will not be visible. Write it to the top-left cell, or unmerge the range.`, {
|
|
2170
2567
|
part,
|
|
@@ -2184,7 +2581,7 @@ function applyEdits$1(bytes, workbook, plans, limits, appended = null) {
|
|
|
2184
2581
|
});
|
|
2185
2582
|
continue;
|
|
2186
2583
|
}
|
|
2187
|
-
const base = existing?.styleIndex ??
|
|
2584
|
+
const base = existing?.styleIndex ?? (sheetScan === null ? 0 : newCellStyle(sheetScan, c.row, c.col));
|
|
2188
2585
|
let patch = c.style ?? {};
|
|
2189
2586
|
const existingIsDate = existing?.numFmt != null && isDateFormat(existing.numFmt);
|
|
2190
2587
|
if (c.date !== null && patch.numFmt === void 0 && !existingIsDate) patch = {
|
|
@@ -2199,13 +2596,21 @@ function applyEdits$1(bytes, workbook, plans, limits, appended = null) {
|
|
|
2199
2596
|
xml: newCellXml(ref, c.value, xf, sst)
|
|
2200
2597
|
});
|
|
2201
2598
|
}
|
|
2202
|
-
if (text === void 0) continue;
|
|
2599
|
+
if (text === void 0 || sheetScan === null) continue;
|
|
2203
2600
|
syncTables(edit, plan, appended !== null && appended.sheet === plan.sheet.name ? appended : null);
|
|
2204
|
-
edit.write(part, editSheetXml(text, part, plan.sheet.name,
|
|
2601
|
+
edit.write(part, editSheetXml(text, part, plan.sheet.name, sheetScan, planned));
|
|
2205
2602
|
}
|
|
2206
2603
|
sst.finish();
|
|
2207
2604
|
styles.finish();
|
|
2208
|
-
|
|
2605
|
+
let hasFormulas = formulasChanged;
|
|
2606
|
+
for (const sheet of workbook.sheets) {
|
|
2607
|
+
if (hasFormulas) break;
|
|
2608
|
+
for (const cell of sheet.cells()) if (cell.formula !== null) {
|
|
2609
|
+
hasFormulas = true;
|
|
2610
|
+
break;
|
|
2611
|
+
}
|
|
2612
|
+
}
|
|
2613
|
+
if (hasFormulas) {
|
|
2209
2614
|
requestFullCalculation(edit, workbookPart);
|
|
2210
2615
|
warnings.add("XLV_W_FORMULAS_NOT_RECALCULATED", "The workbook contains formulas. xlvane does not recalculate them: cached results may be out of date until the file is opened in a spreadsheet application, which is asked to recalculate on load.", { part: workbookPart });
|
|
2211
2616
|
}
|
|
@@ -2248,19 +2653,25 @@ function applyEdits$1(bytes, workbook, plans, limits, appended = null) {
|
|
|
2248
2653
|
function editCells(input, edits, options) {
|
|
2249
2654
|
const bytes = inputBytes(input);
|
|
2250
2655
|
if (!Array.isArray(edits)) throw new XlvError("XLV_E_INVALID_INPUT", "editCells expects an array of edits.", { hint: "Pass an array such as [{ sheet: \"Data\", cell: \"A1\", value: 1 }]." });
|
|
2251
|
-
const
|
|
2252
|
-
|
|
2656
|
+
const { limits, writable } = editLimits(readOptions(options, ["limits"], "editCells options").get("limits"));
|
|
2657
|
+
checkWrites(edits.length, writable, `${String(edits.length)} edits`);
|
|
2658
|
+
const copies = edits.map((e) => typeof e === "object" && e !== null && !Array.isArray(e) ? { ...e } : e);
|
|
2253
2659
|
const workbook = readWorkbook(bytes, { limits });
|
|
2254
|
-
|
|
2660
|
+
checkCellCount(workbook, newCells(workbook, copies), limits.maxCells);
|
|
2661
|
+
return applyEdits$1(bytes, workbook, planEdits(workbook, copies), limits);
|
|
2255
2662
|
}
|
|
2256
2663
|
/**
|
|
2257
2664
|
* Appends rows below the last used row of a sheet (the rows of merged ranges count as used) in
|
|
2258
2665
|
* an existing XLSX file, editing the file surgically like {@link editCells}. Object rows are
|
|
2259
2666
|
* placed under the matching header columns (unknown keys throw `XLV_E_INVALID_ROWS` with
|
|
2260
|
-
* suggestions)
|
|
2261
|
-
*
|
|
2667
|
+
* suggestions). Array rows start at the first used column, or, when the new rows extend a table
|
|
2668
|
+
* (one that reaches the last used row), at that table's first column; when several such tables
|
|
2669
|
+
* start in different columns the call is refused with `XLV_E_INVALID_ARGUMENT` (pass object rows
|
|
2670
|
+
* instead). A table whose last row is the sheet's last used row grows to include the new rows
|
|
2671
|
+
* (its range and filter); if that table has a totals row the call is refused with
|
|
2262
2672
|
* `XLV_E_NOT_SUPPORTED` and a hint to insert the rows above the totals row with `insertRows`.
|
|
2263
|
-
* Calculated columns are not filled in the new rows.
|
|
2673
|
+
* Calculated columns are not filled in the new rows. One call writes at most 1 000 000 cells
|
|
2674
|
+
* unless `limits.maxCells` is given (see `EditOptions`).
|
|
2264
2675
|
*
|
|
2265
2676
|
* @example
|
|
2266
2677
|
* ```ts
|
|
@@ -2271,15 +2682,26 @@ function editCells(input, edits, options) {
|
|
|
2271
2682
|
* { Customer: 'Alice', Total: 120.5 },
|
|
2272
2683
|
* { Customer: 'Bob', Total: 80 },
|
|
2273
2684
|
* ]);
|
|
2685
|
+
* // Array rows that extend a table start at the table's first column.
|
|
2686
|
+
* const { bytes: more } = appendRows(updated, 'Orders', [['Carol', 64]]);
|
|
2687
|
+
* console.log(more.length);
|
|
2274
2688
|
* ```
|
|
2275
2689
|
*/
|
|
2276
2690
|
function appendRows(input, sheet, rows, options) {
|
|
2277
2691
|
const bytes = inputBytes(input);
|
|
2278
2692
|
const o = readOptions(options, ["limits", "header"], "appendRows options");
|
|
2279
|
-
const limits =
|
|
2693
|
+
const { limits, writable } = editLimits(o.get("limits"));
|
|
2280
2694
|
const workbook = readWorkbook(bytes, { limits });
|
|
2281
2695
|
const target = pickSheet(workbook, sheet);
|
|
2282
2696
|
if (!Array.isArray(rows)) throw new XlvError("XLV_E_INVALID_ROWS", "appendRows expects an array of rows.", { hint: "Pass an array of objects or arrays." });
|
|
2697
|
+
let values = 0;
|
|
2698
|
+
for (const row of rows) if (Array.isArray(row)) {
|
|
2699
|
+
for (const value of row) if (value !== null && value !== void 0) values++;
|
|
2700
|
+
} else if (typeof row === "object" && row !== null) {
|
|
2701
|
+
for (const key of Object.keys(row)) if (row[key] !== null && row[key] !== void 0) values++;
|
|
2702
|
+
}
|
|
2703
|
+
checkWrites(values, writable, `Appending ${String(rows.length)} rows`, target.name);
|
|
2704
|
+
checkCellCount(workbook, /* @__PURE__ */ new Map([[target, values]]), limits.maxCells);
|
|
2283
2705
|
let lastRow = 0;
|
|
2284
2706
|
let firstCol = Infinity;
|
|
2285
2707
|
for (const cell of target.cells()) {
|
|
@@ -2299,6 +2721,27 @@ function appendRows(input, sheet, rows, options) {
|
|
|
2299
2721
|
value: MAX_ROWS
|
|
2300
2722
|
}
|
|
2301
2723
|
});
|
|
2724
|
+
let arrayCol = firstCol;
|
|
2725
|
+
if (rows.some((row) => Array.isArray(row))) {
|
|
2726
|
+
const joined = target.tables.map((table) => ({
|
|
2727
|
+
table,
|
|
2728
|
+
range: parseRange(table.ref.trim().toUpperCase())
|
|
2729
|
+
})).filter((t) => t.range !== null && lastRow > 0 && t.range.end.row >= lastRow && t.range.start.row <= lastRow);
|
|
2730
|
+
const starts = new Set(joined.map((t) => t.range.start.col));
|
|
2731
|
+
if (starts.size > 1) {
|
|
2732
|
+
const names = joined.map((t) => `"${t.table.displayName}" (${t.table.ref})`).join(", ");
|
|
2733
|
+
throw new XlvError("XLV_E_INVALID_ARGUMENT", `Tables ${names} of sheet "${target.name}" reach the last used row ${String(lastRow)} and start in different columns, so it is not clear in which column array rows start.`, {
|
|
2734
|
+
location: { sheet: target.name },
|
|
2735
|
+
hint: "Pass the rows as objects keyed by header text, for example [{ Customer: \"Dan\", Total: 42 }], so that each value goes under its column (use { header: n } to name the header row).",
|
|
2736
|
+
details: {
|
|
2737
|
+
tables: joined.map((t) => t.table.displayName),
|
|
2738
|
+
row: lastRow
|
|
2739
|
+
}
|
|
2740
|
+
});
|
|
2741
|
+
}
|
|
2742
|
+
const [start] = starts;
|
|
2743
|
+
if (start !== void 0) arrayCol = start;
|
|
2744
|
+
}
|
|
2302
2745
|
let columns = null;
|
|
2303
2746
|
const edits = [];
|
|
2304
2747
|
rows.forEach((row, i) => {
|
|
@@ -2307,7 +2750,7 @@ function appendRows(input, sheet, rows, options) {
|
|
|
2307
2750
|
row.forEach((value, k) => {
|
|
2308
2751
|
if (value !== null && value !== void 0) edits.push({
|
|
2309
2752
|
sheet: target.name,
|
|
2310
|
-
cell: formatCellRef(r,
|
|
2753
|
+
cell: formatCellRef(r, arrayCol + k),
|
|
2311
2754
|
value
|
|
2312
2755
|
});
|
|
2313
2756
|
});
|
|
@@ -2604,6 +3047,13 @@ function adjustRange(sqref, change) {
|
|
|
2604
3047
|
}
|
|
2605
3048
|
// src/rows.ts
|
|
2606
3049
|
/** Relationship types of a worksheet whose parts do not depend on row positions or are handled here. */
|
|
3050
|
+
/**
|
|
3051
|
+
* Media type of pivot cache definition parts, as Microsoft Excel writes it in
|
|
3052
|
+
* `[Content_Types].xml` (Apache POI corpus files poi/ExcelPivotTableSample.xlsx, poi/54436.xlsx,
|
|
3053
|
+
* poi/DataTableCities.xlsx; B6). The relationship from the workbook ends in
|
|
3054
|
+
* `/pivotCacheDefinition` in the same files.
|
|
3055
|
+
*/
|
|
3056
|
+
const PIVOT_CACHE_DEFINITION_CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCacheDefinition+xml";
|
|
2607
3057
|
const SAFE_SHEET_RELATIONSHIPS = /* @__PURE__ */ new Set([
|
|
2608
3058
|
"hyperlink",
|
|
2609
3059
|
"comments",
|
|
@@ -2645,16 +3095,30 @@ function moveRow(n, c) {
|
|
|
2645
3095
|
if (n < c.at) return n;
|
|
2646
3096
|
return n > end ? n + c.count : null;
|
|
2647
3097
|
}
|
|
3098
|
+
/** Decodes the predefined entities and character references of XML text; a reference to a code point that does not exist stays as written. */
|
|
2648
3099
|
function decodeEntities(s) {
|
|
2649
|
-
return s.replace(/&(#x[0-9a-fA-F]+|#[0-9]+|amp|lt|gt|quot|apos);/g, (
|
|
3100
|
+
return s.replace(/&(#x[0-9a-fA-F]+|#[0-9]+|amp|lt|gt|quot|apos);/g, (m, e) => {
|
|
2650
3101
|
if (e === "amp") return "&";
|
|
2651
3102
|
if (e === "lt") return "<";
|
|
2652
3103
|
if (e === "gt") return ">";
|
|
2653
3104
|
if (e === "quot") return "\"";
|
|
2654
3105
|
if (e === "apos") return "'";
|
|
2655
|
-
|
|
3106
|
+
const cp = e.startsWith("#x") ? parseInt(e.slice(2), 16) : parseInt(e.slice(1), 10);
|
|
3107
|
+
return cp <= 1114111 ? String.fromCodePoint(cp) : m;
|
|
2656
3108
|
});
|
|
2657
3109
|
}
|
|
3110
|
+
/**
|
|
3111
|
+
* Whether the XML text of a part may refer to a sheet: its name, ignoring case, appears in the
|
|
3112
|
+
* text after entity and character references are decoded, as written or in the quoted form of a
|
|
3113
|
+
* formula, where each "'" is doubled (docs/facts/formulas.md F2-11). Searching the raw XML missed names such
|
|
3114
|
+
* as "R&D" (written "R&D") and "O'Brien" (written "'O''Brien'!", B6). It may find the name
|
|
3115
|
+
* where it is not a reference, which only refuses more.
|
|
3116
|
+
*/
|
|
3117
|
+
function mentionsSheet(xml, sheet) {
|
|
3118
|
+
const text = decodeEntities(xml).toLowerCase();
|
|
3119
|
+
const name = sheet.toLowerCase();
|
|
3120
|
+
return text.includes(name) || name.includes("'") && text.includes(name.replace(/'/g, "''"));
|
|
3121
|
+
}
|
|
2658
3122
|
function removeAttr(tag, name) {
|
|
2659
3123
|
return tag.replace(new RegExp(`\\s${name}\\s*=\\s*("[^"]*"|'[^']*')`), "");
|
|
2660
3124
|
}
|
|
@@ -2716,7 +3180,6 @@ function applyEdits(text, edits) {
|
|
|
2716
3180
|
return out.join("");
|
|
2717
3181
|
}
|
|
2718
3182
|
const KEEP_DEPTH3 = /* @__PURE__ */ new Set([
|
|
2719
|
-
"row",
|
|
2720
3183
|
"mergeCell",
|
|
2721
3184
|
"hyperlink",
|
|
2722
3185
|
"cfRule",
|
|
@@ -2725,9 +3188,159 @@ const KEEP_DEPTH3 = /* @__PURE__ */ new Set([
|
|
|
2725
3188
|
"ignoredError",
|
|
2726
3189
|
"protectedRange"
|
|
2727
3190
|
]);
|
|
3191
|
+
/** Integers per row of {@link WorksheetScan.rows}: number, start tag start and end, element end, flags, first cell, cell count. */
|
|
3192
|
+
const ROW_FIELDS = 7;
|
|
3193
|
+
/** Integers per cell of {@link WorksheetScan.cells}: column, start tag start and end, element end, flags. */
|
|
3194
|
+
const CELL_FIELDS = 5;
|
|
3195
|
+
/** Integers per formula of {@link WorksheetScan.formulas}: row, column, start tag start and end, end tag start and end, kind. */
|
|
3196
|
+
const FORMULA_FIELDS = 7;
|
|
3197
|
+
/** Row and cell flags. */
|
|
3198
|
+
const HAS_R = 1;
|
|
3199
|
+
const EMPTY = 2;
|
|
3200
|
+
/** Formula kinds (`f@t`); any other value is handled as a normal formula. */
|
|
3201
|
+
const NORMAL = 0;
|
|
3202
|
+
const SHARED = 1;
|
|
3203
|
+
const ARRAY = 2;
|
|
3204
|
+
const DATA_TABLE = 3;
|
|
3205
|
+
function scanWorksheet(text, part, limits) {
|
|
3206
|
+
const keep = (local, depth, parent, ns) => depth === 2 || depth === 3 && (KEEP_DEPTH3.has(local) || local === "ext" && parent === "extLst") || depth === 4 && (local === "formula" && parent === "cfRule" || (local === "formula1" || local === "formula2") && parent === "dataValidation") || depth === 5 && local === "cfvo" && CFVO_PARENTS.has(parent) || ns === "http://schemas.microsoft.com/office/spreadsheetml/2009/9/main" && (depth === 4 && (local === "conditionalFormattings" || local === "dataValidations") || depth === 5 && (local === "conditionalFormatting" || local === "dataValidation") || depth === 6 && (local === "cfRule" || local === "id" && parent === "ext")) || ns === "http://schemas.microsoft.com/office/excel/2006/main" && (depth === 6 && local === "sqref" || depth >= 7 && local === "f");
|
|
3207
|
+
const elements = [];
|
|
3208
|
+
/** Per open element: the kept Element, or a code for a recorded row (1), cell (2) or formula (3), or null. */
|
|
3209
|
+
const open = [];
|
|
3210
|
+
const names = [];
|
|
3211
|
+
let root = false;
|
|
3212
|
+
const rows = new IntList();
|
|
3213
|
+
const cells = new IntList();
|
|
3214
|
+
const formulas = new IntList();
|
|
3215
|
+
let rowCount = 0;
|
|
3216
|
+
let formulaCount = 0;
|
|
3217
|
+
let currentRow = 0;
|
|
3218
|
+
let lastCol = 0;
|
|
3219
|
+
let hasRow = false;
|
|
3220
|
+
let hasCell = false;
|
|
3221
|
+
let missingRefs = false;
|
|
3222
|
+
let invalidRef = null;
|
|
3223
|
+
parseXml(text, {
|
|
3224
|
+
start(ns, local, attrs, span) {
|
|
3225
|
+
const depth = names.length + 1;
|
|
3226
|
+
const parent = names[names.length - 1] ?? "";
|
|
3227
|
+
names.push(local);
|
|
3228
|
+
if (depth === 1) {
|
|
3229
|
+
root = true;
|
|
3230
|
+
open.push(null);
|
|
3231
|
+
return;
|
|
3232
|
+
}
|
|
3233
|
+
if (depth === 3 && local === "row") {
|
|
3234
|
+
const r = attrs.get("r");
|
|
3235
|
+
const n = r === void 0 ? currentRow + 1 : /^[0-9]{1,7}$/.test(r.trim()) ? Number(r.trim()) : NaN;
|
|
3236
|
+
if (!Number.isInteger(n) || n < 1 || n > 1048576) invalidRef ??= r ?? String(n);
|
|
3237
|
+
currentRow = Number.isInteger(n) ? n : currentRow;
|
|
3238
|
+
lastCol = 0;
|
|
3239
|
+
if (r === void 0) missingRefs = true;
|
|
3240
|
+
rows.push(n);
|
|
3241
|
+
rows.push(span.start);
|
|
3242
|
+
rows.push(span.end);
|
|
3243
|
+
rows.push(span.end);
|
|
3244
|
+
rows.push((r === void 0 ? 0 : HAS_R) | (span.empty ? EMPTY : 0));
|
|
3245
|
+
rows.push(cells.length / CELL_FIELDS);
|
|
3246
|
+
rows.push(0);
|
|
3247
|
+
rowCount++;
|
|
3248
|
+
hasRow = true;
|
|
3249
|
+
hasCell = false;
|
|
3250
|
+
open.push(1);
|
|
3251
|
+
return;
|
|
3252
|
+
}
|
|
3253
|
+
if (depth === 4 && local === "c" && parent === "row" && hasRow) {
|
|
3254
|
+
const r = attrs.get("r");
|
|
3255
|
+
const address = r === void 0 || r === "" ? null : parseCellRef(r);
|
|
3256
|
+
if (r !== void 0 && r !== "" && address === null) invalidRef ??= r;
|
|
3257
|
+
const col = address?.col ?? lastCol + 1;
|
|
3258
|
+
lastCol = col;
|
|
3259
|
+
const hasR = r !== void 0 && r !== "";
|
|
3260
|
+
if (!hasR) missingRefs = true;
|
|
3261
|
+
cells.push(col);
|
|
3262
|
+
cells.push(span.start);
|
|
3263
|
+
cells.push(span.end);
|
|
3264
|
+
cells.push(span.end);
|
|
3265
|
+
cells.push((hasR ? HAS_R : 0) | (span.empty ? EMPTY : 0));
|
|
3266
|
+
const count = (rowCount - 1) * ROW_FIELDS + 6;
|
|
3267
|
+
rows.set(count, rows.get(count) + 1);
|
|
3268
|
+
hasCell = true;
|
|
3269
|
+
open.push(2);
|
|
3270
|
+
return;
|
|
3271
|
+
}
|
|
3272
|
+
if (depth === 5 && local === "f" && parent === "c" && hasCell) {
|
|
3273
|
+
const t = attrs.get("t");
|
|
3274
|
+
formulas.push(currentRow);
|
|
3275
|
+
formulas.push(lastCol);
|
|
3276
|
+
formulas.push(span.start);
|
|
3277
|
+
formulas.push(span.end);
|
|
3278
|
+
formulas.push(span.end);
|
|
3279
|
+
formulas.push(span.end);
|
|
3280
|
+
formulas.push(t === "shared" ? SHARED : t === "array" ? ARRAY : t === "dataTable" ? DATA_TABLE : NORMAL);
|
|
3281
|
+
formulaCount++;
|
|
3282
|
+
open.push(3);
|
|
3283
|
+
return;
|
|
3284
|
+
}
|
|
3285
|
+
if (keep(local, depth, parent, ns)) {
|
|
3286
|
+
const element = {
|
|
3287
|
+
local,
|
|
3288
|
+
ns,
|
|
3289
|
+
depth,
|
|
3290
|
+
attrs,
|
|
3291
|
+
start: span,
|
|
3292
|
+
end: span
|
|
3293
|
+
};
|
|
3294
|
+
elements.push(element);
|
|
3295
|
+
open.push(element);
|
|
3296
|
+
} else open.push(null);
|
|
3297
|
+
},
|
|
3298
|
+
end(_ns, _local, span) {
|
|
3299
|
+
names.pop();
|
|
3300
|
+
const what = open.pop();
|
|
3301
|
+
if (what === 1) rows.set((rowCount - 1) * ROW_FIELDS + 3, span.end);
|
|
3302
|
+
else if (what === 2) cells.set(cells.length - 2, span.end);
|
|
3303
|
+
else if (what === 3) {
|
|
3304
|
+
formulas.set(formulas.length - 3, span.start);
|
|
3305
|
+
formulas.set(formulas.length - 2, span.end);
|
|
3306
|
+
} else if (what !== null && what !== void 0) what.end = span;
|
|
3307
|
+
},
|
|
3308
|
+
text() {}
|
|
3309
|
+
}, {
|
|
3310
|
+
part,
|
|
3311
|
+
limits
|
|
3312
|
+
});
|
|
3313
|
+
if (!root) throw new XlvError("XLV_E_PART_INVALID", "The part has no root element.", {
|
|
3314
|
+
location: { part },
|
|
3315
|
+
hint: "Re-save the file in a spreadsheet application."
|
|
3316
|
+
});
|
|
3317
|
+
return {
|
|
3318
|
+
elements,
|
|
3319
|
+
rows,
|
|
3320
|
+
rowCount,
|
|
3321
|
+
cells,
|
|
3322
|
+
formulas,
|
|
3323
|
+
formulaCount,
|
|
3324
|
+
missingRefs,
|
|
3325
|
+
invalidRef
|
|
3326
|
+
};
|
|
3327
|
+
}
|
|
3328
|
+
/**
|
|
3329
|
+
* The attributes of a start tag given as source text (already checked by the XML parser), with
|
|
3330
|
+
* references decoded. Used for the few tags whose attributes are needed again, instead of keeping
|
|
3331
|
+
* the attributes of every cell and formula.
|
|
3332
|
+
*/
|
|
3333
|
+
function tagAttributes(tag) {
|
|
3334
|
+
const attrs = /* @__PURE__ */ new Map();
|
|
3335
|
+
const pattern = /\s+([^\s=/>]+)\s*=\s*(?:"([^"]*)"|'([^']*)')/y;
|
|
3336
|
+
pattern.lastIndex = tag.search(/[\s/>]/);
|
|
3337
|
+
for (let m = pattern.exec(tag); m !== null; m = pattern.exec(tag)) attrs.set(m[1] ?? "", decodeEntities((m[2] ?? m[3] ?? "").replace(/[\t\n\r]/g, " ")));
|
|
3338
|
+
return attrs;
|
|
3339
|
+
}
|
|
2728
3340
|
/** Rewrites one worksheet part: references everywhere, and row positions on the changed sheet. */
|
|
2729
|
-
function rewriteSheet(text, part, sheet, c, target, style, limits) {
|
|
2730
|
-
const
|
|
3341
|
+
function rewriteSheet(text, part, sheet, c, target, style, limits, workbookCells) {
|
|
3342
|
+
const scanned = scanWorksheet(text, part, limits);
|
|
3343
|
+
const { elements, rows, rowCount, cells, formulas, formulaCount } = scanned;
|
|
2731
3344
|
const edits = [];
|
|
2732
3345
|
const tagOf = (e) => text.slice(e.start.start, e.start.end);
|
|
2733
3346
|
const replaceTag = (e, tag) => {
|
|
@@ -2775,59 +3388,21 @@ function rewriteSheet(text, part, sheet, c, target, style, limits) {
|
|
|
2775
3388
|
if (ext.depth !== 3 || ext.local !== "ext" || knownExts.has(ext)) continue;
|
|
2776
3389
|
const body = text.slice(ext.start.start, ext.end.end);
|
|
2777
3390
|
if (target && /sqref|<xm:f|sparkline|slicer/i.test(body)) throw notSupported(`Sheet "${sheet.name}" has extended content (such as sparklines or slicers) that refers to cells, which xlvane cannot move with its rows yet.`, where(), { element: "extLst" });
|
|
2778
|
-
if (!target && /<xm:f|sparkline/i.test(body) && body
|
|
3391
|
+
if (!target && /<xm:f|sparkline/i.test(body) && mentionsSheet(body, c.sheet)) throw notSupported(`Sheet "${sheet.name}" has extended content (such as sparklines) that may refer to sheet "${c.sheet}", which xlvane cannot rewrite yet.`, where(), { element: "extLst" });
|
|
2779
3392
|
}
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
let currentRow = 0;
|
|
2783
|
-
let lastCol = 0;
|
|
2784
|
-
let lastCell = null;
|
|
2785
|
-
const formulas = [];
|
|
2786
|
-
const invalidRef = (ref) => {
|
|
3393
|
+
if (scanned.invalidRef !== null) {
|
|
3394
|
+
const ref = scanned.invalidRef;
|
|
2787
3395
|
throw new XlvError("XLV_E_INVALID_CELL_REF", `Sheet "${sheet.name}" contains the invalid reference "${ref.slice(0, 20)}", so its rows cannot be moved safely.`, {
|
|
2788
3396
|
location: where(),
|
|
2789
3397
|
hint: "Open and re-save the file in a spreadsheet application, which drops the damaged cells, then try again.",
|
|
2790
3398
|
details: { ref: ref.slice(0, 40) }
|
|
2791
3399
|
});
|
|
2792
|
-
}
|
|
2793
|
-
|
|
3400
|
+
}
|
|
3401
|
+
const hasFormulas = formulaCount > 0;
|
|
2794
3402
|
let extension = null;
|
|
2795
3403
|
for (const e of elements) {
|
|
2796
3404
|
if (e.depth === 3 && e.local === "ext") extension = knownExts.has(e) ? e : null;
|
|
2797
|
-
if (e.depth ===
|
|
2798
|
-
const r = e.attrs.get("r");
|
|
2799
|
-
const n = r === void 0 ? currentRow + 1 : /^[0-9]{1,7}$/.test(r.trim()) ? Number(r.trim()) : NaN;
|
|
2800
|
-
if (!Number.isInteger(n) || n < 1 || n > 1048576) invalidRef(r ?? String(n));
|
|
2801
|
-
currentRow = n;
|
|
2802
|
-
current = {
|
|
2803
|
-
number: n,
|
|
2804
|
-
hasR: r !== void 0,
|
|
2805
|
-
element: e,
|
|
2806
|
-
cells: []
|
|
2807
|
-
};
|
|
2808
|
-
rows.push(current);
|
|
2809
|
-
lastCol = 0;
|
|
2810
|
-
} else if (e.depth === 4 && e.local === "c" && current) {
|
|
2811
|
-
const r = e.attrs.get("r");
|
|
2812
|
-
const address = r === void 0 || r === "" ? null : parseCellRef(r);
|
|
2813
|
-
if (r !== void 0 && r !== "" && address === null) invalidRef(r);
|
|
2814
|
-
const col = address?.col ?? lastCol + 1;
|
|
2815
|
-
lastCol = col;
|
|
2816
|
-
lastCell = {
|
|
2817
|
-
col,
|
|
2818
|
-
hasR: r !== void 0 && r !== "",
|
|
2819
|
-
element: e,
|
|
2820
|
-
f: null
|
|
2821
|
-
};
|
|
2822
|
-
current.cells.push(lastCell);
|
|
2823
|
-
} else if (e.depth === 5 && e.local === "f" && lastCell && current) {
|
|
2824
|
-
lastCell.f = e;
|
|
2825
|
-
hasFormulas = true;
|
|
2826
|
-
formulas.push({
|
|
2827
|
-
cell: lastCell,
|
|
2828
|
-
row: current
|
|
2829
|
-
});
|
|
2830
|
-
} else if (e.depth === 4 && (e.local === "formula" || e.local === "formula1" || e.local === "formula2")) {
|
|
3405
|
+
if (e.depth === 4 && (e.local === "formula" || e.local === "formula1" || e.local === "formula2")) {
|
|
2831
3406
|
const raw = content(text, e);
|
|
2832
3407
|
const out = adjustText(raw, c, sheet.name);
|
|
2833
3408
|
if (out === null) throw notSupported(`A ${e.local === "formula" ? "conditional format" : "data validation"} formula of sheet "${sheet.name}" cannot be adjusted safely: "${decodeEntities(raw).slice(0, 60)}".`, where(), { formula: decodeEntities(raw).slice(0, 80) });
|
|
@@ -2870,12 +3445,14 @@ function rewriteSheet(text, part, sheet, c, target, style, limits) {
|
|
|
2870
3445
|
}
|
|
2871
3446
|
}
|
|
2872
3447
|
const masters = /* @__PURE__ */ new Map();
|
|
2873
|
-
for (
|
|
2874
|
-
const
|
|
2875
|
-
if (
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
3448
|
+
for (let i = 0; i < formulaCount; i++) {
|
|
3449
|
+
const at = i * FORMULA_FIELDS;
|
|
3450
|
+
if (formulas.get(at + 6) !== SHARED) continue;
|
|
3451
|
+
const attrs = tagAttributes(text.slice(formulas.get(at + 2), formulas.get(at + 3)));
|
|
3452
|
+
if (attrs.get("t") === "shared" && attrs.has("ref") && attrs.has("si")) masters.set(attrs.get("si") ?? "", {
|
|
3453
|
+
row: formulas.get(at),
|
|
3454
|
+
col: formulas.get(at + 1),
|
|
3455
|
+
text: text.slice(formulas.get(at + 3), formulas.get(at + 4))
|
|
2879
3456
|
});
|
|
2880
3457
|
}
|
|
2881
3458
|
const expand = /* @__PURE__ */ new Set();
|
|
@@ -2884,65 +3461,77 @@ function rewriteSheet(text, part, sheet, c, target, style, limits) {
|
|
|
2884
3461
|
if (target || out === null || out !== m.text) expand.add(si);
|
|
2885
3462
|
}
|
|
2886
3463
|
const deletedRow = (n) => c.count < 0 && moveRow(n, c) === null;
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
const
|
|
2891
|
-
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);
|
|
2892
3476
|
const failed = (formula) => {
|
|
2893
3477
|
throw notSupported(`The formula in ${ref} of sheet "${sheet.name}" cannot be adjusted safely: "${formula.slice(0, 60)}".`, where(ref), { formula: formula.slice(0, 80) });
|
|
2894
3478
|
};
|
|
2895
|
-
|
|
2896
|
-
|
|
3479
|
+
const tag = text.slice(tagStart, tagEnd);
|
|
3480
|
+
if (kind === SHARED) {
|
|
3481
|
+
const si = tagAttributes(tag).get("si") ?? "";
|
|
2897
3482
|
const master = masters.get(si);
|
|
2898
|
-
if (master === void 0 || !expand.has(si))
|
|
3483
|
+
if (master === void 0 || !expand.has(si)) return [];
|
|
2899
3484
|
const source = decodeEntities(master.text);
|
|
2900
|
-
const own = shiftFormula(source, row
|
|
3485
|
+
const own = shiftFormula(source, row - master.row, col - master.col);
|
|
2901
3486
|
if (own === null) failed(source);
|
|
2902
3487
|
const adjusted = adjustFormula(own, refChange(c, sheet.name));
|
|
2903
3488
|
if (adjusted === null) failed(own);
|
|
2904
|
-
let
|
|
2905
|
-
if (
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
3489
|
+
let plain = removeAttr(removeAttr(removeAttr(tag, "t"), "ref"), "si");
|
|
3490
|
+
if (plain.endsWith("/>")) plain = `${plain.slice(0, -2).trimEnd()}>`;
|
|
3491
|
+
const qname = /^<([^\s/>]+)/.exec(tag)?.[1] ?? "f";
|
|
3492
|
+
return [[
|
|
3493
|
+
tagStart,
|
|
3494
|
+
end,
|
|
3495
|
+
`${plain}${escapeText(adjusted)}</${qname}>`
|
|
3496
|
+
]];
|
|
2912
3497
|
}
|
|
2913
|
-
if (kind ===
|
|
3498
|
+
if (kind === DATA_TABLE) {
|
|
2914
3499
|
if (target) throw notSupported(`Cell ${ref} of sheet "${sheet.name}" is part of a data table, which xlvane cannot move.`, where(ref));
|
|
2915
|
-
|
|
3500
|
+
return [];
|
|
2916
3501
|
}
|
|
2917
|
-
const raw =
|
|
3502
|
+
const raw = text.slice(tagEnd, endStart);
|
|
2918
3503
|
const out = adjustText(raw, c, sheet.name);
|
|
2919
3504
|
if (out === null) failed(decodeEntities(raw));
|
|
2920
|
-
|
|
2921
|
-
if (kind ===
|
|
2922
|
-
const
|
|
3505
|
+
const changes = [];
|
|
3506
|
+
if (kind === ARRAY && target) {
|
|
3507
|
+
const arrayRef = tagAttributes(tag).get("ref") ?? "";
|
|
3508
|
+
const range = parseRange(arrayRef);
|
|
2923
3509
|
if (range !== null) {
|
|
2924
|
-
const
|
|
2925
|
-
if (c.count > 0 ? c.at > range.start.row && c.at <= range.end.row : !(range.start.row >= c.at && range.end.row <=
|
|
2926
|
-
const moved = adjustRange(
|
|
2927
|
-
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
|
+
]);
|
|
2928
3518
|
}
|
|
2929
3519
|
}
|
|
2930
|
-
if (
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
f.end.start,
|
|
3520
|
+
if (out !== raw) changes.push([
|
|
3521
|
+
tagEnd,
|
|
3522
|
+
endStart,
|
|
2934
3523
|
out
|
|
2935
3524
|
]);
|
|
2936
|
-
|
|
2937
|
-
if (!target) return {
|
|
2938
|
-
text: edits.length === 0 ? text : applyEdits(text, edits),
|
|
2939
|
-
hasFormulas
|
|
3525
|
+
return changes;
|
|
2940
3526
|
};
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
3527
|
+
let force = false;
|
|
3528
|
+
if (target) {
|
|
3529
|
+
force = scanned.missingRefs;
|
|
3530
|
+
for (let i = 0; i < rowCount && c.count > 0; i++) {
|
|
3531
|
+
const at = i * ROW_FIELDS;
|
|
3532
|
+
const number = rows.get(at);
|
|
3533
|
+
if (moveRow(number, c) !== null) continue;
|
|
3534
|
+
for (let j = 0; j < rows.get(at + 6); j++) if ((cells.get((rows.get(at + 5) + j) * CELL_FIELDS + 4) & EMPTY) === 0) throw new XlvError("XLV_E_LIMIT_EXCEEDED", `Inserting ${String(c.count)} rows would push row ${String(number)} of sheet "${sheet.name}", which holds data, past the last row of the sheet.`, {
|
|
2946
3535
|
location: where(),
|
|
2947
3536
|
hint: `A sheet has ${String(MAX_ROWS)} rows. Delete the rows at the end of the sheet first, or insert fewer rows.`,
|
|
2948
3537
|
details: {
|
|
@@ -2950,156 +3539,252 @@ function rewriteSheet(text, part, sheet, c, target, style, limits) {
|
|
|
2950
3539
|
value: MAX_ROWS
|
|
2951
3540
|
}
|
|
2952
3541
|
});
|
|
2953
|
-
removeElement(row.element);
|
|
2954
|
-
continue;
|
|
2955
3542
|
}
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
"
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
const
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
3543
|
+
const findRow = (test) => {
|
|
3544
|
+
for (let i = 0; i < rowCount; i++) if (test(rows.get(i * ROW_FIELDS))) return i;
|
|
3545
|
+
return -1;
|
|
3546
|
+
};
|
|
3547
|
+
if (c.count > 0 && style !== "none") {
|
|
3548
|
+
const source = findRow((n) => n === (style === "above" ? c.at - 1 : c.at));
|
|
3549
|
+
if (source >= 0) {
|
|
3550
|
+
const at = source * ROW_FIELDS;
|
|
3551
|
+
const rowTag = text.slice(rows.get(at + 1), rows.get(at + 2));
|
|
3552
|
+
const rowAttrs = tagAttributes(rowTag);
|
|
3553
|
+
const rowQname = /^<([^\s/>]+)/.exec(rowTag)?.[1] ?? "row";
|
|
3554
|
+
const prefix = rowQname.slice(0, rowQname.length - 3);
|
|
3555
|
+
const keep = [
|
|
3556
|
+
"s",
|
|
3557
|
+
"customFormat",
|
|
3558
|
+
"ht",
|
|
3559
|
+
"customHeight"
|
|
3560
|
+
].filter((a) => rowAttrs.has(a));
|
|
3561
|
+
const formats = [];
|
|
3562
|
+
for (let j = 0; j < rows.get(at + 6); j++) {
|
|
3563
|
+
const cell = (rows.get(at + 5) + j) * CELL_FIELDS;
|
|
3564
|
+
const s = tagAttributes(text.slice(cells.get(cell + 1), cells.get(cell + 2))).get("s") ?? "0";
|
|
3565
|
+
if (s !== "0") formats.push({
|
|
3566
|
+
col: cells.get(cell),
|
|
3567
|
+
s
|
|
3568
|
+
});
|
|
3569
|
+
}
|
|
3570
|
+
const added = formats.length * c.count;
|
|
3571
|
+
if (workbookCells + added > limits.maxCells) throw new XlvError("XLV_E_LIMIT_EXCEEDED", `Inserting ${String(c.count)} rows formatted like row ${String(rows.get(at))} of sheet "${sheet.name}" would add ${String(added)} formatted cells, and the workbook would hold more cells than the limit of ${String(limits.maxCells)}.`, {
|
|
3572
|
+
location: where(),
|
|
3573
|
+
hint: `Cells that hold only a style count too. Insert fewer rows, use { style: 'none' } and format the rows afterwards, or, if there is enough memory, raise the limit, for example { limits: { maxCells: ${String(workbookCells + added)} } }.`,
|
|
3574
|
+
details: {
|
|
3575
|
+
limit: "maxCells",
|
|
3576
|
+
value: limits.maxCells
|
|
3577
|
+
}
|
|
3578
|
+
});
|
|
3579
|
+
const attrs = keep.map((a) => ` ${a}="${escapeAttr(rowAttrs.get(a) ?? "")}"`).join("");
|
|
3580
|
+
const created = new TextBuilder();
|
|
3581
|
+
for (let i = 0; i < c.count; i++) {
|
|
3582
|
+
const n = c.at + i;
|
|
3583
|
+
if (formats.length === 0) {
|
|
3584
|
+
created.add(`<${prefix}row r="${String(n)}"${attrs}/>`);
|
|
3585
|
+
continue;
|
|
3586
|
+
}
|
|
3587
|
+
created.add(`<${prefix}row r="${String(n)}"${attrs}>`);
|
|
3588
|
+
for (const f of formats) created.add(`<${prefix}c r="${formatCellRef(n, f.col)}" s="${escapeAttr(f.s)}"/>`);
|
|
3589
|
+
created.add(`</${prefix}row>`);
|
|
3590
|
+
}
|
|
3591
|
+
const createdText = created.finish().join("");
|
|
3592
|
+
const next = findRow((n) => n >= c.at);
|
|
3593
|
+
if (next >= 0) edits.push([
|
|
3594
|
+
rows.get(next * ROW_FIELDS + 1),
|
|
3595
|
+
rows.get(next * ROW_FIELDS + 1),
|
|
3596
|
+
createdText
|
|
2994
3597
|
]);
|
|
3598
|
+
else {
|
|
3599
|
+
const sheetData = elements.find((e) => e.depth === 2 && e.local === "sheetData");
|
|
3600
|
+
if (sheetData && !sheetData.start.empty) edits.push([
|
|
3601
|
+
sheetData.end.start,
|
|
3602
|
+
sheetData.end.start,
|
|
3603
|
+
createdText
|
|
3604
|
+
]);
|
|
3605
|
+
else if (sheetData) edits.push([
|
|
3606
|
+
sheetData.start.start,
|
|
3607
|
+
sheetData.start.end,
|
|
3608
|
+
`${tagOf(sheetData).slice(0, -2).trimEnd()}>${createdText}</${sheetData.start.qname}>`
|
|
3609
|
+
]);
|
|
3610
|
+
}
|
|
2995
3611
|
}
|
|
2996
3612
|
}
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
else
|
|
3020
|
-
|
|
3613
|
+
const removedIds = /* @__PURE__ */ new Set();
|
|
3614
|
+
const linkIds = /* @__PURE__ */ new Map();
|
|
3615
|
+
let format = null;
|
|
3616
|
+
for (const e of elements) if (e.depth === 2) format = e.local === "conditionalFormatting" ? e : null;
|
|
3617
|
+
else if (format !== null && e.depth === 6 && e.ns === "http://schemas.microsoft.com/office/spreadsheetml/2009/9/main" && e.local === "id") {
|
|
3618
|
+
const list = linkIds.get(format) ?? [];
|
|
3619
|
+
list.push(content(text, e).trim().toLowerCase());
|
|
3620
|
+
linkIds.set(format, list);
|
|
3621
|
+
}
|
|
3622
|
+
const containers = /* @__PURE__ */ new Map();
|
|
3623
|
+
let parent2 = null;
|
|
3624
|
+
for (const e of elements) {
|
|
3625
|
+
if (e.depth === 2) {
|
|
3626
|
+
parent2 = e;
|
|
3627
|
+
if (e.local === "dimension" || e.local === "autoFilter") {
|
|
3628
|
+
const ref = e.attrs.get("ref");
|
|
3629
|
+
if (ref === void 0) continue;
|
|
3630
|
+
const out = adjustRange(ref, refChange(c, sheet.name));
|
|
3631
|
+
if (out === null || out === ref) continue;
|
|
3632
|
+
if (e.local === "dimension") replaceTag(e, setAttr(tagOf(e), "ref", out === "" ? "A1" : out.split(" ")[0] ?? "A1"));
|
|
3633
|
+
else if (out === "") removeElement(e);
|
|
3634
|
+
else replaceTag(e, setAttr(tagOf(e), "ref", out));
|
|
3635
|
+
} else if (e.local === "conditionalFormatting") {
|
|
3636
|
+
const sqref = e.attrs.get("sqref") ?? "";
|
|
3637
|
+
const out = adjustRange(sqref, refChange(c, sheet.name));
|
|
3638
|
+
if (out === null) throw notSupported(`A conditional format of sheet "${sheet.name}" has a range xlvane cannot adjust: "${sqref.slice(0, 60)}".`, where());
|
|
3639
|
+
if (out === "") {
|
|
3640
|
+
removeElement(e);
|
|
3641
|
+
for (const id of linkIds.get(e) ?? []) removedIds.add(id);
|
|
3642
|
+
} else if (out !== sqref) replaceTag(e, setAttr(tagOf(e), "sqref", out));
|
|
3643
|
+
}
|
|
3644
|
+
continue;
|
|
3645
|
+
}
|
|
3646
|
+
if (e.depth !== 3 || parent2 === null) continue;
|
|
3647
|
+
const container = parent2;
|
|
3648
|
+
const count = containers.get(container) ?? {
|
|
3649
|
+
kept: 0,
|
|
3650
|
+
total: 0
|
|
3651
|
+
};
|
|
3652
|
+
containers.set(container, count);
|
|
3653
|
+
if (e.local === "mergeCell") {
|
|
3654
|
+
count.total++;
|
|
3655
|
+
const ref = e.attrs.get("ref") ?? "";
|
|
3656
|
+
const range = parseRange(ref);
|
|
3657
|
+
const out = range === null ? null : c.count < 0 && deletedRow(range.start.row) ? "" : adjustRange(ref, refChange(c, sheet.name));
|
|
3658
|
+
if (out === null) {
|
|
3659
|
+
count.kept++;
|
|
3660
|
+
continue;
|
|
3661
|
+
}
|
|
3662
|
+
const moved = out === "" ? null : parseRange(out);
|
|
3663
|
+
if (moved === null || moved.start.row === moved.end.row && moved.start.col === moved.end.col) {
|
|
3664
|
+
removeElement(e);
|
|
3665
|
+
continue;
|
|
3666
|
+
}
|
|
3667
|
+
count.kept++;
|
|
3668
|
+
if (out !== ref) replaceTag(e, setAttr(tagOf(e), "ref", out));
|
|
3669
|
+
} else if (e.local === "dataValidation" || e.local === "ignoredError" || e.local === "protectedRange") {
|
|
3670
|
+
count.total++;
|
|
3021
3671
|
const sqref = e.attrs.get("sqref") ?? "";
|
|
3022
3672
|
const out = adjustRange(sqref, refChange(c, sheet.name));
|
|
3023
|
-
if (out === null) throw notSupported(`
|
|
3673
|
+
if (out === null) throw notSupported(`Sheet "${sheet.name}" has a ${e.local} range xlvane cannot adjust: "${sqref.slice(0, 60)}".`, where());
|
|
3024
3674
|
if (out === "") {
|
|
3025
3675
|
removeElement(e);
|
|
3026
|
-
|
|
3027
|
-
}
|
|
3028
|
-
}
|
|
3029
|
-
continue;
|
|
3030
|
-
}
|
|
3031
|
-
if (e.depth !== 3 || parent2 === null) continue;
|
|
3032
|
-
const container = parent2;
|
|
3033
|
-
const count = containers.get(container) ?? {
|
|
3034
|
-
kept: 0,
|
|
3035
|
-
total: 0
|
|
3036
|
-
};
|
|
3037
|
-
containers.set(container, count);
|
|
3038
|
-
if (e.local === "mergeCell") {
|
|
3039
|
-
count.total++;
|
|
3040
|
-
const ref = e.attrs.get("ref") ?? "";
|
|
3041
|
-
const range = parseRange(ref);
|
|
3042
|
-
const out = range === null ? null : c.count < 0 && deletedRow(range.start.row) ? "" : adjustRange(ref, refChange(c, sheet.name));
|
|
3043
|
-
if (out === null) {
|
|
3676
|
+
continue;
|
|
3677
|
+
}
|
|
3044
3678
|
count.kept++;
|
|
3045
|
-
|
|
3046
|
-
}
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
continue;
|
|
3062
|
-
}
|
|
3063
|
-
count.kept++;
|
|
3064
|
-
if (out !== sqref) replaceTag(e, setAttr(tagOf(e), "sqref", out));
|
|
3065
|
-
} else if (e.local === "hyperlink") {
|
|
3066
|
-
count.total++;
|
|
3067
|
-
if (adjustRange(e.attrs.get("ref") ?? "", refChange(c, sheet.name)) !== "") count.kept++;
|
|
3068
|
-
} else if (e.local === "brk" && container.local === "rowBreaks") {
|
|
3069
|
-
count.total++;
|
|
3070
|
-
const id = Number(e.attrs.get("id") ?? "NaN");
|
|
3071
|
-
if (!Number.isInteger(id)) {
|
|
3679
|
+
if (out !== sqref) replaceTag(e, setAttr(tagOf(e), "sqref", out));
|
|
3680
|
+
} else if (e.local === "hyperlink") {
|
|
3681
|
+
count.total++;
|
|
3682
|
+
if (adjustRange(e.attrs.get("ref") ?? "", refChange(c, sheet.name)) !== "") count.kept++;
|
|
3683
|
+
} else if (e.local === "brk" && container.local === "rowBreaks") {
|
|
3684
|
+
count.total++;
|
|
3685
|
+
const id = Number(e.attrs.get("id") ?? "NaN");
|
|
3686
|
+
if (!Number.isInteger(id)) {
|
|
3687
|
+
count.kept++;
|
|
3688
|
+
continue;
|
|
3689
|
+
}
|
|
3690
|
+
const n = moveRow(id, c);
|
|
3691
|
+
if (n === null) {
|
|
3692
|
+
removeElement(e);
|
|
3693
|
+
continue;
|
|
3694
|
+
}
|
|
3072
3695
|
count.kept++;
|
|
3073
|
-
|
|
3696
|
+
if (n !== id) replaceTag(e, setAttr(tagOf(e), "id", String(n)));
|
|
3074
3697
|
}
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3698
|
+
}
|
|
3699
|
+
for (const [container, n] of containers) {
|
|
3700
|
+
if (n.total === 0 || n.kept === n.total) continue;
|
|
3701
|
+
const counted = [
|
|
3702
|
+
"mergeCells",
|
|
3703
|
+
"dataValidations",
|
|
3704
|
+
"rowBreaks"
|
|
3705
|
+
].includes(container.local);
|
|
3706
|
+
if (n.kept === 0) removeElement(container);
|
|
3707
|
+
else if (counted) {
|
|
3708
|
+
let tag = tagOf(container);
|
|
3709
|
+
if (container.attrs.has("count")) tag = setAttr(tag, "count", String(n.kept));
|
|
3710
|
+
if (container.local === "rowBreaks" && container.attrs.has("manualBreakCount")) tag = setAttr(tag, "manualBreakCount", String(n.kept));
|
|
3711
|
+
if (tag !== tagOf(container)) replaceTag(container, tag);
|
|
3079
3712
|
}
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
if (
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3713
|
+
}
|
|
3714
|
+
rewriteExtensionRanges(text, elements, knownExts, c, sheet, removedIds, edits, where);
|
|
3715
|
+
} else if (edits.length === 0) {
|
|
3716
|
+
let changed = false;
|
|
3717
|
+
for (let i = 0; i < formulaCount && !changed; i++) changed = formulaChanges(i).length > 0;
|
|
3718
|
+
if (!changed) return {
|
|
3719
|
+
text,
|
|
3720
|
+
hasFormulas
|
|
3721
|
+
};
|
|
3722
|
+
}
|
|
3723
|
+
const general = [...edits].sort((a, b) => a[0] - b[0] || Number(b[0] === b[1]) - Number(a[0] === a[1]) || b[1] - a[1]);
|
|
3724
|
+
const out = new TextBuilder();
|
|
3725
|
+
let pos = 0;
|
|
3726
|
+
let g = 0;
|
|
3727
|
+
const flush = (limit) => {
|
|
3728
|
+
while (g < general.length) {
|
|
3729
|
+
const [start, end, replacement] = general[g];
|
|
3730
|
+
if (!(start < limit || start === limit && end === start)) break;
|
|
3731
|
+
g++;
|
|
3732
|
+
if (start < pos) continue;
|
|
3733
|
+
out.add(text.slice(pos, start));
|
|
3734
|
+
out.add(replacement);
|
|
3735
|
+
pos = end;
|
|
3736
|
+
}
|
|
3737
|
+
};
|
|
3738
|
+
const replace = (start, end, replacement) => {
|
|
3739
|
+
flush(start);
|
|
3740
|
+
if (start < pos) return;
|
|
3741
|
+
out.add(text.slice(pos, start));
|
|
3742
|
+
out.add(replacement);
|
|
3743
|
+
pos = end;
|
|
3744
|
+
};
|
|
3745
|
+
let f = 0;
|
|
3746
|
+
/** Formulas that start before `limit`: adjusted, or skipped when `skip`. */
|
|
3747
|
+
const formulasBefore = (limit, skip) => {
|
|
3748
|
+
for (; f < formulaCount && formulas.get(f * FORMULA_FIELDS + 2) < limit; f++) {
|
|
3749
|
+
if (skip) continue;
|
|
3750
|
+
for (const [start, end, replacement] of formulaChanges(f)) replace(start, end, replacement);
|
|
3751
|
+
}
|
|
3752
|
+
};
|
|
3753
|
+
for (let i = 0; i < rowCount; i++) {
|
|
3754
|
+
const at = i * ROW_FIELDS;
|
|
3755
|
+
const number = rows.get(at);
|
|
3756
|
+
const rowStart = rows.get(at + 1);
|
|
3757
|
+
const rowTagEnd = rows.get(at + 2);
|
|
3758
|
+
const rowEnd = rows.get(at + 3);
|
|
3759
|
+
formulasBefore(rowStart, false);
|
|
3760
|
+
const n = target ? moveRow(number, c) : number;
|
|
3761
|
+
if (n === null) {
|
|
3762
|
+
replace(rowStart, rowEnd, "");
|
|
3763
|
+
formulasBefore(rowEnd, true);
|
|
3764
|
+
continue;
|
|
3765
|
+
}
|
|
3766
|
+
const moved = target && (n !== number || force);
|
|
3767
|
+
if (moved) replace(rowStart, rowTagEnd, setAttr(text.slice(rowStart, rowTagEnd), "r", String(n)));
|
|
3768
|
+
const first = rows.get(at + 5);
|
|
3769
|
+
for (let j = 0; j < rows.get(at + 6); j++) {
|
|
3770
|
+
const cell = (first + j) * CELL_FIELDS;
|
|
3771
|
+
const cellStart = cells.get(cell + 1);
|
|
3772
|
+
formulasBefore(cellStart, false);
|
|
3773
|
+
if (moved) replace(cellStart, cells.get(cell + 2), setAttr(text.slice(cellStart, cells.get(cell + 2)), "r", formatCellRef(n, cells.get(cell))));
|
|
3774
|
+
formulasBefore(cells.get(cell + 3), false);
|
|
3775
|
+
}
|
|
3776
|
+
formulasBefore(rowEnd, false);
|
|
3777
|
+
}
|
|
3778
|
+
formulasBefore(text.length + 1, false);
|
|
3779
|
+
flush(text.length + 1);
|
|
3780
|
+
out.add(text.slice(pos));
|
|
3781
|
+
const pieces = out.finish();
|
|
3782
|
+
if (target && /sortState/.test(text)) return {
|
|
3783
|
+
text: rewriteSortStates(pieces.join(""), part, sheet, c, limits),
|
|
3784
|
+
hasFormulas
|
|
3785
|
+
};
|
|
3101
3786
|
return {
|
|
3102
|
-
text:
|
|
3787
|
+
text: pieces,
|
|
3103
3788
|
hasFormulas
|
|
3104
3789
|
};
|
|
3105
3790
|
}
|
|
@@ -3596,22 +4281,55 @@ function changeRows(input, sheetName, at, count, style, limits) {
|
|
|
3596
4281
|
sheet: sheet.name
|
|
3597
4282
|
}, { type: rel.type });
|
|
3598
4283
|
}
|
|
3599
|
-
const
|
|
4284
|
+
const charts = /* @__PURE__ */ new Set();
|
|
4285
|
+
const drawings = /* @__PURE__ */ new Map();
|
|
4286
|
+
const unmatched = (source, r) => notSupported(`The part ${source} has a relationship to ${r.target}, which is not a part of the package (its name may be written differently in the archive), so xlvane cannot check whether a chart there refers to sheet "${sheet.name}".`, {
|
|
4287
|
+
part: source,
|
|
4288
|
+
sheet: sheet.name
|
|
4289
|
+
}, {
|
|
4290
|
+
type: r.type,
|
|
4291
|
+
target: r.target
|
|
4292
|
+
});
|
|
4293
|
+
for (const name of pkg.partNames) {
|
|
4294
|
+
const type = pkg.contentType(name);
|
|
4295
|
+
if (type === "application/vnd.openxmlformats-officedocument.drawingml.chart+xml" || type === PIVOT_CACHE_DEFINITION_CONTENT_TYPE) charts.add(partKey(name));
|
|
4296
|
+
else if (type === "application/vnd.openxmlformats-officedocument.drawing+xml") drawings.set(partKey(name), name);
|
|
4297
|
+
}
|
|
4298
|
+
for (const s of workbook.sheets) {
|
|
4299
|
+
if (s.part === null) continue;
|
|
4300
|
+
for (const r of pkg.relationships(s.part)) {
|
|
4301
|
+
if (r.external || classifyRelationship(r.type)?.name !== "drawing") continue;
|
|
4302
|
+
if (!pkg.hasPart(r.target)) throw unmatched(s.part, r);
|
|
4303
|
+
drawings.set(partKey(r.target), r.target);
|
|
4304
|
+
}
|
|
4305
|
+
}
|
|
4306
|
+
for (const name of drawings.values()) for (const r of pkg.relationships(name)) {
|
|
4307
|
+
if (r.external || classifyRelationship(r.type)?.name !== "chart") continue;
|
|
4308
|
+
if (!pkg.hasPart(r.target)) throw unmatched(name, r);
|
|
4309
|
+
charts.add(partKey(r.target));
|
|
4310
|
+
}
|
|
4311
|
+
for (const r of pkg.relationships(workbookPart)) {
|
|
4312
|
+
if (r.external || classifyRelationship(r.type)?.name !== "pivotCacheDefinition") continue;
|
|
4313
|
+
if (!pkg.hasPart(r.target)) throw unmatched(workbookPart, r);
|
|
4314
|
+
charts.add(partKey(r.target));
|
|
4315
|
+
}
|
|
3600
4316
|
for (const name of pkg.partNames) {
|
|
3601
|
-
if (!/\/charts\/[^/]+\.xml$|pivotCacheDefinition[^/]*\.xml$/i.test(name)) continue;
|
|
4317
|
+
if (!charts.has(partKey(name)) && !/\/charts\/[^/]+\.xml$|pivotCacheDefinition[^/]*\.xml$/i.test(name)) continue;
|
|
3602
4318
|
const text = edit.text(name);
|
|
3603
|
-
if (text !== void 0 && text.
|
|
4319
|
+
if (text !== void 0 && mentionsSheet(text, sheet.name)) throw notSupported(`The part ${name} (a chart or pivot table) may refer to sheet "${sheet.name}", and xlvane cannot rewrite it yet.`, {
|
|
3604
4320
|
part: name,
|
|
3605
4321
|
sheet: sheet.name
|
|
3606
4322
|
});
|
|
3607
4323
|
}
|
|
3608
4324
|
let hasFormulas = false;
|
|
4325
|
+
let workbookCells = 0;
|
|
4326
|
+
for (const s of workbook.sheets) workbookCells += s.cellCount + styledCellCount(s);
|
|
3609
4327
|
for (const s of workbook.sheets) {
|
|
3610
4328
|
if (s.kind !== "worksheet" || s.part === null) continue;
|
|
3611
4329
|
const text = edit.text(s.part);
|
|
3612
4330
|
if (text === void 0) continue;
|
|
3613
4331
|
const target = s.part === sheet.part;
|
|
3614
|
-
const out = rewriteSheet(text, s.part, s, c, target, style, limits);
|
|
4332
|
+
const out = rewriteSheet(text, s.part, s, c, target, style, limits, workbookCells);
|
|
3615
4333
|
hasFormulas ||= out.hasFormulas;
|
|
3616
4334
|
if (out.text !== text) edit.write(s.part, out.text);
|
|
3617
4335
|
rewriteTables(edit, pkg, s.part, s, c, target);
|
|
@@ -3651,7 +4369,9 @@ function changeRows(input, sheetName, at, count, style, limits) {
|
|
|
3651
4369
|
* custom sheet views, other extended content that refers to cells (sparklines, slicers) and
|
|
3652
4370
|
* other related parts xlvane does not know; charts or pivot tables anywhere in the workbook that
|
|
3653
4371
|
* may refer to the sheet; data tables; an array formula the change would split; and formulas it
|
|
3654
|
-
* cannot rewrite safely (docs/facts/ref-adjust.md).
|
|
4372
|
+
* cannot rewrite safely (docs/facts/ref-adjust.md). Rows formatted with `style` add a formatted
|
|
4373
|
+
* cell for each formatted cell of the row they copy; when the workbook would then hold more cells
|
|
4374
|
+
* than `limits.maxCells`, the call throws `XLV_E_LIMIT_EXCEEDED` before building them.
|
|
3655
4375
|
*
|
|
3656
4376
|
* @example
|
|
3657
4377
|
* ```ts
|