diff --git a/src/lexer.rs b/src/lexer.rs index d9ec049..d62453a 100644 --- a/src/lexer.rs +++ b/src/lexer.rs @@ -4,7 +4,6 @@ use std::str::Chars; use crate::atrule::AtRuleKind; use crate::common::{Keyword, Op, Pos, Symbol}; -use crate::selector::{Attribute, AttributeKind, CaseKind}; use crate::{Token, TokenKind, Whitespace}; // Rust does not allow us to escape '\f' @@ -125,14 +124,12 @@ impl<'a> Iterator for Lexer<'a> { '|' => symbol!(self, BitOr), '/' => self.lex_forward_slash(), '%' => symbol!(self, Percent), - '[' => { - self.buf.next(); - self.pos.next_char(); - self.lex_attr() - } + '[' => symbol!(self, OpenSquareBrace), + ']' => symbol!(self, CloseSquareBrace), '!' => self.lex_exclamation(), '<' => symbol!(self, Lt), '>' => symbol!(self, Gt), + '^' => symbol!(self, Xor), '\0' => return None, &v => { self.buf.next(); @@ -147,6 +144,7 @@ impl<'a> Iterator for Lexer<'a> { } } +#[allow(dead_code)] fn is_whitespace(c: char) -> bool { c == ' ' || c == '\n' || c == '\r' || c == FORM_FEED } @@ -189,6 +187,7 @@ impl<'a> Lexer<'a> { } } + #[allow(dead_code)] fn devour_whitespace(&mut self) -> bool { let mut found_whitespace = false; while let Some(c) = self.buf.peek() { @@ -292,139 +291,6 @@ impl<'a> Lexer<'a> { TokenKind::Symbol(Symbol::Hash) } - fn lex_attr(&mut self) -> TokenKind { - let mut attr = String::with_capacity(99); - self.devour_whitespace(); - while let Some(c) = self.buf.peek() { - if !c.is_alphabetic() && c != &'-' && c != &'_' { - break; - } - let tok = self - .buf - .next() - .expect("this is impossible because we have already peeked"); - self.pos.next_char(); - attr.push(tok); - } - - self.devour_whitespace(); - - let kind = match self - .buf - .next() - .expect("todo! expected kind (should be error)") - { - ']' => { - return TokenKind::Attribute(Attribute { - kind: AttributeKind::Any, - attr, - value: String::new(), - case_sensitive: CaseKind::Sensitive, - }) - } - 'i' => { - self.devour_whitespace(); - assert!(self.buf.next() == Some(']')); - return TokenKind::Attribute(Attribute { - kind: AttributeKind::Any, - attr, - value: String::new(), - case_sensitive: CaseKind::InsensitiveLowercase, - }); - } - 'I' => { - self.devour_whitespace(); - assert!(self.buf.next() == Some(']')); - return TokenKind::Attribute(Attribute { - kind: AttributeKind::Any, - attr, - value: String::new(), - case_sensitive: CaseKind::InsensitiveCapital, - }); - } - '=' => AttributeKind::Equals, - '~' => AttributeKind::InList, - '|' => AttributeKind::BeginsWithHyphenOrExact, - '^' => AttributeKind::StartsWith, - '$' => AttributeKind::EndsWith, - '*' => AttributeKind::Contains, - _ => todo!("Expected ']'"), - }; - - if kind != AttributeKind::Equals { - assert!(self.buf.next() == Some('=')); - } - - self.devour_whitespace(); - - let mut value = String::with_capacity(99); - let case_sensitive = CaseKind::Sensitive; - - while let Some(c) = self.buf.peek() { - if c == &']' || c.is_whitespace() { - break; - } - - let tok = self - .buf - .next() - .expect("this is impossible because we have already peeked"); - self.pos.next_char(); - value.push(tok); - } - - if self.devour_whitespace() { - let n = self.buf.next(); - match n { - Some('i') | Some('I') => { - let case_sensitive = match n { - Some('i') => CaseKind::InsensitiveLowercase, - Some('I') => CaseKind::InsensitiveCapital, - _ => unsafe { std::hint::unreachable_unchecked() }, - }; - self.pos.next_char(); - self.devour_whitespace(); - match self.buf.next() { - Some(']') => { - return TokenKind::Attribute(Attribute { - kind, - attr, - value, - case_sensitive, - }) - } - Some(_) => todo!("modifier must be 1 character"), - None => todo!("unexpected EOF"), - } - } - Some(']') => { - return TokenKind::Attribute(Attribute { - kind, - attr, - value, - case_sensitive, - }) - } - Some(c) => { - value.push(' '); - value.push(c.clone()); - self.devour_whitespace(); - assert!(self.buf.next() == Some(']')); - } - None => todo!(), - } - } else { - assert!(self.buf.next() == Some(']')); - } - - TokenKind::Attribute(Attribute { - kind, - attr, - value, - case_sensitive, - }) - } - fn lex_variable(&mut self) -> TokenKind { self.buf.next(); self.pos.next_char(); @@ -444,7 +310,11 @@ impl<'a> Lexer<'a> { name.push(tok); } } - TokenKind::Variable(name) + if name.is_empty() { + TokenKind::Symbol(Symbol::Dollar) + } else { + TokenKind::Variable(name) + } } fn lex_ident(&mut self) -> TokenKind { diff --git a/src/lib.rs b/src/lib.rs index b2f14d2..7f439b3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -67,7 +67,7 @@ use crate::function::Function; use crate::imports::import; use crate::lexer::Lexer; use crate::mixin::{eat_include, Mixin}; -use crate::selector::{Attribute, Selector}; +use crate::selector::Selector; use crate::style::Style; use crate::utils::{devour_whitespace, eat_variable_value, IsComment, IsWhitespace, VariableDecl}; use crate::value::Value; @@ -147,7 +147,6 @@ pub(crate) enum TokenKind { Number(String), Whitespace(Whitespace), Variable(String), - Attribute(Attribute), Op(Op), MultilineComment(String), Interpolation, @@ -169,7 +168,6 @@ impl Display for TokenKind { TokenKind::AtRule(s) => write!(f, "{}", s), TokenKind::Op(s) => write!(f, "{}", s), TokenKind::Whitespace(s) => write!(f, "{}", s), - TokenKind::Attribute(s) => write!(f, "{}", s), TokenKind::Keyword(kw) => write!(f, "{}", kw), TokenKind::MultilineComment(s) => write!(f, "/*{}*/", s), TokenKind::Variable(s) => write!(f, "{}", s), @@ -347,8 +345,8 @@ impl<'a> StyleSheetParser<'a> { while let Some(Token { kind, .. }) = self.lexer.peek() { match kind { TokenKind::Ident(_) - | TokenKind::Attribute(_) | TokenKind::Interpolation + | TokenKind::Symbol(Symbol::OpenSquareBrace) | TokenKind::Symbol(Symbol::Hash) | TokenKind::Symbol(Symbol::Colon) | TokenKind::Symbol(Symbol::Mul) diff --git a/src/selector.rs b/src/selector.rs index 055c7c6..6af2500 100644 --- a/src/selector.rs +++ b/src/selector.rs @@ -1,7 +1,8 @@ use crate::common::{Scope, Symbol, Whitespace}; use crate::error::SassResult; use crate::utils::{ - devour_whitespace, devour_whitespace_or_comment, parse_interpolation, IsWhitespace, + devour_whitespace, devour_whitespace_or_comment, flatten_ident, parse_interpolation, + parse_quoted_string, IsWhitespace, }; use crate::{Token, TokenKind}; use std::fmt::{self, Display, Write}; @@ -288,7 +289,9 @@ impl<'a> SelectorParser<'a> { )?; self.is_interpolated = false; } - TokenKind::Attribute(attr) => self.selectors.push(SelectorKind::Attribute(attr)), + TokenKind::Symbol(Symbol::OpenSquareBrace) => self + .selectors + .push(Attribute::from_tokens(tokens, self.scope)?), _ => todo!("unimplemented selector"), }; } @@ -354,48 +357,145 @@ impl Selector { pub(crate) struct Attribute { pub attr: String, pub value: String, - pub case_sensitive: CaseKind, + pub modifier: String, pub kind: AttributeKind, } -#[derive(Clone, Debug, Eq, PartialEq)] -pub(crate) enum CaseKind { - InsensitiveCapital, - InsensitiveLowercase, - Sensitive, -} +impl Attribute { + pub fn from_tokens( + toks: &mut Peekable>, + scope: &Scope, + ) -> SassResult { + devour_whitespace(toks); + let attr = if let Some(t) = toks.next() { + match t.kind { + TokenKind::Ident(mut s) => { + s.push_str(&flatten_ident(toks, scope)?); + s + } + q @ TokenKind::Symbol(Symbol::DoubleQuote) + | q @ TokenKind::Symbol(Symbol::SingleQuote) => { + parse_quoted_string(toks, scope, q)? + } + _ => return Err("Expected identifier.".into()), + } + } else { + todo!() + }; -impl Display for CaseKind { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::InsensitiveCapital => write!(f, " I"), - Self::InsensitiveLowercase => write!(f, " i"), - Self::Sensitive => write!(f, ""), + devour_whitespace(toks); + + let kind = if let Some(t) = toks.next() { + match t.kind { + TokenKind::Ident(s) if s.len() == 1 => { + devour_whitespace(toks); + match toks.next().unwrap().kind { + TokenKind::Symbol(Symbol::CloseSquareBrace) => {} + _ => return Err("expected \"]\".".into()), + } + return Ok(SelectorKind::Attribute(Attribute { + kind: AttributeKind::Any, + attr, + value: String::new(), + modifier: s, + })); + } + TokenKind::Symbol(Symbol::CloseSquareBrace) => { + return Ok(SelectorKind::Attribute(Attribute { + kind: AttributeKind::Any, + attr, + value: String::new(), + modifier: String::new(), + })); + } + TokenKind::Symbol(Symbol::Equal) => AttributeKind::Equals, + TokenKind::Symbol(Symbol::Tilde) => AttributeKind::InList, + TokenKind::Symbol(Symbol::BitOr) => AttributeKind::BeginsWithHyphenOrExact, + TokenKind::Symbol(Symbol::Xor) => AttributeKind::StartsWith, + TokenKind::Symbol(Symbol::Dollar) => AttributeKind::EndsWith, + TokenKind::Symbol(Symbol::Mul) => AttributeKind::Contains, + _ => return Err("Expected \"]\".".into()), + } + } else { + todo!() + }; + + if kind != AttributeKind::Equals { + match toks.next().unwrap().kind { + TokenKind::Symbol(Symbol::Equal) => {} + _ => return Err("expected \"=\".".into()), + } } + + devour_whitespace(toks); + + let value = if let Some(t) = toks.next() { + match t.kind { + TokenKind::Ident(mut s) => { + s.push_str(&flatten_ident(toks, scope)?); + s + } + q @ TokenKind::Symbol(Symbol::DoubleQuote) + | q @ TokenKind::Symbol(Symbol::SingleQuote) => { + parse_quoted_string(toks, scope, q)? + } + _ => return Err("Expected identifier.".into()), + } + } else { + todo!() + }; + + devour_whitespace(toks); + + let modifier = if let Some(t) = toks.next() { + match t.kind { + TokenKind::Symbol(Symbol::CloseSquareBrace) => { + return Ok(SelectorKind::Attribute(Attribute { + kind, + attr, + value, + modifier: String::new(), + })) + } + TokenKind::Ident(s) if s.len() == 1 => { + match toks.next().unwrap().kind { + TokenKind::Symbol(Symbol::CloseSquareBrace) => {} + _ => return Err("expected \"]\".".into()), + } + format!(" {}", s) + } + _ => return Err("Expected \"]\".".into()), + } + } else { + todo!() + }; + + Ok(SelectorKind::Attribute(Attribute { + kind, + attr, + value, + modifier, + })) } } impl Display for Attribute { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self.kind { - AttributeKind::Any => write!(f, "[{}{}]", self.attr, self.case_sensitive), - AttributeKind::Equals => { - write!(f, "[{}={}{}]", self.attr, self.value, self.case_sensitive) - } - AttributeKind::InList => { - write!(f, "[{}~={}{}]", self.attr, self.value, self.case_sensitive) - } + AttributeKind::Any => write!(f, "[{}{}]", self.attr, self.modifier), + AttributeKind::Equals => write!(f, "[{}={}{}]", self.attr, self.value, self.modifier), + AttributeKind::InList => write!(f, "[{}~={}{}]", self.attr, self.value, self.modifier), AttributeKind::BeginsWithHyphenOrExact => { - write!(f, "[{}|={}{}]", self.attr, self.value, self.case_sensitive) + write!(f, "[{}|={}{}]", self.attr, self.value, self.modifier) } AttributeKind::StartsWith => { - write!(f, "[{}^={}{}]", self.attr, self.value, self.case_sensitive) + write!(f, "[{}^={}{}]", self.attr, self.value, self.modifier) } AttributeKind::EndsWith => { - write!(f, "[{}$={}{}]", self.attr, self.value, self.case_sensitive) + write!(f, "[{}$={}{}]", self.attr, self.value, self.modifier) } AttributeKind::Contains => { - write!(f, "[{}*={}{}]", self.attr, self.value, self.case_sensitive) + write!(f, "[{}*={}{}]", self.attr, self.value, self.modifier) } } } diff --git a/src/utils.rs b/src/utils.rs index 9e125a1..9a236fb 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,4 +1,4 @@ -use crate::common::{Keyword, Symbol}; +use crate::common::{Keyword, QuoteKind, Symbol}; use crate::error::SassResult; use crate::lexer::Lexer; use crate::value::Value; @@ -117,3 +117,78 @@ pub(crate) fn eat_variable_value>( let val = Value::from_tokens(&mut raw.into_iter().peekable(), scope).unwrap(); Ok(VariableDecl::new(val, default)) } + +pub(crate) fn flatten_ident>( + toks: &mut Peekable, + scope: &Scope, +) -> SassResult { + let mut s = String::new(); + while let Some(tok) = toks.peek() { + match tok.kind.clone() { + TokenKind::Interpolation => { + toks.next(); + s.push_str( + &parse_interpolation(toks, scope)? + .iter() + .map(|x| x.kind.to_string()) + .collect::(), + ) + } + TokenKind::Ident(ref i) => { + toks.next(); + s.push_str(i) + } + TokenKind::Number(ref n) => { + toks.next(); + s.push_str(n) + } + _ => break, + } + } + Ok(s) +} + +pub(crate) fn parse_quoted_string>( + toks: &mut Peekable, + scope: &Scope, + q: TokenKind, +) -> SassResult { + let mut s = String::new(); + let mut is_escaped = false; + while let Some(tok) = toks.next() { + match tok.kind { + TokenKind::Symbol(Symbol::DoubleQuote) + if !is_escaped && q == TokenKind::Symbol(Symbol::DoubleQuote) => + { + break + } + TokenKind::Symbol(Symbol::SingleQuote) + if !is_escaped && q == TokenKind::Symbol(Symbol::SingleQuote) => + { + break + } + TokenKind::Symbol(Symbol::BackSlash) if !is_escaped => is_escaped = true, + TokenKind::Symbol(Symbol::BackSlash) => s.push('\\'), + TokenKind::Interpolation => { + s.push_str( + &parse_interpolation(toks, scope)? + .iter() + .map(|x| x.kind.to_string()) + .collect::(), + ); + continue; + } + _ => {} + } + if is_escaped && tok.kind != TokenKind::Symbol(Symbol::BackSlash) { + is_escaped = false; + } + s.push_str(&tok.kind.to_string()); + } + let quotes = match q { + TokenKind::Symbol(Symbol::DoubleQuote) => QuoteKind::Double, + TokenKind::Symbol(Symbol::SingleQuote) => QuoteKind::Single, + _ => unreachable!(), + }; + Ok(format!("{}{}{}", quotes, s, quotes)) +}