iris-cssom 1.1.4

Iris CSS Object Model (CSSOM) implementation: CSS parsing, style computation, CSS Modules, and Web API
Documentation
//! CSSOM Web API surfaces.
//!
//! Provides JavaScript-exposed interfaces:
//! - CSSStyleSheet
//! - CSSRule (CSSStyleRule, CSSKeyframesRule)
//! - CSSStyleDeclaration
//! - CSS (global CSSOM namespace)

use crate::{CssRule, Declaration, StyleSheet};
use std::collections::HashMap;

/// CSSStyleSheet — a single stylesheet as exposed to JS.
#[derive(Debug, Clone)]
pub struct CssStyleSheet {
    /// The parsed CSS rules.
    pub rules: Vec<CssStyleRule>,
    /// Whether the stylesheet is disabled.
    pub disabled: bool,
    /// Owner element ID (for `<style>` tags).
    pub owner_id: Option<String>,
}

impl CssStyleSheet {
    /// Create from a parsed StyleSheet.
    pub fn from_parsed(sheet: &StyleSheet) -> Self {
        CssStyleSheet {
            rules: sheet.rules.iter().map(CssStyleRule::from).collect(),
            disabled: false,
            owner_id: None,
        }
    }

    /// Parse CSS text directly.
    pub fn from_css(css: &str) -> Self {
        let sheet = StyleSheet::parse(css).unwrap_or_default();
        Self::from_parsed(&sheet)
    }

    /// Insert a new rule at the given index.
    pub fn insert_rule(&mut self, rule: &str, index: usize) -> Result<(), String> {
        if let Some((selector_part, body)) = rule.split_once('{') {
            let selector = selector_part.trim();
            let decl_str = body.trim_end_matches('}').trim();
            let declarations = parse_decl_string(decl_str);
            let css_rule = CssStyleRule {
                selector_text: selector.to_string(),
                style: CssStyleDeclaration { properties: declarations },
            };
            if index <= self.rules.len() {
                self.rules.insert(index, css_rule);
            } else {
                self.rules.push(css_rule);
            }
            Ok(())
        } else {
            Err("Invalid rule format".to_string())
        }
    }

    /// Delete a rule at the given index.
    pub fn delete_rule(&mut self, index: usize) {
        if index < self.rules.len() {
            self.rules.remove(index);
        }
    }

    /// Convert to JavaScript-consumable JSON.
    pub fn to_js_json(&self) -> String {
        let rules_json: Vec<String> = self.rules.iter().map(|r| {
            format!(
                r#"{{"selectorText":"{}","style":{}}}"#,
                r.selector_text.replace('"', r#"\""#),
                r.style.to_js_json()
            )
        }).collect();
        format!(r#"{{"rules":[{}],"disabled":{}}}"#, rules_json.join(","), self.disabled)
    }
}

/// CSSStyleRule — a single style rule.
#[derive(Debug, Clone)]
pub struct CssStyleRule {
    pub selector_text: String,
    pub style: CssStyleDeclaration,
}

impl From<&CssRule> for CssStyleRule {
    fn from(rule: &CssRule) -> Self {
        CssStyleRule {
            selector_text: rule.selectors.join(", "),
            style: CssStyleDeclaration::from(&rule.declarations),
        }
    }
}

/// CSSStyleDeclaration — a map of property → value.
#[derive(Debug, Clone)]
pub struct CssStyleDeclaration {
    pub properties: HashMap<String, String>,
}

impl CssStyleDeclaration {
    pub fn new() -> Self { CssStyleDeclaration { properties: HashMap::new() } }

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

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

    pub fn remove_property(&mut self, prop: &str) {
        self.properties.remove(prop);
    }

    pub fn to_js_json(&self) -> String {
        let entries: Vec<String> = self.properties.iter()
            .map(|(k, v)| format!(r#""{}":"{}""#, k.replace('"', r#"\""#), v.replace('"', r#"\""#)))
            .collect();
        format!("{{{}}}", entries.join(","))
    }
}

impl From<&Vec<Declaration>> for CssStyleDeclaration {
    fn from(decls: &Vec<Declaration>) -> Self {
        let mut properties = HashMap::new();
        for d in decls {
            properties.insert(d.property.clone(), d.value.clone());
        }
        CssStyleDeclaration { properties }
    }
}

/// Global CSS namespace utilities for JS injection.
pub struct Css;

impl Css {
    /// Escape a CSS identifier (for use as selector).
    pub fn escape(ident: &str) -> String {
        ident.replace('\\', "\\\\")
            .replace('"', "\\\"")
            .replace('\'', "\\'")
    }

    /// Check if a property supports a given value.
    pub fn supports(property: &str, value: &str) -> bool {
        !property.is_empty() && !value.is_empty()
    }
}

/// Parse a "prop: val; prop: val" string into a HashMap.
fn parse_decl_string(s: &str) -> HashMap<String, String> {
    let mut map = HashMap::new();
    for part in s.split(';') {
        let part = part.trim();
        if part.is_empty() { continue; }
        if let Some((prop, val)) = part.split_once(':') {
            map.insert(prop.trim().to_lowercase(), val.trim().to_string());
        }
    }
    map
}

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

    #[test]
    fn test_css_style_sheet_from_css() {
        let sheet = CssStyleSheet::from_css(".foo { color: red; } .bar { margin: 10px; }");
        assert_eq!(sheet.rules.len(), 2);
        assert_eq!(sheet.rules[0].selector_text, ".foo");
    }

    #[test]
    fn test_insert_rule() {
        let mut sheet = CssStyleSheet::from_css("");
        sheet.insert_rule(".btn { color: blue; }", 0).unwrap();
        assert_eq!(sheet.rules.len(), 1);
        assert_eq!(sheet.rules[0].style.get_property_value("color").unwrap(), "blue");
    }

    #[test]
    fn test_delete_rule() {
        let mut sheet = CssStyleSheet::from_css(".a { x:1; } .b { y:2; }");
        sheet.delete_rule(0);
        assert_eq!(sheet.rules.len(), 1);
    }

    #[test]
    fn test_style_declaration() {
        let mut decl = CssStyleDeclaration::new();
        decl.set_property("color", "red");
        assert_eq!(decl.get_property_value("color").unwrap(), "red");
        decl.remove_property("color");
        assert!(decl.get_property_value("color").is_none());
    }

    #[test]
    fn test_to_js_json() {
        let sheet = CssStyleSheet::from_css(".btn { color: red; font-size: 14px; }");
        let json = sheet.to_js_json();
        assert!(json.contains("color"));
        assert!(json.contains("red"));
        assert!(json.contains("selectorText"));
    }
}