iris-cssom 1.1.5

Iris CSS Object Model (CSSOM) implementation: CSS parsing, style computation, CSS Modules, and Web API
Documentation
//! CSS parser: convert CSS string → StyleSheet.
//!
//! Uses simple string-based parsing (no cssparser dependency at parse time)
//! for broad compatibility across cssparser 0.33+ API changes.

use crate::{CssRule, Declaration, StyleSheet, MediaQuery, MediaRule, KeyframesRule, Keyframe};

/// Parse CSS string into StyleSheet (simple string-based parser).
pub fn parse_css(css: &str) -> Result<StyleSheet, String> {
    Ok(parse_css_simple(css))
}

/// Simple CSS parser — splits by `{` / `}` and `:` / `;`.
///
/// Handles:
/// - Multi-selector rules (`h1, h2 { ... }`)
/// - @media rules (with nested rules)
/// - @keyframes rules (with keyframe blocks)
/// - Other @rules (skipped)
pub fn parse_css_simple(css: &str) -> StyleSheet {
    let mut sheet = StyleSheet::default();
    let mut remaining = css;

    while let Some(open_pos) = remaining.find('{') {
        let before = remaining[..open_pos].trim();
        remaining = &remaining[open_pos + 1..];

        // Find matching closing brace
        let close_pos = match find_matching_brace(remaining) {
            Some(p) => p,
            None => break,
        };

        let block_content = remaining[..close_pos].trim();
        remaining = &remaining[close_pos + 1..];

        // ✅ 处理 @media 规则
        if before.starts_with("@media") {
            let query_str = before[6..].trim();
            let query = MediaQuery::parse(query_str);
            let rules = parse_nested_rules(block_content);
            sheet.media_rules.push(MediaRule { query, rules });
            continue;
        }
        
        // ✅ 处理 @keyframes 规则
        if before.starts_with("@keyframes") {
            let name = before[10..].trim().to_string();
            let keyframes = parse_keyframes(block_content);
            sheet.keyframes_rules.push(KeyframesRule { name, keyframes });
            continue;
        }
        
        // ✅ 处理 @-webkit-keyframes 规则(兼容性)
        if before.starts_with("@-webkit-keyframes") {
            let name = before[18..].trim().to_string();
            let keyframes = parse_keyframes(block_content);
            sheet.keyframes_rules.push(KeyframesRule { name, keyframes });
            continue;
        }

        // Skip other @rules
        if before.starts_with('@') {
            continue;
        }

        // Skip empty/whitespace selectors
        if before.is_empty() {
            continue;
        }

        let selectors: Vec<String> = before.split(',')
            .map(|s| s.trim().to_string())
            .filter(|s| !s.is_empty())
            .collect();

        let declarations = parse_declarations(block_content);

        if !selectors.is_empty() {
            sheet.rules.push(CssRule { selectors, declarations });
        }
    }

    sheet
}

/// ✅ 新增:解析嵌套规则(用于 @media 内部)
fn parse_nested_rules(content: &str) -> Vec<CssRule> {
    let mut rules = Vec::new();
    let mut remaining = content;
    
    while let Some(open_pos) = remaining.find('{') {
        let before = remaining[..open_pos].trim();
        remaining = &remaining[open_pos + 1..];
        
        let close_pos = match find_matching_brace(remaining) {
            Some(p) => p,
            None => break,
        };
        
        let decl_str = remaining[..close_pos].trim();
        remaining = &remaining[close_pos + 1..];
        
        // 跳过嵌套的 @规则
        if before.starts_with('@') {
            continue;
        }
        
        if before.is_empty() {
            continue;
        }
        
        let selectors: Vec<String> = before.split(',')
            .map(|s| s.trim().to_string())
            .filter(|s| !s.is_empty())
            .collect();
        
        let declarations = parse_declarations(decl_str);
        
        if !selectors.is_empty() {
            rules.push(CssRule { selectors, declarations });
        }
    }
    
    rules
}

/// ✅ 新增:解析关键帧(用于 @keyframes 内部)
fn parse_keyframes(content: &str) -> Vec<Keyframe> {
    let mut keyframes = Vec::new();
    let mut remaining = content;
    
    while let Some(open_pos) = remaining.find('{') {
        let before = remaining[..open_pos].trim();
        remaining = &remaining[open_pos + 1..];
        
        let close_pos = match find_matching_brace(remaining) {
            Some(p) => p,
            None => break,
        };
        
        let decl_str = remaining[..close_pos].trim();
        remaining = &remaining[close_pos + 1..];
        
        // 关键帧选择器可以是:from, to, 或百分比(可能有多个,用逗号分隔)
        let selectors: Vec<&str> = before.split(',').map(|s| s.trim()).collect();
        let declarations = parse_declarations(decl_str);
        
        for selector in selectors {
            if !selector.is_empty() {
                keyframes.push(Keyframe {
                    selector: selector.to_string(),
                    declarations: declarations.clone(),
                });
            }
        }
    }
    
    // 按百分比排序
    keyframes.sort_by(|a, b| {
        a.percentage().partial_cmp(&b.percentage()).unwrap_or(std::cmp::Ordering::Equal)
    });
    
    keyframes
}

/// Find the matching closing brace, handling nested braces.
fn find_matching_brace(s: &str) -> Option<usize> {
    let mut depth = 1i32;
    for (i, c) in s.char_indices() {
        match c {
            '{' => depth += 1,
            '}' => {
                depth -= 1;
                if depth == 0 { return Some(i); }
            }
            _ => {}
        }
    }
    None
}

/// Parse CSS declarations from a "prop: val; prop: val" string.
/// ✅ 修复:正确处理 url() 等函数内的冒号和分号
fn parse_declarations(s: &str) -> Vec<Declaration> {
    let mut result = Vec::new();
    let mut remaining = s.trim();

    while !remaining.is_empty() {
        // Find next colon (property: value separator), skipping inside parens
        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();

        // Find semicolon (outside parens) or end of string
        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() {
            result.push(Declaration { property, value });
        }

        remaining = rest;
    }

    result
}

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

    #[test]
    fn test_parse_simple() {
        let sheet = parse_css_simple(".foo { color: red; font-size: 16px; } .bar { margin: 10px; }");
        assert_eq!(sheet.rules.len(), 2);
        assert_eq!(sheet.rules[0].selectors[0], ".foo");
        assert_eq!(sheet.rules[0].declarations[0].property, "color");
    }

    #[test]
    fn test_complex_selector() {
        let sheet = parse_css_simple("div.container > p.highlight { color: blue; }");
        assert_eq!(sheet.rules.len(), 1);
        assert_eq!(sheet.rules[0].declarations[0].value, "blue");
    }

    #[test]
    fn test_multiple_selectors() {
        let sheet = parse_css_simple("h1, h2, h3 { font-weight: bold; }");
        assert_eq!(sheet.rules[0].selectors.len(), 3);
    }

    #[test]
    fn test_at_rule_skipped() {
        let sheet = parse_css_simple("@media screen { .a { color: red; } } .b { color: blue; }");
        // @media is now parsed, .b should still be parsed
        assert_eq!(sheet.rules.len(), 1);
        assert_eq!(sheet.rules[0].selectors[0], ".b");
    }

    #[test]
    fn test_stylesheet_compute() {
        let sheet = parse_css_simple(".btn { color: red; font-size: 14px; } .btn-primary { color: blue; }");
        let map = sheet.compute(&["btn".into(), "btn-primary".into()], "button");
        assert_eq!(map.get("color").unwrap(), "blue");
        assert_eq!(map.get("font-size").unwrap(), "14px");
    }

    #[test]
    fn test_empty_input() {
        let sheet = parse_css_simple("");
        assert_eq!(sheet.rules.len(), 0);
    }

    #[test]
    fn test_nested_braces() {
        let sheet = parse_css_simple(".a { x: 1; } .b { y: 2; }");
        assert_eq!(sheet.rules.len(), 2);
    }
    
    #[test]
    fn test_media_rule_parsing() {
        let sheet = parse_css_simple(
            "@media screen and (min-width: 768px) { .container { width: 750px; } }"
        );
        assert_eq!(sheet.media_rules.len(), 1);
        assert_eq!(sheet.media_rules[0].query.media_type, "screen");
        assert_eq!(sheet.media_rules[0].rules.len(), 1);
        assert_eq!(sheet.media_rules[0].rules[0].selectors[0], ".container");
    }
    
    #[test]
    fn test_keyframes_parsing() {
        let sheet = parse_css_simple(
            "@keyframes fade { from { opacity: 0; } to { opacity: 1; } }"
        );
        assert_eq!(sheet.keyframes_rules.len(), 1);
        assert_eq!(sheet.keyframes_rules[0].name, "fade");
        assert_eq!(sheet.keyframes_rules[0].keyframes.len(), 2);
        assert_eq!(sheet.keyframes_rules[0].keyframes[0].selector, "from");
        assert_eq!(sheet.keyframes_rules[0].keyframes[1].selector, "to");
    }
    
    #[test]
    fn test_keyframes_percentage() {
        let sheet = parse_css_simple(
            "@keyframes pulse { 0% { transform: scale(1); } 50% { transform: scale(1.1); } 100% { transform: scale(1); } }"
        );
        assert_eq!(sheet.keyframes_rules[0].keyframes.len(), 3);
        assert!((sheet.keyframes_rules[0].keyframes[1].percentage() - 0.5).abs() < 0.001);
    }
    
    #[test]
    fn test_media_query_matching() {
        let query = MediaQuery::parse("screen and (min-width: 768px)");
        assert!(query.matches(1024.0, 768.0));  // 宽度 >= 768px
        assert!(!query.matches(480.0, 640.0));  // 宽度 < 768px
    }
    
    #[test]
    fn test_compute_with_media() {
        let sheet = parse_css_simple(
            ".box { width: 100px; } @media screen and (min-width: 768px) { .box { width: 200px; } }"
        );
        // 小屏幕:使用基础样式
        let small_screen = sheet.compute_with_media(&["box".into()], "div", 480.0, 640.0);
        assert_eq!(small_screen.get("width").unwrap(), "100px");
        // 大屏幕:@media 样式覆盖
        let large_screen = sheet.compute_with_media(&["box".into()], "div", 1024.0, 768.0);
        assert_eq!(large_screen.get("width").unwrap(), "200px");
    }
}