3 // Copyright (c) 2018 Guillaume Gomez
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:
12 // The above copyright notice and this permission notice shall be included in all
13 // copies or substantial portions of the Software.
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
23 use std
::convert
::TryFrom
;
25 use std
::iter
::Peekable
;
26 use std
::str::CharIndices
;
28 #[derive(Debug, PartialEq, Eq, Clone, Copy)]
29 pub enum ReservedChar
{
55 impl fmt
::Display
for ReservedChar
{
56 fn fmt(&self, f
: &mut fmt
::Formatter
) -> fmt
::Result
{
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 => '^',
89 impl TryFrom<char> for ReservedChar {
90 type Error = &'static str;
92 fn try_from(value: char) -> Result<ReservedChar, Self::Error> {
94 '\'' => Ok(ReservedChar::Quote),
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
),
110 '
\t'
=> Ok(ReservedChar
::Tab
),
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"),
123 fn is_useless(&self) -> bool
{
124 *self == ReservedChar
::Space
125 || *self == ReservedChar
::Tab
126 || *self == ReservedChar
::Backline
129 fn is_operator(&self) -> bool
{
130 Operator
::try_from(*self).is_ok()
134 #[derive(Debug, PartialEq, Eq, Clone, Copy)]
143 impl fmt
::Display
for Operator
{
144 fn fmt(&self, f
: &mut fmt
::Formatter
) -> fmt
::Result
{
149 Operator
::Plus
=> '
+'
,
150 Operator
::Multiply
=> '
*'
,
151 Operator
::Minus
=> '
-'
,
152 Operator
::Modulo
=> '
%'
,
153 Operator
::Divide
=> '
/'
,
159 impl TryFrom
<char> for Operator
{
160 type Error
= &'
static str;
162 fn try_from(value
: char) -> Result
<Operator
, Self::Error
> {
164 '
+'
=> Ok(Operator
::Plus
),
165 '
*'
=> Ok(Operator
::Multiply
),
166 '
-'
=> Ok(Operator
::Minus
),
167 '
%'
=> Ok(Operator
::Modulo
),
168 '
/'
=> Ok(Operator
::Divide
),
169 _
=> Err("Unknown operator"),
174 impl TryFrom
<ReservedChar
> for Operator
{
175 type Error
= &'
static str;
177 fn try_from(value
: ReservedChar
) -> Result
<Operator
, Self::Error
> {
179 ReservedChar
::Slash
=> Ok(Operator
::Divide
),
180 ReservedChar
::Star
=> Ok(Operator
::Multiply
),
181 ReservedChar
::Plus
=> Ok(Operator
::Plus
),
182 _
=> Err("Unknown operator"),
187 #[derive(Eq, PartialEq, Clone, Debug)]
188 pub enum SelectorElement
<'a
> {
189 PseudoClass(&'a
str),
196 impl<'a
> TryFrom
<&'a
str> for SelectorElement
<'a
> {
197 type Error
= &'
static str;
199 fn try_from(value
: &'a
str) -> Result
<SelectorElement
, Self::Error
> {
200 if let Some(value
) = value
.strip_prefix('
.'
) {
201 if value
.is_empty() {
202 Err("cannot determine selector")
204 Ok(SelectorElement
::Class(value
))
206 } else if let Some(value
) = value
.strip_prefix('
#') {
207 if value
.is_empty() {
208 Err("cannot determine selector")
210 Ok(SelectorElement
::Id(value
))
212 } else if let Some(value
) = value
.strip_prefix('@'
) {
213 if value
.is_empty() {
214 Err("cannot determine selector")
216 Ok(SelectorElement
::Media(value
))
218 } else if let Some(value
) = value
.strip_prefix('
:'
) {
219 if value
.is_empty() {
220 Err("cannot determine selector")
222 Ok(SelectorElement
::PseudoClass(value
))
224 } else if value
.chars().next().unwrap_or(' '
).is_alphabetic() {
225 Ok(SelectorElement
::Tag(value
))
227 Err("unknown selector")
232 impl<'a
> fmt
::Display
for SelectorElement
<'a
> {
233 fn fmt(&self, f
: &mut fmt
::Formatter
) -> fmt
::Result
{
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
),
244 #[derive(Eq, PartialEq, Clone, Debug, Copy)]
245 pub enum SelectorOperator
{
249 EqualsOrStartsWithFollowedByDash
,
258 impl fmt
::Display
for SelectorOperator
{
259 fn fmt(&self, f
: &mut fmt
::Formatter
) -> fmt
::Result
{
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
, "*="),
270 #[derive(Eq, PartialEq, Clone, Debug)]
274 /// Comment starting with `/**`.
278 SelectorElement(SelectorElement
<'a
>),
280 SelectorOperator(SelectorOperator
),
284 impl<'a
> fmt
::Display
for Token
<'a
> {
285 fn fmt(&self, f
: &mut fmt
::Formatter
) -> fmt
::Result
{
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
),
296 Token
::Operator(op
) => write
!(f
, "{}", op
),
302 fn is_comment(&self) -> bool
{
303 matches
!(*self, Token
::Comment(_
))
306 fn is_char(&self) -> bool
{
307 matches
!(*self, Token
::Char(_
))
310 fn get_char(&self) -> Option
<ReservedChar
> {
312 Token
::Char(c
) => Some(c
),
317 fn is_useless(&self) -> bool
{
319 Token
::Char(c
) => c
.is_useless(),
324 fn is_media(&self, media
: &str) -> bool
{
326 Token
::SelectorElement(SelectorElement
::Media(s
)) => s
== media
,
331 fn is_a_media(&self) -> bool
{
332 matches
!(*self, Token
::SelectorElement(SelectorElement
::Media(_
)))
335 fn is_a_license(&self) -> bool
{
336 matches
!(*self, Token
::License(_
))
339 fn is_operator(&self) -> bool
{
341 Token
::Operator(_
) => true,
342 Token
::Char(c
) => c
.is_operator(),
348 impl<'a
> PartialEq
<ReservedChar
> for Token
<'a
> {
349 fn eq(&self, other
: &ReservedChar
) -> bool
{
351 Token
::Char(c
) => c
== *other
,
359 iterator
: &mut Peekable
<CharIndices
>,
360 start_pos
: &mut usize,
361 ) -> Option
<Token
<'a
>> {
362 let mut prev
= ReservedChar
::Quote
;
364 let builder
= if let Some((_
, c
)) = iterator
.next() {
365 if c
== '
!'
|| (c
== '
*'
&& iterator
.peek().map(|(_
, c
)| c
) != Some(&'
/'
)) {
369 if let Ok(c
) = ReservedChar
::try_from(c
) {
378 for (pos
, c
) in iterator
{
379 if let Ok(c
) = ReservedChar
::try_from(c
) {
380 if c
== ReservedChar
::Slash
&& prev
== ReservedChar
::Star
{
381 let ret
= Some(builder(&source
[*start_pos
..pos
- 1]));
387 prev
= ReservedChar
::Space
;
395 iterator
: &mut Peekable
<CharIndices
>,
396 start_pos
: &mut usize,
398 ) -> Option
<Token
<'a
>> {
399 while let Some((pos
, c
)) = iterator
.next() {
401 // we skip next character
405 if let Ok(c
) = ReservedChar
::try_from(c
) {
407 let ret
= Some(Token
::String(&source
[*start_pos
..pos
+ 1]));
418 v
: &mut Vec
<Token
<'a
>>,
423 is_in_attribute_selector
: bool
,
426 if !is_in_attribute_selector
427 && ((is_in_block
== 0 && !is_in_media
) || (is_in_media
&& is_in_block
== 1))
429 let mut is_pseudo_class
= false;
431 if let Some(&Token
::Char(ReservedChar
::Colon
)) = v
.last() {
432 is_pseudo_class
= true;
435 if let Ok(s
) = SelectorElement
::try_from(&source
[start
- add
..pos
]) {
439 v
.push(Token
::SelectorElement(s
));
441 let s
= &source
[start
..pos
];
442 if !s
.starts_with('
:'
)
443 && !s
.starts_with('
.'
)
444 && !s
.starts_with('
#')
445 && !s
.starts_with('@'
)
447 v
.push(Token
::Other(s
));
451 v
.push(Token
::Other(&source
[start
..pos
]));
456 #[allow(clippy::comparison_chain)]
457 pub fn tokenize
<'a
>(source
: &'a
str) -> Result
<Tokens
<'a
>, &'
static str> {
458 let mut v
= Vec
::with_capacity(1000);
459 let mut iterator
= source
.char_indices().peekable();
461 let mut is_in_block
: isize = 0;
462 let mut is_in_media
= false;
463 let mut is_in_attribute_selector
= false;
466 let (mut pos
, c
) = match iterator
.next() {
476 is_in_attribute_selector
,
481 if let Ok(c
) = ReservedChar
::try_from(c
) {
489 is_in_attribute_selector
,
491 is_in_media
= is_in_media
493 .unwrap_or(&Token
::Char(ReservedChar
::Space
))
496 c
== ReservedChar
::Quote
|| c
== ReservedChar
::DoubleQuote
=> {
497 if let Some(s
) = get_string(source
, &mut iterator
, &mut pos
, c
) {
501 c
== ReservedChar
::Star
&&
502 *v
.last().unwrap_or(&Token
::Char(ReservedChar
::Space
)) == ReservedChar
::Slash
=> {
504 if let Some(s
) = get_comment(source
, &mut iterator
, &mut pos
) {
508 c
== ReservedChar
::OpenBracket
=> {
509 if is_in_attribute_selector
{
510 return Err("Already in attribute selector");
512 is_in_attribute_selector
= true;
513 v
.push(Token
::Char(c
));
515 c
== ReservedChar
::CloseBracket
=> {
516 if !is_in_attribute_selector
{
517 return Err("Unexpected ']'");
519 is_in_attribute_selector
= false;
520 v
.push(Token
::Char(c
));
522 c
== ReservedChar
::OpenCurlyBrace
=> {
524 v
.push(Token
::Char(c
));
526 c
== ReservedChar
::CloseCurlyBrace
=> {
529 return Err("Too much '}'");
530 } else if is_in_block
== 0 {
533 v
.push(Token
::Char(c
));
535 c
== ReservedChar
::EqualSign
=> {
537 .unwrap_or(&Token
::Char(ReservedChar
::Space
))
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
),
549 v
.push(Token
::SelectorOperator(r
));
551 None
=> v
.push(Token
::Char(c
)),
555 v
.push(Token
::Char(c
));
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
.last().unwrap_or(&Token
::Char(ReservedChar
::OpenCurlyBrace
))
563 .get_char() == Some(ReservedChar
::CloseBracket
)) => {
564 v
.push(Token
::Char(ReservedChar
::Space
));
566 let Ok(op
) = Operator
::try_from(c
) => {
567 v
.push(Token
::Operator(op
));
573 Ok(Tokens(clean_tokens(v
)))
576 fn clean_tokens(mut v
: Vec
<Token
<'_
>>) -> Vec
<Token
<'_
>> {
578 let mut is_in_calc
= false;
582 if v
[i
] == Token
::Other("calc") {
584 } else if is_in_calc
{
585 if v
[i
] == Token
::Char(ReservedChar
::CloseParenthese
) {
587 is_in_calc
= paren
!= 0;
588 } else if v
[i
] == Token
::Char(ReservedChar
::OpenParenthese
) {
593 if v
[i
].is_useless() {
594 if i
> 0 && v
[i
- 1] == Token
::Char(ReservedChar
::CloseBracket
) {
596 && (v
[i
+ 1].is_useless()
597 || v
[i
+ 1] == Token
::Char(ReservedChar
::OpenCurlyBrace
))
602 } else if i
> 0 && v
[i
- 1] == Token
::Other("and") {
603 // retain the space after an and
604 } else if (is_in_calc
&& v
[i
- 1].is_useless())
607 && ((v
[i
- 1].is_char()
608 && v
[i
- 1] != Token
::Char(ReservedChar
::CloseParenthese
))
609 || v
[i
- 1].is_a_media()
610 || v
[i
- 1].is_a_license()))
611 || (i
< v
.len() - 1 && v
[i
+ 1].is_char()))
616 } else if v
[i
].is_comment() {
625 #[derive(Debug, PartialEq, Eq, Clone)]
626 pub struct Tokens
<'a
>(pub Vec
<Token
<'a
>>);
628 impl<'a
> fmt
::Display
for Tokens
<'a
> {
629 fn fmt(&self, f
: &mut fmt
::Formatter
) -> fmt
::Result
{
630 for token
in self.0.iter
() {
631 write
!(f
, "{}", token
)?
;
640 /*! just some license */
641 .foo > #bar p:hover {
647 @media screen and (max-width: 640px) {
653 Token
::License(" just some license "),
654 Token
::SelectorElement(SelectorElement
::Class("foo")),
655 Token
::Char(ReservedChar
::SuperiorThan
),
656 Token
::SelectorElement(SelectorElement
::Id("bar")),
657 Token
::Char(ReservedChar
::Space
),
658 Token
::SelectorElement(SelectorElement
::Tag("p")),
659 Token
::SelectorElement(SelectorElement
::PseudoClass("hover")),
660 Token
::Char(ReservedChar
::OpenCurlyBrace
),
661 Token
::Other("color"),
662 Token
::Char(ReservedChar
::Colon
),
663 Token
::Other("blue"),
664 Token
::Char(ReservedChar
::SemiColon
),
665 Token
::Other("background"),
666 Token
::Char(ReservedChar
::Colon
),
667 Token
::String("\"blue\""),
668 Token
::Char(ReservedChar
::SemiColon
),
669 Token
::Char(ReservedChar
::CloseCurlyBrace
),
670 Token
::SelectorElement(SelectorElement
::Media("media")),
671 Token
::Other("screen"),
672 Token
::Char(ReservedChar
::Space
),
674 Token
::Char(ReservedChar
::Space
),
675 Token
::Char(ReservedChar
::OpenParenthese
),
676 Token
::Other("max-width"),
677 Token
::Char(ReservedChar
::Colon
),
678 Token
::Other("640px"),
679 Token
::Char(ReservedChar
::CloseParenthese
),
680 Token
::Char(ReservedChar
::OpenCurlyBrace
),
681 Token
::SelectorElement(SelectorElement
::Class("block")),
682 Token
::SelectorElement(SelectorElement
::PseudoClass("hover")),
683 Token
::Char(ReservedChar
::OpenCurlyBrace
),
684 Token
::Other("display"),
685 Token
::Char(ReservedChar
::Colon
),
686 Token
::Other("block"),
687 Token
::Char(ReservedChar
::SemiColon
),
688 Token
::Char(ReservedChar
::CloseCurlyBrace
),
689 Token
::Char(ReservedChar
::CloseCurlyBrace
),
691 assert_eq
!(tokenize(s
), Ok(Tokens(expected
)));
697 /** just some license */
708 background-color: gold;
714 font-size: 12em; /* I love big fonts */
718 Token
::License(" just some license "),
719 Token
::SelectorElement(SelectorElement
::Tag("a")),
720 Token
::Char(ReservedChar
::OpenBracket
),
721 Token
::Other("href"),
722 Token
::SelectorOperator(SelectorOperator
::Contains
),
723 Token
::String("\"example\""),
724 Token
::Char(ReservedChar
::CloseBracket
),
725 Token
::Char(ReservedChar
::OpenCurlyBrace
),
726 Token
::Other("background"),
727 Token
::Char(ReservedChar
::Colon
),
728 Token
::Other("yellow"),
729 Token
::Char(ReservedChar
::SemiColon
),
730 Token
::Char(ReservedChar
::CloseCurlyBrace
),
731 Token
::SelectorElement(SelectorElement
::Tag("a")),
732 Token
::Char(ReservedChar
::OpenBracket
),
733 Token
::Other("href"),
734 Token
::SelectorOperator(SelectorOperator
::EndsWith
),
735 Token
::String("\".org\""),
736 Token
::Char(ReservedChar
::CloseBracket
),
737 Token
::Char(ReservedChar
::OpenCurlyBrace
),
738 Token
::Other("font-style"),
739 Token
::Char(ReservedChar
::Colon
),
740 Token
::Other("italic"),
741 Token
::Char(ReservedChar
::SemiColon
),
742 Token
::Char(ReservedChar
::CloseCurlyBrace
),
743 Token
::SelectorElement(SelectorElement
::Tag("span")),
744 Token
::Char(ReservedChar
::OpenBracket
),
745 Token
::Other("lang"),
746 Token
::SelectorOperator(SelectorOperator
::EqualsOrStartsWithFollowedByDash
),
747 Token
::String("\"zh\""),
748 Token
::Char(ReservedChar
::CloseBracket
),
749 Token
::Char(ReservedChar
::OpenCurlyBrace
),
750 Token
::Other("color"),
751 Token
::Char(ReservedChar
::Colon
),
753 Token
::Char(ReservedChar
::SemiColon
),
754 Token
::Char(ReservedChar
::CloseCurlyBrace
),
755 Token
::SelectorElement(SelectorElement
::Tag("a")),
756 Token
::Char(ReservedChar
::OpenBracket
),
757 Token
::Other("href"),
758 Token
::SelectorOperator(SelectorOperator
::FirstStartsWith
),
759 Token
::String("\"/\""),
760 Token
::Char(ReservedChar
::CloseBracket
),
761 Token
::Char(ReservedChar
::OpenCurlyBrace
),
762 Token
::Other("background-color"),
763 Token
::Char(ReservedChar
::Colon
),
764 Token
::Other("gold"),
765 Token
::Char(ReservedChar
::SemiColon
),
766 Token
::Char(ReservedChar
::CloseCurlyBrace
),
767 Token
::SelectorElement(SelectorElement
::Tag("div")),
768 Token
::Char(ReservedChar
::OpenBracket
),
769 Token
::Other("value"),
770 Token
::SelectorOperator(SelectorOperator
::OneAttributeEquals
),
771 Token
::String("\"test\""),
772 Token
::Char(ReservedChar
::CloseBracket
),
773 Token
::Char(ReservedChar
::OpenCurlyBrace
),
774 Token
::Other("border-width"),
775 Token
::Char(ReservedChar
::Colon
),
777 Token
::Char(ReservedChar
::SemiColon
),
778 Token
::Char(ReservedChar
::CloseCurlyBrace
),
779 Token
::SelectorElement(SelectorElement
::Tag("span")),
780 Token
::Char(ReservedChar
::OpenBracket
),
781 Token
::Other("lang"),
782 Token
::Char(ReservedChar
::EqualSign
),
783 Token
::String("\"pt\""),
784 Token
::Char(ReservedChar
::CloseBracket
),
785 Token
::Char(ReservedChar
::OpenCurlyBrace
),
786 Token
::Other("font-size"),
787 Token
::Char(ReservedChar
::Colon
),
788 Token
::Other("12em"),
789 Token
::Char(ReservedChar
::SemiColon
),
790 Token
::Char(ReservedChar
::CloseCurlyBrace
),
792 assert_eq
!(tokenize(s
), Ok(Tokens(expected
)));
797 let s
= "@media (max-width: 700px) { color: red; }";
800 Token
::SelectorElement(SelectorElement
::Media("media")),
801 Token
::Char(ReservedChar
::OpenParenthese
),
802 Token
::Other("max-width"),
803 Token
::Char(ReservedChar
::Colon
),
804 Token
::Other("700px"),
805 Token
::Char(ReservedChar
::CloseParenthese
),
806 Token
::Char(ReservedChar
::OpenCurlyBrace
),
807 Token
::SelectorElement(SelectorElement
::Tag("color")),
808 Token
::Char(ReservedChar
::Colon
),
810 Token
::Char(ReservedChar
::SemiColon
),
811 Token
::Char(ReservedChar
::CloseCurlyBrace
),
814 assert_eq
!(tokenize(s
), Ok(Tokens(expected
)));
819 let s
= ".foo { width: calc(100% - 34px); }";
822 Token
::SelectorElement(SelectorElement
::Class("foo")),
823 Token
::Char(ReservedChar
::OpenCurlyBrace
),
824 Token
::Other("width"),
825 Token
::Char(ReservedChar
::Colon
),
826 Token
::Other("calc"),
827 Token
::Char(ReservedChar
::OpenParenthese
),
828 Token
::Other("100%"),
829 Token
::Char(ReservedChar
::Space
),
831 Token
::Char(ReservedChar
::Space
),
832 Token
::Other("34px"),
833 Token
::Char(ReservedChar
::CloseParenthese
),
834 Token
::Char(ReservedChar
::SemiColon
),
835 Token
::Char(ReservedChar
::CloseCurlyBrace
),
837 assert_eq
!(tokenize(s
), Ok(Tokens(expected
)));