1use crate::{CssRule, Declaration, StyleSheet};
10use std::collections::HashMap;
11
12#[derive(Debug, Clone)]
14pub struct CssStyleSheet {
15 pub rules: Vec<CssStyleRule>,
17 pub disabled: bool,
19 pub owner_id: Option<String>,
21}
22
23impl CssStyleSheet {
24 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 pub fn from_css(css: &str) -> Self {
35 let sheet = StyleSheet::parse(css).unwrap_or_default();
36 Self::from_parsed(&sheet)
37 }
38
39 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 pub fn delete_rule(&mut self, index: usize) {
62 if index < self.rules.len() {
63 self.rules.remove(index);
64 }
65 }
66
67 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#[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#[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
135pub struct Css;
137
138impl Css {
139 pub fn escape(ident: &str) -> String {
141 ident.replace('\\', "\\\\")
142 .replace('"', "\\\"")
143 .replace('\'', "\\'")
144 }
145
146 pub fn supports(property: &str, value: &str) -> bool {
148 !property.is_empty() && !value.is_empty()
149 }
150}
151
152fn 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}