iris-cssom 1.1.4

Iris CSS Object Model (CSSOM) implementation: CSS parsing, style computation, CSS Modules, and Web API
Documentation
//! Style computation: cascade, inheritance, and resolved values.

use crate::StyleSheet;
use std::collections::HashMap;

/// Resolved, computed styles for a single element.
#[derive(Debug, Clone, Default)]
pub struct ComputedStyle {
    pub properties: HashMap<String, String>,
}

impl ComputedStyle {
    pub fn get(&self, prop: &str) -> Option<&str> {
        self.properties.get(prop).map(|s| s.as_str())
    }

    pub fn set(&mut self, prop: &str, value: &str) {
        self.properties.insert(prop.to_string(), value.to_string());
    }
}

/// Compute styles for a set of classes + tag, cascading from multiple stylesheets.
pub fn compute_styles(
    sheets: &[StyleSheet],
    classes: &[String],
    tag: &str,
    parent_style: Option<&ComputedStyle>,
) -> ComputedStyle {
    let mut map = HashMap::new();

    // 1. Parent inherited properties
    if let Some(parent) = parent_style {
        for (prop, val) in &parent.properties {
            if INHERITED_PROPS.contains(&prop.as_str()) {
                map.entry(prop.clone()).or_insert_with(|| val.clone());
            }
        }
    }

    // 2. Tag selectors (lowest priority)
    for sheet in sheets {
        for decl in sheet.declarations_for_tag(tag) {
            map.entry(decl.property.clone()).or_insert(decl.value);
        }
    }

    // 3. Class selectors (higher priority)
    for sheet in sheets {
        for class in classes {
            for decl in sheet.declarations_for_class(class) {
                map.insert(decl.property, decl.value);
            }
        }
    }

    ComputedStyle { properties: map }
}

/// Properties that inherit from parent by default.
const INHERITED_PROPS: &[&str] = &[
    "color", "font-family", "font-size", "font-style", "font-weight",
    "line-height", "text-align", "visibility", "cursor",
    "direction", "letter-spacing", "word-spacing", "white-space",
];

/// Parse a CSS pixel value (e.g., "16px" → 16.0).
pub fn parse_px(value: &str) -> f32 {
    let value = value.trim();
    if value.ends_with("px") {
        value[..value.len()-2].trim().parse().unwrap_or(0.0)
    } else {
        value.parse().unwrap_or(0.0)
    }
}

/// Convert CSS color string to RGBA tuple (r, g, b, a) in 0-1 range.
/// Supports: #hex, rgb(), rgba(), named colors.
pub fn parse_color(value: &str) -> (f32, f32, f32, f32) {
    let value = value.trim().to_lowercase();

    // Named colors (simplified)
    if let Some(rgba) = named_color(&value) { return rgba; }

    // #hex
    if value.starts_with('#') {
        return parse_hex_color(&value);
    }

    // rgb() / rgba()
    if value.starts_with("rgb") {
        let inner = value.trim_start_matches("rgba(").trim_start_matches("rgb(")
            .trim_end_matches(')');
        let parts: Vec<f32> = inner.split(',').filter_map(|s| {
            let trimmed = s.trim();
            if trimmed.ends_with('%') {
                trimmed[..trimmed.len()-1].parse::<f32>().ok().map(|v| v / 100.0 * 255.0)
            } else {
                trimmed.parse::<f32>().ok()
            }
        }).collect();
        if parts.len() >= 3 {
            return (parts[0] / 255.0, parts[1] / 255.0, parts[2] / 255.0,
                    parts.get(3).copied().unwrap_or(255.0) / 255.0);
        }
    }

    (0.0, 0.0, 0.0, 1.0) // default black
}

fn parse_hex_color(hex: &str) -> (f32, f32, f32, f32) {
    let hex = hex.trim_start_matches('#');
    let r = u8::from_str_radix(&hex[0..2], 16).unwrap_or(0) as f32 / 255.0;
    let g = u8::from_str_radix(&hex[2..4], 16).unwrap_or(0) as f32 / 255.0;
    let b = u8::from_str_radix(&hex[4..6], 16).unwrap_or(0) as f32 / 255.0;
    let a = if hex.len() >= 8 {
        u8::from_str_radix(&hex[6..8], 16).unwrap_or(255) as f32 / 255.0
    } else { 1.0 };
    (r, g, b, a)
}

fn named_color(name: &str) -> Option<(f32, f32, f32, f32)> {
    let c = match name {
        "red" => (1.0, 0.0, 0.0), "green" => (0.0, 0.5, 0.0), "blue" => (0.0, 0.0, 1.0),
        "white" => (1.0, 1.0, 1.0), "black" => (0.0, 0.0, 0.0),
        "gray" | "grey" => (0.5, 0.5, 0.5), "silver" => (0.75, 0.75, 0.75),
        "yellow" => (1.0, 1.0, 0.0), "orange" => (1.0, 0.65, 0.0),
        "purple" => (0.5, 0.0, 0.5), "pink" => (1.0, 0.75, 0.8),
        "brown" => (0.65, 0.16, 0.16), "transparent" => (0.0, 0.0, 0.0),
        _ => return None,
    };
    Some((c.0, c.1, c.2, 1.0))
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_parse_px() {
        assert_eq!(parse_px("16px"), 16.0);
        assert_eq!(parse_px("0"), 0.0);
    }

    #[test]
    fn test_parse_hex_color() {
        let (r, g, b, a) = parse_color("#ff0000");
        assert!((r - 1.0).abs() < 0.01);
        assert!((g - 0.0).abs() < 0.01);
        assert!((a - 1.0).abs() < 0.01);
    }

    #[test]
    fn test_parse_named_color() {
        let (r, g, b, _) = parse_color("red");
        assert!((r - 1.0).abs() < 0.01);
    }

    #[test]
    fn test_compute_styles() {
        let css = ".btn { color: red; } .primary { color: blue; font-weight: bold; }";
        let sheet = crate::parser::parse_css_simple(css);
        let style = compute_styles(&[sheet], &["btn".into(), "primary".into()], "button", None);
        assert_eq!(style.get("color").unwrap(), "blue"); // primary wins over btn
        assert_eq!(style.get("font-weight").unwrap(), "bold");
    }

    #[test]
    fn test_inherited_props() {
        let parent = {
            let mut s = ComputedStyle::default();
            s.set("color", "red");
            s.set("margin", "10px"); // not inherited
            s
        };
        let sheet = StyleSheet::default();
        let child = compute_styles(&[sheet], &[], "span", Some(&parent));
        assert_eq!(child.get("color").unwrap(), "red"); // inherited
        assert!(child.get("margin").is_none()); // not inherited
    }
}