diff --git a/src/atrule/mixin.rs b/src/atrule/mixin.rs index a85bcf8..b89d2f7 100644 --- a/src/atrule/mixin.rs +++ b/src/atrule/mixin.rs @@ -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>; + +#[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, + 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, pub accepts_content_block: bool, pub declared_at_root: bool, } -impl Mixin { +impl UserDefinedMixin { pub fn new( args: FuncArgs, body: Vec, accepts_content_block: bool, declared_at_root: bool, ) -> Self { - Mixin { + Self { args, body, accepts_content_block, diff --git a/src/atrule/mod.rs b/src/atrule/mod.rs index 7c0bda1..80304d0 100644 --- a/src/atrule/mod.rs +++ b/src/atrule/mod.rs @@ -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; diff --git a/src/builtin/modules/meta.rs b/src/builtin/modules/meta.rs index 1e0062e..bd1274e 100644 --- a/src/builtin/modules/meta.rs +++ b/src/builtin/modules/meta.rs @@ -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 { +fn load_css(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult> { 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 { @@ -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); } diff --git a/src/builtin/modules/mod.rs b/src/builtin/modules/mod.rs index aa47e54..1f1bcf6 100644 --- a/src/builtin/modules/mod.rs +++ b/src/builtin/modules/mod.rs @@ -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) -> SassResult { + 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); } diff --git a/src/parse/import.rs b/src/parse/import.rs index 204787a..ac252af 100644 --- a/src/parse/import.rs +++ b/src/parse/import.rs @@ -77,7 +77,11 @@ impl<'a> Parser<'a> { None } - fn parse_single_import(&mut self, file_name: &str, span: Span) -> SassResult> { + pub(crate) fn parse_single_import( + &mut self, + file_name: &str, + span: Span, + ) -> SassResult> { let path: &Path = file_name.as_ref(); if let Some(name) = self.find_import(path) { diff --git a/src/parse/mixin.rs b/src/parse/mixin.rs index 78841b6..e0c0d63 100644 --- a/src/parse/mixin.rs +++ b/src/parse/mixin.rs @@ -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)?; diff --git a/src/parse/mod.rs b/src/parse/mod.rs index 3c4fbb1..1d5bab7 100644 --- a/src/parse/mod.rs +++ b/src/parse/mod.rs @@ -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, diff --git a/src/scope.rs b/src/scope.rs index 2cef3d5..82f27f0 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -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, diff --git a/tests/meta-module.rs b/tests/meta-module.rs index 1b3eca6..e6ee9ba 100644 --- a/tests/meta-module.rs +++ b/tests/meta-module.rs @@ -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); +}