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},
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 {

View File

@ -13,19 +13,33 @@ use crate::{
use super::{Parser, Stmt};
impl<'a> Parser<'a> {
/// 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> {
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 = [
file_path.with_file_name(name).with_extension("scss"),
file_path
path_buf.with_file_name(name).with_extension("scss"),
path_buf
.with_file_name(format!("_{}", name.to_str().unwrap()))
.with_extension("scss"),
file_path.clone(),
file_path.join("index.scss"),
file_path.join("_index.scss"),
path_buf.clone(),
path_buf.join("index.scss"),
path_buf.join("_index.scss"),
];
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() {
vec![
path.join(format!("{}.scss", name.to_str().unwrap())),
@ -63,23 +77,10 @@ fn find_import(file_path: &PathBuf, name: &OsStr, load_paths: &[&Path]) -> Optio
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();
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(".."));
if let Some(name) = find_import(&path_buf, name, &self.options.load_paths) {
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())
}

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

View File

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

View File

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

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