diff --git a/src/selector/attribute.rs b/src/selector/attribute.rs new file mode 100644 index 0000000..4edb123 --- /dev/null +++ b/src/selector/attribute.rs @@ -0,0 +1,183 @@ +use std::fmt::{self, Display}; +use std::iter::Peekable; +use std::string::ToString; + +use super::{Selector, SelectorKind}; +use crate::error::SassResult; +use crate::scope::Scope; +use crate::utils::{ + devour_whitespace, eat_ident, + parse_interpolation, parse_quoted_string, +}; +use crate::Token; + + +#[derive(Clone, Debug, Eq, PartialEq)] +pub(crate) struct Attribute { + pub attr: String, + pub value: String, + pub modifier: String, + pub kind: AttributeKind, +} + +impl Attribute { + pub fn from_tokens>( + toks: &mut Peekable, + scope: &Scope, + super_selector: &Selector, + ) -> SassResult { + devour_whitespace(toks); + let attr = if let Some(t) = toks.next() { + match t.kind { + v @ 'a'..='z' | v @ 'A'..='Z' | v @ '-' | v @ '_' => { + format!("{}{}", v, eat_ident(toks, scope, super_selector)?) + } + '#' if toks.next().unwrap().kind == '{' => { + parse_interpolation(toks, scope, super_selector)?.to_string() + } + q @ '"' | q @ '\'' => { + parse_quoted_string(toks, scope, q, super_selector)?.to_string() + } + _ => return Err("Expected identifier.".into()), + } + } else { + todo!() + }; + + devour_whitespace(toks); + + let kind = if let Some(t) = toks.next() { + match t.kind { + v @ 'a'..='z' | v @ 'A'..='Z' => { + match toks.next().unwrap().kind { + ']' => {} + _ => return Err("expected \"]\".".into()), + } + return Ok(SelectorKind::Attribute(Attribute { + kind: AttributeKind::Any, + attr, + value: String::new(), + modifier: v.to_string(), + })); + } + ']' => { + return Ok(SelectorKind::Attribute(Attribute { + kind: AttributeKind::Any, + attr, + value: String::new(), + modifier: String::new(), + })); + } + '=' => AttributeKind::Equals, + '~' => AttributeKind::InList, + '|' => AttributeKind::BeginsWithHyphenOrExact, + '^' => AttributeKind::StartsWith, + '$' => AttributeKind::EndsWith, + '*' => AttributeKind::Contains, + _ => return Err("Expected \"]\".".into()), + } + } else { + todo!() + }; + + if kind != AttributeKind::Equals { + match toks.next().unwrap().kind { + '=' => {} + _ => return Err("expected \"=\".".into()), + } + } + + devour_whitespace(toks); + + let value = if let Some(t) = toks.next() { + match t.kind { + v @ 'a'..='z' | v @ 'A'..='Z' | v @ '-' | v @ '_' => { + format!("{}{}", v, eat_ident(toks, scope, super_selector)?) + } + q @ '"' | q @ '\'' => { + parse_quoted_string(toks, scope, q, super_selector)?.to_string() + } + _ => return Err("Expected identifier.".into()), + } + } else { + todo!() + }; + + devour_whitespace(toks); + + let modifier = if let Some(t) = toks.next() { + match t.kind { + ']' => { + return Ok(SelectorKind::Attribute(Attribute { + kind, + attr, + value, + modifier: String::new(), + })) + } + v @ 'a'..='z' | v @ 'A'..='Z' => { + match toks.next().unwrap().kind { + ']' => {} + _ => return Err("expected \"]\".".into()), + } + format!(" {}", v) + } + _ => 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.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.modifier) + } + AttributeKind::StartsWith => { + write!(f, "[{}^={}{}]", self.attr, self.value, self.modifier) + } + AttributeKind::EndsWith => { + write!(f, "[{}$={}{}]", self.attr, self.value, self.modifier) + } + AttributeKind::Contains => { + write!(f, "[{}*={}{}]", self.attr, self.value, self.modifier) + } + } + } +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum AttributeKind { + /// [attr] + /// Represents elements with an attribute name of `attr` + Any, + /// [attr=value] + /// Represents elements with an attribute name of `attr` whose value is exactly `value` + Equals, + /// [attr~=value] + /// Represents elements with an attribute name of `attr` whose value is a whitespace-separated list of words, one of which is exactly `value` + InList, + /// [attr|=value] + /// Represents elements with an attribute name of `attr` whose value can be exactly value or can begin with `value` immediately followed by a hyphen (`-`) + BeginsWithHyphenOrExact, + /// [attr^=value] + StartsWith, + /// [attr$=value] + EndsWith, + /// [attr*=value] + /// Represents elements with an attribute name of `attr` whose value contains at least one occurrence of `value` within the string + Contains, +} diff --git a/src/selector.rs b/src/selector/mod.rs similarity index 71% rename from src/selector.rs rename to src/selector/mod.rs index 4c8eaa7..8adffb5 100644 --- a/src/selector.rs +++ b/src/selector/mod.rs @@ -7,10 +7,14 @@ use crate::lexer::Lexer; use crate::scope::Scope; use crate::utils::{ devour_whitespace, devour_whitespace_or_comment, eat_ident, eat_ident_no_interpolation, - parse_interpolation, parse_quoted_string, IsWhitespace, + parse_interpolation, IsWhitespace, }; use crate::Token; +use attribute::Attribute; + +mod attribute; + #[derive(Clone, Debug, Eq, PartialEq)] pub(crate) struct Selector(pub Vec); @@ -424,173 +428,3 @@ impl Selector { self.0.is_empty() } } - -#[derive(Clone, Debug, Eq, PartialEq)] -pub(crate) struct Attribute { - pub attr: String, - pub value: String, - pub modifier: String, - pub kind: AttributeKind, -} - -impl Attribute { - pub fn from_tokens>( - toks: &mut Peekable, - scope: &Scope, - super_selector: &Selector, - ) -> SassResult { - devour_whitespace(toks); - let attr = if let Some(t) = toks.next() { - match t.kind { - v @ 'a'..='z' | v @ 'A'..='Z' | v @ '-' | v @ '_' => { - format!("{}{}", v, eat_ident(toks, scope, super_selector)?) - } - '#' if toks.next().unwrap().kind == '{' => { - parse_interpolation(toks, scope, super_selector)?.to_string() - } - q @ '"' | q @ '\'' => { - parse_quoted_string(toks, scope, q, super_selector)?.to_string() - } - _ => return Err("Expected identifier.".into()), - } - } else { - todo!() - }; - - devour_whitespace(toks); - - let kind = if let Some(t) = toks.next() { - match t.kind { - v @ 'a'..='z' | v @ 'A'..='Z' => { - match toks.next().unwrap().kind { - ']' => {} - _ => return Err("expected \"]\".".into()), - } - return Ok(SelectorKind::Attribute(Attribute { - kind: AttributeKind::Any, - attr, - value: String::new(), - modifier: v.to_string(), - })); - } - ']' => { - return Ok(SelectorKind::Attribute(Attribute { - kind: AttributeKind::Any, - attr, - value: String::new(), - modifier: String::new(), - })); - } - '=' => AttributeKind::Equals, - '~' => AttributeKind::InList, - '|' => AttributeKind::BeginsWithHyphenOrExact, - '^' => AttributeKind::StartsWith, - '$' => AttributeKind::EndsWith, - '*' => AttributeKind::Contains, - _ => return Err("Expected \"]\".".into()), - } - } else { - todo!() - }; - - if kind != AttributeKind::Equals { - match toks.next().unwrap().kind { - '=' => {} - _ => return Err("expected \"=\".".into()), - } - } - - devour_whitespace(toks); - - let value = if let Some(t) = toks.next() { - match t.kind { - v @ 'a'..='z' | v @ 'A'..='Z' | v @ '-' | v @ '_' => { - format!("{}{}", v, eat_ident(toks, scope, super_selector)?) - } - q @ '"' | q @ '\'' => { - parse_quoted_string(toks, scope, q, super_selector)?.to_string() - } - _ => return Err("Expected identifier.".into()), - } - } else { - todo!() - }; - - devour_whitespace(toks); - - let modifier = if let Some(t) = toks.next() { - match t.kind { - ']' => { - return Ok(SelectorKind::Attribute(Attribute { - kind, - attr, - value, - modifier: String::new(), - })) - } - v @ 'a'..='z' | v @ 'A'..='Z' => { - match toks.next().unwrap().kind { - ']' => {} - _ => return Err("expected \"]\".".into()), - } - format!(" {}", v) - } - _ => 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.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.modifier) - } - AttributeKind::StartsWith => { - write!(f, "[{}^={}{}]", self.attr, self.value, self.modifier) - } - AttributeKind::EndsWith => { - write!(f, "[{}$={}{}]", self.attr, self.value, self.modifier) - } - AttributeKind::Contains => { - write!(f, "[{}*={}{}]", self.attr, self.value, self.modifier) - } - } - } -} - -#[derive(Copy, Clone, Debug, Eq, PartialEq)] -pub enum AttributeKind { - /// [attr] - /// Represents elements with an attribute name of `attr` - Any, - /// [attr=value] - /// Represents elements with an attribute name of `attr` whose value is exactly `value` - Equals, - /// [attr~=value] - /// Represents elements with an attribute name of `attr` whose value is a whitespace-separated list of words, one of which is exactly `value` - InList, - /// [attr|=value] - /// Represents elements with an attribute name of `attr` whose value can be exactly value or can begin with `value` immediately followed by a hyphen (`-`) - BeginsWithHyphenOrExact, - /// [attr^=value] - StartsWith, - /// [attr$=value] - EndsWith, - /// [attr*=value] - /// Represents elements with an attribute name of `attr` whose value contains at least one occurrence of `value` within the string - Contains, -}