1use std::collections::HashMap;
18use anyhow::{Context, Result};
19
20#[derive(Debug, Default, Clone)]
22pub struct StyleSheet {
23 rules: HashMap<String, HashMap<String, String>>,
25}
26
27impl StyleSheet {
28 pub fn parse(css: &str) -> Result<Self> {
33 let mut rules: HashMap<String, HashMap<String, String>> = HashMap::new();
34 let mut chars = css.chars().peekable();
35
36 while let Some(&c) = chars.peek() {
37 if c.is_whitespace() {
39 chars.next();
40 continue;
41 }
42
43 if c == '/' {
45 chars.next();
46 if chars.peek() == Some(&'*') {
47 chars.next();
48 loop {
49 match chars.next() {
50 Some('*') if chars.peek() == Some(&'/') => {
51 chars.next();
52 break;
53 }
54 None => break,
55 _ => {}
56 }
57 }
58 }
59 continue;
60 }
61
62 let selector: String = chars.by_ref().take_while(|&c| c != '{').collect();
64 let selector = selector.trim().to_string();
65
66 let block: String = chars.by_ref().take_while(|&c| c != '}').collect();
68
69 if selector.is_empty() {
70 continue;
71 }
72
73 let mut props = HashMap::new();
74 for declaration in block.split(';') {
75 let declaration = declaration.trim();
76 if declaration.is_empty() {
77 continue;
78 }
79 if let Some((key, value)) = declaration.split_once(':') {
80 props.insert(
81 key.trim().to_string(),
82 value.trim().to_string(),
83 );
84 }
85 }
86
87 for sel in selector.split(',') {
89 let sel = sel.trim().trim_start_matches('.').to_string();
90 rules
91 .entry(sel)
92 .or_default()
93 .extend(props.clone());
94 }
95 }
96
97 Ok(Self { rules })
98 }
99
100 pub fn from_file(path: &str) -> Result<Self> {
102 let css = std::fs::read_to_string(path)
103 .with_context(|| format!("Failed to read CSS file: {}", path))?;
104 Self::parse(&css)
105 }
106
107 pub fn resolve(&self, class: &str) -> Option<&HashMap<String, String>> {
109 self.rules.get(class)
110 }
111
112 pub fn get(&self, class: &str, property: &str) -> Option<&str> {
114 self.rules.get(class)?.get(property).map(|s| s.as_str())
115 }
116
117 pub fn merge(&mut self, other: StyleSheet) {
119 for (class, props) in other.rules {
120 self.rules.entry(class).or_default().extend(props);
121 }
122 }
123
124 pub fn rule_count(&self) -> usize {
126 self.rules.len()
127 }
128}
129
130pub const MAIN_CSS_EXAMPLE: &str = r#"
143/* ── Layout ─────────────────────────────────────── */
144.screen {
145 flex-direction: column;
146 align-items: center;
147 padding: 24px;
148 background: #f8f9fa;
149}
150
151/* ── Typography ──────────────────────────────────── */
152.title {
153 font-size: 28px;
154 font-weight: bold;
155 color: #1a1a2e;
156 margin-bottom: 16px;
157}
158
159.label {
160 font-size: 16px;
161 color: #555;
162 margin-bottom: 8px;
163}
164
165/* ── Buttons ─────────────────────────────────────── */
166.primary-btn {
167 background: #4CAF50;
168 color: white;
169 padding: 12px 24px;
170 border-radius: 8px;
171 font-size: 16px;
172 font-weight: 600;
173 margin-top: 12px;
174}
175
176.link-btn {
177 background: transparent;
178 color: #4CAF50;
179 padding: 10px 20px;
180 border-radius: 8px;
181 font-size: 14px;
182 margin-top: 8px;
183}
184
185.danger-btn {
186 background: #e53935;
187 color: white;
188 padding: 12px 24px;
189 border-radius: 8px;
190 font-size: 16px;
191 font-weight: 600;
192 margin-top: 12px;
193}
194
195/* ── Inputs ──────────────────────────────────────── */
196.text-input {
197 background: white;
198 border: 1.5px solid #ddd;
199 border-radius: 8px;
200 padding: 12px 16px;
201 font-size: 16px;
202 width: 100%;
203 margin-top: 16px;
204}
205
206/* ── Avatar ──────────────────────────────────────── */
207.avatar {
208 width: 80px;
209 height: 80px;
210 border-radius: 40px;
211 margin-bottom: 16px;
212 border: 3px solid #4CAF50;
213}
214"#;
215
216#[cfg(test)]
217mod tests {
218 use super::*;
219
220 #[test]
221 fn parse_basic_rule() {
222 let sheet = StyleSheet::parse(".title { font-size: 24px; color: red; }").unwrap();
223 assert_eq!(sheet.get("title", "font-size"), Some("24px"));
224 assert_eq!(sheet.get("title", "color"), Some("red"));
225 }
226
227 #[test]
228 fn parse_multiple_selectors() {
229 let sheet = StyleSheet::parse(".a, .b { color: blue; }").unwrap();
230 assert_eq!(sheet.get("a", "color"), Some("blue"));
231 assert_eq!(sheet.get("b", "color"), Some("blue"));
232 }
233
234 #[test]
235 fn parse_main_css() {
236 let sheet = StyleSheet::parse(MAIN_CSS_EXAMPLE).unwrap();
237 assert!(sheet.rule_count() > 0);
238 assert_eq!(sheet.get("primary-btn", "background"), Some("#4CAF50"));
239 assert_eq!(sheet.get("danger-btn", "background"), Some("#e53935"));
240 }
241}