File system interception, and various other matters (#55)

This commit is contained in:
Chris Morgan 2022-02-04 09:41:10 +11:00 committed by GitHub
parent dd92ebf39b
commit 3c5463ac4c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 218 additions and 274 deletions

View File

@ -1,3 +1,8 @@
# 0.11.0
- `fs` option added to allow interception and reimplementation of all file system operations (such as imports)
- `wasm` feature renamed to/replaced with `wasm-exports`, which no longer materially alters the API: `from_path` is reinstated, and `from_string` once again returns the full error type; but the WASM export `from_string` (which returns a string error) is now a new function `from_string_js`. (It was renamed from `wasm` to `wasm-exports` because the name was misleading; Rust code that uses grass doesnt need this feature, its solely to get this `from_string` WASM export.)
# 0.10.8 # 0.10.8
- bugfix: properly emit the number `0` in compressed mode (#53) - bugfix: properly emit the number `0` in compressed mode (#53)

View File

@ -74,8 +74,8 @@ commandline = ["clap"]
nightly = [] nightly = []
# Option (enabled by default): enable the builtin functions `random([$limit])` and `unique-id()` # Option (enabled by default): enable the builtin functions `random([$limit])` and `unique-id()`
random = ["rand"] random = ["rand"]
# Option: compile to web assembly # Option: expose JavaScript-friendly WebAssembly exports
wasm = ["wasm-bindgen"] wasm-exports = ["wasm-bindgen"]
# Option: enable features that assist in profiling (e.g. inline(never)) # Option: enable features that assist in profiling (e.g. inline(never))
profiling = [] profiling = []
# Option: enable criterion for benchmarking # Option: enable criterion for benchmarking

70
src/fs.rs Normal file
View File

@ -0,0 +1,70 @@
use std::io::{Error, ErrorKind, Result};
use std::path::Path;
/// A trait to allow replacing the file system lookup mechanisms.
///
/// As it stands, this is imperfect: its still using the types and some operations from
/// `std::path`, which constrain it to the target platforms norms. This could be ameliorated by
/// the use of associated types for `Path` and `PathBuf`, and putting all remaining methods on this
/// trait (`is_absolute`, `parent`, `join`, *&c.*); but that would infect too many other APIs to be
/// desirable, so we live with it as it is—which is also acceptable, because the motivating example
/// use case is mostly using this as an optimisation over the real platform underneath.
pub trait Fs: std::fmt::Debug {
/// Returns `true` if the path exists on disk and is pointing at a directory.
fn is_dir(&self, path: &Path) -> bool;
/// Returns `true` if the path exists on disk and is pointing at a regular file.
fn is_file(&self, path: &Path) -> bool;
/// Read the entire contents of a file into a bytes vector.
fn read(&self, path: &Path) -> Result<Vec<u8>>;
}
/// Use [`std::fs`] to read any files from disk.
///
/// This is the default file system implementation.
#[derive(Debug)]
pub struct StdFs;
impl Fs for StdFs {
#[inline]
fn is_file(&self, path: &Path) -> bool {
path.is_file()
}
#[inline]
fn is_dir(&self, path: &Path) -> bool {
path.is_dir()
}
#[inline]
fn read(&self, path: &Path) -> Result<Vec<u8>> {
std::fs::read(path)
}
}
/// A file system implementation that acts like its completely empty.
///
/// This may be useful for security as it denies all access to the file system (so `@import` is
/// prevented from leaking anything); youll need to use [`from_string`][crate::from_string] for
/// this to make any sense (since [`from_path`][crate::from_path] would fail to find a file).
#[derive(Debug)]
pub struct NullFs;
impl Fs for NullFs {
#[inline]
fn is_file(&self, _path: &Path) -> bool {
false
}
#[inline]
fn is_dir(&self, _path: &Path) -> bool {
false
}
#[inline]
fn read(&self, _path: &Path) -> Result<Vec<u8>> {
Err(Error::new(
ErrorKind::NotFound,
"NullFs, there is no file system",
))
}
}

View File

@ -92,9 +92,10 @@ grass input.scss
)] )]
#![cfg_attr(feature = "nightly", feature(track_caller))] #![cfg_attr(feature = "nightly", feature(track_caller))]
#![cfg_attr(feature = "profiling", inline(never))] #![cfg_attr(feature = "profiling", inline(never))]
use std::{fs, path::Path};
#[cfg(feature = "wasm")] use std::path::Path;
#[cfg(feature = "wasm-exports")]
use wasm_bindgen::prelude::*; use wasm_bindgen::prelude::*;
pub(crate) use beef::lean::Cow; pub(crate) use beef::lean::Cow;
@ -102,6 +103,7 @@ pub(crate) use beef::lean::Cow;
use codemap::CodeMap; use codemap::CodeMap;
pub use crate::error::{SassError as Error, SassResult as Result}; pub use crate::error::{SassError as Error, SassResult as Result};
pub use crate::fs::{Fs, NullFs, StdFs};
pub(crate) use crate::token::Token; pub(crate) use crate::token::Token;
use crate::{ use crate::{
builtin::modules::{ModuleConfig, Modules}, builtin::modules::{ModuleConfig, Modules},
@ -121,6 +123,7 @@ mod builtin;
mod color; mod color;
mod common; mod common;
mod error; mod error;
mod fs;
mod interner; mod interner;
mod lexer; mod lexer;
mod output; mod output;
@ -152,6 +155,7 @@ pub enum OutputStyle {
/// more control. /// more control.
#[derive(Debug)] #[derive(Debug)]
pub struct Options<'a> { pub struct Options<'a> {
fs: &'a dyn Fs,
style: OutputStyle, style: OutputStyle,
load_paths: Vec<&'a Path>, load_paths: Vec<&'a Path>,
allows_charset: bool, allows_charset: bool,
@ -163,6 +167,7 @@ impl Default for Options<'_> {
#[inline] #[inline]
fn default() -> Self { fn default() -> Self {
Self { Self {
fs: &StdFs,
style: OutputStyle::Expanded, style: OutputStyle::Expanded,
load_paths: Vec::new(), load_paths: Vec::new(),
allows_charset: true, allows_charset: true,
@ -173,6 +178,17 @@ impl Default for Options<'_> {
} }
impl<'a> Options<'a> { impl<'a> Options<'a> {
/// This option allows you to control the file system that Sass will see.
///
/// By default, it uses [`StdFs`], which is backed by [`std::fs`],
/// allowing direct, unfettered access to the local file system.
#[must_use]
#[inline]
pub fn fs(mut self, fs: &'a dyn Fs) -> Self {
self.fs = fs;
self
}
/// `grass` currently offers 2 different output styles /// `grass` currently offers 2 different output styles
/// ///
/// - `OutputStyle::Expanded` writes each selector and declaration on its own line. /// - `OutputStyle::Expanded` writes each selector and declaration on its own line.
@ -317,9 +333,8 @@ fn from_string_with_file_name(input: String, file_name: &str, options: &Options)
/// ``` /// ```
#[cfg_attr(feature = "profiling", inline(never))] #[cfg_attr(feature = "profiling", inline(never))]
#[cfg_attr(not(feature = "profiling"), inline)] #[cfg_attr(not(feature = "profiling"), inline)]
#[cfg(not(feature = "wasm"))]
pub fn from_path(p: &str, options: &Options) -> Result<String> { pub fn from_path(p: &str, options: &Options) -> Result<String> {
from_string_with_file_name(String::from_utf8(fs::read(p)?)?, p, options) from_string_with_file_name(String::from_utf8(options.fs.read(Path::new(p))?)?, p, options)
} }
/// Compile CSS from a string /// Compile CSS from a string
@ -333,41 +348,12 @@ pub fn from_path(p: &str, options: &Options) -> Result<String> {
/// ``` /// ```
#[cfg_attr(feature = "profiling", inline(never))] #[cfg_attr(feature = "profiling", inline(never))]
#[cfg_attr(not(feature = "profiling"), inline)] #[cfg_attr(not(feature = "profiling"), inline)]
#[cfg(not(feature = "wasm"))]
pub fn from_string(input: String, options: &Options) -> Result<String> { pub fn from_string(input: String, options: &Options) -> Result<String> {
from_string_with_file_name(input, "stdin", options) from_string_with_file_name(input, "stdin", options)
} }
#[cfg(feature = "wasm")] #[cfg(feature = "wasm-exports")]
#[wasm_bindgen] #[wasm_bindgen(js_name = from_string)]
pub fn from_string(p: String) -> std::result::Result<String, JsValue> { pub fn from_string_js(p: String) -> std::result::Result<String, JsValue> {
let mut map = CodeMap::new(); from_string(Options::default()).map_err(|e| e.to_string())
let file = map.add_file("stdin".into(), p);
let empty_span = file.span.subspan(0, 0);
let stmts = Parser {
toks: &mut Lexer::new_from_file(&file),
map: &mut map,
path: Path::new(""),
scopes: &mut Scopes::new(),
global_scope: &mut Scope::new(),
super_selectors: &mut NeverEmptyVec::new(Selector::new(empty_span)),
span_before: empty_span,
content: &mut Vec::new(),
flags: ContextFlags::empty(),
at_root: true,
at_root_has_selector: false,
extender: &mut Extender::new(empty_span),
content_scopes: &mut Scopes::new(),
options: &Options::default(),
modules: &mut Modules::default(),
module_config: &mut ModuleConfig::default(),
}
.parse()
.map_err(|e| raw_to_parse_error(&map, *e, true).to_string())?;
Ok(Css::from_stmts(stmts, false, true)
.map_err(|e| raw_to_parse_error(&map, *e, true).to_string())?
.pretty_print(&map, options.style)
.map_err(|e| raw_to_parse_error(&map, *e, true).to_string())?)
} }

View File

@ -6,7 +6,6 @@ use std::{
use clap::{arg_enum, App, AppSettings, Arg}; use clap::{arg_enum, App, AppSettings, Arg};
#[cfg(not(feature = "wasm"))]
use grass::{from_path, from_string, Options, OutputStyle}; use grass::{from_path, from_string, Options, OutputStyle};
// TODO remove this // TODO remove this
@ -26,10 +25,6 @@ arg_enum! {
} }
} }
#[cfg(feature = "wasm")]
fn main() {}
#[cfg(not(feature = "wasm"))]
#[cfg_attr(feature = "profiling", inline(never))] #[cfg_attr(feature = "profiling", inline(never))]
fn main() -> std::io::Result<()> { fn main() -> std::io::Result<()> {
let matches = App::new("grass") let matches = App::new("grass")

View File

@ -13,6 +13,48 @@ use crate::{
}; };
impl<'a, 'b> Parser<'a, 'b> { impl<'a, 'b> Parser<'a, 'b> {
fn subparser_with_in_control_flow_flag<'c>(&'c mut self) -> Parser<'c, 'b> {
Parser {
toks: self.toks,
map: self.map,
path: self.path,
scopes: self.scopes,
global_scope: self.global_scope,
super_selectors: self.super_selectors,
span_before: self.span_before,
content: self.content,
flags: self.flags | ContextFlags::IN_CONTROL_FLOW,
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,
module_config: self.module_config,
}
}
fn with_toks<'d>(self, toks: &'a mut Lexer<'d>) -> Parser<'a, 'd> {
Parser {
toks,
map: self.map,
path: self.path,
scopes: self.scopes,
global_scope: self.global_scope,
super_selectors: self.super_selectors,
span_before: self.span_before,
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,
module_config: self.module_config,
}
}
pub(super) fn parse_if(&mut self) -> SassResult<Vec<Stmt>> { pub(super) fn parse_if(&mut self) -> SassResult<Vec<Stmt>> {
self.whitespace_or_comment(); self.whitespace_or_comment();
@ -28,29 +70,9 @@ impl<'a, 'b> Parser<'a, 'b> {
} }
if init_cond.is_true() { if init_cond.is_true() {
self.scopes.enter_new_scope();
found_true = true; found_true = true;
self.scopes.enter_new_scope();
body = Parser { body = self.subparser_with_in_control_flow_flag().parse_stmt()?;
toks: self.toks,
map: self.map,
path: self.path,
scopes: self.scopes,
global_scope: self.global_scope,
super_selectors: self.super_selectors,
span_before: self.span_before,
content: self.content,
flags: self.flags | ContextFlags::IN_CONTROL_FLOW,
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,
module_config: self.module_config,
}
.parse_stmt()?;
self.scopes.exit_scope(); self.scopes.exit_scope();
} else { } else {
self.throw_away_until_closing_curly_brace()?; self.throw_away_until_closing_curly_brace()?;
@ -91,26 +113,7 @@ impl<'a, 'b> Parser<'a, 'b> {
if cond { if cond {
found_true = true; found_true = true;
self.scopes.enter_new_scope(); self.scopes.enter_new_scope();
body = Parser { body = self.subparser_with_in_control_flow_flag().parse_stmt()?;
toks: self.toks,
map: self.map,
path: self.path,
scopes: self.scopes,
global_scope: self.global_scope,
super_selectors: self.super_selectors,
span_before: self.span_before,
content: self.content,
flags: self.flags | ContextFlags::IN_CONTROL_FLOW,
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,
module_config: self.module_config,
}
.parse_stmt()?;
self.scopes.exit_scope(); self.scopes.exit_scope();
} else { } else {
self.throw_away_until_closing_curly_brace()?; self.throw_away_until_closing_curly_brace()?;
@ -125,25 +128,7 @@ impl<'a, 'b> Parser<'a, 'b> {
} }
self.scopes.enter_new_scope(); self.scopes.enter_new_scope();
let tmp = Parser { let tmp = self.subparser_with_in_control_flow_flag().parse_stmt();
toks: self.toks,
map: self.map,
path: self.path,
scopes: self.scopes,
global_scope: self.global_scope,
super_selectors: self.super_selectors,
span_before: self.span_before,
content: self.content,
flags: self.flags | ContextFlags::IN_CONTROL_FLOW,
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,
module_config: self.module_config,
}
.parse_stmt();
self.scopes.exit_scope(); self.scopes.exit_scope();
return tmp; return tmp;
} }
@ -272,51 +257,15 @@ impl<'a, 'b> Parser<'a, 'b> {
var.node, var.node,
Value::Dimension(Some(Number::from(i)), Unit::None, true), Value::Dimension(Some(Number::from(i)), Unit::None, true),
); );
if self.flags.in_function() { let mut these_stmts = self.subparser_with_in_control_flow_flag()
let these_stmts = Parser { .with_toks(&mut Lexer::new_ref(&body))
toks: &mut Lexer::new_ref(&body),
map: self.map,
path: self.path,
scopes: self.scopes,
global_scope: self.global_scope,
super_selectors: self.super_selectors,
span_before: self.span_before,
content: self.content,
flags: self.flags | ContextFlags::IN_CONTROL_FLOW,
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,
module_config: self.module_config,
}
.parse_stmt()?; .parse_stmt()?;
if self.flags.in_function() {
if !these_stmts.is_empty() { if !these_stmts.is_empty() {
return Ok(these_stmts); return Ok(these_stmts);
} }
} else { } else {
stmts.append( stmts.append(&mut these_stmts);
&mut Parser {
toks: &mut Lexer::new_ref(&body),
map: self.map,
path: self.path,
scopes: self.scopes,
global_scope: self.global_scope,
super_selectors: self.super_selectors,
span_before: self.span_before,
content: self.content,
flags: self.flags | ContextFlags::IN_CONTROL_FLOW,
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,
module_config: self.module_config,
}
.parse_stmt()?,
);
} }
} }
@ -349,51 +298,15 @@ impl<'a, 'b> Parser<'a, 'b> {
let mut val = self.parse_value_from_vec(&cond, true)?; let mut val = self.parse_value_from_vec(&cond, true)?;
self.scopes.enter_new_scope(); self.scopes.enter_new_scope();
while val.node.is_true() { while val.node.is_true() {
if self.flags.in_function() { let mut these_stmts = self.subparser_with_in_control_flow_flag()
let these_stmts = Parser { .with_toks(&mut Lexer::new_ref(&body))
toks: &mut Lexer::new_ref(&body),
map: self.map,
path: self.path,
scopes: self.scopes,
global_scope: self.global_scope,
super_selectors: self.super_selectors,
span_before: self.span_before,
content: self.content,
flags: self.flags | ContextFlags::IN_CONTROL_FLOW,
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,
module_config: self.module_config,
}
.parse_stmt()?; .parse_stmt()?;
if self.flags.in_function() {
if !these_stmts.is_empty() { if !these_stmts.is_empty() {
return Ok(these_stmts); return Ok(these_stmts);
} }
} else { } else {
stmts.append( stmts.append(&mut these_stmts);
&mut Parser {
toks: &mut Lexer::new_ref(&body),
map: self.map,
path: self.path,
scopes: self.scopes,
global_scope: self.global_scope,
super_selectors: self.super_selectors,
span_before: self.span_before,
content: self.content,
flags: self.flags | ContextFlags::IN_CONTROL_FLOW,
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,
module_config: self.module_config,
}
.parse_stmt()?,
);
} }
val = self.parse_value_from_vec(&cond, true)?; val = self.parse_value_from_vec(&cond, true)?;
} }
@ -461,51 +374,15 @@ impl<'a, 'b> Parser<'a, 'b> {
} }
} }
if self.flags.in_function() { let mut these_stmts = self.subparser_with_in_control_flow_flag()
let these_stmts = Parser { .with_toks(&mut Lexer::new_ref(&body))
toks: &mut Lexer::new_ref(&body),
map: self.map,
path: self.path,
scopes: self.scopes,
global_scope: self.global_scope,
super_selectors: self.super_selectors,
span_before: self.span_before,
content: self.content,
flags: self.flags | ContextFlags::IN_CONTROL_FLOW,
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,
module_config: self.module_config,
}
.parse_stmt()?; .parse_stmt()?;
if self.flags.in_function() {
if !these_stmts.is_empty() { if !these_stmts.is_empty() {
return Ok(these_stmts); return Ok(these_stmts);
} }
} else { } else {
stmts.append( stmts.append(&mut these_stmts);
&mut Parser {
toks: &mut Lexer::new_ref(&body),
map: self.map,
path: self.path,
scopes: self.scopes,
global_scope: self.global_scope,
super_selectors: self.super_selectors,
span_before: self.span_before,
content: self.content,
flags: self.flags | ContextFlags::IN_CONTROL_FLOW,
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,
module_config: self.module_config,
}
.parse_stmt()?,
);
} }
} }

View File

@ -1,4 +1,4 @@
use std::{ffi::OsStr, fs, path::Path, path::PathBuf}; use std::{ffi::OsStr, path::Path, path::PathBuf};
use codemap::{Span, Spanned}; use codemap::{Span, Spanned};
@ -45,49 +45,41 @@ impl<'a, 'b> Parser<'a, 'b> {
let name = path_buf.file_name().unwrap_or_else(|| OsStr::new("..")); let name = path_buf.file_name().unwrap_or_else(|| OsStr::new(".."));
let paths = [ macro_rules! try_path {
path_buf.with_file_name(name).with_extension("scss"), ($name:expr) => {
path_buf let name = $name;
.with_file_name(format!("_{}", name.to_str().unwrap())) if self.options.fs.is_file(&name) {
.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.clone());
}
}
for path in &self.options.load_paths {
let paths: Vec<PathBuf> = if path.is_dir() {
vec![
path.join(&path_buf)
.with_file_name(name)
.with_extension("scss"),
path.join(&path_buf)
.with_file_name(format!("_{}", name.to_str().unwrap()))
.with_extension("scss"),
path.join(&path_buf).join("index.scss"),
path.join(&path_buf).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); return Some(name);
} }
};
}
try_path!(path_buf.with_file_name(name).with_extension("scss"));
try_path!(path_buf
.with_file_name(format!("_{}", name.to_str().unwrap()))
.with_extension("scss"));
try_path!(path_buf.clone());
try_path!(path_buf.join("index.scss"));
try_path!(path_buf.join("_index.scss"));
for path in &self.options.load_paths {
if self.options.fs.is_dir(path) {
try_path!(path.join(&path_buf)
.with_file_name(name)
.with_extension("scss"));
try_path!(path.join(&path_buf)
.with_file_name(format!("_{}", name.to_str().unwrap()))
.with_extension("scss"));
try_path!(path.join(&path_buf).join("index.scss"));
try_path!(path.join(&path_buf).join("_index.scss"));
} else {
try_path!(path.to_path_buf());
try_path!(path.with_file_name(name).with_extension("scss"));
try_path!(path
.with_file_name(format!("_{}", name.to_str().unwrap()))
.with_extension("scss"));
try_path!(path.join("index.scss"));
try_path!(path.join("_index.scss"));
} }
} }
@ -104,7 +96,7 @@ impl<'a, 'b> Parser<'a, 'b> {
if let Some(name) = self.find_import(path) { 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(self.options.fs.read(&name)?)?,
); );
return Parser { return Parser {
toks: &mut Lexer::new_from_file(&file), toks: &mut Lexer::new_from_file(&file),

View File

@ -1,4 +1,4 @@
use std::{convert::TryFrom, fs}; use std::convert::TryFrom;
use codemap::Spanned; use codemap::Spanned;
@ -120,9 +120,10 @@ impl<'a, 'b> Parser<'a, 'b> {
if let Some(import) = self.find_import(name.as_ref()) { if let Some(import) = self.find_import(name.as_ref()) {
let mut global_scope = Scope::new(); let mut global_scope = Scope::new();
let file = self let file = self.map.add_file(
.map name.to_owned(),
.add_file(name.to_owned(), String::from_utf8(fs::read(&import)?)?); String::from_utf8(self.options.fs.read(&import)?)?,
);
let mut modules = Modules::default(); let mut modules = Modules::default();

View File

@ -569,8 +569,7 @@ impl Pseudo {
.any(|pseudo2| self.selector == pseudo2.selector), .any(|pseudo2| self.selector == pseudo2.selector),
"nth-child" | "nth-last-child" => compound.components.iter().any(|pseudo2| { "nth-child" | "nth-last-child" => compound.components.iter().any(|pseudo2| {
if let SimpleSelector::Pseudo( if let SimpleSelector::Pseudo(
pseudo pseudo @ Pseudo {
@ Pseudo {
selector: Some(..), .. selector: Some(..), ..
}, },
) = pseudo2 ) = pseudo2

View File

@ -3,6 +3,25 @@ use std::io::Write;
#[macro_use] #[macro_use]
mod macros; mod macros;
#[test]
fn null_fs_cannot_import() {
let input = "@import \"foo\";";
tempfile!("foo.scss", "");
match grass::from_string(
input.to_string(),
&grass::Options::default().fs(&grass::NullFs),
) {
Err(e)
if e.to_string()
.starts_with("Error: Can't find stylesheet to import.\n") =>
{
()
}
Ok(..) => panic!("did not fail"),
Err(e) => panic!("failed in the wrong way: {}", e),
}
}
#[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}";