1use crate::dom::Dom;
12use crate::node::NodeData;
13use crate::node_id::NodeId;
14
15impl<Ext> Dom<Ext> {
16 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 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 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 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 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 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 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 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 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 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 assert_eq!(dom.get_element_by_id_within(section, "target"), Some(p));
249 assert!(dom.get_element_by_id_within(p, "target").is_none());
251 }
252
253 #[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 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}