use super::ast::{
AttributeSelector, Combinator, ComplexSelector, Compound, NthType, Selector, SelectorPart,
};
pub trait SelectorSubject: Sized {
fn local_name(&self) -> &str;
fn attribute(&self, name: &str) -> Option<&str>;
fn parent(&self) -> Option<Self>;
fn nth_child_index(&self) -> usize;
fn nth_of_type_index(&self) -> usize;
fn has_id(&self, id: &str) -> bool {
self.attribute("id") == Some(id)
}
fn has_class(&self, class: &str) -> bool {
self.attribute("class")
.is_some_and(|value| value.split_ascii_whitespace().any(|c| c == class))
}
}
impl Selector {
pub fn matches<S: SelectorSubject>(&self, subject: &S) -> bool {
self.selectors.iter().any(|c| complex_matches(c, subject))
}
}
fn complex_matches<S: SelectorSubject>(complex: &ComplexSelector, subject: &S) -> bool {
match_from(&complex.parts, complex.parts.len() - 1, subject)
}
fn match_from<S: SelectorSubject>(parts: &[SelectorPart], idx: usize, subject: &S) -> bool {
let part = &parts[idx];
if !compound_matches(&part.compound, subject) {
return false;
}
if idx == 0 {
return true;
}
let Some(combinator) = part.combinator else {
return false;
};
match combinator {
Combinator::Child => subject
.parent()
.is_some_and(|parent| match_from(parts, idx - 1, &parent)),
Combinator::Descendant => {
let mut ancestor = subject.parent();
while let Some(node) = ancestor {
if match_from(parts, idx - 1, &node) {
return true;
}
ancestor = node.parent();
}
false
}
}
}
fn compound_matches<S: SelectorSubject>(compound: &Compound, subject: &S) -> bool {
if let Some(name) = &compound.name
&& !name.matches(subject.local_name())
{
return false;
}
if let Some(id) = &compound.id
&& !subject.has_id(id)
{
return false;
}
if !compound.classes.iter().all(|c| subject.has_class(c)) {
return false;
}
if !compound
.attributes
.iter()
.all(|a| attribute_matches(a, subject))
{
return false;
}
for nth in &compound.nth {
let index = match nth.ty {
NthType::Child => subject.nth_child_index(),
NthType::OfType => subject.nth_of_type_index(),
};
if !nth.matches_index(index) {
return false;
}
}
!compound
.negations
.iter()
.any(|neg| compound_matches(neg, subject))
}
fn attribute_matches<S: SelectorSubject>(selector: &AttributeSelector, subject: &S) -> bool {
subject
.attribute(&selector.name)
.is_some_and(|actual| selector.matches_value(actual.as_bytes()))
}