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}