1// Take a look at the license at the top of the repository in the LICENSE file.
2
3use std::convert::TryFrom;
4use std::fmt;
5use std::iter::Peekable;
6use std::str::CharIndices;
7
8#[derive(Debug, PartialEq, Eq, Clone, Copy)]
9pub 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
36impl 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
71impl 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
105impl 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)]
118pub enum Operator {
119 Plus,
120 Multiply,
121 Minus,
122 Modulo,
123 Divide,
124}
125
126impl 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
142impl 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
157impl 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)]
171pub 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
179impl<'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
215impl 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)]
228pub enum SelectorOperator {
229 /// `~=`
230 OneAttributeEquals,
231 /// `|=`
232 EqualsOrStartsWithFollowedByDash,
233 /// `$=`
234 EndsWith,
235 /// `^=`
236 FirstStartsWith,
237 /// `*=`
238 Contains,
239}
240
241impl 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)]
254pub 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
267impl 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
284impl 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
328impl 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
337fn 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
374fn 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
397fn 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)]
432pub(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
582fn 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)]
684pub(super) struct Tokens<'a>(Vec<Token<'a>>);
685
686impl 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
695impl 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]
705fn 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]
762fn elem_selector() {
763 let s = r#"
764/** just some license */
765a[href*="example"] {
766 background: yellow;
767}
768a[href$=".org"] {
769 font-style: italic;
770}
771span[lang|="zh"] {
772 color: red;
773}
774a[href^="/"] {
775 background-color: gold;
776}
777div[value~="test"] {
778 border-width: 1px;
779}
780span[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]
863fn 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]
885fn 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]
912fn 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]
935fn 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