Skip to main content

rdom_core/
node.rs

1//! `Node<Ext>` — internal arena storage + `NodeData` enum + `NodeType`.
2//!
3//! A `Node` is a tree cell: linked-list pointers (parent, first_child,
4//! last_child, prev_sibling, next_sibling) plus per-type payload in
5//! `NodeData`. Nodes carry presentation data (`Ext`) only on the `Element`
6//! variant — Text/Comment/Fragment don't need it.
7
8use std::collections::{BTreeMap, BTreeSet};
9
10use crate::NodeId;
11
12/// Per-type payload.
13#[derive(Debug, Clone)]
14#[non_exhaustive]
15pub enum NodeData<Ext = ()> {
16    Element {
17        tag: String,
18        /// BTreeMap for deterministic iteration (markup round-trips,
19        /// snapshot test stability).
20        attrs: BTreeMap<String, String>,
21        /// classList tokens. Set semantics (no duplicates, membership).
22        classes: BTreeSet<String>,
23        /// Presentation extension (`()` in core, `TuiExt` in rdom-tui).
24        ext: Ext,
25    },
26    Text {
27        data: String,
28    },
29    Comment {
30        data: String,
31    },
32    /// DocumentFragment — a detachable subtree container. Inserting a
33    /// fragment unwraps it: children move to the target, fragment itself
34    /// stays empty and reusable.
35    Fragment,
36}
37
38/// DOM-spec node types with the numeric values the spec assigns.
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
40#[repr(u8)]
41#[non_exhaustive]
42pub enum NodeType {
43    Element = 1,
44    Text = 3,
45    Comment = 8,
46    Fragment = 11,
47}
48
49impl NodeType {
50    /// Canonical node name (mirrors browser `nodeName` for non-elements).
51    pub fn canonical_name(self) -> &'static str {
52        match self {
53            NodeType::Element => "", // element's actual tag; caller handles
54            NodeType::Text => "#text",
55            NodeType::Comment => "#comment",
56            NodeType::Fragment => "#document-fragment",
57        }
58    }
59}
60
61/// Arena node — linked-list pointers + typed payload.
62///
63/// Never exposed directly; the crate surfaces `NodeRef<'_, Ext>` / `NodeMut<'_, Ext>`
64/// wrappers over `(&Dom, NodeId)` pairs.
65#[derive(Debug, Clone)]
66pub(crate) struct Node<Ext = ()> {
67    pub(crate) parent: Option<NodeId>,
68    pub(crate) first_child: Option<NodeId>,
69    pub(crate) last_child: Option<NodeId>,
70    pub(crate) prev_sibling: Option<NodeId>,
71    pub(crate) next_sibling: Option<NodeId>,
72    pub(crate) data: NodeData<Ext>,
73}
74
75impl<Ext> Node<Ext> {
76    pub(crate) fn new(data: NodeData<Ext>) -> Self {
77        Self {
78            parent: None,
79            first_child: None,
80            last_child: None,
81            prev_sibling: None,
82            next_sibling: None,
83            data,
84        }
85    }
86
87    /// Tag name of an Element, or `None` for other types.
88    pub(crate) fn tag_name(&self) -> Option<&str> {
89        match &self.data {
90            NodeData::Element { tag, .. } => Some(tag),
91            _ => None,
92        }
93    }
94
95    pub(crate) fn node_type(&self) -> NodeType {
96        match &self.data {
97            NodeData::Element { .. } => NodeType::Element,
98            NodeData::Text { .. } => NodeType::Text,
99            NodeData::Comment { .. } => NodeType::Comment,
100            NodeData::Fragment => NodeType::Fragment,
101        }
102    }
103
104    /// Clear pointer fields; used when a node is detached.
105    #[allow(dead_code)] // planned helper for Phase 2 (clone_node, etc.)
106    pub(crate) fn unlink(&mut self) {
107        self.parent = None;
108        self.first_child = None;
109        self.last_child = None;
110        self.prev_sibling = None;
111        self.next_sibling = None;
112    }
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118
119    #[test]
120    fn spec_numeric_values_match_mdn() {
121        assert_eq!(NodeType::Element as u8, 1);
122        assert_eq!(NodeType::Text as u8, 3);
123        assert_eq!(NodeType::Comment as u8, 8);
124        assert_eq!(NodeType::Fragment as u8, 11);
125    }
126
127    #[test]
128    fn canonical_names_match_browser() {
129        assert_eq!(NodeType::Text.canonical_name(), "#text");
130        assert_eq!(NodeType::Comment.canonical_name(), "#comment");
131        assert_eq!(NodeType::Fragment.canonical_name(), "#document-fragment");
132    }
133
134    #[test]
135    fn new_element_node_starts_unlinked() {
136        let n: Node<()> = Node::new(NodeData::Element {
137            tag: "div".into(),
138            attrs: BTreeMap::new(),
139            classes: BTreeSet::new(),
140            ext: (),
141        });
142        assert!(n.parent.is_none());
143        assert!(n.first_child.is_none());
144        assert!(n.last_child.is_none());
145        assert!(n.prev_sibling.is_none());
146        assert!(n.next_sibling.is_none());
147        assert_eq!(n.node_type(), NodeType::Element);
148        assert_eq!(n.tag_name(), Some("div"));
149    }
150
151    #[test]
152    fn non_element_tag_name_is_none() {
153        let n: Node<()> = Node::new(NodeData::Text { data: "hi".into() });
154        assert_eq!(n.tag_name(), None);
155        assert_eq!(n.node_type(), NodeType::Text);
156    }
157}