css_parser/
values.rs

1use super::{ASTNode, CSSToken, ParseError, Span, ToStringSettings, Token};
2use source_map::ToString;
3use tokenizer_lib::TokenReader;
4
5#[derive(Debug, PartialEq, Eq)]
6pub struct Number(pub String);
7
8#[derive(Debug, PartialEq, Eq)]
9pub enum CSSValue {
10    Keyword(String),
11    Function(String, Vec<CSSValue>),
12    StringLiteral(String),
13    Number(Number),
14    NumberWithUnit(Number, String),
15    Percentage(Number),
16    Color(String),
17    List(Vec<CSSValue>),
18    CommaSeparatedList(Vec<CSSValue>),
19}
20
21impl ASTNode for CSSValue {
22    fn from_reader(reader: &mut impl TokenReader<CSSToken, Span>) -> Result<Self, ParseError> {
23        let value = Self::single_value_from_reader(reader)?;
24        macro_rules! css_value_has_ended {
25            () => {
26                matches!(
27                    reader.peek().unwrap().0,
28                    CSSToken::EOS | CSSToken::SemiColon | CSSToken::CloseCurly
29                )
30            };
31        }
32        if !css_value_has_ended!() {
33            let mut values: Vec<CSSValue> = vec![value];
34            while !css_value_has_ended!() {
35                values.push(Self::single_value_from_reader(reader)?);
36            }
37            Ok(CSSValue::List(values))
38        } else {
39            Ok(value)
40        }
41    }
42
43    fn to_string_from_buffer(
44        &self,
45        buf: &mut impl ToString,
46        settings: &ToStringSettings,
47        depth: u8,
48    ) {
49        match self {
50            Self::Keyword(keyword) => buf.push_str(&keyword),
51            Self::Color(color) => {
52                buf.push('#');
53                buf.push_str(&color);
54            }
55            Self::StringLiteral(content) => {
56                buf.push('"');
57                buf.push_str(&content);
58                buf.push('"');
59            }
60            Self::Percentage(percent) => {
61                buf.push_str(&percent.0);
62                buf.push('%');
63            }
64            Self::Number(value) => {
65                buf.push_str(&value.0);
66            }
67            Self::NumberWithUnit(value, unit) => {
68                buf.push_str(&value.0);
69                buf.push_str(&unit);
70            }
71            Self::List(values) => {
72                for (idx, value) in values.iter().enumerate() {
73                    value.to_string_from_buffer(buf, settings, depth);
74                    if idx + 1 < values.len() && !settings.minify {
75                        buf.push(' ');
76                    }
77                }
78            }
79            Self::CommaSeparatedList(values) => {
80                for (idx, value) in values.iter().enumerate() {
81                    value.to_string_from_buffer(buf, settings, depth);
82                    if idx + 1 < values.len() {
83                        buf.push(',');
84                        if !settings.minify {
85                            buf.push(' ');
86                        }
87                    }
88                }
89            }
90            Self::Function(func, arguments) => {
91                buf.push_str(&func);
92                buf.push('(');
93                for (idx, value) in arguments.iter().enumerate() {
94                    value.to_string_from_buffer(buf, settings, depth);
95                    if idx + 1 < arguments.len() {
96                        buf.push(',');
97                        if !settings.minify {
98                            buf.push(' ');
99                        }
100                    }
101                }
102                buf.push(')');
103            }
104        }
105    }
106
107    fn get_position(&self) -> Option<&Span> {
108        unreachable!()
109    }
110}
111
112impl CSSValue {
113    fn single_value_from_reader(
114        reader: &mut impl TokenReader<CSSToken, Span>,
115    ) -> Result<Self, ParseError> {
116        match reader.next().unwrap() {
117            Token(CSSToken::Ident(ident), start_span) => {
118                let Token(peek_type, peek_span) = reader.peek().unwrap();
119                if *peek_type == CSSToken::OpenBracket && start_span.is_adjacent_to(peek_span) {
120                    reader.next();
121                    reader.expect_next(CSSToken::CloseBracket)?;
122                    todo!("Functions")
123                } else {
124                    Ok(CSSValue::Keyword(ident))
125                }
126            }
127            Token(CSSToken::HashPrefixedValue(color), _) => Ok(CSSValue::Color(color)),
128            Token(CSSToken::Number(number), start_position) => {
129                let Token(peek_type, peek_position) = reader.peek().unwrap();
130                let number = Number(number);
131                if start_position.is_adjacent_to(peek_position)
132                    && !matches!(peek_type, CSSToken::EOS | CSSToken::SemiColon)
133                {
134                    match peek_type {
135                        CSSToken::Percentage => {
136                            reader.next();
137                            Ok(CSSValue::Percentage(number))
138                        }
139                        CSSToken::Ident(_) => {
140                            let unit = if let CSSToken::Ident(unit) = reader.next().unwrap().0 {
141                                unit
142                            } else {
143                                unreachable!()
144                            };
145                            Ok(CSSValue::NumberWithUnit(number, unit))
146                        }
147                        _ => Ok(CSSValue::Number(number)),
148                    }
149                } else {
150                    Ok(CSSValue::Number(number))
151                }
152            }
153            Token(CSSToken::String(string), _) => Ok(CSSValue::StringLiteral(string)),
154            Token(token, position) => Err(ParseError {
155                reason: format!("Expected value, found {:?}", token),
156                position,
157            }),
158        }
159    }
160}
161
162#[cfg(test)]
163mod css_values_test {
164    use super::{ASTNode, CSSValue, Number};
165    use source_map::SourceId;
166
167    const NULL_SOURCE_ID: SourceId = SourceId::null();
168
169    macro_rules! test_value {
170        ($test_name:ident, $src:expr, $res:expr) => {
171            #[test]
172            fn $test_name() {
173                assert_eq!(
174                    CSSValue::from_string($src.to_owned(), NULL_SOURCE_ID, None).unwrap(),
175                    $res
176                );
177            }
178        };
179    }
180
181    test_value!(keyword, "block", CSSValue::Keyword("block".to_owned()));
182    test_value!(color, "#00ff00", CSSValue::Color("00ff00".to_owned()));
183    test_value!(number, "1", CSSValue::Number(Number("1".to_owned())));
184    test_value!(
185        number_decimal_shorthand,
186        ".2",
187        CSSValue::Number(Number(".2".to_owned()))
188    );
189    test_value!(
190        percentage,
191        "10%",
192        CSSValue::Percentage(Number("10".to_owned()))
193    );
194    test_value!(
195        number_with_unit,
196        "10px",
197        CSSValue::NumberWithUnit(Number("10".to_owned()), "px".to_owned())
198    );
199    test_value!(
200        list,
201        "2px solid #00ff00",
202        CSSValue::List(vec![
203            CSSValue::NumberWithUnit(Number("2".to_owned()), "px".to_owned()),
204            CSSValue::Keyword("solid".to_owned()),
205            CSSValue::Color("00ff00".to_owned())
206        ])
207    );
208}