diff --git a/src/args.rs b/src/args.rs index a9f702a..4dc1e97 100644 --- a/src/args.rs +++ b/src/args.rs @@ -1,8 +1,9 @@ use std::collections::BTreeMap; use std::iter::Peekable; -use crate::common::{Scope, Symbol}; +use crate::common::Symbol; use crate::error::SassResult; +use crate::scope::Scope; use crate::selector::Selector; use crate::utils::{devour_whitespace, devour_whitespace_or_comment}; use crate::value::Value; diff --git a/src/atrule/mod.rs b/src/atrule/mod.rs index 4b56be9..31b0724 100644 --- a/src/atrule/mod.rs +++ b/src/atrule/mod.rs @@ -3,10 +3,11 @@ use std::iter::Peekable; use num_traits::cast::ToPrimitive; -use crate::common::{Keyword, Pos, Scope, Symbol}; +use crate::common::{Keyword, Pos, Symbol}; use crate::error::SassResult; use crate::function::Function; use crate::mixin::Mixin; +use crate::scope::Scope; use crate::selector::Selector; use crate::units::Unit; use crate::utils::{devour_whitespace, devour_whitespace_or_comment}; diff --git a/src/atrule/parse.rs b/src/atrule/parse.rs index 1fd03d4..af88976 100644 --- a/src/atrule/parse.rs +++ b/src/atrule/parse.rs @@ -1,7 +1,7 @@ use std::iter::Peekable; -use crate::common::Scope; use crate::error::SassResult; +use crate::scope::Scope; use crate::selector::Selector; use crate::{eat_expr, Expr, RuleSet, Stmt, Token}; diff --git a/src/atrule/unknown.rs b/src/atrule/unknown.rs index f398c08..8d54eeb 100644 --- a/src/atrule/unknown.rs +++ b/src/atrule/unknown.rs @@ -1,8 +1,9 @@ use std::iter::Peekable; use super::parse::eat_stmts; -use crate::common::{Scope, Symbol}; +use crate::common::Symbol; use crate::error::SassResult; +use crate::scope::Scope; use crate::selector::Selector; use crate::utils::{devour_whitespace, parse_interpolation}; use crate::{RuleSet, Stmt, Token, TokenKind}; diff --git a/src/builtin/mod.rs b/src/builtin/mod.rs index 91e748b..7eaee7d 100644 --- a/src/builtin/mod.rs +++ b/src/builtin/mod.rs @@ -2,8 +2,8 @@ use once_cell::sync::Lazy; use std::collections::HashMap; use crate::args::CallArgs; -use crate::common::Scope; use crate::error::SassResult; +use crate::scope::Scope; use crate::value::Value; #[macro_use] diff --git a/src/common.rs b/src/common.rs index a9827e3..d7c5e25 100644 --- a/src/common.rs +++ b/src/common.rs @@ -1,12 +1,6 @@ -use std::collections::HashMap; use std::convert::TryFrom; use std::fmt::{self, Display}; -use crate::error::SassResult; -use crate::function::Function; -use crate::mixin::Mixin; -use crate::value::Value; - #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub enum Symbol { /// . @@ -220,6 +214,7 @@ pub enum Keyword { False, Null, Default, + Global, From(String), To(String), Through(String), @@ -243,7 +238,7 @@ impl Display for Keyword { Self::False => write!(f, "false"), Self::Null => write!(f, "null"), Self::Default => write!(f, "!default"), - // todo!(maintain casing for keywords) + Self::Global => write!(f, "!global"), Self::From(s) => write!(f, "{}", s), Self::To(s) => write!(f, "{}", s), Self::Through(s) => write!(f, "{}", s), @@ -269,6 +264,7 @@ impl Into<&'static str> for Keyword { Self::False => "false", Self::Null => "null", Self::Default => "!default", + Self::Global => "!global", Self::From(_) => "from", Self::To(_) => "to", Self::Through(_) => "through", @@ -296,6 +292,7 @@ impl TryFrom<&str> for Keyword { "false" => Ok(Self::False), "null" => Ok(Self::Null), "default" => Ok(Self::Default), + "global" => Ok(Self::Global), "from" => Ok(Self::From(kw.to_owned())), "to" => Ok(Self::To(kw.to_owned())), "through" => Ok(Self::Through(kw.to_owned())), @@ -353,75 +350,6 @@ impl Display for Pos { } } -#[derive(Debug, Clone)] -pub(crate) struct Scope { - vars: HashMap, - mixins: HashMap, - functions: HashMap, -} - -impl Scope { - #[must_use] - pub fn new() -> Self { - Self { - vars: HashMap::new(), - mixins: HashMap::new(), - functions: HashMap::new(), - } - } - - pub fn get_var(&self, v: &str) -> SassResult<&Value> { - match self.vars.get(&v.replace('_', "-")) { - Some(v) => Ok(v), - None => Err("Undefined variable.".into()), - } - } - - pub fn insert_var(&mut self, s: &str, v: Value) -> SassResult> { - Ok(self.vars.insert(s.replace('_', "-"), v.eval()?)) - } - - pub fn var_exists(&self, v: &str) -> bool { - self.vars.contains_key(&v.replace('_', "-")) - } - - pub fn get_mixin(&self, v: &str) -> SassResult<&Mixin> { - match self.mixins.get(&v.replace('_', "-")) { - Some(v) => Ok(v), - None => Err("Undefined mixin.".into()), - } - } - - pub fn insert_mixin(&mut self, s: &str, v: Mixin) -> Option { - self.mixins.insert(s.replace('_', "-"), v) - } - - pub fn mixin_exists(&self, v: &str) -> bool { - self.mixins.contains_key(&v.replace('_', "-")) - } - - pub fn get_fn(&self, v: &str) -> SassResult<&Function> { - match self.functions.get(&v.replace('_', "-")) { - Some(v) => Ok(v), - None => Err("Undefined function.".into()), - } - } - - pub fn insert_fn(&mut self, s: &str, v: Function) -> Option { - self.functions.insert(s.replace('_', "-"), v) - } - - pub fn fn_exists(&self, v: &str) -> bool { - self.functions.contains_key(&v.replace('_', "-")) - } - - pub fn extend(&mut self, other: Scope) { - self.vars.extend(other.vars); - self.mixins.extend(other.mixins); - self.functions.extend(other.functions); - } -} - #[derive(Debug, Copy, Clone, Eq, PartialEq)] pub(crate) enum QuoteKind { Single, diff --git a/src/function.rs b/src/function.rs index 7cbdf72..18845b3 100644 --- a/src/function.rs +++ b/src/function.rs @@ -2,8 +2,9 @@ use std::iter::Peekable; use crate::args::{eat_func_args, CallArgs, FuncArgs}; use crate::atrule::AtRule; -use crate::common::{Scope, Symbol}; +use crate::common::Symbol; use crate::error::SassResult; +use crate::scope::Scope; use crate::selector::Selector; use crate::utils::devour_whitespace; use crate::value::Value; diff --git a/src/imports.rs b/src/imports.rs index f178728..f625617 100644 --- a/src/imports.rs +++ b/src/imports.rs @@ -1,8 +1,8 @@ use std::ffi::OsStr; use std::path::Path; -use crate::common::Scope; use crate::error::SassResult; +use crate::scope::Scope; use crate::{Stmt, StyleSheet}; pub(crate) fn import>(path: P) -> SassResult<(Vec, Scope)> { diff --git a/src/lexer.rs b/src/lexer.rs index ad0b36e..74b07a1 100644 --- a/src/lexer.rs +++ b/src/lexer.rs @@ -180,6 +180,11 @@ impl<'a> Lexer<'a> { assert_char!(self, 'e' 'f' 'a' 'u' 'l' 't'); TokenKind::Keyword(Keyword::Default) } + Some('g') | Some('G') => { + self.buf.next(); + assert_char!(self, 'l' 'o' 'b' 'a' 'l'); + TokenKind::Keyword(Keyword::Global) + } Some('=') => { self.buf.next(); TokenKind::Op(Op::NotEqual) diff --git a/src/lib.rs b/src/lib.rs index 0d5a745..6cb39d1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -85,7 +85,7 @@ use std::iter::{Iterator, Peekable}; use std::path::Path; use crate::atrule::{AtRule, AtRuleKind}; -use crate::common::{Keyword, Op, Pos, Scope, Symbol, Whitespace}; +use crate::common::{Keyword, Op, Pos, Symbol, Whitespace}; use crate::css::Css; use crate::error::SassError; pub use crate::error::SassResult; @@ -94,6 +94,7 @@ use crate::function::Function; use crate::imports::import; use crate::lexer::Lexer; use crate::mixin::{eat_include, Mixin}; +use crate::scope::{insert_global_var, Scope, GLOBAL_SCOPE}; use crate::selector::Selector; use crate::style::Style; use crate::utils::{devour_whitespace, eat_variable_value, IsComment, IsWhitespace, VariableDecl}; @@ -111,12 +112,18 @@ mod function; mod imports; mod lexer; mod mixin; +mod scope; mod selector; mod style; mod units; mod utils; mod value; +pub(crate) fn error>(msg: E) -> ! { + eprintln!("Error: {}", msg.into()); + std::process::exit(1); +} + #[derive(Clone, Debug, Eq, PartialEq)] pub(crate) struct Token { pos: Pos, @@ -391,7 +398,7 @@ impl<'a> StyleSheetParser<'a> { | TokenKind::Symbol(Symbol::Mul) | TokenKind::Symbol(Symbol::Percent) | TokenKind::Symbol(Symbol::Period) => rules - .extend(self.eat_rules(&Selector::new(), &mut self.global_scope.clone())?), + .extend(self.eat_rules(&Selector::new(), &mut GLOBAL_SCOPE.with(|s| s.borrow().clone()))?), TokenKind::Whitespace(_) => { self.lexer.next(); continue; @@ -415,11 +422,16 @@ impl<'a> StyleSheetParser<'a> { { self.error(pos, "unexpected variable use at toplevel"); } - let VariableDecl { val, default } = - eat_variable_value(&mut self.lexer, &self.global_scope, &Selector::new())?; - if !default || self.global_scope.get_var(&name).is_err() { - self.global_scope.insert_var(&name, val)?; - } + let VariableDecl { val, default, .. } = + eat_variable_value(&mut self.lexer, &GLOBAL_SCOPE.with(|s| s.borrow().clone()), &Selector::new())?; + GLOBAL_SCOPE.with(|s| { + if !default || s.borrow().get_var(&name).is_err() { + match s.borrow_mut().insert_var(&name, val) { + Ok(..) => {}, + Err(e) => error(e), + } + } + }) } TokenKind::MultilineComment(_) => { let comment = match self @@ -435,7 +447,7 @@ impl<'a> StyleSheetParser<'a> { } TokenKind::AtRule(AtRuleKind::Include) => rules.extend(eat_include( &mut self.lexer, - &self.global_scope, + &GLOBAL_SCOPE.with(|s| s.borrow().clone()), &Selector::new(), )?), TokenKind::AtRule(AtRuleKind::Import) => { @@ -479,7 +491,9 @@ impl<'a> StyleSheetParser<'a> { let (new_rules, new_scope) = import(file_name)?; rules.extend(new_rules); - self.global_scope.extend(new_scope); + GLOBAL_SCOPE.with(|s| { + s.borrow_mut().extend(new_scope); + }); } TokenKind::AtRule(_) => { if let Some(Token { @@ -487,12 +501,16 @@ impl<'a> StyleSheetParser<'a> { pos, }) = self.lexer.next() { - match AtRule::from_tokens(rule, pos, &mut self.lexer, &mut self.global_scope, &Selector::new())? { + match AtRule::from_tokens(rule, pos, &mut self.lexer, &mut GLOBAL_SCOPE.with(|s| s.borrow().clone()), &Selector::new())? { AtRule::Mixin(name, mixin) => { - self.global_scope.insert_mixin(&name, *mixin); + GLOBAL_SCOPE.with(|s| { + s.borrow_mut().insert_mixin(&name, *mixin); + }); } AtRule::Function(name, func) => { - self.global_scope.insert_fn(&name, *func); + GLOBAL_SCOPE.with(|s| { + s.borrow_mut().insert_fn(&name, *func); + }); } AtRule::Charset => continue, AtRule::Error(pos, message) => self.error(pos, &message), @@ -518,7 +536,7 @@ impl<'a> StyleSheetParser<'a> { }, }; } - Ok((rules, self.global_scope)) + Ok((rules, GLOBAL_SCOPE.with(|s| s.borrow().clone()))) } fn eat_rules(&mut self, super_selector: &Selector, scope: &mut Scope) -> SassResult> { @@ -556,7 +574,7 @@ impl<'a> StyleSheetParser<'a> { Expr::VariableDecl(name, val) => { if self.scope == 0 { scope.insert_var(&name, *val.clone())?; - self.global_scope.insert_var(&name, *val)?; + insert_global_var(&name, *val)?; } else { scope.insert_var(&name, *val)?; } @@ -648,9 +666,14 @@ pub(crate) fn eat_expr>( { toks.next(); devour_whitespace(toks); - let VariableDecl { val, default } = - eat_variable_value(toks, scope, super_selector)?; - if !default || scope.get_var(&name).is_err() { + let VariableDecl { + val, + default, + global, + } = eat_variable_value(toks, scope, super_selector)?; + if global { + insert_global_var(&name, val)?; + } else if !default || scope.get_var(&name).is_err() { return Ok(Some(Expr::VariableDecl(name, Box::new(val)))); } } else { diff --git a/src/mixin.rs b/src/mixin.rs index 082c2fa..6b0ffcd 100644 --- a/src/mixin.rs +++ b/src/mixin.rs @@ -3,8 +3,9 @@ use std::vec::IntoIter; use crate::args::{eat_call_args, eat_func_args, CallArgs, FuncArgs}; use crate::atrule::AtRule; -use crate::common::{Scope, Symbol}; +use crate::common::Symbol; use crate::error::{SassError, SassResult}; +use crate::scope::Scope; use crate::selector::Selector; use crate::utils::devour_whitespace; use crate::{eat_expr, Expr, RuleSet, Stmt, Token, TokenKind}; diff --git a/src/scope.rs b/src/scope.rs new file mode 100644 index 0000000..2ff5975 --- /dev/null +++ b/src/scope.rs @@ -0,0 +1,94 @@ +use std::cell::RefCell; +use std::collections::HashMap; + +use crate::error::SassResult; +use crate::function::Function; +use crate::mixin::Mixin; +use crate::value::Value; + +thread_local!(pub(crate) static GLOBAL_SCOPE: RefCell = RefCell::new(Scope::new())); + +pub(crate) fn get_global_var(s: &str) -> SassResult { + GLOBAL_SCOPE.with(|scope| match scope.borrow().vars().get(s) { + Some(v) => Ok(v.clone()), + None => Err("Undefined variable.".into()), + }) +} + +pub(crate) fn insert_global_var(s: &str, v: Value) -> SassResult> { + GLOBAL_SCOPE.with(|scope| scope.borrow_mut().insert_var(s, v)) +} + +#[derive(Debug, Clone)] +pub(crate) struct Scope { + vars: HashMap, + mixins: HashMap, + functions: HashMap, +} + +impl Scope { + #[must_use] + pub fn new() -> Self { + Self { + vars: HashMap::new(), + mixins: HashMap::new(), + functions: HashMap::new(), + } + } + + pub fn vars(&self) -> &HashMap { + &self.vars + } + + pub fn get_var(&self, v: &str) -> SassResult { + let v = &v.replace('_', "-"); + match self.vars.get(v) { + Some(v) => Ok(v.clone()), + None => get_global_var(v), + } + } + + pub fn insert_var(&mut self, s: &str, v: Value) -> SassResult> { + Ok(self.vars.insert(s.replace('_', "-"), v.eval()?)) + } + + pub fn var_exists(&self, v: &str) -> bool { + self.vars.contains_key(&v.replace('_', "-")) + } + + pub fn get_mixin(&self, v: &str) -> SassResult<&Mixin> { + match self.mixins.get(&v.replace('_', "-")) { + Some(v) => Ok(v), + None => Err("Undefined mixin.".into()), + } + } + + pub fn insert_mixin(&mut self, s: &str, v: Mixin) -> Option { + self.mixins.insert(s.replace('_', "-"), v) + } + + pub fn mixin_exists(&self, v: &str) -> bool { + self.mixins.contains_key(&v.replace('_', "-")) + } + + pub fn get_fn(&self, v: &str) -> SassResult<&Function> { + match self.functions.get(&v.replace('_', "-")) { + Some(v) => Ok(v), + None => Err("Undefined function.".into()), + } + } + + pub fn insert_fn(&mut self, s: &str, v: Function) -> Option { + self.functions.insert(s.replace('_', "-"), v) + } + + pub fn fn_exists(&self, v: &str) -> bool { + self.functions.contains_key(&v.replace('_', "-")) + } + + pub fn extend(&mut self, other: Scope) { + self.vars.extend(other.vars); + self.mixins.extend(other.mixins); + self.functions.extend(other.functions); + } +} diff --git a/src/selector.rs b/src/selector.rs index b797fc0..c0f9bc0 100644 --- a/src/selector.rs +++ b/src/selector.rs @@ -2,9 +2,10 @@ use std::fmt::{self, Display, Write}; use std::iter::Peekable; use std::string::ToString; -use crate::common::{Scope, Symbol, Whitespace}; +use crate::common::{Symbol, Whitespace}; use crate::error::SassResult; use crate::lexer::Lexer; +use crate::scope::Scope; use crate::utils::{ devour_whitespace, devour_whitespace_or_comment, flatten_ident, parse_interpolation, parse_quoted_string, IsWhitespace, diff --git a/src/style.rs b/src/style.rs index e92b1da..30821ce 100644 --- a/src/style.rs +++ b/src/style.rs @@ -1,8 +1,9 @@ use std::fmt::{self, Display}; use std::iter::Peekable; -use crate::common::{Pos, QuoteKind, Scope, Symbol}; +use crate::common::{Pos, QuoteKind, Symbol}; use crate::error::SassResult; +use crate::scope::Scope; use crate::selector::Selector; use crate::utils::{devour_whitespace, parse_interpolation, parse_quoted_string}; use crate::value::Value; diff --git a/src/utils.rs b/src/utils.rs index 50d2d6f..1292065 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -83,11 +83,16 @@ pub(crate) fn parse_interpolation>( pub(crate) struct VariableDecl { pub val: Value, pub default: bool, + pub global: bool, } impl VariableDecl { - pub const fn new(val: Value, default: bool) -> VariableDecl { - VariableDecl { val, default } + pub const fn new(val: Value, default: bool, global: bool) -> VariableDecl { + VariableDecl { + val, + default, + global, + } } } @@ -98,6 +103,7 @@ pub(crate) fn eat_variable_value>( ) -> SassResult { devour_whitespace(toks); let mut default = false; + let mut global = false; let mut raw: Vec = Vec::new(); let mut nesting = 0; while let Some(tok) = toks.peek() { @@ -110,6 +116,10 @@ pub(crate) fn eat_variable_value>( toks.next(); default = true } + TokenKind::Keyword(Keyword::Global) => { + toks.next(); + global = true + } TokenKind::Interpolation | TokenKind::Symbol(Symbol::OpenCurlyBrace) => { nesting += 1; raw.push(toks.next().unwrap()); @@ -127,7 +137,7 @@ pub(crate) fn eat_variable_value>( } devour_whitespace(toks); let val = Value::from_tokens(&mut raw.into_iter().peekable(), scope, super_selector).unwrap(); - Ok(VariableDecl::new(val, default)) + Ok(VariableDecl::new(val, default, global)) } pub(crate) fn flatten_ident>( diff --git a/src/value/parse.rs b/src/value/parse.rs index 209cf40..5a4083e 100644 --- a/src/value/parse.rs +++ b/src/value/parse.rs @@ -8,8 +8,9 @@ use num_traits::pow; use crate::args::eat_call_args; use crate::builtin::GLOBAL_FUNCTIONS; use crate::color::Color; -use crate::common::{Keyword, ListSeparator, Op, QuoteKind, Scope, Symbol}; +use crate::common::{Keyword, ListSeparator, Op, QuoteKind, Symbol}; use crate::error::SassResult; +use crate::scope::Scope; use crate::selector::Selector; use crate::units::Unit; use crate::utils::{ diff --git a/tests/scope.rs b/tests/scope.rs new file mode 100644 index 0000000..ba1f8fc --- /dev/null +++ b/tests/scope.rs @@ -0,0 +1,15 @@ +#![cfg(test)] + +#[macro_use] +mod macros; + +test!( + scoping_var_decl_inner_ruleset, + "a {\n $color: red;\n b {\n $color: blue;\n }\n color: $color;\n}\n", + "a {\n color: blue;\n}\n" +); +test!( + basic_global, + "a {\n $color: red !global;\n}\n\nb {\n color: $color;\n}\n", + "b {\n color: red;\n}\n" +);