diff --git a/crates/compiler/src/evaluate/visitor.rs b/crates/compiler/src/evaluate/visitor.rs index e3a8017..c9335e3 100644 --- a/crates/compiler/src/evaluate/visitor.rs +++ b/crates/compiler/src/evaluate/visitor.rs @@ -2566,7 +2566,7 @@ impl<'a> Visitor<'a> { AstExpr::Paren(expr) => self.visit_expr((*expr).clone())?, AstExpr::ParentSelector => self.visit_parent_selector(), AstExpr::UnaryOp(op, expr, span) => self.visit_unary_op(op, (*expr).clone(), span)?, - AstExpr::Variable { name, namespace } => self.env.get_var(name, namespace)?, + AstExpr::Variable { name, namespace } => self.visit_variable(name, namespace)?, AstExpr::Supports(condition) => Value::String( self.visit_supports_condition((*condition).clone())?, QuoteKind::None, @@ -3079,4 +3079,22 @@ impl<'a> Visitor<'a> { Ok(None) } + + fn visit_variable( + &mut self, + name: Spanned, + namespace: Option>, + ) -> SassResult { + self.env.get_var(name, namespace).or_else(|e| { + if namespace.is_none() { + if let Some(v) = self.options.custom_vars.get(name.as_str()) { + Ok(v.clone()) + } else { + Err(e) + } + } else { + Err(e) + } + }) + } } diff --git a/crates/compiler/src/options.rs b/crates/compiler/src/options.rs index 71bae8a..7988a21 100644 --- a/crates/compiler/src/options.rs +++ b/crates/compiler/src/options.rs @@ -3,7 +3,7 @@ use std::{ path::{Path, PathBuf}, }; -use crate::{builtin::Builtin, Fs, Logger, StdFs, StdLogger}; +use crate::{builtin::Builtin, value::Value, Fs, Logger, StdFs, StdLogger}; /// Configuration for Sass compilation /// @@ -20,6 +20,7 @@ pub struct Options<'a> { pub(crate) quiet: bool, pub(crate) input_syntax: Option, pub(crate) custom_fns: HashMap, + pub(crate) custom_vars: HashMap, } impl Default for Options<'_> { @@ -35,6 +36,7 @@ impl Default for Options<'_> { quiet: false, input_syntax: None, custom_fns: HashMap::new(), + custom_vars: HashMap::new(), } } } @@ -177,6 +179,13 @@ impl<'a> Options<'a> { self } + #[must_use] + #[inline] + pub fn add_custom_var>(mut self, name: S, value: Value) -> Self { + self.custom_vars.insert(name.into(), value); + self + } + pub(crate) fn is_compressed(&self) -> bool { matches!(self.style, OutputStyle::Compressed) } diff --git a/crates/lib/tests/custom-global-variable.rs b/crates/lib/tests/custom-global-variable.rs new file mode 100644 index 0000000..cbc1b24 --- /dev/null +++ b/crates/lib/tests/custom-global-variable.rs @@ -0,0 +1,20 @@ +use grass::{from_string, Options}; +use grass_compiler::sass_value::{QuoteKind, Value}; + +#[test] +fn lookup_custom_global_variable() { + let opts = Options::default().add_custom_var("x", Value::String("foo".into(), QuoteKind::None)); + assert_eq!( + from_string("a {\n test: $x;\n}\n", &opts).ok(), + Some("a {\n test: foo;\n}\n".to_owned()) + ); +} + +#[test] +fn user_defined_takes_precedence_over_global_variable() { + let opts = Options::default().add_custom_var("x", Value::String("foo".into(), QuoteKind::None)); + assert_eq!( + from_string("$x: bar;\na {\n test: $x;\n}\n", &opts).ok(), + Some("a {\n test: bar;\n}\n".to_owned()) + ); +}