From a03ad51b71398608a4e71f2ebdcd2109c02c3f80 Mon Sep 17 00:00:00 2001 From: Connor Skees Date: Thu, 30 Jul 2020 17:21:32 -0400 Subject: [PATCH] allow `@use` of user-defined modules --- src/builtin/modules/mod.rs | 26 +++++---- src/parse/import.rs | 110 ++++++++++++++++++------------------- src/parse/mod.rs | 44 ++++++++++++++- src/scope.rs | 10 ++-- tests/imports.rs | 35 +----------- tests/macros.rs | 34 ++++++++++++ tests/use.rs | 38 +++++++++++++ 7 files changed, 189 insertions(+), 108 deletions(-) diff --git a/src/builtin/modules/mod.rs b/src/builtin/modules/mod.rs index 102772e..4887a0b 100644 --- a/src/builtin/modules/mod.rs +++ b/src/builtin/modules/mod.rs @@ -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, - pub mixins: BTreeMap, - pub functions: BTreeMap, -} +pub(crate) struct Module(pub Scope); impl Module { pub fn get_var(&self, name: Spanned) -> 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 { - 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, ) { 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::>(), ) } + + pub const fn new_from_scope(scope: Scope) -> Self { + Module(scope) + } } pub(crate) fn declare_module_color() -> Module { diff --git a/src/parse/import.rs b/src/parse/import.rs index 5863e3c..28eac59 100644 --- a/src/parse/import.rs +++ b/src/parse/import.rs @@ -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. -/// -/// -fn find_import(file_path: &PathBuf, name: &OsStr, load_paths: &[&Path]) -> Option { - 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 = 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> { - 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. + /// + /// + /// + pub(super) fn find_import(&self, path: &Path) -> Option { 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 = 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> { + 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()) } diff --git a/src/parse/mod.rs b/src/parse/mod.rs index 80c3759..4f189ac 100644 --- a/src/parse/mod.rs +++ b/src/parse/mod.rs @@ -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::>() + .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() { diff --git a/src/scope.rs b/src/scope.rs index 9463e54..b5f3db9 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -12,9 +12,9 @@ use crate::{ #[derive(Debug, Default)] pub(crate) struct Scope { - vars: BTreeMap, - mixins: BTreeMap, - functions: BTreeMap, + pub vars: BTreeMap, + pub mixins: BTreeMap, + pub functions: BTreeMap, } 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); } } diff --git a/tests/imports.rs b/tests/imports.rs index f076028..c0888d0 100644 --- a/tests/imports.rs +++ b/tests/imports.rs @@ -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}"; diff --git a/tests/macros.rs b/tests/macros.rs index c6c1c7c..da4aa99 100644 --- a/tests/macros.rs +++ b/tests/macros.rs @@ -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(); + }; +} diff --git a/tests/use.rs b/tests/use.rs index 4a20f9e..5998c09 100644 --- a/tests/use.rs +++ b/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) + ); +}