implement module mixins and meta.load-css

This commit is contained in:
Connor Skees 2020-08-06 03:46:58 -04:00
parent d043167015
commit d029fd2001
9 changed files with 168 additions and 17 deletions

View File

@ -1,21 +1,67 @@
use crate::{args::FuncArgs, Token};
use std::fmt;
use crate::{
args::{CallArgs, FuncArgs},
error::SassResult,
parse::{Parser, Stmt},
Token,
};
pub(crate) type BuiltinMixin = fn(CallArgs, &mut Parser<'_>) -> SassResult<Vec<Stmt>>;
#[derive(Clone)]
pub(crate) enum Mixin {
UserDefined(UserDefinedMixin),
Builtin(BuiltinMixin),
}
impl fmt::Debug for Mixin {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::UserDefined(u) => f
.debug_struct("UserDefinedMixin")
.field("args", &u.args)
.field("body", &u.body)
.field("accepts_content_block", &u.accepts_content_block)
.field("declared_at_root", &u.declared_at_root)
.finish(),
Self::Builtin(..) => f.debug_struct("BuiltinMixin").finish(),
}
}
}
impl Mixin {
pub fn new_user_defined(
args: FuncArgs,
body: Vec<Token>,
accepts_content_block: bool,
declared_at_root: bool,
) -> Self {
Mixin::UserDefined(UserDefinedMixin::new(
args,
body,
accepts_content_block,
declared_at_root,
))
}
}
#[derive(Debug, Clone)]
pub(crate) struct Mixin {
pub(crate) struct UserDefinedMixin {
pub args: FuncArgs,
pub body: Vec<Token>,
pub accepts_content_block: bool,
pub declared_at_root: bool,
}
impl Mixin {
impl UserDefinedMixin {
pub fn new(
args: FuncArgs,
body: Vec<Token>,
accepts_content_block: bool,
declared_at_root: bool,
) -> Self {
Mixin {
Self {
args,
body,
accepts_content_block,

View File

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

View File

@ -8,13 +8,42 @@ use crate::{
modules::Module,
},
error::SassResult,
parse::Parser,
parse::{Parser, Stmt},
value::Value,
};
fn load_css(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
fn load_css(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Vec<Stmt>> {
args.max_args(2)?;
todo!()
// todo: https://github.com/sass/dart-sass/issues/1054
let url = match args.get_err(0, "module")? {
Value::String(s, ..) => s,
v => {
return Err((
format!("$module: {} is not a string.", v.inspect(args.span())?),
args.span(),
)
.into())
}
};
let with = match args.default_arg(1, "with", Value::Null)? {
Value::Map(map) => Some(map),
Value::Null => None,
v => {
return Err((
format!("$with: {} is not a map.", v.inspect(args.span())?),
args.span(),
)
.into())
}
};
if let Some(with) = with {
todo!("`$with` to `load-css` not yet implemented")
} else {
parser.parse_single_import(&url, args.span())
}
}
fn module_functions(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
@ -69,4 +98,6 @@ pub(crate) fn declare(f: &mut Module) {
f.insert_builtin("module-functions", module_functions);
f.insert_builtin("get-function", get_function);
f.insert_builtin("call", call);
f.insert_builtin_mixin("load-css", load_css);
}

View File

@ -6,7 +6,7 @@ use codemap::{Span, Spanned};
use crate::{
args::CallArgs,
atrule::Mixin,
atrule::mixin::{BuiltinMixin, Mixin},
builtin::Builtin,
common::{Identifier, QuoteKind},
error::SassResult,
@ -65,6 +65,25 @@ impl Module {
}
}
pub fn get_mixin(&self, name: Spanned<Identifier>) -> SassResult<Mixin> {
if name.node.as_str().starts_with('-') {
return Err((
"Private members can't be accessed from outside their modules.",
name.span,
)
.into());
}
match self.0.mixins.get(&name.node) {
Some(v) => Ok(v.clone()),
None => Err(("Undefined mixin.", name.span).into()),
}
}
pub fn insert_builtin_mixin(&mut self, name: &'static str, mixin: BuiltinMixin) {
self.0.mixins.insert(name.into(), Mixin::Builtin(mixin));
}
pub fn insert_builtin_var(&mut self, name: &'static str, value: Value) {
self.0.vars.insert(name.into(), value);
}

View File

@ -77,7 +77,11 @@ impl<'a> Parser<'a> {
None
}
fn parse_single_import(&mut self, file_name: &str, span: Span) -> SassResult<Vec<Stmt>> {
pub(crate) fn parse_single_import(
&mut self,
file_name: &str,
span: Span,
) -> SassResult<Vec<Stmt>> {
let path: &Path = file_name.as_ref();
if let Some(name) = self.find_import(path) {

View File

@ -6,7 +6,7 @@ use peekmore::PeekMore;
use crate::{
args::{CallArgs, FuncArgs},
atrule::{Content, Mixin},
atrule::mixin::{Content, Mixin, UserDefinedMixin},
error::SassResult,
scope::Scopes,
utils::read_until_closing_curly_brace,
@ -55,7 +55,7 @@ impl<'a> Parser<'a> {
// this is blocked on figuring out just how to check for this. presumably we could have a check
// not when parsing initially, but rather when `@include`ing to see if an `@content` was found.
let mixin = Mixin::new(args, body, false, self.at_root);
let mixin = Mixin::new_user_defined(args, body, false, self.at_root);
if self.at_root {
self.global_scope.insert_mixin(name, mixin);
@ -73,6 +73,19 @@ impl<'a> Parser<'a> {
self.whitespace_or_comment();
let name = self.parse_identifier()?.map_node(Into::into);
let mixin = if let Some(Token { kind: '.', .. }) = self.toks.peek() {
self.toks.next();
let module = name;
let name = self.parse_identifier()?.map_node(Into::into);
self.modules
.get(module.node, module.span)?
.get_mixin(name)?
} else {
self.scopes.get_mixin(name, self.global_scope)?
};
self.whitespace_or_comment();
let args = if let Some(Token { kind: '(', .. }) = self.toks.peek() {
@ -125,12 +138,17 @@ impl<'a> Parser<'a> {
self.toks.next();
}
let Mixin {
let UserDefinedMixin {
body,
args: fn_args,
declared_at_root,
..
} = self.scopes.get_mixin(name, self.global_scope)?;
} = match mixin {
Mixin::UserDefined(u) => u,
Mixin::Builtin(b) => {
return b(args, self);
}
};
let scope = self.eval_args(fn_args, args)?;

View File

@ -7,7 +7,8 @@ use crate::{
atrule::{
keyframes::{Keyframes, KeyframesRuleSet},
media::MediaRule,
AtRuleKind, Content, SupportsRule, UnknownAtRule,
mixin::Content,
AtRuleKind, SupportsRule, UnknownAtRule,
},
builtin::modules::{
declare_module_color, declare_module_list, declare_module_map, declare_module_math,

View File

@ -3,7 +3,7 @@ use std::collections::BTreeMap;
use codemap::Spanned;
use crate::{
atrule::Mixin,
atrule::mixin::Mixin,
builtin::{modules::Module, GLOBAL_FUNCTIONS},
common::Identifier,
error::SassResult,

View File

@ -30,3 +30,36 @@ fn mixin_exists_module() {
&grass::from_string(input.to_string(), &grass::Options::default()).expect(input)
);
}
#[test]
fn load_css_simple() {
let input = "@use \"sass:meta\";\na {\n @include meta.load-css(load_css_simple);\n}";
tempfile!("load_css_simple.scss", "a { color: red; }");
assert_eq!(
"a a {\n color: red;\n}\n",
&grass::from_string(input.to_string(), &grass::Options::default()).expect(input)
);
}
#[test]
fn load_css_explicit_args() {
let input = "@use \"sass:meta\";\na {\n @include meta.load-css($module: load_css_explicit_args, $with: null);\n}";
tempfile!("load_css_explicit_args.scss", "a { color: red; }");
assert_eq!(
"a a {\n color: red;\n}\n",
&grass::from_string(input.to_string(), &grass::Options::default()).expect(input)
);
}
#[test]
fn load_css_non_string_url() {
let input = "@use \"sass:meta\";\na {\n @include meta.load-css(2);\n}";
tempfile!("load_css_non_string_url.scss", "a { color: red; }");
assert_err!("Error: $module: 2 is not a string.", input);
}
#[test]
fn load_css_non_map_with() {
let input = "@use \"sass:meta\";\na {\n @include meta.load-css(foo, 2);\n}";
assert_err!("Error: $with: 2 is not a map.", input);
}