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)] #[derive(Debug, Clone)]
pub(crate) struct Mixin { pub(crate) struct UserDefinedMixin {
pub args: FuncArgs, pub args: FuncArgs,
pub body: Vec<Token>, pub body: Vec<Token>,
pub accepts_content_block: bool, pub accepts_content_block: bool,
pub declared_at_root: bool, pub declared_at_root: bool,
} }
impl Mixin { impl UserDefinedMixin {
pub fn new( pub fn new(
args: FuncArgs, args: FuncArgs,
body: Vec<Token>, body: Vec<Token>,
accepts_content_block: bool, accepts_content_block: bool,
declared_at_root: bool, declared_at_root: bool,
) -> Self { ) -> Self {
Mixin { Self {
args, args,
body, body,
accepts_content_block, accepts_content_block,

View File

@ -1,6 +1,5 @@
pub(crate) use function::Function; pub(crate) use function::Function;
pub(crate) use kind::AtRuleKind; pub(crate) use kind::AtRuleKind;
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;
@ -8,6 +7,6 @@ mod function;
pub mod keyframes; pub mod keyframes;
mod kind; mod kind;
pub mod media; pub mod media;
mod mixin; pub mod mixin;
mod supports; mod supports;
mod unknown; mod unknown;

View File

@ -8,13 +8,42 @@ use crate::{
modules::Module, modules::Module,
}, },
error::SassResult, error::SassResult,
parse::Parser, parse::{Parser, Stmt},
value::Value, 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)?; 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> { 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("module-functions", module_functions);
f.insert_builtin("get-function", get_function); f.insert_builtin("get-function", get_function);
f.insert_builtin("call", call); 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::{ use crate::{
args::CallArgs, args::CallArgs,
atrule::Mixin, atrule::mixin::{BuiltinMixin, Mixin},
builtin::Builtin, builtin::Builtin,
common::{Identifier, QuoteKind}, common::{Identifier, QuoteKind},
error::SassResult, 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) { pub fn insert_builtin_var(&mut self, name: &'static str, value: Value) {
self.0.vars.insert(name.into(), value); self.0.vars.insert(name.into(), value);
} }

View File

@ -77,7 +77,11 @@ impl<'a> Parser<'a> {
None 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(); let path: &Path = file_name.as_ref();
if let Some(name) = self.find_import(path) { if let Some(name) = self.find_import(path) {

View File

@ -6,7 +6,7 @@ use peekmore::PeekMore;
use crate::{ use crate::{
args::{CallArgs, FuncArgs}, args::{CallArgs, FuncArgs},
atrule::{Content, Mixin}, atrule::mixin::{Content, Mixin, UserDefinedMixin},
error::SassResult, error::SassResult,
scope::Scopes, scope::Scopes,
utils::read_until_closing_curly_brace, 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 // 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. // 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 { if self.at_root {
self.global_scope.insert_mixin(name, mixin); self.global_scope.insert_mixin(name, mixin);
@ -73,6 +73,19 @@ impl<'a> Parser<'a> {
self.whitespace_or_comment(); self.whitespace_or_comment();
let name = self.parse_identifier()?.map_node(Into::into); 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(); self.whitespace_or_comment();
let args = if let Some(Token { kind: '(', .. }) = self.toks.peek() { let args = if let Some(Token { kind: '(', .. }) = self.toks.peek() {
@ -125,12 +138,17 @@ impl<'a> Parser<'a> {
self.toks.next(); self.toks.next();
} }
let Mixin { let UserDefinedMixin {
body, body,
args: fn_args, args: fn_args,
declared_at_root, 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)?; let scope = self.eval_args(fn_args, args)?;

View File

@ -7,7 +7,8 @@ use crate::{
atrule::{ atrule::{
keyframes::{Keyframes, KeyframesRuleSet}, keyframes::{Keyframes, KeyframesRuleSet},
media::MediaRule, media::MediaRule,
AtRuleKind, Content, SupportsRule, UnknownAtRule, mixin::Content,
AtRuleKind, SupportsRule, UnknownAtRule,
}, },
builtin::modules::{ builtin::modules::{
declare_module_color, declare_module_list, declare_module_map, declare_module_math, 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 codemap::Spanned;
use crate::{ use crate::{
atrule::Mixin, atrule::mixin::Mixin,
builtin::{modules::Module, GLOBAL_FUNCTIONS}, builtin::{modules::Module, GLOBAL_FUNCTIONS},
common::Identifier, common::Identifier,
error::SassResult, error::SassResult,

View File

@ -30,3 +30,36 @@ fn mixin_exists_module() {
&grass::from_string(input.to_string(), &grass::Options::default()).expect(input) &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);
}