support the @use ... with (...) syntax

This commit is contained in:
Connor Skees 2020-08-06 21:00:34 -04:00
parent 94becb4dcb
commit 074d679cbd
13 changed files with 248 additions and 59 deletions

View File

@ -27,6 +27,34 @@ pub(crate) struct Module(pub Scope);
#[derive(Debug, Default)] #[derive(Debug, Default)]
pub(crate) struct Modules(BTreeMap<Identifier, Module>); pub(crate) struct Modules(BTreeMap<Identifier, Module>);
#[derive(Debug, Default)]
pub(crate) struct ModuleConfig(BTreeMap<Identifier, Value>);
impl ModuleConfig {
/// Removes and returns element with name
pub fn get(&mut self, name: Identifier) -> Option<Value> {
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<Identifier>, value: Spanned<Value>) -> 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 { impl Modules {
pub fn insert(&mut self, name: Identifier, module: Module, span: Span) -> SassResult<()> { pub fn insert(&mut self, name: Identifier, module: Module, span: Span) -> SassResult<()> {
if self.0.contains_key(&name) { if self.0.contains_key(&name) {

View File

@ -95,7 +95,7 @@ use peekmore::PeekMore;
pub use crate::error::{SassError as Error, SassResult as Result}; pub use crate::error::{SassError as Error, SassResult as Result};
pub(crate) use crate::token::Token; pub(crate) use crate::token::Token;
use crate::{ use crate::{
builtin::modules::Modules, builtin::modules::{ModuleConfig, Modules},
lexer::Lexer, lexer::Lexer,
output::Css, output::Css,
parse::{ parse::{
@ -295,6 +295,7 @@ pub fn from_path(p: &str, options: &Options) -> Result<String> {
content_scopes: &mut Scopes::new(), content_scopes: &mut Scopes::new(),
options, options,
modules: &mut Modules::default(), modules: &mut Modules::default(),
module_config: &mut ModuleConfig::default(),
} }
.parse() .parse()
.map_err(|e| raw_to_parse_error(&map, *e, options.unicode_error_messages))?; .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<String> {
content_scopes: &mut Scopes::new(), content_scopes: &mut Scopes::new(),
options, options,
modules: &mut Modules::default(), modules: &mut Modules::default(),
module_config: &mut ModuleConfig::default(),
} }
.parse() .parse()
.map_err(|e| raw_to_parse_error(&map, *e, options.unicode_error_messages))?; .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<String, JsValue> {
content_scopes: &mut Scopes::new(), content_scopes: &mut Scopes::new(),
options: &Options::default(), options: &Options::default(),
modules: &mut Modules::default(), modules: &mut Modules::default(),
module_config: &mut ModuleConfig::default(),
} }
.parse() .parse()
.map_err(|e| raw_to_parse_error(&map, *e, true).to_string())?; .map_err(|e| raw_to_parse_error(&map, *e, true).to_string())?;

View File

@ -54,6 +54,7 @@ impl<'a> Parser<'a> {
content_scopes: self.content_scopes, content_scopes: self.content_scopes,
options: self.options, options: self.options,
modules: self.modules, modules: self.modules,
module_config: self.module_config,
} }
.parse_stmt()?; .parse_stmt()?;
} else { } else {
@ -114,6 +115,7 @@ impl<'a> Parser<'a> {
content_scopes: self.content_scopes, content_scopes: self.content_scopes,
options: self.options, options: self.options,
modules: self.modules, modules: self.modules,
module_config: self.module_config,
} }
.parse_stmt()?; .parse_stmt()?;
} else { } else {
@ -143,6 +145,7 @@ impl<'a> Parser<'a> {
content_scopes: self.content_scopes, content_scopes: self.content_scopes,
options: self.options, options: self.options,
modules: self.modules, modules: self.modules,
module_config: self.module_config,
} }
.parse_stmt(); .parse_stmt();
} }
@ -323,6 +326,7 @@ impl<'a> Parser<'a> {
content_scopes: self.content_scopes, content_scopes: self.content_scopes,
options: self.options, options: self.options,
modules: self.modules, modules: self.modules,
module_config: self.module_config,
} }
.parse_stmt()?; .parse_stmt()?;
if !these_stmts.is_empty() { if !these_stmts.is_empty() {
@ -346,6 +350,7 @@ impl<'a> Parser<'a> {
content_scopes: self.content_scopes, content_scopes: self.content_scopes,
options: self.options, options: self.options,
modules: self.modules, modules: self.modules,
module_config: self.module_config,
} }
.parse_stmt()?, .parse_stmt()?,
); );
@ -397,6 +402,7 @@ impl<'a> Parser<'a> {
content_scopes: self.content_scopes, content_scopes: self.content_scopes,
options: self.options, options: self.options,
modules: self.modules, modules: self.modules,
module_config: self.module_config,
} }
.parse_stmt()?; .parse_stmt()?;
if !these_stmts.is_empty() { if !these_stmts.is_empty() {
@ -420,6 +426,7 @@ impl<'a> Parser<'a> {
content_scopes: self.content_scopes, content_scopes: self.content_scopes,
options: self.options, options: self.options,
modules: self.modules, modules: self.modules,
module_config: self.module_config,
} }
.parse_stmt()?, .parse_stmt()?,
); );
@ -512,6 +519,7 @@ impl<'a> Parser<'a> {
content_scopes: self.content_scopes, content_scopes: self.content_scopes,
options: self.options, options: self.options,
modules: self.modules, modules: self.modules,
module_config: self.module_config,
} }
.parse_stmt()?; .parse_stmt()?;
if !these_stmts.is_empty() { if !these_stmts.is_empty() {
@ -535,6 +543,7 @@ impl<'a> Parser<'a> {
content_scopes: self.content_scopes, content_scopes: self.content_scopes,
options: self.options, options: self.options,
modules: self.modules, modules: self.modules,
module_config: self.module_config,
} }
.parse_stmt()?, .parse_stmt()?,
); );

View File

@ -121,6 +121,7 @@ impl<'a> Parser<'a> {
content_scopes: self.content_scopes, content_scopes: self.content_scopes,
options: self.options, options: self.options,
modules: self.modules, modules: self.modules,
module_config: self.module_config,
} }
.parse_stmt()?; .parse_stmt()?;

View File

@ -108,6 +108,7 @@ impl<'a> Parser<'a> {
content_scopes: self.content_scopes, content_scopes: self.content_scopes,
options: self.options, options: self.options,
modules: self.modules, modules: self.modules,
module_config: self.module_config,
} }
.parse(); .parse();
} }

View File

@ -174,6 +174,7 @@ impl<'a> Parser<'a> {
content_scopes: self.content_scopes, content_scopes: self.content_scopes,
options: self.options, options: self.options,
modules: self.modules, modules: self.modules,
module_config: self.module_config,
}) })
.parse_keyframes_selector()?; .parse_keyframes_selector()?;
@ -210,6 +211,7 @@ impl<'a> Parser<'a> {
content_scopes: self.content_scopes, content_scopes: self.content_scopes,
options: self.options, options: self.options,
modules: self.modules, modules: self.modules,
module_config: self.module_config,
} }
.parse_stmt()?; .parse_stmt()?;

View File

@ -25,16 +25,6 @@ impl<'a> Parser<'a> {
Ok(false) 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 { pub fn scan_char(&mut self, c: char) -> bool {
if let Some(Token { kind, .. }) = self.toks.peek() { if let Some(Token { kind, .. }) = self.toks.peek() {
if *kind == c { if *kind == c {

View File

@ -183,6 +183,7 @@ impl<'a> Parser<'a> {
content_scopes: self.content_scopes, content_scopes: self.content_scopes,
options: self.options, options: self.options,
modules: self.modules, modules: self.modules,
module_config: self.module_config,
} }
.parse_stmt()?; .parse_stmt()?;
@ -245,6 +246,7 @@ impl<'a> Parser<'a> {
content_scopes: self.scopes, content_scopes: self.scopes,
options: self.options, options: self.options,
modules: self.modules, modules: self.modules,
module_config: self.module_config,
} }
.parse_stmt()? .parse_stmt()?
} else { } else {

View File

@ -12,7 +12,8 @@ use crate::{
}, },
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,
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, error::SassResult,
lexer::Lexer, lexer::Lexer,
@ -94,6 +95,7 @@ pub(crate) struct Parser<'a> {
pub options: &'a Options<'a>, pub options: &'a Options<'a>,
pub modules: &'a mut Modules, pub modules: &'a mut Modules,
pub module_config: &'a mut ModuleConfig,
} }
impl<'a> Parser<'a> { impl<'a> Parser<'a> {
@ -113,6 +115,99 @@ impl<'a> Parser<'a> {
Ok(stmts) 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<Option<String>> {
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<ModuleConfig> {
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 /// Returns any multiline comments that may have been found
/// while loading modules /// while loading modules
#[allow(clippy::eval_order_dependence)] #[allow(clippy::eval_order_dependence)]
@ -156,44 +251,14 @@ impl<'a> Parser<'a> {
self.whitespace_or_comment(); self.whitespace_or_comment();
let mut module_alias: Option<String> = 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 name_span; let mut config = self.parse_module_config()?;
if let Some(Token { kind: '*', pos }) = self.toks.peek() { self.whitespace_or_comment();
name_span = *pos; self.expect_char(';')?;
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()),
}
let module = match module_name.as_ref() { let module = match module_name.as_ref() {
"sass:color" => declare_module_color(), "sass:color" => declare_module_color(),
@ -232,19 +297,28 @@ impl<'a> Parser<'a> {
content_scopes: self.content_scopes, content_scopes: self.content_scopes,
options: self.options, options: self.options,
modules: self.modules, modules: self.modules,
module_config: &mut config,
} }
.parse()?, .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) Module::new_from_scope(global_scope)
} else { } else {
return Err( return Err(("Can't find stylesheet to import.", span).into());
("Error: 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() { let module_name = match module_alias.as_deref() {
Some("*") => { Some("*") => {
self.global_scope.merge_module(module); self.global_scope.merge_module(module);
@ -599,6 +673,7 @@ impl<'a> Parser<'a> {
content_scopes: self.content_scopes, content_scopes: self.content_scopes,
options: self.options, options: self.options,
modules: self.modules, modules: self.modules,
module_config: self.module_config,
}, },
allows_parent, allows_parent,
true, true,
@ -657,10 +732,11 @@ impl<'a> Parser<'a> {
pub fn parse_interpolation(&mut self) -> SassResult<Spanned<Value>> { pub fn parse_interpolation(&mut self) -> SassResult<Spanned<Value>> {
let val = self.parse_value(true, &|_| false)?; let val = self.parse_value(true, &|_| false)?;
match self.toks.next() {
Some(Token { kind: '}', .. }) => {} self.span_before = val.span;
Some(..) | None => return Err(("expected \"}\".", val.span).into()),
} self.expect_char('}')?;
Ok(val.map_node(Value::unquote)) Ok(val.map_node(Value::unquote))
} }
@ -899,6 +975,7 @@ impl<'a> Parser<'a> {
content_scopes: self.content_scopes, content_scopes: self.content_scopes,
options: self.options, options: self.options,
modules: self.modules, modules: self.modules,
module_config: self.module_config,
} }
.parse_stmt()? .parse_stmt()?
.into_iter() .into_iter()
@ -944,14 +1021,14 @@ impl<'a> Parser<'a> {
content_scopes: self.content_scopes, content_scopes: self.content_scopes,
options: self.options, options: self.options,
modules: self.modules, modules: self.modules,
module_config: self.module_config,
} }
.parse_selector(false, true, String::new())?; .parse_selector(false, true, String::new())?;
// todo: this might be superfluous
self.whitespace(); self.whitespace();
if let Some(Token { kind: ';', .. }) = self.toks.peek() { self.consume_char_if_exists(';');
self.toks.next();
}
let extend_rule = ExtendRule::new(value.clone(), is_optional, self.span_before); let extend_rule = ExtendRule::new(value.clone(), is_optional, self.span_before);

View File

@ -199,6 +199,7 @@ impl<'a> Parser<'a> {
content_scopes: self.content_scopes, content_scopes: self.content_scopes,
options: self.options, options: self.options,
modules: self.modules, modules: self.modules,
module_config: self.module_config,
} }
.parse_value(in_paren, predicate) .parse_value(in_paren, predicate)
} }
@ -224,6 +225,7 @@ impl<'a> Parser<'a> {
content_scopes: self.content_scopes, content_scopes: self.content_scopes,
options: self.options, options: self.options,
modules: self.modules, modules: self.modules,
module_config: self.module_config,
} }
.parse_value(in_paren, &|_| false) .parse_value(in_paren, &|_| false)
} }

View File

@ -39,13 +39,24 @@ impl<'a> Parser<'a> {
} = self.parse_variable_value()?; } = self.parse_variable_value()?;
if default { if default {
let config_val = self.module_config.get(ident);
if self.at_root && !self.flags.in_control_flow() { if self.at_root && !self.flags.in_control_flow() {
if !self.global_scope.var_exists(ident) { 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); self.global_scope.insert_var(ident, value);
} }
} else { } 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) { if global && !self.global_scope.var_exists(ident) {
self.global_scope.insert_var(ident, value.clone()); self.global_scope.insert_var(ident, value.clone());
} }

View File

@ -543,6 +543,7 @@ impl Value {
content_scopes: parser.content_scopes, content_scopes: parser.content_scopes,
options: parser.options, options: parser.options,
modules: parser.modules, modules: parser.modules,
module_config: parser.module_config,
} }
.parse_selector(allows_parent, true, String::new())? .parse_selector(allows_parent, true, String::new())?
.0) .0)

View File

@ -191,3 +191,65 @@ fn use_idempotent_builtin() {
input 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
);
}