use css_cat::{AttrOperator, Combinator, ComplexSelector, CompoundSelector, SimpleSelector};
use crate::document::Document;
use crate::node::{ElementData, Node, NodeId};
#[must_use]
pub fn matches(doc: &Document, node_id: NodeId, selector: &ComplexSelector) -> bool {
doc.get(node_id)
.and_then(Node::as_element)
.is_some_and(|_| matches_complex(doc, node_id, selector))
}
fn matches_complex(doc: &Document, node_id: NodeId, selector: &ComplexSelector) -> bool {
let tail = selector.tail();
if tail.is_empty() {
matches_compound(doc, node_id, selector.head())
} else {
let last_compound = tail.last().map_or(selector.head(), |(_, c)| c);
if matches_compound(doc, node_id, last_compound) {
walk_combinators_left(doc, node_id, selector, tail.len())
} else {
false
}
}
}
fn walk_combinators_left(
doc: &Document,
current: NodeId,
selector: &ComplexSelector,
tail_idx: usize,
) -> bool {
if tail_idx == 0 {
true
} else {
let idx = tail_idx - 1;
let combinator = selector
.tail()
.get(idx)
.map_or(Combinator::Descendant, |(c, _)| *c);
let prior_compound = if idx == 0 {
selector.head()
} else {
selector
.tail()
.get(idx - 1)
.map_or(selector.head(), |(_, c)| c)
};
find_combinator_match(doc, current, combinator, prior_compound)
.is_some_and(|next_node| walk_combinators_left(doc, next_node, selector, idx))
}
}
fn find_combinator_match(
doc: &Document,
current: NodeId,
combinator: Combinator,
prior: &CompoundSelector,
) -> Option<NodeId> {
match combinator {
Combinator::Descendant => find_ancestor(doc, current, prior),
Combinator::Child => doc
.get(current)
.and_then(Node::parent)
.filter(|p| matches_compound(doc, *p, prior)),
Combinator::AdjacentSibling => {
previous_sibling(doc, current).filter(|p| matches_compound(doc, *p, prior))
}
Combinator::GeneralSibling => find_previous_sibling_matching(doc, current, prior),
}
}
fn find_ancestor(doc: &Document, current: NodeId, prior: &CompoundSelector) -> Option<NodeId> {
doc.get(current).and_then(Node::parent).and_then(|p| {
if matches_compound(doc, p, prior) {
Some(p)
} else {
find_ancestor(doc, p, prior)
}
})
}
fn previous_sibling(doc: &Document, current: NodeId) -> Option<NodeId> {
let parent = doc.get(current).and_then(Node::parent)?;
let siblings = doc.get(parent)?.children();
siblings
.iter()
.position(|&id| id == current)
.and_then(|idx| {
if idx == 0 {
None
} else {
siblings.get(idx - 1).copied()
}
})
}
fn find_previous_sibling_matching(
doc: &Document,
current: NodeId,
prior: &CompoundSelector,
) -> Option<NodeId> {
previous_sibling(doc, current).and_then(|prev| {
if matches_compound(doc, prev, prior) {
Some(prev)
} else {
find_previous_sibling_matching(doc, prev, prior)
}
})
}
fn matches_compound(doc: &Document, node_id: NodeId, compound: &CompoundSelector) -> bool {
doc.get(node_id)
.and_then(Node::as_element)
.is_some_and(|el| {
compound
.parts()
.iter()
.all(|simple| matches_simple(doc, node_id, el, simple))
})
}
fn matches_simple(
doc: &Document,
node_id: NodeId,
el: &ElementData,
simple: &SimpleSelector,
) -> bool {
match simple {
SimpleSelector::Universal => true,
SimpleSelector::Type(name) => el.name().eq_ignore_ascii_case(name),
SimpleSelector::Class(name) => el.has_class(name),
SimpleSelector::Id(name) => el.id().is_some_and(|v| v == name),
SimpleSelector::Attribute { name, op, value } => {
match_attribute(el, name, *op, value.as_deref())
}
SimpleSelector::PseudoClass { name, argument } => {
match_pseudo_class(doc, node_id, name, argument.as_deref())
}
}
}
fn match_attribute(
el: &ElementData,
name: &str,
op: Option<AttrOperator>,
value: Option<&str>,
) -> bool {
el.attribute(name).is_some_and(|present| match (op, value) {
(None, _) => true,
(Some(AttrOperator::Equals), Some(v)) => present == v,
(Some(AttrOperator::Includes), Some(v)) => present.split_ascii_whitespace().any(|w| w == v),
(Some(AttrOperator::DashMatch), Some(v)) => {
present == v || present.starts_with(&format!("{v}-"))
}
(Some(AttrOperator::Prefix), Some(v)) => present.starts_with(v),
(Some(AttrOperator::Suffix), Some(v)) => present.ends_with(v),
(Some(AttrOperator::Substring), Some(v)) => present.contains(v),
(Some(_), None) => false,
})
}
fn match_pseudo_class(
doc: &Document,
node_id: NodeId,
name: &str,
_argument: Option<&str>,
) -> bool {
match name.to_ascii_lowercase().as_str() {
"first-child" => is_first_element_child(doc, node_id),
"last-child" => is_last_element_child(doc, node_id),
"only-child" => is_first_element_child(doc, node_id) && is_last_element_child(doc, node_id),
"root" => doc
.get(node_id)
.and_then(Node::parent)
.and_then(|p| doc.get(p))
.is_some_and(|n| matches!(n, Node::Document(_))),
_other => false,
}
}
fn is_first_element_child(doc: &Document, node_id: NodeId) -> bool {
sibling_position(doc, node_id, true)
}
fn is_last_element_child(doc: &Document, node_id: NodeId) -> bool {
sibling_position(doc, node_id, false)
}
fn sibling_position(doc: &Document, node_id: NodeId, first: bool) -> bool {
doc.get(node_id)
.and_then(Node::parent)
.and_then(|p| doc.get(p))
.is_some_and(|parent_node| {
let element_siblings: Vec<NodeId> = parent_node
.children()
.iter()
.copied()
.filter(|id| doc.get(*id).is_some_and(|n| matches!(n, Node::Element(_))))
.collect();
if first {
element_siblings.first().copied() == Some(node_id)
} else {
element_siblings.last().copied() == Some(node_id)
}
})
}