@ape-egg/vibe 2.1.22 → 3.0.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/README.md +112 -5
- package/boot.js +4 -4
- package/component.js +27 -29
- package/hot-module-refresh.js +4 -4
- package/index.js +26 -17
- package/llms.txt +36 -5
- package/package.json +20 -14
- package/runtime/affected.js +159 -36
- package/runtime/cleanup.js +45 -1
- package/runtime/component.js +360 -98
- package/runtime/conditionals.js +111 -14
- package/runtime/debug.js +24 -0
- package/runtime/dispatch.js +172 -0
- package/runtime/hydrate.js +277 -110
- package/runtime/index.js +189 -65
- package/runtime/iterate.js +125 -50
- package/runtime/iteration-utils.js +59 -8
- package/runtime/manifest.js +77 -2
- package/runtime/parse.js +81 -11
- 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 +206 -0
- package/vibe.css +8 -4
- package/CHANGELOG.md +0 -1159
- 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 -2522
- 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 -15
- package/compiler/src/compiler/name_binding_protect.rs +0 -207
- package/compiler/src/compiler/reassignment_analyzer.rs +0 -456
- 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 -1147
- package/compiler/src/config.rs +0 -239
- package/compiler/src/main.rs +0 -347
- 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
|
|
|
@@ -91,71 +242,60 @@ export default (affected, state, manifest = {}, oldState = {}) => {
|
|
|
91
242
|
// reparse rebuilds the knowledge from that attribute. A state change
|
|
92
243
|
// landing inside the swap window still resolves through the
|
|
93
244
|
// replacement chain.
|
|
245
|
+
// Keyed component (`<component src="@[page.src]" key="@[page.path]">`):
|
|
246
|
+
// a key change is a declared identity change — remount the mounted
|
|
247
|
+
// component even when the src is unchanged (param→param navigation on
|
|
248
|
+
// the same route). The first resolution just records the initial key;
|
|
249
|
+
// the mount itself is owned by the normal component pass.
|
|
250
|
+
if (attrName === 'key' && isComponentWrapper(element)) {
|
|
251
|
+
const live = liveComponentWrapper(element);
|
|
252
|
+
const newKey = tracked(aff, effectiveState, () =>
|
|
253
|
+
attrValue.replace(BINDING_REGEX, (_, expr) =>
|
|
254
|
+
evalInScope(expr, effectiveState, live) ?? '',
|
|
255
|
+
),
|
|
256
|
+
);
|
|
257
|
+
live._vibeKeyBinding = attrValue;
|
|
258
|
+
const prevKey = live._vibeMountedKey;
|
|
259
|
+
live._vibeMountedKey = newKey;
|
|
260
|
+
if (prevKey !== undefined && newKey !== prevKey) forceRemount(live);
|
|
261
|
+
if (live._vibeOutgoing) (outgoingRoots ??= []).push(live);
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
|
|
94
265
|
if (attrName === 'src' && isComponentWrapper(element)) {
|
|
95
266
|
const live = liveComponentWrapper(element);
|
|
96
|
-
const newSrc =
|
|
97
|
-
|
|
267
|
+
const newSrc = tracked(aff, effectiveState, () =>
|
|
268
|
+
attrValue.replace(BINDING_REGEX, (_, expr) =>
|
|
269
|
+
evalInScope(expr, effectiveState, live) ?? '',
|
|
270
|
+
),
|
|
98
271
|
);
|
|
99
272
|
live._vibeSrcBinding = attrValue;
|
|
100
|
-
|
|
273
|
+
if (newSrc) {
|
|
274
|
+
remountComponent(live, newSrc);
|
|
275
|
+
if (live._vibeOutgoing) (outgoingRoots ??= []).push(live);
|
|
276
|
+
} else if (live.hasAttribute('src')) {
|
|
277
|
+
// Unresolved src mounts nothing (a no-match deep link leaves
|
|
278
|
+
// $.page.src unset): move the binding onto data-vibe-src — the
|
|
279
|
+
// established transport parse.js already reads — and drop the
|
|
280
|
+
// fetchable src, so component processing and cleanup treat the
|
|
281
|
+
// outlet as settled instead of fetching a stringified binding.
|
|
282
|
+
live.setAttribute('data-vibe-src', attrValue);
|
|
283
|
+
live.removeAttribute('src');
|
|
284
|
+
}
|
|
101
285
|
return;
|
|
102
286
|
}
|
|
103
287
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
attrName.startsWith('data-') ||
|
|
111
|
-
attrName.startsWith('aria-') ||
|
|
112
|
-
attrName.startsWith('on');
|
|
113
|
-
|
|
114
|
-
if (isDomProperty && isPureBinding) {
|
|
115
|
-
// For DOM properties (value, checked, selected), set BOTH property AND attribute
|
|
116
|
-
// Property: Fast runtime updates, what the user sees
|
|
117
|
-
// Attribute: Truthful DOM representation, enables compiler snapshots via outerHTML
|
|
118
|
-
const expr = isPureBinding[1];
|
|
119
|
-
const value = evalInScope(expr, effectiveState, element);
|
|
120
|
-
if (element[attrName] !== value) element[attrName] = value;
|
|
121
|
-
if (attrName === 'value') {
|
|
122
|
-
if (value !== undefined && value !== null) {
|
|
123
|
-
const str = String(value);
|
|
124
|
-
if (element.getAttribute(attrName) !== str) element.setAttribute(attrName, str);
|
|
125
|
-
}
|
|
126
|
-
} else if (value) {
|
|
127
|
-
// checked/selected are boolean — the truthful attribute form is
|
|
128
|
-
// presence (empty) when truthy, absence when falsy. Stringifying
|
|
129
|
-
// would leave checked="false", which is "checked" to CSS and HTML.
|
|
130
|
-
if (element.getAttribute(attrName) !== '') element.setAttribute(attrName, '');
|
|
131
|
-
} else if (element.hasAttribute(attrName)) {
|
|
132
|
-
element.removeAttribute(attrName);
|
|
133
|
-
}
|
|
134
|
-
} else if (!isValueAttr && isPureBinding) {
|
|
135
|
-
// Boolean-like attributes: add or remove based on truthiness.
|
|
136
|
-
// Compare both presence AND value — initial hydration starts with
|
|
137
|
-
// the raw `@[...]` binding text as the attribute value, so
|
|
138
|
-
// `hasAttribute` alone isn't enough to know the canonical state is
|
|
139
|
-
// already set.
|
|
140
|
-
const expr = isPureBinding[1];
|
|
141
|
-
const value = evalInScope(expr, effectiveState, element);
|
|
142
|
-
if (value) {
|
|
143
|
-
if (element.getAttribute(attrName) !== '') {
|
|
144
|
-
element.setAttribute(attrName, '');
|
|
145
|
-
}
|
|
146
|
-
} else if (element.hasAttribute(attrName)) {
|
|
147
|
-
element.removeAttribute(attrName);
|
|
148
|
-
}
|
|
149
|
-
} else {
|
|
150
|
-
// Value attribute - replace bindings with values
|
|
151
|
-
const newValue = attrValue.replace(BINDING_REGEX, (_, expr) => {
|
|
152
|
-
return evalInScope(expr, effectiveState, element);
|
|
153
|
-
});
|
|
154
|
-
if (element.getAttribute(attrName) !== newValue) {
|
|
155
|
-
element.setAttribute(attrName, newValue);
|
|
156
|
-
}
|
|
288
|
+
const parkRoot = parkRootFor(element);
|
|
289
|
+
if (parkRoot) {
|
|
290
|
+
parkBinding(parkRoot, element, 'attr:' + attrName, () =>
|
|
291
|
+
applyAttributeBinding(aff, effectiveState),
|
|
292
|
+
);
|
|
293
|
+
return;
|
|
157
294
|
}
|
|
158
|
-
|
|
295
|
+
tracked(aff, effectiveState, () => applyAttributeBinding(aff, effectiveState));
|
|
296
|
+
} catch (e) {
|
|
297
|
+
reportEvalError(aff.attrValue ?? aff.matchInner, aff.element, e);
|
|
298
|
+
}
|
|
159
299
|
return;
|
|
160
300
|
}
|
|
161
301
|
|
|
@@ -165,7 +305,9 @@ export default (affected, state, manifest = {}, oldState = {}) => {
|
|
|
165
305
|
// This prevents undefined store properties to throw an error
|
|
166
306
|
try {
|
|
167
307
|
// Evaluate the expression with state as context
|
|
168
|
-
const evaluated =
|
|
308
|
+
const evaluated = tracked(aff, effectiveState, () =>
|
|
309
|
+
evalInScope(matchInner, effectiveState, element),
|
|
310
|
+
);
|
|
169
311
|
|
|
170
312
|
// Raw-HTML render: `$.unsafe(str)` returns a RawHtml marker. When the
|
|
171
313
|
// binding is the sole content of its element (`<p>@[$.unsafe(x)]</p>`),
|
|
@@ -185,10 +327,18 @@ export default (affected, state, manifest = {}, oldState = {}) => {
|
|
|
185
327
|
element._vibeRawHtmlValue = html;
|
|
186
328
|
}
|
|
187
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;
|
|
188
335
|
return;
|
|
189
336
|
}
|
|
190
337
|
|
|
191
|
-
|
|
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();
|
|
192
342
|
|
|
193
343
|
affected.forEach((innerAff) => {
|
|
194
344
|
if (innerAff.element === element) {
|
|
@@ -207,11 +357,28 @@ export default (affected, state, manifest = {}, oldState = {}) => {
|
|
|
207
357
|
// value actually changed. Reactivity coverage is unchanged: state changes
|
|
208
358
|
// that produce a new value still apply; state changes that don't are now
|
|
209
359
|
// proper no-ops at the DOM layer.
|
|
210
|
-
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) {
|
|
211
373
|
if (textNode.textContent !== toReplace) textNode.textContent = toReplace;
|
|
212
374
|
} else if (element.textContent !== toReplace) {
|
|
213
375
|
element.textContent = toReplace;
|
|
214
376
|
}
|
|
215
|
-
} catch (e) {
|
|
216
|
-
|
|
377
|
+
} catch (e) {
|
|
378
|
+
reportEvalError(matchInner ?? input, element, e);
|
|
379
|
+
}
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
remountTriggers.forEach(processEntry);
|
|
383
|
+
ordinary.forEach(processEntry);
|
|
217
384
|
};
|