//! CSS value parsing: convert raw component values into typed property values. //! //! Provides `CssValue` enum and parsing from `ComponentValue` lists. use crate::parser::ComponentValue; // --------------------------------------------------------------------------- // Core value types // --------------------------------------------------------------------------- /// A fully parsed, typed CSS value. #[derive(Debug, Clone, PartialEq)] pub enum CssValue { /// A length with resolved unit. Length(f64, LengthUnit), /// A percentage value. Percentage(f64), /// A color value (r, g, b, a in 0–255 range, alpha 0–255). Color(Color), /// A numeric value (unitless). Number(f64), /// A string value. String(String), /// A keyword (ident). Keyword(String), /// The `auto` keyword. Auto, /// The `inherit` keyword. Inherit, /// The `initial` keyword. Initial, /// The `unset` keyword. Unset, /// The `currentColor` keyword. CurrentColor, /// The `none` keyword (for display, background, etc.). None, /// The `transparent` keyword. Transparent, /// Zero (unitless). Zero, /// A list of values (for multi-value properties like margin shorthand). List(Vec), } /// CSS length unit. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum LengthUnit { // Absolute Px, Pt, Cm, Mm, In, Pc, // Font-relative Em, Rem, // Viewport Vw, Vh, Vmin, Vmax, } /// A CSS color in RGBA format. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct Color { pub r: u8, pub g: u8, pub b: u8, pub a: u8, } impl Color { pub fn new(r: u8, g: u8, b: u8, a: u8) -> Self { Self { r, g, b, a } } pub fn rgb(r: u8, g: u8, b: u8) -> Self { Self { r, g, b, a: 255 } } } // --------------------------------------------------------------------------- // Shorthand expansion result // --------------------------------------------------------------------------- /// A longhand property-value pair produced by shorthand expansion. #[derive(Debug, Clone, PartialEq)] pub struct LonghandDeclaration { pub property: String, pub value: CssValue, pub important: bool, } // --------------------------------------------------------------------------- // Value parsing // --------------------------------------------------------------------------- /// Parse a single `CssValue` from a list of component values. pub fn parse_value(values: &[ComponentValue]) -> CssValue { // Filter out whitespace for easier matching let non_ws: Vec<&ComponentValue> = values .iter() .filter(|v| !matches!(v, ComponentValue::Whitespace)) .collect(); if non_ws.is_empty() { return CssValue::Keyword(String::new()); } // Single-value case if non_ws.len() == 1 { return parse_single_value(non_ws[0]); } // Multi-value: parse each non-whitespace value let parsed: Vec = non_ws.iter().copied().map(parse_single_value).collect(); CssValue::List(parsed) } /// Parse a single component value into a `CssValue`. pub fn parse_single_value(cv: &ComponentValue) -> CssValue { match cv { ComponentValue::Ident(s) => parse_keyword(s), ComponentValue::String(s) => CssValue::String(s.clone()), ComponentValue::Number(n, _) => { if *n == 0.0 { CssValue::Zero } else { CssValue::Number(*n) } } ComponentValue::Percentage(n) => CssValue::Percentage(*n), ComponentValue::Dimension(n, _, unit) => parse_dimension(*n, unit), ComponentValue::Hash(s, _) => parse_hex_color(s), ComponentValue::Function(name, args) => parse_function(name, args), ComponentValue::Comma => CssValue::Keyword(",".to_string()), ComponentValue::Delim(c) => CssValue::Keyword(c.to_string()), ComponentValue::Whitespace => CssValue::Keyword(" ".to_string()), } } fn parse_keyword(s: &str) -> CssValue { match s.to_ascii_lowercase().as_str() { "auto" => CssValue::Auto, "inherit" => CssValue::Inherit, "initial" => CssValue::Initial, "unset" => CssValue::Unset, "none" => CssValue::None, "transparent" => CssValue::Transparent, "currentcolor" => CssValue::CurrentColor, // Named colors name => { if let Some(color) = named_color(name) { CssValue::Color(color) } else { CssValue::Keyword(s.to_ascii_lowercase()) } } } } fn parse_dimension(n: f64, unit: &str) -> CssValue { let u = unit.to_ascii_lowercase(); match u.as_str() { "px" => CssValue::Length(n, LengthUnit::Px), "pt" => CssValue::Length(n, LengthUnit::Pt), "cm" => CssValue::Length(n, LengthUnit::Cm), "mm" => CssValue::Length(n, LengthUnit::Mm), "in" => CssValue::Length(n, LengthUnit::In), "pc" => CssValue::Length(n, LengthUnit::Pc), "em" => CssValue::Length(n, LengthUnit::Em), "rem" => CssValue::Length(n, LengthUnit::Rem), "vw" => CssValue::Length(n, LengthUnit::Vw), "vh" => CssValue::Length(n, LengthUnit::Vh), "vmin" => CssValue::Length(n, LengthUnit::Vmin), "vmax" => CssValue::Length(n, LengthUnit::Vmax), _ => CssValue::Keyword(format!("{n}{u}")), } } // --------------------------------------------------------------------------- // Color parsing // --------------------------------------------------------------------------- fn parse_hex_color(hex: &str) -> CssValue { let chars: Vec = hex.chars().collect(); match chars.len() { // #rgb 3 => { let r = hex_digit(chars[0]) * 17; let g = hex_digit(chars[1]) * 17; let b = hex_digit(chars[2]) * 17; CssValue::Color(Color::rgb(r, g, b)) } // #rgba 4 => { let r = hex_digit(chars[0]) * 17; let g = hex_digit(chars[1]) * 17; let b = hex_digit(chars[2]) * 17; let a = hex_digit(chars[3]) * 17; CssValue::Color(Color::new(r, g, b, a)) } // #rrggbb 6 => { let r = hex_byte(chars[0], chars[1]); let g = hex_byte(chars[2], chars[3]); let b = hex_byte(chars[4], chars[5]); CssValue::Color(Color::rgb(r, g, b)) } // #rrggbbaa 8 => { let r = hex_byte(chars[0], chars[1]); let g = hex_byte(chars[2], chars[3]); let b = hex_byte(chars[4], chars[5]); let a = hex_byte(chars[6], chars[7]); CssValue::Color(Color::new(r, g, b, a)) } _ => CssValue::Keyword(format!("#{hex}")), } } fn hex_digit(c: char) -> u8 { match c { '0'..='9' => c as u8 - b'0', 'a'..='f' => c as u8 - b'a' + 10, 'A'..='F' => c as u8 - b'A' + 10, _ => 0, } } fn hex_byte(hi: char, lo: char) -> u8 { hex_digit(hi) * 16 + hex_digit(lo) } fn parse_function(name: &str, args: &[ComponentValue]) -> CssValue { match name.to_ascii_lowercase().as_str() { "rgb" => parse_rgb(args, false), "rgba" => parse_rgb(args, true), _ => CssValue::Keyword(format!("{name}()")), } } fn parse_rgb(args: &[ComponentValue], _has_alpha: bool) -> CssValue { let nums: Vec = args .iter() .filter_map(|cv| match cv { ComponentValue::Number(n, _) => Some(*n), ComponentValue::Percentage(n) => Some(*n * 2.55), _ => Option::None, }) .collect(); match nums.len() { 3 => CssValue::Color(Color::rgb( clamp_u8(nums[0]), clamp_u8(nums[1]), clamp_u8(nums[2]), )), 4 => { let a = if args .iter() .any(|cv| matches!(cv, ComponentValue::Percentage(_))) { // If any arg is a percentage, treat alpha as 0-1 float clamp_u8(nums[3] / 2.55 * 255.0) } else { // Check if alpha looks like a 0-1 range if nums[3] <= 1.0 { clamp_u8(nums[3] * 255.0) } else { clamp_u8(nums[3]) } }; CssValue::Color(Color::new( clamp_u8(nums[0]), clamp_u8(nums[1]), clamp_u8(nums[2]), a, )) } _ => CssValue::Keyword("rgb()".to_string()), } } fn clamp_u8(n: f64) -> u8 { n.round().clamp(0.0, 255.0) as u8 } // --------------------------------------------------------------------------- // Named colors (CSS Level 1 + transparent) // --------------------------------------------------------------------------- fn named_color(name: &str) -> Option { Some(match name { "black" => Color::rgb(0, 0, 0), "silver" => Color::rgb(192, 192, 192), "gray" | "grey" => Color::rgb(128, 128, 128), "white" => Color::rgb(255, 255, 255), "maroon" => Color::rgb(128, 0, 0), "red" => Color::rgb(255, 0, 0), "purple" => Color::rgb(128, 0, 128), "fuchsia" | "magenta" => Color::rgb(255, 0, 255), "green" => Color::rgb(0, 128, 0), "lime" => Color::rgb(0, 255, 0), "olive" => Color::rgb(128, 128, 0), "yellow" => Color::rgb(255, 255, 0), "navy" => Color::rgb(0, 0, 128), "blue" => Color::rgb(0, 0, 255), "teal" => Color::rgb(0, 128, 128), "aqua" | "cyan" => Color::rgb(0, 255, 255), "orange" => Color::rgb(255, 165, 0), _ => return Option::None, }) } // --------------------------------------------------------------------------- // Shorthand expansion // --------------------------------------------------------------------------- /// Expand a CSS declaration into longhand declarations. /// Returns `None` if the property is not a known shorthand. pub fn expand_shorthand( property: &str, values: &[ComponentValue], important: bool, ) -> Option> { match property { "margin" => Some(expand_box_shorthand("margin", values, important)), "padding" => Some(expand_box_shorthand("padding", values, important)), "border" => Some(expand_border(values, important)), "border-width" => Some( expand_box_shorthand("border", values, important) .into_iter() .map(|mut d| { d.property = format!("{}-width", d.property); d }) .collect(), ), "border-style" => Some( expand_box_shorthand("border", values, important) .into_iter() .map(|mut d| { d.property = format!("{}-style", d.property); d }) .collect(), ), "border-color" => Some( expand_box_shorthand("border", values, important) .into_iter() .map(|mut d| { d.property = format!("{}-color", d.property); d }) .collect(), ), "background" => Some(expand_background(values, important)), _ => Option::None, } } /// Expand a box-model shorthand (margin, padding) using the 1-to-4 value pattern. fn expand_box_shorthand( prefix: &str, values: &[ComponentValue], important: bool, ) -> Vec { let parsed: Vec = values .iter() .filter(|v| !matches!(v, ComponentValue::Whitespace | ComponentValue::Comma)) .map(parse_single_value) .collect(); let (top, right, bottom, left) = match parsed.len() { 1 => ( parsed[0].clone(), parsed[0].clone(), parsed[0].clone(), parsed[0].clone(), ), 2 => ( parsed[0].clone(), parsed[1].clone(), parsed[0].clone(), parsed[1].clone(), ), 3 => ( parsed[0].clone(), parsed[1].clone(), parsed[2].clone(), parsed[1].clone(), ), 4 => ( parsed[0].clone(), parsed[1].clone(), parsed[2].clone(), parsed[3].clone(), ), _ => { let fallback = if parsed.is_empty() { CssValue::Zero } else { parsed[0].clone() }; ( fallback.clone(), fallback.clone(), fallback.clone(), fallback, ) } }; vec![ LonghandDeclaration { property: format!("{prefix}-top"), value: top, important, }, LonghandDeclaration { property: format!("{prefix}-right"), value: right, important, }, LonghandDeclaration { property: format!("{prefix}-bottom"), value: bottom, important, }, LonghandDeclaration { property: format!("{prefix}-left"), value: left, important, }, ] } /// Expand `border` shorthand into border-width, border-style, border-color. fn expand_border(values: &[ComponentValue], important: bool) -> Vec { let parsed: Vec = values .iter() .filter(|v| !matches!(v, ComponentValue::Whitespace)) .map(parse_single_value) .collect(); let mut width = CssValue::Keyword("medium".to_string()); let mut style = CssValue::None; let mut color = CssValue::CurrentColor; for val in &parsed { match val { CssValue::Length(_, _) | CssValue::Zero => width = val.clone(), CssValue::Color(_) | CssValue::CurrentColor | CssValue::Transparent => { color = val.clone() } CssValue::Keyword(kw) => match kw.as_str() { "thin" | "medium" | "thick" => width = val.clone(), "none" | "hidden" | "dotted" | "dashed" | "solid" | "double" | "groove" | "ridge" | "inset" | "outset" => style = val.clone(), _ => { // Could be a named color if let Some(c) = named_color(kw) { color = CssValue::Color(c); } } }, _ => {} } } vec![ LonghandDeclaration { property: "border-width".to_string(), value: width, important, }, LonghandDeclaration { property: "border-style".to_string(), value: style, important, }, LonghandDeclaration { property: "border-color".to_string(), value: color, important, }, ] } /// Expand `background` shorthand (basic: color only for now). fn expand_background(values: &[ComponentValue], important: bool) -> Vec { let parsed: Vec = values .iter() .filter(|v| !matches!(v, ComponentValue::Whitespace)) .map(parse_single_value) .collect(); let mut bg_color = CssValue::Transparent; for val in &parsed { match val { CssValue::Color(_) | CssValue::CurrentColor | CssValue::Transparent => { bg_color = val.clone() } CssValue::Keyword(kw) => { if let Some(c) = named_color(kw) { bg_color = CssValue::Color(c); } else { match kw.as_str() { "none" => {} // background-image: none _ => bg_color = val.clone(), } } } _ => {} } } vec![LonghandDeclaration { property: "background-color".to_string(), value: bg_color, important, }] } // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- #[cfg(test)] mod tests { use super::*; use crate::tokenizer::{HashType, NumericType}; // -- Length tests -------------------------------------------------------- #[test] fn test_parse_px() { let cv = ComponentValue::Dimension(16.0, NumericType::Integer, "px".to_string()); assert_eq!( parse_single_value(&cv), CssValue::Length(16.0, LengthUnit::Px) ); } #[test] fn test_parse_em() { let cv = ComponentValue::Dimension(1.5, NumericType::Number, "em".to_string()); assert_eq!( parse_single_value(&cv), CssValue::Length(1.5, LengthUnit::Em) ); } #[test] fn test_parse_rem() { let cv = ComponentValue::Dimension(2.0, NumericType::Number, "rem".to_string()); assert_eq!( parse_single_value(&cv), CssValue::Length(2.0, LengthUnit::Rem) ); } #[test] fn test_parse_pt() { let cv = ComponentValue::Dimension(12.0, NumericType::Integer, "pt".to_string()); assert_eq!( parse_single_value(&cv), CssValue::Length(12.0, LengthUnit::Pt) ); } #[test] fn test_parse_cm() { let cv = ComponentValue::Dimension(2.54, NumericType::Number, "cm".to_string()); assert_eq!( parse_single_value(&cv), CssValue::Length(2.54, LengthUnit::Cm) ); } #[test] fn test_parse_mm() { let cv = ComponentValue::Dimension(10.0, NumericType::Integer, "mm".to_string()); assert_eq!( parse_single_value(&cv), CssValue::Length(10.0, LengthUnit::Mm) ); } #[test] fn test_parse_in() { let cv = ComponentValue::Dimension(1.0, NumericType::Integer, "in".to_string()); assert_eq!( parse_single_value(&cv), CssValue::Length(1.0, LengthUnit::In) ); } #[test] fn test_parse_pc() { let cv = ComponentValue::Dimension(6.0, NumericType::Integer, "pc".to_string()); assert_eq!( parse_single_value(&cv), CssValue::Length(6.0, LengthUnit::Pc) ); } #[test] fn test_parse_vw() { let cv = ComponentValue::Dimension(50.0, NumericType::Integer, "vw".to_string()); assert_eq!( parse_single_value(&cv), CssValue::Length(50.0, LengthUnit::Vw) ); } #[test] fn test_parse_vh() { let cv = ComponentValue::Dimension(100.0, NumericType::Integer, "vh".to_string()); assert_eq!( parse_single_value(&cv), CssValue::Length(100.0, LengthUnit::Vh) ); } #[test] fn test_parse_vmin() { let cv = ComponentValue::Dimension(50.0, NumericType::Integer, "vmin".to_string()); assert_eq!( parse_single_value(&cv), CssValue::Length(50.0, LengthUnit::Vmin) ); } #[test] fn test_parse_vmax() { let cv = ComponentValue::Dimension(50.0, NumericType::Integer, "vmax".to_string()); assert_eq!( parse_single_value(&cv), CssValue::Length(50.0, LengthUnit::Vmax) ); } #[test] fn test_parse_percentage() { let cv = ComponentValue::Percentage(50.0); assert_eq!(parse_single_value(&cv), CssValue::Percentage(50.0)); } #[test] fn test_parse_zero() { let cv = ComponentValue::Number(0.0, NumericType::Integer); assert_eq!(parse_single_value(&cv), CssValue::Zero); } #[test] fn test_parse_number() { let cv = ComponentValue::Number(42.0, NumericType::Integer); assert_eq!(parse_single_value(&cv), CssValue::Number(42.0)); } // -- Color tests -------------------------------------------------------- #[test] fn test_hex_color_3() { let cv = ComponentValue::Hash("f00".to_string(), HashType::Id); assert_eq!( parse_single_value(&cv), CssValue::Color(Color::rgb(255, 0, 0)) ); } #[test] fn test_hex_color_4() { let cv = ComponentValue::Hash("f00a".to_string(), HashType::Id); assert_eq!( parse_single_value(&cv), CssValue::Color(Color::new(255, 0, 0, 170)) ); } #[test] fn test_hex_color_6() { let cv = ComponentValue::Hash("ff8800".to_string(), HashType::Id); assert_eq!( parse_single_value(&cv), CssValue::Color(Color::rgb(255, 136, 0)) ); } #[test] fn test_hex_color_8() { let cv = ComponentValue::Hash("ff880080".to_string(), HashType::Id); assert_eq!( parse_single_value(&cv), CssValue::Color(Color::new(255, 136, 0, 128)) ); } #[test] fn test_named_color_red() { let cv = ComponentValue::Ident("red".to_string()); assert_eq!( parse_single_value(&cv), CssValue::Color(Color::rgb(255, 0, 0)) ); } #[test] fn test_named_color_blue() { let cv = ComponentValue::Ident("blue".to_string()); assert_eq!( parse_single_value(&cv), CssValue::Color(Color::rgb(0, 0, 255)) ); } #[test] fn test_named_color_black() { let cv = ComponentValue::Ident("black".to_string()); assert_eq!( parse_single_value(&cv), CssValue::Color(Color::rgb(0, 0, 0)) ); } #[test] fn test_named_color_white() { let cv = ComponentValue::Ident("white".to_string()); assert_eq!( parse_single_value(&cv), CssValue::Color(Color::rgb(255, 255, 255)) ); } #[test] fn test_transparent() { let cv = ComponentValue::Ident("transparent".to_string()); assert_eq!(parse_single_value(&cv), CssValue::Transparent); } #[test] fn test_current_color() { let cv = ComponentValue::Ident("currentColor".to_string()); assert_eq!(parse_single_value(&cv), CssValue::CurrentColor); } #[test] fn test_rgb_function() { let args = vec![ ComponentValue::Number(255.0, NumericType::Integer), ComponentValue::Comma, ComponentValue::Whitespace, ComponentValue::Number(128.0, NumericType::Integer), ComponentValue::Comma, ComponentValue::Whitespace, ComponentValue::Number(0.0, NumericType::Integer), ]; let cv = ComponentValue::Function("rgb".to_string(), args); assert_eq!( parse_single_value(&cv), CssValue::Color(Color::rgb(255, 128, 0)) ); } #[test] fn test_rgba_function() { let args = vec![ ComponentValue::Number(255.0, NumericType::Integer), ComponentValue::Comma, ComponentValue::Whitespace, ComponentValue::Number(0.0, NumericType::Integer), ComponentValue::Comma, ComponentValue::Whitespace, ComponentValue::Number(0.0, NumericType::Integer), ComponentValue::Comma, ComponentValue::Whitespace, ComponentValue::Number(0.5, NumericType::Number), ]; let cv = ComponentValue::Function("rgba".to_string(), args); assert_eq!( parse_single_value(&cv), CssValue::Color(Color::new(255, 0, 0, 128)) ); } // -- Keyword tests ------------------------------------------------------ #[test] fn test_keyword_auto() { let cv = ComponentValue::Ident("auto".to_string()); assert_eq!(parse_single_value(&cv), CssValue::Auto); } #[test] fn test_keyword_inherit() { let cv = ComponentValue::Ident("inherit".to_string()); assert_eq!(parse_single_value(&cv), CssValue::Inherit); } #[test] fn test_keyword_initial() { let cv = ComponentValue::Ident("initial".to_string()); assert_eq!(parse_single_value(&cv), CssValue::Initial); } #[test] fn test_keyword_unset() { let cv = ComponentValue::Ident("unset".to_string()); assert_eq!(parse_single_value(&cv), CssValue::Unset); } #[test] fn test_keyword_none() { let cv = ComponentValue::Ident("none".to_string()); assert_eq!(parse_single_value(&cv), CssValue::None); } #[test] fn test_keyword_display_block() { let cv = ComponentValue::Ident("block".to_string()); assert_eq!( parse_single_value(&cv), CssValue::Keyword("block".to_string()) ); } #[test] fn test_keyword_display_inline() { let cv = ComponentValue::Ident("inline".to_string()); assert_eq!( parse_single_value(&cv), CssValue::Keyword("inline".to_string()) ); } #[test] fn test_keyword_display_flex() { let cv = ComponentValue::Ident("flex".to_string()); assert_eq!( parse_single_value(&cv), CssValue::Keyword("flex".to_string()) ); } #[test] fn test_keyword_position() { for kw in &["static", "relative", "absolute", "fixed"] { let cv = ComponentValue::Ident(kw.to_string()); assert_eq!(parse_single_value(&cv), CssValue::Keyword(kw.to_string())); } } #[test] fn test_keyword_text_align() { for kw in &["left", "center", "right", "justify"] { let cv = ComponentValue::Ident(kw.to_string()); assert_eq!(parse_single_value(&cv), CssValue::Keyword(kw.to_string())); } } #[test] fn test_keyword_font_weight() { let cv = ComponentValue::Ident("bold".to_string()); assert_eq!( parse_single_value(&cv), CssValue::Keyword("bold".to_string()) ); let cv = ComponentValue::Ident("normal".to_string()); assert_eq!( parse_single_value(&cv), CssValue::Keyword("normal".to_string()) ); // Numeric font-weight let cv = ComponentValue::Number(700.0, NumericType::Integer); assert_eq!(parse_single_value(&cv), CssValue::Number(700.0)); } #[test] fn test_keyword_overflow() { for kw in &["visible", "hidden", "scroll", "auto"] { let cv = ComponentValue::Ident(kw.to_string()); let expected = match *kw { "auto" => CssValue::Auto, _ => CssValue::Keyword(kw.to_string()), }; assert_eq!(parse_single_value(&cv), expected); } } // -- parse_value (multi-value) ------------------------------------------ #[test] fn test_parse_value_single() { let values = vec![ComponentValue::Ident("red".to_string())]; assert_eq!(parse_value(&values), CssValue::Color(Color::rgb(255, 0, 0))); } #[test] fn test_parse_value_multi() { let values = vec![ ComponentValue::Dimension(10.0, NumericType::Integer, "px".to_string()), ComponentValue::Whitespace, ComponentValue::Dimension(20.0, NumericType::Integer, "px".to_string()), ]; assert_eq!( parse_value(&values), CssValue::List(vec![ CssValue::Length(10.0, LengthUnit::Px), CssValue::Length(20.0, LengthUnit::Px), ]) ); } // -- Shorthand expansion tests ------------------------------------------ #[test] fn test_margin_one_value() { let values = vec![ComponentValue::Dimension( 10.0, NumericType::Integer, "px".to_string(), )]; let result = expand_shorthand("margin", &values, false).unwrap(); assert_eq!(result.len(), 4); for decl in &result { assert_eq!(decl.value, CssValue::Length(10.0, LengthUnit::Px)); } assert_eq!(result[0].property, "margin-top"); assert_eq!(result[1].property, "margin-right"); assert_eq!(result[2].property, "margin-bottom"); assert_eq!(result[3].property, "margin-left"); } #[test] fn test_margin_two_values() { let values = vec![ ComponentValue::Dimension(10.0, NumericType::Integer, "px".to_string()), ComponentValue::Whitespace, ComponentValue::Dimension(20.0, NumericType::Integer, "px".to_string()), ]; let result = expand_shorthand("margin", &values, false).unwrap(); assert_eq!(result[0].value, CssValue::Length(10.0, LengthUnit::Px)); // top assert_eq!(result[1].value, CssValue::Length(20.0, LengthUnit::Px)); // right assert_eq!(result[2].value, CssValue::Length(10.0, LengthUnit::Px)); // bottom assert_eq!(result[3].value, CssValue::Length(20.0, LengthUnit::Px)); // left } #[test] fn test_margin_three_values() { let values = vec![ ComponentValue::Dimension(10.0, NumericType::Integer, "px".to_string()), ComponentValue::Whitespace, ComponentValue::Dimension(20.0, NumericType::Integer, "px".to_string()), ComponentValue::Whitespace, ComponentValue::Dimension(30.0, NumericType::Integer, "px".to_string()), ]; let result = expand_shorthand("margin", &values, false).unwrap(); assert_eq!(result[0].value, CssValue::Length(10.0, LengthUnit::Px)); // top assert_eq!(result[1].value, CssValue::Length(20.0, LengthUnit::Px)); // right assert_eq!(result[2].value, CssValue::Length(30.0, LengthUnit::Px)); // bottom assert_eq!(result[3].value, CssValue::Length(20.0, LengthUnit::Px)); // left } #[test] fn test_margin_four_values() { let values = vec![ ComponentValue::Dimension(10.0, NumericType::Integer, "px".to_string()), ComponentValue::Whitespace, ComponentValue::Dimension(20.0, NumericType::Integer, "px".to_string()), ComponentValue::Whitespace, ComponentValue::Dimension(30.0, NumericType::Integer, "px".to_string()), ComponentValue::Whitespace, ComponentValue::Dimension(40.0, NumericType::Integer, "px".to_string()), ]; let result = expand_shorthand("margin", &values, false).unwrap(); assert_eq!(result[0].value, CssValue::Length(10.0, LengthUnit::Px)); // top assert_eq!(result[1].value, CssValue::Length(20.0, LengthUnit::Px)); // right assert_eq!(result[2].value, CssValue::Length(30.0, LengthUnit::Px)); // bottom assert_eq!(result[3].value, CssValue::Length(40.0, LengthUnit::Px)); // left } #[test] fn test_margin_auto() { let values = vec![ ComponentValue::Number(0.0, NumericType::Integer), ComponentValue::Whitespace, ComponentValue::Ident("auto".to_string()), ]; let result = expand_shorthand("margin", &values, false).unwrap(); assert_eq!(result[0].value, CssValue::Zero); // top assert_eq!(result[1].value, CssValue::Auto); // right assert_eq!(result[2].value, CssValue::Zero); // bottom assert_eq!(result[3].value, CssValue::Auto); // left } #[test] fn test_padding_shorthand() { let values = vec![ComponentValue::Dimension( 5.0, NumericType::Integer, "px".to_string(), )]; let result = expand_shorthand("padding", &values, false).unwrap(); assert_eq!(result.len(), 4); assert_eq!(result[0].property, "padding-top"); assert_eq!(result[1].property, "padding-right"); assert_eq!(result[2].property, "padding-bottom"); assert_eq!(result[3].property, "padding-left"); } #[test] fn test_border_shorthand() { let values = vec![ ComponentValue::Dimension(1.0, NumericType::Integer, "px".to_string()), ComponentValue::Whitespace, ComponentValue::Ident("solid".to_string()), ComponentValue::Whitespace, ComponentValue::Ident("red".to_string()), ]; let result = expand_shorthand("border", &values, false).unwrap(); assert_eq!(result.len(), 3); assert_eq!(result[0].property, "border-width"); assert_eq!(result[0].value, CssValue::Length(1.0, LengthUnit::Px)); assert_eq!(result[1].property, "border-style"); assert_eq!(result[1].value, CssValue::Keyword("solid".to_string())); assert_eq!(result[2].property, "border-color"); assert_eq!(result[2].value, CssValue::Color(Color::rgb(255, 0, 0))); } #[test] fn test_border_shorthand_defaults() { // Just a width let values = vec![ComponentValue::Dimension( 2.0, NumericType::Integer, "px".to_string(), )]; let result = expand_shorthand("border", &values, false).unwrap(); assert_eq!(result[0].value, CssValue::Length(2.0, LengthUnit::Px)); assert_eq!(result[1].value, CssValue::None); // default style assert_eq!(result[2].value, CssValue::CurrentColor); // default color } #[test] fn test_background_shorthand_color() { let values = vec![ComponentValue::Hash("ff0000".to_string(), HashType::Id)]; let result = expand_shorthand("background", &values, false).unwrap(); assert_eq!(result.len(), 1); assert_eq!(result[0].property, "background-color"); assert_eq!(result[0].value, CssValue::Color(Color::rgb(255, 0, 0))); } #[test] fn test_non_shorthand_returns_none() { let values = vec![ComponentValue::Ident("red".to_string())]; assert!(expand_shorthand("color", &values, false).is_none()); } #[test] fn test_important_propagated() { let values = vec![ComponentValue::Dimension( 10.0, NumericType::Integer, "px".to_string(), )]; let result = expand_shorthand("margin", &values, true).unwrap(); for decl in &result { assert!(decl.important); } } // -- Integration: parse from CSS text ----------------------------------- #[test] fn test_parse_from_css_text() { use crate::parser::Parser; let ss = Parser::parse("p { color: red; margin: 10px 20px; }"); let rule = match &ss.rules[0] { crate::parser::Rule::Style(r) => r, _ => panic!("expected style rule"), }; // color: red let color_val = parse_value(&rule.declarations[0].value); assert_eq!(color_val, CssValue::Color(Color::rgb(255, 0, 0))); // margin: 10px 20px (multi-value) let margin_val = parse_value(&rule.declarations[1].value); assert_eq!( margin_val, CssValue::List(vec![ CssValue::Length(10.0, LengthUnit::Px), CssValue::Length(20.0, LengthUnit::Px), ]) ); } #[test] fn test_shorthand_from_css_text() { use crate::parser::Parser; let ss = Parser::parse("div { margin: 10px 20px 30px 40px; }"); let rule = match &ss.rules[0] { crate::parser::Rule::Style(r) => r, _ => panic!("expected style rule"), }; let longhands = expand_shorthand( &rule.declarations[0].property, &rule.declarations[0].value, rule.declarations[0].important, ) .unwrap(); assert_eq!(longhands[0].property, "margin-top"); assert_eq!(longhands[0].value, CssValue::Length(10.0, LengthUnit::Px)); assert_eq!(longhands[1].property, "margin-right"); assert_eq!(longhands[1].value, CssValue::Length(20.0, LengthUnit::Px)); assert_eq!(longhands[2].property, "margin-bottom"); assert_eq!(longhands[2].value, CssValue::Length(30.0, LengthUnit::Px)); assert_eq!(longhands[3].property, "margin-left"); assert_eq!(longhands[3].value, CssValue::Length(40.0, LengthUnit::Px)); } #[test] fn test_case_insensitive_units() { let cv = ComponentValue::Dimension(16.0, NumericType::Integer, "PX".to_string()); assert_eq!( parse_single_value(&cv), CssValue::Length(16.0, LengthUnit::Px) ); } #[test] fn test_case_insensitive_color_name() { let cv = ComponentValue::Ident("RED".to_string()); assert_eq!( parse_single_value(&cv), CssValue::Color(Color::rgb(255, 0, 0)) ); } #[test] fn test_case_insensitive_keywords() { let cv = ComponentValue::Ident("AUTO".to_string()); assert_eq!(parse_single_value(&cv), CssValue::Auto); let cv = ComponentValue::Ident("INHERIT".to_string()); assert_eq!(parse_single_value(&cv), CssValue::Inherit); } #[test] fn test_named_color_grey_alias() { let cv = ComponentValue::Ident("grey".to_string()); assert_eq!( parse_single_value(&cv), CssValue::Color(Color::rgb(128, 128, 128)) ); } #[test] fn test_named_color_all_16_plus() { let colors = vec![ ("black", 0, 0, 0), ("silver", 192, 192, 192), ("gray", 128, 128, 128), ("white", 255, 255, 255), ("maroon", 128, 0, 0), ("red", 255, 0, 0), ("purple", 128, 0, 128), ("fuchsia", 255, 0, 255), ("green", 0, 128, 0), ("lime", 0, 255, 0), ("olive", 128, 128, 0), ("yellow", 255, 255, 0), ("navy", 0, 0, 128), ("blue", 0, 0, 255), ("teal", 0, 128, 128), ("aqua", 0, 255, 255), ("orange", 255, 165, 0), ]; for (name, r, g, b) in colors { let cv = ComponentValue::Ident(name.to_string()); assert_eq!( parse_single_value(&cv), CssValue::Color(Color::rgb(r, g, b)), "failed for {name}" ); } } #[test] fn test_border_width_shorthand() { let values = vec![ ComponentValue::Dimension(1.0, NumericType::Integer, "px".to_string()), ComponentValue::Whitespace, ComponentValue::Dimension(2.0, NumericType::Integer, "px".to_string()), ]; let result = expand_shorthand("border-width", &values, false).unwrap(); assert_eq!(result.len(), 4); assert_eq!(result[0].property, "border-top-width"); assert_eq!(result[1].property, "border-right-width"); } #[test] fn test_border_style_shorthand() { let values = vec![ComponentValue::Ident("solid".to_string())]; let result = expand_shorthand("border-style", &values, false).unwrap(); assert_eq!(result.len(), 4); assert_eq!(result[0].property, "border-top-style"); } #[test] fn test_border_color_shorthand() { let values = vec![ComponentValue::Ident("red".to_string())]; let result = expand_shorthand("border-color", &values, false).unwrap(); assert_eq!(result.len(), 4); assert_eq!(result[0].property, "border-top-color"); } #[test] fn test_string_value() { let cv = ComponentValue::String("hello".to_string()); assert_eq!( parse_single_value(&cv), CssValue::String("hello".to_string()) ); } }