Skip to main content

iris_cssom/
web_api.rs

1//! CSSOM Web API surfaces.
2//!
3//! Provides JavaScript-exposed interfaces:
4//! - CSSStyleSheet
5//! - CSSRule (CSSStyleRule, CSSKeyframesRule)
6//! - CSSStyleDeclaration
7//! - CSS (global CSSOM namespace)
8
9use crate::{CssRule, Declaration, StyleSheet};
10use std::collections::HashMap;
11
12/// CSSStyleSheet — a single stylesheet as exposed to JS.
13#[derive(Debug, Clone)]
14pub struct CssStyleSheet {
15    /// The parsed CSS rules.
16    pub rules: Vec<CssStyleRule>,
17    /// Whether the stylesheet is disabled.
18    pub disabled: bool,
19    /// Owner element ID (for `<style>` tags).
20    pub owner_id: Option<String>,
21}
22
23impl CssStyleSheet {
24    /// Create from a parsed StyleSheet.
25    pub fn from_parsed(sheet: &StyleSheet) -> Self {
26        CssStyleSheet {
27            rules: sheet.rules.iter().map(CssStyleRule::from).collect(),
28            disabled: false,
29            owner_id: None,
30        }
31    }
32
33    /// Parse CSS text directly.
34    pub fn from_css(css: &str) -> Self {
35        let sheet = StyleSheet::parse(css).unwrap_or_default();
36        Self::from_parsed(&sheet)
37    }
38
39    /// Insert a new rule at the given index.
40    pub fn insert_rule(&mut self, rule: &str, index: usize) -> Result<(), String> {
41        if let Some((selector_part, body)) = rule.split_once('{') {
42            let selector = selector_part.trim();
43            let decl_str = body.trim_end_matches('}').trim();
44            let declarations = parse_decl_string(decl_str);
45            let css_rule = CssStyleRule {
46                selector_text: selector.to_string(),
47                style: CssStyleDeclaration { properties: declarations },
48            };
49            if index <= self.rules.len() {
50                self.rules.insert(index, css_rule);
51            } else {
52                self.rules.push(css_rule);
53            }
54            Ok(())
55        } else {
56            Err("Invalid rule format".to_string())
57        }
58    }
59
60    /// Delete a rule at the given index.
61    pub fn delete_rule(&mut self, index: usize) {
62        if index < self.rules.len() {
63            self.rules.remove(index);
64        }
65    }
66
67    /// Convert to JavaScript-consumable JSON.
68    pub fn to_js_json(&self) -> String {
69        let rules_json: Vec<String> = self.rules.iter().map(|r| {
70            format!(
71                r#"{{"selectorText":"{}","style":{}}}"#,
72                r.selector_text.replace('"', r#"\""#),
73                r.style.to_js_json()
74            )
75        }).collect();
76        format!(r#"{{"rules":[{}],"disabled":{}}}"#, rules_json.join(","), self.disabled)
77    }
78}
79
80/// CSSStyleRule — a single style rule.
81#[derive(Debug, Clone)]
82pub struct CssStyleRule {
83    pub selector_text: String,
84    pub style: CssStyleDeclaration,
85}
86
87impl From<&CssRule> for CssStyleRule {
88    fn from(rule: &CssRule) -> Self {
89        CssStyleRule {
90            selector_text: rule.selectors.join(", "),
91            style: CssStyleDeclaration::from(&rule.declarations),
92        }
93    }
94}
95
96/// CSSStyleDeclaration — a map of property → value.
97#[derive(Debug, Clone)]
98pub struct CssStyleDeclaration {
99    pub properties: HashMap<String, String>,
100}
101
102impl CssStyleDeclaration {
103    pub fn new() -> Self { CssStyleDeclaration { properties: HashMap::new() } }
104
105    pub fn get_property_value(&self, prop: &str) -> Option<&str> {
106        self.properties.get(prop).map(|s| s.as_str())
107    }
108
109    pub fn set_property(&mut self, prop: &str, value: &str) {
110        self.properties.insert(prop.to_string(), value.to_string());
111    }
112
113    pub fn remove_property(&mut self, prop: &str) {
114        self.properties.remove(prop);
115    }
116
117    pub fn to_js_json(&self) -> String {
118        let entries: Vec<String> = self.properties.iter()
119            .map(|(k, v)| format!(r#""{}":"{}""#, k.replace('"', r#"\""#), v.replace('"', r#"\""#)))
120            .collect();
121        format!("{{{}}}", entries.join(","))
122    }
123}
124
125impl From<&Vec<Declaration>> for CssStyleDeclaration {
126    fn from(decls: &Vec<Declaration>) -> Self {
127        let mut properties = HashMap::new();
128        for d in decls {
129            properties.insert(d.property.clone(), d.value.clone());
130        }
131        CssStyleDeclaration { properties }
132    }
133}
134
135/// Global CSS namespace utilities for JS injection.
136pub struct Css;
137
138impl Css {
139    /// Escape a CSS identifier (for use as selector).
140    pub fn escape(ident: &str) -> String {
141        ident.replace('\\', "\\\\")
142            .replace('"', "\\\"")
143            .replace('\'', "\\'")
144    }
145
146    /// Check if a property supports a given value.
147    pub fn supports(property: &str, value: &str) -> bool {
148        !property.is_empty() && !value.is_empty()
149    }
150}
151
152/// Parse a "prop: val; prop: val" string into a HashMap.
153/// 正确处理 url() 等函数内的冒号和分号。
154fn parse_decl_string(s: &str) -> HashMap<String, String> {
155    let mut map = HashMap::new();
156    let mut remaining = s.trim();
157
158    while !remaining.is_empty() {
159        // 查找冒号(跳过括号内的)
160        let mut paren_depth: i32 = 0;
161        let colon_pos = {
162            let mut pos = None;
163            for (i, ch) in remaining.char_indices() {
164                match ch {
165                    '(' => paren_depth += 1,
166                    ')' => paren_depth = (paren_depth - 1).max(0),
167                    ':' if paren_depth == 0 => {
168                        pos = Some(i);
169                        break;
170                    }
171                    _ => {}
172                }
173            }
174            pos
175        };
176
177        let colon_pos = match colon_pos {
178            Some(p) => p,
179            None => break,
180        };
181
182        let property = remaining[..colon_pos].trim().to_lowercase();
183        remaining = remaining[colon_pos + 1..].trim_start();
184
185        // 查找分号(跳过括号内的)或字符串结束
186        let (value, rest) = {
187            let mut paren_depth: i32 = 0;
188            let sc_pos = {
189                let mut pos = None;
190                for (i, ch) in remaining.char_indices() {
191                    match ch {
192                        '(' => paren_depth += 1,
193                        ')' => paren_depth = (paren_depth - 1).max(0),
194                        ';' if paren_depth == 0 => {
195                            pos = Some(i);
196                            break;
197                        }
198                        _ => {}
199                    }
200                }
201                pos
202            };
203
204            if let Some(p) = sc_pos {
205                (remaining[..p].trim().to_string(), remaining[p + 1..].trim())
206            } else {
207                (remaining.trim().to_string(), "")
208            }
209        };
210
211        if !property.is_empty() {
212            map.insert(property, value);
213        }
214
215        remaining = rest;
216    }
217
218    map
219}
220
221#[cfg(test)]
222mod tests {
223    use super::*;
224
225    #[test]
226    fn test_css_style_sheet_from_css() {
227        let sheet = CssStyleSheet::from_css(".foo { color: red; } .bar { margin: 10px; }");
228        assert_eq!(sheet.rules.len(), 2);
229        assert_eq!(sheet.rules[0].selector_text, ".foo");
230    }
231
232    #[test]
233    fn test_insert_rule() {
234        let mut sheet = CssStyleSheet::from_css("");
235        sheet.insert_rule(".btn { color: blue; }", 0).unwrap();
236        assert_eq!(sheet.rules.len(), 1);
237        assert_eq!(sheet.rules[0].style.get_property_value("color").unwrap(), "blue");
238    }
239
240    #[test]
241    fn test_delete_rule() {
242        let mut sheet = CssStyleSheet::from_css(".a { x:1; } .b { y:2; }");
243        sheet.delete_rule(0);
244        assert_eq!(sheet.rules.len(), 1);
245    }
246
247    #[test]
248    fn test_style_declaration() {
249        let mut decl = CssStyleDeclaration::new();
250        decl.set_property("color", "red");
251        assert_eq!(decl.get_property_value("color").unwrap(), "red");
252        decl.remove_property("color");
253        assert!(decl.get_property_value("color").is_none());
254    }
255
256    #[test]
257    fn test_to_js_json() {
258        let sheet = CssStyleSheet::from_css(".btn { color: red; font-size: 14px; }");
259        let json = sheet.to_js_json();
260        assert!(json.contains("color"));
261        assert!(json.contains("red"));
262        assert!(json.contains("selectorText"));
263    }
264}