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,361 @@
|
|
|
1
|
+
// CodeShape — a monospace code substrate.
|
|
2
|
+
//
|
|
3
|
+
// A `Part` is a single-line span, absolutely positioned with reactive
|
|
4
|
+
// `position` / `opacity` / `rotation` and an optional `key`. A
|
|
5
|
+
// `CodeShape` is a flat list of parts at `(col·charW, row·lineH)` — no
|
|
6
|
+
// line containers, no flow layout; monospace makes layout pure
|
|
7
|
+
// multiplication. Animation is just writes to part signals.
|
|
8
|
+
//
|
|
9
|
+
// Substrate ops: `cut` (split a part at offsets), `uncut` (merge
|
|
10
|
+
// contiguous same-row parts), `group(key)` (parts sharing a key — a
|
|
11
|
+
// multi-line region).
|
|
12
|
+
//
|
|
13
|
+
// Syntax colour: CSS Custom Highlights over Ranges in part text nodes.
|
|
14
|
+
// `paint()` tokenises each row and routes typed tokens to the
|
|
15
|
+
// containing part — independent of cut structure.
|
|
16
|
+
import { cell, derive, effect, num, readNow, vec, } from "../core/index.js";
|
|
17
|
+
import { Shape } from "../shapes/index.js";
|
|
18
|
+
import { morph } from "./morph.js";
|
|
19
|
+
import { tokenize } from "./tokenize.js";
|
|
20
|
+
const DEFAULT_FONT = "ui-monospace, SFMono-Regular, Menlo, 'Cascadia Code', monospace";
|
|
21
|
+
const partCss = "position:absolute;left:0;top:0;white-space:pre;will-change:transform";
|
|
22
|
+
/** A single-line span placed absolutely; `position` / `opacity` /
|
|
23
|
+
* `rotation` are animatable signals. */
|
|
24
|
+
export class Part {
|
|
25
|
+
el;
|
|
26
|
+
/** Current text. Use `setText` to update (instant). */
|
|
27
|
+
text;
|
|
28
|
+
/** Top-left in user units. */
|
|
29
|
+
position;
|
|
30
|
+
/** [0..1]. */
|
|
31
|
+
opacity;
|
|
32
|
+
/** Radians around the part's centre. */
|
|
33
|
+
rotation;
|
|
34
|
+
/** Optional identity tag; shared keys form a `c.group(key)`. */
|
|
35
|
+
key;
|
|
36
|
+
#disposers = [];
|
|
37
|
+
constructor(text, x, y, key) {
|
|
38
|
+
this.text = text;
|
|
39
|
+
this.key = key;
|
|
40
|
+
this.position = vec(x, y);
|
|
41
|
+
this.opacity = num(1);
|
|
42
|
+
this.rotation = num(0);
|
|
43
|
+
this.el = document.createElement("span");
|
|
44
|
+
this.el.style.cssText = partCss;
|
|
45
|
+
this.el.textContent = text;
|
|
46
|
+
this.#disposers.push(effect(() => {
|
|
47
|
+
const p = this.position.value;
|
|
48
|
+
const r = this.rotation.value;
|
|
49
|
+
this.el.style.transform =
|
|
50
|
+
r === 0
|
|
51
|
+
? `translate(${p.x}px, ${p.y}px)`
|
|
52
|
+
: `translate(${p.x}px, ${p.y}px) rotate(${r}rad)`;
|
|
53
|
+
}), effect(() => {
|
|
54
|
+
this.el.style.opacity = String(this.opacity.value);
|
|
55
|
+
}));
|
|
56
|
+
}
|
|
57
|
+
/** Instant text update (text itself doesn't tween — animate around it). */
|
|
58
|
+
setText(t) {
|
|
59
|
+
if (this.text === t)
|
|
60
|
+
return;
|
|
61
|
+
this.text = t;
|
|
62
|
+
this.el.textContent = t;
|
|
63
|
+
}
|
|
64
|
+
dispose() {
|
|
65
|
+
for (const d of this.#disposers)
|
|
66
|
+
d();
|
|
67
|
+
this.#disposers.length = 0;
|
|
68
|
+
this.el.remove();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/** Measure monospace metrics for `(family, size)`. One-off per shape. */
|
|
72
|
+
function measureFont(size, family) {
|
|
73
|
+
const div = document.createElement("div");
|
|
74
|
+
div.style.cssText =
|
|
75
|
+
`position:absolute;visibility:hidden;left:-9999px;top:0;` +
|
|
76
|
+
`font-family:${family};font-size:${size}px;line-height:1.4;white-space:pre`;
|
|
77
|
+
div.textContent = "M";
|
|
78
|
+
document.body.appendChild(div);
|
|
79
|
+
const w = div.offsetWidth;
|
|
80
|
+
const h = div.offsetHeight;
|
|
81
|
+
document.body.removeChild(div);
|
|
82
|
+
return { w, h };
|
|
83
|
+
}
|
|
84
|
+
/** A Shape rendering monospace source code as a list of `Part`s. */
|
|
85
|
+
export class CodeShape extends Shape {
|
|
86
|
+
source;
|
|
87
|
+
width;
|
|
88
|
+
height;
|
|
89
|
+
language;
|
|
90
|
+
/** Host wrapper (`position: relative`) for the absolute parts. */
|
|
91
|
+
wrapper;
|
|
92
|
+
/** Flat parts list; morph re-sorts by (row, col) on completion. */
|
|
93
|
+
parts = [];
|
|
94
|
+
/** Monospace char width and line height in CSS pixels. */
|
|
95
|
+
charW;
|
|
96
|
+
lineH;
|
|
97
|
+
/** When true, the auto-rebuild effect bails — morph owns the parts. */
|
|
98
|
+
#inMorph = false;
|
|
99
|
+
/** Syntax Ranges we own; cleared each `paint`. Other CSS.highlights
|
|
100
|
+
* buckets (user highlights) survive repaints. */
|
|
101
|
+
#syntaxRanges = [];
|
|
102
|
+
constructor(initial, opts = {}) {
|
|
103
|
+
const fontSize = opts.size ?? 14;
|
|
104
|
+
const fontFamily = opts.font ?? DEFAULT_FONT;
|
|
105
|
+
const language = opts.language ?? "typescript";
|
|
106
|
+
const { w: charW, h: lineH } = measureFont(fontSize, fontFamily);
|
|
107
|
+
const initialStr = readNow(initial);
|
|
108
|
+
const lines = initialStr.split("\n");
|
|
109
|
+
const initW = lines.reduce((a, l) => Math.max(a, l.length), 0) * charW;
|
|
110
|
+
const initH = lines.length * lineH;
|
|
111
|
+
const w = cell(initW);
|
|
112
|
+
const h = cell(initH);
|
|
113
|
+
super("foreignObject", () => ({ x: 0, y: 0, w: w.value, h: h.value }), opts, {
|
|
114
|
+
origin: derive(() => ({ x: w.value / 2, y: h.value / 2 })),
|
|
115
|
+
});
|
|
116
|
+
this.width = w;
|
|
117
|
+
this.height = h;
|
|
118
|
+
this.language = language;
|
|
119
|
+
this.source = cell(initialStr);
|
|
120
|
+
this.charW = charW;
|
|
121
|
+
this.lineH = lineH;
|
|
122
|
+
const fo = this.intrinsic;
|
|
123
|
+
fo.setAttribute("x", "0");
|
|
124
|
+
fo.setAttribute("y", "0");
|
|
125
|
+
fo.setAttribute("overflow", "visible");
|
|
126
|
+
fo.style.overflow = "visible";
|
|
127
|
+
this.attrs({ width: w, height: h });
|
|
128
|
+
this.wrapper = document.createElement("div");
|
|
129
|
+
this.wrapper.style.cssText = [
|
|
130
|
+
"position:relative",
|
|
131
|
+
`font-family:${fontFamily}`,
|
|
132
|
+
`font-size:${fontSize}px`,
|
|
133
|
+
`line-height:${lineH}px`,
|
|
134
|
+
"padding:0",
|
|
135
|
+
"margin:0",
|
|
136
|
+
"color:var(--text-color)",
|
|
137
|
+
].join(";");
|
|
138
|
+
fo.appendChild(this.wrapper);
|
|
139
|
+
this.#render(initialStr);
|
|
140
|
+
this.disposers.push(effect(() => {
|
|
141
|
+
const src = this.source.value;
|
|
142
|
+
if (this.#inMorph)
|
|
143
|
+
return;
|
|
144
|
+
this.#render(src);
|
|
145
|
+
}), () => this.#clearSyntaxRanges(), () => {
|
|
146
|
+
for (const p of this.parts)
|
|
147
|
+
p.dispose();
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
/** Full rebuild: one part per source line at (0, row·lineH). Runs on
|
|
151
|
+
* mount and external `source` writes; morph bypasses via `#inMorph`. */
|
|
152
|
+
#render(src) {
|
|
153
|
+
for (const p of this.parts)
|
|
154
|
+
p.dispose();
|
|
155
|
+
this.parts.length = 0;
|
|
156
|
+
const lines = src.split("\n");
|
|
157
|
+
for (let r = 0; r < lines.length; r++) {
|
|
158
|
+
const part = new Part(lines[r], 0, r * this.lineH);
|
|
159
|
+
this.wrapper.appendChild(part.el);
|
|
160
|
+
this.parts.push(part);
|
|
161
|
+
}
|
|
162
|
+
this.#syncSize();
|
|
163
|
+
this.paint();
|
|
164
|
+
}
|
|
165
|
+
/** Reflect the parts' extents into `width` / `height` (absolute children
|
|
166
|
+
* don't size their parent; these drive the foreignObject attrs). */
|
|
167
|
+
#syncSize() {
|
|
168
|
+
let maxW = 0;
|
|
169
|
+
let maxH = 0;
|
|
170
|
+
for (const p of this.parts) {
|
|
171
|
+
const pos = p.position.peek();
|
|
172
|
+
const right = pos.x + p.text.length * this.charW;
|
|
173
|
+
const bottom = pos.y + this.lineH;
|
|
174
|
+
if (right > maxW)
|
|
175
|
+
maxW = right;
|
|
176
|
+
if (bottom > maxH)
|
|
177
|
+
maxH = bottom;
|
|
178
|
+
}
|
|
179
|
+
if (maxW !== this.width.peek())
|
|
180
|
+
this.width.value = maxW;
|
|
181
|
+
if (maxH !== this.height.peek())
|
|
182
|
+
this.height.value = maxH;
|
|
183
|
+
}
|
|
184
|
+
/** Paint syntax highlights: tokenise each row's joined text, route
|
|
185
|
+
* each typed token to a Range in its containing part. Re-entrant
|
|
186
|
+
* (clears prior syntax Ranges; leaves other buckets untouched). */
|
|
187
|
+
paint() {
|
|
188
|
+
this.#clearSyntaxRanges();
|
|
189
|
+
if (typeof CSS === "undefined" || !("highlights" in CSS))
|
|
190
|
+
return;
|
|
191
|
+
const byRow = new Map();
|
|
192
|
+
for (const p of this.parts) {
|
|
193
|
+
const r = Math.round(p.position.peek().y / this.lineH);
|
|
194
|
+
const arr = byRow.get(r);
|
|
195
|
+
if (arr)
|
|
196
|
+
arr.push(p);
|
|
197
|
+
else
|
|
198
|
+
byRow.set(r, [p]);
|
|
199
|
+
}
|
|
200
|
+
for (const parts of byRow.values()) {
|
|
201
|
+
parts.sort((a, b) => a.position.peek().x - b.position.peek().x);
|
|
202
|
+
const fullText = parts.map(p => p.text).join("");
|
|
203
|
+
const tokens = tokenize(fullText, this.language);
|
|
204
|
+
const starts = [];
|
|
205
|
+
let off = 0;
|
|
206
|
+
for (const p of parts) {
|
|
207
|
+
starts.push(off);
|
|
208
|
+
off += p.text.length;
|
|
209
|
+
}
|
|
210
|
+
let pos = 0;
|
|
211
|
+
for (const tok of tokens) {
|
|
212
|
+
const len = tok.text.length;
|
|
213
|
+
if (tok.type !== "" && len > 0 && !tok.text.includes("\n")) {
|
|
214
|
+
for (let i = 0; i < parts.length; i++) {
|
|
215
|
+
const start = starts[i];
|
|
216
|
+
const end = start + parts[i].text.length;
|
|
217
|
+
if (pos >= start && pos + len <= end) {
|
|
218
|
+
const tn = parts[i].el.firstChild;
|
|
219
|
+
if (tn && tn.nodeType === Node.TEXT_NODE) {
|
|
220
|
+
try {
|
|
221
|
+
const r = new Range();
|
|
222
|
+
r.setStart(tn, pos - start);
|
|
223
|
+
r.setEnd(tn, pos - start + len);
|
|
224
|
+
let h = CSS.highlights.get(tok.type);
|
|
225
|
+
if (h === undefined) {
|
|
226
|
+
h = new Highlight();
|
|
227
|
+
CSS.highlights.set(tok.type, h);
|
|
228
|
+
}
|
|
229
|
+
h.add(r);
|
|
230
|
+
this.#syntaxRanges.push(r);
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
// Skip on bad offsets.
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
break;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
pos += len;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
#clearSyntaxRanges() {
|
|
245
|
+
if (this.#syntaxRanges.length === 0)
|
|
246
|
+
return;
|
|
247
|
+
if (typeof CSS !== "undefined" && "highlights" in CSS) {
|
|
248
|
+
for (const r of this.#syntaxRanges) {
|
|
249
|
+
for (const [, h] of CSS.highlights) {
|
|
250
|
+
h.delete(r);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
this.#syntaxRanges.length = 0;
|
|
255
|
+
}
|
|
256
|
+
/** Split `part` at char offsets into N+1 same-row sub-parts (0 and
|
|
257
|
+
* `text.length` implicit). Sub-parts inherit `part.key`; returned
|
|
258
|
+
* left-to-right. */
|
|
259
|
+
cut(part, offsets) {
|
|
260
|
+
const idx = this.parts.indexOf(part);
|
|
261
|
+
if (idx < 0)
|
|
262
|
+
throw new Error("cut: part not in this CodeShape");
|
|
263
|
+
const sorted = [...new Set(offsets)]
|
|
264
|
+
.sort((a, b) => a - b)
|
|
265
|
+
.filter(o => o > 0 && o < part.text.length);
|
|
266
|
+
if (sorted.length === 0)
|
|
267
|
+
return [part];
|
|
268
|
+
const bounds = [0, ...sorted, part.text.length];
|
|
269
|
+
const pos = part.position.peek();
|
|
270
|
+
const subs = [];
|
|
271
|
+
for (let i = 0; i < bounds.length - 1; i++) {
|
|
272
|
+
const start = bounds[i];
|
|
273
|
+
const end = bounds[i + 1];
|
|
274
|
+
const sub = new Part(part.text.slice(start, end), pos.x + start * this.charW, pos.y, part.key);
|
|
275
|
+
this.wrapper.appendChild(sub.el);
|
|
276
|
+
subs.push(sub);
|
|
277
|
+
}
|
|
278
|
+
this.parts.splice(idx, 1, ...subs);
|
|
279
|
+
part.dispose();
|
|
280
|
+
this.paint();
|
|
281
|
+
return subs;
|
|
282
|
+
}
|
|
283
|
+
/** Merge same-row contiguous `parts` into one (inherits the leftmost's
|
|
284
|
+
* key). Single part is a no-op; empty throws. */
|
|
285
|
+
uncut(parts) {
|
|
286
|
+
if (parts.length === 0)
|
|
287
|
+
throw new Error("uncut: no parts");
|
|
288
|
+
if (parts.length === 1)
|
|
289
|
+
return parts[0];
|
|
290
|
+
const sorted = [...parts].sort((a, b) => a.position.peek().x - b.position.peek().x);
|
|
291
|
+
const text = sorted.map(p => p.text).join("");
|
|
292
|
+
const pos = sorted[0].position.peek();
|
|
293
|
+
const merged = new Part(text, pos.x, pos.y, sorted[0].key);
|
|
294
|
+
this.wrapper.appendChild(merged.el);
|
|
295
|
+
const firstIdx = this.parts.indexOf(sorted[0]);
|
|
296
|
+
for (const p of sorted) {
|
|
297
|
+
const i = this.parts.indexOf(p);
|
|
298
|
+
if (i >= 0)
|
|
299
|
+
this.parts.splice(i, 1);
|
|
300
|
+
p.dispose();
|
|
301
|
+
}
|
|
302
|
+
this.parts.splice(firstIdx >= 0 ? firstIdx : this.parts.length, 0, merged);
|
|
303
|
+
this.paint();
|
|
304
|
+
return merged;
|
|
305
|
+
}
|
|
306
|
+
/** All parts sharing `key`. Returns a fresh array. */
|
|
307
|
+
group(key) {
|
|
308
|
+
return this.parts.filter(p => p.key === key);
|
|
309
|
+
}
|
|
310
|
+
/** Animate from current source to `target`. See `morph.ts`. */
|
|
311
|
+
morphTo(target, dur, ease) {
|
|
312
|
+
return morph(this, target, dur, ease);
|
|
313
|
+
}
|
|
314
|
+
/** @internal Morph's on-completion commit: set `source` (rebuild
|
|
315
|
+
* suppressed), re-sort parts row/col, refresh size + highlights. */
|
|
316
|
+
_finalize(src) {
|
|
317
|
+
this.#inMorph = true;
|
|
318
|
+
try {
|
|
319
|
+
this.source.value = src;
|
|
320
|
+
}
|
|
321
|
+
finally {
|
|
322
|
+
this.#inMorph = false;
|
|
323
|
+
}
|
|
324
|
+
this.parts.sort((a, b) => {
|
|
325
|
+
const pa = a.position.peek();
|
|
326
|
+
const pb = b.position.peek();
|
|
327
|
+
return pa.y - pb.y || pa.x - pb.x;
|
|
328
|
+
});
|
|
329
|
+
this.#syncSize();
|
|
330
|
+
this.paint();
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
/** Factory: `code("source", { language: "typescript", size: 14 })`. */
|
|
334
|
+
export const code = (source, opts) => new CodeShape(source, opts);
|
|
335
|
+
/** Prism token-class colours via CSS Custom Highlights. Drop into a
|
|
336
|
+
* `Diagram.styles` block so the rules reach the shadow root. */
|
|
337
|
+
export const codeStyles = `
|
|
338
|
+
::highlight(keyword),
|
|
339
|
+
::highlight(rule) { color: var(--prettylights-keyword, #cf222e); }
|
|
340
|
+
::highlight(string),
|
|
341
|
+
::highlight(attr-value) { color: var(--prettylights-string, #0a3069); }
|
|
342
|
+
::highlight(comment),
|
|
343
|
+
::highlight(prolog),
|
|
344
|
+
::highlight(doctype),
|
|
345
|
+
::highlight(cdata) { color: var(--prettylights-comment, #59636e); }
|
|
346
|
+
::highlight(function),
|
|
347
|
+
::highlight(class-name),
|
|
348
|
+
::highlight(entity),
|
|
349
|
+
::highlight(selector) { color: var(--prettylights-entity, #6639ba); }
|
|
350
|
+
::highlight(tag),
|
|
351
|
+
::highlight(boolean),
|
|
352
|
+
::highlight(property),
|
|
353
|
+
::highlight(symbol) { color: var(--prettylights-entity-tag, #0550ae); }
|
|
354
|
+
::highlight(constant),
|
|
355
|
+
::highlight(attr-name),
|
|
356
|
+
::highlight(builtin),
|
|
357
|
+
::highlight(char),
|
|
358
|
+
::highlight(operator) { color: var(--prettylights-constant, #0550ae); }
|
|
359
|
+
::highlight(variable) { color: var(--prettylights-variable, #953800); }
|
|
360
|
+
::highlight(regex) { color: var(--prettylights-string-regexp, #116329); }
|
|
361
|
+
`;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// bireactive/code — monospace code substrate with reactive `Part` atoms.
|
|
2
|
+
//
|
|
3
|
+
// code(src, opts) → CodeShape (flat parts driven by `source`).
|
|
4
|
+
// c.cut / c.uncut → split / merge same-row parts.
|
|
5
|
+
// c.group(key) → parts sharing a key (multi-line region).
|
|
6
|
+
// c.morphTo(target, dur) → per-line cross-fade + position tween.
|
|
7
|
+
// codeStyles → ::highlight() rules for `Diagram.styles`.
|
|
8
|
+
export { CodeShape, code, codeStyles, Part } from "./code.js";
|
|
9
|
+
export { tokenize } from "./tokenize.js";
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { type Animator, type Easing } from "../animation/index.js";
|
|
2
|
+
import { type CodeShape } from "./code.js";
|
|
3
|
+
/** Animate `c` to `target`. Cancel-safe: `finally` disposes transient
|
|
4
|
+
* parts and commits via `_finalize` (which re-sorts row/col). */
|
|
5
|
+
export declare function morph(c: CodeShape, target: string, dur: number, ease?: Easing): Animator<void>;
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
// morph — animate a CodeShape from its current source to a target.
|
|
2
|
+
//
|
|
3
|
+
// Parts are just absolutely-positioned signal-bearing spans, so morph
|
|
4
|
+
// reduces to: pair old/new lines (LCS over trimmed text), then tween
|
|
5
|
+
// the right signals per pair. No DOM rebuild, no FLIP, no drive loop —
|
|
6
|
+
// every animation is a `signal.to(...)` and the morph is `yield [...]`.
|
|
7
|
+
//
|
|
8
|
+
// Per-line outcomes:
|
|
9
|
+
// Kept(same text) — old part stays; position.y tweens if it moved.
|
|
10
|
+
// Kept(text changed) — old part fades out at its old row; a fresh
|
|
11
|
+
// part fades in at the new row (whole-line cross-fade).
|
|
12
|
+
// Lost — old part fades out, disposed on completion.
|
|
13
|
+
// Gained — fresh part fades in at its new row.
|
|
14
|
+
import { easeInOut } from "../animation/index.js";
|
|
15
|
+
import { vec } from "../core/index.js";
|
|
16
|
+
import { Part } from "./code.js";
|
|
17
|
+
/** LCS over `trimStart`-equal lines; indent-only changes still match
|
|
18
|
+
* (the shift rides through as a text diff on the Kept line). */
|
|
19
|
+
function lcsLines(oldLines, newLines) {
|
|
20
|
+
const eq = (a, b) => a.trimStart() === b.trimStart();
|
|
21
|
+
const m = oldLines.length;
|
|
22
|
+
const n = newLines.length;
|
|
23
|
+
const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
|
|
24
|
+
for (let i = m - 1; i >= 0; i--) {
|
|
25
|
+
for (let j = n - 1; j >= 0; j--) {
|
|
26
|
+
dp[i][j] = eq(oldLines[i], newLines[j])
|
|
27
|
+
? dp[i + 1][j + 1] + 1
|
|
28
|
+
: Math.max(dp[i + 1][j], dp[i][j + 1]);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const ops = [];
|
|
32
|
+
let i = 0, j = 0;
|
|
33
|
+
while (i < m && j < n) {
|
|
34
|
+
if (eq(oldLines[i], newLines[j])) {
|
|
35
|
+
ops.push({ kind: "match", oldIdx: i, newIdx: j });
|
|
36
|
+
i++;
|
|
37
|
+
j++;
|
|
38
|
+
}
|
|
39
|
+
else if (dp[i + 1][j] >= dp[i][j + 1]) {
|
|
40
|
+
ops.push({ kind: "del", oldIdx: i });
|
|
41
|
+
i++;
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
ops.push({ kind: "ins", newIdx: j });
|
|
45
|
+
j++;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
while (i < m)
|
|
49
|
+
ops.push({ kind: "del", oldIdx: i++ });
|
|
50
|
+
while (j < n)
|
|
51
|
+
ops.push({ kind: "ins", newIdx: j++ });
|
|
52
|
+
return ops;
|
|
53
|
+
}
|
|
54
|
+
/** Classify raw LCS ops into Kept/Lost/Gained. Pairs cross-position
|
|
55
|
+
* same-trimmed lines first, then adjacent — prefers "line moved" over
|
|
56
|
+
* "line modified", keeping lines anchored across moves. */
|
|
57
|
+
function classify(raw, oldLines, newLines) {
|
|
58
|
+
// Pass 1 — cross-position pairing (del/ins with equal trimStart).
|
|
59
|
+
const delByText = new Map();
|
|
60
|
+
for (let k = 0; k < raw.length; k++) {
|
|
61
|
+
const op = raw[k];
|
|
62
|
+
if (op.kind !== "del")
|
|
63
|
+
continue;
|
|
64
|
+
const t = oldLines[op.oldIdx].trimStart();
|
|
65
|
+
if (t === "")
|
|
66
|
+
continue;
|
|
67
|
+
const bucket = delByText.get(t);
|
|
68
|
+
if (bucket)
|
|
69
|
+
bucket.push(k);
|
|
70
|
+
else
|
|
71
|
+
delByText.set(t, [k]);
|
|
72
|
+
}
|
|
73
|
+
const paired = new Set();
|
|
74
|
+
const insertPair = new Map();
|
|
75
|
+
for (let k = 0; k < raw.length; k++) {
|
|
76
|
+
const op = raw[k];
|
|
77
|
+
if (op.kind !== "ins")
|
|
78
|
+
continue;
|
|
79
|
+
const t = newLines[op.newIdx].trimStart();
|
|
80
|
+
if (t === "")
|
|
81
|
+
continue;
|
|
82
|
+
const bucket = delByText.get(t);
|
|
83
|
+
if (!bucket)
|
|
84
|
+
continue;
|
|
85
|
+
while (bucket.length > 0 && paired.has(bucket[0]))
|
|
86
|
+
bucket.shift();
|
|
87
|
+
if (bucket.length === 0)
|
|
88
|
+
continue;
|
|
89
|
+
const delIdx = bucket.shift();
|
|
90
|
+
paired.add(delIdx);
|
|
91
|
+
insertPair.set(k, delIdx);
|
|
92
|
+
}
|
|
93
|
+
// Pass 2 — adjacent (del immediately followed by ins) for the rest.
|
|
94
|
+
for (let k = 0; k < raw.length; k++) {
|
|
95
|
+
const op = raw[k];
|
|
96
|
+
if (op.kind !== "ins")
|
|
97
|
+
continue;
|
|
98
|
+
if (insertPair.has(k))
|
|
99
|
+
continue;
|
|
100
|
+
if (k > 0 && raw[k - 1].kind === "del" && !paired.has(k - 1)) {
|
|
101
|
+
paired.add(k - 1);
|
|
102
|
+
insertPair.set(k, k - 1);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
// Emit.
|
|
106
|
+
const out = [];
|
|
107
|
+
for (let k = 0; k < raw.length; k++) {
|
|
108
|
+
const op = raw[k];
|
|
109
|
+
if (op.kind === "match") {
|
|
110
|
+
out.push({ kind: "kept", oldIdx: op.oldIdx, newIdx: op.newIdx });
|
|
111
|
+
}
|
|
112
|
+
else if (op.kind === "del") {
|
|
113
|
+
if (paired.has(k))
|
|
114
|
+
continue;
|
|
115
|
+
out.push({ kind: "lost", oldIdx: op.oldIdx });
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
const delIdx = insertPair.get(k);
|
|
119
|
+
if (delIdx !== undefined) {
|
|
120
|
+
const delOp = raw[delIdx];
|
|
121
|
+
out.push({ kind: "kept", oldIdx: delOp.oldIdx, newIdx: op.newIdx });
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
out.push({ kind: "gained", newIdx: op.newIdx });
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return out;
|
|
129
|
+
}
|
|
130
|
+
/** Animate `c` to `target`. Cancel-safe: `finally` disposes transient
|
|
131
|
+
* parts and commits via `_finalize` (which re-sorts row/col). */
|
|
132
|
+
export function* morph(c, target, dur, ease = easeInOut) {
|
|
133
|
+
const oldSrc = c.source.peek();
|
|
134
|
+
if (oldSrc === target)
|
|
135
|
+
return;
|
|
136
|
+
const oldLines = oldSrc.split("\n");
|
|
137
|
+
const newLines = target.split("\n");
|
|
138
|
+
const ops = classify(lcsLines(oldLines, newLines), oldLines, newLines);
|
|
139
|
+
// Parts indexed by oldIdx; assumed in row/col order (render and
|
|
140
|
+
// `_finalize` both maintain this), one part per old line.
|
|
141
|
+
const oldParts = c.parts.slice();
|
|
142
|
+
const tweens = [];
|
|
143
|
+
const transient = [];
|
|
144
|
+
for (const op of ops) {
|
|
145
|
+
if (op.kind === "kept") {
|
|
146
|
+
const oldPart = oldParts[op.oldIdx];
|
|
147
|
+
const newText = newLines[op.newIdx];
|
|
148
|
+
const newY = op.newIdx * c.lineH;
|
|
149
|
+
if (oldPart.text === newText) {
|
|
150
|
+
// Same content — only animate if the row changed.
|
|
151
|
+
if (oldPart.position.peek().y !== newY) {
|
|
152
|
+
tweens.push(oldPart.position.to(vec(0, newY).value, dur, ease));
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
// Content changed — whole-line cross-fade: old part out at its
|
|
157
|
+
// old row, fresh part in at the new row.
|
|
158
|
+
const fresh = new Part(newText, 0, newY);
|
|
159
|
+
fresh.opacity.value = 0;
|
|
160
|
+
c.wrapper.appendChild(fresh.el);
|
|
161
|
+
c.parts.push(fresh);
|
|
162
|
+
tweens.push(oldPart.opacity.to(0, dur, ease));
|
|
163
|
+
tweens.push(fresh.opacity.to(1, dur, ease));
|
|
164
|
+
transient.push(oldPart);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
else if (op.kind === "lost") {
|
|
168
|
+
const oldPart = oldParts[op.oldIdx];
|
|
169
|
+
tweens.push(oldPart.opacity.to(0, dur, ease));
|
|
170
|
+
transient.push(oldPart);
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
const newText = newLines[op.newIdx];
|
|
174
|
+
const newY = op.newIdx * c.lineH;
|
|
175
|
+
const fresh = new Part(newText, 0, newY);
|
|
176
|
+
fresh.opacity.value = 0;
|
|
177
|
+
c.wrapper.appendChild(fresh.el);
|
|
178
|
+
c.parts.push(fresh);
|
|
179
|
+
tweens.push(fresh.opacity.to(1, dur, ease));
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
try {
|
|
183
|
+
yield tweens;
|
|
184
|
+
}
|
|
185
|
+
finally {
|
|
186
|
+
for (const p of transient) {
|
|
187
|
+
const i = c.parts.indexOf(p);
|
|
188
|
+
if (i >= 0)
|
|
189
|
+
c.parts.splice(i, 1);
|
|
190
|
+
p.dispose();
|
|
191
|
+
}
|
|
192
|
+
c._finalize(target);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface Token {
|
|
2
|
+
/** Prism token type ("keyword", "string", …). `""` for plain text. */
|
|
3
|
+
type: string;
|
|
4
|
+
text: string;
|
|
5
|
+
}
|
|
6
|
+
/** Tokenize `source`; concatenating `tok.text` recovers the input.
|
|
7
|
+
* Unknown language → one untyped token over the whole source. */
|
|
8
|
+
export declare function tokenize(source: string, language?: string): Token[];
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Prism wrapper — tokenize source into a flat `(type, text)` list that
|
|
2
|
+
// concatenates back to the input verbatim. `type` is Prism's
|
|
3
|
+
// classification or `""` for plain text. Languages lazy-load into a
|
|
4
|
+
// shared singleton Prism instance.
|
|
5
|
+
import { Prism } from "prism-esm";
|
|
6
|
+
import { loader as CssLoader } from "prism-esm/components/prism-css.js";
|
|
7
|
+
import { loader as JsLoader } from "prism-esm/components/prism-javascript.js";
|
|
8
|
+
import { loader as TsLoader } from "prism-esm/components/prism-typescript.js";
|
|
9
|
+
const prism = new Prism();
|
|
10
|
+
JsLoader(prism);
|
|
11
|
+
TsLoader(prism);
|
|
12
|
+
CssLoader(prism);
|
|
13
|
+
/** Flatten Prism's token tree; inner strings inherit the parent's type
|
|
14
|
+
* so a nested literal's quotes colour like its body. */
|
|
15
|
+
function flatten(t, inheritedType = "") {
|
|
16
|
+
if (typeof t === "string") {
|
|
17
|
+
return t === "" ? [] : [{ type: inheritedType, text: t }];
|
|
18
|
+
}
|
|
19
|
+
const type = t.type ?? inheritedType;
|
|
20
|
+
if (typeof t.content === "string") {
|
|
21
|
+
return t.content === "" ? [] : [{ type, text: t.content }];
|
|
22
|
+
}
|
|
23
|
+
return t.content.flatMap(c => flatten(c, type));
|
|
24
|
+
}
|
|
25
|
+
/** Split untyped runs on word/whitespace boundaries. Prism glues
|
|
26
|
+
* whitespace onto untyped identifiers, which would trap newlines inside
|
|
27
|
+
* a rename's diff span; splitting lets the diff align them correctly. */
|
|
28
|
+
function splitUntyped(text) {
|
|
29
|
+
return text.match(/\s+|\S+/g) ?? [];
|
|
30
|
+
}
|
|
31
|
+
/** Tokenize `source`; concatenating `tok.text` recovers the input.
|
|
32
|
+
* Unknown language → one untyped token over the whole source. */
|
|
33
|
+
export function tokenize(source, language = "typescript") {
|
|
34
|
+
const lang = prism.languages[language];
|
|
35
|
+
if (!lang)
|
|
36
|
+
return source === "" ? [] : [{ type: "", text: source }];
|
|
37
|
+
const raw = prism.tokenize(source, lang);
|
|
38
|
+
const flat = raw.flatMap(t => flatten(t));
|
|
39
|
+
// Split untyped tokens on whitespace boundaries; typed tokens stay intact.
|
|
40
|
+
const out = [];
|
|
41
|
+
for (const tok of flat) {
|
|
42
|
+
if (tok.type !== "") {
|
|
43
|
+
out.push(tok);
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
for (const piece of splitUntyped(tok.text)) {
|
|
47
|
+
out.push({ type: "", text: piece });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return out;
|
|
51
|
+
}
|