diff --git a/crates/obscura-render/src/dom.rs b/crates/obscura-render/src/dom.rs index ce9484e..06bb3e1 100644 --- a/crates/obscura-render/src/dom.rs +++ b/crates/obscura-render/src/dom.rs @@ -1395,6 +1395,39 @@ fn cascade_walk( let mut style = crate::style::ua_style(elem.local.as_ref()); style.is_replaced_box = crate::inline::is_replaced(elem.local.as_ref()); style.color_scheme_dark = inherited_color_scheme_dark; + if matches!(elem.local.as_ref(), "dir" | "dl" | "menu" | "ol" | "ul") { + let mut list_ancestor_count = 0usize; + let mut has_list_or_definition_ancestor = false; + let mut ancestor = node.parent; + while let Some(ancestor_id) = ancestor { + let Some(ancestor_node) = tree.get_node(ancestor_id) else { + break; + }; + if let Some(ancestor_element) = ancestor_node.as_element() { + let local = ancestor_element.local.as_ref(); + if matches!(local, "dir" | "dl" | "menu" | "ol" | "ul") { + has_list_or_definition_ancestor = true; + } + if matches!(local, "dir" | "menu" | "ol" | "ul") { + list_ancestor_count += 1; + } + } + ancestor = ancestor_node.parent; + } + if has_list_or_definition_ancestor { + style.margin.top = 0.0; + style.margin.bottom = 0.0; + style.margin_relative[0] = None; + style.margin_relative[2] = None; + } + if matches!(elem.local.as_ref(), "dir" | "menu" | "ul") { + style.list_style = match list_ancestor_count { + 0 => Some(crate::ListStyle::Disc), + 1 => Some(crate::ListStyle::Circle), + _ => Some(crate::ListStyle::Square), + }; + } + } if matches!(elem.local.as_ref(), "td" | "th") { if let Some(padding) = inherited_cell_padding { style.padding = crate::Edges { @@ -11185,6 +11218,91 @@ mod tests { ); } + #[test] + fn ua_block_margins_resolve_against_the_computed_element_font() { + let tree = parse_html( + r#" +

Heading

+

Paragraph

+
+

H1

H2

H3

+

H4

H5
H6
+

Inherited

+

Initial

+
+ +
"#, + ); + let laid = layout_dom(&tree, (800.0, 400.0)); + let heading = tree.get_element_by_id("heading").unwrap(); + let paragraph = tree.get_element_by_id("paragraph").unwrap(); + + assert!( + (laid.styles[&heading].margin.top - 26.8).abs() < 0.01 + && (laid.styles[&heading].margin.bottom - 26.8).abs() < 0.01, + "h1's 0.67em UA margins must follow its authored 40px font: {:?}", + laid.styles[&heading].margin + ); + assert!( + (laid.styles[¶graph].margin.top - 24.0).abs() < 0.01 + && (laid.styles[¶graph].margin.bottom - 24.0).abs() < 0.01, + "p's 1em UA margins must follow its authored 24px font: {:?}", + laid.styles[¶graph].margin + ); + for (id, size, margin) in [ + ("h1", 40.0, 26.8), + ("h2", 30.0, 24.9), + ("h3", 23.4, 23.4), + ("h4", 20.0, 26.6), + ("h5", 16.6, 27.722), + ("h6", 13.4, 31.222), + ] { + let style = &laid.styles[&tree.get_element_by_id(id).unwrap()]; + assert!( + (style.font_size.unwrap() - size).abs() < 0.01 + && (style.margin.top - margin).abs() < 0.01 + && (style.margin.bottom - margin).abs() < 0.01, + "{id} UA font and margins must resolve from the inherited 20px font: size={:?}, margin={:?}", + style.font_size, + style.margin + ); + } + for (id, size, margin) in [ + ("inherit-size", 20.0, 13.4), + ("initial-size", 16.0, 10.72), + ] { + let style = &laid.styles[&tree.get_element_by_id(id).unwrap()]; + assert!( + (style.font_size.unwrap() - size).abs() < 0.01 + && (style.margin.top - margin).abs() < 0.01, + "{id} CSS-wide font-size semantics must override the UA heading size: size={:?}, margin={:?}", + style.font_size, + style.margin + ); + } + for (id, marker) in [ + ("second-list", crate::ListStyle::Circle), + ("third-list", crate::ListStyle::Square), + ("definition-list-child", crate::ListStyle::Disc), + ] { + let style = &laid.styles[&tree.get_element_by_id(id).unwrap()]; + assert_eq!(style.margin.top, 0.0, "{id} must lose nested UA margins"); + assert_eq!( + style.margin.bottom, 0.0, + "{id} must lose nested UA margins" + ); + assert_eq!(style.list_style, Some(marker), "{id} marker depth"); + } + } + #[test] fn generated_inline_boxes_apply_sizes_only_after_display_blockification() { let tree = parse_html( diff --git a/crates/obscura-render/src/style.rs b/crates/obscura-render/src/style.rs index c07bfe7..05a5e1d 100644 --- a/crates/obscura-render/src/style.rs +++ b/crates/obscura-render/src/style.rs @@ -73,23 +73,44 @@ pub fn ua_style(tag: &str) -> LayoutStyle { } else if tag == "body" { style.margin = Edges { top: 8.0, right: 8.0, bottom: 8.0, left: 8.0 }; } else if tag == "h1" { - style.font_size = Some(32.0); + style.font_size = None; + style.font_size_raw = Some(crate::Dimension::Em(2.0)); style.font_weight = Some("bold".to_string()); - style.margin = Edges { top: 21.0, bottom: 21.0, left: 0.0, right: 0.0 }; + style.margin_relative[0] = Some(crate::Dimension::Em(0.67)); + style.margin_relative[2] = Some(crate::Dimension::Em(0.67)); } else if tag == "h2" { - style.font_size = Some(24.0); + style.font_size = None; + style.font_size_raw = Some(crate::Dimension::Em(1.5)); style.font_weight = Some("bold".to_string()); - style.margin = Edges { top: 19.0, bottom: 19.0, left: 0.0, right: 0.0 }; + style.margin_relative[0] = Some(crate::Dimension::Em(0.83)); + style.margin_relative[2] = Some(crate::Dimension::Em(0.83)); } else if tag == "h3" { - style.font_size = Some(18.7); + style.font_size = None; + style.font_size_raw = Some(crate::Dimension::Em(1.17)); style.font_weight = Some("bold".to_string()); - style.margin = Edges { top: 18.0, bottom: 18.0, left: 0.0, right: 0.0 }; - } else if tag == "h4" || tag == "h5" || tag == "h6" { - style.font_size = Some(16.0); + style.margin_relative[0] = Some(crate::Dimension::Em(1.0)); + style.margin_relative[2] = Some(crate::Dimension::Em(1.0)); + } else if tag == "h4" { + style.font_size = None; + style.font_size_raw = Some(crate::Dimension::Em(1.0)); style.font_weight = Some("bold".to_string()); - style.margin = Edges { top: 21.0, bottom: 21.0, left: 0.0, right: 0.0 }; - } else if matches!(tag, "p" | "ul" | "ol" | "menu" | "dir") { - style.margin = Edges { top: 16.0, bottom: 16.0, left: 0.0, right: 0.0 }; + style.margin_relative[0] = Some(crate::Dimension::Em(1.33)); + style.margin_relative[2] = Some(crate::Dimension::Em(1.33)); + } else if tag == "h5" { + style.font_size = None; + style.font_size_raw = Some(crate::Dimension::Em(0.83)); + style.font_weight = Some("bold".to_string()); + style.margin_relative[0] = Some(crate::Dimension::Em(1.67)); + style.margin_relative[2] = Some(crate::Dimension::Em(1.67)); + } else if tag == "h6" { + style.font_size = None; + style.font_size_raw = Some(crate::Dimension::Em(0.67)); + style.font_weight = Some("bold".to_string()); + style.margin_relative[0] = Some(crate::Dimension::Em(2.33)); + style.margin_relative[2] = Some(crate::Dimension::Em(2.33)); + } else if matches!(tag, "p" | "dl" | "ul" | "ol" | "menu" | "dir") { + style.margin_relative[0] = Some(crate::Dimension::Em(1.0)); + style.margin_relative[2] = Some(crate::Dimension::Em(1.0)); if matches!(tag, "ul" | "menu" | "dir") { style.list_style = Some(crate::ListStyle::Disc); style.padding.left = 40.0; @@ -4307,10 +4328,31 @@ fn font_size_keyword(v: &str) -> Option { } fn apply_font_size(style: &mut LayoutStyle, value: &str) { - if value.trim().contains('(') { + let value = value.trim(); + match value.to_ascii_lowercase().as_str() { + // font-size is inherited; unset therefore has inherit semantics. + "inherit" | "unset" => { + style.font_size = None; + style.font_size_raw = None; + style.font_size_expression = None; + return; + } + "initial" => { + style.font_size = Some(16.0); + style.font_size_raw = None; + style.font_size_expression = None; + return; + } + // The compact cascade does not retain origin/layer history. Preserve + // the already-applied lower-origin value instead of treating revert + // as inherit or initial. + "revert" | "revert-layer" => return, + _ => {} + } + if value.contains('(') { style.font_size = None; style.font_size_raw = None; - style.font_size_expression = Some(value.trim().to_string()); + style.font_size_expression = Some(value.to_string()); return; } style.font_size_expression = None;