use rdom_core::selectors::{
Combinator, ComplexSelector, CompoundSelector, PseudoClass, SimpleSelector,
};
use crate::style::Stylesheet;
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub(crate) struct SiblingTriggers {
all: bool,
state: bool,
attribute_state: bool,
attributes: Vec<String>,
}
#[derive(Debug, Clone, Copy)]
pub(crate) enum Cause<'a> {
State,
Attribute(&'a str),
}
impl SiblingTriggers {
pub(crate) fn all() -> Self {
Self {
all: true,
..Self::default()
}
}
pub(crate) fn none() -> Self {
Self::default()
}
pub(crate) fn of_sheets<'a>(sheets: impl IntoIterator<Item = &'a Stylesheet>) -> Self {
let mut t = Self::none();
for sheet in sheets {
for rule in sheet.rules() {
for complex in &rule.selector.0 {
t.visit_complex(complex);
}
}
}
t
}
pub(crate) fn fires(&self, cause: Cause<'_>) -> bool {
self.all
|| match cause {
Cause::State => self.state,
Cause::Attribute(name) => {
self.attribute_state
|| self.attributes.iter().any(|a| a.eq_ignore_ascii_case(name))
}
}
}
fn visit_complex(&mut self, complex: &ComplexSelector) {
for (combinator, compound) in &complex.ancestors {
if matches!(
combinator,
Combinator::AdjacentSibling | Combinator::GeneralSibling
) {
self.add_left(compound);
}
}
for compound in std::iter::once(&complex.subject)
.chain(complex.ancestors.iter().map(|(_, compound)| compound))
{
for simple in &compound.simples {
if let SimpleSelector::Not(list) | SimpleSelector::Where(list) = simple {
for inner in &list.0 {
self.visit_complex(inner);
}
}
}
}
}
fn add_left(&mut self, compound: &CompoundSelector) {
for simple in &compound.simples {
match simple {
SimpleSelector::Universal | SimpleSelector::Type(_) => {}
SimpleSelector::Id(_) => self.add_attribute("id"),
SimpleSelector::Class(_) => self.add_attribute("class"),
SimpleSelector::Attribute { name, .. } => self.add_attribute(name),
SimpleSelector::Pseudo(p) => {
self.state = true;
self.attribute_state |= reads_attributes(*p);
}
SimpleSelector::Not(list) | SimpleSelector::Where(list) => {
for inner in &list.0 {
self.add_left(&inner.subject);
for (_, c) in &inner.ancestors {
self.add_left(c);
}
}
}
_ => self.all = true,
}
}
}
fn add_attribute(&mut self, name: &str) {
if !self.attributes.iter().any(|a| a.eq_ignore_ascii_case(name)) {
self.attributes.push(name.to_ascii_lowercase());
}
}
}
fn reads_attributes(p: PseudoClass) -> bool {
!matches!(
p,
PseudoClass::FirstChild
| PseudoClass::LastChild
| PseudoClass::OnlyChild
| PseudoClass::Empty
| PseudoClass::Root
| PseudoClass::Hover
| PseudoClass::Active
| PseudoClass::Focus
| PseudoClass::FocusWithin
| PseudoClass::FocusVisible
)
}
#[cfg(test)]
mod tests {
use super::*;
fn triggers(css: &str) -> SiblingTriggers {
SiblingTriggers::of_sheets([&rdom_css::parse(css).stylesheet])
}
#[test]
fn a_type_only_left_compound_reads_nothing() {
let t = triggers("h1 + p { color: red; } h2 ~ p { color: red; }");
assert!(!t.fires(Cause::State));
assert!(!t.fires(Cause::Attribute("class")));
}
#[test]
fn a_pseudo_class_left_compound_fires_on_state() {
let t = triggers("a:hover + b { color: red; }");
assert!(t.fires(Cause::State));
assert!(!t.fires(Cause::Attribute("x")), ":hover reads no attribute");
}
#[test]
fn an_attribute_left_compound_fires_for_that_attribute_only() {
let t = triggers("[x] + b { color: red; } .on ~ i { color: red; } #k + u { color: red; }");
assert!(!t.fires(Cause::State));
for name in ["x", "X", "class", "id"] {
assert!(t.fires(Cause::Attribute(name)), "{name}");
}
assert!(!t.fires(Cause::Attribute("y")));
}
#[test]
fn an_attribute_pseudo_class_fires_on_any_attribute() {
let t = triggers("input:checked + label { color: red; }");
assert!(t.fires(Cause::Attribute("checked")));
}
#[test]
fn a_compound_left_of_a_child_combinator_only_is_not_a_trigger() {
let t = triggers("a:hover > b + c { color: red; }");
assert!(
!t.fires(Cause::State),
"b reads nothing; a's subtree is marked anyway"
);
}
#[test]
fn sibling_combinators_inside_not_count() {
let t = triggers("p:not(.x + *) { color: red; }");
assert!(t.fires(Cause::Attribute("class")));
assert!(!t.fires(Cause::State));
}
#[test]
fn no_sibling_combinator_fires_nothing() {
let t = triggers("a:hover b { color: red; } [x] > p { color: red; }");
assert_eq!(t, SiblingTriggers::none());
}
}