use crate::error::{CssError, Result};
use crate::node::{Classes, Position, State, StyledNode};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PseudoClass {
Focus,
Hover,
Disabled,
Checked,
Active,
}
impl PseudoClass {
fn parse(s: &str) -> Option<Self> {
Some(match s.to_ascii_lowercase().as_str() {
"focus" => Self::Focus,
"hover" => Self::Hover,
"disabled" => Self::Disabled,
"checked" => Self::Checked,
"active" => Self::Active,
_ => return None,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct NthExpr {
pub a: i32,
pub b: i32,
}
impl NthExpr {
pub fn matches(self, i: i32) -> bool {
if self.a == 0 {
i == self.b
} else {
let d = i - self.b;
d % self.a == 0 && d / self.a >= 0
}
}
pub fn parse(s: &str) -> Result<Self> {
parse_nth(s)
}
}
fn parse_nth(input: &str) -> Result<NthExpr> {
let raw = input.trim();
if raw.is_empty() {
return Err(CssError::invalid_selector("empty nth expression"));
}
let lower = raw.to_ascii_lowercase();
if lower == "odd" {
return Ok(NthExpr { a: 2, b: 1 });
}
if lower == "even" {
return Ok(NthExpr { a: 2, b: 0 });
}
let Some(n_pos) = lower.find('n') else {
return parse_bare_int(raw).map(|b| NthExpr { a: 0, b });
};
let rest_after_n = &lower[n_pos + 1..];
if rest_after_n.chars().any(|c| c.is_ascii_alphabetic()) {
return Err(CssError::invalid_selector(format!(
"invalid nth expression `{input}`"
)));
}
let coef_part = raw[..n_pos].trim();
let a = match coef_part {
"" | "+" => 1,
"-" => -1,
_ => parse_bare_int(coef_part)?,
};
let const_part = raw[n_pos + 1..].trim();
let b = if const_part.is_empty() {
0
} else {
parse_bare_int(const_part)?
};
Ok(NthExpr { a, b })
}
fn parse_bare_int(s: &str) -> Result<i32> {
let s = s.trim();
let bytes = s.as_bytes();
let mut i = 0;
let sign = match bytes.first() {
Some(b'+') => {
i += 1;
"+"
}
Some(b'-') => {
i += 1;
"-"
}
_ => "",
};
while i < bytes.len() && bytes[i].is_ascii_whitespace() {
i += 1;
}
if i >= bytes.len() || !bytes[i..].iter().all(|b| b.is_ascii_digit()) {
return Err(CssError::invalid_selector(format!(
"invalid integer `{s}` in nth expression"
)));
}
let digits = &s[i..];
let cleaned: String = sign.chars().chain(digits.chars()).collect();
cleaned
.parse::<i32>()
.map_err(|_| CssError::invalid_selector(format!("nth integer out of range: `{s}`")))
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Pseudo {
State(PseudoClass),
FirstChild,
LastChild,
OnlyChild,
NthChild(NthExpr),
NthLastChild(NthExpr),
NthOfType(NthExpr),
FirstOfType,
LastOfType,
OnlyOfType,
NthLastOfType(NthExpr),
}
impl Pseudo {
fn parse(token: &str) -> Result<Self> {
if let Some(open) = token.find('(') {
let name = token[..open].trim();
if !token.ends_with(')') {
return Err(CssError::invalid_selector(format!(
"unterminated pseudo-class `{token}`"
)));
}
let inner = &token[open + 1..token.len() - 1];
return match name.to_ascii_lowercase().as_str() {
"nth-child" => Ok(Self::NthChild(parse_nth(inner)?)),
"nth-last-child" => Ok(Self::NthLastChild(parse_nth(inner)?)),
"nth-of-type" => Ok(Self::NthOfType(parse_nth(inner)?)),
"nth-last-of-type" => Ok(Self::NthLastOfType(parse_nth(inner)?)),
other => Err(CssError::invalid_selector(format!(
"unsupported pseudo-class `:{other}`"
))),
};
}
match token.to_ascii_lowercase().as_str() {
"first-child" => Ok(Self::FirstChild),
"last-child" => Ok(Self::LastChild),
"only-child" => Ok(Self::OnlyChild),
"first-of-type" => Ok(Self::FirstOfType),
"last-of-type" => Ok(Self::LastOfType),
"only-of-type" => Ok(Self::OnlyOfType),
other => match PseudoClass::parse(other) {
Some(p) => Ok(Self::State(p)),
None => Err(CssError::invalid_selector(format!(
"unsupported pseudo-class `:{other}`"
))),
},
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Combinator {
Descendant,
Child,
Adjacent,
Sibling,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Selector {
pub type_name: Option<String>,
pub classes: Vec<String>,
pub id: Option<String>,
pub pseudos: Vec<Pseudo>,
pub ancestor: Option<(Combinator, Box<Selector>)>,
}
impl Default for Selector {
fn default() -> Self {
Self::universal()
}
}
#[derive(Clone, Debug, Default)]
pub(crate) struct NodeIdentity {
pub type_name: String,
pub id: Option<String>,
pub classes: Vec<String>,
pub state: State,
pub position: Position,
}
impl NodeIdentity {
pub(crate) fn from_node(node: &dyn StyledNode) -> Self {
Self {
type_name: node.type_name().to_string(),
id: node.id().map(str::to_string),
classes: node.classes().as_slice().iter().map(|s| (*s).to_string()).collect(),
state: node.state(),
position: node.position(),
}
}
}
impl Selector {
pub fn universal() -> Self {
Self {
type_name: None,
classes: Vec::new(),
id: None,
pseudos: Vec::new(),
ancestor: None,
}
}
pub fn parse_list(s: &str) -> Result<Vec<Self>> {
s.split(',').map(|part| Self::parse_chain(part.trim())).collect()
}
pub fn parse_compound(s: &str) -> Result<Self> {
Self::parse_compound_into(s)
}
pub fn parse_chain(s: &str) -> Result<Self> {
let s = s.trim();
if s.is_empty() {
return Err(CssError::invalid_selector("empty selector"));
}
let tokens = tokenize_chain(s)?;
let mut iter = tokens.into_iter();
let first_compound = match iter.next() {
Some(ChainToken::Compound(c)) => c,
Some(ChainToken::Combinator(_)) => {
return Err(CssError::invalid_selector(format!(
"selector `{s}` begins with a combinator"
)));
}
None => return Err(CssError::invalid_selector("empty selector")),
};
let mut subject = Self::parse_compound_into(&first_compound)?;
while let Some(comb) = iter.next() {
let combinator = match comb {
ChainToken::Combinator(c) => c,
ChainToken::Compound(_) => unreachable!("non-first compound without combinator"),
};
let Some(ChainToken::Compound(c)) = iter.next() else {
return Err(CssError::invalid_selector(format!(
"selector `{s}` ends with a combinator"
)));
};
let new_subject = Self::parse_compound_into(&c)?;
subject = Self {
type_name: new_subject.type_name,
classes: new_subject.classes,
id: new_subject.id,
pseudos: new_subject.pseudos,
ancestor: Some((combinator, Box::new(subject))),
};
}
Ok(subject)
}
fn parse_compound_into(s: &str) -> Result<Self> {
let s = s.trim();
if s.is_empty() {
return Err(CssError::invalid_selector("empty selector"));
}
let mut sel = Self::universal();
let bytes = s.as_bytes();
let len = s.len();
let mut idx = 0usize;
if let Some(&c) = bytes.first() {
if c == b'*' {
idx += 1;
} else if !matches!(c, b'.' | b'#' | b':') {
let start = idx;
while idx < len {
let c = bytes[idx];
if matches!(c, b'.' | b'#' | b':') {
break;
}
idx += 1;
}
sel.type_name = Some(s[start..idx].to_string());
}
}
while idx < len {
let delim = bytes[idx] as char;
idx += 1;
let start = idx;
if delim == ':' {
while idx < len && !matches!(bytes[idx], b'.' | b'#' | b':' | b'(') {
idx += 1;
}
if idx < len && bytes[idx] == b'(' {
idx += 1; while idx < len && bytes[idx] != b')' {
idx += 1;
}
if idx >= len {
return Err(CssError::invalid_selector(format!(
"unterminated pseudo-class in `{s}`"
)));
}
idx += 1; }
} else {
while idx < len && !matches!(bytes[idx], b'.' | b'#' | b':') {
idx += 1;
}
}
if idx == start {
return Err(CssError::invalid_selector(format!(
"selector `{s}` has a dangling `{delim}`"
)));
}
let token = &s[start..idx];
match delim {
'.' => sel.classes.push(token.to_string()),
'#' => {
if sel.id.is_some() {
return Err(CssError::invalid_selector(format!(
"selector `{s}` has multiple ids"
)));
}
sel.id = Some(token.to_string());
}
':' => sel.pseudos.push(Pseudo::parse(token)?),
_ => unreachable!("delimiter handled above"),
}
}
Ok(sel)
}
pub fn specificity(&self) -> (u32, u32, u32) {
let ids = if self.id.is_some() { 1 } else { 0 };
let cp = (self.classes.len() + self.pseudos.len()) as u32;
let ty = if self.type_name.is_some() { 1 } else { 0 };
let (a_ids, a_cp, a_ty) = match &self.ancestor {
None => (0u32, 0u32, 0u32),
Some((_, anc)) => anc.specificity(),
};
(ids + a_ids, cp + a_cp, ty + a_ty)
}
pub fn matches(&self, node: &dyn StyledNode) -> bool {
let position = node.position();
self.matches_values(
node.type_name(),
node.id(),
&node.classes(),
node.state(),
&position,
)
}
pub(crate) fn matches_values(
&self,
type_name: &str,
id: Option<&str>,
classes: &Classes<'_>,
state: State,
position: &Position,
) -> bool {
if !self.matches_subject_raw(type_name, id, classes, state, position, 0) {
return false;
}
self.ancestor.is_none()
}
fn matches_subject_raw(
&self,
type_name: &str,
id: Option<&str>,
classes: &Classes<'_>,
state: State,
position: &Position,
same_type_before: i32,
) -> bool {
if let Some(t) = &self.type_name
&& !type_name.eq_ignore_ascii_case(t)
{
return false;
}
if let Some(sel_id) = &self.id
&& id != Some(sel_id.as_str())
{
return false;
}
for c in &self.classes {
if !classes.contains(c.as_str()) {
return false;
}
}
self.pseudos_satisfied(state, position, same_type_before)
}
fn matches_subject(&self, id: &NodeIdentity, siblings: &[NodeIdentity]) -> bool {
let class_strs: Vec<&str> = id.classes.iter().map(String::as_str).collect();
let classes = Classes::from_slice(&class_strs);
let same_type_before = siblings
.iter()
.filter(|s| s.type_name.eq_ignore_ascii_case(&id.type_name))
.count() as i32;
self.matches_subject_raw(
&id.type_name,
id.id.as_deref(),
&classes,
id.state,
&id.position,
same_type_before,
)
}
pub(crate) fn matches_chain(
&self,
node_id: &NodeIdentity,
ancestors: &[NodeIdentity],
siblings: &[NodeIdentity],
) -> bool {
if !self.matches_subject(node_id, siblings) {
return false;
}
match &self.ancestor {
None => true,
Some((Combinator::Child, anc)) => match ancestors.last() {
None => false,
Some(parent) => anc.matches_chain(parent, &ancestors[..ancestors.len() - 1], &[]),
},
Some((Combinator::Descendant, anc)) => {
for i in (0..ancestors.len()).rev() {
if anc.matches_chain(&ancestors[i], &ancestors[..i], &[]) {
return true;
}
}
false
}
Some((Combinator::Adjacent, anc)) => {
let n = siblings.len();
if n == 0 {
return false;
}
let last = &siblings[n - 1];
anc.matches_chain(last, ancestors, &siblings[..n - 1])
}
Some((Combinator::Sibling, anc)) => {
for i in (0..siblings.len()).rev() {
if anc.matches_chain(&siblings[i], ancestors, &siblings[..i]) {
return true;
}
}
false
}
}
}
fn pseudos_satisfied(&self, state: State, position: &Position, same_type_before: i32) -> bool {
for p in &self.pseudos {
let on = match p {
Pseudo::State(PseudoClass::Focus) => state.focus,
Pseudo::State(PseudoClass::Hover) => state.hover,
Pseudo::State(PseudoClass::Disabled) => state.disabled,
Pseudo::State(PseudoClass::Checked) => state.checked,
Pseudo::State(PseudoClass::Active) => state.active,
Pseudo::FirstChild => position.sibling_count > 0 && position.index == 0,
Pseudo::LastChild => {
position.sibling_count > 0 && position.index == position.sibling_count - 1
}
Pseudo::OnlyChild => position.sibling_count == 1,
Pseudo::NthChild(expr) => {
position.sibling_count > 0 && expr.matches(position.index as i32 + 1)
}
Pseudo::NthLastChild(expr) => {
position.sibling_count > 0
&& expr.matches(position.sibling_count as i32 - position.index as i32)
}
Pseudo::NthOfType(expr) => {
if position.of_type_count > 0 {
expr.matches(position.of_type_index as i32 + 1)
} else {
expr.matches(same_type_before + 1)
}
}
Pseudo::FirstOfType => {
if position.of_type_count > 0 {
position.of_type_index == 0
} else {
same_type_before == 0
}
}
Pseudo::LastOfType => {
position.of_type_count > 0
&& position.of_type_index == position.of_type_count - 1
}
Pseudo::OnlyOfType => position.of_type_count == 1,
Pseudo::NthLastOfType(expr) => {
position.of_type_count > 0
&& expr.matches(position.of_type_count as i32 - position.of_type_index as i32)
}
};
if !on {
return false;
}
}
true
}
}
enum ChainToken {
Compound(String),
Combinator(Combinator),
}
fn tokenize_chain(s: &str) -> Result<Vec<ChainToken>> {
let mut tokens: Vec<ChainToken> = Vec::new();
let mut cur = String::new();
let bytes = s.as_bytes();
let mut i = 0;
let mut depth: u32 = 0;
macro_rules! flush {
() => {{
if !cur.is_empty() {
tokens.push(ChainToken::Compound(std::mem::take(&mut cur)));
}
}};
}
while i < bytes.len() {
let b = bytes[i];
if depth == 0 {
match b {
b'(' => {
depth += 1;
cur.push('(');
}
b')' => {
cur.push(')');
}
b' ' | b'\t' | b'\n' | b'\r' => {
let mut j = i;
while j < bytes.len() && bytes[j].is_ascii_whitespace() {
j += 1;
}
i = j;
if i >= bytes.len() {
break;
}
match bytes[i] {
b'>' => {
flush!();
tokens.push(ChainToken::Combinator(Combinator::Child));
i += 1; while i < bytes.len() && bytes[i].is_ascii_whitespace() {
i += 1;
}
continue;
}
b'+' => {
flush!();
tokens.push(ChainToken::Combinator(Combinator::Adjacent));
i += 1;
while i < bytes.len() && bytes[i].is_ascii_whitespace() {
i += 1;
}
continue;
}
b'~' => {
flush!();
tokens.push(ChainToken::Combinator(Combinator::Sibling));
i += 1;
while i < bytes.len() && bytes[i].is_ascii_whitespace() {
i += 1;
}
continue;
}
_ => {
flush!();
tokens.push(ChainToken::Combinator(Combinator::Descendant));
continue;
}
}
}
b'>' => {
flush!();
tokens.push(ChainToken::Combinator(Combinator::Child));
i += 1;
while i < bytes.len() && bytes[i].is_ascii_whitespace() {
i += 1;
}
continue;
}
b'+' => {
flush!();
tokens.push(ChainToken::Combinator(Combinator::Adjacent));
i += 1;
while i < bytes.len() && bytes[i].is_ascii_whitespace() {
i += 1;
}
continue;
}
b'~' => {
flush!();
tokens.push(ChainToken::Combinator(Combinator::Sibling));
i += 1;
while i < bytes.len() && bytes[i].is_ascii_whitespace() {
i += 1;
}
continue;
}
_ => {
let ch = s[i..].chars().next().expect("non-empty slice");
cur.push(ch);
i += ch.len_utf8();
continue;
}
}
} else {
match b {
b'(' => depth += 1,
b')' => depth -= 1,
_ => {}
}
let ch = s[i..].chars().next().expect("non-empty slice");
cur.push(ch);
i += ch.len_utf8();
continue;
}
i += 1;
}
flush!();
Ok(tokens)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::node::{OwnedNode, Position, State};
#[test]
fn parse_compound() {
let s = Selector::parse_compound("Button.primary#save:focus").unwrap();
assert_eq!(s.type_name.as_deref(), Some("Button"));
assert_eq!(s.classes, vec!["primary"]);
assert_eq!(s.id.as_deref(), Some("save"));
assert_eq!(s.pseudos, vec![Pseudo::State(PseudoClass::Focus)]);
assert_eq!(s.specificity(), (1, 2, 1));
}
#[test]
fn universal_specificity() {
assert_eq!(Selector::universal().specificity(), (0, 0, 0));
}
#[test]
fn matching() {
let sel = Selector::parse_compound("Button.primary").unwrap();
let n = OwnedNode::new("Button").with_classes(["primary"]);
assert!(sel.matches(&n));
let wrong_type = OwnedNode::new("Text").with_classes(["primary"]);
assert!(!sel.matches(&wrong_type));
let missing_class = OwnedNode::new("Button");
assert!(!sel.matches(&missing_class));
}
#[test]
fn matching_with_state() {
let sel = Selector::parse_compound("Button:disabled").unwrap();
let on = OwnedNode::new("Button").with_state(State::disabled());
let off = OwnedNode::new("Button");
assert!(sel.matches(&on));
assert!(!sel.matches(&off));
}
#[test]
fn comma_list() {
let list = Selector::parse_list("Text, .muted, #title").unwrap();
assert_eq!(list.len(), 3);
}
#[test]
fn nth_matches_odd_even() {
let odd = NthExpr { a: 2, b: 1 };
let even = NthExpr { a: 2, b: 0 };
for i in 1..=6 {
assert_eq!(odd.matches(i), i % 2 == 1, "odd @ {i}");
assert_eq!(even.matches(i), i % 2 == 0, "even @ {i}");
}
}
#[test]
fn nth_matches_2n_plus_1() {
let e = NthExpr { a: 2, b: 1 };
for i in 1..=6 {
assert_eq!(e.matches(i), i % 2 == 1, "2n+1 @ {i}");
}
}
#[test]
fn nth_matches_minus_n_plus_2() {
let e = NthExpr { a: -1, b: 2 };
assert!(e.matches(1));
assert!(e.matches(2));
for i in 3..=6 {
assert!(!e.matches(i), "-n+2 should not match {i}");
}
}
#[test]
fn nth_matches_bare_int() {
let e = NthExpr { a: 0, b: 3 };
assert!(e.matches(3));
assert!(!e.matches(1));
assert!(!e.matches(2));
assert!(!e.matches(4));
}
#[test]
fn nth_parse_keywords() {
assert_eq!(NthExpr::parse("odd").unwrap(), NthExpr { a: 2, b: 1 });
assert_eq!(NthExpr::parse("even").unwrap(), NthExpr { a: 2, b: 0 });
assert_eq!(NthExpr::parse("ODD").unwrap(), NthExpr { a: 2, b: 1 });
assert_eq!(NthExpr::parse("Even").unwrap(), NthExpr { a: 2, b: 0 });
}
#[test]
fn nth_parse_bare_int() {
assert_eq!(NthExpr::parse("3").unwrap(), NthExpr { a: 0, b: 3 });
assert_eq!(NthExpr::parse("+3").unwrap(), NthExpr { a: 0, b: 3 });
assert_eq!(NthExpr::parse("-3").unwrap(), NthExpr { a: 0, b: -3 });
}
#[test]
fn nth_parse_n_forms() {
assert_eq!(NthExpr::parse("n").unwrap(), NthExpr { a: 1, b: 0 });
assert_eq!(NthExpr::parse("2n").unwrap(), NthExpr { a: 2, b: 0 });
assert_eq!(NthExpr::parse("-3n").unwrap(), NthExpr { a: -3, b: 0 });
assert_eq!(NthExpr::parse("-n").unwrap(), NthExpr { a: -1, b: 0 });
}
#[test]
fn nth_parse_an_plus_b_forms() {
assert_eq!(NthExpr::parse("2n+1").unwrap(), NthExpr { a: 2, b: 1 });
assert_eq!(NthExpr::parse("2n-1").unwrap(), NthExpr { a: 2, b: -1 });
assert_eq!(NthExpr::parse("-n+2").unwrap(), NthExpr { a: -1, b: 2 });
assert_eq!(NthExpr::parse("-2n+3").unwrap(), NthExpr { a: -2, b: 3 });
assert_eq!(NthExpr::parse("2n + 1").unwrap(), NthExpr { a: 2, b: 1 });
assert_eq!(NthExpr::parse(" -n + 2 ").unwrap(), NthExpr { a: -1, b: 2 });
}
#[test]
fn nth_parse_garbage_errors() {
assert!(NthExpr::parse("").is_err());
assert!(NthExpr::parse("abc").is_err());
assert!(NthExpr::parse("2x").is_err());
assert!(NthExpr::parse("n+").is_err());
assert!(NthExpr::parse("2n+").is_err());
assert!(NthExpr::parse("--").is_err());
assert!(NthExpr::parse("2n+1x").is_err());
assert!(NthExpr::parse("+").is_err());
}
#[test]
fn parse_nth_child_selector() {
let s = Selector::parse_compound("Item:nth-child(2n+1)").unwrap();
assert_eq!(s.type_name.as_deref(), Some("Item"));
assert_eq!(s.pseudos.len(), 1);
assert_eq!(s.pseudos[0], Pseudo::NthChild(NthExpr { a: 2, b: 1 }));
}
#[test]
fn parse_first_child_selector() {
let s = Selector::parse_compound("tr:first-child").unwrap();
assert_eq!(s.pseudos, vec![Pseudo::FirstChild]);
}
#[test]
fn parse_last_child_selector() {
let s = Selector::parse_compound("li:last-child").unwrap();
assert_eq!(s.pseudos, vec![Pseudo::LastChild]);
}
#[test]
fn parse_only_child_selector() {
let s = Selector::parse_compound("td:only-child").unwrap();
assert_eq!(s.pseudos, vec![Pseudo::OnlyChild]);
}
#[test]
fn parse_nth_last_child_selector() {
let s = Selector::parse_compound("tr:nth-last-child(odd)").unwrap();
assert_eq!(
s.pseudos,
vec![Pseudo::NthLastChild(NthExpr { a: 2, b: 1 })]
);
}
#[test]
fn nth_child_specificity_counts_as_one() {
let s = Selector::parse_compound(":nth-child(2n+1)").unwrap();
assert_eq!(s.specificity(), (0, 1, 0));
}
#[test]
fn unknown_pseudo_errors() {
assert!(Selector::parse_compound("a:visited").is_err());
assert!(Selector::parse_compound("a:nth-of-foo(2)").is_err());
assert!(Selector::parse_compound("a:empty").is_err());
}
#[test]
fn unterminated_pseudo_errors() {
assert!(Selector::parse_compound("Item:nth-child(2n+1").is_err());
}
fn pos(index: usize, count: usize) -> Position {
Position::new(index, count)
}
#[test]
fn first_child_matches() {
let sel = Selector::parse_compound("Item:first-child").unwrap();
let classes = Classes::from_slice(&[]);
assert!(sel.matches_values("Item", None, &classes, State::empty(), &pos(0, 3)));
assert!(!sel.matches_values("Item", None, &classes, State::empty(), &pos(1, 3)));
}
#[test]
fn last_child_matches() {
let sel = Selector::parse_compound("Item:last-child").unwrap();
let classes = Classes::from_slice(&[]);
assert!(sel.matches_values("Item", None, &classes, State::empty(), &pos(2, 3)));
assert!(!sel.matches_values("Item", None, &classes, State::empty(), &pos(1, 3)));
}
#[test]
fn only_child_matches() {
let sel = Selector::parse_compound("Item:only-child").unwrap();
let classes = Classes::from_slice(&[]);
assert!(sel.matches_values("Item", None, &classes, State::empty(), &pos(0, 1)));
assert!(!sel.matches_values("Item", None, &classes, State::empty(), &pos(0, 3)));
}
#[test]
fn nth_child_matches() {
let sel = Selector::parse_compound("Item:nth-child(odd)").unwrap();
let classes = Classes::from_slice(&[]);
assert!(sel.matches_values("Item", None, &classes, State::empty(), &pos(0, 3)));
assert!(!sel.matches_values("Item", None, &classes, State::empty(), &pos(1, 3)));
assert!(sel.matches_values("Item", None, &classes, State::empty(), &pos(2, 3)));
}
#[test]
fn nth_last_child_matches() {
let sel = Selector::parse_compound("Item:nth-last-child(1)").unwrap();
let classes = Classes::from_slice(&[]);
assert!(sel.matches_values("Item", None, &classes, State::empty(), &pos(2, 3)));
assert!(!sel.matches_values("Item", None, &classes, State::empty(), &pos(1, 3)));
}
#[test]
fn default_position_does_not_match_structural() {
let sel = Selector::parse_compound("Item:first-child").unwrap();
let classes = Classes::from_slice(&[]);
let default = Position::default();
assert_eq!(default.sibling_count, 0);
assert_eq!(default.index, 0);
assert!(!sel.matches_values("Item", None, &classes, State::empty(), &default));
let only = Selector::parse_compound("Item:only-child").unwrap();
assert!(!only.matches_values("Item", None, &classes, State::empty(), &default));
let nth = Selector::parse_compound("Item:nth-child(1)").unwrap();
assert!(!nth.matches_values("Item", None, &classes, State::empty(), &default));
}
#[test]
fn structural_matching_via_owned_node() {
let sel = Selector::parse_compound("Item:first-child").unwrap();
let first = OwnedNode::new("Item").with_position(Position::new(0, 3));
let second = OwnedNode::new("Item").with_position(Position::new(1, 3));
assert!(sel.matches(&first));
assert!(!sel.matches(&second));
}
#[test]
fn parse_descendant_chain() {
let s = Selector::parse_chain("Panel Button").unwrap();
assert_eq!(s.type_name.as_deref(), Some("Button"));
assert!(s.classes.is_empty());
let (comb, anc) = s.ancestor.as_ref().expect("ancestor present");
assert_eq!(*comb, Combinator::Descendant);
assert_eq!(anc.type_name.as_deref(), Some("Panel"));
assert!(anc.ancestor.is_none());
assert_eq!(s.specificity(), (0, 0, 2));
}
#[test]
fn parse_child_chain() {
let s = Selector::parse_chain("Panel > Button").unwrap();
assert_eq!(s.type_name.as_deref(), Some("Button"));
let (comb, anc) = s.ancestor.as_ref().expect("ancestor present");
assert_eq!(*comb, Combinator::Child);
assert_eq!(anc.type_name.as_deref(), Some("Panel"));
}
#[test]
fn parse_child_chain_with_rich_subject() {
let s = Selector::parse_chain("Panel > Button.primary#save:focus:nth-child(2n+1)").unwrap();
assert_eq!(s.type_name.as_deref(), Some("Button"));
assert_eq!(s.classes, vec!["primary"]);
assert_eq!(s.id.as_deref(), Some("save"));
assert_eq!(s.pseudos.len(), 2);
assert_eq!(s.pseudos[0], Pseudo::State(PseudoClass::Focus));
assert_eq!(s.pseudos[1], Pseudo::NthChild(NthExpr { a: 2, b: 1 }));
let (comb, anc) = s.ancestor.as_ref().expect("ancestor present");
assert_eq!(*comb, Combinator::Child);
assert_eq!(anc.type_name.as_deref(), Some("Panel"));
}
#[test]
fn parse_three_compound_chain() {
let s = Selector::parse_chain("A B C").unwrap();
assert_eq!(s.type_name.as_deref(), Some("C"));
let (comb_b, anc_b) = s.ancestor.as_ref().expect("ancestor B");
assert_eq!(*comb_b, Combinator::Descendant);
assert_eq!(anc_b.type_name.as_deref(), Some("B"));
let (comb_a, anc_a) = anc_b.ancestor.as_ref().expect("ancestor A");
assert_eq!(*comb_a, Combinator::Descendant);
assert_eq!(anc_a.type_name.as_deref(), Some("A"));
assert!(anc_a.ancestor.is_none());
}
#[test]
fn parse_combinator_preserves_nth_paren_spaces() {
let s = Selector::parse_chain("List Item:nth-child(2n + 1)").unwrap();
assert_eq!(s.type_name.as_deref(), Some("Item"));
assert_eq!(s.pseudos.len(), 1);
assert_eq!(s.pseudos[0], Pseudo::NthChild(NthExpr { a: 2, b: 1 }));
let (comb, anc) = s.ancestor.as_ref().expect("ancestor");
assert_eq!(*comb, Combinator::Descendant);
assert_eq!(anc.type_name.as_deref(), Some("List"));
}
#[test]
fn parse_adjacent_sibling_chain() {
let s = Selector::parse_chain("Label + Input").unwrap();
assert_eq!(s.type_name.as_deref(), Some("Input"));
let (comb, anc) = s.ancestor.as_ref().expect("ancestor present");
assert_eq!(*comb, Combinator::Adjacent);
assert_eq!(anc.type_name.as_deref(), Some("Label"));
assert!(anc.ancestor.is_none());
assert_eq!(s.specificity(), (0, 0, 2));
}
#[test]
fn parse_general_sibling_chain() {
let s = Selector::parse_chain("Label ~ Input").unwrap();
assert_eq!(s.type_name.as_deref(), Some("Input"));
let (comb, anc) = s.ancestor.as_ref().expect("ancestor present");
assert_eq!(*comb, Combinator::Sibling);
assert_eq!(anc.type_name.as_deref(), Some("Label"));
}
#[test]
fn sibling_specificity_matches_descendant() {
let desc = Selector::parse_chain("Label Input").unwrap();
let adj = Selector::parse_chain("Label + Input").unwrap();
let sib = Selector::parse_chain("Label ~ Input").unwrap();
assert_eq!(desc.specificity(), adj.specificity());
assert_eq!(desc.specificity(), sib.specificity());
}
#[test]
fn sibling_combinator_inside_parens_is_not_a_combinator() {
let s = Selector::parse_chain("Item:nth-child(2n + 1) + Item").unwrap();
assert_eq!(s.type_name.as_deref(), Some("Item"));
assert!(s.classes.is_empty());
let (comb, anc) = s.ancestor.as_ref().expect("ancestor present");
assert_eq!(*comb, Combinator::Adjacent);
assert_eq!(anc.type_name.as_deref(), Some("Item"));
assert_eq!(anc.pseudos.len(), 1);
assert_eq!(anc.pseudos[0], Pseudo::NthChild(NthExpr { a: 2, b: 1 }));
}
#[test]
fn sibling_combinator_with_descendant_ancestor() {
let s = Selector::parse_chain("Panel Label + Input").unwrap();
assert_eq!(s.type_name.as_deref(), Some("Input"));
let (comb_label, anc_label) = s.ancestor.as_ref().expect("ancestor Label");
assert_eq!(*comb_label, Combinator::Adjacent);
assert_eq!(anc_label.type_name.as_deref(), Some("Label"));
let (comb_panel, anc_panel) = anc_label.ancestor.as_ref().expect("ancestor Panel");
assert_eq!(*comb_panel, Combinator::Descendant);
assert_eq!(anc_panel.type_name.as_deref(), Some("Panel"));
}
fn nid(type_name: &str) -> NodeIdentity {
NodeIdentity {
type_name: type_name.to_string(),
id: None,
classes: Vec::new(),
state: State::empty(),
position: Position::default(),
}
}
#[test]
fn adjacent_matches_when_last_sibling_is_label() {
let sel = Selector::parse_chain("Label + Input").unwrap();
let input = nid("Input");
let label = nid("Label");
assert!(sel.matches_chain(&input, &[], std::slice::from_ref(&label)));
}
#[test]
fn adjacent_does_not_match_empty_siblings() {
let sel = Selector::parse_chain("Label + Input").unwrap();
let input = nid("Input");
assert!(!sel.matches_chain(&input, &[], &[]));
}
#[test]
fn adjacent_does_not_match_when_last_sibling_is_not_label() {
let sel = Selector::parse_chain("Label + Input").unwrap();
let input = nid("Input");
let span = nid("Span");
assert!(!sel.matches_chain(&input, &[], &[span]));
}
#[test]
fn adjacent_checks_only_immediate_sibling() {
let sel = Selector::parse_chain("Label + Input").unwrap();
let input = nid("Input");
let label = nid("Label");
let span = nid("Span");
assert!(!sel.matches_chain(&input, &[], &[label, span]));
}
#[test]
fn general_sibling_matches_when_some_prior_sibling_is_label() {
let sel = Selector::parse_chain("Label ~ Input").unwrap();
let input = nid("Input");
let label = nid("Label");
let span = nid("Span");
assert!(sel.matches_chain(&input, &[], &[label.clone(), span.clone()]));
assert!(sel.matches_chain(&input, &[], &[span, label]));
}
#[test]
fn general_sibling_does_not_match_when_no_label_among_prior() {
let sel = Selector::parse_chain("Label ~ Input").unwrap();
let input = nid("Input");
let span = nid("Span");
assert!(!sel.matches_chain(&input, &[], &[span]));
assert!(!sel.matches_chain(&input, &[], &[]));
}
#[test]
fn specificity_of_child_chain_sums() {
let s = Selector::parse_chain("Panel > Button.primary").unwrap();
assert_eq!(s.specificity(), (0, 1, 2));
}
#[test]
fn comma_list_with_combinators() {
let list = Selector::parse_list("Panel Button, .modal > Button").unwrap();
assert_eq!(list.len(), 2);
assert_eq!(list[0].type_name.as_deref(), Some("Button"));
assert_eq!(list[1].type_name.as_deref(), Some("Button"));
let (c0, _) = list[0].ancestor.as_ref().expect("anc0");
assert_eq!(*c0, Combinator::Descendant);
let (c1, _) = list[1].ancestor.as_ref().expect("anc1");
assert_eq!(*c1, Combinator::Child);
}
#[test]
fn nested_adjacent_chain_matches() {
let sel = Selector::parse_chain("Label + Input + Button").unwrap();
assert_eq!(sel.type_name.as_deref(), Some("Button"));
let label = nid("Label");
let input = nid("Input");
let button = nid("Button");
assert!(sel.matches_chain(&button, &[], &[label.clone(), input.clone()]));
let span = nid("Span");
assert!(!sel.matches_chain(&button, &[], &[span, input]));
assert!(!sel.matches_chain(&button, &[], std::slice::from_ref(&label)));
}
#[test]
fn nested_general_sibling_chain() {
let sel = Selector::parse_chain("A ~ B ~ C").unwrap();
assert_eq!(sel.type_name.as_deref(), Some("C"));
let a = nid("A");
let b = nid("B");
let c = nid("C");
let x = nid("X");
assert!(sel.matches_chain(&c, &[], &[a.clone(), x.clone(), b.clone()]));
assert!(!sel.matches_chain(&c, &[], &[b, a.clone()]));
assert!(!sel.matches_chain(&c, &[], &[a, x]));
}
#[test]
fn mixed_adjacent_then_general_sibling_chain() {
let sel = Selector::parse_chain("A + B ~ C").unwrap();
assert_eq!(sel.type_name.as_deref(), Some("C"));
let a = nid("A");
let b = nid("B");
let c = nid("C");
let x = nid("X");
assert!(sel.matches_chain(&c, &[], &[a.clone(), b.clone(), x.clone()]));
assert!(sel.matches_chain(&c, &[], &[x, a, b]));
let x2 = nid("X");
let b2 = nid("B");
assert!(!sel.matches_chain(&c, &[], &[x2, b2]));
}
#[test]
fn mixed_sibling_and_descendant() {
let sel = Selector::parse_chain("Panel Item + Item").unwrap();
assert_eq!(sel.type_name.as_deref(), Some("Item"));
let panel = nid("Panel");
let item1 = nid("Item");
let item2 = nid("Item");
assert!(sel.matches_chain(&item2, std::slice::from_ref(&panel), std::slice::from_ref(&item1)));
let other = nid("Other");
assert!(!sel.matches_chain(&item2, std::slice::from_ref(&other), &[item1]));
}
#[test]
fn single_adjacent_still_matches() {
let sel = Selector::parse_chain("Label + Input").unwrap();
let label = nid("Label");
let input = nid("Input");
assert!(sel.matches_chain(&input, &[], std::slice::from_ref(&label)));
assert!(!sel.matches_chain(&input, &[], &[]));
}
#[test]
fn parse_nth_of_type() {
let s = Selector::parse_compound("Item:nth-of-type(2n+1)").unwrap();
assert_eq!(s.type_name.as_deref(), Some("Item"));
assert_eq!(s.pseudos.len(), 1);
assert_eq!(s.pseudos[0], Pseudo::NthOfType(NthExpr { a: 2, b: 1 }));
}
#[test]
fn parse_first_of_type() {
let s = Selector::parse_compound("Item:first-of-type").unwrap();
assert_eq!(s.pseudos, vec![Pseudo::FirstOfType]);
}
#[test]
fn nth_of_type_counts_same_type_only() {
let div = nid("Div");
let item = nid("Item");
let second_item = nid("Item");
let siblings = [div.clone(), item.clone(), div.clone()];
let nth2 = Selector::parse_compound("Item:nth-of-type(2)").unwrap();
assert!(nth2.matches_chain(&second_item, &[], &siblings));
let nth1 = Selector::parse_compound("Item:nth-of-type(1)").unwrap();
assert!(!nth1.matches_chain(&second_item, &[], &siblings));
let second_div = nid("Div");
let div_siblings = [div.clone(), item.clone()];
let div_nth1 = Selector::parse_compound("Div:nth-of-type(1)").unwrap();
let div_nth2 = Selector::parse_compound("Div:nth-of-type(2)").unwrap();
assert!(!div_nth1.matches_chain(&second_div, &[], &div_siblings));
assert!(div_nth2.matches_chain(&second_div, &[], &div_siblings));
let first_div = nid("Div");
let first_siblings = [item.clone(), item.clone()]; assert!(div_nth1.matches_chain(&first_div, &[], &first_siblings));
}
#[test]
fn first_of_type_matches_first_same_type() {
let item = nid("Item");
let div = nid("Div");
let second_item = nid("Item");
let sel = Selector::parse_compound("Item:first-of-type").unwrap();
assert!(sel.matches_chain(&item, &[], std::slice::from_ref(&div)));
assert!(!sel.matches_chain(&second_item, &[], std::slice::from_ref(&item)));
assert!(sel.matches_chain(&item, &[], &[div.clone(), div.clone()]));
}
#[test]
fn nth_of_type_specificity_counts_as_one() {
let s = Selector::parse_compound(":nth-of-type(2n+1)").unwrap();
assert_eq!(s.specificity(), (0, 1, 0));
let s2 = Selector::parse_compound(":first-of-type").unwrap();
assert_eq!(s2.specificity(), (0, 1, 0));
}
#[test]
fn nth_of_type_does_not_match_on_oneshot_path() {
let nth2 = Selector::parse_compound("Item:nth-of-type(2)").unwrap();
let classes = Classes::from_slice(&[]);
assert!(!nth2.matches_values("Item", None, &classes, State::empty(), &pos(1, 3)));
let first = Selector::parse_compound("Item:first-of-type").unwrap();
assert!(first.matches_values("Item", None, &classes, State::empty(), &pos(0, 3)));
let nth1 = Selector::parse_compound("Item:nth-of-type(1)").unwrap();
assert!(nth1.matches_values("Item", None, &classes, State::empty(), &pos(0, 3)));
}
#[test]
fn parse_last_of_type() {
let s = Selector::parse_compound("Item:last-of-type").unwrap();
assert_eq!(s.type_name.as_deref(), Some("Item"));
assert_eq!(s.pseudos, vec![Pseudo::LastOfType]);
}
#[test]
fn parse_only_of_type() {
let s = Selector::parse_compound("Item:only-of-type").unwrap();
assert_eq!(s.pseudos, vec![Pseudo::OnlyOfType]);
}
#[test]
fn parse_nth_last_of_type() {
let s = Selector::parse_compound("Item:nth-last-of-type(2n+1)").unwrap();
assert_eq!(s.type_name.as_deref(), Some("Item"));
assert_eq!(s.pseudos.len(), 1);
assert_eq!(s.pseudos[0], Pseudo::NthLastOfType(NthExpr { a: 2, b: 1 }));
}
#[test]
fn of_type_pseudos_specificity_counts_as_one() {
assert_eq!(Selector::parse_compound(":last-of-type").unwrap().specificity(), (0, 1, 0));
assert_eq!(Selector::parse_compound(":only-of-type").unwrap().specificity(), (0, 1, 0));
assert_eq!(
Selector::parse_compound(":nth-last-of-type(2n+1)").unwrap().specificity(),
(0, 1, 0)
);
}
fn nid_of_type(type_name: &str, of_type_index: usize, of_type_count: usize) -> NodeIdentity {
NodeIdentity {
type_name: type_name.to_string(),
id: None,
classes: Vec::new(),
state: State::empty(),
position: Position::default().with_of_type(of_type_index, of_type_count),
}
}
#[test]
fn last_of_type_matches_last_same_type() {
let sel = Selector::parse_compound("Item:last-of-type").unwrap();
let last = nid_of_type("Item", 2, 3);
assert!(sel.matches_chain(&last, &[], &[]));
let middle = nid_of_type("Item", 1, 3);
assert!(!sel.matches_chain(&middle, &[], &[]));
let sole = nid_of_type("Item", 0, 1);
assert!(sel.matches_chain(&sole, &[], &[]));
}
#[test]
fn only_of_type_matches_when_count_is_one() {
let sel = Selector::parse_compound("Item:only-of-type").unwrap();
assert!(sel.matches_chain(&nid_of_type("Item", 0, 1), &[], &[]));
assert!(!sel.matches_chain(&nid_of_type("Item", 1, 3), &[], &[]));
assert!(!sel.matches_chain(&nid_of_type("Item", 0, 3), &[], &[]));
}
#[test]
fn nth_last_of_type_counts_from_end() {
let first_of_three = nid_of_type("Item", 0, 3);
let nl3 = Selector::parse_compound("Item:nth-last-of-type(3)").unwrap();
assert!(nl3.matches_chain(&first_of_three, &[], &[]));
let nl1 = Selector::parse_compound("Item:nth-last-of-type(1)").unwrap();
assert!(!nl1.matches_chain(&first_of_three, &[], &[]));
let last_of_three = nid_of_type("Item", 2, 3);
assert!(nl1.matches_chain(&last_of_three, &[], &[]));
assert!(!nl3.matches_chain(&last_of_three, &[], &[]));
let nl_odd = Selector::parse_compound("Item:nth-last-of-type(odd)").unwrap();
assert!(nl_odd.matches_chain(&nid_of_type("Item", 1, 4), &[], &[]));
assert!(!nl_odd.matches_chain(&nid_of_type("Item", 0, 4), &[], &[]));
}
#[test]
fn last_of_type_unknown_count_does_not_match() {
let default = nid("Item"); assert_eq!(default.position.of_type_count, 0);
let last = Selector::parse_compound("Item:last-of-type").unwrap();
assert!(!last.matches_chain(&default, &[], &[]));
let only = Selector::parse_compound("Item:only-of-type").unwrap();
assert!(!only.matches_chain(&default, &[], &[]));
let nl1 = Selector::parse_compound("Item:nth-last-of-type(1)").unwrap();
assert!(!nl1.matches_chain(&default, &[], &[]));
}
#[test]
fn nth_of_type_prefers_host_of_type_info() {
let host_wins = nid_of_type("Item", 1, 2);
let nth2 = Selector::parse_compound("Item:nth-of-type(2)").unwrap();
let nth1 = Selector::parse_compound("Item:nth-of-type(1)").unwrap();
assert!(nth2.matches_chain(&host_wins, &[], &[]));
assert!(!nth1.matches_chain(&host_wins, &[], &[]));
let first = Selector::parse_compound("Item:first-of-type").unwrap();
assert!(!first.matches_chain(&host_wins, &[], &[]));
let host_first = nid_of_type("Item", 0, 3);
assert!(first.matches_chain(&host_first, &[], &[]));
let consistent = nid_of_type("Item", 0, 1);
assert!(nth1.matches_chain(&consistent, &[], &[]));
assert!(first.matches_chain(&consistent, &[], &[]));
}
#[test]
fn nth_of_type_still_uses_prev_siblings_when_no_host_info() {
let div = nid("Div");
let item = nid("Item");
let second_item = nid("Item");
let siblings = [div.clone(), item.clone(), div.clone()];
let nth2 = Selector::parse_compound("Item:nth-of-type(2)").unwrap();
assert!(nth2.matches_chain(&second_item, &[], &siblings));
let nth1 = Selector::parse_compound("Item:nth-of-type(1)").unwrap();
assert!(!nth1.matches_chain(&second_item, &[], &siblings));
let first = Selector::parse_compound("Item:first-of-type").unwrap();
assert!(!first.matches_chain(&second_item, &[], &siblings));
assert!(first.matches_chain(&item, &[], std::slice::from_ref(&div)));
}
}