@barefootjs/client 0.26.4 → 0.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/dist/reactive.d.ts.map +1 -1
  2. package/dist/runtime/claim-slots.d.ts +222 -0
  3. package/dist/runtime/claim-slots.d.ts.map +1 -0
  4. package/dist/runtime/component.d.ts +40 -18
  5. package/dist/runtime/component.d.ts.map +1 -1
  6. package/dist/runtime/dynamic-text.d.ts +24 -1
  7. package/dist/runtime/dynamic-text.d.ts.map +1 -1
  8. package/dist/runtime/index.d.ts +4 -5
  9. package/dist/runtime/index.d.ts.map +1 -1
  10. package/dist/runtime/index.js +467 -259
  11. package/dist/runtime/loop-markers.d.ts +26 -0
  12. package/dist/runtime/loop-markers.d.ts.map +1 -0
  13. package/dist/runtime/map-array-lazy.d.ts +164 -0
  14. package/dist/runtime/map-array-lazy.d.ts.map +1 -0
  15. package/dist/runtime/map-array.d.ts +35 -0
  16. package/dist/runtime/map-array.d.ts.map +1 -1
  17. package/dist/runtime/qsa-item.d.ts +7 -0
  18. package/dist/runtime/qsa-item.d.ts.map +1 -1
  19. package/dist/runtime/registry.d.ts.map +1 -1
  20. package/dist/runtime/standalone.js +455 -248
  21. package/package.json +2 -2
  22. package/src/reactive.ts +2 -1
  23. package/src/runtime/claim-slots.ts +647 -0
  24. package/src/runtime/component.ts +153 -70
  25. package/src/runtime/dynamic-text.ts +24 -1
  26. package/src/runtime/index.ts +20 -7
  27. package/src/runtime/insert.ts +1 -1
  28. package/src/runtime/loop-markers.ts +100 -0
  29. package/src/runtime/map-array-lazy.ts +470 -0
  30. package/src/runtime/map-array.ts +68 -11
  31. package/src/runtime/qsa-item.ts +9 -3
  32. package/src/runtime/registry.ts +5 -3
  33. package/dist/runtime/client-marker.d.ts +0 -21
  34. package/dist/runtime/client-marker.d.ts.map +0 -1
  35. package/dist/runtime/list.d.ts +0 -21
  36. package/dist/runtime/list.d.ts.map +0 -1
  37. package/dist/runtime/patch-slot-range.d.ts +0 -47
  38. package/dist/runtime/patch-slot-range.d.ts.map +0 -1
  39. package/dist/runtime/reconcile-elements.d.ts +0 -44
  40. package/dist/runtime/reconcile-elements.d.ts.map +0 -1
  41. package/src/runtime/client-marker.ts +0 -46
  42. package/src/runtime/list.ts +0 -47
  43. package/src/runtime/patch-slot-range.ts +0 -105
  44. package/src/runtime/reconcile-elements.ts +0 -391
@@ -2,7 +2,7 @@
2
2
  * BarefootJS - Component Creation
3
3
  *
4
4
  * Functions for dynamically creating component instances at runtime.
5
- * Used by reconcileList() when rendering components in loops.
5
+ * Used by mapArray()/mapArrayAnchored() when rendering components in loops.
6
6
  */
7
7
 
8
8
  import { getTemplate } from './template.ts'
@@ -25,14 +25,40 @@ export function setParentScopeId(id: string | null): void {
25
25
  _parentScopeId = id
26
26
  }
27
27
 
28
- // WeakMap to store props update functions for each component element
29
- // This allows reconcileList to update props when an element is reused
30
- const propsUpdateMap = new WeakMap<HTMLElement, (props: Record<string, unknown>) => void>()
31
-
32
- // WeakMap to store the current props for each component element
33
- // Used to pass props to existing elements when they are reused
34
- const propsMap = new WeakMap<HTMLElement, Record<string, unknown>>()
28
+ /** Where `mapArray` wants a fresh loop row connected before its `init` runs. */
29
+ export type RowMountPoint = { container: Node; anchor: Node | null }
30
+
31
+ // Ambient mount point for a loop row.
32
+ //
33
+ // A loop row created by `renderItem` has no placeholder to replace, so it
34
+ // cannot use `mountAt` and its `init` runs detached (see the known-limitation
35
+ // docstring in `__tests__/runtime/csr-loop-row-init-connected.test.ts`).
36
+ // `mapArray` sets this around the `renderItem` call so the OUTERMOST
37
+ // `createComponent` inside it connects the row at `container`/`anchor` before
38
+ // running `init` — same guarantee `mountAt` gives the child-slot path.
39
+ //
40
+ // Consumed once (take-and-clear) so nested `createComponent` calls made from
41
+ // the row's own init don't re-use the row's mount point.
42
+ //
43
+ // `setRowMountPoint` returns the previous value so a caller can restore it
44
+ // instead of clearing to `null`. That matters because the ambient is a single
45
+ // slot: a row whose own `init` drives a nested `mapArray` would otherwise have
46
+ // the inner list's teardown blank out an outer mount point that had not been
47
+ // consumed yet. Save-and-restore makes the slot behave like a stack without
48
+ // paying for one.
49
+ let _rowMountPoint: RowMountPoint | null = null
50
+
51
+ export function setRowMountPoint(p: RowMountPoint | null): RowMountPoint | null {
52
+ const prev = _rowMountPoint
53
+ _rowMountPoint = p
54
+ return prev
55
+ }
35
56
 
57
+ function takeRowMountPoint(): RowMountPoint | null {
58
+ const p = _rowMountPoint
59
+ _rowMountPoint = null
60
+ return p
61
+ }
36
62
 
37
63
  /**
38
64
  * Create a component instance with DOM element and initialized state.
@@ -47,6 +73,10 @@ const propsMap = new WeakMap<HTMLElement, Record<string, unknown>>()
47
73
  * @param name - Component name (e.g., 'TodoItem')
48
74
  * @param props - Props to pass to the component
49
75
  * @param key - Optional key for list reconciliation
76
+ * @param slot - Slot-relationship markers stamped as `bf-h` / `bf-m`
77
+ * @param mountAt - Placeholder this component replaces. When given, the
78
+ * element is connected to the document *before* `init` runs — see
79
+ * "Connect before init" below.
50
80
  * @returns Created DOM element
51
81
  *
52
82
  * @example
@@ -77,15 +107,49 @@ export function createComponent(
77
107
  props: Record<string, unknown> = {},
78
108
  key?: string | number,
79
109
  slot?: CreateComponentSlotInfo,
110
+ mountAt?: Element | null,
111
+ ): HTMLElement {
112
+ const element = materializeComponent(nameOrDef, props, key, slot, mountAt)
113
+ // `mountAt` is an unconditional obligation: callers used to run
114
+ // `ph.replaceWith(comp)` themselves on every outcome, so every path that
115
+ // did NOT consume the placeholder still owes the replacement — a missing or
116
+ // empty template (either mode), and the root-deferred-placeholder shape,
117
+ // which must stay detached so its self-replacement stays recoverable.
118
+ // `parentNode` (not `isConnected`) is the right "still unconsumed" probe: it
119
+ // survives a `mountAt` that was itself detached, which is the normal case
120
+ // during multi-root loop-body setup.
121
+ if (mountAt && mountAt.parentNode && element !== mountAt) {
122
+ mountAt.replaceWith(element)
123
+ }
124
+ return element
125
+ }
126
+
127
+ /**
128
+ * Build the element, connect it at `mountAt` when there is one, and run
129
+ * `init` — in that order, which is the whole point (see step 7b).
130
+ *
131
+ * Named "materialize" rather than anything with "unmounted" in it because it
132
+ * DOES mount: the connect has to happen inside, before `init`, and only the
133
+ * paths that cannot consume the placeholder leave that to `createComponent`.
134
+ */
135
+ function materializeComponent(
136
+ nameOrDef: string | ComponentDef,
137
+ props: Record<string, unknown> = {},
138
+ key?: string | number,
139
+ slot?: CreateComponentSlotInfo,
140
+ mountAt?: Element | null,
80
141
  ): HTMLElement {
81
142
  // A bare callable shim invoked from user code (e.g. an object-literal
82
143
  // value `LOGOS[id]()` whose arrow the compiler hoisted into a component)
83
144
  // reaches us with no props (#1663). Normalize to an empty object so the
84
145
  // descriptor probes below don't throw on `undefined`.
85
146
  if (props == null) props = {}
147
+ // Take the row mount point BEFORE any template eval / init can run, so the
148
+ // outermost call for the row is the only one that can consume it.
149
+ const rowMount = mountAt ? null : takeRowMountPoint()
86
150
  // ComponentDef mode: use def directly instead of registry lookup
87
151
  if (typeof nameOrDef !== 'string') {
88
- return createComponentFromDef(nameOrDef, props, key)
152
+ return createComponentFromDef(nameOrDef, props, key, mountAt, rowMount)
89
153
  }
90
154
 
91
155
  const name = nameOrDef
@@ -182,6 +246,35 @@ export function createComponent(
182
246
  element.setAttribute(BF_KEY, String(key))
183
247
  }
184
248
 
249
+ // 7b. Connect before init.
250
+ //
251
+ // `initFn` resolves context by DOM position (`useContext` walks
252
+ // `parentElement` from the current scope) and may measure layout. Both
253
+ // need this element to be in the document, so when the caller told us
254
+ // which placeholder we replace, do the replacement NOW rather than
255
+ // after init. Running init detached made `useContext` fall through to
256
+ // the global, last-writer-wins context store, so a child materialised
257
+ // after a sibling provider had run picked up the wrong provider's
258
+ // value. This aligns the CSR path with the SSR one, where the
259
+ // doc-order walker only ever inits elements already in the document
260
+ // (`hydrate.ts`).
261
+ //
262
+ // A root-level deferred placeholder is excluded: its init replaces
263
+ // `element` itself, which the block below recovers via a throwaway
264
+ // wrapper. Connecting first would make that replacement happen in the
265
+ // live DOM with no handle on the result, so this shape keeps the
266
+ // detached behaviour.
267
+ const rootIsDeferredPlaceholder = element.hasAttribute(BF_PLACEHOLDER)
268
+ if (mountAt && !rootIsDeferredPlaceholder) {
269
+ mountAt.replaceWith(element)
270
+ } else if (rowMount && !rootIsDeferredPlaceholder) {
271
+ // Loop row: no placeholder exists, so connect at the position `mapArray`
272
+ // handed down. The reorder step may move the row afterwards; any position
273
+ // inside the container yields the same ancestor chain, which is all
274
+ // `useContext`'s parentElement walk needs.
275
+ rowMount.container.insertBefore(element, rowMount.anchor)
276
+ }
277
+
185
278
  // 8. Set currentScope so provideContext/useContext are element-scoped.
186
279
  // This allows context providers in initFn to store context on this element.
187
280
  const prevScope = setCurrentScope(element)
@@ -193,7 +286,6 @@ export function createComponent(
193
286
  // `replaceWith` — but a detached root node can't replace itself in
194
287
  // place. Park it in a throwaway wrapper so the replacement lands
195
288
  // somewhere we can recover, then return the materialised child.
196
- const rootIsDeferredPlaceholder = element.hasAttribute(BF_PLACEHOLDER)
197
289
  let placeholderWrapper: HTMLElement | null = null
198
290
  if (rootIsDeferredPlaceholder) {
199
291
  placeholderWrapper = parseHTML('<div></div>').firstChild as HTMLElement
@@ -214,14 +306,12 @@ export function createComponent(
214
306
  if (materialised && !materialised.hasAttribute(BF_PLACEHOLDER)) {
215
307
  // The deferred child was created in place of the placeholder.
216
308
  // `materialised` is the child's OWN element, created via
217
- // upsertChild -> createComponent, which already registered itself
218
- // (hydratedScopes / propsMap / registerPropsUpdate) keyed to the
219
- // child with the child's own props. We must NOT re-register it here:
220
- // overwriting propsMap/registerPropsUpdate with the *parent's* props
221
- // would mis-key the child (e.g. a later getComponentProps would read
222
- // the parent's props), and re-running the parent's init on an element
223
- // whose placeholder is already gone could not re-materialise. So just
224
- // restore the scope and return the already-registered child.
309
+ // upsertChild -> createComponent, which already marked itself
310
+ // hydrated with its own props. We must NOT re-run this function's
311
+ // own registration steps on it here that would re-run the
312
+ // *parent's* init on an element whose placeholder is already gone
313
+ // and could not re-materialise. So just restore the scope and
314
+ // return the already-registered child.
225
315
  // (Parent-scope effects are unaffected: createEffect ownership lives
226
316
  // in the EffectContext tree, not the discarded placeholder element.)
227
317
  setCurrentScope(prevScope)
@@ -248,51 +338,9 @@ export function createComponent(
248
338
  // 12. Mark element as initialized
249
339
  hydratedScopes.add(element)
250
340
 
251
- // 13. Store props and register update function for element reuse in reconcileList
252
- propsMap.set(element, props)
253
- registerPropsUpdate(element, name, props)
254
-
255
341
  return element
256
342
  }
257
343
 
258
- /**
259
- * Get the props stored for a component element.
260
- * Used by reconcileList to pass props to an existing element.
261
- */
262
- export function getComponentProps(element: HTMLElement): Record<string, unknown> | undefined {
263
- return propsMap.get(element)
264
- }
265
-
266
- /**
267
- * Register a props update function for a component element.
268
- * When called, this function re-initializes the component with new props.
269
- */
270
- function registerPropsUpdate(
271
- element: HTMLElement,
272
- name: string,
273
- _initialProps: Record<string, unknown>
274
- ): void {
275
- // Register update function that will be called by reconcileList
276
- propsUpdateMap.set(element, (newProps: Record<string, unknown>) => {
277
- // Re-initialize the component with new props
278
- // This allows the component to capture new values (e.g., todo with editing: true)
279
- // and set up new effects that reference the new values
280
- const init = getComponentInit(name)
281
- if (init) {
282
- init(element, newProps)
283
- }
284
- })
285
- }
286
-
287
- /**
288
- * Get the props update function for an element.
289
- * Used by reconcileList to update props when reusing an element.
290
- */
291
- export function getPropsUpdateFn(element: HTMLElement): ((props: Record<string, unknown>) => void) | undefined {
292
- return propsUpdateMap.get(element)
293
- }
294
-
295
-
296
344
  /**
297
345
  * Render a child component's template to an HTML string.
298
346
  * Used by compiler-generated template functions when a stateless component
@@ -471,18 +519,44 @@ export function escapeAttr(value: unknown): string {
471
519
  *
472
520
  * A nullish value renders as empty text — the JSX/Solid semantics the Hono
473
521
  * SSR reference follows (`{undefined}` / `{null}` produce no text), and
474
- * what the reactive text-update path already does (`dynamic-text.ts` and
475
- * `client-marker.ts` both `String(value ?? '')`). Only this initial-render
476
- * escape site used to stringify `undefined` / `null` into literal
477
- * "undefined" / "null" text, so a bare `{props.x}` on an absent prop
478
- * diverged from SSR at first paint (#2137). Non-nullish values (including
479
- * `0` and `false`) keep their `String()` form, matching the reactive path.
522
+ * what the reactive text-update path already does (`claim-slots.ts`'s
523
+ * `writeText`/`writeMarkup` and `dynamic-text.ts` all `String(value ?? '')`).
524
+ * Only this initial-render escape site used to stringify `undefined` /
525
+ * `null` into literal "undefined" / "null" text, so a bare `{props.x}` on
526
+ * an absent prop diverged from SSR at first paint (#2137). Non-nullish
527
+ * values (including `0` and `false`) keep their `String()` form, matching
528
+ * the reactive path.
480
529
  */
481
530
  export function escapeText(value: unknown): string {
482
531
  if (value == null) return ''
483
532
  return escapeAttr(value)
484
533
  }
485
534
 
535
+ /**
536
+ * `escapeText`'s counterpart for a claimed 'markup' slot's REACTIVE write
537
+ * (slot unification A3 follow-up), where the value is a plain-JS expression
538
+ * that may resolve to either a string or a live `Node` (e.g. `{cond &&
539
+ * logo(id)}`, a hoisted `renderNode` callback, #1213). `writeMarkup`
540
+ * (`claim-slots.ts`) inserts a string via `<template>.innerHTML =`, which —
541
+ * unlike the old `__bfText`'s plain `Text.nodeValue =` assignment — DOES
542
+ * interpret HTML, so a raw un-escaped string is an injection/corruption
543
+ * risk exactly where the initial SSR/CSR TEMPLATE already calls
544
+ * `escapeText` on the same expression (`html-template.ts`'s
545
+ * `escapeTextSlotExpr`). A live `Node`, by contrast, must pass through
546
+ * untouched — `escapeText(node)` would stringify it to garbage, and
547
+ * `writeMarkup`'s own `instanceof Node` check needs the real object to
548
+ * splice in by identity. This is the single call every "dynamic JSX/text
549
+ * slot, value may be a Node" emission site (`emit-reactive.ts`,
550
+ * `stringify/loop-child-arm.ts`, `stringify/insert.ts`) wraps the value in
551
+ * before handing it to a 'markup' writer — NOT the preamble-region case
552
+ * (`stringify/loop.ts`), whose value is already-built HTML from a nested
553
+ * compiled render and must stay unescaped.
554
+ */
555
+ export function escapeTextOrNode(value: unknown): string | Node {
556
+ if (typeof Node !== 'undefined' && value instanceof Node) return value
557
+ return escapeText(value)
558
+ }
559
+
486
560
  const SVG_NS = 'http://www.w3.org/2000/svg'
487
561
 
488
562
  /**
@@ -559,7 +633,9 @@ function insertGetterChildren(element: HTMLElement, children: unknown): void {
559
633
  function createComponentFromDef(
560
634
  def: ComponentDef,
561
635
  props: Record<string, unknown>,
562
- key?: string | number
636
+ key?: string | number,
637
+ mountAt?: Element | null,
638
+ rowMount?: { container: Node; anchor: Node | null } | null,
563
639
  ): HTMLElement {
564
640
  if (!def.template) {
565
641
  throw new Error('[BarefootJS] createComponent with ComponentDef requires a template function')
@@ -587,14 +663,21 @@ function createComponentFromDef(
587
663
  element.setAttribute(BF_KEY, String(key))
588
664
  }
589
665
 
666
+ // Connect before init, for the same reason the registry path does (see
667
+ // `materializeComponent` step 7b): `def.init` may resolve context by DOM
668
+ // position or measure layout, and neither works detached. Keeps `mountAt`
669
+ // one contract across both modes instead of a registry-only guarantee.
670
+ if (mountAt) {
671
+ mountAt.replaceWith(element)
672
+ } else if (rowMount) {
673
+ rowMount.container.insertBefore(element, rowMount.anchor)
674
+ }
675
+
590
676
  // Initialize
591
677
  def.init(element, props)
592
678
 
593
679
  // Mark as initialized
594
680
  hydratedScopes.add(element)
595
681
 
596
- // Store props for element reuse
597
- propsMap.set(element, props)
598
-
599
682
  return element
600
683
  }
@@ -1,7 +1,30 @@
1
1
  /**
2
2
  * Dynamic text/JSX slot updater (#1663).
3
3
  *
4
- * The compiler wraps reactive child expressions (`<div>{expr}</div>`) in a
4
+ * Slot unification A3 (`spec/slot-unification.md` §5-A3) replaced every
5
+ * OTHER `__bfText` call site with a claimed 'markup' slot writer
6
+ * (`claim-slots.ts`'s `writeMarkup` provides the identical Node/text
7
+ * contract). ONE emission site still calls `__bfText` directly and is
8
+ * deliberately deferred: `emitDynamicTextUpdates`'s `conditionalElems`
9
+ * path (`ir-to-client-js/emit-reactive.ts`) — a dynamic text/JSX
10
+ * expression nested inside a top-level (non-loop) conditional, tracked by
11
+ * an effect OUTSIDE the conditional's own `insert()` `bindEvents`. That
12
+ * effect re-resolves its anchor via `$t(__scope, slotId)` on EVERY run
13
+ * because `insert()` may swap the branch independently of this effect's own
14
+ * reruns — a cached `lazySlots` claim would go stale across such a swap, and
15
+ * a 'markup' slot's dedup `last` state can't safely survive being re-claimed
16
+ * fresh every run either: a fresh claim's `last` always starts `undefined`,
17
+ * so re-claiming per-run would throw away the dedup skip on every single
18
+ * run (every write would re-clear-and-reparse even when the value hasn't
19
+ * changed) — unlike the 'text'-kind conditional cases elsewhere in the
20
+ * compiler, which have no such state to go stale and so DO re-claim fresh
21
+ * each run safely. Moving this one case onto the claim-plan model needs the
22
+ * slot's claim door tied to the branch's OWN activation lifecycle instead of
23
+ * this separate effect's — real architectural work, not a mechanical swap —
24
+ * so it stays on `$t`/`__bfText` for now.
25
+ *
26
+ * The mechanism itself, for the reader who lands here from that one site:
27
+ * the compiler wraps reactive child expressions (`<div>{expr}</div>`) in a
5
28
  * `createEffect` that writes the value into the text node sitting between
6
29
  * the slot's `<!--bf:sX-->` / `<!--/-->` comment markers. That was a pure
7
30
  * `nodeValue = String(value)` assignment, which is correct for primitives
@@ -77,13 +77,21 @@ export {
77
77
  type PortalChildren,
78
78
  } from './portal.ts'
79
79
 
80
- // List reconciliation
81
- export { reconcileList, type RenderItemFn } from './list.ts'
82
- export { reconcileElements, getLoopChildren, getLoopNodes } from './reconcile-elements.ts'
80
+ // Loop boundary marker lookup (used by mapArray/mapArrayAnchored consumers
81
+ // and compiler-generated clearing code see ./loop-markers.ts docstring)
82
+ export { getLoopChildren, getLoopNodes } from './loop-markers.ts'
83
83
  export { qsaItem, upsertChildItem } from './qsa-item.ts'
84
84
  export { mapArray, mapArrayAnchored } from './map-array.ts'
85
+ // Lazy row graph (slot unification §9, L2) — keyed list rendering with no
86
+ // per-row reactive resources; compiler targets it for eligible plain loops
87
+ // (L3). See ./map-array-lazy.ts for the pinned row-plan contract.
88
+ export { mapArrayLazy, type LazyRowEntry, type LazyRowPlan } from './map-array-lazy.ts'
85
89
  export { patchLeaf } from './patch-leaf.ts'
86
- export { patchSlotRange } from './patch-slot-range.ts'
90
+
91
+ // Claim-plan interpreter (slot unification A2/A3, spec/slot-unification.md)
92
+ // — the ONE content-slot update mechanism, wired up by the compiler in A3.
93
+ // Supersedes (deleted) `patchSlotRange` and `updateClientMarker`.
94
+ export { claimSlots, lazySlots, lazyClaimSlots, textOrNode, type SlotSpec, type ClaimPlan, type ClaimedSlots, type ClaimedSlotsRW, type SlotWriter } from './claim-slots.ts'
87
95
 
88
96
  // Template registry
89
97
  export { registerTemplate, getTemplate, hasTemplate, type TemplateFn } from './template.ts'
@@ -92,11 +100,10 @@ export { registerTemplate, getTemplate, hasTemplate, type TemplateFn } from './t
92
100
  export {
93
101
  createComponent,
94
102
  renderChild,
95
- getPropsUpdateFn,
96
- getComponentProps,
97
103
  parseHTML,
98
104
  escapeAttr,
99
105
  escapeText,
106
+ escapeTextOrNode,
100
107
  } from './component.ts'
101
108
 
102
109
  // Spread props helpers
@@ -111,8 +118,14 @@ export { hydrate, rehydrateAll, rehydrateScope, disposeScope, flushHydration, ge
111
118
  export { registerComponent, getComponentInit, initChild, upsertChild } from './registry.ts'
112
119
  export { insert, type BranchConfig, type BranchTemplateResult } from './insert.ts'
113
120
  export { __bfSlot } from './branch-slot.ts'
121
+ // `__bfText` (dynamic-text.ts) and `$t` (query.ts) are kept: one narrow
122
+ // emission site — a `@client`-nested-inside-a-top-level-conditional dynamic
123
+ // text/JSX expression (`emitDynamicTextUpdates`'s `conditionalElems` path in
124
+ // `ir-to-client-js/emit-reactive.ts`) still uses them, deferred from slot
125
+ // unification A3 (see that function's docstring for why the claim-plan
126
+ // model doesn't fit that one case cleanly). `updateClientMarker` had no
127
+ // such holdout and is fully deleted.
114
128
  export { __bfText } from './dynamic-text.ts'
115
- export { updateClientMarker } from './client-marker.ts'
116
129
 
117
130
  // Hydration state
118
131
  export { hydratedScopes } from './hydration-state.ts'
@@ -338,7 +338,7 @@ export function insert(
338
338
  function autoFocusConditionalElement(region: CondRegion, id: string): void {
339
339
  // Use requestAnimationFrame to defer focus until after DOM updates.
340
340
  // This is necessary because createComponent() may call insert() before
341
- // the element is added to the document by reconcileList().
341
+ // the element is added to the document by mapArray()/mapArrayAnchored().
342
342
  requestAnimationFrame(() => {
343
343
  const condEl = region.anchor
344
344
  ? findCondElInRange(region.anchor, id)
@@ -0,0 +1,100 @@
1
+ /**
2
+ * BarefootJS - Loop Boundary Marker Lookup
3
+ *
4
+ * `<!--bf-loop:<id>-->` / `<!--bf-/loop:<id>-->` comment markers delimit a
5
+ * loop's rendered range inside its container so `mapArray`/`mapArrayAnchored`
6
+ * (`./map-array.ts`) can reconcile only that range without disturbing
7
+ * non-loop siblings. These lookups used to also back the now-removed
8
+ * `reconcileElements`/`reconcileList` element-reconciler (slot unification
9
+ * A4); they remain as the compiler-facing marker API.
10
+ */
11
+
12
+ import { BF_LOOP_START, BF_LOOP_END, loopStartMarker, loopEndMarker } from '@barefootjs/shared'
13
+
14
+ /**
15
+ * Find loop boundary comment markers in a container.
16
+ *
17
+ * `markerId` scopes the lookup to `<!--bf-loop:<id>-->` / `<!--bf-/loop:<id>-->`
18
+ * so sibling loops under the same parent disambiguate (#1087). Without an id,
19
+ * accepts the legacy unscoped form too — used by tests that build containers
20
+ * without compiler-emitted markers.
21
+ */
22
+ function findLoopMarkers(
23
+ container: HTMLElement,
24
+ markerId?: string,
25
+ ): { startMarker: Comment | null; endMarker: Comment | null } {
26
+ let startMarker: Comment | null = null
27
+ let endMarker: Comment | null = null
28
+ if (markerId) {
29
+ const startVal = loopStartMarker(markerId)
30
+ const endVal = loopEndMarker(markerId)
31
+ for (const node of Array.from(container.childNodes)) {
32
+ if (node.nodeType !== Node.COMMENT_NODE) continue
33
+ const value = (node as Comment).nodeValue
34
+ if (value === startVal) startMarker = node as Comment
35
+ else if (value === endVal) endMarker = node as Comment
36
+ }
37
+ } else {
38
+ const startPrefix = `${BF_LOOP_START}:`
39
+ const endPrefix = `${BF_LOOP_END}:`
40
+ for (const node of Array.from(container.childNodes)) {
41
+ if (node.nodeType !== Node.COMMENT_NODE) continue
42
+ const value = (node as Comment).nodeValue ?? ''
43
+ if (!startMarker && (value === BF_LOOP_START || value.startsWith(startPrefix))) {
44
+ startMarker = node as Comment
45
+ } else if (!endMarker && (value === BF_LOOP_END || value.startsWith(endPrefix))) {
46
+ endMarker = node as Comment
47
+ }
48
+ }
49
+ }
50
+ if (startMarker && endMarker) return { startMarker, endMarker }
51
+ return { startMarker: null, endMarker: null }
52
+ }
53
+
54
+ /** Get all Element nodes between start and end comment markers. */
55
+ function getElementsBetweenMarkers(start: Comment, end: Comment): Element[] {
56
+ const elements: Element[] = []
57
+ let node: Node | null = start.nextSibling
58
+ while (node && node !== end) {
59
+ if (node.nodeType === Node.ELEMENT_NODE) {
60
+ elements.push(node as Element)
61
+ }
62
+ node = node.nextSibling
63
+ }
64
+ return elements
65
+ }
66
+
67
+ /**
68
+ * Get loop children from a container, respecting bf-loop boundary markers.
69
+ * When markers are present, returns only elements between them.
70
+ * When absent, returns all children (backward compatible).
71
+ * Exported for use by compiler-generated hydration code.
72
+ */
73
+ export function getLoopChildren(container: HTMLElement, markerId?: string): HTMLElement[] {
74
+ const { startMarker, endMarker } = findLoopMarkers(container, markerId)
75
+ if (startMarker && endMarker) {
76
+ return getElementsBetweenMarkers(startMarker, endMarker) as HTMLElement[]
77
+ }
78
+ return Array.from(container.children) as HTMLElement[]
79
+ }
80
+
81
+ /**
82
+ * Like {@link getLoopChildren}, but returns every node between the loop
83
+ * boundary markers — Comments (per-item `<!--bf-loop-i-->` markers) and
84
+ * text included. The branch-clearing path needs to remove the per-item
85
+ * marker comments alongside elements; otherwise stale markers would
86
+ * accumulate when a branch swap forces mapArray to start over (#1212).
87
+ */
88
+ export function getLoopNodes(container: HTMLElement, markerId?: string): Node[] {
89
+ const { startMarker, endMarker } = findLoopMarkers(container, markerId)
90
+ const nodes: Node[] = []
91
+ if (startMarker && endMarker) {
92
+ let node: Node | null = startMarker.nextSibling
93
+ while (node && node !== endMarker) {
94
+ nodes.push(node)
95
+ node = node.nextSibling
96
+ }
97
+ return nodes
98
+ }
99
+ return Array.from(container.childNodes)
100
+ }