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>, +} + +impl Media { + pub fn from_tokens>( + toks: &mut PeekMoreIterator, + scope: &mut Scope, + super_selector: &Selector, + kind_span: Span, + content: Option<&[Spanned]>, + ) -> SassResult { + 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>), If(If), + Media(Media), AtRoot(Vec>), } impl AtRule { pub fn from_tokens>( - rule: &AtRuleKind, + rule: AtRuleKind, kind_span: Span, toks: &mut PeekMoreIterator, 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>( toks: &mut PeekMoreIterator, - name: &str, + name: String, scope: &mut Scope, super_selector: &Selector, kind_span: Span, content: Option<&[Spanned]>, ) -> SassResult { 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>( 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>( 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" +);