use crate::{
html::css_parse::CssDisplay,
layout::{FormattingContext, OuterDisplay},
tdom::node::{NodeArena, NodeId, NodeType, document_base_href},
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GeneratedBoxKind {
Normal,
Text,
Anonymous,
Replaced,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GeneratedBox {
pub node_id: NodeId,
pub kind: GeneratedBoxKind,
pub outer_display: OuterDisplay,
pub inner_formatting_context: FormattingContext,
pub payload: GeneratedPayload,
pub children: Vec<GeneratedBox>,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub enum GeneratedPayload {
#[default]
None,
Image { src: String },
Input,
Select,
Textarea,
Button,
Transparent,
}
pub fn generate_box_tree(arena: &NodeArena, root_id: NodeId) -> Vec<GeneratedBox> {
arena[root_id]
.children
.iter()
.filter_map(|&child_id| generate_node(arena, child_id))
.collect()
}
pub fn generate_node(arena: &NodeArena, node_id: NodeId) -> Option<GeneratedBox> {
let node = &arena[node_id];
if is_non_rendered(&node.node_type) {
return None;
}
if matches!(node.node_type, NodeType::Text) {
return Some(GeneratedBox {
node_id,
kind: GeneratedBoxKind::Text,
outer_display: OuterDisplay::Inline { block: false },
inner_formatting_context: FormattingContext::Flow,
payload: GeneratedPayload::None,
children: Vec::new(),
});
}
let display = node
.css_attr
.display()
.unwrap_or_else(|| node.node_type.ua_default_display());
if display.is_dropped() {
return None;
}
if node.node_type == NodeType::Dialog
&& !node.attributes.contains_key("open")
&& !node.css_attr.is_set("display")
{
return None;
}
if node.attributes.contains_key("hidden") && !node.css_attr.is_set("display") {
return None;
}
if display.is_transparent() {
return Some(GeneratedBox {
node_id,
kind: GeneratedBoxKind::Anonymous,
outer_display: OuterDisplay::Inline { block: false },
inner_formatting_context: FormattingContext::Flow,
payload: GeneratedPayload::Transparent,
children: Vec::new(),
});
}
let (kind, payload) = if matches!(node.node_type, NodeType::Img) {
let raw_src = node.get_src().cloned().unwrap_or_default();
let src = if raw_src.is_empty() {
raw_src
} else {
let base = document_base_href(arena, node_id);
crate::resources::resolve_url(base.as_deref(), &raw_src)
};
(GeneratedBoxKind::Replaced, GeneratedPayload::Image { src })
} else if matches!(node.node_type, NodeType::Input) {
if node
.get_type()
.is_some_and(|t| t.eq_ignore_ascii_case("hidden"))
{
return None;
}
(GeneratedBoxKind::Replaced, GeneratedPayload::Input)
} else if matches!(node.node_type, NodeType::Textarea) {
(GeneratedBoxKind::Replaced, GeneratedPayload::Textarea)
} else if matches!(node.node_type, NodeType::Button) {
(GeneratedBoxKind::Replaced, GeneratedPayload::Button)
} else if matches!(node.node_type, NodeType::Select) {
(GeneratedBoxKind::Replaced, GeneratedPayload::Select)
} else {
(GeneratedBoxKind::Normal, GeneratedPayload::None)
};
let outer_display = OuterDisplay::from_css_display(display);
let inner_formatting_context = if display.is_table_family() {
FormattingContext::Table
} else if display.is_flex() {
FormattingContext::Flex
} else if matches!(display, CssDisplay::Grid | CssDisplay::InlineGrid) {
FormattingContext::Grid
} else {
FormattingContext::Flow
};
Some(GeneratedBox {
node_id,
kind,
outer_display,
inner_formatting_context,
payload,
children: generate_children(arena, node_id),
})
}
pub fn visible_children(arena: &NodeArena, parent_id: NodeId) -> Vec<NodeId> {
let parent = &arena[parent_id];
let ids = parent.children.clone();
if parent.node_type != NodeType::Details || parent.attributes.contains_key("open") {
return ids;
}
match ids
.iter()
.position(|&id| arena[id].node_type == NodeType::Summary)
{
Some(pos) => vec![ids[pos]],
None => Vec::new(),
}
}
fn generate_children(arena: &NodeArena, parent_id: NodeId) -> Vec<GeneratedBox> {
let mut children = Vec::new();
let mut inline_group = Vec::new();
for &child_id in &visible_children(arena, parent_id) {
let child_node = &arena[child_id];
if let Some(d) = child_node.css_attr.display()
&& d.is_transparent()
{
for &grandchild_id in &child_node.children {
append_child_box(
arena,
parent_id,
grandchild_id,
&mut inline_group,
&mut children,
);
}
continue;
}
append_child_box(arena, parent_id, child_id, &mut inline_group, &mut children);
}
flush_inline_group(parent_id, &mut inline_group, &mut children);
children
}
fn append_child_box(
arena: &NodeArena,
parent_id: NodeId,
child_id: NodeId,
inline_group: &mut Vec<GeneratedBox>,
children: &mut Vec<GeneratedBox>,
) {
let Some(child) = generate_node(arena, child_id) else {
return;
};
if child.kind == GeneratedBoxKind::Text || child.outer_display.is_inline() {
inline_group.push(child);
return;
}
flush_inline_group(parent_id, inline_group, children);
children.push(child);
}
fn flush_inline_group(
parent_id: NodeId,
inline_group: &mut Vec<GeneratedBox>,
children: &mut Vec<GeneratedBox>,
) {
if inline_group.is_empty() {
return;
}
children.push(GeneratedBox {
node_id: parent_id,
kind: GeneratedBoxKind::Anonymous,
outer_display: OuterDisplay::BlockLevel { container: true },
inner_formatting_context: FormattingContext::Flow,
payload: GeneratedPayload::None,
children: std::mem::take(inline_group),
});
}
fn is_non_rendered(node_type: &NodeType) -> bool {
matches!(
node_type,
NodeType::Head
| NodeType::Style
| NodeType::Link
| NodeType::Meta
| NodeType::Base
| NodeType::Noscript
| NodeType::Template
| NodeType::Datalist
| NodeType::Comment
| NodeType::DocumentType
| NodeType::DocumentFragment
)
}