]>
Commit | Line | Data |
---|---|---|
5099ac24 | 1 | // Take a look at the license at the top of the repository in the LICENSE file. |
8faf50e0 | 2 | |
6a06907d | 3 | use std::convert::TryFrom; |
8faf50e0 XL |
4 | use std::fmt; |
5 | use std::iter::Peekable; | |
6 | use std::str::CharIndices; | |
7 | ||
8faf50e0 XL |
8 | #[derive(Debug, PartialEq, Eq, Clone, Copy)] |
9 | pub enum ReservedChar { | |
10 | Comma, | |
11 | SuperiorThan, | |
12 | OpenParenthese, | |
13 | CloseParenthese, | |
14 | OpenCurlyBrace, | |
15 | CloseCurlyBrace, | |
16 | OpenBracket, | |
17 | CloseBracket, | |
18 | Colon, | |
19 | SemiColon, | |
20 | Slash, | |
21 | Plus, | |
22 | EqualSign, | |
23 | Space, | |
24 | Tab, | |
25 | Backline, | |
26 | Star, | |
27 | Quote, | |
28 | DoubleQuote, | |
29 | Pipe, | |
30 | Tilde, | |
31 | Dollar, | |
32 | Circumflex, | |
33 | } | |
34 | ||
35 | impl fmt::Display for ReservedChar { | |
923072b8 | 36 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
6a06907d XL |
37 | write!( |
38 | f, | |
39 | "{}", | |
40 | match *self { | |
41 | ReservedChar::Comma => ',', | |
42 | ReservedChar::OpenParenthese => '(', | |
43 | ReservedChar::CloseParenthese => ')', | |
44 | ReservedChar::OpenCurlyBrace => '{', | |
45 | ReservedChar::CloseCurlyBrace => '}', | |
46 | ReservedChar::OpenBracket => '[', | |
47 | ReservedChar::CloseBracket => ']', | |
48 | ReservedChar::Colon => ':', | |
49 | ReservedChar::SemiColon => ';', | |
50 | ReservedChar::Slash => '/', | |
51 | ReservedChar::Star => '*', | |
52 | ReservedChar::Plus => '+', | |
53 | ReservedChar::EqualSign => '=', | |
54 | ReservedChar::Space => ' ', | |
55 | ReservedChar::Tab => '\t', | |
56 | ReservedChar::Backline => '\n', | |
57 | ReservedChar::SuperiorThan => '>', | |
58 | ReservedChar::Quote => '\'', | |
59 | ReservedChar::DoubleQuote => '"', | |
60 | ReservedChar::Pipe => '|', | |
61 | ReservedChar::Tilde => '~', | |
62 | ReservedChar::Dollar => '$', | |
63 | ReservedChar::Circumflex => '^', | |
64 | } | |
65 | ) | |
8faf50e0 XL |
66 | } |
67 | } | |
68 | ||
6a06907d | 69 | impl TryFrom<char> for ReservedChar { |
8faf50e0 XL |
70 | type Error = &'static str; |
71 | ||
72 | fn try_from(value: char) -> Result<ReservedChar, Self::Error> { | |
73 | match value { | |
74 | '\'' => Ok(ReservedChar::Quote), | |
6a06907d XL |
75 | '"' => Ok(ReservedChar::DoubleQuote), |
76 | ',' => Ok(ReservedChar::Comma), | |
77 | '(' => Ok(ReservedChar::OpenParenthese), | |
78 | ')' => Ok(ReservedChar::CloseParenthese), | |
79 | '{' => Ok(ReservedChar::OpenCurlyBrace), | |
80 | '}' => Ok(ReservedChar::CloseCurlyBrace), | |
81 | '[' => Ok(ReservedChar::OpenBracket), | |
82 | ']' => Ok(ReservedChar::CloseBracket), | |
83 | ':' => Ok(ReservedChar::Colon), | |
84 | ';' => Ok(ReservedChar::SemiColon), | |
85 | '/' => Ok(ReservedChar::Slash), | |
86 | '*' => Ok(ReservedChar::Star), | |
87 | '+' => Ok(ReservedChar::Plus), | |
88 | '=' => Ok(ReservedChar::EqualSign), | |
89 | ' ' => Ok(ReservedChar::Space), | |
8faf50e0 | 90 | '\t' => Ok(ReservedChar::Tab), |
6a06907d XL |
91 | '\n' | '\r' => Ok(ReservedChar::Backline), |
92 | '>' => Ok(ReservedChar::SuperiorThan), | |
93 | '|' => Ok(ReservedChar::Pipe), | |
94 | '~' => Ok(ReservedChar::Tilde), | |
95 | '$' => Ok(ReservedChar::Dollar), | |
96 | '^' => Ok(ReservedChar::Circumflex), | |
97 | _ => Err("Unknown reserved char"), | |
8faf50e0 XL |
98 | } |
99 | } | |
100 | } | |
101 | ||
102 | impl ReservedChar { | |
103 | fn is_useless(&self) -> bool { | |
6a06907d XL |
104 | *self == ReservedChar::Space |
105 | || *self == ReservedChar::Tab | |
106 | || *self == ReservedChar::Backline | |
8faf50e0 | 107 | } |
0bf4aa26 XL |
108 | |
109 | fn is_operator(&self) -> bool { | |
110 | Operator::try_from(*self).is_ok() | |
111 | } | |
112 | } | |
113 | ||
114 | #[derive(Debug, PartialEq, Eq, Clone, Copy)] | |
115 | pub enum Operator { | |
116 | Plus, | |
117 | Multiply, | |
118 | Minus, | |
119 | Modulo, | |
120 | Divide, | |
121 | } | |
122 | ||
123 | impl fmt::Display for Operator { | |
923072b8 | 124 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
6a06907d XL |
125 | write!( |
126 | f, | |
127 | "{}", | |
128 | match *self { | |
129 | Operator::Plus => '+', | |
130 | Operator::Multiply => '*', | |
131 | Operator::Minus => '-', | |
132 | Operator::Modulo => '%', | |
133 | Operator::Divide => '/', | |
134 | } | |
135 | ) | |
0bf4aa26 XL |
136 | } |
137 | } | |
138 | ||
6a06907d | 139 | impl TryFrom<char> for Operator { |
0bf4aa26 XL |
140 | type Error = &'static str; |
141 | ||
142 | fn try_from(value: char) -> Result<Operator, Self::Error> { | |
143 | match value { | |
144 | '+' => Ok(Operator::Plus), | |
145 | '*' => Ok(Operator::Multiply), | |
146 | '-' => Ok(Operator::Minus), | |
147 | '%' => Ok(Operator::Modulo), | |
148 | '/' => Ok(Operator::Divide), | |
6a06907d | 149 | _ => Err("Unknown operator"), |
0bf4aa26 XL |
150 | } |
151 | } | |
152 | } | |
153 | ||
6a06907d | 154 | impl TryFrom<ReservedChar> for Operator { |
0bf4aa26 XL |
155 | type Error = &'static str; |
156 | ||
157 | fn try_from(value: ReservedChar) -> Result<Operator, Self::Error> { | |
158 | match value { | |
6a06907d XL |
159 | ReservedChar::Slash => Ok(Operator::Divide), |
160 | ReservedChar::Star => Ok(Operator::Multiply), | |
161 | ReservedChar::Plus => Ok(Operator::Plus), | |
162 | _ => Err("Unknown operator"), | |
0bf4aa26 XL |
163 | } |
164 | } | |
8faf50e0 XL |
165 | } |
166 | ||
167 | #[derive(Eq, PartialEq, Clone, Debug)] | |
168 | pub enum SelectorElement<'a> { | |
169 | PseudoClass(&'a str), | |
170 | Class(&'a str), | |
171 | Id(&'a str), | |
172 | Tag(&'a str), | |
173 | Media(&'a str), | |
174 | } | |
175 | ||
6a06907d | 176 | impl<'a> TryFrom<&'a str> for SelectorElement<'a> { |
8faf50e0 XL |
177 | type Error = &'static str; |
178 | ||
923072b8 | 179 | fn try_from(value: &'a str) -> Result<SelectorElement<'_>, Self::Error> { |
6a06907d XL |
180 | if let Some(value) = value.strip_prefix('.') { |
181 | if value.is_empty() { | |
8faf50e0 | 182 | Err("cannot determine selector") |
8faf50e0 | 183 | } else { |
6a06907d | 184 | Ok(SelectorElement::Class(value)) |
8faf50e0 | 185 | } |
6a06907d XL |
186 | } else if let Some(value) = value.strip_prefix('#') { |
187 | if value.is_empty() { | |
8faf50e0 | 188 | Err("cannot determine selector") |
6a06907d XL |
189 | } else { |
190 | Ok(SelectorElement::Id(value)) | |
8faf50e0 | 191 | } |
6a06907d XL |
192 | } else if let Some(value) = value.strip_prefix('@') { |
193 | if value.is_empty() { | |
194 | Err("cannot determine selector") | |
8faf50e0 | 195 | } else { |
6a06907d XL |
196 | Ok(SelectorElement::Media(value)) |
197 | } | |
198 | } else if let Some(value) = value.strip_prefix(':') { | |
199 | if value.is_empty() { | |
8faf50e0 | 200 | Err("cannot determine selector") |
6a06907d XL |
201 | } else { |
202 | Ok(SelectorElement::PseudoClass(value)) | |
8faf50e0 XL |
203 | } |
204 | } else if value.chars().next().unwrap_or(' ').is_alphabetic() { | |
205 | Ok(SelectorElement::Tag(value)) | |
206 | } else { | |
207 | Err("unknown selector") | |
208 | } | |
209 | } | |
210 | } | |
211 | ||
212 | impl<'a> fmt::Display for SelectorElement<'a> { | |
923072b8 | 213 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
8faf50e0 XL |
214 | match *self { |
215 | SelectorElement::Class(c) => write!(f, ".{}", c), | |
216 | SelectorElement::Id(i) => write!(f, "#{}", i), | |
217 | SelectorElement::Tag(t) => write!(f, "{}", t), | |
218 | SelectorElement::Media(m) => write!(f, "@{} ", m), | |
219 | SelectorElement::PseudoClass(pc) => write!(f, ":{}", pc), | |
220 | } | |
221 | } | |
222 | } | |
223 | ||
224 | #[derive(Eq, PartialEq, Clone, Debug, Copy)] | |
225 | pub enum SelectorOperator { | |
226 | /// `~=` | |
227 | OneAttributeEquals, | |
228 | /// `|=` | |
229 | EqualsOrStartsWithFollowedByDash, | |
230 | /// `$=` | |
231 | EndsWith, | |
232 | /// `^=` | |
233 | FirstStartsWith, | |
234 | /// `*=` | |
235 | Contains, | |
236 | } | |
237 | ||
238 | impl fmt::Display for SelectorOperator { | |
923072b8 | 239 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
8faf50e0 | 240 | match *self { |
6a06907d XL |
241 | SelectorOperator::OneAttributeEquals => write!(f, "~="), |
242 | SelectorOperator::EqualsOrStartsWithFollowedByDash => write!(f, "|="), | |
243 | SelectorOperator::EndsWith => write!(f, "$="), | |
244 | SelectorOperator::FirstStartsWith => write!(f, "^="), | |
245 | SelectorOperator::Contains => write!(f, "*="), | |
8faf50e0 XL |
246 | } |
247 | } | |
248 | } | |
249 | ||
250 | #[derive(Eq, PartialEq, Clone, Debug)] | |
251 | pub enum Token<'a> { | |
252 | /// Comment. | |
253 | Comment(&'a str), | |
254 | /// Comment starting with `/**`. | |
255 | License(&'a str), | |
256 | Char(ReservedChar), | |
257 | Other(&'a str), | |
258 | SelectorElement(SelectorElement<'a>), | |
259 | String(&'a str), | |
260 | SelectorOperator(SelectorOperator), | |
0bf4aa26 | 261 | Operator(Operator), |
8faf50e0 XL |
262 | } |
263 | ||
264 | impl<'a> fmt::Display for Token<'a> { | |
923072b8 | 265 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
8faf50e0 XL |
266 | match *self { |
267 | // Token::AtRule(at_rule) => write!(f, "{}", at_rule, content), | |
268 | // Token::ElementRule(selectors) => write!(f, "{}", x), | |
269 | Token::Comment(c) => write!(f, "{}", c), | |
270 | Token::License(l) => writeln!(f, "/*!{}*/", l), | |
271 | Token::Char(c) => write!(f, "{}", c), | |
272 | Token::Other(s) => write!(f, "{}", s), | |
273 | Token::SelectorElement(ref se) => write!(f, "{}", se), | |
274 | Token::String(s) => write!(f, "{}", s), | |
275 | Token::SelectorOperator(so) => write!(f, "{}", so), | |
0bf4aa26 | 276 | Token::Operator(op) => write!(f, "{}", op), |
8faf50e0 XL |
277 | } |
278 | } | |
279 | } | |
280 | ||
281 | impl<'a> Token<'a> { | |
282 | fn is_comment(&self) -> bool { | |
6a06907d | 283 | matches!(*self, Token::Comment(_)) |
8faf50e0 XL |
284 | } |
285 | ||
286 | fn is_char(&self) -> bool { | |
6a06907d | 287 | matches!(*self, Token::Char(_)) |
8faf50e0 XL |
288 | } |
289 | ||
290 | fn get_char(&self) -> Option<ReservedChar> { | |
291 | match *self { | |
292 | Token::Char(c) => Some(c), | |
293 | _ => None, | |
294 | } | |
295 | } | |
296 | ||
297 | fn is_useless(&self) -> bool { | |
298 | match *self { | |
299 | Token::Char(c) => c.is_useless(), | |
300 | _ => false, | |
301 | } | |
302 | } | |
303 | ||
8faf50e0 | 304 | fn is_a_media(&self) -> bool { |
6a06907d | 305 | matches!(*self, Token::SelectorElement(SelectorElement::Media(_))) |
8faf50e0 XL |
306 | } |
307 | ||
308 | fn is_a_license(&self) -> bool { | |
6a06907d | 309 | matches!(*self, Token::License(_)) |
8faf50e0 | 310 | } |
0bf4aa26 XL |
311 | |
312 | fn is_operator(&self) -> bool { | |
313 | match *self { | |
314 | Token::Operator(_) => true, | |
315 | Token::Char(c) => c.is_operator(), | |
316 | _ => false, | |
317 | } | |
318 | } | |
8faf50e0 XL |
319 | } |
320 | ||
321 | impl<'a> PartialEq<ReservedChar> for Token<'a> { | |
322 | fn eq(&self, other: &ReservedChar) -> bool { | |
323 | match *self { | |
324 | Token::Char(c) => c == *other, | |
325 | _ => false, | |
326 | } | |
327 | } | |
328 | } | |
329 | ||
6a06907d XL |
330 | fn get_comment<'a>( |
331 | source: &'a str, | |
923072b8 | 332 | iterator: &mut Peekable<CharIndices<'_>>, |
6a06907d XL |
333 | start_pos: &mut usize, |
334 | ) -> Option<Token<'a>> { | |
8faf50e0 XL |
335 | let mut prev = ReservedChar::Quote; |
336 | *start_pos += 1; | |
337 | let builder = if let Some((_, c)) = iterator.next() { | |
17df50a5 | 338 | if c == '!' || (c == '*' && iterator.peek().map(|(_, c)| c) != Some(&'/')) { |
8faf50e0 XL |
339 | *start_pos += 1; |
340 | Token::License | |
341 | } else { | |
342 | if let Ok(c) = ReservedChar::try_from(c) { | |
343 | prev = c; | |
344 | } | |
345 | Token::Comment | |
346 | } | |
347 | } else { | |
348 | Token::Comment | |
349 | }; | |
350 | ||
6a06907d | 351 | for (pos, c) in iterator { |
8faf50e0 XL |
352 | if let Ok(c) = ReservedChar::try_from(c) { |
353 | if c == ReservedChar::Slash && prev == ReservedChar::Star { | |
354 | let ret = Some(builder(&source[*start_pos..pos - 1])); | |
355 | *start_pos = pos; | |
356 | return ret; | |
357 | } | |
358 | prev = c; | |
359 | } else { | |
360 | prev = ReservedChar::Space; | |
361 | } | |
362 | } | |
363 | None | |
364 | } | |
365 | ||
6a06907d XL |
366 | fn get_string<'a>( |
367 | source: &'a str, | |
923072b8 | 368 | iterator: &mut Peekable<CharIndices<'_>>, |
6a06907d XL |
369 | start_pos: &mut usize, |
370 | start: ReservedChar, | |
371 | ) -> Option<Token<'a>> { | |
8faf50e0 XL |
372 | while let Some((pos, c)) = iterator.next() { |
373 | if c == '\\' { | |
374 | // we skip next character | |
375 | iterator.next(); | |
6a06907d | 376 | continue; |
8faf50e0 XL |
377 | } |
378 | if let Ok(c) = ReservedChar::try_from(c) { | |
379 | if c == start { | |
380 | let ret = Some(Token::String(&source[*start_pos..pos + 1])); | |
381 | *start_pos = pos; | |
382 | return ret; | |
383 | } | |
384 | } | |
385 | } | |
386 | None | |
387 | } | |
388 | ||
6a06907d XL |
389 | fn fill_other<'a>( |
390 | source: &'a str, | |
391 | v: &mut Vec<Token<'a>>, | |
392 | start: usize, | |
393 | pos: usize, | |
394 | is_in_block: isize, | |
395 | is_in_media: bool, | |
396 | is_in_attribute_selector: bool, | |
397 | ) { | |
8faf50e0 | 398 | if start < pos { |
6a06907d XL |
399 | if !is_in_attribute_selector |
400 | && ((is_in_block == 0 && !is_in_media) || (is_in_media && is_in_block == 1)) | |
401 | { | |
8faf50e0 XL |
402 | let mut is_pseudo_class = false; |
403 | let mut add = 0; | |
404 | if let Some(&Token::Char(ReservedChar::Colon)) = v.last() { | |
405 | is_pseudo_class = true; | |
406 | add = 1; | |
407 | } | |
408 | if let Ok(s) = SelectorElement::try_from(&source[start - add..pos]) { | |
409 | if is_pseudo_class { | |
410 | v.pop(); | |
411 | } | |
412 | v.push(Token::SelectorElement(s)); | |
413 | } else { | |
414 | let s = &source[start..pos]; | |
6a06907d XL |
415 | if !s.starts_with(':') |
416 | && !s.starts_with('.') | |
417 | && !s.starts_with('#') | |
418 | && !s.starts_with('@') | |
419 | { | |
b7449926 | 420 | v.push(Token::Other(s)); |
6a06907d | 421 | } |
8faf50e0 XL |
422 | } |
423 | } else { | |
424 | v.push(Token::Other(&source[start..pos])); | |
425 | } | |
426 | } | |
427 | } | |
428 | ||
6a06907d | 429 | #[allow(clippy::comparison_chain)] |
923072b8 | 430 | pub(super) fn tokenize<'a>(source: &'a str) -> Result<Tokens<'a>, &'static str> { |
8faf50e0 XL |
431 | let mut v = Vec::with_capacity(1000); |
432 | let mut iterator = source.char_indices().peekable(); | |
433 | let mut start = 0; | |
434 | let mut is_in_block: isize = 0; | |
435 | let mut is_in_media = false; | |
436 | let mut is_in_attribute_selector = false; | |
437 | ||
438 | loop { | |
439 | let (mut pos, c) = match iterator.next() { | |
440 | Some(x) => x, | |
441 | None => { | |
6a06907d XL |
442 | fill_other( |
443 | source, | |
444 | &mut v, | |
445 | start, | |
446 | source.len(), | |
447 | is_in_block, | |
448 | is_in_media, | |
449 | is_in_attribute_selector, | |
450 | ); | |
451 | break; | |
8faf50e0 XL |
452 | } |
453 | }; | |
454 | if let Ok(c) = ReservedChar::try_from(c) { | |
6a06907d XL |
455 | fill_other( |
456 | source, | |
457 | &mut v, | |
458 | start, | |
459 | pos, | |
460 | is_in_block, | |
461 | is_in_media, | |
462 | is_in_attribute_selector, | |
463 | ); | |
464 | is_in_media = is_in_media | |
465 | || v.last() | |
466 | .unwrap_or(&Token::Char(ReservedChar::Space)) | |
5099ac24 | 467 | .is_a_media(); |
5e7ed085 FG |
468 | match c { |
469 | ReservedChar::Quote | ReservedChar::DoubleQuote => { | |
b7449926 XL |
470 | if let Some(s) = get_string(source, &mut iterator, &mut pos, c) { |
471 | v.push(s); | |
8faf50e0 | 472 | } |
5e7ed085 FG |
473 | } |
474 | ReservedChar::Star | |
475 | if *v.last().unwrap_or(&Token::Char(ReservedChar::Space)) | |
476 | == ReservedChar::Slash => | |
477 | { | |
b7449926 XL |
478 | v.pop(); |
479 | if let Some(s) = get_comment(source, &mut iterator, &mut pos) { | |
480 | v.push(s); | |
481 | } | |
5e7ed085 FG |
482 | } |
483 | ReservedChar::OpenBracket => { | |
b7449926 XL |
484 | if is_in_attribute_selector { |
485 | return Err("Already in attribute selector"); | |
486 | } | |
487 | is_in_attribute_selector = true; | |
488 | v.push(Token::Char(c)); | |
5e7ed085 FG |
489 | } |
490 | ReservedChar::CloseBracket => { | |
b7449926 XL |
491 | if !is_in_attribute_selector { |
492 | return Err("Unexpected ']'"); | |
493 | } | |
494 | is_in_attribute_selector = false; | |
495 | v.push(Token::Char(c)); | |
5e7ed085 FG |
496 | } |
497 | ReservedChar::OpenCurlyBrace => { | |
b7449926 XL |
498 | is_in_block += 1; |
499 | v.push(Token::Char(c)); | |
5e7ed085 FG |
500 | } |
501 | ReservedChar::CloseCurlyBrace => { | |
b7449926 XL |
502 | is_in_block -= 1; |
503 | if is_in_block < 0 { | |
504 | return Err("Too much '}'"); | |
505 | } else if is_in_block == 0 { | |
506 | is_in_media = false; | |
507 | } | |
508 | v.push(Token::Char(c)); | |
5e7ed085 FG |
509 | } |
510 | ReservedChar::SemiColon if is_in_block == 0 => { | |
511 | is_in_media = false; | |
512 | v.push(Token::Char(c)); | |
513 | } | |
514 | ReservedChar::EqualSign => { | |
515 | match match v | |
516 | .last() | |
517 | .unwrap_or(&Token::Char(ReservedChar::Space)) | |
518 | .get_char() | |
519 | .unwrap_or(ReservedChar::Space) | |
520 | { | |
b7449926 | 521 | ReservedChar::Tilde => Some(SelectorOperator::OneAttributeEquals), |
5e7ed085 FG |
522 | ReservedChar::Pipe => { |
523 | Some(SelectorOperator::EqualsOrStartsWithFollowedByDash) | |
524 | } | |
b7449926 XL |
525 | ReservedChar::Dollar => Some(SelectorOperator::EndsWith), |
526 | ReservedChar::Circumflex => Some(SelectorOperator::FirstStartsWith), | |
527 | ReservedChar::Star => Some(SelectorOperator::Contains), | |
528 | _ => None, | |
529 | } { | |
530 | Some(r) => { | |
531 | v.pop(); | |
532 | v.push(Token::SelectorOperator(r)); | |
533 | } | |
534 | None => v.push(Token::Char(c)), | |
535 | } | |
5e7ed085 FG |
536 | } |
537 | c if !c.is_useless() => { | |
b7449926 | 538 | v.push(Token::Char(c)); |
5e7ed085 FG |
539 | } |
540 | c => { | |
541 | if !v | |
542 | .last() | |
543 | .unwrap_or(&Token::Char(ReservedChar::Space)) | |
544 | .is_useless() | |
545 | && (!v | |
546 | .last() | |
547 | .unwrap_or(&Token::Char(ReservedChar::OpenCurlyBrace)) | |
548 | .is_char() | |
549 | || v.last() | |
550 | .unwrap_or(&Token::Char(ReservedChar::OpenCurlyBrace)) | |
551 | .is_operator() | |
552 | || v.last() | |
553 | .unwrap_or(&Token::Char(ReservedChar::OpenCurlyBrace)) | |
554 | .get_char() | |
555 | == Some(ReservedChar::CloseParenthese) | |
556 | || v.last() | |
557 | .unwrap_or(&Token::Char(ReservedChar::OpenCurlyBrace)) | |
558 | .get_char() | |
559 | == Some(ReservedChar::CloseBracket)) | |
560 | { | |
561 | v.push(Token::Char(ReservedChar::Space)); | |
562 | } else if let Ok(op) = Operator::try_from(c) { | |
563 | v.push(Token::Operator(op)); | |
564 | } | |
565 | } | |
8faf50e0 XL |
566 | } |
567 | start = pos + 1; | |
568 | } | |
569 | } | |
570 | Ok(Tokens(clean_tokens(v))) | |
571 | } | |
572 | ||
6a06907d | 573 | fn clean_tokens(mut v: Vec<Token<'_>>) -> Vec<Token<'_>> { |
8faf50e0 | 574 | let mut i = 0; |
0bf4aa26 XL |
575 | let mut is_in_calc = false; |
576 | let mut paren = 0; | |
8faf50e0 XL |
577 | |
578 | while i < v.len() { | |
0bf4aa26 XL |
579 | if v[i] == Token::Other("calc") { |
580 | is_in_calc = true; | |
6a06907d | 581 | } else if is_in_calc { |
0bf4aa26 XL |
582 | if v[i] == Token::Char(ReservedChar::CloseParenthese) { |
583 | paren -= 1; | |
584 | is_in_calc = paren != 0; | |
585 | } else if v[i] == Token::Char(ReservedChar::OpenParenthese) { | |
586 | paren += 1; | |
587 | } | |
588 | } | |
6a06907d | 589 | |
8faf50e0 | 590 | if v[i].is_useless() { |
6a06907d XL |
591 | if i > 0 && v[i - 1] == Token::Char(ReservedChar::CloseBracket) { |
592 | if i + 1 < v.len() | |
593 | && (v[i + 1].is_useless() | |
594 | || v[i + 1] == Token::Char(ReservedChar::OpenCurlyBrace)) | |
595 | { | |
596 | v.remove(i); | |
597 | continue; | |
598 | } | |
5099ac24 FG |
599 | } else if i > 0 |
600 | && (v[i - 1] == Token::Other("and") | |
601 | || v[i - 1] == Token::Other("or") | |
602 | || v[i - 1] == Token::Other("not")) | |
603 | { | |
604 | // retain the space after "and", "or" or "not" | |
6a06907d XL |
605 | } else if (is_in_calc && v[i - 1].is_useless()) |
606 | || !is_in_calc | |
607 | && ((i > 0 | |
608 | && ((v[i - 1].is_char() | |
609 | && v[i - 1] != Token::Char(ReservedChar::CloseParenthese)) | |
610 | || v[i - 1].is_a_media() | |
611 | || v[i - 1].is_a_license())) | |
612 | || (i < v.len() - 1 && v[i + 1].is_char())) | |
613 | { | |
8faf50e0 | 614 | v.remove(i); |
6a06907d | 615 | continue; |
8faf50e0 XL |
616 | } |
617 | } else if v[i].is_comment() { | |
618 | v.remove(i); | |
6a06907d | 619 | continue; |
8faf50e0 XL |
620 | } |
621 | i += 1; | |
622 | } | |
623 | v | |
624 | } | |
625 | ||
626 | #[derive(Debug, PartialEq, Eq, Clone)] | |
923072b8 FG |
627 | pub(super) struct Tokens<'a>(Vec<Token<'a>>); |
628 | ||
629 | impl<'a> Tokens<'a> { | |
630 | pub(super) fn write<W: std::io::Write>(self, mut w: W) -> std::io::Result<()> { | |
631 | for token in self.0.iter() { | |
632 | write!(w, "{}", token)?; | |
633 | } | |
634 | Ok(()) | |
635 | } | |
636 | } | |
8faf50e0 XL |
637 | |
638 | impl<'a> fmt::Display for Tokens<'a> { | |
923072b8 | 639 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
8faf50e0 XL |
640 | for token in self.0.iter() { |
641 | write!(f, "{}", token)?; | |
642 | } | |
643 | Ok(()) | |
644 | } | |
645 | } | |
646 | ||
647 | #[test] | |
648 | fn css_basic() { | |
649 | let s = r#" | |
650 | /*! just some license */ | |
651 | .foo > #bar p:hover { | |
652 | color: blue; | |
653 | background: "blue"; | |
654 | } | |
655 | ||
17df50a5 | 656 | /* a comment! */ |
8faf50e0 XL |
657 | @media screen and (max-width: 640px) { |
658 | .block:hover { | |
659 | display: block; | |
660 | } | |
661 | }"#; | |
6a06907d XL |
662 | let expected = vec![ |
663 | Token::License(" just some license "), | |
664 | Token::SelectorElement(SelectorElement::Class("foo")), | |
665 | Token::Char(ReservedChar::SuperiorThan), | |
666 | Token::SelectorElement(SelectorElement::Id("bar")), | |
667 | Token::Char(ReservedChar::Space), | |
668 | Token::SelectorElement(SelectorElement::Tag("p")), | |
669 | Token::SelectorElement(SelectorElement::PseudoClass("hover")), | |
670 | Token::Char(ReservedChar::OpenCurlyBrace), | |
671 | Token::Other("color"), | |
672 | Token::Char(ReservedChar::Colon), | |
673 | Token::Other("blue"), | |
674 | Token::Char(ReservedChar::SemiColon), | |
675 | Token::Other("background"), | |
676 | Token::Char(ReservedChar::Colon), | |
677 | Token::String("\"blue\""), | |
678 | Token::Char(ReservedChar::SemiColon), | |
679 | Token::Char(ReservedChar::CloseCurlyBrace), | |
680 | Token::SelectorElement(SelectorElement::Media("media")), | |
681 | Token::Other("screen"), | |
682 | Token::Char(ReservedChar::Space), | |
683 | Token::Other("and"), | |
684 | Token::Char(ReservedChar::Space), | |
685 | Token::Char(ReservedChar::OpenParenthese), | |
686 | Token::Other("max-width"), | |
687 | Token::Char(ReservedChar::Colon), | |
688 | Token::Other("640px"), | |
689 | Token::Char(ReservedChar::CloseParenthese), | |
690 | Token::Char(ReservedChar::OpenCurlyBrace), | |
691 | Token::SelectorElement(SelectorElement::Class("block")), | |
692 | Token::SelectorElement(SelectorElement::PseudoClass("hover")), | |
693 | Token::Char(ReservedChar::OpenCurlyBrace), | |
694 | Token::Other("display"), | |
695 | Token::Char(ReservedChar::Colon), | |
696 | Token::Other("block"), | |
697 | Token::Char(ReservedChar::SemiColon), | |
698 | Token::Char(ReservedChar::CloseCurlyBrace), | |
699 | Token::Char(ReservedChar::CloseCurlyBrace), | |
700 | ]; | |
8faf50e0 XL |
701 | assert_eq!(tokenize(s), Ok(Tokens(expected))); |
702 | } | |
703 | ||
704 | #[test] | |
705 | fn elem_selector() { | |
706 | let s = r#" | |
707 | /** just some license */ | |
708 | a[href*="example"] { | |
709 | background: yellow; | |
710 | } | |
711 | a[href$=".org"] { | |
712 | font-style: italic; | |
713 | } | |
714 | span[lang|="zh"] { | |
715 | color: red; | |
716 | } | |
717 | a[href^="/"] { | |
718 | background-color: gold; | |
719 | } | |
720 | div[value~="test"] { | |
721 | border-width: 1px; | |
722 | } | |
723 | span[lang="pt"] { | |
17df50a5 | 724 | font-size: 12em; /* I love big fonts */ |
8faf50e0 XL |
725 | } |
726 | "#; | |
6a06907d XL |
727 | let expected = vec![ |
728 | Token::License(" just some license "), | |
729 | Token::SelectorElement(SelectorElement::Tag("a")), | |
730 | Token::Char(ReservedChar::OpenBracket), | |
731 | Token::Other("href"), | |
732 | Token::SelectorOperator(SelectorOperator::Contains), | |
733 | Token::String("\"example\""), | |
734 | Token::Char(ReservedChar::CloseBracket), | |
735 | Token::Char(ReservedChar::OpenCurlyBrace), | |
736 | Token::Other("background"), | |
737 | Token::Char(ReservedChar::Colon), | |
738 | Token::Other("yellow"), | |
739 | Token::Char(ReservedChar::SemiColon), | |
740 | Token::Char(ReservedChar::CloseCurlyBrace), | |
741 | Token::SelectorElement(SelectorElement::Tag("a")), | |
742 | Token::Char(ReservedChar::OpenBracket), | |
743 | Token::Other("href"), | |
744 | Token::SelectorOperator(SelectorOperator::EndsWith), | |
745 | Token::String("\".org\""), | |
746 | Token::Char(ReservedChar::CloseBracket), | |
747 | Token::Char(ReservedChar::OpenCurlyBrace), | |
748 | Token::Other("font-style"), | |
749 | Token::Char(ReservedChar::Colon), | |
750 | Token::Other("italic"), | |
751 | Token::Char(ReservedChar::SemiColon), | |
752 | Token::Char(ReservedChar::CloseCurlyBrace), | |
753 | Token::SelectorElement(SelectorElement::Tag("span")), | |
754 | Token::Char(ReservedChar::OpenBracket), | |
755 | Token::Other("lang"), | |
756 | Token::SelectorOperator(SelectorOperator::EqualsOrStartsWithFollowedByDash), | |
757 | Token::String("\"zh\""), | |
758 | Token::Char(ReservedChar::CloseBracket), | |
759 | Token::Char(ReservedChar::OpenCurlyBrace), | |
760 | Token::Other("color"), | |
761 | Token::Char(ReservedChar::Colon), | |
762 | Token::Other("red"), | |
763 | Token::Char(ReservedChar::SemiColon), | |
764 | Token::Char(ReservedChar::CloseCurlyBrace), | |
765 | Token::SelectorElement(SelectorElement::Tag("a")), | |
766 | Token::Char(ReservedChar::OpenBracket), | |
767 | Token::Other("href"), | |
768 | Token::SelectorOperator(SelectorOperator::FirstStartsWith), | |
769 | Token::String("\"/\""), | |
770 | Token::Char(ReservedChar::CloseBracket), | |
771 | Token::Char(ReservedChar::OpenCurlyBrace), | |
772 | Token::Other("background-color"), | |
773 | Token::Char(ReservedChar::Colon), | |
774 | Token::Other("gold"), | |
775 | Token::Char(ReservedChar::SemiColon), | |
776 | Token::Char(ReservedChar::CloseCurlyBrace), | |
777 | Token::SelectorElement(SelectorElement::Tag("div")), | |
778 | Token::Char(ReservedChar::OpenBracket), | |
779 | Token::Other("value"), | |
780 | Token::SelectorOperator(SelectorOperator::OneAttributeEquals), | |
781 | Token::String("\"test\""), | |
782 | Token::Char(ReservedChar::CloseBracket), | |
783 | Token::Char(ReservedChar::OpenCurlyBrace), | |
784 | Token::Other("border-width"), | |
785 | Token::Char(ReservedChar::Colon), | |
786 | Token::Other("1px"), | |
787 | Token::Char(ReservedChar::SemiColon), | |
788 | Token::Char(ReservedChar::CloseCurlyBrace), | |
789 | Token::SelectorElement(SelectorElement::Tag("span")), | |
790 | Token::Char(ReservedChar::OpenBracket), | |
791 | Token::Other("lang"), | |
792 | Token::Char(ReservedChar::EqualSign), | |
793 | Token::String("\"pt\""), | |
794 | Token::Char(ReservedChar::CloseBracket), | |
795 | Token::Char(ReservedChar::OpenCurlyBrace), | |
796 | Token::Other("font-size"), | |
797 | Token::Char(ReservedChar::Colon), | |
798 | Token::Other("12em"), | |
799 | Token::Char(ReservedChar::SemiColon), | |
800 | Token::Char(ReservedChar::CloseCurlyBrace), | |
801 | ]; | |
8faf50e0 XL |
802 | assert_eq!(tokenize(s), Ok(Tokens(expected))); |
803 | } | |
804 | ||
805 | #[test] | |
806 | fn check_media() { | |
807 | let s = "@media (max-width: 700px) { color: red; }"; | |
808 | ||
6a06907d XL |
809 | let expected = vec![ |
810 | Token::SelectorElement(SelectorElement::Media("media")), | |
811 | Token::Char(ReservedChar::OpenParenthese), | |
812 | Token::Other("max-width"), | |
813 | Token::Char(ReservedChar::Colon), | |
814 | Token::Other("700px"), | |
815 | Token::Char(ReservedChar::CloseParenthese), | |
816 | Token::Char(ReservedChar::OpenCurlyBrace), | |
817 | Token::SelectorElement(SelectorElement::Tag("color")), | |
818 | Token::Char(ReservedChar::Colon), | |
819 | Token::Other("red"), | |
820 | Token::Char(ReservedChar::SemiColon), | |
821 | Token::Char(ReservedChar::CloseCurlyBrace), | |
822 | ]; | |
b7449926 XL |
823 | |
824 | assert_eq!(tokenize(s), Ok(Tokens(expected))); | |
825 | } | |
826 | ||
5099ac24 FG |
827 | #[test] |
828 | fn check_supports() { | |
829 | let s = "@supports not (display: grid) { div { float: right; } }"; | |
830 | ||
831 | let expected = vec![ | |
832 | Token::SelectorElement(SelectorElement::Media("supports")), | |
833 | Token::Other("not"), | |
834 | Token::Char(ReservedChar::Space), | |
835 | Token::Char(ReservedChar::OpenParenthese), | |
836 | Token::Other("display"), | |
837 | Token::Char(ReservedChar::Colon), | |
838 | Token::Other("grid"), | |
839 | Token::Char(ReservedChar::CloseParenthese), | |
840 | Token::Char(ReservedChar::OpenCurlyBrace), | |
841 | Token::SelectorElement(SelectorElement::Tag("div")), | |
842 | Token::Char(ReservedChar::OpenCurlyBrace), | |
843 | Token::Other("float"), | |
844 | Token::Char(ReservedChar::Colon), | |
845 | Token::Other("right"), | |
846 | Token::Char(ReservedChar::SemiColon), | |
847 | Token::Char(ReservedChar::CloseCurlyBrace), | |
848 | Token::Char(ReservedChar::CloseCurlyBrace), | |
849 | ]; | |
850 | ||
851 | assert_eq!(tokenize(s), Ok(Tokens(expected))); | |
852 | } | |
853 | ||
b7449926 XL |
854 | #[test] |
855 | fn check_calc() { | |
856 | let s = ".foo { width: calc(100% - 34px); }"; | |
857 | ||
6a06907d XL |
858 | let expected = vec![ |
859 | Token::SelectorElement(SelectorElement::Class("foo")), | |
860 | Token::Char(ReservedChar::OpenCurlyBrace), | |
861 | Token::Other("width"), | |
862 | Token::Char(ReservedChar::Colon), | |
863 | Token::Other("calc"), | |
864 | Token::Char(ReservedChar::OpenParenthese), | |
865 | Token::Other("100%"), | |
866 | Token::Char(ReservedChar::Space), | |
867 | Token::Other("-"), | |
868 | Token::Char(ReservedChar::Space), | |
869 | Token::Other("34px"), | |
870 | Token::Char(ReservedChar::CloseParenthese), | |
871 | Token::Char(ReservedChar::SemiColon), | |
872 | Token::Char(ReservedChar::CloseCurlyBrace), | |
873 | ]; | |
8faf50e0 XL |
874 | assert_eq!(tokenize(s), Ok(Tokens(expected))); |
875 | } |