@ape-egg/vibe 1.8.0 → 1.9.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 +107 -0
- package/README.md +72 -2
- package/boot.js +5 -1
- package/compiler/native/vibe-compiler-darwin-arm64 +0 -0
- package/compiler/native/vibe-compiler-linux-x64 +0 -0
- package/compiler/src/parser/html.rs +15 -15
- package/index.js +15 -0
- package/llms.txt +151 -84
- package/package.json +3 -2
- package/runtime/_vibe-compiled-iteration-batch.js +18 -9
- package/runtime/affected.js +36 -16
- package/runtime/component.js +237 -86
- package/runtime/conditionals.js +12 -1
- package/runtime/constants.js +17 -3
- package/runtime/hydrate.js +27 -10
- package/runtime/index.js +91 -34
- package/runtime/iterate.js +323 -108
- package/runtime/manifest.js +2 -1
- package/runtime/parse.js +67 -80
- package/runtime/pre-compiled-manifest.js +38 -13
- package/runtime/reconcile.js +621 -0
- package/runtime/state.js +5 -2
- package/runtime/utils.js +77 -2
- package/vibe.css +3 -1
package/runtime/iterate.js
CHANGED
|
@@ -2,17 +2,178 @@ import parse from './parse.js';
|
|
|
2
2
|
import affected from './affected.js';
|
|
3
3
|
import hydrate from './hydrate.js';
|
|
4
4
|
import { resolvePath, getItemKey, computeDiff } from './iteration-utils.js';
|
|
5
|
-
import { resolveThisPath } from './utils.js';
|
|
5
|
+
import { resolveThisPath, evalInScope } from './utils.js';
|
|
6
6
|
import { managedNodes } from './conditionals.js';
|
|
7
|
-
|
|
8
|
-
// Fast path for iteration rendering (opt-in via window.__VIBE_FAST_ITERATION__)
|
|
9
|
-
// See: _vibe-compiled-iteration-batch.js for implementation details
|
|
10
|
-
// This is a preview of what Vibe Compiled (Phase 2) will do automatically
|
|
11
|
-
import * as fastPath from './_vibe-compiled-iteration-batch.js';
|
|
7
|
+
import { BINDING_REGEX } from './constants.js';
|
|
12
8
|
|
|
13
9
|
// Pre-compiled iteration optimization (production)
|
|
14
10
|
import * as compiled from './pre-compiled-iterations.js';
|
|
15
11
|
|
|
12
|
+
// Runtime batch-render helpers for full-replacement of simple templates.
|
|
13
|
+
// Build an HTML string via template-literal compilation, then parse once —
|
|
14
|
+
// avoids per-item clone/parse/hydrate in the hot path.
|
|
15
|
+
// Only used for templates without nested <!-- each --> / <!-- if -->.
|
|
16
|
+
const batchParseTemplate = document.createElement('template');
|
|
17
|
+
|
|
18
|
+
// innerHTML serialization encodes <, >, &, ", ' inside attribute values.
|
|
19
|
+
// Decode them back before wrapping @[expr] in ${...} for the template literal.
|
|
20
|
+
const decodeEntities = (s) => s
|
|
21
|
+
.replace(/</g, '<')
|
|
22
|
+
.replace(/>/g, '>')
|
|
23
|
+
.replace(/"/g, '"')
|
|
24
|
+
.replace(/'/g, "'")
|
|
25
|
+
.replace(/&/g, '&');
|
|
26
|
+
|
|
27
|
+
const hasNestedStructures = (tree) => {
|
|
28
|
+
if (!tree || !tree.children) return false;
|
|
29
|
+
for (const key in tree.children) {
|
|
30
|
+
const child = tree.children[key];
|
|
31
|
+
if (!child) continue;
|
|
32
|
+
if (child.type === 'iteration' || child.type === 'conditional') return true;
|
|
33
|
+
if (hasNestedStructures(child)) return true;
|
|
34
|
+
}
|
|
35
|
+
return false;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// Batch render template-literal-interpolates `@[expr]` as `${expr}` — that
|
|
39
|
+
// stringifies object/array values, which breaks `<component src>` props that
|
|
40
|
+
// rely on resolveIterationComponentProps to stash non-primitives in the
|
|
41
|
+
// registry. Templates carrying any `<component src>` go through the
|
|
42
|
+
// clone+hydrate path instead.
|
|
43
|
+
const hasComponentSrc = (templateEl) =>
|
|
44
|
+
!!templateEl.querySelector?.('component[src], div.component[src]');
|
|
45
|
+
|
|
46
|
+
const canUseBatchRender = (template) =>
|
|
47
|
+
!hasNestedStructures(template) &&
|
|
48
|
+
template.element.children.length <= 1 &&
|
|
49
|
+
!hasComponentSrc(template.element);
|
|
50
|
+
|
|
51
|
+
const compileBatchFn = (template, itemAlias, indexAlias, stateKeys) => {
|
|
52
|
+
const templateHtml = template.element.innerHTML.trim();
|
|
53
|
+
const escaped = templateHtml.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$\{/g, '\\${');
|
|
54
|
+
const code = escaped.replace(BINDING_REGEX, (_, expr) => '${' + decodeEntities(expr) + '}');
|
|
55
|
+
|
|
56
|
+
return new Function(
|
|
57
|
+
'arr',
|
|
58
|
+
...stateKeys,
|
|
59
|
+
`
|
|
60
|
+
let html = '';
|
|
61
|
+
const len = arr.length;
|
|
62
|
+
for (let ${indexAlias} = 0; ${indexAlias} < len; ${indexAlias}++) {
|
|
63
|
+
const ${itemAlias} = arr[${indexAlias}];
|
|
64
|
+
html += \`${code}\`;
|
|
65
|
+
}
|
|
66
|
+
return html;
|
|
67
|
+
`,
|
|
68
|
+
);
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// Registry for non-primitive iteration prop snapshots. Lives on `window` (not
|
|
72
|
+
// on `$`) so it doesn't pollute user-visible state enumeration, but is still
|
|
73
|
+
// reachable from binding expressions because `window` is in evalInScope's
|
|
74
|
+
// known-globals list. Each entry is freed when the owning component element
|
|
75
|
+
// is detached (see releaseOrphanedIterationProps).
|
|
76
|
+
let __vibeIterPropCounter = 0;
|
|
77
|
+
const ensureIterPropsRegistry = () => {
|
|
78
|
+
if (!window.__vibeIterProps) window.__vibeIterProps = {};
|
|
79
|
+
return window.__vibeIterProps;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// Walk a removed subtree and free any iteration-prop registry slots stashed
|
|
83
|
+
// on `<component>` elements inside it. Called from the mutation-observer
|
|
84
|
+
// cleanup path after DOM detachment.
|
|
85
|
+
export const releaseOrphanedIterationProps = (nodes) => {
|
|
86
|
+
if (!window.__vibeIterProps) return;
|
|
87
|
+
for (const node of nodes) {
|
|
88
|
+
if (node.nodeType !== 1) continue;
|
|
89
|
+
const free = (el) => {
|
|
90
|
+
const ids = el._vibeIterPropIds;
|
|
91
|
+
if (!ids) return;
|
|
92
|
+
for (const id of ids) delete window.__vibeIterProps[id];
|
|
93
|
+
el._vibeIterPropIds = null;
|
|
94
|
+
};
|
|
95
|
+
free(node);
|
|
96
|
+
node.querySelectorAll?.('[data-vibe-iter-prop]').forEach(free);
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// For <component src> elements inside an iteration instance, evaluate any
|
|
101
|
+
// `@[expr]` attribute bindings against the iteration's scoped state and replace
|
|
102
|
+
// them with the resolved value. Primitives stringify into the attribute as
|
|
103
|
+
// before. Non-primitives (objects, arrays) snapshot into the registry and the
|
|
104
|
+
// attribute becomes a binding into that slot — preserving live object/array
|
|
105
|
+
// access for the child component's template (`@[prop.x]`,
|
|
106
|
+
// `<!-- each prop as item -->`, etc.).
|
|
107
|
+
//
|
|
108
|
+
// Component[src] attributes intentionally bypass hydrate (parse.js) so they
|
|
109
|
+
// reach processComponent as bindings — but bindings that depend on
|
|
110
|
+
// iteration-local vars (item, index) can't resolve later when processComponent
|
|
111
|
+
// inlines the component, since by then iteration scope is gone. Only called
|
|
112
|
+
// from iteration code paths; conditionals don't need this because their branch
|
|
113
|
+
// content is registered in the global manifest and reacts to state updates.
|
|
114
|
+
const resolveIterationComponentProps = (nodes, scopedState) => {
|
|
115
|
+
for (let n = 0; n < nodes.length; n++) {
|
|
116
|
+
const node = nodes[n];
|
|
117
|
+
if (node.nodeType !== 1) continue;
|
|
118
|
+
const components = node.matches?.('component[src], div.component[src]')
|
|
119
|
+
? [node, ...node.querySelectorAll('component[src], div.component[src]')]
|
|
120
|
+
: [...node.querySelectorAll('component[src], div.component[src]')];
|
|
121
|
+
for (let i = 0; i < components.length; i++) {
|
|
122
|
+
const el = components[i];
|
|
123
|
+
const attrs = el.attributes;
|
|
124
|
+
for (let j = 0; j < attrs.length; j++) {
|
|
125
|
+
const attr = attrs[j];
|
|
126
|
+
if (attr.name === 'src') continue;
|
|
127
|
+
const match = attr.value.match(/^@\[(.+)\]$/);
|
|
128
|
+
if (!match) continue;
|
|
129
|
+
try {
|
|
130
|
+
const value = evalInScope(match[1], scopedState, el);
|
|
131
|
+
if (value === undefined) continue;
|
|
132
|
+
if (value === null || (typeof value !== 'object' && typeof value !== 'function')) {
|
|
133
|
+
el.setAttribute(attr.name, String(value));
|
|
134
|
+
} else {
|
|
135
|
+
const registry = ensureIterPropsRegistry();
|
|
136
|
+
const id = `_p${__vibeIterPropCounter++}`;
|
|
137
|
+
registry[id] = value;
|
|
138
|
+
el.setAttribute(attr.name, `@[window.__vibeIterProps.${id}]`);
|
|
139
|
+
el.setAttribute('data-vibe-iter-prop', '');
|
|
140
|
+
(el._vibeIterPropIds = el._vibeIterPropIds || []).push(id);
|
|
141
|
+
}
|
|
142
|
+
} catch {
|
|
143
|
+
// Leave binding raw — processComponent will handle it as a binding
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const renderBatch = (iterationNode, array, state, parent, endComment) => {
|
|
151
|
+
const { itemAlias, indexAlias, template } = iterationNode.meta;
|
|
152
|
+
|
|
153
|
+
if (!iterationNode.runtime.batchFn) {
|
|
154
|
+
const stateKeys = Object.keys(state);
|
|
155
|
+
iterationNode.runtime.batchFn = compileBatchFn(template, itemAlias, indexAlias, stateKeys);
|
|
156
|
+
iterationNode.runtime.stateKeys = stateKeys;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const { batchFn, stateKeys } = iterationNode.runtime;
|
|
160
|
+
const stateValues = stateKeys.map((k) => state[k]);
|
|
161
|
+
const html = batchFn(array, ...stateValues);
|
|
162
|
+
|
|
163
|
+
batchParseTemplate.innerHTML = html;
|
|
164
|
+
const frag = batchParseTemplate.content;
|
|
165
|
+
const kids = frag.children;
|
|
166
|
+
|
|
167
|
+
const arrayLen = array.length;
|
|
168
|
+
const instances = new Array(arrayLen);
|
|
169
|
+
for (let i = 0; i < arrayLen; i++) {
|
|
170
|
+
instances[i] = { element: kids[i], item: array[i], index: i };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
parent.insertBefore(frag, endComment);
|
|
174
|
+
iterationNode.runtime.instances = instances;
|
|
175
|
+
};
|
|
176
|
+
|
|
16
177
|
/**
|
|
17
178
|
* Find a comment node with matching text content in the given nodes.
|
|
18
179
|
*/
|
|
@@ -159,6 +320,18 @@ export const initializeBlock = (templateNodes, scopedState, cachedTree = null) =
|
|
|
159
320
|
}
|
|
160
321
|
}
|
|
161
322
|
|
|
323
|
+
// Preserve raw slot content of nested <component src> elements before renderAllConditionals /
|
|
324
|
+
// renderAllIterations runs on this branch/iteration instance — those paths strip <!-- if -->
|
|
325
|
+
// and <!-- each --> templates from the live DOM, so by the time processComponent reads
|
|
326
|
+
// el.innerHTML (next microtask, when MutationObserver fires) the inactive branch templates
|
|
327
|
+
// would be gone. cloneNode(true) does not copy expando JS properties, so we must (re)capture
|
|
328
|
+
// _vibeSlotContent on every clone.
|
|
329
|
+
const components = parseContainer.querySelectorAll('component[src], div.component[src]');
|
|
330
|
+
for (let i = 0; i < components.length; i++) {
|
|
331
|
+
const el = components[i];
|
|
332
|
+
if (el._vibeSlotContent === undefined) el._vibeSlotContent = el.innerHTML;
|
|
333
|
+
}
|
|
334
|
+
|
|
162
335
|
if (useCachedTree) {
|
|
163
336
|
// Fast path: map cached tree structure onto cloned DOM (no regex, no DOM walking)
|
|
164
337
|
tree = cloneTreeWithElements(cachedTree, parseContainer);
|
|
@@ -313,10 +486,12 @@ export const renderIteration = (iterationNode, state, manifest, parentScope = {}
|
|
|
313
486
|
|
|
314
487
|
const parent = startComment.parentNode;
|
|
315
488
|
|
|
316
|
-
//
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
489
|
+
// Evaluate the array expression — supports state paths (items),
|
|
490
|
+
// window globals (window.fights), method calls (items.filter(...)),
|
|
491
|
+
// and inline literals (['a', 'b']). Falls back to resolvePath for
|
|
492
|
+
// simple paths that evalInScope might miss in scoped contexts.
|
|
493
|
+
const resolvedExpr = resolveThisPath(arrayPath, startComment.parentElement);
|
|
494
|
+
const array = evalInScope(resolvedExpr, state, startComment.parentElement) ?? resolvePath(state, resolvedExpr);
|
|
320
495
|
if (!Array.isArray(array) || array.length === 0) {
|
|
321
496
|
iterationNode.runtime.instances = [];
|
|
322
497
|
return;
|
|
@@ -347,6 +522,9 @@ export const renderIteration = (iterationNode, state, manifest, parentScope = {}
|
|
|
347
522
|
// Clone, parse, hydrate
|
|
348
523
|
const { element, tree, clonedNodes } = initializeBlock(templateNodes, scopedState, template);
|
|
349
524
|
|
|
525
|
+
// Pre-resolve <component src> binding props against iteration scope (see helper comment)
|
|
526
|
+
resolveIterationComponentProps(clonedNodes, scopedState);
|
|
527
|
+
|
|
350
528
|
// Collect nodes in DocumentFragment (single DOM insertion at end)
|
|
351
529
|
for (let j = 0; j < clonedNodes.length; j++) {
|
|
352
530
|
frag.appendChild(clonedNodes[j]);
|
|
@@ -381,11 +559,10 @@ export const updateIteration = (iterationNode, newState, oldState, manifest, par
|
|
|
381
559
|
|
|
382
560
|
const { arrayPath, template, startComment, endComment } = iterationNode.meta;
|
|
383
561
|
|
|
384
|
-
|
|
385
|
-
const resolvedArrayPath = resolveThisPath(arrayPath, startComment.parentElement);
|
|
562
|
+
const resolvedExpr = resolveThisPath(arrayPath, startComment.parentElement);
|
|
386
563
|
|
|
387
|
-
const stateOldArray = resolvePath(oldState,
|
|
388
|
-
const newArray = resolvePath(newState,
|
|
564
|
+
const stateOldArray = evalInScope(resolvedExpr, oldState, startComment.parentElement) ?? resolvePath(oldState, resolvedExpr) ?? [];
|
|
565
|
+
const newArray = evalInScope(resolvedExpr, newState, startComment.parentElement) ?? resolvePath(newState, resolvedExpr) ?? [];
|
|
389
566
|
|
|
390
567
|
// Use instances (what's actually rendered) as ground truth for old array
|
|
391
568
|
// when oldState disagrees with the rendered count.
|
|
@@ -457,7 +634,7 @@ export const updateIteration = (iterationNode, newState, oldState, manifest, par
|
|
|
457
634
|
// Bulk replacement: clear all DOM and re-render from scratch
|
|
458
635
|
// Used when arrays share no common keys (avoids O(n²) LCS)
|
|
459
636
|
const bulkReplace = (iterationNode, newArray, state, manifest, parentScope) => {
|
|
460
|
-
const {
|
|
637
|
+
const { template, startComment, endComment } = iterationNode.meta;
|
|
461
638
|
const parent = startComment.parentNode;
|
|
462
639
|
|
|
463
640
|
// Clear all existing DOM between comments using Range (single operation)
|
|
@@ -475,8 +652,8 @@ const bulkReplace = (iterationNode, newArray, state, manifest, parentScope) => {
|
|
|
475
652
|
|
|
476
653
|
// For simple templates (no nested iterations/conditionals, single root element),
|
|
477
654
|
// use batch string rendering: one string concatenation loop + one innerHTML parse
|
|
478
|
-
if (
|
|
479
|
-
|
|
655
|
+
if (canUseBatchRender(template)) {
|
|
656
|
+
renderBatch(iterationNode, newArray, state, parent, endComment);
|
|
480
657
|
const instances = iterationNode.runtime.instances;
|
|
481
658
|
for (let i = 0; i < instances.length; i++) {
|
|
482
659
|
if (instances[i].element?.nodeType === 1) managedNodes.add(instances[i].element);
|
|
@@ -484,135 +661,173 @@ const bulkReplace = (iterationNode, newArray, state, manifest, parentScope) => {
|
|
|
484
661
|
return;
|
|
485
662
|
}
|
|
486
663
|
|
|
487
|
-
// Complex templates:
|
|
664
|
+
// Complex templates: build each instance, batch-append into a DocumentFragment,
|
|
665
|
+
// finalize (mark managed + render nested), then commit to the DOM in one
|
|
666
|
+
// parent.insertBefore call.
|
|
488
667
|
const instances = [];
|
|
489
|
-
const templateNodes = template.element.childNodes;
|
|
490
668
|
const frag = document.createDocumentFragment();
|
|
491
|
-
|
|
492
669
|
for (let i = 0; i < newArray.length; i++) {
|
|
493
|
-
const
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
if (clonedNodes[j].nodeType === 1) managedNodes.add(clonedNodes[j]);
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
if (tree) {
|
|
504
|
-
const nestedScope = { ...parentScope, ...localVars };
|
|
505
|
-
renderAllIterations(tree, scopedState, manifest, nestedScope);
|
|
506
|
-
_renderAllConditionals(tree, scopedState, manifest, nestedScope);
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
instances.push({ element, tree, item, index: i, clonedNodes, scopedState });
|
|
670
|
+
const built = buildInstance(iterationNode, newArray[i], i, state, parentScope);
|
|
671
|
+
for (let j = 0; j < built.clonedNodes.length; j++) frag.appendChild(built.clonedNodes[j]);
|
|
672
|
+
finalizeInstance(built, manifest, parentScope);
|
|
673
|
+
instances.push({
|
|
674
|
+
element: built.element, tree: built.tree, item: newArray[i], index: i,
|
|
675
|
+
clonedNodes: built.clonedNodes, scopedState: built.scopedState,
|
|
676
|
+
});
|
|
510
677
|
}
|
|
511
|
-
|
|
512
678
|
parent.insertBefore(frag, endComment);
|
|
513
679
|
iterationNode.runtime.instances = instances;
|
|
514
680
|
};
|
|
515
681
|
|
|
516
|
-
//
|
|
517
|
-
|
|
518
|
-
|
|
682
|
+
// Find an instance's canonical in-DOM anchor (the first of its cloned nodes
|
|
683
|
+
// that still lives directly under the iteration's parent). Nested primitives
|
|
684
|
+
// inside the iteration template — <!-- if -->, <!-- each -->, <component> —
|
|
685
|
+
// can move/replace cloned nodes between iteration renders (inactive branches
|
|
686
|
+
// get hoisted into template containers; component[src] wrappers get swapped
|
|
687
|
+
// for processed wrappers). Any of those mutations make `clonedNodes[0]` a
|
|
688
|
+
// stale reference to a node no longer under the iteration parent. Callers use
|
|
689
|
+
// this anchor instead of trusting `clonedNodes[0]` directly, so that
|
|
690
|
+
// insert-before / move operations always resolve against the iteration's real
|
|
691
|
+
// DOM slot.
|
|
692
|
+
const findInstanceAnchor = (instance, iterationParent) => {
|
|
693
|
+
const nodes = instance.clonedNodes || (instance.element ? [instance.element] : []);
|
|
694
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
695
|
+
if (nodes[i]?.parentNode === iterationParent) return nodes[i];
|
|
696
|
+
}
|
|
697
|
+
return null;
|
|
698
|
+
};
|
|
519
699
|
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
700
|
+
// Resolve the reference node for `parent.insertBefore(..., ref)` at a given
|
|
701
|
+
// logical iteration index. Walks later instances until it finds one with a
|
|
702
|
+
// live anchor under the iteration parent; falls back to `endComment` when no
|
|
703
|
+
// later instance has any node currently mounted in the iteration.
|
|
704
|
+
const resolveInsertBefore = (iterationNode, index, parent) => {
|
|
705
|
+
const { instances } = iterationNode.runtime;
|
|
706
|
+
for (let i = index; i < instances.length; i++) {
|
|
707
|
+
const anchor = findInstanceAnchor(instances[i], parent);
|
|
708
|
+
if (anchor) return anchor;
|
|
709
|
+
}
|
|
710
|
+
return iterationNode.meta.endComment;
|
|
711
|
+
};
|
|
523
712
|
|
|
524
|
-
|
|
713
|
+
// Detach every DOM node belonging to a logical instance, including content
|
|
714
|
+
// mounted by nested primitives (conditional branches, nested each rows,
|
|
715
|
+
// fetched component wrappers) that isn't tracked in `instance.clonedNodes`.
|
|
716
|
+
// Walks iteration-parent siblings from this instance's anchor up to the next
|
|
717
|
+
// instance's anchor / endComment, so anything in between — clones, mounted
|
|
718
|
+
// branches, swapped-in component wrappers — all gets detached. Also sweeps
|
|
719
|
+
// any clonedNodes that were hoisted out of the iteration parent (e.g. into a
|
|
720
|
+
// sibling conditional's template container).
|
|
721
|
+
const detachInstanceDom = (iterationNode, index, parent) => {
|
|
722
|
+
const instance = iterationNode.runtime.instances[index];
|
|
723
|
+
const { endComment } = iterationNode.meta;
|
|
724
|
+
const anchor = findInstanceAnchor(instance, parent);
|
|
725
|
+
const nextAnchor = resolveInsertBefore(iterationNode, index + 1, parent);
|
|
726
|
+
|
|
727
|
+
if (anchor) {
|
|
728
|
+
let cur = anchor;
|
|
729
|
+
// endComment caps the walk even if nextAnchor ordering is ever
|
|
730
|
+
// corrupted — iteration DOM is bounded by startComment / endComment.
|
|
731
|
+
while (cur && cur !== nextAnchor && cur !== endComment) {
|
|
732
|
+
const nextSibling = cur.nextSibling;
|
|
733
|
+
parent.removeChild(cur);
|
|
734
|
+
cur = nextSibling;
|
|
735
|
+
}
|
|
736
|
+
}
|
|
525
737
|
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
738
|
+
const nodes = instance.clonedNodes || (instance.element ? [instance.element] : []);
|
|
739
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
740
|
+
const n = nodes[i];
|
|
741
|
+
if (n && n.parentNode && n.parentNode !== parent) n.parentNode.removeChild(n);
|
|
742
|
+
}
|
|
743
|
+
};
|
|
532
744
|
|
|
533
|
-
|
|
534
|
-
|
|
745
|
+
// Build a fresh instance's DOM + tree + scope from the iteration template.
|
|
746
|
+
// Pure function — no DOM insertion, no side effects on iteration state.
|
|
747
|
+
// Callers decide where the clones go (iteration parent, DocumentFragment).
|
|
748
|
+
const buildInstance = (iterationNode, item, index, state, parentScope) => {
|
|
749
|
+
const { itemAlias, indexAlias, template } = iterationNode.meta;
|
|
750
|
+
const localVars = { [itemAlias]: item, [indexAlias]: index };
|
|
751
|
+
const scopedState = createScopedState(state, localVars, parentScope);
|
|
752
|
+
const built = initializeBlock([...template.element.childNodes], scopedState, template);
|
|
753
|
+
resolveIterationComponentProps(built.clonedNodes, scopedState);
|
|
754
|
+
return { ...built, scopedState, localVars };
|
|
755
|
+
};
|
|
535
756
|
|
|
536
|
-
|
|
757
|
+
// After a built instance's clones are placed in the DOM (directly or via a
|
|
758
|
+
// fragment), mark element clones as managed so the page-level MutationObserver
|
|
759
|
+
// skips them in processMutations, then fire nested iteration/conditional
|
|
760
|
+
// renders. Without the managed mark those clones would be re-parsed + hydrated
|
|
761
|
+
// on top of the internal render, duplicating every nested branch.
|
|
762
|
+
const finalizeInstance = (built, manifest, parentScope) => {
|
|
763
|
+
const { clonedNodes, tree, scopedState, localVars } = built;
|
|
764
|
+
for (let i = 0; i < clonedNodes.length; i++) {
|
|
765
|
+
if (clonedNodes[i].nodeType === 1) managedNodes.add(clonedNodes[i]);
|
|
766
|
+
}
|
|
537
767
|
if (tree) {
|
|
538
768
|
const nestedScope = { ...parentScope, ...localVars };
|
|
539
769
|
renderAllIterations(tree, scopedState, manifest, nestedScope);
|
|
540
770
|
_renderAllConditionals(tree, scopedState, manifest, nestedScope);
|
|
541
771
|
}
|
|
772
|
+
};
|
|
542
773
|
|
|
543
|
-
|
|
774
|
+
const addInstance = (iterationNode, item, index, state, manifest, parentScope) => {
|
|
775
|
+
const parent = iterationNode.meta.startComment.parentNode;
|
|
776
|
+
const built = buildInstance(iterationNode, item, index, state, parentScope);
|
|
777
|
+
const insertBefore = resolveInsertBefore(iterationNode, index, parent);
|
|
778
|
+
for (let i = 0; i < built.clonedNodes.length; i++) parent.insertBefore(built.clonedNodes[i], insertBefore);
|
|
779
|
+
finalizeInstance(built, manifest, parentScope);
|
|
780
|
+
iterationNode.runtime.instances.splice(index, 0, {
|
|
781
|
+
element: built.element, tree: built.tree, item, index, clonedNodes: built.clonedNodes,
|
|
782
|
+
});
|
|
544
783
|
};
|
|
545
784
|
|
|
546
|
-
// Remove an instance at the specified index
|
|
547
785
|
const removeInstance = (iterationNode, index) => {
|
|
548
786
|
if (index < 0 || index >= iterationNode.runtime.instances.length) return;
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
// Remove all cloned nodes from DOM
|
|
553
|
-
(instance.clonedNodes || [instance.element]).forEach((node) =>
|
|
554
|
-
node?.parentNode?.removeChild(node),
|
|
555
|
-
);
|
|
556
|
-
|
|
787
|
+
const parent = iterationNode.meta.startComment.parentNode;
|
|
788
|
+
if (parent) detachInstanceDom(iterationNode, index, parent);
|
|
557
789
|
iterationNode.runtime.instances.splice(index, 1);
|
|
558
790
|
};
|
|
559
791
|
|
|
560
|
-
// Move an instance from one position to another
|
|
561
792
|
const moveInstance = (iterationNode, fromIndex, toIndex) => {
|
|
562
793
|
if (fromIndex === toIndex) return;
|
|
563
|
-
|
|
564
|
-
if (
|
|
794
|
+
const { instances } = iterationNode.runtime;
|
|
795
|
+
if (fromIndex < 0 || fromIndex >= instances.length) return;
|
|
796
|
+
if (toIndex < 0 || toIndex >= instances.length) return;
|
|
797
|
+
const parent = iterationNode.meta.startComment.parentNode;
|
|
565
798
|
|
|
566
|
-
const instance =
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
iterationNode.runtime.instances.splice(fromIndex, 1);
|
|
571
|
-
iterationNode.runtime.instances.splice(toIndex, 0, instance);
|
|
572
|
-
|
|
573
|
-
// Find new insertion point
|
|
574
|
-
const nextInstance = iterationNode.runtime.instances[toIndex + 1];
|
|
575
|
-
const insertBefore = nextInstance
|
|
576
|
-
? nextInstance.clonedNodes?.[0] || nextInstance.element
|
|
577
|
-
: iterationNode.meta.endComment;
|
|
799
|
+
const instance = instances[fromIndex];
|
|
800
|
+
instances.splice(fromIndex, 1);
|
|
801
|
+
instances.splice(toIndex, 0, instance);
|
|
578
802
|
|
|
579
|
-
|
|
580
|
-
nodes.
|
|
803
|
+
const insertBefore = resolveInsertBefore(iterationNode, toIndex + 1, parent);
|
|
804
|
+
const nodes = instance.clonedNodes || [instance.element];
|
|
805
|
+
// Re-insert only nodes currently under the iteration parent — those hoisted
|
|
806
|
+
// into nested-conditional template containers stay there so we don't
|
|
807
|
+
// double-count branch content.
|
|
808
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
809
|
+
const n = nodes[i];
|
|
810
|
+
if (n?.parentNode === parent) parent.insertBefore(n, insertBefore);
|
|
811
|
+
}
|
|
581
812
|
};
|
|
582
813
|
|
|
583
|
-
// Update an instance with new item data
|
|
584
814
|
const updateInstance = (iterationNode, index, newItem, state, manifest, parentScope = {}) => {
|
|
585
815
|
if (index < 0 || index >= iterationNode.runtime.instances.length) return;
|
|
586
|
-
|
|
587
|
-
const { itemAlias, indexAlias, template } = iterationNode.meta;
|
|
816
|
+
const parent = iterationNode.meta.startComment.parentNode;
|
|
588
817
|
const instance = iterationNode.runtime.instances[index];
|
|
589
818
|
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
const
|
|
593
|
-
|
|
594
|
-
const
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
const oldNodes = instance.clonedNodes || [instance.element];
|
|
598
|
-
const insertBefore = oldNodes[oldNodes.length - 1]?.nextSibling;
|
|
599
|
-
const parent = oldNodes[0]?.parentNode;
|
|
600
|
-
|
|
601
|
-
oldNodes.forEach((node) => node?.parentNode?.removeChild(node));
|
|
602
|
-
clonedNodes.forEach((node) => parent.insertBefore(node, insertBefore));
|
|
603
|
-
|
|
604
|
-
// Recursively render nested iterations and conditionals
|
|
605
|
-
if (tree) {
|
|
606
|
-
const nestedScope = { ...parentScope, ...localVars };
|
|
607
|
-
renderAllIterations(tree, scopedState, manifest, nestedScope);
|
|
608
|
-
_renderAllConditionals(tree, scopedState, manifest, nestedScope);
|
|
609
|
-
}
|
|
819
|
+
// Build fresh first, then detach old — keeps the old DOM as a stable
|
|
820
|
+
// anchor reference until we know how the new nodes are shaped.
|
|
821
|
+
const built = buildInstance(iterationNode, newItem, index, state, parentScope);
|
|
822
|
+
detachInstanceDom(iterationNode, index, parent);
|
|
823
|
+
const insertBefore = resolveInsertBefore(iterationNode, index + 1, parent);
|
|
824
|
+
for (let i = 0; i < built.clonedNodes.length; i++) parent.insertBefore(built.clonedNodes[i], insertBefore);
|
|
825
|
+
finalizeInstance(built, manifest, parentScope);
|
|
610
826
|
|
|
611
|
-
|
|
612
|
-
instance.
|
|
613
|
-
instance.tree = tree;
|
|
827
|
+
instance.element = built.element;
|
|
828
|
+
instance.tree = built.tree;
|
|
614
829
|
instance.item = newItem;
|
|
615
|
-
instance.clonedNodes = clonedNodes;
|
|
830
|
+
instance.clonedNodes = built.clonedNodes;
|
|
616
831
|
};
|
|
617
832
|
|
|
618
833
|
export default {
|
package/runtime/manifest.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const recursive = (tree, results, tagChain) => {
|
|
2
|
-
|
|
2
|
+
const path = tagChain.join('.');
|
|
3
|
+
results[path] = tree.element;
|
|
3
4
|
|
|
4
5
|
if (tree.children && Object.keys(tree.children).length > 0) {
|
|
5
6
|
Object.keys(tree.children).forEach((tag) => {
|