allow @use of user-defined modules

This commit is contained in:
Connor Skees 2020-07-30 17:21:32 -04:00
parent af9864ff85
commit a03ad51b71
7 changed files with 189 additions and 108 deletions

View File

@ -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 {

View File

@ -13,60 +13,13 @@ use crate::{
use super::{Parser, Stmt}; 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> { impl<'a> Parser<'a> {
fn parse_single_import(&mut self, file_name: &str, span: Span) -> SassResult<Vec<Stmt>> { /// Searches the current directory of the file then searches in `load_paths` directories
let path: &Path = file_name.as_ref(); /// 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() { let path_buf = if path.is_absolute() {
// todo: test for absolute path imports // todo: test for absolute path imports
path.into() path.into()
@ -79,7 +32,55 @@ impl<'a> Parser<'a> {
let name = path_buf.file_name().unwrap_or_else(|| OsStr::new("..")); 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( 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())
} }

View File

@ -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() {

View File

@ -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);
} }
} }

View File

@ -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}";

View File

@ -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();
};
}

View File

@ -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)
);
}