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}
34
35impl 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
69impl 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
102impl 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)]
115pub enum Operator {
116 Plus,
117 Multiply,
118 Minus,
119 Modulo,
120 Divide,
121}
122
123impl 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
139impl 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
154impl 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)]
168pub 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
176impl<'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
212impl<'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)]
225pub enum SelectorOperator {
226 /// `~=`
227 OneAttributeEquals,
228 /// `|=`
229 EqualsOrStartsWithFollowedByDash,
230 /// `$=`
231 EndsWith,
232 /// `^=`
233 FirstStartsWith,
234 /// `*=`
235 Contains,
236}
237
238impl 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)]
251pub 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
264impl<'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
281impl<'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
321impl<'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
330fn 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
366fn 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
389fn 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)]
430pub(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
573fn 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)]
644pub(super) struct Tokens<'a>(Vec<Token<'a>>);
645
646impl<'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
655impl<'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]
665fn 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]
722fn elem_selector() {
723 let s = r#"
724/** just some license */
725a[href*="example"] {
726 background: yellow;
727}
728a[href$=".org"] {
729 font-style: italic;
730}
731span[lang|="zh"] {
732 color: red;
733}
734a[href^="/"] {
735 background-color: gold;
736}
737div[value~="test"] {
738 border-width: 1px;
739}
740span[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]
823fn 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]
845fn 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]
872fn 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