@particle-academy/holy-sheet 2.4.0 → 2.4.2

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.d.cts CHANGED
@@ -223,7 +223,7 @@ interface SetMetaOp {
223
223
  }
224
224
 
225
225
  /** This package's own version, pinned to package.json by `version.test.ts`. */
226
- declare const VERSION = "2.4.0";
226
+ declare const VERSION = "2.4.2";
227
227
  type Any$6 = any;
228
228
  /**
229
229
  * Agent — the structured-tool surface for Holy Sheet. Mirrors PHP `Agent`.
@@ -295,7 +295,7 @@ type Any$5 = any;
295
295
  * over `Agent`. Provided for parity / DI ergonomics; most callers use `Agent`.
296
296
  */
297
297
  declare class HolySheet {
298
- static readonly VERSION = "2.4.0";
298
+ static readonly VERSION = "2.4.2";
299
299
  validate(schema: Any$5): ValidationError[];
300
300
  toBytes(schema: Any$5): Uint8Array;
301
301
  write(schema: Any$5, path: string): Promise<WriteResult>;
@@ -702,6 +702,21 @@ declare const SheetDiff: {
702
702
  * example `columnWidths` decoded from `{"1":140,"0":120}`) as a map, not a
703
703
  * list, and so as different from the same widths in order. A JS object has
704
704
  * no such order to compare.
705
+ *
706
+ * Throws a `TypeError` on a value JSON cannot hold, as PHP 2.3.2 throws
707
+ * `JsonException`, rather than calling two such values the same:
708
+ *
709
+ * - a non-finite number (PHP: NAN, INF);
710
+ * - a lone UTF-16 surrogate in a string or a key, which is this runtime's
711
+ * invalid UTF-8 (a JS string cannot hold invalid UTF-8, and PHP's own
712
+ * `json_decode` refuses a lone surrogate);
713
+ * - a function or a symbol, which `JSON.stringify` would silently drop
714
+ * (a bigint already throws there);
715
+ * - nesting deeper than PHP's `json_encode` depth of 4096, where every object
716
+ * and array is a level, an empty one included. The walk is iterative, so
717
+ * this is PHP's limit and not the call stack's.
718
+ *
719
+ * `undefined` stays what JSON makes it: an absent key, and `null` in a list.
705
720
  */
706
721
  same(a: unknown, b: unknown): boolean;
707
722
  /**
package/dist/index.d.ts CHANGED
@@ -223,7 +223,7 @@ interface SetMetaOp {
223
223
  }
224
224
 
225
225
  /** This package's own version, pinned to package.json by `version.test.ts`. */
226
- declare const VERSION = "2.4.0";
226
+ declare const VERSION = "2.4.2";
227
227
  type Any$6 = any;
228
228
  /**
229
229
  * Agent — the structured-tool surface for Holy Sheet. Mirrors PHP `Agent`.
@@ -295,7 +295,7 @@ type Any$5 = any;
295
295
  * over `Agent`. Provided for parity / DI ergonomics; most callers use `Agent`.
296
296
  */
297
297
  declare class HolySheet {
298
- static readonly VERSION = "2.4.0";
298
+ static readonly VERSION = "2.4.2";
299
299
  validate(schema: Any$5): ValidationError[];
300
300
  toBytes(schema: Any$5): Uint8Array;
301
301
  write(schema: Any$5, path: string): Promise<WriteResult>;
@@ -702,6 +702,21 @@ declare const SheetDiff: {
702
702
  * example `columnWidths` decoded from `{"1":140,"0":120}`) as a map, not a
703
703
  * list, and so as different from the same widths in order. A JS object has
704
704
  * no such order to compare.
705
+ *
706
+ * Throws a `TypeError` on a value JSON cannot hold, as PHP 2.3.2 throws
707
+ * `JsonException`, rather than calling two such values the same:
708
+ *
709
+ * - a non-finite number (PHP: NAN, INF);
710
+ * - a lone UTF-16 surrogate in a string or a key, which is this runtime's
711
+ * invalid UTF-8 (a JS string cannot hold invalid UTF-8, and PHP's own
712
+ * `json_decode` refuses a lone surrogate);
713
+ * - a function or a symbol, which `JSON.stringify` would silently drop
714
+ * (a bigint already throws there);
715
+ * - nesting deeper than PHP's `json_encode` depth of 4096, where every object
716
+ * and array is a level, an empty one included. The walk is iterative, so
717
+ * this is PHP's limit and not the call stack's.
718
+ *
719
+ * `undefined` stays what JSON makes it: an absent key, and `null` in a list.
705
720
  */
706
721
  same(a: unknown, b: unknown): boolean;
707
722
  /**
package/dist/index.js CHANGED
@@ -175,7 +175,7 @@ var CsvBuilder = {
175
175
  return ArrayBuilder.build(dataRows, headers, sheetName2, options);
176
176
  }
177
177
  };
178
- function parseRows(csv, delimiter, quote) {
178
+ function parseRows(csv, delimiter, quote2) {
179
179
  const rows = [];
180
180
  let field = "";
181
181
  let row = [];
@@ -195,9 +195,9 @@ function parseRows(csv, delimiter, quote) {
195
195
  for (let i = 0; i < csv.length; i++) {
196
196
  const ch = csv[i];
197
197
  if (inQuotes) {
198
- if (ch === quote) {
199
- if (csv[i + 1] === quote) {
200
- field += quote;
198
+ if (ch === quote2) {
199
+ if (csv[i + 1] === quote2) {
200
+ field += quote2;
201
201
  i++;
202
202
  } else {
203
203
  inQuotes = false;
@@ -207,7 +207,7 @@ function parseRows(csv, delimiter, quote) {
207
207
  }
208
208
  continue;
209
209
  }
210
- if (ch === quote) {
210
+ if (ch === quote2) {
211
211
  inQuotes = true;
212
212
  started = true;
213
213
  continue;
@@ -2680,13 +2680,13 @@ function unescapeXml(s) {
2680
2680
  });
2681
2681
  }
2682
2682
  function findTagEnd(src, from) {
2683
- let quote = "";
2683
+ let quote2 = "";
2684
2684
  for (let i = from; i < src.length; i++) {
2685
2685
  const ch = src[i];
2686
- if (quote) {
2687
- if (ch === quote) quote = "";
2686
+ if (quote2) {
2687
+ if (ch === quote2) quote2 = "";
2688
2688
  } else if (ch === '"' || ch === "'") {
2689
- quote = ch;
2689
+ quote2 = ch;
2690
2690
  } else if (ch === ">") {
2691
2691
  return i;
2692
2692
  }
@@ -3834,16 +3834,132 @@ function phpString(value) {
3834
3834
  if (isArr(value)) return "Array";
3835
3835
  return String(value);
3836
3836
  }
3837
+ function phpTrim2(value) {
3838
+ return value.replace(/^[ \t\n\r\x00\x0B]+|[ \t\n\r\x00\x0B]+$/g, "");
3839
+ }
3840
+ function phpInteger(value) {
3841
+ if (typeof value === "number") {
3842
+ return Number.isInteger(value) && Math.abs(value) <= 2 ** 63 ? value || 0 : null;
3843
+ }
3844
+ return typeof value === "string" && /^[0-9]+$/.test(value) ? phpInt2(value) : null;
3845
+ }
3846
+ function isIndexKey(key) {
3847
+ return /^[0-9]+$/.test(key) || /^-[1-9][0-9]*$/.test(key) && BigInt(key) >= -(2n ** 63n);
3848
+ }
3837
3849
  function asciiUpper(value) {
3838
3850
  return value.replace(/[a-z]+/g, (s) => s.toUpperCase());
3839
3851
  }
3840
3852
  function parseAddress(address) {
3841
- const m = /^([A-Z]+)(\d+)$/.exec(asciiUpper(address.replace(/^[ \t\n\r\v\0]+|[ \t\n\r\v\0]+$/g, "")));
3853
+ const m = /^([A-Z]+)(\d+)$/.exec(asciiUpper(phpTrim2(address)));
3842
3854
  if (!m) return null;
3843
3855
  return [CellAddress.index(m[1]), parseInt(m[2], 10)];
3844
3856
  }
3845
3857
  var letter = CellAddress.letter;
3846
3858
 
3859
+ // src/ops/sheet-op-schema.ts
3860
+ var SheetOpSchema = {
3861
+ /** Every op type, in the order the variants are listed. */
3862
+ TYPES: [
3863
+ "set_cell",
3864
+ "set_range",
3865
+ "set_workbook",
3866
+ "clear_cell",
3867
+ "insert_rows",
3868
+ "delete_rows",
3869
+ "insert_columns",
3870
+ "delete_columns",
3871
+ "add_sheet",
3872
+ "remove_sheet",
3873
+ "rename_sheet",
3874
+ "move_sheet",
3875
+ "replace_sheet",
3876
+ "set_merged_regions",
3877
+ "set_column_widths",
3878
+ "set_frozen",
3879
+ "set_meta"
3880
+ ],
3881
+ /** A fresh object on every call, so a caller editing it changes nothing here. */
3882
+ jsonSchema() {
3883
+ const address = () => ({ type: "string", pattern: "^[A-Za-z]+[0-9]+$" });
3884
+ const sheet = () => ({ type: "string", minLength: 1 });
3885
+ const count = () => ({ type: "integer", minimum: 1 });
3886
+ const position2 = () => ({ type: "integer", minimum: 1 });
3887
+ const object = () => ({ type: "object" });
3888
+ const nullableObject = () => ({ type: ["object", "null"] });
3889
+ const value = () => ({ type: ["string", "number", "boolean", "null"] });
3890
+ const variants = [
3891
+ variant(
3892
+ "set_cell",
3893
+ { sheet: sheet(), address: address(), value: value(), formula: { type: "string" }, computedValue: value(), format: nullableObject(), comment: nullableObject() },
3894
+ ["sheet", "address"],
3895
+ "Write one cell. Omitting formula or computedValue clears it; omitting format or comment keeps it; null clears it."
3896
+ ),
3897
+ variant(
3898
+ "set_range",
3899
+ { sheet: sheet(), start: address(), end: address(), values: { type: "array", items: { type: "array", items: value() } } },
3900
+ ["sheet", "start", "values"],
3901
+ "Write a block of values row-major from start, each as a set_cell with no formula."
3902
+ ),
3903
+ variant("set_workbook", { data: object() }, ["data"], "Replace the whole workbook schema."),
3904
+ variant("clear_cell", { sheet: sheet(), address: address() }, ["sheet", "address"], "Remove one cell."),
3905
+ variant("insert_rows", { sheet: sheet(), at: position2(), count: count() }, ["sheet", "at", "count"], "Insert rows before 1-based row `at`; cells, merges below move down."),
3906
+ variant("delete_rows", { sheet: sheet(), at: position2(), count: count() }, ["sheet", "at", "count"], "Delete rows starting at 1-based row `at`; cells, merges below move up."),
3907
+ variant("insert_columns", { sheet: sheet(), at: position2(), count: count() }, ["sheet", "at", "count"], "Insert columns before 1-based column `at` (A = 1); cells, merges and widths move right."),
3908
+ variant("delete_columns", { sheet: sheet(), at: position2(), count: count() }, ["sheet", "at", "count"], "Delete columns starting at 1-based column `at`; cells, merges and widths move left."),
3909
+ variant("add_sheet", { index: { type: "integer", minimum: 0 }, sheet: object() }, ["index", "sheet"], "Insert a sheet at a 0-based position."),
3910
+ variant("remove_sheet", { sheet: sheet() }, ["sheet"], "Remove a sheet by name."),
3911
+ variant("rename_sheet", { sheet: sheet(), name: sheet() }, ["sheet", "name"], "Rename a sheet."),
3912
+ variant("move_sheet", { sheet: sheet(), toIndex: { type: "integer", minimum: 0 } }, ["sheet", "toIndex"], "Move a sheet to a 0-based position."),
3913
+ variant("replace_sheet", { sheet: sheet(), data: object() }, ["sheet", "data"], "Replace one sheet whole."),
3914
+ variant(
3915
+ "set_merged_regions",
3916
+ { sheet: sheet(), mergedRegions: { type: "array", items: { type: "object", required: ["start", "end"], properties: { start: address(), end: address() } } } },
3917
+ ["sheet", "mergedRegions"],
3918
+ "Set every merged region of a sheet."
3919
+ ),
3920
+ // A LIST too, as PHP 2.3.2 declares. PHP encodes a map whose keys run
3921
+ // 0..n-1 as a JSON list, so widths for columns A, B and C arrive as
3922
+ // `[120, 80, 140]` and no widths as `[]`; this port passes such a list
3923
+ // through a diff unchanged. A list's position is the column index.
3924
+ variant(
3925
+ "set_column_widths",
3926
+ {
3927
+ sheet: sheet(),
3928
+ columnWidths: {
3929
+ type: ["object", "array"],
3930
+ items: { type: "number", minimum: 0 },
3931
+ additionalProperties: { type: "number", minimum: 0 }
3932
+ }
3933
+ },
3934
+ ["sheet", "columnWidths"],
3935
+ "Set every column width of a sheet (0-based column index to pixels; a list is indexed by position); empty removes them."
3936
+ ),
3937
+ variant(
3938
+ "set_frozen",
3939
+ { sheet: sheet(), rows: { type: "integer", minimum: 0 }, cols: { type: "integer", minimum: 0 } },
3940
+ ["sheet", "rows", "cols"],
3941
+ "Set frozen rows and columns."
3942
+ ),
3943
+ variant("set_meta", { meta: nullableObject() }, ["meta"], "Replace the workbook meta, or remove it with null.")
3944
+ ];
3945
+ return {
3946
+ $schema: "http://json-schema.org/draft-07/schema#",
3947
+ title: "Holy Sheet op",
3948
+ description: "One op from Agent::diff, applied by Agent::reduce.",
3949
+ oneOf: variants
3950
+ };
3951
+ }
3952
+ };
3953
+ function variant(type, properties, required, description) {
3954
+ return {
3955
+ type: "object",
3956
+ description,
3957
+ required: ["type", ...required],
3958
+ additionalProperties: false,
3959
+ properties: { type: { const: type }, ...properties }
3960
+ };
3961
+ }
3962
+
3847
3963
  // src/ops/sheet-reducer.ts
3848
3964
  var SheetReducer = {
3849
3965
  /** Sheet keys the granular ops address. A sheet with any other key is authored form. */
@@ -3856,6 +3972,9 @@ var SheetReducer = {
3856
3972
  },
3857
3973
  apply(schema, op) {
3858
3974
  const type = get(op, "type");
3975
+ if (typeof type !== "string" || !SheetOpSchema.TYPES.includes(type)) {
3976
+ return schema;
3977
+ }
3859
3978
  if (type === "set_workbook") {
3860
3979
  return isArr(get(op, "data")) ? op.data : schema;
3861
3980
  }
@@ -3873,14 +3992,22 @@ var SheetReducer = {
3873
3992
  if (!isArr(get(op, "sheet"))) {
3874
3993
  return schema;
3875
3994
  }
3876
- const index = Math.max(0, Math.min(sheets.length, phpInt2(get(op, "index") ?? sheets.length)));
3877
- sheets.splice(index, 0, op.sheet);
3995
+ const index = has(op, "index") ? phpInteger(op.index) : sheets.length;
3996
+ if (index === null) {
3997
+ return schema;
3998
+ }
3999
+ sheets.splice(Math.max(0, Math.min(sheets.length, index)), 0, op.sheet);
3878
4000
  return { ...schema, sheets };
3879
4001
  }
3880
4002
  const at2 = find(sheets, phpString(get(op, "sheet") ?? ""));
3881
4003
  if (at2 === null) {
3882
4004
  return schema;
3883
4005
  }
4006
+ for (const field of POSITION_FIELDS) {
4007
+ if (has(op, field) && phpInteger(op[field]) === null) {
4008
+ return schema;
4009
+ }
4010
+ }
3884
4011
  switch (type) {
3885
4012
  case "remove_sheet":
3886
4013
  sheets.splice(at2, 1);
@@ -3890,7 +4017,7 @@ var SheetReducer = {
3890
4017
  break;
3891
4018
  case "move_sheet": {
3892
4019
  const [moved] = sheets.splice(at2, 1);
3893
- const to = Math.max(0, Math.min(sheets.length, phpInt2(get(op, "toIndex") ?? at2)));
4020
+ const to = Math.max(0, Math.min(sheets.length, phpInteger(get(op, "toIndex") ?? at2) ?? at2));
3894
4021
  sheets.splice(to, 0, moved);
3895
4022
  break;
3896
4023
  }
@@ -3906,8 +4033,8 @@ var SheetReducer = {
3906
4033
  sheets[at2] = setOrUnset(sheets[at2], "columnWidths", get(op, "columnWidths") ?? [], EMPTY);
3907
4034
  break;
3908
4035
  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);
4036
+ sheets[at2] = setOrUnset(sheets[at2], "frozenRows", phpInteger(get(op, "rows") ?? 0) ?? 0, 0);
4037
+ sheets[at2] = setOrUnset(sheets[at2], "frozenCols", phpInteger(get(op, "cols") ?? 0) ?? 0, 0);
3911
4038
  break;
3912
4039
  case "set_cell":
3913
4040
  sheets[at2] = setCell(sheets[at2], op);
@@ -3916,7 +4043,7 @@ var SheetReducer = {
3916
4043
  sheets[at2] = setRange(sheets[at2], op);
3917
4044
  break;
3918
4045
  case "clear_cell": {
3919
- const address = asciiUpper(phpString(get(op, "address") ?? ""));
4046
+ const address = asciiUpper(phpTrim2(phpString(get(op, "address") ?? "")));
3920
4047
  const cells = sheets[at2].cells;
3921
4048
  if (isArr(cells) && !Array.isArray(cells) && has(cells, address)) {
3922
4049
  const copy = { ...cells };
@@ -3926,16 +4053,16 @@ var SheetReducer = {
3926
4053
  break;
3927
4054
  }
3928
4055
  case "insert_rows":
3929
- sheets[at2] = shiftRows(sheets[at2], phpInt2(get(op, "at") ?? 0), Math.max(0, phpInt2(get(op, "count") ?? 0)));
4056
+ sheets[at2] = shiftRows(sheets[at2], position(op, "at"), Math.max(0, position(op, "count")));
3930
4057
  break;
3931
4058
  case "delete_rows":
3932
- sheets[at2] = shiftRows(sheets[at2], phpInt2(get(op, "at") ?? 0), -Math.max(0, phpInt2(get(op, "count") ?? 0)));
4059
+ sheets[at2] = shiftRows(sheets[at2], position(op, "at"), -Math.max(0, position(op, "count")));
3933
4060
  break;
3934
4061
  case "insert_columns":
3935
- sheets[at2] = shiftColumns(sheets[at2], phpInt2(get(op, "at") ?? 0), Math.max(0, phpInt2(get(op, "count") ?? 0)));
4062
+ sheets[at2] = shiftColumns(sheets[at2], position(op, "at"), Math.max(0, position(op, "count")));
3936
4063
  break;
3937
4064
  case "delete_columns":
3938
- sheets[at2] = shiftColumns(sheets[at2], phpInt2(get(op, "at") ?? 0), -Math.max(0, phpInt2(get(op, "count") ?? 0)));
4065
+ sheets[at2] = shiftColumns(sheets[at2], position(op, "at"), -Math.max(0, position(op, "count")));
3939
4066
  break;
3940
4067
  default:
3941
4068
  return schema;
@@ -3943,6 +4070,10 @@ var SheetReducer = {
3943
4070
  return { ...schema, sheets };
3944
4071
  }
3945
4072
  };
4073
+ var POSITION_FIELDS = ["index", "toIndex", "rows", "cols", "at", "count"];
4074
+ function position(op, key) {
4075
+ return phpInteger(get(op, key) ?? 0) ?? 0;
4076
+ }
3946
4077
  var EMPTY = /* @__PURE__ */ Symbol("empty");
3947
4078
  function find(sheets, name) {
3948
4079
  for (let i = 0; i < sheets.length; i++) {
@@ -3962,7 +4093,7 @@ function setOrUnset(sheet, key, value, empty) {
3962
4093
  return out;
3963
4094
  }
3964
4095
  function setCell(sheet, op) {
3965
- const address = asciiUpper(phpString(get(op, "address") ?? ""));
4096
+ const address = asciiUpper(phpTrim2(phpString(get(op, "address") ?? "")));
3966
4097
  if (parseAddress(address) === null) {
3967
4098
  return sheet;
3968
4099
  }
@@ -4042,6 +4173,9 @@ function shiftColumns(sheet, at2, delta) {
4042
4173
  if (isArr(get(sheet, "columnWidths"))) {
4043
4174
  const widths = /* @__PURE__ */ new Map();
4044
4175
  for (const [index, width] of entriesOf(sheet.columnWidths)) {
4176
+ if (!isIndexKey(index)) {
4177
+ continue;
4178
+ }
4045
4179
  const i = phpInt2(index);
4046
4180
  const number = i + 1;
4047
4181
  if (number < at2) {
@@ -4168,6 +4302,21 @@ var SheetDiff = {
4168
4302
  * example `columnWidths` decoded from `{"1":140,"0":120}`) as a map, not a
4169
4303
  * list, and so as different from the same widths in order. A JS object has
4170
4304
  * no such order to compare.
4305
+ *
4306
+ * Throws a `TypeError` on a value JSON cannot hold, as PHP 2.3.2 throws
4307
+ * `JsonException`, rather than calling two such values the same:
4308
+ *
4309
+ * - a non-finite number (PHP: NAN, INF);
4310
+ * - a lone UTF-16 surrogate in a string or a key, which is this runtime's
4311
+ * invalid UTF-8 (a JS string cannot hold invalid UTF-8, and PHP's own
4312
+ * `json_decode` refuses a lone surrogate);
4313
+ * - a function or a symbol, which `JSON.stringify` would silently drop
4314
+ * (a bigint already throws there);
4315
+ * - nesting deeper than PHP's `json_encode` depth of 4096, where every object
4316
+ * and array is a level, an empty one included. The walk is iterative, so
4317
+ * this is PHP's limit and not the call stack's.
4318
+ *
4319
+ * `undefined` stays what JSON makes it: an absent key, and `null` in a list.
4171
4320
  */
4172
4321
  same(a, b) {
4173
4322
  return canon(a) === canon(b);
@@ -4419,19 +4568,19 @@ function lines(sheet, axis) {
4419
4568
  }
4420
4569
  const [col, row] = parsed;
4421
4570
  const line = axis === "row" ? row : col + 1;
4422
- const position = axis === "row" ? col : row;
4571
+ const position2 = axis === "row" ? col : row;
4423
4572
  let group = grouped.get(line);
4424
4573
  if (group === void 0) {
4425
4574
  group = /* @__PURE__ */ new Map();
4426
4575
  grouped.set(line, group);
4427
4576
  }
4428
- group.set(position, cell);
4577
+ group.set(position2, cell);
4429
4578
  max = Math.max(max, line);
4430
4579
  }
4431
4580
  const out = [];
4432
4581
  for (let i = 1; i <= max; i++) {
4433
4582
  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])));
4583
+ out.push(canonEntries(positions.map(([position2, cell]) => [String(position2), cell])));
4435
4584
  }
4436
4585
  return out;
4437
4586
  }
@@ -4458,108 +4607,72 @@ function rowMajor(addresses) {
4458
4607
  return parsed.map(([, , address]) => address);
4459
4608
  }
4460
4609
  function canon(value) {
4461
- if (Array.isArray(value)) {
4462
- return "[" + value.map(canon).join(",") + "]";
4610
+ return isArr(value) ? canonEntries(entriesFor(value)) : scalar(value);
4611
+ }
4612
+ var JSON_DEPTH = 4096;
4613
+ function canonEntries(root) {
4614
+ const stack = [];
4615
+ const open = (key, entries) => {
4616
+ if (stack.length === JSON_DEPTH) {
4617
+ throw unencodable("Maximum stack depth exceeded");
4618
+ }
4619
+ const list = entries.every(([k], i) => k === String(i));
4620
+ if (!list) {
4621
+ entries = entries.slice().sort((x, y) => x[0] < y[0] ? -1 : x[0] > y[0] ? 1 : 0);
4622
+ }
4623
+ stack.push({ key, entries, list, next: 0, parts: [] });
4624
+ };
4625
+ open("", root);
4626
+ for (; ; ) {
4627
+ const top = stack[stack.length - 1];
4628
+ if (top.next < top.entries.length) {
4629
+ const [key, item] = top.entries[top.next++];
4630
+ if (isArr(item)) {
4631
+ open(key, entriesFor(item));
4632
+ } else {
4633
+ top.parts.push(member(top, key, scalar(item)));
4634
+ }
4635
+ continue;
4636
+ }
4637
+ stack.pop();
4638
+ const text = top.list ? "[" + top.parts.join(",") + "]" : "{" + top.parts.join(",") + "}";
4639
+ const parent = stack[stack.length - 1];
4640
+ if (parent === void 0) {
4641
+ return text;
4642
+ }
4643
+ parent.parts.push(member(parent, top.key, text));
4463
4644
  }
4464
- if (isArr(value)) {
4465
- return canonEntries(entriesOf(value));
4645
+ }
4646
+ function entriesFor(value) {
4647
+ return Array.isArray(value) ? Array.from(value, (item, i) => [String(i), item]) : entriesOf(value);
4648
+ }
4649
+ function member(frame, key, text) {
4650
+ return frame.list ? text : quote(key) + ":" + text;
4651
+ }
4652
+ function scalar(value) {
4653
+ switch (typeof value) {
4654
+ case "number":
4655
+ if (!Number.isFinite(value)) {
4656
+ throw unencodable("Inf and NaN cannot be JSON encoded");
4657
+ }
4658
+ break;
4659
+ case "string":
4660
+ return quote(value);
4661
+ case "function":
4662
+ case "symbol":
4663
+ throw unencodable(`a ${typeof value} cannot be JSON encoded`);
4466
4664
  }
4467
4665
  return JSON.stringify(value) ?? "null";
4468
4666
  }
4469
- function canonEntries(entries) {
4470
- if (entries.every(([key], i) => key === String(i))) {
4471
- return "[" + entries.map(([, item]) => canon(item)).join(",") + "]";
4667
+ var LONE_SURROGATE = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/;
4668
+ function quote(text) {
4669
+ if (LONE_SURROGATE.test(text)) {
4670
+ throw unencodable("Single unpaired UTF-16 surrogate");
4472
4671
  }
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(",") + "}";
4672
+ return JSON.stringify(text);
4475
4673
  }
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
- };
4674
+ function unencodable(reason) {
4675
+ return new TypeError(`[holy-sheet] cannot compare a value JSON cannot hold: ${reason}`);
4563
4676
  }
4564
4677
 
4565
4678
  // src/holy-sheet.schema.json
@@ -4926,7 +5039,7 @@ var holy_sheet_schema_default = {
4926
5039
  };
4927
5040
 
4928
5041
  // src/agent.ts
4929
- var VERSION = "2.4.0";
5042
+ var VERSION = "2.4.2";
4930
5043
  function toU8(input) {
4931
5044
  return input instanceof Uint8Array ? input : new Uint8Array(input);
4932
5045
  }