@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.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,
|
|
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 ===
|
|
201
|
-
if (csv[i + 1] ===
|
|
202
|
-
field +=
|
|
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 ===
|
|
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
|
-
|
|
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
|
|
2782
|
+
let quote2 = "";
|
|
2686
2783
|
for (let i = from; i < src.length; i++) {
|
|
2687
2784
|
const ch = src[i];
|
|
2688
|
-
if (
|
|
2689
|
-
if (ch ===
|
|
2785
|
+
if (quote2) {
|
|
2786
|
+
if (ch === quote2) quote2 = "";
|
|
2690
2787
|
} else if (ch === '"' || ch === "'") {
|
|
2691
|
-
|
|
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
|
|
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 =
|
|
3879
|
-
|
|
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,
|
|
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",
|
|
3912
|
-
sheets[at2] = setOrUnset(sheets[at2], "frozenCols",
|
|
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],
|
|
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],
|
|
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],
|
|
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],
|
|
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
|
|
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(
|
|
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(([
|
|
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
|
-
|
|
4464
|
-
|
|
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
|
-
|
|
4467
|
-
|
|
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
|
-
|
|
4472
|
-
|
|
4473
|
-
|
|
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
|
-
|
|
4476
|
-
return "{" + sorted.map(([key, item]) => JSON.stringify(key) + ":" + canon(item)).join(",") + "}";
|
|
4771
|
+
return JSON.stringify(text);
|
|
4477
4772
|
}
|
|
4478
|
-
|
|
4479
|
-
|
|
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.
|
|
5141
|
+
var VERSION = "2.4.3";
|
|
4940
5142
|
function toU8(input) {
|
|
4941
5143
|
return input instanceof Uint8Array ? input : new Uint8Array(input);
|
|
4942
5144
|
}
|