use cssbox_core::style::ComputedStyle;
use cssbox_core::tree::{BoxTree, BoxTreeBuilder};
use crate::cascade::resolve_styles;
use crate::dom::{DomNodeId, DomTree};
pub fn build_box_tree(dom: &DomTree, stylesheets: &[String]) -> BoxTree {
let element_styles = resolve_styles(dom, stylesheets);
let mut style_map: std::collections::HashMap<DomNodeId, ComputedStyle> =
element_styles.into_iter().collect();
let mut builder = BoxTreeBuilder::new();
let dom_root = find_layout_root(dom);
let root_style = style_map
.remove(&dom_root)
.unwrap_or_else(ComputedStyle::block);
let box_root = builder.root(root_style);
build_children(dom, dom_root, box_root, &mut style_map, &mut builder);
builder.build()
}
fn find_layout_root(dom: &DomTree) -> DomNodeId {
if let Some(body) = dom.find_body() {
return body;
}
if let Some(html) = dom.find_element_by_tag("html") {
return html;
}
dom.root()
}
fn build_children(
dom: &DomTree,
dom_parent: DomNodeId,
box_parent: cssbox_core::tree::NodeId,
style_map: &mut std::collections::HashMap<DomNodeId, ComputedStyle>,
builder: &mut BoxTreeBuilder,
) {
for &child_id in dom.children(dom_parent) {
let child_node = dom.node(child_id);
match &child_node.kind {
crate::dom::DomNodeKind::Text(text) => {
let trimmed = text.trim();
if !trimmed.is_empty() {
builder.text(box_parent, trimmed);
}
}
crate::dom::DomNodeKind::Element { tag, .. } => {
if matches!(
tag.to_lowercase().as_str(),
"script" | "style" | "link" | "meta" | "title" | "head"
) {
continue;
}
let style = style_map.remove(&child_id).unwrap_or_default();
if style.display.is_none() {
continue;
}
let box_child = builder.element(box_parent, style);
build_children(dom, child_id, box_child, style_map, builder);
}
_ => {}
}
}
}
pub fn html_to_box_tree(html: &str) -> BoxTree {
let dom = crate::html::parse_html_simple(html);
let mut stylesheets = Vec::new();
extract_stylesheets(&dom, dom.root(), &mut stylesheets);
build_box_tree(&dom, &stylesheets)
}
fn extract_stylesheets(dom: &DomTree, node: DomNodeId, sheets: &mut Vec<String>) {
let dom_node = dom.node(node);
if let Some(tag) = dom_node.tag_name() {
if tag.eq_ignore_ascii_case("style") {
let mut css = String::new();
for &child in dom.children(node) {
if let Some(text) = dom.node(child).text_content() {
css.push_str(text);
}
}
if !css.is_empty() {
sheets.push(css);
}
}
}
for &child in dom.children(node) {
extract_stylesheets(dom, child, sheets);
}
}
#[cfg(test)]
mod tests {
use super::*;
use cssbox_core::geometry::Size;
use cssbox_core::layout::{compute_layout, FixedWidthTextMeasure};
#[test]
fn test_html_to_box_tree_basic() {
let html = r#"
<div style="width: 200px; height: 100px"></div>
"#;
let tree = html_to_box_tree(html);
assert!(tree.len() >= 2);
let result = compute_layout(&tree, &FixedWidthTextMeasure, Size::new(800.0, 600.0));
let root_rect = result.bounding_rect(tree.root()).unwrap();
assert!(root_rect.width > 0.0);
}
#[test]
fn test_html_to_box_tree_with_style_tag() {
let html = r#"
<style>
.box { width: 100px; height: 50px; }
</style>
<div class="box"></div>
"#;
let tree = html_to_box_tree(html);
assert!(tree.len() >= 2);
}
#[test]
fn test_html_to_box_tree_nested() {
let html = r#"
<div style="width: 400px">
<div style="width: 200px; height: 100px"></div>
<div style="width: 200px; height: 100px"></div>
</div>
"#;
let tree = html_to_box_tree(html);
let result = compute_layout(&tree, &FixedWidthTextMeasure, Size::new(800.0, 600.0));
let root_rect = result.bounding_rect(tree.root()).unwrap();
assert!(
root_rect.height >= 200.0,
"Root height {} should be >= 200",
root_rect.height
);
}
}