@ape-egg/vibe 1.6.1 → 1.7.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/CHANGELOG.md +62 -2
- package/README.md +19 -10
- package/boot.js +11 -0
- package/compiler/native/vibe-compiler-darwin-arm64 +0 -0
- package/compiler/native/vibe-compiler-linux-x64 +0 -0
- package/compiler/src/Cargo.lock +67 -197
- package/compiler/src/Cargo.toml +2 -2
- package/compiler/src/compiler/compile.rs +136 -38
- package/compiler/src/compiler/component_tagger.rs +68 -40
- package/compiler/src/compiler/value_stamper.rs +75 -5
- package/compiler/src/compiler/watcher.rs +69 -2
- package/compiler/src/config.rs +10 -5
- package/compiler/src/main.rs +38 -25
- package/compiler/src/parser/html.rs +204 -87
- package/index.js +34 -5
- package/llms.txt +1 -1
- package/package.json +1 -1
- package/runtime/cleanup.js +0 -9
- package/runtime/component.js +18 -20
- package/runtime/index.js +20 -2
- package/runtime/iterate.js +63 -15
- package/runtime/parse.js +3 -2
- package/runtime/pre-compiled-manifest.js +115 -58
package/runtime/iterate.js
CHANGED
|
@@ -16,9 +16,10 @@ import * as compiled from './pre-compiled-iterations.js';
|
|
|
16
16
|
* Find a comment node with matching text content in the given nodes.
|
|
17
17
|
*/
|
|
18
18
|
const findComment = (nodes, text) => {
|
|
19
|
+
const trimmedText = text.trim();
|
|
19
20
|
for (let i = 0; i < nodes.length; i++) {
|
|
20
21
|
const node = nodes[i];
|
|
21
|
-
if (node.nodeType === 8 && node.textContent.trim() ===
|
|
22
|
+
if (node.nodeType === 8 && node.textContent.trim() === trimmedText) {
|
|
22
23
|
return node;
|
|
23
24
|
}
|
|
24
25
|
}
|
|
@@ -36,9 +37,16 @@ const cloneTreeWithElements = (originalTree, clonedRoot) => {
|
|
|
36
37
|
parsed: originalTree.parsed,
|
|
37
38
|
element: clonedRoot,
|
|
38
39
|
children: {},
|
|
39
|
-
...(originalTree.attributes && { attributes: originalTree.attributes }),
|
|
40
40
|
};
|
|
41
41
|
|
|
42
|
+
// Avoid spread operator for performance
|
|
43
|
+
if (originalTree.attributes) {
|
|
44
|
+
cloned.attributes = originalTree.attributes;
|
|
45
|
+
}
|
|
46
|
+
if (originalTree.nameBindings) {
|
|
47
|
+
cloned.nameBindings = originalTree.nameBindings;
|
|
48
|
+
}
|
|
49
|
+
|
|
42
50
|
if (!originalTree.children) return cloned;
|
|
43
51
|
|
|
44
52
|
const clonedChildNodes = clonedRoot?.childNodes;
|
|
@@ -130,26 +138,42 @@ export const initializeBlock = (templateNodes, scopedState, cachedTree = null) =
|
|
|
130
138
|
let clonedNodes = [];
|
|
131
139
|
let firstElement = null;
|
|
132
140
|
|
|
133
|
-
//
|
|
134
|
-
|
|
141
|
+
// TEMPORARY: Disable fast path to test if it's causing duplication
|
|
142
|
+
let canUseFastPath = false;
|
|
135
143
|
|
|
136
144
|
// Create a container for parsing (to capture all nodes including comment nodes like <!-- if -->)
|
|
137
145
|
const parseContainer = document.createElement('div');
|
|
138
146
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
firstElement
|
|
147
|
+
if (canUseFastPath) {
|
|
148
|
+
// Fast path: clone template nodes and map to cached tree structure
|
|
149
|
+
for (let i = 0; i < templateNodes.length; i++) {
|
|
150
|
+
const cloned = templateNodes[i].cloneNode(true);
|
|
151
|
+
parseContainer.appendChild(cloned);
|
|
152
|
+
if (!firstElement && cloned.nodeType === 1) {
|
|
153
|
+
firstElement = cloned;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
tree = cloneTreeWithElements(cachedTree, parseContainer);
|
|
157
|
+
} else {
|
|
158
|
+
// Slow path: clone and parse from scratch
|
|
159
|
+
for (let i = 0; i < templateNodes.length; i++) {
|
|
160
|
+
const cloned = templateNodes[i].cloneNode(true);
|
|
161
|
+
parseContainer.appendChild(cloned);
|
|
162
|
+
if (!firstElement && cloned.nodeType === 1) {
|
|
163
|
+
firstElement = cloned;
|
|
164
|
+
}
|
|
145
165
|
}
|
|
166
|
+
// Parse the entire container (includes all nodes + conditionals)
|
|
167
|
+
tree = parse(parseContainer);
|
|
146
168
|
}
|
|
147
169
|
|
|
148
|
-
// Parse the entire container (includes all nodes + conditionals)
|
|
149
|
-
tree = parse(parseContainer);
|
|
150
|
-
|
|
151
170
|
// Extract the cloned nodes from the container (these are the same nodes the tree references)
|
|
152
|
-
|
|
171
|
+
// Avoid Array.from for performance
|
|
172
|
+
const childNodes = parseContainer.childNodes;
|
|
173
|
+
clonedNodes = [];
|
|
174
|
+
for (let i = 0; i < childNodes.length; i++) {
|
|
175
|
+
clonedNodes.push(childNodes[i]);
|
|
176
|
+
}
|
|
153
177
|
|
|
154
178
|
// If no firstElement found, use parseContainer as fallback
|
|
155
179
|
if (!firstElement) {
|
|
@@ -271,6 +295,21 @@ export const renderIteration = (iterationNode, state, manifest, parentScope = {}
|
|
|
271
295
|
return;
|
|
272
296
|
}
|
|
273
297
|
|
|
298
|
+
// Fallback check: If markers are lost (e.g., comment nodes replaced by component loading),
|
|
299
|
+
// check actual DOM state between comments for hydrated nodes
|
|
300
|
+
let currentNode = startComment.nextSibling;
|
|
301
|
+
while (currentNode && currentNode !== endComment) {
|
|
302
|
+
if (currentNode.nodeType === 1) {
|
|
303
|
+
// Element node
|
|
304
|
+
const html = currentNode.outerHTML || '';
|
|
305
|
+
// If node doesn't have any @[...] syntax, it's been hydrated
|
|
306
|
+
if (!html.includes('@[')) {
|
|
307
|
+
return; // Already rendered
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
currentNode = currentNode.nextSibling;
|
|
311
|
+
}
|
|
312
|
+
|
|
274
313
|
// Remove template nodes from DOM on first render
|
|
275
314
|
if (!iterationNode.runtime.templateRemoved) {
|
|
276
315
|
let node = startComment.nextSibling;
|
|
@@ -361,7 +400,16 @@ export const updateIteration = (iterationNode, newState, oldState, manifest, par
|
|
|
361
400
|
// Compiled path: Use pre-compiled batch function when available
|
|
362
401
|
if (compiled.canUseCompiled(iterationNode)) {
|
|
363
402
|
const compiledMeta = compiled.getCompiledMeta(iterationNode);
|
|
364
|
-
if (
|
|
403
|
+
if (
|
|
404
|
+
compiled.updateCompiled(
|
|
405
|
+
iterationNode,
|
|
406
|
+
newArray,
|
|
407
|
+
newState,
|
|
408
|
+
compiledMeta,
|
|
409
|
+
startComment,
|
|
410
|
+
endComment,
|
|
411
|
+
)
|
|
412
|
+
) {
|
|
365
413
|
return;
|
|
366
414
|
}
|
|
367
415
|
// Fall through to runtime path if compiled failed
|
package/runtime/parse.js
CHANGED
|
@@ -12,7 +12,7 @@ const parseHTML = (children, rootKey = undefined) =>
|
|
|
12
12
|
children.reduce((s, element, i) => {
|
|
13
13
|
const { nodeName, textContent } = element;
|
|
14
14
|
if (['#comment'].includes(nodeName)) {
|
|
15
|
-
return
|
|
15
|
+
return s;
|
|
16
16
|
}
|
|
17
17
|
const name = nodeName.startsWith('#') ? nodeName.slice(1) : nodeName;
|
|
18
18
|
const innerNodeIdentifier = `${name}_${i}`.toLowerCase();
|
|
@@ -77,8 +77,9 @@ const recursive = (children, rootKey = undefined, skipIndices = new Set(), stats
|
|
|
77
77
|
},
|
|
78
78
|
},
|
|
79
79
|
// Preserve runtime data from previous parse if it exists (stored on startComment by iterate.js)
|
|
80
|
+
// Only restore if the comment node is still connected to the DOM (not replaced by component loading)
|
|
80
81
|
// @ts-ignore - custom property added by iterate.js
|
|
81
|
-
runtime: element.__vibeIterationRuntime || {
|
|
82
|
+
runtime: (element.isConnected && element.__vibeIterationRuntime) || {
|
|
82
83
|
instances: [],
|
|
83
84
|
templateRemoved: false,
|
|
84
85
|
},
|
|
@@ -43,7 +43,7 @@ export const buildHyperspeedManifest = (parsedTree) => {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
// Check if parsed string contains bindings - split into array
|
|
46
|
-
if (typeof node.parsed ===
|
|
46
|
+
if (typeof node.parsed === "string" && node.parsed.includes("@[")) {
|
|
47
47
|
const parsedArray = splitByMarkers(node.parsed);
|
|
48
48
|
if (parsedArray) {
|
|
49
49
|
result.hyperspeedRestoration = {
|
|
@@ -53,10 +53,10 @@ export const buildHyperspeedManifest = (parsedTree) => {
|
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
// Check for attribute bindings
|
|
56
|
-
if (node.attributes && typeof node.attributes ===
|
|
56
|
+
if (node.attributes && typeof node.attributes === "object") {
|
|
57
57
|
const attrBindings = {};
|
|
58
58
|
for (const [key, value] of Object.entries(node.attributes)) {
|
|
59
|
-
if (typeof value ===
|
|
59
|
+
if (typeof value === "string" && value.includes("@[")) {
|
|
60
60
|
attrBindings[key] = value;
|
|
61
61
|
}
|
|
62
62
|
}
|
|
@@ -69,7 +69,7 @@ export const buildHyperspeedManifest = (parsedTree) => {
|
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
// For iterations, extract template as HTML string
|
|
72
|
-
if (node.type ===
|
|
72
|
+
if (node.type === "iteration" && node.meta?.template?.element) {
|
|
73
73
|
const templateElement = node.meta.template.element;
|
|
74
74
|
if (templateElement && templateElement.innerHTML) {
|
|
75
75
|
if (!result.hyperspeedRestoration) {
|
|
@@ -80,7 +80,7 @@ export const buildHyperspeedManifest = (parsedTree) => {
|
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
// For conditionals, extract branch templates as HTML strings
|
|
83
|
-
if (node.type ===
|
|
83
|
+
if (node.type === "conditional" && node.meta?.branches) {
|
|
84
84
|
const trueBranch = node.meta.branches.true?.element;
|
|
85
85
|
const falseBranch = node.meta.branches.false?.element;
|
|
86
86
|
|
|
@@ -98,7 +98,7 @@ export const buildHyperspeedManifest = (parsedTree) => {
|
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
// Recursively process children (skip runtime-only nodes)
|
|
101
|
-
if (node.children && typeof node.children ===
|
|
101
|
+
if (node.children && typeof node.children === "object") {
|
|
102
102
|
for (const [key, childNode] of Object.entries(node.children)) {
|
|
103
103
|
result.children[key] = walkNode(childNode);
|
|
104
104
|
}
|
|
@@ -116,8 +116,8 @@ export const buildHyperspeedManifest = (parsedTree) => {
|
|
|
116
116
|
element: null,
|
|
117
117
|
parsed: [],
|
|
118
118
|
children: walkNode(parsedTree).children, // Use children directly
|
|
119
|
-
}
|
|
120
|
-
}
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
121
|
};
|
|
122
122
|
};
|
|
123
123
|
|
|
@@ -138,19 +138,19 @@ const detectHyperspeed = async () => {
|
|
|
138
138
|
let pagePath = window.location.pathname;
|
|
139
139
|
|
|
140
140
|
// Normalize path: handle directory URLs and missing extensions
|
|
141
|
-
if (pagePath.endsWith(
|
|
141
|
+
if (pagePath.endsWith("/")) {
|
|
142
142
|
// /compiled/ -> /compiled/index.html
|
|
143
|
-
pagePath = pagePath +
|
|
144
|
-
} else if (!pagePath.includes(
|
|
143
|
+
pagePath = pagePath + "index.html";
|
|
144
|
+
} else if (!pagePath.includes(".")) {
|
|
145
145
|
// /compiled/mypage -> /compiled/mypage.html
|
|
146
|
-
const lastSlash = pagePath.lastIndexOf(
|
|
146
|
+
const lastSlash = pagePath.lastIndexOf("/");
|
|
147
147
|
const lastSegment = pagePath.substring(lastSlash + 1);
|
|
148
|
-
if (lastSegment && !lastSegment.includes(
|
|
149
|
-
pagePath = pagePath +
|
|
148
|
+
if (lastSegment && !lastSegment.includes(".")) {
|
|
149
|
+
pagePath = pagePath + ".html";
|
|
150
150
|
}
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
-
const pathSegments = pagePath.split(
|
|
153
|
+
const pathSegments = pagePath.split("/").filter((s) => s);
|
|
154
154
|
|
|
155
155
|
if (pathSegments.length === 0) return null;
|
|
156
156
|
|
|
@@ -165,9 +165,11 @@ const detectHyperspeed = async () => {
|
|
|
165
165
|
// Strategy 1: vibe-hyperspeed at the same level as parent directory
|
|
166
166
|
// /compiled/playground/test.html -> /compiled/vibe-hyperspeed/playground/test.html.manifest.js
|
|
167
167
|
if (dirSegments.length >= 1) {
|
|
168
|
-
const subPath = dirSegments.slice(1).join(
|
|
169
|
-
const baseDir =
|
|
170
|
-
possiblePaths.push(
|
|
168
|
+
const subPath = dirSegments.slice(1).join("/"); // Everything after first dir
|
|
169
|
+
const baseDir = "/" + dirSegments[0]; // First directory segment
|
|
170
|
+
possiblePaths.push(
|
|
171
|
+
`${baseDir}/vibe-hyperspeed/${subPath ? subPath + "/" : ""}${fileName}.manifest.js`,
|
|
172
|
+
);
|
|
171
173
|
}
|
|
172
174
|
|
|
173
175
|
// Strategy 2: vibe-hyperspeed at web root (original behavior)
|
|
@@ -177,8 +179,10 @@ const detectHyperspeed = async () => {
|
|
|
177
179
|
// Strategy 3: vibe-hyperspeed relative to immediate parent
|
|
178
180
|
// /playground/test.html -> /vibe-hyperspeed/playground/test.html.manifest.js
|
|
179
181
|
if (dirSegments.length > 0) {
|
|
180
|
-
const relativePath = dirSegments.join(
|
|
181
|
-
possiblePaths.push(
|
|
182
|
+
const relativePath = dirSegments.join("/");
|
|
183
|
+
possiblePaths.push(
|
|
184
|
+
`/vibe-hyperspeed/${relativePath}/${fileName}.manifest.js`,
|
|
185
|
+
);
|
|
182
186
|
}
|
|
183
187
|
|
|
184
188
|
// Try each possible path
|
|
@@ -187,7 +191,7 @@ const detectHyperspeed = async () => {
|
|
|
187
191
|
const module = await import(manifestPath);
|
|
188
192
|
hyperspeedData = {
|
|
189
193
|
manifest: module.default,
|
|
190
|
-
path: manifestPath
|
|
194
|
+
path: manifestPath,
|
|
191
195
|
};
|
|
192
196
|
return hyperspeedData;
|
|
193
197
|
} catch (e) {
|
|
@@ -217,7 +221,11 @@ const detectHyperspeed = async () => {
|
|
|
217
221
|
* @param {Object} subtree - The matching subtree from manifest (e.g., manifest.children.body)
|
|
218
222
|
* @param {Object} fullManifest - The full manifest (unused now, kept for compatibility)
|
|
219
223
|
*/
|
|
220
|
-
export const restoreMarkersFromManifest = (
|
|
224
|
+
export const restoreMarkersFromManifest = (
|
|
225
|
+
rootElement,
|
|
226
|
+
subtree,
|
|
227
|
+
fullManifest = null,
|
|
228
|
+
) => {
|
|
221
229
|
const walkTree = (tree, element) => {
|
|
222
230
|
if (!tree || !element) return;
|
|
223
231
|
|
|
@@ -227,17 +235,25 @@ export const restoreMarkersFromManifest = (rootElement, subtree, fullManifest =
|
|
|
227
235
|
if (restoration) {
|
|
228
236
|
// Restore text content with markers if parsed contains bindings
|
|
229
237
|
if (restoration.parsed && Array.isArray(restoration.parsed)) {
|
|
230
|
-
const hasBindings = restoration.parsed.some(
|
|
231
|
-
typeof item ===
|
|
238
|
+
const hasBindings = restoration.parsed.some(
|
|
239
|
+
(item) => typeof item === "string" && item.includes("@["),
|
|
232
240
|
);
|
|
233
241
|
|
|
234
242
|
if (hasBindings) {
|
|
235
243
|
// Reconstruct original content with markers
|
|
236
|
-
|
|
244
|
+
let originalContent = restoration.parsed.join("");
|
|
245
|
+
|
|
246
|
+
// Transform component-scoped bindings back to this. format
|
|
247
|
+
// Compiler transforms @[this.count] → @[_c0.count] for stamping
|
|
248
|
+
// Runtime expects @[this.count], so transform back
|
|
249
|
+
originalContent = originalContent.replace(/@\[_c\d+\./g, "@[this.");
|
|
237
250
|
|
|
238
251
|
// For text nodes, update parent's innerHTML
|
|
239
252
|
// For elements with children, update only text nodes
|
|
240
|
-
if (
|
|
253
|
+
if (
|
|
254
|
+
element.childNodes.length === 1 &&
|
|
255
|
+
element.childNodes[0].nodeType === 3
|
|
256
|
+
) {
|
|
241
257
|
// Single text node - replace it
|
|
242
258
|
element.childNodes[0].textContent = originalContent;
|
|
243
259
|
} else if (element.childNodes.length === 0) {
|
|
@@ -253,7 +269,13 @@ export const restoreMarkersFromManifest = (rootElement, subtree, fullManifest =
|
|
|
253
269
|
// 1. Boolean-like attributes with falsy values were removed - restore them
|
|
254
270
|
// 2. Value attributes were stamped - replace stamped values with markers
|
|
255
271
|
if (restoration.attributes) {
|
|
256
|
-
for (
|
|
272
|
+
for (let [attrName, attrValue] of Object.entries(
|
|
273
|
+
restoration.attributes,
|
|
274
|
+
)) {
|
|
275
|
+
// Transform component-scoped bindings back to this. format
|
|
276
|
+
// Compiler transforms @[this.count] → @[_c0.count], runtime expects @[this.count]
|
|
277
|
+
attrValue = attrValue.replace(/@\[_c\d+\./g, "@[this.");
|
|
278
|
+
|
|
257
279
|
// Always set the attribute to restore the marker
|
|
258
280
|
// - If missing (boolean-like, falsy): adds it back
|
|
259
281
|
// - If present (value attr, stamped): replaces stamped value with marker
|
|
@@ -288,17 +310,22 @@ export const restoreMarkersFromManifest = (rootElement, subtree, fullManifest =
|
|
|
288
310
|
// Text nodes use childNodes indices which become invalid after DOM modifications
|
|
289
311
|
if (tree.children) {
|
|
290
312
|
for (const key in tree.children) {
|
|
291
|
-
if (key.startsWith(
|
|
313
|
+
if (key.startsWith("text_")) {
|
|
292
314
|
const childTree = tree.children[key];
|
|
293
315
|
const restoration = childTree.compiled?.restoration;
|
|
294
316
|
|
|
295
317
|
if (restoration?.parsed && Array.isArray(restoration.parsed)) {
|
|
296
|
-
const hasBindings = restoration.parsed.some(
|
|
297
|
-
typeof item ===
|
|
318
|
+
const hasBindings = restoration.parsed.some(
|
|
319
|
+
(item) => typeof item === "string" && item.includes("@["),
|
|
298
320
|
);
|
|
299
321
|
|
|
300
322
|
if (hasBindings) {
|
|
301
|
-
|
|
323
|
+
// Transform component-scoped bindings back to this. format
|
|
324
|
+
let originalContent = restoration.parsed.join("");
|
|
325
|
+
originalContent = originalContent.replace(
|
|
326
|
+
/@\[_c\d+\./g,
|
|
327
|
+
"@[this.",
|
|
328
|
+
);
|
|
302
329
|
|
|
303
330
|
// Extract index from key (e.g., text_0 -> 0)
|
|
304
331
|
const match = key.match(/_(\d+)$/);
|
|
@@ -329,7 +356,7 @@ export const restoreMarkersFromManifest = (rootElement, subtree, fullManifest =
|
|
|
329
356
|
const childTree = tree.children[key];
|
|
330
357
|
|
|
331
358
|
// Handle iteration restoration
|
|
332
|
-
if (childTree.type ===
|
|
359
|
+
if (childTree.type === "iteration" && childTree.compiled?.restoration) {
|
|
333
360
|
const restoration = childTree.compiled.restoration;
|
|
334
361
|
|
|
335
362
|
// Find iteration comment by matching the expression
|
|
@@ -337,7 +364,10 @@ export const restoreMarkersFromManifest = (rootElement, subtree, fullManifest =
|
|
|
337
364
|
let endComment = null;
|
|
338
365
|
let depth = 0;
|
|
339
366
|
|
|
340
|
-
const walker = document.createTreeWalker(
|
|
367
|
+
const walker = document.createTreeWalker(
|
|
368
|
+
element,
|
|
369
|
+
NodeFilter.SHOW_COMMENT,
|
|
370
|
+
);
|
|
341
371
|
while (walker.nextNode()) {
|
|
342
372
|
const comment = walker.currentNode;
|
|
343
373
|
const trimmed = comment.textContent.trim();
|
|
@@ -351,9 +381,9 @@ export const restoreMarkersFromManifest = (rootElement, subtree, fullManifest =
|
|
|
351
381
|
startComment = comment;
|
|
352
382
|
depth = 1;
|
|
353
383
|
} else if (startComment) {
|
|
354
|
-
if (trimmed.startsWith(
|
|
384
|
+
if (trimmed.startsWith("each ")) {
|
|
355
385
|
depth++;
|
|
356
|
-
} else if (trimmed ===
|
|
386
|
+
} else if (trimmed === "/each") {
|
|
357
387
|
depth--;
|
|
358
388
|
if (depth === 0) {
|
|
359
389
|
endComment = comment;
|
|
@@ -363,6 +393,10 @@ export const restoreMarkersFromManifest = (rootElement, subtree, fullManifest =
|
|
|
363
393
|
}
|
|
364
394
|
}
|
|
365
395
|
|
|
396
|
+
// Always delete the iteration node from tree - runtime will re-create it
|
|
397
|
+
// Do this even if comments weren't found (they might have been removed by parent restoration)
|
|
398
|
+
delete tree.children[key];
|
|
399
|
+
|
|
366
400
|
if (startComment && endComment) {
|
|
367
401
|
// Mark as processed
|
|
368
402
|
startComment._vibeProcessed = true;
|
|
@@ -382,7 +416,7 @@ export const restoreMarkersFromManifest = (rootElement, subtree, fullManifest =
|
|
|
382
416
|
}
|
|
383
417
|
|
|
384
418
|
// Insert the template (single item)
|
|
385
|
-
const tempContainer = document.createElement(
|
|
419
|
+
const tempContainer = document.createElement("div");
|
|
386
420
|
tempContainer.innerHTML = restoration.template;
|
|
387
421
|
|
|
388
422
|
const fragment = document.createDocumentFragment();
|
|
@@ -392,30 +426,35 @@ export const restoreMarkersFromManifest = (rootElement, subtree, fullManifest =
|
|
|
392
426
|
parent.insertBefore(fragment, endComment);
|
|
393
427
|
}
|
|
394
428
|
|
|
395
|
-
// Delete the iteration node from tree - runtime will re-create it
|
|
396
|
-
delete tree.children[key];
|
|
397
429
|
continue;
|
|
398
430
|
}
|
|
399
431
|
|
|
400
|
-
if (
|
|
432
|
+
if (
|
|
433
|
+
childTree.type === "conditional" &&
|
|
434
|
+
childTree.compiled?.restoration
|
|
435
|
+
) {
|
|
401
436
|
const restoration = childTree.compiled.restoration;
|
|
402
437
|
|
|
403
438
|
// Find conditional comment markers in the current element
|
|
404
439
|
// But SKIP conditionals that are inside iteration blocks
|
|
405
|
-
const walker = document.createTreeWalker(
|
|
440
|
+
const walker = document.createTreeWalker(
|
|
441
|
+
element,
|
|
442
|
+
NodeFilter.SHOW_COMMENT,
|
|
443
|
+
);
|
|
406
444
|
let startComment = null;
|
|
407
445
|
let endComment = null;
|
|
408
446
|
let insideIteration = false;
|
|
447
|
+
let conditionalDepth = 0;
|
|
409
448
|
|
|
410
449
|
while (walker.nextNode()) {
|
|
411
450
|
const comment = walker.currentNode;
|
|
412
451
|
const text = comment.textContent.trim();
|
|
413
452
|
|
|
414
453
|
// Track if we're inside an iteration block
|
|
415
|
-
if (text.startsWith(
|
|
454
|
+
if (text.startsWith("each ")) {
|
|
416
455
|
insideIteration = true;
|
|
417
456
|
continue;
|
|
418
|
-
} else if (text ===
|
|
457
|
+
} else if (text === "/each") {
|
|
419
458
|
insideIteration = false;
|
|
420
459
|
continue;
|
|
421
460
|
}
|
|
@@ -426,14 +465,31 @@ export const restoreMarkersFromManifest = (rootElement, subtree, fullManifest =
|
|
|
426
465
|
// Skip already processed conditionals
|
|
427
466
|
if (comment._vibeProcessed) continue;
|
|
428
467
|
|
|
429
|
-
if (text.startsWith(
|
|
430
|
-
startComment
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
468
|
+
if (text.startsWith("if")) {
|
|
469
|
+
if (!startComment) {
|
|
470
|
+
const expression = childTree.meta?.expression;
|
|
471
|
+
const expectedText = expression ? 'if ' + expression : null;
|
|
472
|
+
if (expectedText && text === expectedText) {
|
|
473
|
+
startComment = comment;
|
|
474
|
+
conditionalDepth = 1;
|
|
475
|
+
}
|
|
476
|
+
} else {
|
|
477
|
+
// Track nested conditionals
|
|
478
|
+
conditionalDepth++;
|
|
479
|
+
}
|
|
480
|
+
} else if (text === "/if" && startComment) {
|
|
481
|
+
conditionalDepth--;
|
|
482
|
+
if (conditionalDepth === 0) {
|
|
483
|
+
endComment = comment;
|
|
484
|
+
break;
|
|
485
|
+
}
|
|
434
486
|
}
|
|
435
487
|
}
|
|
436
488
|
|
|
489
|
+
// Always delete the conditional node from tree - runtime will re-create it
|
|
490
|
+
// Do this even if comments weren't found (they might have been removed by parent restoration)
|
|
491
|
+
delete tree.children[key];
|
|
492
|
+
|
|
437
493
|
if (startComment && endComment) {
|
|
438
494
|
// Mark as processed
|
|
439
495
|
startComment._vibeProcessed = true;
|
|
@@ -444,12 +500,12 @@ export const restoreMarkersFromManifest = (rootElement, subtree, fullManifest =
|
|
|
444
500
|
let current = startComment.nextSibling;
|
|
445
501
|
while (current && current !== endComment) {
|
|
446
502
|
const next = current.nextSibling;
|
|
447
|
-
current.remove();
|
|
503
|
+
current.remove(); // Remove ALL nodes, including comment nodes
|
|
448
504
|
current = next;
|
|
449
505
|
}
|
|
450
506
|
|
|
451
507
|
// Insert the template content BEFORE endComment
|
|
452
|
-
const tempContainer = document.createElement(
|
|
508
|
+
const tempContainer = document.createElement("div");
|
|
453
509
|
tempContainer.innerHTML = restoration.template;
|
|
454
510
|
|
|
455
511
|
const fragment = document.createDocumentFragment();
|
|
@@ -458,10 +514,6 @@ export const restoreMarkersFromManifest = (rootElement, subtree, fullManifest =
|
|
|
458
514
|
}
|
|
459
515
|
endComment.parentNode.insertBefore(fragment, endComment);
|
|
460
516
|
}
|
|
461
|
-
|
|
462
|
-
// Delete restoration data AND remove the conditional node from tree
|
|
463
|
-
// Runtime will re-create it from the restored DOM
|
|
464
|
-
delete tree.children[key];
|
|
465
517
|
}
|
|
466
518
|
}
|
|
467
519
|
}
|
|
@@ -472,28 +524,33 @@ export const restoreMarkersFromManifest = (rootElement, subtree, fullManifest =
|
|
|
472
524
|
const childTree = tree.children[key];
|
|
473
525
|
|
|
474
526
|
// Skip conditionals and iterations (already processed/deleted above)
|
|
475
|
-
if (
|
|
527
|
+
if (
|
|
528
|
+
childTree.type === "conditional" ||
|
|
529
|
+
childTree.type === "iteration"
|
|
530
|
+
) {
|
|
476
531
|
continue;
|
|
477
532
|
}
|
|
478
533
|
|
|
479
534
|
// Skip text nodes - already handled before conditional/iteration processing
|
|
480
|
-
if (key.startsWith(
|
|
535
|
+
if (key.startsWith("text_")) {
|
|
481
536
|
continue;
|
|
482
537
|
}
|
|
483
538
|
|
|
484
539
|
// Find corresponding child element by tag name and index
|
|
485
540
|
// Keys like "layout_1" mean the node at childNodes index 1 (includes text nodes)
|
|
486
|
-
// NOT the second layout element
|
|
487
541
|
// Tag names can contain digits (h1, h2, etc.) so use [a-z0-9-]+
|
|
488
542
|
const match = key.match(/^([a-z0-9-]+)_(\d+)$/);
|
|
489
543
|
if (match) {
|
|
490
544
|
const tagName = match[1].toUpperCase();
|
|
491
545
|
const nodeIndex = parseInt(match[2], 10);
|
|
492
546
|
|
|
493
|
-
// Get the node at this childNodes index
|
|
494
547
|
const childNode = element.childNodes[nodeIndex];
|
|
495
548
|
|
|
496
|
-
if (
|
|
549
|
+
if (
|
|
550
|
+
childNode &&
|
|
551
|
+
childNode.nodeType === Node.ELEMENT_NODE &&
|
|
552
|
+
childNode.nodeName === tagName
|
|
553
|
+
) {
|
|
497
554
|
walkTree(childTree, childNode);
|
|
498
555
|
}
|
|
499
556
|
}
|