iris-cssom 1.1.5

Iris CSS Object Model (CSSOM) implementation: CSS parsing, style computation, CSS Modules, and Web API
Documentation
//! Iris CSSOM — CSS Object Model implementation.
//!
//! Parse CSS strings, compute resolved styles for elements,
//! support CSS Modules (scoped class names), and provide
//! CSSOM Web API surfaces (CSSStyleSheet, CSSRule, etc.).

mod parser;
pub mod computed;
pub mod css_modules;
pub mod web_api;

pub use parser::*;
pub use computed::*;
pub use css_modules::*;
pub use web_api::*;

use std::collections::HashMap;

/// A single CSS declaration (property: value).
#[derive(Debug, Clone, PartialEq)]
pub struct Declaration {
    pub property: String,
    pub value: String,
}

/// A CSS rule: selector + declarations.
#[derive(Debug, Clone)]
pub struct CssRule {
    pub selectors: Vec<String>,
    pub declarations: Vec<Declaration>,
}

/// ✅ 新增:媒体查询条件
#[derive(Debug, Clone)]
pub struct MediaQuery {
    /// 媒体类型:screen, print, all 等
    pub media_type: String,
    /// 条件表达式:(min-width: 768px) 等
    pub conditions: Vec<String>,
}

impl MediaQuery {
    /// 解析媒体查询字符串,如 "screen and (min-width: 768px)"
    pub fn parse(query: &str) -> Self {
        let query = query.trim();
        let parts: Vec<&str> = query.split(" and ").collect();
        let media_type = parts.first()
            .map(|s| s.trim().to_lowercase())
            .unwrap_or_else(|| "all".to_string());
        
        let conditions = parts.iter()
            .skip(1)
            .filter_map(|s| {
                let s = s.trim();
                if s.starts_with('(') && s.ends_with(')') {
                    Some(s[1..s.len()-1].trim().to_string())
                } else {
                    None
                }
            })
            .collect();
        
        Self { media_type, conditions }
    }
    
    /// 检查媒体查询是否匹配当前视口
    pub fn matches(&self, viewport_width: f32, viewport_height: f32) -> bool {
        // 检查媒体类型(简化:假设总是匹配 screen 或 all)
        if self.media_type != "screen" && self.media_type != "all" {
            return false;
        }
        
        // 检查条件
        for cond in &self.conditions {
            if !self.check_condition(cond, viewport_width, viewport_height) {
                return false;
            }
        }
        true
    }
    
    fn check_condition(&self, cond: &str, viewport_width: f32, viewport_height: f32) -> bool {
        let parts: Vec<&str> = cond.split(':').collect();
        if parts.len() != 2 {
            return true;
        }
        let feature = parts[0].trim();
        let raw_value = parts[1].trim();
        
        // 解析数值并转换单位到 px
        let num: f32 = if raw_value.ends_with("rem") || raw_value.ends_with("em") {
            // em/rem: 假设基准字体大小 16px
            let base_font_size = 16.0_f32;
            raw_value.trim_end_matches("px")
                .trim_end_matches("em")
                .trim_end_matches("rem")
                .parse::<f32>()
                .unwrap_or(0.0) * base_font_size
        } else {
            raw_value.trim_end_matches("px")
                .parse::<f32>()
                .unwrap_or(0.0)
        };
        
        match feature {
            "min-width" => viewport_width >= num,
            "max-width" => viewport_width <= num,
            "min-height" => viewport_height >= num,
            "max-height" => viewport_height <= num,
            _ => true,
        }
    }
}

/// ✅ 新增:@media 规则
#[derive(Debug, Clone)]
pub struct MediaRule {
    pub query: MediaQuery,
    pub rules: Vec<CssRule>,
}

/// ✅ 新增:关键帧
#[derive(Debug, Clone)]
pub struct Keyframe {
    /// 选择器:from, to, 或百分比 (0%, 50%, 100%)
    pub selector: String,
    pub declarations: Vec<Declaration>,
}

impl Keyframe {
    /// 获取百分比位置 (0.0 - 1.0)
    pub fn percentage(&self) -> f32 {
        match self.selector.to_lowercase().as_str() {
            "from" => 0.0,
            "to" => 1.0,
            s if s.ends_with('%') => {
                s.trim_end_matches('%')
                    .parse()
                    .unwrap_or(0.0) / 100.0
            }
            _ => 0.0,
        }
    }
}

/// ✅ 新增:@keyframes 规则
#[derive(Debug, Clone)]
pub struct KeyframesRule {
    pub name: String,
    pub keyframes: Vec<Keyframe>,
}

/// A parsed stylesheet (list of rules).
#[derive(Debug, Clone, Default)]
pub struct StyleSheet {
    pub rules: Vec<CssRule>,
    /// ✅ 新增:@media 规则列表
    pub media_rules: Vec<MediaRule>,
    /// ✅ 新增:@keyframes 规则列表
    pub keyframes_rules: Vec<KeyframesRule>,
}

impl StyleSheet {
    /// Parse a CSS string into a StyleSheet.
    pub fn parse(css: &str) -> Result<Self, String> {
        parser::parse_css(css)
    }

    /// Query all declarations matching a given class name.
    pub fn declarations_for_class(&self, class: &str) -> Vec<Declaration> {
        let mut result = Vec::new();
        let class_selector = format!(".{}", class);
        for rule in &self.rules {
            if rule.selectors.iter().any(|s| {
                // 精确匹配或逗号分隔的多选择器中精确匹配
                s == &class_selector
                    || s.split(',').any(|part| part.trim() == class_selector)
            }) {
                result.extend(rule.declarations.clone());
            }
        }
        result
    }

    /// Query all declarations matching a given tag name.
    pub fn declarations_for_tag(&self, tag: &str) -> Vec<Declaration> {
        let mut result = Vec::new();
        for rule in &self.rules {
            if rule.selectors.iter().any(|s| s == tag) {
                result.extend(rule.declarations.clone());
            }
        }
        result
    }

    /// Compute the full set of resolved declarations for a set of class names and tag.
    pub fn compute(&self, classes: &[String], tag: &str) -> HashMap<String, String> {
        let mut map = HashMap::new();
        for class in classes {
            for decl in self.declarations_for_class(class) {
                map.insert(decl.property, decl.value);
            }
        }
        for decl in self.declarations_for_tag(tag) {
            map.entry(decl.property).or_insert(decl.value);
        }
        map
    }
    
    /// ✅ 新增:获取匹配当前视口的 @media 规则中的样式
    pub fn compute_with_media(&self, classes: &[String], tag: &str, viewport_width: f32, viewport_height: f32) -> HashMap<String, String> {
        let mut map = self.compute(classes, tag);
        
        // 应用匹配的 @media 规则
        for media_rule in &self.media_rules {
            if media_rule.query.matches(viewport_width, viewport_height) {
                for rule in &media_rule.rules {
                    // 检查选择器是否匹配
                    let matches = rule.selectors.iter().any(|s| {
                        let s_lower = s.to_lowercase();
                        // 类选择器匹配
                        classes.iter().any(|c| s_lower == format!(".{}", c.to_lowercase())) ||
                        // 标签选择器匹配
                        s_lower == tag.to_lowercase()
                    });
                    if matches {
                        for decl in &rule.declarations {
                            map.insert(decl.property.clone(), decl.value.clone());
                        }
                    }
                }
            }
        }
        
        map
    }
    
    /// ✅ 新增:获取指定名称的 @keyframes 规则
    pub fn get_keyframes(&self, name: &str) -> Option<&KeyframesRule> {
        self.keyframes_rules.iter().find(|k| k.name == name)
    }
}