Skip to main content

rdom_core/
clone.rs

1//! `clone_node(id, deep)` โ€” copy a node (or subtree) into a new orphan in
2//! the same arena. Matches MDN semantics: attrs/classes/text preserved,
3//! `parent` on the clone is `None`, event listeners **not** copied.
4//! An element's `Ext` is copied with `Ext::clone`, which acts as the
5//! backend's cloning steps (DOM ยง4.5): a backend whose `Ext` carries
6//! per-activation state implements `Clone` to reset it (rdom-tui's
7//! `TuiExt` does).
8
9use crate::dom::Dom;
10use crate::node::{Node, NodeData};
11use crate::node_id::NodeId;
12
13impl<Ext: Clone> Dom<Ext> {
14    /// Produce an orphan clone of `id`. If `deep`, recursively clone all
15    /// descendants. If not, only the node itself.
16    ///
17    /// Returns the new orphan's `NodeId`. The caller must attach it with
18    /// `append_child` / `insert_before` to make it live in the tree.
19    pub fn clone_node(&mut self, id: NodeId, deep: bool) -> NodeId {
20        let new_id = self.clone_one(id);
21        if !deep {
22            return new_id;
23        }
24        // Iterative post-order (`P7G-CORE-SMALL-1`): each frame is a
25        // source node, its clone and the next source child to clone. A
26        // clone is appended to its parent's clone once its own children
27        // are in, while that parent is still detached โ€” so each append's
28        // ancestor check is O(1) and any depth is safe.
29        let first = |dom: &Self, n: NodeId| dom.get_node(n).and_then(|n| n.first_child);
30        let mut stack = vec![(new_id, first(self, id))];
31        while let Some(top) = stack.last_mut() {
32            let dst = top.0;
33            if let Some(src) = top.1 {
34                top.1 = self.get_node(src).and_then(|n| n.next_sibling);
35                let clone = self.clone_one(src);
36                stack.push((clone, first(self, src)));
37                continue;
38            }
39            stack.pop();
40            if let Some(&(parent, _)) = stack.last() {
41                self.append_child(parent, dst)
42                    .expect("clone_node deep: append failed");
43            }
44        }
45        new_id
46    }
47
48    /// A detached copy of `id` alone: its data (tag, attributes, classes,
49    /// `ext`, text), no children.
50    fn clone_one(&mut self, id: NodeId) -> NodeId {
51        let data = match &self.get_node(id).expect("clone_node: invalid id").data {
52            NodeData::Element {
53                tag,
54                attrs,
55                classes,
56                ext,
57            } => NodeData::Element {
58                tag: tag.clone(),
59                attrs: attrs.clone(),
60                classes: classes.clone(),
61                ext: ext.clone(),
62            },
63            NodeData::Text { data } => NodeData::Text { data: data.clone() },
64            NodeData::Comment { data } => NodeData::Comment { data: data.clone() },
65            NodeData::Fragment => NodeData::Fragment,
66        };
67        self.alloc(Node::new(data))
68    }
69}
70
71#[cfg(test)]
72mod tests {
73    use crate::Dom;
74
75    #[test]
76    fn shallow_clone_copies_tag_and_attrs() {
77        let mut dom: Dom = Dom::new();
78        let el = dom.create_element("div");
79        dom.set_attribute(el, "role", "banner").unwrap();
80        dom.add_class(el, "active").unwrap();
81
82        let c = dom.clone_node(el, false);
83        assert_eq!(dom.node(c).tag_name(), Some("div"));
84        assert_eq!(dom.node(c).get_attribute("role"), Some("banner"));
85        assert!(dom.node(c).has_class("active"));
86        assert!(dom.node(c).parent_node().is_none());
87    }
88
89    #[test]
90    fn shallow_clone_has_no_children() {
91        let mut dom: Dom = Dom::new();
92        let parent = dom.create_element("div");
93        let child = dom.create_element("span");
94        dom.append_child(parent, child).unwrap();
95
96        let c = dom.clone_node(parent, false);
97        assert!(!dom.node(c).has_child_nodes());
98    }
99
100    #[test]
101    fn deep_clone_recursively_copies_children() {
102        let mut dom: Dom = Dom::new();
103        let parent = dom.create_element("div");
104        let child = dom.create_element("span");
105        let text = dom.create_text_node("hello");
106        dom.append_child(child, text).unwrap();
107        dom.append_child(parent, child).unwrap();
108
109        let c = dom.clone_node(parent, true);
110        assert_eq!(dom.node(c).child_element_count(), 1);
111        let first = dom.node(c).first_element_child().unwrap();
112        assert_eq!(first.tag_name(), Some("span"));
113        let first_text = first.first_child().unwrap();
114        assert_eq!(first_text.node_value(), Some("hello"));
115        assert!(dom.is_equal_node(parent, c));
116    }
117
118    #[test]
119    fn clone_text_node() {
120        let mut dom: Dom = Dom::new();
121        let t = dom.create_text_node("hello");
122        let c = dom.clone_node(t, true);
123        assert_eq!(dom.node(c).node_value(), Some("hello"));
124    }
125
126    #[test]
127    fn clone_is_independent() {
128        let mut dom: Dom = Dom::new();
129        let el = dom.create_element("div");
130        dom.set_attribute(el, "class", "original").unwrap();
131
132        let c = dom.clone_node(el, false);
133        dom.set_attribute(c, "class", "modified").unwrap();
134
135        assert_eq!(dom.node(el).get_attribute("class"), Some("original"));
136        assert_eq!(dom.node(c).get_attribute("class"), Some("modified"));
137    }
138
139    #[test]
140    fn cloned_fragment_unwraps_normally_on_append() {
141        let mut dom: Dom = Dom::new();
142        let frag = dom.create_document_fragment();
143        let a = dom.create_element("a");
144        let b = dom.create_element("b");
145        dom.append_child(frag, a).unwrap();
146        dom.append_child(frag, b).unwrap();
147
148        let cloned_frag = dom.clone_node(frag, true);
149        let root = dom.root();
150        dom.append_child(root, cloned_frag).unwrap();
151        // Fragment children moved out; cloned_frag is empty.
152        assert_eq!(dom.node(root).child_element_count(), 2);
153    }
154}