1 | // Take a look at the license at the top of the repository in the LICENSE file. |
2 | |
3 | use std::convert::TryFrom; |
4 | use std::fmt; |
5 | use std::iter::Peekable; |
6 | use std::str::CharIndices; |
7 | |
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 { |
36 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
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 | ) |
66 | } |
67 | } |
68 | |
69 | impl TryFrom<char> for ReservedChar { |
70 | type Error = &'static str; |
71 | |
72 | fn try_from(value: char) -> Result<ReservedChar, Self::Error> { |
73 | match value { |
74 | ' \'' => Ok(ReservedChar::Quote), |
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), |
90 | ' \t' => Ok(ReservedChar::Tab), |
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" ), |
98 | } |
99 | } |
100 | } |
101 | |
102 | impl ReservedChar { |
103 | fn is_useless(&self) -> bool { |
104 | *self == ReservedChar::Space |
105 | || *self == ReservedChar::Tab |
106 | || *self == ReservedChar::Backline |
107 | } |
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 { |
124 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
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 | ) |
136 | } |
137 | } |
138 | |
139 | impl TryFrom<char> for Operator { |
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), |
149 | _ => Err("Unknown operator" ), |
150 | } |
151 | } |
152 | } |
153 | |
154 | impl TryFrom<ReservedChar> for Operator { |
155 | type Error = &'static str; |
156 | |
157 | fn try_from(value: ReservedChar) -> Result<Operator, Self::Error> { |
158 | match value { |
159 | ReservedChar::Slash => Ok(Operator::Divide), |
160 | ReservedChar::Star => Ok(Operator::Multiply), |
161 | ReservedChar::Plus => Ok(Operator::Plus), |
162 | _ => Err("Unknown operator" ), |
163 | } |
164 | } |
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 | |
176 | impl<'a> TryFrom<&'a str> for SelectorElement<'a> { |
177 | type Error = &'static str; |
178 | |
179 | fn try_from(value: &'a str) -> Result<SelectorElement<'_>, Self::Error> { |
180 | if let Some(value) = value.strip_prefix('.' ) { |
181 | if value.is_empty() { |
182 | Err("cannot determine selector" ) |
183 | } else { |
184 | Ok(SelectorElement::Class(value)) |
185 | } |
186 | } else if let Some(value) = value.strip_prefix('#' ) { |
187 | if value.is_empty() { |
188 | Err("cannot determine selector" ) |
189 | } else { |
190 | Ok(SelectorElement::Id(value)) |
191 | } |
192 | } else if let Some(value) = value.strip_prefix('@' ) { |
193 | if value.is_empty() { |
194 | Err("cannot determine selector" ) |
195 | } else { |
196 | Ok(SelectorElement::Media(value)) |
197 | } |
198 | } else if let Some(value) = value.strip_prefix(':' ) { |
199 | if value.is_empty() { |
200 | Err("cannot determine selector" ) |
201 | } else { |
202 | Ok(SelectorElement::PseudoClass(value)) |
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> { |
213 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
214 | match *self { |
215 | SelectorElement::Class(c: &str) => write!(f, ". {}" , c), |
216 | SelectorElement::Id(i: &str) => write!(f, "# {}" , i), |
217 | SelectorElement::Tag(t: &str) => write!(f, " {}" , t), |
218 | SelectorElement::Media(m: &str) => write!(f, "@ {} " , m), |
219 | SelectorElement::PseudoClass(pc: &str) => 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 { |
239 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
240 | match *self { |
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, "*=" ), |
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), |
261 | Operator(Operator), |
262 | } |
263 | |
264 | impl<'a> fmt::Display for Token<'a> { |
265 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
266 | match *self { |
267 | // Token::AtRule(at_rule) => write!(f, "{}", at_rule, content), |
268 | // Token::ElementRule(selectors) => write!(f, "{}", x), |
269 | Token::Comment(c: &str) => write!(f, " {}" , c), |
270 | Token::License(l: &str) => writeln!(f, "/*! {}*/" , l), |
271 | Token::Char(c: ReservedChar) => write!(f, " {}" , c), |
272 | Token::Other(s: &str) => write!(f, " {}" , s), |
273 | Token::SelectorElement(ref se: &SelectorElement<'_>) => write!(f, " {}" , se), |
274 | Token::String(s: &str) => write!(f, " {}" , s), |
275 | Token::SelectorOperator(so: SelectorOperator) => write!(f, " {}" , so), |
276 | Token::Operator(op: Operator) => write!(f, " {}" , op), |
277 | } |
278 | } |
279 | } |
280 | |
281 | impl<'a> Token<'a> { |
282 | fn is_comment(&self) -> bool { |
283 | matches!(*self, Token::Comment(_)) |
284 | } |
285 | |
286 | fn is_char(&self) -> bool { |
287 | matches!(*self, Token::Char(_)) |
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 | |
304 | fn is_a_media(&self) -> bool { |
305 | matches!(*self, Token::SelectorElement(SelectorElement::Media(_))) |
306 | } |
307 | |
308 | fn is_a_license(&self) -> bool { |
309 | matches!(*self, Token::License(_)) |
310 | } |
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 | } |
319 | } |
320 | |
321 | impl<'a> PartialEq<ReservedChar> for Token<'a> { |
322 | fn eq(&self, other: &ReservedChar) -> bool { |
323 | match *self { |
324 | Token::Char(c: ReservedChar) => c == *other, |
325 | _ => false, |
326 | } |
327 | } |
328 | } |
329 | |
330 | fn get_comment<'a>( |
331 | source: &'a str, |
332 | iterator: &mut Peekable<CharIndices<'_>>, |
333 | start_pos: &mut usize, |
334 | ) -> Option<Token<'a>> { |
335 | let mut prev = ReservedChar::Quote; |
336 | *start_pos += 1; |
337 | let builder = if let Some((_, c)) = iterator.next() { |
338 | if c == '!' || (c == '*' && iterator.peek().map(|(_, c)| c) != Some(&'/' )) { |
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 | |
351 | for (pos, c) in iterator { |
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 | |
366 | fn get_string<'a>( |
367 | source: &'a str, |
368 | iterator: &mut Peekable<CharIndices<'_>>, |
369 | start_pos: &mut usize, |
370 | start: ReservedChar, |
371 | ) -> Option<Token<'a>> { |
372 | while let Some((pos: usize, c: char)) = iterator.next() { |
373 | if c == ' \\' { |
374 | // we skip next character |
375 | iterator.next(); |
376 | continue; |
377 | } |
378 | if let Ok(c: ReservedChar) = ReservedChar::try_from(c) { |
379 | if c == start { |
380 | let ret: Option> = Some(Token::String(&source[*start_pos..pos + 1])); |
381 | *start_pos = pos; |
382 | return ret; |
383 | } |
384 | } |
385 | } |
386 | None |
387 | } |
388 | |
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 | ) { |
398 | if start < pos { |
399 | if !is_in_attribute_selector |
400 | && ((is_in_block == 0 && !is_in_media) || (is_in_media && is_in_block == 1)) |
401 | { |
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]; |
415 | if !s.starts_with(':' ) |
416 | && !s.starts_with('.' ) |
417 | && !s.starts_with('#' ) |
418 | && !s.starts_with('@' ) |
419 | { |
420 | v.push(Token::Other(s)); |
421 | } |
422 | } |
423 | } else { |
424 | v.push(Token::Other(&source[start..pos])); |
425 | } |
426 | } |
427 | } |
428 | |
429 | #[allow (clippy::comparison_chain)] |
430 | pub(super) fn tokenize(source: &str) -> Result<Tokens<'_>, &'static str> { |
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 => { |
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; |
452 | } |
453 | }; |
454 | if let Ok(c) = ReservedChar::try_from(c) { |
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)) |
467 | .is_a_media(); |
468 | match c { |
469 | ReservedChar::Quote | ReservedChar::DoubleQuote => { |
470 | if let Some(s) = get_string(source, &mut iterator, &mut pos, c) { |
471 | v.push(s); |
472 | } |
473 | } |
474 | ReservedChar::Star |
475 | if *v.last().unwrap_or(&Token::Char(ReservedChar::Space)) |
476 | == ReservedChar::Slash => |
477 | { |
478 | v.pop(); |
479 | if let Some(s) = get_comment(source, &mut iterator, &mut pos) { |
480 | v.push(s); |
481 | } |
482 | } |
483 | ReservedChar::OpenBracket => { |
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)); |
489 | } |
490 | ReservedChar::CloseBracket => { |
491 | if !is_in_attribute_selector { |
492 | return Err("Unexpected ']'" ); |
493 | } |
494 | is_in_attribute_selector = false; |
495 | v.push(Token::Char(c)); |
496 | } |
497 | ReservedChar::OpenCurlyBrace => { |
498 | is_in_block += 1; |
499 | v.push(Token::Char(c)); |
500 | } |
501 | ReservedChar::CloseCurlyBrace => { |
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)); |
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 | { |
521 | ReservedChar::Tilde => Some(SelectorOperator::OneAttributeEquals), |
522 | ReservedChar::Pipe => { |
523 | Some(SelectorOperator::EqualsOrStartsWithFollowedByDash) |
524 | } |
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 | } |
536 | } |
537 | c if !c.is_useless() => { |
538 | v.push(Token::Char(c)); |
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 | } |
566 | } |
567 | start = pos + 1; |
568 | } |
569 | } |
570 | Ok(Tokens(clean_tokens(v))) |
571 | } |
572 | |
573 | fn clean_tokens(mut v: Vec<Token<'_>>) -> Vec<Token<'_>> { |
574 | // This function may remove multiple elements from the vector. Ideally we'd |
575 | // use `Vec::retain`, but the traversal requires inspecting the previously |
576 | // retained token and the next token, which `Vec::retain` doesn't allow. So |
577 | // we have to use a lower-level mechanism. |
578 | let mut i = 0; |
579 | // Index of the previous retained token, if there is one. |
580 | let mut ip: Option<usize> = None; |
581 | let mut is_in_calc = false; |
582 | let mut is_in_container = false; |
583 | let mut paren = 0; |
584 | // A vector of bools indicating which elements are to be retained. |
585 | let mut b = Vec::with_capacity(v.len()); |
586 | |
587 | while i < v.len() { |
588 | if v[i] == Token::Other("calc" ) { |
589 | is_in_calc = true; |
590 | } else if is_in_calc { |
591 | if v[i] == Token::Char(ReservedChar::CloseParenthese) { |
592 | paren -= 1; |
593 | is_in_calc = paren != 0; |
594 | } else if v[i] == Token::Char(ReservedChar::OpenParenthese) { |
595 | paren += 1; |
596 | } |
597 | } |
598 | if v[i] == Token::SelectorElement(SelectorElement::Media("container" )) { |
599 | is_in_container = true; |
600 | } |
601 | |
602 | let mut retain = true; |
603 | if v[i].is_useless() { |
604 | #[allow (clippy::if_same_then_else)] |
605 | if ip.is_some() && v[ip.unwrap()] == Token::Char(ReservedChar::CloseBracket) { |
606 | if i + 1 < v.len() |
607 | && (v[i + 1].is_useless() |
608 | || v[i + 1] == Token::Char(ReservedChar::OpenCurlyBrace)) |
609 | { |
610 | retain = false; |
611 | } |
612 | } else if ip.is_some() && matches!(v[ip.unwrap()], Token::Other("and" | "or" | "not" )) { |
613 | // retain the space after "and", "or" or "not" |
614 | } else if is_in_calc && v[ip.unwrap()].is_useless() { |
615 | retain = false; |
616 | } else if is_in_container && matches!(v[ip.unwrap()], Token::Other(_)) { |
617 | // retain spaces between keywords in container queryes |
618 | } else if !is_in_calc |
619 | && ((ip.is_some() && { |
620 | let prev = &v[ip.unwrap()]; |
621 | (prev.is_char() && prev != &Token::Char(ReservedChar::CloseParenthese)) |
622 | || prev.is_a_media() |
623 | || prev.is_a_license() |
624 | }) || (i < v.len() - 1 && v[i + 1].is_char())) |
625 | { |
626 | retain = false; |
627 | } |
628 | } else if v[i].is_comment() { |
629 | retain = false; |
630 | } |
631 | if retain { |
632 | ip = Some(i); |
633 | } |
634 | b.push(retain); |
635 | i += 1; |
636 | } |
637 | assert_eq!(v.len(), b.len()); |
638 | let mut b = b.into_iter(); |
639 | v.retain(|_| b.next().unwrap()); |
640 | v |
641 | } |
642 | |
643 | #[derive (Debug, PartialEq, Eq, Clone)] |
644 | pub(super) struct Tokens<'a>(Vec<Token<'a>>); |
645 | |
646 | impl<'a> Tokens<'a> { |
647 | pub(super) fn write<W: std::io::Write>(self, mut w: W) -> std::io::Result<()> { |
648 | for token: &Token<'_> in self.0.iter() { |
649 | write!(w, " {}" , token)?; |
650 | } |
651 | Ok(()) |
652 | } |
653 | } |
654 | |
655 | impl<'a> fmt::Display for Tokens<'a> { |
656 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
657 | for token: &Token<'_> in self.0.iter() { |
658 | write!(f, " {}" , token)?; |
659 | } |
660 | Ok(()) |
661 | } |
662 | } |
663 | |
664 | #[test ] |
665 | fn css_basic() { |
666 | let s = r#" |
667 | /*! just some license */ |
668 | .foo > #bar p:hover { |
669 | color: blue; |
670 | background: "blue"; |
671 | } |
672 | |
673 | /* a comment! */ |
674 | @media screen and (max-width: 640px) { |
675 | .block:hover { |
676 | display: block; |
677 | } |
678 | }"# ; |
679 | let expected = vec![ |
680 | Token::License(" just some license " ), |
681 | Token::SelectorElement(SelectorElement::Class("foo" )), |
682 | Token::Char(ReservedChar::SuperiorThan), |
683 | Token::SelectorElement(SelectorElement::Id("bar" )), |
684 | Token::Char(ReservedChar::Space), |
685 | Token::SelectorElement(SelectorElement::Tag("p" )), |
686 | Token::SelectorElement(SelectorElement::PseudoClass("hover" )), |
687 | Token::Char(ReservedChar::OpenCurlyBrace), |
688 | Token::Other("color" ), |
689 | Token::Char(ReservedChar::Colon), |
690 | Token::Other("blue" ), |
691 | Token::Char(ReservedChar::SemiColon), |
692 | Token::Other("background" ), |
693 | Token::Char(ReservedChar::Colon), |
694 | Token::String(" \"blue \"" ), |
695 | Token::Char(ReservedChar::SemiColon), |
696 | Token::Char(ReservedChar::CloseCurlyBrace), |
697 | Token::SelectorElement(SelectorElement::Media("media" )), |
698 | Token::Other("screen" ), |
699 | Token::Char(ReservedChar::Space), |
700 | Token::Other("and" ), |
701 | Token::Char(ReservedChar::Space), |
702 | Token::Char(ReservedChar::OpenParenthese), |
703 | Token::Other("max-width" ), |
704 | Token::Char(ReservedChar::Colon), |
705 | Token::Other("640px" ), |
706 | Token::Char(ReservedChar::CloseParenthese), |
707 | Token::Char(ReservedChar::OpenCurlyBrace), |
708 | Token::SelectorElement(SelectorElement::Class("block" )), |
709 | Token::SelectorElement(SelectorElement::PseudoClass("hover" )), |
710 | Token::Char(ReservedChar::OpenCurlyBrace), |
711 | Token::Other("display" ), |
712 | Token::Char(ReservedChar::Colon), |
713 | Token::Other("block" ), |
714 | Token::Char(ReservedChar::SemiColon), |
715 | Token::Char(ReservedChar::CloseCurlyBrace), |
716 | Token::Char(ReservedChar::CloseCurlyBrace), |
717 | ]; |
718 | assert_eq!(tokenize(s), Ok(Tokens(expected))); |
719 | } |
720 | |
721 | #[test ] |
722 | fn elem_selector() { |
723 | let s = r#" |
724 | /** just some license */ |
725 | a[href*="example"] { |
726 | background: yellow; |
727 | } |
728 | a[href$=".org"] { |
729 | font-style: italic; |
730 | } |
731 | span[lang|="zh"] { |
732 | color: red; |
733 | } |
734 | a[href^="/"] { |
735 | background-color: gold; |
736 | } |
737 | div[value~="test"] { |
738 | border-width: 1px; |
739 | } |
740 | span[lang="pt"] { |
741 | font-size: 12em; /* I love big fonts */ |
742 | } |
743 | "# ; |
744 | let expected = vec![ |
745 | Token::License(" just some license " ), |
746 | Token::SelectorElement(SelectorElement::Tag("a" )), |
747 | Token::Char(ReservedChar::OpenBracket), |
748 | Token::Other("href" ), |
749 | Token::SelectorOperator(SelectorOperator::Contains), |
750 | Token::String(" \"example \"" ), |
751 | Token::Char(ReservedChar::CloseBracket), |
752 | Token::Char(ReservedChar::OpenCurlyBrace), |
753 | Token::Other("background" ), |
754 | Token::Char(ReservedChar::Colon), |
755 | Token::Other("yellow" ), |
756 | Token::Char(ReservedChar::SemiColon), |
757 | Token::Char(ReservedChar::CloseCurlyBrace), |
758 | Token::SelectorElement(SelectorElement::Tag("a" )), |
759 | Token::Char(ReservedChar::OpenBracket), |
760 | Token::Other("href" ), |
761 | Token::SelectorOperator(SelectorOperator::EndsWith), |
762 | Token::String(" \".org \"" ), |
763 | Token::Char(ReservedChar::CloseBracket), |
764 | Token::Char(ReservedChar::OpenCurlyBrace), |
765 | Token::Other("font-style" ), |
766 | Token::Char(ReservedChar::Colon), |
767 | Token::Other("italic" ), |
768 | Token::Char(ReservedChar::SemiColon), |
769 | Token::Char(ReservedChar::CloseCurlyBrace), |
770 | Token::SelectorElement(SelectorElement::Tag("span" )), |
771 | Token::Char(ReservedChar::OpenBracket), |
772 | Token::Other("lang" ), |
773 | Token::SelectorOperator(SelectorOperator::EqualsOrStartsWithFollowedByDash), |
774 | Token::String(" \"zh \"" ), |
775 | Token::Char(ReservedChar::CloseBracket), |
776 | Token::Char(ReservedChar::OpenCurlyBrace), |
777 | Token::Other("color" ), |
778 | Token::Char(ReservedChar::Colon), |
779 | Token::Other("red" ), |
780 | Token::Char(ReservedChar::SemiColon), |
781 | Token::Char(ReservedChar::CloseCurlyBrace), |
782 | Token::SelectorElement(SelectorElement::Tag("a" )), |
783 | Token::Char(ReservedChar::OpenBracket), |
784 | Token::Other("href" ), |
785 | Token::SelectorOperator(SelectorOperator::FirstStartsWith), |
786 | Token::String(" \"/ \"" ), |
787 | Token::Char(ReservedChar::CloseBracket), |
788 | Token::Char(ReservedChar::OpenCurlyBrace), |
789 | Token::Other("background-color" ), |
790 | Token::Char(ReservedChar::Colon), |
791 | Token::Other("gold" ), |
792 | Token::Char(ReservedChar::SemiColon), |
793 | Token::Char(ReservedChar::CloseCurlyBrace), |
794 | Token::SelectorElement(SelectorElement::Tag("div" )), |
795 | Token::Char(ReservedChar::OpenBracket), |
796 | Token::Other("value" ), |
797 | Token::SelectorOperator(SelectorOperator::OneAttributeEquals), |
798 | Token::String(" \"test \"" ), |
799 | Token::Char(ReservedChar::CloseBracket), |
800 | Token::Char(ReservedChar::OpenCurlyBrace), |
801 | Token::Other("border-width" ), |
802 | Token::Char(ReservedChar::Colon), |
803 | Token::Other("1px" ), |
804 | Token::Char(ReservedChar::SemiColon), |
805 | Token::Char(ReservedChar::CloseCurlyBrace), |
806 | Token::SelectorElement(SelectorElement::Tag("span" )), |
807 | Token::Char(ReservedChar::OpenBracket), |
808 | Token::Other("lang" ), |
809 | Token::Char(ReservedChar::EqualSign), |
810 | Token::String(" \"pt \"" ), |
811 | Token::Char(ReservedChar::CloseBracket), |
812 | Token::Char(ReservedChar::OpenCurlyBrace), |
813 | Token::Other("font-size" ), |
814 | Token::Char(ReservedChar::Colon), |
815 | Token::Other("12em" ), |
816 | Token::Char(ReservedChar::SemiColon), |
817 | Token::Char(ReservedChar::CloseCurlyBrace), |
818 | ]; |
819 | assert_eq!(tokenize(s), Ok(Tokens(expected))); |
820 | } |
821 | |
822 | #[test ] |
823 | fn check_media() { |
824 | let s: &str = "@media (max-width: 700px) { color: red; }" ; |
825 | |
826 | let expected: Vec> = vec![ |
827 | Token::SelectorElement(SelectorElement::Media("media" )), |
828 | Token::Char(ReservedChar::OpenParenthese), |
829 | Token::Other("max-width" ), |
830 | Token::Char(ReservedChar::Colon), |
831 | Token::Other("700px" ), |
832 | Token::Char(ReservedChar::CloseParenthese), |
833 | Token::Char(ReservedChar::OpenCurlyBrace), |
834 | Token::SelectorElement(SelectorElement::Tag("color" )), |
835 | Token::Char(ReservedChar::Colon), |
836 | Token::Other("red" ), |
837 | Token::Char(ReservedChar::SemiColon), |
838 | Token::Char(ReservedChar::CloseCurlyBrace), |
839 | ]; |
840 | |
841 | assert_eq!(tokenize(s), Ok(Tokens(expected))); |
842 | } |
843 | |
844 | #[test ] |
845 | fn check_supports() { |
846 | let s = "@supports not (display: grid) { div { float: right; } }" ; |
847 | |
848 | let expected = vec![ |
849 | Token::SelectorElement(SelectorElement::Media("supports" )), |
850 | Token::Other("not" ), |
851 | Token::Char(ReservedChar::Space), |
852 | Token::Char(ReservedChar::OpenParenthese), |
853 | Token::Other("display" ), |
854 | Token::Char(ReservedChar::Colon), |
855 | Token::Other("grid" ), |
856 | Token::Char(ReservedChar::CloseParenthese), |
857 | Token::Char(ReservedChar::OpenCurlyBrace), |
858 | Token::SelectorElement(SelectorElement::Tag("div" )), |
859 | Token::Char(ReservedChar::OpenCurlyBrace), |
860 | Token::Other("float" ), |
861 | Token::Char(ReservedChar::Colon), |
862 | Token::Other("right" ), |
863 | Token::Char(ReservedChar::SemiColon), |
864 | Token::Char(ReservedChar::CloseCurlyBrace), |
865 | Token::Char(ReservedChar::CloseCurlyBrace), |
866 | ]; |
867 | |
868 | assert_eq!(tokenize(s), Ok(Tokens(expected))); |
869 | } |
870 | |
871 | #[test ] |
872 | fn check_calc() { |
873 | let s: &str = ".foo { width: calc(100% - 34px); }" ; |
874 | |
875 | let expected: Vec> = vec![ |
876 | Token::SelectorElement(SelectorElement::Class("foo" )), |
877 | Token::Char(ReservedChar::OpenCurlyBrace), |
878 | Token::Other("width" ), |
879 | Token::Char(ReservedChar::Colon), |
880 | Token::Other("calc" ), |
881 | Token::Char(ReservedChar::OpenParenthese), |
882 | Token::Other("100%" ), |
883 | Token::Char(ReservedChar::Space), |
884 | Token::Other("-" ), |
885 | Token::Char(ReservedChar::Space), |
886 | Token::Other("34px" ), |
887 | Token::Char(ReservedChar::CloseParenthese), |
888 | Token::Char(ReservedChar::SemiColon), |
889 | Token::Char(ReservedChar::CloseCurlyBrace), |
890 | ]; |
891 | assert_eq!(tokenize(s), Ok(Tokens(expected))); |
892 | } |
893 | |