Skip to main content

cssbox_dom/
computed.rs

1//! Specified → computed value resolution.
2//!
3//! Handles CSS inheritance, initial values, and value computation.
4
5use cssbox_core::style::ComputedStyle;
6use cssbox_core::tree::{BoxTree, BoxTreeBuilder};
7
8use crate::cascade::resolve_styles;
9use crate::dom::{DomNodeId, DomTree};
10
11/// Build a BoxTree from a DomTree with resolved styles.
12///
13/// This is the main integration point: takes HTML/CSS input and produces
14/// the tree structure that the layout engine expects.
15pub fn build_box_tree(dom: &DomTree, stylesheets: &[String]) -> BoxTree {
16    // 1. Resolve styles for all elements
17    let element_styles = resolve_styles(dom, stylesheets);
18    let mut style_map: std::collections::HashMap<DomNodeId, ComputedStyle> =
19        element_styles.into_iter().collect();
20
21    // 2. Build box tree
22    let mut builder = BoxTreeBuilder::new();
23
24    // Find the root element (typically <html> or <body>)
25    let dom_root = find_layout_root(dom);
26
27    let root_style = style_map
28        .remove(&dom_root)
29        .unwrap_or_else(ComputedStyle::block);
30
31    let box_root = builder.root(root_style);
32
33    // 3. Recursively build children
34    build_children(dom, dom_root, box_root, &mut style_map, &mut builder);
35
36    builder.build()
37}
38
39/// Find the element to use as the layout root.
40fn find_layout_root(dom: &DomTree) -> DomNodeId {
41    // Prefer <body>, fall back to <html>, then document root
42    if let Some(body) = dom.find_body() {
43        return body;
44    }
45    if let Some(html) = dom.find_element_by_tag("html") {
46        return html;
47    }
48    dom.root()
49}
50
51/// Recursively build box tree children from DOM children.
52fn build_children(
53    dom: &DomTree,
54    dom_parent: DomNodeId,
55    box_parent: cssbox_core::tree::NodeId,
56    style_map: &mut std::collections::HashMap<DomNodeId, ComputedStyle>,
57    builder: &mut BoxTreeBuilder,
58) {
59    for &child_id in dom.children(dom_parent) {
60        let child_node = dom.node(child_id);
61
62        match &child_node.kind {
63            crate::dom::DomNodeKind::Text(text) => {
64                let trimmed = text.trim();
65                if !trimmed.is_empty() {
66                    builder.text(box_parent, trimmed);
67                }
68            }
69            crate::dom::DomNodeKind::Element { tag, .. } => {
70                // Skip non-visual elements
71                if matches!(
72                    tag.to_lowercase().as_str(),
73                    "script" | "style" | "link" | "meta" | "title" | "head"
74                ) {
75                    continue;
76                }
77
78                let style = style_map.remove(&child_id).unwrap_or_default();
79
80                // Skip display: none
81                if style.display.is_none() {
82                    continue;
83                }
84
85                let box_child = builder.element(box_parent, style);
86                build_children(dom, child_id, box_child, style_map, builder);
87            }
88            _ => {}
89        }
90    }
91}
92
93/// High-level function: parse HTML + CSS and produce a BoxTree ready for layout.
94pub fn html_to_box_tree(html: &str) -> BoxTree {
95    let dom = crate::html::parse_html_simple(html);
96
97    // Extract <style> contents
98    let mut stylesheets = Vec::new();
99    extract_stylesheets(&dom, dom.root(), &mut stylesheets);
100
101    build_box_tree(&dom, &stylesheets)
102}
103
104/// Extract CSS from <style> elements in the DOM.
105fn extract_stylesheets(dom: &DomTree, node: DomNodeId, sheets: &mut Vec<String>) {
106    let dom_node = dom.node(node);
107
108    if let Some(tag) = dom_node.tag_name() {
109        if tag.eq_ignore_ascii_case("style") {
110            // Collect text content from children
111            let mut css = String::new();
112            for &child in dom.children(node) {
113                if let Some(text) = dom.node(child).text_content() {
114                    css.push_str(text);
115                }
116            }
117            if !css.is_empty() {
118                sheets.push(css);
119            }
120        }
121    }
122
123    for &child in dom.children(node) {
124        extract_stylesheets(dom, child, sheets);
125    }
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131    use cssbox_core::geometry::Size;
132    use cssbox_core::layout::{compute_layout, FixedWidthTextMeasure};
133
134    #[test]
135    fn test_html_to_box_tree_basic() {
136        let html = r#"
137            <div style="width: 200px; height: 100px"></div>
138        "#;
139        let tree = html_to_box_tree(html);
140        assert!(tree.len() >= 2); // root + div
141
142        let result = compute_layout(&tree, &FixedWidthTextMeasure, Size::new(800.0, 600.0));
143        let root_rect = result.bounding_rect(tree.root()).unwrap();
144        assert!(root_rect.width > 0.0);
145    }
146
147    #[test]
148    fn test_html_to_box_tree_with_style_tag() {
149        let html = r#"
150            <style>
151                .box { width: 100px; height: 50px; }
152            </style>
153            <div class="box"></div>
154        "#;
155        let tree = html_to_box_tree(html);
156        assert!(tree.len() >= 2);
157    }
158
159    #[test]
160    fn test_html_to_box_tree_nested() {
161        let html = r#"
162            <div style="width: 400px">
163                <div style="width: 200px; height: 100px"></div>
164                <div style="width: 200px; height: 100px"></div>
165            </div>
166        "#;
167        let tree = html_to_box_tree(html);
168
169        let result = compute_layout(&tree, &FixedWidthTextMeasure, Size::new(800.0, 600.0));
170        let root_rect = result.bounding_rect(tree.root()).unwrap();
171        // Root height includes the outer div which wraps two 100px children
172        // The exact height depends on the DOM structure produced by parsing
173        assert!(
174            root_rect.height >= 200.0,
175            "Root height {} should be >= 200",
176            root_rect.height
177        );
178    }
179}