summaryrefslogtreecommitdiffstats
path: root/vendor/minifier/src/css
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-17 12:02:58 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-17 12:02:58 +0000
commit698f8c2f01ea549d77d7dc3338a12e04c11057b9 (patch)
tree173a775858bd501c378080a10dca74132f05bc50 /vendor/minifier/src/css
parentInitial commit. (diff)
downloadrustc-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.rs40
-rw-r--r--vendor/minifier/src/css/tests.rs286
-rw-r--r--vendor/minifier/src/css/token.rs875
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)));
+}