#[cfg(not(feature = "std"))]
use crate::ParseError;
use crate::{stream::Stream, util};
use super::Selector;
pub struct Parser<'a> {
stream: Stream<'a, u8>,
}
impl<'a> Parser<'a> {
pub fn new(input: &'a [u8]) -> Self {
Self {
stream: Stream::new(input),
}
}
#[cfg(not(feature = "std"))]
fn skip_whitespaces(&mut self) -> bool {
let has_whitespace = self.stream.expect_and_skip_cond(b' ');
while !self.stream.is_eof() {
if self.stream.expect_and_skip(b' ').is_none() {
break;
}
}
has_whitespace
}
fn read_identifier(&mut self) -> &'a [u8] {
let start = self.stream.idx;
while !self.stream.is_eof() {
let is_ident = self.stream.current().copied().is_some_and(util::is_ident);
if !is_ident {
break;
} else {
self.stream.advance();
}
}
self.stream.slice(start, self.stream.idx)
}
#[cfg(feature = "std")]
fn read_css_identifier(&mut self) -> &'a [u8] {
let start = self.stream.idx;
while self
.stream
.current_cpy()
.is_some_and(|c| c.is_ascii_alphanumeric() || c == b'-' || c == b'_' || c >= 128)
{
self.stream.advance();
}
self.stream.slice(start, self.stream.idx)
}
#[cfg(not(feature = "std"))]
fn parse_combinator<const MAX_SELECTOR_NODES: usize>(
&mut self,
left: Selector<'a, MAX_SELECTOR_NODES>,
) -> Result<Selector<'a, MAX_SELECTOR_NODES>, ParseError> {
let has_whitespaces = self.skip_whitespaces();
if self.stream.current_cpy().is_none() {
return Ok(left);
}
if has_whitespaces || matches!(self.stream.current_cpy(), Some(b',' | b'>')) {
return Err(ParseError::SelectorCapacityExceeded);
}
Err(ParseError::SelectorCapacityExceeded)
}
fn parse_attribute<const MAX_SELECTOR_NODES: usize>(
&mut self,
) -> Option<Selector<'a, MAX_SELECTOR_NODES>> {
let attribute = self.read_identifier();
if attribute.is_empty() {
return None;
}
let ty = match self.stream.current_cpy() {
Some(b']') => {
self.stream.advance();
Selector::Attribute(attribute)
}
Some(b'=') => {
self.stream.advance();
let quote = self.stream.expect_oneof_and_skip(b"\"'");
let value = if let Some(q) = quote {
let start = self.stream.idx;
while self
.stream
.current_cpy()
.is_some_and(|c| c != q && c != b'\\')
{
self.stream.advance();
}
self.stream.slice(start, self.stream.idx)
} else {
self.read_identifier()
};
if let Some(quote) = quote {
self.stream.expect_and_skip(quote)?;
}
self.stream.expect_and_skip(b']')?;
Selector::AttributeValue(attribute, value)
}
Some(c @ b'~' | c @ b'^' | c @ b'$' | c @ b'*') => {
self.stream.advance();
self.stream.expect_and_skip(b'=')?;
let quote = self.stream.expect_oneof_and_skip(b"\"'");
let value = if let Some(q) = quote {
let start = self.stream.idx;
while self
.stream
.current_cpy()
.is_some_and(|c| c != q && c != b'\\')
{
self.stream.advance();
}
self.stream.slice(start, self.stream.idx)
} else {
self.read_identifier()
};
if let Some(quote) = quote {
self.stream.expect_and_skip(quote)?;
}
self.stream.expect_and_skip(b']')?;
match c {
b'~' => Selector::AttributeValueWhitespacedContains(attribute, value),
b'^' => Selector::AttributeValueStartsWith(attribute, value),
b'$' => Selector::AttributeValueEndsWith(attribute, value),
b'*' => Selector::AttributeValueSubstring(attribute, value),
_ => unreachable!(),
}
}
_ => return None,
};
Some(ty)
}
#[cfg(feature = "std")]
pub fn selector(&mut self) -> Option<Selector<'a>> {
css(self.stream.data())
}
#[cfg(not(feature = "std"))]
pub fn selector<const MAX_SELECTOR_NODES: usize>(
&mut self,
) -> Result<Selector<'a, MAX_SELECTOR_NODES>, ParseError> {
self.skip_whitespaces();
let tok = self
.stream
.current_cpy()
.ok_or(ParseError::SelectorCapacityExceeded)?;
let left = match tok {
b'#' => {
self.stream.advance();
let id = self.read_identifier();
Selector::Id(id)
}
b'.' => {
self.stream.advance();
let class = self.read_identifier();
Selector::Class(class)
}
b'*' => {
self.stream.advance();
Selector::All
}
b'[' => {
self.stream.advance();
self.parse_attribute::<MAX_SELECTOR_NODES>()
.ok_or(ParseError::SelectorCapacityExceeded)?
}
_ if util::is_ident(tok) => {
let tag = self.read_identifier();
Selector::Tag(tag)
}
_ => return Err(ParseError::SelectorCapacityExceeded),
};
self.parse_combinator(left)
}
}
#[cfg(feature = "std")]
fn css(input: &[u8]) -> Option<Selector<'_>> {
css_at(input, 0)
}
#[cfg(feature = "std")]
fn css_at(input: &[u8], depth_limit: usize) -> Option<Selector<'_>> {
if depth_limit >= 64 {
return None;
}
let input = trim(input);
if input.is_empty() {
return None;
}
let mut depth = 0usize;
let mut quote = None;
let mut relations = Vec::new();
let mut commas = Vec::new();
for (i, &c) in input.iter().enumerate() {
if let Some(q) = quote {
if c == q {
quote = None;
}
continue;
}
match c {
b'\'' | b'"' => quote = Some(c),
b'[' | b'(' => depth += 1,
b']' | b')' => depth = depth.checked_sub(1)?,
b',' if depth == 0 => commas.push(i),
b'>' | b'+' | b'~' if depth == 0 => relations.push((i, c)),
c if c.is_ascii_whitespace() && depth == 0 => relations.push((i, b' ')),
_ => {}
}
}
if depth != 0 || quote.is_some() {
return None;
}
if let Some(&i) = commas.first() {
return Some(Selector::Or(
Box::new(css_at(&input[..i], depth_limit + 1)?),
Box::new(css_at(&input[i + 1..], depth_limit + 1)?),
));
}
for &(i, c) in relations.iter().rev() {
let left = trim(&input[..i]);
let right = trim(&input[i + 1..]);
if c == b' '
&& (left.is_empty()
|| right.is_empty()
|| left.last().is_some_and(|c| b">+~".contains(c))
|| right.first().is_some_and(|c| b">+~".contains(c)))
{
continue;
}
let left = Box::new(css_at(left, depth_limit + 1)?);
let right = Box::new(css_at(right, depth_limit + 1)?);
return Some(match c {
b'>' => Selector::Parent(left, right),
b'+' => Selector::Adjacent(left, right),
b'~' => Selector::Sibling(left, right),
_ => Selector::Descendant(left, right),
});
}
let mut parser = Parser::new(input);
let mut result = None;
let mut compounds = 0;
while let Some(tok) = parser.stream.current_cpy() {
compounds += 1;
if compounds >= 64 {
return None;
}
let simple = match tok {
b'#' | b'.' => {
parser.stream.advance();
let ident = parser.read_css_identifier();
if ident.is_empty() {
return None;
}
if tok == b'#' {
Selector::Id(ident)
} else {
Selector::Class(ident)
}
}
b'*' => {
parser.stream.advance();
Selector::All
}
b'[' => {
parser.stream.advance();
parser.parse_attribute::<0>()?
}
b':' => {
parser.stream.advance();
let name = parser.read_css_identifier();
match name {
b"first-child" => Selector::NthChild(0, 1),
b"nth-child" | b"not" | b"has" => {
parser.stream.expect_and_skip(b'(')?;
let start = parser.stream.idx;
let mut depth = 1;
let mut quote = None;
while let Some(c) = parser.stream.current_cpy() {
if let Some(q) = quote {
if c == q {
quote = None;
}
} else {
match c {
b'\'' | b'"' => quote = Some(c),
b'(' => depth += 1,
b')' => {
depth -= 1;
if depth == 0 {
break;
}
}
_ => {}
}
}
parser.stream.advance();
}
let arg = trim(parser.stream.slice(start, parser.stream.idx));
parser.stream.expect_and_skip(b')')?;
match name {
b"not" => Selector::Not(Box::new(css_at(arg, depth_limit + 1)?)),
b"has" => Selector::Has(Box::new(css_at(arg, depth_limit + 1)?)),
_ => {
let text = core::str::from_utf8(arg).ok()?;
let (a, b) = match text {
"odd" => (2, 1),
"even" => (2, 0),
_ if text.contains('n') => {
let (a, b) = text.split_once('n')?;
let a = match a.trim() {
"" | "+" => 1,
"-" => -1,
a => a.parse::<i32>().ok()?,
};
let b = b.trim();
let b = if b.is_empty() {
0
} else {
if !b.starts_with('+') && !b.starts_with('-') {
return None;
}
let compact: String = b
.chars()
.filter(|c| !c.is_ascii_whitespace())
.collect();
compact.parse::<i32>().ok()?
};
(a, b)
}
_ => (0, text.parse::<i32>().ok()?),
};
Selector::NthChild(a, b)
}
}
}
_ => return None,
}
}
_ if tok.is_ascii_alphanumeric() || tok == b'_' || tok == b'-' || tok >= 128 => {
if result.is_some() {
return None;
}
Selector::Tag(parser.read_css_identifier())
}
_ => return None,
};
result = Some(match result {
None => simple,
Some(left) => Selector::And(Box::new(left), Box::new(simple)),
});
}
result
}
#[cfg(feature = "std")]
fn trim(mut input: &[u8]) -> &[u8] {
while input.first().is_some_and(u8::is_ascii_whitespace) {
input = &input[1..];
}
while input.last().is_some_and(u8::is_ascii_whitespace) {
input = &input[..input.len() - 1];
}
input
}