bireactive 0.2.4 → 0.3.0
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/animation/anim.js +4 -0
- package/dist/coll.d.ts +7 -7
- package/dist/core/cell.d.ts +89 -66
- package/dist/core/cell.js +642 -401
- package/dist/core/index.d.ts +4 -14
- package/dist/core/index.js +4 -14
- package/dist/core/lenses/aggregates.d.ts +1 -1
- package/dist/core/lenses/aggregates.js +4 -3
- package/dist/core/lenses/closed-form-policies.js +6 -6
- package/dist/core/lenses/decompositions.js +3 -3
- package/dist/core/lenses/domain-aggregates.js +5 -5
- package/dist/core/lenses/geometry.d.ts +1 -1
- package/dist/core/lenses/geometry.js +6 -7
- package/dist/core/lenses/memory.d.ts +2 -2
- package/dist/core/lenses/memory.js +3 -3
- package/dist/core/lenses/typed-factor.js +4 -3
- package/dist/core/traits.d.ts +1 -0
- package/dist/core/values/box.js +7 -7
- package/dist/core/values/color.js +5 -5
- package/dist/core/values/field.d.ts +70 -0
- package/dist/core/values/field.js +230 -0
- package/dist/core/values/gpu.d.ts +4 -2
- package/dist/core/values/gpu.js +11 -4
- package/dist/core/values/matrix.js +7 -7
- package/dist/core/values/num.d.ts +1 -1
- package/dist/core/values/num.js +1 -1
- package/dist/core/values/pose.js +4 -4
- package/dist/core/values/range.js +6 -6
- package/dist/core/values/template.d.ts +1 -1
- package/dist/core/values/template.js +2 -1
- package/dist/core/values/transform.js +7 -7
- package/dist/core/values/tri.js +3 -3
- package/dist/core/values/vec.js +8 -12
- package/dist/ext/timeline.js +2 -2
- package/dist/formats/cst.d.ts +127 -0
- package/dist/formats/cst.js +280 -0
- package/dist/formats/edn.d.ts +2 -0
- package/dist/formats/edn.js +301 -0
- package/dist/formats/index.d.ts +6 -0
- package/dist/formats/index.js +8 -0
- package/dist/formats/json.d.ts +2 -0
- package/dist/formats/json.js +332 -0
- package/dist/formats/lens.d.ts +8 -0
- package/dist/formats/lens.js +54 -0
- package/dist/formats/toml.d.ts +2 -0
- package/dist/formats/toml.js +526 -0
- package/dist/formats/yaml.d.ts +2 -0
- package/dist/formats/yaml.js +661 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +10 -0
- package/dist/learn/data.d.ts +49 -0
- package/dist/learn/data.js +181 -0
- package/dist/learn/index.d.ts +3 -0
- package/dist/learn/index.js +6 -0
- package/dist/learn/lens-net.d.ts +63 -0
- package/dist/learn/lens-net.js +219 -0
- package/dist/learn/mlp.d.ts +77 -0
- package/dist/learn/mlp.js +292 -0
- package/dist/propagators/csp.d.ts +13 -0
- package/dist/propagators/csp.js +52 -0
- package/dist/propagators/flex.d.ts +31 -0
- package/dist/propagators/flex.js +189 -0
- package/dist/propagators/graph.d.ts +73 -0
- package/dist/propagators/graph.js +543 -0
- package/dist/propagators/index.d.ts +8 -6
- package/dist/propagators/index.js +15 -6
- package/dist/propagators/lattice.d.ts +45 -0
- package/dist/propagators/lattice.js +113 -0
- package/dist/propagators/layout.d.ts +1 -27
- package/dist/propagators/layout.js +6 -175
- package/dist/propagators/numeric.d.ts +17 -0
- package/dist/propagators/numeric.js +93 -0
- package/dist/propagators/solver.d.ts +51 -0
- package/dist/propagators/solver.js +175 -0
- package/dist/schema/index.d.ts +1 -0
- package/dist/schema/index.js +3 -0
- package/dist/schema/lens.d.ts +121 -0
- package/dist/schema/lens.js +429 -0
- package/dist/shapes/annular-sector.js +4 -4
- package/dist/shapes/button.js +1 -1
- package/dist/shapes/circle.js +1 -1
- package/dist/shapes/handle.js +2 -2
- package/dist/shapes/label.js +1 -1
- package/dist/shapes/layout.js +2 -2
- package/dist/shapes/rect.js +7 -7
- package/dist/shapes/shape.js +8 -8
- package/dist/web/diagram.js +2 -2
- package/package.json +1 -1
- package/dist/propagators/network.d.ts +0 -52
- package/dist/propagators/network.js +0 -185
- package/dist/propagators/propagator.d.ts +0 -12
- package/dist/propagators/propagator.js +0 -16
- package/dist/propagators/range.d.ts +0 -45
- package/dist/propagators/range.js +0 -147
- package/dist/propagators/relations.d.ts +0 -60
- package/dist/propagators/relations.js +0 -343
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
// edn.ts — tolerant EDN adapter (maps, vectors, strings, numbers,
|
|
2
|
+
// booleans, nil, keywords). Keyword map keys project to plain string
|
|
3
|
+
// keys in the abstract value; commas are whitespace; `;` comments run
|
|
4
|
+
// to end of line. Recovery mirrors the JSON adapter: garbage runs to
|
|
5
|
+
// the next delimiter at depth zero become ErrorNodes.
|
|
6
|
+
import { indentOf, } from "./cst.js";
|
|
7
|
+
const WS = /[ \t\r\n,]/;
|
|
8
|
+
const NUM = /^[-+]?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?/;
|
|
9
|
+
const KEYWORD = /^:[A-Za-z_*+!?<>=.][\w*+!?<>=.-]*/;
|
|
10
|
+
const SAFE_KEY = /^[A-Za-z_][\w-]*$/;
|
|
11
|
+
class P {
|
|
12
|
+
text;
|
|
13
|
+
pos = 0;
|
|
14
|
+
errors = [];
|
|
15
|
+
constructor(text) {
|
|
16
|
+
this.text = text;
|
|
17
|
+
}
|
|
18
|
+
err(start, end, message) {
|
|
19
|
+
this.errors.push({ start, end, message });
|
|
20
|
+
}
|
|
21
|
+
skipWs() {
|
|
22
|
+
for (;;) {
|
|
23
|
+
while (this.pos < this.text.length && WS.test(this.text[this.pos]))
|
|
24
|
+
this.pos++;
|
|
25
|
+
if (this.text[this.pos] === ";") {
|
|
26
|
+
while (this.pos < this.text.length && this.text[this.pos] !== "\n")
|
|
27
|
+
this.pos++;
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
peek() {
|
|
34
|
+
return this.text[this.pos];
|
|
35
|
+
}
|
|
36
|
+
garbage(start, message) {
|
|
37
|
+
let depth = 0;
|
|
38
|
+
while (this.pos < this.text.length) {
|
|
39
|
+
const c = this.text[this.pos];
|
|
40
|
+
if (c === '"') {
|
|
41
|
+
this.pos++;
|
|
42
|
+
while (this.pos < this.text.length) {
|
|
43
|
+
const s = this.text[this.pos];
|
|
44
|
+
if (s === "\\")
|
|
45
|
+
this.pos += 2;
|
|
46
|
+
else if (s === '"' || s === "\n") {
|
|
47
|
+
this.pos++;
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
else
|
|
51
|
+
this.pos++;
|
|
52
|
+
}
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (c === "{" || c === "[" || c === "(")
|
|
56
|
+
depth++;
|
|
57
|
+
else if (c === "}" || c === "]" || c === ")") {
|
|
58
|
+
if (depth === 0)
|
|
59
|
+
break;
|
|
60
|
+
depth--;
|
|
61
|
+
}
|
|
62
|
+
else if (depth === 0 && WS.test(c))
|
|
63
|
+
break;
|
|
64
|
+
this.pos++;
|
|
65
|
+
}
|
|
66
|
+
let end = this.pos;
|
|
67
|
+
if (end === start)
|
|
68
|
+
end = Math.min(start + 1, this.text.length);
|
|
69
|
+
this.err(start, end, message);
|
|
70
|
+
return { kind: "error", start, end };
|
|
71
|
+
}
|
|
72
|
+
scanString() {
|
|
73
|
+
const start = this.pos;
|
|
74
|
+
this.pos++;
|
|
75
|
+
let out = "";
|
|
76
|
+
while (this.pos < this.text.length) {
|
|
77
|
+
const c = this.text[this.pos];
|
|
78
|
+
if (c === '"') {
|
|
79
|
+
this.pos++;
|
|
80
|
+
return { value: out, end: this.pos, ok: true };
|
|
81
|
+
}
|
|
82
|
+
if (c === "\n")
|
|
83
|
+
break;
|
|
84
|
+
if (c === "\\") {
|
|
85
|
+
const esc = this.text[this.pos + 1];
|
|
86
|
+
this.pos += 2;
|
|
87
|
+
if (esc === "n")
|
|
88
|
+
out += "\n";
|
|
89
|
+
else if (esc === "t")
|
|
90
|
+
out += "\t";
|
|
91
|
+
else if (esc === "r")
|
|
92
|
+
out += "\r";
|
|
93
|
+
else
|
|
94
|
+
out += esc ?? "";
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
out += c;
|
|
98
|
+
this.pos++;
|
|
99
|
+
}
|
|
100
|
+
this.err(start, this.pos, "unterminated string");
|
|
101
|
+
return { value: out, end: this.pos, ok: false };
|
|
102
|
+
}
|
|
103
|
+
parseValue() {
|
|
104
|
+
const start = this.pos;
|
|
105
|
+
const c = this.peek();
|
|
106
|
+
if (c === "{")
|
|
107
|
+
return this.parseMap();
|
|
108
|
+
if (c === "[")
|
|
109
|
+
return this.parseVector();
|
|
110
|
+
if (c === '"') {
|
|
111
|
+
const s = this.scanString();
|
|
112
|
+
if (!s.ok)
|
|
113
|
+
return { kind: "error", start, end: s.end };
|
|
114
|
+
return { kind: "scalar", start, end: s.end, value: s.value };
|
|
115
|
+
}
|
|
116
|
+
const rest = this.text.slice(this.pos);
|
|
117
|
+
const kw = KEYWORD.exec(rest);
|
|
118
|
+
if (kw) {
|
|
119
|
+
this.pos += kw[0].length;
|
|
120
|
+
return { kind: "scalar", start, end: this.pos, value: kw[0].slice(1) };
|
|
121
|
+
}
|
|
122
|
+
const num = NUM.exec(rest);
|
|
123
|
+
if (num && num[0].length > 0 && /^[-+]?\d/.test(rest)) {
|
|
124
|
+
this.pos += num[0].length;
|
|
125
|
+
const after = this.peek();
|
|
126
|
+
if (after !== undefined && !WS.test(after) && !"}])".includes(after)) {
|
|
127
|
+
return this.garbage(start, "malformed number");
|
|
128
|
+
}
|
|
129
|
+
return { kind: "scalar", start, end: this.pos, value: Number(num[0]) };
|
|
130
|
+
}
|
|
131
|
+
for (const [word, value] of [
|
|
132
|
+
["true", true],
|
|
133
|
+
["false", false],
|
|
134
|
+
["nil", null],
|
|
135
|
+
]) {
|
|
136
|
+
if (rest.startsWith(word)) {
|
|
137
|
+
const after = this.text[this.pos + word.length];
|
|
138
|
+
if (after === undefined || WS.test(after) || "}])".includes(after)) {
|
|
139
|
+
this.pos += word.length;
|
|
140
|
+
return { kind: "scalar", start, end: this.pos, value };
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return this.garbage(start, "expected a value");
|
|
145
|
+
}
|
|
146
|
+
parseMap() {
|
|
147
|
+
const start = this.pos;
|
|
148
|
+
this.pos++; // {
|
|
149
|
+
const entries = [];
|
|
150
|
+
for (;;) {
|
|
151
|
+
this.skipWs();
|
|
152
|
+
const c = this.peek();
|
|
153
|
+
if (c === undefined) {
|
|
154
|
+
this.err(start, start + 1, "unclosed map");
|
|
155
|
+
return { kind: "object", start, end: this.pos, entries };
|
|
156
|
+
}
|
|
157
|
+
if (c === "}") {
|
|
158
|
+
this.pos++;
|
|
159
|
+
return { kind: "object", start, end: this.pos, entries };
|
|
160
|
+
}
|
|
161
|
+
const entryStart = this.pos;
|
|
162
|
+
let key;
|
|
163
|
+
if (c === ":") {
|
|
164
|
+
const kw = KEYWORD.exec(this.text.slice(this.pos));
|
|
165
|
+
if (kw) {
|
|
166
|
+
key = kw[0].slice(1);
|
|
167
|
+
this.pos += kw[0].length;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
else if (c === '"') {
|
|
171
|
+
const s = this.scanString();
|
|
172
|
+
if (s.ok)
|
|
173
|
+
key = s.value;
|
|
174
|
+
}
|
|
175
|
+
if (key === undefined) {
|
|
176
|
+
const node = this.garbage(entryStart, "expected a keyword or string key");
|
|
177
|
+
entries.push({ key: undefined, start: entryStart, end: node.end, node });
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
this.skipWs();
|
|
181
|
+
if (this.peek() === undefined || this.peek() === "}") {
|
|
182
|
+
const end = this.pos;
|
|
183
|
+
this.err(entryStart, end, "key without a value");
|
|
184
|
+
entries.push({
|
|
185
|
+
key: undefined,
|
|
186
|
+
start: entryStart,
|
|
187
|
+
end,
|
|
188
|
+
node: { kind: "error", start: entryStart, end },
|
|
189
|
+
});
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
const node = this.parseValue();
|
|
193
|
+
entries.push({
|
|
194
|
+
key: node.kind === "error" ? undefined : key,
|
|
195
|
+
start: entryStart,
|
|
196
|
+
end: node.end,
|
|
197
|
+
node,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
parseVector() {
|
|
202
|
+
const start = this.pos;
|
|
203
|
+
this.pos++; // [
|
|
204
|
+
const items = [];
|
|
205
|
+
for (;;) {
|
|
206
|
+
this.skipWs();
|
|
207
|
+
const c = this.peek();
|
|
208
|
+
if (c === undefined) {
|
|
209
|
+
this.err(start, start + 1, "unclosed vector");
|
|
210
|
+
return { kind: "array", start, end: this.pos, items };
|
|
211
|
+
}
|
|
212
|
+
if (c === "]") {
|
|
213
|
+
this.pos++;
|
|
214
|
+
return { kind: "array", start, end: this.pos, items };
|
|
215
|
+
}
|
|
216
|
+
items.push(this.parseValue());
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
function parse(text) {
|
|
221
|
+
const p = new P(text);
|
|
222
|
+
p.skipWs();
|
|
223
|
+
if (p.pos >= text.length) {
|
|
224
|
+
p.err(0, 0, "empty document");
|
|
225
|
+
return { tree: { kind: "error", start: 0, end: 0 }, errors: p.errors };
|
|
226
|
+
}
|
|
227
|
+
const tree = p.parseValue();
|
|
228
|
+
p.skipWs();
|
|
229
|
+
if (p.pos < text.length)
|
|
230
|
+
p.err(p.pos, text.length, "unexpected trailing content");
|
|
231
|
+
return { tree, errors: p.errors };
|
|
232
|
+
}
|
|
233
|
+
function printScalar(v) {
|
|
234
|
+
if (v === null)
|
|
235
|
+
return "nil";
|
|
236
|
+
return JSON.stringify(v);
|
|
237
|
+
}
|
|
238
|
+
function printKey(k) {
|
|
239
|
+
return SAFE_KEY.test(k) ? `:${k}` : JSON.stringify(k);
|
|
240
|
+
}
|
|
241
|
+
const allScalar = (a) => a.every(v => v === null || typeof v !== "object");
|
|
242
|
+
function printVal(v, depth) {
|
|
243
|
+
if (v === null || typeof v !== "object")
|
|
244
|
+
return printScalar(v);
|
|
245
|
+
const ind = indentOf(depth);
|
|
246
|
+
const inner = indentOf(depth + 1);
|
|
247
|
+
if (Array.isArray(v)) {
|
|
248
|
+
if (v.length === 0)
|
|
249
|
+
return "[]";
|
|
250
|
+
if (allScalar(v))
|
|
251
|
+
return `[${v.map(x => printScalar(x)).join(" ")}]`;
|
|
252
|
+
return `[\n${v.map(x => inner + printVal(x, depth + 1)).join("\n")}\n${ind}]`;
|
|
253
|
+
}
|
|
254
|
+
const keys = Object.keys(v);
|
|
255
|
+
if (keys.length === 0)
|
|
256
|
+
return "{}";
|
|
257
|
+
const body = keys.map(k => `${inner}${printKey(k)} ${printVal(v[k], depth + 1)}`).join("\n");
|
|
258
|
+
return `{\n${body}\n${ind}}`;
|
|
259
|
+
}
|
|
260
|
+
function print(value) {
|
|
261
|
+
return `${printVal(value, 0)}\n`;
|
|
262
|
+
}
|
|
263
|
+
function opToEdit(op, text) {
|
|
264
|
+
switch (op.type) {
|
|
265
|
+
case "replace":
|
|
266
|
+
return [{ start: op.node.start, end: op.node.end, text: printVal(op.value, op.depth) }];
|
|
267
|
+
case "insert": {
|
|
268
|
+
const { obj, key, value, depth } = op;
|
|
269
|
+
const inner = indentOf(depth + 1);
|
|
270
|
+
const printed = `${printKey(key)} ${printVal(value, depth + 1)}`;
|
|
271
|
+
if (obj.entries.length === 0) {
|
|
272
|
+
return [
|
|
273
|
+
{
|
|
274
|
+
start: obj.start + 1,
|
|
275
|
+
end: obj.start + 1,
|
|
276
|
+
text: `\n${inner}${printed}\n${indentOf(depth)}`,
|
|
277
|
+
},
|
|
278
|
+
];
|
|
279
|
+
}
|
|
280
|
+
const anchor = (op.after ?? obj.entries[obj.entries.length - 1]).end;
|
|
281
|
+
const between = text.slice(obj.start + 1, obj.entries[0].start);
|
|
282
|
+
const sep = between.includes("\n") ? `\n${inner}` : " ";
|
|
283
|
+
return [{ start: anchor, end: anchor, text: sep + printed }];
|
|
284
|
+
}
|
|
285
|
+
case "delete": {
|
|
286
|
+
// Forward-tiling spans; see the JSON adapter for the rationale.
|
|
287
|
+
const { obj, entry } = op;
|
|
288
|
+
const idx = obj.entries.indexOf(entry);
|
|
289
|
+
const next = obj.entries[idx + 1];
|
|
290
|
+
if (next !== undefined)
|
|
291
|
+
return [{ start: entry.start, end: next.start, text: "" }];
|
|
292
|
+
const prev = obj.entries[idx - 1];
|
|
293
|
+
const sepStart = prev !== undefined ? prev.end : obj.start + 1;
|
|
294
|
+
return [
|
|
295
|
+
{ start: sepStart, end: entry.start, text: "" },
|
|
296
|
+
{ start: entry.start, end: entry.end, text: "" },
|
|
297
|
+
];
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
export const ednFormat = { name: "EDN", parse, print, opToEdit };
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { type FormatAdapter, type JsonValue, lineColOf, type ParseError, valueOf, } from "./cst.js";
|
|
2
|
+
export { ednFormat } from "./edn.js";
|
|
3
|
+
export { jsonFormat } from "./json.js";
|
|
4
|
+
export { formatSpoke, valueHub } from "./lens.js";
|
|
5
|
+
export { tomlFormat } from "./toml.js";
|
|
6
|
+
export { yamlFormat } from "./yaml.js";
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// formats — concrete-syntax lenses over a shared abstract value.
|
|
2
|
+
// See cst.ts for the machinery, lens.ts for the reactive wiring.
|
|
3
|
+
export { lineColOf, valueOf, } from "./cst.js";
|
|
4
|
+
export { ednFormat } from "./edn.js";
|
|
5
|
+
export { jsonFormat } from "./json.js";
|
|
6
|
+
export { formatSpoke, valueHub } from "./lens.js";
|
|
7
|
+
export { tomlFormat } from "./toml.js";
|
|
8
|
+
export { yamlFormat } from "./yaml.js";
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
// json.ts — tolerant JSON adapter.
|
|
2
|
+
//
|
|
3
|
+
// Recovery strategy: garbage at a value/key position is consumed up to
|
|
4
|
+
// the next delimiter at bracket depth zero (`,` `}` `]`) and becomes an
|
|
5
|
+
// ErrorNode; a missing comma between entries is a zero-width error that
|
|
6
|
+
// leaves both neighbouring entries valid. So a half-typed edit breaks
|
|
7
|
+
// only its own region — the rest of the document stays addressable for
|
|
8
|
+
// surgical writes.
|
|
9
|
+
import { indentOf, } from "./cst.js";
|
|
10
|
+
const WS = /[ \t\r\n]/;
|
|
11
|
+
const NUM = /^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?/;
|
|
12
|
+
const DELIM = new Set([",", "}", "]", undefined]);
|
|
13
|
+
class P {
|
|
14
|
+
text;
|
|
15
|
+
pos = 0;
|
|
16
|
+
errors = [];
|
|
17
|
+
constructor(text) {
|
|
18
|
+
this.text = text;
|
|
19
|
+
}
|
|
20
|
+
err(start, end, message) {
|
|
21
|
+
this.errors.push({ start, end, message });
|
|
22
|
+
}
|
|
23
|
+
skipWs() {
|
|
24
|
+
while (this.pos < this.text.length && WS.test(this.text[this.pos]))
|
|
25
|
+
this.pos++;
|
|
26
|
+
}
|
|
27
|
+
peek() {
|
|
28
|
+
return this.text[this.pos];
|
|
29
|
+
}
|
|
30
|
+
/** Consume a garbage run: balanced brackets, strings capped at EOL,
|
|
31
|
+
* stops at `,` `}` `]` at depth zero. Returns the error node. */
|
|
32
|
+
garbage(start, message) {
|
|
33
|
+
let depth = 0;
|
|
34
|
+
while (this.pos < this.text.length) {
|
|
35
|
+
const c = this.text[this.pos];
|
|
36
|
+
if (c === '"') {
|
|
37
|
+
this.pos++;
|
|
38
|
+
while (this.pos < this.text.length) {
|
|
39
|
+
const s = this.text[this.pos];
|
|
40
|
+
if (s === "\\")
|
|
41
|
+
this.pos += 2;
|
|
42
|
+
else if (s === '"' || s === "\n") {
|
|
43
|
+
this.pos++;
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
else
|
|
47
|
+
this.pos++;
|
|
48
|
+
}
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (c === "{" || c === "[")
|
|
52
|
+
depth++;
|
|
53
|
+
else if (c === "}" || c === "]") {
|
|
54
|
+
if (depth === 0)
|
|
55
|
+
break;
|
|
56
|
+
depth--;
|
|
57
|
+
}
|
|
58
|
+
else if (c === "," && depth === 0)
|
|
59
|
+
break;
|
|
60
|
+
this.pos++;
|
|
61
|
+
}
|
|
62
|
+
let end = this.pos;
|
|
63
|
+
while (end > start && WS.test(this.text[end - 1]))
|
|
64
|
+
end--;
|
|
65
|
+
if (end === start)
|
|
66
|
+
end = Math.min(start + 1, this.text.length);
|
|
67
|
+
this.err(start, end, message);
|
|
68
|
+
return { kind: "error", start, end };
|
|
69
|
+
}
|
|
70
|
+
/** Raw string token; `ok: false` on an unterminated string. */
|
|
71
|
+
scanString() {
|
|
72
|
+
const start = this.pos;
|
|
73
|
+
this.pos++; // opening quote
|
|
74
|
+
let out = "";
|
|
75
|
+
while (this.pos < this.text.length) {
|
|
76
|
+
const c = this.text[this.pos];
|
|
77
|
+
if (c === '"') {
|
|
78
|
+
this.pos++;
|
|
79
|
+
return { value: out, end: this.pos, ok: true };
|
|
80
|
+
}
|
|
81
|
+
if (c === "\n")
|
|
82
|
+
break;
|
|
83
|
+
if (c === "\\") {
|
|
84
|
+
const esc = this.text[this.pos + 1];
|
|
85
|
+
this.pos += 2;
|
|
86
|
+
if (esc === "n")
|
|
87
|
+
out += "\n";
|
|
88
|
+
else if (esc === "t")
|
|
89
|
+
out += "\t";
|
|
90
|
+
else if (esc === "r")
|
|
91
|
+
out += "\r";
|
|
92
|
+
else if (esc === "b")
|
|
93
|
+
out += "\b";
|
|
94
|
+
else if (esc === "f")
|
|
95
|
+
out += "\f";
|
|
96
|
+
else if (esc === "u") {
|
|
97
|
+
const hex = this.text.slice(this.pos, this.pos + 4);
|
|
98
|
+
if (/^[0-9a-fA-F]{4}$/.test(hex)) {
|
|
99
|
+
out += String.fromCharCode(Number.parseInt(hex, 16));
|
|
100
|
+
this.pos += 4;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
else
|
|
104
|
+
out += esc ?? "";
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
out += c;
|
|
108
|
+
this.pos++;
|
|
109
|
+
}
|
|
110
|
+
this.err(start, this.pos, "unterminated string");
|
|
111
|
+
return { value: out, end: this.pos, ok: false };
|
|
112
|
+
}
|
|
113
|
+
parseValue() {
|
|
114
|
+
const start = this.pos;
|
|
115
|
+
const c = this.peek();
|
|
116
|
+
if (c === "{")
|
|
117
|
+
return this.parseObject();
|
|
118
|
+
if (c === "[")
|
|
119
|
+
return this.parseArray();
|
|
120
|
+
if (c === '"') {
|
|
121
|
+
const s = this.scanString();
|
|
122
|
+
if (!s.ok)
|
|
123
|
+
return { kind: "error", start, end: s.end };
|
|
124
|
+
return this.checkTail({ kind: "scalar", start, end: s.end, value: s.value }, start);
|
|
125
|
+
}
|
|
126
|
+
const rest = this.text.slice(this.pos);
|
|
127
|
+
const num = NUM.exec(rest);
|
|
128
|
+
if (num && num[0].length > 0 && (c === "-" || (c >= "0" && c <= "9"))) {
|
|
129
|
+
this.pos += num[0].length;
|
|
130
|
+
return this.checkTail({ kind: "scalar", start, end: this.pos, value: Number(num[0]) }, start);
|
|
131
|
+
}
|
|
132
|
+
for (const [word, value] of [
|
|
133
|
+
["true", true],
|
|
134
|
+
["false", false],
|
|
135
|
+
["null", null],
|
|
136
|
+
]) {
|
|
137
|
+
if (rest.startsWith(word)) {
|
|
138
|
+
this.pos += word.length;
|
|
139
|
+
return this.checkTail({ kind: "scalar", start, end: this.pos, value }, start);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return this.garbage(start, "expected a value");
|
|
143
|
+
}
|
|
144
|
+
/** A scalar must be followed by a delimiter; trailing junk turns the
|
|
145
|
+
* whole run into an error node (typing `808x` must not push `808`). */
|
|
146
|
+
checkTail(node, start) {
|
|
147
|
+
const c = this.peek();
|
|
148
|
+
if (DELIM.has(c) || (c !== undefined && WS.test(c)) || c === ":")
|
|
149
|
+
return node;
|
|
150
|
+
return this.garbage(start, "malformed value");
|
|
151
|
+
}
|
|
152
|
+
parseObject() {
|
|
153
|
+
const start = this.pos;
|
|
154
|
+
this.pos++; // {
|
|
155
|
+
const entries = [];
|
|
156
|
+
for (;;) {
|
|
157
|
+
this.skipWs();
|
|
158
|
+
const c = this.peek();
|
|
159
|
+
if (c === undefined) {
|
|
160
|
+
this.err(start, start + 1, "unclosed object");
|
|
161
|
+
return { kind: "object", start, end: this.pos, entries };
|
|
162
|
+
}
|
|
163
|
+
if (c === "}") {
|
|
164
|
+
this.pos++;
|
|
165
|
+
return { kind: "object", start, end: this.pos, entries };
|
|
166
|
+
}
|
|
167
|
+
const entryStart = this.pos;
|
|
168
|
+
if (c !== '"') {
|
|
169
|
+
const node = this.garbage(entryStart, "expected a key");
|
|
170
|
+
entries.push({ key: undefined, start: entryStart, end: node.end, node });
|
|
171
|
+
if (this.peek() === ",")
|
|
172
|
+
this.pos++;
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
const key = this.scanString();
|
|
176
|
+
if (!key.ok) {
|
|
177
|
+
const node = { kind: "error", start: entryStart, end: key.end };
|
|
178
|
+
entries.push({ key: undefined, start: entryStart, end: key.end, node });
|
|
179
|
+
if (this.peek() === ",")
|
|
180
|
+
this.pos++;
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
this.skipWs();
|
|
184
|
+
if (this.peek() !== ":") {
|
|
185
|
+
const node = this.garbage(entryStart, "expected ':' after key");
|
|
186
|
+
entries.push({ key: undefined, start: entryStart, end: node.end, node });
|
|
187
|
+
if (this.peek() === ",")
|
|
188
|
+
this.pos++;
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
this.pos++; // :
|
|
192
|
+
this.skipWs();
|
|
193
|
+
const node = this.parseValue();
|
|
194
|
+
entries.push({
|
|
195
|
+
key: node.kind === "error" ? undefined : key.value,
|
|
196
|
+
start: entryStart,
|
|
197
|
+
end: node.end,
|
|
198
|
+
node,
|
|
199
|
+
});
|
|
200
|
+
this.skipWs();
|
|
201
|
+
const d = this.peek();
|
|
202
|
+
if (d === ",") {
|
|
203
|
+
this.pos++;
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
if (d === "}")
|
|
207
|
+
continue; // loop closes
|
|
208
|
+
if (d === undefined)
|
|
209
|
+
continue; // loop reports unclosed
|
|
210
|
+
if (d === '"') {
|
|
211
|
+
// Missing comma — recover, both entries stay valid.
|
|
212
|
+
this.err(this.pos, this.pos + 1, "expected ',' between entries");
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
const junk = this.garbage(this.pos, "expected ',' or '}'");
|
|
216
|
+
entries.push({ key: undefined, start: junk.start, end: junk.end, node: junk });
|
|
217
|
+
if (this.peek() === ",")
|
|
218
|
+
this.pos++;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
parseArray() {
|
|
222
|
+
const start = this.pos;
|
|
223
|
+
this.pos++; // [
|
|
224
|
+
const items = [];
|
|
225
|
+
for (;;) {
|
|
226
|
+
this.skipWs();
|
|
227
|
+
const c = this.peek();
|
|
228
|
+
if (c === undefined) {
|
|
229
|
+
this.err(start, start + 1, "unclosed array");
|
|
230
|
+
return { kind: "array", start, end: this.pos, items };
|
|
231
|
+
}
|
|
232
|
+
if (c === "]") {
|
|
233
|
+
this.pos++;
|
|
234
|
+
return { kind: "array", start, end: this.pos, items };
|
|
235
|
+
}
|
|
236
|
+
const node = this.parseValue();
|
|
237
|
+
items.push(node);
|
|
238
|
+
this.skipWs();
|
|
239
|
+
const d = this.peek();
|
|
240
|
+
if (d === ",") {
|
|
241
|
+
this.pos++;
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
if (d === "]" || d === undefined)
|
|
245
|
+
continue;
|
|
246
|
+
this.err(this.pos, this.pos + 1, "expected ',' or ']'");
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
function parse(text) {
|
|
251
|
+
const p = new P(text);
|
|
252
|
+
p.skipWs();
|
|
253
|
+
if (p.pos >= text.length) {
|
|
254
|
+
p.err(0, 0, "empty document");
|
|
255
|
+
return { tree: { kind: "error", start: 0, end: 0 }, errors: p.errors };
|
|
256
|
+
}
|
|
257
|
+
const tree = p.parseValue();
|
|
258
|
+
p.skipWs();
|
|
259
|
+
if (p.pos < text.length)
|
|
260
|
+
p.err(p.pos, text.length, "unexpected trailing content");
|
|
261
|
+
return { tree, errors: p.errors };
|
|
262
|
+
}
|
|
263
|
+
function printScalar(v) {
|
|
264
|
+
return JSON.stringify(v);
|
|
265
|
+
}
|
|
266
|
+
const allScalar = (a) => a.every(v => v === null || typeof v !== "object");
|
|
267
|
+
function printVal(v, depth) {
|
|
268
|
+
if (v === null || typeof v !== "object")
|
|
269
|
+
return printScalar(v);
|
|
270
|
+
const ind = indentOf(depth);
|
|
271
|
+
const inner = indentOf(depth + 1);
|
|
272
|
+
if (Array.isArray(v)) {
|
|
273
|
+
if (v.length === 0)
|
|
274
|
+
return "[]";
|
|
275
|
+
if (allScalar(v))
|
|
276
|
+
return `[${v.map(x => printScalar(x)).join(", ")}]`;
|
|
277
|
+
return `[\n${v.map(x => inner + printVal(x, depth + 1)).join(",\n")}\n${ind}]`;
|
|
278
|
+
}
|
|
279
|
+
const keys = Object.keys(v);
|
|
280
|
+
if (keys.length === 0)
|
|
281
|
+
return "{}";
|
|
282
|
+
const body = keys
|
|
283
|
+
.map(k => `${inner}${JSON.stringify(k)}: ${printVal(v[k], depth + 1)}`)
|
|
284
|
+
.join(",\n");
|
|
285
|
+
return `{\n${body}\n${ind}}`;
|
|
286
|
+
}
|
|
287
|
+
function print(value) {
|
|
288
|
+
return `${printVal(value, 0)}\n`;
|
|
289
|
+
}
|
|
290
|
+
function opToEdit(op, text) {
|
|
291
|
+
switch (op.type) {
|
|
292
|
+
case "replace":
|
|
293
|
+
return [{ start: op.node.start, end: op.node.end, text: printVal(op.value, op.depth) }];
|
|
294
|
+
case "insert": {
|
|
295
|
+
const { obj, key, value, depth } = op;
|
|
296
|
+
const inner = indentOf(depth + 1);
|
|
297
|
+
const printed = `${JSON.stringify(key)}: ${printVal(value, depth + 1)}`;
|
|
298
|
+
if (obj.entries.length === 0) {
|
|
299
|
+
return [
|
|
300
|
+
{
|
|
301
|
+
start: obj.start + 1,
|
|
302
|
+
end: obj.start + 1,
|
|
303
|
+
text: `\n${inner}${printed}\n${indentOf(depth)}`,
|
|
304
|
+
},
|
|
305
|
+
];
|
|
306
|
+
}
|
|
307
|
+
const anchor = (op.after ?? obj.entries[obj.entries.length - 1]).end;
|
|
308
|
+
// Match the document's separator style: newline if entries are
|
|
309
|
+
// line-separated, inline otherwise.
|
|
310
|
+
const between = text.slice(obj.start + 1, obj.entries[0].start);
|
|
311
|
+
const sep = between.includes("\n") ? `,\n${inner}` : ", ";
|
|
312
|
+
return [{ start: anchor, end: anchor, text: sep + printed }];
|
|
313
|
+
}
|
|
314
|
+
case "delete": {
|
|
315
|
+
// Forward-tiling spans so consecutive deletes never overlap; a
|
|
316
|
+
// last-entry delete adds its leading separator as a second edit
|
|
317
|
+
// (harmlessly dropped when the previous entry is deleted too).
|
|
318
|
+
const { obj, entry } = op;
|
|
319
|
+
const idx = obj.entries.indexOf(entry);
|
|
320
|
+
const next = obj.entries[idx + 1];
|
|
321
|
+
if (next !== undefined)
|
|
322
|
+
return [{ start: entry.start, end: next.start, text: "" }];
|
|
323
|
+
const prev = obj.entries[idx - 1];
|
|
324
|
+
const sepStart = prev !== undefined ? prev.end : obj.start + 1;
|
|
325
|
+
return [
|
|
326
|
+
{ start: sepStart, end: entry.start, text: "" },
|
|
327
|
+
{ start: entry.start, end: entry.end, text: "" },
|
|
328
|
+
];
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
export const jsonFormat = { name: "JSON", parse, print, opToEdit };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type Cell, type Writable } from "../core/cell.js";
|
|
2
|
+
import { type FormatAdapter, type JsonValue } from "./cst.js";
|
|
3
|
+
/** Writable hub for a shared abstract value (deep-equality pruned). */
|
|
4
|
+
export declare function valueHub(initial: JsonValue): Writable<Cell<JsonValue>>;
|
|
5
|
+
/** Writable text view of `hub` in `adapter`'s syntax. Valid edits push
|
|
6
|
+
* through; broken edits hold the hub and merge external changes around
|
|
7
|
+
* the error regions. */
|
|
8
|
+
export declare function formatSpoke(hub: Writable<Cell<JsonValue>>, adapter: FormatAdapter): Writable<Cell<string>>;
|