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> {
155 let mut map = HashMap::new();
156 let mut remaining = s.trim();
157
158 while !remaining.is_empty() {
159 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 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}