allow @content to take arguments

This commit is contained in:
Connor Skees 2020-07-02 10:31:32 -04:00
parent 4b98ec198d
commit a88f07da54
10 changed files with 209 additions and 62 deletions

View File

@ -23,6 +23,15 @@ impl FuncArgs {
pub const fn new() -> Self { pub const fn new() -> Self {
FuncArgs(Vec::new()) 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)] #[derive(Debug, Clone)]

View File

@ -23,3 +23,17 @@ impl Mixin {
} }
} }
} }
pub(crate) struct Content {
pub content: Option<Vec<Token>>,
pub content_args: Option<FuncArgs>,
}
impl Content {
pub const fn new() -> Self {
Self {
content: None,
content_args: None,
}
}
}

View File

@ -1,6 +1,6 @@
pub(crate) use function::Function; pub(crate) use function::Function;
pub(crate) use kind::AtRuleKind; pub(crate) use kind::AtRuleKind;
pub(crate) use mixin::Mixin; pub(crate) use mixin::{Content, Mixin};
pub(crate) use supports::SupportsRule; pub(crate) use supports::SupportsRule;
pub(crate) use unknown::UnknownAtRule; pub(crate) use unknown::UnknownAtRule;

View File

@ -245,7 +245,7 @@ fn content_exists(args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value>
) )
.into()); .into());
} }
Ok(Value::bool(parser.content.is_some())) Ok(Value::bool(parser.content.content.is_some()))
} }
pub(crate) fn declare(f: &mut GlobalFunctionMap) { pub(crate) fn declare(f: &mut GlobalFunctionMap) {

View File

@ -102,6 +102,7 @@ use peekmore::PeekMore;
pub use crate::error::{SassError as Error, SassResult as Result}; pub use crate::error::{SassError as Error, SassResult as Result};
pub(crate) use crate::token::Token; pub(crate) use crate::token::Token;
use crate::{ use crate::{
atrule::Content,
lexer::Lexer, lexer::Lexer,
output::Css, output::Css,
parse::{common::NeverEmptyVec, Parser}, parse::{common::NeverEmptyVec, Parser},
@ -159,7 +160,7 @@ pub fn from_path(p: &str) -> Result<String> {
global_scope: &mut Scope::new(), global_scope: &mut Scope::new(),
super_selectors: &mut NeverEmptyVec::new(Selector::new(empty_span)), super_selectors: &mut NeverEmptyVec::new(Selector::new(empty_span)),
span_before: empty_span, span_before: empty_span,
content: None, content: &Content::new(),
in_mixin: false, in_mixin: false,
in_function: false, in_function: false,
in_control_flow: false, in_control_flow: false,
@ -205,7 +206,7 @@ pub fn from_string(p: String) -> Result<String> {
global_scope: &mut Scope::new(), global_scope: &mut Scope::new(),
super_selectors: &mut NeverEmptyVec::new(Selector::new(empty_span)), super_selectors: &mut NeverEmptyVec::new(Selector::new(empty_span)),
span_before: empty_span, span_before: empty_span,
content: None, content: &Content::new(),
in_mixin: false, in_mixin: false,
in_function: false, in_function: false,
in_control_flow: false, in_control_flow: false,
@ -241,7 +242,7 @@ pub fn from_string(p: String) -> std::result::Result<String, JsValue> {
global_scope: &mut Scope::new(), global_scope: &mut Scope::new(),
super_selectors: &mut NeverEmptyVec::new(Selector::new(empty_span)), super_selectors: &mut NeverEmptyVec::new(Selector::new(empty_span)),
span_before: empty_span, span_before: empty_span,
content: None, content: &Content::new(),
in_mixin: false, in_mixin: false,
in_function: false, in_function: false,
in_control_flow: false, in_control_flow: false,

View File

@ -115,6 +115,7 @@ impl<'a> Parser<'a> {
self.whitespace(); self.whitespace();
} }
self.whitespace(); self.whitespace();
// TODO: this should NOT eat the opening curly brace
match self.toks.next() { match self.toks.next() {
Some(v) if v.kind == '{' => {} Some(v) if v.kind == '{' => {}
Some(..) | None => return Err(("expected \"{\".", close_paren_span).into()), Some(..) | None => return Err(("expected \"{\".", close_paren_span).into()),

View File

@ -4,6 +4,7 @@ use codemap::Spanned;
use crate::{value::Value, Token}; use crate::{value::Value, Token};
#[derive(Debug, Clone)]
pub(crate) struct NeverEmptyVec<T> { pub(crate) struct NeverEmptyVec<T> {
first: T, first: T,
rest: Vec<T>, rest: Vec<T>,

View File

@ -6,8 +6,9 @@ use peekmore::PeekMore;
use crate::{ use crate::{
args::{CallArgs, FuncArgs}, args::{CallArgs, FuncArgs},
atrule::Mixin, atrule::{Content, Mixin},
error::SassResult, error::SassResult,
scope::Scope,
utils::read_until_closing_curly_brace, utils::read_until_closing_curly_brace,
value::Value, value::Value,
Token, Token,
@ -58,58 +59,79 @@ impl<'a> Parser<'a> {
self.whitespace_or_comment(); self.whitespace_or_comment();
let mut has_content = false; let args = if let Some(Token { kind: '(', .. }) = self.toks.peek() {
self.toks.next();
let args = match self.toks.next() { self.parse_call_args()?
Some(Token { kind: ';', .. }) => CallArgs::new(name.span), } else {
Some(Token { kind: '(', .. }) => { CallArgs::new(name.span)
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()),
}; };
self.whitespace(); self.whitespace_or_comment();
let content = if has_content { let content_args = if let Some(Token { kind: 'u', .. }) | Some(Token { kind: 'U', .. }) =
Some(self.parse_content()?) 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 { } else {
None None
}; };
let mut mixin = self.scopes.last().get_mixin(name, self.global_scope)?; self.whitespace_or_comment();
self.eval_mixin_args(&mut mixin, args)?;
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 { let body = Parser {
toks: &mut mixin.body.into_iter().peekmore(), toks: &mut body.into_iter().peekmore(),
map: self.map, map: self.map,
path: self.path, path: self.path,
scopes: &mut NeverEmptyVec::new(mixin.scope), scopes: &mut NeverEmptyVec::new(scope),
global_scope: self.global_scope, global_scope: self.global_scope,
super_selectors: self.super_selectors, super_selectors: self.super_selectors,
span_before: self.span_before, span_before: self.span_before,
in_mixin: true, in_mixin: true,
in_function: self.in_function, in_function: self.in_function,
in_control_flow: self.in_control_flow, in_control_flow: self.in_control_flow,
content: content.as_deref(), content: &Content {
content,
content_args,
},
at_root: false, at_root: false,
at_root_has_selector: self.at_root_has_selector, at_root_has_selector: self.at_root_has_selector,
extender: self.extender, extender: self.extender,
@ -119,18 +141,64 @@ impl<'a> Parser<'a> {
Ok(body) Ok(body)
} }
pub(super) fn parse_content(&mut self) -> SassResult<Vec<Stmt>> { pub(super) fn parse_content_rule(&mut self) -> SassResult<Vec<Stmt>> {
self.parse_stmt() 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()); 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 { if arg.is_variadic {
let span = args.span(); let span = args.span();
// todo: does this get the most recent scope? // todo: does this get the most recent scope?
let arg_list = Value::ArgList(self.variadic_args(args)?); let arg_list = Value::ArgList(self.variadic_args(args)?);
mixin.scope.insert_var( scope.insert_var(
arg.name.clone(), arg.name.clone(),
Spanned { Spanned {
node: arg_list, node: arg_list,
@ -153,7 +221,7 @@ impl<'a> Parser<'a> {
self.scopes self.scopes
.last_mut() .last_mut()
.insert_var(arg.name.clone(), val.clone())?; .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(); self.scopes.pop();
Ok(()) Ok(())

View File

@ -5,7 +5,7 @@ use num_traits::cast::ToPrimitive;
use peekmore::{PeekMore, PeekMoreIterator}; use peekmore::{PeekMore, PeekMoreIterator};
use crate::{ use crate::{
atrule::{media::MediaRule, AtRuleKind, SupportsRule, UnknownAtRule}, atrule::{media::MediaRule, AtRuleKind, Content, SupportsRule, UnknownAtRule},
common::{Brackets, ListSeparator}, common::{Brackets, ListSeparator},
error::SassResult, error::SassResult,
scope::Scope, scope::Scope,
@ -70,7 +70,7 @@ pub(crate) struct Parser<'a> {
pub scopes: &'a mut NeverEmptyVec<Scope>, pub scopes: &'a mut NeverEmptyVec<Scope>,
pub super_selectors: &'a mut NeverEmptyVec<Selector>, pub super_selectors: &'a mut NeverEmptyVec<Selector>,
pub span_before: Span, pub span_before: Span,
pub content: Option<&'a [Stmt]>, pub content: &'a Content,
pub in_mixin: bool, pub in_mixin: bool,
pub in_function: bool, pub in_function: bool,
pub in_control_flow: bool, pub in_control_flow: bool,
@ -111,19 +111,7 @@ impl<'a> Parser<'a> {
match AtRuleKind::try_from(&kind_string)? { match AtRuleKind::try_from(&kind_string)? {
AtRuleKind::Import => stmts.append(&mut self.import()?), AtRuleKind::Import => stmts.append(&mut self.import()?),
AtRuleKind::Mixin => self.parse_mixin()?, AtRuleKind::Mixin => self.parse_mixin()?,
AtRuleKind::Content => { AtRuleKind::Content => stmts.append(&mut self.parse_content_rule()?),
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::Include => stmts.append(&mut self.parse_include()?), AtRuleKind::Include => stmts.append(&mut self.parse_include()?),
AtRuleKind::Function => self.parse_function()?, AtRuleKind::Function => self.parse_function()?,
AtRuleKind::Return => { AtRuleKind::Return => {

View File

@ -247,3 +247,68 @@ test!(
"@mixin foo {}\n\na {\n $a: red;\n @include foo;\n color: $a;\n}\n", "@mixin foo {}\n\na {\n $a: red;\n @include foo;\n color: $a;\n}\n",
"a {\n color: red;\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."
);