diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-17 12:02:58 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-17 12:02:58 +0000 |
commit | 698f8c2f01ea549d77d7dc3338a12e04c11057b9 (patch) | |
tree | 173a775858bd501c378080a10dca74132f05bc50 /vendor/minifier/src/css | |
parent | Initial commit. (diff) | |
download | rustc-698f8c2f01ea549d77d7dc3338a12e04c11057b9.tar.xz rustc-698f8c2f01ea549d77d7dc3338a12e04c11057b9.zip |
Adding upstream version 1.64.0+dfsg1.upstream/1.64.0+dfsg1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'vendor/minifier/src/css')
-rw-r--r-- | vendor/minifier/src/css/mod.rs | 40 | ||||
-rw-r--r-- | vendor/minifier/src/css/tests.rs | 286 | ||||
-rw-r--r-- | vendor/minifier/src/css/token.rs | 875 |
3 files changed, 1201 insertions, 0 deletions
diff --git a/vendor/minifier/src/css/mod.rs b/vendor/minifier/src/css/mod.rs new file mode 100644 index 000000000..224ad8126 --- /dev/null +++ b/vendor/minifier/src/css/mod.rs @@ -0,0 +1,40 @@ +// Take a look at the license at the top of the repository in the LICENSE file. + +use std::{fmt, io}; + +mod token; + +/// Minifies a given CSS source code. +/// +/// # Example +/// +/// ```rust +/// use minifier::css::minify; +/// +/// let css = r#" +/// .foo > p { +/// color: red; +/// }"#.into(); +/// let css_minified = minify(css).expect("minification failed"); +/// assert_eq!(&css_minified.to_string(), ".foo>p{color:red;}"); +/// ``` +pub fn minify<'a>(content: &'a str) -> Result<Minified<'a>, &'static str> { + token::tokenize(content).map(Minified) +} + +pub struct Minified<'a>(token::Tokens<'a>); + +impl<'a> Minified<'a> { + pub fn write<W: io::Write>(self, w: W) -> io::Result<()> { + self.0.write(w) + } +} + +impl<'a> fmt::Display for Minified<'a> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.fmt(f) + } +} + +#[cfg(test)] +mod tests; diff --git a/vendor/minifier/src/css/tests.rs b/vendor/minifier/src/css/tests.rs new file mode 100644 index 000000000..dd696afde --- /dev/null +++ b/vendor/minifier/src/css/tests.rs @@ -0,0 +1,286 @@ +// Take a look at the license at the top of the repository in the LICENSE file. + +use crate::css::minify; + +/*enum Element { + /// Rule starting with `@`: + /// + /// * charset + /// * font-face + /// * import + /// * keyframes + /// * media + AtRule(AtRule<'a>), + /// Any "normal" CSS rule block. + /// + /// Contains the selector(s) and its content. + ElementRule(Vec<&'a str>, Vec<Property<'a>>), +} + +fn get_property<'a>(source: &'a str, iterator: &mut Peekable<CharIndices>, + start_pos: &mut usize) -> Option<Property<'a>> { + let mut end_pos = None; + // First we get the property name. + while let Some((pos, c)) = iterator.next() { + if let Ok(c) = ReservedChar::try_from(c) { + if c.is_useless() { + continue + } else if c == ReservedChar::OpenCurlyBrace { + return None + } else if c == ReservedChar::Colon { + end_pos = Some(pos); + break + } else { // Invalid character. + return None; + } + } else if (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '-' { + // everything's fine for now... + } else { + return None; // invalid character + } + } + if end_pos.is_none() || end_pos == Some(*start_pos + 1) { + return None; + } + while let Some((pos, c)) = iterator.next() { + if let Ok(c) = ReservedChar::try_from(c) { + if c == ReservedChar::DoubleQuote || c == ReservedChar::Quote { + get_string(source, iterator, &mut 0, c) + } else if c == ReservedChar::SemiColon { + // we reached the end! + let end_pos = end_pos.unwrap(); + *start_pos = pos; + return Property { + name: &source[start_pos..end_pos], + value: &source[end_pos..pos], + } + } + } + } + None +} + +enum Selector<'a> { + Tag(&'a str), + /// '.' + Class(&'a str), + /// '#' + Id(&'a str), + /// '<', '>', '(', ')', '+', ' ', '[', ']' + Operator(char), +} + +struct ElementRule<'a> { + selectors: Vec<Selector<'a>>, + properties: Vec<Property<'a>>, +} + +fn get_element_rule<'a>(source: &'a str, iterator: &mut Peekable<CharIndices>, + c: char) -> Option<Token<'a>> { + let mut selectors = Vec::with_capacity(2); + + while let Some(s) = get_next_selector(source, iterator, c) { + if !selectors.is_empty() || !s.empty_operator() { + } + selectors.push(s); + } +} + +fn get_media_query<'a>(source: &'a str, iterator: &mut Peekable<CharIndices>, + start_pos: &mut usize) -> Option<Token<'a>> { + while let Some((pos, c)) = iterator.next() { + if c == '{' { + ; + } + } + None // An error occurred, sad life... +} + + +fn get_properties<'a>(source: &'a str, iterator: &mut Peekable<CharIndices>, + start_pos: &mut usize) -> Vec<Property> { + let mut ret = Vec::with_capacity(2); + while let Some(property) = get_property(source, iterator, start_pos) { + ret.push(property); + } + ret +} + +pub struct Property<'a> { + name: &'a str, + value: &'a str, +} + +pub enum AtRule<'a> { + /// Contains the charset. Supposed to be the first rule in the style sheet and be present + /// only once. + Charset(&'a str), + /// font-face rule. + FontFace(Vec<Property<'a>>), + /// Contains the import. + Import(&'a str), + /// Contains the rule and the block. + Keyframes(&'a str, Tokens<'a>), + /// Contains the rules and the block. + Media(Vec<&'a str>, Tokens<'a>), +} + +impl fmt::Display for AtRule { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "@{}", &match *self { + AtRule::Charset(c) => format!("charset {};", c), + AtRule::FontFace(t) => format!("font-face {{{}}};", t), + AtRule::Import(i) => format!("import {};", i), + AtRule::Keyframes(r, t) => format!("keyframes {} {{{}}}", r, t), + AtRule::Media(r, t) => format!("media {} {{{}}}", r.join(" ").collect::<String>(), t), + }) + } +}*/ + +#[test] +fn check_minification() { + let s = r#" +/** Baguette! */ +.b > p + div:hover { + background: #fff; +} + +a[target = "_blank"] { + /* I like weird tests. */ + border: 1px solid yellow ; +} +"#; + let expected = r#"/*! Baguette! */ +.b>p+div:hover{background:#fff;}a[target="_blank"]{border:1px solid yellow;}"#; + assert_eq!(minify(s).expect("minify failed").to_string(), expected); +} + +#[test] +fn check_minification2() { + let s = r#" +h2, h3:not(.impl):not(.method):not(.type) { + background-color: #0a042f !important; +} + +:target { background: #494a3d; } + +.table-display tr td:first-child { + float: right; +} + +/* just some + * long + * + * very + * long + * comment :) + */ +@media (max-width: 700px) { + .theme-picker { + left: 10px; + top: 54px; + z-index: 1; + background-color: rgba(0, 0 , 0 , 0); + font: 15px "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace; + } +}"#; + let expected = "h2,h3:not(.impl):not(.method):not(.type){background-color:#0a042f !important;}\ + :target{background:#494a3d;}.table-display tr td:first-child{float:right;}\ + @media (max-width:700px){.theme-picker{left:10px;top:54px;z-index:1;\ + background-color:rgba(0,0,0,0);font:15px \"SFMono-Regular\",Consolas,\ + \"Liberation Mono\",Menlo,Courier,monospace;}}"; + assert_eq!(minify(s).expect("minify failed").to_string(), expected); +} + +#[test] +fn check_calc() { + let s = ".foo { width: calc(100% - 34px); }"; + let expected = ".foo{width:calc(100% - 34px);}"; + assert_eq!(minify(s).expect("minify failed").to_string(), expected); +} + +#[test] +fn check_spaces() { + let s = ".line-numbers .line-highlighted { color: #0a042f !important; }"; + let expected = ".line-numbers .line-highlighted{color:#0a042f !important;}"; + assert_eq!(minify(s).expect("minify failed").to_string(), expected); +} + +#[test] +fn check_space_after_paren() { + let s = ".docblock:not(.type-decl) a:not(.srclink) {}"; + let expected = ".docblock:not(.type-decl) a:not(.srclink){}"; + assert_eq!(minify(s).expect("minify failed").to_string(), expected); +} + +#[test] +fn check_space_after_and() { + let s = "@media only screen and (max-width : 600px) {}"; + let expected = "@media only screen and (max-width:600px){}"; + assert_eq!(minify(s).expect("minify failed").to_string(), expected); +} + +#[test] +fn check_space_after_or_not() { + let s = "@supports not ((text-align-last: justify) or (-moz-text-align-last: justify)) {}"; + let expected = "@supports not ((text-align-last:justify) or (-moz-text-align-last:justify)){}"; + assert_eq!(minify(s).expect("minify failed").to_string(), expected); +} + +#[test] +fn check_space_after_brackets() { + let s = "#main[data-behavior = \"1\"] {}"; + let expected = "#main[data-behavior=\"1\"]{}"; + assert_eq!(minify(s).expect("minify failed").to_string(), expected); + + let s = "#main[data-behavior = \"1\"] .aclass"; + let expected = "#main[data-behavior=\"1\"] .aclass"; + assert_eq!(minify(s).expect("minify failed").to_string(), expected); + + let s = "#main[data-behavior = \"1\"] ul.aclass"; + let expected = "#main[data-behavior=\"1\"] ul.aclass"; + assert_eq!(minify(s).expect("minify failed").to_string(), expected); +} + +#[test] +fn check_whitespaces_in_calc() { + let s = ".foo { width: calc(130px + 10%); }"; + let expected = ".foo{width:calc(130px + 10%);}"; + assert_eq!(minify(s).expect("minify failed").to_string(), expected); + + let s = ".foo { width: calc(130px + (45% - 10% + (12 * 2px))); }"; + let expected = ".foo{width:calc(130px + (45% - 10% + (12 * 2px)));}"; + assert_eq!(minify(s).expect("minify failed").to_string(), expected); +} + +#[test] +fn check_weird_comments() { + let s = ".test1 { + font-weight: 30em; +}/**/ +.test2 { + font-weight: 30em; +}/**/ +.test3 { + font-weight: 30em; +}/**/"; + let expected = ".test1{font-weight:30em;}.test2{font-weight:30em;}.test3{font-weight:30em;}"; + assert_eq!(minify(s).expect("minify failed").to_string(), expected); +} + +#[test] +fn check_slash_slash() { + let s = "body { + background-image: url(data:image/webp;base64,c//S4KP//ZZ/19Uj/UA==); +}"; + let expected = "body{background-image:url(data:image/webp;base64,c//S4KP//ZZ/19Uj/UA==);}"; + assert_eq!(minify(s).expect("minify failed").to_string(), expected); +} + +#[test] +fn issue_80() { + assert_eq!( + minify("@import 'i';t{x: #fff;}").unwrap().to_string(), + "@import 'i';t{x:#fff;}", + ); +} diff --git a/vendor/minifier/src/css/token.rs b/vendor/minifier/src/css/token.rs new file mode 100644 index 000000000..d2d738840 --- /dev/null +++ b/vendor/minifier/src/css/token.rs @@ -0,0 +1,875 @@ +// 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<'_>> { + let mut i = 0; + let mut is_in_calc = false; + let mut paren = 0; + + 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; + } + } + + if v[i].is_useless() { + if i > 0 && v[i - 1] == Token::Char(ReservedChar::CloseBracket) { + if i + 1 < v.len() + && (v[i + 1].is_useless() + || v[i + 1] == Token::Char(ReservedChar::OpenCurlyBrace)) + { + v.remove(i); + continue; + } + } else if i > 0 + && (v[i - 1] == Token::Other("and") + || v[i - 1] == Token::Other("or") + || v[i - 1] == Token::Other("not")) + { + // retain the space after "and", "or" or "not" + } else if (is_in_calc && v[i - 1].is_useless()) + || !is_in_calc + && ((i > 0 + && ((v[i - 1].is_char() + && v[i - 1] != Token::Char(ReservedChar::CloseParenthese)) + || v[i - 1].is_a_media() + || v[i - 1].is_a_license())) + || (i < v.len() - 1 && v[i + 1].is_char())) + { + v.remove(i); + continue; + } + } else if v[i].is_comment() { + v.remove(i); + continue; + } + i += 1; + } + 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))); +} |