1use cssparser::{Parser, ParserInput};
6
7#[allow(missing_docs)]
9#[derive(Debug, Clone, PartialEq)]
10pub enum SelectorType {
11 Tag(String),
13 Id(String),
15 Class(String),
17 Attribute { name: String, value: Option<String> },
19 Universal,
21 Compound(Vec<SelectorType>),
23 Descendant(Box<SelectorType>, Box<SelectorType>),
25 Child(Box<SelectorType>, Box<SelectorType>),
27}
28
29#[derive(Debug, Clone, PartialEq)]
31pub struct Selector {
32 pub text: String,
34 pub selector_type: SelectorType,
36}
37
38impl Selector {
39 pub fn new(text: &str) -> Self {
41 let selector_type = parse_selector_type(text);
42 Self {
43 text: text.to_string(),
44 selector_type,
45 }
46 }
47
48 pub fn with_type(text: &str, selector_type: SelectorType) -> Self {
50 Self {
51 text: text.to_string(),
52 selector_type,
53 }
54 }
55
56 pub fn is_id(&self) -> bool {
58 matches!(self.selector_type, SelectorType::Id(_))
59 }
60
61 pub fn is_class(&self) -> bool {
63 matches!(self.selector_type, SelectorType::Class(_))
64 }
65
66 pub fn is_tag(&self) -> bool {
68 matches!(self.selector_type, SelectorType::Tag(_))
69 }
70
71 pub fn is_compound(&self) -> bool {
73 matches!(self.selector_type, SelectorType::Compound(_))
74 }
75}
76
77fn parse_selector_type(text: &str) -> SelectorType {
79 let text = text.trim();
80
81 if text == "*" {
83 return SelectorType::Universal;
84 }
85
86 if text.starts_with('[') && text.ends_with(']') {
88 let content = &text[1..text.len() - 1];
89 if let Some(eq_pos) = content.find('=') {
90 let name = content[..eq_pos].trim().to_string();
91 let value = Some(content[eq_pos + 1..].trim().trim_matches(|c| c == '"' || c == '\'').to_string());
92 return SelectorType::Attribute { name, value };
93 } else {
94 return SelectorType::Attribute {
95 name: content.trim().to_string(),
96 value: None,
97 };
98 }
99 }
100
101 if (text.contains('.') || text.contains('#')) && !text.starts_with('.') && !text.starts_with('#') {
103 let mut parts = Vec::new();
105 let mut current = String::new();
106 let mut chars = text.chars().peekable();
107
108 while let Some(ch) = chars.next() {
109 if ch == '.' || ch == '#' {
110 if !current.is_empty() {
111 if parts.is_empty() && !current.starts_with('.') && !current.starts_with('#') {
113 parts.push(SelectorType::Tag(current.clone()));
114 } else if current.starts_with('.') {
115 parts.push(SelectorType::Class(current[1..].to_string()));
116 } else if current.starts_with('#') {
117 parts.push(SelectorType::Id(current[1..].to_string()));
118 }
119 current.clear();
120 }
121 current.push(ch);
122 } else {
123 current.push(ch);
124 }
125 }
126
127 if !current.is_empty() {
129 if current.starts_with('.') {
130 parts.push(SelectorType::Class(current[1..].to_string()));
131 } else if current.starts_with('#') {
132 parts.push(SelectorType::Id(current[1..].to_string()));
133 } else {
134 parts.push(SelectorType::Class(current)); }
136 }
137
138 if parts.len() > 1 {
139 return SelectorType::Compound(parts);
140 }
141 }
142
143 if text.starts_with('#') {
145 SelectorType::Id(text[1..].to_string())
146 } else if text.starts_with('.') {
147 SelectorType::Class(text[1..].to_string())
148 } else {
149 SelectorType::Tag(text.to_string())
150 }
151}
152
153#[derive(Debug, Clone)]
155pub struct Declaration {
156 pub property: String,
158 pub value: String,
160}
161
162#[derive(Debug, Clone)]
164pub struct CSSRule {
165 pub selector: Selector,
167 pub declarations: Vec<Declaration>,
169}
170
171impl CSSRule {
172 pub fn new(selector: Selector, declarations: Vec<Declaration>) -> Self {
174 Self {
175 selector,
176 declarations,
177 }
178 }
179}
180
181#[derive(Debug, Clone)]
183pub struct Stylesheet {
184 pub rules: Vec<CSSRule>,
186}
187
188impl Stylesheet {
189 pub fn new() -> Self {
191 Self {
192 rules: Vec::new(),
193 }
194 }
195
196 pub fn add_rule(&mut self, rule: CSSRule) {
198 self.rules.push(rule);
199 }
200}
201
202pub fn parse_stylesheet(css: &str) -> Stylesheet {
225 let mut input = ParserInput::new(css);
226 let _parser = Parser::new(&mut input);
227
228 let mut stylesheet = Stylesheet::new();
229
230 parse_css_manual(css, &mut stylesheet);
233
234 stylesheet
235}
236
237fn parse_css_manual(css: &str, stylesheet: &mut Stylesheet) {
239 let css = remove_comments(css);
241
242 let rules = split_rules(&css);
244
245 for rule_text in rules {
246 if let Some(rule) = parse_single_rule(rule_text.trim()) {
247 stylesheet.add_rule(rule);
248 }
249 }
250}
251
252fn remove_comments(css: &str) -> String {
254 let mut result = String::new();
255 let mut chars = css.chars().peekable();
256
257 while let Some(ch) = chars.next() {
258 if ch == '/' {
259 if let Some(&'*') = chars.peek() {
260 chars.next(); while let Some(ch) = chars.next() {
263 if ch == '*' {
264 if let Some(&'/') = chars.peek() {
265 chars.next(); break;
267 }
268 }
269 }
270 } else {
271 result.push(ch);
272 }
273 } else {
274 result.push(ch);
275 }
276 }
277
278 result
279}
280
281fn split_rules(css: &str) -> Vec<&str> {
283 let mut rules = Vec::new();
284 let mut start = 0;
285 let mut brace_count = 0;
286
287 for (i, ch) in css.char_indices() {
288 match ch {
289 '{' => brace_count += 1,
290 '}' => {
291 brace_count -= 1;
292 if brace_count == 0 {
293 rules.push(&css[start..=i]);
294 start = i + 1;
295 }
296 }
297 _ => {}
298 }
299 }
300
301 rules
302}
303
304fn parse_single_rule(rule_text: &str) -> Option<CSSRule> {
306 let brace_pos = rule_text.find('{')?;
308
309 let selector_text = rule_text[..brace_pos].trim();
310 let declarations_text = &rule_text[brace_pos + 1..rule_text.len() - 1]; if selector_text.is_empty() {
313 return None;
314 }
315
316 let selector = Selector::new(selector_text);
317 let declarations = parse_declarations(declarations_text);
318
319 Some(CSSRule::new(selector, declarations))
320}
321
322fn parse_declarations(text: &str) -> Vec<Declaration> {
324 let mut declarations = Vec::new();
325
326 for decl_text in text.split(';') {
327 let decl_text = decl_text.trim();
328 if decl_text.is_empty() {
329 continue;
330 }
331
332 if let Some(colon_pos) = decl_text.find(':') {
333 let property = decl_text[..colon_pos].trim().to_string();
334 let value = decl_text[colon_pos + 1..].trim().to_string();
335
336 declarations.push(Declaration { property, value });
337 }
338 }
339
340 declarations
341}
342
343#[cfg(test)]
344mod tests {
345 use super::*;
346
347 #[test]
348 fn test_parse_simple_css() {
349 let css = r#"
350 .container {
351 padding: 20px;
352 background-color: white;
353 }
354 "#;
355
356 let stylesheet = parse_stylesheet(css);
357 assert_eq!(stylesheet.rules.len(), 1);
358 assert_eq!(stylesheet.rules[0].selector.text, ".container");
359 assert_eq!(stylesheet.rules[0].declarations.len(), 2);
360 }
361
362 #[test]
363 fn test_parse_multiple_rules() {
364 let css = r#"
365 .class1 { color: red; }
366 #id1 { font-size: 16px; }
367 div { margin: 0; }
368 "#;
369
370 let stylesheet = parse_stylesheet(css);
371 assert_eq!(stylesheet.rules.len(), 3);
372 }
373
374 #[test]
375 fn test_selector_types() {
376 let class_sel = Selector::new(".container");
377 let id_sel = Selector::new("#main");
378 let tag_sel = Selector::new("div");
379
380 assert!(class_sel.is_class());
381 assert!(id_sel.is_id());
382 assert!(tag_sel.is_tag());
383 }
384
385 #[test]
386 fn test_parse_with_comments() {
387 let css = r#"
388 /* This is a comment */
389 .container {
390 padding: 20px; /* inline comment */
391 }
392 "#;
393
394 let stylesheet = parse_stylesheet(css);
395 assert_eq!(stylesheet.rules.len(), 1);
396 }
397
398 #[test]
399 fn test_attribute_selector() {
400 let sel1 = Selector::new("[data-type]");
401 let sel2 = Selector::new("[data-type=button]");
402 let sel3 = Selector::new("[href=\"https://example.com\"]");
403
404 assert!(sel1.is_compound() || matches!(sel1.selector_type, crate::css::SelectorType::Attribute { .. }));
405 assert!(sel2.is_compound() || matches!(sel2.selector_type, crate::css::SelectorType::Attribute { .. }));
406 assert!(sel3.is_compound() || matches!(sel3.selector_type, crate::css::SelectorType::Attribute { .. }));
407 }
408
409 #[test]
410 fn test_compound_selector() {
411 let sel = Selector::new("div.container");
412 assert!(sel.is_compound());
413
414 let sel2 = Selector::new("div#main.container");
415 assert!(sel2.is_compound());
416 }
417
418 #[test]
419 fn test_universal_selector() {
420 let sel = Selector::new("*");
421 assert!(matches!(sel.selector_type, crate::css::SelectorType::Universal));
422 }
423
424 #[test]
425 fn test_selector_type_parsing() {
426 use crate::css::SelectorType;
427
428 let id_sel = Selector::new("#main");
429 assert!(matches!(id_sel.selector_type, SelectorType::Id(s) if s == "main"));
430
431 let class_sel = Selector::new(".container");
432 assert!(matches!(class_sel.selector_type, SelectorType::Class(s) if s == "container"));
433
434 let tag_sel = Selector::new("div");
435 assert!(matches!(tag_sel.selector_type, SelectorType::Tag(s) if s == "div"));
436 }
437}