diff --git a/src/common.rs b/src/common.rs index 6003c3c..3ca09a5 100644 --- a/src/common.rs +++ b/src/common.rs @@ -218,6 +218,7 @@ pub enum Keyword { True, False, Null, + Default, // Infinity, // NaN, // Auto, @@ -237,6 +238,7 @@ impl Display for Keyword { Self::True => write!(f, "true"), Self::False => write!(f, "false"), Self::Null => write!(f, "null"), + Self::Default => write!(f, "!default"), // Self::Infinity => write!(f, "Infinity"), // Self::NaN => write!(f, "NaN"), // Self::Auto => write!(f, "auto"), @@ -258,6 +260,7 @@ impl Into<&'static str> for Keyword { Self::True => "true", Self::False => "false", Self::Null => "null", + Self::Default => "!default", // Self::Infinity => "Infinity", // Self::NaN => "NaN", // Self::Auto => "auto", @@ -282,6 +285,7 @@ impl TryFrom<&str> for Keyword { "true" => Ok(Self::True), "false" => Ok(Self::False), "null" => Ok(Self::Null), + "default" => Ok(Self::Default), // "infinity" => Ok(Self::Infinity), // "nan" => Ok(Self::NaN), // "auto" => Ok(Self::Auto), diff --git a/src/lexer.rs b/src/lexer.rs index 9f25577..3c392fe 100644 --- a/src/lexer.rs +++ b/src/lexer.rs @@ -117,14 +117,19 @@ impl<'a> Lexer<'a> { Some('i') | Some('I') => { self.buf.next(); assert_char!(self, 'm' 'p' 'o' 'r' 't' 'a' 'n' 't'); + TokenKind::Keyword(Keyword::Important) + } + Some('d') | Some('D') => { + self.buf.next(); + assert_char!(self, 'e' 'f' 'a' 'u' 'l' 't'); + TokenKind::Keyword(Keyword::Default) } Some('=') => { self.buf.next(); - return TokenKind::Op(Op::NotEqual); + TokenKind::Op(Op::NotEqual) } _ => todo!("expected either `i` or `=` after `!`"), - }; - TokenKind::Keyword(Keyword::Important) + } } fn devour_whitespace(&mut self) -> bool { diff --git a/src/lib.rs b/src/lib.rs index 1df6f4d..4db5f94 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -58,7 +58,7 @@ use crate::lexer::Lexer; use crate::mixin::{eat_include, Mixin}; use crate::selector::{Attribute, Selector}; use crate::style::Style; -use crate::utils::{devour_whitespace, eat_variable_value, IsComment, IsWhitespace}; +use crate::utils::{devour_whitespace, eat_variable_value, IsComment, IsWhitespace, VariableDecl}; use crate::value::Value; mod args; @@ -346,9 +346,11 @@ impl<'a> StyleSheetParser<'a> { { self.error(pos, "unexpected variable use at toplevel"); } - let val = eat_variable_value(&mut self.lexer, &self.global_scope) + let VariableDecl { val, default } = eat_variable_value(&mut self.lexer, &self.global_scope) .unwrap_or_else(|err| self.error(err.0, &err.1)); - self.global_scope.vars.insert(name, val); + if !default || self.global_scope.vars.get(&name).is_none() { + self.global_scope.vars.insert(name, val); + } } TokenKind::MultilineComment(_) => { let comment = match self @@ -533,10 +535,10 @@ pub(crate) fn eat_expr>( { toks.next(); devour_whitespace(toks); - return Ok(Some(Expr::VariableDecl( - name, - eat_variable_value(toks, scope)?, - ))); + let VariableDecl { val, default } = eat_variable_value(toks, scope)?; + if !default || scope.vars.get(&name).is_none() { + return Ok(Some(Expr::VariableDecl(name, val))); + } } else { values.push(Token { kind: TokenKind::Variable(name), diff --git a/src/selector.rs b/src/selector.rs index 5187e6d..c6fafcf 100644 --- a/src/selector.rs +++ b/src/selector.rs @@ -272,7 +272,9 @@ impl<'a> SelectorParser<'a> { TokenKind::Symbol(Symbol::Plus) => self.selectors.push(SelectorKind::Following), TokenKind::Symbol(Symbol::Tilde) => self.selectors.push(SelectorKind::Preceding), TokenKind::Symbol(Symbol::Mul) => self.selectors.push(SelectorKind::Universal), - TokenKind::Symbol(Symbol::Percent) => self.selectors.push(SelectorKind::Placeholder), + TokenKind::Symbol(Symbol::Percent) => { + self.selectors.push(SelectorKind::Placeholder) + } TokenKind::Symbol(Symbol::BitAnd) => self.selectors.push(if self.is_interpolated { SelectorKind::InterpolatedSuper } else { diff --git a/src/utils.rs b/src/utils.rs index e29ea65..8d63852 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,4 +1,4 @@ -use crate::common::{Pos, Symbol}; +use crate::common::{Keyword, Pos, Symbol}; use crate::lexer::Lexer; use crate::value::Value; use crate::{Scope, Token, TokenKind}; @@ -64,15 +64,51 @@ pub(crate) fn eat_interpolation>( val } +pub(crate) struct VariableDecl { + pub val: Value, + pub default: bool, +} + +impl VariableDecl { + pub fn new(val: Value, default: bool) -> VariableDecl { + VariableDecl { val, default } + } +} + pub(crate) fn eat_variable_value>( toks: &mut Peekable, scope: &Scope, -) -> Result { +) -> Result { devour_whitespace(toks); - // todo!(line might not end with semicolon) - let iter1: Vec = toks - .take_while(|x| x.kind != TokenKind::Symbol(Symbol::SemiColon)) - .collect(); + let mut default = false; + let mut raw: Vec = Vec::new(); + let mut nesting = 0; + while let Some(tok) = toks.peek() { + match tok.kind { + TokenKind::Symbol(Symbol::SemiColon) => { + toks.next(); + break; + } + TokenKind::Keyword(Keyword::Default) => { + toks.next(); + default = true + } + TokenKind::Interpolation | TokenKind::Symbol(Symbol::OpenCurlyBrace) => { + nesting += 1; + raw.push(toks.next().unwrap()); + } + TokenKind::Symbol(Symbol::CloseCurlyBrace) => { + if nesting == 0 { + break; + } else { + nesting -= 1; + raw.push(toks.next().unwrap()); + } + } + _ => raw.push(toks.next().unwrap()), + } + } devour_whitespace(toks); - Ok(Value::from_tokens(&mut iter1.into_iter().peekable(), scope).unwrap()) + let val = Value::from_tokens(&mut raw.into_iter().peekable(), scope).unwrap(); + Ok(VariableDecl::new(val, default)) } diff --git a/src/value.rs b/src/value.rs index 2072b21..a30d9ec 100644 --- a/src/value.rs +++ b/src/value.rs @@ -304,7 +304,9 @@ impl Value { } Some(Value::Ident(s, QuoteKind::Single)) } - TokenKind::Variable(ref v) => Some(scope.vars.get(v).expect("expected variable").clone()), + TokenKind::Variable(ref v) => { + Some(scope.vars.get(v).expect("expected variable").clone()) + } TokenKind::Interpolation => { let mut s = eat_interpolation(toks, scope) .iter() diff --git a/tests/main.rs b/tests/main.rs index 30ec77b..7b66384 100644 --- a/tests/main.rs +++ b/tests/main.rs @@ -100,6 +100,36 @@ mod test_variables { "$var1: red; a {\n color: $var1;\n}\n", "a {\n color: red;\n}\n" ); + test!( + default_var_after, + "$a: red;\n$a: blue !default;\na {\n color: $a;\n}", + "a {\n color: red;\n}\n" + ); + test!( + default_var_before, + "$a: red !default;\n$a: blue;\na {\n color: $a;\n}", + "a {\n color: blue;\n}\n" + ); + test!( + default_var_whitespace, + "$a: red !default ;\na {\n color: $a;\n}", + "a {\n color: red;\n}\n" + ); + test!( + default_var_inside_rule, + "a {\n $a: red;\n $a: blue !default;\n color: $a;\n}", + "a {\n color: red;\n}\n" + ); + test!( + interpolation_in_variable, + "$a: #{red};\na {\n color: $a\n}\n", + "a {\n color: red;\n}\n" + ); + test!( + variable_decl_doesnt_end_in_semicolon, + "a {\n $a: red\n}\n\nb {\n color: blue;\n}\n", + "b {\n color: blue;\n}\n" + ); } #[cfg(test)]