@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 +332 -130
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +17 -2
- package/dist/index.d.ts +17 -2
- package/dist/index.js +332 -130
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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,
|
|
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 ===
|
|
199
|
-
if (csv[i + 1] ===
|
|
200
|
-
field +=
|
|
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 ===
|
|
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
|
-
|
|
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
|
|
2780
|
+
let quote2 = "";
|
|
2684
2781
|
for (let i = from; i < src.length; i++) {
|
|
2685
2782
|
const ch = src[i];
|
|
2686
|
-
if (
|
|
2687
|
-
if (ch ===
|
|
2783
|
+
if (quote2) {
|
|
2784
|
+
if (ch === quote2) quote2 = "";
|
|
2688
2785
|
} else if (ch === '"' || ch === "'") {
|
|
2689
|
-
|
|
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
|
|
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 =
|
|
3877
|
-
|
|
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,
|
|
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",
|
|
3910
|
-
sheets[at2] = setOrUnset(sheets[at2], "frozenCols",
|
|
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],
|
|
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],
|
|
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],
|
|
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],
|
|
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
|
|
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(
|
|
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(([
|
|
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
|
-
|
|
4462
|
-
|
|
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
|
-
|
|
4465
|
-
|
|
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
|
-
|
|
4470
|
-
|
|
4471
|
-
|
|
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
|
-
|
|
4474
|
-
return "{" + sorted.map(([key, item]) => JSON.stringify(key) + ":" + canon(item)).join(",") + "}";
|
|
4769
|
+
return JSON.stringify(text);
|
|
4475
4770
|
}
|
|
4476
|
-
|
|
4477
|
-
|
|
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.
|
|
5139
|
+
var VERSION = "2.4.3";
|
|
4938
5140
|
function toU8(input) {
|
|
4939
5141
|
return input instanceof Uint8Array ? input : new Uint8Array(input);
|
|
4940
5142
|
}
|