iris-cssom 1.1.5

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.
/// 正确处理 url() 等函数内的冒号和分号。
fn parse_decl_string(s: &str) -> HashMap<String, String> {
    let mut map = HashMap::new();
    let mut remaining = s.trim();

    while !remaining.is_empty() {
        // 查找冒号(跳过括号内的)
        let mut paren_depth: i32 = 0;
        let colon_pos = {
            let mut pos = None;
            for (i, ch) in remaining.char_indices() {
                match ch {
                    '(' => paren_depth += 1,
                    ')' => paren_depth = (paren_depth - 1).max(0),
                    ':' if paren_depth == 0 => {
                        pos = Some(i);
                        break;
                    }
                    _ => {}
                }
            }
            pos
        };

        let colon_pos = match colon_pos {
            Some(p) => p,
            None => break,
        };

        let property = remaining[..colon_pos].trim().to_lowercase();
        remaining = remaining[colon_pos + 1..].trim_start();

        // 查找分号(跳过括号内的)或字符串结束
        let (value, rest) = {
            let mut paren_depth: i32 = 0;
            let sc_pos = {
                let mut pos = None;
                for (i, ch) in remaining.char_indices() {
                    match ch {
                        '(' => paren_depth += 1,
                        ')' => paren_depth = (paren_depth - 1).max(0),
                        ';' if paren_depth == 0 => {
                            pos = Some(i);
                            break;
                        }
                        _ => {}
                    }
                }
                pos
            };

            if let Some(p) = sc_pos {
                (remaining[..p].trim().to_string(), remaining[p + 1..].trim())
            } else {
                (remaining.trim().to_string(), "")
            }
        };

        if !property.is_empty() {
            map.insert(property, value);
        }

        remaining = rest;
    }

    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"));
    }
}