Skip to main content

cssbox_dom/
dom.rs

1//! DOM tree types for the layout engine.
2
3use std::collections::HashMap;
4
5/// A unique identifier for a DOM node.
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
7pub struct DomNodeId(pub usize);
8
9/// The type of a DOM node.
10#[derive(Debug, Clone)]
11pub enum DomNodeKind {
12    /// Document root.
13    Document,
14    /// An element with a tag name and attributes.
15    Element {
16        tag: String,
17        attributes: HashMap<String, String>,
18        namespace: String,
19    },
20    /// A text node.
21    Text(String),
22    /// A comment node (ignored for layout).
23    Comment(String),
24}
25
26/// A single DOM node.
27#[derive(Debug, Clone)]
28pub struct DomNode {
29    pub id: DomNodeId,
30    pub kind: DomNodeKind,
31    pub children: Vec<DomNodeId>,
32    pub parent: Option<DomNodeId>,
33}
34
35impl DomNode {
36    pub fn tag_name(&self) -> Option<&str> {
37        match &self.kind {
38            DomNodeKind::Element { tag, .. } => Some(tag),
39            _ => None,
40        }
41    }
42
43    pub fn get_attribute(&self, name: &str) -> Option<&str> {
44        match &self.kind {
45            DomNodeKind::Element { attributes, .. } => attributes.get(name).map(|s| s.as_str()),
46            _ => None,
47        }
48    }
49
50    pub fn text_content(&self) -> Option<&str> {
51        match &self.kind {
52            DomNodeKind::Text(text) => Some(text),
53            _ => None,
54        }
55    }
56
57    pub fn is_element(&self) -> bool {
58        matches!(self.kind, DomNodeKind::Element { .. })
59    }
60}
61
62/// Arena-allocated DOM tree.
63#[derive(Debug, Clone)]
64pub struct DomTree {
65    nodes: Vec<DomNode>,
66    root: DomNodeId,
67}
68
69impl DomTree {
70    pub fn new() -> Self {
71        let root_node = DomNode {
72            id: DomNodeId(0),
73            kind: DomNodeKind::Document,
74            children: Vec::new(),
75            parent: None,
76        };
77        Self {
78            nodes: vec![root_node],
79            root: DomNodeId(0),
80        }
81    }
82
83    pub fn root(&self) -> DomNodeId {
84        self.root
85    }
86
87    pub fn add_element(
88        &mut self,
89        parent: DomNodeId,
90        tag: &str,
91        attributes: HashMap<String, String>,
92    ) -> DomNodeId {
93        let id = DomNodeId(self.nodes.len());
94        self.nodes.push(DomNode {
95            id,
96            kind: DomNodeKind::Element {
97                tag: tag.to_string(),
98                attributes,
99                namespace: "http://www.w3.org/1999/xhtml".to_string(),
100            },
101            children: Vec::new(),
102            parent: Some(parent),
103        });
104        self.nodes[parent.0].children.push(id);
105        id
106    }
107
108    pub fn add_text(&mut self, parent: DomNodeId, text: &str) -> DomNodeId {
109        let id = DomNodeId(self.nodes.len());
110        self.nodes.push(DomNode {
111            id,
112            kind: DomNodeKind::Text(text.to_string()),
113            children: Vec::new(),
114            parent: Some(parent),
115        });
116        self.nodes[parent.0].children.push(id);
117        id
118    }
119
120    pub fn node(&self, id: DomNodeId) -> &DomNode {
121        &self.nodes[id.0]
122    }
123
124    pub fn children(&self, id: DomNodeId) -> &[DomNodeId] {
125        &self.nodes[id.0].children
126    }
127
128    pub fn parent(&self, id: DomNodeId) -> Option<DomNodeId> {
129        self.nodes[id.0].parent
130    }
131
132    pub fn len(&self) -> usize {
133        self.nodes.len()
134    }
135
136    pub fn is_empty(&self) -> bool {
137        self.nodes.is_empty()
138    }
139
140    /// Find the <body> element.
141    pub fn find_body(&self) -> Option<DomNodeId> {
142        self.find_element_by_tag("body")
143    }
144
145    /// Find the first element with a given tag name (depth-first).
146    pub fn find_element_by_tag(&self, tag: &str) -> Option<DomNodeId> {
147        self.find_element_recursive(self.root, tag)
148    }
149
150    fn find_element_recursive(&self, node: DomNodeId, tag: &str) -> Option<DomNodeId> {
151        let n = &self.nodes[node.0];
152        if let Some(t) = n.tag_name() {
153            if t.eq_ignore_ascii_case(tag) {
154                return Some(node);
155            }
156        }
157        for &child in &n.children {
158            if let Some(found) = self.find_element_recursive(child, tag) {
159                return Some(found);
160            }
161        }
162        None
163    }
164
165    /// Find an element by its `id` attribute.
166    pub fn find_element_by_id(&self, id: &str) -> Option<DomNodeId> {
167        self.find_by_attr_recursive(self.root, "id", id)
168    }
169
170    fn find_by_attr_recursive(
171        &self,
172        node: DomNodeId,
173        attr: &str,
174        value: &str,
175    ) -> Option<DomNodeId> {
176        let n = &self.nodes[node.0];
177        if let Some(v) = n.get_attribute(attr) {
178            if v == value {
179                return Some(node);
180            }
181        }
182        for &child in &n.children {
183            if let Some(found) = self.find_by_attr_recursive(child, attr, value) {
184                return Some(found);
185            }
186        }
187        None
188    }
189
190    /// Iterate over all nodes in depth-first order.
191    pub fn iter_dfs(&self) -> DfsIter<'_> {
192        DfsIter {
193            tree: self,
194            stack: vec![self.root],
195        }
196    }
197}
198
199impl Default for DomTree {
200    fn default() -> Self {
201        Self::new()
202    }
203}
204
205/// Depth-first iterator over DOM nodes.
206pub struct DfsIter<'a> {
207    tree: &'a DomTree,
208    stack: Vec<DomNodeId>,
209}
210
211impl<'a> Iterator for DfsIter<'a> {
212    type Item = DomNodeId;
213
214    fn next(&mut self) -> Option<DomNodeId> {
215        let id = self.stack.pop()?;
216        let node = self.tree.node(id);
217        // Push children in reverse order so left children are visited first
218        for child in node.children.iter().rev() {
219            self.stack.push(*child);
220        }
221        Some(id)
222    }
223}
224
225#[cfg(test)]
226mod tests {
227    use super::*;
228
229    #[test]
230    fn test_dom_tree_build() {
231        let mut tree = DomTree::new();
232        let root = tree.root();
233        let body = tree.add_element(root, "body", HashMap::new());
234        let div = tree.add_element(body, "div", HashMap::new());
235        tree.add_text(div, "Hello");
236
237        assert_eq!(tree.len(), 4);
238        assert_eq!(tree.children(root).len(), 1);
239        assert_eq!(tree.find_body(), Some(body));
240    }
241
242    #[test]
243    fn test_find_by_id() {
244        let mut tree = DomTree::new();
245        let root = tree.root();
246        let mut attrs = HashMap::new();
247        attrs.insert("id".to_string(), "target".to_string());
248        let el = tree.add_element(root, "div", attrs);
249
250        assert_eq!(tree.find_element_by_id("target"), Some(el));
251        assert_eq!(tree.find_element_by_id("missing"), None);
252    }
253}