use std::str::FromStr;
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("unsupported pseudo-class: :{0}")]
UnsupportedPseudoClass(String),
#[error("unsupported CSS tag: {0:?}")]
UnsupportedTag(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>,
}
#[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, PartialEq, Eq, Hash)]
pub enum Selector {
Universal,
Tag(NodeType),
Class(String),
Id(String),
NthChild(NthExpr),
PseudoClass(PseudoClass),
}
#[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>,
}
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 compounds = Vec::with_capacity(segments.len());
for seg in &segments {
let mut compound = CompoundSelector::new();
for part in seg.split_whitespace().filter(|p| !p.is_empty()) {
compound.extend(parse_compound(part)?);
}
compounds.push(compound);
}
Ok(CssSelector {
compounds,
combinators,
})
}
}
impl CssSelector {
pub fn specificity(&self) -> (u32, u32, u32) {
let mut spec = (0u32, 0u32, 0u32);
for compound in &self.compounds {
for sel in compound {
match sel {
Selector::Universal => {}
Selector::Id(_) => spec.0 += 1,
Selector::Class(_) | Selector::PseudoClass(_) | Selector::NthChild(_) => {
spec.1 += 1
}
Selector::Tag(_) => spec.2 += 1,
}
}
}
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> {
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] != ':' {
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] != ':' {
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] != ':'
{
i += 1;
}
let name: String = chars[name_start..i].iter().collect();
if name.eq_ignore_ascii_case("hover") {
if i < chars.len() && chars[i] == '(' {
return Err(SelectorError::HoverTakesNoArguments);
}
out.push(Selector::PseudoClass(PseudoClass::Hover));
} else if name.eq_ignore_ascii_case("focus") {
if i < chars.len() && chars[i] == '(' {
return Err(SelectorError::FocusTakesNoArguments);
}
out.push(Selector::PseudoClass(PseudoClass::Focus));
} else if name.eq_ignore_ascii_case("nth-child") {
if i >= chars.len() || chars[i] != '(' {
return Err(SelectorError::NthChildRequiresParens);
}
i += 1;
let arg_start = i;
while i < chars.len() && chars[i] != ')' {
i += 1;
}
if i >= chars.len() {
return Err(SelectorError::NthChildMissingCloseParen);
}
let arg: String = chars[arg_start..i].iter().collect();
i += 1; let expr = parse_nth_child_arg(&arg)
.ok_or_else(|| SelectorError::InvalidNthChildArg(arg.clone()))?;
out.push(Selector::NthChild(expr));
} else {
return Err(SelectorError::UnsupportedPseudoClass(name));
}
}
_ => {
let start = i;
while i < chars.len() && 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 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()))
}