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