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.
153fn parse_decl_string(s: &str) -> HashMap<String, String> {
154    let mut map = HashMap::new();
155    for part in s.split(';') {
156        let part = part.trim();
157        if part.is_empty() { continue; }
158        if let Some((prop, val)) = part.split_once(':') {
159            map.insert(prop.trim().to_lowercase(), val.trim().to_string());
160        }
161    }
162    map
163}
164
165#[cfg(test)]
166mod tests {
167    use super::*;
168
169    #[test]
170    fn test_css_style_sheet_from_css() {
171        let sheet = CssStyleSheet::from_css(".foo { color: red; } .bar { margin: 10px; }");
172        assert_eq!(sheet.rules.len(), 2);
173        assert_eq!(sheet.rules[0].selector_text, ".foo");
174    }
175
176    #[test]
177    fn test_insert_rule() {
178        let mut sheet = CssStyleSheet::from_css("");
179        sheet.insert_rule(".btn { color: blue; }", 0).unwrap();
180        assert_eq!(sheet.rules.len(), 1);
181        assert_eq!(sheet.rules[0].style.get_property_value("color").unwrap(), "blue");
182    }
183
184    #[test]
185    fn test_delete_rule() {
186        let mut sheet = CssStyleSheet::from_css(".a { x:1; } .b { y:2; }");
187        sheet.delete_rule(0);
188        assert_eq!(sheet.rules.len(), 1);
189    }
190
191    #[test]
192    fn test_style_declaration() {
193        let mut decl = CssStyleDeclaration::new();
194        decl.set_property("color", "red");
195        assert_eq!(decl.get_property_value("color").unwrap(), "red");
196        decl.remove_property("color");
197        assert!(decl.get_property_value("color").is_none());
198    }
199
200    #[test]
201    fn test_to_js_json() {
202        let sheet = CssStyleSheet::from_css(".btn { color: red; font-size: 14px; }");
203        let json = sheet.to_js_json();
204        assert!(json.contains("color"));
205        assert!(json.contains("red"));
206        assert!(json.contains("selectorText"));
207    }
208}