support the @use ... with (...)
syntax
This commit is contained in:
parent
94becb4dcb
commit
074d679cbd
@ -27,6 +27,34 @@ pub(crate) struct Module(pub Scope);
|
||||
#[derive(Debug, Default)]
|
||||
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 {
|
||||
pub fn insert(&mut self, name: Identifier, module: Module, span: Span) -> SassResult<()> {
|
||||
if self.0.contains_key(&name) {
|
||||
|
@ -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<String> {
|
||||
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<String> {
|
||||
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<String, JsValue> {
|
||||
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())?;
|
||||
|
@ -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()?,
|
||||
);
|
||||
|
@ -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()?;
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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()?;
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
169
src/parse/mod.rs
169
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<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
|
||||
/// 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<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 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<Spanned<Value>> {
|
||||
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);
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
|
@ -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)
|
||||
|
62
tests/use.rs
62
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
|
||||
);
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user