use super::selectors::{
AnPlusB, AttributeCase, AttributeOp, AttributeSelector, Combinator, ComplexSelector,
CompoundSelector, ElementSelector, PseudoClass, SelectorList, SubclassSelector,
};
pub trait Element: Sized + Copy {
fn local_name(&self) -> &str;
fn id(&self) -> Option<&str>;
fn has_class(&self, class_name: &str) -> bool;
fn attribute(&self, name: &str) -> Option<&str>;
fn has_attribute(&self, name: &str) -> bool;
fn parent(&self) -> Option<Self>;
fn prev_element_sibling(&self) -> Option<Self>;
fn next_element_sibling(&self) -> Option<Self>;
fn is_root(&self) -> bool {
self.parent().is_none()
}
fn is_empty(&self) -> bool;
fn sibling_index(&self) -> usize {
let mut count = 1usize;
let mut cur = self.prev_element_sibling();
while let Some(s) = cur {
count += 1;
cur = s.prev_element_sibling();
}
count
}
fn sibling_count(&self) -> usize {
let forward = {
let mut count = 0usize;
let mut cur = self.next_element_sibling();
while let Some(s) = cur {
count += 1;
cur = s.next_element_sibling();
}
count
};
self.sibling_index() + forward
}
fn sibling_index_of_type(&self) -> usize {
let me = self.local_name();
let mut count = 1usize;
let mut cur = self.prev_element_sibling();
while let Some(s) = cur {
if s.local_name() == me {
count += 1;
}
cur = s.prev_element_sibling();
}
count
}
fn sibling_count_of_type(&self) -> usize {
let me = self.local_name();
let forward = {
let mut count = 0usize;
let mut cur = self.next_element_sibling();
while let Some(s) = cur {
if s.local_name() == me {
count += 1;
}
cur = s.next_element_sibling();
}
count
};
self.sibling_index_of_type() + forward
}
fn any_descendant<F: FnMut(Self) -> bool>(&self, mut pred: F) -> bool {
let mut stack: Vec<Self> = Vec::new();
for c in self.element_children() {
stack.push(c);
}
while let Some(node) = stack.pop() {
if pred(node) {
return true;
}
for c in node.element_children() {
stack.push(c);
}
}
false
}
fn element_children(&self) -> ElementChildren<Self> {
ElementChildren {
current: self.first_element_child(),
}
}
fn first_element_child(&self) -> Option<Self>;
}
pub struct ElementChildren<E> {
current: Option<E>,
}
impl<E: Element> Iterator for ElementChildren<E> {
type Item = E;
fn next(&mut self) -> Option<E> {
let c = self.current?;
self.current = c.next_element_sibling();
Some(c)
}
}
pub fn match_selector_list<E: Element>(list: &SelectorList, element: E) -> bool {
list.selectors
.iter()
.any(|sel| match_complex_selector(sel, element))
}
pub fn match_complex_selector<E: Element>(selector: &ComplexSelector, element: E) -> bool {
if !match_compound(&selector.compounds[0], element) {
return false;
}
walk_combinators(selector, 0, element)
}
fn walk_combinators<E: Element>(
selector: &ComplexSelector,
matched_idx: usize,
matched: E,
) -> bool {
if matched_idx + 1 >= selector.compounds.len() {
return true;
}
let combinator = selector.combinators[matched_idx];
let next_compound = &selector.compounds[matched_idx + 1];
match combinator {
Combinator::Descendant => {
let mut cur = matched.parent();
while let Some(anc) = cur {
if match_compound(next_compound, anc)
&& walk_combinators(selector, matched_idx + 1, anc)
{
return true;
}
cur = anc.parent();
}
false
},
Combinator::Child => {
if let Some(parent) = matched.parent() {
if match_compound(next_compound, parent) {
return walk_combinators(selector, matched_idx + 1, parent);
}
}
false
},
Combinator::NextSibling => {
if let Some(sib) = matched.prev_element_sibling() {
if match_compound(next_compound, sib) {
return walk_combinators(selector, matched_idx + 1, sib);
}
}
false
},
Combinator::SubsequentSibling => {
let mut cur = matched.prev_element_sibling();
while let Some(sib) = cur {
if match_compound(next_compound, sib)
&& walk_combinators(selector, matched_idx + 1, sib)
{
return true;
}
cur = sib.prev_element_sibling();
}
false
},
}
}
fn match_compound<E: Element>(compound: &CompoundSelector, element: E) -> bool {
if let Some(elt_sel) = &compound.element {
if !match_element_selector(elt_sel, element) {
return false;
}
}
for sub in &compound.subclasses {
if !match_subclass(sub, element) {
return false;
}
}
true
}
fn match_element_selector<E: Element>(sel: &ElementSelector, element: E) -> bool {
match sel {
ElementSelector::Universal => true,
ElementSelector::Type(name) => element.local_name().eq_ignore_ascii_case(name),
}
}
fn match_subclass<E: Element>(sub: &SubclassSelector, element: E) -> bool {
match sub {
SubclassSelector::Class(c) => element.has_class(c),
SubclassSelector::Id(id) => element.id().map(|s| s == id).unwrap_or(false),
SubclassSelector::Attribute(a) => match_attribute(a, element),
SubclassSelector::PseudoClass(pc) => match_pseudo_class(pc, element),
SubclassSelector::PseudoElement(_) => true,
}
}
fn match_attribute<E: Element>(a: &AttributeSelector, element: E) -> bool {
let Some(actual) = element.attribute(&a.name) else {
return a.op.is_none() && element.has_attribute(&a.name);
};
let Some(op) = a.op else {
return true;
};
let Some(expected) = a.value.as_deref() else {
return false;
};
let case_insensitive = match a.case {
AttributeCase::Sensitive => false,
AttributeCase::Insensitive => true,
AttributeCase::Default => false,
};
let cmp = |a: &str, b: &str| {
if case_insensitive {
a.eq_ignore_ascii_case(b)
} else {
a == b
}
};
match op {
AttributeOp::Equals => cmp(actual, expected),
AttributeOp::Includes => actual.split_whitespace().any(|tok| cmp(tok, expected)),
AttributeOp::DashMatch => {
cmp(actual, expected)
|| actual
.strip_prefix(expected)
.map(|rest| rest.starts_with('-'))
.unwrap_or(false)
},
AttributeOp::Prefix => {
!expected.is_empty()
&& (if case_insensitive {
actual
.to_ascii_lowercase()
.starts_with(&expected.to_ascii_lowercase())
} else {
actual.starts_with(expected)
})
},
AttributeOp::Suffix => {
!expected.is_empty()
&& (if case_insensitive {
actual
.to_ascii_lowercase()
.ends_with(&expected.to_ascii_lowercase())
} else {
actual.ends_with(expected)
})
},
AttributeOp::Substring => {
!expected.is_empty()
&& (if case_insensitive {
actual
.to_ascii_lowercase()
.contains(&expected.to_ascii_lowercase())
} else {
actual.contains(expected)
})
},
}
}
fn match_pseudo_class<E: Element>(pc: &PseudoClass, element: E) -> bool {
match pc {
PseudoClass::Root => element.is_root(),
PseudoClass::Empty => element.is_empty(),
PseudoClass::FirstChild => element.sibling_index() == 1,
PseudoClass::LastChild => element.sibling_index() == element.sibling_count(),
PseudoClass::OnlyChild => element.sibling_count() == 1,
PseudoClass::FirstOfType => element.sibling_index_of_type() == 1,
PseudoClass::LastOfType => {
element.sibling_index_of_type() == element.sibling_count_of_type()
},
PseudoClass::OnlyOfType => element.sibling_count_of_type() == 1,
PseudoClass::NthChild(an_b) => an_plus_b_matches(*an_b, element.sibling_index()),
PseudoClass::NthLastChild(an_b) => {
an_plus_b_matches(*an_b, element.sibling_count() - element.sibling_index() + 1)
},
PseudoClass::NthOfType(an_b) => an_plus_b_matches(*an_b, element.sibling_index_of_type()),
PseudoClass::NthLastOfType(an_b) => an_plus_b_matches(
*an_b,
element.sibling_count_of_type() - element.sibling_index_of_type() + 1,
),
PseudoClass::Is(list) | PseudoClass::Where(list) => match_selector_list(list, element),
PseudoClass::Not(list) => !match_selector_list(list, element),
PseudoClass::Has(list) => element.any_descendant(|d| match_selector_list(list, d)),
PseudoClass::UaState(_) | PseudoClass::Functional { .. } => false,
}
}
fn an_plus_b_matches(an_b: AnPlusB, index: usize) -> bool {
let i = index as i64;
let a = an_b.a as i64;
let b = an_b.b as i64;
if a == 0 {
return i == b;
}
let diff = i - b;
if diff == 0 {
return true;
}
if a.signum() != diff.signum() {
return false;
}
diff % a == 0
}
#[cfg(test)]
mod tests {
use super::*;
use crate::html_css::css::parser::parse_stylesheet;
use crate::html_css::css::parser::Rule;
use crate::html_css::css::selectors::parse_selector_list;
struct MockNode {
tag: String,
id: Option<String>,
classes: Vec<String>,
attrs: Vec<(String, String)>,
parent: Option<usize>,
children: Vec<usize>,
text_content_nonempty: bool,
}
struct MockDom {
nodes: Vec<MockNode>,
}
impl MockDom {
fn new() -> Self {
Self { nodes: Vec::new() }
}
fn add(
&mut self,
parent: Option<usize>,
tag: &str,
id: Option<&str>,
classes: &[&str],
attrs: &[(&str, &str)],
) -> usize {
let idx = self.nodes.len();
self.nodes.push(MockNode {
tag: tag.to_ascii_lowercase(),
id: id.map(|s| s.to_string()),
classes: classes.iter().map(|s| s.to_string()).collect(),
attrs: attrs
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect(),
parent,
children: Vec::new(),
text_content_nonempty: false,
});
if let Some(p) = parent {
self.nodes[p].children.push(idx);
}
idx
}
}
#[derive(Clone, Copy)]
struct MockEl<'a> {
dom: &'a MockDom,
idx: usize,
}
impl<'a> Element for MockEl<'a> {
fn local_name(&self) -> &str {
&self.dom.nodes[self.idx].tag
}
fn id(&self) -> Option<&str> {
self.dom.nodes[self.idx].id.as_deref()
}
fn has_class(&self, c: &str) -> bool {
self.dom.nodes[self.idx].classes.iter().any(|x| x == c)
}
fn attribute(&self, name: &str) -> Option<&str> {
self.dom.nodes[self.idx]
.attrs
.iter()
.find(|(k, _)| k.eq_ignore_ascii_case(name))
.map(|(_, v)| v.as_str())
}
fn has_attribute(&self, name: &str) -> bool {
self.attribute(name).is_some()
}
fn parent(&self) -> Option<Self> {
self.dom.nodes[self.idx].parent.map(|i| MockEl {
dom: self.dom,
idx: i,
})
}
fn prev_element_sibling(&self) -> Option<Self> {
let p = self.dom.nodes[self.idx].parent?;
let kids = &self.dom.nodes[p].children;
let pos = kids.iter().position(|&k| k == self.idx)?;
if pos == 0 {
None
} else {
Some(MockEl {
dom: self.dom,
idx: kids[pos - 1],
})
}
}
fn next_element_sibling(&self) -> Option<Self> {
let p = self.dom.nodes[self.idx].parent?;
let kids = &self.dom.nodes[p].children;
let pos = kids.iter().position(|&k| k == self.idx)?;
kids.get(pos + 1).map(|&k| MockEl {
dom: self.dom,
idx: k,
})
}
fn is_empty(&self) -> bool {
self.dom.nodes[self.idx].children.is_empty()
&& !self.dom.nodes[self.idx].text_content_nonempty
}
fn first_element_child(&self) -> Option<Self> {
self.dom.nodes[self.idx].children.first().map(|&k| MockEl {
dom: self.dom,
idx: k,
})
}
}
fn parse_one_selector(src: &str) -> super::super::selectors::SelectorList {
let ss = parse_stylesheet(src).unwrap();
let r = match &ss.rules[0] {
Rule::Qualified(q) => q,
_ => unreachable!(),
};
parse_selector_list(&r.prelude).unwrap()
}
fn matches(src: &str, dom: &MockDom, idx: usize) -> bool {
let list = parse_one_selector(&format!("{src} {{}}"));
match_selector_list(&list, MockEl { dom, idx })
}
fn build_basic_dom() -> (MockDom, Vec<usize>) {
let mut d = MockDom::new();
let html = d.add(None, "html", None, &[], &[]);
let body = d.add(Some(html), "body", None, &[], &[]);
let div = d.add(Some(body), "div", Some("main"), &["container"], &[]);
let p1 = d.add(Some(div), "p", None, &["lead"], &[]);
let p2 = d.add(Some(div), "p", None, &[], &[]);
let span = d.add(Some(div), "span", None, &[], &[("data-x", "42"), ("lang", "en-US")]);
(d, vec![html, body, div, p1, p2, span])
}
#[test]
fn match_type() {
let (d, ix) = build_basic_dom();
assert!(matches("p", &d, ix[3]));
assert!(matches("p", &d, ix[4]));
assert!(!matches("p", &d, ix[5]));
}
#[test]
fn match_universal() {
let (d, ix) = build_basic_dom();
for &i in &ix {
assert!(matches("*", &d, i));
}
}
#[test]
fn match_class() {
let (d, ix) = build_basic_dom();
assert!(matches(".lead", &d, ix[3]));
assert!(!matches(".lead", &d, ix[4]));
assert!(matches(".container", &d, ix[2]));
}
#[test]
fn match_id() {
let (d, ix) = build_basic_dom();
assert!(matches("#main", &d, ix[2]));
assert!(!matches("#main", &d, ix[3]));
}
#[test]
fn match_compound() {
let (d, ix) = build_basic_dom();
assert!(matches("div.container", &d, ix[2]));
assert!(matches("div#main.container", &d, ix[2]));
assert!(!matches("p.container", &d, ix[3]));
}
#[test]
fn match_descendant_combinator() {
let (d, ix) = build_basic_dom();
assert!(matches("body p", &d, ix[3]));
assert!(matches("body p", &d, ix[4]));
assert!(matches("body span", &d, ix[5]));
assert!(matches("div.container > p", &d, ix[3]));
assert!(matches("div.container > p", &d, ix[4]));
}
#[test]
fn match_child_combinator_negative() {
let (d, ix) = build_basic_dom();
assert!(!matches("body > p", &d, ix[3]));
}
#[test]
fn match_next_sibling_combinator() {
let (d, ix) = build_basic_dom();
assert!(matches(".lead + p", &d, ix[4]));
assert!(!matches(".lead + span", &d, ix[5]));
}
#[test]
fn match_subsequent_sibling_combinator() {
let (d, ix) = build_basic_dom();
assert!(matches(".lead ~ span", &d, ix[5]));
assert!(matches(".lead ~ p", &d, ix[4]));
}
#[test]
fn match_attribute_presence() {
let (d, ix) = build_basic_dom();
assert!(matches("[data-x]", &d, ix[5]));
assert!(!matches("[data-x]", &d, ix[3]));
}
#[test]
fn match_attribute_equals() {
let (d, ix) = build_basic_dom();
assert!(matches(r#"[data-x="42"]"#, &d, ix[5]));
assert!(!matches(r#"[data-x="99"]"#, &d, ix[5]));
}
#[test]
fn match_attribute_dash_match() {
let (d, ix) = build_basic_dom();
assert!(matches(r#"[lang|="en"]"#, &d, ix[5]));
}
#[test]
fn match_attribute_prefix_suffix_substring() {
let (d, ix) = build_basic_dom();
assert!(matches(r#"[lang^="en"]"#, &d, ix[5]));
assert!(matches(r#"[lang$="US"]"#, &d, ix[5]));
assert!(matches(r#"[lang*="-"]"#, &d, ix[5]));
}
#[test]
fn match_first_child_last_child_only_child() {
let (d, ix) = build_basic_dom();
assert!(matches(":first-child", &d, ix[3]));
assert!(matches(":last-child", &d, ix[5]));
assert!(matches(":only-child", &d, ix[2]));
}
#[test]
fn match_first_of_type() {
let (d, ix) = build_basic_dom();
assert!(matches("p:first-of-type", &d, ix[3]));
assert!(!matches("p:first-of-type", &d, ix[4]));
assert!(matches("span:first-of-type", &d, ix[5])); }
#[test]
fn match_nth_child() {
let (d, ix) = build_basic_dom();
assert!(matches(":nth-child(1)", &d, ix[3]));
assert!(matches(":nth-child(2)", &d, ix[4]));
assert!(matches(":nth-child(3)", &d, ix[5]));
assert!(matches(":nth-child(odd)", &d, ix[3]));
assert!(!matches(":nth-child(odd)", &d, ix[4]));
assert!(matches(":nth-child(odd)", &d, ix[5]));
}
#[test]
fn match_is_takes_max_specificity_but_or_semantics() {
let (d, ix) = build_basic_dom();
assert!(matches(":is(p, span)", &d, ix[3]));
assert!(matches(":is(p, span)", &d, ix[5]));
assert!(!matches(":is(p, span)", &d, ix[2]));
}
#[test]
fn match_not() {
let (d, ix) = build_basic_dom();
assert!(matches("p:not(.lead)", &d, ix[4]));
assert!(!matches("p:not(.lead)", &d, ix[3]));
}
#[test]
fn match_has_descendant() {
let (d, ix) = build_basic_dom();
assert!(matches("div:has(span)", &d, ix[2]));
assert!(matches("body:has(span)", &d, ix[1]));
assert!(!matches("p:has(span)", &d, ix[3]));
}
#[test]
fn match_root() {
let (d, ix) = build_basic_dom();
assert!(matches(":root", &d, ix[0]));
assert!(!matches(":root", &d, ix[1]));
}
#[test]
fn ua_state_pseudos_never_match() {
let (d, ix) = build_basic_dom();
for s in [":hover", ":focus", ":visited", ":checked"] {
assert!(!matches(s, &d, ix[3]), "{s} matched in PDF context");
}
}
#[test]
fn long_descendant_chain_matches() {
let (d, ix) = build_basic_dom();
assert!(matches("html body div p", &d, ix[4]));
}
#[test]
fn match_empty() {
let (d, ix) = build_basic_dom();
assert!(matches(":empty", &d, ix[3]));
assert!(!matches(":empty", &d, ix[2]));
}
}