allow @use
of user-defined modules
This commit is contained in:
parent
af9864ff85
commit
a03ad51b71
@ -11,6 +11,7 @@ use crate::{
|
||||
common::{Identifier, QuoteKind},
|
||||
error::SassResult,
|
||||
parse::Parser,
|
||||
scope::Scope,
|
||||
value::{SassFunction, SassMap, Value},
|
||||
};
|
||||
|
||||
@ -23,26 +24,22 @@ mod selector;
|
||||
mod string;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub(crate) struct Module {
|
||||
pub vars: BTreeMap<Identifier, Value>,
|
||||
pub mixins: BTreeMap<Identifier, Mixin>,
|
||||
pub functions: BTreeMap<Identifier, SassFunction>,
|
||||
}
|
||||
pub(crate) struct Module(pub Scope);
|
||||
|
||||
impl Module {
|
||||
pub fn get_var(&self, name: Spanned<Identifier>) -> SassResult<&Value> {
|
||||
match self.vars.get(&name.node) {
|
||||
match self.0.vars.get(&name.node) {
|
||||
Some(v) => Ok(v),
|
||||
None => Err(("Undefined variable.", name.span).into()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert_builtin_var(&mut self, name: &'static str, value: Value) {
|
||||
self.vars.insert(name.into(), value);
|
||||
self.0.vars.insert(name.into(), value);
|
||||
}
|
||||
|
||||
pub fn get_fn(&self, name: Identifier) -> Option<SassFunction> {
|
||||
self.functions.get(&name).cloned()
|
||||
self.0.functions.get(&name).cloned()
|
||||
}
|
||||
|
||||
pub fn insert_builtin(
|
||||
@ -51,13 +48,15 @@ impl Module {
|
||||
function: fn(CallArgs, &mut Parser<'_>) -> SassResult<Value>,
|
||||
) {
|
||||
let ident = name.into();
|
||||
self.functions
|
||||
self.0
|
||||
.functions
|
||||
.insert(ident, SassFunction::Builtin(Builtin::new(function), ident));
|
||||
}
|
||||
|
||||
pub fn functions(&self) -> SassMap {
|
||||
SassMap::new_with(
|
||||
self.functions
|
||||
self.0
|
||||
.functions
|
||||
.iter()
|
||||
.map(|(key, value)| {
|
||||
(
|
||||
@ -71,7 +70,8 @@ impl Module {
|
||||
|
||||
pub fn variables(&self) -> SassMap {
|
||||
SassMap::new_with(
|
||||
self.vars
|
||||
self.0
|
||||
.vars
|
||||
.iter()
|
||||
.map(|(key, value)| {
|
||||
(
|
||||
@ -82,6 +82,10 @@ impl Module {
|
||||
.collect::<Vec<(Value, Value)>>(),
|
||||
)
|
||||
}
|
||||
|
||||
pub const fn new_from_scope(scope: Scope) -> Self {
|
||||
Module(scope)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn declare_module_color() -> Module {
|
||||
|
@ -13,60 +13,13 @@ use crate::{
|
||||
|
||||
use super::{Parser, Stmt};
|
||||
|
||||
/// Searches the current directory of the file then searches in `load_paths` directories
|
||||
/// if the import has not yet been found.
|
||||
/// <https://sass-lang.com/documentation/at-rules/import#finding-the-file>
|
||||
/// <https://sass-lang.com/documentation/at-rules/import#load-paths>
|
||||
fn find_import(file_path: &PathBuf, name: &OsStr, load_paths: &[&Path]) -> Option<PathBuf> {
|
||||
let paths = [
|
||||
file_path.with_file_name(name).with_extension("scss"),
|
||||
file_path
|
||||
.with_file_name(format!("_{}", name.to_str().unwrap()))
|
||||
.with_extension("scss"),
|
||||
file_path.clone(),
|
||||
file_path.join("index.scss"),
|
||||
file_path.join("_index.scss"),
|
||||
];
|
||||
|
||||
for name in &paths {
|
||||
if name.is_file() {
|
||||
return Some(name.to_path_buf());
|
||||
}
|
||||
}
|
||||
|
||||
for path in load_paths {
|
||||
let paths: Vec<PathBuf> = if path.is_dir() {
|
||||
vec![
|
||||
path.join(format!("{}.scss", name.to_str().unwrap())),
|
||||
path.join(format!("_{}.scss", name.to_str().unwrap())),
|
||||
path.join("index.scss"),
|
||||
path.join("_index.scss"),
|
||||
]
|
||||
} else {
|
||||
vec![
|
||||
path.to_path_buf(),
|
||||
path.with_file_name(name).with_extension("scss"),
|
||||
path.with_file_name(format!("_{}", name.to_str().unwrap()))
|
||||
.with_extension("scss"),
|
||||
path.join("index.scss"),
|
||||
path.join("_index.scss"),
|
||||
]
|
||||
};
|
||||
|
||||
for name in paths {
|
||||
if name.is_file() {
|
||||
return Some(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
impl<'a> Parser<'a> {
|
||||
fn parse_single_import(&mut self, file_name: &str, span: Span) -> SassResult<Vec<Stmt>> {
|
||||
let path: &Path = file_name.as_ref();
|
||||
|
||||
/// Searches the current directory of the file then searches in `load_paths` directories
|
||||
/// if the import has not yet been found.
|
||||
///
|
||||
/// <https://sass-lang.com/documentation/at-rules/import#finding-the-file>
|
||||
/// <https://sass-lang.com/documentation/at-rules/import#load-paths>
|
||||
pub(super) fn find_import(&self, path: &Path) -> Option<PathBuf> {
|
||||
let path_buf = if path.is_absolute() {
|
||||
// todo: test for absolute path imports
|
||||
path.into()
|
||||
@ -79,7 +32,55 @@ impl<'a> Parser<'a> {
|
||||
|
||||
let name = path_buf.file_name().unwrap_or_else(|| OsStr::new(".."));
|
||||
|
||||
if let Some(name) = find_import(&path_buf, name, &self.options.load_paths) {
|
||||
let paths = [
|
||||
path_buf.with_file_name(name).with_extension("scss"),
|
||||
path_buf
|
||||
.with_file_name(format!("_{}", name.to_str().unwrap()))
|
||||
.with_extension("scss"),
|
||||
path_buf.clone(),
|
||||
path_buf.join("index.scss"),
|
||||
path_buf.join("_index.scss"),
|
||||
];
|
||||
|
||||
for name in &paths {
|
||||
if name.is_file() {
|
||||
return Some(name.to_path_buf());
|
||||
}
|
||||
}
|
||||
|
||||
for path in &self.options.load_paths {
|
||||
let paths: Vec<PathBuf> = if path.is_dir() {
|
||||
vec![
|
||||
path.join(format!("{}.scss", name.to_str().unwrap())),
|
||||
path.join(format!("_{}.scss", name.to_str().unwrap())),
|
||||
path.join("index.scss"),
|
||||
path.join("_index.scss"),
|
||||
]
|
||||
} else {
|
||||
vec![
|
||||
path.to_path_buf(),
|
||||
path.with_file_name(name).with_extension("scss"),
|
||||
path.with_file_name(format!("_{}", name.to_str().unwrap()))
|
||||
.with_extension("scss"),
|
||||
path.join("index.scss"),
|
||||
path.join("_index.scss"),
|
||||
]
|
||||
};
|
||||
|
||||
for name in paths {
|
||||
if name.is_file() {
|
||||
return Some(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn parse_single_import(&mut self, file_name: &str, span: Span) -> SassResult<Vec<Stmt>> {
|
||||
let path: &Path = file_name.as_ref();
|
||||
|
||||
if let Some(name) = self.find_import(path) {
|
||||
let file = self.map.add_file(
|
||||
name.to_string_lossy().into(),
|
||||
String::from_utf8(fs::read(&name)?)?,
|
||||
@ -106,7 +107,6 @@ impl<'a> Parser<'a> {
|
||||
}
|
||||
.parse();
|
||||
}
|
||||
self.whitespace();
|
||||
|
||||
Err(("Can't find stylesheet to import.", span).into())
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
use std::{collections::HashMap, convert::TryFrom, path::Path, vec::IntoIter};
|
||||
use std::{collections::HashMap, convert::TryFrom, fs, path::Path, vec::IntoIter};
|
||||
|
||||
use codemap::{CodeMap, Span, Spanned};
|
||||
use peekmore::{PeekMore, PeekMoreIterator};
|
||||
@ -14,6 +14,7 @@ use crate::{
|
||||
declare_module_meta, declare_module_selector, declare_module_string, Module,
|
||||
},
|
||||
error::SassResult,
|
||||
lexer::Lexer,
|
||||
scope::{Scope, Scopes},
|
||||
selector::{
|
||||
ComplexSelectorComponent, ExtendRule, ExtendedSelector, Extender, Selector, SelectorParser,
|
||||
@ -201,7 +202,46 @@ impl<'a> Parser<'a> {
|
||||
"sass:meta" => declare_module_meta(),
|
||||
"sass:selector" => declare_module_selector(),
|
||||
"sass:string" => declare_module_string(),
|
||||
_ => todo!("@use not yet implemented"),
|
||||
_ => {
|
||||
if let Some(import) = self.find_import(module_name.as_ref().as_ref()) {
|
||||
let mut global_scope = Scope::new();
|
||||
|
||||
let file = self.map.add_file(
|
||||
module_name.clone().into_owned(),
|
||||
String::from_utf8(fs::read(&import)?)?,
|
||||
);
|
||||
|
||||
comments.append(
|
||||
&mut Parser {
|
||||
toks: &mut Lexer::new(&file)
|
||||
.collect::<Vec<Token>>()
|
||||
.into_iter()
|
||||
.peekmore(),
|
||||
map: self.map,
|
||||
path: &import,
|
||||
scopes: self.scopes,
|
||||
global_scope: &mut global_scope,
|
||||
super_selectors: self.super_selectors,
|
||||
span_before: file.span.subspan(0, 0),
|
||||
content: self.content,
|
||||
flags: self.flags,
|
||||
at_root: self.at_root,
|
||||
at_root_has_selector: self.at_root_has_selector,
|
||||
extender: self.extender,
|
||||
content_scopes: self.content_scopes,
|
||||
options: self.options,
|
||||
modules: self.modules,
|
||||
}
|
||||
.parse()?,
|
||||
);
|
||||
|
||||
Module::new_from_scope(global_scope)
|
||||
} else {
|
||||
return Err(
|
||||
("Error: Can't find stylesheet to import.", span).into()
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let module_name = match module_alias.as_deref() {
|
||||
|
10
src/scope.rs
10
src/scope.rs
@ -12,9 +12,9 @@ use crate::{
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub(crate) struct Scope {
|
||||
vars: BTreeMap<Identifier, Value>,
|
||||
mixins: BTreeMap<Identifier, Mixin>,
|
||||
functions: BTreeMap<Identifier, SassFunction>,
|
||||
pub vars: BTreeMap<Identifier, Value>,
|
||||
pub mixins: BTreeMap<Identifier, Mixin>,
|
||||
pub functions: BTreeMap<Identifier, SassFunction>,
|
||||
}
|
||||
|
||||
impl Scope {
|
||||
@ -81,9 +81,7 @@ impl Scope {
|
||||
}
|
||||
|
||||
pub fn merge_module(&mut self, other: Module) {
|
||||
self.vars.extend(other.vars);
|
||||
self.mixins.extend(other.mixins);
|
||||
self.functions.extend(other.functions);
|
||||
self.merge(other.0);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,43 +1,10 @@
|
||||
#![cfg(test)]
|
||||
|
||||
use std::io::Write;
|
||||
use tempfile::Builder;
|
||||
|
||||
#[macro_use]
|
||||
mod macros;
|
||||
|
||||
/// Create a temporary file with the given name
|
||||
/// and contents.
|
||||
///
|
||||
/// This must be a macro rather than a function
|
||||
/// because the tempfile will be deleted when it
|
||||
/// exits scope
|
||||
macro_rules! tempfile {
|
||||
($name:literal, $content:literal) => {
|
||||
let mut f = Builder::new()
|
||||
.rand_bytes(0)
|
||||
.prefix("")
|
||||
.suffix($name)
|
||||
.tempfile_in("")
|
||||
.unwrap();
|
||||
write!(f, "{}", $content).unwrap();
|
||||
};
|
||||
($name:literal, $content:literal, dir=$dir:literal) => {
|
||||
let _d = Builder::new()
|
||||
.rand_bytes(0)
|
||||
.prefix("")
|
||||
.suffix($dir)
|
||||
.tempdir_in("")
|
||||
.unwrap();
|
||||
let mut f = Builder::new()
|
||||
.rand_bytes(0)
|
||||
.prefix("")
|
||||
.suffix($name)
|
||||
.tempfile_in($dir)
|
||||
.unwrap();
|
||||
write!(f, "{}", $content).unwrap();
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn imports_variable() {
|
||||
let input = "@import \"imports_variable\";\na {\n color: $a;\n}";
|
||||
|
@ -51,3 +51,37 @@ macro_rules! error {
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// Create a temporary file with the given name
|
||||
/// and contents.
|
||||
///
|
||||
/// This must be a macro rather than a function
|
||||
/// because the tempfile will be deleted when it
|
||||
/// exits scope
|
||||
#[macro_export]
|
||||
macro_rules! tempfile {
|
||||
($name:literal, $content:literal) => {
|
||||
let mut f = tempfile::Builder::new()
|
||||
.rand_bytes(0)
|
||||
.prefix("")
|
||||
.suffix($name)
|
||||
.tempfile_in("")
|
||||
.unwrap();
|
||||
write!(f, "{}", $content).unwrap();
|
||||
};
|
||||
($name:literal, $content:literal, dir=$dir:literal) => {
|
||||
let _d = tempfile::Builder::new()
|
||||
.rand_bytes(0)
|
||||
.prefix("")
|
||||
.suffix($dir)
|
||||
.tempdir_in("")
|
||||
.unwrap();
|
||||
let mut f = tempfile::Builder::new()
|
||||
.rand_bytes(0)
|
||||
.prefix("")
|
||||
.suffix($name)
|
||||
.tempfile_in($dir)
|
||||
.unwrap();
|
||||
write!(f, "{}", $content).unwrap();
|
||||
};
|
||||
}
|
||||
|
38
tests/use.rs
38
tests/use.rs
@ -1,5 +1,7 @@
|
||||
#![cfg(test)]
|
||||
|
||||
use std::io::Write;
|
||||
|
||||
#[macro_use]
|
||||
mod macros;
|
||||
|
||||
@ -58,3 +60,39 @@ test!(
|
||||
}",
|
||||
"a {\n color: -0.4161468365;\n}\n"
|
||||
);
|
||||
|
||||
#[test]
|
||||
fn use_user_defined_same_directory() {
|
||||
let input = "@use \"use_user_defined_same_directory\";\na {\n color: use_user_defined_same_directory.$a;\n}";
|
||||
tempfile!(
|
||||
"use_user_defined_same_directory.scss",
|
||||
"$a: red; a { color: $a; }"
|
||||
);
|
||||
assert_eq!(
|
||||
"a {\n color: red;\n}\n\na {\n color: red;\n}\n",
|
||||
&grass::from_string(input.to_string(), &grass::Options::default()).expect(input)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn use_user_defined_as() {
|
||||
let input = "@use \"use_user_defined_as\" as module;\na {\n color: module.$a;\n}";
|
||||
tempfile!("use_user_defined_as.scss", "$a: red; a { color: $a; }");
|
||||
assert_eq!(
|
||||
"a {\n color: red;\n}\n\na {\n color: red;\n}\n",
|
||||
&grass::from_string(input.to_string(), &grass::Options::default()).expect(input)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn use_user_defined_function() {
|
||||
let input = "@use \"use_user_defined_function\" as module;\na {\n color: module.foo(red);\n}";
|
||||
tempfile!(
|
||||
"use_user_defined_function.scss",
|
||||
"@function foo($a) { @return $a; }"
|
||||
);
|
||||
assert_eq!(
|
||||
"a {\n color: red;\n}\n",
|
||||
&grass::from_string(input.to_string(), &grass::Options::default()).expect(input)
|
||||
);
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user