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