Skip to main content

iris_cssom/
lib.rs

1//! Iris CSSOM — CSS Object Model implementation.
2//!
3//! Parse CSS strings, compute resolved styles for elements,
4//! support CSS Modules (scoped class names), and provide
5//! CSSOM Web API surfaces (CSSStyleSheet, CSSRule, etc.).
6
7mod parser;
8pub mod computed;
9pub mod css_modules;
10pub mod web_api;
11
12pub use parser::*;
13pub use computed::*;
14pub use css_modules::*;
15pub use web_api::*;
16
17use std::collections::HashMap;
18
19/// A single CSS declaration (property: value).
20#[derive(Debug, Clone, PartialEq)]
21pub struct Declaration {
22    pub property: String,
23    pub value: String,
24}
25
26/// A CSS rule: selector + declarations.
27#[derive(Debug, Clone)]
28pub struct CssRule {
29    pub selectors: Vec<String>,
30    pub declarations: Vec<Declaration>,
31}
32
33/// ✅ 新增:媒体查询条件
34#[derive(Debug, Clone)]
35pub struct MediaQuery {
36    /// 媒体类型:screen, print, all 等
37    pub media_type: String,
38    /// 条件表达式:(min-width: 768px) 等
39    pub conditions: Vec<String>,
40}
41
42impl MediaQuery {
43    /// 解析媒体查询字符串,如 "screen and (min-width: 768px)"
44    pub fn parse(query: &str) -> Self {
45        let query = query.trim();
46        let parts: Vec<&str> = query.split(" and ").collect();
47        let media_type = parts.first()
48            .map(|s| s.trim().to_lowercase())
49            .unwrap_or_else(|| "all".to_string());
50        
51        let conditions = parts.iter()
52            .skip(1)
53            .filter_map(|s| {
54                let s = s.trim();
55                if s.starts_with('(') && s.ends_with(')') {
56                    Some(s[1..s.len()-1].trim().to_string())
57                } else {
58                    None
59                }
60            })
61            .collect();
62        
63        Self { media_type, conditions }
64    }
65    
66    /// 检查媒体查询是否匹配当前视口
67    pub fn matches(&self, viewport_width: f32, viewport_height: f32) -> bool {
68        // 检查媒体类型(简化:假设总是匹配 screen 或 all)
69        if self.media_type != "screen" && self.media_type != "all" {
70            return false;
71        }
72        
73        // 检查条件
74        for cond in &self.conditions {
75            if !self.check_condition(cond, viewport_width, viewport_height) {
76                return false;
77            }
78        }
79        true
80    }
81    
82    fn check_condition(&self, cond: &str, viewport_width: f32, viewport_height: f32) -> bool {
83        let parts: Vec<&str> = cond.split(':').collect();
84        if parts.len() != 2 {
85            return true;
86        }
87        let feature = parts[0].trim();
88        let raw_value = parts[1].trim();
89        
90        // 解析数值并转换单位到 px
91        let num: f32 = if raw_value.ends_with("rem") || raw_value.ends_with("em") {
92            // em/rem: 假设基准字体大小 16px
93            let base_font_size = 16.0_f32;
94            raw_value.trim_end_matches("px")
95                .trim_end_matches("em")
96                .trim_end_matches("rem")
97                .parse::<f32>()
98                .unwrap_or(0.0) * base_font_size
99        } else {
100            raw_value.trim_end_matches("px")
101                .parse::<f32>()
102                .unwrap_or(0.0)
103        };
104        
105        match feature {
106            "min-width" => viewport_width >= num,
107            "max-width" => viewport_width <= num,
108            "min-height" => viewport_height >= num,
109            "max-height" => viewport_height <= num,
110            _ => true,
111        }
112    }
113}
114
115/// ✅ 新增:@media 规则
116#[derive(Debug, Clone)]
117pub struct MediaRule {
118    pub query: MediaQuery,
119    pub rules: Vec<CssRule>,
120}
121
122/// ✅ 新增:关键帧
123#[derive(Debug, Clone)]
124pub struct Keyframe {
125    /// 选择器:from, to, 或百分比 (0%, 50%, 100%)
126    pub selector: String,
127    pub declarations: Vec<Declaration>,
128}
129
130impl Keyframe {
131    /// 获取百分比位置 (0.0 - 1.0)
132    pub fn percentage(&self) -> f32 {
133        match self.selector.to_lowercase().as_str() {
134            "from" => 0.0,
135            "to" => 1.0,
136            s if s.ends_with('%') => {
137                s.trim_end_matches('%')
138                    .parse()
139                    .unwrap_or(0.0) / 100.0
140            }
141            _ => 0.0,
142        }
143    }
144}
145
146/// ✅ 新增:@keyframes 规则
147#[derive(Debug, Clone)]
148pub struct KeyframesRule {
149    pub name: String,
150    pub keyframes: Vec<Keyframe>,
151}
152
153/// A parsed stylesheet (list of rules).
154#[derive(Debug, Clone, Default)]
155pub struct StyleSheet {
156    pub rules: Vec<CssRule>,
157    /// ✅ 新增:@media 规则列表
158    pub media_rules: Vec<MediaRule>,
159    /// ✅ 新增:@keyframes 规则列表
160    pub keyframes_rules: Vec<KeyframesRule>,
161}
162
163impl StyleSheet {
164    /// Parse a CSS string into a StyleSheet.
165    pub fn parse(css: &str) -> Result<Self, String> {
166        parser::parse_css(css)
167    }
168
169    /// Query all declarations matching a given class name.
170    pub fn declarations_for_class(&self, class: &str) -> Vec<Declaration> {
171        let mut result = Vec::new();
172        let class_selector = format!(".{}", class);
173        for rule in &self.rules {
174            if rule.selectors.iter().any(|s| {
175                // 精确匹配或逗号分隔的多选择器中精确匹配
176                s == &class_selector
177                    || s.split(',').any(|part| part.trim() == class_selector)
178            }) {
179                result.extend(rule.declarations.clone());
180            }
181        }
182        result
183    }
184
185    /// Query all declarations matching a given tag name.
186    pub fn declarations_for_tag(&self, tag: &str) -> Vec<Declaration> {
187        let mut result = Vec::new();
188        for rule in &self.rules {
189            if rule.selectors.iter().any(|s| s == tag) {
190                result.extend(rule.declarations.clone());
191            }
192        }
193        result
194    }
195
196    /// Compute the full set of resolved declarations for a set of class names and tag.
197    pub fn compute(&self, classes: &[String], tag: &str) -> HashMap<String, String> {
198        let mut map = HashMap::new();
199        for class in classes {
200            for decl in self.declarations_for_class(class) {
201                map.insert(decl.property, decl.value);
202            }
203        }
204        for decl in self.declarations_for_tag(tag) {
205            map.entry(decl.property).or_insert(decl.value);
206        }
207        map
208    }
209    
210    /// ✅ 新增:获取匹配当前视口的 @media 规则中的样式
211    pub fn compute_with_media(&self, classes: &[String], tag: &str, viewport_width: f32, viewport_height: f32) -> HashMap<String, String> {
212        let mut map = self.compute(classes, tag);
213        
214        // 应用匹配的 @media 规则
215        for media_rule in &self.media_rules {
216            if media_rule.query.matches(viewport_width, viewport_height) {
217                for rule in &media_rule.rules {
218                    // 检查选择器是否匹配
219                    let matches = rule.selectors.iter().any(|s| {
220                        let s_lower = s.to_lowercase();
221                        // 类选择器匹配
222                        classes.iter().any(|c| s_lower == format!(".{}", c.to_lowercase())) ||
223                        // 标签选择器匹配
224                        s_lower == tag.to_lowercase()
225                    });
226                    if matches {
227                        for decl in &rule.declarations {
228                            map.insert(decl.property.clone(), decl.value.clone());
229                        }
230                    }
231                }
232            }
233        }
234        
235        map
236    }
237    
238    /// ✅ 新增:获取指定名称的 @keyframes 规则
239    pub fn get_keyframes(&self, name: &str) -> Option<&KeyframesRule> {
240        self.keyframes_rules.iter().find(|k| k.name == name)
241    }
242}