@ape-egg/vibe 2.3.0 → 3.0.1
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 -4
- package/boot.js +4 -4
- package/component.js +27 -29
- package/hot-module-refresh.js +4 -4
- package/index.js +10 -15
- package/llms.txt +8 -6
- package/package.json +19 -14
- package/runtime/affected.js +159 -36
- package/runtime/cleanup.js +45 -1
- package/runtime/component.js +312 -99
- package/runtime/conditionals.js +111 -14
- package/runtime/debug.js +24 -0
- package/runtime/dispatch.js +172 -0
- package/runtime/hydrate.js +251 -111
- package/runtime/index.js +180 -71
- package/runtime/iterate.js +125 -50
- package/runtime/iteration-utils.js +59 -8
- package/runtime/manifest.js +77 -2
- package/runtime/parse.js +69 -5
- package/runtime/pre-compiled-iterations.js +19 -6
- package/runtime/pre-compiled-manifest.js +13 -4
- package/runtime/staging.js +153 -0
- package/runtime/state.js +31 -0
- package/runtime/tracking.js +173 -0
- package/runtime/utils.js +155 -78
- package/spa.js +77 -14
- package/vibe.css +8 -4
- package/CHANGELOG.md +0 -1196
- package/ROADMAP.md +0 -397
- package/compiler/bin/vibe-compile.js +0 -121
- package/compiler/native/.gitkeep +0 -0
- package/compiler/native/vibe-compiler-darwin-arm64 +0 -0
- package/compiler/native/vibe-compiler-linux-x64 +0 -0
- package/compiler/src/Cargo.lock +0 -2023
- package/compiler/src/Cargo.toml +0 -38
- package/compiler/src/compiler/PRE-RENDERING-IMPLEMENTATION.md +0 -241
- package/compiler/src/compiler/binding_case.rs +0 -88
- package/compiler/src/compiler/compile.rs +0 -2880
- package/compiler/src/compiler/component_tagger.rs +0 -469
- package/compiler/src/compiler/iteration_optimizer.rs +0 -455
- package/compiler/src/compiler/js_analyzer.rs +0 -715
- package/compiler/src/compiler/manifest_builder.rs +0 -693
- package/compiler/src/compiler/mod.rs +0 -16
- package/compiler/src/compiler/name_binding_protect.rs +0 -207
- package/compiler/src/compiler/reassignment_analyzer.rs +0 -456
- package/compiler/src/compiler/spa.rs +0 -477
- package/compiler/src/compiler/state_extractor.rs +0 -263
- package/compiler/src/compiler/value_stamper.rs +0 -921
- package/compiler/src/compiler/watcher.rs +0 -1278
- package/compiler/src/config.rs +0 -279
- package/compiler/src/main.rs +0 -358
- package/compiler/src/parser/element.rs +0 -96
- package/compiler/src/parser/html.rs +0 -1004
- package/compiler/src/parser/mod.rs +0 -8
- package/runtime/pre-compiled-manifest.test.mjs +0 -58
- package/runtime/scope.js +0 -50
- package/test-results/.last-run.json +0 -4
package/runtime/hydrate.js
CHANGED
|
@@ -1,12 +1,201 @@
|
|
|
1
1
|
import { updateIteration } from './iterate.js';
|
|
2
2
|
import { updateConditional, managedNodes } from './conditionals.js';
|
|
3
|
-
import {
|
|
3
|
+
import { liveComponentWrapper, remountComponent, forceRemount } from './component.js';
|
|
4
|
+
import { isComponentWrapper, parkRootFor, parkBinding } from './staging.js';
|
|
4
5
|
import { VALUE_ATTRS, DOM_PROPERTIES, BINDING_REGEX, PURE_BINDING_REGEX } from './constants.js';
|
|
5
|
-
import { evalInScope, resolveCaseInsensitivePath } from './utils.js';
|
|
6
|
+
import { evalInScope, resolveCaseInsensitivePath, overlayKeysOf } from './utils.js';
|
|
6
7
|
import { RawHtml } from './raw-html.js';
|
|
8
|
+
import { beginTracking, endTracking } from './tracking.js';
|
|
9
|
+
import { reportEvalError } from './debug.js';
|
|
10
|
+
|
|
11
|
+
// Run one entry's evaluation inside a tracking window. The subscriber is
|
|
12
|
+
// hosted on the walk's cached binding object (aff.binding) — stable per
|
|
13
|
+
// binding across flushes, invalidated exactly when the binding cache
|
|
14
|
+
// rebuilds. Entries without a binding host (hand-built lists from external
|
|
15
|
+
// callers) evaluate untracked, exactly as before.
|
|
16
|
+
const tracked = (aff, effectiveState, fn) => {
|
|
17
|
+
const m = aff.binding;
|
|
18
|
+
if (!m) return fn();
|
|
19
|
+
const sub = (m._sub ??= {
|
|
20
|
+
kind: aff.type ?? 'text',
|
|
21
|
+
anchor: aff.textNode ?? aff.element,
|
|
22
|
+
});
|
|
23
|
+
sub.entry = aff; // latest entry shape — dispatch rebuilds scope at fire time
|
|
24
|
+
sub.lastScope = effectiveState; // overlay source for dispatch-time rescoping
|
|
25
|
+
beginTracking(sub, overlayKeysOf(effectiveState));
|
|
26
|
+
try {
|
|
27
|
+
return fn();
|
|
28
|
+
} finally {
|
|
29
|
+
endTracking();
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const applyNameBinding = (aff, effectiveState) => {
|
|
34
|
+
const { nameBinding, matchInner, element } = aff;
|
|
35
|
+
try {
|
|
36
|
+
// HTML lowercases attribute names, so we need case-insensitive lookup.
|
|
37
|
+
// Try exact match first, then walk the path case-insensitively if the
|
|
38
|
+
// exact lookup returned nothing — this recovers camelCase property
|
|
39
|
+
// names in dotted paths like `<icon @[fx.convertsIcon]>` (arrives at
|
|
40
|
+
// runtime as `@[fx.convertsicon]`).
|
|
41
|
+
let attrName = evalInScope(matchInner, effectiveState, element);
|
|
42
|
+
|
|
43
|
+
if (!attrName) {
|
|
44
|
+
attrName = resolveCaseInsensitivePath(effectiveState, matchInner);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Track multiple name bindings per element (need a map of binding -> evaluated attr)
|
|
48
|
+
if (!element._vibeNameBindings) {
|
|
49
|
+
element._vibeNameBindings = new Map();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Remove the old evaluated attribute for this specific binding
|
|
53
|
+
const oldAttrName = element._vibeNameBindings.get(nameBinding);
|
|
54
|
+
if (oldAttrName) {
|
|
55
|
+
element.removeAttribute(oldAttrName);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Remove the binding attribute itself
|
|
59
|
+
if (element.hasAttribute(nameBinding)) {
|
|
60
|
+
element.removeAttribute(nameBinding);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Set the new attribute (empty value for boolean-like attributes)
|
|
64
|
+
if (attrName) {
|
|
65
|
+
element.setAttribute(attrName, '');
|
|
66
|
+
element._vibeNameBindings.set(nameBinding, attrName);
|
|
67
|
+
} else {
|
|
68
|
+
element._vibeNameBindings.delete(nameBinding);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// A binding relocated into `data-vibe-namebind` (clone path) has served its
|
|
72
|
+
// transport purpose once the real attribute is set — drop it so the rendered
|
|
73
|
+
// DOM matches the batch path, which never emits it.
|
|
74
|
+
if (element.hasAttribute('data-vibe-namebind')) {
|
|
75
|
+
element.removeAttribute('data-vibe-namebind');
|
|
76
|
+
}
|
|
77
|
+
} catch (e) {
|
|
78
|
+
console.error('Error hydrating name binding:', e);
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const applyAttributeBinding = (aff, effectiveState) => {
|
|
83
|
+
const { attrName, attrValue, element } = aff;
|
|
84
|
+
try {
|
|
85
|
+
// Check if this is a pure binding (e.g., value="@[inputValue]")
|
|
86
|
+
const isPureBinding = attrValue.match(PURE_BINDING_REGEX);
|
|
87
|
+
const isDomProperty = DOM_PROPERTIES.includes(attrName);
|
|
88
|
+
// Value attrs keep their string value; everything else is boolean-like (removed when falsy)
|
|
89
|
+
const isValueAttr =
|
|
90
|
+
VALUE_ATTRS.includes(attrName) ||
|
|
91
|
+
attrName.startsWith('data-') ||
|
|
92
|
+
attrName.startsWith('aria-') ||
|
|
93
|
+
attrName.startsWith('on');
|
|
94
|
+
|
|
95
|
+
if (isDomProperty && isPureBinding) {
|
|
96
|
+
// For DOM properties (value, checked, selected), set BOTH property AND attribute
|
|
97
|
+
// Property: Fast runtime updates, what the user sees
|
|
98
|
+
// Attribute: Truthful DOM representation, enables compiler snapshots via outerHTML
|
|
99
|
+
const expr = isPureBinding[1];
|
|
100
|
+
const value = evalInScope(expr, effectiveState, element);
|
|
101
|
+
if (element[attrName] !== value) element[attrName] = value;
|
|
102
|
+
if (attrName === 'value') {
|
|
103
|
+
if (value !== undefined && value !== null) {
|
|
104
|
+
const str = String(value);
|
|
105
|
+
if (element.getAttribute(attrName) !== str) element.setAttribute(attrName, str);
|
|
106
|
+
}
|
|
107
|
+
} else if (value) {
|
|
108
|
+
// checked/selected are boolean — the truthful attribute form is
|
|
109
|
+
// presence (empty) when truthy, absence when falsy. Stringifying
|
|
110
|
+
// would leave checked="false", which is "checked" to CSS and HTML.
|
|
111
|
+
if (element.getAttribute(attrName) !== '') element.setAttribute(attrName, '');
|
|
112
|
+
} else if (element.hasAttribute(attrName)) {
|
|
113
|
+
element.removeAttribute(attrName);
|
|
114
|
+
}
|
|
115
|
+
} else if (!isValueAttr && isPureBinding) {
|
|
116
|
+
// Boolean-like attributes: add or remove based on truthiness.
|
|
117
|
+
// Compare both presence AND value — initial hydration starts with
|
|
118
|
+
// the raw `@[...]` binding text as the attribute value, so
|
|
119
|
+
// `hasAttribute` alone isn't enough to know the canonical state is
|
|
120
|
+
// already set.
|
|
121
|
+
const expr = isPureBinding[1];
|
|
122
|
+
const value = evalInScope(expr, effectiveState, element);
|
|
123
|
+
if (value) {
|
|
124
|
+
if (element.getAttribute(attrName) !== '') {
|
|
125
|
+
element.setAttribute(attrName, '');
|
|
126
|
+
}
|
|
127
|
+
} else if (element.hasAttribute(attrName)) {
|
|
128
|
+
element.removeAttribute(attrName);
|
|
129
|
+
}
|
|
130
|
+
} else {
|
|
131
|
+
// Value attribute - replace bindings with values
|
|
132
|
+
const newValue = attrValue.replace(BINDING_REGEX, (_, expr) => {
|
|
133
|
+
return evalInScope(expr, effectiveState, element);
|
|
134
|
+
});
|
|
135
|
+
if (element.getAttribute(attrName) !== newValue) {
|
|
136
|
+
element.setAttribute(attrName, newValue);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
} catch (e) {
|
|
140
|
+
reportEvalError(attrValue, element, e);
|
|
141
|
+
}
|
|
142
|
+
};
|
|
7
143
|
|
|
8
144
|
export default (affected, state, manifest = {}, oldState = {}) => {
|
|
9
|
-
|
|
145
|
+
// Wrappers that entered a reactive-src remount DURING this pass (the src/key
|
|
146
|
+
// handlers below). affected() collected this list before the outgoing flag
|
|
147
|
+
// existed, so the outgoing subtree's entries are still in it — and this is
|
|
148
|
+
// exactly the flush where route state flips. Applying them would paint the
|
|
149
|
+
// outgoing page against the incoming route for the whole fetch+mount window
|
|
150
|
+
// (the mid-navigation CSS collapse). Tree-walk order guarantees the
|
|
151
|
+
// wrapper's own src/key entries precede its subtree's entries, so skipping
|
|
152
|
+
// by ancestry here is airtight. Null until a remount actually happens —
|
|
153
|
+
// zero cost on ordinary flushes.
|
|
154
|
+
let outgoingRoots = null;
|
|
155
|
+
const underOutgoingRoot = (aff) => {
|
|
156
|
+
if (!outgoingRoots) return false;
|
|
157
|
+
const el = aff.element ?? aff.node?.meta?.startComment?.parentElement;
|
|
158
|
+
if (!el) return false;
|
|
159
|
+
for (let i = 0; i < outgoingRoots.length; i++) {
|
|
160
|
+
const root = outgoingRoots[i];
|
|
161
|
+
if (root !== el && root.contains(el)) return true;
|
|
162
|
+
}
|
|
163
|
+
return false;
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
// Two passes: wrapper src/key entries (remount triggers) first, everything
|
|
167
|
+
// else second. A remount marks its wrapper outgoing and starts staging —
|
|
168
|
+
// the freeze skip below and the ancestor-binding parking both depend on
|
|
169
|
+
// that registration, and tree-walk order would otherwise process an
|
|
170
|
+
// ancestor's bindings (the `<page @[page.name]>` styling context) before
|
|
171
|
+
// the outlet entry that begins the staging they must park against.
|
|
172
|
+
const remountTriggers = [];
|
|
173
|
+
const ordinary = [];
|
|
174
|
+
for (const aff of affected) {
|
|
175
|
+
const isRemountTrigger =
|
|
176
|
+
aff.type === 'attribute' &&
|
|
177
|
+
(aff.attrName === 'src' || aff.attrName === 'key') &&
|
|
178
|
+
isComponentWrapper(aff.element);
|
|
179
|
+
(isRemountTrigger ? remountTriggers : ordinary).push(aff);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const processEntry = (aff) => {
|
|
183
|
+
if (underOutgoingRoot(aff)) return;
|
|
184
|
+
|
|
185
|
+
// Torn down mid-pass: the entry's target was connected when the walk
|
|
186
|
+
// collected it but left the document before this entry processed — an
|
|
187
|
+
// earlier entry's teardown (an iteration emptying, a branch flip)
|
|
188
|
+
// removed it. Same ordering hole as the outgoing-remount skip above.
|
|
189
|
+
// Processing it would write to dead DOM and re-register a subscriber
|
|
190
|
+
// the teardown just pruned, pinning the removed subtree. Entries that
|
|
191
|
+
// were ALREADY detached at collection (initializeBlock hydrating a
|
|
192
|
+
// fresh row inside its parse container) pass through untouched.
|
|
193
|
+
if (aff.wasConnected) {
|
|
194
|
+
const target =
|
|
195
|
+
aff.textNode ?? aff.element ?? aff.node?.meta?.startComment;
|
|
196
|
+
if (target && !target.isConnected) return;
|
|
197
|
+
}
|
|
198
|
+
|
|
10
199
|
// Use scoped state if provided (from iteration instances)
|
|
11
200
|
const effectiveState = aff.scopedState || state;
|
|
12
201
|
|
|
@@ -30,52 +219,14 @@ export default (affected, state, manifest = {}, oldState = {}) => {
|
|
|
30
219
|
|
|
31
220
|
// Handle name bindings (e.g., <icon @[section.icon]>)
|
|
32
221
|
if (aff.type === 'nameBinding') {
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
// runtime as `@[fx.convertsicon]`).
|
|
40
|
-
let attrName = evalInScope(matchInner, effectiveState, element);
|
|
41
|
-
|
|
42
|
-
if (!attrName) {
|
|
43
|
-
attrName = resolveCaseInsensitivePath(effectiveState, matchInner);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// Track multiple name bindings per element (need a map of binding -> evaluated attr)
|
|
47
|
-
if (!element._vibeNameBindings) {
|
|
48
|
-
element._vibeNameBindings = new Map();
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// Remove the old evaluated attribute for this specific binding
|
|
52
|
-
const oldAttrName = element._vibeNameBindings.get(nameBinding);
|
|
53
|
-
if (oldAttrName) {
|
|
54
|
-
element.removeAttribute(oldAttrName);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// Remove the binding attribute itself
|
|
58
|
-
if (element.hasAttribute(nameBinding)) {
|
|
59
|
-
element.removeAttribute(nameBinding);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// Set the new attribute (empty value for boolean-like attributes)
|
|
63
|
-
if (attrName) {
|
|
64
|
-
element.setAttribute(attrName, '');
|
|
65
|
-
element._vibeNameBindings.set(nameBinding, attrName);
|
|
66
|
-
} else {
|
|
67
|
-
element._vibeNameBindings.delete(nameBinding);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// A binding relocated into `data-vibe-namebind` (clone path) has served its
|
|
71
|
-
// transport purpose once the real attribute is set — drop it so the rendered
|
|
72
|
-
// DOM matches the batch path, which never emits it.
|
|
73
|
-
if (element.hasAttribute('data-vibe-namebind')) {
|
|
74
|
-
element.removeAttribute('data-vibe-namebind');
|
|
75
|
-
}
|
|
76
|
-
} catch (e) {
|
|
77
|
-
console.error('Error hydrating name binding:', e);
|
|
222
|
+
const parkRoot = parkRootFor(aff.element);
|
|
223
|
+
if (parkRoot) {
|
|
224
|
+
parkBinding(parkRoot, aff.element, 'nb:' + aff.nameBinding, () =>
|
|
225
|
+
applyNameBinding(aff, effectiveState),
|
|
226
|
+
);
|
|
227
|
+
return;
|
|
78
228
|
}
|
|
229
|
+
tracked(aff, effectiveState, () => applyNameBinding(aff, effectiveState));
|
|
79
230
|
return;
|
|
80
231
|
}
|
|
81
232
|
|
|
@@ -98,24 +249,30 @@ export default (affected, state, manifest = {}, oldState = {}) => {
|
|
|
98
249
|
// the mount itself is owned by the normal component pass.
|
|
99
250
|
if (attrName === 'key' && isComponentWrapper(element)) {
|
|
100
251
|
const live = liveComponentWrapper(element);
|
|
101
|
-
const newKey =
|
|
102
|
-
|
|
252
|
+
const newKey = tracked(aff, effectiveState, () =>
|
|
253
|
+
attrValue.replace(BINDING_REGEX, (_, expr) =>
|
|
254
|
+
evalInScope(expr, effectiveState, live) ?? '',
|
|
255
|
+
),
|
|
103
256
|
);
|
|
104
257
|
live._vibeKeyBinding = attrValue;
|
|
105
258
|
const prevKey = live._vibeMountedKey;
|
|
106
259
|
live._vibeMountedKey = newKey;
|
|
107
260
|
if (prevKey !== undefined && newKey !== prevKey) forceRemount(live);
|
|
261
|
+
if (live._vibeOutgoing) (outgoingRoots ??= []).push(live);
|
|
108
262
|
return;
|
|
109
263
|
}
|
|
110
264
|
|
|
111
265
|
if (attrName === 'src' && isComponentWrapper(element)) {
|
|
112
266
|
const live = liveComponentWrapper(element);
|
|
113
|
-
const newSrc =
|
|
114
|
-
|
|
267
|
+
const newSrc = tracked(aff, effectiveState, () =>
|
|
268
|
+
attrValue.replace(BINDING_REGEX, (_, expr) =>
|
|
269
|
+
evalInScope(expr, effectiveState, live) ?? '',
|
|
270
|
+
),
|
|
115
271
|
);
|
|
116
272
|
live._vibeSrcBinding = attrValue;
|
|
117
273
|
if (newSrc) {
|
|
118
274
|
remountComponent(live, newSrc);
|
|
275
|
+
if (live._vibeOutgoing) (outgoingRoots ??= []).push(live);
|
|
119
276
|
} else if (live.hasAttribute('src')) {
|
|
120
277
|
// Unresolved src mounts nothing (a no-match deep link leaves
|
|
121
278
|
// $.page.src unset): move the binding onto data-vibe-src — the
|
|
@@ -128,61 +285,17 @@ export default (affected, state, manifest = {}, oldState = {}) => {
|
|
|
128
285
|
return;
|
|
129
286
|
}
|
|
130
287
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
attrName.startsWith('data-') ||
|
|
138
|
-
attrName.startsWith('aria-') ||
|
|
139
|
-
attrName.startsWith('on');
|
|
140
|
-
|
|
141
|
-
if (isDomProperty && isPureBinding) {
|
|
142
|
-
// For DOM properties (value, checked, selected), set BOTH property AND attribute
|
|
143
|
-
// Property: Fast runtime updates, what the user sees
|
|
144
|
-
// Attribute: Truthful DOM representation, enables compiler snapshots via outerHTML
|
|
145
|
-
const expr = isPureBinding[1];
|
|
146
|
-
const value = evalInScope(expr, effectiveState, element);
|
|
147
|
-
if (element[attrName] !== value) element[attrName] = value;
|
|
148
|
-
if (attrName === 'value') {
|
|
149
|
-
if (value !== undefined && value !== null) {
|
|
150
|
-
const str = String(value);
|
|
151
|
-
if (element.getAttribute(attrName) !== str) element.setAttribute(attrName, str);
|
|
152
|
-
}
|
|
153
|
-
} else if (value) {
|
|
154
|
-
// checked/selected are boolean — the truthful attribute form is
|
|
155
|
-
// presence (empty) when truthy, absence when falsy. Stringifying
|
|
156
|
-
// would leave checked="false", which is "checked" to CSS and HTML.
|
|
157
|
-
if (element.getAttribute(attrName) !== '') element.setAttribute(attrName, '');
|
|
158
|
-
} else if (element.hasAttribute(attrName)) {
|
|
159
|
-
element.removeAttribute(attrName);
|
|
160
|
-
}
|
|
161
|
-
} else if (!isValueAttr && isPureBinding) {
|
|
162
|
-
// Boolean-like attributes: add or remove based on truthiness.
|
|
163
|
-
// Compare both presence AND value — initial hydration starts with
|
|
164
|
-
// the raw `@[...]` binding text as the attribute value, so
|
|
165
|
-
// `hasAttribute` alone isn't enough to know the canonical state is
|
|
166
|
-
// already set.
|
|
167
|
-
const expr = isPureBinding[1];
|
|
168
|
-
const value = evalInScope(expr, effectiveState, element);
|
|
169
|
-
if (value) {
|
|
170
|
-
if (element.getAttribute(attrName) !== '') {
|
|
171
|
-
element.setAttribute(attrName, '');
|
|
172
|
-
}
|
|
173
|
-
} else if (element.hasAttribute(attrName)) {
|
|
174
|
-
element.removeAttribute(attrName);
|
|
175
|
-
}
|
|
176
|
-
} else {
|
|
177
|
-
// Value attribute - replace bindings with values
|
|
178
|
-
const newValue = attrValue.replace(BINDING_REGEX, (_, expr) => {
|
|
179
|
-
return evalInScope(expr, effectiveState, element);
|
|
180
|
-
});
|
|
181
|
-
if (element.getAttribute(attrName) !== newValue) {
|
|
182
|
-
element.setAttribute(attrName, newValue);
|
|
183
|
-
}
|
|
288
|
+
const parkRoot = parkRootFor(element);
|
|
289
|
+
if (parkRoot) {
|
|
290
|
+
parkBinding(parkRoot, element, 'attr:' + attrName, () =>
|
|
291
|
+
applyAttributeBinding(aff, effectiveState),
|
|
292
|
+
);
|
|
293
|
+
return;
|
|
184
294
|
}
|
|
185
|
-
|
|
295
|
+
tracked(aff, effectiveState, () => applyAttributeBinding(aff, effectiveState));
|
|
296
|
+
} catch (e) {
|
|
297
|
+
reportEvalError(aff.attrValue ?? aff.matchInner, aff.element, e);
|
|
298
|
+
}
|
|
186
299
|
return;
|
|
187
300
|
}
|
|
188
301
|
|
|
@@ -192,7 +305,9 @@ export default (affected, state, manifest = {}, oldState = {}) => {
|
|
|
192
305
|
// This prevents undefined store properties to throw an error
|
|
193
306
|
try {
|
|
194
307
|
// Evaluate the expression with state as context
|
|
195
|
-
const evaluated =
|
|
308
|
+
const evaluated = tracked(aff, effectiveState, () =>
|
|
309
|
+
evalInScope(matchInner, effectiveState, element),
|
|
310
|
+
);
|
|
196
311
|
|
|
197
312
|
// Raw-HTML render: `$.unsafe(str)` returns a RawHtml marker. When the
|
|
198
313
|
// binding is the sole content of its element (`<p>@[$.unsafe(x)]</p>`),
|
|
@@ -212,10 +327,18 @@ export default (affected, state, manifest = {}, oldState = {}) => {
|
|
|
212
327
|
element._vibeRawHtmlValue = html;
|
|
213
328
|
}
|
|
214
329
|
element._vibeRawHtml = true;
|
|
330
|
+
// The injection consumed the entry's original text node. Re-anchor
|
|
331
|
+
// the subscriber on the ELEMENT (which survives every injection) —
|
|
332
|
+
// otherwise the dispatch engine prunes it as dead after the first
|
|
333
|
+
// render and the binding silently stops re-rendering.
|
|
334
|
+
if (aff.binding?._sub) aff.binding._sub.anchor = element;
|
|
215
335
|
return;
|
|
216
336
|
}
|
|
217
337
|
|
|
218
|
-
|
|
338
|
+
// Function replacer: a string replacement would run GetSubstitution on
|
|
339
|
+
// the VALUE — `$$` collapses, `$&` re-inserts the binding text into the
|
|
340
|
+
// DOM (which the settle gates then read as an unhydrated binding).
|
|
341
|
+
const toReplace = input.replaceAll(matchOuter, () => evaluated).trim();
|
|
219
342
|
|
|
220
343
|
affected.forEach((innerAff) => {
|
|
221
344
|
if (innerAff.element === element) {
|
|
@@ -234,11 +357,28 @@ export default (affected, state, manifest = {}, oldState = {}) => {
|
|
|
234
357
|
// value actually changed. Reactivity coverage is unchanged: state changes
|
|
235
358
|
// that produce a new value still apply; state changes that don't are now
|
|
236
359
|
// proper no-ops at the DOM layer.
|
|
237
|
-
if (textNode && textNode.
|
|
360
|
+
if (element._vibeRawHtml && !(textNode && textNode.isConnected)) {
|
|
361
|
+
// unsafe → escaped transition: an earlier injection consumed the
|
|
362
|
+
// entry's text node, so the element itself takes the escaped text —
|
|
363
|
+
// and the entry ADOPTS the fresh node, so the NEXT escaped update
|
|
364
|
+
// writes live DOM instead of the consumed original.
|
|
365
|
+
element.textContent = toReplace;
|
|
366
|
+
element._vibeRawHtml = false;
|
|
367
|
+
element._vibeRawHtmlValue = undefined;
|
|
368
|
+
if (textNode) {
|
|
369
|
+
if (!element.firstChild) element.appendChild(document.createTextNode(''));
|
|
370
|
+
aff.textNode = element.firstChild;
|
|
371
|
+
}
|
|
372
|
+
} else if (textNode && textNode.nodeType === 3) {
|
|
238
373
|
if (textNode.textContent !== toReplace) textNode.textContent = toReplace;
|
|
239
374
|
} else if (element.textContent !== toReplace) {
|
|
240
375
|
element.textContent = toReplace;
|
|
241
376
|
}
|
|
242
|
-
} catch (e) {
|
|
243
|
-
|
|
377
|
+
} catch (e) {
|
|
378
|
+
reportEvalError(matchInner ?? input, element, e);
|
|
379
|
+
}
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
remountTriggers.forEach(processEntry);
|
|
383
|
+
ordinary.forEach(processEntry);
|
|
244
384
|
};
|