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 {
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)]

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 kind::AtRuleKind;
pub(crate) use mixin::Mixin;
pub(crate) use mixin::{Content, Mixin};
pub(crate) use supports::SupportsRule;
pub(crate) use unknown::UnknownAtRule;

View File

@ -245,7 +245,7 @@ fn content_exists(args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value>
)
.into());
}
Ok(Value::bool(parser.content.is_some()))
Ok(Value::bool(parser.content.content.is_some()))
}
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(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<String> {
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<String> {
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<String, JsValue> {
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,

View File

@ -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()),

View File

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

View File

@ -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<Vec<Stmt>> {
self.parse_stmt()
pub(super) fn parse_content_rule(&mut self) -> SassResult<Vec<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());
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(())

View File

@ -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<Scope>,
pub super_selectors: &'a mut NeverEmptyVec<Selector>,
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 => {

View File

@ -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."
);