@particle-academy/holy-sheet 2.4.1 → 2.4.3

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.cjs CHANGED
@@ -177,7 +177,7 @@ var CsvBuilder = {
177
177
  return ArrayBuilder.build(dataRows, headers, sheetName2, options);
178
178
  }
179
179
  };
180
- function parseRows(csv, delimiter, quote) {
180
+ function parseRows(csv, delimiter, quote2) {
181
181
  const rows = [];
182
182
  let field = "";
183
183
  let row = [];
@@ -197,9 +197,9 @@ function parseRows(csv, delimiter, quote) {
197
197
  for (let i = 0; i < csv.length; i++) {
198
198
  const ch = csv[i];
199
199
  if (inQuotes) {
200
- if (ch === quote) {
201
- if (csv[i + 1] === quote) {
202
- field += quote;
200
+ if (ch === quote2) {
201
+ if (csv[i + 1] === quote2) {
202
+ field += quote2;
203
203
  i++;
204
204
  } else {
205
205
  inQuotes = false;
@@ -209,7 +209,7 @@ function parseRows(csv, delimiter, quote) {
209
209
  }
210
210
  continue;
211
211
  }
212
- if (ch === quote) {
212
+ if (ch === quote2) {
213
213
  inQuotes = true;
214
214
  started = true;
215
215
  continue;
@@ -287,6 +287,39 @@ var Cell = class {
287
287
  }
288
288
  };
289
289
 
290
+ // src/schema/column-widths.ts
291
+ var ColumnWidths = {
292
+ MAX_INDEX: 16383,
293
+ index(key) {
294
+ if (typeof key === "number") {
295
+ return Number.isInteger(key) && key >= 0 && key <= ColumnWidths.MAX_INDEX ? key : null;
296
+ }
297
+ if (typeof key === "string" && /^[0-9]{1,5}$/.test(key)) {
298
+ const index = parseInt(key, 10);
299
+ return index <= ColumnWidths.MAX_INDEX ? index : null;
300
+ }
301
+ return null;
302
+ },
303
+ width(px) {
304
+ if (typeof px === "number") {
305
+ return Number.isFinite(px) && px >= 0 ? px : null;
306
+ }
307
+ if (typeof px === "string" && /^[0-9]+(\.[0-9]+)?$/.test(px)) {
308
+ return Number(px);
309
+ }
310
+ return null;
311
+ },
312
+ /**
313
+ * A column letter key (`"B"`, `"aa"`) as an index, for repair only. One or two
314
+ * letters (A to ZZ): a longer run like `"abc"` is more likely a mistake than
315
+ * column ABC, and a repair must not guess.
316
+ */
317
+ fromLetters(key) {
318
+ if (typeof key !== "string" || !/^[A-Za-z]{1,2}$/.test(key.trim())) return null;
319
+ return CellAddress.index(key.trim());
320
+ }
321
+ };
322
+
290
323
  // src/workbook/cell-comment.ts
291
324
  var CellComment = class {
292
325
  constructor(text, author = null, color2 = null) {
@@ -631,7 +664,10 @@ var Normalizer = class {
631
664
  normalizeColumnWidths(widths) {
632
665
  const out = /* @__PURE__ */ new Map();
633
666
  for (const [key, px] of Object.entries(widths)) {
634
- out.set(parseInt(key, 10), Number(px));
667
+ const index = ColumnWidths.index(key);
668
+ const width = ColumnWidths.width(px);
669
+ if (index === null || width === null) continue;
670
+ out.set(index, width);
635
671
  }
636
672
  return out;
637
673
  }
@@ -1329,6 +1365,30 @@ var Repairer = class {
1329
1365
  sheet.theme = "default";
1330
1366
  this.repairs.push(`changed '${path}.theme' from '${original}' to 'default' (unknown theme)`);
1331
1367
  }
1368
+ if (sheet.columnWidths != null && (isPlainObject(sheet.columnWidths) || Array.isArray(sheet.columnWidths))) {
1369
+ const widths = {};
1370
+ let changed = false;
1371
+ for (const [key, px] of Object.entries(sheet.columnWidths)) {
1372
+ let index = ColumnWidths.index(key);
1373
+ if (index === null) {
1374
+ const letters = ColumnWidths.fromLetters(key);
1375
+ if (letters !== null) {
1376
+ index = letters;
1377
+ this.repairs.push(`converted '${path}.columnWidths.${key}' to column index ${index}`);
1378
+ changed = true;
1379
+ }
1380
+ }
1381
+ if (index === null || ColumnWidths.width(px) === null) {
1382
+ this.repairs.push(`dropped '${path}.columnWidths.${key}' (not a column index and a width)`);
1383
+ changed = true;
1384
+ continue;
1385
+ }
1386
+ widths[String(index)] = px;
1387
+ }
1388
+ if (changed) {
1389
+ sheet.columnWidths = widths;
1390
+ }
1391
+ }
1332
1392
  if (sheet.cells != null && isPlainObject(sheet.cells)) {
1333
1393
  const cleaned = {};
1334
1394
  let changed = false;
@@ -1534,6 +1594,43 @@ var Validator = class {
1534
1594
  )
1535
1595
  );
1536
1596
  }
1597
+ if (sheet.columnWidths != null) {
1598
+ if (!isPlainObject(sheet.columnWidths) && !Array.isArray(sheet.columnWidths)) {
1599
+ errors.push(
1600
+ error(
1601
+ `${path}.columnWidths`,
1602
+ "object keyed by 0-based column index",
1603
+ typeOf(sheet.columnWidths),
1604
+ sheet.columnWidths,
1605
+ 'Column widths map a 0-based column index to pixels: {"0": 120, "1": 80}.'
1606
+ )
1607
+ );
1608
+ } else {
1609
+ for (const [key, px] of Object.entries(sheet.columnWidths)) {
1610
+ if (ColumnWidths.index(key) === null) {
1611
+ errors.push(
1612
+ error(
1613
+ `${path}.columnWidths.${key}`,
1614
+ `a 0-based column index from 0 to ${ColumnWidths.MAX_INDEX}`,
1615
+ typeOf(key),
1616
+ key,
1617
+ 'Keys are 0-based column indexes: "0" is column A, "1" is column B. Use the index, not the letter.'
1618
+ )
1619
+ );
1620
+ } else if (ColumnWidths.width(px) === null) {
1621
+ errors.push(
1622
+ error(
1623
+ `${path}.columnWidths.${key}`,
1624
+ "a non-negative number of pixels",
1625
+ typeOf(px),
1626
+ px,
1627
+ "A width is a number of pixels, like 120."
1628
+ )
1629
+ );
1630
+ }
1631
+ }
1632
+ }
1633
+ }
1537
1634
  return errors;
1538
1635
  }
1539
1636
  validateColumn(col, path) {
@@ -2682,13 +2779,13 @@ function unescapeXml(s) {
2682
2779
  });
2683
2780
  }
2684
2781
  function findTagEnd(src, from) {
2685
- let quote = "";
2782
+ let quote2 = "";
2686
2783
  for (let i = from; i < src.length; i++) {
2687
2784
  const ch = src[i];
2688
- if (quote) {
2689
- if (ch === quote) quote = "";
2785
+ if (quote2) {
2786
+ if (ch === quote2) quote2 = "";
2690
2787
  } else if (ch === '"' || ch === "'") {
2691
- quote = ch;
2788
+ quote2 = ch;
2692
2789
  } else if (ch === ">") {
2693
2790
  return i;
2694
2791
  }
@@ -3836,16 +3933,132 @@ function phpString(value) {
3836
3933
  if (isArr(value)) return "Array";
3837
3934
  return String(value);
3838
3935
  }
3936
+ function phpTrim2(value) {
3937
+ return value.replace(/^[ \t\n\r\x00\x0B]+|[ \t\n\r\x00\x0B]+$/g, "");
3938
+ }
3939
+ function phpInteger(value) {
3940
+ if (typeof value === "number") {
3941
+ return Number.isInteger(value) && Math.abs(value) <= 2 ** 63 ? value || 0 : null;
3942
+ }
3943
+ return typeof value === "string" && /^[0-9]+$/.test(value) ? phpInt2(value) : null;
3944
+ }
3945
+ function isIndexKey(key) {
3946
+ return /^[0-9]+$/.test(key) || /^-[1-9][0-9]*$/.test(key) && BigInt(key) >= -(2n ** 63n);
3947
+ }
3839
3948
  function asciiUpper(value) {
3840
3949
  return value.replace(/[a-z]+/g, (s) => s.toUpperCase());
3841
3950
  }
3842
3951
  function parseAddress(address) {
3843
- const m = /^([A-Z]+)(\d+)$/.exec(asciiUpper(address.replace(/^[ \t\n\r\v\0]+|[ \t\n\r\v\0]+$/g, "")));
3952
+ const m = /^([A-Z]+)(\d+)$/.exec(asciiUpper(phpTrim2(address)));
3844
3953
  if (!m) return null;
3845
3954
  return [CellAddress.index(m[1]), parseInt(m[2], 10)];
3846
3955
  }
3847
3956
  var letter = CellAddress.letter;
3848
3957
 
3958
+ // src/ops/sheet-op-schema.ts
3959
+ var SheetOpSchema = {
3960
+ /** Every op type, in the order the variants are listed. */
3961
+ TYPES: [
3962
+ "set_cell",
3963
+ "set_range",
3964
+ "set_workbook",
3965
+ "clear_cell",
3966
+ "insert_rows",
3967
+ "delete_rows",
3968
+ "insert_columns",
3969
+ "delete_columns",
3970
+ "add_sheet",
3971
+ "remove_sheet",
3972
+ "rename_sheet",
3973
+ "move_sheet",
3974
+ "replace_sheet",
3975
+ "set_merged_regions",
3976
+ "set_column_widths",
3977
+ "set_frozen",
3978
+ "set_meta"
3979
+ ],
3980
+ /** A fresh object on every call, so a caller editing it changes nothing here. */
3981
+ jsonSchema() {
3982
+ const address = () => ({ type: "string", pattern: "^[A-Za-z]+[0-9]+$" });
3983
+ const sheet = () => ({ type: "string", minLength: 1 });
3984
+ const count = () => ({ type: "integer", minimum: 1 });
3985
+ const position2 = () => ({ type: "integer", minimum: 1 });
3986
+ const object = () => ({ type: "object" });
3987
+ const nullableObject = () => ({ type: ["object", "null"] });
3988
+ const value = () => ({ type: ["string", "number", "boolean", "null"] });
3989
+ const variants = [
3990
+ variant(
3991
+ "set_cell",
3992
+ { sheet: sheet(), address: address(), value: value(), formula: { type: "string" }, computedValue: value(), format: nullableObject(), comment: nullableObject() },
3993
+ ["sheet", "address"],
3994
+ "Write one cell. Omitting formula or computedValue clears it; omitting format or comment keeps it; null clears it."
3995
+ ),
3996
+ variant(
3997
+ "set_range",
3998
+ { sheet: sheet(), start: address(), end: address(), values: { type: "array", items: { type: "array", items: value() } } },
3999
+ ["sheet", "start", "values"],
4000
+ "Write a block of values row-major from start, each as a set_cell with no formula."
4001
+ ),
4002
+ variant("set_workbook", { data: object() }, ["data"], "Replace the whole workbook schema."),
4003
+ variant("clear_cell", { sheet: sheet(), address: address() }, ["sheet", "address"], "Remove one cell."),
4004
+ variant("insert_rows", { sheet: sheet(), at: position2(), count: count() }, ["sheet", "at", "count"], "Insert rows before 1-based row `at`; cells, merges below move down."),
4005
+ 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."),
4006
+ 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."),
4007
+ 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."),
4008
+ variant("add_sheet", { index: { type: "integer", minimum: 0 }, sheet: object() }, ["index", "sheet"], "Insert a sheet at a 0-based position."),
4009
+ variant("remove_sheet", { sheet: sheet() }, ["sheet"], "Remove a sheet by name."),
4010
+ variant("rename_sheet", { sheet: sheet(), name: sheet() }, ["sheet", "name"], "Rename a sheet."),
4011
+ variant("move_sheet", { sheet: sheet(), toIndex: { type: "integer", minimum: 0 } }, ["sheet", "toIndex"], "Move a sheet to a 0-based position."),
4012
+ variant("replace_sheet", { sheet: sheet(), data: object() }, ["sheet", "data"], "Replace one sheet whole."),
4013
+ variant(
4014
+ "set_merged_regions",
4015
+ { sheet: sheet(), mergedRegions: { type: "array", items: { type: "object", required: ["start", "end"], properties: { start: address(), end: address() } } } },
4016
+ ["sheet", "mergedRegions"],
4017
+ "Set every merged region of a sheet."
4018
+ ),
4019
+ // A LIST too, as PHP 2.3.2 declares. PHP encodes a map whose keys run
4020
+ // 0..n-1 as a JSON list, so widths for columns A, B and C arrive as
4021
+ // `[120, 80, 140]` and no widths as `[]`; this port passes such a list
4022
+ // through a diff unchanged. A list's position is the column index.
4023
+ variant(
4024
+ "set_column_widths",
4025
+ {
4026
+ sheet: sheet(),
4027
+ columnWidths: {
4028
+ type: ["object", "array"],
4029
+ items: { type: "number", minimum: 0 },
4030
+ additionalProperties: { type: "number", minimum: 0 }
4031
+ }
4032
+ },
4033
+ ["sheet", "columnWidths"],
4034
+ "Set every column width of a sheet (0-based column index to pixels; a list is indexed by position); empty removes them."
4035
+ ),
4036
+ variant(
4037
+ "set_frozen",
4038
+ { sheet: sheet(), rows: { type: "integer", minimum: 0 }, cols: { type: "integer", minimum: 0 } },
4039
+ ["sheet", "rows", "cols"],
4040
+ "Set frozen rows and columns."
4041
+ ),
4042
+ variant("set_meta", { meta: nullableObject() }, ["meta"], "Replace the workbook meta, or remove it with null.")
4043
+ ];
4044
+ return {
4045
+ $schema: "http://json-schema.org/draft-07/schema#",
4046
+ title: "Holy Sheet op",
4047
+ description: "One op from Agent::diff, applied by Agent::reduce.",
4048
+ oneOf: variants
4049
+ };
4050
+ }
4051
+ };
4052
+ function variant(type, properties, required, description) {
4053
+ return {
4054
+ type: "object",
4055
+ description,
4056
+ required: ["type", ...required],
4057
+ additionalProperties: false,
4058
+ properties: { type: { const: type }, ...properties }
4059
+ };
4060
+ }
4061
+
3849
4062
  // src/ops/sheet-reducer.ts
3850
4063
  var SheetReducer = {
3851
4064
  /** Sheet keys the granular ops address. A sheet with any other key is authored form. */
@@ -3858,6 +4071,9 @@ var SheetReducer = {
3858
4071
  },
3859
4072
  apply(schema, op) {
3860
4073
  const type = get(op, "type");
4074
+ if (typeof type !== "string" || !SheetOpSchema.TYPES.includes(type)) {
4075
+ return schema;
4076
+ }
3861
4077
  if (type === "set_workbook") {
3862
4078
  return isArr(get(op, "data")) ? op.data : schema;
3863
4079
  }
@@ -3875,14 +4091,22 @@ var SheetReducer = {
3875
4091
  if (!isArr(get(op, "sheet"))) {
3876
4092
  return schema;
3877
4093
  }
3878
- const index = Math.max(0, Math.min(sheets.length, phpInt2(get(op, "index") ?? sheets.length)));
3879
- sheets.splice(index, 0, op.sheet);
4094
+ const index = has(op, "index") ? phpInteger(op.index) : sheets.length;
4095
+ if (index === null) {
4096
+ return schema;
4097
+ }
4098
+ sheets.splice(Math.max(0, Math.min(sheets.length, index)), 0, op.sheet);
3880
4099
  return { ...schema, sheets };
3881
4100
  }
3882
4101
  const at2 = find(sheets, phpString(get(op, "sheet") ?? ""));
3883
4102
  if (at2 === null) {
3884
4103
  return schema;
3885
4104
  }
4105
+ for (const field of POSITION_FIELDS) {
4106
+ if (has(op, field) && phpInteger(op[field]) === null) {
4107
+ return schema;
4108
+ }
4109
+ }
3886
4110
  switch (type) {
3887
4111
  case "remove_sheet":
3888
4112
  sheets.splice(at2, 1);
@@ -3892,7 +4116,7 @@ var SheetReducer = {
3892
4116
  break;
3893
4117
  case "move_sheet": {
3894
4118
  const [moved] = sheets.splice(at2, 1);
3895
- const to = Math.max(0, Math.min(sheets.length, phpInt2(get(op, "toIndex") ?? at2)));
4119
+ const to = Math.max(0, Math.min(sheets.length, phpInteger(get(op, "toIndex") ?? at2) ?? at2));
3896
4120
  sheets.splice(to, 0, moved);
3897
4121
  break;
3898
4122
  }
@@ -3908,8 +4132,8 @@ var SheetReducer = {
3908
4132
  sheets[at2] = setOrUnset(sheets[at2], "columnWidths", get(op, "columnWidths") ?? [], EMPTY);
3909
4133
  break;
3910
4134
  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);
4135
+ sheets[at2] = setOrUnset(sheets[at2], "frozenRows", phpInteger(get(op, "rows") ?? 0) ?? 0, 0);
4136
+ sheets[at2] = setOrUnset(sheets[at2], "frozenCols", phpInteger(get(op, "cols") ?? 0) ?? 0, 0);
3913
4137
  break;
3914
4138
  case "set_cell":
3915
4139
  sheets[at2] = setCell(sheets[at2], op);
@@ -3918,7 +4142,7 @@ var SheetReducer = {
3918
4142
  sheets[at2] = setRange(sheets[at2], op);
3919
4143
  break;
3920
4144
  case "clear_cell": {
3921
- const address = asciiUpper(phpString(get(op, "address") ?? ""));
4145
+ const address = asciiUpper(phpTrim2(phpString(get(op, "address") ?? "")));
3922
4146
  const cells = sheets[at2].cells;
3923
4147
  if (isArr(cells) && !Array.isArray(cells) && has(cells, address)) {
3924
4148
  const copy = { ...cells };
@@ -3928,16 +4152,16 @@ var SheetReducer = {
3928
4152
  break;
3929
4153
  }
3930
4154
  case "insert_rows":
3931
- sheets[at2] = shiftRows(sheets[at2], phpInt2(get(op, "at") ?? 0), Math.max(0, phpInt2(get(op, "count") ?? 0)));
4155
+ sheets[at2] = shiftRows(sheets[at2], position(op, "at"), Math.max(0, position(op, "count")));
3932
4156
  break;
3933
4157
  case "delete_rows":
3934
- sheets[at2] = shiftRows(sheets[at2], phpInt2(get(op, "at") ?? 0), -Math.max(0, phpInt2(get(op, "count") ?? 0)));
4158
+ sheets[at2] = shiftRows(sheets[at2], position(op, "at"), -Math.max(0, position(op, "count")));
3935
4159
  break;
3936
4160
  case "insert_columns":
3937
- sheets[at2] = shiftColumns(sheets[at2], phpInt2(get(op, "at") ?? 0), Math.max(0, phpInt2(get(op, "count") ?? 0)));
4161
+ sheets[at2] = shiftColumns(sheets[at2], position(op, "at"), Math.max(0, position(op, "count")));
3938
4162
  break;
3939
4163
  case "delete_columns":
3940
- sheets[at2] = shiftColumns(sheets[at2], phpInt2(get(op, "at") ?? 0), -Math.max(0, phpInt2(get(op, "count") ?? 0)));
4164
+ sheets[at2] = shiftColumns(sheets[at2], position(op, "at"), -Math.max(0, position(op, "count")));
3941
4165
  break;
3942
4166
  default:
3943
4167
  return schema;
@@ -3945,6 +4169,10 @@ var SheetReducer = {
3945
4169
  return { ...schema, sheets };
3946
4170
  }
3947
4171
  };
4172
+ var POSITION_FIELDS = ["index", "toIndex", "rows", "cols", "at", "count"];
4173
+ function position(op, key) {
4174
+ return phpInteger(get(op, key) ?? 0) ?? 0;
4175
+ }
3948
4176
  var EMPTY = /* @__PURE__ */ Symbol("empty");
3949
4177
  function find(sheets, name) {
3950
4178
  for (let i = 0; i < sheets.length; i++) {
@@ -3964,7 +4192,7 @@ function setOrUnset(sheet, key, value, empty) {
3964
4192
  return out;
3965
4193
  }
3966
4194
  function setCell(sheet, op) {
3967
- const address = asciiUpper(phpString(get(op, "address") ?? ""));
4195
+ const address = asciiUpper(phpTrim2(phpString(get(op, "address") ?? "")));
3968
4196
  if (parseAddress(address) === null) {
3969
4197
  return sheet;
3970
4198
  }
@@ -4044,6 +4272,9 @@ function shiftColumns(sheet, at2, delta) {
4044
4272
  if (isArr(get(sheet, "columnWidths"))) {
4045
4273
  const widths = /* @__PURE__ */ new Map();
4046
4274
  for (const [index, width] of entriesOf(sheet.columnWidths)) {
4275
+ if (!isIndexKey(index)) {
4276
+ continue;
4277
+ }
4047
4278
  const i = phpInt2(index);
4048
4279
  const number = i + 1;
4049
4280
  if (number < at2) {
@@ -4170,6 +4401,21 @@ var SheetDiff = {
4170
4401
  * example `columnWidths` decoded from `{"1":140,"0":120}`) as a map, not a
4171
4402
  * list, and so as different from the same widths in order. A JS object has
4172
4403
  * no such order to compare.
4404
+ *
4405
+ * Throws a `TypeError` on a value JSON cannot hold, as PHP 2.3.2 throws
4406
+ * `JsonException`, rather than calling two such values the same:
4407
+ *
4408
+ * - a non-finite number (PHP: NAN, INF);
4409
+ * - a lone UTF-16 surrogate in a string or a key, which is this runtime's
4410
+ * invalid UTF-8 (a JS string cannot hold invalid UTF-8, and PHP's own
4411
+ * `json_decode` refuses a lone surrogate);
4412
+ * - a function or a symbol, which `JSON.stringify` would silently drop
4413
+ * (a bigint already throws there);
4414
+ * - nesting deeper than PHP's `json_encode` depth of 4096, where every object
4415
+ * and array is a level, an empty one included. The walk is iterative, so
4416
+ * this is PHP's limit and not the call stack's.
4417
+ *
4418
+ * `undefined` stays what JSON makes it: an absent key, and `null` in a list.
4173
4419
  */
4174
4420
  same(a, b) {
4175
4421
  return canon(a) === canon(b);
@@ -4421,19 +4667,19 @@ function lines(sheet, axis) {
4421
4667
  }
4422
4668
  const [col, row] = parsed;
4423
4669
  const line = axis === "row" ? row : col + 1;
4424
- const position = axis === "row" ? col : row;
4670
+ const position2 = axis === "row" ? col : row;
4425
4671
  let group = grouped.get(line);
4426
4672
  if (group === void 0) {
4427
4673
  group = /* @__PURE__ */ new Map();
4428
4674
  grouped.set(line, group);
4429
4675
  }
4430
- group.set(position, cell);
4676
+ group.set(position2, cell);
4431
4677
  max = Math.max(max, line);
4432
4678
  }
4433
4679
  const out = [];
4434
4680
  for (let i = 1; i <= max; i++) {
4435
4681
  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])));
4682
+ out.push(canonEntries(positions.map(([position2, cell]) => [String(position2), cell])));
4437
4683
  }
4438
4684
  return out;
4439
4685
  }
@@ -4460,116 +4706,72 @@ function rowMajor(addresses) {
4460
4706
  return parsed.map(([, , address]) => address);
4461
4707
  }
4462
4708
  function canon(value) {
4463
- if (Array.isArray(value)) {
4464
- return "[" + value.map(canon).join(",") + "]";
4709
+ return isArr(value) ? canonEntries(entriesFor(value)) : scalar(value);
4710
+ }
4711
+ var JSON_DEPTH = 4096;
4712
+ function canonEntries(root) {
4713
+ const stack = [];
4714
+ const open = (key, entries) => {
4715
+ if (stack.length === JSON_DEPTH) {
4716
+ throw unencodable("Maximum stack depth exceeded");
4717
+ }
4718
+ const list = entries.every(([k], i) => k === String(i));
4719
+ if (!list) {
4720
+ entries = entries.slice().sort((x, y) => x[0] < y[0] ? -1 : x[0] > y[0] ? 1 : 0);
4721
+ }
4722
+ stack.push({ key, entries, list, next: 0, parts: [] });
4723
+ };
4724
+ open("", root);
4725
+ for (; ; ) {
4726
+ const top = stack[stack.length - 1];
4727
+ if (top.next < top.entries.length) {
4728
+ const [key, item] = top.entries[top.next++];
4729
+ if (isArr(item)) {
4730
+ open(key, entriesFor(item));
4731
+ } else {
4732
+ top.parts.push(member(top, key, scalar(item)));
4733
+ }
4734
+ continue;
4735
+ }
4736
+ stack.pop();
4737
+ const text = top.list ? "[" + top.parts.join(",") + "]" : "{" + top.parts.join(",") + "}";
4738
+ const parent = stack[stack.length - 1];
4739
+ if (parent === void 0) {
4740
+ return text;
4741
+ }
4742
+ parent.parts.push(member(parent, top.key, text));
4465
4743
  }
4466
- if (isArr(value)) {
4467
- return canonEntries(entriesOf(value));
4744
+ }
4745
+ function entriesFor(value) {
4746
+ return Array.isArray(value) ? Array.from(value, (item, i) => [String(i), item]) : entriesOf(value);
4747
+ }
4748
+ function member(frame, key, text) {
4749
+ return frame.list ? text : quote(key) + ":" + text;
4750
+ }
4751
+ function scalar(value) {
4752
+ switch (typeof value) {
4753
+ case "number":
4754
+ if (!Number.isFinite(value)) {
4755
+ throw unencodable("Inf and NaN cannot be JSON encoded");
4756
+ }
4757
+ break;
4758
+ case "string":
4759
+ return quote(value);
4760
+ case "function":
4761
+ case "symbol":
4762
+ throw unencodable(`a ${typeof value} cannot be JSON encoded`);
4468
4763
  }
4469
4764
  return JSON.stringify(value) ?? "null";
4470
4765
  }
4471
- function canonEntries(entries) {
4472
- if (entries.every(([key], i) => key === String(i))) {
4473
- return "[" + entries.map(([, item]) => canon(item)).join(",") + "]";
4766
+ var LONE_SURROGATE = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/;
4767
+ function quote(text) {
4768
+ if (LONE_SURROGATE.test(text)) {
4769
+ throw unencodable("Single unpaired UTF-16 surrogate");
4474
4770
  }
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(",") + "}";
4771
+ return JSON.stringify(text);
4477
4772
  }
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
- // An EMPTY array too, as PHP 2.3.1 declares: PHP encodes an empty map as
4541
- // `[]`, which its diff emits when every width is removed. This port emits
4542
- // `{}`. Both mean no widths.
4543
- variant(
4544
- "set_column_widths",
4545
- { sheet: sheet(), columnWidths: { type: ["object", "array"], maxItems: 0 } },
4546
- ["sheet", "columnWidths"],
4547
- "Set every column width of a sheet (0-based column index to pixels); empty removes them."
4548
- ),
4549
- variant(
4550
- "set_frozen",
4551
- { sheet: sheet(), rows: { type: "integer", minimum: 0 }, cols: { type: "integer", minimum: 0 } },
4552
- ["sheet", "rows", "cols"],
4553
- "Set frozen rows and columns."
4554
- ),
4555
- variant("set_meta", { meta: nullableObject() }, ["meta"], "Replace the workbook meta, or remove it with null.")
4556
- ];
4557
- return {
4558
- $schema: "http://json-schema.org/draft-07/schema#",
4559
- title: "Holy Sheet op",
4560
- description: "One op from Agent::diff, applied by Agent::reduce.",
4561
- oneOf: variants
4562
- };
4563
- }
4564
- };
4565
- function variant(type, properties, required, description) {
4566
- return {
4567
- type: "object",
4568
- description,
4569
- required: ["type", ...required],
4570
- additionalProperties: false,
4571
- properties: { type: { const: type }, ...properties }
4572
- };
4773
+ function unencodable(reason) {
4774
+ return new TypeError(`[holy-sheet] cannot compare a value JSON cannot hold: ${reason}`);
4573
4775
  }
4574
4776
 
4575
4777
  // src/holy-sheet.schema.json
@@ -4936,7 +5138,7 @@ var holy_sheet_schema_default = {
4936
5138
  };
4937
5139
 
4938
5140
  // src/agent.ts
4939
- var VERSION = "2.4.1";
5141
+ var VERSION = "2.4.3";
4940
5142
  function toU8(input) {
4941
5143
  return input instanceof Uint8Array ? input : new Uint8Array(input);
4942
5144
  }