//! Computed-style lite: parse inline CSS declarations plus a small UA default //! sheet into the layout-relevant subset of [`crate::LayoutStyle`]. //! //! This is deliberately not a full CSS cascade. It handles the properties that //! influence phase-1 box layout (display, width/height, margin, padding, border //! width) from inline `style="..."` attributes, layered on a tiny built-in UA //! sheet. A real cascade with selector matching and `cssparser` can harden this //! later; for inline-style attributes a compact tokenizer is enough and keeps //! the crate dependency-light. use crate::{Display, Edges, LayoutStyle}; /// Compute the layout-relevant style for an element: UA defaults for its tag, /// overridden by its inline `style="..."` declarations. pub fn compute_style(tag: &str, inline_css: Option<&str>) -> LayoutStyle { let mut style = ua_style(tag); if let Some(css) = inline_css { apply_inline(&mut style, css); } style } /// Built-in UA defaults. Inline elements currently map to block layout; real /// inline/text layout arrives with the text/paint phase. pub fn ua_style(tag: &str) -> LayoutStyle { let _ = tag; LayoutStyle { display: Display::Block, ..Default::default() } } pub fn apply_inline(style: &mut LayoutStyle, css: &str) { for raw in css.split(';') { let decl = raw.trim(); if decl.is_empty() { continue; } let Some((name, value)) = decl.split_once(':') else { continue }; let name = name.trim().to_ascii_lowercase(); // Drop `!important`: it does not change the computed value here. let value: String = value .trim() .split_whitespace() .take_while(|t| !t.eq_ignore_ascii_case("!important") && *t != "!") .collect::>() .join(" "); apply_value(style, &name, &value); } } fn apply_value(style: &mut LayoutStyle, name: &str, value: &str) { match name { "display" => match value { "block" => style.display = Display::Block, "flex" => style.display = Display::Flex, "none" => style.display = Display::None, _ => {} }, "width" => style.width = px(value), "height" => style.height = px(value), "margin" => { if let Some(e) = edges(value) { style.margin = e; } } "margin-top" => set_edge(&mut style.margin, Side::Top, px(value)), "margin-right" => set_edge(&mut style.margin, Side::Right, px(value)), "margin-bottom" => set_edge(&mut style.margin, Side::Bottom, px(value)), "margin-left" => set_edge(&mut style.margin, Side::Left, px(value)), "padding" => { if let Some(e) = edges(value) { style.padding = e; } } "padding-top" => set_edge(&mut style.padding, Side::Top, px(value)), "padding-right" => set_edge(&mut style.padding, Side::Right, px(value)), "padding-bottom" => set_edge(&mut style.padding, Side::Bottom, px(value)), "padding-left" => set_edge(&mut style.padding, Side::Left, px(value)), "border-width" | "border" => { if let Some(e) = edges(value) { style.border = e; } } "border-top-width" | "border-top" => set_edge(&mut style.border, Side::Top, px(value)), "border-right-width" | "border-right" => set_edge(&mut style.border, Side::Right, px(value)), "border-bottom-width" | "border-bottom" => set_edge(&mut style.border, Side::Bottom, px(value)), "border-left-width" | "border-left" => set_edge(&mut style.border, Side::Left, px(value)), "background-color" | "background" => style.background_color = parse_color(value), "color" => style.color = parse_color(value), "border-color" => style.border_color = parse_color(value), _ => {} } } /// Parse a CSS color to RGBA. Handles #rgb, #rgba, #rrggbb, #rrggbbaa hex and a /// small set of named colors. Returns None for anything else (transparent). fn parse_color(value: &str) -> Option<[u8; 4]> { let v = value.split_whitespace().next()?.to_ascii_lowercase(); if let Some(h) = v.strip_prefix('#') { let (r, g, b, a) = match h.len() { 3 => ( u8::from_str_radix(&h[0..1].repeat(2), 16).ok()?, u8::from_str_radix(&h[1..2].repeat(2), 16).ok()?, u8::from_str_radix(&h[2..3].repeat(2), 16).ok()?, 255u8, ), 4 => ( u8::from_str_radix(&h[0..1].repeat(2), 16).ok()?, u8::from_str_radix(&h[1..2].repeat(2), 16).ok()?, u8::from_str_radix(&h[2..3].repeat(2), 16).ok()?, u8::from_str_radix(&h[3..4].repeat(2), 16).ok()?, ), 6 => ( u8::from_str_radix(&h[0..2], 16).ok()?, u8::from_str_radix(&h[2..4], 16).ok()?, u8::from_str_radix(&h[4..6], 16).ok()?, 255u8, ), 8 => ( u8::from_str_radix(&h[0..2], 16).ok()?, u8::from_str_radix(&h[2..4], 16).ok()?, u8::from_str_radix(&h[4..6], 16).ok()?, u8::from_str_radix(&h[6..8], 16).ok()?, ), _ => return None, }; return Some([r, g, b, a]); } // Named colors (the common few). let named = match v.as_str() { "transparent" => [0, 0, 0, 0], "black" => [0, 0, 0, 255], "white" => [255, 255, 255, 255], "red" => [255, 0, 0, 255], "green" => [0, 128, 0, 255], "blue" => [0, 0, 255, 255], "yellow" => [255, 255, 0, 255], "gray" | "grey" => [128, 128, 128, 255], _ => return None, }; Some(named) } enum Side { Top, Right, Bottom, Left } fn set_edge(edges: &mut Edges, side: Side, v: Option) { let v = match v { Some(v) => v, None => return }; match side { Side::Top => edges.top = v, Side::Right => edges.right = v, Side::Bottom => edges.bottom = v, Side::Left => edges.left = v, } } /// Parse the first length in a value as CSS pixels. `auto`, percentages, and /// non-numeric values return None (treated as "no explicit size" in phase 1). fn px(value: &str) -> Option { token(value).and_then(px_value) } /// Parse every length token in a value as px (for box shorthands). fn edges(value: &str) -> Option { let dims: Vec = value.split_whitespace().filter_map(px_value).collect(); edges_from(dims) } fn px_value(tok: &str) -> Option { let n = tok.strip_suffix("px").unwrap_or(tok); // Reject percentages and other units for phase 1. if n.chars().any(|c| !(c.is_ascii_digit() || c == '.' || c == '-')) { return None; } n.parse::().ok() } fn token(value: &str) -> Option<&str> { value.split_whitespace().next() } /// CSS box shorthand: 1 value applies to all sides; 2 values are (v, h); /// 3 values are (top, h, bottom); 4 values are (top, right, bottom, left). fn edges_from(dims: Vec) -> Option { match dims.len() { 0 => None, 1 => Some(Edges { top: dims[0], right: dims[0], bottom: dims[0], left: dims[0] }), 2 => Some(Edges { top: dims[0], right: dims[1], bottom: dims[0], left: dims[1] }), 3 => Some(Edges { top: dims[0], right: dims[1], bottom: dims[2], left: dims[1] }), _ => Some(Edges { top: dims[0], right: dims[1], bottom: dims[2], left: dims[3] }), } } #[cfg(test)] mod tests { use super::*; #[test] fn parses_display_and_size() { let s = compute_style("div", Some("display: flex; width: 200px; height: 50px")); assert_eq!(s.display, Display::Flex); assert_eq!(s.width, Some(200.0)); assert_eq!(s.height, Some(50.0)); } #[test] fn margin_shorthand_expands() { let s = compute_style("div", Some("margin: 10px 20px")); assert_eq!(s.margin, Edges { top: 10.0, right: 20.0, bottom: 10.0, left: 20.0 }); } #[test] fn longhand_overrides_shorthand() { let s = compute_style("div", Some("padding: 5px; padding-left: 30px")); assert_eq!(s.padding.top, 5.0); assert_eq!(s.padding.left, 30.0); } #[test] fn ua_defaults_and_ignore_unknown() { let s = compute_style("span", Some("color: red; ; bogus: ; display: none")); assert_eq!(s.display, Display::None); } #[test] fn important_and_auto() { let s = compute_style("div", Some("width: 100px !important; height: auto")); assert_eq!(s.width, Some(100.0)); assert_eq!(s.height, None); } }