Skip to main content

rdom_core/
tree.rs

1//! Tree mutation: `append_child`, `remove_child`, `insert_before`, etc.
2//!
3//! ## Observer record ordering
4//!
5//! Every detach path (`remove_child`, `replace_child`, `replace_with`,
6//! `clear_children`, `drop_subtree`) routes through
7//! `detach_from_parent`, which clears any interaction state
8//! (`focused` / `hovered` / `pointer_capture` / `selection`) that
9//! pointed into the subtree being removed. The
10//! `InteractionChanged` / `SelectionChanged` records for those
11//! cleanups fire *during* the detach, before the caller's
12//! `ChildListChanged` record. Observers therefore see "effect"
13//! records before the "cause" record. The alternative —
14//! post-detach purge in every caller — would centralize the
15//! ordering at the cost of forgetting it on a future tree-mutation
16//! API. Document, don't decentralize.
17//!
18//! Every mutation maintains the doubly-linked sibling chain + first_child/
19//! last_child + parent invariants. Fragment insertion unwraps the fragment's
20//! children. Cycle detection via `is_ancestor`.
21//!
22//! These are the primitives every user-facing mutation builds on.
23
24use crate::dom::Dom;
25use crate::error::{DomError, Result};
26use crate::node::NodeData;
27use crate::node_id::NodeId;
28use crate::observer::Mutation;
29
30/// Position relative to a reference node, for `insert_adjacent*`.
31///
32/// Mirrors HTML `insertAdjacentElement`:
33///   - `BeforeBegin` — as previous sibling of reference
34///   - `AfterBegin`  — as first child of reference
35///   - `BeforeEnd`   — as last child of reference
36///   - `AfterEnd`    — as next sibling of reference
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub enum AdjacentPosition {
39    BeforeBegin,
40    AfterBegin,
41    BeforeEnd,
42    AfterEnd,
43}
44
45impl<Ext: 'static> Dom<Ext> {
46    /// Append `child` as the last child of `parent`. If `child` is a
47    /// Fragment, its children are appended and the fragment is emptied.
48    ///
49    /// Returns `Err(HierarchyRequest)` if `child` is an ancestor of
50    /// `parent` (would create a cycle), `Err(InvalidNode)` for unknown ids.
51    pub fn append_child(&mut self, parent: NodeId, child: NodeId) -> Result<()> {
52        self.validate_insert(parent, child)?;
53
54        // Fragment: splice its children in, leave the fragment empty.
55        if matches!(
56            self.get_node(child).map(|n| &n.data),
57            Some(NodeData::Fragment)
58        ) {
59            let mut current = self.get_node(child).and_then(|n| n.first_child);
60            // Clear fragment's child pointers up front; we re-link below.
61            if let Some(n) = self.get_node_mut(child) {
62                n.first_child = None;
63                n.last_child = None;
64            }
65            while let Some(c) = current {
66                // Capture next sibling before detaching.
67                let next = self.get_node(c).and_then(|n| n.next_sibling);
68                if let Some(n) = self.get_node_mut(c) {
69                    n.prev_sibling = None;
70                    n.next_sibling = None;
71                    n.parent = None;
72                }
73                self.append_child(parent, c)?;
74                current = next;
75            }
76            return Ok(());
77        }
78
79        // Detach child from current parent if any.
80        self.detach_from_parent(child)?;
81
82        // Link child under parent at the end.
83        let last = self.get_node(parent).and_then(|n| n.last_child);
84        self.get_node_mut(child)
85            .ok_or(DomError::InvalidNode(child))?
86            .parent = Some(parent);
87        self.get_node_mut(child).unwrap().prev_sibling = last;
88        self.get_node_mut(child).unwrap().next_sibling = None;
89
90        match last {
91            Some(prev) => {
92                self.get_node_mut(prev).unwrap().next_sibling = Some(child);
93            }
94            None => {
95                // Empty parent — also becomes first_child.
96                self.get_node_mut(parent).unwrap().first_child = Some(child);
97            }
98        }
99        self.get_node_mut(parent).unwrap().last_child = Some(child);
100        self.fire_mutation(Mutation::ChildListChanged {
101            parent,
102            added: vec![child],
103            removed: vec![],
104        });
105        Ok(())
106    }
107
108    /// Prepend `child` as the first child of `parent`.
109    pub fn prepend_child(&mut self, parent: NodeId, child: NodeId) -> Result<()> {
110        let first = self.get_node(parent).and_then(|n| n.first_child);
111        self.insert_before(parent, child, first)
112    }
113
114    /// Insert `new_child` before `reference_child` within `parent`.
115    /// If `reference_child` is `None`, appends at the end (matches spec
116    /// behavior).
117    pub fn insert_before(
118        &mut self,
119        parent: NodeId,
120        new_child: NodeId,
121        reference_child: Option<NodeId>,
122    ) -> Result<()> {
123        // Null reference → append.
124        let Some(reference) = reference_child else {
125            return self.append_child(parent, new_child);
126        };
127
128        // Reference must be an actual child of parent.
129        if self.get_node(reference).and_then(|n| n.parent) != Some(parent) {
130            return Err(DomError::NotFound);
131        }
132
133        self.validate_insert(parent, new_child)?;
134
135        // Fragment unwrap — iterate children and insert each before reference.
136        if matches!(
137            self.get_node(new_child).map(|n| &n.data),
138            Some(NodeData::Fragment)
139        ) {
140            let mut current = self.get_node(new_child).and_then(|n| n.first_child);
141            if let Some(n) = self.get_node_mut(new_child) {
142                n.first_child = None;
143                n.last_child = None;
144            }
145            while let Some(c) = current {
146                let next = self.get_node(c).and_then(|n| n.next_sibling);
147                if let Some(n) = self.get_node_mut(c) {
148                    n.prev_sibling = None;
149                    n.next_sibling = None;
150                    n.parent = None;
151                }
152                self.insert_before(parent, c, Some(reference))?;
153                current = next;
154            }
155            return Ok(());
156        }
157
158        self.detach_from_parent(new_child)?;
159
160        // Link: prev_of_reference <-> new_child <-> reference
161        let before = self.get_node(reference).and_then(|n| n.prev_sibling);
162        let new_node = self.get_node_mut(new_child).unwrap();
163        new_node.parent = Some(parent);
164        new_node.prev_sibling = before;
165        new_node.next_sibling = Some(reference);
166
167        match before {
168            Some(prev) => {
169                self.get_node_mut(prev).unwrap().next_sibling = Some(new_child);
170            }
171            None => {
172                self.get_node_mut(parent).unwrap().first_child = Some(new_child);
173            }
174        }
175        self.get_node_mut(reference).unwrap().prev_sibling = Some(new_child);
176        self.fire_mutation(Mutation::ChildListChanged {
177            parent,
178            added: vec![new_child],
179            removed: vec![],
180        });
181        Ok(())
182    }
183
184    /// Remove `child` from `parent`. Child is detached (parent + sibling
185    /// pointers cleared) but **remains in the arena as an orphan** — it
186    /// can be reattached elsewhere, or explicitly freed via
187    /// [`drop_subtree`](Self::drop_subtree).
188    ///
189    /// The arena has no GC: a detached node is never reclaimed on its
190    /// own. Code that removes nodes it will **not** reattach — especially
191    /// high-churn UIs (a virtualized list/table re-materializing rows on
192    /// every scroll) — must free them, or arena slots leak. Use
193    /// [`remove_child_dropping`](Self::remove_child_dropping) to remove
194    /// and free in one call.
195    pub fn remove_child(&mut self, parent: NodeId, child: NodeId) -> Result<()> {
196        if self.get_node(child).and_then(|n| n.parent) != Some(parent) {
197            return Err(DomError::NotFound);
198        }
199        self.detach_from_parent(child)?;
200        self.fire_mutation(Mutation::ChildListChanged {
201            parent,
202            added: vec![],
203            removed: vec![child],
204        });
205        Ok(())
206    }
207
208    /// Replace `old_child` with `new_child` under `parent`. `old_child`
209    /// is detached and becomes an orphan.
210    pub fn replace_child(
211        &mut self,
212        parent: NodeId,
213        old_child: NodeId,
214        new_child: NodeId,
215    ) -> Result<()> {
216        if self.get_node(old_child).and_then(|n| n.parent) != Some(parent) {
217            return Err(DomError::NotFound);
218        }
219        self.validate_insert(parent, new_child)?;
220
221        let next = self.get_node(old_child).and_then(|n| n.next_sibling);
222        self.detach_from_parent(old_child)?;
223        self.insert_before(parent, new_child, next)
224    }
225
226    /// `insertAdjacentElement(position, new_child)`. `reference` is the
227    /// node relative to which we insert.
228    pub fn insert_adjacent(
229        &mut self,
230        reference: NodeId,
231        position: AdjacentPosition,
232        new_child: NodeId,
233    ) -> Result<()> {
234        match position {
235            AdjacentPosition::BeforeBegin => {
236                let parent = self
237                    .get_node(reference)
238                    .and_then(|n| n.parent)
239                    .ok_or(DomError::HierarchyRequest)?;
240                self.insert_before(parent, new_child, Some(reference))
241            }
242            AdjacentPosition::AfterBegin => self.prepend_child(reference, new_child),
243            AdjacentPosition::BeforeEnd => self.append_child(reference, new_child),
244            AdjacentPosition::AfterEnd => {
245                let parent = self
246                    .get_node(reference)
247                    .and_then(|n| n.parent)
248                    .ok_or(DomError::HierarchyRequest)?;
249                let after = self.get_node(reference).and_then(|n| n.next_sibling);
250                self.insert_before(parent, new_child, after)
251            }
252        }
253    }
254
255    /// Remove all children from `parent`. They become **orphans in the
256    /// arena** (not freed — see [`remove_child`](Self::remove_child) on
257    /// the no-GC contract). Fires a single `ChildListChanged` record with
258    /// every removed child. To remove and free in one call, use
259    /// [`clear_children_dropping`](Self::clear_children_dropping).
260    pub fn clear_children(&mut self, parent: NodeId) -> Result<()> {
261        self.node_or_err(parent)?;
262        let mut removed: Vec<NodeId> = Vec::new();
263        while let Some(first) = self.get_node(parent).and_then(|n| n.first_child) {
264            removed.push(first);
265            self.detach_from_parent(first)?;
266        }
267        if !removed.is_empty() {
268            self.fire_mutation(Mutation::ChildListChanged {
269                parent,
270                added: vec![],
271                removed,
272            });
273        }
274        Ok(())
275    }
276
277    /// Drop `id` and its entire subtree from the arena — frees every slot.
278    /// Useful when you know you'll never reattach the nodes.
279    ///
280    /// The root cannot be dropped (`HierarchyRequest`): `Dom::root` must
281    /// stay live for the lifetime of the arena. Use
282    /// [`clear_children_dropping`](Self::clear_children_dropping) to
283    /// empty it.
284    pub fn drop_subtree(&mut self, id: NodeId) -> Result<()> {
285        self.node_or_err(id)?;
286        if id == self.root {
287            return Err(DomError::HierarchyRequest);
288        }
289        let parent = self.get_node(id).and_then(|n| n.parent);
290        // Snapshot the subtree to free WHILE it's still alive and before
291        // anything can panic.
292        let mut to_free = Vec::new();
293        self.collect_descendants(id, &mut to_free);
294        // Detach, then fire the mutation BEFORE freeing, so observers (the
295        // dirty tracker, implicit blur/focusout-on-detach) can still read
296        // the removed nodes in their callback — same contract as
297        // `remove_child`, and what the MutationObserver spec requires
298        // (`removedNodes` are inspectable). Both steps fire records
299        // (detach purges focus / hover / selection inside the subtree); a
300        // panicking observer is re-raised by `fire_mutation`, so the whole
301        // window runs under one guard and the slots are reclaimed on the
302        // way out (`CORE-DROP-PANIC-LEAK-1`).
303        let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
304            let _ = self.detach_from_parent(id);
305            if let Some(parent) = parent {
306                self.fire_mutation(Mutation::ChildListChanged {
307                    parent,
308                    added: vec![],
309                    removed: vec![id],
310                });
311            }
312        }));
313        self.free_if_detached(id, to_free);
314        if let Err(payload) = outcome {
315            std::panic::resume_unwind(payload);
316        }
317        Ok(())
318    }
319
320    /// Reclaim `to_free` (a subtree rooted at `root`) — unless `root` is
321    /// still attached, which happens when an observer panicked in the
322    /// `PreDetach` window, before the unlink: then the tree is intact and
323    /// freeing it would leave the parent pointing at dead slots.
324    fn free_if_detached(&mut self, root: NodeId, to_free: Vec<NodeId>) {
325        if self.get_node(root).is_some_and(|n| n.parent.is_some()) {
326            return;
327        }
328        for n in to_free {
329            self.free(n);
330        }
331    }
332
333    /// Remove `child` from `parent` **and free** its subtree from the
334    /// arena (the non-leaking [`remove_child`](Self::remove_child)).
335    /// Use when you won't reattach the removed node. Fires the same
336    /// single `ChildListChanged` record `remove_child` does (the
337    /// follow-up free runs on the already-detached orphan, so it adds no
338    /// extra record). Observers still see the removed node alive in their
339    /// synchronous callback — it's freed only after dispatch returns.
340    pub fn remove_child_dropping(&mut self, parent: NodeId, child: NodeId) -> Result<()> {
341        if self.get_node(child).and_then(|n| n.parent) != Some(parent) {
342            return Err(DomError::NotFound);
343        }
344        let mut to_free = Vec::new();
345        self.collect_descendants(child, &mut to_free);
346        // Same guard as `drop_subtree`: the record fires inside
347        // `remove_child`; a panicking observer must not leak the orphan.
348        let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
349            self.remove_child(parent, child)
350        }));
351        self.free_if_detached(child, to_free);
352        match outcome {
353            Ok(result) => result,
354            Err(payload) => std::panic::resume_unwind(payload),
355        }
356    }
357
358    /// Remove all children from `parent` **and free** their subtrees
359    /// from the arena (the non-leaking [`clear_children`](Self::clear_children)).
360    /// Fires the same single `ChildListChanged` record `clear_children`
361    /// does; the frees run on the already-detached orphans.
362    pub fn clear_children_dropping(&mut self, parent: NodeId) -> Result<()> {
363        self.node_or_err(parent)?;
364        // Snapshot each child's subtree before detaching anything.
365        let mut subtrees: Vec<(NodeId, Vec<NodeId>)> = Vec::new();
366        let mut cur = self.get_node(parent).and_then(|n| n.first_child);
367        while let Some(id) = cur {
368            let mut to_free = Vec::new();
369            self.collect_descendants(id, &mut to_free);
370            subtrees.push((id, to_free));
371            cur = self.get_node(id).and_then(|n| n.next_sibling);
372        }
373        // Detaches all, one batch record; guarded like `drop_subtree`.
374        let outcome =
375            std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| self.clear_children(parent)));
376        for (child, to_free) in subtrees {
377            self.free_if_detached(child, to_free);
378        }
379        match outcome {
380            Ok(result) => result,
381            Err(payload) => std::panic::resume_unwind(payload),
382        }
383    }
384
385    // ── Internal helpers ─────────────────────────────────────────────
386
387    /// Validate that inserting `child` under `parent` is legal.
388    /// Cycle check + id existence.
389    fn validate_insert(&self, parent: NodeId, child: NodeId) -> Result<()> {
390        self.node_or_err(parent)?;
391        self.node_or_err(child)?;
392        if self.is_ancestor(child, parent) {
393            return Err(DomError::HierarchyRequest);
394        }
395        Ok(())
396    }
397
398    /// Detach `id` from its parent. Fixes sibling chain + first/last_child
399    /// on parent. Safe no-op if the node has no parent.
400    ///
401    /// Also clears any interaction state (`focused`, `hovered`,
402    /// `active`, `pointer_capture`, `selection`) that pointed into the
403    /// detached subtree. Without this, a `set_focused`/etc. pointing
404    /// at a now-orphaned node leaves the Dom in an internally
405    /// inconsistent state — `dom.focused()` returns a NodeId that's
406    /// no longer in the tree, and `:focus` keeps matching it.
407    ///
408    /// Record-emission order is: `PreDetach`, then
409    /// `InteractionChanged`/`SelectionChanged` for any cleared
410    /// state — both while the subtree is still connected, so an
411    /// observer can walk the cleared node's ancestors (whose
412    /// `:hover` / `:active` / `:focus-within` flip) — then the
413    /// structural pointer update. The `ChildListChanged` record that motivates the
414    /// detach is fired by the caller (`remove_child`,
415    /// `replace_with`, `clear_children`, ...) AFTER this returns,
416    /// so observers see the interaction-state changes before the
417    /// tree change that caused them. The simpler causal order
418    /// would require each caller to remember a post-detach purge
419    /// step — centralizing in this function trades that
420    /// observability nuance for a structurally-guaranteed cleanup.
421    pub(crate) fn detach_from_parent(&mut self, id: NodeId) -> Result<()> {
422        self.node_or_err(id)?;
423        // **Pre-detach event window** — fire `Mutation::PreDetach`
424        // BEFORE structural unlink, while focused/hovered's
425        // ancestor chains are still intact. Observers (notably
426        // `rdom-tui`'s App-level observer) use this to dispatch
427        // implicit `blur` / `focusout` / `mouseleave` / `mouseout`
428        // events with normal bubbling semantics. Only emitted
429        // when at least one of focused/hovered is actually inside
430        // the subtree being detached — empty PreDetach records
431        // would be noise. Membership is an O(depth) ancestor walk
432        // from the state's node, not a walk of the subtree.
433        let focused_in = self.focused.filter(|&f| self.is_ancestor(id, f));
434        let hovered_in = self.hovered.filter(|&h| self.is_ancestor(id, h));
435        if focused_in.is_some() || hovered_in.is_some() {
436            self.fire_mutation(Mutation::PreDetach {
437                detached_root: id,
438                focused: focused_in,
439                hovered: hovered_in,
440            });
441        }
442
443        // Clear the interaction state inside the subtree while it is
444        // still connected: `:hover`, `:active` and `:focus-within` also
445        // match the ancestors, so an observer of the `InteractionChanged`
446        // record must be able to walk from `prev` to them. A panicking
447        // observer does not stop the unlink (the caller frees a dropped
448        // subtree on the way out); whatever the purge had not reached
449        // yet is cleared silently before the panic resumes.
450        let purged = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
451            self.purge_interaction_state_for_subtree(id, true);
452        }));
453
454        let node = self.node_or_err(id)?;
455        let parent = node.parent;
456        let prev = node.prev_sibling;
457        let next = node.next_sibling;
458
459        if let Some(prev) = prev {
460            self.get_node_mut(prev).unwrap().next_sibling = next;
461        }
462        if let Some(next) = next {
463            self.get_node_mut(next).unwrap().prev_sibling = prev;
464        }
465        if let Some(parent) = parent {
466            if self.get_node(parent).and_then(|n| n.first_child) == Some(id) {
467                self.get_node_mut(parent).unwrap().first_child = next;
468            }
469            if self.get_node(parent).and_then(|n| n.last_child) == Some(id) {
470                self.get_node_mut(parent).unwrap().last_child = prev;
471            }
472        }
473
474        let n = self.get_node_mut(id).unwrap();
475        n.parent = None;
476        n.prev_sibling = None;
477        n.next_sibling = None;
478
479        if let Err(payload) = purged {
480            self.purge_interaction_state_for_subtree(id, false);
481            std::panic::resume_unwind(payload);
482        }
483        Ok(())
484    }
485
486    /// Clear any document-level interaction state
487    /// (`focused`, `hovered`, `active`, `pointer_capture`, `selection`)
488    /// whose referenced node lives inside the subtree rooted at `root`
489    /// (inclusive). Called by `detach_from_parent` so detachment
490    /// can never leave dangling interaction pointers.
491    ///
492    /// With `notify`, each cleared field that has a mutation type
493    /// (`focused`, `hovered`, `active`, `selection`) goes through its
494    /// public setter so the appropriate `InteractionChanged` /
495    /// `SelectionChanged` record fires; without it (the cleanup after a
496    /// panicking observer) the fields are cleared directly.
497    /// `pointer_capture` always clears silently because it has no
498    /// associated record type (it's a runtime-routing flag, not a
499    /// cascade-affecting state).
500    ///
501    /// Membership is `is_ancestor(root, node)`: O(depth) per field, no
502    /// subtree walk, correct before and after `root` is unlinked (the
503    /// links inside the subtree stay).
504    fn purge_interaction_state_for_subtree(&mut self, root: NodeId, notify: bool) {
505        let inside =
506            |dom: &Self, node: Option<NodeId>| node.is_some_and(|n| dom.is_ancestor(root, n));
507        if inside(self, self.focused) {
508            if notify {
509                self.set_focused(None);
510            } else {
511                self.focused = None;
512            }
513        }
514        if inside(self, self.hovered) {
515            if notify {
516                self.set_hovered(None);
517            } else {
518                self.hovered = None;
519            }
520        }
521        if inside(self, self.active) {
522            if notify {
523                self.set_active(None);
524            } else {
525                self.active = None;
526            }
527        }
528        if inside(self, self.pointer_capture) {
529            self.pointer_capture = None;
530        }
531        let selected = self
532            .selection
533            .as_ref()
534            .map(|sel| (sel.anchor.node, sel.focus.node));
535        if let Some((anchor, focus)) = selected
536            && (inside(self, Some(anchor)) || inside(self, Some(focus)))
537        {
538            if notify {
539                self.set_selection(None);
540            } else {
541                self.selection = None;
542                self.selection_serial = self.selection_serial.next();
543            }
544        }
545    }
546
547    /// Depth-first descendants including `root` (iterative). Used by
548    /// `drop_subtree`.
549    fn collect_descendants(&self, root: NodeId, out: &mut Vec<NodeId>) {
550        self.walk_subtree(root, &mut |id, _| out.push(id));
551    }
552}
553
554#[cfg(test)]
555mod tests {
556    use super::*;
557
558    fn sample() -> (Dom, NodeId, NodeId, NodeId) {
559        let mut dom: Dom = Dom::new();
560        let a = dom.create_element("a");
561        let b = dom.create_element("b");
562        let c = dom.create_element("c");
563        (dom, a, b, c)
564    }
565
566    // ── append_child ─────────────────────────────────────────────────
567
568    #[test]
569    fn append_to_empty_parent() {
570        let (mut dom, a, _, _) = sample();
571        let root = dom.root();
572        dom.append_child(root, a).unwrap();
573        assert_eq!(dom.get_node(root).unwrap().first_child, Some(a));
574        assert_eq!(dom.get_node(root).unwrap().last_child, Some(a));
575        assert_eq!(dom.get_node(a).unwrap().parent, Some(root));
576        assert!(dom.get_node(a).unwrap().prev_sibling.is_none());
577        assert!(dom.get_node(a).unwrap().next_sibling.is_none());
578    }
579
580    #[test]
581    fn append_multiple_maintains_sibling_chain() {
582        let (mut dom, a, b, c) = sample();
583        let root = dom.root();
584        dom.append_child(root, a).unwrap();
585        dom.append_child(root, b).unwrap();
586        dom.append_child(root, c).unwrap();
587
588        assert_eq!(dom.get_node(root).unwrap().first_child, Some(a));
589        assert_eq!(dom.get_node(root).unwrap().last_child, Some(c));
590        assert_eq!(dom.get_node(a).unwrap().next_sibling, Some(b));
591        assert_eq!(dom.get_node(b).unwrap().prev_sibling, Some(a));
592        assert_eq!(dom.get_node(b).unwrap().next_sibling, Some(c));
593        assert_eq!(dom.get_node(c).unwrap().prev_sibling, Some(b));
594    }
595
596    #[test]
597    fn append_moves_node_from_old_parent() {
598        let (mut dom, a, b, _) = sample();
599        let root = dom.root();
600        dom.append_child(root, a).unwrap();
601        dom.append_child(a, b).unwrap();
602        dom.append_child(root, b).unwrap(); // re-parent b
603        assert_eq!(dom.get_node(b).unwrap().parent, Some(root));
604        assert!(dom.get_node(a).unwrap().first_child.is_none());
605        assert!(dom.get_node(a).unwrap().last_child.is_none());
606    }
607
608    #[test]
609    fn append_rejects_cycle() {
610        let (mut dom, a, b, _) = sample();
611        let root = dom.root();
612        dom.append_child(root, a).unwrap();
613        dom.append_child(a, b).unwrap();
614        // Try to append a under b — cycle.
615        assert!(matches!(
616            dom.append_child(b, a).unwrap_err(),
617            DomError::HierarchyRequest
618        ));
619    }
620
621    /// `CORE-DROP-PANIC-LEAK-1`: the `ChildListChanged` record fires
622    /// before the slots are freed (observers may inspect the removed
623    /// subtree). A panicking observer must not turn that ordering into
624    /// a leak — the subtree is freed on the way out, then the panic
625    /// continues.
626    #[test]
627    fn drop_subtree_frees_even_when_an_observer_panics() {
628        struct Bomb;
629        impl crate::MutationObserver<()> for Bomb {
630            fn observe(&mut self, _dom: &mut Dom, _record: &crate::Mutation) {
631                panic!("observer bomb");
632            }
633        }
634        let mut dom: Dom = Dom::new();
635        let root = dom.root();
636        let div = dom.create_element("div");
637        let span = dom.create_element("span");
638        dom.append_child(root, div).unwrap();
639        dom.append_child(div, span).unwrap();
640        dom.add_mutation_observer(Box::new(Bomb));
641
642        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
643            dom.drop_subtree(div).unwrap();
644        }));
645        assert!(result.is_err(), "the bomb must actually fire");
646        assert!(!dom.contains(div), "subtree root was freed");
647        assert!(!dom.contains(span), "subtree descendant was freed");
648        assert_eq!(dom.len(), 1, "only the root remains");
649        assert!(dom.validate().is_empty());
650    }
651
652    /// A panicking observer on the *purge* records that `detach_from_parent`
653    /// fires (`InteractionChanged` for a focused descendant, `SelectionChanged`
654    /// for a selection anchored inside) must not leak either: those fire while
655    /// the subtree is still connected, but the unlink still happens (the rest
656    /// of the purge runs silently), so the subtree is freed on the way out.
657    #[test]
658    fn drop_subtree_frees_when_the_focus_purge_observer_panics() {
659        struct Bomb;
660        impl crate::MutationObserver<()> for Bomb {
661            fn observe(&mut self, _dom: &mut Dom, record: &crate::Mutation) {
662                if matches!(record, crate::Mutation::InteractionChanged { .. }) {
663                    panic!("observer bomb");
664                }
665            }
666        }
667        let mut dom: Dom = Dom::new();
668        let root = dom.root();
669        let div = dom.create_element("div");
670        let span = dom.create_element("span");
671        dom.append_child(root, div).unwrap();
672        dom.append_child(div, span).unwrap();
673        dom.set_focused(Some(span));
674        dom.add_mutation_observer(Box::new(Bomb));
675
676        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
677            dom.drop_subtree(div).unwrap();
678        }));
679        assert!(result.is_err(), "the bomb must actually fire");
680        assert!(
681            !dom.contains(div) && !dom.contains(span),
682            "subtree was freed"
683        );
684        assert_eq!(dom.len(), 1);
685        assert_eq!(dom.focused(), None);
686        assert!(dom.validate().is_empty());
687    }
688
689    #[test]
690    fn drop_subtree_frees_when_the_selection_purge_observer_panics() {
691        struct Bomb;
692        impl crate::MutationObserver<()> for Bomb {
693            fn observe(&mut self, _dom: &mut Dom, record: &crate::Mutation) {
694                if matches!(record, crate::Mutation::SelectionChanged { .. }) {
695                    panic!("observer bomb");
696                }
697            }
698        }
699        let mut dom: Dom = Dom::new();
700        let root = dom.root();
701        let div = dom.create_element("div");
702        let text = dom.create_text_node("hello");
703        dom.append_child(root, div).unwrap();
704        dom.append_child(div, text).unwrap();
705        let at = crate::Position {
706            node: text,
707            offset: 1,
708        };
709        dom.set_selection(Some(crate::Selection {
710            anchor: at,
711            focus: at,
712        }));
713        dom.add_mutation_observer(Box::new(Bomb));
714
715        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
716            dom.drop_subtree(div).unwrap();
717        }));
718        assert!(result.is_err(), "the bomb must actually fire");
719        assert!(
720            !dom.contains(div) && !dom.contains(text),
721            "subtree was freed"
722        );
723        assert_eq!(dom.len(), 1);
724        assert!(dom.validate().is_empty());
725    }
726
727    /// `remove_child_dropping` / `clear_children_dropping` fire their
728    /// `ChildListChanged` before the orphan reaches `drop_subtree`; the
729    /// same guarantee holds there.
730    #[test]
731    fn dropping_wrappers_free_when_an_observer_panics() {
732        struct Bomb;
733        impl crate::MutationObserver<()> for Bomb {
734            fn observe(&mut self, _dom: &mut Dom, record: &crate::Mutation) {
735                if matches!(record, crate::Mutation::ChildListChanged { .. }) {
736                    panic!("observer bomb");
737                }
738            }
739        }
740        let mut dom: Dom = Dom::new();
741        let root = dom.root();
742        let a = dom.create_element("a");
743        let a_kid = dom.create_element("kid");
744        let b = dom.create_element("b");
745        let c = dom.create_element("c");
746        dom.append_child(root, a).unwrap();
747        dom.append_child(a, a_kid).unwrap();
748        dom.append_child(root, b).unwrap();
749        dom.append_child(root, c).unwrap();
750        dom.add_mutation_observer(Box::new(Bomb));
751
752        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
753            dom.remove_child_dropping(root, a).unwrap();
754        }));
755        assert!(result.is_err());
756        assert!(
757            !dom.contains(a) && !dom.contains(a_kid),
758            "removed subtree was freed"
759        );
760        assert_eq!(dom.len(), 3, "root, b, c");
761
762        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
763            dom.clear_children_dropping(root).unwrap();
764        }));
765        assert!(result.is_err());
766        assert!(
767            !dom.contains(b) && !dom.contains(c),
768            "cleared children were freed"
769        );
770        assert_eq!(dom.len(), 1);
771        assert!(dom.validate().is_empty());
772    }
773
774    /// The other side of the rule: a panic in the `PreDetach` window
775    /// happens *before* the unlink, so the subtree is still attached and
776    /// must not be freed out from under its parent.
777    #[test]
778    fn drop_subtree_keeps_an_attached_subtree_when_pre_detach_panics() {
779        struct Bomb;
780        impl crate::MutationObserver<()> for Bomb {
781            fn observe(&mut self, _dom: &mut Dom, record: &crate::Mutation) {
782                if matches!(record, crate::Mutation::PreDetach { .. }) {
783                    panic!("observer bomb");
784                }
785            }
786        }
787        let mut dom: Dom = Dom::new();
788        let root = dom.root();
789        let div = dom.create_element("div");
790        let span = dom.create_element("span");
791        dom.append_child(root, div).unwrap();
792        dom.append_child(div, span).unwrap();
793        dom.set_focused(Some(span));
794        dom.add_mutation_observer(Box::new(Bomb));
795
796        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
797            dom.drop_subtree(div).unwrap();
798        }));
799        assert!(result.is_err());
800        assert!(
801            dom.contains(div) && dom.contains(span),
802            "still attached, not freed"
803        );
804        assert_eq!(dom.node(root).first_child().map(|n| n.id()), Some(div));
805        assert!(dom.validate().is_empty());
806    }
807
808    /// The root is the arena's anchor; dropping it would leave `Dom::root`
809    /// pointing at a dead slot and break every later call.
810    #[test]
811    fn drop_subtree_rejects_the_root() {
812        let mut dom: Dom = Dom::new();
813        let root = dom.root();
814        let child = dom.create_element("div");
815        dom.append_child(root, child).unwrap();
816        assert!(matches!(
817            dom.drop_subtree(root).unwrap_err(),
818            DomError::HierarchyRequest
819        ));
820        assert!(dom.contains(root));
821        assert!(dom.contains(child), "nothing was freed");
822        assert!(dom.validate().is_empty());
823    }
824
825    #[test]
826    fn append_rejects_invalid_parent() {
827        let mut dom: Dom = Dom::new();
828        // A NodeId that was never allocated — guaranteed invalid.
829        let ghost = NodeId::from_parts(999, std::num::NonZeroU32::MIN);
830        let child = dom.create_element("child");
831        assert!(matches!(
832            dom.append_child(ghost, child).unwrap_err(),
833            DomError::InvalidNode(_)
834        ));
835    }
836
837    // ── insert_before ─────────────────────────────────────────────────
838
839    #[test]
840    fn insert_before_first_becomes_new_first() {
841        let (mut dom, a, b, _) = sample();
842        let root = dom.root();
843        dom.append_child(root, a).unwrap();
844        dom.insert_before(root, b, Some(a)).unwrap();
845        assert_eq!(dom.get_node(root).unwrap().first_child, Some(b));
846        assert_eq!(dom.get_node(root).unwrap().last_child, Some(a));
847        assert_eq!(dom.get_node(b).unwrap().next_sibling, Some(a));
848        assert_eq!(dom.get_node(a).unwrap().prev_sibling, Some(b));
849    }
850
851    #[test]
852    fn insert_before_middle_updates_chain() {
853        let (mut dom, a, b, c) = sample();
854        let root = dom.root();
855        dom.append_child(root, a).unwrap();
856        dom.append_child(root, c).unwrap();
857        dom.insert_before(root, b, Some(c)).unwrap();
858        // Order should be a, b, c.
859        let names: Vec<_> = iter_children(&dom, root)
860            .map(|id| dom.get_node(id).unwrap().tag_name().unwrap().to_string())
861            .collect();
862        assert_eq!(names, vec!["a", "b", "c"]);
863    }
864
865    #[test]
866    fn insert_before_null_appends() {
867        let (mut dom, a, b, _) = sample();
868        let root = dom.root();
869        dom.append_child(root, a).unwrap();
870        dom.insert_before(root, b, None).unwrap();
871        assert_eq!(dom.get_node(root).unwrap().last_child, Some(b));
872    }
873
874    #[test]
875    fn insert_before_rejects_non_child_reference() {
876        let (mut dom, a, b, _) = sample();
877        let root = dom.root();
878        dom.append_child(root, a).unwrap();
879        // b is not a child of root.
880        assert!(matches!(
881            dom.insert_before(root, a, Some(b)).unwrap_err(),
882            DomError::NotFound
883        ));
884    }
885
886    // ── remove_child ─────────────────────────────────────────────────
887
888    #[test]
889    fn remove_child_detaches_but_keeps_in_arena() {
890        let (mut dom, a, _, _) = sample();
891        let root = dom.root();
892        dom.append_child(root, a).unwrap();
893        dom.remove_child(root, a).unwrap();
894        assert!(dom.get_node(root).unwrap().first_child.is_none());
895        assert!(dom.get_node(a).unwrap().parent.is_none());
896        assert!(dom.contains(a)); // still in arena as orphan
897    }
898
899    #[test]
900    fn remove_middle_child_fixes_siblings() {
901        let (mut dom, a, b, c) = sample();
902        let root = dom.root();
903        dom.append_child(root, a).unwrap();
904        dom.append_child(root, b).unwrap();
905        dom.append_child(root, c).unwrap();
906        dom.remove_child(root, b).unwrap();
907        assert_eq!(dom.get_node(a).unwrap().next_sibling, Some(c));
908        assert_eq!(dom.get_node(c).unwrap().prev_sibling, Some(a));
909    }
910
911    #[test]
912    fn remove_nonchild_errors() {
913        let (mut dom, a, _, _) = sample();
914        let root = dom.root();
915        assert!(matches!(
916            dom.remove_child(root, a).unwrap_err(),
917            DomError::NotFound
918        ));
919    }
920
921    // ── replace_child ────────────────────────────────────────────────
922
923    #[test]
924    fn replace_child_preserves_position() {
925        let (mut dom, a, b, c) = sample();
926        let root = dom.root();
927        dom.append_child(root, a).unwrap();
928        dom.append_child(root, b).unwrap();
929        dom.append_child(root, c).unwrap();
930
931        let d = dom.create_element("d");
932        dom.replace_child(root, b, d).unwrap();
933        let names: Vec<_> = iter_children(&dom, root)
934            .map(|id| dom.get_node(id).unwrap().tag_name().unwrap().to_string())
935            .collect();
936        assert_eq!(names, vec!["a", "d", "c"]);
937    }
938
939    // ── Fragment unwrap ──────────────────────────────────────────────
940
941    #[test]
942    fn fragment_unwraps_on_append() {
943        let mut dom: Dom = Dom::new();
944        let root = dom.root();
945        let frag = dom.create_document_fragment();
946        let a = dom.create_element("a");
947        let b = dom.create_element("b");
948        dom.append_child(frag, a).unwrap();
949        dom.append_child(frag, b).unwrap();
950
951        dom.append_child(root, frag).unwrap();
952
953        // a and b are now direct children of root; frag is empty.
954        assert_eq!(dom.get_node(root).unwrap().first_child, Some(a));
955        assert_eq!(dom.get_node(root).unwrap().last_child, Some(b));
956        assert!(dom.get_node(frag).unwrap().first_child.is_none());
957    }
958
959    #[test]
960    fn fragment_unwraps_on_insert_before() {
961        let mut dom: Dom = Dom::new();
962        let root = dom.root();
963        let existing = dom.create_element("existing");
964        dom.append_child(root, existing).unwrap();
965
966        let frag = dom.create_document_fragment();
967        let x = dom.create_element("x");
968        let y = dom.create_element("y");
969        dom.append_child(frag, x).unwrap();
970        dom.append_child(frag, y).unwrap();
971
972        dom.insert_before(root, frag, Some(existing)).unwrap();
973
974        let names: Vec<_> = iter_children(&dom, root)
975            .map(|id| dom.get_node(id).unwrap().tag_name().unwrap().to_string())
976            .collect();
977        assert_eq!(names, vec!["x", "y", "existing"]);
978    }
979
980    // ── insert_adjacent ──────────────────────────────────────────────
981
982    #[test]
983    fn insert_adjacent_before_begin() {
984        let (mut dom, a, b, _) = sample();
985        let root = dom.root();
986        dom.append_child(root, a).unwrap();
987        dom.insert_adjacent(a, AdjacentPosition::BeforeBegin, b)
988            .unwrap();
989        assert_eq!(dom.get_node(root).unwrap().first_child, Some(b));
990        assert_eq!(dom.get_node(b).unwrap().next_sibling, Some(a));
991    }
992
993    #[test]
994    fn insert_adjacent_after_end() {
995        let (mut dom, a, b, _) = sample();
996        let root = dom.root();
997        dom.append_child(root, a).unwrap();
998        dom.insert_adjacent(a, AdjacentPosition::AfterEnd, b)
999            .unwrap();
1000        assert_eq!(dom.get_node(a).unwrap().next_sibling, Some(b));
1001        assert_eq!(dom.get_node(root).unwrap().last_child, Some(b));
1002    }
1003
1004    #[test]
1005    fn insert_adjacent_after_begin_prepends() {
1006        let (mut dom, a, b, c) = sample();
1007        let root = dom.root();
1008        dom.append_child(root, a).unwrap();
1009        dom.append_child(root, b).unwrap();
1010        // c becomes new first child of root.
1011        dom.insert_adjacent(root, AdjacentPosition::AfterBegin, c)
1012            .unwrap();
1013        assert_eq!(dom.get_node(root).unwrap().first_child, Some(c));
1014    }
1015
1016    // ── clear + drop ─────────────────────────────────────────────────
1017
1018    #[test]
1019    fn clear_children_detaches_all() {
1020        let (mut dom, a, b, c) = sample();
1021        let root = dom.root();
1022        dom.append_child(root, a).unwrap();
1023        dom.append_child(root, b).unwrap();
1024        dom.append_child(root, c).unwrap();
1025
1026        dom.clear_children(root).unwrap();
1027        assert!(dom.get_node(root).unwrap().first_child.is_none());
1028        // Children are orphans but still in arena.
1029        assert!(dom.contains(a));
1030        assert!(dom.contains(b));
1031        assert!(dom.contains(c));
1032        assert!(dom.get_node(a).unwrap().parent.is_none());
1033    }
1034
1035    #[test]
1036    fn remove_child_dropping_frees_the_node() {
1037        let (mut dom, a, b, _c) = sample();
1038        let root = dom.root();
1039        dom.append_child(root, a).unwrap();
1040        dom.append_child(a, b).unwrap();
1041
1042        dom.remove_child_dropping(root, a).unwrap();
1043        // Detached AND freed — the whole subtree is gone from the arena.
1044        assert!(!dom.contains(a));
1045        assert!(!dom.contains(b));
1046        assert!(dom.get_node(root).unwrap().first_child.is_none());
1047    }
1048
1049    #[test]
1050    fn clear_children_dropping_frees_all() {
1051        let (mut dom, a, b, c) = sample();
1052        let root = dom.root();
1053        dom.append_child(root, a).unwrap();
1054        dom.append_child(root, b).unwrap();
1055        dom.append_child(root, c).unwrap();
1056
1057        dom.clear_children_dropping(root).unwrap();
1058        assert!(dom.get_node(root).unwrap().first_child.is_none());
1059        // Unlike clear_children, every child is freed, not orphaned.
1060        assert!(!dom.contains(a));
1061        assert!(!dom.contains(b));
1062        assert!(!dom.contains(c));
1063    }
1064
1065    #[test]
1066    fn drop_subtree_frees_everything() {
1067        let mut dom: Dom = Dom::new();
1068        let root = dom.root();
1069        let a = dom.create_element("a");
1070        let b = dom.create_element("b");
1071        let c = dom.create_element("c");
1072        dom.append_child(root, a).unwrap();
1073        dom.append_child(a, b).unwrap();
1074        dom.append_child(b, c).unwrap();
1075
1076        dom.drop_subtree(a).unwrap();
1077        assert!(!dom.contains(a));
1078        assert!(!dom.contains(b));
1079        assert!(!dom.contains(c));
1080        assert!(dom.get_node(root).unwrap().first_child.is_none());
1081    }
1082
1083    // ── helpers ──────────────────────────────────────────────────────
1084
1085    fn iter_children(dom: &Dom, parent: NodeId) -> impl Iterator<Item = NodeId> + '_ {
1086        let mut cur = dom.get_node(parent).and_then(|n| n.first_child);
1087        std::iter::from_fn(move || {
1088            let c = cur?;
1089            cur = dom.get_node(c).and_then(|n| n.next_sibling);
1090            Some(c)
1091        })
1092    }
1093}