use crate::{
css_parser::{Token, TokenKind, Tokenizer, resolve_escapes},
css_selectors::{ast::*, error::SelectorParseError, nth::parse_nth},
};
pub fn parse_selector_list(input: &str) -> Result<SelectorList, SelectorParseError> {
let mut parser = SelectorParser::new(input);
parser.parse_selector_list()
}
pub fn parse_selector_list_forgiving(input: &str) -> SelectorList {
let mut parser = SelectorParser::new(input);
parser.parse_selector_list_forgiving()
}
struct SelectorParser<'a> {
tokens: Vec<Token<'a>>,
pos: usize,
}
impl<'a> SelectorParser<'a> {
fn new(input: &'a str) -> Self {
let tokens: Vec<Token<'a>> = Tokenizer::new(input).collect();
Self { tokens, pos: 0 }
}
fn from_tokens(tokens: Vec<Token<'a>>) -> Self {
Self { tokens, pos: 0 }
}
fn current_kind(&self) -> TokenKind<'a> {
if self.pos < self.tokens.len() {
self.tokens[self.pos].kind.clone()
} else {
TokenKind::Eof
}
}
fn current_token(&self) -> Option<&Token<'a>> {
self.tokens.get(self.pos)
}
fn advance(&mut self) {
if self.pos < self.tokens.len() {
self.pos += 1;
}
}
fn skip_whitespace(&mut self) {
while matches!(self.current_kind(), TokenKind::Whitespace) {
self.advance();
}
}
fn is_eof(&self) -> bool {
self.pos >= self.tokens.len()
}
fn parse_selector_list(&mut self) -> Result<SelectorList, SelectorParseError> {
let mut selectors = Vec::new();
self.skip_whitespace();
if self.is_eof() {
return Err(SelectorParseError::EmptySelector);
}
selectors.push(self.parse_complex_selector()?);
loop {
self.skip_whitespace();
if self.is_eof() {
break;
}
if matches!(self.current_kind(), TokenKind::Comma) {
self.advance();
self.skip_whitespace();
selectors.push(self.parse_complex_selector()?);
} else {
break;
}
}
Ok(selectors)
}
fn parse_selector_list_forgiving(&mut self) -> SelectorList {
let mut selectors = Vec::new();
self.skip_whitespace();
if self.is_eof() {
return selectors;
}
if let Ok(sel) = self.try_parse_complex_selector() {
selectors.push(sel);
}
loop {
self.skip_whitespace();
if self.is_eof() {
break;
}
if matches!(self.current_kind(), TokenKind::Comma) {
self.advance();
self.skip_whitespace();
if let Ok(sel) = self.try_parse_complex_selector() {
selectors.push(sel);
} else {
while !self.is_eof() && !matches!(self.current_kind(), TokenKind::Comma) {
self.advance();
}
}
} else {
break;
}
}
selectors
}
fn try_parse_complex_selector(&mut self) -> Result<Selector, SelectorParseError> {
let saved = self.pos;
match self.parse_complex_selector() {
Ok(s) => Ok(s),
Err(e) => {
self.pos = saved;
Err(e)
}
}
}
fn parse_complex_selector(&mut self) -> Result<Selector, SelectorParseError> {
let mut components = Vec::new();
let compound = self.parse_compound_selector()?;
components.extend(compound);
loop {
let had_whitespace = matches!(self.current_kind(), TokenKind::Whitespace);
self.skip_whitespace();
if self.is_eof()
|| matches!(
self.current_kind(),
TokenKind::Comma | TokenKind::CloseParen | TokenKind::CloseCurly
)
{
break;
}
let combinator = match self.current_kind() {
TokenKind::Delim('>') => {
self.advance();
self.skip_whitespace();
Combinator::Child
}
TokenKind::Delim('+') => {
self.advance();
self.skip_whitespace();
Combinator::NextSibling
}
TokenKind::Delim('~') => {
self.advance();
self.skip_whitespace();
Combinator::SubsequentSibling
}
_ if had_whitespace => Combinator::Descendant,
_ => break,
};
let compound = self.parse_compound_selector()?;
components.push(Component::Combinator(combinator));
components.extend(compound);
}
components.reverse();
let specificity = compute_specificity_from_components(&components);
Ok(Selector::new(components, specificity))
}
fn parse_compound_selector(&mut self) -> Result<Vec<Component>, SelectorParseError> {
let mut components = Vec::new();
loop {
match self.current_kind() {
TokenKind::Ident(name) => {
let name = resolve_escapes(name).to_string();
components.push(Component::Simple(SimpleSelector::Type(name)));
self.advance();
}
TokenKind::Delim('*') => {
components.push(Component::Simple(SimpleSelector::Universal));
self.advance();
}
TokenKind::Delim('.') => {
self.advance();
match self.current_kind() {
TokenKind::Ident(name) => {
let name = resolve_escapes(name).to_string();
components.push(Component::Simple(SimpleSelector::Class(name)));
self.advance();
}
_ => {
return Err(SelectorParseError::UnexpectedToken {
loc: self.current_token().map(|t| t.loc).unwrap_or_default(),
message: "expected class name after '.'".into(),
});
}
}
}
TokenKind::Hash { value, .. } => {
let value = resolve_escapes(value).to_string();
components.push(Component::Simple(SimpleSelector::Id(value)));
self.advance();
}
TokenKind::OpenSquare => {
components.push(Component::Simple(self.parse_attribute_selector()?));
}
TokenKind::Colon => {
self.advance();
if matches!(self.current_kind(), TokenKind::Colon) {
self.advance();
components.push(Component::Simple(self.parse_pseudo_element()?));
} else {
components.push(Component::Simple(self.parse_pseudo_class()?));
}
}
TokenKind::Delim('&') => {
components.push(Component::Simple(SimpleSelector::Nesting));
self.advance();
}
_ => break,
}
}
if components.is_empty() {
return Err(SelectorParseError::EmptySelector);
}
Ok(components)
}
fn parse_attribute_selector(&mut self) -> Result<SimpleSelector, SelectorParseError> {
self.advance(); self.skip_whitespace();
let name = match self.current_kind() {
TokenKind::Ident(n) => {
let n = resolve_escapes(n).to_string();
self.advance();
n
}
_ => {
return Err(SelectorParseError::UnexpectedToken {
loc: self.current_token().map(|t| t.loc).unwrap_or_default(),
message: "expected attribute name".into(),
});
}
};
self.skip_whitespace();
if matches!(self.current_kind(), TokenKind::CloseSquare) {
self.advance();
return Ok(SimpleSelector::Attribute {
name,
operator: None,
value: None,
case_sensitivity: CaseSensitivity::Default,
});
}
let operator = self.parse_attribute_operator()?;
self.skip_whitespace();
let value = match self.current_kind() {
TokenKind::String(s) => {
let s = resolve_escapes(s).to_string();
self.advance();
s
}
TokenKind::Ident(s) => {
let s = resolve_escapes(s).to_string();
self.advance();
s
}
_ => {
return Err(SelectorParseError::UnexpectedToken {
loc: self.current_token().map(|t| t.loc).unwrap_or_default(),
message: "expected attribute value".into(),
});
}
};
self.skip_whitespace();
let case_sensitivity = match self.current_kind() {
TokenKind::Ident(flag) if flag.eq_ignore_ascii_case("i") => {
self.advance();
self.skip_whitespace();
CaseSensitivity::CaseInsensitive
}
TokenKind::Ident(flag) if flag.eq_ignore_ascii_case("s") => {
self.advance();
self.skip_whitespace();
CaseSensitivity::CaseSensitive
}
_ => CaseSensitivity::Default,
};
if matches!(self.current_kind(), TokenKind::CloseSquare) {
self.advance();
}
Ok(SimpleSelector::Attribute {
name,
operator: Some(operator),
value: Some(value),
case_sensitivity,
})
}
fn parse_attribute_operator(&mut self) -> Result<AttributeOperator, SelectorParseError> {
match self.current_kind() {
TokenKind::Delim('=') => {
self.advance();
Ok(AttributeOperator::Exact)
}
TokenKind::Delim('~') => {
self.advance();
if matches!(self.current_kind(), TokenKind::Delim('=')) {
self.advance();
}
Ok(AttributeOperator::Includes)
}
TokenKind::Delim('|') => {
self.advance();
if matches!(self.current_kind(), TokenKind::Delim('=')) {
self.advance();
}
Ok(AttributeOperator::DashMatch)
}
TokenKind::Delim('^') => {
self.advance();
if matches!(self.current_kind(), TokenKind::Delim('=')) {
self.advance();
}
Ok(AttributeOperator::Prefix)
}
TokenKind::Delim('$') => {
self.advance();
if matches!(self.current_kind(), TokenKind::Delim('=')) {
self.advance();
}
Ok(AttributeOperator::Suffix)
}
TokenKind::Delim('*') => {
self.advance();
if matches!(self.current_kind(), TokenKind::Delim('=')) {
self.advance();
}
Ok(AttributeOperator::Substring)
}
_ => Err(SelectorParseError::UnexpectedToken {
loc: self.current_token().map(|t| t.loc).unwrap_or_default(),
message: "expected attribute operator".into(),
}),
}
}
fn parse_pseudo_class(&mut self) -> Result<SimpleSelector, SelectorParseError> {
match self.current_kind() {
TokenKind::Ident(name) => {
let name_lower = name.to_ascii_lowercase();
self.advance();
let pc = match name_lower.as_str() {
"hover" => PseudoClass::Hover,
"active" => PseudoClass::Active,
"focus" => PseudoClass::Focus,
"focus-within" => PseudoClass::FocusWithin,
"focus-visible" => PseudoClass::FocusVisible,
"link" => PseudoClass::Link,
"visited" => PseudoClass::Visited,
"any-link" => PseudoClass::AnyLink,
"target" => PseudoClass::Target,
"enabled" => PseudoClass::Enabled,
"disabled" => PseudoClass::Disabled,
"checked" => PseudoClass::Checked,
"default" => PseudoClass::Default,
"indeterminate" => PseudoClass::Indeterminate,
"required" => PseudoClass::Required,
"optional" => PseudoClass::Optional,
"valid" => PseudoClass::Valid,
"invalid" => PseudoClass::Invalid,
"in-range" => PseudoClass::InRange,
"out-of-range" => PseudoClass::OutOfRange,
"read-write" => PseudoClass::ReadWrite,
"read-only" => PseudoClass::ReadOnly,
"placeholder-shown" => PseudoClass::PlaceholderShown,
"root" => PseudoClass::Root,
"empty" => PseudoClass::Empty,
"first-child" => PseudoClass::FirstChild,
"last-child" => PseudoClass::LastChild,
"only-child" => PseudoClass::OnlyChild,
"first-of-type" => PseudoClass::FirstOfType,
"last-of-type" => PseudoClass::LastOfType,
"only-of-type" => PseudoClass::OnlyOfType,
_ => {
return Err(SelectorParseError::UnsupportedPseudoClass(name_lower));
}
};
Ok(SimpleSelector::PseudoClass(pc))
}
TokenKind::Function(name) => {
let name_lower = name.to_ascii_lowercase();
self.advance();
let result = match name_lower.as_str() {
"nth-child" => self.parse_nth_function(false, false),
"nth-last-child" => self.parse_nth_function(true, false),
"nth-of-type" => self.parse_nth_function(false, true),
"nth-last-of-type" => self.parse_nth_function(true, true),
"not" => self.parse_functional_pseudo(PseudoClass::Not),
"is" => self.parse_functional_pseudo_forgiving(PseudoClass::Is),
"where" => self.parse_functional_pseudo_forgiving(PseudoClass::Where),
"has" => self.parse_has_pseudo(),
"lang" => self.parse_lang_pseudo(),
_ => Err(SelectorParseError::UnsupportedPseudoClass(name_lower)),
};
if matches!(self.current_kind(), TokenKind::CloseParen) {
self.advance();
}
Ok(SimpleSelector::PseudoClass(result?))
}
_ => Err(SelectorParseError::UnexpectedToken {
loc: self.current_token().map(|t| t.loc).unwrap_or_default(),
message: "expected pseudo-class name".into(),
}),
}
}
fn parse_nth_function(
&mut self,
from_end: bool,
of_type: bool,
) -> Result<PseudoClass, SelectorParseError> {
let mut nth_tokens = Vec::new();
loop {
match self.current_kind() {
TokenKind::CloseParen | TokenKind::Eof => break,
TokenKind::Ident(name) if name.eq_ignore_ascii_case("of") && !of_type => {
self.advance();
break;
}
_ => {
if let Some(t) = self.current_token() {
nth_tokens.push(t.clone());
}
self.advance();
}
}
}
let nth = parse_nth(&nth_tokens)?;
if of_type {
if from_end {
Ok(PseudoClass::NthLastOfType(nth))
} else {
Ok(PseudoClass::NthOfType(nth))
}
} else {
self.skip_whitespace();
let selector_list =
(!matches!(self.current_kind(), TokenKind::CloseParen | TokenKind::Eof)).then(
|| {
let mut inner_tokens = Vec::new();
let mut depth = 0;
loop {
match self.current_kind() {
TokenKind::CloseParen if depth == 0 => break,
TokenKind::Eof => break,
TokenKind::OpenParen => {
depth += 1;
if let Some(t) = self.current_token() {
inner_tokens.push(t.clone());
}
self.advance();
}
TokenKind::CloseParen => {
depth -= 1;
if let Some(t) = self.current_token() {
inner_tokens.push(t.clone());
}
self.advance();
}
_ => {
if let Some(t) = self.current_token() {
inner_tokens.push(t.clone());
}
self.advance();
}
}
}
let mut inner_parser = SelectorParser::from_tokens(inner_tokens);
inner_parser.parse_selector_list().unwrap_or_default()
},
);
if from_end {
Ok(PseudoClass::NthLastChild(nth, selector_list))
} else {
Ok(PseudoClass::NthChild(nth, selector_list))
}
}
}
fn parse_functional_pseudo(
&mut self,
constructor: impl FnOnce(SelectorList) -> PseudoClass,
) -> Result<PseudoClass, SelectorParseError> {
let inner_tokens = self.collect_until_close_paren();
let mut inner_parser = SelectorParser::from_tokens(inner_tokens);
let list = inner_parser.parse_selector_list()?;
Ok(constructor(list))
}
fn parse_functional_pseudo_forgiving(
&mut self,
constructor: impl FnOnce(SelectorList) -> PseudoClass,
) -> Result<PseudoClass, SelectorParseError> {
let inner_tokens = self.collect_until_close_paren();
let mut inner_parser = SelectorParser::from_tokens(inner_tokens);
let list = inner_parser.parse_selector_list_forgiving();
Ok(constructor(list))
}
fn parse_has_pseudo(&mut self) -> Result<PseudoClass, SelectorParseError> {
let inner_tokens = self.collect_until_close_paren();
let mut inner_parser = SelectorParser::from_tokens(inner_tokens);
let list = inner_parser.parse_selector_list()?;
let relatives = list
.into_iter()
.map(|s| RelativeSelector {
combinator: None,
selector: s,
})
.collect();
Ok(PseudoClass::Has(relatives))
}
fn parse_lang_pseudo(&mut self) -> Result<PseudoClass, SelectorParseError> {
let mut langs = Vec::new();
loop {
self.skip_whitespace();
match self.current_kind() {
TokenKind::Ident(name) => {
langs.push(name.to_string());
self.advance();
}
TokenKind::String(s) => {
langs.push(s.to_string());
self.advance();
}
TokenKind::Comma => {
self.advance();
}
_ => break,
}
}
Ok(PseudoClass::Lang(langs))
}
fn parse_pseudo_element(&mut self) -> Result<SimpleSelector, SelectorParseError> {
match self.current_kind() {
TokenKind::Ident(name) => {
let name_lower = name.to_ascii_lowercase();
self.advance();
let pe = match name_lower.as_str() {
"before" => PseudoElement::Before,
"after" => PseudoElement::After,
"first-line" => PseudoElement::FirstLine,
"first-letter" => PseudoElement::FirstLetter,
"placeholder" => PseudoElement::Placeholder,
"selection" => PseudoElement::Selection,
_ => PseudoElement::Custom(name_lower),
};
Ok(SimpleSelector::PseudoElement(pe))
}
_ => Err(SelectorParseError::UnexpectedToken {
loc: self.current_token().map(|t| t.loc).unwrap_or_default(),
message: "expected pseudo-element name".into(),
}),
}
}
fn collect_until_close_paren(&mut self) -> Vec<Token<'a>> {
let mut tokens = Vec::new();
let mut depth = 0;
loop {
match self.current_kind() {
TokenKind::CloseParen if depth == 0 => break,
TokenKind::Eof => break,
TokenKind::OpenParen => {
depth += 1;
if let Some(t) = self.current_token() {
tokens.push(t.clone());
}
self.advance();
}
TokenKind::CloseParen => {
depth -= 1;
if let Some(t) = self.current_token() {
tokens.push(t.clone());
}
self.advance();
}
_ => {
if let Some(t) = self.current_token() {
tokens.push(t.clone());
}
self.advance();
}
}
}
tokens
}
}
fn compute_specificity_from_components(components: &[Component]) -> Specificity {
let mut spec = Specificity::default();
for c in components {
if let Component::Simple(s) = c {
spec += crate::css_selectors::specificity::simple_specificity_pub(s);
}
}
spec
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_type_selector() {
let list = parse_selector_list("div").unwrap();
assert_eq!(list.len(), 1);
assert_eq!(
list[0].components(),
&[Component::Simple(SimpleSelector::Type("div".into()))]
);
}
#[test]
fn parse_class_selector() {
let list = parse_selector_list(".foo").unwrap();
assert_eq!(list.len(), 1);
assert_eq!(
list[0].components(),
&[Component::Simple(SimpleSelector::Class("foo".into()))]
);
}
#[test]
fn parse_id_selector() {
let list = parse_selector_list("#bar").unwrap();
assert_eq!(list.len(), 1);
assert_eq!(
list[0].components(),
&[Component::Simple(SimpleSelector::Id("bar".into()))]
);
}
#[test]
fn parse_compound() {
let list = parse_selector_list("div.foo#bar").unwrap();
let comps = list[0].components();
assert_eq!(comps.len(), 3);
assert!(matches!(&comps[0], Component::Simple(SimpleSelector::Id(s)) if s == "bar"));
assert!(matches!(&comps[1], Component::Simple(SimpleSelector::Class(s)) if s == "foo"));
assert!(matches!(&comps[2], Component::Simple(SimpleSelector::Type(s)) if s == "div"));
}
#[test]
fn parse_descendant() {
let list = parse_selector_list("div span").unwrap();
let comps = list[0].components();
assert_eq!(comps.len(), 3);
assert!(matches!(&comps[0], Component::Simple(SimpleSelector::Type(s)) if s == "span"));
assert_eq!(comps[1], Component::Combinator(Combinator::Descendant));
assert!(matches!(&comps[2], Component::Simple(SimpleSelector::Type(s)) if s == "div"));
}
#[test]
fn parse_child_combinator() {
let list = parse_selector_list("div > span").unwrap();
let comps = list[0].components();
assert_eq!(comps.len(), 3);
assert!(matches!(&comps[0], Component::Simple(SimpleSelector::Type(s)) if s == "span"));
assert_eq!(comps[1], Component::Combinator(Combinator::Child));
}
#[test]
fn parse_selector_list_comma() {
let list = parse_selector_list("h1, h2, h3").unwrap();
assert_eq!(list.len(), 3);
}
#[test]
fn parse_attribute_exact() {
let list = parse_selector_list("[type=\"text\"]").unwrap();
assert!(matches!(
&list[0].components()[0],
Component::Simple(SimpleSelector::Attribute {
name,
operator: Some(AttributeOperator::Exact),
value: Some(v),
..
}) if name == "type" && v == "text"
));
}
#[test]
fn parse_pseudo_class_hover() {
let list = parse_selector_list("a:hover").unwrap();
let comps = list[0].components();
assert!(matches!(
&comps[0],
Component::Simple(SimpleSelector::PseudoClass(PseudoClass::Hover))
));
}
#[test]
fn parse_nth_child() {
let list = parse_selector_list(":nth-child(2n+1)").unwrap();
assert!(matches!(
&list[0].components()[0],
Component::Simple(SimpleSelector::PseudoClass(PseudoClass::NthChild(
NthExpr { a: 2, b: 1 },
None
)))
));
}
#[test]
fn specificity_computed() {
let list = parse_selector_list("#main .content > p:first-child").unwrap();
let spec = list[0].specificity();
assert_eq!(spec, Specificity::new(1, 2, 1));
}
#[test]
fn parse_empty_selector_is_error() {
assert!(parse_selector_list("").is_err());
}
}