css_parser/
rules.rs

1use crate::token_as_ident;
2
3use super::{ASTNode, CSSToken, CSSValue, ParseError, Selector, ToStringSettings};
4use source_map::{Span, ToString};
5use tokenizer_lib::{Token, TokenReader};
6
7/// A css rule with a selector and collection of declarations
8#[derive(Debug)]
9pub struct Rule {
10    pub selectors: Vec<Selector>,
11    pub nested_rules: Option<Vec<Rule>>,
12    pub declarations: Vec<(String, CSSValue)>,
13    pub position: Option<Span>,
14}
15
16impl ASTNode for Rule {
17    fn from_reader(reader: &mut impl TokenReader<CSSToken, Span>) -> Result<Self, ParseError> {
18        let mut selectors = vec![Selector::from_reader(reader)?];
19        while let Token(CSSToken::Comma, _) = reader.peek().unwrap() {
20            reader.next();
21            selectors.push(Selector::from_reader(reader)?);
22        }
23        let first_span = selectors.first().unwrap().get_position().unwrap();
24        reader.expect_next(CSSToken::OpenCurly)?;
25
26        // Parse declarations and nested rules
27        let mut declarations: Vec<(String, CSSValue)> = Vec::new();
28        let mut nested_rules: Option<Vec<Rule>> = None;
29        while let Some(Token(token_type, _)) = reader.peek() {
30            if token_type == &CSSToken::CloseCurly {
31                break;
32            }
33            let mut is_rule: Option<bool> = None;
34            reader.scan(|token, _| {
35                match token {
36                    CSSToken::Colon | CSSToken::CloseCurly => is_rule = Some(false),
37                    CSSToken::OpenCurly => is_rule = Some(true),
38                    _ => {}
39                }
40                is_rule.is_some()
41            });
42
43            if is_rule.unwrap_or_default() {
44                nested_rules
45                    .get_or_insert_with(|| Vec::new())
46                    .push(Rule::from_reader(reader)?);
47            } else {
48                let (property_name, _) = token_as_ident(reader.next().unwrap())?;
49                reader.expect_next(CSSToken::Colon)?;
50                let value = CSSValue::from_reader(reader)?;
51                declarations.push((property_name, value));
52                if let Token(CSSToken::CloseCurly, last_span) = reader.next().unwrap() {
53                    return Ok(Self {
54                        position: Some(first_span.union(&last_span)),
55                        selectors,
56                        declarations,
57                        nested_rules,
58                    });
59                }
60            }
61        }
62        let last_span = reader.expect_next(CSSToken::CloseCurly)?;
63        Ok(Self {
64            position: Some(first_span.union(&last_span)),
65            selectors,
66            declarations,
67            nested_rules,
68        })
69    }
70
71    fn to_string_from_buffer(
72        &self,
73        buf: &mut impl ToString,
74        settings: &ToStringSettings,
75        depth: u8,
76    ) {
77        for (idx, selector) in self.selectors.iter().enumerate() {
78            selector.to_string_from_buffer(buf, settings, depth);
79            if idx + 1 < self.selectors.len() {
80                if settings.minify {
81                    buf.push(',');
82                } else {
83                    buf.push_str(", ");
84                }
85            }
86        }
87        if !settings.minify {
88            buf.push(' ');
89        }
90        buf.push('{');
91        for (idx, (name, value)) in self.declarations.iter().enumerate() {
92            if !settings.minify {
93                buf.push_new_line();
94                buf.push_str(&settings.indent_with.repeat(depth as usize + 1));
95            }
96            buf.push_str(name);
97            buf.push(':');
98
99            if !settings.minify {
100                buf.push(' ');
101            }
102            value.to_string_from_buffer(buf, settings, depth);
103            buf.push(';');
104            if !settings.minify && idx == self.declarations.len() - 1 {
105                buf.push_new_line();
106                buf.push_str(&settings.indent_with.repeat(depth as usize));
107            }
108        }
109        buf.push('}');
110    }
111
112    fn get_position(&self) -> Option<&Span> {
113        self.position.as_ref()
114    }
115}