Skip to main content

rdom_core/
node_list.rs

1//! `NodeList` — snapshot wrapper for query-selector-all-style
2//! collections.
3//!
4//! ## Live vs snapshot
5//!
6//! Per the M4 scope lock (parity ledger §25 #2), wrapper **shape**
7//! ships but **liveness** does not. The `NodeId`s in `nodes` are
8//! frozen at construction; if a node gets removed from the tree
9//! between the snapshot and an `item(i)` / `iter` call, the
10//! resolver returns `None` for that slot.
11//!
12//! ## API shape
13//!
14//! Matches the JS `NodeList` interface: `length` / `item(i)` /
15//! iteration. `ids()` is an rdom-side escape hatch that hands back
16//! the raw `&[NodeId]` for callers who want to do their own
17//! resolution.
18
19use crate::accessor::NodeRef;
20use crate::dom::Dom;
21use crate::node_id::NodeId;
22
23/// Snapshot of node ids captured at construction. `item(i)` and
24/// `iter()` resolve against the borrowed `&Dom`; if a node has
25/// been removed since the snapshot, the resolution returns `None`
26/// for that slot (filtered out by `iter()`).
27pub struct NodeList<'a, Ext: 'static> {
28    nodes: Vec<NodeId>,
29    dom: &'a Dom<Ext>,
30}
31
32impl<'a, Ext: 'static> NodeList<'a, Ext> {
33    /// Construct from an iterator of node ids and a borrowed Dom.
34    /// Used by `Dom::query_selector_all` (M4b step 18) and tests.
35    pub fn from_ids(dom: &'a Dom<Ext>, nodes: impl IntoIterator<Item = NodeId>) -> Self {
36        Self {
37            nodes: nodes.into_iter().collect(),
38            dom,
39        }
40    }
41
42    /// Number of node ids in the snapshot. DOM `length`.
43    pub fn len(&self) -> usize {
44        self.nodes.len()
45    }
46
47    /// `true` iff the snapshot has no node ids.
48    pub fn is_empty(&self) -> bool {
49        self.nodes.is_empty()
50    }
51
52    /// Borrow the `i`-th node if (a) the index is in range and
53    /// (b) the underlying id still resolves to a live node. DOM
54    /// `item(i)`.
55    pub fn item(&self, index: usize) -> Option<NodeRef<'a, Ext>> {
56        let id = *self.nodes.get(index)?;
57        if self.dom.contains(id) {
58            Some(self.dom.node(id))
59        } else {
60            None
61        }
62    }
63
64    /// Iterate over the live members of the snapshot in
65    /// snapshot order. Nodes that have been removed from the tree
66    /// since the snapshot was taken are skipped.
67    pub fn iter(&self) -> impl Iterator<Item = NodeRef<'a, Ext>> + '_ {
68        self.nodes
69            .iter()
70            .copied()
71            .filter(|id| self.dom.contains(*id))
72            .map(move |id| self.dom.node(id))
73    }
74
75    /// Raw access to the snapshot's id slice. Escape hatch for
76    /// callers who want to do their own resolution (e.g. compare
77    /// snapshots, build their own indices).
78    pub fn ids(&self) -> &[NodeId] {
79        &self.nodes
80    }
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86
87    #[test]
88    fn snapshot_len_item_ids() {
89        let mut dom: Dom = Dom::new();
90        let a = dom.create_element("a");
91        let b = dom.create_element("b");
92        let c = dom.create_element("c");
93        let list = NodeList::from_ids(&dom, [a, b, c]);
94
95        assert_eq!(list.len(), 3);
96        assert!(!list.is_empty());
97        assert_eq!(list.ids(), &[a, b, c][..]);
98
99        assert_eq!(list.item(0).map(|n| n.id()), Some(a));
100        assert_eq!(list.item(1).map(|n| n.id()), Some(b));
101        assert_eq!(list.item(2).map(|n| n.id()), Some(c));
102        assert_eq!(list.item(3).map(|n| n.id()), None);
103    }
104
105    #[test]
106    fn empty_snapshot_is_empty() {
107        let dom: Dom = Dom::new();
108        let list = NodeList::from_ids(&dom, Vec::<NodeId>::new());
109        assert!(list.is_empty());
110        assert_eq!(list.len(), 0);
111        assert_eq!(list.item(0).map(|n| n.id()), None);
112    }
113
114    #[test]
115    fn iter_yields_snapshot_order() {
116        let mut dom: Dom = Dom::new();
117        let a = dom.create_element("a");
118        let b = dom.create_element("b");
119        let list = NodeList::from_ids(&dom, [a, b]);
120        let ids: Vec<NodeId> = list.iter().map(|n| n.id()).collect();
121        assert_eq!(ids, vec![a, b]);
122    }
123
124    #[test]
125    fn removed_node_returns_none_from_item() {
126        // Snapshot before removal; after removal, item() returns
127        // None for the removed slot, matching JS behavior for a
128        // live NodeList post-removal.
129        let mut dom: Dom = Dom::new();
130        let root = dom.root();
131        let a = dom.create_element("a");
132        let b = dom.create_element("b");
133        dom.append_child(root, a).unwrap();
134        dom.append_child(root, b).unwrap();
135
136        let snapshot_ids = vec![a, b];
137        dom.remove_child(root, a).unwrap();
138        dom.drop_subtree(a).unwrap();
139
140        let list = NodeList::from_ids(&dom, snapshot_ids);
141        // Slot 0 (the removed `a`) resolves to None…
142        assert!(list.item(0).is_none());
143        // …but slot 1 (`b`) is still live.
144        assert_eq!(list.item(1).map(|n| n.id()), Some(b));
145    }
146
147    #[test]
148    fn iter_skips_removed_nodes() {
149        let mut dom: Dom = Dom::new();
150        let root = dom.root();
151        let a = dom.create_element("a");
152        let b = dom.create_element("b");
153        let c = dom.create_element("c");
154        dom.append_child(root, a).unwrap();
155        dom.append_child(root, b).unwrap();
156        dom.append_child(root, c).unwrap();
157
158        let snapshot_ids = vec![a, b, c];
159        dom.remove_child(root, b).unwrap();
160        dom.drop_subtree(b).unwrap();
161
162        let list = NodeList::from_ids(&dom, snapshot_ids);
163        let live: Vec<NodeId> = list.iter().map(|n| n.id()).collect();
164        assert_eq!(live, vec![a, c]);
165    }
166}