From d39a45090a4f44784cd7395ae7c0de6028d25577 Mon Sep 17 00:00:00 2001
From: ConnorSkees <39542938+ConnorSkees@users.noreply.github.com>
Date: Wed, 20 May 2020 20:13:53 -0400
Subject: [PATCH] separate media and unknown at rules

---
 src/atrule/kind.rs       |  3 ++
 src/atrule/media.rs      | 87 ++++++++++++++++++++++++++++++++++++++++
 src/atrule/mod.rs        | 15 ++++++-
 src/atrule/unknown.rs    | 14 ++++++-
 src/lib.rs               |  3 +-
 src/output.rs            | 26 +++++++++---
 src/stylesheet.rs        |  7 +++-
 tests/media.rs           | 13 ++----
 tests/unknown-at-rule.rs | 17 ++++++++
 9 files changed, 165 insertions(+), 20 deletions(-)
 create mode 100644 src/atrule/media.rs
 create mode 100644 tests/unknown-at-rule.rs

diff --git a/src/atrule/kind.rs b/src/atrule/kind.rs
index aef77a0..b24f538 100644
--- a/src/atrule/kind.rs
+++ b/src/atrule/kind.rs
@@ -41,6 +41,8 @@ pub enum AtRuleKind {
     For,
     While,
 
+    Media,
+
     // CSS @rules
     /// Defines the character set used by the style sheet
     Charset,
@@ -78,6 +80,7 @@ impl From<&str> for AtRuleKind {
             "supports" => Self::Supports,
             "keyframes" => Self::Keyframes,
             "content" => Self::Content,
+            "media" => Self::Media,
             s => Self::Unknown(s.to_owned()),
         }
     }
diff --git a/src/atrule/media.rs b/src/atrule/media.rs
new file mode 100644
index 0000000..fdd7092
--- /dev/null
+++ b/src/atrule/media.rs
@@ -0,0 +1,87 @@
+use codemap::{Span, Spanned};
+
+use peekmore::PeekMoreIterator;
+
+use super::parse::ruleset_eval;
+use crate::error::SassResult;
+use crate::scope::Scope;
+use crate::selector::Selector;
+use crate::utils::{devour_whitespace, parse_interpolation};
+use crate::{RuleSet, Stmt, Token};
+
+#[derive(Debug, Clone)]
+pub(crate) struct Media {
+    pub super_selector: Selector,
+    pub params: String,
+    pub body: Vec<Spanned<Stmt>>,
+}
+
+impl Media {
+    pub fn from_tokens<I: Iterator<Item = Token>>(
+        toks: &mut PeekMoreIterator<I>,
+        scope: &mut Scope,
+        super_selector: &Selector,
+        kind_span: Span,
+        content: Option<&[Spanned<Stmt>]>,
+    ) -> SassResult<Media> {
+        let mut params = String::new();
+        while let Some(tok) = toks.next() {
+            match tok.kind {
+                '{' => break,
+                '#' => {
+                    if toks.peek().unwrap().kind == '{' {
+                        toks.next();
+                        let interpolation = parse_interpolation(toks, scope, super_selector)?;
+                        params.push_str(&interpolation.node.to_css_string(interpolation.span)?);
+                        continue;
+                    } else {
+                        params.push(tok.kind);
+                    }
+                }
+                '\n' | ' ' | '\t' => {
+                    devour_whitespace(toks);
+                    params.push(' ');
+                    continue;
+                }
+                _ => {}
+            }
+            params.push(tok.kind);
+        }
+
+        if params.is_empty() {
+            return Err(("Expected identifier.", kind_span).into());
+        }
+
+        let mut raw_body = Vec::new();
+        ruleset_eval(toks, scope, super_selector, false, content, &mut raw_body)?;
+        let mut rules = Vec::with_capacity(raw_body.len());
+        let mut body = Vec::new();
+
+        for stmt in raw_body {
+            match stmt.node {
+                Stmt::Style(..) => body.push(stmt),
+                _ => rules.push(stmt),
+            }
+        }
+
+        if super_selector.is_empty() {
+            body.append(&mut rules);
+        } else {
+            body = vec![Spanned {
+                node: Stmt::RuleSet(RuleSet {
+                    selector: super_selector.clone(),
+                    rules: body,
+                    super_selector: Selector::new(),
+                }),
+                span: kind_span,
+            }];
+            body.append(&mut rules);
+        }
+
+        Ok(Media {
+            super_selector: Selector::new(),
+            params: params.trim().to_owned(),
+            body,
+        })
+    }
+}
diff --git a/src/atrule/mod.rs b/src/atrule/mod.rs
index e78f6e8..f6212c9 100644
--- a/src/atrule/mod.rs
+++ b/src/atrule/mod.rs
@@ -17,6 +17,7 @@ use for_rule::For;
 pub(crate) use function::Function;
 pub(crate) use if_rule::If;
 pub(crate) use kind::AtRuleKind;
+use media::Media;
 pub(crate) use mixin::{eat_include, Mixin};
 use parse::{eat_stmts, eat_stmts_at_root, ruleset_eval};
 use unknown::UnknownAtRule;
@@ -27,6 +28,7 @@ mod for_rule;
 mod function;
 mod if_rule;
 mod kind;
+mod media;
 mod mixin;
 mod parse;
 mod unknown;
@@ -47,12 +49,13 @@ pub(crate) enum AtRule {
     While(While),
     Include(Vec<Spanned<Stmt>>),
     If(If),
+    Media(Media),
     AtRoot(Vec<Spanned<Stmt>>),
 }
 
 impl AtRule {
     pub fn from_tokens<I: Iterator<Item = Token>>(
-        rule: &AtRuleKind,
+        rule: AtRuleKind,
         kind_span: Span,
         toks: &mut PeekMoreIterator<I>,
         scope: &mut Scope,
@@ -246,6 +249,16 @@ impl AtRule {
                 )?),
                 span: kind_span,
             },
+            AtRuleKind::Media => Spanned {
+                node: AtRule::Media(Media::from_tokens(
+                    toks,
+                    scope,
+                    super_selector,
+                    kind_span,
+                    content,
+                )?),
+                span: kind_span,
+            },
             AtRuleKind::Import => todo!("@import not yet implemented"),
             AtRuleKind::Forward => todo!("@forward not yet implemented"),
             AtRuleKind::Supports => todo!("@supports not yet implemented"),
diff --git a/src/atrule/unknown.rs b/src/atrule/unknown.rs
index 12cea3c..afc0d42 100644
--- a/src/atrule/unknown.rs
+++ b/src/atrule/unknown.rs
@@ -20,13 +20,23 @@ pub(crate) struct UnknownAtRule {
 impl UnknownAtRule {
     pub fn from_tokens<I: Iterator<Item = Token>>(
         toks: &mut PeekMoreIterator<I>,
-        name: &str,
+        name: String,
         scope: &mut Scope,
         super_selector: &Selector,
         kind_span: Span,
         content: Option<&[Spanned<Stmt>]>,
     ) -> SassResult<UnknownAtRule> {
         let mut params = String::new();
+        devour_whitespace(toks);
+        if let Some(Token { kind: ';', .. }) | None = toks.peek() {
+            toks.next();
+            return Ok(UnknownAtRule {
+                name,
+                super_selector: Selector::new(),
+                params: String::new(),
+                body: Vec::new(),
+            });
+        }
         while let Some(tok) = toks.next() {
             match tok.kind {
                 '{' => break,
@@ -77,7 +87,7 @@ impl UnknownAtRule {
         }
 
         Ok(UnknownAtRule {
-            name: name.to_owned(),
+            name,
             super_selector: Selector::new(),
             params: params.trim().to_owned(),
             body,
diff --git a/src/lib.rs b/src/lib.rs
index 58ef1c4..7de70f1 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -341,7 +341,7 @@ pub(crate) fn eat_expr<I: Iterator<Item = Token>>(
                 let Spanned { node: ident, span } = eat_ident(toks, scope, super_selector, span)?;
                 devour_whitespace(toks);
                 let rule = AtRule::from_tokens(
-                    &AtRuleKind::from(ident.as_str()),
+                    AtRuleKind::from(ident.as_str()),
                     span,
                     toks,
                     scope,
@@ -364,6 +364,7 @@ pub(crate) fn eat_expr<I: Iterator<Item = Token>>(
                         u @ AtRule::Unknown(..) => Expr::AtRule(u),
                         u @ AtRule::AtRoot(..) => Expr::AtRule(u),
                         u @ AtRule::Include(..) => Expr::AtRule(u),
+                        u @ AtRule::Media(..) => Expr::AtRule(u),
                     },
                     span,
                 }));
diff --git a/src/output.rs b/src/output.rs
index 42344c3..da1d3ae 100644
--- a/src/output.rs
+++ b/src/output.rs
@@ -185,15 +185,29 @@ impl Css {
                 Toplevel::AtRule(r) => {
                     match r {
                         AtRule::Unknown(u) => {
+                            if u.params.is_empty() {
+                                write!(buf, "{}@{}", padding, u.name)?;
+                            } else {
+                                write!(buf, "{}@{} {}", padding, u.name, u.params)?;
+                            }
+
                             if u.body.is_empty() {
+                                writeln!(buf, ";")?;
+                                continue;
+                            } else {
+                                writeln!(buf, " {{")?;
+                            }
+
+                            Css::from_stylesheet(StyleSheet::from_stmts(u.body))?
+                                ._inner_pretty_print(buf, map, nesting + 1)?;
+                            writeln!(buf, "{}}}", padding)?;
+                        }
+                        AtRule::Media(m) => {
+                            if m.body.is_empty() {
                                 continue;
                             }
-                            if u.params.is_empty() {
-                                writeln!(buf, "{}@{} {{", padding, u.name)?;
-                            } else {
-                                writeln!(buf, "{}@{} {} {{", padding, u.name, u.params)?;
-                            }
-                            Css::from_stylesheet(StyleSheet::from_stmts(u.body))?
+                            writeln!(buf, "{}@media {} {{", padding, m.params)?;
+                            Css::from_stylesheet(StyleSheet::from_stmts(m.body))?
                                 ._inner_pretty_print(buf, map, nesting + 1)?;
                             writeln!(buf, "{}}}", padding)?;
                         }
diff --git a/src/stylesheet.rs b/src/stylesheet.rs
index 87ef088..b033030 100644
--- a/src/stylesheet.rs
+++ b/src/stylesheet.rs
@@ -251,7 +251,7 @@ impl<'a> StyleSheetParser<'a> {
                             });
                         }
                         v => {
-                            let rule = AtRule::from_tokens(&v, span, &mut self.lexer, &mut Scope::new(), &Selector::new(), None)?;
+                            let rule = AtRule::from_tokens(v, span, &mut self.lexer, &mut Scope::new(), &Selector::new(), None)?;
                             match rule.node {
                                 AtRule::Mixin(name, mixin) => {
                                     insert_global_mixin(&name, *mixin);
@@ -281,6 +281,7 @@ impl<'a> StyleSheetParser<'a> {
                                 }
                                 AtRule::AtRoot(root_rules) => rules.extend(root_rules),
                                 AtRule::Unknown(..) => rules.push(rule.map_node(Stmt::AtRule)),
+                                AtRule::Media(..) => rules.push(rule.map_node(Stmt::AtRule)),
                             }
                         }
                     }
@@ -339,6 +340,10 @@ impl<'a> StyleSheetParser<'a> {
                         node: Stmt::AtRule(r),
                         span,
                     }),
+                    r @ AtRule::Media(..) => stmts.push(Spanned {
+                        node: Stmt::AtRule(r),
+                        span,
+                    }),
                 },
                 Expr::Styles(s) => stmts.extend(
                     s.into_iter()
diff --git a/tests/media.rs b/tests/media.rs
index 1dde3bd..10a4e92 100644
--- a/tests/media.rs
+++ b/tests/media.rs
@@ -7,18 +7,13 @@ test!(
     basic_toplevel,
     "@media foo {\n  a {\n    color: red;\n  }\n}\n"
 );
-test!(
-    toplevel_no_params,
-    "@media {\n  a {\n    color: red;\n  }\n}\n"
+error!(
+    no_params,
+    "@media {\n  a {\n    color: red;\n  }\n}\n", "Error: Expected identifier."
 );
 test!(
     basic_nested,
     "a {\n  @media foo {\n  color: red;\n  }\n}\n",
     "@media foo {\n  a {\n    color: red;\n  }\n}\n"
 );
-test!(
-    basic_unknown_at_rule,
-    "@foo {\n  a {\n    color: red;\n  }\n}\n"
-);
-test!(unknown_at_rule_no_selector, "@foo {\n  color: red;\n}\n");
-test!(empty, "@media (min-width: 2px) {}", "");
+test!(empty_body, "@media (min-width: 2px) {}", "");
diff --git a/tests/unknown-at-rule.rs b/tests/unknown-at-rule.rs
new file mode 100644
index 0000000..3abfe13
--- /dev/null
+++ b/tests/unknown-at-rule.rs
@@ -0,0 +1,17 @@
+#![cfg(test)]
+
+#[macro_use]
+mod macros;
+
+test!(
+    basic_unknown_at_rule,
+    "@foo {\n  a {\n    color: red;\n  }\n}\n"
+);
+test!(unknown_at_rule_no_selector, "@foo {\n  color: red;\n}\n");
+test!(unknown_at_rule_no_body, "@foo;\n");
+test!(unknown_at_rule_no_body_eof, "@foo", "@foo;\n");
+test!(
+    unknown_at_rule_interpolated_eof_no_body,
+    "@#{()if(0,0<0,0)}",
+    "@false;\n"
+);