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