1use crate::{CssRule, Declaration, StyleSheet, MediaQuery, MediaRule, KeyframesRule, Keyframe};
7
8pub fn parse_css(css: &str) -> Result<StyleSheet, String> {
10 Ok(parse_css_simple(css))
11}
12
13pub fn parse_css_simple(css: &str) -> StyleSheet {
21 let mut sheet = StyleSheet::default();
22 let mut remaining = css;
23
24 while let Some(open_pos) = remaining.find('{') {
25 let before = remaining[..open_pos].trim();
26 remaining = &remaining[open_pos + 1..];
27
28 let close_pos = match find_matching_brace(remaining) {
30 Some(p) => p,
31 None => break,
32 };
33
34 let block_content = remaining[..close_pos].trim();
35 remaining = &remaining[close_pos + 1..];
36
37 if before.starts_with("@media") {
39 let query_str = before[6..].trim();
40 let query = MediaQuery::parse(query_str);
41 let rules = parse_nested_rules(block_content);
42 sheet.media_rules.push(MediaRule { query, rules });
43 continue;
44 }
45
46 if before.starts_with("@keyframes") {
48 let name = before[10..].trim().to_string();
49 let keyframes = parse_keyframes(block_content);
50 sheet.keyframes_rules.push(KeyframesRule { name, keyframes });
51 continue;
52 }
53
54 if before.starts_with("@-webkit-keyframes") {
56 let name = before[18..].trim().to_string();
57 let keyframes = parse_keyframes(block_content);
58 sheet.keyframes_rules.push(KeyframesRule { name, keyframes });
59 continue;
60 }
61
62 if before.starts_with('@') {
64 continue;
65 }
66
67 if before.is_empty() {
69 continue;
70 }
71
72 let selectors: Vec<String> = before.split(',')
73 .map(|s| s.trim().to_string())
74 .filter(|s| !s.is_empty())
75 .collect();
76
77 let declarations = parse_declarations(block_content);
78
79 if !selectors.is_empty() {
80 sheet.rules.push(CssRule { selectors, declarations });
81 }
82 }
83
84 sheet
85}
86
87fn parse_nested_rules(content: &str) -> Vec<CssRule> {
89 let mut rules = Vec::new();
90 let mut remaining = content;
91
92 while let Some(open_pos) = remaining.find('{') {
93 let before = remaining[..open_pos].trim();
94 remaining = &remaining[open_pos + 1..];
95
96 let close_pos = match find_matching_brace(remaining) {
97 Some(p) => p,
98 None => break,
99 };
100
101 let decl_str = remaining[..close_pos].trim();
102 remaining = &remaining[close_pos + 1..];
103
104 if before.starts_with('@') {
106 continue;
107 }
108
109 if before.is_empty() {
110 continue;
111 }
112
113 let selectors: Vec<String> = before.split(',')
114 .map(|s| s.trim().to_string())
115 .filter(|s| !s.is_empty())
116 .collect();
117
118 let declarations = parse_declarations(decl_str);
119
120 if !selectors.is_empty() {
121 rules.push(CssRule { selectors, declarations });
122 }
123 }
124
125 rules
126}
127
128fn parse_keyframes(content: &str) -> Vec<Keyframe> {
130 let mut keyframes = Vec::new();
131 let mut remaining = content;
132
133 while let Some(open_pos) = remaining.find('{') {
134 let before = remaining[..open_pos].trim();
135 remaining = &remaining[open_pos + 1..];
136
137 let close_pos = match find_matching_brace(remaining) {
138 Some(p) => p,
139 None => break,
140 };
141
142 let decl_str = remaining[..close_pos].trim();
143 remaining = &remaining[close_pos + 1..];
144
145 let selectors: Vec<&str> = before.split(',').map(|s| s.trim()).collect();
147 let declarations = parse_declarations(decl_str);
148
149 for selector in selectors {
150 if !selector.is_empty() {
151 keyframes.push(Keyframe {
152 selector: selector.to_string(),
153 declarations: declarations.clone(),
154 });
155 }
156 }
157 }
158
159 keyframes.sort_by(|a, b| {
161 a.percentage().partial_cmp(&b.percentage()).unwrap_or(std::cmp::Ordering::Equal)
162 });
163
164 keyframes
165}
166
167fn find_matching_brace(s: &str) -> Option<usize> {
169 let mut depth = 1i32;
170 for (i, c) in s.char_indices() {
171 match c {
172 '{' => depth += 1,
173 '}' => {
174 depth -= 1;
175 if depth == 0 { return Some(i); }
176 }
177 _ => {}
178 }
179 }
180 None
181}
182
183fn parse_declarations(s: &str) -> Vec<Declaration> {
186 let mut result = Vec::new();
187 let mut remaining = s.trim();
188
189 while !remaining.is_empty() {
190 let mut paren_depth: i32 = 0;
192 let colon_pos = {
193 let mut pos = None;
194 for (i, ch) in remaining.char_indices() {
195 match ch {
196 '(' => paren_depth += 1,
197 ')' => paren_depth = (paren_depth - 1).max(0),
198 ':' if paren_depth == 0 => {
199 pos = Some(i);
200 break;
201 }
202 _ => {}
203 }
204 }
205 pos
206 };
207
208 let colon_pos = match colon_pos {
209 Some(p) => p,
210 None => break,
211 };
212
213 let property = remaining[..colon_pos].trim().to_lowercase();
214 remaining = remaining[colon_pos + 1..].trim_start();
215
216 let (value, rest) = {
218 let mut paren_depth: i32 = 0;
219 let sc_pos = {
220 let mut pos = None;
221 for (i, ch) in remaining.char_indices() {
222 match ch {
223 '(' => paren_depth += 1,
224 ')' => paren_depth = (paren_depth - 1).max(0),
225 ';' if paren_depth == 0 => {
226 pos = Some(i);
227 break;
228 }
229 _ => {}
230 }
231 }
232 pos
233 };
234
235 if let Some(p) = sc_pos {
236 (remaining[..p].trim().to_string(), remaining[p + 1..].trim())
237 } else {
238 (remaining.trim().to_string(), "")
239 }
240 };
241
242 if !property.is_empty() {
243 result.push(Declaration { property, value });
244 }
245
246 remaining = rest;
247 }
248
249 result
250}
251
252#[cfg(test)]
253mod tests {
254 use super::*;
255
256 #[test]
257 fn test_parse_simple() {
258 let sheet = parse_css_simple(".foo { color: red; font-size: 16px; } .bar { margin: 10px; }");
259 assert_eq!(sheet.rules.len(), 2);
260 assert_eq!(sheet.rules[0].selectors[0], ".foo");
261 assert_eq!(sheet.rules[0].declarations[0].property, "color");
262 }
263
264 #[test]
265 fn test_complex_selector() {
266 let sheet = parse_css_simple("div.container > p.highlight { color: blue; }");
267 assert_eq!(sheet.rules.len(), 1);
268 assert_eq!(sheet.rules[0].declarations[0].value, "blue");
269 }
270
271 #[test]
272 fn test_multiple_selectors() {
273 let sheet = parse_css_simple("h1, h2, h3 { font-weight: bold; }");
274 assert_eq!(sheet.rules[0].selectors.len(), 3);
275 }
276
277 #[test]
278 fn test_at_rule_skipped() {
279 let sheet = parse_css_simple("@media screen { .a { color: red; } } .b { color: blue; }");
280 assert_eq!(sheet.rules.len(), 1);
282 assert_eq!(sheet.rules[0].selectors[0], ".b");
283 }
284
285 #[test]
286 fn test_stylesheet_compute() {
287 let sheet = parse_css_simple(".btn { color: red; font-size: 14px; } .btn-primary { color: blue; }");
288 let map = sheet.compute(&["btn".into(), "btn-primary".into()], "button");
289 assert_eq!(map.get("color").unwrap(), "blue");
290 assert_eq!(map.get("font-size").unwrap(), "14px");
291 }
292
293 #[test]
294 fn test_empty_input() {
295 let sheet = parse_css_simple("");
296 assert_eq!(sheet.rules.len(), 0);
297 }
298
299 #[test]
300 fn test_nested_braces() {
301 let sheet = parse_css_simple(".a { x: 1; } .b { y: 2; }");
302 assert_eq!(sheet.rules.len(), 2);
303 }
304
305 #[test]
306 fn test_media_rule_parsing() {
307 let sheet = parse_css_simple(
308 "@media screen and (min-width: 768px) { .container { width: 750px; } }"
309 );
310 assert_eq!(sheet.media_rules.len(), 1);
311 assert_eq!(sheet.media_rules[0].query.media_type, "screen");
312 assert_eq!(sheet.media_rules[0].rules.len(), 1);
313 assert_eq!(sheet.media_rules[0].rules[0].selectors[0], ".container");
314 }
315
316 #[test]
317 fn test_keyframes_parsing() {
318 let sheet = parse_css_simple(
319 "@keyframes fade { from { opacity: 0; } to { opacity: 1; } }"
320 );
321 assert_eq!(sheet.keyframes_rules.len(), 1);
322 assert_eq!(sheet.keyframes_rules[0].name, "fade");
323 assert_eq!(sheet.keyframes_rules[0].keyframes.len(), 2);
324 assert_eq!(sheet.keyframes_rules[0].keyframes[0].selector, "from");
325 assert_eq!(sheet.keyframes_rules[0].keyframes[1].selector, "to");
326 }
327
328 #[test]
329 fn test_keyframes_percentage() {
330 let sheet = parse_css_simple(
331 "@keyframes pulse { 0% { transform: scale(1); } 50% { transform: scale(1.1); } 100% { transform: scale(1); } }"
332 );
333 assert_eq!(sheet.keyframes_rules[0].keyframes.len(), 3);
334 assert!((sheet.keyframes_rules[0].keyframes[1].percentage() - 0.5).abs() < 0.001);
335 }
336
337 #[test]
338 fn test_media_query_matching() {
339 let query = MediaQuery::parse("screen and (min-width: 768px)");
340 assert!(query.matches(1024.0, 768.0)); assert!(!query.matches(480.0, 640.0)); }
343
344 #[test]
345 fn test_compute_with_media() {
346 let sheet = parse_css_simple(
347 ".box { width: 100px; } @media screen and (min-width: 768px) { .box { width: 200px; } }"
348 );
349 let small_screen = sheet.compute_with_media(&["box".into()], "div", 480.0, 640.0);
351 assert_eq!(small_screen.get("width").unwrap(), "100px");
352 let large_screen = sheet.compute_with_media(&["box".into()], "div", 1024.0, 768.0);
354 assert_eq!(large_screen.get("width").unwrap(), "200px");
355 }
356}