use htui::html::{css_parse, html_parse};
use htui::layout::layout::TermMap;
use htui::layout::{
FormattingContext, GeneratedBox, GeneratedBoxKind, GeneratedPayload, OuterDisplay,
flatten_contents_children, generate_node,
};
use htui::tdom::node::{NodeArena, NodeId, NodeType};
use htui::tui::graph_api::TermLayout;
fn setup(html: &str, css: &str) -> (NodeId, NodeArena) {
let mut p = html_parse::Parser::new(html);
let (root_id, mut arena, embedded) = p.parse();
let combined = if css.is_empty() {
embedded
} else if embedded.is_empty() {
css.to_string()
} else {
format!("{embedded}\n{css}")
};
let mut cp = css_parse::Parser::new(&combined);
css_parse::css_into_node(&mut arena, root_id, &cp.parse());
(root_id, arena)
}
fn find_node_id(
arena: &NodeArena,
root_id: NodeId,
pred: impl Fn(&NodeType) -> bool + Copy,
) -> NodeId {
fn walk<F: Fn(&NodeType) -> bool + Copy>(
arena: &NodeArena,
id: NodeId,
pred: &F,
) -> Option<NodeId> {
let node = &arena[id];
if pred(&node.node_type) {
return Some(id);
}
node.children.iter().find_map(|&c| walk(arena, c, pred))
}
walk(arena, root_id, &pred).expect("node present in document")
}
fn classify(arena: &NodeArena, id: NodeId) -> GeneratedBox {
generate_node(arena, id).unwrap_or_else(|| panic!("node should generate a box"))
}
fn layout_canvas(html: &str, css: &str) -> String {
let (root_id, arena) = setup(html, css);
let term_map = TermMap {
columns: 80,
rows: 24,
};
let mut tl = TermLayout::new(&term_map, 1.0);
tl.draw(&arena, root_id, &term_map);
tl.layout.iter().map(|b| b.word.as_str()).collect()
}
#[test]
fn display_none_excludes_box_from_tree() {
let (root_id, arena) = setup(
"<div><p>visible</p><p style=\"display: none\">gone</p></div>",
"",
);
let hidden_p = find_node_id(&arena, root_id, |nt| matches!(nt, NodeType::P));
let all_ps: Vec<NodeId> = {
let mut out = Vec::new();
let mut stack = vec![root_id];
while let Some(id) = stack.pop() {
if matches!(arena[id].node_type, NodeType::P) {
out.push(id);
}
stack.extend(arena[id].children.iter().copied());
}
out
};
let (hidden, visible) = all_ps
.into_iter()
.partition::<Vec<_>, _>(|&id| arena[id].text_content(&arena) == "gone");
assert_eq!(hidden.len(), 1);
assert_eq!(visible.len(), 1);
assert!(
generate_node(&arena, hidden[0]).is_none(),
"display: none <p> must generate no box"
);
assert!(
generate_node(&arena, visible[0]).is_some(),
"the visible <p> should generate a box"
);
let _ = hidden_p; }
#[test]
fn display_none_subtree_also_excluded() {
let (root_id, arena) = setup(
"<div style=\"display: none\"><p>never</p><span>shown</span></div>",
"",
);
let boxes = htui::layout::generate_box_tree(&arena, root_id);
assert!(
boxes
.iter()
.all(|g| !matches!(arena[g.node_id].node_type, NodeType::Div)),
"display: none div and all its descendants must be absent"
);
assert!(
!layout_canvas(
"<div style=\"display: none\"><p>never</p><span>shown</span></div>",
"",
)
.contains("never"),
"a display: none subtree must not render"
);
}
#[test]
fn hidden_attribute_excludes_box_from_tree() {
let (root_id, arena) = setup("<div><p>visible</p><p hidden>gone</p></div>", "");
let ps: Vec<NodeId> = {
let mut out = Vec::new();
let mut stack = vec![root_id];
while let Some(id) = stack.pop() {
if matches!(arena[id].node_type, NodeType::P) {
out.push(id);
}
stack.extend(arena[id].children.iter().copied());
}
out
};
let (hidden, visible) = ps
.into_iter()
.partition::<Vec<_>, _>(|&id| arena[id].attributes.contains_key("hidden"));
assert_eq!(hidden.len(), 1);
assert_eq!(visible.len(), 1);
assert!(
generate_node(&arena, hidden[0]).is_none(),
"the hidden <p> must generate no box"
);
assert!(generate_node(&arena, visible[0]).is_some());
assert!(
!layout_canvas("<div><p>visible</p><p hidden>gone</p></div>", "").contains("gone"),
"the hidden <p>'s content must not render"
);
}
#[test]
fn hidden_attribute_drops_the_subtree() {
let (root_id, arena) = setup("<div hidden><p>never</p><span>shown</span></div>", "");
let boxes = htui::layout::generate_box_tree(&arena, root_id);
assert!(
boxes
.iter()
.all(|g| !matches!(arena[g.node_id].node_type, NodeType::Div)),
"a hidden div and all its descendants must be absent"
);
}
#[test]
fn hidden_attribute_yields_to_author_display() {
let (root_id, arena) = setup(
"<div><p hidden style=\"display: block\">shown</p></div>",
"",
);
let p = find_node_id(&arena, root_id, |nt| matches!(nt, NodeType::P));
assert!(
generate_node(&arena, p).is_some(),
"an author display on a hidden element must keep it rendering"
);
}
#[test]
fn display_contents_classifies_as_transparent_marker() {
let (root_id, arena) = setup(
"<div><span style=\"display: contents\">X</span><p>after</p></div>",
"",
);
let span = find_node_id(&arena, root_id, |nt| matches!(nt, NodeType::Span));
let box_ = classify(&arena, span);
assert_eq!(box_.kind, GeneratedBoxKind::Anonymous);
assert_eq!(box_.payload, GeneratedPayload::Transparent);
let div = find_node_id(&arena, root_id, |nt| matches!(nt, NodeType::Div));
let spliced = flatten_contents_children(&arena, &arena[div].children);
let p = find_node_id(&arena, root_id, |nt| matches!(nt, NodeType::P));
assert_eq!(
spliced,
vec![span.children_(&arena)[0], p],
"the contents span's children take its place in document order"
);
assert!(
layout_canvas(
"<div><span style=\"display: contents\">X</span><p>after</p></div>",
"",
)
.contains("X"),
"the text 'X' must survive flattening"
);
}
#[test]
fn display_inline_flex_is_flex_formatting_context() {
let (root_id, arena) = setup("<div style=\"display: inline-flex\">x</div>", "");
let div = find_node_id(&arena, root_id, |nt| matches!(nt, NodeType::Div));
let box_ = classify(&arena, div);
assert_eq!(box_.inner_formatting_context, FormattingContext::Flex);
assert!(
matches!(box_.outer_display, OuterDisplay::Inline { block: true }),
"inline-flex should yield Inline {{ block: true }}, got {:?}",
box_.outer_display
);
}
#[test]
fn display_inline_block_is_inline_outer_with_block_fc() {
let (root_id, arena) = setup("<div style=\"display: inline-block\">x</div>", "");
let div = find_node_id(&arena, root_id, |nt| matches!(nt, NodeType::Div));
let box_ = classify(&arena, div);
assert!(matches!(
box_.outer_display,
OuterDisplay::Inline { block: true }
));
assert_eq!(box_.inner_formatting_context, FormattingContext::Flow);
}
#[test]
fn display_table_on_non_table_element_uses_table_fc() {
let (root_id, arena) = setup(
"<div style=\"display: table\">\
<div style=\"display: table-row\">\
<div style=\"display: table-cell\">c</div>\
</div>\
</div>",
"",
);
let div = find_node_id(&arena, root_id, |nt| matches!(nt, NodeType::Div));
let box_ = classify(&arena, div);
assert_eq!(box_.inner_formatting_context, FormattingContext::Table);
assert!(matches!(
box_.outer_display,
OuterDisplay::BlockLevel { container: true }
));
}
#[test]
fn img_uses_ua_default_inline_block_outer_display() {
let (root_id, arena) = setup("<img src='x.png'>", "");
let img = find_node_id(&arena, root_id, |nt| matches!(nt, NodeType::Img));
let box_ = classify(&arena, img);
assert_eq!(box_.kind, GeneratedBoxKind::Replaced);
assert!(
matches!(box_.outer_display, OuterDisplay::Inline { block: true }),
"<img> UA default should be inline-block, got {:?}",
box_.outer_display
);
}
#[test]
fn img_explicit_block_outer_overrides_ua_default() {
let (root_id, arena) = setup("<img src='x.png' style='display: block'>", "");
let img = find_node_id(&arena, root_id, |nt| matches!(nt, NodeType::Img));
let box_ = classify(&arena, img);
assert_eq!(box_.kind, GeneratedBoxKind::Replaced);
assert!(
matches!(
box_.outer_display,
OuterDisplay::BlockLevel { container: true }
),
"explicit display: block must win, got {:?}",
box_.outer_display
);
}
#[test]
fn display_grid_is_grid_formatting_context() {
let (root_id, arena) = setup("<div style=\"display: grid\">x</div>", "");
let div = find_node_id(&arena, root_id, |nt| matches!(nt, NodeType::Div));
let box_ = classify(&arena, div);
assert_eq!(box_.inner_formatting_context, FormattingContext::Grid);
}
#[test]
fn display_contents_children_render_in_flow() {
let canvas = layout_canvas(
"<div>before<div style=\"display: contents\"><p>inner</p></div>after</div>",
"",
);
assert!(
canvas.contains("inner"),
"the contents element's children must render in the parent's flow"
);
assert!(
canvas.contains("after"),
"siblings after the contents element must still render"
);
}
#[test]
fn display_contents_flattens_recursively() {
let canvas = layout_canvas(
"<div style=\"display: contents\"><div style=\"display: contents\"><p>deep</p></div></div>",
"",
);
assert!(
canvas.contains("deep"),
"nested display: contents elements must flatten recursively"
);
}
#[test]
fn display_contents_children_become_flex_items() {
let canvas = layout_canvas(
"<div style=\"display: flex\"><div style=\"display: contents\"><p>alpha</p><p>beta</p></div></div>",
"",
);
assert!(
canvas.contains("alpha") && canvas.contains("beta"),
"the contents element's children must become flex items, not vanish"
);
}
#[test]
fn grid_variants_render_their_items() {
let canvas = layout_canvas(
"<div>a<div style=\"display: grid\">G</div>b<span style=\"display: inline-grid\">H</span>c</div>",
"",
);
assert!(
canvas.contains("a") && canvas.contains("b") && canvas.contains("c"),
"the surrounding text must render: {canvas:?}"
);
assert!(
canvas.contains('G') && canvas.contains('H'),
"grid and inline-grid content must both render: {canvas:?}"
);
}
trait ChildrenExt {
fn children_(&self, arena: &NodeArena) -> Vec<NodeId>;
}
impl ChildrenExt for NodeId {
fn children_(&self, arena: &NodeArena) -> Vec<NodeId> {
arena[*self].children.clone()
}
}