Skip to main content

iris_cssom/
parser.rs

1//! CSS parser: convert CSS string → StyleSheet.
2//!
3//! Uses simple string-based parsing (no cssparser dependency at parse time)
4//! for broad compatibility across cssparser 0.33+ API changes.
5
6use crate::{CssRule, Declaration, StyleSheet, MediaQuery, MediaRule, KeyframesRule, Keyframe};
7
8/// Parse CSS string into StyleSheet (simple string-based parser).
9pub fn parse_css(css: &str) -> Result<StyleSheet, String> {
10    Ok(parse_css_simple(css))
11}
12
13/// Simple CSS parser — splits by `{` / `}` and `:` / `;`.
14///
15/// Handles:
16/// - Multi-selector rules (`h1, h2 { ... }`)
17/// - @media rules (with nested rules)
18/// - @keyframes rules (with keyframe blocks)
19/// - Other @rules (skipped)
20pub fn parse_css_simple(css: &str) -> StyleSheet {
21    let mut sheet = StyleSheet::default();
22    let mut remaining = css;
23
24    while let Some(open_pos) = remaining.find('{') {
25        let before = remaining[..open_pos].trim();
26        remaining = &remaining[open_pos + 1..];
27
28        // Find matching closing brace
29        let close_pos = match find_matching_brace(remaining) {
30            Some(p) => p,
31            None => break,
32        };
33
34        let block_content = remaining[..close_pos].trim();
35        remaining = &remaining[close_pos + 1..];
36
37        // ✅ 处理 @media 规则
38        if before.starts_with("@media") {
39            let query_str = before[6..].trim();
40            let query = MediaQuery::parse(query_str);
41            let rules = parse_nested_rules(block_content);
42            sheet.media_rules.push(MediaRule { query, rules });
43            continue;
44        }
45        
46        // ✅ 处理 @keyframes 规则
47        if before.starts_with("@keyframes") {
48            let name = before[10..].trim().to_string();
49            let keyframes = parse_keyframes(block_content);
50            sheet.keyframes_rules.push(KeyframesRule { name, keyframes });
51            continue;
52        }
53        
54        // ✅ 处理 @-webkit-keyframes 规则(兼容性)
55        if before.starts_with("@-webkit-keyframes") {
56            let name = before[18..].trim().to_string();
57            let keyframes = parse_keyframes(block_content);
58            sheet.keyframes_rules.push(KeyframesRule { name, keyframes });
59            continue;
60        }
61
62        // Skip other @rules
63        if before.starts_with('@') {
64            continue;
65        }
66
67        // Skip empty/whitespace selectors
68        if before.is_empty() {
69            continue;
70        }
71
72        let selectors: Vec<String> = before.split(',')
73            .map(|s| s.trim().to_string())
74            .filter(|s| !s.is_empty())
75            .collect();
76
77        let declarations = parse_declarations(block_content);
78
79        if !selectors.is_empty() {
80            sheet.rules.push(CssRule { selectors, declarations });
81        }
82    }
83
84    sheet
85}
86
87/// ✅ 新增:解析嵌套规则(用于 @media 内部)
88fn parse_nested_rules(content: &str) -> Vec<CssRule> {
89    let mut rules = Vec::new();
90    let mut remaining = content;
91    
92    while let Some(open_pos) = remaining.find('{') {
93        let before = remaining[..open_pos].trim();
94        remaining = &remaining[open_pos + 1..];
95        
96        let close_pos = match find_matching_brace(remaining) {
97            Some(p) => p,
98            None => break,
99        };
100        
101        let decl_str = remaining[..close_pos].trim();
102        remaining = &remaining[close_pos + 1..];
103        
104        // 跳过嵌套的 @规则
105        if before.starts_with('@') {
106            continue;
107        }
108        
109        if before.is_empty() {
110            continue;
111        }
112        
113        let selectors: Vec<String> = before.split(',')
114            .map(|s| s.trim().to_string())
115            .filter(|s| !s.is_empty())
116            .collect();
117        
118        let declarations = parse_declarations(decl_str);
119        
120        if !selectors.is_empty() {
121            rules.push(CssRule { selectors, declarations });
122        }
123    }
124    
125    rules
126}
127
128/// ✅ 新增:解析关键帧(用于 @keyframes 内部)
129fn parse_keyframes(content: &str) -> Vec<Keyframe> {
130    let mut keyframes = Vec::new();
131    let mut remaining = content;
132    
133    while let Some(open_pos) = remaining.find('{') {
134        let before = remaining[..open_pos].trim();
135        remaining = &remaining[open_pos + 1..];
136        
137        let close_pos = match find_matching_brace(remaining) {
138            Some(p) => p,
139            None => break,
140        };
141        
142        let decl_str = remaining[..close_pos].trim();
143        remaining = &remaining[close_pos + 1..];
144        
145        // 关键帧选择器可以是:from, to, 或百分比(可能有多个,用逗号分隔)
146        let selectors: Vec<&str> = before.split(',').map(|s| s.trim()).collect();
147        let declarations = parse_declarations(decl_str);
148        
149        for selector in selectors {
150            if !selector.is_empty() {
151                keyframes.push(Keyframe {
152                    selector: selector.to_string(),
153                    declarations: declarations.clone(),
154                });
155            }
156        }
157    }
158    
159    // 按百分比排序
160    keyframes.sort_by(|a, b| {
161        a.percentage().partial_cmp(&b.percentage()).unwrap_or(std::cmp::Ordering::Equal)
162    });
163    
164    keyframes
165}
166
167/// Find the matching closing brace, handling nested braces.
168fn find_matching_brace(s: &str) -> Option<usize> {
169    let mut depth = 1i32;
170    for (i, c) in s.char_indices() {
171        match c {
172            '{' => depth += 1,
173            '}' => {
174                depth -= 1;
175                if depth == 0 { return Some(i); }
176            }
177            _ => {}
178        }
179    }
180    None
181}
182
183/// Parse CSS declarations from a "prop: val; prop: val" string.
184/// ✅ 修复:正确处理 url() 等函数内的冒号和分号
185fn parse_declarations(s: &str) -> Vec<Declaration> {
186    let mut result = Vec::new();
187    let mut remaining = s.trim();
188
189    while !remaining.is_empty() {
190        // Find next colon (property: value separator), skipping inside parens
191        let mut paren_depth: i32 = 0;
192        let colon_pos = {
193            let mut pos = None;
194            for (i, ch) in remaining.char_indices() {
195                match ch {
196                    '(' => paren_depth += 1,
197                    ')' => paren_depth = (paren_depth - 1).max(0),
198                    ':' if paren_depth == 0 => {
199                        pos = Some(i);
200                        break;
201                    }
202                    _ => {}
203                }
204            }
205            pos
206        };
207
208        let colon_pos = match colon_pos {
209            Some(p) => p,
210            None => break,
211        };
212
213        let property = remaining[..colon_pos].trim().to_lowercase();
214        remaining = remaining[colon_pos + 1..].trim_start();
215
216        // Find semicolon (outside parens) or end of string
217        let (value, rest) = {
218            let mut paren_depth: i32 = 0;
219            let sc_pos = {
220                let mut pos = None;
221                for (i, ch) in remaining.char_indices() {
222                    match ch {
223                        '(' => paren_depth += 1,
224                        ')' => paren_depth = (paren_depth - 1).max(0),
225                        ';' if paren_depth == 0 => {
226                            pos = Some(i);
227                            break;
228                        }
229                        _ => {}
230                    }
231                }
232                pos
233            };
234
235            if let Some(p) = sc_pos {
236                (remaining[..p].trim().to_string(), remaining[p + 1..].trim())
237            } else {
238                (remaining.trim().to_string(), "")
239            }
240        };
241
242        if !property.is_empty() {
243            result.push(Declaration { property, value });
244        }
245
246        remaining = rest;
247    }
248
249    result
250}
251
252#[cfg(test)]
253mod tests {
254    use super::*;
255
256    #[test]
257    fn test_parse_simple() {
258        let sheet = parse_css_simple(".foo { color: red; font-size: 16px; } .bar { margin: 10px; }");
259        assert_eq!(sheet.rules.len(), 2);
260        assert_eq!(sheet.rules[0].selectors[0], ".foo");
261        assert_eq!(sheet.rules[0].declarations[0].property, "color");
262    }
263
264    #[test]
265    fn test_complex_selector() {
266        let sheet = parse_css_simple("div.container > p.highlight { color: blue; }");
267        assert_eq!(sheet.rules.len(), 1);
268        assert_eq!(sheet.rules[0].declarations[0].value, "blue");
269    }
270
271    #[test]
272    fn test_multiple_selectors() {
273        let sheet = parse_css_simple("h1, h2, h3 { font-weight: bold; }");
274        assert_eq!(sheet.rules[0].selectors.len(), 3);
275    }
276
277    #[test]
278    fn test_at_rule_skipped() {
279        let sheet = parse_css_simple("@media screen { .a { color: red; } } .b { color: blue; }");
280        // @media is now parsed, .b should still be parsed
281        assert_eq!(sheet.rules.len(), 1);
282        assert_eq!(sheet.rules[0].selectors[0], ".b");
283    }
284
285    #[test]
286    fn test_stylesheet_compute() {
287        let sheet = parse_css_simple(".btn { color: red; font-size: 14px; } .btn-primary { color: blue; }");
288        let map = sheet.compute(&["btn".into(), "btn-primary".into()], "button");
289        assert_eq!(map.get("color").unwrap(), "blue");
290        assert_eq!(map.get("font-size").unwrap(), "14px");
291    }
292
293    #[test]
294    fn test_empty_input() {
295        let sheet = parse_css_simple("");
296        assert_eq!(sheet.rules.len(), 0);
297    }
298
299    #[test]
300    fn test_nested_braces() {
301        let sheet = parse_css_simple(".a { x: 1; } .b { y: 2; }");
302        assert_eq!(sheet.rules.len(), 2);
303    }
304    
305    #[test]
306    fn test_media_rule_parsing() {
307        let sheet = parse_css_simple(
308            "@media screen and (min-width: 768px) { .container { width: 750px; } }"
309        );
310        assert_eq!(sheet.media_rules.len(), 1);
311        assert_eq!(sheet.media_rules[0].query.media_type, "screen");
312        assert_eq!(sheet.media_rules[0].rules.len(), 1);
313        assert_eq!(sheet.media_rules[0].rules[0].selectors[0], ".container");
314    }
315    
316    #[test]
317    fn test_keyframes_parsing() {
318        let sheet = parse_css_simple(
319            "@keyframes fade { from { opacity: 0; } to { opacity: 1; } }"
320        );
321        assert_eq!(sheet.keyframes_rules.len(), 1);
322        assert_eq!(sheet.keyframes_rules[0].name, "fade");
323        assert_eq!(sheet.keyframes_rules[0].keyframes.len(), 2);
324        assert_eq!(sheet.keyframes_rules[0].keyframes[0].selector, "from");
325        assert_eq!(sheet.keyframes_rules[0].keyframes[1].selector, "to");
326    }
327    
328    #[test]
329    fn test_keyframes_percentage() {
330        let sheet = parse_css_simple(
331            "@keyframes pulse { 0% { transform: scale(1); } 50% { transform: scale(1.1); } 100% { transform: scale(1); } }"
332        );
333        assert_eq!(sheet.keyframes_rules[0].keyframes.len(), 3);
334        assert!((sheet.keyframes_rules[0].keyframes[1].percentage() - 0.5).abs() < 0.001);
335    }
336    
337    #[test]
338    fn test_media_query_matching() {
339        let query = MediaQuery::parse("screen and (min-width: 768px)");
340        assert!(query.matches(1024.0, 768.0));  // 宽度 >= 768px
341        assert!(!query.matches(480.0, 640.0));  // 宽度 < 768px
342    }
343    
344    #[test]
345    fn test_compute_with_media() {
346        let sheet = parse_css_simple(
347            ".box { width: 100px; } @media screen and (min-width: 768px) { .box { width: 200px; } }"
348        );
349        // 小屏幕:使用基础样式
350        let small_screen = sheet.compute_with_media(&["box".into()], "div", 480.0, 640.0);
351        assert_eq!(small_screen.get("width").unwrap(), "100px");
352        // 大屏幕:@media 样式覆盖
353        let large_screen = sheet.compute_with_media(&["box".into()], "div", 1024.0, 768.0);
354        assert_eq!(large_screen.get("width").unwrap(), "200px");
355    }
356}