bireactive 0.2.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/LICENSE +21 -0
- package/README.md +81 -0
- package/dist/animation/anim.d.ts +57 -0
- package/dist/animation/anim.js +318 -0
- package/dist/animation/combinators.d.ts +39 -0
- package/dist/animation/combinators.js +113 -0
- package/dist/animation/easings.d.ts +5 -0
- package/dist/animation/easings.js +5 -0
- package/dist/animation/index.d.ts +3 -0
- package/dist/animation/index.js +3 -0
- package/dist/assert/algebra.d.ts +20 -0
- package/dist/assert/algebra.js +79 -0
- package/dist/assert/claim.d.ts +40 -0
- package/dist/assert/claim.js +129 -0
- package/dist/assert/index.d.ts +7 -0
- package/dist/assert/index.js +19 -0
- package/dist/assert/predicates.d.ts +18 -0
- package/dist/assert/predicates.js +43 -0
- package/dist/assert/record.d.ts +20 -0
- package/dist/assert/record.js +78 -0
- package/dist/assert/scope.d.ts +42 -0
- package/dist/assert/scope.js +233 -0
- package/dist/assert/span.d.ts +37 -0
- package/dist/assert/span.js +68 -0
- package/dist/assert/tree.d.ts +22 -0
- package/dist/assert/tree.js +65 -0
- package/dist/code/code.d.ts +70 -0
- package/dist/code/code.js +361 -0
- package/dist/code/index.d.ts +2 -0
- package/dist/code/index.js +9 -0
- package/dist/code/morph.d.ts +5 -0
- package/dist/code/morph.js +194 -0
- package/dist/code/tokenize.d.ts +8 -0
- package/dist/code/tokenize.js +51 -0
- package/dist/constraints/cluster.d.ts +83 -0
- package/dist/constraints/cluster.js +213 -0
- package/dist/constraints/drivers.d.ts +15 -0
- package/dist/constraints/drivers.js +40 -0
- package/dist/constraints/factories.d.ts +73 -0
- package/dist/constraints/factories.js +248 -0
- package/dist/constraints/index.d.ts +11 -0
- package/dist/constraints/index.js +39 -0
- package/dist/constraints/interaction.d.ts +21 -0
- package/dist/constraints/interaction.js +148 -0
- package/dist/constraints/linalg.d.ts +18 -0
- package/dist/constraints/linalg.js +141 -0
- package/dist/constraints/phases.d.ts +21 -0
- package/dist/constraints/phases.js +60 -0
- package/dist/constraints/physics.d.ts +34 -0
- package/dist/constraints/physics.js +128 -0
- package/dist/constraints/rigid.d.ts +210 -0
- package/dist/constraints/rigid.js +835 -0
- package/dist/constraints/solver.d.ts +107 -0
- package/dist/constraints/solver.js +510 -0
- package/dist/constraints/term.d.ts +50 -0
- package/dist/constraints/term.js +80 -0
- package/dist/constraints/terms.d.ts +80 -0
- package/dist/constraints/terms.js +302 -0
- package/dist/constraints/world.d.ts +31 -0
- package/dist/constraints/world.js +245 -0
- package/dist/core/aggregates.d.ts +64 -0
- package/dist/core/aggregates.js +198 -0
- package/dist/core/anim.d.ts +84 -0
- package/dist/core/anim.js +301 -0
- package/dist/core/index.d.ts +38 -0
- package/dist/core/index.js +38 -0
- package/dist/core/introspect.d.ts +5 -0
- package/dist/core/introspect.js +31 -0
- package/dist/core/lenses/closed-form-policies.d.ts +64 -0
- package/dist/core/lenses/closed-form-policies.js +452 -0
- package/dist/core/lenses/domain-aggregates.d.ts +54 -0
- package/dist/core/lenses/domain-aggregates.js +259 -0
- package/dist/core/lenses/factor-lens.d.ts +42 -0
- package/dist/core/lenses/factor-lens.js +419 -0
- package/dist/core/lenses/index.d.ts +5 -0
- package/dist/core/lenses/index.js +16 -0
- package/dist/core/lenses/memory.d.ts +47 -0
- package/dist/core/lenses/memory.js +102 -0
- package/dist/core/lenses/typed-factor.d.ts +45 -0
- package/dist/core/lenses/typed-factor.js +376 -0
- package/dist/core/network-utils.d.ts +14 -0
- package/dist/core/network-utils.js +62 -0
- package/dist/core/new-primitives.d.ts +33 -0
- package/dist/core/new-primitives.js +113 -0
- package/dist/core/signal.d.ts +254 -0
- package/dist/core/signal.js +1349 -0
- package/dist/core/traits.d.ts +61 -0
- package/dist/core/traits.js +56 -0
- package/dist/core/tree.d.ts +23 -0
- package/dist/core/tree.js +62 -0
- package/dist/core/values/anchor.d.ts +23 -0
- package/dist/core/values/anchor.js +23 -0
- package/dist/core/values/audio.d.ts +33 -0
- package/dist/core/values/audio.js +107 -0
- package/dist/core/values/bool.d.ts +37 -0
- package/dist/core/values/bool.js +75 -0
- package/dist/core/values/box.d.ts +77 -0
- package/dist/core/values/box.js +211 -0
- package/dist/core/values/canvas.d.ts +71 -0
- package/dist/core/values/canvas.js +495 -0
- package/dist/core/values/color.d.ts +49 -0
- package/dist/core/values/color.js +106 -0
- package/dist/core/values/flags.d.ts +18 -0
- package/dist/core/values/flags.js +50 -0
- package/dist/core/values/gpu.d.ts +74 -0
- package/dist/core/values/gpu.js +426 -0
- package/dist/core/values/matrix.d.ts +53 -0
- package/dist/core/values/matrix.js +140 -0
- package/dist/core/values/num.d.ts +62 -0
- package/dist/core/values/num.js +166 -0
- package/dist/core/values/pose.d.ts +31 -0
- package/dist/core/values/pose.js +83 -0
- package/dist/core/values/range.d.ts +83 -0
- package/dist/core/values/range.js +167 -0
- package/dist/core/values/str.d.ts +76 -0
- package/dist/core/values/str.js +346 -0
- package/dist/core/values/template.d.ts +49 -0
- package/dist/core/values/template.js +148 -0
- package/dist/core/values/transform.d.ts +49 -0
- package/dist/core/values/transform.js +115 -0
- package/dist/core/values/tri.d.ts +31 -0
- package/dist/core/values/tri.js +95 -0
- package/dist/core/values/vec.d.ts +72 -0
- package/dist/core/values/vec.js +219 -0
- package/dist/core/writable.d.ts +15 -0
- package/dist/core/writable.js +29 -0
- package/dist/ext/events.d.ts +10 -0
- package/dist/ext/events.js +31 -0
- package/dist/ext/index.d.ts +4 -0
- package/dist/ext/index.js +4 -0
- package/dist/ext/snapshot.d.ts +8 -0
- package/dist/ext/snapshot.js +29 -0
- package/dist/ext/timeline.d.ts +56 -0
- package/dist/ext/timeline.js +94 -0
- package/dist/ext/waapi.d.ts +25 -0
- package/dist/ext/waapi.js +198 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +10 -0
- package/dist/propagators/index.d.ts +6 -0
- package/dist/propagators/index.js +6 -0
- package/dist/propagators/layout.d.ts +68 -0
- package/dist/propagators/layout.js +336 -0
- package/dist/propagators/network.d.ts +52 -0
- package/dist/propagators/network.js +185 -0
- package/dist/propagators/propagator.d.ts +12 -0
- package/dist/propagators/propagator.js +16 -0
- package/dist/propagators/range.d.ts +45 -0
- package/dist/propagators/range.js +147 -0
- package/dist/propagators/relations.d.ts +60 -0
- package/dist/propagators/relations.js +343 -0
- package/dist/shapes/annular-sector.d.ts +15 -0
- package/dist/shapes/annular-sector.js +64 -0
- package/dist/shapes/button.d.ts +14 -0
- package/dist/shapes/button.js +31 -0
- package/dist/shapes/choreographers.d.ts +22 -0
- package/dist/shapes/choreographers.js +69 -0
- package/dist/shapes/circle.d.ts +17 -0
- package/dist/shapes/circle.js +57 -0
- package/dist/shapes/clip.d.ts +5 -0
- package/dist/shapes/clip.js +31 -0
- package/dist/shapes/connect.d.ts +16 -0
- package/dist/shapes/connect.js +70 -0
- package/dist/shapes/curve.d.ts +60 -0
- package/dist/shapes/curve.js +285 -0
- package/dist/shapes/dashed.d.ts +16 -0
- package/dist/shapes/dashed.js +142 -0
- package/dist/shapes/debug.d.ts +43 -0
- package/dist/shapes/debug.js +97 -0
- package/dist/shapes/group.d.ts +5 -0
- package/dist/shapes/group.js +10 -0
- package/dist/shapes/handle.d.ts +32 -0
- package/dist/shapes/handle.js +88 -0
- package/dist/shapes/index.d.ts +23 -0
- package/dist/shapes/index.js +23 -0
- package/dist/shapes/interaction.d.ts +32 -0
- package/dist/shapes/interaction.js +187 -0
- package/dist/shapes/label.d.ts +20 -0
- package/dist/shapes/label.js +42 -0
- package/dist/shapes/layout.d.ts +29 -0
- package/dist/shapes/layout.js +74 -0
- package/dist/shapes/line.d.ts +21 -0
- package/dist/shapes/line.js +79 -0
- package/dist/shapes/list.d.ts +18 -0
- package/dist/shapes/list.js +51 -0
- package/dist/shapes/mount.d.ts +7 -0
- package/dist/shapes/mount.js +10 -0
- package/dist/shapes/path.d.ts +77 -0
- package/dist/shapes/path.js +227 -0
- package/dist/shapes/rect.d.ts +30 -0
- package/dist/shapes/rect.js +131 -0
- package/dist/shapes/shape.d.ts +132 -0
- package/dist/shapes/shape.js +306 -0
- package/dist/shapes/text.d.ts +24 -0
- package/dist/shapes/text.js +53 -0
- package/dist/shapes/tokens.d.ts +28 -0
- package/dist/shapes/tokens.js +27 -0
- package/dist/shapes/transitions.d.ts +23 -0
- package/dist/shapes/transitions.js +62 -0
- package/dist/tex/decorations.d.ts +26 -0
- package/dist/tex/decorations.js +116 -0
- package/dist/tex/index.d.ts +5 -0
- package/dist/tex/index.js +5 -0
- package/dist/tex/marker.d.ts +17 -0
- package/dist/tex/marker.js +63 -0
- package/dist/tex/motion.d.ts +43 -0
- package/dist/tex/motion.js +290 -0
- package/dist/tex/parts.d.ts +65 -0
- package/dist/tex/parts.js +149 -0
- package/dist/tex/tex.d.ts +45 -0
- package/dist/tex/tex.js +244 -0
- package/dist/web/attr.d.ts +16 -0
- package/dist/web/attr.js +98 -0
- package/dist/web/diagram.d.ts +49 -0
- package/dist/web/diagram.js +260 -0
- package/dist/web/index.d.ts +6 -0
- package/dist/web/index.js +6 -0
- package/dist/web/md-marker.d.ts +6 -0
- package/dist/web/md-marker.js +39 -0
- package/dist/web/md-tex.d.ts +6 -0
- package/dist/web/md-tex.js +61 -0
- package/dist/web/raf.d.ts +6 -0
- package/dist/web/raf.js +24 -0
- package/dist/web/viewport.d.ts +7 -0
- package/dist/web/viewport.js +13 -0
- package/package.json +87 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
// range.ts — reactive numeric interval `[lo, hi]`.
|
|
2
|
+
//
|
|
3
|
+
// Home for sliders, scrollbars, and timeline clip spans. Field lenses
|
|
4
|
+
// give start-knob (`.lo`) and end-knob (`.hi`) drag; `.start` is the
|
|
5
|
+
// body-drag (shifts both, preserving width); `.slider(t)` is the
|
|
6
|
+
// bidirectional `t ↔ lo + t·(hi - lo)` iso. Traits: `linear`, `lerp`,
|
|
7
|
+
// `equals`, `pack` (scalar-only).
|
|
8
|
+
import { tween } from "../anim.js";
|
|
9
|
+
import { Cell, isComputed, reader, readNow, } from "../signal.js";
|
|
10
|
+
import { derived, field } from "../writable.js";
|
|
11
|
+
import { Bool } from "./bool.js";
|
|
12
|
+
import { Num, num } from "./num.js";
|
|
13
|
+
export const add = (a, b) => ({ lo: a.lo + b.lo, hi: a.hi + b.hi });
|
|
14
|
+
export const sub = (a, b) => ({ lo: a.lo - b.lo, hi: a.hi - b.hi });
|
|
15
|
+
export const scale = (a, k) => ({ lo: a.lo * k, hi: a.hi * k });
|
|
16
|
+
export const lerp = (a, b, t) => ({
|
|
17
|
+
lo: a.lo + (b.lo - a.lo) * t,
|
|
18
|
+
hi: a.hi + (b.hi - a.hi) * t,
|
|
19
|
+
});
|
|
20
|
+
export const equals = (a, b) => a === b || (a.lo === b.lo && a.hi === b.hi);
|
|
21
|
+
/** L2 distance over (lo, hi). Treats a range as a point in 2-space. */
|
|
22
|
+
export const metric = (a, b) => Math.hypot(a.lo - b.lo, a.hi - b.hi);
|
|
23
|
+
export const width = (r) => r.hi - r.lo;
|
|
24
|
+
export const center = (r) => (r.lo + r.hi) / 2;
|
|
25
|
+
export const contains = (r, v) => v >= r.lo && v <= r.hi;
|
|
26
|
+
export const clamp = (r, v) => (v < r.lo ? r.lo : v > r.hi ? r.hi : v);
|
|
27
|
+
/** Closest value STRICTLY outside `[lo, hi]`, displaced past the
|
|
28
|
+
* nearest endpoint by `eps`. Used by `Range#contains` as the bwd's
|
|
29
|
+
* false-side policy. */
|
|
30
|
+
export const eject = (r, v, eps = 1e-6) => {
|
|
31
|
+
if (!contains(r, v))
|
|
32
|
+
return v;
|
|
33
|
+
return v - r.lo <= r.hi - v ? r.lo - eps : r.hi + eps;
|
|
34
|
+
};
|
|
35
|
+
/** Sample at parameter `t`: `lo + t·(hi - lo)`. `t ∈ [0, 1]` stays
|
|
36
|
+
* inside the range; values outside extrapolate linearly. */
|
|
37
|
+
export const sample = (r, t) => r.lo + t * (r.hi - r.lo);
|
|
38
|
+
/** Inverse of `sample`: given a value, recover the `t` that would
|
|
39
|
+
* produce it. Degenerate (zero-width) ranges return 0. */
|
|
40
|
+
export const paramOf = (r, v) => {
|
|
41
|
+
const w = r.hi - r.lo;
|
|
42
|
+
return w === 0 ? 0 : (v - r.lo) / w;
|
|
43
|
+
};
|
|
44
|
+
const linearImpl = { add, sub, scale };
|
|
45
|
+
const packImpl = {
|
|
46
|
+
dim: 2,
|
|
47
|
+
read: (v, a, o) => {
|
|
48
|
+
a[o] = v.lo;
|
|
49
|
+
a[o + 1] = v.hi;
|
|
50
|
+
},
|
|
51
|
+
write: (a, o) => ({ lo: a[o], hi: a[o + 1] }),
|
|
52
|
+
};
|
|
53
|
+
export class Range extends Cell {
|
|
54
|
+
static traits = {
|
|
55
|
+
linear: linearImpl,
|
|
56
|
+
lerp,
|
|
57
|
+
metric,
|
|
58
|
+
equals,
|
|
59
|
+
pack: packImpl,
|
|
60
|
+
};
|
|
61
|
+
constructor(v = { lo: 0, hi: 1 }) {
|
|
62
|
+
super(v, { equals });
|
|
63
|
+
}
|
|
64
|
+
/** Start endpoint. Writes preserve `hi` (start-knob semantics). */
|
|
65
|
+
get lo() {
|
|
66
|
+
return field(this, "lo", Num);
|
|
67
|
+
}
|
|
68
|
+
/** End endpoint. Writes preserve `lo` (end-knob semantics). */
|
|
69
|
+
get hi() {
|
|
70
|
+
return field(this, "hi", Num);
|
|
71
|
+
}
|
|
72
|
+
get width() {
|
|
73
|
+
return derived(this, "width", Num, width);
|
|
74
|
+
}
|
|
75
|
+
get center() {
|
|
76
|
+
return derived(this, "center", Num, center);
|
|
77
|
+
}
|
|
78
|
+
/** Translate by `by`. Reads shift the interval; writes shift back. */
|
|
79
|
+
shift(by) {
|
|
80
|
+
const f = reader(by);
|
|
81
|
+
return this.lens(v => ({ lo: v.lo + f(), hi: v.hi + f() }), n => ({ lo: n.lo - f(), hi: n.hi - f() }));
|
|
82
|
+
}
|
|
83
|
+
/** Scale uniformly about the origin. Iso for `k ≠ 0`. */
|
|
84
|
+
scale(k) {
|
|
85
|
+
const kf = reader(k);
|
|
86
|
+
return this.lens(v => {
|
|
87
|
+
const k = kf();
|
|
88
|
+
return { lo: v.lo * k, hi: v.hi * k };
|
|
89
|
+
}, n => {
|
|
90
|
+
const k = kf();
|
|
91
|
+
return { lo: n.lo / k, hi: n.hi / k };
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
/** Body-drag handle: read returns `lo`; write shifts the range so `lo`
|
|
95
|
+
* matches (width preserved). For start-knob editing use `.lo`. */
|
|
96
|
+
get start() {
|
|
97
|
+
return Num.lens(this, v => v.lo, (newLo, src) => ({ lo: newLo, hi: newLo + (src.hi - src.lo) }));
|
|
98
|
+
}
|
|
99
|
+
/** RO sample at `t`. `t ∈ [0, 1]` stays inside; outside extrapolates. */
|
|
100
|
+
sample(t) {
|
|
101
|
+
return Num.derive(() => sample(this.value, readNow(t)));
|
|
102
|
+
}
|
|
103
|
+
/** Bidirectional `t ↔ value` slider. Read `lo + t·(hi - lo)`; write
|
|
104
|
+
* solves for `t` and updates `t` only, leaving `lo` / `hi` put. */
|
|
105
|
+
slider(t) {
|
|
106
|
+
// `this as Range` pins the tuple element type (polymorphic `this`
|
|
107
|
+
// defeats the mapped-tuple inference on `[this, t]`).
|
|
108
|
+
return Num.lens([this, t], ([r, tv]) => sample(r, tv), (v, [r]) => {
|
|
109
|
+
const w = r.hi - r.lo;
|
|
110
|
+
return [undefined, w === 0 ? 0 : (v - r.lo) / w];
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
/** Membership predicate. Conditional return type: a writable `Num`
|
|
114
|
+
* yields `Writable<Bool>` and flipping the view bumps the source
|
|
115
|
+
* (`true` clamps into `[lo, hi]`, `false` ejects past the nearest
|
|
116
|
+
* endpoint by `eps`). Literal / RO inputs yield a bare RO `Bool`. */
|
|
117
|
+
contains(v) {
|
|
118
|
+
if (v instanceof Num) {
|
|
119
|
+
// RO computed Num has no backward path → RO branch. Sources and
|
|
120
|
+
// writable lenses both accept write-back.
|
|
121
|
+
if (!isComputed(v)) {
|
|
122
|
+
return Bool.lens([this, v], (vals) => contains(vals[0], vals[1]), (target, vals) => {
|
|
123
|
+
const [r, n] = vals;
|
|
124
|
+
if (contains(r, n) === target)
|
|
125
|
+
return [undefined, undefined];
|
|
126
|
+
return [undefined, target ? clamp(r, n) : eject(r, n)];
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return Bool.derive(() => contains(this.value, readNow(v)));
|
|
131
|
+
}
|
|
132
|
+
/** RO clamp: read `v` into `[lo, hi]`. For a writable clamping lens
|
|
133
|
+
* on a single Num, see `Num#clamp(lo, hi)`. */
|
|
134
|
+
clampedRead(v) {
|
|
135
|
+
return Num.derive(() => clamp(this.value, readNow(v)));
|
|
136
|
+
}
|
|
137
|
+
/** Inverse of `sample`: derive the `t` that would produce `v`. */
|
|
138
|
+
paramOf(v) {
|
|
139
|
+
return Num.derive(() => paramOf(this.value, readNow(v)));
|
|
140
|
+
}
|
|
141
|
+
/** Tween-builder; animates `{lo, hi}` jointly. */
|
|
142
|
+
to(target, dur, ease) {
|
|
143
|
+
return tween(this, target, dur, ease);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/** @internal — 2-input lens over two writable `Num`s; `range()` delegates
|
|
147
|
+
* here after lifting literals. */
|
|
148
|
+
function ends(lo, hi) {
|
|
149
|
+
return Range.lens([lo, hi], (vals) => ({ lo: vals[0], hi: vals[1] }), (target) => [target.lo, target.hi]);
|
|
150
|
+
}
|
|
151
|
+
/** Range over `[at, at + dur]`, parameterised by start + duration. The
|
|
152
|
+
* timeline-clip shape: `.lo` slides the start, `.hi` the end, `.start`
|
|
153
|
+
* body-drags (preserving width). Backed by the live `at` / `dur` Nums. */
|
|
154
|
+
export function span(at, dur) {
|
|
155
|
+
return Range.lens([at, dur], (vals) => ({ lo: vals[0], hi: vals[0] + vals[1] }), (target) => [target.lo, target.hi - target.lo]);
|
|
156
|
+
}
|
|
157
|
+
/** Writable `Range` over `[lo, hi]`. Each endpoint is a literal `number`
|
|
158
|
+
* (lifted to a fresh seed) or an existing `Writable<Num>` (identity
|
|
159
|
+
* passthrough). RO sources are rejected at the type level — use
|
|
160
|
+
* `Range.derive(...)` for reactive RO tracking, or `cell.value` to
|
|
161
|
+
* snapshot. Lock an endpoint with `Num.pin(c)`. */
|
|
162
|
+
export function range(lo = 0, hi = 1) {
|
|
163
|
+
if (typeof lo === "number" && typeof hi === "number") {
|
|
164
|
+
return new Range({ lo, hi });
|
|
165
|
+
}
|
|
166
|
+
return ends(num(lo), num(hi));
|
|
167
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { Cell, type Init, type Writable } from "../signal.js";
|
|
2
|
+
type V = string;
|
|
3
|
+
export declare const equals: (a: V, b: V) => boolean;
|
|
4
|
+
/** Reverse a string by Unicode code points. */
|
|
5
|
+
export declare const reverseStr: (s: V) => V;
|
|
6
|
+
/** ROT13 cipher. Involutive: `rot13(rot13(s)) === s`. */
|
|
7
|
+
export declare const rot13Str: (s: V) => V;
|
|
8
|
+
/** Per-character case mask: `U` upper letter, `L` lower letter,
|
|
9
|
+
* `" "` non-letter. Length matches the source. */
|
|
10
|
+
export declare function caseMaskOf(s: V): string;
|
|
11
|
+
/** Apply a case mask to `target`, position by position. Mask positions
|
|
12
|
+
* beyond `target.length` are ignored; target positions beyond the
|
|
13
|
+
* mask keep their native case (e.g. user appended a longer word). */
|
|
14
|
+
export declare function applyCaseMask(target: V, mask: string): V;
|
|
15
|
+
/** Apply the case pattern of a source word to a target word. Detects
|
|
16
|
+
* all-upper / all-lower / title case, else falls back to position-wise
|
|
17
|
+
* `applyCaseMask`. Non-letter target chars always pass through unchanged
|
|
18
|
+
* (title-casing "-gng" → "-Gng"); this letter-awareness is what makes
|
|
19
|
+
* GetPut hold when source words contain non-letters. */
|
|
20
|
+
export declare function applyCasePattern(target: V, mask: string): V;
|
|
21
|
+
/** Split `s` into words and separators. Returns:
|
|
22
|
+
*
|
|
23
|
+
* words[i] — the i-th run of word characters
|
|
24
|
+
* seps[0] — leading non-word characters (possibly empty)
|
|
25
|
+
* seps[i] — for 1 ≤ i ≤ words.length-1, the separator BETWEEN
|
|
26
|
+
* `words[i-1]` and `words[i]`
|
|
27
|
+
* seps[words.length] — trailing non-word characters
|
|
28
|
+
*
|
|
29
|
+
* Always satisfies `seps.length === words.length + 1`. */
|
|
30
|
+
export declare function parseWords(s: V): {
|
|
31
|
+
words: V[];
|
|
32
|
+
seps: V[];
|
|
33
|
+
};
|
|
34
|
+
/** Inverse of `parseWords`. Interleaves words with `seps`; added words
|
|
35
|
+
* get `" "` gaps, removed words keep the original trailing separator.
|
|
36
|
+
* A zero-word original (`seps.length === 1`) treats its one entry as
|
|
37
|
+
* lead only, so words append after it without double-counting as trail. */
|
|
38
|
+
export declare function rebuildWords(words: V[], seps: V[]): V;
|
|
39
|
+
export declare class Str extends Cell<V> {
|
|
40
|
+
static traits: {
|
|
41
|
+
equals: (a: V, b: V) => boolean;
|
|
42
|
+
};
|
|
43
|
+
readonly _t: typeof Str.traits;
|
|
44
|
+
constructor(v?: V);
|
|
45
|
+
/** Reverse. Involution. */
|
|
46
|
+
reverse(): this;
|
|
47
|
+
/** ROT13. Involution. */
|
|
48
|
+
rot13(): this;
|
|
49
|
+
/** Trim edge whitespace; the complement restores the original padding
|
|
50
|
+
* on write. Edge whitespace in a write is stripped first (the view's
|
|
51
|
+
* contract is "no edge whitespace", else the complement grows). */
|
|
52
|
+
trim(): Writable<Str>;
|
|
53
|
+
/** Lowercase view with word-aware case recovery on write. Lookup
|
|
54
|
+
* priority: (1) content match — recover the source mask by word
|
|
55
|
+
* (FIFO across duplicates); (2) per-position fallback for new content;
|
|
56
|
+
* (3) native for content beyond the source structure. */
|
|
57
|
+
lowercase(): Writable<Str>;
|
|
58
|
+
/** Uppercase view. Dual of `lowercase`; same per-word case recovery. */
|
|
59
|
+
uppercase(): Writable<Str>;
|
|
60
|
+
/** Words view, one word per line. Read splits on non-word chars; the
|
|
61
|
+
* complement is the separator layout, restored on write (added words
|
|
62
|
+
* get single spaces). Non-word chars typed into a line are stripped —
|
|
63
|
+
* edit `Trimmed` / `Lowercased` / `Source` to add punctuation. */
|
|
64
|
+
words(): Writable<Str>;
|
|
65
|
+
/** Sorted, case-insensitively-unique words, one per line. The
|
|
66
|
+
* complement records each entry's source positions and original case,
|
|
67
|
+
* so editing one line broadcasts to every occurrence in the source —
|
|
68
|
+
* each rebuilt with its own original casing. */
|
|
69
|
+
sortedUnique(): Writable<Str>;
|
|
70
|
+
}
|
|
71
|
+
/** Writable `Str`. Strict factory: literal seeds a fresh cell;
|
|
72
|
+
* existing `Writable<Str>` passes through by identity. RO sources
|
|
73
|
+
* are rejected at the type level — use `Str.derive(...)` for
|
|
74
|
+
* reactive RO tracking, or `cell.value` to snapshot. */
|
|
75
|
+
export declare function str(v?: Init<Str>): Writable<Str>;
|
|
76
|
+
export {};
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
// str.ts — reactive string with a symmetric lens chain.
|
|
2
|
+
//
|
|
3
|
+
// String projections are the canonical use of the engine's stateful-lens
|
|
4
|
+
// primitive (`statefulLens`). Every useful view loses information the
|
|
5
|
+
// engine recovers on write via a per-cell `complement`, so editing through
|
|
6
|
+
// ANY view round-trips with case, whitespace, separators, and duplicate
|
|
7
|
+
// positions preserved (the Foster/Pierce case-preserving find-and-replace
|
|
8
|
+
// demo).
|
|
9
|
+
//
|
|
10
|
+
// `reverse()` and `rot13()` are involutions on the plain endo
|
|
11
|
+
// `.lens(fwd, bwd)` — no complement; they chain like any endo lens.
|
|
12
|
+
// Everything else is `Str.lens(parent, spec)` with a complement:
|
|
13
|
+
//
|
|
14
|
+
// trim — leading + trailing whitespace
|
|
15
|
+
// lowercase — per-character case mask of the source
|
|
16
|
+
// uppercase — dual of lowercase
|
|
17
|
+
// words — separator pattern between words
|
|
18
|
+
// sortedUnique — source positions + original case per unique word
|
|
19
|
+
import { Cell } from "../signal.js";
|
|
20
|
+
// Complement-carrying endo lens.
|
|
21
|
+
//
|
|
22
|
+
// The complement is state recorded forward from the source and consumed
|
|
23
|
+
// on write-back. It persists across the lens's own writes (so `trim`
|
|
24
|
+
// keeps its padding even when the view is emptied) and refreshes on
|
|
25
|
+
// external source changes — the engine's `external` flag drives `step`.
|
|
26
|
+
/** Endo lens backed by a complement recorded from the source. `record`
|
|
27
|
+
* rebuilds the complement (kept on the lens's own writes), `project`
|
|
28
|
+
* is the forward view, `reconstruct` is the backward source. */
|
|
29
|
+
function complementLens(parent, record, project, reconstruct) {
|
|
30
|
+
return Str.lens([parent], {
|
|
31
|
+
init: ([s]) => record(s),
|
|
32
|
+
step: ([s], c, external) => (external ? record(s) : c),
|
|
33
|
+
fwd: ([s]) => project(s),
|
|
34
|
+
bwd: (target, _s, c) => ({ updates: [reconstruct(target, c)], complement: c }),
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
export const equals = (a, b) => a === b;
|
|
38
|
+
/** Reverse a string by Unicode code points. */
|
|
39
|
+
export const reverseStr = (s) => [...s].reverse().join("");
|
|
40
|
+
/** ROT13 cipher. Involutive: `rot13(rot13(s)) === s`. */
|
|
41
|
+
export const rot13Str = (s) => s.replace(/[a-zA-Z]/g, c => {
|
|
42
|
+
const code = c.charCodeAt(0);
|
|
43
|
+
const base = code >= 97 ? 97 : 65;
|
|
44
|
+
return String.fromCharCode(((code - base + 13) % 26) + base);
|
|
45
|
+
});
|
|
46
|
+
/** Per-character case mask: `U` upper letter, `L` lower letter,
|
|
47
|
+
* `" "` non-letter. Length matches the source. */
|
|
48
|
+
export function caseMaskOf(s) {
|
|
49
|
+
let mask = "";
|
|
50
|
+
for (let i = 0; i < s.length; i++) {
|
|
51
|
+
const c = s[i];
|
|
52
|
+
if (c >= "A" && c <= "Z")
|
|
53
|
+
mask += "U";
|
|
54
|
+
else if (c >= "a" && c <= "z")
|
|
55
|
+
mask += "L";
|
|
56
|
+
else
|
|
57
|
+
mask += " ";
|
|
58
|
+
}
|
|
59
|
+
return mask;
|
|
60
|
+
}
|
|
61
|
+
/** Apply a case mask to `target`, position by position. Mask positions
|
|
62
|
+
* beyond `target.length` are ignored; target positions beyond the
|
|
63
|
+
* mask keep their native case (e.g. user appended a longer word). */
|
|
64
|
+
export function applyCaseMask(target, mask) {
|
|
65
|
+
let out = "";
|
|
66
|
+
for (let i = 0; i < target.length; i++) {
|
|
67
|
+
const c = target[i];
|
|
68
|
+
const m = i < mask.length ? mask[i] : " ";
|
|
69
|
+
if (m === "U")
|
|
70
|
+
out += c.toUpperCase();
|
|
71
|
+
else if (m === "L")
|
|
72
|
+
out += c.toLowerCase();
|
|
73
|
+
else
|
|
74
|
+
out += c;
|
|
75
|
+
}
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
78
|
+
const ASCII_LETTER = (c) => (c >= "a" && c <= "z") || (c >= "A" && c <= "Z");
|
|
79
|
+
/** Apply the case pattern of a source word to a target word. Detects
|
|
80
|
+
* all-upper / all-lower / title case, else falls back to position-wise
|
|
81
|
+
* `applyCaseMask`. Non-letter target chars always pass through unchanged
|
|
82
|
+
* (title-casing "-gng" → "-Gng"); this letter-awareness is what makes
|
|
83
|
+
* GetPut hold when source words contain non-letters. */
|
|
84
|
+
export function applyCasePattern(target, mask) {
|
|
85
|
+
if (target.length === 0 || mask.length === 0)
|
|
86
|
+
return target;
|
|
87
|
+
const letters = [...mask].filter(c => c === "U" || c === "L");
|
|
88
|
+
if (letters.length === 0)
|
|
89
|
+
return target;
|
|
90
|
+
if (letters.every(c => c === "U"))
|
|
91
|
+
return target.toUpperCase();
|
|
92
|
+
if (letters.every(c => c === "L"))
|
|
93
|
+
return target.toLowerCase();
|
|
94
|
+
if (letters[0] === "U" && letters.slice(1).every(c => c === "L")) {
|
|
95
|
+
// Title case: uppercase the first letter (skipping leading
|
|
96
|
+
// non-letters), lowercase the rest, pass non-letters through.
|
|
97
|
+
let out = "";
|
|
98
|
+
let firstLetterDone = false;
|
|
99
|
+
for (let i = 0; i < target.length; i++) {
|
|
100
|
+
const c = target[i];
|
|
101
|
+
if (ASCII_LETTER(c)) {
|
|
102
|
+
out += firstLetterDone ? c.toLowerCase() : c.toUpperCase();
|
|
103
|
+
firstLetterDone = true;
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
out += c;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return out;
|
|
110
|
+
}
|
|
111
|
+
return applyCaseMask(target, mask);
|
|
112
|
+
}
|
|
113
|
+
/** A "word" character: letters, digits, underscore, apostrophe, hyphen
|
|
114
|
+
* (handles "don't", "co-op"). Everything else is a separator. */
|
|
115
|
+
const WORD_CHAR = /[\p{L}\p{N}_'-]/u;
|
|
116
|
+
/** Strip every non-word character. Keeps user-typed punctuation in the
|
|
117
|
+
* `words` / `sortedUnique` views from leaking into the source via the
|
|
118
|
+
* separator complement (where it would accumulate across edits). */
|
|
119
|
+
const stripNonWord = (s) => s.replace(/[^\p{L}\p{N}_'-]/gu, "");
|
|
120
|
+
/** Split `s` into words and separators. Returns:
|
|
121
|
+
*
|
|
122
|
+
* words[i] — the i-th run of word characters
|
|
123
|
+
* seps[0] — leading non-word characters (possibly empty)
|
|
124
|
+
* seps[i] — for 1 ≤ i ≤ words.length-1, the separator BETWEEN
|
|
125
|
+
* `words[i-1]` and `words[i]`
|
|
126
|
+
* seps[words.length] — trailing non-word characters
|
|
127
|
+
*
|
|
128
|
+
* Always satisfies `seps.length === words.length + 1`. */
|
|
129
|
+
export function parseWords(s) {
|
|
130
|
+
const words = [];
|
|
131
|
+
const seps = [];
|
|
132
|
+
let cur = "";
|
|
133
|
+
let inWord = false;
|
|
134
|
+
for (let i = 0; i < s.length; i++) {
|
|
135
|
+
const c = s[i];
|
|
136
|
+
if (WORD_CHAR.test(c)) {
|
|
137
|
+
if (!inWord) {
|
|
138
|
+
seps.push(cur);
|
|
139
|
+
cur = "";
|
|
140
|
+
inWord = true;
|
|
141
|
+
}
|
|
142
|
+
cur += c;
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
if (inWord) {
|
|
146
|
+
words.push(cur);
|
|
147
|
+
cur = "";
|
|
148
|
+
inWord = false;
|
|
149
|
+
}
|
|
150
|
+
cur += c;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (inWord) {
|
|
154
|
+
words.push(cur);
|
|
155
|
+
seps.push("");
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
seps.push(cur);
|
|
159
|
+
}
|
|
160
|
+
return { words, seps };
|
|
161
|
+
}
|
|
162
|
+
/** Inverse of `parseWords`. Interleaves words with `seps`; added words
|
|
163
|
+
* get `" "` gaps, removed words keep the original trailing separator.
|
|
164
|
+
* A zero-word original (`seps.length === 1`) treats its one entry as
|
|
165
|
+
* lead only, so words append after it without double-counting as trail. */
|
|
166
|
+
export function rebuildWords(words, seps) {
|
|
167
|
+
const n = words.length;
|
|
168
|
+
if (n === 0)
|
|
169
|
+
return seps[0] ?? "";
|
|
170
|
+
const lead = seps[0] ?? "";
|
|
171
|
+
const trail = seps.length > 1 ? (seps[seps.length - 1] ?? "") : "";
|
|
172
|
+
let out = lead;
|
|
173
|
+
for (let i = 0; i < n; i++) {
|
|
174
|
+
out += words[i];
|
|
175
|
+
if (i < n - 1) {
|
|
176
|
+
const idx = i + 1;
|
|
177
|
+
// Interior separators only; the final `seps` entry is the trail.
|
|
178
|
+
const sep = idx < seps.length - 1 ? seps[idx] : undefined;
|
|
179
|
+
out += sep !== undefined ? sep : " ";
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
out += trail;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return out;
|
|
186
|
+
}
|
|
187
|
+
/** (Re)build the case complement: positional `wordMasks` and content-keyed
|
|
188
|
+
* `byContent` in one pass. `byContent` lists stay in source order for FIFO
|
|
189
|
+
* consumption in `putl`. */
|
|
190
|
+
function refreshCaseComplement(s, c) {
|
|
191
|
+
const { words } = parseWords(s);
|
|
192
|
+
const wordMasks = words.map(caseMaskOf);
|
|
193
|
+
const byContent = new Map();
|
|
194
|
+
for (let i = 0; i < words.length; i++) {
|
|
195
|
+
const key = words[i].toLowerCase();
|
|
196
|
+
let arr = byContent.get(key);
|
|
197
|
+
if (arr === undefined) {
|
|
198
|
+
arr = [];
|
|
199
|
+
byContent.set(key, arr);
|
|
200
|
+
}
|
|
201
|
+
arr.push(wordMasks[i]);
|
|
202
|
+
}
|
|
203
|
+
c.wordMasks = wordMasks;
|
|
204
|
+
c.byContent = byContent;
|
|
205
|
+
}
|
|
206
|
+
/** Apply the case complement to a target string and rebuild. Each
|
|
207
|
+
* target word goes through three lookup tiers — content match
|
|
208
|
+
* (FIFO-consumed from a per-call clone), positional fallback, then
|
|
209
|
+
* native pass-through. */
|
|
210
|
+
function applyCaseComplement(target, c) {
|
|
211
|
+
const { words, seps } = parseWords(target);
|
|
212
|
+
// Per-call clone: consume FIFO without mutating the stored map, so
|
|
213
|
+
// repeated `putl` calls start from the same state.
|
|
214
|
+
const remaining = new Map();
|
|
215
|
+
for (const [k, arr] of c.byContent)
|
|
216
|
+
remaining.set(k, arr.slice());
|
|
217
|
+
const cased = words.map((w, i) => {
|
|
218
|
+
const key = w.toLowerCase();
|
|
219
|
+
const matches = remaining.get(key);
|
|
220
|
+
if (matches !== undefined && matches.length > 0) {
|
|
221
|
+
return applyCasePattern(w, matches.shift());
|
|
222
|
+
}
|
|
223
|
+
const mask = i < c.wordMasks.length ? c.wordMasks[i] : "";
|
|
224
|
+
return mask.length === 0 ? w : applyCasePattern(w, mask);
|
|
225
|
+
});
|
|
226
|
+
return rebuildWords(cased, seps);
|
|
227
|
+
}
|
|
228
|
+
/** Build a fresh case complement from a source string. */
|
|
229
|
+
function buildCaseComplement(s) {
|
|
230
|
+
const c = { wordMasks: [], byContent: new Map() };
|
|
231
|
+
refreshCaseComplement(s, c);
|
|
232
|
+
return c;
|
|
233
|
+
}
|
|
234
|
+
export class Str extends Cell {
|
|
235
|
+
static traits = { equals };
|
|
236
|
+
constructor(v = "") {
|
|
237
|
+
super(v, { equals });
|
|
238
|
+
}
|
|
239
|
+
/** Reverse. Involution. */
|
|
240
|
+
reverse() {
|
|
241
|
+
return this.lens(reverseStr, reverseStr);
|
|
242
|
+
}
|
|
243
|
+
/** ROT13. Involution. */
|
|
244
|
+
rot13() {
|
|
245
|
+
return this.lens(rot13Str, rot13Str);
|
|
246
|
+
}
|
|
247
|
+
/** Trim edge whitespace; the complement restores the original padding
|
|
248
|
+
* on write. Edge whitespace in a write is stripped first (the view's
|
|
249
|
+
* contract is "no edge whitespace", else the complement grows). */
|
|
250
|
+
trim() {
|
|
251
|
+
return complementLens(this, s => {
|
|
252
|
+
const lead = /^\s*/.exec(s)?.[0] ?? "";
|
|
253
|
+
// Slice lead off first so trail can't overlap it on all-whitespace.
|
|
254
|
+
const remain = s.slice(lead.length);
|
|
255
|
+
const trail = /\s*$/.exec(remain)?.[0] ?? "";
|
|
256
|
+
return { lead, trail };
|
|
257
|
+
}, s => {
|
|
258
|
+
const lead = /^\s*/.exec(s)?.[0] ?? "";
|
|
259
|
+
const remain = s.slice(lead.length);
|
|
260
|
+
const trail = /\s*$/.exec(remain)?.[0] ?? "";
|
|
261
|
+
return remain.slice(0, remain.length - trail.length);
|
|
262
|
+
},
|
|
263
|
+
// Edge whitespace in the write is dropped; complement restores pad.
|
|
264
|
+
(target, c) => c.lead + target.replace(/^\s+/, "").replace(/\s+$/, "") + c.trail);
|
|
265
|
+
}
|
|
266
|
+
/** Lowercase view with word-aware case recovery on write. Lookup
|
|
267
|
+
* priority: (1) content match — recover the source mask by word
|
|
268
|
+
* (FIFO across duplicates); (2) per-position fallback for new content;
|
|
269
|
+
* (3) native for content beyond the source structure. */
|
|
270
|
+
lowercase() {
|
|
271
|
+
return complementLens(this, s => buildCaseComplement(s), s => s.toLowerCase(), (target, c) => applyCaseComplement(target, c));
|
|
272
|
+
}
|
|
273
|
+
/** Uppercase view. Dual of `lowercase`; same per-word case recovery. */
|
|
274
|
+
uppercase() {
|
|
275
|
+
return complementLens(this, s => buildCaseComplement(s), s => s.toUpperCase(), (target, c) => applyCaseComplement(target, c));
|
|
276
|
+
}
|
|
277
|
+
/** Words view, one word per line. Read splits on non-word chars; the
|
|
278
|
+
* complement is the separator layout, restored on write (added words
|
|
279
|
+
* get single spaces). Non-word chars typed into a line are stripped —
|
|
280
|
+
* edit `Trimmed` / `Lowercased` / `Source` to add punctuation. */
|
|
281
|
+
words() {
|
|
282
|
+
return complementLens(this, s => ({ separators: parseWords(s).seps }), s => parseWords(s).words.join("\n"), (target, c) => {
|
|
283
|
+
const words = target
|
|
284
|
+
.split(/\n/)
|
|
285
|
+
.map(stripNonWord)
|
|
286
|
+
.filter(w => w.length > 0);
|
|
287
|
+
return rebuildWords(words, c.separators);
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
/** Sorted, case-insensitively-unique words, one per line. The
|
|
291
|
+
* complement records each entry's source positions and original case,
|
|
292
|
+
* so editing one line broadcasts to every occurrence in the source —
|
|
293
|
+
* each rebuilt with its own original casing. */
|
|
294
|
+
sortedUnique() {
|
|
295
|
+
return complementLens(this, s => {
|
|
296
|
+
const { words, seps } = parseWords(s);
|
|
297
|
+
const buckets = new Map();
|
|
298
|
+
for (let i = 0; i < words.length; i++) {
|
|
299
|
+
const w = words[i];
|
|
300
|
+
const key = w.toLowerCase();
|
|
301
|
+
let arr = buckets.get(key);
|
|
302
|
+
if (arr === undefined) {
|
|
303
|
+
arr = [];
|
|
304
|
+
buckets.set(key, arr);
|
|
305
|
+
}
|
|
306
|
+
arr.push({ index: i, sourceCase: w });
|
|
307
|
+
}
|
|
308
|
+
const unique = [...buckets.keys()].sort();
|
|
309
|
+
return {
|
|
310
|
+
unique,
|
|
311
|
+
positions: unique.map(k => buckets.get(k)),
|
|
312
|
+
separators: seps,
|
|
313
|
+
sourceWords: words,
|
|
314
|
+
};
|
|
315
|
+
}, s => {
|
|
316
|
+
const { words } = parseWords(s);
|
|
317
|
+
return [...new Set(words.map(w => w.toLowerCase()))].sort().join("\n");
|
|
318
|
+
}, (target, c) => {
|
|
319
|
+
// Strip non-word chars typed into the view, same as `words`.
|
|
320
|
+
const edited = target
|
|
321
|
+
.split(/\n/)
|
|
322
|
+
.map(stripNonWord)
|
|
323
|
+
.filter(w => w.length > 0);
|
|
324
|
+
const sourceWords = c.sourceWords.slice();
|
|
325
|
+
const n = Math.min(edited.length, c.unique.length);
|
|
326
|
+
for (let i = 0; i < n; i++) {
|
|
327
|
+
const newWord = edited[i];
|
|
328
|
+
for (const { index, sourceCase } of c.positions[i]) {
|
|
329
|
+
if (index >= sourceWords.length)
|
|
330
|
+
continue;
|
|
331
|
+
sourceWords[index] = applyCasePattern(newWord, caseMaskOf(sourceCase));
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
return rebuildWords(sourceWords, c.separators);
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
/** Writable `Str`. Strict factory: literal seeds a fresh cell;
|
|
339
|
+
* existing `Writable<Str>` passes through by identity. RO sources
|
|
340
|
+
* are rejected at the type level — use `Str.derive(...)` for
|
|
341
|
+
* reactive RO tracking, or `cell.value` to snapshot. */
|
|
342
|
+
export function str(v = "") {
|
|
343
|
+
if (v instanceof Str)
|
|
344
|
+
return v;
|
|
345
|
+
return new Str(v);
|
|
346
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { Read, Writable } from "../signal.js";
|
|
2
|
+
import { type Num } from "./num.js";
|
|
3
|
+
import { Str } from "./str.js";
|
|
4
|
+
/** Textual codec for a slot value. `parse` returns `undefined` to reject. */
|
|
5
|
+
export interface Codec<T> {
|
|
6
|
+
format(v: T): string;
|
|
7
|
+
parse(s: string): T | undefined;
|
|
8
|
+
}
|
|
9
|
+
/** Identity codec for string slots. */
|
|
10
|
+
export declare const strCodec: Codec<string>;
|
|
11
|
+
/** Numeric codec; `int` rounds on both read and write. Rejects non-finite. */
|
|
12
|
+
export declare const numCodec: (int?: boolean) => Codec<number>;
|
|
13
|
+
/** Enumerated string codec: accepts only members of `options`. */
|
|
14
|
+
export declare const enumCodec: (options: readonly string[]) => Codec<string>;
|
|
15
|
+
/** A typed hole: the parent cell plus its `string ⇄ T` codec. */
|
|
16
|
+
export interface Slot<T> {
|
|
17
|
+
name: string;
|
|
18
|
+
cell: Writable<Read<T>>;
|
|
19
|
+
codec: Codec<T>;
|
|
20
|
+
}
|
|
21
|
+
/** Slot constructors that infer the codec from the cell's value class. */
|
|
22
|
+
export declare const slot: {
|
|
23
|
+
str: (cell: Writable<Str>, name?: string) => Slot<string>;
|
|
24
|
+
num: (cell: Writable<Num>, name?: string) => Slot<number>;
|
|
25
|
+
int: (cell: Writable<Num>, name?: string) => Slot<number>;
|
|
26
|
+
pick: (cell: Writable<Str>, options: readonly string[], name?: string) => Slot<string>;
|
|
27
|
+
};
|
|
28
|
+
/** Core builder: `literals.length === slots.length + 1`. Returns the
|
|
29
|
+
* rendered text as a `Writable<Str>` that parses back into the slots. */
|
|
30
|
+
export declare function template(literals: readonly string[], slots: readonly Slot<unknown>[]): Writable<Str>;
|
|
31
|
+
/** Tagged template; interpolate `slot.*` holes. Bring your own cells:
|
|
32
|
+
*
|
|
33
|
+
* tpl`Hello ${slot.str(name)}, ×${slot.int(count)}` */
|
|
34
|
+
export declare function tpl(strings: TemplateStringsArray, ...slots: Slot<unknown>[]): Writable<Str>;
|
|
35
|
+
type ParamKind = "str" | "int" | "num";
|
|
36
|
+
type ParamCell<K extends ParamKind> = K extends "str" ? Writable<Str> : Writable<Num>;
|
|
37
|
+
/** Parse a `:name` pattern against a kind schema, owning fresh slot cells.
|
|
38
|
+
* Returns the rendered URL `text` and the typed `params` cells; edit
|
|
39
|
+
* either side and the other stays in sync.
|
|
40
|
+
*
|
|
41
|
+
* const { text, params } = route("/users/:id/posts/:slug",
|
|
42
|
+
* { id: "int", slug: "str" }); */
|
|
43
|
+
export declare function route<S extends Record<string, ParamKind>>(pattern: string, schema: S): {
|
|
44
|
+
text: Writable<Str>;
|
|
45
|
+
params: {
|
|
46
|
+
[K in keyof S]: ParamCell<S[K]>;
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
export {};
|