kerfjs 0.4.2 → 0.5.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/dist/index.js CHANGED
@@ -1,7 +1,8 @@
1
- import { isSafeHtml, listSafeHtml, flatten, collectLists, flattenWithoutListItems } from './chunk-WK5D3OO7.js';
2
- export { Fragment, SafeHtml, isSafeHtml, raw } from './chunk-WK5D3OO7.js';
3
- import { effect } from './chunk-IZJIKRCE.js';
4
- export { batch, computed, defineStore, effect, resetAllStores, signal } from './chunk-IZJIKRCE.js';
1
+ import { isSafeHtml, listSafeHtml, flattenWithoutListItems, collectLists, flatten, granularListSafeHtml } from './chunk-KFOSUCCC.js';
2
+ export { Fragment, SafeHtml, isSafeHtml, raw } from './chunk-KFOSUCCC.js';
3
+ export { defineStore, resetAllStores } from './chunk-GQGJFCWL.js';
4
+ import { effect } from './chunk-FN2ID4QO.js';
5
+ export { batch, computed, effect, signal } from './chunk-FN2ID4QO.js';
5
6
 
6
7
  // src/delegate.ts
7
8
  var NON_BUBBLING = /* @__PURE__ */ new Set([
@@ -54,14 +55,87 @@ function delegateCapture(rootEl, type, selector, handler) {
54
55
  }
55
56
 
56
57
  // src/each.ts
57
- var ROW_CACHE = /* @__PURE__ */ new WeakMap();
58
- var listCounter = null;
59
- function _setListCounter(c) {
60
- listCounter = c;
58
+ var ARRAY_SIGNAL_BRAND = /* @__PURE__ */ Symbol.for("kerfjs.ArraySignal");
59
+ function isArraySignal(value) {
60
+ return typeof value === "object" && value !== null && value[ARRAY_SIGNAL_BRAND] === true;
61
+ }
62
+ var context = null;
63
+ function _setRenderContext(c) {
64
+ context = c;
61
65
  }
62
66
  function each(items, render, key) {
63
- const id = listCounter !== null ? String(listCounter.value++) : "orphan";
67
+ if (isArraySignal(items) && context !== null) {
68
+ return eachGranular(items, render, key);
69
+ }
70
+ const snapshotItems = isArraySignal(items) ? items.value : items;
71
+ return eachSnapshot(snapshotItems, render, key);
72
+ }
73
+ function eachSnapshot(items, render, key) {
74
+ let id;
75
+ if (context !== null) {
76
+ id = String(context.counter++);
77
+ } else {
78
+ id = "orphan";
79
+ }
80
+ return eachSnapshotById(items, render, key, id);
81
+ }
82
+ function eachGranular(sig, render, key) {
83
+ const ctx = context;
84
+ const id = String(ctx.counter++);
85
+ const previousBindingCount = ctx.bindingCounts.get(id);
86
+ const patches = sig._consumePatches();
87
+ const snapshot = sig.value;
88
+ if (previousBindingCount === void 0 || patches.length === 0) {
89
+ return eachSnapshotById(snapshot, render, key, id);
90
+ }
91
+ let netDelta = 0;
92
+ for (const p of patches) {
93
+ if (p.type === "insert") netDelta += 1;
94
+ else if (p.type === "remove") netDelta -= 1;
95
+ else if (p.type === "replace") {
96
+ return eachSnapshotById(snapshot, render, key, id);
97
+ }
98
+ }
99
+ if (previousBindingCount + netDelta !== snapshot.length) {
100
+ return eachSnapshotById(snapshot, render, key, id);
101
+ }
102
+ const renderFnInternal = (item, index) => {
103
+ const out = render(item, index);
104
+ return isSafeHtml(out) ? out.toString() : out;
105
+ };
106
+ const internalPatches = new Array(patches.length);
107
+ try {
108
+ for (let i = 0; i < patches.length; i++) {
109
+ const p = patches[i];
110
+ if (p.type === "insert" || p.type === "update") {
111
+ internalPatches[i] = {
112
+ type: p.type,
113
+ index: p.index,
114
+ item: p.item,
115
+ html: renderFnInternal(p.item, p.index)
116
+ };
117
+ } else {
118
+ internalPatches[i] = p;
119
+ }
120
+ }
121
+ } catch {
122
+ ctx.bindingCounts.delete(id);
123
+ return eachSnapshotById(snapshot, render, key, id);
124
+ }
125
+ return granularListSafeHtml(id, [], internalPatches);
126
+ }
127
+ function eachSnapshotById(items, render, key, id) {
128
+ let cache = null;
129
+ if (context !== null) {
130
+ let c = context.caches.get(id);
131
+ if (c === void 0) {
132
+ c = /* @__PURE__ */ new WeakMap();
133
+ context.caches.set(id, c);
134
+ }
135
+ cache = c;
136
+ }
64
137
  const segItems = new Array(items.length);
138
+ const seen = /* @__PURE__ */ new Set();
65
139
  for (let i = 0; i < items.length; i++) {
66
140
  const item = items[i];
67
141
  if (typeof item !== "object" || item === null) {
@@ -69,15 +143,21 @@ function each(items, render, key) {
69
143
  `each(): items must be objects (the per-item HTML cache is a WeakMap), got ${item === null ? "null" : typeof item} at index ${i}. Wrap primitives if you need to iterate them, e.g. items.map(v => ({ v })).`
70
144
  );
71
145
  }
146
+ if (seen.has(item)) {
147
+ throw new Error(
148
+ `each(): the same object reference appears at multiple indices in items (first seen earlier, again at index ${i}). The per-item HTML cache is keyed on object identity, so duplicate references break the keyed reconciler and can leak DOM nodes on re-render. Use a fresh object per row (e.g. items.map(o => ({ ...o })) before passing to each()).`
149
+ );
150
+ }
151
+ seen.add(item);
72
152
  const k = key ? key(item, i) : void 0;
73
- const cached = ROW_CACHE.get(item);
74
153
  let html;
154
+ const cached = cache !== null ? cache.get(item) : void 0;
75
155
  if (cached !== void 0 && cached.key === k) {
76
156
  html = cached.html;
77
157
  } else {
78
158
  const out = render(item, i);
79
159
  html = isSafeHtml(out) ? out.toString() : out;
80
- ROW_CACHE.set(item, { key: k, html });
160
+ if (cache !== null) cache.set(item, { key: k, html });
81
161
  }
82
162
  segItems[i] = { ref: item, cacheKey: k, html };
83
163
  }
@@ -99,16 +179,23 @@ function getNodeKey(node) {
99
179
  }
100
180
  return void 0;
101
181
  }
102
- function diff(liveRoot, templateRoot, listParents) {
103
- diffChildren(liveRoot, templateRoot, listParents);
182
+ function diff(liveRoot, templateRoot, ownedItems) {
183
+ diffChildren(liveRoot, templateRoot, ownedItems);
184
+ }
185
+ function skipOwned(node, ownedItems) {
186
+ while (node !== null && node.nodeType === ELEMENT_NODE && ownedItems.has(node)) {
187
+ node = node.nextSibling;
188
+ }
189
+ return node;
104
190
  }
105
- function diffChildren(fromParent, toParent, listParents) {
191
+ function diffChildren(fromParent, toParent, ownedItems) {
106
192
  const keyed = /* @__PURE__ */ new Map();
107
193
  for (let c = fromParent.firstChild; c !== null; c = c.nextSibling) {
194
+ if (c.nodeType === ELEMENT_NODE && ownedItems.has(c)) continue;
108
195
  const k = getNodeKey(c);
109
196
  if (k !== void 0) keyed.set(k, c);
110
197
  }
111
- let fromChild = fromParent.firstChild;
198
+ let fromChild = skipOwned(fromParent.firstChild, ownedItems);
112
199
  let toChild = toParent.firstChild;
113
200
  while (toChild !== null) {
114
201
  const toNext = toChild.nextSibling;
@@ -120,15 +207,15 @@ function diffChildren(fromParent, toParent, listParents) {
120
207
  if (matched !== fromChild) {
121
208
  fromParent.insertBefore(matched, fromChild);
122
209
  } else {
123
- fromChild = fromChild.nextSibling;
210
+ fromChild = skipOwned(fromChild.nextSibling, ownedItems);
124
211
  }
125
212
  }
126
213
  if (matched === null && fromChild !== null && fromChild.nodeType === toChild.nodeType && (toChild.nodeType !== ELEMENT_NODE || fromChild.tagName === toChild.tagName && getNodeKey(fromChild) === void 0)) {
127
214
  matched = fromChild;
128
- fromChild = fromChild.nextSibling;
215
+ fromChild = skipOwned(fromChild.nextSibling, ownedItems);
129
216
  }
130
217
  if (matched !== null) {
131
- morphNode(matched, toChild, listParents);
218
+ morphNode(matched, toChild, ownedItems);
132
219
  } else {
133
220
  const cloned = toChild.cloneNode(true);
134
221
  fromParent.insertBefore(cloned, fromChild);
@@ -137,13 +224,15 @@ function diffChildren(fromParent, toParent, listParents) {
137
224
  }
138
225
  while (fromChild !== null) {
139
226
  const next = fromChild.nextSibling;
140
- fromParent.removeChild(fromChild);
227
+ if (fromChild.nodeType !== ELEMENT_NODE || !ownedItems.has(fromChild)) {
228
+ fromParent.removeChild(fromChild);
229
+ }
141
230
  fromChild = next;
142
231
  }
143
232
  }
144
- function morphNode(fromNode, toNode, listParents) {
233
+ function morphNode(fromNode, toNode, ownedItems) {
145
234
  if (fromNode.nodeType === ELEMENT_NODE) {
146
- morphElement(fromNode, toNode, listParents);
235
+ morphElement(fromNode, toNode, ownedItems);
147
236
  return;
148
237
  }
149
238
  if (fromNode.nodeType === TEXT_NODE || fromNode.nodeType === COMMENT_NODE) {
@@ -152,7 +241,7 @@ function morphNode(fromNode, toNode, listParents) {
152
241
  if (fromText.data !== toText.data) fromText.data = toText.data;
153
242
  }
154
243
  }
155
- function morphElement(fromEl, toEl, listParents) {
244
+ function morphElement(fromEl, toEl, ownedItems) {
156
245
  if (fromEl.tagName !== toEl.tagName) {
157
246
  const replacement = toEl.cloneNode(true);
158
247
  fromEl.parentNode?.replaceChild(replacement, fromEl);
@@ -166,8 +255,10 @@ function morphElement(fromEl, toEl, listParents) {
166
255
  if (isTextInputOrTextarea(fromEl)) preserveTextEntryState(fromEl, toEl);
167
256
  }
168
257
  morphAttributes(fromEl, toEl);
169
- if (listParents.has(fromEl)) return;
170
- diffChildren(fromEl, toEl, listParents);
258
+ diffChildren(fromEl, toEl, ownedItems);
259
+ }
260
+ function isUserAgentOwnedAttr(tagName, name) {
261
+ return name === "open" && (tagName === "DETAILS" || tagName === "DIALOG");
171
262
  }
172
263
  function morphAttributes(fromEl, toEl) {
173
264
  const toAttrs = toEl.attributes;
@@ -185,13 +276,14 @@ function morphAttributes(fromEl, toEl) {
185
276
  }
186
277
  }
187
278
  const fromAttrs = fromEl.attributes;
279
+ const fromTag = fromEl.tagName;
188
280
  for (let i = fromAttrs.length - 1; i >= 0; i--) {
189
281
  const attr = fromAttrs[i];
190
282
  const ns = attr.namespaceURI;
191
283
  const name = attr.localName;
192
284
  if (ns !== null) {
193
285
  if (!toEl.hasAttributeNS(ns, name)) fromEl.removeAttributeNS(ns, name);
194
- } else if (!toEl.hasAttribute(name)) {
286
+ } else if (!toEl.hasAttribute(name) && !isUserAgentOwnedAttr(fromTag, name)) {
195
287
  fromEl.removeAttribute(name);
196
288
  }
197
289
  }
@@ -216,6 +308,14 @@ function preserveTextEntryState(fromEl, toEl) {
216
308
  }
217
309
  }
218
310
 
311
+ // src/list-binding.ts
312
+ function endAnchor(binding) {
313
+ if (binding.items.length > 0) {
314
+ return binding.items[binding.items.length - 1].node.nextElementSibling;
315
+ }
316
+ return binding.marker.nextElementSibling;
317
+ }
318
+
219
319
  // src/list-reconcile-focus.ts
220
320
  function captureFocus(liveParent) {
221
321
  const active = document.activeElement;
@@ -245,13 +345,212 @@ function restoreFocus(snap) {
245
345
  }
246
346
  }
247
347
 
248
- // src/list-reconcile.ts
348
+ // src/utils/rowContract.ts
349
+ var ROW_HTML_SNIPPET_MAX = 120;
350
+ function truncateRowHtml(html) {
351
+ return html.length > ROW_HTML_SNIPPET_MAX ? html.slice(0, ROW_HTML_SNIPPET_MAX) + "\u2026" : html;
352
+ }
353
+ function parseRowTemplate(html) {
354
+ const tpl = document.createElement("template");
355
+ tpl.innerHTML = html;
356
+ return { tpl, count: tpl.content.children.length };
357
+ }
358
+ function rowContractError(index, html) {
359
+ const { count } = parseRowTemplate(html);
360
+ const reason = count === 0 ? "produced no top-level element" : `produced ${count} top-level elements; exactly one is required`;
361
+ return new Error(
362
+ `each(): row render at index ${index} ${reason}. Each item's render must return exactly one element \u2014 wrap multiple roots in a single parent (e.g. <li>...</li>). Got HTML: ${JSON.stringify(truncateRowHtml(html))}`
363
+ );
364
+ }
365
+
366
+ // src/list-reconcile-granular.ts
367
+ function reconcileGranular(binding, patches) {
368
+ const { liveParent } = binding;
369
+ const items = binding.items;
370
+ const focusSnap = captureFocus(liveParent);
371
+ let i = 0;
372
+ while (i < patches.length) {
373
+ const patch = patches[i];
374
+ if (patch.type === "replace") {
375
+ i += 1;
376
+ continue;
377
+ }
378
+ if (patch.type === "update") {
379
+ let runEnd = i + 1;
380
+ while (runEnd < patches.length && patches[runEnd].type === "update") {
381
+ runEnd += 1;
382
+ }
383
+ const runLen = runEnd - i;
384
+ if (runLen === 1) {
385
+ applySingleUpdate(liveParent, items, patch);
386
+ } else {
387
+ applyBulkUpdate(liveParent, items, patches, i, runEnd);
388
+ }
389
+ i = runEnd;
390
+ continue;
391
+ }
392
+ if (patch.type === "insert") {
393
+ let runEnd = i + 1;
394
+ while (runEnd < patches.length && patches[runEnd].type === "insert" && patches[runEnd].index === patches[runEnd - 1].index + 1) {
395
+ runEnd += 1;
396
+ }
397
+ const runLen = runEnd - i;
398
+ if (runLen === 1) {
399
+ applySingleInsert(liveParent, items, patch, endAnchor(binding));
400
+ } else {
401
+ applyBulkInsert(liveParent, items, patches, i, runEnd, endAnchor(binding));
402
+ }
403
+ i = runEnd;
404
+ continue;
405
+ }
406
+ if (patch.type === "remove") {
407
+ const entry = items[patch.index];
408
+ liveParent.removeChild(entry.node);
409
+ items.splice(patch.index, 1);
410
+ i += 1;
411
+ continue;
412
+ }
413
+ if (patch.type === "move") {
414
+ const moved = items[patch.from];
415
+ let anchorIdx = patch.to;
416
+ if (patch.from < patch.to) anchorIdx += 1;
417
+ const anchor = anchorIdx < items.length ? items[anchorIdx].node : endAnchor(binding);
418
+ liveParent.insertBefore(moved.node, anchor);
419
+ items.splice(patch.from, 1);
420
+ items.splice(patch.to, 0, moved);
421
+ i += 1;
422
+ continue;
423
+ }
424
+ }
425
+ if (focusSnap !== null) restoreFocus(focusSnap);
426
+ }
427
+ function applySingleInsert(liveParent, items, patch, tailAnchor) {
428
+ const { html } = patch;
429
+ const newNode = parseSingleRow(html);
430
+ const anchor = patch.index < items.length ? items[patch.index].node : tailAnchor;
431
+ liveParent.insertBefore(newNode, anchor);
432
+ items.splice(patch.index, 0, {
433
+ ref: patch.item,
434
+ cacheKey: void 0,
435
+ html,
436
+ node: newNode
437
+ });
438
+ }
439
+ function applySingleUpdate(liveParent, items, patch) {
440
+ const { html } = patch;
441
+ const oldEntry = items[patch.index];
442
+ if (html === oldEntry.html) return;
443
+ const newNode = parseSingleRow(html);
444
+ liveParent.replaceChild(newNode, oldEntry.node);
445
+ items[patch.index] = { ref: patch.item, cacheKey: void 0, html, node: newNode };
446
+ }
447
+ function applyBulkUpdate(liveParent, items, patches, start, end) {
448
+ const changes = [];
449
+ for (let k = start; k < end; k++) {
450
+ const p = patches[k];
451
+ if (p.html !== items[p.index].html) {
452
+ changes.push({ patchIdx: k, html: p.html });
453
+ }
454
+ }
455
+ if (changes.length === 0) return;
456
+ const { tpl, count } = parseRowTemplate(changes.map((c) => c.html).join(""));
457
+ if (count !== changes.length) {
458
+ throw findOffendingChange(patches, changes);
459
+ }
460
+ const newNodes = new Array(changes.length);
461
+ let child = tpl.content.firstElementChild;
462
+ for (let k = 0; k < newNodes.length; k++) {
463
+ newNodes[k] = child;
464
+ child = child.nextElementSibling;
465
+ }
466
+ for (let k = 0; k < changes.length; k++) {
467
+ const c = changes[k];
468
+ const p = patches[c.patchIdx];
469
+ const oldEntry = items[p.index];
470
+ liveParent.replaceChild(newNodes[k], oldEntry.node);
471
+ items[p.index] = { ref: p.item, cacheKey: void 0, html: c.html, node: newNodes[k] };
472
+ }
473
+ }
474
+ function applyBulkInsert(liveParent, items, patches, start, end, tailAnchor) {
475
+ const startIdx = patches[start].index;
476
+ const htmls = new Array(end - start);
477
+ for (let k = start; k < end; k++) {
478
+ const p = patches[k];
479
+ htmls[k - start] = p.html;
480
+ }
481
+ const { tpl, count } = parseRowTemplate(htmls.join(""));
482
+ if (count !== htmls.length) {
483
+ throw findOffendingInsert(patches, start, htmls);
484
+ }
485
+ const newNodes = new Array(end - start);
486
+ let child = tpl.content.firstElementChild;
487
+ for (let k = 0; k < newNodes.length; k++) {
488
+ newNodes[k] = child;
489
+ child = child.nextElementSibling;
490
+ }
491
+ const anchor = startIdx < items.length ? items[startIdx].node : tailAnchor;
492
+ liveParent.insertBefore(tpl.content, anchor);
493
+ const newEntries = new Array(end - start);
494
+ for (let k = 0; k < newEntries.length; k++) {
495
+ const p = patches[start + k];
496
+ newEntries[k] = {
497
+ ref: p.item,
498
+ cacheKey: void 0,
499
+ html: htmls[k],
500
+ node: newNodes[k]
501
+ };
502
+ }
503
+ items.splice(startIdx, 0, ...newEntries);
504
+ }
505
+ function parseSingleRow(html) {
506
+ const { tpl, count } = parseRowTemplate(html);
507
+ if (count !== 1) {
508
+ const reason = count === 0 ? "produced no top-level element" : `produced ${count} top-level elements; exactly one is required`;
509
+ throw new Error(
510
+ `each() granular reconcile: row render ${reason}. Each item's render must return exactly one element. Got HTML: ${JSON.stringify(truncateRowHtml(html))}`
511
+ );
512
+ }
513
+ return tpl.content.firstElementChild;
514
+ }
515
+ function findOffendingInsert(patches, start, htmls) {
516
+ for (let i = 0; i < htmls.length; i++) {
517
+ if (parseRowTemplate(htmls[i]).count !== 1) {
518
+ const p = patches[start + i];
519
+ return rowContractError(p.index, htmls[i]);
520
+ }
521
+ }
522
+ return new Error("each(): bulk-insert mismatch with no per-row offender (kerf bug).");
523
+ }
524
+ function findOffendingChange(patches, changes) {
525
+ for (const c of changes) {
526
+ if (parseRowTemplate(c.html).count !== 1) {
527
+ const p = patches[c.patchIdx];
528
+ return rowContractError(p.index, c.html);
529
+ }
530
+ }
531
+ return new Error("each(): bulk-update mismatch with no per-row offender (kerf bug).");
532
+ }
533
+
534
+ // src/list-reconcile-snapshot.ts
535
+ function reconcileSnapshot(binding, listSeg) {
536
+ const { liveParent } = binding;
537
+ const { newRecord, prevIdx, replacedNodes, freshIndices, freshHtmls } = classifyItems(binding.items, listSeg);
538
+ if (replacedNodes.length === 0 && freshIndices.length === 0 && isInOrder(prevIdx)) {
539
+ binding.items = newRecord;
540
+ return;
541
+ }
542
+ const tailAnchor = endAnchor(binding);
543
+ buildFreshNodes(newRecord, freshIndices, freshHtmls);
544
+ const focusSnap = captureFocus(liveParent);
545
+ removeOldNodes(liveParent, replacedNodes);
546
+ applyMoves(liveParent, newRecord, prevIdx, lis(prevIdx), tailAnchor);
547
+ if (focusSnap !== null) restoreFocus(focusSnap);
548
+ binding.items = newRecord;
549
+ }
249
550
  function classifyItems(oldItems, listSeg) {
250
551
  const oldByRef = /* @__PURE__ */ new Map();
251
- const oldIndex = /* @__PURE__ */ new Map();
252
552
  for (let i = 0; i < oldItems.length; i++) {
253
- oldByRef.set(oldItems[i].ref, oldItems[i]);
254
- oldIndex.set(oldItems[i].ref, i);
553
+ oldByRef.set(oldItems[i].ref, [oldItems[i], i]);
255
554
  }
256
555
  const newRecord = new Array(listSeg.items.length);
257
556
  const prevIdx = new Array(listSeg.items.length);
@@ -263,12 +562,12 @@ function classifyItems(oldItems, listSeg) {
263
562
  const oi = oldByRef.get(ni.ref);
264
563
  if (oi !== void 0) {
265
564
  oldByRef.delete(ni.ref);
266
- if (oi.html === ni.html) {
267
- newRecord[i] = oi;
268
- prevIdx[i] = oldIndex.get(ni.ref);
565
+ if (oi[0].html === ni.html) {
566
+ newRecord[i] = oi[0];
567
+ prevIdx[i] = oi[1];
269
568
  continue;
270
569
  }
271
- replacedNodes.push(oi.node);
570
+ replacedNodes.push(oi[0].node);
272
571
  }
273
572
  newRecord[i] = {
274
573
  ref: ni.ref,
@@ -280,32 +579,37 @@ function classifyItems(oldItems, listSeg) {
280
579
  freshIndices.push(i);
281
580
  freshHtmls.push(ni.html);
282
581
  }
283
- for (const [, orphan] of oldByRef) replacedNodes.push(orphan.node);
582
+ for (const [, orphan] of oldByRef) replacedNodes.push(orphan[0].node);
284
583
  return { newRecord, prevIdx, replacedNodes, freshIndices, freshHtmls };
285
584
  }
286
585
  function buildFreshNodes(newRecord, freshIndices, freshHtmls) {
287
586
  if (freshHtmls.length === 0) return;
288
- const tpl = document.createElement("template");
289
- tpl.innerHTML = freshHtmls.join("");
587
+ const { tpl, count } = parseRowTemplate(freshHtmls.join(""));
588
+ if (count !== freshHtmls.length) {
589
+ throw findOffendingRow(newRecord, freshIndices, freshHtmls);
590
+ }
290
591
  let node = tpl.content.firstElementChild;
291
592
  for (const idx of freshIndices) {
292
- if (node === null) {
293
- throw new Error(
294
- `each(): row render produced no top-level element. Each item's render must return exactly one element. Got HTML: ${newRecord[idx].html.slice(0, 120)}`
295
- );
296
- }
297
593
  const next = node.nextElementSibling;
298
594
  newRecord[idx].node = node;
299
595
  node = next;
300
596
  }
301
597
  }
598
+ function findOffendingRow(newRecord, freshIndices, freshHtmls) {
599
+ for (let i = 0; i < freshHtmls.length; i++) {
600
+ if (parseRowTemplate(freshHtmls[i]).count !== 1) {
601
+ return rowContractError(freshIndices[i], newRecord[freshIndices[i]].html);
602
+ }
603
+ }
604
+ return new Error("each(): bulk-parse mismatch with no per-row offender (kerf bug).");
605
+ }
302
606
  function removeOldNodes(liveParent, replacedNodes) {
303
607
  for (const node of replacedNodes) {
304
608
  if (node.parentElement === liveParent) liveParent.removeChild(node);
305
609
  }
306
610
  }
307
- function applyMoves(liveParent, newRecord, prevIdx, stable) {
308
- let nextSibling = null;
611
+ function applyMoves(liveParent, newRecord, prevIdx, stable, tailAnchor) {
612
+ let nextSibling = tailAnchor;
309
613
  for (let i = newRecord.length - 1; i >= 0; i--) {
310
614
  const node = newRecord[i].node;
311
615
  if (prevIdx[i] === -1 || !stable.has(i)) {
@@ -343,15 +647,20 @@ function lis(arr) {
343
647
  }
344
648
  return out;
345
649
  }
650
+ function isInOrder(prevIdx) {
651
+ for (let i = 0; i < prevIdx.length; i++) {
652
+ if (prevIdx[i] !== i) return false;
653
+ }
654
+ return true;
655
+ }
656
+
657
+ // src/list-reconcile.ts
346
658
  function reconcileList(binding, listSeg) {
347
- const { liveParent } = binding;
348
- const { newRecord, prevIdx, replacedNodes, freshIndices, freshHtmls } = classifyItems(binding.items, listSeg);
349
- buildFreshNodes(newRecord, freshIndices, freshHtmls);
350
- const focusSnap = captureFocus(liveParent);
351
- removeOldNodes(liveParent, replacedNodes);
352
- applyMoves(liveParent, newRecord, prevIdx, lis(prevIdx));
353
- if (focusSnap !== null) restoreFocus(focusSnap);
354
- binding.items = newRecord;
659
+ if (listSeg.patches !== void 0 && binding.items.length > 0) {
660
+ reconcileGranular(binding, listSeg.patches);
661
+ return;
662
+ }
663
+ reconcileSnapshot(binding, listSeg);
355
664
  }
356
665
 
357
666
  // src/mount.ts
@@ -363,58 +672,113 @@ function mount(rootEl, render) {
363
672
  );
364
673
  }
365
674
  const bindings = /* @__PURE__ */ new Map();
366
- const counter = { value: 0 };
675
+ const renderCtx = {
676
+ counter: 0,
677
+ caches: /* @__PURE__ */ new Map(),
678
+ bindingCounts: /* @__PURE__ */ new Map()
679
+ };
367
680
  let isFirst = true;
681
+ let prevStaticHtml = "";
368
682
  return effect(() => {
369
- counter.value = 0;
370
- _setListCounter(counter);
683
+ renderCtx.counter = 0;
684
+ _setRenderContext(renderCtx);
371
685
  let result;
372
686
  try {
373
687
  result = render();
374
688
  } finally {
375
- _setListCounter(null);
689
+ _setRenderContext(null);
376
690
  }
377
- const segment = isSafeHtml(result) ? result.__segment ?? { kind: "static", html: result.__html } : { kind: "static", html: result };
691
+ const segment = isSafeHtml(result) ? result.__segment ?? { kind: "static", html: result.__html } : { kind: "static", html: coerceRenderResult(result) };
378
692
  if (isFirst) {
379
- rootEl.innerHTML = flatten(segment, true);
380
- bindListsFromMarkers(rootEl, segment, bindings);
693
+ runFirstRender(rootEl, segment, bindings);
694
+ prevStaticHtml = flattenWithoutListItems(segment);
381
695
  isFirst = false;
382
696
  } else {
383
- const template = rootEl.cloneNode(false);
384
- template.innerHTML = flattenWithoutListItems(segment);
385
- const listParents = /* @__PURE__ */ new Set();
386
- for (const b of bindings.values()) listParents.add(b.liveParent);
387
- diff(rootEl, template, listParents);
388
- bindListsFromMarkers(rootEl, segment, bindings);
697
+ prevStaticHtml = runSubsequentRender(rootEl, segment, bindings, renderCtx, prevStaticHtml);
389
698
  }
390
699
  for (const listSeg of collectLists(segment).values()) {
391
700
  const binding = bindings.get(listSeg.id);
392
701
  reconcileList(binding, listSeg);
702
+ renderCtx.bindingCounts.set(listSeg.id, binding.items.length);
393
703
  }
394
704
  });
395
705
  }
396
- function bindListsFromMarkers(rootEl, segment, bindings) {
706
+ function runFirstRender(rootEl, segment, bindings) {
707
+ rootEl.innerHTML = flatten(segment, true);
708
+ bindListsFromMarkers(rootEl, segment, bindings, true);
709
+ }
710
+ function runSubsequentRender(rootEl, segment, bindings, renderCtx, prevStaticHtml) {
711
+ const currentStaticHtml = flattenWithoutListItems(segment);
712
+ if (currentStaticHtml === prevStaticHtml) {
713
+ return prevStaticHtml;
714
+ }
715
+ cleanupOrphanBindings(segment, bindings, renderCtx);
716
+ const template = rootEl.cloneNode(false);
717
+ template.innerHTML = currentStaticHtml;
718
+ diff(rootEl, template, collectOwnedItems(bindings));
719
+ bindListsFromMarkers(rootEl, segment, bindings, false);
720
+ return currentStaticHtml;
721
+ }
722
+ function coerceRenderResult(result) {
723
+ if (result === null || result === void 0) return "";
724
+ if (result === false || result === true) return "";
725
+ return String(result);
726
+ }
727
+ function bindListsFromMarkers(rootEl, segment, bindings, inlinedItems) {
397
728
  const lists = collectLists(segment);
398
729
  const found = [];
399
730
  collectComments(rootEl, found);
400
731
  for (const marker of found) {
401
732
  if (!marker.data.startsWith(LIST_MARKER_PREFIX)) continue;
402
733
  const id = marker.data.slice(LIST_MARKER_PREFIX.length);
734
+ if (bindings.has(id)) continue;
403
735
  const listSeg = lists.get(id);
404
736
  const liveParent = marker.parentElement;
405
737
  const items = [];
406
- let next = marker.nextElementSibling;
407
- for (let i = 0; i < listSeg.items.length && next !== null; i++) {
408
- items.push({
409
- ref: listSeg.items[i].ref,
410
- cacheKey: listSeg.items[i].cacheKey,
411
- html: listSeg.items[i].html,
412
- node: next
413
- });
414
- next = next.nextElementSibling;
415
- }
416
- bindings.set(id, { liveParent, items });
417
- marker.remove();
738
+ if (inlinedItems) {
739
+ let next = marker.nextElementSibling;
740
+ for (let i = 0; i < listSeg.items.length && next !== null; i++) {
741
+ validateInlinedRowMatch(listSeg.items[i].html, i, next);
742
+ items.push({
743
+ ref: listSeg.items[i].ref,
744
+ cacheKey: listSeg.items[i].cacheKey,
745
+ html: listSeg.items[i].html,
746
+ node: next
747
+ });
748
+ next = next.nextElementSibling;
749
+ }
750
+ }
751
+ bindings.set(id, { liveParent, items, marker });
752
+ }
753
+ }
754
+ function validateInlinedRowMatch(expectedHtml, index, boundEl) {
755
+ if (boundEl.outerHTML === expectedHtml) return;
756
+ const { count } = parseRowTemplate(expectedHtml);
757
+ if (count === 1) return;
758
+ throw rowContractError(index, expectedHtml);
759
+ }
760
+ function collectOwnedItems(bindings) {
761
+ const owned = /* @__PURE__ */ new Set();
762
+ for (const b of bindings.values()) {
763
+ for (const item of b.items) owned.add(item.node);
764
+ }
765
+ return owned;
766
+ }
767
+ function cleanupOrphanBindings(segment, bindings, renderCtx) {
768
+ const liveIds = collectLists(segment);
769
+ for (const [id, binding] of bindings) {
770
+ if (liveIds.has(id)) continue;
771
+ for (const item of binding.items) {
772
+ if (item.node.parentElement !== null) {
773
+ item.node.parentElement.removeChild(item.node);
774
+ }
775
+ }
776
+ if (binding.marker.parentElement !== null) {
777
+ binding.marker.parentElement.removeChild(binding.marker);
778
+ }
779
+ bindings.delete(id);
780
+ renderCtx.bindingCounts.delete(id);
781
+ renderCtx.caches.delete(id);
418
782
  }
419
783
  }
420
784
  function collectComments(node, out) {