use thiserror::Error;
use crate::html_css::css::{
cascade, parse_property, ComputedStyles, ResolvedValue, Stylesheet, Value,
};
use crate::html_css::html::{Dom, NodeId, NodeKind};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DisplayOutside {
Block,
Inline,
ListItem,
Other,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DisplayInside {
Flow,
FlowRoot,
Flex,
Grid,
Table,
InlineBlock,
None,
Contents,
}
impl DisplayInside {
pub fn generates_box(self) -> bool {
!matches!(self, DisplayInside::None | DisplayInside::Contents)
}
}
pub type BoxId = u32;
#[derive(Debug, Clone)]
pub struct BoxNode {
pub element: Option<NodeId>,
pub kind: BoxKind,
pub outside: DisplayOutside,
pub inside: DisplayInside,
pub children: Vec<BoxId>,
pub parent: Option<BoxId>,
}
#[derive(Debug, Clone)]
pub enum BoxKind {
Element,
AnonymousBlock,
Text(String),
}
#[derive(Debug, Clone, Default)]
pub struct BoxTree {
pub boxes: Vec<BoxNode>,
}
impl BoxTree {
pub const ROOT: BoxId = 0;
pub fn get(&self, id: BoxId) -> &BoxNode {
&self.boxes[id as usize]
}
pub fn len(&self) -> usize {
self.boxes.len()
}
pub fn is_empty(&self) -> bool {
self.boxes.len() <= 1
}
pub fn iter_ids(&self) -> Vec<BoxId> {
let mut out = Vec::with_capacity(self.boxes.len());
let mut stack: Vec<BoxId> = vec![Self::ROOT];
while let Some(id) = stack.pop() {
out.push(id);
for &c in self.boxes[id as usize].children.iter().rev() {
stack.push(c);
}
}
out
}
}
#[derive(Debug, Clone, Error, PartialEq)]
pub enum BoxTreeError {
#[error("invalid display value on element {0}")]
InvalidDisplay(NodeId),
}
pub fn build_box_tree(dom: &Dom, stylesheet: &Stylesheet<'_>) -> Result<BoxTree, BoxTreeError> {
let mut tree = BoxTree::default();
tree.boxes.push(BoxNode {
element: None,
kind: BoxKind::AnonymousBlock,
outside: DisplayOutside::Block,
inside: DisplayInside::FlowRoot,
children: Vec::new(),
parent: None,
});
let doc_node = dom.node(Dom::ROOT);
for &child_id in &doc_node.children {
build_subtree(dom, child_id, BoxTree::ROOT, stylesheet, None, &mut tree)?;
}
insert_anonymous_blocks(&mut tree);
Ok(tree)
}
fn build_subtree<'i>(
dom: &Dom,
node_id: NodeId,
parent_box: BoxId,
stylesheet: &'i Stylesheet<'i>,
parent_styles: Option<&ComputedStyles<'i>>,
tree: &mut BoxTree,
) -> Result<(), BoxTreeError> {
match &dom.node(node_id).kind {
NodeKind::Element { .. } => {
let element = dom
.element(node_id)
.expect("NodeKind::Element guarantees DomElement");
let styles = cascade(stylesheet, element, parent_styles);
let tag_for_default = match &dom.node(node_id).kind {
NodeKind::Element { tag, .. } => Some(tag.as_str()),
_ => None,
};
let (outside, inside) = resolve_display_for(&styles, tag_for_default);
if matches!(inside, DisplayInside::None) {
return Ok(()); }
if matches!(inside, DisplayInside::Contents) {
for &child in &dom.node(node_id).children {
build_subtree(dom, child, parent_box, stylesheet, Some(&styles), tree)?;
}
return Ok(());
}
let box_id = push_box(
tree,
BoxNode {
element: Some(node_id),
kind: BoxKind::Element,
outside,
inside,
children: Vec::new(),
parent: Some(parent_box),
},
);
for &child in &dom.node(node_id).children {
build_subtree(dom, child, box_id, stylesheet, Some(&styles), tree)?;
}
},
NodeKind::Text(s) => {
push_box(
tree,
BoxNode {
element: None,
kind: BoxKind::Text(s.clone()),
outside: DisplayOutside::Inline,
inside: DisplayInside::Flow,
children: Vec::new(),
parent: Some(parent_box),
},
);
},
NodeKind::Comment(_) | NodeKind::RawText { .. } | NodeKind::Document => {
},
}
Ok(())
}
fn push_box(tree: &mut BoxTree, mut node: BoxNode) -> BoxId {
let id = tree.boxes.len() as BoxId;
let parent = node.parent.expect("non-root boxes always have a parent");
tree.boxes.push(node.clone());
tree.boxes[parent as usize].children.push(id);
let _ = &mut node;
id
}
#[allow(dead_code)] fn resolve_display(styles: &ComputedStyles<'_>) -> (DisplayOutside, DisplayInside) {
resolve_display_for(styles, None)
}
fn resolve_display_for(
styles: &ComputedStyles<'_>,
tag: Option<&str>,
) -> (DisplayOutside, DisplayInside) {
let display = styles
.get("display")
.and_then(|rv| typed_keyword(rv))
.unwrap_or_else(|| ua_default_display(tag).to_string());
match display.as_str() {
"none" => (DisplayOutside::Block, DisplayInside::None),
"contents" => (DisplayOutside::Inline, DisplayInside::Contents),
"block" => (DisplayOutside::Block, DisplayInside::Flow),
"flow-root" => (DisplayOutside::Block, DisplayInside::FlowRoot),
"inline" => (DisplayOutside::Inline, DisplayInside::Flow),
"inline-block" => (DisplayOutside::Inline, DisplayInside::InlineBlock),
"flex" => (DisplayOutside::Block, DisplayInside::Flex),
"inline-flex" => (DisplayOutside::Inline, DisplayInside::Flex),
"grid" => (DisplayOutside::Block, DisplayInside::Grid),
"inline-grid" => (DisplayOutside::Inline, DisplayInside::Grid),
"list-item" => (DisplayOutside::ListItem, DisplayInside::Flow),
"table" => (DisplayOutside::Block, DisplayInside::Table),
"inline-table" => (DisplayOutside::Inline, DisplayInside::Table),
"table-row" | "table-row-group" | "table-header-group" | "table-footer-group"
| "table-cell" | "table-caption" | "table-column" | "table-column-group" => {
(DisplayOutside::Other, DisplayInside::Flow)
},
_ => (DisplayOutside::Block, DisplayInside::Flow),
}
}
fn ua_default_display(tag: Option<&str>) -> &'static str {
let Some(tag) = tag else {
return "inline";
};
match tag {
"html" | "body" | "div" | "p" | "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "header"
| "footer" | "main" | "section" | "article" | "aside" | "nav" | "address"
| "blockquote" | "figure" | "figcaption" | "form" | "fieldset" | "hr" | "pre"
| "dialog" => "block",
"ul" | "ol" | "menu" | "dl" => "block",
"li" | "dt" | "dd" => "list-item",
"table" => "table",
"thead" => "table-header-group",
"tbody" => "table-row-group",
"tfoot" => "table-footer-group",
"tr" => "table-row",
"td" | "th" => "table-cell",
"caption" => "table-caption",
"col" => "table-column",
"colgroup" => "table-column-group",
"head" | "title" | "meta" | "link" | "style" | "script" => "none",
_ => "inline",
}
}
fn typed_keyword(rv: &ResolvedValue<'_>) -> Option<String> {
match parse_property("display", &rv.value) {
Ok(Value::Keyword(s)) => Some(s),
_ => None,
}
}
fn insert_anonymous_blocks(tree: &mut BoxTree) {
let mut i = 0u32;
while (i as usize) < tree.boxes.len() {
let host = &tree.boxes[i as usize];
if matches!(host.outside, DisplayOutside::Block | DisplayOutside::ListItem)
&& matches!(host.inside, DisplayInside::Flow | DisplayInside::FlowRoot)
{
let kids: Vec<BoxId> = host.children.clone();
let has_block = kids.iter().any(|&c| {
matches!(
tree.boxes[c as usize].outside,
DisplayOutside::Block | DisplayOutside::ListItem
)
});
if has_block {
let new_kids = wrap_inline_runs(tree, i, &kids);
tree.boxes[i as usize].children = new_kids;
}
}
i += 1;
}
}
fn wrap_inline_runs(tree: &mut BoxTree, parent: BoxId, kids: &[BoxId]) -> Vec<BoxId> {
let mut out = Vec::with_capacity(kids.len());
let mut current_run: Vec<BoxId> = Vec::new();
let flush = |tree: &mut BoxTree, parent: BoxId, run: &mut Vec<BoxId>, out: &mut Vec<BoxId>| {
if !run.is_empty() {
let anon_id = tree.boxes.len() as BoxId;
tree.boxes.push(BoxNode {
element: None,
kind: BoxKind::AnonymousBlock,
outside: DisplayOutside::Block,
inside: DisplayInside::Flow,
children: std::mem::take(run),
parent: Some(parent),
});
let moved = tree.boxes[anon_id as usize].children.clone();
for c in moved {
tree.boxes[c as usize].parent = Some(anon_id);
}
out.push(anon_id);
}
};
for &k in kids {
let kind = tree.boxes[k as usize].outside;
match kind {
DisplayOutside::Inline => current_run.push(k),
_ => {
flush(tree, parent, &mut current_run, &mut out);
out.push(k);
},
}
}
flush(tree, parent, &mut current_run, &mut out);
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::html_css::css::parse_stylesheet;
use crate::html_css::html::parse_document;
fn build(html: &str, css: &'static str) -> BoxTree {
let dom = parse_document(html);
let ss = parse_stylesheet(css).unwrap();
let ss: &'static _ = Box::leak(Box::new(ss));
let _ = ss;
let ss = Box::leak(Box::new(parse_stylesheet(css).unwrap()));
let dom: &'static _ = Box::leak(Box::new(parse_document(html)));
build_box_tree(dom, ss).unwrap()
}
fn count_kind(tree: &BoxTree, pred: impl Fn(&BoxNode) -> bool) -> usize {
tree.iter_ids()
.into_iter()
.filter(|&id| pred(tree.get(id)))
.count()
}
#[test]
fn empty_html_produces_just_root() {
let tree = build("", "");
assert_eq!(tree.len(), 1);
}
#[test]
fn single_paragraph_produces_one_element_box() {
let tree = build("<p>hi</p>", "");
assert_eq!(tree.len(), 3);
let element_boxes = count_kind(&tree, |b| matches!(b.kind, BoxKind::Element));
assert_eq!(element_boxes, 1);
let text_boxes = count_kind(&tree, |b| matches!(b.kind, BoxKind::Text(_)));
assert_eq!(text_boxes, 1);
}
#[test]
fn display_none_skips_subtree() {
let tree = build("<p>visible</p><p class=hide>x</p>", ".hide { display: none; }");
let element_boxes = count_kind(&tree, |b| matches!(b.kind, BoxKind::Element));
assert_eq!(element_boxes, 1);
}
#[test]
fn display_contents_unwraps() {
let tree = build("<div><span>x</span></div>", "div { display: contents; }");
let element_boxes = count_kind(&tree, |b| matches!(b.kind, BoxKind::Element));
assert_eq!(element_boxes, 1); }
#[test]
fn block_outside_for_default_div() {
let tree = build("<div>x</div>", "");
let div_box = tree
.iter_ids()
.into_iter()
.find(|&id| matches!(tree.get(id).kind, BoxKind::Element))
.unwrap();
assert_eq!(tree.get(div_box).outside, DisplayOutside::Block);
assert_eq!(tree.get(div_box).inside, DisplayInside::Flow);
}
#[test]
fn flex_inside_for_display_flex() {
let tree = build("<div>x</div>", "div { display: flex; }");
let div_box = tree
.iter_ids()
.into_iter()
.find(|&id| matches!(tree.get(id).kind, BoxKind::Element))
.unwrap();
assert_eq!(tree.get(div_box).outside, DisplayOutside::Block);
assert_eq!(tree.get(div_box).inside, DisplayInside::Flex);
}
#[test]
fn inline_outside_for_display_inline() {
let tree = build("<div><a>x</a></div>", "a { display: inline; }");
let a_box = tree.iter_ids().into_iter().find(|&id| {
matches!(tree.get(id).kind, BoxKind::Element)
&& tree.get(id).outside == DisplayOutside::Inline
});
assert!(a_box.is_some());
}
#[test]
fn anonymous_block_wraps_inline_run_amid_blocks() {
let tree = build("<div>before<p>middle</p>after</div>", "");
let div_box = tree.iter_ids().into_iter().find(|&id| {
matches!(tree.get(id).kind, BoxKind::Element)
&& tree.get(id).element
== Some(
tree.iter_ids()
.into_iter()
.find(|&id2| matches!(tree.get(id2).kind, BoxKind::Element))
.unwrap_or(0),
)
});
let anon = count_kind(&tree, |b| matches!(b.kind, BoxKind::AnonymousBlock));
assert_eq!(anon, 3);
let _ = div_box;
}
#[test]
fn no_anonymous_wrap_when_all_inline() {
let tree = build("<p>just <em>inline</em> text</p>", "");
let anon = count_kind(&tree, |b| matches!(b.kind, BoxKind::AnonymousBlock));
assert_eq!(anon, 1);
}
#[test]
fn list_item_keeps_outside_class() {
let tree = build("<li>x</li>", "li { display: list-item; }");
let li = tree
.iter_ids()
.into_iter()
.find(|&id| matches!(tree.get(id).kind, BoxKind::Element))
.unwrap();
assert_eq!(tree.get(li).outside, DisplayOutside::ListItem);
}
#[test]
fn document_order_preserved() {
let tree = build("<p>1</p><p>2</p><p>3</p>", "");
let ps: Vec<&BoxNode> = tree
.iter_ids()
.into_iter()
.map(|id| tree.get(id))
.filter(|b| matches!(b.kind, BoxKind::Element))
.collect();
assert_eq!(ps.len(), 3);
let texts: Vec<&str> = tree
.iter_ids()
.into_iter()
.filter_map(|id| match &tree.get(id).kind {
BoxKind::Text(s) => Some(s.as_str()),
_ => None,
})
.collect();
assert_eq!(texts, vec!["1", "2", "3"]);
}
}