From b337e17e4d17029c0b1ac272f00eb003edd7c3f4 Mon Sep 17 00:00:00 2001 From: Pierre Le Fevre Date: Wed, 4 Mar 2026 20:57:45 +0100 Subject: [PATCH] Implement CSS cascade and computed style resolution Adds the `computed` module to the `style` crate with: - `ComputedStyle` struct with typed fields for all CSS properties: display, margin, padding, border (width/style/color), width, height, color, font-size, font-weight, font-style, font-family, text-align, text-decoration, line-height, background-color, position, top/right/ bottom/left, overflow, visibility - Cascade algorithm: 1. Collects matching rules via selector matching (from existing matching module) 2. Sorts by origin (UA < author) and specificity 3. Separates normal vs !important declarations 4. Applies inline styles (from style attribute) with highest priority 5. Expands shorthand properties (margin, padding, border, background) - Property inheritance: - Inherited properties (color, font-size, font-weight, font-style, font-family, text-align, text-decoration, line-height, visibility) automatically inherit from parent computed values - Non-inherited properties reset to initial values - `inherit`, `initial`, `unset` keyword support - User-agent default stylesheet with standard HTML element defaults: display types, body margin, heading sizes/weights/margins, paragraph margins, bold/italic/underline for semantic elements - Relative value resolution: - `em` units relative to parent font-size (for font-size) or current font-size (for other properties) - `rem` units relative to root (16px) - Physical unit conversion (pt, cm, mm, in, pc) - Percentage resolution for font-size - `resolve_styles()` API that builds a styled tree (StyledNode) from a DOM document and author stylesheets - 34 comprehensive tests covering UA defaults, author overrides, cascade ordering, specificity, inheritance, inherit/initial/unset keywords, em resolution, inline styles, shorthand expansion, multiple stylesheets, and display:none removal Co-Authored-By: Claude Opus 4.6 --- crates/style/src/computed.rs | 1767 ++++++++++++++++++++++++++++++++++ crates/style/src/lib.rs | 1 + 2 files changed, 1768 insertions(+) create mode 100644 crates/style/src/computed.rs diff --git a/crates/style/src/computed.rs b/crates/style/src/computed.rs new file mode 100644 index 0000000..7c8f86e --- /dev/null +++ b/crates/style/src/computed.rs @@ -0,0 +1,1767 @@ +//! CSS cascade and computed style resolution. +//! +//! For each DOM element, resolves the final computed value of every CSS property +//! by collecting matching rules, applying the cascade (specificity + source order), +//! handling property inheritance, and resolving relative values. + +use we_css::parser::{Declaration, Stylesheet}; +use we_css::values::{expand_shorthand, parse_value, Color, CssValue, LengthUnit}; +use we_dom::{Document, NodeData, NodeId}; + +use crate::matching::collect_matching_rules; + +// --------------------------------------------------------------------------- +// Display +// --------------------------------------------------------------------------- + +/// CSS `display` property values (subset). +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum Display { + Block, + #[default] + Inline, + None, +} + +// --------------------------------------------------------------------------- +// Position +// --------------------------------------------------------------------------- + +/// CSS `position` property values. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum Position { + #[default] + Static, + Relative, + Absolute, + Fixed, +} + +// --------------------------------------------------------------------------- +// FontWeight +// --------------------------------------------------------------------------- + +/// CSS `font-weight` as a numeric value (100-900). +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct FontWeight(pub f32); + +impl Default for FontWeight { + fn default() -> Self { + FontWeight(400.0) // normal + } +} + +// --------------------------------------------------------------------------- +// FontStyle +// --------------------------------------------------------------------------- + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum FontStyle { + #[default] + Normal, + Italic, + Oblique, +} + +// --------------------------------------------------------------------------- +// TextAlign +// --------------------------------------------------------------------------- + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum TextAlign { + #[default] + Left, + Right, + Center, + Justify, +} + +// --------------------------------------------------------------------------- +// TextDecoration +// --------------------------------------------------------------------------- + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum TextDecoration { + #[default] + None, + Underline, + Overline, + LineThrough, +} + +// --------------------------------------------------------------------------- +// Overflow +// --------------------------------------------------------------------------- + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum Overflow { + #[default] + Visible, + Hidden, + Scroll, + Auto, +} + +// --------------------------------------------------------------------------- +// Visibility +// --------------------------------------------------------------------------- + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum Visibility { + #[default] + Visible, + Hidden, + Collapse, +} + +// --------------------------------------------------------------------------- +// BorderStyle +// --------------------------------------------------------------------------- + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum BorderStyle { + #[default] + None, + Hidden, + Dotted, + Dashed, + Solid, + Double, + Groove, + Ridge, + Inset, + Outset, +} + +// --------------------------------------------------------------------------- +// LengthOrAuto +// --------------------------------------------------------------------------- + +/// A computed length (resolved to px) or `auto`. +#[derive(Debug, Clone, Copy, PartialEq, Default)] +pub enum LengthOrAuto { + Length(f32), + #[default] + Auto, +} + +// --------------------------------------------------------------------------- +// ComputedStyle +// --------------------------------------------------------------------------- + +/// The fully resolved computed style for a single element. +#[derive(Debug, Clone, PartialEq)] +pub struct ComputedStyle { + // Display + pub display: Display, + + // Box model: margin + pub margin_top: LengthOrAuto, + pub margin_right: LengthOrAuto, + pub margin_bottom: LengthOrAuto, + pub margin_left: LengthOrAuto, + + // Box model: padding (no auto for padding) + pub padding_top: f32, + pub padding_right: f32, + pub padding_bottom: f32, + pub padding_left: f32, + + // Box model: border width + pub border_top_width: f32, + pub border_right_width: f32, + pub border_bottom_width: f32, + pub border_left_width: f32, + + // Box model: border style + pub border_top_style: BorderStyle, + pub border_right_style: BorderStyle, + pub border_bottom_style: BorderStyle, + pub border_left_style: BorderStyle, + + // Box model: border color + pub border_top_color: Color, + pub border_right_color: Color, + pub border_bottom_color: Color, + pub border_left_color: Color, + + // Box model: dimensions + pub width: LengthOrAuto, + pub height: LengthOrAuto, + + // Text / inherited + pub color: Color, + pub font_size: f32, + pub font_weight: FontWeight, + pub font_style: FontStyle, + pub font_family: String, + pub text_align: TextAlign, + pub text_decoration: TextDecoration, + pub line_height: f32, + + // Background + pub background_color: Color, + + // Position + pub position: Position, + pub top: LengthOrAuto, + pub right: LengthOrAuto, + pub bottom: LengthOrAuto, + pub left: LengthOrAuto, + + // Overflow + pub overflow: Overflow, + + // Visibility (inherited) + pub visibility: Visibility, +} + +impl Default for ComputedStyle { + fn default() -> Self { + ComputedStyle { + display: Display::Inline, + + margin_top: LengthOrAuto::Length(0.0), + margin_right: LengthOrAuto::Length(0.0), + margin_bottom: LengthOrAuto::Length(0.0), + margin_left: LengthOrAuto::Length(0.0), + + padding_top: 0.0, + padding_right: 0.0, + padding_bottom: 0.0, + padding_left: 0.0, + + border_top_width: 0.0, + border_right_width: 0.0, + border_bottom_width: 0.0, + border_left_width: 0.0, + + border_top_style: BorderStyle::None, + border_right_style: BorderStyle::None, + border_bottom_style: BorderStyle::None, + border_left_style: BorderStyle::None, + + border_top_color: Color::rgb(0, 0, 0), + border_right_color: Color::rgb(0, 0, 0), + border_bottom_color: Color::rgb(0, 0, 0), + border_left_color: Color::rgb(0, 0, 0), + + width: LengthOrAuto::Auto, + height: LengthOrAuto::Auto, + + color: Color::rgb(0, 0, 0), + font_size: 16.0, + font_weight: FontWeight(400.0), + font_style: FontStyle::Normal, + font_family: String::new(), + text_align: TextAlign::Left, + text_decoration: TextDecoration::None, + line_height: 19.2, // 1.2 * 16 + + background_color: Color::new(0, 0, 0, 0), // transparent + + position: Position::Static, + top: LengthOrAuto::Auto, + right: LengthOrAuto::Auto, + bottom: LengthOrAuto::Auto, + left: LengthOrAuto::Auto, + + overflow: Overflow::Visible, + visibility: Visibility::Visible, + } + } +} + +// --------------------------------------------------------------------------- +// Property classification: inherited vs non-inherited +// --------------------------------------------------------------------------- + +fn is_inherited_property(property: &str) -> bool { + matches!( + property, + "color" + | "font-size" + | "font-weight" + | "font-style" + | "font-family" + | "text-align" + | "text-decoration" + | "line-height" + | "visibility" + ) +} + +// --------------------------------------------------------------------------- +// User-agent stylesheet +// --------------------------------------------------------------------------- + +/// Returns the user-agent default stylesheet. +pub fn ua_stylesheet() -> Stylesheet { + use we_css::parser::Parser; + Parser::parse(UA_CSS) +} + +const UA_CSS: &str = r#" +html, body, div, p, pre, h1, h2, h3, h4, h5, h6, +ul, ol, li, blockquote, section, article, nav, +header, footer, main, hr { + display: block; +} + +span, a, em, strong, b, i, u, code, small, sub, sup, br { + display: inline; +} + +head, title, script, style, link, meta { + display: none; +} + +body { + margin: 8px; +} + +h1 { + font-size: 2em; + margin-top: 0.67em; + margin-bottom: 0.67em; + font-weight: bold; +} + +h2 { + font-size: 1.5em; + margin-top: 0.83em; + margin-bottom: 0.83em; + font-weight: bold; +} + +h3 { + font-size: 1.17em; + margin-top: 1em; + margin-bottom: 1em; + font-weight: bold; +} + +h4 { + font-size: 1em; + margin-top: 1.33em; + margin-bottom: 1.33em; + font-weight: bold; +} + +h5 { + font-size: 0.83em; + margin-top: 1.67em; + margin-bottom: 1.67em; + font-weight: bold; +} + +h6 { + font-size: 0.67em; + margin-top: 2.33em; + margin-bottom: 2.33em; + font-weight: bold; +} + +p { + margin-top: 1em; + margin-bottom: 1em; +} + +strong, b { + font-weight: bold; +} + +em, i { + font-style: italic; +} + +a { + color: blue; + text-decoration: underline; +} + +u { + text-decoration: underline; +} +"#; + +// --------------------------------------------------------------------------- +// Resolve a CssValue to f32 px given context +// --------------------------------------------------------------------------- + +fn resolve_length(value: &CssValue, parent_font_size: f32, current_font_size: f32) -> Option { + match value { + CssValue::Length(n, unit) => Some(resolve_length_unit(*n, *unit, parent_font_size)), + CssValue::Percentage(p) => Some((*p / 100.0) as f32 * current_font_size), + CssValue::Zero => Some(0.0), + CssValue::Number(n) if *n == 0.0 => Some(0.0), + _ => None, + } +} + +fn resolve_length_unit(value: f64, unit: LengthUnit, em_base: f32) -> f32 { + let v = value as f32; + match unit { + LengthUnit::Px => v, + LengthUnit::Em => v * em_base, + LengthUnit::Rem => v * 16.0, // root font size is always 16px for now + LengthUnit::Pt => v * (96.0 / 72.0), + LengthUnit::Cm => v * (96.0 / 2.54), + LengthUnit::Mm => v * (96.0 / 25.4), + LengthUnit::In => v * 96.0, + LengthUnit::Pc => v * 16.0, + // Viewport units: use a reasonable default (can be parameterized later) + LengthUnit::Vw | LengthUnit::Vh | LengthUnit::Vmin | LengthUnit::Vmax => v, + } +} + +fn resolve_length_or_auto( + value: &CssValue, + parent_font_size: f32, + current_font_size: f32, +) -> LengthOrAuto { + match value { + CssValue::Auto => LengthOrAuto::Auto, + _ => resolve_length(value, parent_font_size, current_font_size) + .map(LengthOrAuto::Length) + .unwrap_or(LengthOrAuto::Auto), + } +} + +fn resolve_color(value: &CssValue, current_color: Color) -> Option { + match value { + CssValue::Color(c) => Some(*c), + CssValue::CurrentColor => Some(current_color), + CssValue::Transparent => Some(Color::new(0, 0, 0, 0)), + _ => None, + } +} + +// --------------------------------------------------------------------------- +// Apply a single property value to a ComputedStyle +// --------------------------------------------------------------------------- + +fn apply_property( + style: &mut ComputedStyle, + property: &str, + value: &CssValue, + parent: &ComputedStyle, +) { + // Handle inherit/initial/unset + match value { + CssValue::Inherit => { + inherit_property(style, property, parent); + return; + } + CssValue::Initial => { + reset_property_to_initial(style, property); + return; + } + CssValue::Unset => { + if is_inherited_property(property) { + inherit_property(style, property, parent); + } else { + reset_property_to_initial(style, property); + } + return; + } + _ => {} + } + + let parent_fs = parent.font_size; + let current_fs = style.font_size; + + match property { + "display" => { + style.display = match value { + CssValue::Keyword(k) => match k.as_str() { + "block" => Display::Block, + "inline" => Display::Inline, + _ => Display::Block, + }, + CssValue::None => Display::None, + _ => style.display, + }; + } + + // Margin + "margin-top" => { + style.margin_top = resolve_length_or_auto(value, parent_fs, current_fs); + } + "margin-right" => { + style.margin_right = resolve_length_or_auto(value, parent_fs, current_fs); + } + "margin-bottom" => { + style.margin_bottom = resolve_length_or_auto(value, parent_fs, current_fs); + } + "margin-left" => { + style.margin_left = resolve_length_or_auto(value, parent_fs, current_fs); + } + + // Padding + "padding-top" => { + if let Some(v) = resolve_length(value, parent_fs, current_fs) { + style.padding_top = v; + } + } + "padding-right" => { + if let Some(v) = resolve_length(value, parent_fs, current_fs) { + style.padding_right = v; + } + } + "padding-bottom" => { + if let Some(v) = resolve_length(value, parent_fs, current_fs) { + style.padding_bottom = v; + } + } + "padding-left" => { + if let Some(v) = resolve_length(value, parent_fs, current_fs) { + style.padding_left = v; + } + } + + // Border width + "border-top-width" | "border-right-width" | "border-bottom-width" | "border-left-width" => { + let w = resolve_border_width(value, parent_fs); + match property { + "border-top-width" => style.border_top_width = w, + "border-right-width" => style.border_right_width = w, + "border-bottom-width" => style.border_bottom_width = w, + "border-left-width" => style.border_left_width = w, + _ => {} + } + } + + // Border width shorthand (single value applied to all sides) + "border-width" => { + let w = resolve_border_width(value, parent_fs); + style.border_top_width = w; + style.border_right_width = w; + style.border_bottom_width = w; + style.border_left_width = w; + } + + // Border style + "border-top-style" | "border-right-style" | "border-bottom-style" | "border-left-style" => { + let s = parse_border_style(value); + match property { + "border-top-style" => style.border_top_style = s, + "border-right-style" => style.border_right_style = s, + "border-bottom-style" => style.border_bottom_style = s, + "border-left-style" => style.border_left_style = s, + _ => {} + } + } + + "border-style" => { + let s = parse_border_style(value); + style.border_top_style = s; + style.border_right_style = s; + style.border_bottom_style = s; + style.border_left_style = s; + } + + // Border color + "border-top-color" | "border-right-color" | "border-bottom-color" | "border-left-color" => { + if let Some(c) = resolve_color(value, style.color) { + match property { + "border-top-color" => style.border_top_color = c, + "border-right-color" => style.border_right_color = c, + "border-bottom-color" => style.border_bottom_color = c, + "border-left-color" => style.border_left_color = c, + _ => {} + } + } + } + + "border-color" => { + if let Some(c) = resolve_color(value, style.color) { + style.border_top_color = c; + style.border_right_color = c; + style.border_bottom_color = c; + style.border_left_color = c; + } + } + + // Dimensions + "width" => { + style.width = resolve_length_or_auto(value, parent_fs, current_fs); + } + "height" => { + style.height = resolve_length_or_auto(value, parent_fs, current_fs); + } + + // Color (inherited) + "color" => { + if let Some(c) = resolve_color(value, parent.color) { + style.color = c; + // Update border colors to match (currentColor default) + } + } + + // Font-size (inherited) — special: em units relative to parent + "font-size" => { + match value { + CssValue::Length(n, unit) => { + style.font_size = resolve_length_unit(*n, *unit, parent_fs); + } + CssValue::Percentage(p) => { + style.font_size = (*p / 100.0) as f32 * parent_fs; + } + CssValue::Zero => { + style.font_size = 0.0; + } + CssValue::Keyword(k) => { + style.font_size = match k.as_str() { + "xx-small" => 9.0, + "x-small" => 10.0, + "small" => 13.0, + "medium" => 16.0, + "large" => 18.0, + "x-large" => 24.0, + "xx-large" => 32.0, + "smaller" => parent_fs * 0.833, + "larger" => parent_fs * 1.2, + _ => style.font_size, + }; + } + _ => {} + } + // Update line-height when font-size changes + style.line_height = style.font_size * 1.2; + } + + // Font-weight (inherited) + "font-weight" => { + style.font_weight = match value { + CssValue::Keyword(k) => match k.as_str() { + "normal" => FontWeight(400.0), + "bold" => FontWeight(700.0), + "lighter" => FontWeight((parent.font_weight.0 - 100.0).max(100.0)), + "bolder" => FontWeight((parent.font_weight.0 + 300.0).min(900.0)), + _ => style.font_weight, + }, + CssValue::Number(n) => FontWeight(*n as f32), + _ => style.font_weight, + }; + } + + // Font-style (inherited) + "font-style" => { + style.font_style = match value { + CssValue::Keyword(k) => match k.as_str() { + "normal" => FontStyle::Normal, + "italic" => FontStyle::Italic, + "oblique" => FontStyle::Oblique, + _ => style.font_style, + }, + _ => style.font_style, + }; + } + + // Font-family (inherited) + "font-family" => { + if let CssValue::String(s) | CssValue::Keyword(s) = value { + style.font_family = s.clone(); + } + } + + // Text-align (inherited) + "text-align" => { + style.text_align = match value { + CssValue::Keyword(k) => match k.as_str() { + "left" => TextAlign::Left, + "right" => TextAlign::Right, + "center" => TextAlign::Center, + "justify" => TextAlign::Justify, + _ => style.text_align, + }, + _ => style.text_align, + }; + } + + // Text-decoration (inherited) + "text-decoration" => { + style.text_decoration = match value { + CssValue::Keyword(k) => match k.as_str() { + "underline" => TextDecoration::Underline, + "overline" => TextDecoration::Overline, + "line-through" => TextDecoration::LineThrough, + _ => style.text_decoration, + }, + CssValue::None => TextDecoration::None, + _ => style.text_decoration, + }; + } + + // Line-height (inherited) + "line-height" => match value { + CssValue::Keyword(k) if k == "normal" => { + style.line_height = style.font_size * 1.2; + } + CssValue::Number(n) => { + style.line_height = *n as f32 * style.font_size; + } + CssValue::Length(n, unit) => { + style.line_height = resolve_length_unit(*n, *unit, style.font_size); + } + CssValue::Percentage(p) => { + style.line_height = (*p / 100.0) as f32 * style.font_size; + } + _ => {} + }, + + // Background color + "background-color" => { + if let Some(c) = resolve_color(value, style.color) { + style.background_color = c; + } + } + + // Position + "position" => { + style.position = match value { + CssValue::Keyword(k) => match k.as_str() { + "static" => Position::Static, + "relative" => Position::Relative, + "absolute" => Position::Absolute, + "fixed" => Position::Fixed, + _ => style.position, + }, + _ => style.position, + }; + } + + // Position offsets + "top" => style.top = resolve_length_or_auto(value, parent_fs, current_fs), + "bottom" => style.bottom = resolve_length_or_auto(value, parent_fs, current_fs), + + // Overflow + "overflow" => { + style.overflow = match value { + CssValue::Keyword(k) => match k.as_str() { + "visible" => Overflow::Visible, + "hidden" => Overflow::Hidden, + "scroll" => Overflow::Scroll, + _ => style.overflow, + }, + CssValue::Auto => Overflow::Auto, + _ => style.overflow, + }; + } + + // Visibility (inherited) + "visibility" => { + style.visibility = match value { + CssValue::Keyword(k) => match k.as_str() { + "visible" => Visibility::Visible, + "hidden" => Visibility::Hidden, + "collapse" => Visibility::Collapse, + _ => style.visibility, + }, + _ => style.visibility, + }; + } + + _ => {} // Unknown property — ignore + } +} + +fn resolve_border_width(value: &CssValue, em_base: f32) -> f32 { + match value { + CssValue::Length(n, unit) => resolve_length_unit(*n, *unit, em_base), + CssValue::Zero => 0.0, + CssValue::Number(n) if *n == 0.0 => 0.0, + CssValue::Keyword(k) => match k.as_str() { + "thin" => 1.0, + "medium" => 3.0, + "thick" => 5.0, + _ => 0.0, + }, + _ => 0.0, + } +} + +fn parse_border_style(value: &CssValue) -> BorderStyle { + match value { + CssValue::Keyword(k) => match k.as_str() { + "none" => BorderStyle::None, + "hidden" => BorderStyle::Hidden, + "dotted" => BorderStyle::Dotted, + "dashed" => BorderStyle::Dashed, + "solid" => BorderStyle::Solid, + "double" => BorderStyle::Double, + "groove" => BorderStyle::Groove, + "ridge" => BorderStyle::Ridge, + "inset" => BorderStyle::Inset, + "outset" => BorderStyle::Outset, + _ => BorderStyle::None, + }, + CssValue::None => BorderStyle::None, + _ => BorderStyle::None, + } +} + +fn inherit_property(style: &mut ComputedStyle, property: &str, parent: &ComputedStyle) { + match property { + "color" => style.color = parent.color, + "font-size" => { + style.font_size = parent.font_size; + style.line_height = style.font_size * 1.2; + } + "font-weight" => style.font_weight = parent.font_weight, + "font-style" => style.font_style = parent.font_style, + "font-family" => style.font_family = parent.font_family.clone(), + "text-align" => style.text_align = parent.text_align, + "text-decoration" => style.text_decoration = parent.text_decoration, + "line-height" => style.line_height = parent.line_height, + "visibility" => style.visibility = parent.visibility, + // Non-inherited properties: inherit from parent if explicitly requested + "display" => style.display = parent.display, + "margin-top" => style.margin_top = parent.margin_top, + "margin-right" => style.margin_right = parent.margin_right, + "margin-bottom" => style.margin_bottom = parent.margin_bottom, + "margin-left" => style.margin_left = parent.margin_left, + "padding-top" => style.padding_top = parent.padding_top, + "padding-right" => style.padding_right = parent.padding_right, + "padding-bottom" => style.padding_bottom = parent.padding_bottom, + "padding-left" => style.padding_left = parent.padding_left, + "width" => style.width = parent.width, + "height" => style.height = parent.height, + "background-color" => style.background_color = parent.background_color, + "position" => style.position = parent.position, + "overflow" => style.overflow = parent.overflow, + _ => {} + } +} + +fn reset_property_to_initial(style: &mut ComputedStyle, property: &str) { + let initial = ComputedStyle::default(); + match property { + "display" => style.display = initial.display, + "margin-top" => style.margin_top = initial.margin_top, + "margin-right" => style.margin_right = initial.margin_right, + "margin-bottom" => style.margin_bottom = initial.margin_bottom, + "margin-left" => style.margin_left = initial.margin_left, + "padding-top" => style.padding_top = initial.padding_top, + "padding-right" => style.padding_right = initial.padding_right, + "padding-bottom" => style.padding_bottom = initial.padding_bottom, + "padding-left" => style.padding_left = initial.padding_left, + "border-top-width" => style.border_top_width = initial.border_top_width, + "border-right-width" => style.border_right_width = initial.border_right_width, + "border-bottom-width" => style.border_bottom_width = initial.border_bottom_width, + "border-left-width" => style.border_left_width = initial.border_left_width, + "width" => style.width = initial.width, + "height" => style.height = initial.height, + "color" => style.color = initial.color, + "font-size" => { + style.font_size = initial.font_size; + style.line_height = initial.line_height; + } + "font-weight" => style.font_weight = initial.font_weight, + "font-style" => style.font_style = initial.font_style, + "font-family" => style.font_family = initial.font_family.clone(), + "text-align" => style.text_align = initial.text_align, + "text-decoration" => style.text_decoration = initial.text_decoration, + "line-height" => style.line_height = initial.line_height, + "background-color" => style.background_color = initial.background_color, + "position" => style.position = initial.position, + "top" => style.top = initial.top, + "right" => style.right = initial.right, + "bottom" => style.bottom = initial.bottom, + "left" => style.left = initial.left, + "overflow" => style.overflow = initial.overflow, + "visibility" => style.visibility = initial.visibility, + _ => {} + } +} + +// --------------------------------------------------------------------------- +// Styled tree +// --------------------------------------------------------------------------- + +/// A node in the styled tree: a DOM node paired with its computed style. +#[derive(Debug)] +pub struct StyledNode { + pub node: NodeId, + pub style: ComputedStyle, + pub children: Vec, +} + +/// Resolve styles for an entire document tree. +/// +/// `stylesheets` is a list of author stylesheets (the UA stylesheet is +/// automatically prepended). +pub fn resolve_styles(doc: &Document, author_stylesheets: &[Stylesheet]) -> Option { + let ua = ua_stylesheet(); + + // Combine UA + author stylesheets into a single list for rule collection. + // UA rules come first (lower priority), author rules come after. + let mut combined = Stylesheet { + rules: ua.rules.clone(), + }; + for ss in author_stylesheets { + combined.rules.extend(ss.rules.iter().cloned()); + } + + let root = doc.root(); + resolve_node(doc, root, &combined, &ComputedStyle::default()) +} + +fn resolve_node( + doc: &Document, + node: NodeId, + stylesheet: &Stylesheet, + parent_style: &ComputedStyle, +) -> Option { + match doc.node_data(node) { + NodeData::Document => { + // Document node: resolve children, return first element child or wrapper. + let mut children = Vec::new(); + for child in doc.children(node) { + if let Some(styled) = resolve_node(doc, child, stylesheet, parent_style) { + children.push(styled); + } + } + if children.len() == 1 { + children.into_iter().next() + } else if children.is_empty() { + None + } else { + Some(StyledNode { + node, + style: parent_style.clone(), + children, + }) + } + } + NodeData::Element { .. } => { + let style = compute_style_for_element(doc, node, stylesheet, parent_style); + + if style.display == Display::None { + return None; + } + + let mut children = Vec::new(); + for child in doc.children(node) { + if let Some(styled) = resolve_node(doc, child, stylesheet, &style) { + children.push(styled); + } + } + + Some(StyledNode { + node, + style, + children, + }) + } + NodeData::Text { data } => { + if data.trim().is_empty() { + return None; + } + // Text nodes inherit all properties from their parent. + Some(StyledNode { + node, + style: parent_style.clone(), + children: Vec::new(), + }) + } + NodeData::Comment { .. } => None, + } +} + +/// Compute the style for a single element node. +fn compute_style_for_element( + doc: &Document, + node: NodeId, + stylesheet: &Stylesheet, + parent_style: &ComputedStyle, +) -> ComputedStyle { + // Start from initial values, inheriting inherited properties from parent + let mut style = ComputedStyle { + color: parent_style.color, + font_size: parent_style.font_size, + font_weight: parent_style.font_weight, + font_style: parent_style.font_style, + font_family: parent_style.font_family.clone(), + text_align: parent_style.text_align, + text_decoration: parent_style.text_decoration, + line_height: parent_style.line_height, + visibility: parent_style.visibility, + ..ComputedStyle::default() + }; + + // Step 2: Collect matching rules, sorted by specificity + source order + let matched_rules = collect_matching_rules(doc, node, stylesheet); + + // Step 3: Separate normal and !important declarations + let mut normal_decls: Vec<(String, CssValue)> = Vec::new(); + let mut important_decls: Vec<(String, CssValue)> = Vec::new(); + + for matched in &matched_rules { + for decl in &matched.rule.declarations { + let property = &decl.property; + + // Try shorthand expansion first + if let Some(longhands) = expand_shorthand(property, &decl.value, decl.important) { + for lh in longhands { + if lh.important { + important_decls.push((lh.property, lh.value)); + } else { + normal_decls.push((lh.property, lh.value)); + } + } + } else { + // Regular longhand property + let value = parse_value(&decl.value); + if decl.important { + important_decls.push((property.clone(), value)); + } else { + normal_decls.push((property.clone(), value)); + } + } + } + } + + // Step 4: Apply inline style declarations (from style attribute). + // Inline styles have specificity (1,0,0,0) — higher than any selector. + // We apply them after stylesheet rules so they override. + let inline_decls = parse_inline_style(doc, node); + + // Step 5: Apply normal declarations (already in specificity order) + for (prop, value) in &normal_decls { + apply_property(&mut style, prop, value, parent_style); + } + + // Step 6: Apply inline style normal declarations (override stylesheet normals) + for decl in &inline_decls { + if !decl.important { + let property = decl.property.as_str(); + if let Some(longhands) = expand_shorthand(property, &decl.value, false) { + for lh in &longhands { + apply_property(&mut style, &lh.property, &lh.value, parent_style); + } + } else { + let value = parse_value(&decl.value); + apply_property(&mut style, property, &value, parent_style); + } + } + } + + // Step 7: Apply !important declarations (override everything normal) + for (prop, value) in &important_decls { + apply_property(&mut style, prop, value, parent_style); + } + + // Step 8: Apply inline style !important declarations (highest priority) + for decl in &inline_decls { + if decl.important { + let property = decl.property.as_str(); + if let Some(longhands) = expand_shorthand(property, &decl.value, true) { + for lh in &longhands { + apply_property(&mut style, &lh.property, &lh.value, parent_style); + } + } else { + let value = parse_value(&decl.value); + apply_property(&mut style, property, &value, parent_style); + } + } + } + + style +} + +/// Parse inline style from the `style` attribute of an element. +fn parse_inline_style(doc: &Document, node: NodeId) -> Vec { + if let Some(style_attr) = doc.get_attribute(node, "style") { + // Wrap in a dummy rule so the CSS parser can parse it + let css = format!("x {{ {style_attr} }}"); + let ss = we_css::parser::Parser::parse(&css); + if let Some(we_css::parser::Rule::Style(rule)) = ss.rules.into_iter().next() { + rule.declarations + } else { + Vec::new() + } + } else { + Vec::new() + } +} + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +#[cfg(test)] +mod tests { + use super::*; + use we_css::parser::Parser; + + fn make_doc_with_body() -> (Document, NodeId, NodeId, NodeId) { + let mut doc = Document::new(); + let root = doc.root(); + let html = doc.create_element("html"); + let body = doc.create_element("body"); + doc.append_child(root, html); + doc.append_child(html, body); + (doc, root, html, body) + } + + // ----------------------------------------------------------------------- + // UA defaults + // ----------------------------------------------------------------------- + + #[test] + fn ua_body_has_8px_margin() { + let (doc, _, _, _) = make_doc_with_body(); + let styled = resolve_styles(&doc, &[]).unwrap(); + // styled is , first child is + let body = &styled.children[0]; + assert_eq!(body.style.margin_top, LengthOrAuto::Length(8.0)); + assert_eq!(body.style.margin_right, LengthOrAuto::Length(8.0)); + assert_eq!(body.style.margin_bottom, LengthOrAuto::Length(8.0)); + assert_eq!(body.style.margin_left, LengthOrAuto::Length(8.0)); + } + + #[test] + fn ua_h1_font_size() { + let (mut doc, _, _, body) = make_doc_with_body(); + let h1 = doc.create_element("h1"); + let text = doc.create_text("Title"); + doc.append_child(body, h1); + doc.append_child(h1, text); + + let styled = resolve_styles(&doc, &[]).unwrap(); + let body_node = &styled.children[0]; + let h1_node = &body_node.children[0]; + + // h1 = 2em of parent (16px) = 32px + assert_eq!(h1_node.style.font_size, 32.0); + assert_eq!(h1_node.style.font_weight, FontWeight(700.0)); + } + + #[test] + fn ua_h2_font_size() { + let (mut doc, _, _, body) = make_doc_with_body(); + let h2 = doc.create_element("h2"); + doc.append_child(body, h2); + + let styled = resolve_styles(&doc, &[]).unwrap(); + let body_node = &styled.children[0]; + let h2_node = &body_node.children[0]; + + // h2 = 1.5em = 24px + assert_eq!(h2_node.style.font_size, 24.0); + } + + #[test] + fn ua_p_has_1em_margins() { + let (mut doc, _, _, body) = make_doc_with_body(); + let p = doc.create_element("p"); + let text = doc.create_text("text"); + doc.append_child(body, p); + doc.append_child(p, text); + + let styled = resolve_styles(&doc, &[]).unwrap(); + let body_node = &styled.children[0]; + let p_node = &body_node.children[0]; + + // p margin-top/bottom = 1em = 16px + assert_eq!(p_node.style.margin_top, LengthOrAuto::Length(16.0)); + assert_eq!(p_node.style.margin_bottom, LengthOrAuto::Length(16.0)); + } + + #[test] + fn ua_display_block_elements() { + let (mut doc, _, _, body) = make_doc_with_body(); + let div = doc.create_element("div"); + doc.append_child(body, div); + + let styled = resolve_styles(&doc, &[]).unwrap(); + let body_node = &styled.children[0]; + let div_node = &body_node.children[0]; + assert_eq!(div_node.style.display, Display::Block); + } + + #[test] + fn ua_display_inline_elements() { + let (mut doc, _, _, body) = make_doc_with_body(); + let p = doc.create_element("p"); + let span = doc.create_element("span"); + let text = doc.create_text("x"); + doc.append_child(body, p); + doc.append_child(p, span); + doc.append_child(span, text); + + let styled = resolve_styles(&doc, &[]).unwrap(); + let body_node = &styled.children[0]; + let p_node = &body_node.children[0]; + let span_node = &p_node.children[0]; + assert_eq!(span_node.style.display, Display::Inline); + } + + #[test] + fn ua_display_none_for_head() { + let (mut doc, _, html, _) = make_doc_with_body(); + let head = doc.create_element("head"); + let title = doc.create_element("title"); + doc.append_child(html, head); + doc.append_child(head, title); + + let styled = resolve_styles(&doc, &[]).unwrap(); + // head should not appear in styled tree (display: none) + for child in &styled.children { + if let NodeData::Element { tag_name, .. } = doc.node_data(child.node) { + assert_ne!(tag_name.as_str(), "head"); + } + } + } + + // ----------------------------------------------------------------------- + // Author stylesheets + // ----------------------------------------------------------------------- + + #[test] + fn author_color_override() { + let (mut doc, _, _, body) = make_doc_with_body(); + let p = doc.create_element("p"); + let text = doc.create_text("hello"); + doc.append_child(body, p); + doc.append_child(p, text); + + let ss = Parser::parse("p { color: red; }"); + let styled = resolve_styles(&doc, &[ss]).unwrap(); + let body_node = &styled.children[0]; + let p_node = &body_node.children[0]; + assert_eq!(p_node.style.color, Color::rgb(255, 0, 0)); + } + + #[test] + fn author_background_color() { + let (mut doc, _, _, body) = make_doc_with_body(); + let div = doc.create_element("div"); + doc.append_child(body, div); + + let ss = Parser::parse("div { background-color: blue; }"); + let styled = resolve_styles(&doc, &[ss]).unwrap(); + let body_node = &styled.children[0]; + let div_node = &body_node.children[0]; + assert_eq!(div_node.style.background_color, Color::rgb(0, 0, 255)); + } + + #[test] + fn author_font_size_px() { + let (mut doc, _, _, body) = make_doc_with_body(); + let p = doc.create_element("p"); + doc.append_child(body, p); + + let ss = Parser::parse("p { font-size: 24px; }"); + let styled = resolve_styles(&doc, &[ss]).unwrap(); + let body_node = &styled.children[0]; + let p_node = &body_node.children[0]; + assert_eq!(p_node.style.font_size, 24.0); + } + + #[test] + fn author_margin_px() { + let (mut doc, _, _, body) = make_doc_with_body(); + let div = doc.create_element("div"); + doc.append_child(body, div); + + let ss = Parser::parse("div { margin-top: 20px; margin-bottom: 10px; }"); + let styled = resolve_styles(&doc, &[ss]).unwrap(); + let body_node = &styled.children[0]; + let div_node = &body_node.children[0]; + assert_eq!(div_node.style.margin_top, LengthOrAuto::Length(20.0)); + assert_eq!(div_node.style.margin_bottom, LengthOrAuto::Length(10.0)); + } + + // ----------------------------------------------------------------------- + // Cascade: specificity ordering + // ----------------------------------------------------------------------- + + #[test] + fn higher_specificity_wins() { + let (mut doc, _, _, body) = make_doc_with_body(); + let p = doc.create_element("p"); + doc.set_attribute(p, "class", "highlight"); + let text = doc.create_text("x"); + doc.append_child(body, p); + doc.append_child(p, text); + + // .highlight (0,1,0) > p (0,0,1) + let ss = Parser::parse("p { color: red; } .highlight { color: green; }"); + let styled = resolve_styles(&doc, &[ss]).unwrap(); + let body_node = &styled.children[0]; + let p_node = &body_node.children[0]; + assert_eq!(p_node.style.color, Color::rgb(0, 128, 0)); + } + + #[test] + fn source_order_tiebreak() { + let (mut doc, _, _, body) = make_doc_with_body(); + let p = doc.create_element("p"); + let text = doc.create_text("x"); + doc.append_child(body, p); + doc.append_child(p, text); + + // Same specificity: later wins + let ss = Parser::parse("p { color: red; } p { color: blue; }"); + let styled = resolve_styles(&doc, &[ss]).unwrap(); + let body_node = &styled.children[0]; + let p_node = &body_node.children[0]; + assert_eq!(p_node.style.color, Color::rgb(0, 0, 255)); + } + + #[test] + fn important_overrides_specificity() { + let (mut doc, _, _, body) = make_doc_with_body(); + let p = doc.create_element("p"); + doc.set_attribute(p, "id", "main"); + let text = doc.create_text("x"); + doc.append_child(body, p); + doc.append_child(p, text); + + // #main (1,0,0) has higher specificity, but p has !important + let ss = Parser::parse("#main { color: blue; } p { color: red !important; }"); + let styled = resolve_styles(&doc, &[ss]).unwrap(); + let body_node = &styled.children[0]; + let p_node = &body_node.children[0]; + assert_eq!(p_node.style.color, Color::rgb(255, 0, 0)); + } + + // ----------------------------------------------------------------------- + // Inheritance + // ----------------------------------------------------------------------- + + #[test] + fn color_inherits_to_children() { + let (mut doc, _, _, body) = make_doc_with_body(); + let div = doc.create_element("div"); + let p = doc.create_element("p"); + let text = doc.create_text("x"); + doc.append_child(body, div); + doc.append_child(div, p); + doc.append_child(p, text); + + let ss = Parser::parse("div { color: green; }"); + let styled = resolve_styles(&doc, &[ss]).unwrap(); + let body_node = &styled.children[0]; + let div_node = &body_node.children[0]; + let p_node = &div_node.children[0]; + + assert_eq!(div_node.style.color, Color::rgb(0, 128, 0)); + // p inherits color from div + assert_eq!(p_node.style.color, Color::rgb(0, 128, 0)); + } + + #[test] + fn font_size_inherits() { + let (mut doc, _, _, body) = make_doc_with_body(); + let div = doc.create_element("div"); + let p = doc.create_element("p"); + let text = doc.create_text("x"); + doc.append_child(body, div); + doc.append_child(div, p); + doc.append_child(p, text); + + let ss = Parser::parse("div { font-size: 20px; }"); + let styled = resolve_styles(&doc, &[ss]).unwrap(); + let body_node = &styled.children[0]; + let div_node = &body_node.children[0]; + let p_node = &div_node.children[0]; + + assert_eq!(div_node.style.font_size, 20.0); + assert_eq!(p_node.style.font_size, 20.0); + } + + #[test] + fn margin_does_not_inherit() { + let (mut doc, _, _, body) = make_doc_with_body(); + let div = doc.create_element("div"); + let span = doc.create_element("span"); + let text = doc.create_text("x"); + doc.append_child(body, div); + doc.append_child(div, span); + doc.append_child(span, text); + + let ss = Parser::parse("div { margin-top: 50px; }"); + let styled = resolve_styles(&doc, &[ss]).unwrap(); + let body_node = &styled.children[0]; + let div_node = &body_node.children[0]; + let span_node = &div_node.children[0]; + + assert_eq!(div_node.style.margin_top, LengthOrAuto::Length(50.0)); + // span should NOT inherit margin + assert_eq!(span_node.style.margin_top, LengthOrAuto::Length(0.0)); + } + + // ----------------------------------------------------------------------- + // inherit / initial / unset keywords + // ----------------------------------------------------------------------- + + #[test] + fn inherit_keyword_for_non_inherited() { + let (mut doc, _, _, body) = make_doc_with_body(); + let div = doc.create_element("div"); + let p = doc.create_element("p"); + let text = doc.create_text("x"); + doc.append_child(body, div); + doc.append_child(div, p); + doc.append_child(p, text); + + let ss = Parser::parse("div { background-color: red; } p { background-color: inherit; }"); + let styled = resolve_styles(&doc, &[ss]).unwrap(); + let body_node = &styled.children[0]; + let div_node = &body_node.children[0]; + let p_node = &div_node.children[0]; + + assert_eq!(div_node.style.background_color, Color::rgb(255, 0, 0)); + assert_eq!(p_node.style.background_color, Color::rgb(255, 0, 0)); + } + + #[test] + fn initial_keyword_resets() { + let (mut doc, _, _, body) = make_doc_with_body(); + let div = doc.create_element("div"); + let p = doc.create_element("p"); + let text = doc.create_text("x"); + doc.append_child(body, div); + doc.append_child(div, p); + doc.append_child(p, text); + + // div sets color to red, p resets to initial (black) + let ss = Parser::parse("div { color: red; } p { color: initial; }"); + let styled = resolve_styles(&doc, &[ss]).unwrap(); + let body_node = &styled.children[0]; + let div_node = &body_node.children[0]; + let p_node = &div_node.children[0]; + + assert_eq!(div_node.style.color, Color::rgb(255, 0, 0)); + assert_eq!(p_node.style.color, Color::rgb(0, 0, 0)); // initial + } + + #[test] + fn unset_inherits_for_inherited_property() { + let (mut doc, _, _, body) = make_doc_with_body(); + let div = doc.create_element("div"); + let p = doc.create_element("p"); + let text = doc.create_text("x"); + doc.append_child(body, div); + doc.append_child(div, p); + doc.append_child(p, text); + + // color is inherited, so unset => inherit + let ss = Parser::parse("div { color: green; } p { color: unset; }"); + let styled = resolve_styles(&doc, &[ss]).unwrap(); + let body_node = &styled.children[0]; + let div_node = &body_node.children[0]; + let p_node = &div_node.children[0]; + + assert_eq!(p_node.style.color, div_node.style.color); + } + + #[test] + fn unset_resets_for_non_inherited_property() { + let (mut doc, _, _, body) = make_doc_with_body(); + let div = doc.create_element("div"); + let p = doc.create_element("p"); + let text = doc.create_text("x"); + doc.append_child(body, div); + doc.append_child(div, p); + doc.append_child(p, text); + + // margin is non-inherited, so unset => initial (0) + let ss = Parser::parse("p { margin-top: unset; }"); + let styled = resolve_styles(&doc, &[ss]).unwrap(); + let body_node = &styled.children[0]; + let p_node = &body_node.children[0]; + + // UA sets p margin-top to 1em=16px, but unset resets to initial (0) + assert_eq!(p_node.style.margin_top, LengthOrAuto::Length(0.0)); + } + + // ----------------------------------------------------------------------- + // Em unit resolution + // ----------------------------------------------------------------------- + + #[test] + fn em_margin_relative_to_font_size() { + let (mut doc, _, _, body) = make_doc_with_body(); + let div = doc.create_element("div"); + let text = doc.create_text("x"); + doc.append_child(body, div); + doc.append_child(div, text); + + let ss = Parser::parse("div { font-size: 20px; margin-top: 2em; }"); + let styled = resolve_styles(&doc, &[ss]).unwrap(); + let body_node = &styled.children[0]; + let div_node = &body_node.children[0]; + + assert_eq!(div_node.style.font_size, 20.0); + // margin-top em resolves relative to parent font-size (16px body) + // because margin is not font-size itself + assert_eq!(div_node.style.margin_top, LengthOrAuto::Length(32.0)); + } + + #[test] + fn em_font_size_relative_to_parent() { + let (mut doc, _, _, body) = make_doc_with_body(); + let div = doc.create_element("div"); + let p = doc.create_element("p"); + let text = doc.create_text("x"); + doc.append_child(body, div); + doc.append_child(div, p); + doc.append_child(p, text); + + let ss = Parser::parse("div { font-size: 20px; } p { font-size: 1.5em; }"); + let styled = resolve_styles(&doc, &[ss]).unwrap(); + let body_node = &styled.children[0]; + let div_node = &body_node.children[0]; + let p_node = &div_node.children[0]; + + assert_eq!(div_node.style.font_size, 20.0); + // p font-size = 1.5 * parent(20px) = 30px + assert_eq!(p_node.style.font_size, 30.0); + } + + // ----------------------------------------------------------------------- + // Inline styles + // ----------------------------------------------------------------------- + + #[test] + fn inline_style_overrides_stylesheet() { + let (mut doc, _, _, body) = make_doc_with_body(); + let p = doc.create_element("p"); + doc.set_attribute(p, "style", "color: green;"); + let text = doc.create_text("x"); + doc.append_child(body, p); + doc.append_child(p, text); + + let ss = Parser::parse("p { color: red; }"); + let styled = resolve_styles(&doc, &[ss]).unwrap(); + let body_node = &styled.children[0]; + let p_node = &body_node.children[0]; + + // Inline style wins over author stylesheet + assert_eq!(p_node.style.color, Color::rgb(0, 128, 0)); + } + + #[test] + fn inline_style_important() { + let (mut doc, _, _, body) = make_doc_with_body(); + let p = doc.create_element("p"); + doc.set_attribute(p, "style", "color: green !important;"); + let text = doc.create_text("x"); + doc.append_child(body, p); + doc.append_child(p, text); + + let ss = Parser::parse("p { color: red !important; }"); + let styled = resolve_styles(&doc, &[ss]).unwrap(); + let body_node = &styled.children[0]; + let p_node = &body_node.children[0]; + + // Inline !important beats stylesheet !important + assert_eq!(p_node.style.color, Color::rgb(0, 128, 0)); + } + + // ----------------------------------------------------------------------- + // Shorthand expansion + // ----------------------------------------------------------------------- + + #[test] + fn margin_shorthand_in_stylesheet() { + let (mut doc, _, _, body) = make_doc_with_body(); + let div = doc.create_element("div"); + let text = doc.create_text("x"); + doc.append_child(body, div); + doc.append_child(div, text); + + let ss = Parser::parse("div { margin: 10px 20px; }"); + let styled = resolve_styles(&doc, &[ss]).unwrap(); + let body_node = &styled.children[0]; + let div_node = &body_node.children[0]; + + assert_eq!(div_node.style.margin_top, LengthOrAuto::Length(10.0)); + assert_eq!(div_node.style.margin_right, LengthOrAuto::Length(20.0)); + assert_eq!(div_node.style.margin_bottom, LengthOrAuto::Length(10.0)); + assert_eq!(div_node.style.margin_left, LengthOrAuto::Length(20.0)); + } + + #[test] + fn padding_shorthand() { + let (mut doc, _, _, body) = make_doc_with_body(); + let div = doc.create_element("div"); + let text = doc.create_text("x"); + doc.append_child(body, div); + doc.append_child(div, text); + + let ss = Parser::parse("div { padding: 5px; }"); + let styled = resolve_styles(&doc, &[ss]).unwrap(); + let body_node = &styled.children[0]; + let div_node = &body_node.children[0]; + + assert_eq!(div_node.style.padding_top, 5.0); + assert_eq!(div_node.style.padding_right, 5.0); + assert_eq!(div_node.style.padding_bottom, 5.0); + assert_eq!(div_node.style.padding_left, 5.0); + } + + // ----------------------------------------------------------------------- + // UA + author cascade + // ----------------------------------------------------------------------- + + #[test] + fn author_overrides_ua() { + let (mut doc, _, _, body) = make_doc_with_body(); + let p = doc.create_element("p"); + let text = doc.create_text("x"); + doc.append_child(body, p); + doc.append_child(p, text); + + // UA gives p margin-top=1em=16px. Author overrides to 0. + let ss = Parser::parse("p { margin-top: 0; margin-bottom: 0; }"); + let styled = resolve_styles(&doc, &[ss]).unwrap(); + let body_node = &styled.children[0]; + let p_node = &body_node.children[0]; + + assert_eq!(p_node.style.margin_top, LengthOrAuto::Length(0.0)); + assert_eq!(p_node.style.margin_bottom, LengthOrAuto::Length(0.0)); + } + + // ----------------------------------------------------------------------- + // Text node inherits parent style + // ----------------------------------------------------------------------- + + #[test] + fn text_node_inherits_style() { + let (mut doc, _, _, body) = make_doc_with_body(); + let p = doc.create_element("p"); + let text = doc.create_text("hello"); + doc.append_child(body, p); + doc.append_child(p, text); + + let ss = Parser::parse("p { color: red; font-size: 20px; }"); + let styled = resolve_styles(&doc, &[ss]).unwrap(); + let body_node = &styled.children[0]; + let p_node = &body_node.children[0]; + let text_node = &p_node.children[0]; + + assert_eq!(text_node.style.color, Color::rgb(255, 0, 0)); + assert_eq!(text_node.style.font_size, 20.0); + } + + // ----------------------------------------------------------------------- + // Multiple stylesheets + // ----------------------------------------------------------------------- + + #[test] + fn multiple_author_stylesheets() { + let (mut doc, _, _, body) = make_doc_with_body(); + let p = doc.create_element("p"); + let text = doc.create_text("x"); + doc.append_child(body, p); + doc.append_child(p, text); + + let ss1 = Parser::parse("p { color: red; }"); + let ss2 = Parser::parse("p { color: blue; }"); + let styled = resolve_styles(&doc, &[ss1, ss2]).unwrap(); + let body_node = &styled.children[0]; + let p_node = &body_node.children[0]; + + // Later stylesheet wins (higher source order) + assert_eq!(p_node.style.color, Color::rgb(0, 0, 255)); + } + + // ----------------------------------------------------------------------- + // Border + // ----------------------------------------------------------------------- + + #[test] + fn border_shorthand_all_sides() { + let (mut doc, _, _, body) = make_doc_with_body(); + let div = doc.create_element("div"); + let text = doc.create_text("x"); + doc.append_child(body, div); + doc.append_child(div, text); + + let ss = Parser::parse("div { border: 2px solid red; }"); + let styled = resolve_styles(&doc, &[ss]).unwrap(); + let body_node = &styled.children[0]; + let div_node = &body_node.children[0]; + + assert_eq!(div_node.style.border_top_width, 2.0); + assert_eq!(div_node.style.border_top_style, BorderStyle::Solid); + assert_eq!(div_node.style.border_top_color, Color::rgb(255, 0, 0)); + } + + // ----------------------------------------------------------------------- + // Position + // ----------------------------------------------------------------------- + + #[test] + fn position_property() { + let (mut doc, _, _, body) = make_doc_with_body(); + let div = doc.create_element("div"); + let text = doc.create_text("x"); + doc.append_child(body, div); + doc.append_child(div, text); + + let ss = Parser::parse("div { position: relative; top: 10px; }"); + let styled = resolve_styles(&doc, &[ss]).unwrap(); + let body_node = &styled.children[0]; + let div_node = &body_node.children[0]; + + assert_eq!(div_node.style.position, Position::Relative); + assert_eq!(div_node.style.top, LengthOrAuto::Length(10.0)); + } + + // ----------------------------------------------------------------------- + // Display: none removes from tree + // ----------------------------------------------------------------------- + + #[test] + fn display_none_removes_element() { + let (mut doc, _, _, body) = make_doc_with_body(); + let div1 = doc.create_element("div"); + let t1 = doc.create_text("visible"); + doc.append_child(body, div1); + doc.append_child(div1, t1); + + let div2 = doc.create_element("div"); + doc.set_attribute(div2, "class", "hidden"); + let t2 = doc.create_text("hidden"); + doc.append_child(body, div2); + doc.append_child(div2, t2); + + let ss = Parser::parse(".hidden { display: none; }"); + let styled = resolve_styles(&doc, &[ss]).unwrap(); + let body_node = &styled.children[0]; + + // Only div1 should appear + assert_eq!(body_node.children.len(), 1); + } + + // ----------------------------------------------------------------------- + // Visibility + // ----------------------------------------------------------------------- + + #[test] + fn visibility_inherited() { + let (mut doc, _, _, body) = make_doc_with_body(); + let div = doc.create_element("div"); + let p = doc.create_element("p"); + let text = doc.create_text("x"); + doc.append_child(body, div); + doc.append_child(div, p); + doc.append_child(p, text); + + let ss = Parser::parse("div { visibility: hidden; }"); + let styled = resolve_styles(&doc, &[ss]).unwrap(); + let body_node = &styled.children[0]; + let div_node = &body_node.children[0]; + let p_node = &div_node.children[0]; + + assert_eq!(div_node.style.visibility, Visibility::Hidden); + assert_eq!(p_node.style.visibility, Visibility::Hidden); + } +} diff --git a/crates/style/src/lib.rs b/crates/style/src/lib.rs index 16396c0..24d4d38 100644 --- a/crates/style/src/lib.rs +++ b/crates/style/src/lib.rs @@ -1,3 +1,4 @@ //! Selector matching and computed style resolution. +pub mod computed; pub mod matching; -- 2.51.2