use super::types::{
AttributeOp, AttributeSelector, Combinator, NthExpr, PseudoClass, Selector, SelectorParseError,
SelectorPart,
};
const MAX_IDENTIFIER_LENGTH: usize = 256;
pub fn parse_selector(input: &str) -> Result<Selector, SelectorParseError> {
let mut parser = SelectorParser::new(input);
parser.parse_selector()
}
pub fn parse_selectors(input: &str) -> Result<Vec<Selector>, SelectorParseError> {
let mut selectors = Vec::new();
for part in input.split(',') {
let trimmed = part.trim();
if !trimmed.is_empty() {
selectors.push(parse_selector(trimmed)?);
}
}
Ok(selectors)
}
struct SelectorParser<'a> {
_input: &'a str,
chars: Vec<char>,
pos: usize,
}
impl<'a> SelectorParser<'a> {
fn new(input: &'a str) -> Self {
Self {
_input: input,
chars: input.chars().collect(),
pos: 0,
}
}
fn peek(&self) -> Option<char> {
self.chars.get(self.pos).copied()
}
fn advance(&mut self) -> Option<char> {
let ch = self.peek();
if ch.is_some() {
self.pos += 1;
}
ch
}
fn skip_whitespace(&mut self) {
while let Some(ch) = self.peek() {
if ch.is_whitespace() {
self.advance();
} else {
break;
}
}
}
fn parse_identifier(&mut self) -> Result<String, SelectorParseError> {
let mut ident = String::new();
while let Some(ch) = self.peek() {
if ch.is_alphanumeric() || ch == '-' || ch == '_' {
ident.push(ch);
self.advance();
if ident.len() > MAX_IDENTIFIER_LENGTH {
return Err(SelectorParseError {
message: format!(
"Identifier exceeds maximum length of {}",
MAX_IDENTIFIER_LENGTH
),
position: self.pos,
});
}
} else {
break;
}
}
Ok(ident)
}
fn parse_selector(&mut self) -> Result<Selector, SelectorParseError> {
let mut parts = Vec::new();
loop {
self.skip_whitespace();
if self.peek().is_none() {
break;
}
let combinator = if !parts.is_empty() {
self.parse_combinator()
} else {
None
};
self.skip_whitespace();
let part = self.parse_selector_part()?;
if part.is_empty() && combinator.is_some() {
return Err(SelectorParseError {
message: "Expected selector after combinator".to_string(),
position: self.pos,
});
}
if part.is_empty() {
break;
}
if let Some(comb) = combinator {
if let Some(last) = parts.last_mut() {
let (_, ref mut prev_comb) = last;
*prev_comb = Some(comb);
}
} else if !parts.is_empty() {
if let Some(last) = parts.last_mut() {
let (_, ref mut prev_comb) = last;
if prev_comb.is_none() {
*prev_comb = Some(Combinator::Descendant);
}
}
}
parts.push((part, None));
}
Ok(Selector { parts })
}
fn parse_combinator(&mut self) -> Option<Combinator> {
self.skip_whitespace();
match self.peek() {
Some('>') => {
self.advance();
Some(Combinator::Child)
}
Some('+') => {
self.advance();
Some(Combinator::AdjacentSibling)
}
Some('~') => {
self.advance();
Some(Combinator::GeneralSibling)
}
_ => None,
}
}
fn parse_selector_part(&mut self) -> Result<SelectorPart, SelectorParseError> {
let mut part = SelectorPart::new();
loop {
match self.peek() {
Some('*') => {
self.advance();
part.universal = true;
}
Some('#') => {
self.advance();
let id = self.parse_identifier()?;
if id.is_empty() {
return Err(SelectorParseError {
message: "Expected ID after #".to_string(),
position: self.pos,
});
}
part.id = Some(id);
}
Some('.') => {
self.advance();
let class = self.parse_identifier()?;
if class.is_empty() {
return Err(SelectorParseError {
message: "Expected class name after .".to_string(),
position: self.pos,
});
}
part.classes.push(class);
}
Some(':') => {
self.advance();
let pseudo = self.parse_pseudo_class()?;
part.pseudo_classes.push(pseudo);
}
Some('[') => {
self.advance();
let attr = self.parse_attribute_selector()?;
part.attributes.push(attr);
}
Some(ch) if ch.is_alphanumeric() || ch == '_' => {
if part.element.is_none() && !part.universal {
let elem = self.parse_identifier()?;
part.element = Some(elem);
} else {
break;
}
}
_ => break,
}
}
Ok(part)
}
fn parse_pseudo_class(&mut self) -> Result<PseudoClass, SelectorParseError> {
let name = self.parse_identifier()?;
let pseudo = match name.to_lowercase().as_str() {
"focus" => PseudoClass::Focus,
"hover" => PseudoClass::Hover,
"active" => PseudoClass::Active,
"disabled" => PseudoClass::Disabled,
"enabled" => PseudoClass::Enabled,
"checked" => PseudoClass::Checked,
"selected" => PseudoClass::Selected,
"empty" => PseudoClass::Empty,
"first-child" => PseudoClass::FirstChild,
"last-child" => PseudoClass::LastChild,
"only-child" => PseudoClass::OnlyChild,
"nth-child" => {
let n = self.parse_nth_argument()?;
PseudoClass::NthChild(n)
}
"nth-last-child" => {
let n = self.parse_nth_argument()?;
PseudoClass::NthLastChild(n)
}
"not" => {
if self.peek() == Some('(') {
self.advance();
self.skip_whitespace();
if self.peek() == Some(':') {
self.advance();
let inner = self.parse_pseudo_class()?;
self.skip_whitespace();
if self.peek() == Some(')') {
self.advance();
}
PseudoClass::Not(Box::new(inner))
} else {
return Err(SelectorParseError {
message: "Expected pseudo-class in :not()".to_string(),
position: self.pos,
});
}
} else {
return Err(SelectorParseError {
message: "Expected ( after :not".to_string(),
position: self.pos,
});
}
}
_ => {
return Err(SelectorParseError {
message: format!("Unknown pseudo-class: {}", name),
position: self.pos,
});
}
};
Ok(pseudo)
}
fn parse_nth_argument(&mut self) -> Result<NthExpr, SelectorParseError> {
if self.peek() != Some('(') {
return Err(SelectorParseError {
message: "Expected ( for nth argument".to_string(),
position: self.pos,
});
}
self.advance();
self.skip_whitespace();
let start = self.pos;
let mut depth = 1;
while let Some(ch) = self.peek() {
if ch == ')' {
depth -= 1;
if depth == 0 {
break;
}
} else if ch == '(' {
depth += 1;
}
self.advance();
}
let content: String = self.chars[start..self.pos].iter().collect();
let content = content.trim();
if self.peek() == Some(')') {
self.advance();
}
Self::parse_nth_expr(content).map_err(|msg| SelectorParseError {
message: msg,
position: start,
})
}
fn parse_nth_expr(input: &str) -> Result<NthExpr, String> {
let s = input.trim().to_lowercase();
if s == "odd" {
return Ok(NthExpr::new(2, 1));
}
if s == "even" {
return Ok(NthExpr::new(2, 0));
}
if let Some(n_pos) = s.find('n') {
let a_str = &s[..n_pos];
let a = match a_str {
"" | "+" => 1,
"-" => -1,
_ => a_str
.parse::<i32>()
.map_err(|_| format!("Invalid An+B expression: {}", input))?,
};
let b_str = s[n_pos + 1..].trim();
let b = if b_str.is_empty() {
0
} else {
b_str
.replace(' ', "")
.parse::<i32>()
.map_err(|_| format!("Invalid An+B expression: {}", input))?
};
Ok(NthExpr::new(a, b))
} else {
let b = s
.parse::<i32>()
.map_err(|_| format!("Invalid nth argument: {}", input))?;
Ok(NthExpr::new(0, b))
}
}
fn parse_attribute_selector(&mut self) -> Result<AttributeSelector, SelectorParseError> {
self.skip_whitespace();
let name = self.parse_identifier()?;
if name.is_empty() {
return Err(SelectorParseError {
message: "Expected attribute name".to_string(),
position: self.pos,
});
}
self.skip_whitespace();
let (op, value) = if self.peek() == Some(']') {
(AttributeOp::Exists, None)
} else {
let op = self.parse_attribute_op()?;
self.skip_whitespace();
let value = self.parse_attribute_value()?;
(op, Some(value))
};
self.skip_whitespace();
let case_insensitive = if self.peek() == Some('i') || self.peek() == Some('I') {
self.advance();
self.skip_whitespace();
true
} else {
false
};
if self.peek() == Some(']') {
self.advance();
}
Ok(AttributeSelector {
name,
op,
value,
case_insensitive,
})
}
fn parse_attribute_op(&mut self) -> Result<AttributeOp, SelectorParseError> {
match self.peek() {
Some('=') => {
self.advance();
Ok(AttributeOp::Equals)
}
Some('~') => {
self.advance();
if self.peek() == Some('=') {
self.advance();
Ok(AttributeOp::ContainsWord)
} else {
Err(SelectorParseError {
message: "Expected = after ~".to_string(),
position: self.pos,
})
}
}
Some('|') => {
self.advance();
if self.peek() == Some('=') {
self.advance();
Ok(AttributeOp::StartsWithWord)
} else {
Err(SelectorParseError {
message: "Expected = after |".to_string(),
position: self.pos,
})
}
}
Some('^') => {
self.advance();
if self.peek() == Some('=') {
self.advance();
Ok(AttributeOp::StartsWith)
} else {
Err(SelectorParseError {
message: "Expected = after ^".to_string(),
position: self.pos,
})
}
}
Some('$') => {
self.advance();
if self.peek() == Some('=') {
self.advance();
Ok(AttributeOp::EndsWith)
} else {
Err(SelectorParseError {
message: "Expected = after $".to_string(),
position: self.pos,
})
}
}
Some('*') => {
self.advance();
if self.peek() == Some('=') {
self.advance();
Ok(AttributeOp::Contains)
} else {
Err(SelectorParseError {
message: "Expected = after *".to_string(),
position: self.pos,
})
}
}
_ => Err(SelectorParseError {
message: "Expected attribute operator".to_string(),
position: self.pos,
}),
}
}
fn parse_attribute_value(&mut self) -> Result<String, SelectorParseError> {
let quote = match self.peek() {
Some('"') | Some('\'') => self.advance(), _ => None,
};
let mut value = String::new();
while let Some(ch) = self.peek() {
if let Some(q) = quote {
if ch == q {
self.advance();
break;
}
} else if ch == ']' || ch.is_whitespace() {
break;
}
value.push(ch);
self.advance();
}
Ok(value)
}
}