diff --git a/src/builtin/modules/mod.rs b/src/builtin/modules/mod.rs index 708bbbe..1490adc 100644 --- a/src/builtin/modules/mod.rs +++ b/src/builtin/modules/mod.rs @@ -27,6 +27,34 @@ pub(crate) struct Module(pub Scope); #[derive(Debug, Default)] pub(crate) struct Modules(BTreeMap); +#[derive(Debug, Default)] +pub(crate) struct ModuleConfig(BTreeMap); + +impl ModuleConfig { + /// Removes and returns element with name + pub fn get(&mut self, name: Identifier) -> Option { + self.0.remove(&name) + } + + /// If this structure is not empty at the end of + /// an `@use`, we must throw an error + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + pub fn insert(&mut self, name: Spanned, value: Spanned) -> SassResult<()> { + if self.0.insert(name.node, value.node).is_some() { + Err(( + "The same variable may only be configured once.", + name.span.merge(value.span), + ) + .into()) + } else { + Ok(()) + } + } +} + impl Modules { pub fn insert(&mut self, name: Identifier, module: Module, span: Span) -> SassResult<()> { if self.0.contains_key(&name) { diff --git a/src/lib.rs b/src/lib.rs index 6f20adc..f691da6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -95,7 +95,7 @@ use peekmore::PeekMore; pub use crate::error::{SassError as Error, SassResult as Result}; pub(crate) use crate::token::Token; use crate::{ - builtin::modules::Modules, + builtin::modules::{ModuleConfig, Modules}, lexer::Lexer, output::Css, parse::{ @@ -295,6 +295,7 @@ pub fn from_path(p: &str, options: &Options) -> Result { content_scopes: &mut Scopes::new(), options, modules: &mut Modules::default(), + module_config: &mut ModuleConfig::default(), } .parse() .map_err(|e| raw_to_parse_error(&map, *e, options.unicode_error_messages))?; @@ -340,6 +341,7 @@ pub fn from_string(p: String, options: &Options) -> Result { content_scopes: &mut Scopes::new(), options, modules: &mut Modules::default(), + module_config: &mut ModuleConfig::default(), } .parse() .map_err(|e| raw_to_parse_error(&map, *e, options.unicode_error_messages))?; @@ -376,6 +378,7 @@ pub fn from_string(p: String) -> std::result::Result { content_scopes: &mut Scopes::new(), options: &Options::default(), modules: &mut Modules::default(), + module_config: &mut ModuleConfig::default(), } .parse() .map_err(|e| raw_to_parse_error(&map, *e, true).to_string())?; diff --git a/src/parse/control_flow.rs b/src/parse/control_flow.rs index babbd43..cb0816e 100644 --- a/src/parse/control_flow.rs +++ b/src/parse/control_flow.rs @@ -54,6 +54,7 @@ impl<'a> Parser<'a> { content_scopes: self.content_scopes, options: self.options, modules: self.modules, + module_config: self.module_config, } .parse_stmt()?; } else { @@ -114,6 +115,7 @@ impl<'a> Parser<'a> { content_scopes: self.content_scopes, options: self.options, modules: self.modules, + module_config: self.module_config, } .parse_stmt()?; } else { @@ -143,6 +145,7 @@ impl<'a> Parser<'a> { content_scopes: self.content_scopes, options: self.options, modules: self.modules, + module_config: self.module_config, } .parse_stmt(); } @@ -323,6 +326,7 @@ impl<'a> Parser<'a> { content_scopes: self.content_scopes, options: self.options, modules: self.modules, + module_config: self.module_config, } .parse_stmt()?; if !these_stmts.is_empty() { @@ -346,6 +350,7 @@ impl<'a> Parser<'a> { content_scopes: self.content_scopes, options: self.options, modules: self.modules, + module_config: self.module_config, } .parse_stmt()?, ); @@ -397,6 +402,7 @@ impl<'a> Parser<'a> { content_scopes: self.content_scopes, options: self.options, modules: self.modules, + module_config: self.module_config, } .parse_stmt()?; if !these_stmts.is_empty() { @@ -420,6 +426,7 @@ impl<'a> Parser<'a> { content_scopes: self.content_scopes, options: self.options, modules: self.modules, + module_config: self.module_config, } .parse_stmt()?, ); @@ -512,6 +519,7 @@ impl<'a> Parser<'a> { content_scopes: self.content_scopes, options: self.options, modules: self.modules, + module_config: self.module_config, } .parse_stmt()?; if !these_stmts.is_empty() { @@ -535,6 +543,7 @@ impl<'a> Parser<'a> { content_scopes: self.content_scopes, options: self.options, modules: self.modules, + module_config: self.module_config, } .parse_stmt()?, ); diff --git a/src/parse/function.rs b/src/parse/function.rs index a5a8b19..1f1830a 100644 --- a/src/parse/function.rs +++ b/src/parse/function.rs @@ -121,6 +121,7 @@ impl<'a> Parser<'a> { content_scopes: self.content_scopes, options: self.options, modules: self.modules, + module_config: self.module_config, } .parse_stmt()?; diff --git a/src/parse/import.rs b/src/parse/import.rs index ac252af..cd17e8c 100644 --- a/src/parse/import.rs +++ b/src/parse/import.rs @@ -108,6 +108,7 @@ impl<'a> Parser<'a> { content_scopes: self.content_scopes, options: self.options, modules: self.modules, + module_config: self.module_config, } .parse(); } diff --git a/src/parse/keyframes.rs b/src/parse/keyframes.rs index adf792c..4a5caf9 100644 --- a/src/parse/keyframes.rs +++ b/src/parse/keyframes.rs @@ -174,6 +174,7 @@ impl<'a> Parser<'a> { content_scopes: self.content_scopes, options: self.options, modules: self.modules, + module_config: self.module_config, }) .parse_keyframes_selector()?; @@ -210,6 +211,7 @@ impl<'a> Parser<'a> { content_scopes: self.content_scopes, options: self.options, modules: self.modules, + module_config: self.module_config, } .parse_stmt()?; diff --git a/src/parse/media.rs b/src/parse/media.rs index 2a0c036..61f124a 100644 --- a/src/parse/media.rs +++ b/src/parse/media.rs @@ -25,16 +25,6 @@ impl<'a> Parser<'a> { Ok(false) } - pub fn expect_char(&mut self, c: char) -> SassResult<()> { - if let Some(Token { kind, .. }) = self.toks.peek() { - if *kind == c { - self.toks.next(); - return Ok(()); - } - } - Err((format!("expected \"{}\".", c), self.span_before).into()) - } - pub fn scan_char(&mut self, c: char) -> bool { if let Some(Token { kind, .. }) = self.toks.peek() { if *kind == c { diff --git a/src/parse/mixin.rs b/src/parse/mixin.rs index e0c0d63..6fcc1d6 100644 --- a/src/parse/mixin.rs +++ b/src/parse/mixin.rs @@ -183,6 +183,7 @@ impl<'a> Parser<'a> { content_scopes: self.content_scopes, options: self.options, modules: self.modules, + module_config: self.module_config, } .parse_stmt()?; @@ -245,6 +246,7 @@ impl<'a> Parser<'a> { content_scopes: self.scopes, options: self.options, modules: self.modules, + module_config: self.module_config, } .parse_stmt()? } else { diff --git a/src/parse/mod.rs b/src/parse/mod.rs index 45b67ca..7a29a34 100644 --- a/src/parse/mod.rs +++ b/src/parse/mod.rs @@ -12,7 +12,8 @@ use crate::{ }, builtin::modules::{ declare_module_color, declare_module_list, declare_module_map, declare_module_math, - declare_module_meta, declare_module_selector, declare_module_string, Module, Modules, + declare_module_meta, declare_module_selector, declare_module_string, Module, ModuleConfig, + Modules, }, error::SassResult, lexer::Lexer, @@ -94,6 +95,7 @@ pub(crate) struct Parser<'a> { pub options: &'a Options<'a>, pub modules: &'a mut Modules, + pub module_config: &'a mut ModuleConfig, } impl<'a> Parser<'a> { @@ -113,6 +115,99 @@ impl<'a> Parser<'a> { Ok(stmts) } + pub fn expect_char(&mut self, c: char) -> SassResult<()> { + match self.toks.peek() { + Some(Token { kind, pos }) if *kind == c => { + self.span_before = *pos; + self.toks.next(); + Ok(()) + } + Some(Token { pos, .. }) => Err((format!("expected \"{}\".", c), *pos).into()), + None => Err((format!("expected \"{}\".", c), self.span_before).into()), + } + } + + pub fn consume_char_if_exists(&mut self, c: char) { + if let Some(Token { kind, .. }) = self.toks.peek() { + if *kind == c { + self.toks.next(); + } + } + } + + fn parse_module_alias(&mut self) -> SassResult> { + if let Some(Token { kind: 'a', .. }) | Some(Token { kind: 'A', .. }) = self.toks.peek() { + let mut ident = peek_ident_no_interpolation(self.toks, false, self.span_before)?; + ident.node.make_ascii_lowercase(); + if ident.node != "as" { + return Err(("expected \";\".", ident.span).into()); + } + + self.whitespace_or_comment(); + + if let Some(Token { kind: '*', .. }) = self.toks.peek() { + self.toks.next(); + return Ok(Some('*'.to_string())); + } else { + let name = self.parse_identifier_no_interpolation(false)?; + + return Ok(Some(name.node)); + } + } + + Ok(None) + } + + fn parse_module_config(&mut self) -> SassResult { + let mut config = ModuleConfig::default(); + + if let Some(Token { kind: 'w', .. }) | Some(Token { kind: 'W', .. }) = self.toks.peek() { + let mut ident = peek_ident_no_interpolation(self.toks, false, self.span_before)?; + ident.node.make_ascii_lowercase(); + if ident.node != "with" { + return Err(("expected \";\".", ident.span).into()); + } + + self.whitespace_or_comment(); + + self.span_before = ident.span; + + self.expect_char('(')?; + + loop { + self.whitespace_or_comment(); + self.expect_char('$')?; + + let name = self.parse_identifier_no_interpolation(false)?; + + self.whitespace_or_comment(); + self.expect_char(':')?; + self.whitespace_or_comment(); + + let value = self.parse_value(false, &|toks| match toks.peek() { + Some(Token { kind: ',', .. }) | Some(Token { kind: ')', .. }) => true, + _ => false, + })?; + + config.insert(name.map_node(|n| n.into()), value)?; + + match self.toks.next() { + Some(Token { kind: ',', .. }) => { + continue; + } + Some(Token { kind: ')', .. }) => { + break; + } + Some(..) | None => { + return Err(("expected \")\".", self.span_before).into()); + } + } + } + } + + Ok(config) + } + /// Returns any multiline comments that may have been found /// while loading modules #[allow(clippy::eval_order_dependence)] @@ -156,44 +251,14 @@ impl<'a> Parser<'a> { self.whitespace_or_comment(); - let mut module_alias: Option = None; + let module_alias = self.parse_module_alias()?; - match self.toks.peek() { - Some(Token { kind: ';', .. }) => { - self.toks.next(); - } - Some(Token { kind: 'a', .. }) | Some(Token { kind: 'A', .. }) => { - let mut ident = - peek_ident_no_interpolation(self.toks, false, self.span_before)?; - ident.node.make_ascii_lowercase(); - if ident.node != "as" { - return Err(("expected \";\".", ident.span).into()); - } + self.whitespace_or_comment(); - self.whitespace_or_comment(); + let mut config = self.parse_module_config()?; - let name_span; - - if let Some(Token { kind: '*', pos }) = self.toks.peek() { - name_span = *pos; - self.toks.next(); - module_alias = Some('*'.to_string()); - } else { - let name = self.parse_identifier_no_interpolation(false)?; - - module_alias = Some(name.node); - name_span = name.span; - } - - if !matches!(self.toks.next(), Some(Token { kind: ';', .. })) { - return Err(("expected \";\".", name_span).into()); - } - } - Some(Token { kind: 'w', .. }) | Some(Token { kind: 'W', .. }) => { - todo!("with") - } - Some(..) | None => return Err(("expected \";\".", span).into()), - } + self.whitespace_or_comment(); + self.expect_char(';')?; let module = match module_name.as_ref() { "sass:color" => declare_module_color(), @@ -232,19 +297,28 @@ impl<'a> Parser<'a> { content_scopes: self.content_scopes, options: self.options, modules: self.modules, + module_config: &mut config, } .parse()?, ); + if !config.is_empty() { + return Err(("This variable was not declared with !default in the @used module.", span).into()); + } + Module::new_from_scope(global_scope) } else { - return Err( - ("Error: Can't find stylesheet to import.", span).into() - ); + return Err(("Can't find stylesheet to import.", span).into()); } } }; + // if the config isn't empty here, that means + // variables were passed to a builtin module + if !config.is_empty() { + return Err(("Built-in modules can't be configured.", span).into()); + } + let module_name = match module_alias.as_deref() { Some("*") => { self.global_scope.merge_module(module); @@ -599,6 +673,7 @@ impl<'a> Parser<'a> { content_scopes: self.content_scopes, options: self.options, modules: self.modules, + module_config: self.module_config, }, allows_parent, true, @@ -657,10 +732,11 @@ impl<'a> Parser<'a> { pub fn parse_interpolation(&mut self) -> SassResult> { let val = self.parse_value(true, &|_| false)?; - match self.toks.next() { - Some(Token { kind: '}', .. }) => {} - Some(..) | None => return Err(("expected \"}\".", val.span).into()), - } + + self.span_before = val.span; + + self.expect_char('}')?; + Ok(val.map_node(Value::unquote)) } @@ -899,6 +975,7 @@ impl<'a> Parser<'a> { content_scopes: self.content_scopes, options: self.options, modules: self.modules, + module_config: self.module_config, } .parse_stmt()? .into_iter() @@ -944,14 +1021,14 @@ impl<'a> Parser<'a> { content_scopes: self.content_scopes, options: self.options, modules: self.modules, + module_config: self.module_config, } .parse_selector(false, true, String::new())?; + // todo: this might be superfluous self.whitespace(); - if let Some(Token { kind: ';', .. }) = self.toks.peek() { - self.toks.next(); - } + self.consume_char_if_exists(';'); let extend_rule = ExtendRule::new(value.clone(), is_optional, self.span_before); diff --git a/src/parse/value/parse.rs b/src/parse/value/parse.rs index 5a826ab..1962fba 100644 --- a/src/parse/value/parse.rs +++ b/src/parse/value/parse.rs @@ -199,6 +199,7 @@ impl<'a> Parser<'a> { content_scopes: self.content_scopes, options: self.options, modules: self.modules, + module_config: self.module_config, } .parse_value(in_paren, predicate) } @@ -224,6 +225,7 @@ impl<'a> Parser<'a> { content_scopes: self.content_scopes, options: self.options, modules: self.modules, + module_config: self.module_config, } .parse_value(in_paren, &|_| false) } diff --git a/src/parse/variable.rs b/src/parse/variable.rs index ad08142..78df9b2 100644 --- a/src/parse/variable.rs +++ b/src/parse/variable.rs @@ -39,13 +39,24 @@ impl<'a> Parser<'a> { } = self.parse_variable_value()?; if default { + let config_val = self.module_config.get(ident); if self.at_root && !self.flags.in_control_flow() { if !self.global_scope.var_exists(ident) { - let value = self.parse_value_from_vec(val_toks, true)?.node; + let value = if let Some(config_val) = config_val { + config_val + } else { + self.parse_value_from_vec(val_toks, true)?.node + }; + self.global_scope.insert_var(ident, value); } } else { - let value = self.parse_value_from_vec(val_toks, true)?.node; + let value = if let Some(config_val) = config_val { + config_val + } else { + self.parse_value_from_vec(val_toks, true)?.node + }; + if global && !self.global_scope.var_exists(ident) { self.global_scope.insert_var(ident, value.clone()); } diff --git a/src/value/mod.rs b/src/value/mod.rs index d9b13b3..7e7291a 100644 --- a/src/value/mod.rs +++ b/src/value/mod.rs @@ -543,6 +543,7 @@ impl Value { content_scopes: parser.content_scopes, options: parser.options, modules: parser.modules, + module_config: parser.module_config, } .parse_selector(allows_parent, true, String::new())? .0) diff --git a/tests/use.rs b/tests/use.rs index d30af39..035a86d 100644 --- a/tests/use.rs +++ b/tests/use.rs @@ -191,3 +191,65 @@ fn use_idempotent_builtin() { input ); } + +#[test] +fn use_with_simple() { + let input = "@use \"use_with_simple\" with ($a: red);\na {\n color: use_with_simple.$a;\n}"; + tempfile!("use_with_simple.scss", "$a: green !default;"); + assert_eq!( + "a {\n color: red;\n}\n", + &grass::from_string(input.to_string(), &grass::Options::default()).expect(input) + ); +} + +#[test] +fn use_as_with() { + let input = "@use \"use_as_with\" as module with ($a: red);\na {\n color: module.$a;\n}"; + tempfile!("use_as_with.scss", "$a: green !default;"); + assert_eq!( + "a {\n color: red;\n}\n", + &grass::from_string(input.to_string(), &grass::Options::default()).expect(input) + ); +} + +#[test] +fn use_whitespace_and_comments() { + let input = "@use /**/ \"use_whitespace_and_comments\" /**/ as /**/ foo /**/ with /**/ ( /**/ $a /**/ : /**/ red /**/ ) /**/ ;"; + tempfile!( + "use_whitespace_and_comments.scss", + "$a: green !default; a { color: $a }" + ); + assert_eq!( + "a {\n color: red;\n}\n", + &grass::from_string(input.to_string(), &grass::Options::default()).expect(input) + ); +} + +#[test] +fn use_with_builtin_module() { + let input = "@use \"sass:math\" with ($e: 2.7);"; + + assert_err!("Error: Built-in modules can't be configured.", input); +} + +#[test] +fn use_with_variable_never_used() { + let input = "@use \"use_with_variable_never_used\" with ($a: red);"; + tempfile!("use_with_variable_never_used.scss", ""); + + assert_err!( + "Error: This variable was not declared with !default in the @used module.", + input + ); +} + +#[test] +fn use_with_same_variable_multiple_times() { + let input = "@use \"use_with_same_variable_multiple_times\" as foo with ($a: b, $a: c);"; + tempfile!("use_with_same_variable_multiple_times.scss", ""); + + assert_err!( + "Error: The same variable may only be configured once.", + input + ); +}