use crate::tdom::node::{NodeArena, NodeId, NodeType};
use super::selector::{
Combinator, CompoundSelector, CssSelector, DynamicState, NthExpr, PseudoClass, Selector,
};
impl PseudoClass {
pub fn matches(&self, arena: &NodeArena, node_id: NodeId, state: &DynamicState) -> bool {
match self {
PseudoClass::Hover => state
.hovered
.is_some_and(|h| is_hovered_or_ancestor(arena, node_id, h)),
PseudoClass::Focus => state.focused == Some(node_id),
}
}
pub fn affected_candidates(&self, arena: &NodeArena, target: Option<NodeId>) -> Vec<NodeId> {
match self {
PseudoClass::Hover => ancestor_chain(arena, target),
PseudoClass::Focus => target.into_iter().collect(),
}
}
pub fn state_with_target(&self, target: Option<NodeId>) -> DynamicState {
match self {
PseudoClass::Hover => DynamicState {
hovered: target,
..Default::default()
},
PseudoClass::Focus => DynamicState {
focused: target,
..Default::default()
},
}
}
}
fn ancestor_chain(arena: &NodeArena, target: Option<NodeId>) -> Vec<NodeId> {
let mut out = Vec::new();
let mut cur = target;
while let Some(id) = cur {
out.push(id);
cur = arena.get(id).and_then(|n| n.parent);
}
out
}
fn matches_css(arena: &NodeArena, node_id: NodeId, sel: &Selector, state: &DynamicState) -> bool {
let Some(node) = arena.get(node_id) else {
return false;
};
match sel {
Selector::Universal => !matches!(
node.node_type,
NodeType::Document
| NodeType::DocumentType
| NodeType::DocumentFragment
| NodeType::Text
| NodeType::Comment
),
Selector::Tag(nt) => node.node_type == *nt,
Selector::Class(name) => node.has_class(name),
Selector::Id(id) => node
.get_id()
.map(|s| s.as_str() == id.as_str())
.unwrap_or(false),
Selector::NthChild(expr) => nth_child_matches(arena, node_id, *expr),
Selector::NthLastChild(expr) => nth_last_child_matches(arena, node_id, *expr),
Selector::NthOfType(expr) => nth_of_type_matches(arena, node_id, *expr, false),
Selector::NthLastOfType(expr) => nth_of_type_matches(arena, node_id, *expr, true),
Selector::Root => is_root_element(arena, node_id),
Selector::Empty => is_empty_element(arena, node_id),
Selector::Not(compounds) => !compounds
.iter()
.any(|c| matches_compound(arena, node_id, c, state)),
Selector::Checked => checked_matches(arena, node_id),
Selector::Enabled => enabled_matches(arena, node_id),
Selector::Disabled => disabled_matches(arena, node_id),
Selector::Lang(tag) => lang_matches(arena, node_id, tag),
Selector::PseudoClass(p) => p.matches(arena, node_id, state),
Selector::Attribute(attr) => {
let node2 = arena.get(node_id);
let node2 = match node2 {
Some(n) => n,
None => return false,
};
attr.matches_lookup(&|name| node2.attributes.get(name).cloned())
}
}
}
fn matches_compound(
arena: &NodeArena,
node_id: NodeId,
compound: &CompoundSelector,
state: &DynamicState,
) -> bool {
compound
.iter()
.all(|s| matches_css(arena, node_id, s, state))
}
fn nth_child_matches(arena: &NodeArena, node_id: NodeId, expr: NthExpr) -> bool {
let Some(parent_id) = arena[node_id].parent else {
return false;
};
let parent = &arena[parent_id];
let mut index = 1usize;
for &cid in &parent.children {
if cid == node_id {
return expr.matches(index);
}
index = index.saturating_add(1);
}
false
}
fn nth_last_child_matches(arena: &NodeArena, node_id: NodeId, expr: NthExpr) -> bool {
let Some(parent_id) = arena.get(node_id).and_then(|n| n.parent) else {
return false;
};
let parent = &arena[parent_id];
let Some(pos) = parent.children.iter().position(|&c| c == node_id) else {
return false;
};
expr.matches(parent.children.len() - pos)
}
fn nth_of_type_matches(arena: &NodeArena, node_id: NodeId, expr: NthExpr, from_end: bool) -> bool {
let Some(node) = arena.get(node_id) else {
return false;
};
let Some(parent_id) = node.parent else {
return false;
};
let node_type = node.node_type;
let parent = &arena[parent_id];
let typed: Vec<NodeId> = parent
.children
.iter()
.copied()
.filter(|&c| arena.get(c).is_some_and(|n| n.node_type == node_type))
.collect();
let Some(pos) = typed.iter().position(|&c| c == node_id) else {
return false;
};
let index = if from_end { typed.len() - pos } else { pos + 1 };
expr.matches(index)
}
fn is_root_element(arena: &NodeArena, node_id: NodeId) -> bool {
arena
.get(node_id)
.and_then(|n| n.parent)
.and_then(|pid| arena.get(pid))
.is_some_and(|p| p.node_type == NodeType::Document)
}
fn is_empty_element(arena: &NodeArena, node_id: NodeId) -> bool {
let Some(node) = arena.get(node_id) else {
return false;
};
node.children.iter().all(|&c| match arena.get(c) {
Some(child) => match child.node_type {
NodeType::Comment => true,
NodeType::Text => child.data.as_deref().is_none_or(str::is_empty),
_ => false,
},
None => false,
})
}
fn checked_matches(arena: &NodeArena, node_id: NodeId) -> bool {
let Some(node) = arena.get(node_id) else {
return false;
};
match node.node_type {
NodeType::Input => {
let toggle = matches!(
node.attributes.get("type").map(String::as_str),
Some("checkbox" | "radio")
);
toggle && node.attributes.contains_key("checked")
}
NodeType::HtmlOption => node.attributes.contains_key("selected"),
_ => false,
}
}
fn disabled_matches(arena: &NodeArena, node_id: NodeId) -> bool {
let Some(node) = arena.get(node_id) else {
return false;
};
if is_form_control(node.node_type) && node.attributes.contains_key("disabled") {
return true;
}
let mut cur = node.parent;
while let Some(id) = cur {
let Some(n) = arena.get(id) else {
break;
};
if n.node_type == NodeType::Fieldset && n.attributes.contains_key("disabled") {
return true;
}
cur = n.parent;
}
false
}
fn enabled_matches(arena: &NodeArena, node_id: NodeId) -> bool {
let Some(node) = arena.get(node_id) else {
return false;
};
is_form_control(node.node_type) && !disabled_matches(arena, node_id)
}
fn lang_matches(arena: &NodeArena, node_id: NodeId, tag: &str) -> bool {
let mut cur = Some(node_id);
while let Some(id) = cur
&& let Some(node) = arena.get(id)
{
if let Some(declared) = node.attributes.get("lang") {
let declared = declared.trim().to_ascii_lowercase();
let primary = declared.split(['-', '_']).next().unwrap_or_default();
return primary == tag;
}
cur = node.parent;
}
false
}
fn is_form_control(node_type: NodeType) -> bool {
matches!(
node_type,
NodeType::Input
| NodeType::Textarea
| NodeType::Select
| NodeType::Button
| NodeType::Optgroup
| NodeType::HtmlOption
| NodeType::Fieldset
)
}
#[doc(hidden)]
pub fn selector_chain_matches(arena: &NodeArena, node_id: NodeId, selector: &CssSelector) -> bool {
selector_chain_matches_with_hover(arena, node_id, selector, None)
}
pub fn selector_chain_matches_with_state(
arena: &NodeArena,
node_id: NodeId,
selector: &CssSelector,
state: &DynamicState,
) -> bool {
let n = selector.compounds.len();
if n == 0 {
return false;
}
if !matches_compound(arena, node_id, &selector.compounds[n - 1], state) {
return false;
}
if n == 1 {
return true;
}
match_combinators(
arena,
node_id,
&selector.compounds,
&selector.combinators,
n - 1,
state,
)
}
pub fn selector_chain_matches_with_hover(
arena: &NodeArena,
node_id: NodeId,
selector: &CssSelector,
hovered: Option<NodeId>,
) -> bool {
selector_chain_matches_with_state(
arena,
node_id,
selector,
&DynamicState {
hovered,
..Default::default()
},
)
}
fn is_hovered_or_ancestor(arena: &NodeArena, node_id: NodeId, hovered: NodeId) -> bool {
let mut current = Some(hovered);
while let Some(id) = current {
if id == node_id {
return true;
}
current = arena.get(id).and_then(|n| n.parent);
}
false
}
fn match_combinators(
arena: &NodeArena,
node_id: NodeId,
compounds: &[CompoundSelector],
combinators: &[Combinator],
idx: usize,
state: &DynamicState,
) -> bool {
if idx == 0 {
return true;
}
let combinator = combinators[idx - 1];
let target = &compounds[idx - 1];
match combinator {
Combinator::Descendant => {
let mut cur = arena[node_id].parent;
while let Some(pid) = cur {
let ancestor = &arena[pid];
if matches_compound(arena, pid, target, state)
&& match_combinators(arena, pid, compounds, combinators, idx - 1, state)
{
return true;
}
cur = ancestor.parent;
}
false
}
Combinator::Child => match arena[node_id].parent {
Some(pid) if matches_compound(arena, pid, target, state) => {
match_combinators(arena, pid, compounds, combinators, idx - 1, state)
}
_ => false,
},
Combinator::NextSibling => match prev_sibling_id(arena, node_id) {
Some(sid) if matches_compound(arena, sid, target, state) => {
match_combinators(arena, sid, compounds, combinators, idx - 1, state)
}
_ => false,
},
Combinator::SubsequentSibling => {
let mut cur = prev_sibling_id(arena, node_id);
while let Some(sid) = cur {
if matches_compound(arena, sid, target, state)
&& match_combinators(arena, sid, compounds, combinators, idx - 1, state)
{
return true;
}
cur = prev_sibling_id(arena, sid);
}
false
}
}
}
fn prev_sibling_id(arena: &NodeArena, node_id: NodeId) -> Option<NodeId> {
let node = &arena[node_id];
let parent_id = node.parent?;
let parent = &arena[parent_id];
let mut prev = None;
for &cid in &parent.children {
if cid == node_id {
return prev;
}
prev = Some(cid);
}
None
}