Skip to main content

rdom_core/accessor/
mod.rs

1//! `NodeRef<'a, Ext>` / `NodeMut<'a, Ext>` — ergonomic wrappers around
2//! `(&Dom, NodeId)` / `(&mut Dom, NodeId)` pairs so call sites read DOM-ish:
3//!
4//! ```ignore
5//! dom.node(row).get_attribute("data-sel");
6//! dom.node_mut(hero).set_attribute("role", "banner");
7//! ```
8
9use crate::dom::Dom;
10use crate::dom_string_map::{DomStringMap, DomStringMapMut};
11use crate::error::{DomError, Result};
12use crate::node::{NodeData, NodeType};
13use crate::node_id::NodeId;
14use crate::node_list::NodeList;
15use crate::node_or_string::NodeOrString;
16use crate::token_list::{DomTokenList, DomTokenListMut};
17use crate::tree::AdjacentPosition;
18
19// ─────────────────────────────────────────────────────────────────────
20//  NodeRef
21// ─────────────────────────────────────────────────────────────────────
22
23/// Read-only handle to a node in the arena.
24#[derive(Clone, Copy)]
25pub struct NodeRef<'a, Ext: 'static = ()> {
26    pub(crate) dom: &'a Dom<Ext>,
27    pub(crate) id: NodeId,
28}
29
30impl<'a, Ext: 'static> NodeRef<'a, Ext> {
31    // ── Identity ──────────────────────────────────────────────────
32
33    pub fn id(&self) -> NodeId {
34        self.id
35    }
36
37    /// Borrow the owning `Dom<Ext>`. Lifetime `'a` matches the
38    /// underlying borrow that produced this `NodeRef`, so callers can
39    /// hold the returned reference for the same scope.
40    ///
41    /// Exposed for extension traits (e.g. `rdom-tui::TuiAccessors`)
42    /// that need to reach back to dom-level helpers operating on
43    /// `(dom, id)` pairs (runtime focus, builtin form helpers, etc.).
44    pub fn dom(&self) -> &'a Dom<Ext> {
45        self.dom
46    }
47
48    /// # Panics
49    ///
50    /// Panics if the id behind this `NodeRef` is not live (freed, or a
51    /// stale handle to a recycled slot). `Dom::node` does not validate
52    /// up front; check `Dom::contains` first when the id may be stale.
53    pub fn node_type(&self) -> NodeType {
54        self.dom
55            .get_node(self.id)
56            .map(|n| n.node_type())
57            .unwrap_or_else(|| panic!("NodeRef::node_type on a node that is not live: {}", self.id))
58    }
59
60    /// Canonical `nodeName`: element tag, or `#text` / `#comment` /
61    /// `#document-fragment` for non-elements.
62    ///
63    /// # Panics
64    ///
65    /// Panics if the id is not live; see [`NodeRef::node_type`].
66    pub fn node_name(&self) -> &'a str {
67        let n = self.dom.get_node(self.id).unwrap_or_else(|| {
68            panic!("NodeRef::node_name on a node that is not live: {}", self.id)
69        });
70        match &n.data {
71            NodeData::Element { tag, .. } => tag,
72            NodeData::Text { .. } => "#text",
73            NodeData::Comment { .. } => "#comment",
74            NodeData::Fragment => "#document-fragment",
75        }
76    }
77
78    pub fn tag_name(&self) -> Option<&'a str> {
79        self.dom.get_node(self.id).and_then(|n| n.tag_name())
80    }
81
82    /// Borrow the per-element extension data.
83    ///
84    /// This is the hook by which rdom-tui (or any downstream crate
85    /// parameterizing `Dom<Ext>`) reads presentation / layout / styling
86    /// state attached to each Element. Returns `None` for Text / Comment /
87    /// Fragment nodes — those don't carry `Ext`.
88    pub fn ext(&self) -> Option<&'a Ext> {
89        match &self.dom.get_node(self.id)?.data {
90            NodeData::Element { ext, .. } => Some(ext),
91            _ => None,
92        }
93    }
94
95    pub fn node_value(&self) -> Option<&'a str> {
96        match &self.dom.get_node(self.id)?.data {
97            NodeData::Text { data } | NodeData::Comment { data } => Some(data),
98            _ => None,
99        }
100    }
101
102    /// `CharacterData.data` — MDN alias for Text/Comment `nodeValue`. Same
103    /// behaviour, different name; exists because the spec treats these as
104    /// two different interface members.
105    pub fn data(&self) -> Option<&'a str> {
106        self.node_value()
107    }
108
109    /// `textContent` — concatenate the string content of this node and all
110    /// its descendants. Comments are excluded per spec.
111    pub fn text_content(&self) -> String {
112        self.dom.text_content(self.id)
113    }
114
115    // ── Tree: core navigation ─────────────────────────────────────
116
117    pub fn parent_node(&self) -> Option<NodeRef<'a, Ext>> {
118        let p = self.dom.get_node(self.id)?.parent?;
119        Some(NodeRef {
120            dom: self.dom,
121            id: p,
122        })
123    }
124
125    /// Parent that is an element (skips fragment parents).
126    pub fn parent_element(&self) -> Option<NodeRef<'a, Ext>> {
127        let mut current = self.parent_node();
128        while let Some(p) = current {
129            if p.node_type() == NodeType::Element {
130                return Some(p);
131            }
132            current = p.parent_node();
133        }
134        None
135    }
136
137    pub fn first_child(&self) -> Option<NodeRef<'a, Ext>> {
138        let f = self.dom.get_node(self.id)?.first_child?;
139        Some(NodeRef {
140            dom: self.dom,
141            id: f,
142        })
143    }
144
145    pub fn last_child(&self) -> Option<NodeRef<'a, Ext>> {
146        let l = self.dom.get_node(self.id)?.last_child?;
147        Some(NodeRef {
148            dom: self.dom,
149            id: l,
150        })
151    }
152
153    pub fn previous_sibling(&self) -> Option<NodeRef<'a, Ext>> {
154        let p = self.dom.get_node(self.id)?.prev_sibling?;
155        Some(NodeRef {
156            dom: self.dom,
157            id: p,
158        })
159    }
160
161    pub fn next_sibling(&self) -> Option<NodeRef<'a, Ext>> {
162        let n = self.dom.get_node(self.id)?.next_sibling?;
163        Some(NodeRef {
164            dom: self.dom,
165            id: n,
166        })
167    }
168
169    pub fn has_child_nodes(&self) -> bool {
170        self.dom
171            .get_node(self.id)
172            .and_then(|n| n.first_child)
173            .is_some()
174    }
175
176    pub fn child_nodes(&self) -> ChildIter<'a, Ext> {
177        ChildIter {
178            dom: self.dom,
179            next: self.dom.get_node(self.id).and_then(|n| n.first_child),
180        }
181    }
182
183    // ── Element-only navigation ────────────────────────────────────
184
185    pub fn first_element_child(&self) -> Option<NodeRef<'a, Ext>> {
186        let mut c = self.first_child();
187        while let Some(n) = c {
188            if n.node_type() == NodeType::Element {
189                return Some(n);
190            }
191            c = n.next_sibling();
192        }
193        None
194    }
195
196    pub fn last_element_child(&self) -> Option<NodeRef<'a, Ext>> {
197        let mut c = self.last_child();
198        while let Some(n) = c {
199            if n.node_type() == NodeType::Element {
200                return Some(n);
201            }
202            c = n.previous_sibling();
203        }
204        None
205    }
206
207    pub fn previous_element_sibling(&self) -> Option<NodeRef<'a, Ext>> {
208        let mut s = self.previous_sibling();
209        while let Some(n) = s {
210            if n.node_type() == NodeType::Element {
211                return Some(n);
212            }
213            s = n.previous_sibling();
214        }
215        None
216    }
217
218    pub fn next_element_sibling(&self) -> Option<NodeRef<'a, Ext>> {
219        let mut s = self.next_sibling();
220        while let Some(n) = s {
221            if n.node_type() == NodeType::Element {
222                return Some(n);
223            }
224            s = n.next_sibling();
225        }
226        None
227    }
228
229    pub fn children(&self) -> ElementChildIter<'a, Ext> {
230        ElementChildIter {
231            inner: self.child_nodes(),
232        }
233    }
234
235    pub fn child_element_count(&self) -> usize {
236        self.children().count()
237    }
238
239    // ── Attributes / classes ──────────────────────────────────────
240
241    pub fn id_attr(&self) -> Option<&'a str> {
242        self.get_attribute("id")
243    }
244
245    pub fn get_attribute(&self, key: &str) -> Option<&'a str> {
246        self.dom.get_attribute(self.id, key)
247    }
248
249    pub fn has_attribute(&self, key: &str) -> bool {
250        self.dom.has_attribute(self.id, key)
251    }
252
253    pub fn has_class(&self, class: &str) -> bool {
254        self.dom.has_class(self.id, class)
255    }
256
257    /// Iterate `(name, value)` pairs in deterministic order.
258    pub fn attributes(&self) -> impl Iterator<Item = (&'a str, &'a str)> {
259        self.dom.attributes(self.id)
260    }
261
262    /// Raw `class` attribute value, or `""` if absent. DOM
263    /// `Element.className`.
264    ///
265    /// This is the unparsed string. For token-level access use
266    /// [`Self::class_list`]; for hot-path membership tests use
267    /// [`Self::has_class`].
268    pub fn class_name(&self) -> &'a str {
269        self.get_attribute("class").unwrap_or("")
270    }
271
272    /// Snapshot of the element's class tokens. DOM
273    /// `Element.classList`.
274    ///
275    /// **Hot-path footgun.** Each call allocates a fresh `Vec`
276    /// snapshot. For per-paint or per-event membership checks,
277    /// prefer [`Self::has_class`].
278    pub fn class_list(&self) -> DomTokenList {
279        DomTokenList::from_tokens(self.dom.class_list(self.id).map(str::to_owned))
280    }
281
282    // ── Predicates ────────────────────────────────────────────────
283
284    pub fn contains(&self, other: NodeId) -> bool {
285        self.dom.is_ancestor(self.id, other)
286    }
287
288    pub fn is_same_node(&self, other: NodeId) -> bool {
289        self.id == other
290    }
291
292    /// `true` iff this node is reachable from the document root by
293    /// walking parent pointers. DOM `Node.isConnected`.
294    pub fn is_connected(&self) -> bool {
295        self.dom.is_ancestor(self.dom.root(), self.id)
296    }
297
298    /// Walk parent pointers until none and return the topmost node.
299    /// DOM `Node.getRootNode()` (options ignored — no shadow DOM in
300    /// rdom).
301    pub fn get_root_node(&self) -> NodeRef<'a, Ext> {
302        let mut cur = self.id;
303        loop {
304            match self.dom.get_node(cur).and_then(|n| n.parent) {
305                Some(p) => cur = p,
306                None => {
307                    return NodeRef {
308                        dom: self.dom,
309                        id: cur,
310                    };
311                }
312            }
313        }
314    }
315
316    // ── Selector queries (element-rooted) ─────────────────────────
317
318    /// Does this element match `selector`? DOM `Element.matches`.
319    ///
320    /// **Divergence from browser:** browser throws `SyntaxError` on
321    /// malformed selectors; rdom returns `false`. Authors who need
322    /// to surface parser errors can call
323    /// [`Dom::matches`](crate::Dom::matches) directly.
324    pub fn matches(&self, selector: &str) -> bool {
325        self.dom.matches(self.id, selector).unwrap_or(false)
326    }
327
328    /// Walk from this node (inclusive) up the ancestor chain and
329    /// return the first match. DOM `Element.closest`.
330    ///
331    /// Same parser-error policy as [`Self::matches`].
332    pub fn closest(&self, selector: &str) -> Option<NodeRef<'a, Ext>> {
333        self.dom
334            .closest(self.id, selector)
335            .ok()
336            .flatten()
337            .map(|id| NodeRef { dom: self.dom, id })
338    }
339
340    /// First descendant matching `selector`, in document order. DOM
341    /// `Element.querySelector`. The subject element itself is **not**
342    /// a candidate (spec).
343    ///
344    /// Same parser-error policy as [`Self::matches`].
345    pub fn query_selector(&self, selector: &str) -> Option<NodeRef<'a, Ext>> {
346        self.dom
347            .query_selector_in(self.id, selector)
348            .ok()
349            .flatten()
350            .filter(|&id| id != self.id)
351            .map(|id| NodeRef { dom: self.dom, id })
352    }
353
354    /// All descendants matching `selector`, in document order, as a
355    /// snapshot `NodeList`. DOM `Element.querySelectorAll`. The
356    /// subject element itself is **not** a candidate.
357    ///
358    /// Same parser-error policy as [`Self::matches`]; an unparseable
359    /// selector yields an empty list.
360    pub fn query_selector_all(&self, selector: &str) -> NodeList<'a, Ext> {
361        let ids = self
362            .dom
363            .query_selector_all_in(self.id, selector)
364            .unwrap_or_default()
365            .into_iter()
366            .filter(|&id| id != self.id);
367        NodeList::from_ids(self.dom, ids)
368    }
369
370    // ── HTMLElement IDL accessors (rdom-core, raw shapes) ─────────
371
372    /// DOM `HTMLElement.dataset` — snapshot view of `data-*`
373    /// attributes keyed by their camelCase form.
374    pub fn dataset(&self) -> DomStringMap<'a, Ext> {
375        DomStringMap::new(NodeRef {
376            dom: self.dom,
377            id: self.id,
378        })
379    }
380
381    /// DOM `HTMLElement.tabIndex` — raw `tabindex` attribute
382    /// parsed as `i32`. `None` when the attribute is absent or
383    /// unparseable.
384    ///
385    /// **Note:** this is the *raw* value. The TUI-aware effective
386    /// tab index (honoring implicit focusability per
387    /// `runtime::focus::tabindex`) lives on
388    /// `rdom_tui::TuiAccessors::effective_tab_index` per §13 and
389    /// ships in M4b step 21.
390    pub fn tab_index(&self) -> Option<i32> {
391        self.get_attribute("tabindex")?.parse().ok()
392    }
393
394    /// DOM `HTMLElement.hidden` — `true` iff the `hidden` boolean
395    /// attribute is present.
396    pub fn hidden(&self) -> bool {
397        self.has_attribute("hidden")
398    }
399
400    /// DOM `HTMLElement.contentEditable` getter (HTML §6.8.1): the
401    /// canonical keyword of the attribute's state — `"true"`, `"false"`
402    /// or `"plaintext-only"`, matched ASCII case-insensitively — and
403    /// `"inherit"` when the attribute is missing or invalid.
404    pub fn content_editable(&self) -> &'static str {
405        self.dom
406            .content_editable_state(self.id)
407            .map_or("inherit", crate::ContentEditableState::keyword)
408    }
409
410    /// DOM `Element.innerHTML` getter — markup of this element's
411    /// children. Delegates to `Dom::inner_markup`.
412    ///
413    /// **Hot-path footgun.** Each call re-serializes the entire
414    /// subtree; for tight loops, prefer attribute / child walks
415    /// against the live tree.
416    pub fn inner_html(&self) -> String {
417        self.dom.inner_markup(self.id)
418    }
419
420    /// DOM `Element.outerHTML` getter — markup of this element
421    /// including itself. Delegates to `Dom::outer_markup`.
422    pub fn outer_html(&self) -> String {
423        self.dom.outer_markup(self.id)
424    }
425}
426
427mod node_mut;
428pub use node_mut::NodeMut;
429
430// ─────────────────────────────────────────────────────────────────────
431//  Iterators
432// ─────────────────────────────────────────────────────────────────────
433
434/// Iterator over all direct children (any node type), in document order.
435pub struct ChildIter<'a, Ext: 'static> {
436    dom: &'a Dom<Ext>,
437    next: Option<NodeId>,
438}
439
440impl<'a, Ext: 'static> Iterator for ChildIter<'a, Ext> {
441    type Item = NodeRef<'a, Ext>;
442    fn next(&mut self) -> Option<Self::Item> {
443        let current = self.next?;
444        self.next = self.dom.get_node(current).and_then(|n| n.next_sibling);
445        Some(NodeRef {
446            dom: self.dom,
447            id: current,
448        })
449    }
450}
451
452/// Iterator over element children only.
453pub struct ElementChildIter<'a, Ext: 'static> {
454    inner: ChildIter<'a, Ext>,
455}
456
457impl<'a, Ext: 'static> Iterator for ElementChildIter<'a, Ext> {
458    type Item = NodeRef<'a, Ext>;
459    fn next(&mut self) -> Option<Self::Item> {
460        self.inner
461            .by_ref()
462            .find(|n| n.node_type() == NodeType::Element)
463    }
464}
465
466// ─────────────────────────────────────────────────────────────────────
467//  Dom -> accessor helpers
468// ─────────────────────────────────────────────────────────────────────
469
470impl<Ext> Dom<Ext> {
471    /// Read accessor for `id`. Construction does not validate the id:
472    /// most `NodeRef` methods return `None` / empty for a dead id, but
473    /// the ones that have no empty value (`node_type`, `node_name`)
474    /// panic. Check [`Dom::contains`] first when the id may be stale.
475    pub fn node(&self, id: NodeId) -> NodeRef<'_, Ext> {
476        NodeRef { dom: self, id }
477    }
478
479    /// Mutable accessor for `id`. Same validation contract as
480    /// [`Dom::node`]: mutation methods return `Err(InvalidNode)` for a
481    /// dead id.
482    pub fn node_mut(&mut self, id: NodeId) -> NodeMut<'_, Ext> {
483        NodeMut { dom: self, id }
484    }
485
486    /// Convenience: `NodeRef` for the root.
487    pub fn root_ref(&self) -> NodeRef<'_, Ext> {
488        self.node(self.root())
489    }
490}
491
492#[cfg(test)]
493mod tests;