Skip to main content

rdom_core/
indexes.rs

1//! Indexes: id → `set<NodeId>`, tag → `set<NodeId>`, class → `set<NodeId>`.
2//!
3//! Every mutation entry point calls a hook that keeps these in sync. The
4//! payoff: `get_element_by_id` is a hashmap hit; tag/class getters return
5//! pre-filtered candidate lists. On very large trees (10k+ nodes) this is
6//! orders of magnitude faster than DFS.
7//!
8//! Buckets are `BTreeSet<NodeId>`: O(log n) register / unregister (a
9//! `Vec` made building or tearing down n same-tag nodes O(n²)), and
10//! iteration comes out in arena order, which is the deterministic order
11//! the bulk getters promise.
12//!
13//! ## Invariants
14//!
15//! For every live Element node `E` with id `I`, tag `T`, classes `Cs`:
16//! - `id_index[I]` contains `E` (if `I` is non-empty). When multiple
17//!   elements share an id, `get_element_by_id` returns the first one in
18//!   **document order** among those connected to the root (the web's
19//!   answer); detached duplicates are considered only when no connected
20//!   element carries the id.
21//! - `tag_index[T]` contains `E`.
22//! - For every `c ∈ Cs`, `class_index[c]` contains `E`.
23//!
24//! When `E` is freed (via `free` or `drop_subtree`), it is removed from
25//! every index entry. When `E`'s attrs/classes change, affected entries
26//! are updated atomically.
27
28use std::collections::{BTreeSet, HashMap};
29
30use crate::dom::Dom;
31use crate::node::NodeData;
32use crate::node_id::NodeId;
33
34pub(crate) type Bucket = BTreeSet<NodeId>;
35
36#[derive(Debug, Default, Clone)]
37pub(crate) struct Indexes {
38    pub(crate) by_id: HashMap<String, Bucket>,
39    pub(crate) by_tag: HashMap<String, Bucket>,
40    pub(crate) by_class: HashMap<String, Bucket>,
41}
42
43impl Indexes {
44    fn push_unique(bucket: &mut Bucket, id: NodeId) {
45        bucket.insert(id);
46    }
47
48    fn remove_from(map: &mut HashMap<String, Bucket>, key: &str, id: NodeId) {
49        if let Some(bucket) = map.get_mut(key) {
50            bucket.remove(&id);
51            if bucket.is_empty() {
52                map.remove(key);
53            }
54        }
55    }
56
57    pub(crate) fn register_id(&mut self, id: NodeId, id_value: &str) {
58        if id_value.is_empty() {
59            return;
60        }
61        Self::push_unique(self.by_id.entry(id_value.to_string()).or_default(), id);
62    }
63
64    pub(crate) fn unregister_id(&mut self, id: NodeId, id_value: &str) {
65        if id_value.is_empty() {
66            return;
67        }
68        Self::remove_from(&mut self.by_id, id_value, id);
69    }
70
71    pub(crate) fn register_tag(&mut self, id: NodeId, tag: &str) {
72        Self::push_unique(self.by_tag.entry(tag.to_string()).or_default(), id);
73    }
74
75    pub(crate) fn unregister_tag(&mut self, id: NodeId, tag: &str) {
76        Self::remove_from(&mut self.by_tag, tag, id);
77    }
78
79    pub(crate) fn register_class(&mut self, id: NodeId, class: &str) {
80        Self::push_unique(self.by_class.entry(class.to_string()).or_default(), id);
81    }
82
83    pub(crate) fn unregister_class(&mut self, id: NodeId, class: &str) {
84        Self::remove_from(&mut self.by_class, class, id);
85    }
86}
87
88// ─── Hook helpers (called from dom.rs / attrs.rs / tree.rs) ─────────
89
90impl<Ext> Dom<Ext> {
91    /// Register a newly-allocated Element's tag, id, classes. Non-Element
92    /// nodes are ignored. Called from `alloc` after the node is inserted.
93    pub(crate) fn hook_register(&mut self, id: NodeId) {
94        let Some(node) = self.get_node(id) else {
95            return;
96        };
97        let (tag, id_attr, classes) = match &node.data {
98            NodeData::Element {
99                tag,
100                attrs,
101                classes,
102                ..
103            } => {
104                let tag = tag.clone();
105                let id_attr = attrs.get("id").cloned();
106                let classes: Vec<String> = classes.iter().cloned().collect();
107                (tag, id_attr, classes)
108            }
109            _ => return,
110        };
111        self.indexes.register_tag(id, &tag);
112        if let Some(v) = id_attr {
113            self.indexes.register_id(id, &v);
114        }
115        for c in classes {
116            self.indexes.register_class(id, &c);
117        }
118    }
119
120    /// Remove an Element from every index. Non-Element nodes are ignored.
121    /// Called from `free` before the slot is wiped.
122    pub(crate) fn hook_unregister(&mut self, id: NodeId) {
123        let Some(node) = self.get_node(id) else {
124            return;
125        };
126        let (tag, id_attr, classes) = match &node.data {
127            NodeData::Element {
128                tag,
129                attrs,
130                classes,
131                ..
132            } => {
133                let tag = tag.clone();
134                let id_attr = attrs.get("id").cloned();
135                let classes: Vec<String> = classes.iter().cloned().collect();
136                (tag, id_attr, classes)
137            }
138            _ => return,
139        };
140        self.indexes.unregister_tag(id, &tag);
141        if let Some(v) = id_attr {
142            self.indexes.unregister_id(id, &v);
143        }
144        for c in classes {
145            self.indexes.unregister_class(id, &c);
146        }
147    }
148
149    // ─── Public arena-wide lookups ───────────────────────────────────
150
151    /// The element carrying this `id` attribute, or `None`.
152    ///
153    /// `document.getElementById` semantics for the common case: a hash
154    /// lookup, O(1) when the id is unique. When several elements share
155    /// the id, the first one in **document order** among those connected
156    /// to the root wins (the web's answer); that path walks the few
157    /// candidates' ancestor chains.
158    ///
159    /// Divergence (see `DIVERGENCES.md`): the lookup is arena-wide, so
160    /// a *detached* element is found when no connected element carries
161    /// the id. The web only searches the document tree.
162    pub fn get_element_by_id(&self, id_value: &str) -> Option<NodeId> {
163        let bucket = self.indexes.by_id.get(id_value)?;
164        if bucket.len() == 1 {
165            return bucket.iter().next().copied();
166        }
167        // Duplicate ids: first in document order among the connected
168        // candidates (DOM §4.5 `getElementById` walks the tree in order).
169        // Candidates are few — this is the exceptional path.
170        use crate::position::DocumentPosition;
171        let root = self.root();
172        let mut best: Option<NodeId> = None;
173        for &candidate in bucket {
174            let connected = self.root_of(candidate) == Some(root);
175            if !connected {
176                continue;
177            }
178            best = Some(match best {
179                None => candidate,
180                Some(b)
181                    if self
182                        .compare_document_position(b, candidate)
183                        .contains(DocumentPosition::PRECEDING) =>
184                {
185                    candidate
186                }
187                Some(b) => b,
188            });
189        }
190        best.or_else(|| bucket.iter().next().copied())
191    }
192
193    /// All elements with the given tag name across the entire arena, in
194    /// arena order (creation order, except for recycled slots). The
195    /// wildcard `"*"` returns every element in the arena.
196    pub fn get_elements_by_tag_name_all(&self, tag: &str) -> Vec<NodeId> {
197        if tag == "*" {
198            // Merge the per-tag buckets; arena order for determinism.
199            let mut out: Vec<NodeId> = self
200                .indexes
201                .by_tag
202                .values()
203                .flat_map(|b| b.iter().copied())
204                .collect();
205            out.sort_unstable();
206            out
207        } else {
208            self.indexes
209                .by_tag
210                .get(tag)
211                .map(|b| b.iter().copied().collect())
212                .unwrap_or_default()
213        }
214    }
215
216    /// All elements whose classList contains every class in the whitespace-
217    /// separated `names` string, across the entire arena. Empty `names`
218    /// returns every element.
219    pub fn get_elements_by_class_name_all(&self, names: &str) -> Vec<NodeId> {
220        let wanted: Vec<&str> = names.split_ascii_whitespace().collect();
221        if wanted.is_empty() {
222            return self.get_elements_by_tag_name_all("*");
223        }
224        // Start with the smallest class bucket to minimize the scan.
225        let mut buckets: Vec<&Bucket> = wanted
226            .iter()
227            .filter_map(|w| self.indexes.by_class.get(*w))
228            .collect();
229        if buckets.len() != wanted.len() {
230            return Vec::new(); // one class isn't indexed anywhere
231        }
232        buckets.sort_by_key(|b| b.len());
233        let smallest = buckets[0];
234        // Iterating a `BTreeSet` yields arena order already.
235        smallest
236            .iter()
237            .copied()
238            .filter(|id| buckets[1..].iter().all(|b| b.contains(id)))
239            .collect()
240    }
241}
242
243#[cfg(test)]
244mod tests {
245    use crate::Dom;
246
247    /// `getElementById` with duplicate ids returns the first element in
248    /// **document order**, not the first one created.
249    #[test]
250    fn duplicate_ids_resolve_in_document_order() {
251        let mut dom: Dom = Dom::new();
252        let root = dom.root();
253        let later = dom.create_element("p");
254        dom.set_attribute(later, "id", "dup").unwrap();
255        let earlier = dom.create_element("p");
256        dom.set_attribute(earlier, "id", "dup").unwrap();
257        // `earlier` was created second but sits first in the tree.
258        dom.append_child(root, later).unwrap();
259        dom.insert_before(root, earlier, Some(later)).unwrap();
260        assert_eq!(dom.get_element_by_id("dup"), Some(earlier));
261        // Detaching the first makes the next one in document order win.
262        dom.remove_child_dropping(root, earlier).unwrap();
263        assert_eq!(dom.get_element_by_id("dup"), Some(later));
264    }
265
266    /// Registering and freeing many same-tag nodes keeps the index exact
267    /// (the bucket is a set: no duplicates, empty bucket removed). The
268    /// log-time cost is structural — `Bucket` is a `BTreeSet` — so no
269    /// wall-clock assertion here (it would be flaky under load).
270    #[test]
271    fn tag_index_stays_exact_across_many_nodes() {
272        let mut dom: Dom = Dom::new();
273        let root = dom.root();
274        let ids: Vec<_> = (0..20_000)
275            .map(|_| {
276                let d = dom.create_element("div");
277                dom.append_child(root, d).unwrap();
278                d
279            })
280            .collect();
281        assert_eq!(dom.get_elements_by_tag_name_all("div").len(), 20_000);
282        for id in ids {
283            dom.remove_child_dropping(root, id).unwrap();
284        }
285        assert!(dom.get_elements_by_tag_name_all("div").is_empty());
286        assert!(
287            !dom.indexes.by_tag.contains_key("div"),
288            "empty bucket is removed"
289        );
290        assert!(dom.validate().is_empty());
291    }
292
293    #[test]
294    fn id_index_populated_on_set_attribute() {
295        let mut dom: Dom = Dom::new();
296        let el = dom.create_element("div");
297        dom.set_attribute(el, "id", "main").unwrap();
298        assert_eq!(dom.get_element_by_id("main"), Some(el));
299    }
300
301    #[test]
302    fn id_index_unregisters_on_removal() {
303        let mut dom: Dom = Dom::new();
304        let el = dom.create_element("div");
305        dom.set_attribute(el, "id", "main").unwrap();
306        dom.remove_attribute(el, "id").unwrap();
307        assert_eq!(dom.get_element_by_id("main"), None);
308    }
309
310    #[test]
311    fn id_index_updates_on_reassignment() {
312        let mut dom: Dom = Dom::new();
313        let el = dom.create_element("div");
314        dom.set_attribute(el, "id", "old").unwrap();
315        dom.set_attribute(el, "id", "new").unwrap();
316        assert_eq!(dom.get_element_by_id("old"), None);
317        assert_eq!(dom.get_element_by_id("new"), Some(el));
318    }
319
320    #[test]
321    fn id_index_survives_node_drop() {
322        let mut dom: Dom = Dom::new();
323        let el = dom.create_element("div");
324        dom.set_attribute(el, "id", "main").unwrap();
325        let root = dom.root();
326        dom.append_child(root, el).unwrap();
327        dom.drop_subtree(el).unwrap();
328        assert_eq!(dom.get_element_by_id("main"), None);
329    }
330
331    #[test]
332    fn tag_index_finds_elements() {
333        let mut dom: Dom = Dom::new();
334        let a = dom.create_element("div");
335        let b = dom.create_element("div");
336        let c = dom.create_element("span");
337        let divs = dom.get_elements_by_tag_name_all("div");
338        assert!(divs.contains(&a));
339        assert!(divs.contains(&b));
340        assert!(!divs.contains(&c));
341    }
342
343    #[test]
344    fn tag_index_wildcard_returns_all() {
345        let mut dom: Dom = Dom::new();
346        let _ = dom.create_element("a");
347        let _ = dom.create_element("b");
348        // root is a Fragment, not an Element — not in tag index.
349        let all = dom.get_elements_by_tag_name_all("*");
350        assert_eq!(all.len(), 2);
351    }
352
353    #[test]
354    fn tag_index_clears_on_free() {
355        let mut dom: Dom = Dom::new();
356        let a = dom.create_element("a");
357        assert_eq!(dom.get_elements_by_tag_name_all("a"), vec![a]);
358        let root = dom.root();
359        dom.append_child(root, a).unwrap();
360        dom.drop_subtree(a).unwrap();
361        assert!(dom.get_elements_by_tag_name_all("a").is_empty());
362    }
363
364    #[test]
365    fn class_index_basic() {
366        let mut dom: Dom = Dom::new();
367        let el = dom.create_element("div");
368        dom.add_class(el, "foo").unwrap();
369        assert_eq!(dom.get_elements_by_class_name_all("foo"), vec![el]);
370    }
371
372    #[test]
373    fn class_index_intersection() {
374        let mut dom: Dom = Dom::new();
375        let a = dom.create_element("div");
376        dom.add_class(a, "x").unwrap();
377        dom.add_class(a, "y").unwrap();
378        let b = dom.create_element("div");
379        dom.add_class(b, "x").unwrap(); // only x
380        let c = dom.create_element("div");
381        dom.add_class(c, "y").unwrap(); // only y
382
383        assert_eq!(dom.get_elements_by_class_name_all("x y"), vec![a]);
384        let xs = dom.get_elements_by_class_name_all("x");
385        assert!(xs.contains(&a) && xs.contains(&b));
386    }
387
388    #[test]
389    fn class_index_handles_toggle_and_replace() {
390        let mut dom: Dom = Dom::new();
391        let el = dom.create_element("div");
392        dom.add_class(el, "old").unwrap();
393        assert_eq!(dom.get_elements_by_class_name_all("old"), vec![el]);
394
395        dom.replace_class(el, "old", "new").unwrap();
396        assert!(dom.get_elements_by_class_name_all("old").is_empty());
397        assert_eq!(dom.get_elements_by_class_name_all("new"), vec![el]);
398
399        dom.toggle_class(el, "new").unwrap(); // removes
400        assert!(dom.get_elements_by_class_name_all("new").is_empty());
401    }
402
403    #[test]
404    fn id_attribute_via_set_id_sugar_indexed() {
405        let mut dom: Dom = Dom::new();
406        let el = dom.create_element("div");
407        dom.set_id(el, "hero").unwrap();
408        assert_eq!(dom.get_element_by_id("hero"), Some(el));
409    }
410
411    #[test]
412    fn freed_slot_reuse_does_not_leak_old_index_entries() {
413        let mut dom: Dom = Dom::new();
414        let a = dom.create_element("div");
415        dom.set_attribute(a, "id", "x").unwrap();
416        dom.add_class(a, "c").unwrap();
417        dom.free(a); // drops without structural cleanup — still must unindex
418
419        // Reuse the slot with a new element that has different identity.
420        let b = dom.create_element("span");
421        assert_eq!(dom.get_element_by_id("x"), None);
422        assert!(dom.get_elements_by_class_name_all("c").is_empty());
423        assert_eq!(dom.get_elements_by_tag_name_all("span"), vec![b]);
424        assert!(dom.get_elements_by_tag_name_all("div").is_empty());
425    }
426
427    #[test]
428    fn duplicate_ids_first_wins() {
429        let mut dom: Dom = Dom::new();
430        let a = dom.create_element("div");
431        dom.set_attribute(a, "id", "dup").unwrap();
432        let b = dom.create_element("span");
433        dom.set_attribute(b, "id", "dup").unwrap();
434        assert_eq!(dom.get_element_by_id("dup"), Some(a));
435        // Remove the first — second takes over.
436        dom.remove_attribute(a, "id").unwrap();
437        assert_eq!(dom.get_element_by_id("dup"), Some(b));
438    }
439}