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