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;