use crate::Node;
#[derive(Debug, Clone)]
pub enum Selector<'a, const MAX_SELECTOR_NODES: usize = 0> {
Tag(&'a [u8]),
Id(&'a [u8]),
Class(&'a [u8]),
All,
#[cfg(feature = "std")]
And(
Box<Selector<'a, MAX_SELECTOR_NODES>>,
Box<Selector<'a, MAX_SELECTOR_NODES>>,
),
#[cfg(feature = "std")]
Or(
Box<Selector<'a, MAX_SELECTOR_NODES>>,
Box<Selector<'a, MAX_SELECTOR_NODES>>,
),
#[cfg(feature = "std")]
Descendant(
Box<Selector<'a, MAX_SELECTOR_NODES>>,
Box<Selector<'a, MAX_SELECTOR_NODES>>,
),
#[cfg(feature = "std")]
Parent(
Box<Selector<'a, MAX_SELECTOR_NODES>>,
Box<Selector<'a, MAX_SELECTOR_NODES>>,
),
#[cfg(feature = "std")]
Adjacent(Box<Self>, Box<Self>),
#[cfg(feature = "std")]
Sibling(Box<Self>, Box<Self>),
#[cfg(feature = "std")]
Not(Box<Self>),
#[cfg(feature = "std")]
Has(Box<Self>),
#[cfg(feature = "std")]
NthChild(i32, i32),
Attribute(&'a [u8]),
AttributeValue(&'a [u8], &'a [u8]),
AttributeValueWhitespacedContains(&'a [u8], &'a [u8]),
AttributeValueStartsWith(&'a [u8], &'a [u8]),
AttributeValueEndsWith(&'a [u8], &'a [u8]),
AttributeValueSubstring(&'a [u8], &'a [u8]),
}
impl<'a, const MAX_SELECTOR_NODES: usize> Selector<'a, MAX_SELECTOR_NODES> {
pub fn matches<'b>(&self, node: &Node<'b>) -> bool {
match self {
Self::Tag(tag) => node
.as_tag()
.is_some_and(|t| t._name.as_bytes().eq_ignore_ascii_case(tag)),
Self::Id(id) => node
.as_tag()
.is_some_and(|t| t._attributes.id == Some((*id).into())),
Self::Class(class) => node
.as_tag()
.is_some_and(|t| t._attributes.is_class_member(*class)),
#[cfg(feature = "std")]
Self::And(a, b) => a.matches(node) && b.matches(node),
#[cfg(feature = "std")]
Self::Or(a, b) => a.matches(node) || b.matches(node),
#[cfg(feature = "std")]
Self::Not(a) => node.as_tag().is_some() && !a.matches(node),
Self::All => node.as_tag().is_some(),
Self::Attribute(attribute) => node
.as_tag()
.is_some_and(|t| t._attributes.get(*attribute).is_some()),
Self::AttributeValue(attribute, value) => {
check_attribute(node, attribute, value, |attr, value| attr == value)
}
Self::AttributeValueEndsWith(attribute, value) => {
check_attribute(node, attribute, value, |attr, value| {
!value.is_empty() && attr.ends_with(value)
})
}
Self::AttributeValueStartsWith(attribute, value) => {
check_attribute(node, attribute, value, |attr, value| {
!value.is_empty() && attr.starts_with(value)
})
}
Self::AttributeValueSubstring(attribute, value) => {
check_attribute(node, attribute, value, |attr, value| {
!value.is_empty() && attr.contains(value)
})
}
Self::AttributeValueWhitespacedContains(attribute, value) => {
check_attribute(node, attribute, value, |attr, value| {
attr.split_whitespace().any(|x| x == value)
})
}
#[cfg(feature = "std")]
_ => false,
}
}
}
fn check_attribute<F>(node: &Node, attribute: &[u8], value: &[u8], callback: F) -> bool
where
F: Fn(&str, &str) -> bool,
{
let Ok(value) = core::str::from_utf8(value) else {
return false;
};
node.as_tag().is_some_and(|t| {
t._attributes
.get(attribute)
.flatten()
.and_then(|attr| attr.try_as_utf8_str())
.is_some_and(|attr| callback(attr, value))
})
}
#[cfg(feature = "std")]
#[derive(Clone)]
pub(crate) struct Context {
parent: Vec<Option<usize>>,
previous: Vec<Option<usize>>,
position: Vec<usize>,
}
#[cfg(feature = "std")]
impl Context {
pub(crate) fn new(nodes: &[Node<'_>]) -> Self {
let mut result = Self {
parent: vec![None; nodes.len()],
previous: vec![None; nodes.len()],
position: vec![0; nodes.len()],
};
for (id, node) in nodes.iter().enumerate() {
if let Some(tag) = node.as_tag() {
let mut previous = None;
let mut position = 0;
for child in tag.children().top().iter() {
let child = child.get_inner() as usize;
if let Some(node) = nodes.get(child) {
result.parent[child] = Some(id);
if node.as_tag().is_some() {
position += 1;
result.previous[child] = previous;
result.position[child] = position;
previous = Some(child);
}
}
}
}
}
let mut previous = None;
let mut position = 0;
for (id, node) in nodes.iter().enumerate() {
if result.parent[id].is_none() && node.as_tag().is_some() {
position += 1;
result.previous[id] = previous;
result.position[id] = position;
previous = Some(id);
}
}
result
}
}
#[cfg(feature = "std")]
impl<const N: usize> Selector<'_, N> {
pub(crate) fn needs_context(&self) -> bool {
match self {
Self::And(a, b) | Self::Or(a, b) => a.needs_context() || b.needs_context(),
Self::Not(a) => a.needs_context(),
Self::Parent(..)
| Self::Descendant(..)
| Self::Adjacent(..)
| Self::Sibling(..)
| Self::NthChild(..)
| Self::Has(..) => true,
_ => false,
}
}
pub(crate) fn matches_in(&self, id: usize, nodes: &[Node<'_>], ctx: &Context) -> bool {
self.matches_scoped(id, nodes, ctx, None)
}
fn matches_scoped(
&self,
id: usize,
nodes: &[Node<'_>],
ctx: &Context,
scope: Option<usize>,
) -> bool {
let Some(node) = nodes.get(id) else {
return false;
};
if node.as_tag().is_none() {
return false;
}
match self {
Self::And(a, b) => {
a.matches_scoped(id, nodes, ctx, scope) && b.matches_scoped(id, nodes, ctx, scope)
}
Self::Or(a, b) => {
a.matches_scoped(id, nodes, ctx, scope) || b.matches_scoped(id, nodes, ctx, scope)
}
Self::Not(a) => !a.matches_scoped(id, nodes, ctx, scope),
Self::Parent(a, b) => {
b.matches_scoped(id, nodes, ctx, scope)
&& ctx.parent[id]
.filter(|p| Some(*p) != scope)
.is_some_and(|p| a.matches_scoped(p, nodes, ctx, scope))
}
Self::Adjacent(a, b) => {
b.matches_scoped(id, nodes, ctx, scope)
&& ctx.previous[id].is_some_and(|p| a.matches_scoped(p, nodes, ctx, scope))
}
Self::Descendant(a, b) | Self::Sibling(a, b) => {
if !b.matches_scoped(id, nodes, ctx, scope) {
return false;
}
let links = if matches!(self, Self::Descendant(..)) {
&ctx.parent
} else {
&ctx.previous
};
let mut cursor = links[id]
.filter(|next| !matches!(self, Self::Descendant(..)) || Some(*next) != scope);
for _ in 0..nodes.len() {
let Some(p) = cursor else {
break;
};
if a.matches_scoped(p, nodes, ctx, scope) {
return true;
}
cursor = links[p].filter(|next| {
!matches!(self, Self::Descendant(..)) || Some(*next) != scope
});
}
false
}
Self::NthChild(a, b) => {
let delta = ctx.position[id] as i64 - i64::from(*b);
let a = i64::from(*a);
if a == 0 {
delta == 0
} else {
delta % a == 0 && delta / a >= 0
}
}
Self::Has(a) => {
nodes.iter().enumerate().any(|(candidate, _)| {
let mut cursor = ctx.parent[candidate];
for _ in 0..nodes.len() {
let Some(p) = cursor else {
break;
};
if p == id {
return a.matches_scoped(candidate, nodes, ctx, Some(id));
}
cursor = ctx.parent[p];
}
false
})
}
_ => self.matches(node),
}
}
}