allow @use
of user-defined modules
This commit is contained in:
parent
af9864ff85
commit
a03ad51b71
@ -11,6 +11,7 @@ use crate::{
|
|||||||
common::{Identifier, QuoteKind},
|
common::{Identifier, QuoteKind},
|
||||||
error::SassResult,
|
error::SassResult,
|
||||||
parse::Parser,
|
parse::Parser,
|
||||||
|
scope::Scope,
|
||||||
value::{SassFunction, SassMap, Value},
|
value::{SassFunction, SassMap, Value},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -23,26 +24,22 @@ mod selector;
|
|||||||
mod string;
|
mod string;
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
pub(crate) struct Module {
|
pub(crate) struct Module(pub Scope);
|
||||||
pub vars: BTreeMap<Identifier, Value>,
|
|
||||||
pub mixins: BTreeMap<Identifier, Mixin>,
|
|
||||||
pub functions: BTreeMap<Identifier, SassFunction>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Module {
|
impl Module {
|
||||||
pub fn get_var(&self, name: Spanned<Identifier>) -> SassResult<&Value> {
|
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),
|
Some(v) => Ok(v),
|
||||||
None => Err(("Undefined variable.", name.span).into()),
|
None => Err(("Undefined variable.", name.span).into()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn insert_builtin_var(&mut self, name: &'static str, value: Value) {
|
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> {
|
pub fn get_fn(&self, name: Identifier) -> Option<SassFunction> {
|
||||||
self.functions.get(&name).cloned()
|
self.0.functions.get(&name).cloned()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn insert_builtin(
|
pub fn insert_builtin(
|
||||||
@ -51,13 +48,15 @@ impl Module {
|
|||||||
function: fn(CallArgs, &mut Parser<'_>) -> SassResult<Value>,
|
function: fn(CallArgs, &mut Parser<'_>) -> SassResult<Value>,
|
||||||
) {
|
) {
|
||||||
let ident = name.into();
|
let ident = name.into();
|
||||||
self.functions
|
self.0
|
||||||
|
.functions
|
||||||
.insert(ident, SassFunction::Builtin(Builtin::new(function), ident));
|
.insert(ident, SassFunction::Builtin(Builtin::new(function), ident));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn functions(&self) -> SassMap {
|
pub fn functions(&self) -> SassMap {
|
||||||
SassMap::new_with(
|
SassMap::new_with(
|
||||||
self.functions
|
self.0
|
||||||
|
.functions
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(key, value)| {
|
.map(|(key, value)| {
|
||||||
(
|
(
|
||||||
@ -71,7 +70,8 @@ impl Module {
|
|||||||
|
|
||||||
pub fn variables(&self) -> SassMap {
|
pub fn variables(&self) -> SassMap {
|
||||||
SassMap::new_with(
|
SassMap::new_with(
|
||||||
self.vars
|
self.0
|
||||||
|
.vars
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(key, value)| {
|
.map(|(key, value)| {
|
||||||
(
|
(
|
||||||
@ -82,6 +82,10 @@ impl Module {
|
|||||||
.collect::<Vec<(Value, Value)>>(),
|
.collect::<Vec<(Value, Value)>>(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub const fn new_from_scope(scope: Scope) -> Self {
|
||||||
|
Module(scope)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn declare_module_color() -> Module {
|
pub(crate) fn declare_module_color() -> Module {
|
||||||
|
@ -13,19 +13,33 @@ use crate::{
|
|||||||
|
|
||||||
use super::{Parser, Stmt};
|
use super::{Parser, Stmt};
|
||||||
|
|
||||||
/// Searches the current directory of the file then searches in `load_paths` directories
|
impl<'a> Parser<'a> {
|
||||||
/// if the import has not yet been found.
|
/// Searches the current directory of the file then searches in `load_paths` directories
|
||||||
/// <https://sass-lang.com/documentation/at-rules/import#finding-the-file>
|
/// if the import has not yet been found.
|
||||||
/// <https://sass-lang.com/documentation/at-rules/import#load-paths>
|
///
|
||||||
fn find_import(file_path: &PathBuf, name: &OsStr, load_paths: &[&Path]) -> Option<PathBuf> {
|
/// <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()
|
||||||
|
} else {
|
||||||
|
self.path
|
||||||
|
.parent()
|
||||||
|
.unwrap_or_else(|| Path::new(""))
|
||||||
|
.join(path)
|
||||||
|
};
|
||||||
|
|
||||||
|
let name = path_buf.file_name().unwrap_or_else(|| OsStr::new(".."));
|
||||||
|
|
||||||
let paths = [
|
let paths = [
|
||||||
file_path.with_file_name(name).with_extension("scss"),
|
path_buf.with_file_name(name).with_extension("scss"),
|
||||||
file_path
|
path_buf
|
||||||
.with_file_name(format!("_{}", name.to_str().unwrap()))
|
.with_file_name(format!("_{}", name.to_str().unwrap()))
|
||||||
.with_extension("scss"),
|
.with_extension("scss"),
|
||||||
file_path.clone(),
|
path_buf.clone(),
|
||||||
file_path.join("index.scss"),
|
path_buf.join("index.scss"),
|
||||||
file_path.join("_index.scss"),
|
path_buf.join("_index.scss"),
|
||||||
];
|
];
|
||||||
|
|
||||||
for name in &paths {
|
for name in &paths {
|
||||||
@ -34,7 +48,7 @@ fn find_import(file_path: &PathBuf, name: &OsStr, load_paths: &[&Path]) -> Optio
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for path in load_paths {
|
for path in &self.options.load_paths {
|
||||||
let paths: Vec<PathBuf> = if path.is_dir() {
|
let paths: Vec<PathBuf> = if path.is_dir() {
|
||||||
vec![
|
vec![
|
||||||
path.join(format!("{}.scss", name.to_str().unwrap())),
|
path.join(format!("{}.scss", name.to_str().unwrap())),
|
||||||
@ -61,25 +75,12 @@ fn find_import(file_path: &PathBuf, name: &OsStr, load_paths: &[&Path]) -> Optio
|
|||||||
}
|
}
|
||||||
|
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Parser<'a> {
|
|
||||||
fn parse_single_import(&mut self, file_name: &str, span: Span) -> SassResult<Vec<Stmt>> {
|
fn parse_single_import(&mut self, file_name: &str, span: Span) -> SassResult<Vec<Stmt>> {
|
||||||
let path: &Path = file_name.as_ref();
|
let path: &Path = file_name.as_ref();
|
||||||
|
|
||||||
let path_buf = if path.is_absolute() {
|
if let Some(name) = self.find_import(path) {
|
||||||
// todo: test for absolute path imports
|
|
||||||
path.into()
|
|
||||||
} else {
|
|
||||||
self.path
|
|
||||||
.parent()
|
|
||||||
.unwrap_or_else(|| Path::new(""))
|
|
||||||
.join(path)
|
|
||||||
};
|
|
||||||
|
|
||||||
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 file = self.map.add_file(
|
let file = self.map.add_file(
|
||||||
name.to_string_lossy().into(),
|
name.to_string_lossy().into(),
|
||||||
String::from_utf8(fs::read(&name)?)?,
|
String::from_utf8(fs::read(&name)?)?,
|
||||||
@ -106,7 +107,6 @@ impl<'a> Parser<'a> {
|
|||||||
}
|
}
|
||||||
.parse();
|
.parse();
|
||||||
}
|
}
|
||||||
self.whitespace();
|
|
||||||
|
|
||||||
Err(("Can't find stylesheet to import.", span).into())
|
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 codemap::{CodeMap, Span, Spanned};
|
||||||
use peekmore::{PeekMore, PeekMoreIterator};
|
use peekmore::{PeekMore, PeekMoreIterator};
|
||||||
@ -14,6 +14,7 @@ use crate::{
|
|||||||
declare_module_meta, declare_module_selector, declare_module_string, Module,
|
declare_module_meta, declare_module_selector, declare_module_string, Module,
|
||||||
},
|
},
|
||||||
error::SassResult,
|
error::SassResult,
|
||||||
|
lexer::Lexer,
|
||||||
scope::{Scope, Scopes},
|
scope::{Scope, Scopes},
|
||||||
selector::{
|
selector::{
|
||||||
ComplexSelectorComponent, ExtendRule, ExtendedSelector, Extender, Selector, SelectorParser,
|
ComplexSelectorComponent, ExtendRule, ExtendedSelector, Extender, Selector, SelectorParser,
|
||||||
@ -201,7 +202,46 @@ impl<'a> Parser<'a> {
|
|||||||
"sass:meta" => declare_module_meta(),
|
"sass:meta" => declare_module_meta(),
|
||||||
"sass:selector" => declare_module_selector(),
|
"sass:selector" => declare_module_selector(),
|
||||||
"sass:string" => declare_module_string(),
|
"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() {
|
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)]
|
#[derive(Debug, Default)]
|
||||||
pub(crate) struct Scope {
|
pub(crate) struct Scope {
|
||||||
vars: BTreeMap<Identifier, Value>,
|
pub vars: BTreeMap<Identifier, Value>,
|
||||||
mixins: BTreeMap<Identifier, Mixin>,
|
pub mixins: BTreeMap<Identifier, Mixin>,
|
||||||
functions: BTreeMap<Identifier, SassFunction>,
|
pub functions: BTreeMap<Identifier, SassFunction>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Scope {
|
impl Scope {
|
||||||
@ -81,9 +81,7 @@ impl Scope {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn merge_module(&mut self, other: Module) {
|
pub fn merge_module(&mut self, other: Module) {
|
||||||
self.vars.extend(other.vars);
|
self.merge(other.0);
|
||||||
self.mixins.extend(other.mixins);
|
|
||||||
self.functions.extend(other.functions);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,43 +1,10 @@
|
|||||||
#![cfg(test)]
|
#![cfg(test)]
|
||||||
|
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use tempfile::Builder;
|
|
||||||
|
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
mod macros;
|
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]
|
#[test]
|
||||||
fn imports_variable() {
|
fn imports_variable() {
|
||||||
let input = "@import \"imports_variable\";\na {\n color: $a;\n}";
|
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)]
|
#![cfg(test)]
|
||||||
|
|
||||||
|
use std::io::Write;
|
||||||
|
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
mod macros;
|
mod macros;
|
||||||
|
|
||||||
@ -58,3 +60,39 @@ test!(
|
|||||||
}",
|
}",
|
||||||
"a {\n color: -0.4161468365;\n}\n"
|
"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