@particle-academy/holy-sheet 2.3.0 → 2.4.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 +35 -0
- package/dist/index.cjs +889 -28
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +328 -27
- package/dist/index.d.ts +328 -27
- package/dist/index.js +887 -29
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -131,16 +131,16 @@ function allStringish(sample) {
|
|
|
131
131
|
|
|
132
132
|
// src/helpers/array-builder.ts
|
|
133
133
|
var ArrayBuilder = {
|
|
134
|
-
build(rows, headers = null,
|
|
134
|
+
build(rows, headers = null, sheetName2 = "Sheet 1", options = {}) {
|
|
135
135
|
let hdrs = headers;
|
|
136
136
|
let data = rows;
|
|
137
137
|
if (hdrs === null) {
|
|
138
|
-
if (rows.length === 0) return wrap([], [],
|
|
138
|
+
if (rows.length === 0) return wrap([], [], sheetName2, options);
|
|
139
139
|
hdrs = (rows[0] ?? []).map((v) => String(v));
|
|
140
140
|
data = rows.slice(1);
|
|
141
141
|
}
|
|
142
142
|
const columns = inferColumns(data, hdrs, options);
|
|
143
|
-
return wrap(columns, data,
|
|
143
|
+
return wrap(columns, data, sheetName2, options);
|
|
144
144
|
}
|
|
145
145
|
};
|
|
146
146
|
function inferColumns(rows, headers, options) {
|
|
@@ -149,8 +149,8 @@ function inferColumns(rows, headers, options) {
|
|
|
149
149
|
return Inference.detect(columnValues, String(headerName), options);
|
|
150
150
|
});
|
|
151
151
|
}
|
|
152
|
-
function wrap(columns, rows,
|
|
153
|
-
const sheet = { name:
|
|
152
|
+
function wrap(columns, rows, sheetName2, options) {
|
|
153
|
+
const sheet = { name: sheetName2, columns, rows };
|
|
154
154
|
if (options.theme !== void 0) sheet.theme = options.theme;
|
|
155
155
|
if (options.totals && typeof options.totals === "object") sheet.totals = options.totals;
|
|
156
156
|
if (options.frozenRows !== void 0) sheet.frozenRows = Math.trunc(Number(options.frozenRows));
|
|
@@ -163,16 +163,16 @@ var CsvBuilder = {
|
|
|
163
163
|
build(csv, options = {}) {
|
|
164
164
|
const delimiter = options.delimiter ?? ",";
|
|
165
165
|
const enclosure = options.enclosure ?? '"';
|
|
166
|
-
const
|
|
166
|
+
const sheetName2 = options.sheetName ?? "Sheet 1";
|
|
167
167
|
const rows = parseRows(csv, delimiter, enclosure);
|
|
168
168
|
if (rows.length === 0) {
|
|
169
|
-
return { sheets: [{ name:
|
|
169
|
+
return { sheets: [{ name: sheetName2, columns: [], rows: [] }] };
|
|
170
170
|
}
|
|
171
171
|
const headers = rows[0].map((v) => String(v));
|
|
172
172
|
const dataRows = rows.slice(1).map(
|
|
173
173
|
(row) => row.map((cell) => isNumericString(cell) ? numericStringToNumber(cell) : cell)
|
|
174
174
|
);
|
|
175
|
-
return ArrayBuilder.build(dataRows, headers,
|
|
175
|
+
return ArrayBuilder.build(dataRows, headers, sheetName2, options);
|
|
176
176
|
}
|
|
177
177
|
};
|
|
178
178
|
function parseRows(csv, delimiter, quote) {
|
|
@@ -719,6 +719,7 @@ var ERR_REF = "#REF!";
|
|
|
719
719
|
var ERR_NAME = "#NAME?";
|
|
720
720
|
var ERR_DIV0 = "#DIV/0!";
|
|
721
721
|
var ERR_CIRC = "#CIRC!";
|
|
722
|
+
var SHEET_QUALIFIER = "('(?:[^']|'')+'|[A-Za-z_][A-Za-z0-9_.]*)!";
|
|
722
723
|
var LinterError = class extends Error {
|
|
723
724
|
constructor(errorCode) {
|
|
724
725
|
super(errorCode);
|
|
@@ -729,9 +730,16 @@ var FormulaLinter = class {
|
|
|
729
730
|
constructor() {
|
|
730
731
|
this.index = /* @__PURE__ */ new Map();
|
|
731
732
|
this.cache = /* @__PURE__ */ new Map();
|
|
733
|
+
/**
|
|
734
|
+
* Every sheet name, keyed by its lower-cased form. Excel matches sheet names
|
|
735
|
+
* without regard to case, and a reference to a sheet that is not here is
|
|
736
|
+
* `#REF!` rather than a range of blanks.
|
|
737
|
+
*/
|
|
738
|
+
this.sheetNames = /* @__PURE__ */ new Map();
|
|
732
739
|
}
|
|
733
740
|
lint(schema) {
|
|
734
741
|
const workbook = new Normalizer().normalize(schema);
|
|
742
|
+
this.sheetNames = new Map(workbook.sheets.map((sheet) => [sheet.name.toLowerCase(), sheet.name]));
|
|
735
743
|
this.index = this.buildIndex(workbook);
|
|
736
744
|
this.cache = /* @__PURE__ */ new Map();
|
|
737
745
|
const issues = [];
|
|
@@ -807,6 +815,24 @@ var FormulaLinter = class {
|
|
|
807
815
|
tokens.push({ type: "STRING", value });
|
|
808
816
|
continue;
|
|
809
817
|
}
|
|
818
|
+
if (ch === "'") {
|
|
819
|
+
const start = i;
|
|
820
|
+
i++;
|
|
821
|
+
for (; ; ) {
|
|
822
|
+
if (i >= len) throw new LinterError(ERR_NAME);
|
|
823
|
+
if (src[i] === "'") {
|
|
824
|
+
if (i + 1 < len && src[i + 1] === "'") {
|
|
825
|
+
i += 2;
|
|
826
|
+
continue;
|
|
827
|
+
}
|
|
828
|
+
i++;
|
|
829
|
+
break;
|
|
830
|
+
}
|
|
831
|
+
i++;
|
|
832
|
+
}
|
|
833
|
+
tokens.push({ type: "SHEET", value: src.slice(start, i) });
|
|
834
|
+
continue;
|
|
835
|
+
}
|
|
810
836
|
if (isAlpha(ch) || ch === "_" || ch === "$") {
|
|
811
837
|
const start = i;
|
|
812
838
|
while (i < len && (isAlnum(src[i]) || src[i] === "_" || src[i] === "$" || src[i] === ".")) i++;
|
|
@@ -924,16 +950,18 @@ var FormulaLinter = class {
|
|
|
924
950
|
c.pos++;
|
|
925
951
|
return tok.value;
|
|
926
952
|
}
|
|
927
|
-
if (tok.type === "IDENT") {
|
|
953
|
+
if (tok.type === "SHEET" || tok.type === "IDENT") {
|
|
928
954
|
const next = c.tokens[c.pos + 1];
|
|
929
955
|
const next2 = c.tokens[c.pos + 2];
|
|
930
956
|
if (next && next.type === "OP" && next.value === "!" && next2 && next2.type === "IDENT") {
|
|
931
|
-
const
|
|
957
|
+
const sheetName2 = this.sheetNames.get(this.cleanSheetName(tok.value).toLowerCase());
|
|
958
|
+
if (sheetName2 === void 0) throw new LinterError(ERR_REF);
|
|
932
959
|
c.pos += 2;
|
|
933
960
|
const startTok = c.tokens[c.pos].value;
|
|
934
961
|
c.pos++;
|
|
935
|
-
return this.resolveRefOrRange(startTok,
|
|
962
|
+
return this.resolveRefOrRange(startTok, sheetName2, c, stack);
|
|
936
963
|
}
|
|
964
|
+
if (tok.type === "SHEET") throw new LinterError(ERR_NAME);
|
|
937
965
|
if (next && next.type === "OP" && next.value === "(") {
|
|
938
966
|
const name = tok.value.toUpperCase();
|
|
939
967
|
c.pos += 2;
|
|
@@ -978,9 +1006,13 @@ var FormulaLinter = class {
|
|
|
978
1006
|
const r = ref.replace(/\$/g, "").toUpperCase();
|
|
979
1007
|
return /^[A-Z]+\d+$/.test(r) ? r : null;
|
|
980
1008
|
}
|
|
1009
|
+
/**
|
|
1010
|
+
* Unwrap the single quotes Excel puts around a sheet name that needs them,
|
|
1011
|
+
* and turn its doubled quote back into one: 'Q3 ''Final''' is Q3 'Final'.
|
|
1012
|
+
*/
|
|
981
1013
|
cleanSheetName(name) {
|
|
982
1014
|
if (name.length >= 2 && name[0] === "'" && name[name.length - 1] === "'") {
|
|
983
|
-
return name.slice(1, -1);
|
|
1015
|
+
return name.slice(1, -1).split("''").join("'");
|
|
984
1016
|
}
|
|
985
1017
|
return name;
|
|
986
1018
|
}
|
|
@@ -1148,7 +1180,7 @@ var FormulaLinter = class {
|
|
|
1148
1180
|
case ERR_VALUE:
|
|
1149
1181
|
return this.hintValue(formula, sheet);
|
|
1150
1182
|
case ERR_REF:
|
|
1151
|
-
return
|
|
1183
|
+
return this.hintRef(formula);
|
|
1152
1184
|
case ERR_NAME:
|
|
1153
1185
|
return "The formula references an unknown function or has a syntax error. Holy Sheet supports: SUM, AVERAGE, COUNT, COUNTA, MIN, MAX, IF, ROUND, ABS, LEN, UPPER, LOWER, CONCAT.";
|
|
1154
1186
|
case ERR_DIV0:
|
|
@@ -1159,20 +1191,29 @@ var FormulaLinter = class {
|
|
|
1159
1191
|
return "Formula evaluation failed.";
|
|
1160
1192
|
}
|
|
1161
1193
|
}
|
|
1194
|
+
hintRef(formula) {
|
|
1195
|
+
for (const m of formula.matchAll(new RegExp(SHEET_QUALIFIER, "g"))) {
|
|
1196
|
+
const name = this.cleanSheetName(m[1]);
|
|
1197
|
+
if (!this.sheetNames.has(name.toLowerCase())) {
|
|
1198
|
+
return `The formula refers to a sheet named '${name}', and this workbook has no such sheet. Its sheets are: ${[...this.sheetNames.values()].join(", ")}. Quote a name that contains spaces or punctuation: 'My Sheet'!A1.`;
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
return "A cell reference points to a cell that doesn't exist in the workbook. Check column letters and row numbers.";
|
|
1202
|
+
}
|
|
1162
1203
|
hintValue(formula, sheet) {
|
|
1163
|
-
const re =
|
|
1204
|
+
const re = new RegExp(`(?:${SHEET_QUALIFIER})?\\$?([A-Z]+)\\$?(\\d+)`, "gi");
|
|
1164
1205
|
const offenders = [];
|
|
1165
1206
|
for (const m of formula.matchAll(re)) {
|
|
1166
|
-
const
|
|
1207
|
+
const sheetName2 = m[1] ? this.sheetNames.get(this.cleanSheetName(m[1]).toLowerCase()) ?? this.cleanSheetName(m[1]) : sheet;
|
|
1167
1208
|
const a1 = m[2].toUpperCase() + m[3];
|
|
1168
|
-
const key =
|
|
1209
|
+
const key = sheetName2 + "!" + a1;
|
|
1169
1210
|
const cell = this.index.get(key);
|
|
1170
1211
|
if (!cell) continue;
|
|
1171
1212
|
const value = this.cache.has(key) ? this.cache.get(key) : cell.formula !== null ? null : cell.value;
|
|
1172
1213
|
if (typeof value === "string" && !isNumericString(value) && value !== "") {
|
|
1173
1214
|
const row = parseInt(m[3], 10);
|
|
1174
1215
|
const col = m[2];
|
|
1175
|
-
const next =
|
|
1216
|
+
const next = sheetName2 + "!" + col.toUpperCase() + (row + 1);
|
|
1176
1217
|
const nextCell = this.index.get(next);
|
|
1177
1218
|
let suggestion = "";
|
|
1178
1219
|
if (nextCell && isNumericString(nextCell.value)) {
|
|
@@ -2388,7 +2429,7 @@ var OdsStyles = class {
|
|
|
2388
2429
|
}
|
|
2389
2430
|
}
|
|
2390
2431
|
const parts = childrenIn(el2, NS.NUMBER);
|
|
2391
|
-
const
|
|
2432
|
+
const has2 = (local) => parts.some((p) => p.name === local);
|
|
2392
2433
|
const number = parts.find((p) => p.name === "number");
|
|
2393
2434
|
let decimals = null;
|
|
2394
2435
|
if (number) {
|
|
@@ -2397,7 +2438,7 @@ var OdsStyles = class {
|
|
|
2397
2438
|
}
|
|
2398
2439
|
switch (el2.name) {
|
|
2399
2440
|
case "number-style": {
|
|
2400
|
-
if (
|
|
2441
|
+
if (has2("scientific-number") || has2("fraction")) return null;
|
|
2401
2442
|
for (const text of parts.filter((p) => p.name === "text")) {
|
|
2402
2443
|
const iso = currencyCode(ownText(text).replace(/[-()"\u00A0]/g, ""));
|
|
2403
2444
|
if (iso !== null) return { displayFormat: "currency", decimals: decimals ?? 0, currency: iso };
|
|
@@ -2415,7 +2456,7 @@ var OdsStyles = class {
|
|
|
2415
2456
|
return out;
|
|
2416
2457
|
}
|
|
2417
2458
|
case "date-style":
|
|
2418
|
-
return ["hours", "minutes", "seconds", "am-pm"].some(
|
|
2459
|
+
return ["hours", "minutes", "seconds", "am-pm"].some(has2) ? { displayFormat: "datetime" } : { displayFormat: "date" };
|
|
2419
2460
|
case "time-style":
|
|
2420
2461
|
return { displayFormat: "datetime" };
|
|
2421
2462
|
case "text-style":
|
|
@@ -2482,11 +2523,11 @@ function currencyCode(symbol) {
|
|
|
2482
2523
|
// src/reader/ods/ods-text.ts
|
|
2483
2524
|
var SKIPPED = /* @__PURE__ */ new Set(["annotation", "note"]);
|
|
2484
2525
|
function paragraphs(node) {
|
|
2485
|
-
const
|
|
2526
|
+
const lines2 = [];
|
|
2486
2527
|
for (const c of node.children) {
|
|
2487
|
-
if (c.ns === NS.TEXT && (c.name === "p" || c.name === "h"))
|
|
2528
|
+
if (c.ns === NS.TEXT && (c.name === "p" || c.name === "h")) lines2.push(paragraph(c));
|
|
2488
2529
|
}
|
|
2489
|
-
return
|
|
2530
|
+
return lines2.length === 0 ? null : lines2.join("\n");
|
|
2490
2531
|
}
|
|
2491
2532
|
function paragraph(p) {
|
|
2492
2533
|
const state = { out: "", ignoreSpace: true };
|
|
@@ -3749,6 +3790,778 @@ function gmDateZ() {
|
|
|
3749
3790
|
return (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
3750
3791
|
}
|
|
3751
3792
|
|
|
3793
|
+
// src/ops/php.ts
|
|
3794
|
+
function isArr(value) {
|
|
3795
|
+
return typeof value === "object" && value !== null;
|
|
3796
|
+
}
|
|
3797
|
+
function has(value, key) {
|
|
3798
|
+
return isArr(value) && Object.prototype.hasOwnProperty.call(value, key) && value[key] !== void 0;
|
|
3799
|
+
}
|
|
3800
|
+
function get(value, key) {
|
|
3801
|
+
return has(value, key) ? value[key] ?? null : null;
|
|
3802
|
+
}
|
|
3803
|
+
function isEmptyArr(value) {
|
|
3804
|
+
return isArr(value) && Object.keys(value).every((k) => value[k] === void 0);
|
|
3805
|
+
}
|
|
3806
|
+
function valuesOf(value) {
|
|
3807
|
+
if (Array.isArray(value)) return value.slice();
|
|
3808
|
+
if (isArr(value)) return Object.keys(value).filter((k) => value[k] !== void 0).map((k) => value[k]);
|
|
3809
|
+
return [];
|
|
3810
|
+
}
|
|
3811
|
+
function entriesOf(value) {
|
|
3812
|
+
if (Array.isArray(value)) return value.map((item, i) => [String(i), item]);
|
|
3813
|
+
if (isArr(value)) return Object.keys(value).filter((k) => value[k] !== void 0).map((k) => [k, value[k]]);
|
|
3814
|
+
return [];
|
|
3815
|
+
}
|
|
3816
|
+
function phpInt2(value) {
|
|
3817
|
+
if (value === null || value === void 0) return 0;
|
|
3818
|
+
if (typeof value === "boolean") return value ? 1 : 0;
|
|
3819
|
+
if (typeof value === "number") return Number.isFinite(value) ? Math.trunc(value) || 0 : 0;
|
|
3820
|
+
if (typeof value === "string") {
|
|
3821
|
+
const m = /^[ \t\n\r\v\f]*[+-]?(\d+(\.\d*)?|\.\d+)([eE][+-]?\d+)?/.exec(value);
|
|
3822
|
+
if (!m) return 0;
|
|
3823
|
+
const n = Number(m[0].trim());
|
|
3824
|
+
return Number.isFinite(n) ? Math.trunc(n) || 0 : 0;
|
|
3825
|
+
}
|
|
3826
|
+
if (isArr(value)) return isEmptyArr(value) ? 0 : 1;
|
|
3827
|
+
return 0;
|
|
3828
|
+
}
|
|
3829
|
+
function phpString(value) {
|
|
3830
|
+
if (value === null || value === void 0 || value === false) return "";
|
|
3831
|
+
if (value === true) return "1";
|
|
3832
|
+
if (typeof value === "string") return value;
|
|
3833
|
+
if (typeof value === "number") return String(value);
|
|
3834
|
+
if (isArr(value)) return "Array";
|
|
3835
|
+
return String(value);
|
|
3836
|
+
}
|
|
3837
|
+
function asciiUpper(value) {
|
|
3838
|
+
return value.replace(/[a-z]+/g, (s) => s.toUpperCase());
|
|
3839
|
+
}
|
|
3840
|
+
function parseAddress(address) {
|
|
3841
|
+
const m = /^([A-Z]+)(\d+)$/.exec(asciiUpper(address.replace(/^[ \t\n\r\v\0]+|[ \t\n\r\v\0]+$/g, "")));
|
|
3842
|
+
if (!m) return null;
|
|
3843
|
+
return [CellAddress.index(m[1]), parseInt(m[2], 10)];
|
|
3844
|
+
}
|
|
3845
|
+
var letter = CellAddress.letter;
|
|
3846
|
+
|
|
3847
|
+
// src/ops/sheet-reducer.ts
|
|
3848
|
+
var SheetReducer = {
|
|
3849
|
+
/** Sheet keys the granular ops address. A sheet with any other key is authored form. */
|
|
3850
|
+
CELL_FORM_KEYS: ["name", "cells", "mergedRegions", "columnWidths", "frozenRows", "frozenCols"],
|
|
3851
|
+
applyAll(schema, ops) {
|
|
3852
|
+
for (const op of ops) {
|
|
3853
|
+
schema = SheetReducer.apply(schema, op);
|
|
3854
|
+
}
|
|
3855
|
+
return schema;
|
|
3856
|
+
},
|
|
3857
|
+
apply(schema, op) {
|
|
3858
|
+
const type = get(op, "type");
|
|
3859
|
+
if (type === "set_workbook") {
|
|
3860
|
+
return isArr(get(op, "data")) ? op.data : schema;
|
|
3861
|
+
}
|
|
3862
|
+
if (type === "set_meta") {
|
|
3863
|
+
const out = { ...schema };
|
|
3864
|
+
if (get(op, "meta") === null) {
|
|
3865
|
+
delete out["meta"];
|
|
3866
|
+
} else {
|
|
3867
|
+
out["meta"] = op.meta;
|
|
3868
|
+
}
|
|
3869
|
+
return out;
|
|
3870
|
+
}
|
|
3871
|
+
const sheets = isArr(get(schema, "sheets")) ? valuesOf(schema.sheets) : [];
|
|
3872
|
+
if (type === "add_sheet") {
|
|
3873
|
+
if (!isArr(get(op, "sheet"))) {
|
|
3874
|
+
return schema;
|
|
3875
|
+
}
|
|
3876
|
+
const index = Math.max(0, Math.min(sheets.length, phpInt2(get(op, "index") ?? sheets.length)));
|
|
3877
|
+
sheets.splice(index, 0, op.sheet);
|
|
3878
|
+
return { ...schema, sheets };
|
|
3879
|
+
}
|
|
3880
|
+
const at2 = find(sheets, phpString(get(op, "sheet") ?? ""));
|
|
3881
|
+
if (at2 === null) {
|
|
3882
|
+
return schema;
|
|
3883
|
+
}
|
|
3884
|
+
switch (type) {
|
|
3885
|
+
case "remove_sheet":
|
|
3886
|
+
sheets.splice(at2, 1);
|
|
3887
|
+
break;
|
|
3888
|
+
case "rename_sheet":
|
|
3889
|
+
sheets[at2] = { ...sheets[at2], name: phpString(get(op, "name") ?? sheets[at2].name) };
|
|
3890
|
+
break;
|
|
3891
|
+
case "move_sheet": {
|
|
3892
|
+
const [moved] = sheets.splice(at2, 1);
|
|
3893
|
+
const to = Math.max(0, Math.min(sheets.length, phpInt2(get(op, "toIndex") ?? at2)));
|
|
3894
|
+
sheets.splice(to, 0, moved);
|
|
3895
|
+
break;
|
|
3896
|
+
}
|
|
3897
|
+
case "replace_sheet":
|
|
3898
|
+
if (isArr(get(op, "data"))) {
|
|
3899
|
+
sheets[at2] = op.data;
|
|
3900
|
+
}
|
|
3901
|
+
break;
|
|
3902
|
+
case "set_merged_regions":
|
|
3903
|
+
sheets[at2] = setOrUnset(sheets[at2], "mergedRegions", get(op, "mergedRegions") ?? [], EMPTY);
|
|
3904
|
+
break;
|
|
3905
|
+
case "set_column_widths":
|
|
3906
|
+
sheets[at2] = setOrUnset(sheets[at2], "columnWidths", get(op, "columnWidths") ?? [], EMPTY);
|
|
3907
|
+
break;
|
|
3908
|
+
case "set_frozen":
|
|
3909
|
+
sheets[at2] = setOrUnset(sheets[at2], "frozenRows", phpInt2(get(op, "rows") ?? 0), 0);
|
|
3910
|
+
sheets[at2] = setOrUnset(sheets[at2], "frozenCols", phpInt2(get(op, "cols") ?? 0), 0);
|
|
3911
|
+
break;
|
|
3912
|
+
case "set_cell":
|
|
3913
|
+
sheets[at2] = setCell(sheets[at2], op);
|
|
3914
|
+
break;
|
|
3915
|
+
case "set_range":
|
|
3916
|
+
sheets[at2] = setRange(sheets[at2], op);
|
|
3917
|
+
break;
|
|
3918
|
+
case "clear_cell": {
|
|
3919
|
+
const address = asciiUpper(phpString(get(op, "address") ?? ""));
|
|
3920
|
+
const cells = sheets[at2].cells;
|
|
3921
|
+
if (isArr(cells) && !Array.isArray(cells) && has(cells, address)) {
|
|
3922
|
+
const copy = { ...cells };
|
|
3923
|
+
delete copy[address];
|
|
3924
|
+
sheets[at2] = { ...sheets[at2], cells: copy };
|
|
3925
|
+
}
|
|
3926
|
+
break;
|
|
3927
|
+
}
|
|
3928
|
+
case "insert_rows":
|
|
3929
|
+
sheets[at2] = shiftRows(sheets[at2], phpInt2(get(op, "at") ?? 0), Math.max(0, phpInt2(get(op, "count") ?? 0)));
|
|
3930
|
+
break;
|
|
3931
|
+
case "delete_rows":
|
|
3932
|
+
sheets[at2] = shiftRows(sheets[at2], phpInt2(get(op, "at") ?? 0), -Math.max(0, phpInt2(get(op, "count") ?? 0)));
|
|
3933
|
+
break;
|
|
3934
|
+
case "insert_columns":
|
|
3935
|
+
sheets[at2] = shiftColumns(sheets[at2], phpInt2(get(op, "at") ?? 0), Math.max(0, phpInt2(get(op, "count") ?? 0)));
|
|
3936
|
+
break;
|
|
3937
|
+
case "delete_columns":
|
|
3938
|
+
sheets[at2] = shiftColumns(sheets[at2], phpInt2(get(op, "at") ?? 0), -Math.max(0, phpInt2(get(op, "count") ?? 0)));
|
|
3939
|
+
break;
|
|
3940
|
+
default:
|
|
3941
|
+
return schema;
|
|
3942
|
+
}
|
|
3943
|
+
return { ...schema, sheets };
|
|
3944
|
+
}
|
|
3945
|
+
};
|
|
3946
|
+
var EMPTY = /* @__PURE__ */ Symbol("empty");
|
|
3947
|
+
function find(sheets, name) {
|
|
3948
|
+
for (let i = 0; i < sheets.length; i++) {
|
|
3949
|
+
if (get(sheets[i], "name") === name) {
|
|
3950
|
+
return i;
|
|
3951
|
+
}
|
|
3952
|
+
}
|
|
3953
|
+
return null;
|
|
3954
|
+
}
|
|
3955
|
+
function setOrUnset(sheet, key, value, empty) {
|
|
3956
|
+
const out = { ...sheet };
|
|
3957
|
+
if (value === null || (empty === EMPTY ? isEmptyArr(value) : value === empty)) {
|
|
3958
|
+
delete out[key];
|
|
3959
|
+
} else {
|
|
3960
|
+
out[key] = value;
|
|
3961
|
+
}
|
|
3962
|
+
return out;
|
|
3963
|
+
}
|
|
3964
|
+
function setCell(sheet, op) {
|
|
3965
|
+
const address = asciiUpper(phpString(get(op, "address") ?? ""));
|
|
3966
|
+
if (parseAddress(address) === null) {
|
|
3967
|
+
return sheet;
|
|
3968
|
+
}
|
|
3969
|
+
const cells = isArr(get(sheet, "cells")) ? { ...sheet.cells } : {};
|
|
3970
|
+
const existing = get(cells, address);
|
|
3971
|
+
const value = get(op, "value");
|
|
3972
|
+
const carries = (key) => has(op, key) && op[key] !== null;
|
|
3973
|
+
if (existing === null && value === null && !carries("formula") && !carries("computedValue") && !carries("format") && !carries("comment")) {
|
|
3974
|
+
return sheet;
|
|
3975
|
+
}
|
|
3976
|
+
const cell = has(op, "value") ? { value } : {};
|
|
3977
|
+
for (const key of ["formula", "computedValue"]) {
|
|
3978
|
+
if (carries(key)) {
|
|
3979
|
+
cell[key] = op[key];
|
|
3980
|
+
}
|
|
3981
|
+
}
|
|
3982
|
+
for (const key of ["format", "comment"]) {
|
|
3983
|
+
if (has(op, key)) {
|
|
3984
|
+
if (op[key] !== null) {
|
|
3985
|
+
cell[key] = op[key];
|
|
3986
|
+
}
|
|
3987
|
+
} else if (isArr(existing) && has(existing, key)) {
|
|
3988
|
+
cell[key] = existing[key];
|
|
3989
|
+
}
|
|
3990
|
+
}
|
|
3991
|
+
if (Object.keys(cell).length === 0) {
|
|
3992
|
+
delete cells[address];
|
|
3993
|
+
} else {
|
|
3994
|
+
cells[address] = cell;
|
|
3995
|
+
}
|
|
3996
|
+
return { ...sheet, cells };
|
|
3997
|
+
}
|
|
3998
|
+
function setRange(sheet, op) {
|
|
3999
|
+
const start = parseAddress(phpString(get(op, "start") ?? ""));
|
|
4000
|
+
if (start === null || !isArr(get(op, "values"))) {
|
|
4001
|
+
return sheet;
|
|
4002
|
+
}
|
|
4003
|
+
const [col0, row0] = start;
|
|
4004
|
+
const rows = valuesOf(op.values);
|
|
4005
|
+
for (let r = 0; r < rows.length; r++) {
|
|
4006
|
+
const row = valuesOf(isArr(rows[r]) ? rows[r] : []);
|
|
4007
|
+
for (let c = 0; c < row.length; c++) {
|
|
4008
|
+
sheet = setCell(sheet, { address: letter(col0 + c) + String(row0 + r), value: row[c] === void 0 ? null : row[c] });
|
|
4009
|
+
}
|
|
4010
|
+
}
|
|
4011
|
+
return sheet;
|
|
4012
|
+
}
|
|
4013
|
+
function shiftRows(sheet, at2, delta) {
|
|
4014
|
+
if (at2 < 1 || delta === 0) {
|
|
4015
|
+
return sheet;
|
|
4016
|
+
}
|
|
4017
|
+
sheet = remapCells(sheet, (col, row) => {
|
|
4018
|
+
if (row < at2) {
|
|
4019
|
+
return [col, row];
|
|
4020
|
+
}
|
|
4021
|
+
if (delta < 0 && row < at2 - delta) {
|
|
4022
|
+
return null;
|
|
4023
|
+
}
|
|
4024
|
+
return [col, row + delta];
|
|
4025
|
+
});
|
|
4026
|
+
return remapMerges(sheet, "row", at2, delta);
|
|
4027
|
+
}
|
|
4028
|
+
function shiftColumns(sheet, at2, delta) {
|
|
4029
|
+
if (at2 < 1 || delta === 0) {
|
|
4030
|
+
return sheet;
|
|
4031
|
+
}
|
|
4032
|
+
sheet = remapCells(sheet, (col, row) => {
|
|
4033
|
+
const number = col + 1;
|
|
4034
|
+
if (number < at2) {
|
|
4035
|
+
return [col, row];
|
|
4036
|
+
}
|
|
4037
|
+
if (delta < 0 && number < at2 - delta) {
|
|
4038
|
+
return null;
|
|
4039
|
+
}
|
|
4040
|
+
return [col + delta, row];
|
|
4041
|
+
});
|
|
4042
|
+
if (isArr(get(sheet, "columnWidths"))) {
|
|
4043
|
+
const widths = /* @__PURE__ */ new Map();
|
|
4044
|
+
for (const [index, width] of entriesOf(sheet.columnWidths)) {
|
|
4045
|
+
const i = phpInt2(index);
|
|
4046
|
+
const number = i + 1;
|
|
4047
|
+
if (number < at2) {
|
|
4048
|
+
widths.set(i, width);
|
|
4049
|
+
} else if (delta > 0 || number >= at2 - delta) {
|
|
4050
|
+
widths.set(i + delta, width);
|
|
4051
|
+
}
|
|
4052
|
+
}
|
|
4053
|
+
const sorted = {};
|
|
4054
|
+
for (const [index, width] of [...widths.entries()].sort((x, y) => x[0] - y[0])) {
|
|
4055
|
+
sorted[index] = width;
|
|
4056
|
+
}
|
|
4057
|
+
sheet = setOrUnset(sheet, "columnWidths", sorted, EMPTY);
|
|
4058
|
+
}
|
|
4059
|
+
return remapMerges(sheet, "col", at2, delta);
|
|
4060
|
+
}
|
|
4061
|
+
function shiftSpan(start, end, at2, delta) {
|
|
4062
|
+
if (delta > 0) {
|
|
4063
|
+
return [start >= at2 ? start + delta : start, end >= at2 ? end + delta : end];
|
|
4064
|
+
}
|
|
4065
|
+
const last = at2 - delta - 1;
|
|
4066
|
+
const newStart = start < at2 ? start : start > last ? start + delta : at2;
|
|
4067
|
+
const newEnd = end < at2 ? end : end > last ? end + delta : at2 - 1;
|
|
4068
|
+
return newEnd < newStart ? null : [newStart, newEnd];
|
|
4069
|
+
}
|
|
4070
|
+
function remapCells(sheet, move) {
|
|
4071
|
+
if (!isArr(get(sheet, "cells"))) {
|
|
4072
|
+
return sheet;
|
|
4073
|
+
}
|
|
4074
|
+
const placed = [];
|
|
4075
|
+
for (const [address, cell] of entriesOf(sheet.cells)) {
|
|
4076
|
+
const parsed = parseAddress(address);
|
|
4077
|
+
if (parsed === null) {
|
|
4078
|
+
continue;
|
|
4079
|
+
}
|
|
4080
|
+
const to = move(parsed[0], parsed[1]);
|
|
4081
|
+
if (to !== null) {
|
|
4082
|
+
placed.push([to[1], to[0], letter(to[0]) + String(to[1]), cell]);
|
|
4083
|
+
}
|
|
4084
|
+
}
|
|
4085
|
+
placed.sort((x, y) => x[0] - y[0] || x[1] - y[1]);
|
|
4086
|
+
const cells = {};
|
|
4087
|
+
for (const [, , address, cell] of placed) {
|
|
4088
|
+
cells[address] = cell;
|
|
4089
|
+
}
|
|
4090
|
+
return { ...sheet, cells };
|
|
4091
|
+
}
|
|
4092
|
+
function remapMerges(sheet, axis, at2, delta) {
|
|
4093
|
+
if (!isArr(get(sheet, "mergedRegions"))) {
|
|
4094
|
+
return sheet;
|
|
4095
|
+
}
|
|
4096
|
+
const regions = [];
|
|
4097
|
+
for (const region of valuesOf(sheet.mergedRegions)) {
|
|
4098
|
+
const start = parseAddress(phpString(get(region, "start") ?? ""));
|
|
4099
|
+
const end = parseAddress(phpString(get(region, "end") ?? ""));
|
|
4100
|
+
if (start === null || end === null) {
|
|
4101
|
+
regions.push(region);
|
|
4102
|
+
continue;
|
|
4103
|
+
}
|
|
4104
|
+
if (axis === "row") {
|
|
4105
|
+
const moved = shiftSpan(start[1], end[1], at2, delta);
|
|
4106
|
+
if (moved === null) {
|
|
4107
|
+
continue;
|
|
4108
|
+
}
|
|
4109
|
+
regions.push({ start: letter(start[0]) + String(moved[0]), end: letter(end[0]) + String(moved[1]) });
|
|
4110
|
+
} else {
|
|
4111
|
+
const moved = shiftSpan(start[0] + 1, end[0] + 1, at2, delta);
|
|
4112
|
+
if (moved === null) {
|
|
4113
|
+
continue;
|
|
4114
|
+
}
|
|
4115
|
+
regions.push({ start: letter(moved[0] - 1) + String(start[1]), end: letter(moved[1] - 1) + String(end[1]) });
|
|
4116
|
+
}
|
|
4117
|
+
}
|
|
4118
|
+
return setOrUnset(sheet, "mergedRegions", regions, EMPTY);
|
|
4119
|
+
}
|
|
4120
|
+
|
|
4121
|
+
// src/ops/sheet-diff.ts
|
|
4122
|
+
var SheetDiff = {
|
|
4123
|
+
/** Above this many LCS cells (after trimming the common prefix and suffix), rows or columns are not aligned. */
|
|
4124
|
+
ALIGN_LIMIT: 25e4,
|
|
4125
|
+
diff(a, b) {
|
|
4126
|
+
if (SheetDiff.same(a, b) || SheetDiff.equivalent(a, b)) {
|
|
4127
|
+
return [];
|
|
4128
|
+
}
|
|
4129
|
+
const ops = granular(a, b);
|
|
4130
|
+
if (SheetDiff.same(SheetReducer.applyAll(a, ops), b)) {
|
|
4131
|
+
return ops;
|
|
4132
|
+
}
|
|
4133
|
+
return [{ type: "set_workbook", data: b }];
|
|
4134
|
+
},
|
|
4135
|
+
/**
|
|
4136
|
+
* Whether two schemas write the same workbook.
|
|
4137
|
+
*
|
|
4138
|
+
* Both are written and read back. `meta.created` is compared only when both
|
|
4139
|
+
* schemas name one: the writer stamps the current time on a schema that
|
|
4140
|
+
* does not, and that timestamp is not an edit.
|
|
4141
|
+
*/
|
|
4142
|
+
equivalent(a, b) {
|
|
4143
|
+
let describedA = described(a);
|
|
4144
|
+
let describedB = described(b);
|
|
4145
|
+
if (!namesCreated(a) || !namesCreated(b)) {
|
|
4146
|
+
describedA = withoutCreated(describedA);
|
|
4147
|
+
describedB = withoutCreated(describedB);
|
|
4148
|
+
}
|
|
4149
|
+
return SheetDiff.same(describedA, describedB);
|
|
4150
|
+
},
|
|
4151
|
+
/**
|
|
4152
|
+
* Structural equality with map key order ignored and list order kept.
|
|
4153
|
+
*
|
|
4154
|
+
* Equality is on values, never on insertion order — a JS object cannot keep
|
|
4155
|
+
* the order of integer-like keys anyway (`columnWidths`). It follows PHP's
|
|
4156
|
+
* canonical form, where lists and maps are one array type: `{}` equals `[]`,
|
|
4157
|
+
* and an object keyed exactly "0".."n-1" equals the list of its values.
|
|
4158
|
+
*
|
|
4159
|
+
* Two divergences from the PHP `same()` cannot be removed, because the
|
|
4160
|
+
* distinctions do not exist in JS values:
|
|
4161
|
+
*
|
|
4162
|
+
* - PHP encodes the float `1.0` as `1.0` and the int `1` as `1`, so it calls
|
|
4163
|
+
* them different; JS has one number, so here they are the same. A PHP diff
|
|
4164
|
+
* can therefore emit a `set_cell` that rewrites `1` as `1.0` where this one
|
|
4165
|
+
* emits nothing. Parsed JSON gives PHP a float only for a literal written
|
|
4166
|
+
* with a fraction or exponent.
|
|
4167
|
+
* - PHP treats an int-keyed array whose keys are 0..n-1 but out of order (for
|
|
4168
|
+
* example `columnWidths` decoded from `{"1":140,"0":120}`) as a map, not a
|
|
4169
|
+
* list, and so as different from the same widths in order. A JS object has
|
|
4170
|
+
* no such order to compare.
|
|
4171
|
+
*/
|
|
4172
|
+
same(a, b) {
|
|
4173
|
+
return canon(a) === canon(b);
|
|
4174
|
+
},
|
|
4175
|
+
/**
|
|
4176
|
+
* Hunks of a longest-common-subsequence alignment: [start in a, deleted, inserted].
|
|
4177
|
+
*
|
|
4178
|
+
* Ties break toward deleting first. Past `ALIGN_LIMIT` the changed middle is
|
|
4179
|
+
* one hunk.
|
|
4180
|
+
*/
|
|
4181
|
+
hunks(a, b) {
|
|
4182
|
+
const n = a.length;
|
|
4183
|
+
const m = b.length;
|
|
4184
|
+
let prefix = 0;
|
|
4185
|
+
while (prefix < n && prefix < m && a[prefix] === b[prefix]) {
|
|
4186
|
+
prefix++;
|
|
4187
|
+
}
|
|
4188
|
+
let suffix = 0;
|
|
4189
|
+
while (suffix < n - prefix && suffix < m - prefix && a[n - 1 - suffix] === b[m - 1 - suffix]) {
|
|
4190
|
+
suffix++;
|
|
4191
|
+
}
|
|
4192
|
+
const midA = a.slice(prefix, n - suffix);
|
|
4193
|
+
const midB = b.slice(prefix, m - suffix);
|
|
4194
|
+
const rows = midA.length;
|
|
4195
|
+
const cols = midB.length;
|
|
4196
|
+
if (rows === 0 && cols === 0) {
|
|
4197
|
+
return [];
|
|
4198
|
+
}
|
|
4199
|
+
if (rows * cols > SheetDiff.ALIGN_LIMIT) {
|
|
4200
|
+
return [[prefix, rows, cols]];
|
|
4201
|
+
}
|
|
4202
|
+
const lengths = [];
|
|
4203
|
+
for (let i2 = 0; i2 <= rows; i2++) {
|
|
4204
|
+
lengths.push(new Int32Array(cols + 1));
|
|
4205
|
+
}
|
|
4206
|
+
for (let i2 = rows - 1; i2 >= 0; i2--) {
|
|
4207
|
+
for (let j2 = cols - 1; j2 >= 0; j2--) {
|
|
4208
|
+
lengths[i2][j2] = midA[i2] === midB[j2] ? lengths[i2 + 1][j2 + 1] + 1 : Math.max(lengths[i2 + 1][j2], lengths[i2][j2 + 1]);
|
|
4209
|
+
}
|
|
4210
|
+
}
|
|
4211
|
+
const hunks = [];
|
|
4212
|
+
let open = null;
|
|
4213
|
+
let i = 0;
|
|
4214
|
+
let j = 0;
|
|
4215
|
+
while (i < rows || j < cols) {
|
|
4216
|
+
if (i < rows && j < cols && midA[i] === midB[j]) {
|
|
4217
|
+
if (open !== null) {
|
|
4218
|
+
hunks.push(open);
|
|
4219
|
+
open = null;
|
|
4220
|
+
}
|
|
4221
|
+
i++;
|
|
4222
|
+
j++;
|
|
4223
|
+
continue;
|
|
4224
|
+
}
|
|
4225
|
+
open ?? (open = [prefix + i, 0, 0]);
|
|
4226
|
+
if (j >= cols || i < rows && lengths[i + 1][j] >= lengths[i][j + 1]) {
|
|
4227
|
+
open[1]++;
|
|
4228
|
+
i++;
|
|
4229
|
+
} else {
|
|
4230
|
+
open[2]++;
|
|
4231
|
+
j++;
|
|
4232
|
+
}
|
|
4233
|
+
}
|
|
4234
|
+
if (open !== null) {
|
|
4235
|
+
hunks.push(open);
|
|
4236
|
+
}
|
|
4237
|
+
return hunks;
|
|
4238
|
+
}
|
|
4239
|
+
};
|
|
4240
|
+
function namesCreated(schema) {
|
|
4241
|
+
return get(get(schema, "meta"), "created") !== null;
|
|
4242
|
+
}
|
|
4243
|
+
function withoutCreated(schema) {
|
|
4244
|
+
const out = { ...schema };
|
|
4245
|
+
if (isArr(out["meta"]) && !Array.isArray(out["meta"])) {
|
|
4246
|
+
const meta = { ...out["meta"] };
|
|
4247
|
+
delete meta["created"];
|
|
4248
|
+
out["meta"] = meta;
|
|
4249
|
+
}
|
|
4250
|
+
if (has(out, "meta") && isEmptyArr(out["meta"])) {
|
|
4251
|
+
delete out["meta"];
|
|
4252
|
+
}
|
|
4253
|
+
return out;
|
|
4254
|
+
}
|
|
4255
|
+
function described(schema) {
|
|
4256
|
+
return Agent.read(Agent.toBytes(schema));
|
|
4257
|
+
}
|
|
4258
|
+
function sheetName(sheet) {
|
|
4259
|
+
return phpString(get(sheet, "name") ?? "");
|
|
4260
|
+
}
|
|
4261
|
+
function granular(a, b) {
|
|
4262
|
+
const sheetsA = isArr(get(a, "sheets")) ? valuesOf(a.sheets) : [];
|
|
4263
|
+
const sheetsB = isArr(get(b, "sheets")) ? valuesOf(b.sheets) : [];
|
|
4264
|
+
const namesA = sheetsA.map(sheetName);
|
|
4265
|
+
const namesB = sheetsB.map(sheetName);
|
|
4266
|
+
if (new Set(namesA).size !== namesA.length || new Set(namesB).size !== namesB.length) {
|
|
4267
|
+
return [{ type: "set_workbook", data: b }];
|
|
4268
|
+
}
|
|
4269
|
+
const ops = [];
|
|
4270
|
+
if (!SheetDiff.same(get(a, "meta"), get(b, "meta"))) {
|
|
4271
|
+
ops.push({ type: "set_meta", meta: get(b, "meta") });
|
|
4272
|
+
}
|
|
4273
|
+
const removed = namesA.filter((name) => !namesB.includes(name));
|
|
4274
|
+
const added = namesB.filter((name) => !namesA.includes(name));
|
|
4275
|
+
const renames = /* @__PURE__ */ new Map();
|
|
4276
|
+
const renamedTo = (name) => [...renames.values()].includes(name);
|
|
4277
|
+
for (const old of removed) {
|
|
4278
|
+
for (const name of added) {
|
|
4279
|
+
if (renamedTo(name)) {
|
|
4280
|
+
continue;
|
|
4281
|
+
}
|
|
4282
|
+
if (SheetDiff.same(withoutName(sheetsA[namesA.indexOf(old)]), withoutName(sheetsB[namesB.indexOf(name)]))) {
|
|
4283
|
+
renames.set(old, name);
|
|
4284
|
+
break;
|
|
4285
|
+
}
|
|
4286
|
+
}
|
|
4287
|
+
}
|
|
4288
|
+
let unmatchedRemoved = removed.filter((name) => !renames.has(name));
|
|
4289
|
+
let unmatchedAdded = added.filter((name) => !renamedTo(name));
|
|
4290
|
+
if (unmatchedRemoved.length === 1 && unmatchedAdded.length === 1) {
|
|
4291
|
+
renames.set(unmatchedRemoved[0], unmatchedAdded[0]);
|
|
4292
|
+
unmatchedRemoved = [];
|
|
4293
|
+
unmatchedAdded = [];
|
|
4294
|
+
}
|
|
4295
|
+
for (const name of unmatchedRemoved) {
|
|
4296
|
+
ops.push({ type: "remove_sheet", sheet: name });
|
|
4297
|
+
}
|
|
4298
|
+
for (const name of namesA) {
|
|
4299
|
+
if (renames.has(name)) {
|
|
4300
|
+
ops.push({ type: "rename_sheet", sheet: name, name: renames.get(name) });
|
|
4301
|
+
}
|
|
4302
|
+
}
|
|
4303
|
+
namesB.forEach((name, index) => {
|
|
4304
|
+
if (unmatchedAdded.includes(name)) {
|
|
4305
|
+
ops.push({ type: "add_sheet", index, sheet: sheetsB[index] });
|
|
4306
|
+
}
|
|
4307
|
+
});
|
|
4308
|
+
let state = SheetReducer.applyAll(a, ops);
|
|
4309
|
+
namesB.forEach((name, index) => {
|
|
4310
|
+
const current = new Map(entriesOf(get(state, "sheets") ?? []).map(([key, sheet]) => [key, sheetName(sheet)]));
|
|
4311
|
+
if ((current.get(String(index)) ?? null) !== name) {
|
|
4312
|
+
const op = { type: "move_sheet", sheet: name, toIndex: index };
|
|
4313
|
+
ops.push(op);
|
|
4314
|
+
state = SheetReducer.apply(state, op);
|
|
4315
|
+
}
|
|
4316
|
+
});
|
|
4317
|
+
sheetsB.forEach((target, index) => {
|
|
4318
|
+
const current = state.sheets[index];
|
|
4319
|
+
if (SheetDiff.same(current, target)) {
|
|
4320
|
+
return;
|
|
4321
|
+
}
|
|
4322
|
+
for (const op of sheetOps(current, target)) {
|
|
4323
|
+
ops.push(op);
|
|
4324
|
+
}
|
|
4325
|
+
});
|
|
4326
|
+
return ops;
|
|
4327
|
+
}
|
|
4328
|
+
function sheetOps(sheet, target) {
|
|
4329
|
+
const name = phpString(target["name"]);
|
|
4330
|
+
const replace = [{ type: "replace_sheet", sheet: name, data: target }];
|
|
4331
|
+
if (!isCellForm(sheet) || !isCellForm(target)) {
|
|
4332
|
+
return replace;
|
|
4333
|
+
}
|
|
4334
|
+
const ops = [];
|
|
4335
|
+
let work = sheet;
|
|
4336
|
+
for (const axis of ["row", "col"]) {
|
|
4337
|
+
const structural = structuralOps(name, work, target, axis);
|
|
4338
|
+
for (const op of structural) {
|
|
4339
|
+
ops.push(op);
|
|
4340
|
+
work = applyToSheet(work, op);
|
|
4341
|
+
}
|
|
4342
|
+
}
|
|
4343
|
+
const cellsW = isArr(get(work, "cells")) ? work["cells"] : {};
|
|
4344
|
+
const cellsT = isArr(get(target, "cells")) ? target["cells"] : {};
|
|
4345
|
+
const addresses = [...new Set([...entriesOf(cellsW), ...entriesOf(cellsT)].map(([address]) => address))];
|
|
4346
|
+
for (const address of rowMajor(addresses)) {
|
|
4347
|
+
if (!has(cellsT, address)) {
|
|
4348
|
+
ops.push({ type: "clear_cell", sheet: name, address });
|
|
4349
|
+
continue;
|
|
4350
|
+
}
|
|
4351
|
+
const was = get(cellsW, address);
|
|
4352
|
+
if (was !== null && SheetDiff.same(was, cellsT[address])) {
|
|
4353
|
+
continue;
|
|
4354
|
+
}
|
|
4355
|
+
ops.push(setCellOp(name, address, isArr(was) ? was : null, isArr(cellsT[address]) ? cellsT[address] : {}));
|
|
4356
|
+
}
|
|
4357
|
+
if (!SheetDiff.same(get(work, "mergedRegions") ?? [], get(target, "mergedRegions") ?? [])) {
|
|
4358
|
+
ops.push({ type: "set_merged_regions", sheet: name, mergedRegions: get(target, "mergedRegions") ?? [] });
|
|
4359
|
+
}
|
|
4360
|
+
if (!SheetDiff.same(get(work, "columnWidths") ?? [], get(target, "columnWidths") ?? [])) {
|
|
4361
|
+
ops.push({ type: "set_column_widths", sheet: name, columnWidths: get(target, "columnWidths") ?? {} });
|
|
4362
|
+
}
|
|
4363
|
+
if (!identical(get(work, "frozenRows") ?? 0, get(target, "frozenRows") ?? 0) || !identical(get(work, "frozenCols") ?? 0, get(target, "frozenCols") ?? 0)) {
|
|
4364
|
+
ops.push({
|
|
4365
|
+
type: "set_frozen",
|
|
4366
|
+
sheet: name,
|
|
4367
|
+
rows: get(target, "frozenRows") ?? 0,
|
|
4368
|
+
cols: get(target, "frozenCols") ?? 0
|
|
4369
|
+
});
|
|
4370
|
+
}
|
|
4371
|
+
let result = sheet;
|
|
4372
|
+
for (const op of ops) {
|
|
4373
|
+
result = applyToSheet(result, op);
|
|
4374
|
+
}
|
|
4375
|
+
return SheetDiff.same(result, target) ? ops : replace;
|
|
4376
|
+
}
|
|
4377
|
+
function identical(x, y) {
|
|
4378
|
+
return isArr(x) || isArr(y) ? SheetDiff.same(x, y) : x === y;
|
|
4379
|
+
}
|
|
4380
|
+
function setCellOp(sheet, address, was, cell) {
|
|
4381
|
+
const op = { type: "set_cell", sheet, address };
|
|
4382
|
+
if (has(cell, "value")) {
|
|
4383
|
+
op["value"] = cell["value"];
|
|
4384
|
+
}
|
|
4385
|
+
for (const key of ["formula", "computedValue"]) {
|
|
4386
|
+
if (has(cell, key)) {
|
|
4387
|
+
op[key] = cell[key];
|
|
4388
|
+
}
|
|
4389
|
+
}
|
|
4390
|
+
for (const key of ["format", "comment"]) {
|
|
4391
|
+
if (!SheetDiff.same(get(was, key), get(cell, key))) {
|
|
4392
|
+
op[key] = get(cell, key);
|
|
4393
|
+
}
|
|
4394
|
+
}
|
|
4395
|
+
return op;
|
|
4396
|
+
}
|
|
4397
|
+
function structuralOps(name, sheet, target, axis) {
|
|
4398
|
+
const sheetLines = lines(sheet, axis);
|
|
4399
|
+
const targetLines = lines(target, axis);
|
|
4400
|
+
const ops = [];
|
|
4401
|
+
for (const [start, deleted, inserted] of SheetDiff.hunks(sheetLines, targetLines).reverse()) {
|
|
4402
|
+
const kept = Math.min(deleted, inserted);
|
|
4403
|
+
const at2 = start + kept + 1;
|
|
4404
|
+
if (deleted > inserted) {
|
|
4405
|
+
ops.push({ type: axis === "row" ? "delete_rows" : "delete_columns", sheet: name, at: at2, count: deleted - inserted });
|
|
4406
|
+
} else if (inserted > deleted && at2 <= sheetLines.length) {
|
|
4407
|
+
ops.push({ type: axis === "row" ? "insert_rows" : "insert_columns", sheet: name, at: at2, count: inserted - deleted });
|
|
4408
|
+
}
|
|
4409
|
+
}
|
|
4410
|
+
return ops;
|
|
4411
|
+
}
|
|
4412
|
+
function lines(sheet, axis) {
|
|
4413
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
4414
|
+
let max = 0;
|
|
4415
|
+
for (const [address, cell] of entriesOf(isArr(get(sheet, "cells")) ? sheet["cells"] : {})) {
|
|
4416
|
+
const parsed = parseAddress(address);
|
|
4417
|
+
if (parsed === null) {
|
|
4418
|
+
continue;
|
|
4419
|
+
}
|
|
4420
|
+
const [col, row] = parsed;
|
|
4421
|
+
const line = axis === "row" ? row : col + 1;
|
|
4422
|
+
const position = axis === "row" ? col : row;
|
|
4423
|
+
let group = grouped.get(line);
|
|
4424
|
+
if (group === void 0) {
|
|
4425
|
+
group = /* @__PURE__ */ new Map();
|
|
4426
|
+
grouped.set(line, group);
|
|
4427
|
+
}
|
|
4428
|
+
group.set(position, cell);
|
|
4429
|
+
max = Math.max(max, line);
|
|
4430
|
+
}
|
|
4431
|
+
const out = [];
|
|
4432
|
+
for (let i = 1; i <= max; i++) {
|
|
4433
|
+
const positions = [...(grouped.get(i) ?? /* @__PURE__ */ new Map()).entries()].sort((x, y) => x[0] - y[0]);
|
|
4434
|
+
out.push(canonEntries(positions.map(([position, cell]) => [String(position), cell])));
|
|
4435
|
+
}
|
|
4436
|
+
return out;
|
|
4437
|
+
}
|
|
4438
|
+
function applyToSheet(sheet, op) {
|
|
4439
|
+
return SheetReducer.apply({ sheets: [sheet] }, op).sheets[0];
|
|
4440
|
+
}
|
|
4441
|
+
function isCellForm(sheet) {
|
|
4442
|
+
return entriesOf(sheet).every(([key]) => SheetReducer.CELL_FORM_KEYS.includes(key));
|
|
4443
|
+
}
|
|
4444
|
+
function withoutName(sheet) {
|
|
4445
|
+
const out = isArr(sheet) ? { ...sheet } : {};
|
|
4446
|
+
delete out["name"];
|
|
4447
|
+
return out;
|
|
4448
|
+
}
|
|
4449
|
+
function rowMajor(addresses) {
|
|
4450
|
+
const parsed = [];
|
|
4451
|
+
for (const address of addresses) {
|
|
4452
|
+
const cell = parseAddress(address);
|
|
4453
|
+
if (cell !== null) {
|
|
4454
|
+
parsed.push([cell[1], cell[0], address]);
|
|
4455
|
+
}
|
|
4456
|
+
}
|
|
4457
|
+
parsed.sort((x, y) => x[0] - y[0] || x[1] - y[1]);
|
|
4458
|
+
return parsed.map(([, , address]) => address);
|
|
4459
|
+
}
|
|
4460
|
+
function canon(value) {
|
|
4461
|
+
if (Array.isArray(value)) {
|
|
4462
|
+
return "[" + value.map(canon).join(",") + "]";
|
|
4463
|
+
}
|
|
4464
|
+
if (isArr(value)) {
|
|
4465
|
+
return canonEntries(entriesOf(value));
|
|
4466
|
+
}
|
|
4467
|
+
return JSON.stringify(value) ?? "null";
|
|
4468
|
+
}
|
|
4469
|
+
function canonEntries(entries) {
|
|
4470
|
+
if (entries.every(([key], i) => key === String(i))) {
|
|
4471
|
+
return "[" + entries.map(([, item]) => canon(item)).join(",") + "]";
|
|
4472
|
+
}
|
|
4473
|
+
const sorted = entries.slice().sort((x, y) => x[0] < y[0] ? -1 : x[0] > y[0] ? 1 : 0);
|
|
4474
|
+
return "{" + sorted.map(([key, item]) => JSON.stringify(key) + ":" + canon(item)).join(",") + "}";
|
|
4475
|
+
}
|
|
4476
|
+
|
|
4477
|
+
// src/ops/sheet-op-schema.ts
|
|
4478
|
+
var SheetOpSchema = {
|
|
4479
|
+
/** Every op type, in the order the variants are listed. */
|
|
4480
|
+
TYPES: [
|
|
4481
|
+
"set_cell",
|
|
4482
|
+
"set_range",
|
|
4483
|
+
"set_workbook",
|
|
4484
|
+
"clear_cell",
|
|
4485
|
+
"insert_rows",
|
|
4486
|
+
"delete_rows",
|
|
4487
|
+
"insert_columns",
|
|
4488
|
+
"delete_columns",
|
|
4489
|
+
"add_sheet",
|
|
4490
|
+
"remove_sheet",
|
|
4491
|
+
"rename_sheet",
|
|
4492
|
+
"move_sheet",
|
|
4493
|
+
"replace_sheet",
|
|
4494
|
+
"set_merged_regions",
|
|
4495
|
+
"set_column_widths",
|
|
4496
|
+
"set_frozen",
|
|
4497
|
+
"set_meta"
|
|
4498
|
+
],
|
|
4499
|
+
/** A fresh object on every call, so a caller editing it changes nothing here. */
|
|
4500
|
+
jsonSchema() {
|
|
4501
|
+
const address = () => ({ type: "string", pattern: "^[A-Za-z]+[0-9]+$" });
|
|
4502
|
+
const sheet = () => ({ type: "string", minLength: 1 });
|
|
4503
|
+
const count = () => ({ type: "integer", minimum: 1 });
|
|
4504
|
+
const position = () => ({ type: "integer", minimum: 1 });
|
|
4505
|
+
const object = () => ({ type: "object" });
|
|
4506
|
+
const nullableObject = () => ({ type: ["object", "null"] });
|
|
4507
|
+
const value = () => ({ type: ["string", "number", "boolean", "null"] });
|
|
4508
|
+
const variants = [
|
|
4509
|
+
variant(
|
|
4510
|
+
"set_cell",
|
|
4511
|
+
{ sheet: sheet(), address: address(), value: value(), formula: { type: "string" }, computedValue: value(), format: nullableObject(), comment: nullableObject() },
|
|
4512
|
+
["sheet", "address"],
|
|
4513
|
+
"Write one cell. Omitting formula or computedValue clears it; omitting format or comment keeps it; null clears it."
|
|
4514
|
+
),
|
|
4515
|
+
variant(
|
|
4516
|
+
"set_range",
|
|
4517
|
+
{ sheet: sheet(), start: address(), end: address(), values: { type: "array", items: { type: "array", items: value() } } },
|
|
4518
|
+
["sheet", "start", "values"],
|
|
4519
|
+
"Write a block of values row-major from start, each as a set_cell with no formula."
|
|
4520
|
+
),
|
|
4521
|
+
variant("set_workbook", { data: object() }, ["data"], "Replace the whole workbook schema."),
|
|
4522
|
+
variant("clear_cell", { sheet: sheet(), address: address() }, ["sheet", "address"], "Remove one cell."),
|
|
4523
|
+
variant("insert_rows", { sheet: sheet(), at: position(), count: count() }, ["sheet", "at", "count"], "Insert rows before 1-based row `at`; cells, merges below move down."),
|
|
4524
|
+
variant("delete_rows", { sheet: sheet(), at: position(), count: count() }, ["sheet", "at", "count"], "Delete rows starting at 1-based row `at`; cells, merges below move up."),
|
|
4525
|
+
variant("insert_columns", { sheet: sheet(), at: position(), count: count() }, ["sheet", "at", "count"], "Insert columns before 1-based column `at` (A = 1); cells, merges and widths move right."),
|
|
4526
|
+
variant("delete_columns", { sheet: sheet(), at: position(), count: count() }, ["sheet", "at", "count"], "Delete columns starting at 1-based column `at`; cells, merges and widths move left."),
|
|
4527
|
+
variant("add_sheet", { index: { type: "integer", minimum: 0 }, sheet: object() }, ["index", "sheet"], "Insert a sheet at a 0-based position."),
|
|
4528
|
+
variant("remove_sheet", { sheet: sheet() }, ["sheet"], "Remove a sheet by name."),
|
|
4529
|
+
variant("rename_sheet", { sheet: sheet(), name: sheet() }, ["sheet", "name"], "Rename a sheet."),
|
|
4530
|
+
variant("move_sheet", { sheet: sheet(), toIndex: { type: "integer", minimum: 0 } }, ["sheet", "toIndex"], "Move a sheet to a 0-based position."),
|
|
4531
|
+
variant("replace_sheet", { sheet: sheet(), data: object() }, ["sheet", "data"], "Replace one sheet whole."),
|
|
4532
|
+
variant(
|
|
4533
|
+
"set_merged_regions",
|
|
4534
|
+
{ sheet: sheet(), mergedRegions: { type: "array", items: { type: "object", required: ["start", "end"], properties: { start: address(), end: address() } } } },
|
|
4535
|
+
["sheet", "mergedRegions"],
|
|
4536
|
+
"Set every merged region of a sheet."
|
|
4537
|
+
),
|
|
4538
|
+
variant("set_column_widths", { sheet: sheet(), columnWidths: object() }, ["sheet", "columnWidths"], "Set every column width of a sheet (0-based column index to pixels)."),
|
|
4539
|
+
variant(
|
|
4540
|
+
"set_frozen",
|
|
4541
|
+
{ sheet: sheet(), rows: { type: "integer", minimum: 0 }, cols: { type: "integer", minimum: 0 } },
|
|
4542
|
+
["sheet", "rows", "cols"],
|
|
4543
|
+
"Set frozen rows and columns."
|
|
4544
|
+
),
|
|
4545
|
+
variant("set_meta", { meta: nullableObject() }, ["meta"], "Replace the workbook meta, or remove it with null.")
|
|
4546
|
+
];
|
|
4547
|
+
return {
|
|
4548
|
+
$schema: "http://json-schema.org/draft-07/schema#",
|
|
4549
|
+
title: "Holy Sheet op",
|
|
4550
|
+
description: "One op from Agent::diff, applied by Agent::reduce.",
|
|
4551
|
+
oneOf: variants
|
|
4552
|
+
};
|
|
4553
|
+
}
|
|
4554
|
+
};
|
|
4555
|
+
function variant(type, properties, required, description) {
|
|
4556
|
+
return {
|
|
4557
|
+
type: "object",
|
|
4558
|
+
description,
|
|
4559
|
+
required: ["type", ...required],
|
|
4560
|
+
additionalProperties: false,
|
|
4561
|
+
properties: { type: { const: type }, ...properties }
|
|
4562
|
+
};
|
|
4563
|
+
}
|
|
4564
|
+
|
|
3752
4565
|
// src/holy-sheet.schema.json
|
|
3753
4566
|
var holy_sheet_schema_default = {
|
|
3754
4567
|
$schema: "http://json-schema.org/draft-07/schema#",
|
|
@@ -4113,7 +4926,7 @@ var holy_sheet_schema_default = {
|
|
|
4113
4926
|
};
|
|
4114
4927
|
|
|
4115
4928
|
// src/agent.ts
|
|
4116
|
-
var VERSION = "2.
|
|
4929
|
+
var VERSION = "2.4.0";
|
|
4117
4930
|
function toU8(input) {
|
|
4118
4931
|
return input instanceof Uint8Array ? input : new Uint8Array(input);
|
|
4119
4932
|
}
|
|
@@ -4166,8 +4979,8 @@ var Agent = {
|
|
|
4166
4979
|
return new Validator().validateAndRepair(schema);
|
|
4167
4980
|
},
|
|
4168
4981
|
/** Build a schema from a flat array of rows (+ optional headers). */
|
|
4169
|
-
fromArray(rows, headers = null,
|
|
4170
|
-
return ArrayBuilder.build(rows, headers,
|
|
4982
|
+
fromArray(rows, headers = null, sheetName2 = "Sheet 1", options = {}) {
|
|
4983
|
+
return ArrayBuilder.build(rows, headers, sheetName2, options);
|
|
4171
4984
|
},
|
|
4172
4985
|
/** Build a schema from CSV content (read files yourself in Node). */
|
|
4173
4986
|
fromCsv(csv, options = {}) {
|
|
@@ -4177,10 +4990,55 @@ var Agent = {
|
|
|
4177
4990
|
lint(schema) {
|
|
4178
4991
|
return new FormulaLinter().lint(schema);
|
|
4179
4992
|
},
|
|
4993
|
+
/**
|
|
4994
|
+
* The ops that turn schema `a` into schema `b`. Mirrors PHP `Agent::diff`, and
|
|
4995
|
+
* gives the same ops in the same order for the same inputs.
|
|
4996
|
+
*
|
|
4997
|
+
* - `reduce(a, diff(a, b))` equals `b` (key order aside).
|
|
4998
|
+
* - Schemas that write the same workbook diff to `[]`, so
|
|
4999
|
+
* `diff(s, read(toBytes(s)))` is `[]`: a save without a change records
|
|
5000
|
+
* nothing.
|
|
5001
|
+
* - One changed cell is one `set_cell`; an inserted row is one
|
|
5002
|
+
* `insert_rows` plus its cells.
|
|
5003
|
+
*
|
|
5004
|
+
* Store `diff(newer, older)` to keep a version as the ops that restore it.
|
|
5005
|
+
* Both schemas must be valid: the "same workbook" check writes them.
|
|
5006
|
+
*/
|
|
5007
|
+
diff(a, b) {
|
|
5008
|
+
return SheetDiff.diff(a, b);
|
|
5009
|
+
},
|
|
5010
|
+
/**
|
|
5011
|
+
* Apply one op, or a list of them, to a schema; returns a new schema and never
|
|
5012
|
+
* modifies the input. An op naming a sheet that is not there is skipped.
|
|
5013
|
+
*/
|
|
5014
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
5015
|
+
reduce(schema, opOrOps) {
|
|
5016
|
+
return SheetReducer.applyAll(schema, isOpList(opOrOps) ? Object.values(opOrOps) : [opOrOps]);
|
|
5017
|
+
},
|
|
5018
|
+
/**
|
|
5019
|
+
* JSON Schema for one op. `set_cell`, `set_range` and `set_workbook` are
|
|
5020
|
+
* fancy-sheets' `SheetOp` shapes.
|
|
5021
|
+
*/
|
|
5022
|
+
opSchema() {
|
|
5023
|
+
return SheetOpSchema.jsonSchema();
|
|
5024
|
+
},
|
|
5025
|
+
/**
|
|
5026
|
+
* Whether two schemas write the same workbook: a columns/rows sheet and the
|
|
5027
|
+
* cell map it becomes are equivalent, and so are a schema without
|
|
5028
|
+
* `meta.created` and its written copy.
|
|
5029
|
+
*/
|
|
5030
|
+
equivalent(a, b) {
|
|
5031
|
+
return SheetDiff.equivalent(a, b);
|
|
5032
|
+
},
|
|
4180
5033
|
version() {
|
|
4181
5034
|
return VERSION;
|
|
4182
5035
|
}
|
|
4183
5036
|
};
|
|
5037
|
+
function isOpList(opOrOps) {
|
|
5038
|
+
if (Array.isArray(opOrOps)) return true;
|
|
5039
|
+
if (typeof opOrOps !== "object" || opOrOps === null) return false;
|
|
5040
|
+
return Object.keys(opOrOps).every((key, i) => key === String(i));
|
|
5041
|
+
}
|
|
4184
5042
|
|
|
4185
5043
|
// src/holy-sheet.ts
|
|
4186
5044
|
var HolySheet = class {
|
|
@@ -4205,8 +5063,8 @@ var HolySheet = class {
|
|
|
4205
5063
|
validateAndRepair(schema) {
|
|
4206
5064
|
return Agent.validateAndRepair(schema);
|
|
4207
5065
|
}
|
|
4208
|
-
fromArray(rows, headers = null,
|
|
4209
|
-
return Agent.fromArray(rows, headers,
|
|
5066
|
+
fromArray(rows, headers = null, sheetName2 = "Sheet 1", options = {}) {
|
|
5067
|
+
return Agent.fromArray(rows, headers, sheetName2, options);
|
|
4210
5068
|
}
|
|
4211
5069
|
fromCsv(csv, options = {}) {
|
|
4212
5070
|
return Agent.fromCsv(csv, options);
|
|
@@ -4223,6 +5081,6 @@ var HolySheet = class {
|
|
|
4223
5081
|
};
|
|
4224
5082
|
HolySheet.VERSION = VERSION;
|
|
4225
5083
|
|
|
4226
|
-
export { Agent, ArrayBuilder, CellAddress, CsvBuilder, FormatSniffer, FormulaLinter, HolySheet, Inference, Normalizer, OdsReader, Repairer, SchemaException, Theme, UnsupportedFormatException, VERSION, Validator, XlsxReader, XlsxWriter, unzipSync, zipSync };
|
|
5084
|
+
export { Agent, ArrayBuilder, CellAddress, CsvBuilder, FormatSniffer, FormulaLinter, HolySheet, Inference, Normalizer, OdsReader, Repairer, SchemaException, SheetDiff, SheetOpSchema, SheetReducer, Theme, UnsupportedFormatException, VERSION, Validator, XlsxReader, XlsxWriter, unzipSync, zipSync };
|
|
4227
5085
|
//# sourceMappingURL=index.js.map
|
|
4228
5086
|
//# sourceMappingURL=index.js.map
|