minifier 0.2.2

Minifier tool/lib for JS/CSS/JSON files
Documentation
// Take a look at the license at the top of the repository in the LICENSE file.

use std::convert::TryFrom;
use std::fmt;
use std::iter::Peekable;
use std::str::CharIndices;

#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum ReservedChar {
    Comma,
    SuperiorThan,
    OpenParenthese,
    CloseParenthese,
    OpenCurlyBrace,
    CloseCurlyBrace,
    OpenBracket,
    CloseBracket,
    Colon,
    SemiColon,
    Slash,
    Plus,
    EqualSign,
    Space,
    Tab,
    Backline,
    Star,
    Quote,
    DoubleQuote,
    Pipe,
    Tilde,
    Dollar,
    Circumflex,
}

impl fmt::Display for ReservedChar {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(
            f,
            "{}",
            match *self {
                ReservedChar::Comma => ',',
                ReservedChar::OpenParenthese => '(',
                ReservedChar::CloseParenthese => ')',
                ReservedChar::OpenCurlyBrace => '{',
                ReservedChar::CloseCurlyBrace => '}',
                ReservedChar::OpenBracket => '[',
                ReservedChar::CloseBracket => ']',
                ReservedChar::Colon => ':',
                ReservedChar::SemiColon => ';',
                ReservedChar::Slash => '/',
                ReservedChar::Star => '*',
                ReservedChar::Plus => '+',
                ReservedChar::EqualSign => '=',
                ReservedChar::Space => ' ',
                ReservedChar::Tab => '\t',
                ReservedChar::Backline => '\n',
                ReservedChar::SuperiorThan => '>',
                ReservedChar::Quote => '\'',
                ReservedChar::DoubleQuote => '"',
                ReservedChar::Pipe => '|',
                ReservedChar::Tilde => '~',
                ReservedChar::Dollar => '$',
                ReservedChar::Circumflex => '^',
            }
        )
    }
}

impl TryFrom<char> for ReservedChar {
    type Error = &'static str;

    fn try_from(value: char) -> Result<ReservedChar, Self::Error> {
        match value {
            '\'' => Ok(ReservedChar::Quote),
            '"' => Ok(ReservedChar::DoubleQuote),
            ',' => Ok(ReservedChar::Comma),
            '(' => Ok(ReservedChar::OpenParenthese),
            ')' => Ok(ReservedChar::CloseParenthese),
            '{' => Ok(ReservedChar::OpenCurlyBrace),
            '}' => Ok(ReservedChar::CloseCurlyBrace),
            '[' => Ok(ReservedChar::OpenBracket),
            ']' => Ok(ReservedChar::CloseBracket),
            ':' => Ok(ReservedChar::Colon),
            ';' => Ok(ReservedChar::SemiColon),
            '/' => Ok(ReservedChar::Slash),
            '*' => Ok(ReservedChar::Star),
            '+' => Ok(ReservedChar::Plus),
            '=' => Ok(ReservedChar::EqualSign),
            ' ' => Ok(ReservedChar::Space),
            '\t' => Ok(ReservedChar::Tab),
            '\n' | '\r' => Ok(ReservedChar::Backline),
            '>' => Ok(ReservedChar::SuperiorThan),
            '|' => Ok(ReservedChar::Pipe),
            '~' => Ok(ReservedChar::Tilde),
            '$' => Ok(ReservedChar::Dollar),
            '^' => Ok(ReservedChar::Circumflex),
            _ => Err("Unknown reserved char"),
        }
    }
}

impl ReservedChar {
    fn is_useless(&self) -> bool {
        *self == ReservedChar::Space
            || *self == ReservedChar::Tab
            || *self == ReservedChar::Backline
    }

    fn is_operator(&self) -> bool {
        Operator::try_from(*self).is_ok()
    }
}

#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum Operator {
    Plus,
    Multiply,
    Minus,
    Modulo,
    Divide,
}

impl fmt::Display for Operator {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(
            f,
            "{}",
            match *self {
                Operator::Plus => '+',
                Operator::Multiply => '*',
                Operator::Minus => '-',
                Operator::Modulo => '%',
                Operator::Divide => '/',
            }
        )
    }
}

impl TryFrom<char> for Operator {
    type Error = &'static str;

    fn try_from(value: char) -> Result<Operator, Self::Error> {
        match value {
            '+' => Ok(Operator::Plus),
            '*' => Ok(Operator::Multiply),
            '-' => Ok(Operator::Minus),
            '%' => Ok(Operator::Modulo),
            '/' => Ok(Operator::Divide),
            _ => Err("Unknown operator"),
        }
    }
}

impl TryFrom<ReservedChar> for Operator {
    type Error = &'static str;

    fn try_from(value: ReservedChar) -> Result<Operator, Self::Error> {
        match value {
            ReservedChar::Slash => Ok(Operator::Divide),
            ReservedChar::Star => Ok(Operator::Multiply),
            ReservedChar::Plus => Ok(Operator::Plus),
            _ => Err("Unknown operator"),
        }
    }
}

#[derive(Eq, PartialEq, Clone, Debug)]
pub enum SelectorElement<'a> {
    PseudoClass(&'a str),
    Class(&'a str),
    Id(&'a str),
    Tag(&'a str),
    Media(&'a str),
}

impl<'a> TryFrom<&'a str> for SelectorElement<'a> {
    type Error = &'static str;

    fn try_from(value: &'a str) -> Result<SelectorElement<'_>, Self::Error> {
        if let Some(value) = value.strip_prefix('.') {
            if value.is_empty() {
                Err("cannot determine selector")
            } else {
                Ok(SelectorElement::Class(value))
            }
        } else if let Some(value) = value.strip_prefix('#') {
            if value.is_empty() {
                Err("cannot determine selector")
            } else {
                Ok(SelectorElement::Id(value))
            }
        } else if let Some(value) = value.strip_prefix('@') {
            if value.is_empty() {
                Err("cannot determine selector")
            } else {
                Ok(SelectorElement::Media(value))
            }
        } else if let Some(value) = value.strip_prefix(':') {
            if value.is_empty() {
                Err("cannot determine selector")
            } else {
                Ok(SelectorElement::PseudoClass(value))
            }
        } else if value.chars().next().unwrap_or(' ').is_alphabetic() {
            Ok(SelectorElement::Tag(value))
        } else {
            Err("unknown selector")
        }
    }
}

impl<'a> fmt::Display for SelectorElement<'a> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match *self {
            SelectorElement::Class(c) => write!(f, ".{}", c),
            SelectorElement::Id(i) => write!(f, "#{}", i),
            SelectorElement::Tag(t) => write!(f, "{}", t),
            SelectorElement::Media(m) => write!(f, "@{} ", m),
            SelectorElement::PseudoClass(pc) => write!(f, ":{}", pc),
        }
    }
}

#[derive(Eq, PartialEq, Clone, Debug, Copy)]
pub enum SelectorOperator {
    /// `~=`
    OneAttributeEquals,
    /// `|=`
    EqualsOrStartsWithFollowedByDash,
    /// `$=`
    EndsWith,
    /// `^=`
    FirstStartsWith,
    /// `*=`
    Contains,
}

impl fmt::Display for SelectorOperator {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match *self {
            SelectorOperator::OneAttributeEquals => write!(f, "~="),
            SelectorOperator::EqualsOrStartsWithFollowedByDash => write!(f, "|="),
            SelectorOperator::EndsWith => write!(f, "$="),
            SelectorOperator::FirstStartsWith => write!(f, "^="),
            SelectorOperator::Contains => write!(f, "*="),
        }
    }
}

#[derive(Eq, PartialEq, Clone, Debug)]
pub enum Token<'a> {
    /// Comment.
    Comment(&'a str),
    /// Comment starting with `/**`.
    License(&'a str),
    Char(ReservedChar),
    Other(&'a str),
    SelectorElement(SelectorElement<'a>),
    String(&'a str),
    SelectorOperator(SelectorOperator),
    Operator(Operator),
}

impl<'a> fmt::Display for Token<'a> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match *self {
            // Token::AtRule(at_rule) => write!(f, "{}", at_rule, content),
            // Token::ElementRule(selectors) => write!(f, "{}", x),
            Token::Comment(c) => write!(f, "{}", c),
            Token::License(l) => writeln!(f, "/*!{}*/", l),
            Token::Char(c) => write!(f, "{}", c),
            Token::Other(s) => write!(f, "{}", s),
            Token::SelectorElement(ref se) => write!(f, "{}", se),
            Token::String(s) => write!(f, "{}", s),
            Token::SelectorOperator(so) => write!(f, "{}", so),
            Token::Operator(op) => write!(f, "{}", op),
        }
    }
}

impl<'a> Token<'a> {
    fn is_comment(&self) -> bool {
        matches!(*self, Token::Comment(_))
    }

    fn is_char(&self) -> bool {
        matches!(*self, Token::Char(_))
    }

    fn get_char(&self) -> Option<ReservedChar> {
        match *self {
            Token::Char(c) => Some(c),
            _ => None,
        }
    }

    fn is_useless(&self) -> bool {
        match *self {
            Token::Char(c) => c.is_useless(),
            _ => false,
        }
    }

    fn is_a_media(&self) -> bool {
        matches!(*self, Token::SelectorElement(SelectorElement::Media(_)))
    }

    fn is_a_license(&self) -> bool {
        matches!(*self, Token::License(_))
    }

    fn is_operator(&self) -> bool {
        match *self {
            Token::Operator(_) => true,
            Token::Char(c) => c.is_operator(),
            _ => false,
        }
    }
}

impl<'a> PartialEq<ReservedChar> for Token<'a> {
    fn eq(&self, other: &ReservedChar) -> bool {
        match *self {
            Token::Char(c) => c == *other,
            _ => false,
        }
    }
}

fn get_comment<'a>(
    source: &'a str,
    iterator: &mut Peekable<CharIndices<'_>>,
    start_pos: &mut usize,
) -> Option<Token<'a>> {
    let mut prev = ReservedChar::Quote;
    *start_pos += 1;
    let builder = if let Some((_, c)) = iterator.next() {
        if c == '!' || (c == '*' && iterator.peek().map(|(_, c)| c) != Some(&'/')) {
            *start_pos += 1;
            Token::License
        } else {
            if let Ok(c) = ReservedChar::try_from(c) {
                prev = c;
            }
            Token::Comment
        }
    } else {
        Token::Comment
    };

    for (pos, c) in iterator {
        if let Ok(c) = ReservedChar::try_from(c) {
            if c == ReservedChar::Slash && prev == ReservedChar::Star {
                let ret = Some(builder(&source[*start_pos..pos - 1]));
                *start_pos = pos;
                return ret;
            }
            prev = c;
        } else {
            prev = ReservedChar::Space;
        }
    }
    None
}

fn get_string<'a>(
    source: &'a str,
    iterator: &mut Peekable<CharIndices<'_>>,
    start_pos: &mut usize,
    start: ReservedChar,
) -> Option<Token<'a>> {
    while let Some((pos, c)) = iterator.next() {
        if c == '\\' {
            // we skip next character
            iterator.next();
            continue;
        }
        if let Ok(c) = ReservedChar::try_from(c) {
            if c == start {
                let ret = Some(Token::String(&source[*start_pos..pos + 1]));
                *start_pos = pos;
                return ret;
            }
        }
    }
    None
}

fn fill_other<'a>(
    source: &'a str,
    v: &mut Vec<Token<'a>>,
    start: usize,
    pos: usize,
    is_in_block: isize,
    is_in_media: bool,
    is_in_attribute_selector: bool,
) {
    if start < pos {
        if !is_in_attribute_selector
            && ((is_in_block == 0 && !is_in_media) || (is_in_media && is_in_block == 1))
        {
            let mut is_pseudo_class = false;
            let mut add = 0;
            if let Some(&Token::Char(ReservedChar::Colon)) = v.last() {
                is_pseudo_class = true;
                add = 1;
            }
            if let Ok(s) = SelectorElement::try_from(&source[start - add..pos]) {
                if is_pseudo_class {
                    v.pop();
                }
                v.push(Token::SelectorElement(s));
            } else {
                let s = &source[start..pos];
                if !s.starts_with(':')
                    && !s.starts_with('.')
                    && !s.starts_with('#')
                    && !s.starts_with('@')
                {
                    v.push(Token::Other(s));
                }
            }
        } else {
            v.push(Token::Other(&source[start..pos]));
        }
    }
}

#[allow(clippy::comparison_chain)]
pub(super) fn tokenize<'a>(source: &'a str) -> Result<Tokens<'a>, &'static str> {
    let mut v = Vec::with_capacity(1000);
    let mut iterator = source.char_indices().peekable();
    let mut start = 0;
    let mut is_in_block: isize = 0;
    let mut is_in_media = false;
    let mut is_in_attribute_selector = false;

    loop {
        let (mut pos, c) = match iterator.next() {
            Some(x) => x,
            None => {
                fill_other(
                    source,
                    &mut v,
                    start,
                    source.len(),
                    is_in_block,
                    is_in_media,
                    is_in_attribute_selector,
                );
                break;
            }
        };
        if let Ok(c) = ReservedChar::try_from(c) {
            fill_other(
                source,
                &mut v,
                start,
                pos,
                is_in_block,
                is_in_media,
                is_in_attribute_selector,
            );
            is_in_media = is_in_media
                || v.last()
                    .unwrap_or(&Token::Char(ReservedChar::Space))
                    .is_a_media();
            match c {
                ReservedChar::Quote | ReservedChar::DoubleQuote => {
                    if let Some(s) = get_string(source, &mut iterator, &mut pos, c) {
                        v.push(s);
                    }
                }
                ReservedChar::Star
                    if *v.last().unwrap_or(&Token::Char(ReservedChar::Space))
                        == ReservedChar::Slash =>
                {
                    v.pop();
                    if let Some(s) = get_comment(source, &mut iterator, &mut pos) {
                        v.push(s);
                    }
                }
                ReservedChar::OpenBracket => {
                    if is_in_attribute_selector {
                        return Err("Already in attribute selector");
                    }
                    is_in_attribute_selector = true;
                    v.push(Token::Char(c));
                }
                ReservedChar::CloseBracket => {
                    if !is_in_attribute_selector {
                        return Err("Unexpected ']'");
                    }
                    is_in_attribute_selector = false;
                    v.push(Token::Char(c));
                }
                ReservedChar::OpenCurlyBrace => {
                    is_in_block += 1;
                    v.push(Token::Char(c));
                }
                ReservedChar::CloseCurlyBrace => {
                    is_in_block -= 1;
                    if is_in_block < 0 {
                        return Err("Too much '}'");
                    } else if is_in_block == 0 {
                        is_in_media = false;
                    }
                    v.push(Token::Char(c));
                }
                ReservedChar::SemiColon if is_in_block == 0 => {
                    is_in_media = false;
                    v.push(Token::Char(c));
                }
                ReservedChar::EqualSign => {
                    match match v
                        .last()
                        .unwrap_or(&Token::Char(ReservedChar::Space))
                        .get_char()
                        .unwrap_or(ReservedChar::Space)
                    {
                        ReservedChar::Tilde => Some(SelectorOperator::OneAttributeEquals),
                        ReservedChar::Pipe => {
                            Some(SelectorOperator::EqualsOrStartsWithFollowedByDash)
                        }
                        ReservedChar::Dollar => Some(SelectorOperator::EndsWith),
                        ReservedChar::Circumflex => Some(SelectorOperator::FirstStartsWith),
                        ReservedChar::Star => Some(SelectorOperator::Contains),
                        _ => None,
                    } {
                        Some(r) => {
                            v.pop();
                            v.push(Token::SelectorOperator(r));
                        }
                        None => v.push(Token::Char(c)),
                    }
                }
                c if !c.is_useless() => {
                    v.push(Token::Char(c));
                }
                c => {
                    if !v
                        .last()
                        .unwrap_or(&Token::Char(ReservedChar::Space))
                        .is_useless()
                        && (!v
                            .last()
                            .unwrap_or(&Token::Char(ReservedChar::OpenCurlyBrace))
                            .is_char()
                            || v.last()
                                .unwrap_or(&Token::Char(ReservedChar::OpenCurlyBrace))
                                .is_operator()
                            || v.last()
                                .unwrap_or(&Token::Char(ReservedChar::OpenCurlyBrace))
                                .get_char()
                                == Some(ReservedChar::CloseParenthese)
                            || v.last()
                                .unwrap_or(&Token::Char(ReservedChar::OpenCurlyBrace))
                                .get_char()
                                == Some(ReservedChar::CloseBracket))
                    {
                        v.push(Token::Char(ReservedChar::Space));
                    } else if let Ok(op) = Operator::try_from(c) {
                        v.push(Token::Operator(op));
                    }
                }
            }
            start = pos + 1;
        }
    }
    Ok(Tokens(clean_tokens(v)))
}

fn clean_tokens(mut v: Vec<Token<'_>>) -> Vec<Token<'_>> {
    // This function may remove multiple elements from the vector. Ideally we'd
    // use `Vec::retain`, but the traversal requires inspecting the previously
    // retained token and the next token, which `Vec::retain` doesn't allow. So
    // we have to use a lower-level mechanism.
    let mut i = 0;
    // Index of the previous retained token, if there is one.
    let mut ip: Option<usize> = None;
    let mut is_in_calc = false;
    let mut paren = 0;
    // A vector of bools indicating which elements are to be retained.
    let mut b = Vec::with_capacity(v.len());

    while i < v.len() {
        if v[i] == Token::Other("calc") {
            is_in_calc = true;
        } else if is_in_calc {
            if v[i] == Token::Char(ReservedChar::CloseParenthese) {
                paren -= 1;
                is_in_calc = paren != 0;
            } else if v[i] == Token::Char(ReservedChar::OpenParenthese) {
                paren += 1;
            }
        }

        let mut retain = true;
        if v[i].is_useless() {
            #[allow(clippy::if_same_then_else)]
            if ip.is_some() && v[ip.unwrap()] == Token::Char(ReservedChar::CloseBracket) {
                if i + 1 < v.len()
                    && (v[i + 1].is_useless()
                        || v[i + 1] == Token::Char(ReservedChar::OpenCurlyBrace))
                {
                    retain = false;
                }
            } else if ip.is_some() && matches!(v[ip.unwrap()], Token::Other("and" | "or" | "not")) {
                // retain the space after "and", "or" or "not"
            } else if is_in_calc && v[ip.unwrap()].is_useless() {
                retain = false;
            } else if !is_in_calc
                && ((ip.is_some() && {
                    let prev = &v[ip.unwrap()];
                    (prev.is_char() && prev != &Token::Char(ReservedChar::CloseParenthese))
                        || prev.is_a_media()
                        || prev.is_a_license()
                }) || (i < v.len() - 1 && v[i + 1].is_char()))
            {
                retain = false;
            }
        } else if v[i].is_comment() {
            retain = false;
        }
        if retain {
            ip = Some(i);
        }
        b.push(retain);
        i += 1;
    }
    assert_eq!(v.len(), b.len());
    let mut b = b.into_iter();
    v.retain(|_| b.next().unwrap());
    v
}

#[derive(Debug, PartialEq, Eq, Clone)]
pub(super) struct Tokens<'a>(Vec<Token<'a>>);

impl<'a> Tokens<'a> {
    pub(super) fn write<W: std::io::Write>(self, mut w: W) -> std::io::Result<()> {
        for token in self.0.iter() {
            write!(w, "{}", token)?;
        }
        Ok(())
    }
}

impl<'a> fmt::Display for Tokens<'a> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        for token in self.0.iter() {
            write!(f, "{}", token)?;
        }
        Ok(())
    }
}

#[test]
fn css_basic() {
    let s = r#"
/*! just some license */
.foo > #bar p:hover {
    color: blue;
    background: "blue";
}

/* a comment! */
@media screen and (max-width: 640px) {
    .block:hover {
        display:    block;
    }
}"#;
    let expected = vec![
        Token::License(" just some license "),
        Token::SelectorElement(SelectorElement::Class("foo")),
        Token::Char(ReservedChar::SuperiorThan),
        Token::SelectorElement(SelectorElement::Id("bar")),
        Token::Char(ReservedChar::Space),
        Token::SelectorElement(SelectorElement::Tag("p")),
        Token::SelectorElement(SelectorElement::PseudoClass("hover")),
        Token::Char(ReservedChar::OpenCurlyBrace),
        Token::Other("color"),
        Token::Char(ReservedChar::Colon),
        Token::Other("blue"),
        Token::Char(ReservedChar::SemiColon),
        Token::Other("background"),
        Token::Char(ReservedChar::Colon),
        Token::String("\"blue\""),
        Token::Char(ReservedChar::SemiColon),
        Token::Char(ReservedChar::CloseCurlyBrace),
        Token::SelectorElement(SelectorElement::Media("media")),
        Token::Other("screen"),
        Token::Char(ReservedChar::Space),
        Token::Other("and"),
        Token::Char(ReservedChar::Space),
        Token::Char(ReservedChar::OpenParenthese),
        Token::Other("max-width"),
        Token::Char(ReservedChar::Colon),
        Token::Other("640px"),
        Token::Char(ReservedChar::CloseParenthese),
        Token::Char(ReservedChar::OpenCurlyBrace),
        Token::SelectorElement(SelectorElement::Class("block")),
        Token::SelectorElement(SelectorElement::PseudoClass("hover")),
        Token::Char(ReservedChar::OpenCurlyBrace),
        Token::Other("display"),
        Token::Char(ReservedChar::Colon),
        Token::Other("block"),
        Token::Char(ReservedChar::SemiColon),
        Token::Char(ReservedChar::CloseCurlyBrace),
        Token::Char(ReservedChar::CloseCurlyBrace),
    ];
    assert_eq!(tokenize(s), Ok(Tokens(expected)));
}

#[test]
fn elem_selector() {
    let s = r#"
/** just some license */
a[href*="example"] {
    background: yellow;
}
a[href$=".org"] {
  font-style: italic;
}
span[lang|="zh"] {
    color: red;
}
a[href^="/"] {
    background-color: gold;
}
div[value~="test"] {
    border-width: 1px;
}
span[lang="pt"] {
    font-size: 12em; /* I love big fonts */
}
"#;
    let expected = vec![
        Token::License(" just some license "),
        Token::SelectorElement(SelectorElement::Tag("a")),
        Token::Char(ReservedChar::OpenBracket),
        Token::Other("href"),
        Token::SelectorOperator(SelectorOperator::Contains),
        Token::String("\"example\""),
        Token::Char(ReservedChar::CloseBracket),
        Token::Char(ReservedChar::OpenCurlyBrace),
        Token::Other("background"),
        Token::Char(ReservedChar::Colon),
        Token::Other("yellow"),
        Token::Char(ReservedChar::SemiColon),
        Token::Char(ReservedChar::CloseCurlyBrace),
        Token::SelectorElement(SelectorElement::Tag("a")),
        Token::Char(ReservedChar::OpenBracket),
        Token::Other("href"),
        Token::SelectorOperator(SelectorOperator::EndsWith),
        Token::String("\".org\""),
        Token::Char(ReservedChar::CloseBracket),
        Token::Char(ReservedChar::OpenCurlyBrace),
        Token::Other("font-style"),
        Token::Char(ReservedChar::Colon),
        Token::Other("italic"),
        Token::Char(ReservedChar::SemiColon),
        Token::Char(ReservedChar::CloseCurlyBrace),
        Token::SelectorElement(SelectorElement::Tag("span")),
        Token::Char(ReservedChar::OpenBracket),
        Token::Other("lang"),
        Token::SelectorOperator(SelectorOperator::EqualsOrStartsWithFollowedByDash),
        Token::String("\"zh\""),
        Token::Char(ReservedChar::CloseBracket),
        Token::Char(ReservedChar::OpenCurlyBrace),
        Token::Other("color"),
        Token::Char(ReservedChar::Colon),
        Token::Other("red"),
        Token::Char(ReservedChar::SemiColon),
        Token::Char(ReservedChar::CloseCurlyBrace),
        Token::SelectorElement(SelectorElement::Tag("a")),
        Token::Char(ReservedChar::OpenBracket),
        Token::Other("href"),
        Token::SelectorOperator(SelectorOperator::FirstStartsWith),
        Token::String("\"/\""),
        Token::Char(ReservedChar::CloseBracket),
        Token::Char(ReservedChar::OpenCurlyBrace),
        Token::Other("background-color"),
        Token::Char(ReservedChar::Colon),
        Token::Other("gold"),
        Token::Char(ReservedChar::SemiColon),
        Token::Char(ReservedChar::CloseCurlyBrace),
        Token::SelectorElement(SelectorElement::Tag("div")),
        Token::Char(ReservedChar::OpenBracket),
        Token::Other("value"),
        Token::SelectorOperator(SelectorOperator::OneAttributeEquals),
        Token::String("\"test\""),
        Token::Char(ReservedChar::CloseBracket),
        Token::Char(ReservedChar::OpenCurlyBrace),
        Token::Other("border-width"),
        Token::Char(ReservedChar::Colon),
        Token::Other("1px"),
        Token::Char(ReservedChar::SemiColon),
        Token::Char(ReservedChar::CloseCurlyBrace),
        Token::SelectorElement(SelectorElement::Tag("span")),
        Token::Char(ReservedChar::OpenBracket),
        Token::Other("lang"),
        Token::Char(ReservedChar::EqualSign),
        Token::String("\"pt\""),
        Token::Char(ReservedChar::CloseBracket),
        Token::Char(ReservedChar::OpenCurlyBrace),
        Token::Other("font-size"),
        Token::Char(ReservedChar::Colon),
        Token::Other("12em"),
        Token::Char(ReservedChar::SemiColon),
        Token::Char(ReservedChar::CloseCurlyBrace),
    ];
    assert_eq!(tokenize(s), Ok(Tokens(expected)));
}

#[test]
fn check_media() {
    let s = "@media (max-width: 700px) { color: red; }";

    let expected = vec![
        Token::SelectorElement(SelectorElement::Media("media")),
        Token::Char(ReservedChar::OpenParenthese),
        Token::Other("max-width"),
        Token::Char(ReservedChar::Colon),
        Token::Other("700px"),
        Token::Char(ReservedChar::CloseParenthese),
        Token::Char(ReservedChar::OpenCurlyBrace),
        Token::SelectorElement(SelectorElement::Tag("color")),
        Token::Char(ReservedChar::Colon),
        Token::Other("red"),
        Token::Char(ReservedChar::SemiColon),
        Token::Char(ReservedChar::CloseCurlyBrace),
    ];

    assert_eq!(tokenize(s), Ok(Tokens(expected)));
}

#[test]
fn check_supports() {
    let s = "@supports not (display: grid) { div { float: right; } }";

    let expected = vec![
        Token::SelectorElement(SelectorElement::Media("supports")),
        Token::Other("not"),
        Token::Char(ReservedChar::Space),
        Token::Char(ReservedChar::OpenParenthese),
        Token::Other("display"),
        Token::Char(ReservedChar::Colon),
        Token::Other("grid"),
        Token::Char(ReservedChar::CloseParenthese),
        Token::Char(ReservedChar::OpenCurlyBrace),
        Token::SelectorElement(SelectorElement::Tag("div")),
        Token::Char(ReservedChar::OpenCurlyBrace),
        Token::Other("float"),
        Token::Char(ReservedChar::Colon),
        Token::Other("right"),
        Token::Char(ReservedChar::SemiColon),
        Token::Char(ReservedChar::CloseCurlyBrace),
        Token::Char(ReservedChar::CloseCurlyBrace),
    ];

    assert_eq!(tokenize(s), Ok(Tokens(expected)));
}

#[test]
fn check_calc() {
    let s = ".foo { width: calc(100% - 34px); }";

    let expected = vec![
        Token::SelectorElement(SelectorElement::Class("foo")),
        Token::Char(ReservedChar::OpenCurlyBrace),
        Token::Other("width"),
        Token::Char(ReservedChar::Colon),
        Token::Other("calc"),
        Token::Char(ReservedChar::OpenParenthese),
        Token::Other("100%"),
        Token::Char(ReservedChar::Space),
        Token::Other("-"),
        Token::Char(ReservedChar::Space),
        Token::Other("34px"),
        Token::Char(ReservedChar::CloseParenthese),
        Token::Char(ReservedChar::SemiColon),
        Token::Char(ReservedChar::CloseCurlyBrace),
    ];
    assert_eq!(tokenize(s), Ok(Tokens(expected)));
}