Skip to main content

rdom_core/
query.rs

1//! Tree-walking getters: `get_elements_by_tag_name`, `get_elements_by_class_name`.
2//!
3//! DOM spec getters that return *live* collections in the browser; here we
4//! return a `Vec<NodeId>` snapshot — the caller owns the result and re-queries
5//! if the tree has changed. Document order = pre-order DFS.
6//!
7//! Phase 2 ships the DFS implementation. Phase 4 will layer O(1) indexes on
8//! top of the same public API (`get_elements_by_tag_name` becomes an index
9//! probe; same function signature, same results).
10
11use crate::dom::Dom;
12use crate::node::NodeData;
13use crate::node_id::NodeId;
14
15impl<Ext> Dom<Ext> {
16    /// Return descendants of `root_id` whose tag matches `tag`, in document
17    /// order. The special wildcard `"*"` matches every element.
18    ///
19    /// `root_id` itself is NOT included in the result (matches browser
20    /// behaviour: `element.getElementsByTagName("div")` returns descendants
21    /// only). If you also want `root_id` to be considered, wrap it in a
22    /// parent or use `matches` yourself.
23    pub fn get_elements_by_tag_name(&self, root_id: NodeId, tag: &str) -> Vec<NodeId> {
24        let mut out = Vec::new();
25        self.walk_descendants(root_id, &mut |id, data| {
26            if let NodeData::Element { tag: t, .. } = data
27                && (tag == "*" || t == tag)
28            {
29                out.push(id);
30            }
31        });
32        out
33    }
34
35    /// Return descendants of `root_id` whose classList contains *all* of
36    /// the given space-separated class names. Empty `names` returns all
37    /// elements (matches DOM spec for `getElementsByClassName("")`).
38    pub fn get_elements_by_class_name(&self, root_id: NodeId, names: &str) -> Vec<NodeId> {
39        let wanted: Vec<&str> = names.split_ascii_whitespace().collect();
40        let mut out = Vec::new();
41        self.walk_descendants(root_id, &mut |id, data| {
42            if let NodeData::Element { classes, .. } = data
43                && wanted.iter().all(|w| classes.contains(*w))
44            {
45                out.push(id);
46            }
47        });
48        out
49    }
50
51    /// Return the first descendant of `root_id` with the given id attribute.
52    /// Subtree-scoped; for the arena-wide O(1) lookup use
53    /// `Dom::get_element_by_id` (defined in `indexes.rs`).
54    pub fn get_element_by_id_within(&self, root_id: NodeId, id_value: &str) -> Option<NodeId> {
55        let mut found = None;
56        self.walk_descendants(root_id, &mut |id, data| {
57            if found.is_some() {
58                return;
59            }
60            if let NodeData::Element { attrs, .. } = data
61                && attrs.get("id").map(String::as_str) == Some(id_value)
62            {
63                found = Some(id);
64            }
65        });
66        found
67    }
68
69    /// Pre-order DFS of descendants (excluding `root_id` itself). Calls
70    /// `f(id, &data)` for each descendant Element/Text/Comment/Fragment.
71    /// The closure CANNOT mutate the arena (shared borrow).
72    pub(crate) fn walk_descendants<F>(&self, root_id: NodeId, f: &mut F)
73    where
74        F: FnMut(NodeId, &NodeData<Ext>),
75    {
76        let Some(root) = self.get_node(root_id) else {
77            return;
78        };
79        let mut child = root.first_child;
80        while let Some(c) = child {
81            self.walk_subtree(c, f);
82            child = self.get_node(c).and_then(|n| n.next_sibling);
83        }
84    }
85
86    /// Pre-order DFS *including* `id` itself. Iterative — it follows
87    /// the child / sibling / parent links with no stack of its own, so
88    /// any depth is safe (`P7G-CORE-SMALL-1`).
89    pub(crate) fn walk_subtree<F>(&self, id: NodeId, f: &mut F)
90    where
91        F: FnMut(NodeId, &NodeData<Ext>),
92    {
93        let Some(node) = self.get_node(id) else {
94            return;
95        };
96        f(id, &node.data);
97        let mut cur = node.first_child;
98        while let Some(c) = cur {
99            let Some(n) = self.get_node(c) else {
100                return;
101            };
102            f(c, &n.data);
103            cur = n.first_child.or_else(|| self.next_in_subtree(c, id));
104        }
105    }
106
107    /// The pre-order successor of `from` that is not one of its
108    /// descendants, inside `root`'s subtree: `from`'s next sibling, else
109    /// the nearest ancestor's (below `root`). `None` at the subtree's end.
110    fn next_in_subtree(&self, from: NodeId, root: NodeId) -> Option<NodeId> {
111        let mut up = from;
112        while up != root {
113            let n = self.get_node(up)?;
114            if let Some(next) = n.next_sibling {
115                return Some(next);
116            }
117            up = n.parent?;
118        }
119        None
120    }
121}
122
123#[cfg(test)]
124mod tests {
125    use crate::Dom;
126
127    // Build:
128    //   root
129    //     div#outer.alpha
130    //       span.alpha.beta      "hello"
131    //       span.beta
132    //       section
133    //         span.alpha
134    //         p#target.beta
135    fn build() -> (Dom, [crate::NodeId; 6]) {
136        let mut dom: Dom = Dom::new();
137        let root = dom.root();
138
139        let outer = dom.create_element("div");
140        dom.set_attribute(outer, "id", "outer").unwrap();
141        dom.add_class(outer, "alpha").unwrap();
142
143        let s1 = dom.create_element("span");
144        dom.add_class(s1, "alpha").unwrap();
145        dom.add_class(s1, "beta").unwrap();
146        let t = dom.create_text_node("hello");
147        dom.append_child(s1, t).unwrap();
148
149        let s2 = dom.create_element("span");
150        dom.add_class(s2, "beta").unwrap();
151
152        let section = dom.create_element("section");
153        let s3 = dom.create_element("span");
154        dom.add_class(s3, "alpha").unwrap();
155        let p = dom.create_element("p");
156        dom.set_attribute(p, "id", "target").unwrap();
157        dom.add_class(p, "beta").unwrap();
158
159        dom.append_child(section, s3).unwrap();
160        dom.append_child(section, p).unwrap();
161
162        dom.append_child(outer, s1).unwrap();
163        dom.append_child(outer, s2).unwrap();
164        dom.append_child(outer, section).unwrap();
165
166        dom.append_child(root, outer).unwrap();
167
168        (dom, [outer, s1, s2, s3, p, section])
169    }
170
171    #[test]
172    fn tag_name_returns_in_document_order() {
173        let (dom, [_, s1, s2, s3, _, _]) = build();
174        let root = dom.root();
175        let spans = dom.get_elements_by_tag_name(root, "span");
176        assert_eq!(spans, vec![s1, s2, s3]);
177    }
178
179    #[test]
180    fn tag_name_wildcard_matches_every_element() {
181        let (dom, _) = build();
182        let root = dom.root();
183        let all = dom.get_elements_by_tag_name(root, "*");
184        // outer, s1, s2, section, s3, p  =  6 elements (text nodes excluded).
185        assert_eq!(all.len(), 6);
186    }
187
188    #[test]
189    fn tag_name_excludes_root_itself() {
190        let mut dom: Dom = Dom::new();
191        let div = dom.create_element("div");
192        // Asking for "div" on this orphan returns nothing — root not included.
193        assert!(dom.get_elements_by_tag_name(div, "div").is_empty());
194    }
195
196    #[test]
197    fn class_name_single_class_matches() {
198        let (dom, [outer, s1, _, s3, _, _]) = build();
199        let root = dom.root();
200        let alphas = dom.get_elements_by_class_name(root, "alpha");
201        assert_eq!(alphas, vec![outer, s1, s3]);
202    }
203
204    #[test]
205    fn class_name_multiple_classes_requires_all() {
206        let (dom, [_, s1, _, _, _, _]) = build();
207        let root = dom.root();
208        let ab = dom.get_elements_by_class_name(root, "alpha beta");
209        // Only s1 has both.
210        assert_eq!(ab, vec![s1]);
211    }
212
213    #[test]
214    fn class_name_empty_returns_all_elements() {
215        let (dom, _) = build();
216        let root = dom.root();
217        let any = dom.get_elements_by_class_name(root, "");
218        assert_eq!(any.len(), 6);
219    }
220
221    #[test]
222    fn class_name_whitespace_is_tolerated() {
223        let (dom, [_, s1, _, _, _, _]) = build();
224        let root = dom.root();
225        let ab = dom.get_elements_by_class_name(root, "  alpha   beta  ");
226        assert_eq!(ab, vec![s1]);
227    }
228
229    #[test]
230    fn element_by_id_finds_match() {
231        let (dom, [outer, _, _, _, p, _]) = build();
232        let root = dom.root();
233        assert_eq!(dom.get_element_by_id_within(root, "outer"), Some(outer));
234        assert_eq!(dom.get_element_by_id_within(root, "target"), Some(p));
235    }
236
237    #[test]
238    fn element_by_id_missing_returns_none() {
239        let (dom, _) = build();
240        let root = dom.root();
241        assert!(dom.get_element_by_id_within(root, "nope").is_none());
242    }
243
244    #[test]
245    fn element_by_id_scoped_to_subtree() {
246        let (dom, [_, _, _, _, p, section]) = build();
247        // section contains p#target. From section we can find it.
248        assert_eq!(dom.get_element_by_id_within(section, "target"), Some(p));
249        // From p itself (no descendants) we can't find itself.
250        assert!(dom.get_element_by_id_within(p, "target").is_none());
251    }
252
253    /// `P7G-CORE-SMALL-1`: the tree walks are iterative, so a very deep
254    /// tree does not overflow the stack (a test thread has 2 MiB).
255    #[test]
256    fn tree_walks_handle_a_100_000_deep_chain() {
257        const DEPTH: usize = 100_000;
258        let mut dom: Dom = Dom::new();
259        let root = dom.root();
260        // Built bottom-up, so each insertion's cycle check is O(1).
261        let mut top = dom.create_text_node("x");
262        for _ in 0..DEPTH {
263            let parent = dom.create_element("div");
264            dom.append_child(parent, top).unwrap();
265            top = parent;
266        }
267        dom.append_child(root, top).unwrap();
268
269        assert_eq!(dom.elements_by_tag("div").len(), DEPTH);
270        assert_eq!(dom.get_elements_by_tag_name(root, "div").len(), DEPTH);
271        assert_eq!(dom.text_content(root), "x");
272        let copy = dom.clone_node(top, true);
273        assert!(dom.is_equal_node(top, copy));
274        assert_eq!(
275            dom.outer_markup(top),
276            format!("{}x{}", "<div>".repeat(DEPTH), "</div>".repeat(DEPTH))
277        );
278        dom.drop_subtree(copy).unwrap();
279        dom.drop_subtree(top).unwrap();
280        assert_eq!(dom.elements_by_tag("div").len(), 0);
281    }
282}