1use cssbox_core::style::ComputedStyle;
6use cssbox_core::tree::{BoxTree, BoxTreeBuilder};
7
8use crate::cascade::resolve_styles;
9use crate::dom::{DomNodeId, DomTree};
10
11pub fn build_box_tree(dom: &DomTree, stylesheets: &[String]) -> BoxTree {
16 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 let mut builder = BoxTreeBuilder::new();
23
24 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 build_children(dom, dom_root, box_root, &mut style_map, &mut builder);
35
36 builder.build()
37}
38
39fn find_layout_root(dom: &DomTree) -> DomNodeId {
41 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
51fn 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 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 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
93pub fn html_to_box_tree(html: &str) -> BoxTree {
95 let dom = crate::html::parse_html_simple(html);
96
97 let mut stylesheets = Vec::new();
99 extract_stylesheets(&dom, dom.root(), &mut stylesheets);
100
101 build_box_tree(&dom, &stylesheets)
102}
103
104fn 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 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); 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 assert!(
174 root_rect.height >= 200.0,
175 "Root height {} should be >= 200",
176 root_rect.height
177 );
178 }
179}