bireactive 0.3.0 → 0.3.2
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/README.md +14 -7
- package/dist/automerge/doc-cell.d.ts +20 -0
- package/dist/automerge/doc-cell.js +80 -0
- package/dist/automerge/index.d.ts +3 -0
- package/dist/automerge/index.js +12 -0
- package/dist/automerge/reconcile.d.ts +5 -0
- package/dist/automerge/reconcile.js +63 -0
- package/dist/core/_counts.d.ts +48 -0
- package/dist/core/_counts.js +51 -0
- package/dist/core/cell.d.ts +148 -112
- package/dist/core/cell.js +945 -768
- package/dist/core/debug.d.ts +25 -0
- package/dist/core/debug.js +121 -0
- package/dist/core/derived-geometry.js +4 -7
- package/dist/core/index.d.ts +9 -2
- package/dist/core/index.js +8 -1
- package/dist/core/lenses/aggregates.d.ts +42 -52
- package/dist/core/lenses/aggregates.js +225 -116
- package/dist/core/lenses/geometry.d.ts +22 -4
- package/dist/core/lenses/geometry.js +59 -27
- package/dist/core/lenses/index.d.ts +6 -6
- package/dist/core/lenses/index.js +6 -6
- package/dist/core/lenses/memory.js +4 -17
- package/dist/core/lenses/numerical.d.ts +100 -0
- package/dist/core/lenses/{typed-factor.js → numerical.js} +136 -34
- package/dist/core/lenses/point-cloud.d.ts +67 -0
- package/dist/core/lenses/{closed-form-policies.js → point-cloud.js} +226 -84
- package/dist/core/lenses/snap.d.ts +18 -0
- package/dist/core/lenses/snap.js +138 -0
- package/dist/core/lenses/text.d.ts +40 -0
- package/dist/core/lenses/text.js +202 -0
- package/dist/core/lifecycle.js +3 -6
- package/dist/core/linalg.js +5 -11
- package/dist/core/optic.d.ts +13 -0
- package/dist/core/optic.js +39 -0
- package/dist/core/optics.d.ts +10 -0
- package/dist/core/optics.js +26 -0
- package/dist/core/store.d.ts +9 -0
- package/dist/core/store.js +77 -0
- package/dist/core/traits.d.ts +4 -7
- package/dist/core/traits.js +8 -12
- package/dist/core/values/anchor.js +0 -4
- package/dist/core/values/arr.d.ts +110 -0
- package/dist/core/values/arr.js +336 -0
- package/dist/core/values/audio.d.ts +8 -9
- package/dist/core/values/audio.js +11 -28
- package/dist/core/values/bool.d.ts +11 -11
- package/dist/core/values/bool.js +12 -22
- package/dist/core/values/box.d.ts +15 -20
- package/dist/core/values/box.js +20 -33
- package/dist/core/values/canvas.d.ts +18 -25
- package/dist/core/values/canvas.js +32 -66
- package/dist/core/values/color.d.ts +5 -7
- package/dist/core/values/color.js +5 -11
- package/dist/core/values/field.d.ts +6 -7
- package/dist/core/values/field.js +10 -35
- package/dist/core/values/flags.d.ts +1 -2
- package/dist/core/values/flags.js +1 -17
- package/dist/core/values/gpu.d.ts +6 -10
- package/dist/core/values/gpu.js +8 -22
- package/dist/core/values/matrix.d.ts +2 -4
- package/dist/core/values/matrix.js +2 -12
- package/dist/core/values/num.d.ts +19 -28
- package/dist/core/values/num.js +23 -41
- package/dist/core/values/pose.d.ts +2 -4
- package/dist/core/values/pose.js +3 -12
- package/dist/core/values/range.d.ts +18 -26
- package/dist/core/values/range.js +22 -39
- package/dist/core/values/reg/ambiguity.d.ts +8 -0
- package/dist/core/values/reg/ambiguity.js +131 -0
- package/dist/core/values/reg/engine.d.ts +91 -0
- package/dist/core/values/reg/engine.js +373 -0
- package/dist/core/values/reg/nfa.d.ts +42 -0
- package/dist/core/values/reg/nfa.js +391 -0
- package/dist/core/values/reg/regex.d.ts +7 -0
- package/dist/core/values/reg/regex.js +318 -0
- package/dist/core/values/reg/types.d.ts +60 -0
- package/dist/core/values/reg/types.js +3 -0
- package/dist/core/values/reg.d.ts +250 -0
- package/dist/core/values/reg.js +649 -0
- package/dist/core/values/str.d.ts +16 -60
- package/dist/core/values/str.js +133 -315
- package/dist/core/values/template.js +1 -24
- package/dist/core/values/transform.d.ts +3 -5
- package/dist/core/values/transform.js +3 -12
- package/dist/core/values/tri.d.ts +9 -10
- package/dist/core/values/tri.js +9 -15
- package/dist/core/values/vec.d.ts +9 -24
- package/dist/core/values/vec.js +9 -64
- package/dist/formats/lens.js +6 -9
- package/dist/index.d.ts +0 -11
- package/dist/index.js +1 -11
- package/dist/jsx-dev-runtime.d.ts +2 -0
- package/dist/jsx-dev-runtime.js +5 -0
- package/dist/jsx-runtime.d.ts +54 -0
- package/dist/jsx-runtime.js +219 -0
- package/dist/schema/lens.js +5 -5
- package/dist/shapes/drag-behaviors.d.ts +56 -0
- package/dist/shapes/drag-behaviors.js +102 -0
- package/dist/shapes/drag-spec.d.ts +52 -0
- package/dist/shapes/drag-spec.js +112 -0
- package/dist/shapes/index.d.ts +3 -1
- package/dist/shapes/index.js +3 -1
- package/dist/shapes/interaction.d.ts +2 -3
- package/dist/shapes/interaction.js +77 -56
- package/dist/shapes/label.js +6 -0
- package/dist/shapes/layout.d.ts +47 -1
- package/dist/shapes/layout.js +59 -1
- package/package.json +22 -1
- package/dist/coll.d.ts +0 -74
- package/dist/coll.js +0 -210
- package/dist/core/lenses/closed-form-policies.d.ts +0 -57
- package/dist/core/lenses/decompositions.d.ts +0 -14
- package/dist/core/lenses/decompositions.js +0 -224
- package/dist/core/lenses/domain-aggregates.d.ts +0 -42
- package/dist/core/lenses/domain-aggregates.js +0 -245
- package/dist/core/lenses/typed-factor.d.ts +0 -40
|
@@ -0,0 +1,649 @@
|
|
|
1
|
+
import { Cell } from "../cell.js";
|
|
2
|
+
import { optic } from "../optic.js";
|
|
3
|
+
import { Arr } from "./arr.js";
|
|
4
|
+
import { concatAmbiguity, intersects } from "./reg/ambiguity.js";
|
|
5
|
+
import { accepts, alphabetOf, altAll, CharSet, chr, EPS, language, nullable as reNullable, seq as reSeq, star as reStar, seqAll, } from "./reg/engine.js";
|
|
6
|
+
import { compileProgram, parseValue, recognize } from "./reg/nfa.js";
|
|
7
|
+
import { compileRegex, RegError } from "./reg/regex.js";
|
|
8
|
+
import { Str } from "./str.js";
|
|
9
|
+
import { numCodec } from "./template.js";
|
|
10
|
+
const isSilent = (n) => n.kind === "lit";
|
|
11
|
+
// ── memoized structural views (toRe / program) ───────────────────────
|
|
12
|
+
const reMemo = new WeakMap();
|
|
13
|
+
const progMemo = new WeakMap();
|
|
14
|
+
/** The compiled Thompson program for a node (built once, then cached). */
|
|
15
|
+
function progOf(n) {
|
|
16
|
+
let p = progMemo.get(n);
|
|
17
|
+
if (p === undefined) {
|
|
18
|
+
p = compileProgram(n);
|
|
19
|
+
progMemo.set(n, p);
|
|
20
|
+
}
|
|
21
|
+
return p;
|
|
22
|
+
}
|
|
23
|
+
function reOf(n) {
|
|
24
|
+
let r = reMemo.get(n);
|
|
25
|
+
if (r === undefined) {
|
|
26
|
+
r = toRe(n);
|
|
27
|
+
reMemo.set(n, r);
|
|
28
|
+
}
|
|
29
|
+
return r;
|
|
30
|
+
}
|
|
31
|
+
// ── parse (get): linear PikeVM, whole-string ──────────────────────────
|
|
32
|
+
/** Parse `s` fully; `null` if it doesn't match. */
|
|
33
|
+
function parseNode(n, s, spans) {
|
|
34
|
+
const r = parseValue(n, progOf(n), s, spans);
|
|
35
|
+
return r === null ? null : r.val;
|
|
36
|
+
}
|
|
37
|
+
/** Whole-string match of a leaf's language — for validating a scalar write. */
|
|
38
|
+
function fullLeafMatch(leaf, s) {
|
|
39
|
+
return accepts(leaf.engine, s);
|
|
40
|
+
}
|
|
41
|
+
/** Does `n` parse all of `s`? General write-validation. */
|
|
42
|
+
function fullNodeMatch(n, s) {
|
|
43
|
+
return recognize(progOf(n), s);
|
|
44
|
+
}
|
|
45
|
+
// ── print (put) ──────────────────────────────────────────────────────
|
|
46
|
+
function printNode(n, val) {
|
|
47
|
+
switch (n.kind) {
|
|
48
|
+
case "lit":
|
|
49
|
+
return n.text;
|
|
50
|
+
case "copy":
|
|
51
|
+
return String(val ?? "");
|
|
52
|
+
case "of":
|
|
53
|
+
return n.codec.format(val);
|
|
54
|
+
case "seq": {
|
|
55
|
+
const vals = val ?? [];
|
|
56
|
+
let out = "";
|
|
57
|
+
let vi = 0;
|
|
58
|
+
for (const part of n.parts) {
|
|
59
|
+
if (isSilent(part))
|
|
60
|
+
out += printNode(part, null);
|
|
61
|
+
else
|
|
62
|
+
out += printNode(part, vals[vi++] ?? defaultVal(part));
|
|
63
|
+
}
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
case "alt": {
|
|
67
|
+
const a = val ?? { branch: 0, val: defaultVal(n.branches[0]) };
|
|
68
|
+
const branch = n.branches[a.branch] ?? n.branches[0];
|
|
69
|
+
return printNode(branch, a.val);
|
|
70
|
+
}
|
|
71
|
+
case "opt":
|
|
72
|
+
if (val === null || val === undefined)
|
|
73
|
+
return "";
|
|
74
|
+
return printNode(n.part, isSilent(n.part) ? null : val);
|
|
75
|
+
case "star": {
|
|
76
|
+
const sv = val ?? { items: [], seps: [] };
|
|
77
|
+
let out = "";
|
|
78
|
+
for (let k = 0; k < sv.items.length; k++) {
|
|
79
|
+
if (k > 0)
|
|
80
|
+
out += sv.seps[k - 1] ?? n.joiner;
|
|
81
|
+
out += printNode(n.part, sv.items[k]);
|
|
82
|
+
}
|
|
83
|
+
return out;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// ── totalization + nullability ───────────────────────────────────────
|
|
88
|
+
function defaultVal(n) {
|
|
89
|
+
switch (n.kind) {
|
|
90
|
+
case "lit":
|
|
91
|
+
return null;
|
|
92
|
+
case "copy":
|
|
93
|
+
return "";
|
|
94
|
+
case "of":
|
|
95
|
+
return (n.codec.parse("") ?? n.codec.parse("0") ?? null);
|
|
96
|
+
case "seq":
|
|
97
|
+
return n.parts.filter(p => !isSilent(p)).map(defaultVal);
|
|
98
|
+
case "alt":
|
|
99
|
+
return { branch: 0, val: defaultVal(n.branches[0]) };
|
|
100
|
+
case "opt":
|
|
101
|
+
return null;
|
|
102
|
+
case "star":
|
|
103
|
+
return { items: [], seps: [] };
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function nullable(n) {
|
|
107
|
+
switch (n.kind) {
|
|
108
|
+
case "lit":
|
|
109
|
+
return n.text === "";
|
|
110
|
+
case "copy":
|
|
111
|
+
case "of":
|
|
112
|
+
return reNullable(n.engine);
|
|
113
|
+
case "seq":
|
|
114
|
+
return n.parts.every(nullable);
|
|
115
|
+
case "alt":
|
|
116
|
+
return n.branches.some(nullable);
|
|
117
|
+
case "opt":
|
|
118
|
+
return true;
|
|
119
|
+
case "star":
|
|
120
|
+
return n.sep === undefined && n.min === 0 ? true : nullable(n.part);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
// ── unambiguity checks (thrown at construction) ──────────────────────
|
|
124
|
+
// Each combinator validates only its own new seams (children are already valid),
|
|
125
|
+
// deciding on the derivative automaton (see `reg/ambiguity.ts`). Each error
|
|
126
|
+
// names a concrete witness string that would parse two ways.
|
|
127
|
+
const quote = (s) => (s === "" ? '""' : JSON.stringify(s));
|
|
128
|
+
function checkSeq(parts) {
|
|
129
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
130
|
+
const a = reOf(parts[i]);
|
|
131
|
+
const b = seqAll(parts.slice(i + 1).map(reOf));
|
|
132
|
+
const w = concatAmbiguity(a, b);
|
|
133
|
+
if (w !== null) {
|
|
134
|
+
throw new RegError(`Reg.seq: the boundary after part ${i} is ambiguous — ${quote(w)} splits two ways. Insert a lit() delimiter or use disjoint character classes.`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
function checkAlt(branches) {
|
|
139
|
+
for (let i = 0; i < branches.length; i++) {
|
|
140
|
+
for (let j = i + 1; j < branches.length; j++) {
|
|
141
|
+
const w = intersects(reOf(branches[i]), reOf(branches[j]));
|
|
142
|
+
if (w !== null) {
|
|
143
|
+
throw new RegError(`Reg.alt: branches ${i} and ${j} both match ${quote(w)} — make the branches' languages disjoint.`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
function checkOpt(part) {
|
|
149
|
+
if (nullable(part)) {
|
|
150
|
+
throw new RegError("Reg.opt: the element is itself nullable — present-vs-absent is ambiguous.");
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
function checkStar(part, sep) {
|
|
154
|
+
const e = reOf(part);
|
|
155
|
+
if (sep === undefined) {
|
|
156
|
+
if (nullable(part)) {
|
|
157
|
+
throw new RegError('Reg.star/plus: a nullable element with no separator iterates ambiguously — add a separator (e.g. star(lit(","))).');
|
|
158
|
+
}
|
|
159
|
+
const w = concatAmbiguity(e, reStar(e)); // element · element* unambiguous?
|
|
160
|
+
if (w !== null) {
|
|
161
|
+
throw new RegError(`Reg.star/plus: element boundaries are not self-delimiting — ${quote(w)} iterates two ways. Add a separator.`);
|
|
162
|
+
}
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
if (nullable(sep)) {
|
|
166
|
+
throw new RegError("Reg.star/plus: a nullable separator cannot pin element boundaries.");
|
|
167
|
+
}
|
|
168
|
+
// Pattern is `E (S E)*`; check each seam: S·E, the (S·E) repetition, and the
|
|
169
|
+
// leading E · (S E)*.
|
|
170
|
+
const s = reOf(sep);
|
|
171
|
+
const se = reSeq(s, e);
|
|
172
|
+
const tail = reStar(se);
|
|
173
|
+
const w = concatAmbiguity(s, e) ?? concatAmbiguity(se, tail) ?? concatAmbiguity(e, reSeq(s, tail));
|
|
174
|
+
if (w !== null) {
|
|
175
|
+
throw new RegError(`Reg.star/plus: element and separator boundaries overlap — ${quote(w)} parses two ways.`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
// ── grammar → capture-free automaton (recognition) ────────────────────
|
|
179
|
+
function toRe(n) {
|
|
180
|
+
switch (n.kind) {
|
|
181
|
+
case "lit": {
|
|
182
|
+
const parts = [];
|
|
183
|
+
for (let i = 0; i < n.text.length; i++)
|
|
184
|
+
parts.push(chr(CharSet.char(n.text.charCodeAt(i))));
|
|
185
|
+
return seqAll(parts);
|
|
186
|
+
}
|
|
187
|
+
case "copy":
|
|
188
|
+
case "of":
|
|
189
|
+
return n.engine;
|
|
190
|
+
case "seq":
|
|
191
|
+
return seqAll(n.parts.map(toRe));
|
|
192
|
+
case "alt":
|
|
193
|
+
return altAll(n.branches.map(toRe));
|
|
194
|
+
case "opt":
|
|
195
|
+
return altAll([EPS, toRe(n.part)]);
|
|
196
|
+
case "star": {
|
|
197
|
+
const P = toRe(n.part);
|
|
198
|
+
const S = n.sep !== undefined ? toRe(n.sep) : EPS;
|
|
199
|
+
return altAll([EPS, reSeq(P, reStar(reSeq(S, P)))]);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
/** The separator string to emit when inserting into a star: the literal text
|
|
204
|
+
* for a `lit`, otherwise the *shortest* string in the separator's language.
|
|
205
|
+
* A separator whose language is empty has no insertable member, so the star
|
|
206
|
+
* is rejected at construction rather than silently writing off-language text. */
|
|
207
|
+
function joinerFor(sep) {
|
|
208
|
+
if (sep === undefined)
|
|
209
|
+
return "";
|
|
210
|
+
if (sep.kind === "lit")
|
|
211
|
+
return sep.text;
|
|
212
|
+
const re = reOf(sep);
|
|
213
|
+
for (const w of language(re, [...alphabetOf(re)], 1024, 1))
|
|
214
|
+
return w;
|
|
215
|
+
throw new RegError("Reg.star/plus: the separator matches no string, so nothing can be inserted between elements.");
|
|
216
|
+
}
|
|
217
|
+
function collectCaptures(n, path, acc) {
|
|
218
|
+
switch (n.kind) {
|
|
219
|
+
case "copy":
|
|
220
|
+
case "of":
|
|
221
|
+
case "star":
|
|
222
|
+
if (n.name !== undefined)
|
|
223
|
+
acc.set(n.name, { node: n, path: path.slice() });
|
|
224
|
+
return;
|
|
225
|
+
case "seq": {
|
|
226
|
+
let vi = 0;
|
|
227
|
+
for (const part of n.parts) {
|
|
228
|
+
if (isSilent(part))
|
|
229
|
+
continue;
|
|
230
|
+
collectCaptures(part, [...path, { seq: vi }], acc);
|
|
231
|
+
vi++;
|
|
232
|
+
}
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
case "opt":
|
|
236
|
+
collectCaptures(n.part, path, acc);
|
|
237
|
+
return;
|
|
238
|
+
case "alt": {
|
|
239
|
+
for (let b = 0; b < n.branches.length; b++) {
|
|
240
|
+
collectCaptures(n.branches[b], [...path, { alt: b }], acc);
|
|
241
|
+
}
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
case "lit":
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
/** Structural value equality, for the print-validate (PutGet) write check. */
|
|
249
|
+
function regEqual(a, b) {
|
|
250
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
251
|
+
}
|
|
252
|
+
const isAltVal = (v) => v !== null && typeof v === "object" && "branch" in v;
|
|
253
|
+
function getAt(val, path) {
|
|
254
|
+
let v = val;
|
|
255
|
+
for (const step of path) {
|
|
256
|
+
if ("seq" in step)
|
|
257
|
+
v = v?.[step.seq] ?? null;
|
|
258
|
+
else if (isAltVal(v) && v.branch === step.alt)
|
|
259
|
+
v = v.val;
|
|
260
|
+
else
|
|
261
|
+
return null; // inactive alt branch
|
|
262
|
+
}
|
|
263
|
+
return v;
|
|
264
|
+
}
|
|
265
|
+
function setAt(val, path, next) {
|
|
266
|
+
if (path.length === 0)
|
|
267
|
+
return next;
|
|
268
|
+
const [head, ...rest] = path;
|
|
269
|
+
if ("seq" in head) {
|
|
270
|
+
const arr = (val ?? []).slice();
|
|
271
|
+
arr[head.seq] = setAt(arr[head.seq] ?? null, rest, next);
|
|
272
|
+
return arr;
|
|
273
|
+
}
|
|
274
|
+
// alt step: descend only into the active branch; otherwise the write is a no-op.
|
|
275
|
+
if (isAltVal(val) && val.branch === head.alt) {
|
|
276
|
+
return { branch: val.branch, val: setAt(val.val, rest, next) };
|
|
277
|
+
}
|
|
278
|
+
return val;
|
|
279
|
+
}
|
|
280
|
+
// ── the Reg class ────────────────────────────────────────────────────
|
|
281
|
+
/** An immutable bidirectional string-lens description. Build with the typed
|
|
282
|
+
* leaf builders and combinators, then `bind`/`view` onto a `Cell<string>`.
|
|
283
|
+
*
|
|
284
|
+
* The four type parameters are phantom: `V` is the parsed value, `N` whether
|
|
285
|
+
* it accepts "", and `F`/`L` the character classes its match can begin/end
|
|
286
|
+
* with. `F`/`L`/`N` drive the compile-time ambiguity checks; they have no
|
|
287
|
+
* runtime presence. */
|
|
288
|
+
export class Reg {
|
|
289
|
+
/** @internal */
|
|
290
|
+
root;
|
|
291
|
+
#lastParse = null;
|
|
292
|
+
/** @internal — use the static builders. */
|
|
293
|
+
constructor(root) {
|
|
294
|
+
this.root = root;
|
|
295
|
+
}
|
|
296
|
+
// ── leaf builders ─────────────────────────────────────────────────
|
|
297
|
+
/** A fixed delimiter: matched and printed, never surfaced as a value. */
|
|
298
|
+
static lit(text) {
|
|
299
|
+
return new Reg({ kind: "lit", text });
|
|
300
|
+
}
|
|
301
|
+
/** Text up to (but not including) the delimiter `c` — i.e. `[^c]*`. Nullable
|
|
302
|
+
* (an empty field is allowed); the natural companion of `star(lit(c))`. */
|
|
303
|
+
static until(c) {
|
|
304
|
+
const escaped = c.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
305
|
+
return new Reg({
|
|
306
|
+
kind: "copy",
|
|
307
|
+
re: new RegExp(`[^${escaped}]*`),
|
|
308
|
+
engine: compileRegex(new RegExp(`[^${escaped}]*`)),
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
/** One or more digits, `\d+`, as a string. */
|
|
312
|
+
static digits() {
|
|
313
|
+
return new Reg({ kind: "copy", re: /\d+/, engine: compileRegex(/\d+/) });
|
|
314
|
+
}
|
|
315
|
+
/** One or more digits, decoded as a `number` (a quotient lens — leading
|
|
316
|
+
* zeros are not preserved). */
|
|
317
|
+
static int() {
|
|
318
|
+
return new Reg({
|
|
319
|
+
kind: "of",
|
|
320
|
+
re: /\d+/,
|
|
321
|
+
engine: compileRegex(/\d+/),
|
|
322
|
+
codec: numCodec(true),
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
/** One or more ASCII letters, `[A-Za-z]+`. */
|
|
326
|
+
static letters() {
|
|
327
|
+
return new Reg({ kind: "copy", re: /[A-Za-z]+/, engine: compileRegex(/[A-Za-z]+/) });
|
|
328
|
+
}
|
|
329
|
+
/** One or more word characters, `\w+` (letters, digits, underscore). */
|
|
330
|
+
static word() {
|
|
331
|
+
return new Reg({ kind: "copy", re: /\w+/, engine: compileRegex(/\w+/) });
|
|
332
|
+
}
|
|
333
|
+
/** The escape hatch: capture the text matched by an arbitrary regular `re`.
|
|
334
|
+
* Non-regular constructs (anchors, lookaround, backreferences) throw. The
|
|
335
|
+
* boundary is opaque to the type system (`AnyBound`), so adjacency can't be
|
|
336
|
+
* checked at compile time — the construction-time check still applies. */
|
|
337
|
+
static copy(re) {
|
|
338
|
+
return new Reg({ kind: "copy", re, engine: compileRegex(re) });
|
|
339
|
+
}
|
|
340
|
+
/** Typed escape hatch: `re` recognizes, `codec` decodes/encodes. */
|
|
341
|
+
static of(re, codec) {
|
|
342
|
+
return new Reg({
|
|
343
|
+
kind: "of",
|
|
344
|
+
re,
|
|
345
|
+
engine: compileRegex(re),
|
|
346
|
+
codec: codec,
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
// ── combinators ───────────────────────────────────────────────────
|
|
350
|
+
/** Unambiguous concatenation; every boundary is checked here and throws on
|
|
351
|
+
* ambiguity. (For compile-time adjacency checking, prefer the fluent
|
|
352
|
+
* `a.then(b).then(c)`, which validates each link.) */
|
|
353
|
+
static seq(...parts) {
|
|
354
|
+
const flat = flattenSeq(parts);
|
|
355
|
+
checkSeq(flat);
|
|
356
|
+
return new Reg({ kind: "seq", parts: flat });
|
|
357
|
+
}
|
|
358
|
+
/** Ordered union; branches must be first-disjoint (checked). */
|
|
359
|
+
static alt(...branches) {
|
|
360
|
+
const bs = branches.map(b => b.root);
|
|
361
|
+
checkAlt(bs);
|
|
362
|
+
return new Reg({ kind: "alt", branches: bs });
|
|
363
|
+
}
|
|
364
|
+
/** Optional (`part` or nothing). `part` must be non-nullable. The value is
|
|
365
|
+
* the inner value when present and `null` when absent; an optional with no
|
|
366
|
+
* value of its own (e.g. `lit(...).optional()`) records presence as `true`. */
|
|
367
|
+
static opt(part) {
|
|
368
|
+
checkOpt(part.root);
|
|
369
|
+
return new Reg({ kind: "opt", part: part.root });
|
|
370
|
+
}
|
|
371
|
+
/** `seq(this, ...next)`. A provably-overlapping boundary between `this` and
|
|
372
|
+
* the next part is a *type* error — so `a.then(b).then(c)` statically checks
|
|
373
|
+
* every link. Interior boundaries of a multi-arg call, and the `copy`/`of`
|
|
374
|
+
* escapes, are checked at construction (throws). */
|
|
375
|
+
then(...next) {
|
|
376
|
+
const flat = flattenSeq([this, ...next]);
|
|
377
|
+
checkSeq(flat);
|
|
378
|
+
return new Reg({ kind: "seq", parts: flat });
|
|
379
|
+
}
|
|
380
|
+
/** `alt(this, other)`. */
|
|
381
|
+
or(other) {
|
|
382
|
+
return Reg.alt(this, other);
|
|
383
|
+
}
|
|
384
|
+
/** `opt(this)`. Only available when `this` is non-nullable. */
|
|
385
|
+
optional() {
|
|
386
|
+
return Reg.opt(this);
|
|
387
|
+
}
|
|
388
|
+
/** Iterate zero-or-more, optionally separated by `sep`; binds to an `Arr`.
|
|
389
|
+
* A separated star is a *split* (≥1 piece, like `Str.split`). Pass
|
|
390
|
+
* `opts.key` for resourceful alignment across reorders. */
|
|
391
|
+
star(sep, opts = {}) {
|
|
392
|
+
const part = this.root;
|
|
393
|
+
const sepNode = sep === undefined ? undefined : sep.root;
|
|
394
|
+
checkStar(part, sepNode);
|
|
395
|
+
return new Reg({
|
|
396
|
+
kind: "star",
|
|
397
|
+
part,
|
|
398
|
+
sep: sepNode,
|
|
399
|
+
joiner: joinerFor(sepNode),
|
|
400
|
+
min: 0,
|
|
401
|
+
key: opts.key,
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
/** Iterate one-or-more (forbids the empty list when unseparated). */
|
|
405
|
+
plus(sep, opts = {}) {
|
|
406
|
+
const part = this.root;
|
|
407
|
+
const sepNode = sep === undefined ? undefined : sep.root;
|
|
408
|
+
checkStar(part, sepNode);
|
|
409
|
+
return new Reg({
|
|
410
|
+
kind: "star",
|
|
411
|
+
part,
|
|
412
|
+
sep: sepNode,
|
|
413
|
+
joiner: joinerFor(sepNode),
|
|
414
|
+
min: 1,
|
|
415
|
+
key: opts.key,
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
/** Name this capture so `bind` exposes it as a handle (`copy`/`of`/`star`). */
|
|
419
|
+
as(name) {
|
|
420
|
+
const r = this.root;
|
|
421
|
+
if (r.kind !== "copy" && r.kind !== "of" && r.kind !== "star") {
|
|
422
|
+
throw new RegError(`Reg.as: can only name copy/of/star, got "${r.kind}"`);
|
|
423
|
+
}
|
|
424
|
+
return new Reg({ ...r, name });
|
|
425
|
+
}
|
|
426
|
+
/** Attach a codec to a `copy` leaf, turning it into a typed `of` capture. */
|
|
427
|
+
map(codec) {
|
|
428
|
+
const r = this.root;
|
|
429
|
+
if (r.kind !== "copy")
|
|
430
|
+
throw new RegError(`Reg.map: only on a copy leaf, got "${r.kind}"`);
|
|
431
|
+
return new Reg({
|
|
432
|
+
kind: "of",
|
|
433
|
+
re: r.re,
|
|
434
|
+
engine: r.engine,
|
|
435
|
+
codec: codec,
|
|
436
|
+
name: r.name,
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
// ── pure parser / printer ────────────────────────────────────────
|
|
440
|
+
/** Parse `s` fully (must consume to the end); `null` if it doesn't match.
|
|
441
|
+
* Single-pass and linear. */
|
|
442
|
+
match(s) {
|
|
443
|
+
if (this.#lastParse !== null && this.#lastParse.s === s)
|
|
444
|
+
return this.#lastParse.v;
|
|
445
|
+
const v = parseNode(this.root, s);
|
|
446
|
+
this.#lastParse = { s, v };
|
|
447
|
+
return v;
|
|
448
|
+
}
|
|
449
|
+
/** Reflective print: render a value back to source text. */
|
|
450
|
+
print(v) {
|
|
451
|
+
return printNode(this.root, v);
|
|
452
|
+
}
|
|
453
|
+
/** Does `s` fully match? Linear. */
|
|
454
|
+
test(s) {
|
|
455
|
+
return recognize(progOf(this.root), s);
|
|
456
|
+
}
|
|
457
|
+
/** Source spans of each named capture, keyed by name — the `get`/`put`
|
|
458
|
+
* correspondence made visible. Empty if `s` doesn't fully match. */
|
|
459
|
+
spans(s) {
|
|
460
|
+
const m = new Map();
|
|
461
|
+
const r = parseValue(this.root, progOf(this.root), s, m);
|
|
462
|
+
return r !== null ? Object.fromEntries(m) : {};
|
|
463
|
+
}
|
|
464
|
+
// ── reactive binding ─────────────────────────────────────────────
|
|
465
|
+
/** This grammar as a first-class, composable `Optic<string, V>`: `get`
|
|
466
|
+
* parses (falling back to the default value off-language), `put` reprints
|
|
467
|
+
* and round-trip-guards (an off-language source or a non-round-tripping
|
|
468
|
+
* value leaves the source untouched). Drops straight into `compose(...)`
|
|
469
|
+
* and `cell.through(...)`, so it chains with `atKey`/`iso` and string
|
|
470
|
+
* lenses like `caseFold`. */
|
|
471
|
+
optic() {
|
|
472
|
+
const def = defaultVal(this.root);
|
|
473
|
+
return optic((s) => (this.match(s) ?? def), (v, s) => {
|
|
474
|
+
if (this.match(s) === null)
|
|
475
|
+
return s; // source off-language: don't clobber
|
|
476
|
+
const next = this.print(v);
|
|
477
|
+
return this.match(next) === null ? s : next; // print must round-trip
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
/** The whole abstract value as a writable lens over `source`. */
|
|
481
|
+
view(source) {
|
|
482
|
+
return source.through(this.optic());
|
|
483
|
+
}
|
|
484
|
+
bind(source, opts = {}) {
|
|
485
|
+
const captures = new Map();
|
|
486
|
+
collectCaptures(this.root, [], captures);
|
|
487
|
+
if (opts.schema !== undefined) {
|
|
488
|
+
for (const [name, kind] of Object.entries(opts.schema)) {
|
|
489
|
+
const cap = captures.get(name);
|
|
490
|
+
if (cap === undefined)
|
|
491
|
+
throw new RegError(`Reg.bind: schema names "${name}", which isn't a capture`);
|
|
492
|
+
const isArr = cap.node.kind === "star";
|
|
493
|
+
if ((kind === "arr") !== isArr) {
|
|
494
|
+
throw new RegError(`Reg.bind: schema says "${name}" is "${kind}" but it's a ${isArr ? "star" : "scalar"} capture`);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
const def = defaultVal(this.root);
|
|
499
|
+
const out = {};
|
|
500
|
+
for (const [name, cap] of captures) {
|
|
501
|
+
out[name] =
|
|
502
|
+
cap.node.kind === "star"
|
|
503
|
+
? this.#starHandle(source, cap, def)
|
|
504
|
+
: this.#scalarHandle(source, cap, def);
|
|
505
|
+
}
|
|
506
|
+
return out;
|
|
507
|
+
}
|
|
508
|
+
// ── internals ────────────────────────────────────────────────────
|
|
509
|
+
#scalarHandle(source, cap, def) {
|
|
510
|
+
const leaf = cap.node;
|
|
511
|
+
const path = cap.path;
|
|
512
|
+
return Str.lens(source, (s) => {
|
|
513
|
+
const v = getAt(this.match(s) ?? def, path);
|
|
514
|
+
return leaf.kind === "of" ? leaf.codec.format(v) : String(v ?? "");
|
|
515
|
+
}, (target, s) => {
|
|
516
|
+
if (!fullLeafMatch(leaf, target))
|
|
517
|
+
return s;
|
|
518
|
+
const decoded = leaf.kind === "of" ? leaf.codec.parse(target) : target;
|
|
519
|
+
if (decoded === undefined)
|
|
520
|
+
return s;
|
|
521
|
+
const base = this.match(s);
|
|
522
|
+
if (base === null)
|
|
523
|
+
return s;
|
|
524
|
+
const next = this.print(setAt(base, path, decoded));
|
|
525
|
+
const back = this.match(next);
|
|
526
|
+
if (back === null || !regEqual(getAt(back, path), decoded))
|
|
527
|
+
return s;
|
|
528
|
+
return next;
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
#starHandle(source, cap, def) {
|
|
532
|
+
const starNode = cap.node;
|
|
533
|
+
if (starNode.part.kind !== "copy" && starNode.part.kind !== "of") {
|
|
534
|
+
throw new RegError(`Reg.bind: named star "${starNode.name}" needs a copy/of element for an Arr handle (got "${starNode.part.kind}") — use view() for structured elements`);
|
|
535
|
+
}
|
|
536
|
+
const path = cap.path;
|
|
537
|
+
const self = this;
|
|
538
|
+
const readStar = (s) => getAt(self.match(s) ?? def, path) ?? { items: [], seps: [] };
|
|
539
|
+
const writeStar = (s, sv) => {
|
|
540
|
+
const base = self.match(s);
|
|
541
|
+
if (base === null)
|
|
542
|
+
return s;
|
|
543
|
+
const next = self.print(setAt(base, path, sv));
|
|
544
|
+
return self.match(next) === null ? s : next;
|
|
545
|
+
};
|
|
546
|
+
const keyFn = starNode.key;
|
|
547
|
+
const slotIdsOf = (items) => {
|
|
548
|
+
if (keyFn === undefined)
|
|
549
|
+
return items.map((_, i) => String(i));
|
|
550
|
+
const occ = new Map();
|
|
551
|
+
return items.map(it => {
|
|
552
|
+
const k = keyFn(String(it ?? ""));
|
|
553
|
+
const n = occ.get(k) ?? 0;
|
|
554
|
+
occ.set(k, n + 1);
|
|
555
|
+
return `${k}#${n}`;
|
|
556
|
+
});
|
|
557
|
+
};
|
|
558
|
+
const indexOfId = (items, id) => keyFn === undefined
|
|
559
|
+
? Number(id) < items.length
|
|
560
|
+
? Number(id)
|
|
561
|
+
: -1
|
|
562
|
+
: slotIdsOf(items).indexOf(id);
|
|
563
|
+
const segCache = new Map();
|
|
564
|
+
const idOfCell = new WeakMap();
|
|
565
|
+
const seg = (id) => {
|
|
566
|
+
let c = segCache.get(id);
|
|
567
|
+
if (c === undefined) {
|
|
568
|
+
c = Str.lens(source, (s) => {
|
|
569
|
+
const sv = readStar(s);
|
|
570
|
+
const idx = indexOfId(sv.items, id);
|
|
571
|
+
return idx < 0 ? "" : String(sv.items[idx] ?? "");
|
|
572
|
+
}, (target, s) => {
|
|
573
|
+
const sv = readStar(s);
|
|
574
|
+
const idx = indexOfId(sv.items, id);
|
|
575
|
+
if (idx < 0)
|
|
576
|
+
return s;
|
|
577
|
+
if (!fullNodeMatch(starNode.part, target))
|
|
578
|
+
return s;
|
|
579
|
+
const items = sv.items.slice();
|
|
580
|
+
items[idx] = target;
|
|
581
|
+
return writeStar(s, { items, seps: sv.seps });
|
|
582
|
+
});
|
|
583
|
+
segCache.set(id, c);
|
|
584
|
+
idOfCell.set(c, id);
|
|
585
|
+
}
|
|
586
|
+
return c;
|
|
587
|
+
};
|
|
588
|
+
const write = (sv) => {
|
|
589
|
+
source.value = writeStar(source.peek(), sv);
|
|
590
|
+
};
|
|
591
|
+
return Arr.fromSource(source, (s) => {
|
|
592
|
+
const items = readStar(s).items;
|
|
593
|
+
return slotIdsOf(items).map(seg);
|
|
594
|
+
}, {
|
|
595
|
+
insert: (v, at) => {
|
|
596
|
+
const text = v instanceof Cell ? v.value : v;
|
|
597
|
+
const sv = readStar(source.peek());
|
|
598
|
+
const items = sv.items.slice();
|
|
599
|
+
const seps = sv.seps.slice();
|
|
600
|
+
const idx = at == null || at > items.length ? items.length : Math.max(0, at);
|
|
601
|
+
items.splice(idx, 0, text);
|
|
602
|
+
if (items.length > 1)
|
|
603
|
+
seps.splice(Math.min(idx, seps.length), 0, starNode.joiner);
|
|
604
|
+
write({ items, seps });
|
|
605
|
+
return seg(slotIdsOf(items)[idx]);
|
|
606
|
+
},
|
|
607
|
+
remove: e => {
|
|
608
|
+
const sv = readStar(source.peek());
|
|
609
|
+
const id = idOfCell.get(e);
|
|
610
|
+
const idx = id === undefined ? -1 : indexOfId(sv.items, id);
|
|
611
|
+
if (idx < 0)
|
|
612
|
+
return;
|
|
613
|
+
const items = sv.items.slice();
|
|
614
|
+
const seps = sv.seps.slice();
|
|
615
|
+
items.splice(idx, 1);
|
|
616
|
+
if (seps.length > 0)
|
|
617
|
+
seps.splice(Math.min(idx, seps.length - 1), 1);
|
|
618
|
+
write({ items, seps });
|
|
619
|
+
},
|
|
620
|
+
moveBefore: (e, anchor) => {
|
|
621
|
+
const sv = readStar(source.peek());
|
|
622
|
+
const fromId = idOfCell.get(e);
|
|
623
|
+
const from = fromId === undefined ? -1 : indexOfId(sv.items, fromId);
|
|
624
|
+
if (from < 0)
|
|
625
|
+
return;
|
|
626
|
+
const items = sv.items.slice();
|
|
627
|
+
const [moved] = items.splice(from, 1);
|
|
628
|
+
const anchorId = anchor == null ? undefined : idOfCell.get(anchor);
|
|
629
|
+
const ai = anchorId === undefined ? -1 : indexOfId(sv.items, anchorId);
|
|
630
|
+
const at = ai < 0 ? items.length : ai > from ? ai - 1 : ai;
|
|
631
|
+
items.splice(at, 0, moved);
|
|
632
|
+
write({ items, seps: sv.seps });
|
|
633
|
+
},
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
/** Flatten nested seqs so `a.then(b).then(c)` and `seq(a,b,c)` agree (a flat
|
|
638
|
+
* tuple of visible values), and adjacency checks see real neighbours. */
|
|
639
|
+
function flattenSeq(parts) {
|
|
640
|
+
const flat = [];
|
|
641
|
+
for (const p of parts) {
|
|
642
|
+
const r = p.root;
|
|
643
|
+
if (r.kind === "seq")
|
|
644
|
+
flat.push(...r.parts);
|
|
645
|
+
else
|
|
646
|
+
flat.push(r);
|
|
647
|
+
}
|
|
648
|
+
return flat;
|
|
649
|
+
}
|