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