@particle-academy/holy-sheet 2.3.1 → 2.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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, sheetName = "Sheet 1", options = {}) {
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([], [], sheetName, options);
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, sheetName, options);
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, sheetName, options) {
153
- const sheet = { name: sheetName, columns, rows };
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 sheetName = options.sheetName ?? "Sheet 1";
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: sheetName, columns: [], rows: [] }] };
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, sheetName, options);
175
+ return ArrayBuilder.build(dataRows, headers, sheetName2, options);
176
176
  }
177
177
  };
178
178
  function parseRows(csv, delimiter, quote) {
@@ -954,12 +954,12 @@ var FormulaLinter = class {
954
954
  const next = c.tokens[c.pos + 1];
955
955
  const next2 = c.tokens[c.pos + 2];
956
956
  if (next && next.type === "OP" && next.value === "!" && next2 && next2.type === "IDENT") {
957
- const sheetName = this.sheetNames.get(this.cleanSheetName(tok.value).toLowerCase());
958
- if (sheetName === void 0) throw new LinterError(ERR_REF);
957
+ const sheetName2 = this.sheetNames.get(this.cleanSheetName(tok.value).toLowerCase());
958
+ if (sheetName2 === void 0) throw new LinterError(ERR_REF);
959
959
  c.pos += 2;
960
960
  const startTok = c.tokens[c.pos].value;
961
961
  c.pos++;
962
- return this.resolveRefOrRange(startTok, sheetName, c, stack);
962
+ return this.resolveRefOrRange(startTok, sheetName2, c, stack);
963
963
  }
964
964
  if (tok.type === "SHEET") throw new LinterError(ERR_NAME);
965
965
  if (next && next.type === "OP" && next.value === "(") {
@@ -1204,16 +1204,16 @@ var FormulaLinter = class {
1204
1204
  const re = new RegExp(`(?:${SHEET_QUALIFIER})?\\$?([A-Z]+)\\$?(\\d+)`, "gi");
1205
1205
  const offenders = [];
1206
1206
  for (const m of formula.matchAll(re)) {
1207
- const sheetName = m[1] ? this.sheetNames.get(this.cleanSheetName(m[1]).toLowerCase()) ?? this.cleanSheetName(m[1]) : sheet;
1207
+ const sheetName2 = m[1] ? this.sheetNames.get(this.cleanSheetName(m[1]).toLowerCase()) ?? this.cleanSheetName(m[1]) : sheet;
1208
1208
  const a1 = m[2].toUpperCase() + m[3];
1209
- const key = sheetName + "!" + a1;
1209
+ const key = sheetName2 + "!" + a1;
1210
1210
  const cell = this.index.get(key);
1211
1211
  if (!cell) continue;
1212
1212
  const value = this.cache.has(key) ? this.cache.get(key) : cell.formula !== null ? null : cell.value;
1213
1213
  if (typeof value === "string" && !isNumericString(value) && value !== "") {
1214
1214
  const row = parseInt(m[3], 10);
1215
1215
  const col = m[2];
1216
- const next = sheetName + "!" + col.toUpperCase() + (row + 1);
1216
+ const next = sheetName2 + "!" + col.toUpperCase() + (row + 1);
1217
1217
  const nextCell = this.index.get(next);
1218
1218
  let suggestion = "";
1219
1219
  if (nextCell && isNumericString(nextCell.value)) {
@@ -2429,7 +2429,7 @@ var OdsStyles = class {
2429
2429
  }
2430
2430
  }
2431
2431
  const parts = childrenIn(el2, NS.NUMBER);
2432
- const has = (local) => parts.some((p) => p.name === local);
2432
+ const has2 = (local) => parts.some((p) => p.name === local);
2433
2433
  const number = parts.find((p) => p.name === "number");
2434
2434
  let decimals = null;
2435
2435
  if (number) {
@@ -2438,7 +2438,7 @@ var OdsStyles = class {
2438
2438
  }
2439
2439
  switch (el2.name) {
2440
2440
  case "number-style": {
2441
- if (has("scientific-number") || has("fraction")) return null;
2441
+ if (has2("scientific-number") || has2("fraction")) return null;
2442
2442
  for (const text of parts.filter((p) => p.name === "text")) {
2443
2443
  const iso = currencyCode(ownText(text).replace(/[-()"\u00A0]/g, ""));
2444
2444
  if (iso !== null) return { displayFormat: "currency", decimals: decimals ?? 0, currency: iso };
@@ -2456,7 +2456,7 @@ var OdsStyles = class {
2456
2456
  return out;
2457
2457
  }
2458
2458
  case "date-style":
2459
- return ["hours", "minutes", "seconds", "am-pm"].some(has) ? { displayFormat: "datetime" } : { displayFormat: "date" };
2459
+ return ["hours", "minutes", "seconds", "am-pm"].some(has2) ? { displayFormat: "datetime" } : { displayFormat: "date" };
2460
2460
  case "time-style":
2461
2461
  return { displayFormat: "datetime" };
2462
2462
  case "text-style":
@@ -2523,11 +2523,11 @@ function currencyCode(symbol) {
2523
2523
  // src/reader/ods/ods-text.ts
2524
2524
  var SKIPPED = /* @__PURE__ */ new Set(["annotation", "note"]);
2525
2525
  function paragraphs(node) {
2526
- const lines = [];
2526
+ const lines2 = [];
2527
2527
  for (const c of node.children) {
2528
- if (c.ns === NS.TEXT && (c.name === "p" || c.name === "h")) lines.push(paragraph(c));
2528
+ if (c.ns === NS.TEXT && (c.name === "p" || c.name === "h")) lines2.push(paragraph(c));
2529
2529
  }
2530
- return lines.length === 0 ? null : lines.join("\n");
2530
+ return lines2.length === 0 ? null : lines2.join("\n");
2531
2531
  }
2532
2532
  function paragraph(p) {
2533
2533
  const state = { out: "", ignoreSpace: true };
@@ -3790,6 +3790,786 @@ function gmDateZ() {
3790
3790
  return (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, "Z");
3791
3791
  }
3792
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
+ // An EMPTY array too, as PHP 2.3.1 declares: PHP encodes an empty map as
4539
+ // `[]`, which its diff emits when every width is removed. This port emits
4540
+ // `{}`. Both mean no widths.
4541
+ variant(
4542
+ "set_column_widths",
4543
+ { sheet: sheet(), columnWidths: { type: ["object", "array"], maxItems: 0 } },
4544
+ ["sheet", "columnWidths"],
4545
+ "Set every column width of a sheet (0-based column index to pixels); empty removes them."
4546
+ ),
4547
+ variant(
4548
+ "set_frozen",
4549
+ { sheet: sheet(), rows: { type: "integer", minimum: 0 }, cols: { type: "integer", minimum: 0 } },
4550
+ ["sheet", "rows", "cols"],
4551
+ "Set frozen rows and columns."
4552
+ ),
4553
+ variant("set_meta", { meta: nullableObject() }, ["meta"], "Replace the workbook meta, or remove it with null.")
4554
+ ];
4555
+ return {
4556
+ $schema: "http://json-schema.org/draft-07/schema#",
4557
+ title: "Holy Sheet op",
4558
+ description: "One op from Agent::diff, applied by Agent::reduce.",
4559
+ oneOf: variants
4560
+ };
4561
+ }
4562
+ };
4563
+ function variant(type, properties, required, description) {
4564
+ return {
4565
+ type: "object",
4566
+ description,
4567
+ required: ["type", ...required],
4568
+ additionalProperties: false,
4569
+ properties: { type: { const: type }, ...properties }
4570
+ };
4571
+ }
4572
+
3793
4573
  // src/holy-sheet.schema.json
3794
4574
  var holy_sheet_schema_default = {
3795
4575
  $schema: "http://json-schema.org/draft-07/schema#",
@@ -4154,7 +4934,7 @@ var holy_sheet_schema_default = {
4154
4934
  };
4155
4935
 
4156
4936
  // src/agent.ts
4157
- var VERSION = "2.3.1";
4937
+ var VERSION = "2.4.1";
4158
4938
  function toU8(input) {
4159
4939
  return input instanceof Uint8Array ? input : new Uint8Array(input);
4160
4940
  }
@@ -4207,8 +4987,8 @@ var Agent = {
4207
4987
  return new Validator().validateAndRepair(schema);
4208
4988
  },
4209
4989
  /** Build a schema from a flat array of rows (+ optional headers). */
4210
- fromArray(rows, headers = null, sheetName = "Sheet 1", options = {}) {
4211
- return ArrayBuilder.build(rows, headers, sheetName, options);
4990
+ fromArray(rows, headers = null, sheetName2 = "Sheet 1", options = {}) {
4991
+ return ArrayBuilder.build(rows, headers, sheetName2, options);
4212
4992
  },
4213
4993
  /** Build a schema from CSV content (read files yourself in Node). */
4214
4994
  fromCsv(csv, options = {}) {
@@ -4218,10 +4998,55 @@ var Agent = {
4218
4998
  lint(schema) {
4219
4999
  return new FormulaLinter().lint(schema);
4220
5000
  },
5001
+ /**
5002
+ * The ops that turn schema `a` into schema `b`. Mirrors PHP `Agent::diff`, and
5003
+ * gives the same ops in the same order for the same inputs.
5004
+ *
5005
+ * - `reduce(a, diff(a, b))` equals `b` (key order aside).
5006
+ * - Schemas that write the same workbook diff to `[]`, so
5007
+ * `diff(s, read(toBytes(s)))` is `[]`: a save without a change records
5008
+ * nothing.
5009
+ * - One changed cell is one `set_cell`; an inserted row is one
5010
+ * `insert_rows` plus its cells.
5011
+ *
5012
+ * Store `diff(newer, older)` to keep a version as the ops that restore it.
5013
+ * Both schemas must be valid: the "same workbook" check writes them.
5014
+ */
5015
+ diff(a, b) {
5016
+ return SheetDiff.diff(a, b);
5017
+ },
5018
+ /**
5019
+ * Apply one op, or a list of them, to a schema; returns a new schema and never
5020
+ * modifies the input. An op naming a sheet that is not there is skipped.
5021
+ */
5022
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
5023
+ reduce(schema, opOrOps) {
5024
+ return SheetReducer.applyAll(schema, isOpList(opOrOps) ? Object.values(opOrOps) : [opOrOps]);
5025
+ },
5026
+ /**
5027
+ * JSON Schema for one op. `set_cell`, `set_range` and `set_workbook` are
5028
+ * fancy-sheets' `SheetOp` shapes.
5029
+ */
5030
+ opSchema() {
5031
+ return SheetOpSchema.jsonSchema();
5032
+ },
5033
+ /**
5034
+ * Whether two schemas write the same workbook: a columns/rows sheet and the
5035
+ * cell map it becomes are equivalent, and so are a schema without
5036
+ * `meta.created` and its written copy.
5037
+ */
5038
+ equivalent(a, b) {
5039
+ return SheetDiff.equivalent(a, b);
5040
+ },
4221
5041
  version() {
4222
5042
  return VERSION;
4223
5043
  }
4224
5044
  };
5045
+ function isOpList(opOrOps) {
5046
+ if (Array.isArray(opOrOps)) return true;
5047
+ if (typeof opOrOps !== "object" || opOrOps === null) return false;
5048
+ return Object.keys(opOrOps).every((key, i) => key === String(i));
5049
+ }
4225
5050
 
4226
5051
  // src/holy-sheet.ts
4227
5052
  var HolySheet = class {
@@ -4246,8 +5071,8 @@ var HolySheet = class {
4246
5071
  validateAndRepair(schema) {
4247
5072
  return Agent.validateAndRepair(schema);
4248
5073
  }
4249
- fromArray(rows, headers = null, sheetName = "Sheet 1", options = {}) {
4250
- return Agent.fromArray(rows, headers, sheetName, options);
5074
+ fromArray(rows, headers = null, sheetName2 = "Sheet 1", options = {}) {
5075
+ return Agent.fromArray(rows, headers, sheetName2, options);
4251
5076
  }
4252
5077
  fromCsv(csv, options = {}) {
4253
5078
  return Agent.fromCsv(csv, options);
@@ -4264,6 +5089,6 @@ var HolySheet = class {
4264
5089
  };
4265
5090
  HolySheet.VERSION = VERSION;
4266
5091
 
4267
- export { Agent, ArrayBuilder, CellAddress, CsvBuilder, FormatSniffer, FormulaLinter, HolySheet, Inference, Normalizer, OdsReader, Repairer, SchemaException, Theme, UnsupportedFormatException, VERSION, Validator, XlsxReader, XlsxWriter, unzipSync, zipSync };
5092
+ export { Agent, ArrayBuilder, CellAddress, CsvBuilder, FormatSniffer, FormulaLinter, HolySheet, Inference, Normalizer, OdsReader, Repairer, SchemaException, SheetDiff, SheetOpSchema, SheetReducer, Theme, UnsupportedFormatException, VERSION, Validator, XlsxReader, XlsxWriter, unzipSync, zipSync };
4268
5093
  //# sourceMappingURL=index.js.map
4269
5094
  //# sourceMappingURL=index.js.map