From a88f07da5451d875c60384126dd1904c9377133e Mon Sep 17 00:00:00 2001 From: Connor Skees Date: Thu, 2 Jul 2020 10:31:32 -0400 Subject: [PATCH] allow `@content` to take arguments --- src/args.rs | 9 +++ src/atrule/mixin.rs | 14 ++++ src/atrule/mod.rs | 2 +- src/builtin/meta.rs | 2 +- src/lib.rs | 7 +- src/parse/args.rs | 1 + src/parse/common.rs | 1 + src/parse/mixin.rs | 152 ++++++++++++++++++++++++++++++++------------ src/parse/mod.rs | 18 +----- tests/mixins.rs | 65 +++++++++++++++++++ 10 files changed, 209 insertions(+), 62 deletions(-) diff --git a/src/args.rs b/src/args.rs index ea1626f..0314f1f 100644 --- a/src/args.rs +++ b/src/args.rs @@ -23,6 +23,15 @@ impl FuncArgs { pub const fn new() -> Self { FuncArgs(Vec::new()) } + + pub fn len(&self) -> usize { + self.0.len() + } + + #[allow(dead_code)] + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } } #[derive(Debug, Clone)] diff --git a/src/atrule/mixin.rs b/src/atrule/mixin.rs index 3d32fc8..436d28a 100644 --- a/src/atrule/mixin.rs +++ b/src/atrule/mixin.rs @@ -23,3 +23,17 @@ impl Mixin { } } } + +pub(crate) struct Content { + pub content: Option>, + pub content_args: Option, +} + +impl Content { + pub const fn new() -> Self { + Self { + content: None, + content_args: None, + } + } +} diff --git a/src/atrule/mod.rs b/src/atrule/mod.rs index e12a576..c84a063 100644 --- a/src/atrule/mod.rs +++ b/src/atrule/mod.rs @@ -1,6 +1,6 @@ pub(crate) use function::Function; pub(crate) use kind::AtRuleKind; -pub(crate) use mixin::Mixin; +pub(crate) use mixin::{Content, Mixin}; pub(crate) use supports::SupportsRule; pub(crate) use unknown::UnknownAtRule; diff --git a/src/builtin/meta.rs b/src/builtin/meta.rs index 6868de7..ac1638d 100644 --- a/src/builtin/meta.rs +++ b/src/builtin/meta.rs @@ -245,7 +245,7 @@ fn content_exists(args: CallArgs, parser: &mut Parser<'_>) -> SassResult ) .into()); } - Ok(Value::bool(parser.content.is_some())) + Ok(Value::bool(parser.content.content.is_some())) } pub(crate) fn declare(f: &mut GlobalFunctionMap) { diff --git a/src/lib.rs b/src/lib.rs index 46fde45..982c78d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -102,6 +102,7 @@ use peekmore::PeekMore; pub use crate::error::{SassError as Error, SassResult as Result}; pub(crate) use crate::token::Token; use crate::{ + atrule::Content, lexer::Lexer, output::Css, parse::{common::NeverEmptyVec, Parser}, @@ -159,7 +160,7 @@ pub fn from_path(p: &str) -> Result { global_scope: &mut Scope::new(), super_selectors: &mut NeverEmptyVec::new(Selector::new(empty_span)), span_before: empty_span, - content: None, + content: &Content::new(), in_mixin: false, in_function: false, in_control_flow: false, @@ -205,7 +206,7 @@ pub fn from_string(p: String) -> Result { global_scope: &mut Scope::new(), super_selectors: &mut NeverEmptyVec::new(Selector::new(empty_span)), span_before: empty_span, - content: None, + content: &Content::new(), in_mixin: false, in_function: false, in_control_flow: false, @@ -241,7 +242,7 @@ pub fn from_string(p: String) -> std::result::Result { global_scope: &mut Scope::new(), super_selectors: &mut NeverEmptyVec::new(Selector::new(empty_span)), span_before: empty_span, - content: None, + content: &Content::new(), in_mixin: false, in_function: false, in_control_flow: false, diff --git a/src/parse/args.rs b/src/parse/args.rs index fdc4a8e..e7ee5e8 100644 --- a/src/parse/args.rs +++ b/src/parse/args.rs @@ -115,6 +115,7 @@ impl<'a> Parser<'a> { self.whitespace(); } self.whitespace(); + // TODO: this should NOT eat the opening curly brace match self.toks.next() { Some(v) if v.kind == '{' => {} Some(..) | None => return Err(("expected \"{\".", close_paren_span).into()), diff --git a/src/parse/common.rs b/src/parse/common.rs index a34c093..03f3cdd 100644 --- a/src/parse/common.rs +++ b/src/parse/common.rs @@ -4,6 +4,7 @@ use codemap::Spanned; use crate::{value::Value, Token}; +#[derive(Debug, Clone)] pub(crate) struct NeverEmptyVec { first: T, rest: Vec, diff --git a/src/parse/mixin.rs b/src/parse/mixin.rs index 9c27086..a82fae1 100644 --- a/src/parse/mixin.rs +++ b/src/parse/mixin.rs @@ -6,8 +6,9 @@ use peekmore::PeekMore; use crate::{ args::{CallArgs, FuncArgs}, - atrule::Mixin, + atrule::{Content, Mixin}, error::SassResult, + scope::Scope, utils::read_until_closing_curly_brace, value::Value, Token, @@ -58,58 +59,79 @@ impl<'a> Parser<'a> { self.whitespace_or_comment(); - let mut has_content = false; - - let args = match self.toks.next() { - Some(Token { kind: ';', .. }) => CallArgs::new(name.span), - Some(Token { kind: '(', .. }) => { - let tmp = self.parse_call_args()?; - self.whitespace_or_comment(); - if let Some(tok) = self.toks.peek() { - match tok.kind { - ';' => { - self.toks.next(); - } - '{' => { - self.toks.next(); - has_content = true - } - _ => {} - } - } - tmp - } - Some(Token { kind: '{', .. }) => { - has_content = true; - CallArgs::new(name.span) - } - Some(Token { pos, .. }) => return Err(("expected \"{\".", pos).into()), - None => return Err(("expected \"{\".", name.span).into()), + let args = if let Some(Token { kind: '(', .. }) = self.toks.peek() { + self.toks.next(); + self.parse_call_args()? + } else { + CallArgs::new(name.span) }; - self.whitespace(); + self.whitespace_or_comment(); - let content = if has_content { - Some(self.parse_content()?) + let content_args = if let Some(Token { kind: 'u', .. }) | Some(Token { kind: 'U', .. }) = + self.toks.peek() + { + let mut ident = self.parse_identifier_no_interpolation(false)?; + ident.node.make_ascii_lowercase(); + if ident.node == "using" { + self.whitespace_or_comment(); + if !matches!(self.toks.next(), Some(Token { kind: '(', .. })) { + return Err(("expected \"(\".", ident.span).into()); + } + + Some(self.parse_func_args()?) + } else { + return Err(("expected keyword \"using\".", ident.span).into()); + } } else { None }; - let mut mixin = self.scopes.last().get_mixin(name, self.global_scope)?; - self.eval_mixin_args(&mut mixin, args)?; + self.whitespace_or_comment(); + + let content = if content_args.is_some() + || matches!(self.toks.peek(), Some(Token { kind: '{', .. })) + { + if matches!(self.toks.peek(), Some(Token { kind: '{', .. })) { + self.toks.next(); + } + let mut toks = read_until_closing_curly_brace(self.toks)?; + if let Some(tok) = self.toks.peek() { + toks.push(*tok); + self.toks.next(); + } + Some(toks) + } else { + None + }; + + if let Some(Token { kind: ';', .. }) = self.toks.peek() { + self.toks.next(); + } + + let Mixin { + mut scope, + body, + args: fn_args, + .. + } = self.scopes.last().get_mixin(name, self.global_scope)?; + self.eval_args(fn_args, args, &mut scope)?; let body = Parser { - toks: &mut mixin.body.into_iter().peekmore(), + toks: &mut body.into_iter().peekmore(), map: self.map, path: self.path, - scopes: &mut NeverEmptyVec::new(mixin.scope), + scopes: &mut NeverEmptyVec::new(scope), global_scope: self.global_scope, super_selectors: self.super_selectors, span_before: self.span_before, in_mixin: true, in_function: self.in_function, in_control_flow: self.in_control_flow, - content: content.as_deref(), + content: &Content { + content, + content_args, + }, at_root: false, at_root_has_selector: self.at_root_has_selector, extender: self.extender, @@ -119,18 +141,64 @@ impl<'a> Parser<'a> { Ok(body) } - pub(super) fn parse_content(&mut self) -> SassResult> { - self.parse_stmt() + pub(super) fn parse_content_rule(&mut self) -> SassResult> { + if self.in_mixin { + let mut scope = self.scopes.last().clone(); + if let Some(Token { kind: '(', .. }) = self.toks.peek() { + self.toks.next(); + let args = self.parse_call_args()?; + if let Some(content_args) = self.content.content_args.clone() { + args.max_args(content_args.len())?; + + self.eval_args(content_args, args, &mut scope)?; + } else { + args.max_args(0)?; + } + } + + Ok(if let Some(content) = &self.content.content { + Parser { + toks: &mut content.to_vec().into_iter().peekmore(), + map: self.map, + path: self.path, + scopes: &mut NeverEmptyVec::new(scope), + global_scope: self.global_scope, + super_selectors: self.super_selectors, + span_before: self.span_before, + in_mixin: false, + in_function: self.in_function, + in_control_flow: self.in_control_flow, + content: self.content, + at_root: false, + at_root_has_selector: self.at_root_has_selector, + extender: self.extender, + } + .parse()? + } else { + Vec::new() + }) + } else { + Err(( + "@content is only allowed within mixin declarations.", + self.span_before, + ) + .into()) + } } - fn eval_mixin_args(&mut self, mixin: &mut Mixin, mut args: CallArgs) -> SassResult<()> { + fn eval_args( + &mut self, + mut fn_args: FuncArgs, + mut args: CallArgs, + scope: &mut Scope, + ) -> SassResult<()> { self.scopes.push(self.scopes.last().clone()); - for (idx, arg) in mixin.args.0.iter_mut().enumerate() { + for (idx, arg) in fn_args.0.iter_mut().enumerate() { if arg.is_variadic { let span = args.span(); // todo: does this get the most recent scope? let arg_list = Value::ArgList(self.variadic_args(args)?); - mixin.scope.insert_var( + scope.insert_var( arg.name.clone(), Spanned { node: arg_list, @@ -153,7 +221,7 @@ impl<'a> Parser<'a> { self.scopes .last_mut() .insert_var(arg.name.clone(), val.clone())?; - mixin.scope.insert_var(mem::take(&mut arg.name), val)?; + scope.insert_var(mem::take(&mut arg.name), val)?; } self.scopes.pop(); Ok(()) diff --git a/src/parse/mod.rs b/src/parse/mod.rs index e685452..2bdb86e 100644 --- a/src/parse/mod.rs +++ b/src/parse/mod.rs @@ -5,7 +5,7 @@ use num_traits::cast::ToPrimitive; use peekmore::{PeekMore, PeekMoreIterator}; use crate::{ - atrule::{media::MediaRule, AtRuleKind, SupportsRule, UnknownAtRule}, + atrule::{media::MediaRule, AtRuleKind, Content, SupportsRule, UnknownAtRule}, common::{Brackets, ListSeparator}, error::SassResult, scope::Scope, @@ -70,7 +70,7 @@ pub(crate) struct Parser<'a> { pub scopes: &'a mut NeverEmptyVec, pub super_selectors: &'a mut NeverEmptyVec, pub span_before: Span, - pub content: Option<&'a [Stmt]>, + pub content: &'a Content, pub in_mixin: bool, pub in_function: bool, pub in_control_flow: bool, @@ -111,19 +111,7 @@ impl<'a> Parser<'a> { match AtRuleKind::try_from(&kind_string)? { AtRuleKind::Import => stmts.append(&mut self.import()?), AtRuleKind::Mixin => self.parse_mixin()?, - AtRuleKind::Content => { - if self.in_mixin { - if let Some(content) = &self.content { - stmts.extend_from_slice(content); - } - } else { - return Err(( - "@content is only allowed within mixin declarations.", - kind_string.span, - ) - .into()); - } - } + AtRuleKind::Content => stmts.append(&mut self.parse_content_rule()?), AtRuleKind::Include => stmts.append(&mut self.parse_include()?), AtRuleKind::Function => self.parse_function()?, AtRuleKind::Return => { diff --git a/tests/mixins.rs b/tests/mixins.rs index a5cf865..51f9299 100644 --- a/tests/mixins.rs +++ b/tests/mixins.rs @@ -247,3 +247,68 @@ test!( "@mixin foo {}\n\na {\n $a: red;\n @include foo;\n color: $a;\n}\n", "a {\n color: red;\n}\n" ); +test!( + empty_content_args, + "@mixin foo { + @content() + } + + a { + @include foo { + color: red; + }; + }", + "a {\n color: red;\n}\n" +); +test!( + empty_content_args_using_empty_args, + "@mixin foo { + @content() + } + + a { + @include foo using () { + color: red; + }; + }", + "a {\n color: red;\n}\n" +); +test!( + content_using_one_arg, + "@mixin foo { + @content(red) + } + + a { + @include foo using ($a) { + color: $a; + } + }", + "a {\n color: red;\n}\n" +); +error!( + content_using_too_many_args, + "@mixin foo { + @content(red, blue) + } + + a { + @include foo using ($a) { + color: $a; + } + }", + "Error: Only 1 argument allowed, but 2 were passed." +); +error!( + content_using_too_few_args, + "@mixin foo { + @content() + } + + a { + @include foo using ($a) { + color: $a; + } + }", + "Error: Missing argument $a." +);