@ape-egg/vibe 4.2.1 → 4.3.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.
@@ -1,4 +1,29 @@
1
+ import parse, { rewriteHandler } from './parse.js';
2
+ import hydrate from './hydrate.js';
3
+ import affected, { ownBindingsOf } from './affected.js';
4
+ import { findNodeByElement, addToManifest, manifestPathOf } from './manifest.js';
5
+ import {
6
+ managedNodes,
7
+ branchNodeRegistry,
8
+ dispatchConditional,
9
+ remountBranch,
10
+ renderAllConditionals,
11
+ } from './conditionals.js';
12
+ import {
13
+ renderAllIterations,
14
+ createScopedState,
15
+ swapIterationTemplate,
16
+ rerenderIteration,
17
+ patchTreelessInstances,
18
+ instanceRange,
19
+ prepareTemplateComponents,
20
+ } from './iterate.js';
21
+ import { forceRemount, remountComponent, renderComponentTemplate } from './component.js';
22
+ import { eachComponentIn } from './component-registry.js';
1
23
  import { parkFetchableSrc } from './iteration-utils.js';
24
+ import { stampInstanceScopes } from './loop-scope.js';
25
+ import { isInert } from './inert.js';
26
+ import { findComponentIdForElement } from './utils.js';
2
27
  import {
3
28
  ITERATION_START_REGEX,
4
29
  CONDITIONAL_START_REGEX,
@@ -14,6 +39,7 @@ const isIterationStart = (n) =>
14
39
  n.nodeType === COMMENT && ITERATION_START_REGEX.test(n.textContent.trim());
15
40
  const isConditionalStart = (n) =>
16
41
  n.nodeType === COMMENT && CONDITIONAL_START_REGEX.test(n.textContent.trim());
42
+ const isRegionStart = (n) => isIterationStart(n) || isConditionalStart(n);
17
43
  const isRegionEnd = (n) => {
18
44
  if (n.nodeType !== COMMENT) return false;
19
45
  const t = n.textContent.trim();
@@ -22,10 +48,12 @@ const isRegionEnd = (n) => {
22
48
  const isComponentWrapper = (n) =>
23
49
  n.nodeType === ELEMENT &&
24
50
  (n.tagName === 'COMPONENT' || (n.tagName === 'DIV' && n.classList?.contains('component')));
25
-
26
51
  const isSlotElement = (n) =>
27
52
  n.nodeType === ELEMENT &&
28
53
  (n.tagName === 'SLOT' || (n.tagName === 'DIV' && n.classList?.contains('slot')));
54
+ const isScript = (n) => n.nodeType === ELEMENT && n.tagName === 'SCRIPT';
55
+
56
+ const hasBinding = (s) => /@\[.+?\]/.test(s);
29
57
 
30
58
  const nodesMatch = (a, b) => {
31
59
  if (!a || !b) return false;
@@ -49,6 +77,19 @@ const findRealignment = (liveNodes, li, sourceNodes, si) => {
49
77
  return null;
50
78
  };
51
79
 
80
+ const findRegionEnd = (nodes, startIdx) => {
81
+ let depth = 1;
82
+ for (let i = startIdx + 1; i < nodes.length; i++) {
83
+ const n = nodes[i];
84
+ if (isRegionStart(n)) depth++;
85
+ else if (isRegionEnd(n)) {
86
+ depth--;
87
+ if (depth === 0) return i;
88
+ }
89
+ }
90
+ return nodes.length - 1;
91
+ };
92
+
52
93
  const trimWsEdges = (nodes) => {
53
94
  let start = 0;
54
95
  let end = nodes.length;
@@ -57,10 +98,6 @@ const trimWsEdges = (nodes) => {
57
98
  return nodes.slice(start, end);
58
99
  };
59
100
 
60
- const firstElementChildOf = (parent) => {
61
- for (const n of parent.children || []) return n;
62
- return null;
63
- };
64
101
  const firstElementOfNodes = (nodes) => {
65
102
  for (const n of nodes) if (n.nodeType === ELEMENT) return n;
66
103
  return null;
@@ -69,87 +106,15 @@ const findOwnSlot = (el, srcChildren) => {
69
106
  const srcFirst = firstElementOfNodes(srcChildren);
70
107
  if (!srcFirst) return null;
71
108
  for (const slot of el.querySelectorAll('slot')) {
72
- const liveFirst = firstElementChildOf(slot);
73
- if (liveFirst?.tagName === srcFirst.tagName) return slot;
109
+ if (slot.firstElementChild?.tagName === srcFirst.tagName) return slot;
74
110
  }
75
111
  return null;
76
112
  };
77
113
 
78
- const hasBinding = (s) => /@\[.+?\]/.test(s);
79
-
80
- const boundSourceCache = new WeakMap();
81
-
82
- const updateBoundText = (live, newSrc, parent, log) => {
83
- const oldSrc = boundSourceCache.get(live);
84
- boundSourceCache.set(live, newSrc);
85
- if (oldSrc === undefined || oldSrc === newSrc) return;
86
-
87
- const oldBindings = oldSrc.match(/@\[[^\]]+\]/g) || [];
88
- const newBindings = newSrc.match(/@\[[^\]]+\]/g) || [];
89
- if (oldBindings.length !== newBindings.length) return;
90
- for (let i = 0; i < oldBindings.length; i++) {
91
- if (oldBindings[i] !== newBindings[i]) return;
92
- }
93
-
94
- const oldStatic = oldSrc.split(/@\[[^\]]+\]/);
95
- const newStatic = newSrc.split(/@\[[^\]]+\]/);
96
-
97
- const liveText = live.textContent;
98
- const values = [];
99
- let pos = 0;
100
- for (let i = 0; i < oldStatic.length - 1; i++) {
101
- const before = oldStatic[i];
102
- const after = oldStatic[i + 1];
103
- if (liveText.slice(pos, pos + before.length) !== before) return;
104
- pos += before.length;
105
- let endPos;
106
- if (i === oldStatic.length - 2) {
107
- endPos = liveText.length - after.length;
108
- if (endPos < pos || liveText.slice(endPos) !== after) return;
109
- } else {
110
- endPos = liveText.indexOf(after, pos);
111
- if (endPos < 0) return;
112
- }
113
- values.push(liveText.slice(pos, endPos));
114
- pos = endPos;
115
- }
116
-
117
- let result = newStatic[0];
118
- for (let i = 0; i < values.length; i++) {
119
- result += values[i] + newStatic[i + 1];
120
- }
121
- if (result === liveText) return;
122
-
123
- live.textContent = result;
124
- log.text++;
125
- log.changes.push(`bound text in ${describe(parent)}: ${JSON.stringify(result.slice(0, 60))}`);
126
- };
127
-
128
- const findRegionEnd = (nodes, startIdx, endLimit) => {
129
- const limit = endLimit ?? nodes.length;
130
- let depth = 1;
131
- for (let i = startIdx + 1; i < limit; i++) {
132
- const n = nodes[i];
133
- if (isIterationStart(n) || isConditionalStart(n)) depth++;
134
- else if (isRegionEnd(n)) {
135
- depth--;
136
- if (depth === 0) return i;
137
- }
138
- }
139
- return limit - 1;
140
- };
141
-
142
- const findElseMarker = (nodes, startIdx, endIdx) => {
143
- let depth = 0;
144
- for (let i = startIdx; i < endIdx; i++) {
145
- const n = nodes[i];
146
- if (n.nodeType !== COMMENT) continue;
147
- const t = n.textContent.trim();
148
- if (t.startsWith('if ') || t.startsWith('each ')) depth++;
149
- else if (t === '/if' || t === '/each') depth--;
150
- else if (depth === 0 && t === 'else') return i;
151
- }
152
- return -1;
114
+ const rangeBetween = (from, to) => {
115
+ const nodes = [];
116
+ for (let cur = from; cur && cur !== to; cur = cur.nextSibling) nodes.push(cur);
117
+ return nodes;
153
118
  };
154
119
 
155
120
  const PRESERVED_ATTRS = new Set([FOUC_CLASS_OR_ATTR]);
@@ -171,150 +136,159 @@ const describe = (el) => {
171
136
  return el.tagName.toLowerCase() + id + cls;
172
137
  };
173
138
 
174
- const reconcileAttributes = (live, src, log) => {
175
- let hasNameBinding = false;
176
- for (const attr of src.attributes) {
177
- if (attr.name.includes('@[')) { hasNameBinding = true; continue; }
178
- if (hasBinding(attr.value)) continue;
179
- const next = attr.name === 'class' ? mergeClass(attr.value, live) : attr.value;
180
- if (live.getAttribute(attr.name) !== next) {
181
- live.setAttribute(attr.name, next);
182
- log.attr++;
183
- log.changes.push(`attr ${describe(live)} [${attr.name}=${JSON.stringify(next)}]`);
184
- }
185
- }
186
- if (hasNameBinding) return;
187
- for (const attr of [...live.attributes]) {
188
- if (src.hasAttribute(attr.name)) continue;
189
- if (isPreservedAttr(attr.name)) continue;
190
- if (src.hasAttribute('data-' + attr.name)) continue;
191
- if (attr.name === 'class') {
192
- const kept = [...live.classList].filter((t) => PRESERVED_CLASSES.has(t));
193
- if (kept.length) live.setAttribute('class', kept.join(' '));
194
- else live.removeAttribute('class');
195
- log.attr++;
196
- log.changes.push(`attr ${describe(live)} [class trimmed]`);
197
- continue;
198
- }
199
- live.removeAttribute(attr.name);
200
- log.attr++;
201
- log.changes.push(`attr ${describe(live)} [-${attr.name}]`);
139
+ const sameRecord = (a, b) => {
140
+ const ak = Object.keys(a ?? {});
141
+ const bk = Object.keys(b ?? {});
142
+ return ak.length === bk.length && ak.every((k) => a[k] === b?.[k]);
143
+ };
144
+ const sameList = (a, b) => {
145
+ const al = a ?? [];
146
+ const bl = b ?? [];
147
+ return al.length === bl.length && al.every((v, i) => v === bl[i]);
148
+ };
149
+
150
+ const childrenByNode = (tree) => {
151
+ const map = new Map();
152
+ if (!tree?.children) return map;
153
+ for (const key in tree.children) {
154
+ const child = tree.children[key];
155
+ if (!child || typeof child !== 'object') continue;
156
+ const node = child.meta?.startComment ?? child.textNode ?? child.element;
157
+ if (node && !map.has(node)) map.set(node, { key, tree: child });
202
158
  }
159
+ return map;
160
+ };
161
+
162
+ const nextKey = (tree, sample) => {
163
+ tree._nextChildIndex ??= Object.keys(tree.children).length;
164
+ return `${sample.slice(0, sample.lastIndexOf('_'))}_${tree._nextChildIndex++}`;
165
+ };
166
+
167
+ const attach = (parentTree, key, tree, parent, ctx) => {
168
+ const name = nextKey(parentTree, key);
169
+ if (tree.type === 'conditional') tree._key = name;
170
+ parentTree.children[name] = tree;
171
+ if (ctx.manifestBase === null) return;
172
+ const base = ctx.manifestBase ?? manifestPathOf(ctx.manifest, parent);
173
+ if (base !== null) addToManifest(tree, ctx.manifest, `${base}.${name}`);
203
174
  };
204
175
 
205
- const reconcileRange = (liveParent, liveNodes, liStart, liEnd, srcNodes, siStart, sEnd, log, insideIteration = false) => {
206
- let li = liStart;
207
- let si = siStart;
176
+ const rehydrate = (tree, ctx) => {
177
+ const entries = ownBindingsOf(tree, ctx.state);
178
+ if (entries.length) hydrate(entries, ctx.state, ctx.manifest, ctx.state);
179
+ };
180
+
181
+ const mountFresh = (tree, ctx) => {
182
+ const entries = affected(tree, ctx.state, ctx.state);
183
+ if (entries.length) hydrate(entries, ctx.state, ctx.manifest, ctx.state);
184
+ renderAllIterations(tree, ctx.state, ctx.manifest, ctx.parentScope);
185
+ renderAllConditionals(tree, ctx.state, ctx.manifest, ctx.parentScope);
186
+ };
187
+
188
+ const materialize = (srcNodes, parent, ctx) => {
189
+ const box = document.createElement('div');
190
+ box._vibeComponentId = findComponentIdForElement(parent);
191
+ for (const n of srcNodes) box.appendChild(n.cloneNode(true));
192
+ prepareTemplateComponents(box, ctx.state);
193
+ const trees = parse(box, undefined, ctx.aliases).children;
194
+ return { nodes: [...box.childNodes], trees };
195
+ };
208
196
 
209
- while (li < liEnd || si < sEnd) {
210
- const live = li < liEnd ? liveNodes[li] : null;
211
- const src = si < sEnd ? srcNodes[si] : null;
197
+ const adoptElement = (live, parent, oldTree, ctx) => {
198
+ if (!oldTree || isInert(live)) return null;
199
+ const tree = parse(live, undefined, ctx.aliases);
200
+ attach(oldTree, `${live.tagName.toLowerCase()}_0`, tree, parent, ctx);
201
+ return tree;
202
+ };
203
+
204
+ const reconcileList = (parent, liveNodes, srcNodes, oldTree, newTree, ctx, anchor = null) => {
205
+ const { log } = ctx;
206
+ const oldMap = childrenByNode(oldTree);
207
+ const newMap = childrenByNode(newTree);
208
+ const oldOf = (n) => oldMap.get(n)?.tree;
209
+ const newOf = (n) => newMap.get(n)?.tree;
210
+ let li = 0;
211
+ let si = 0;
212
+ let structural = 0;
213
+
214
+ const insert = (from, to, ref) => {
215
+ const { nodes, trees } = materialize(srcNodes.slice(from, to + 1).filter((n) => !isScript(n)), parent, ctx);
216
+ for (const n of nodes) {
217
+ if (oldTree && n.nodeType === ELEMENT) managedNodes.add(n);
218
+ parent.insertBefore(n, ref);
219
+ }
220
+ if (oldTree) {
221
+ for (const key in trees) {
222
+ attach(oldTree, key, trees[key], parent, ctx);
223
+ mountFresh(trees[key], ctx);
224
+ }
225
+ }
226
+ log.insert += nodes.length;
227
+ log.changes.push(`insert ${nodes.map(describe).join(', ')} into ${describe(parent)}`);
228
+ structural++;
229
+ };
230
+
231
+ const remove = (from, to) => {
232
+ for (let k = from; k <= to; k++) {
233
+ const n = liveNodes[k];
234
+ if (isScript(n)) continue;
235
+ const hit = oldMap.get(n);
236
+ if (hit && oldTree) delete oldTree.children[hit.key];
237
+ n.remove();
238
+ log.remove++;
239
+ log.changes.push(`remove ${describe(n)} from ${describe(parent)}`);
240
+ }
241
+ structural++;
242
+ };
243
+
244
+ while (li < liveNodes.length || si < srcNodes.length) {
245
+ const live = li < liveNodes.length ? liveNodes[li] : null;
246
+ const src = si < srcNodes.length ? srcNodes[si] : null;
212
247
 
213
248
  if (!live) {
214
- liveParent.appendChild(srcNodes[si].cloneNode(true));
215
- log.insert++;
216
- log.changes.push(`insert ${describe(srcNodes[si])} into ${describe(liveParent)}`);
217
- si++;
249
+ const end = isRegionStart(src) ? findRegionEnd(srcNodes, si) : si;
250
+ insert(si, end, anchor);
251
+ si = end + 1;
218
252
  continue;
219
253
  }
220
254
  if (!src) {
221
- log.remove++;
222
- log.changes.push(`remove ${describe(live)} from ${describe(liveParent)}`);
223
- live.remove();
224
- li++;
255
+ const end = isRegionStart(live) ? findRegionEnd(liveNodes, li) : li;
256
+ remove(li, end);
257
+ li = end + 1;
225
258
  continue;
226
259
  }
227
260
 
228
- const srcIsScript = src?.nodeType === ELEMENT && src.tagName === 'SCRIPT';
229
- const liveIsScript = live?.nodeType === ELEMENT && live.tagName === 'SCRIPT';
230
- if (srcIsScript && !liveIsScript) { si++; continue; }
231
- if (liveIsScript && !srcIsScript) { li++; continue; }
232
-
233
- const liveIsRegion = isIterationStart(live) || isConditionalStart(live);
234
- const srcIsRegion = isIterationStart(src) || isConditionalStart(src);
235
-
236
- if (liveIsRegion && srcIsRegion) {
237
- const liveEnd = findRegionEnd(liveNodes, li, liEnd);
238
- const srcEnd = findRegionEnd(srcNodes, si, sEnd);
239
- if (isConditionalStart(live) && isConditionalStart(src)) {
240
- reconcileConditionalRegion(liveParent, liveNodes, li, liveEnd, srcNodes, si, srcEnd, log, insideIteration);
241
- } else if (isIterationStart(live) && isIterationStart(src)) {
242
- reconcileIterationRegion(liveParent, liveNodes, li, liveEnd, srcNodes, si, srcEnd, log);
243
- }
261
+ if (isScript(src) && !isScript(live)) { si++; continue; }
262
+ if (isScript(live) && !isScript(src)) { li++; continue; }
263
+
264
+ const liveRegion = isRegionStart(live);
265
+ const srcRegion = isRegionStart(src);
266
+
267
+ if (liveRegion && srcRegion && isIterationStart(live) === isIterationStart(src)) {
268
+ const liveEnd = findRegionEnd(liveNodes, li);
269
+ const srcEnd = findRegionEnd(srcNodes, si);
270
+ const region = { parent, liveStart: live, srcStart: src, oldT: oldOf(live), newT: newOf(src) };
271
+ if (isIterationStart(live)) reconcileIteration(region, ctx);
272
+ else reconcileConditional(region, ctx);
244
273
  li = liveEnd + 1;
245
274
  si = srcEnd + 1;
246
275
  continue;
247
276
  }
248
- if (liveIsRegion) {
249
- const end = findRegionEnd(liveNodes, li, liEnd);
250
- for (let k = li; k <= end; k++) {
251
- log.remove++;
252
- liveNodes[k].remove();
253
- }
254
- log.changes.push(`remove vibe region from ${describe(liveParent)}`);
277
+ if (liveRegion) {
278
+ const end = findRegionEnd(liveNodes, li);
279
+ remove(li, end);
255
280
  li = end + 1;
256
281
  continue;
257
282
  }
258
- if (srcIsRegion) {
259
- const end = findRegionEnd(srcNodes, si, sEnd);
260
- for (let k = si; k <= end; k++) {
261
- liveParent.insertBefore(srcNodes[k].cloneNode(true), live);
262
- log.insert++;
263
- }
264
- log.changes.push(`insert vibe region into ${describe(liveParent)}`);
283
+ if (srcRegion) {
284
+ const end = findRegionEnd(srcNodes, si);
285
+ insert(si, end, live);
265
286
  si = end + 1;
266
287
  continue;
267
288
  }
268
289
 
269
290
  if (isComponentWrapper(live) && isComponentWrapper(src)) {
270
- if (insideIteration) {
271
- li++;
272
- si++;
273
- continue;
274
- }
275
- if (live._vibeProps && src.hasAttribute('src')) {
276
- const srcProps = {};
277
- for (const a of src.attributes) {
278
- if (a.name === FOUC_CLASS_OR_ATTR) continue;
279
- if (a.name === 'class') {
280
- const kept = a.value.split(/\s+/).filter((t) => t && t !== FOUC_CLASS_OR_ATTR);
281
- if (kept.length) srcProps.class = kept.join(' ');
282
- continue;
283
- }
284
- srcProps[a.name] = a.value;
285
- }
286
- const liveProps = live._vibeProps;
287
- const liveKeys = Object.keys(liveProps);
288
- const srcKeys = Object.keys(srcProps);
289
- const diff =
290
- liveKeys.length !== srcKeys.length ||
291
- liveKeys.some((k) => liveProps[k] !== srcProps[k]) ||
292
- srcKeys.some((k) => !(k in liveProps));
293
-
294
- if (diff) {
295
- const fresh = document.createElement(src.tagName);
296
- if (src.tagName === 'DIV') fresh.className = 'component';
297
- for (const [name, value] of Object.entries(srcProps)) {
298
- fresh.setAttribute(name, value);
299
- }
300
- fresh.setAttribute(FOUC_CLASS_OR_ATTR, '');
301
- const slotHtml = src.innerHTML.trim();
302
- fresh._vibeSlotContent = slotHtml;
303
- fresh._vibePluginSlot = slotHtml;
304
- live.replaceWith(fresh);
305
- log.replace++;
306
- log.changes.push(
307
- `re-mount ${describe(live)} (prop change) live=${JSON.stringify(liveProps)} src=${JSON.stringify(srcProps)}`,
308
- );
309
- li++;
310
- si++;
311
- continue;
312
- }
313
- }
314
-
315
- const srcChildren = trimWsEdges([...src.childNodes]);
316
- const liveSlot = findOwnSlot(live, srcChildren);
317
- if (liveSlot) reconcileChildren(liveSlot, srcChildren, log, insideIteration);
291
+ reconcileComponent(live, src, oldOf(live), newOf(src), ctx);
318
292
  li++;
319
293
  si++;
320
294
  continue;
@@ -327,14 +301,7 @@ const reconcileRange = (liveParent, liveNodes, liStart, liEnd, srcNodes, siStart
327
301
  }
328
302
 
329
303
  if (live.nodeType === TEXT && src.nodeType === TEXT) {
330
- if (hasBinding(src.textContent)) {
331
- updateBoundText(live, src.textContent, liveParent, log);
332
- } else if (live.textContent !== src.textContent) {
333
- live.textContent = src.textContent;
334
- log.text++;
335
- const preview = src.textContent.trim().slice(0, 60);
336
- log.changes.push(`text in ${describe(liveParent)}: ${JSON.stringify(preview)}`);
337
- }
304
+ reconcileText(parent, live, src, oldTree, oldOf(live), ctx);
338
305
  li++;
339
306
  si++;
340
307
  continue;
@@ -350,124 +317,356 @@ const reconcileRange = (liveParent, liveNodes, liStart, liEnd, srcNodes, siStart
350
317
  continue;
351
318
  }
352
319
 
353
- if (live.nodeType === ELEMENT && src.nodeType === ELEMENT &&
354
- live.tagName === src.tagName) {
355
- reconcileAttributes(live, src, log);
356
- reconcileChildren(live, [...src.childNodes], log, insideIteration);
320
+ if (live.nodeType === ELEMENT && src.nodeType === ELEMENT && live.tagName === src.tagName) {
321
+ reconcileElement(live, src, oldOf(live) ?? adoptElement(live, parent, oldTree, ctx), newOf(src), ctx);
357
322
  li++;
358
323
  si++;
359
324
  continue;
360
325
  }
361
326
 
362
327
  const align = findRealignment(liveNodes, li, srcNodes, si);
363
- if (align && li + align.dl < liEnd && si + align.ds < sEnd) {
364
- for (let i = 0; i < align.dl; i++) {
365
- const n = liveNodes[li + i];
366
- if (n.nodeType === ELEMENT && n.tagName === 'SCRIPT') continue;
367
- log.remove++;
368
- log.changes.push(`remove (extra) ${describe(n)} from ${describe(liveParent)}`);
369
- n.remove();
370
- }
371
- const anchor = liveNodes[li + align.dl];
372
- for (let i = 0; i < align.ds; i++) {
373
- const n = srcNodes[si + i];
374
- if (n.nodeType === ELEMENT && n.tagName === 'SCRIPT') continue;
375
- log.insert++;
376
- log.changes.push(`insert (new) ${describe(n)} into ${describe(liveParent)}`);
377
- liveParent.insertBefore(n.cloneNode(true), anchor);
378
- }
328
+ if (align && li + align.dl < liveNodes.length && si + align.ds < srcNodes.length) {
329
+ if (align.dl) remove(li, li + align.dl - 1);
330
+ if (align.ds) insert(si, si + align.ds - 1, liveNodes[li + align.dl]);
379
331
  li += align.dl;
380
332
  si += align.ds;
381
333
  continue;
382
334
  }
383
335
 
384
336
  log.replace++;
385
- log.changes.push(`replace ${describe(live)} → ${describe(src)} in ${describe(liveParent)}`);
386
- live.replaceWith(src.cloneNode(true));
337
+ remove(li, li);
338
+ insert(si, si, li + 1 < liveNodes.length ? liveNodes[li + 1] : anchor);
387
339
  li++;
388
340
  si++;
389
341
  }
390
- };
391
342
 
392
- const reconcileChildren = (liveParent, sourceNodes, log, insideIteration = false) => {
393
- const liveNodes = [...liveParent.childNodes];
394
- reconcileRange(liveParent, liveNodes, 0, liveNodes.length, sourceNodes, 0, sourceNodes.length, log, insideIteration);
343
+ return structural;
395
344
  };
396
345
 
397
- const reconcileConditionalRegion = (liveParent, liveNodes, liStart, liEnd, srcNodes, siStart, sEnd, log, insideIteration = false) => {
398
- const elseIdx = findElseMarker(srcNodes, siStart + 1, sEnd);
399
- const ifStart = siStart + 1;
400
- const ifEnd = elseIdx === -1 ? sEnd : elseIdx;
401
- const elseStart = elseIdx === -1 ? sEnd : elseIdx + 1;
402
- const elseEnd = sEnd;
346
+ const reconcileText = (parent, live, src, oldTree, oldT, ctx) => {
347
+ const { log } = ctx;
348
+ const template = src.textContent;
349
+ let tree = oldT;
350
+ if (!tree && oldTree) {
351
+ tree = { parsed: live.textContent, element: parent, textNode: live, children: {} };
352
+ attach(oldTree, 'text_0', tree, parent, ctx);
353
+ }
354
+ if (tree) {
355
+ if (tree.parsed !== template) {
356
+ tree.parsed = template;
357
+ rehydrate(tree, ctx);
358
+ log.text++;
359
+ log.changes.push(`text in ${describe(parent)}: ${JSON.stringify(template.trim().slice(0, 60))}`);
360
+ }
361
+ if (hasBinding(template)) return;
362
+ } else if (hasBinding(template)) {
363
+ return;
364
+ }
365
+ if (live.textContent !== src.textContent) {
366
+ live.textContent = src.textContent;
367
+ if (!tree) {
368
+ log.text++;
369
+ log.changes.push(`text in ${describe(parent)}: ${JSON.stringify(template.trim().slice(0, 60))}`);
370
+ }
371
+ }
372
+ };
403
373
 
404
- let liveFirstEl = null;
405
- for (let i = liStart + 1; i < liEnd; i++) {
406
- if (liveNodes[i].nodeType === ELEMENT) { liveFirstEl = liveNodes[i]; break; }
374
+ const reconcileAttributes = (live, src, oldT, newT, ctx) => {
375
+ const { log } = ctx;
376
+ let srcHasNameBinding = false;
377
+ for (const attr of src.attributes) {
378
+ if (attr.name.includes('@[')) { srcHasNameBinding = true; continue; }
379
+ if (attr.name.startsWith(DEFER_ATTR_PREFIX) || hasBinding(attr.value)) continue;
380
+ let next = attr.name === 'class' ? mergeClass(attr.value, live) : attr.value;
381
+ if (attr.name.startsWith('on')) next = rewriteHandler(next, ctx.detached ? null : live, ctx.aliases);
382
+ if (live.getAttribute(attr.name) !== next) {
383
+ live.setAttribute(attr.name, next);
384
+ log.attr++;
385
+ log.changes.push(`attr ${describe(live)} [${attr.name}=${JSON.stringify(next)}]`);
386
+ }
407
387
  }
408
- if (!liveFirstEl) return;
409
-
410
- let chosenStart = -1;
411
- let chosenEnd = -1;
412
- for (let i = ifStart; i < ifEnd; i++) {
413
- if (srcNodes[i].nodeType === ELEMENT) {
414
- if (srcNodes[i].tagName === liveFirstEl.tagName) {
415
- chosenStart = ifStart; chosenEnd = ifEnd;
388
+ if (!srcHasNameBinding) {
389
+ for (const attr of [...live.attributes]) {
390
+ if (src.hasAttribute(attr.name)) continue;
391
+ if (isPreservedAttr(attr.name)) continue;
392
+ if (src.hasAttribute('data-' + attr.name) || src.hasAttribute(DEFER_ATTR_PREFIX + attr.name)) continue;
393
+ if (newT?.attributes?.[attr.name] !== undefined) continue;
394
+ if (attr.name === 'class') {
395
+ const kept = [...live.classList].filter((t) => PRESERVED_CLASSES.has(t));
396
+ if (kept.length) live.setAttribute('class', kept.join(' '));
397
+ else live.removeAttribute('class');
398
+ log.attr++;
399
+ log.changes.push(`attr ${describe(live)} [class trimmed]`);
400
+ continue;
416
401
  }
417
- break;
402
+ live.removeAttribute(attr.name);
403
+ live._vibeBoundAttrs?.delete(attr.name);
404
+ log.attr++;
405
+ log.changes.push(`attr ${describe(live)} [-${attr.name}]`);
418
406
  }
419
407
  }
420
- if (chosenStart === -1) {
421
- for (let i = elseStart; i < elseEnd; i++) {
422
- if (srcNodes[i].nodeType === ELEMENT) {
423
- if (srcNodes[i].tagName === liveFirstEl.tagName) {
424
- chosenStart = elseStart; chosenEnd = elseEnd;
425
- }
426
- break;
427
- }
408
+
409
+ if (!oldT || !newT) return;
410
+ if (sameRecord(oldT.attributes, newT.attributes) && sameList(oldT.nameBindings, newT.nameBindings)) return;
411
+ for (const nb of oldT.nameBindings ?? []) {
412
+ if (newT.nameBindings?.includes(nb)) continue;
413
+ const produced = live._vibeNameBindings?.get(nb);
414
+ if (produced) {
415
+ live.removeAttribute(produced);
416
+ live._vibeNameBindings.delete(nb);
428
417
  }
429
418
  }
430
- if (chosenStart === -1) return;
419
+ if (newT.attributes) oldT.attributes = newT.attributes;
420
+ else delete oldT.attributes;
421
+ if (newT.nameBindings) oldT.nameBindings = newT.nameBindings;
422
+ else delete oldT.nameBindings;
423
+ rehydrate(oldT, ctx);
424
+ log.attr++;
425
+ log.changes.push(`bound attrs ${describe(live)} ${JSON.stringify(newT.attributes ?? {})}`);
426
+ };
431
427
 
432
- reconcileRange(
433
- liveParent, liveNodes,
434
- liStart + 1, liEnd,
435
- srcNodes, chosenStart, chosenEnd,
436
- log,
437
- insideIteration,
438
- );
428
+ const reconcileRawHost = (live, src, oldT, ctx) => {
429
+ const textTree = Object.values(oldT.children).find((c) => c.textNode);
430
+ const srcText = src.childNodes.length === 1 && src.firstChild.nodeType === TEXT ? src.firstChild : null;
431
+ if (textTree && srcText) {
432
+ if (textTree.parsed !== srcText.textContent) {
433
+ textTree.parsed = srcText.textContent;
434
+ rehydrate(textTree, ctx);
435
+ ctx.log.text++;
436
+ ctx.log.changes.push(`raw html in ${describe(live)}: ${JSON.stringify(srcText.textContent.trim().slice(0, 60))}`);
437
+ }
438
+ return true;
439
+ }
440
+ for (const key in oldT.children) delete oldT.children[key];
441
+ live.textContent = '';
442
+ live._vibeRawHtml = false;
443
+ live._vibeRawHtmlValue = undefined;
444
+ return false;
445
+ };
446
+
447
+ const reconcileElement = (live, src, oldT, newT, ctx) => {
448
+ reconcileAttributes(live, src, oldT, newT, ctx);
449
+ if (isScript(live)) return;
450
+ if (live._vibeRawHtml && oldT && reconcileRawHost(live, src, oldT, ctx)) return;
451
+ reconcileList(live, [...live.childNodes], [...src.childNodes], oldT, newT, ctx);
452
+ };
453
+
454
+ const propsOf = (src) => {
455
+ const props = {};
456
+ for (const a of src.attributes) {
457
+ if (a.name !== 'src' && a.name !== 'key' && !a.name.startsWith(DEFER_ATTR_PREFIX)) props[a.name] = a.value;
458
+ }
459
+ return props;
439
460
  };
440
461
 
441
- const reconcileIterationRegion = (liveParent, liveNodes, liStart, liEnd, srcNodes, siStart, sEnd, log) => {
442
- const tplEls = [];
443
- for (let i = siStart + 1; i < sEnd; i++) {
444
- if (srcNodes[i].nodeType === ELEMENT) tplEls.push(srcNodes[i]);
462
+ const propsDiffer = (live, srcProps) => {
463
+ const liveProps = live._vibeRemountProps ?? {};
464
+ const exprs = live._vibeIterPropExprs ?? [];
465
+ for (const k of new Set([...Object.keys(liveProps), ...Object.keys(srcProps)])) {
466
+ if (liveProps[k] === srcProps[k]) continue;
467
+ const bound = srcProps[k]?.match(/^@\[(.+)\]$/);
468
+ if (bound && exprs.some((e) => e.attrName === k && e.expr === bound[1])) continue;
469
+ return true;
470
+ }
471
+ return false;
472
+ };
473
+
474
+ const reconcileComponent = (live, src, oldT, newT, ctx) => {
475
+ if (live._vibeMountedSrc === undefined) {
476
+ if (!live.hasAttribute('src')) reconcileElement(live, src, oldT, newT, ctx);
477
+ return;
478
+ }
479
+ if (live.hasAttribute('src')) return;
480
+ const { log } = ctx;
481
+ const srcProps = propsOf(src);
482
+ const literalSrc = src.getAttribute('src');
483
+ const srcChanged = !!literalSrc && !hasBinding(literalSrc) && literalSrc !== live._vibeMountedSrc;
484
+ if (srcChanged || propsDiffer(live, srcProps)) {
485
+ live._vibeRemountProps = srcProps;
486
+ live._vibeSlotContent = src.innerHTML.trim();
487
+ if (srcChanged) remountComponent(live, literalSrc);
488
+ else forceRemount(live);
489
+ log.replace++;
490
+ log.changes.push(`re-mount ${describe(live)} (${srcChanged ? 'src' : 'props'} changed)`);
491
+ return;
445
492
  }
446
- if (!tplEls.length) return;
447
-
448
- let instanceIdx = 0;
449
- for (let i = liStart + 1; i < liEnd; i++) {
450
- const liveEl = liveNodes[i];
451
- if (liveEl.nodeType !== ELEMENT) continue;
452
- const tplEl = tplEls[instanceIdx % tplEls.length];
453
- instanceIdx++;
454
- if (liveEl.tagName !== tplEl.tagName) continue;
455
- reconcileAttributes(liveEl, tplEl, log);
456
- reconcileChildren(liveEl, [...tplEl.childNodes], log, true);
493
+ if (oldT && newT && !sameRecord(oldT.attributes, newT.attributes)) {
494
+ if (newT.attributes) oldT.attributes = newT.attributes;
495
+ else delete oldT.attributes;
496
+ rehydrate(oldT, ctx);
497
+ log.attr++;
498
+ log.changes.push(`bound attrs ${describe(live)} ${JSON.stringify(newT.attributes ?? {})}`);
457
499
  }
500
+ const srcChildren = trimWsEdges([...src.childNodes]);
501
+ const liveSlot = findOwnSlot(live, srcChildren);
502
+ if (!liveSlot) return;
503
+ const wrapperTree = oldT ?? live._vibeTree ?? null;
504
+ reconcileList(liveSlot, [...liveSlot.childNodes], srcChildren, findNodeByElement(wrapperTree, liveSlot), newT, ctx);
458
505
  };
459
506
 
460
- export const reconcile = (target, source) => {
461
- const live = typeof target === 'string' ? document.querySelector(target) : target;
462
- if (!live) return Promise.resolve({ text: 0, attr: 0, insert: 0, remove: 0, replace: 0, changes: [] });
507
+ const ITERATION_HEADER = ['arrayPath', 'itemAlias', 'indexAlias', 'keyExpr'];
508
+
509
+ const reconcileIteration = ({ parent, liveStart, srcStart, oldT, newT }, ctx) => {
510
+ if (!oldT || !newT) return;
511
+ const { log } = ctx;
512
+ const headerChanged = ITERATION_HEADER.some((k) => oldT.meta[k] !== newT.meta[k]);
513
+ const templateChanged = oldT.meta.template.element.innerHTML !== newT.meta.template.element.innerHTML;
514
+ if (!headerChanged && !templateChanged) return;
515
+
516
+ liveStart.textContent = srcStart.textContent;
517
+ swapIterationTemplate(oldT, newT.meta);
518
+ if (!oldT.runtime.templateRemoved) return;
519
+
520
+ if (headerChanged) {
521
+ rerenderIteration(oldT, ctx.state, ctx.manifest, ctx.parentScope);
522
+ log.replace++;
523
+ log.changes.push(`re-render iteration ${oldT.meta.arrayPath} (header changed)`);
524
+ return;
525
+ }
526
+
527
+ const instances = oldT.runtime.instances;
528
+ if (!instances.length) return;
529
+ const { itemAlias, indexAlias, scopeAliases } = oldT.meta;
530
+ const rowBase = { ...ctx, aliases: new Set(scopeAliases), detached: true, manifestBase: null };
531
+
532
+ if (!instances[0].tree) {
533
+ patchTreelessInstances(oldT, ctx.state, ctx.manifest, ctx.parentScope, (liveRow, freshRow) =>
534
+ reconcileElement(liveRow, freshRow, null, null, rowBase),
535
+ );
536
+ log.changes.push(`patch ${instances.length} rows of ${oldT.meta.arrayPath}`);
537
+ return;
538
+ }
539
+
540
+ for (let i = 0; i < instances.length; i++) {
541
+ const inst = instances[i];
542
+ const item = inst.liveItem !== undefined ? inst.liveItem : inst.item;
543
+ const locals = { [itemAlias]: item, [indexAlias]: inst.index };
544
+ const rowCtx = {
545
+ ...rowBase,
546
+ state: createScopedState(ctx.state, locals, ctx.parentScope),
547
+ parentScope: { ...ctx.parentScope, ...locals },
548
+ };
549
+ const { nodes, anchor } = instanceRange(oldT, i);
550
+ if (!nodes.length) continue;
551
+ const before = nodes[0].previousSibling;
552
+ const tpl = materialize([...oldT.meta.template.element.childNodes], parent, rowCtx);
553
+ const structural = reconcileList(parent, nodes, tpl.nodes, inst.tree, { children: tpl.trees }, rowCtx, anchor);
554
+ if (structural) {
555
+ inst.clonedNodes = rangeBetween(before ? before.nextSibling : parent.firstChild, anchor);
556
+ inst.element = inst.clonedNodes.find((n) => n.nodeType === ELEMENT) ?? inst.element;
557
+ }
558
+ }
559
+ stampInstanceScopes(oldT, ctx.parentScope);
560
+ log.changes.push(`patch ${instances.length} rows of ${oldT.meta.arrayPath}`);
561
+ };
463
562
 
563
+ const branchHtml = (branch) => branch?.element.innerHTML ?? null;
564
+
565
+ const reconcileConditional = ({ parent, liveStart, srcStart, oldT, newT }, ctx) => {
566
+ if (!oldT || !newT) return;
567
+ const { log } = ctx;
568
+ const oldBranches = oldT.meta.branches;
569
+ const newBranches = newT.meta.branches;
570
+ const exprChanged = oldT.meta.expression !== newT.meta.expression;
571
+ const templateChanged =
572
+ branchHtml(oldBranches.if) !== branchHtml(newBranches.if) ||
573
+ branchHtml(oldBranches.else) !== branchHtml(newBranches.else);
574
+ if (!exprChanged && !templateChanged) return;
575
+
576
+ const side =
577
+ oldT.runtime.activeBranch === oldBranches.if ? 'if' :
578
+ oldT.runtime.activeBranch === oldBranches.else ? 'else' : null;
579
+ liveStart.textContent = srcStart.textContent;
580
+ oldT.meta.expression = newT.meta.expression;
581
+ oldT.meta.branches = newBranches;
582
+ if (side) {
583
+ oldT.runtime.activeBranch = newBranches[side];
584
+ if (oldT.runtime.activeInstance) oldT.runtime.activeInstance.branch = newBranches[side];
585
+ }
586
+ if (!oldT.runtime.templateRemoved) return;
587
+
588
+ const before = oldT.runtime.activeInstance;
589
+ if (side && !newBranches[side]) remountBranch(oldT, ctx.state, ctx.manifest, ctx.parentScope);
590
+ else dispatchConditional(oldT, ctx.state, ctx.manifest, ctx.parentScope);
591
+ const active = oldT.runtime.activeInstance;
592
+ if (active !== before) {
593
+ log.replace++;
594
+ log.changes.push(`re-mount branch of if ${oldT.meta.expression}`);
595
+ return;
596
+ }
597
+ if (!active || !templateChanged) return;
598
+
599
+ const parentPath = ctx.manifestBase === null ? null : manifestPathOf(ctx.manifest, parent);
600
+ const branchCtx = {
601
+ ...ctx,
602
+ detached: true,
603
+ manifestBase: parentPath === null ? null : `${parentPath}.${oldT._key}`,
604
+ };
605
+ const { endComment } = oldT.meta;
606
+ const tpl = materialize([...oldT.runtime.activeBranch.element.childNodes], parent, branchCtx);
607
+ const nodes = rangeBetween(liveStart.nextSibling, endComment);
608
+ const structural = reconcileList(parent, nodes, tpl.nodes, active.parsedTree, { children: tpl.trees }, branchCtx, endComment);
609
+ if (structural) {
610
+ active.nodes = rangeBetween(liveStart.nextSibling, endComment);
611
+ active.nodes.forEach((n, i) => {
612
+ branchNodeRegistry.set(n, { nodes: active.nodes, index: i });
613
+ if (n.nodeType !== ELEMENT) return;
614
+ managedNodes.add(n);
615
+ if (oldT.meta.scopeAliases?.length) n.__vibeScope = ctx.state;
616
+ });
617
+ }
618
+ for (const key in oldT.children) delete oldT.children[key];
619
+ for (const key in active.parsedTree?.children ?? {}) oldT.children[key] = active.parsedTree.children[key];
620
+ log.changes.push(`patch branch of if ${oldT.meta.expression}`);
621
+ };
622
+
623
+ const scopeAround = (el) => {
624
+ for (let n = el; n; n = n.parentNode) if (n.__vibeScope) return n.__vibeScope;
625
+ return null;
626
+ };
627
+
628
+ const contextFor = (live, manifest, log) => {
629
+ const root = manifest.__live;
630
+ const overlay = scopeAround(live);
631
+ return {
632
+ manifest,
633
+ log,
634
+ state: overlay ? createScopedState(root, overlay) : root,
635
+ parentScope: overlay ?? {},
636
+ aliases: new Set(Object.keys(overlay ?? {})),
637
+ detached: !!overlay && live._vibeMountedSrc === undefined,
638
+ manifestBase: overlay ? null : undefined,
639
+ };
640
+ };
641
+
642
+ export const reconcile = (target, source, manifest) => {
643
+ const live = typeof target === 'string' ? document.querySelector(target) : target;
464
644
  const log = { text: 0, attr: 0, insert: 0, remove: 0, replace: 0, changes: [] };
645
+ if (!live) return Promise.resolve(log);
646
+
465
647
  const tpl = document.createElement('template');
466
648
  tpl.innerHTML = typeof source === 'string' ? source : source.outerHTML;
467
649
  parkFetchableSrc(tpl.content);
468
- reconcileChildren(live, [...tpl.content.childNodes], log);
650
+ const box = document.createElement('div');
651
+ box._vibeComponentId = findComponentIdForElement(live);
652
+ box.append(...tpl.content.childNodes);
653
+
654
+ const ctx = contextFor(live, manifest, log);
655
+ const newTree = parse(box, undefined, ctx.aliases);
656
+ const oldTree = live._vibeTree ?? findNodeByElement(manifest.__tree, live);
657
+ reconcileList(live, [...live.childNodes], [...box.childNodes], oldTree, newTree, ctx);
469
658
 
470
659
  return new Promise((resolve) => {
471
660
  requestAnimationFrame(() => requestAnimationFrame(() => resolve(log)));
472
661
  });
473
662
  };
663
+
664
+ export const refreshComponent = (el, rawHtml, manifest) => {
665
+ const componentIds = [];
666
+ eachComponentIn(el, (node, id) => {
667
+ if (!componentIds.includes(id)) componentIds.push(id);
668
+ });
669
+ const html = renderComponentTemplate(rawHtml, el._vibeRemountProps ?? {}, el._vibeSlotContent ?? '', { componentIds });
670
+ el._vibeRawSource = rawHtml;
671
+ return reconcile(el, html, manifest);
672
+ };