@ape-egg/vibe 1.1.2 → 1.3.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/CHANGELOG.md +140 -0
- package/README.md +30 -23
- package/compiler/native/vibe-compiler-darwin-arm64 +0 -0
- package/compiler/src/compiler/compile.rs +204 -51
- package/compiler/src/compiler/manifest_builder.rs +432 -0
- package/compiler/src/compiler/mod.rs +3 -0
- package/compiler/src/compiler/state_extractor.rs +148 -0
- package/compiler/src/compiler/value_stamper.rs +222 -0
- package/compiler/src/config.rs +0 -5
- package/compiler/src/main.rs +45 -20
- package/compiler/src/parser/html.rs +22 -8
- package/index.js +40 -2
- package/package.json +2 -1
- package/runtime/affected.js +44 -24
- package/runtime/cleanup.js +45 -24
- package/runtime/component-state.js +63 -0
- package/runtime/component.js +59 -15
- package/runtime/conditionals.js +4 -4
- package/runtime/constants.js +46 -2
- package/runtime/debug.js +1 -0
- package/runtime/hydrate.js +23 -21
- package/runtime/hyperspeed.js +425 -0
- package/runtime/index.js +322 -69
- package/runtime/iterate.js +15 -4
- package/runtime/parse.js +48 -10
- package/runtime/scope.js +50 -0
- package/runtime/state.js +10 -5
- package/runtime/utils.js +63 -5
- package/{runtime/vibe.css → vibe.css} +4 -2
package/runtime/hydrate.js
CHANGED
|
@@ -3,27 +3,20 @@ import { updateConditional } from './conditionals.js';
|
|
|
3
3
|
import { VALUE_ATTRS, DOM_PROPERTIES, BINDING_REGEX, PURE_BINDING_REGEX } from './constants.js';
|
|
4
4
|
import { evalInScope } from './utils.js';
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
let previousState = {};
|
|
8
|
-
|
|
9
|
-
export const setPreviousState = (state) => {
|
|
10
|
-
previousState = { ...state };
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
export default (affected, state, manifest = {}) => {
|
|
6
|
+
export default (affected, state, manifest = {}, oldState = {}) => {
|
|
14
7
|
affected.forEach((aff) => {
|
|
15
8
|
// Use scoped state if provided (from iteration instances)
|
|
16
9
|
const effectiveState = aff.scopedState || state;
|
|
17
10
|
|
|
18
11
|
// Handle iteration updates
|
|
19
12
|
if (aff.type === 'iteration') {
|
|
20
|
-
updateIteration(aff.node, state,
|
|
13
|
+
updateIteration(aff.node, state, oldState, manifest);
|
|
21
14
|
return;
|
|
22
15
|
}
|
|
23
16
|
|
|
24
17
|
// Handle conditional updates
|
|
25
18
|
if (aff.type === 'conditional') {
|
|
26
|
-
updateConditional(aff.node, state,
|
|
19
|
+
updateConditional(aff.node, state, oldState, manifest);
|
|
27
20
|
return;
|
|
28
21
|
}
|
|
29
22
|
|
|
@@ -33,7 +26,7 @@ export default (affected, state, manifest = {}) => {
|
|
|
33
26
|
try {
|
|
34
27
|
// HTML lowercases attribute names, so we need case-insensitive lookup
|
|
35
28
|
// Try exact match first, then try finding a case-insensitive match
|
|
36
|
-
let attrName = evalInScope(matchInner, effectiveState);
|
|
29
|
+
let attrName = evalInScope(matchInner, effectiveState, element);
|
|
37
30
|
|
|
38
31
|
// If exact match failed and expression is a simple property (no dots/brackets)
|
|
39
32
|
if (!attrName && !matchInner.includes('.') && !matchInner.includes('[')) {
|
|
@@ -89,14 +82,19 @@ export default (affected, state, manifest = {}) => {
|
|
|
89
82
|
attrName.startsWith('on');
|
|
90
83
|
|
|
91
84
|
if (isDomProperty && isPureBinding) {
|
|
92
|
-
// For DOM properties
|
|
85
|
+
// For DOM properties (value, checked, selected), set BOTH property AND attribute
|
|
86
|
+
// Property: Fast runtime updates, what the user sees
|
|
87
|
+
// Attribute: Truthful DOM representation, enables compiler snapshots via outerHTML
|
|
93
88
|
const expr = isPureBinding[1];
|
|
94
|
-
const value = evalInScope(expr, effectiveState);
|
|
89
|
+
const value = evalInScope(expr, effectiveState, element);
|
|
95
90
|
element[attrName] = value;
|
|
91
|
+
if (value !== undefined && value !== null) {
|
|
92
|
+
element.setAttribute(attrName, String(value));
|
|
93
|
+
}
|
|
96
94
|
} else if (!isValueAttr && isPureBinding) {
|
|
97
95
|
// Boolean-like attributes: add or remove based on truthiness
|
|
98
96
|
const expr = isPureBinding[1];
|
|
99
|
-
const value = evalInScope(expr, effectiveState);
|
|
97
|
+
const value = evalInScope(expr, effectiveState, element);
|
|
100
98
|
if (value) {
|
|
101
99
|
element.setAttribute(attrName, '');
|
|
102
100
|
} else {
|
|
@@ -105,7 +103,7 @@ export default (affected, state, manifest = {}) => {
|
|
|
105
103
|
} else {
|
|
106
104
|
// Value attribute - replace bindings with values
|
|
107
105
|
const newValue = attrValue.replace(BINDING_REGEX, (_, expr) => {
|
|
108
|
-
return evalInScope(expr, effectiveState);
|
|
106
|
+
return evalInScope(expr, effectiveState, element);
|
|
109
107
|
});
|
|
110
108
|
element.setAttribute(attrName, newValue);
|
|
111
109
|
}
|
|
@@ -114,12 +112,12 @@ export default (affected, state, manifest = {}) => {
|
|
|
114
112
|
}
|
|
115
113
|
|
|
116
114
|
// Handle regular element updates
|
|
117
|
-
const { matches, matchOuter, matchInner, input, element } = aff;
|
|
115
|
+
const { matches, matchOuter, matchInner, input, element, textNode } = aff;
|
|
118
116
|
|
|
119
117
|
// This prevents undefined store properties to throw an error
|
|
120
118
|
try {
|
|
121
119
|
// Evaluate the expression with state as context
|
|
122
|
-
const evaluated = evalInScope(matchInner, effectiveState);
|
|
120
|
+
const evaluated = evalInScope(matchInner, effectiveState, element);
|
|
123
121
|
|
|
124
122
|
const toReplace = input.replaceAll(matchOuter, evaluated).trim();
|
|
125
123
|
|
|
@@ -129,10 +127,14 @@ export default (affected, state, manifest = {}) => {
|
|
|
129
127
|
}
|
|
130
128
|
});
|
|
131
129
|
|
|
132
|
-
|
|
130
|
+
// If we have a direct reference to the text node, update it specifically
|
|
131
|
+
// This prevents wiping child elements when parent has both text and element children
|
|
132
|
+
if (textNode && textNode.nodeType === 3) {
|
|
133
|
+
textNode.textContent = toReplace;
|
|
134
|
+
} else {
|
|
135
|
+
// Fallback: element has no children or is just a text container
|
|
136
|
+
element.textContent = toReplace;
|
|
137
|
+
}
|
|
133
138
|
} catch (e) {}
|
|
134
139
|
});
|
|
135
|
-
|
|
136
|
-
// Update previous state for next diff
|
|
137
|
-
setPreviousState(state);
|
|
138
140
|
};
|
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hyperspeed - Pre-compiled manifest support for Vibe
|
|
3
|
+
*
|
|
4
|
+
* Handles detection and restoration of pre-compiled pages.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// Build hyperspeed manifest with restoration data (captures markers before hydration)
|
|
8
|
+
export const buildHyperspeedManifest = (parsedTree) => {
|
|
9
|
+
// Helper to split text content by @[...] markers into array
|
|
10
|
+
const splitByMarkers = (text) => {
|
|
11
|
+
const regex = /@\[[^\]]+\]/g;
|
|
12
|
+
const parts = [];
|
|
13
|
+
let lastIndex = 0;
|
|
14
|
+
|
|
15
|
+
text.replace(regex, (match, index) => {
|
|
16
|
+
// Add text before marker
|
|
17
|
+
if (index > lastIndex) {
|
|
18
|
+
parts.push(text.slice(lastIndex, index));
|
|
19
|
+
}
|
|
20
|
+
// Add marker
|
|
21
|
+
parts.push(match);
|
|
22
|
+
lastIndex = index + match.length;
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// Add remaining text
|
|
26
|
+
if (lastIndex < text.length) {
|
|
27
|
+
parts.push(text.slice(lastIndex));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return parts.length > 0 ? parts : null;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const walkNode = (node) => {
|
|
34
|
+
const result = {
|
|
35
|
+
element: null,
|
|
36
|
+
parsed: [],
|
|
37
|
+
children: {},
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// Copy type for iterations/conditionals
|
|
41
|
+
if (node.type) {
|
|
42
|
+
result.type = node.type;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Check if parsed string contains bindings - split into array
|
|
46
|
+
if (typeof node.parsed === 'string' && node.parsed.includes('@[')) {
|
|
47
|
+
const parsedArray = splitByMarkers(node.parsed);
|
|
48
|
+
if (parsedArray) {
|
|
49
|
+
result.hyperspeedRestoration = {
|
|
50
|
+
parsed: parsedArray,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Check for attribute bindings
|
|
56
|
+
if (node.attributes && typeof node.attributes === 'object') {
|
|
57
|
+
const attrBindings = {};
|
|
58
|
+
for (const [key, value] of Object.entries(node.attributes)) {
|
|
59
|
+
if (typeof value === 'string' && value.includes('@[')) {
|
|
60
|
+
attrBindings[key] = value;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (Object.keys(attrBindings).length > 0) {
|
|
64
|
+
if (!result.hyperspeedRestoration) {
|
|
65
|
+
result.hyperspeedRestoration = {};
|
|
66
|
+
}
|
|
67
|
+
result.hyperspeedRestoration.attributes = attrBindings;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// For iterations, extract template as HTML string
|
|
72
|
+
if (node.type === 'iteration' && node.meta?.template?.element) {
|
|
73
|
+
const templateElement = node.meta.template.element;
|
|
74
|
+
if (templateElement && templateElement.innerHTML) {
|
|
75
|
+
if (!result.hyperspeedRestoration) {
|
|
76
|
+
result.hyperspeedRestoration = {};
|
|
77
|
+
}
|
|
78
|
+
result.hyperspeedRestoration.template = templateElement.innerHTML;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// For conditionals, extract branch templates as HTML strings
|
|
83
|
+
if (node.type === 'conditional' && node.meta?.branches) {
|
|
84
|
+
const trueBranch = node.meta.branches.true?.element;
|
|
85
|
+
const falseBranch = node.meta.branches.false?.element;
|
|
86
|
+
|
|
87
|
+
if (trueBranch?.innerHTML || falseBranch?.innerHTML) {
|
|
88
|
+
if (!result.hyperspeedRestoration) {
|
|
89
|
+
result.hyperspeedRestoration = {};
|
|
90
|
+
}
|
|
91
|
+
if (trueBranch?.innerHTML) {
|
|
92
|
+
result.hyperspeedRestoration.trueTemplate = trueBranch.innerHTML;
|
|
93
|
+
}
|
|
94
|
+
if (falseBranch?.innerHTML) {
|
|
95
|
+
result.hyperspeedRestoration.falseTemplate = falseBranch.innerHTML;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Recursively process children (skip runtime-only nodes)
|
|
101
|
+
if (node.children && typeof node.children === 'object') {
|
|
102
|
+
for (const [key, childNode] of Object.entries(node.children)) {
|
|
103
|
+
result.children[key] = walkNode(childNode);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return result;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
// Wrap in body structure (parsedTree is already the body's children)
|
|
111
|
+
return {
|
|
112
|
+
element: null,
|
|
113
|
+
parsed: [],
|
|
114
|
+
children: {
|
|
115
|
+
body: {
|
|
116
|
+
element: null,
|
|
117
|
+
parsed: [],
|
|
118
|
+
children: walkNode(parsedTree).children, // Use children directly
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
// Hyperspeed detection - per-page manifest
|
|
125
|
+
// Each compiled page has its own manifest: /vibe-hyperspeed/{page-path}.manifest.js
|
|
126
|
+
let hyperspeedManifest = null;
|
|
127
|
+
let hyperspeedDetectionAttempted = false;
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Detect page-specific manifest (async, cached after first call)
|
|
131
|
+
*/
|
|
132
|
+
const detectHyperspeed = async () => {
|
|
133
|
+
if (hyperspeedDetectionAttempted) return hyperspeedManifest;
|
|
134
|
+
hyperspeedDetectionAttempted = true;
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
const pagePath = window.location.pathname;
|
|
138
|
+
const pathSegments = pagePath.split('/').filter(s => s);
|
|
139
|
+
|
|
140
|
+
if (pathSegments.length === 0) return null;
|
|
141
|
+
|
|
142
|
+
// Extract file name and directory parts
|
|
143
|
+
// For /compiled/playground/test.html -> ['compiled', 'playground', 'test.html']
|
|
144
|
+
const fileName = pathSegments[pathSegments.length - 1];
|
|
145
|
+
const dirSegments = pathSegments.slice(0, -1); // All parts except filename
|
|
146
|
+
|
|
147
|
+
// Build possible manifest paths
|
|
148
|
+
const possiblePaths = [];
|
|
149
|
+
|
|
150
|
+
// Strategy 1: vibe-hyperspeed at the same level as parent directory
|
|
151
|
+
// /compiled/playground/test.html -> /compiled/vibe-hyperspeed/playground/test.html.manifest.js
|
|
152
|
+
if (dirSegments.length >= 1) {
|
|
153
|
+
const subPath = dirSegments.slice(1).join('/'); // Everything after first dir
|
|
154
|
+
const baseDir = '/' + dirSegments[0]; // First directory segment
|
|
155
|
+
possiblePaths.push(`${baseDir}/vibe-hyperspeed/${subPath ? subPath + '/' : ''}${fileName}.manifest.js`);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Strategy 2: vibe-hyperspeed at web root (original behavior)
|
|
159
|
+
// /compiled/playground/test.html -> /vibe-hyperspeed/compiled/playground/test.html.manifest.js
|
|
160
|
+
possiblePaths.push(`/vibe-hyperspeed${pagePath}.manifest.js`);
|
|
161
|
+
|
|
162
|
+
// Strategy 3: vibe-hyperspeed relative to immediate parent
|
|
163
|
+
// /playground/test.html -> /vibe-hyperspeed/playground/test.html.manifest.js
|
|
164
|
+
if (dirSegments.length > 0) {
|
|
165
|
+
const relativePath = dirSegments.join('/');
|
|
166
|
+
possiblePaths.push(`/vibe-hyperspeed/${relativePath}/${fileName}.manifest.js`);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Try each possible path
|
|
170
|
+
for (const manifestPath of possiblePaths) {
|
|
171
|
+
try {
|
|
172
|
+
const module = await import(manifestPath);
|
|
173
|
+
hyperspeedManifest = module.default;
|
|
174
|
+
return hyperspeedManifest;
|
|
175
|
+
} catch {
|
|
176
|
+
// Try next path
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// No manifest found
|
|
182
|
+
return null;
|
|
183
|
+
} catch {
|
|
184
|
+
// No manifest for this page - runtime-only mode
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Restore DOM from pre-rendered values to @[...] markers using hyperspeed manifest
|
|
191
|
+
* This enables FOUC-free loading while maintaining runtime reactivity
|
|
192
|
+
*
|
|
193
|
+
* Flow:
|
|
194
|
+
* 1. Page loads with pre-rendered values: <strong>John Doe</strong> (visible, no FOUC)
|
|
195
|
+
* 2. Restoration: Replace with markers: <strong>@[firstName] @[lastName]</strong>
|
|
196
|
+
* 3. Runtime processes normally: Finds markers, makes reactive
|
|
197
|
+
*
|
|
198
|
+
* @param {Element} rootElement - The DOM element to restore (e.g., <body>)
|
|
199
|
+
* @param {Object} subtree - The matching subtree from manifest (e.g., manifest.children.body)
|
|
200
|
+
* @param {Object} fullManifest - The full manifest (unused now, kept for compatibility)
|
|
201
|
+
*/
|
|
202
|
+
export const restoreMarkersFromManifest = (rootElement, subtree, fullManifest = null) => {
|
|
203
|
+
const walkTree = (tree, element) => {
|
|
204
|
+
if (!tree || !element) return;
|
|
205
|
+
|
|
206
|
+
// Check for hyperspeed restoration data
|
|
207
|
+
const restoration = tree.hyperspeedRestoration;
|
|
208
|
+
|
|
209
|
+
if (restoration) {
|
|
210
|
+
// Restore text content with markers if parsed contains bindings
|
|
211
|
+
if (restoration.parsed && Array.isArray(restoration.parsed)) {
|
|
212
|
+
const hasBindings = restoration.parsed.some(item =>
|
|
213
|
+
typeof item === 'string' && item.includes('@[')
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
if (hasBindings) {
|
|
217
|
+
// Reconstruct original content with markers
|
|
218
|
+
const originalContent = restoration.parsed.join('');
|
|
219
|
+
|
|
220
|
+
// For text nodes, update parent's innerHTML
|
|
221
|
+
// For elements with children, update only text nodes
|
|
222
|
+
if (element.childNodes.length === 1 && element.childNodes[0].nodeType === 3) {
|
|
223
|
+
// Single text node - replace it
|
|
224
|
+
element.childNodes[0].textContent = originalContent;
|
|
225
|
+
} else if (element.childNodes.length === 0) {
|
|
226
|
+
// No children - set textContent
|
|
227
|
+
element.textContent = originalContent;
|
|
228
|
+
}
|
|
229
|
+
// If element has multiple children, they'll be handled recursively
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Skip attribute restoration - manifest merge provides all binding metadata
|
|
234
|
+
// Attributes are already pre-rendered correctly by compiler
|
|
235
|
+
// restoration.attributes exists only for manifest merge, not for DOM restoration
|
|
236
|
+
|
|
237
|
+
// Restore conditionals
|
|
238
|
+
if ((restoration.trueTemplate || restoration.falseTemplate) && tree.type === 'conditional') {
|
|
239
|
+
// Find comment markers
|
|
240
|
+
const walker = document.createTreeWalker(element, NodeFilter.SHOW_COMMENT);
|
|
241
|
+
let startComment = null;
|
|
242
|
+
let elseComment = null;
|
|
243
|
+
let endComment = null;
|
|
244
|
+
|
|
245
|
+
while (walker.nextNode()) {
|
|
246
|
+
const comment = walker.currentNode;
|
|
247
|
+
const text = comment.textContent.trim();
|
|
248
|
+
if (text.startsWith('if')) {
|
|
249
|
+
startComment = comment;
|
|
250
|
+
} else if (text === 'else') {
|
|
251
|
+
elseComment = comment;
|
|
252
|
+
} else if (text === '/if') {
|
|
253
|
+
endComment = comment;
|
|
254
|
+
break;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (startComment && endComment) {
|
|
259
|
+
// Remove pre-rendered content in true branch
|
|
260
|
+
let current = startComment.nextSibling;
|
|
261
|
+
while (current && current !== (elseComment || endComment)) {
|
|
262
|
+
const next = current.nextSibling;
|
|
263
|
+
if (current.nodeType === Node.ELEMENT_NODE) {
|
|
264
|
+
current.remove();
|
|
265
|
+
}
|
|
266
|
+
current = next;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Remove pre-rendered content in false branch if exists
|
|
270
|
+
if (elseComment) {
|
|
271
|
+
current = elseComment.nextSibling;
|
|
272
|
+
while (current && current !== endComment) {
|
|
273
|
+
const next = current.nextSibling;
|
|
274
|
+
if (current.nodeType === Node.ELEMENT_NODE) {
|
|
275
|
+
current.remove();
|
|
276
|
+
}
|
|
277
|
+
current = next;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Insert templates (runtime will process them)
|
|
282
|
+
if (restoration.trueTemplate) {
|
|
283
|
+
const template = document.createElement('div');
|
|
284
|
+
template.innerHTML = restoration.trueTemplate;
|
|
285
|
+
const templateNode = template.firstElementChild; // Use firstElementChild, not firstChild
|
|
286
|
+
if (templateNode) {
|
|
287
|
+
(elseComment || endComment).parentNode.insertBefore(
|
|
288
|
+
templateNode,
|
|
289
|
+
elseComment || endComment
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (restoration.falseTemplate && elseComment) {
|
|
295
|
+
const template = document.createElement('div');
|
|
296
|
+
template.innerHTML = restoration.falseTemplate;
|
|
297
|
+
const templateNode = template.firstElementChild; // Use firstElementChild, not firstChild
|
|
298
|
+
if (templateNode) {
|
|
299
|
+
endComment.parentNode.insertBefore(templateNode, endComment);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// After restoration, delete hyperspeedRestoration (runtime will parse DOM fresh)
|
|
306
|
+
delete tree.hyperspeedRestoration;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Recursively restore children
|
|
310
|
+
if (tree.children) {
|
|
311
|
+
for (const key in tree.children) {
|
|
312
|
+
const childTree = tree.children[key];
|
|
313
|
+
|
|
314
|
+
// Handle iteration nodes with restoration data
|
|
315
|
+
if (childTree.type === 'iteration' && childTree.hyperspeedRestoration) {
|
|
316
|
+
const restoration = childTree.hyperspeedRestoration;
|
|
317
|
+
|
|
318
|
+
// Find iteration comment by searching (not using index)
|
|
319
|
+
// This avoids index shifting issues after restoration changes DOM
|
|
320
|
+
let startComment = null;
|
|
321
|
+
let endComment = null;
|
|
322
|
+
|
|
323
|
+
const walker = document.createTreeWalker(element, NodeFilter.SHOW_COMMENT);
|
|
324
|
+
while (walker.nextNode()) {
|
|
325
|
+
const comment = walker.currentNode;
|
|
326
|
+
const trimmed = comment.textContent.trim();
|
|
327
|
+
|
|
328
|
+
// Skip already processed iterations
|
|
329
|
+
if (comment._vibeProcessed) continue;
|
|
330
|
+
|
|
331
|
+
if (trimmed.startsWith('each ') && !startComment) {
|
|
332
|
+
startComment = comment;
|
|
333
|
+
} else if (trimmed === '/each' && startComment && !endComment) {
|
|
334
|
+
endComment = comment;
|
|
335
|
+
break;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (startComment && endComment) {
|
|
340
|
+
// Mark as processed
|
|
341
|
+
startComment._vibeProcessed = true;
|
|
342
|
+
endComment._vibeProcessed = true;
|
|
343
|
+
|
|
344
|
+
// Remove all pre-rendered items between comments
|
|
345
|
+
let current = startComment.nextSibling;
|
|
346
|
+
while (current && current !== endComment) {
|
|
347
|
+
const next = current.nextSibling;
|
|
348
|
+
if (current.nodeType === Node.ELEMENT_NODE) {
|
|
349
|
+
current.remove();
|
|
350
|
+
}
|
|
351
|
+
current = next;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Insert template from restoration data
|
|
355
|
+
const temp = document.createElement('div');
|
|
356
|
+
temp.innerHTML = restoration.template;
|
|
357
|
+
const templateNode = temp.firstElementChild;
|
|
358
|
+
if (templateNode) {
|
|
359
|
+
endComment.parentNode.insertBefore(templateNode, endComment);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
delete childTree.hyperspeedRestoration;
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Handle conditional nodes (similar approach)
|
|
368
|
+
if (childTree.type === 'conditional') {
|
|
369
|
+
// TODO: Implement conditional restoration when needed
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Handle text nodes with restoration data
|
|
374
|
+
if (key.startsWith('text_') && childTree.hyperspeedRestoration) {
|
|
375
|
+
const restoration = childTree.hyperspeedRestoration;
|
|
376
|
+
if (restoration.parsed && Array.isArray(restoration.parsed)) {
|
|
377
|
+
const hasBindings = restoration.parsed.some(item =>
|
|
378
|
+
typeof item === 'string' && item.includes('@[')
|
|
379
|
+
);
|
|
380
|
+
|
|
381
|
+
if (hasBindings) {
|
|
382
|
+
const originalContent = restoration.parsed.join('');
|
|
383
|
+
if (element.childNodes.length === 1 && element.childNodes[0].nodeType === 3) {
|
|
384
|
+
element.childNodes[0].textContent = originalContent;
|
|
385
|
+
} else if (element.childNodes.length === 0) {
|
|
386
|
+
element.textContent = originalContent;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
delete childTree.hyperspeedRestoration;
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Find corresponding child element
|
|
395
|
+
// Keys like "strong_0" use childNodes indices (includes text nodes/comments)
|
|
396
|
+
const match = key.match(/_(\d+)$/);
|
|
397
|
+
if (match) {
|
|
398
|
+
const index = parseInt(match[1], 10);
|
|
399
|
+
const childNode = element.childNodes[index]; // Use childNodes (all nodes)
|
|
400
|
+
|
|
401
|
+
// Only walk if it's an element node
|
|
402
|
+
if (childNode && childNode.nodeType === Node.ELEMENT_NODE) {
|
|
403
|
+
walkTree(childTree, childNode);
|
|
404
|
+
}
|
|
405
|
+
} else {
|
|
406
|
+
// Try to find by tag name for non-indexed keys
|
|
407
|
+
const tagName = key.split('_')[0].toUpperCase();
|
|
408
|
+
const childElement = Array.from(element.children).find(
|
|
409
|
+
child => child.nodeName === tagName
|
|
410
|
+
);
|
|
411
|
+
if (childElement) {
|
|
412
|
+
walkTree(childTree, childElement);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
walkTree(subtree, rootElement);
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
// Top-level await to detect before module exports
|
|
423
|
+
hyperspeedManifest = await detectHyperspeed();
|
|
424
|
+
|
|
425
|
+
export { hyperspeedManifest };
|