1use crate::{CssRule, Declaration, StyleSheet};
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 {
20 let mut sheet = StyleSheet::default();
21 let mut remaining = css;
22
23 while let Some(open_pos) = remaining.find('{') {
24 let before = remaining[..open_pos].trim();
25 remaining = &remaining[open_pos + 1..];
26
27 let close_pos = match find_matching_brace(remaining) {
29 Some(p) => p,
30 None => break,
31 };
32
33 let decl_str = remaining[..close_pos].trim();
34 remaining = &remaining[close_pos + 1..];
35
36 if before.starts_with('@') {
38 continue;
39 }
40
41 if before.is_empty() {
43 continue;
44 }
45
46 let selectors: Vec<String> = before.split(',')
47 .map(|s| s.trim().to_string())
48 .filter(|s| !s.is_empty())
49 .collect();
50
51 let declarations = parse_declarations(decl_str);
52
53 if !selectors.is_empty() {
54 sheet.rules.push(CssRule { selectors, declarations });
55 }
56 }
57
58 sheet
59}
60
61fn find_matching_brace(s: &str) -> Option<usize> {
63 let mut depth = 1i32;
64 for (i, c) in s.char_indices() {
65 match c {
66 '{' => depth += 1,
67 '}' => {
68 depth -= 1;
69 if depth == 0 { return Some(i); }
70 }
71 _ => {}
72 }
73 }
74 None
75}
76
77fn parse_declarations(s: &str) -> Vec<Declaration> {
79 let mut result = Vec::new();
80 let mut remaining = s.trim();
81
82 while !remaining.is_empty() {
83 let colon_pos = match remaining.find(':') {
85 Some(p) => p,
86 None => break,
87 };
88
89 let property = remaining[..colon_pos].trim().to_lowercase();
90 remaining = remaining[colon_pos + 1..].trim_start();
91
92 let (value, rest) = if let Some(sc_pos) = remaining.find(';') {
94 (remaining[..sc_pos].trim().to_string(), remaining[sc_pos + 1..].trim())
95 } else {
96 (remaining.trim().to_string(), "")
97 };
98
99 if !property.is_empty() {
100 result.push(Declaration { property, value });
101 }
102
103 remaining = rest;
104 }
105
106 result
107}
108
109#[cfg(test)]
110mod tests {
111 use super::*;
112
113 #[test]
114 fn test_parse_simple() {
115 let sheet = parse_css_simple(".foo { color: red; font-size: 16px; } .bar { margin: 10px; }");
116 assert_eq!(sheet.rules.len(), 2);
117 assert_eq!(sheet.rules[0].selectors[0], ".foo");
118 assert_eq!(sheet.rules[0].declarations[0].property, "color");
119 }
120
121 #[test]
122 fn test_complex_selector() {
123 let sheet = parse_css_simple("div.container > p.highlight { color: blue; }");
124 assert_eq!(sheet.rules.len(), 1);
125 assert_eq!(sheet.rules[0].declarations[0].value, "blue");
126 }
127
128 #[test]
129 fn test_multiple_selectors() {
130 let sheet = parse_css_simple("h1, h2, h3 { font-weight: bold; }");
131 assert_eq!(sheet.rules[0].selectors.len(), 3);
132 }
133
134 #[test]
135 fn test_at_rule_skipped() {
136 let sheet = parse_css_simple("@media screen { .a { color: red; } } .b { color: blue; }");
137 assert_eq!(sheet.rules.len(), 1);
139 assert_eq!(sheet.rules[0].selectors[0], ".b");
140 }
141
142 #[test]
143 fn test_stylesheet_compute() {
144 let sheet = parse_css_simple(".btn { color: red; font-size: 14px; } .btn-primary { color: blue; }");
145 let map = sheet.compute(&["btn".into(), "btn-primary".into()], "button");
146 assert_eq!(map.get("color").unwrap(), "blue");
147 assert_eq!(map.get("font-size").unwrap(), "14px");
148 }
149
150 #[test]
151 fn test_empty_input() {
152 let sheet = parse_css_simple("");
153 assert_eq!(sheet.rules.len(), 0);
154 }
155
156 #[test]
157 fn test_nested_braces() {
158 let sheet = parse_css_simple(".a { x: 1; } .b { y: 2; }");
159 assert_eq!(sheet.rules.len(), 2);
160 }
161}