use std::str::FromStr;
use super::values::Viewport;
use crate::tdom::node::{NodeId, NodeType};
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum SelectorError {
#[error("empty selector")]
EmptySelector,
#[error("empty compound")]
EmptyCompound,
#[error("invalid class")]
InvalidClass,
#[error("invalid id")]
InvalidId,
#[error("invalid tag")]
InvalidTag,
#[error(":hover takes no arguments")]
HoverTakesNoArguments,
#[error(":focus takes no arguments")]
FocusTakesNoArguments,
#[error(":nth-child requires `(...)`")]
NthChildRequiresParens,
#[error(":nth-child(...) missing ')'")]
NthChildMissingCloseParen,
#[error("invalid :nth-child arg: {0:?}")]
InvalidNthChildArg(String),
#[error(":nth-last-child requires `(...)`")]
NthLastChildRequiresParens,
#[error(":nth-last-child(...) missing ')'")]
NthLastChildMissingCloseParen,
#[error("invalid :nth-last-child arg: {0:?}")]
InvalidNthLastChildArg(String),
#[error(":not requires `(...)`")]
NotRequiresParens,
#[error(":not(...) missing ')'")]
NotMissingCloseParen,
#[error(":not(...) nesting too deep")]
NotNestedTooDeep,
#[error("unsupported pseudo-class: :{0}")]
UnsupportedPseudoClass(String),
#[error("unsupported CSS tag: {0:?}")]
UnsupportedTag(String),
#[error("invalid attribute selector: [{0}]")]
InvalidAttributeSelector(String),
#[error(":{0} requires `(...)`")]
NthOfTypeRequiresParens(String),
#[error(":{0}(...) missing ')'")]
NthOfTypeMissingCloseParen(String),
#[error("invalid :{0} arg: {1:?}")]
InvalidNthOfTypeArg(String, String),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct NthExpr {
pub a: usize,
pub b: usize,
}
impl NthExpr {
pub const EVEN: NthExpr = NthExpr { a: 2, b: 0 };
pub const ODD: NthExpr = NthExpr { a: 2, b: 1 };
pub fn matches(self, index_1based: usize) -> bool {
if self.a == 0 {
return index_1based == self.b;
}
index_1based >= self.b && (index_1based - self.b).is_multiple_of(self.a)
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct DynamicState {
pub hovered: Option<NodeId>,
pub focused: Option<NodeId>,
pub viewport: Viewport,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PseudoClassSet(u8);
impl PseudoClassSet {
pub const EMPTY: Self = Self(0);
pub const HOVER: Self = Self(1 << 0);
pub const FOCUS: Self = Self(1 << 1);
pub fn contains_pseudo(&self, pseudo: PseudoClass) -> bool {
self.0 & pseudo.as_flag().0 != 0
}
pub fn union(self, other: Self) -> Self {
Self(self.0 | other.0)
}
pub fn from_selector(sel: &CssSelector) -> Self {
let mut set = Self::EMPTY;
for compound in &sel.compounds {
for s in compound {
if let Selector::PseudoClass(p) = s {
set = set.union(p.as_flag());
}
}
}
set
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum PseudoClass {
Hover,
Focus,
}
impl PseudoClass {
pub fn as_flag(&self) -> PseudoClassSet {
match self {
PseudoClass::Hover => PseudoClassSet::HOVER,
PseudoClass::Focus => PseudoClassSet::FOCUS,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum AttributeOp {
Exists,
Equals,
Includes,
DashMatch,
PrefixMatch,
SuffixMatch,
SubstringMatch,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct AttributeSelector {
pub name: String,
pub op: AttributeOp,
pub value: String,
pub case_insensitive: bool,
}
impl AttributeSelector {
pub fn matches_lookup(&self, lookup: &dyn Fn(&str) -> Option<String>) -> bool {
let Some(raw) = lookup(&self.name) else {
return false;
};
let raw = raw.as_str();
match self.op {
AttributeOp::Exists => true,
_ => {
let val = self.value.as_str();
let (raw, val) = if self.case_insensitive {
(raw.to_ascii_lowercase(), val.to_ascii_lowercase())
} else {
(raw.to_string(), val.to_string())
};
match self.op {
AttributeOp::Equals => raw == val,
AttributeOp::Includes => {
!val.is_empty() && raw.split_whitespace().any(|w| w == val)
}
AttributeOp::DashMatch => raw == val || raw.starts_with(&format!("{val}-")),
AttributeOp::PrefixMatch => !val.is_empty() && raw.starts_with(&val),
AttributeOp::SuffixMatch => !val.is_empty() && raw.ends_with(&val),
AttributeOp::SubstringMatch => !val.is_empty() && raw.contains(&val),
AttributeOp::Exists => true,
}
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum Selector {
Universal,
Tag(NodeType),
Class(String),
Id(String),
NthChild(NthExpr),
NthLastChild(NthExpr),
NthOfType(NthExpr),
NthLastOfType(NthExpr),
Root,
Empty,
Not(Vec<CompoundSelector>),
Checked,
Enabled,
Disabled,
Lang(String),
PseudoClass(PseudoClass),
Attribute(AttributeSelector),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Combinator {
Descendant,
Child,
NextSibling,
SubsequentSibling,
}
pub type CompoundSelector = Vec<Selector>;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct CssSelector {
pub compounds: Vec<CompoundSelector>,
pub combinators: Vec<Combinator>,
pub pseudo_element: Option<PseudoElement>,
}
impl FromStr for CssSelector {
type Err = SelectorError;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
let s = s.trim();
if s.is_empty() {
return Err(SelectorError::EmptySelector);
}
let (segments, combinators) = split_by_combinators(s);
if segments.is_empty() {
return Err(SelectorError::EmptySelector);
}
let mut pseudo_element = None;
let mut segments: Vec<String> = segments.iter().map(|s| s.to_string()).collect();
if let Some(last) = segments.last_mut()
&& let Some((trimmed, element)) = strip_pseudo_element(last)
{
pseudo_element = Some(element);
if trimmed.trim().is_empty() {
return Err(SelectorError::UnsupportedPseudoClass(match element {
PseudoElement::Before => "::before".to_string(),
PseudoElement::After => "::after".to_string(),
PseudoElement::SliderThumb => "::slider-thumb".to_string(),
PseudoElement::SliderTrack => "::slider-track".to_string(),
PseudoElement::SliderProgress => "::slider-progress".to_string(),
}));
}
*last = trimmed.trim().to_string();
}
let mut compounds = Vec::with_capacity(segments.len());
for seg in &segments {
let mut compound = CompoundSelector::new();
for part in split_top_level_whitespace(seg) {
compound.extend(parse_compound(part)?);
}
compounds.push(compound);
}
Ok(CssSelector {
compounds,
combinators,
pseudo_element,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum PseudoElement {
Before,
After,
SliderThumb,
SliderTrack,
SliderProgress,
}
fn strip_pseudo_element(seg: &str) -> Option<(String, PseudoElement)> {
let lower = seg.to_ascii_lowercase();
for (suffix, element) in [
(
"::-webkit-slider-runnable-track",
PseudoElement::SliderTrack,
),
("::-moz-range-progress", PseudoElement::SliderProgress),
("::-moz-range-track", PseudoElement::SliderTrack),
("::-webkit-slider-thumb", PseudoElement::SliderThumb),
("::-moz-range-thumb", PseudoElement::SliderThumb),
("::before", PseudoElement::Before),
(":before", PseudoElement::Before),
("::after", PseudoElement::After),
(":after", PseudoElement::After),
] {
if let Some(stripped) = lower.strip_suffix(suffix) {
return Some((seg[..stripped.len()].to_string(), element));
}
}
None
}
impl CssSelector {
pub fn specificity(&self) -> (u32, u32, u32) {
let mut spec = (0u32, 0u32, 0u32);
for compound in &self.compounds {
let part = compound_specificity(compound);
spec.0 += part.0;
spec.1 += part.1;
spec.2 += part.2;
}
spec
}
}
fn split_by_combinators(s: &str) -> (Vec<&str>, Vec<Combinator>) {
let mut segments: Vec<&str> = Vec::new();
let mut combinators: Vec<Combinator> = Vec::new();
let mut iter = s.char_indices().peekable();
let mut start = 0;
let mut depth: usize = 0;
while let Some((i, ch)) = iter.next() {
match ch {
'(' | '[' => {
depth = depth.saturating_add(1);
continue;
}
')' | ']' => {
depth = depth.saturating_sub(1);
continue;
}
_ => {}
}
if depth > 0 {
continue;
}
if !ch.is_whitespace() && !matches!(ch, '>' | '+' | '~') {
continue;
}
let before = s[start..i].trim();
if !before.is_empty() {
segments.push(before);
}
while let Some(&(_, next_ch)) = iter.peek() {
if next_ch.is_whitespace() {
iter.next();
} else {
break;
}
}
let mut combinator = Combinator::Descendant;
if let Some(&(_, next_ch)) = iter.peek() {
combinator = match next_ch {
'>' => {
iter.next();
Combinator::Child
}
'+' => {
iter.next();
Combinator::NextSibling
}
'~' => {
iter.next();
Combinator::SubsequentSibling
}
_ => Combinator::Descendant,
};
while let Some(&(_, next_ch)) = iter.peek() {
if next_ch.is_whitespace() {
iter.next();
} else {
break;
}
}
}
if iter.peek().is_some() {
combinators.push(combinator);
}
start = iter.peek().map(|&(i, _)| i).unwrap_or(s.len());
}
let last = s[start..].trim();
if !last.is_empty() {
segments.push(last);
}
(segments, combinators)
}
fn parse_compound(s: &str) -> std::result::Result<CompoundSelector, SelectorError> {
parse_compound_inner(s, 0)
}
const NOT_MAX_NESTING: usize = 8;
fn parse_compound_inner(
s: &str,
not_depth: usize,
) -> std::result::Result<CompoundSelector, SelectorError> {
let s = s.trim();
if s.is_empty() {
return Err(SelectorError::EmptyCompound);
}
let mut out = Vec::new();
let chars: Vec<char> = s.chars().collect();
let mut i = 0usize;
while i < chars.len() {
match chars[i] {
'.' => {
i += 1;
let start = i;
while i < chars.len()
&& chars[i] != '.'
&& chars[i] != '#'
&& chars[i] != ':'
&& chars[i] != '['
{
i += 1;
}
let name: String = chars[start..i].iter().collect();
if name.is_empty() {
return Err(SelectorError::InvalidClass);
}
out.push(Selector::Class(name));
}
'#' => {
i += 1;
let start = i;
while i < chars.len()
&& chars[i] != '.'
&& chars[i] != '#'
&& chars[i] != ':'
&& chars[i] != '['
{
i += 1;
}
let name: String = chars[start..i].iter().collect();
if name.is_empty() {
return Err(SelectorError::InvalidId);
}
out.push(Selector::Id(name));
}
':' => {
i += 1;
let name_start = i;
while i < chars.len()
&& chars[i] != '('
&& chars[i] != '.'
&& chars[i] != '#'
&& chars[i] != ':'
&& chars[i] != '['
{
i += 1;
}
let name: String = chars[name_start..i].iter().collect();
match name.to_ascii_lowercase().as_str() {
"hover" => {
if i < chars.len() && chars[i] == '(' {
return Err(SelectorError::HoverTakesNoArguments);
}
out.push(Selector::PseudoClass(PseudoClass::Hover));
}
"focus" => {
if i < chars.len() && chars[i] == '(' {
return Err(SelectorError::FocusTakesNoArguments);
}
out.push(Selector::PseudoClass(PseudoClass::Focus));
}
"nth-child" | "nth-last-child" => {
let from_last = name.eq_ignore_ascii_case("nth-last-child");
if i >= chars.len() || chars[i] != '(' {
return Err(if from_last {
SelectorError::NthLastChildRequiresParens
} else {
SelectorError::NthChildRequiresParens
});
}
let Some(arg) = capture_balanced_arg(&chars, &mut i) else {
return Err(if from_last {
SelectorError::NthLastChildMissingCloseParen
} else {
SelectorError::NthChildMissingCloseParen
});
};
let expr = parse_nth_child_arg(&arg).ok_or_else(|| {
if from_last {
SelectorError::InvalidNthLastChildArg(arg.clone())
} else {
SelectorError::InvalidNthChildArg(arg.clone())
}
})?;
out.push(if from_last {
Selector::NthLastChild(expr)
} else {
Selector::NthChild(expr)
});
}
"nth-of-type" | "nth-last-of-type" => {
let lower_name = name.to_ascii_lowercase();
if i >= chars.len() || chars[i] != '(' {
return Err(SelectorError::NthOfTypeRequiresParens(lower_name));
}
let Some(arg) = capture_balanced_arg(&chars, &mut i) else {
return Err(SelectorError::NthOfTypeMissingCloseParen(lower_name));
};
let from_last = lower_name == "nth-last-of-type";
let expr = parse_nth_child_arg(&arg).ok_or_else(|| {
SelectorError::InvalidNthOfTypeArg(lower_name, arg.clone())
})?;
out.push(if from_last {
Selector::NthLastOfType(expr)
} else {
Selector::NthOfType(expr)
});
}
"first-child" => out.push(Selector::NthChild(FIRST_CHILD_EXPR)),
"last-child" => out.push(Selector::NthLastChild(FIRST_CHILD_EXPR)),
"only-child" => {
out.push(Selector::NthChild(FIRST_CHILD_EXPR));
out.push(Selector::NthLastChild(FIRST_CHILD_EXPR));
}
"first-of-type" => out.push(Selector::NthOfType(FIRST_CHILD_EXPR)),
"last-of-type" => out.push(Selector::NthLastOfType(FIRST_CHILD_EXPR)),
"only-of-type" => {
out.push(Selector::NthOfType(FIRST_CHILD_EXPR));
out.push(Selector::NthLastOfType(FIRST_CHILD_EXPR));
}
"root" => out.push(Selector::Root),
"empty" => out.push(Selector::Empty),
"checked" => out.push(Selector::Checked),
"enabled" => out.push(Selector::Enabled),
"disabled" => out.push(Selector::Disabled),
"lang" => {
if i >= chars.len() || chars[i] != '(' {
return Err(SelectorError::NthOfTypeRequiresParens("lang".to_string()));
}
let Some(arg) = capture_balanced_arg(&chars, &mut i) else {
return Err(SelectorError::NthOfTypeMissingCloseParen(
"lang".to_string(),
));
};
let tag = arg.trim().to_ascii_lowercase();
if tag.is_empty() || tag.contains(char::is_whitespace) {
return Err(SelectorError::InvalidNthOfTypeArg(
"lang".to_string(),
arg.clone(),
));
}
out.push(Selector::Lang(tag));
}
"not" => {
if i >= chars.len() || chars[i] != '(' {
return Err(SelectorError::NotRequiresParens);
}
let Some(arg) = capture_balanced_arg(&chars, &mut i) else {
return Err(SelectorError::NotMissingCloseParen);
};
if not_depth >= NOT_MAX_NESTING {
return Err(SelectorError::NotNestedTooDeep);
}
out.push(Selector::Not(parse_not_args(&arg, not_depth + 1)?));
}
_ => return Err(SelectorError::UnsupportedPseudoClass(name)),
}
}
'[' => {
let Some(inner) = capture_balanced_bracket(&chars, &mut i) else {
return Err(SelectorError::InvalidAttributeSelector(
chars.iter().collect(),
));
};
out.push(Selector::Attribute(parse_attribute_selector(&inner)?));
}
_ => {
let start = i;
while i < chars.len()
&& chars[i] != '.'
&& chars[i] != '#'
&& chars[i] != ':'
&& chars[i] != '['
{
i += 1;
}
let tag: String = chars[start..i].iter().collect();
if tag.is_empty() {
return Err(SelectorError::InvalidTag);
}
if tag == "*" {
out.push(Selector::Universal);
} else {
out.push(Selector::Tag(tag_str_to_node_type(&tag)?));
}
}
}
}
Ok(out)
}
fn parse_nth_child_arg(s: &str) -> Option<NthExpr> {
let s = s.trim();
if s.is_empty() {
return None;
}
let lower = s.to_ascii_lowercase();
if lower == "even" {
return Some(NthExpr::EVEN);
}
if lower == "odd" {
return Some(NthExpr::ODD);
}
if let Ok(n) = s.parse::<usize>() {
if n == 0 {
return None;
}
return Some(NthExpr { a: 0, b: n });
}
let (coef_part, tail) = {
let idx = lower.find('n')?;
lower.split_at(idx)
};
let tail = &tail[1..]; let a: usize = if coef_part.is_empty() {
return None; } else {
coef_part.parse().ok()?
};
if a == 0 {
return None;
}
let tail = tail.trim();
if tail.is_empty() {
return Some(NthExpr { a, b: 0 });
}
let (sign, num) = match tail.chars().next()? {
'+' => (1i32, &tail[1..]),
'-' => (-1i32, &tail[1..]),
_ => return None,
};
let n: i32 = num.trim().parse().ok()?;
if n == 0 {
return None;
}
let b_signed = n * sign;
if b_signed <= 0 {
return None;
}
Some(NthExpr {
a,
b: b_signed as usize,
})
}
fn compound_specificity(compound: &[Selector]) -> (u32, u32, u32) {
let mut spec = (0u32, 0u32, 0u32);
for sel in compound {
match sel {
Selector::Universal => {}
Selector::Id(_) => spec.0 += 1,
Selector::Class(_)
| Selector::Attribute(_)
| Selector::PseudoClass(_)
| Selector::NthChild(_)
| Selector::NthLastChild(_)
| Selector::NthOfType(_)
| Selector::NthLastOfType(_)
| Selector::Root
| Selector::Empty
| Selector::Checked
| Selector::Enabled
| Selector::Disabled
| Selector::Lang(_) => spec.1 += 1,
Selector::Not(compounds) => {
let arg = compounds
.iter()
.map(|c| compound_specificity(c))
.max()
.unwrap_or((0, 0, 0));
spec.0 += arg.0;
spec.1 += arg.1;
spec.2 += arg.2;
}
Selector::Tag(_) => spec.2 += 1,
}
}
spec
}
fn tag_str_to_node_type(tag: &str) -> std::result::Result<NodeType, SelectorError> {
NodeType::from_tag_name(tag).ok_or_else(|| SelectorError::UnsupportedTag(tag.to_string()))
}
const FIRST_CHILD_EXPR: NthExpr = NthExpr { a: 0, b: 1 };
fn capture_balanced_arg(chars: &[char], i: &mut usize) -> Option<String> {
let arg_start = *i + 1;
let mut depth = 0usize;
while *i < chars.len() {
match chars[*i] {
'(' => depth = depth.saturating_add(1),
')' => {
depth = depth.saturating_sub(1);
if depth == 0 {
let arg: String = chars[arg_start..*i].iter().collect();
*i += 1;
return Some(arg);
}
}
_ => {}
}
*i += 1;
}
None
}
fn parse_not_args(
arg: &str,
not_depth: usize,
) -> std::result::Result<Vec<CompoundSelector>, SelectorError> {
let parts = split_top_level_commas(arg);
if parts.is_empty() {
return Err(SelectorError::EmptyCompound);
}
parts
.into_iter()
.map(|part| parse_compound_inner(part, not_depth))
.collect()
}
fn split_top_level_whitespace(s: &str) -> Vec<&str> {
split_top_level(s, char::is_whitespace)
.into_iter()
.filter(|p| !p.is_empty())
.collect()
}
fn split_top_level_commas(s: &str) -> Vec<&str> {
split_top_level(s, |c| c == ',')
.into_iter()
.map(str::trim)
.filter(|p| !p.is_empty())
.collect()
}
fn split_top_level(s: &str, is_sep: impl Fn(char) -> bool) -> Vec<&str> {
let mut parts = Vec::new();
let mut depth = 0usize;
let mut start = 0usize;
for (i, ch) in s.char_indices() {
match ch {
'(' | '[' => depth = depth.saturating_add(1),
')' | ']' => depth = depth.saturating_sub(1),
c if depth == 0 && is_sep(c) => {
parts.push(&s[start..i]);
start = i + ch.len_utf8();
}
_ => {}
}
}
parts.push(&s[start..]);
parts
}
fn capture_balanced_bracket(chars: &[char], i: &mut usize) -> Option<String> {
debug_assert_eq!(chars[*i], '[');
let start = *i + 1;
let mut depth = 0usize;
while *i < chars.len() {
match chars[*i] {
'[' => depth += 1,
']' => {
depth -= 1;
if depth == 0 {
let inner: String = chars[start..*i].iter().collect();
*i += 1;
return Some(inner);
}
}
_ => {}
}
*i += 1;
}
None
}
fn parse_attribute_selector(inner: &str) -> std::result::Result<AttributeSelector, SelectorError> {
let inner = inner.trim();
if inner.is_empty() {
return Err(SelectorError::InvalidAttributeSelector(inner.to_string()));
}
let mut case_insensitive = false;
let mut body = inner;
for flag in [" i", " I"] {
if let Some(stripped) = body.strip_suffix(flag) {
case_insensitive = true;
body = stripped.trim_end();
break;
}
}
if !body.contains('=') {
let name = body.trim().to_ascii_lowercase();
if name.is_empty() {
return Err(SelectorError::InvalidAttributeSelector(inner.to_string()));
}
return Ok(AttributeSelector {
name,
op: AttributeOp::Exists,
value: String::new(),
case_insensitive,
});
}
let bytes = body.as_bytes();
let mut op_start = None; let mut op = AttributeOp::Equals;
let mut name_end = None; for (idx, &b) in bytes.iter().enumerate() {
if b != b'=' || idx == 0 {
continue;
}
match bytes[idx - 1] {
b'~' => {
op = AttributeOp::Includes;
name_end = Some(idx - 1);
op_start = Some(idx + 1);
}
b'|' => {
op = AttributeOp::DashMatch;
name_end = Some(idx - 1);
op_start = Some(idx + 1);
}
b'^' => {
op = AttributeOp::PrefixMatch;
name_end = Some(idx - 1);
op_start = Some(idx + 1);
}
b'$' => {
op = AttributeOp::SuffixMatch;
name_end = Some(idx - 1);
op_start = Some(idx + 1);
}
b'*' => {
op = AttributeOp::SubstringMatch;
name_end = Some(idx - 1);
op_start = Some(idx + 1);
}
_ => {
op = AttributeOp::Equals;
name_end = Some(idx);
op_start = Some(idx + 1);
}
}
break;
}
let (Some(name_end), Some(op_start)) = (name_end, op_start) else {
return Err(SelectorError::InvalidAttributeSelector(inner.to_string()));
};
let name = body[..name_end].trim().to_ascii_lowercase();
if name.is_empty() {
return Err(SelectorError::InvalidAttributeSelector(inner.to_string()));
}
let mut value = body[op_start..].trim();
if value.len() >= 2
&& ((value.starts_with('"') && value.ends_with('"'))
|| (value.starts_with('\'') && value.ends_with('\'')))
{
value = &value[1..value.len() - 1];
}
Ok(AttributeSelector {
name,
op,
value: value.to_string(),
case_insensitive,
})
}