use alloc::borrow::Cow;
use alloc::string::String;
use alloc::vec::Vec;
use core::cell::{Cell, RefCell};
use core::fmt;
use core::str::FromStr;
use crate::node::NodeId;
use crate::tree::{Dom, NodeRef};
const MAX_DEPTH: u32 = 32;
#[derive(Clone)]
pub struct Selector {
list: Vec<Complex>,
source: String,
}
impl Selector {
pub fn parse(selector: &str) -> Result<Selector, SelectorError> {
let mut p = Parser {
src: selector,
pos: 0,
depth: 0,
in_has: false,
slots: SHARED_SLOTS,
};
let list = p.list(false)?;
match p.peek() {
None => Ok(Selector {
list,
source: selector.into(),
}),
Some(_) => p.unexpected(),
}
}
pub fn matches(&self, node: NodeRef<'_>) -> bool {
self.matches_in(node, None, &Memo::default())
}
fn matches_in<'a>(&self, node: NodeRef<'a>, scope: Option<NodeId>, memo: &Memo<'a>) -> bool {
let cx = Ctx {
scope,
anchor: None,
memo,
};
any_matches(&self.list, node, cx)
}
}
impl FromStr for Selector {
type Err = SelectorError;
fn from_str(s: &str) -> Result<Selector, SelectorError> {
Selector::parse(s)
}
}
impl fmt::Debug for Selector {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_tuple("Selector").field(&self.source).finish()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SelectorError {
position: usize,
message: String,
}
impl SelectorError {
pub fn position(&self) -> usize {
self.position
}
pub fn message(&self) -> &str {
&self.message
}
}
impl fmt::Display for SelectorError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} at byte {}", self.message, self.position)
}
}
#[cfg(feature = "std")]
impl std::error::Error for SelectorError {}
pub trait AsSelector {
fn as_selector(&self) -> Option<Cow<'_, Selector>>;
}
impl AsSelector for str {
fn as_selector(&self) -> Option<Cow<'_, Selector>> {
Selector::parse(self).ok().map(Cow::Owned)
}
}
impl AsSelector for String {
fn as_selector(&self) -> Option<Cow<'_, Selector>> {
self.as_str().as_selector()
}
}
impl AsSelector for Selector {
fn as_selector(&self) -> Option<Cow<'_, Selector>> {
Some(Cow::Borrowed(self))
}
}
impl Dom {
pub fn select<'a, S: AsSelector + ?Sized>(
&'a self,
selector: &S,
) -> impl Iterator<Item = NodeRef<'a>> {
let sel = selector.as_selector();
let memo = Memo {
dense: self.len(),
..Memo::default()
};
self.nodes()
.filter(move |n| sel.as_ref().is_some_and(|s| s.matches_in(*n, None, &memo)))
}
pub fn select_first<S: AsSelector + ?Sized>(&self, selector: &S) -> Option<NodeRef<'_>> {
self.select(selector).next()
}
}
impl<'a> NodeRef<'a> {
pub fn select<'s, S: AsSelector + ?Sized>(
&self,
selector: &'s S,
) -> impl Iterator<Item = NodeRef<'a>> + use<'a, 's, S> {
let sel = selector.as_selector();
let (scope, memo) = (Some(self.id()), Memo::default());
self.descendants()
.filter(move |n| sel.as_ref().is_some_and(|s| s.matches_in(*n, scope, &memo)))
}
pub fn select_first<S: AsSelector + ?Sized>(&self, selector: &S) -> Option<NodeRef<'a>> {
self.select(selector).next()
}
pub fn matches<S: AsSelector + ?Sized>(&self, selector: &S) -> bool {
selector
.as_selector()
.is_some_and(|s| s.matches_in(*self, Some(self.id()), &Memo::default()))
}
pub fn closest<S: AsSelector + ?Sized>(&self, selector: &S) -> Option<NodeRef<'a>> {
let sel = selector.as_selector()?;
let (scope, memo) = (Some(self.id()), Memo::default());
core::iter::once(*self)
.chain(self.ancestors())
.find(|n| sel.matches_in(*n, scope, &memo))
}
}
#[derive(Clone)]
struct Complex {
compounds: Vec<Compound>,
combs: Vec<Comb>,
slot: u32,
relative: bool,
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum Comb {
Descendant,
Child,
Next,
Later,
}
#[derive(Clone)]
struct Compound {
tag: Option<String>,
simples: Vec<Simple>,
}
#[derive(Clone)]
enum Simple {
Id(String),
Class(String),
Attr {
name: String,
op: AttrOp,
value: String,
ci: bool,
},
Nth(Nth),
Only(bool),
Empty,
Root,
Scope,
Anchor,
Checked,
Disabled,
Enabled,
Not(Vec<Complex>),
Is(Vec<Complex>),
Has(Vec<Complex>),
Contains(String),
}
#[derive(Clone, Copy)]
enum AttrOp {
Exists,
Equals,
Includes,
Dash,
Prefix,
Suffix,
Substring,
}
#[derive(Clone)]
struct Nth {
a: i32,
b: i32,
last: bool,
of_type: bool,
of: Vec<Complex>,
slot: u32,
}
const SHARED_SLOTS: u32 = 4;
fn shared_slot(last: bool, of_type: bool) -> u32 {
u32::from(last) | u32::from(of_type) << 1
}
impl Simple {
fn cost(&self) -> u8 {
match self {
Simple::Id(_) | Simple::Class(_) => 0,
Simple::Not(_) | Simple::Is(_) => 2,
Simple::Has(_) | Simple::Contains(_) => 3,
_ => 1,
}
}
}
struct Parser<'s> {
src: &'s str,
pos: usize,
depth: u32,
in_has: bool,
slots: u32,
}
impl<'s> Parser<'s> {
fn rest(&self) -> &'s str {
self.src.get(self.pos..).unwrap_or("")
}
fn peek(&self) -> Option<char> {
self.rest().chars().next()
}
fn bump(&mut self) -> Option<char> {
let c = self.peek()?;
self.pos += c.len_utf8();
Some(c)
}
fn eat(&mut self, c: char) -> bool {
let hit = self.peek() == Some(c);
if hit {
self.pos += c.len_utf8();
}
hit
}
fn ws(&mut self) -> bool {
let start = self.pos;
while self.peek().is_some_and(|c| c.is_ascii_whitespace()) {
self.pos += 1;
}
self.pos > start
}
fn reserve(&mut self, n: usize) -> u32 {
let first = self.slots;
let n = u32::try_from(n).unwrap_or(u32::MAX);
self.slots = first.saturating_add(n);
first
}
fn error<T>(&self, at: usize, message: impl Into<String>) -> Result<T, SelectorError> {
Err(SelectorError {
position: at,
message: message.into(),
})
}
fn unexpected<T>(&self) -> Result<T, SelectorError> {
match self.peek() {
Some(c) => self.error(
self.pos,
["unexpected '", c.encode_utf8(&mut [0; 4]), "'"].concat(),
),
None => self.error(self.pos, "unexpected end of selector"),
}
}
fn list(&mut self, relative: bool) -> Result<Vec<Complex>, SelectorError> {
let mut list = Vec::new();
loop {
list.push(self.complex(relative)?);
if !self.eat(',') {
return Ok(list);
}
}
}
fn nested(&mut self, relative: bool) -> Result<Vec<Complex>, SelectorError> {
if self.depth >= MAX_DEPTH {
return self.error(self.pos, "selectors nested too deeply");
}
self.depth += 1;
let list = self.list(relative);
self.depth -= 1;
list
}
fn combinator(&mut self) -> Option<Comb> {
let comb = match self.peek()? {
'>' => Comb::Child,
'+' => Comb::Next,
'~' => Comb::Later,
_ => return None,
};
self.pos += 1;
self.ws();
Some(comb)
}
fn complex(&mut self, relative: bool) -> Result<Complex, SelectorError> {
self.ws();
let mut compounds = Vec::new();
let mut combs = Vec::new();
if relative {
compounds.push(Compound {
tag: None,
simples: alloc::vec![Simple::Anchor],
});
combs.push(self.combinator().unwrap_or(Comb::Descendant));
}
loop {
compounds.push(self.compound()?);
let spaced = self.ws();
let comb = match self.combinator() {
Some(comb) => comb,
None => match self.peek() {
None | Some(',' | ')') => break,
Some(_) if spaced => Comb::Descendant,
Some(_) => return self.unexpected(),
},
};
combs.push(comb);
}
compounds.reverse();
combs.reverse();
Ok(Complex {
slot: self.reserve(combs.len()),
compounds,
combs,
relative,
})
}
fn compound(&mut self) -> Result<Compound, SelectorError> {
let start = self.pos;
let mut tag = None;
if !self.eat('*') && self.peek().is_some_and(|c| c == '\\' || is_name_char(c)) {
tag = Some(self.name()?.to_ascii_lowercase());
}
let mut simples: Vec<Simple> = Vec::new();
loop {
let at = self.pos;
let simple = match self.peek() {
Some('#') => {
self.pos += 1;
Simple::Id(self.name()?)
}
Some('.') => {
self.pos += 1;
Simple::Class(self.name()?)
}
Some('[') => {
self.pos += 1;
self.attribute()?
}
Some(':') => {
self.pos += 1;
self.pseudo(at)?
}
_ => break,
};
let i = simples
.iter()
.position(|s| s.cost() > simple.cost())
.unwrap_or(simples.len());
simples.insert(i, simple);
}
if self.pos == start {
return match self.peek() {
None => self.error(start, "expected a selector"),
Some(_) => self.unexpected(),
};
}
Ok(Compound { tag, simples })
}
fn name(&mut self) -> Result<String, SelectorError> {
let mut out = String::new();
while let Some(c) = self.peek() {
if c == '\\' {
self.pos += 1;
out.push(self.escape());
} else if is_name_char(c) {
self.pos += c.len_utf8();
out.push(c);
} else {
break;
}
}
if out.is_empty() {
return self.unexpected();
}
Ok(out)
}
fn escape(&mut self) -> char {
let start = self.pos;
let mut code = 0u32;
while self.pos - start < 6 {
match self.peek().and_then(|c| c.to_digit(16)) {
Some(d) => {
code = code * 16 + d;
self.pos += 1;
}
None => break,
}
}
if self.pos > start {
if self.peek().is_some_and(|c| c.is_ascii_whitespace()) {
self.pos += 1;
}
return char::from_u32(code)
.filter(|&c| c != '\0')
.unwrap_or('\u{FFFD}');
}
self.bump().unwrap_or('\u{FFFD}')
}
fn string(&mut self) -> Result<String, SelectorError> {
let start = self.pos;
let quote = self.bump();
let mut out = String::new();
loop {
match self.bump() {
None => return self.error(start, "unterminated string"),
Some('\\') => match self.peek() {
Some('\n') => self.pos += 1, Some(_) => out.push(self.escape()),
None => {}
},
Some(c) if Some(c) == quote => return Ok(out),
Some(c) => out.push(c),
}
}
}
fn attribute(&mut self) -> Result<Simple, SelectorError> {
const OPS: [(&str, AttrOp); 6] = [
("=", AttrOp::Equals),
("~=", AttrOp::Includes),
("|=", AttrOp::Dash),
("^=", AttrOp::Prefix),
("$=", AttrOp::Suffix),
("*=", AttrOp::Substring),
];
self.ws();
let name = self.name()?.to_ascii_lowercase();
self.ws();
if self.eat(']') {
return Ok(Simple::Attr {
name,
op: AttrOp::Exists,
value: String::new(),
ci: false,
});
}
let rest = self.rest();
let Some(&(token, op)) = OPS.iter().find(|(t, _)| rest.starts_with(t)) else {
return self.unexpected();
};
self.pos += token.len();
self.ws();
let value = match self.peek() {
Some('"' | '\'') => self.string()?,
_ => self.unquoted()?,
};
self.ws();
let mut ci = CASE_INSENSITIVE_ATTRS.contains(&name.as_str());
if let Some(flag @ ('i' | 'I' | 's' | 'S')) = self.peek() {
self.pos += 1;
ci = flag.eq_ignore_ascii_case(&'i');
self.ws();
}
if !self.eat(']') {
return self.unexpected();
}
Ok(Simple::Attr {
name,
op,
value,
ci,
})
}
fn unquoted(&mut self) -> Result<String, SelectorError> {
let mut out = String::new();
while let Some(c) = self.peek() {
if c.is_ascii_whitespace() || matches!(c, '[' | ']' | '"' | '\'') {
break;
}
self.pos += c.len_utf8();
if c == '\\' {
out.push(self.escape());
} else {
out.push(c);
}
}
if out.is_empty() {
return self.unexpected();
}
Ok(out)
}
fn pseudo(&mut self, at: usize) -> Result<Simple, SelectorError> {
if self.peek() == Some(':') {
return self.error(at, "pseudo-elements are not supported");
}
let name = self.name()?.to_ascii_lowercase();
if !self.eat('(') {
let nth = |last, of_type| {
Simple::Nth(Nth {
a: 0,
b: 1,
last,
of_type,
of: Vec::new(),
slot: shared_slot(last, of_type),
})
};
return Ok(match name.as_str() {
"first-child" => nth(false, false),
"last-child" => nth(true, false),
"first-of-type" => nth(false, true),
"last-of-type" => nth(true, true),
"only-child" => Simple::Only(false),
"only-of-type" => Simple::Only(true),
"empty" => Simple::Empty,
"root" => Simple::Root,
"scope" => Simple::Scope,
"checked" => Simple::Checked,
"disabled" => Simple::Disabled,
"enabled" => Simple::Enabled,
_ => return self.error(at, ["unsupported pseudo-class ':", &name, "'"].concat()),
});
}
self.ws();
let simple = match name.as_str() {
"not" => Simple::Not(self.nested(false)?),
"is" | "where" => Simple::Is(self.nested(false)?),
"has" if self.in_has => return self.error(at, ":has() cannot be nested"),
"has" => {
self.in_has = true;
let list = self.nested(true);
self.in_has = false;
Simple::Has(list?)
}
"contains" => Simple::Contains(self.contains_arg()?),
"nth-child" | "nth-last-child" | "nth-of-type" | "nth-last-of-type" => {
self.nth(&name)?
}
_ => return self.error(at, ["unsupported pseudo-class ':", &name, "()'"].concat()),
};
self.ws();
if !self.eat(')') {
return self.unexpected();
}
Ok(simple)
}
fn contains_arg(&mut self) -> Result<String, SelectorError> {
if let Some('"' | '\'') = self.peek() {
return self.string();
}
let rest = self.rest();
let end = rest.find(')').unwrap_or(rest.len());
let text = rest.get(..end).unwrap_or("").trim_end();
if text.is_empty() {
return self.unexpected();
}
self.pos += text.len();
Ok(text.into())
}
fn nth(&mut self, name: &str) -> Result<Simple, SelectorError> {
let (a, b) = self.an_plus_b()?;
let of_type = name.ends_with("of-type");
let mut of = Vec::new();
if !of_type
&& self.ws()
&& self
.rest()
.get(..2)
.is_some_and(|k| k.eq_ignore_ascii_case("of"))
{
self.pos += 2;
if !self.ws() {
return self.unexpected();
}
of = self.nested(false)?;
}
let last = name.starts_with("nth-last");
Ok(Simple::Nth(Nth {
a,
b,
last,
of_type,
slot: if of.is_empty() {
shared_slot(last, of_type)
} else {
self.reserve(1)
},
of,
}))
}
fn an_plus_b(&mut self) -> Result<(i32, i32), SelectorError> {
let start = self.pos;
for (word, ab) in [("odd", (2, 1)), ("even", (2, 0))] {
if self
.rest()
.get(..word.len())
.is_some_and(|w| w.eq_ignore_ascii_case(word))
{
self.pos += word.len();
return Ok(ab);
}
}
let neg = self.eat('-');
if !neg {
self.eat('+');
}
let digits = self.integer()?;
if !(self.eat('n') || self.eat('N')) {
return match digits {
Some(b) => Ok((0, if neg { -b } else { b })),
None => self.error(start, "invalid :nth-*() argument"),
};
}
let a = digits.unwrap_or(1);
let a = if neg { -a } else { a };
let before = self.pos;
self.ws();
let b = match self.peek() {
Some(sign @ ('+' | '-')) => {
self.pos += 1;
self.ws();
match self.integer()? {
Some(b) if sign == '-' => -b,
Some(b) => b,
None => return self.unexpected(),
}
}
_ => {
self.pos = before; 0
}
};
Ok((a, b))
}
fn integer(&mut self) -> Result<Option<i32>, SelectorError> {
let start = self.pos;
let mut n: i32 = 0;
while let Some(d) = self.peek().and_then(|c| c.to_digit(10)) {
n = match n.checked_mul(10).and_then(|n| n.checked_add(d as i32)) {
Some(n) => n,
None => return self.error(start, "number too large"),
};
self.pos += 1;
}
Ok((self.pos > start).then_some(n))
}
}
#[rustfmt::skip]
const CASE_INSENSITIVE_ATTRS: &[&str] = &[
"accept", "accept-charset", "align", "alink", "axis", "bgcolor", "charset", "checked", "clear",
"codetype", "color", "compact", "declare", "defer", "dir", "direction", "disabled", "enctype",
"face", "frame", "hreflang", "http-equiv", "lang", "language", "link", "media", "method",
"multiple", "nohref", "noresize", "noshade", "nowrap", "readonly", "rel", "rev", "rules",
"scope", "scrolling", "selected", "shape", "target", "text", "type", "valign", "valuetype",
"vlink",
];
fn is_name_char(c: char) -> bool {
c.is_ascii_alphanumeric() || c == '-' || c == '_' || !c.is_ascii()
}
#[derive(Clone, Copy)]
struct Ctx<'m, 'a> {
scope: Option<NodeId>,
anchor: Option<NodeId>,
memo: &'m Memo<'a>,
}
#[derive(Default)]
struct Memo<'a> {
dense: usize,
tables: RefCell<Vec<u32>>,
map: RefCell<Sparse<'a>>,
lookups: Cell<u32>,
}
const UNKNOWN: u32 = 0;
const NONE: u32 = u32::MAX;
const DENSE: u32 = 16;
const SPARSE: usize = 2;
const WARMUP: u32 = 32;
type Entry<'a> = (u32, Option<NodeRef<'a>>);
#[derive(Default)]
struct Map<'a> {
buckets: Vec<((u32, u32), Entry<'a>)>,
len: usize,
}
impl<'a> Map<'a> {
fn probe(&self, key: (u32, u32)) -> usize {
let mask = self.buckets.len().wrapping_sub(1);
let hash = (u64::from(key.0) << 32 | u64::from(key.1)).wrapping_mul(0x9E37_79B9_7F4A_7C15);
let mut b = (hash >> 32) as usize & mask;
for _ in 0..self.buckets.len() {
match self.buckets.get(b) {
Some(&(k, (v, _))) if v != UNKNOWN && k != key => b = (b + 1) & mask,
_ => break,
}
}
b
}
fn get(&self, key: (u32, u32)) -> Entry<'a> {
match self.buckets.get(self.probe(key)) {
Some(&(k, entry)) if k == key => entry,
_ => (UNKNOWN, None),
}
}
fn set(&mut self, key: (u32, u32), entry: Entry<'a>) {
if 2 * (self.len + 1) > self.buckets.len() {
let size = (2 * self.buckets.len()).max(16);
let empty = ((0, 0), (UNKNOWN, None));
let old = core::mem::replace(&mut self.buckets, alloc::vec![empty; size]);
self.len = 0;
for (k, e) in old {
if e.0 != UNKNOWN {
self.set(k, e);
}
}
}
let b = self.probe(key);
if let Some(bucket) = self.buckets.get_mut(b) {
self.len += usize::from(bucket.1.0 == UNKNOWN);
*bucket = (key, entry);
}
}
}
#[derive(Default)]
struct Sparse<'a> {
maps: [Map<'a>; 2],
nodes: usize,
slots: usize,
}
impl<'a> Sparse<'a> {
fn get(&self, key: (u32, u32)) -> Entry<'a> {
let [new, old] = &self.maps;
match new.get(key) {
(UNKNOWN, _) => old.get(key),
entry => entry,
}
}
fn set(&mut self, key: (u32, u32), entry: Entry<'a>) {
self.nodes = self.nodes.max(key.1 as usize + 1);
self.slots = self.slots.max(key.0 as usize + 1);
if self.maps[0].len >= SPARSE * self.nodes + 4 * self.slots {
self.maps[1] = core::mem::take(&mut self.maps[0]);
}
self.maps[0].set(key, entry);
}
}
impl<'a> Memo<'a> {
fn dense(&self, slot: u32) -> bool {
self.dense > 0 && slot < DENSE
}
fn get(&self, slot: u32, node: NodeRef<'_>) -> u32 {
if !self.dense(slot) {
return self.find(slot, node).0;
}
let at = slot as usize * self.dense + node.id().index();
let tables = self.tables.try_borrow();
tables
.ok()
.and_then(|t| t.get(at).copied())
.unwrap_or(UNKNOWN)
}
fn set(&self, slot: u32, node: NodeRef<'_>, value: u32) -> bool {
if !self.dense(slot) {
return self.store(slot, node, (value, None));
}
let at = slot as usize * self.dense + node.id().index();
let Ok(mut tables) = self.tables.try_borrow_mut() else {
return false;
};
if tables.len() <= at {
tables.resize((slot as usize + 1) * self.dense, UNKNOWN);
}
tables.get_mut(at).map(|e| *e = value).is_some()
}
fn find(&self, slot: u32, node: NodeRef<'_>) -> Entry<'a> {
self.lookups.set(self.lookups.get().saturating_add(1));
let map = self.map.try_borrow();
map.map_or((UNKNOWN, None), |m| m.get((slot, node.id().index() as u32)))
}
fn store(&self, slot: u32, node: NodeRef<'_>, entry: Entry<'a>) -> bool {
self.lookups.get() >= WARMUP
&& self
.map
.try_borrow_mut()
.map(|mut m| m.set((slot, node.id().index() as u32), entry))
.is_ok()
}
fn search(
&self,
slot: Option<u32>,
start: Option<NodeRef<'a>>,
next: impl Fn(NodeRef<'a>) -> Option<NodeRef<'a>>,
test: impl Fn(NodeRef<'a>) -> Option<(NodeRef<'a>, usize)>,
) -> Option<(NodeRef<'a>, usize)> {
let mut walked = 0;
let mut cur = start;
let found = loop {
let Some(n) = cur else { break None };
if let Some(slot) = slot {
match self.find(slot, n) {
(NONE, _) => break None,
(UNKNOWN, _) | (_, None) => walked += 1,
(i, Some(el)) => break Some((el, i as usize - 1)),
}
}
if let Some(hit) = test(n) {
break Some(hit);
}
cur = next(n);
};
let entry = match found {
None => (NONE, None),
Some((el, i)) => match u32::try_from(i + 1) {
Ok(v) if v != NONE => (v, Some(el)),
_ => return found,
},
};
if let Some(slot) = slot {
for n in core::iter::successors(start, |n| next(*n)).take(walked) {
self.store(slot, n, entry);
}
}
found
}
}
fn any_matches<'a>(list: &[Complex], el: NodeRef<'a>, cx: Ctx<'_, 'a>) -> bool {
list.iter().any(|c| c.matches(el, cx))
}
impl Complex {
fn matches<'a>(&self, el: NodeRef<'a>, cx: Ctx<'_, 'a>) -> bool {
let Some((mut el, mut i)) = self.chunk(0, el, cx) else {
return false;
};
while self.combs.get(i).is_some() {
let up = |a: NodeRef<'a>| a.parent();
let found = cx
.memo
.search(self.slot(i), el.parent(), up, |a| self.chunk(i + 1, a, cx));
let Some(hit) = found else { return false };
(el, i) = hit;
}
true
}
fn chunk<'a>(
&self,
i: usize,
el: NodeRef<'a>,
cx: Ctx<'_, 'a>,
) -> Option<(NodeRef<'a>, usize)> {
let (mut el, mut i) = self.run(i, el, cx)?;
while self.combs.get(i) == Some(&Comb::Later) {
let prev = |p: NodeRef<'a>| p.prev_element_sibling();
(el, i) = cx
.memo
.search(self.slot(i), prev(el), prev, |p| self.run(i + 1, p, cx))?;
}
Some((el, i))
}
fn run<'a>(
&self,
mut i: usize,
mut el: NodeRef<'a>,
cx: Ctx<'_, 'a>,
) -> Option<(NodeRef<'a>, usize)> {
loop {
if !self.compounds.get(i)?.matches(el, cx) {
return None;
}
el = match self.combs.get(i) {
Some(Comb::Child) => el.parent()?,
Some(Comb::Next) => el.prev_element_sibling()?,
_ => return Some((el, i)),
};
i += 1;
}
}
fn slot(&self, i: usize) -> Option<u32> {
if self.relative {
return None;
}
u32::try_from(i).ok().and_then(|i| self.slot.checked_add(i))
}
}
impl Compound {
fn matches<'a>(&self, el: NodeRef<'a>, cx: Ctx<'_, 'a>) -> bool {
let Some(tag) = el.tag_name() else {
return false;
};
self.tag
.as_deref()
.is_none_or(|t| t.eq_ignore_ascii_case(tag))
&& self.simples.iter().all(|s| s.matches(el, cx))
}
}
impl Simple {
fn matches<'a>(&self, el: NodeRef<'a>, cx: Ctx<'_, 'a>) -> bool {
match self {
Simple::Id(id) => el.attr("id") == Some(id.as_str()),
Simple::Class(class) => el.has_class(class),
Simple::Attr {
name,
op,
value,
ci,
} => el
.attr(name)
.is_some_and(|have| attr_matches(*op, have, value, *ci)),
Simple::Nth(nth) => nth.matches(el, cx),
Simple::Only(of_type) => {
let same =
|s: NodeRef<'_>| s.is_element() && (!of_type || s.tag_name() == el.tag_name());
!core::iter::successors(el.prev_sibling(), NodeRef::prev_sibling).any(same)
&& !core::iter::successors(el.next_sibling(), NodeRef::next_sibling).any(same)
}
Simple::Empty => el
.children()
.all(|c| c.comment().is_some() || c.text() == Some("")),
Simple::Root => el.parent().is_none(),
Simple::Scope => match cx.scope {
Some(id) => el.id() == id,
None => el.parent().is_none(),
},
Simple::Anchor => cx.anchor == Some(el.id()),
Simple::Checked => match el.tag_name() {
Some("input") => el.has_attr("checked"),
Some("option") => el.has_attr("selected"),
_ => false,
},
Simple::Disabled => is_form_control(el) && el.has_attr("disabled"),
Simple::Enabled => is_form_control(el) && !el.has_attr("disabled"),
Simple::Not(list) => !any_matches(list, el, cx),
Simple::Is(list) => any_matches(list, el, cx),
Simple::Has(list) => has(list, el, cx),
Simple::Contains(text) => el.text_content().contains(text.as_str()),
}
}
}
impl Nth {
fn matches<'a>(&self, el: NodeRef<'a>, cx: Ctx<'_, 'a>) -> bool {
let Some(pos) = self.position(el, cx) else {
return false;
};
let (a, d) = (i64::from(self.a), i64::from(pos) - i64::from(self.b));
if a == 0 {
d == 0
} else {
d % a == 0 && d / a >= 0
}
}
fn counts<'a>(&self, s: NodeRef<'a>, el: NodeRef<'a>, cx: Ctx<'_, 'a>) -> bool {
if self.of_type {
return s.tag_name() == el.tag_name();
}
if self.of.is_empty() {
return true;
}
match cx.memo.get(self.slot, s) {
UNKNOWN => {
let hit = any_matches(&self.of, s, cx);
if !hit {
cx.memo.set(self.slot, s, NONE);
}
hit
}
known => known != NONE,
}
}
fn position<'a>(&self, el: NodeRef<'a>, cx: Ctx<'_, 'a>) -> Option<u32> {
match cx.memo.get(self.slot, el) {
UNKNOWN => {}
NONE => return None,
pos => return Some(pos),
}
if !self.counts(el, el, cx) {
return None;
}
let step = |n: NodeRef<'a>| {
if self.last {
n.next_sibling()
} else {
n.prev_sibling()
}
};
let (mut walked, mut base) = (0u32, 0);
let mut sib = step(el);
while let Some(s) = sib {
if s.is_element() && self.counts(s, el, cx) {
match cx.memo.get(self.slot, s) {
UNKNOWN => walked += 1,
pos => {
base = pos;
break;
}
}
if self.a <= 0 && i64::from(walked) >= i64::from(self.b) {
return None;
}
}
sib = step(s);
}
let pos = walked.checked_add(base)?.checked_add(1)?;
if cx.memo.set(self.slot, el, pos) {
let (mut p, mut sib) = (pos, step(el));
while p > base + 1 {
let Some(s) = sib else { break };
if s.is_element() && self.counts(s, el, cx) {
p -= 1;
cx.memo.set(self.slot, s, p);
}
sib = step(s);
}
}
Some(pos)
}
}
fn attr_matches(op: AttrOp, have: &str, want: &str, ci: bool) -> bool {
let (have, want) = (have.as_bytes(), want.as_bytes());
let eq = |s: &[u8]| {
if ci {
s.eq_ignore_ascii_case(want)
} else {
s == want
}
};
match op {
AttrOp::Exists => true,
AttrOp::Equals => eq(have),
AttrOp::Dash => {
eq(have)
|| (have.get(want.len()) == Some(&b'-') && have.get(..want.len()).is_some_and(eq))
}
_ if want.is_empty() => false,
AttrOp::Includes => have.split(u8::is_ascii_whitespace).any(eq),
AttrOp::Prefix => have.get(..want.len()).is_some_and(eq),
AttrOp::Suffix => have
.len()
.checked_sub(want.len())
.and_then(|i| have.get(i..))
.is_some_and(eq),
AttrOp::Substring => have.windows(want.len()).any(eq),
}
}
fn has<'a>(rels: &[Complex], el: NodeRef<'a>, cx: Ctx<'_, 'a>) -> bool {
let cx = Ctx {
anchor: Some(el.id()),
..cx
};
rels.iter().any(|rel| match rel.combs.last() {
Some(Comb::Next | Comb::Later) => {
let deep = rel
.combs
.iter()
.any(|c| matches!(c, Comb::Child | Comb::Descendant));
let reach = if rel.combs.iter().all(|c| *c == Comb::Next) {
rel.combs.len()
} else {
usize::MAX
};
core::iter::successors(el.next_element_sibling(), NodeRef::next_element_sibling)
.take(reach)
.any(|s| {
rel.matches(s, cx) || (deep && s.descendants().any(|d| rel.matches(d, cx)))
})
}
_ => el.descendants().any(|d| rel.matches(d, cx)),
})
}
fn is_form_control(el: NodeRef<'_>) -> bool {
matches!(
el.tag_name(),
Some("button" | "input" | "select" | "textarea" | "optgroup" | "option" | "fieldset")
)
}