use std::iter::Peekable;
use std::str::Chars;
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct UnexpectedTokenError(pub char);
#[derive(Clone, Debug)]
pub struct CompoundSelector {
pub scope: Scope,
pub parts: Vec<Selector>,
}
#[derive(Clone, Copy, PartialEq, Debug)]
pub enum Scope {
DirectChild,
IndirectChild,
}
#[derive(Clone, Debug)]
pub enum Selector {
Id(String),
TagName(String),
Attribute(String, MatchType, String),
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum MatchType {
Equals,
}
macro_rules! expect_token {
($token_option: expr, $token: expr) => {
match $token_option {
Some($token) => {}
Some(token) => return Err(UnexpectedTokenError(token)),
None => return Err(UnexpectedTokenError(' ')),
}
};
}
#[inline]
fn non_digit(c: char) -> bool {
('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z')
}
#[inline]
fn allowed_character(c: char) -> bool {
non_digit(c) || ('0' <= c && c <= '9') || c == '-' || c == '_'
}
#[inline]
fn valid_start_token(c: char) -> bool {
c == '#' || c == '['
}
fn extract_valid_string(chars: &mut Peekable<Chars>) -> Result<String, UnexpectedTokenError> {
extract_valid_string_until_token(chars, ' ')
}
fn extract_valid_string_until_token(
chars: &mut Peekable<Chars>,
stop_token: char,
) -> Result<String, UnexpectedTokenError> {
let mut string = String::new();
while let Some(&c) = chars.peek() {
if c == stop_token {
chars.next().unwrap();
break;
} else if allowed_character(c) {
string.push(chars.next().unwrap());
} else if valid_start_token(c) {
break;
} else {
return Err(UnexpectedTokenError(c));
}
}
return Ok(string);
}
impl Selector {
fn create_list(string: &str) -> Result<Vec<Selector>, UnexpectedTokenError> {
let mut selectors = Vec::new();
let mut chars = string.chars().peekable();
while let Some(&c) = chars.peek() {
match Selector::next_selector(c, &mut chars) {
Ok(selector) => selectors.push(selector),
Err(err) => return Err(err),
}
}
return Ok(selectors);
}
fn next_selector(
c: char,
chars: &mut Peekable<Chars>,
) -> Result<Selector, UnexpectedTokenError> {
if non_digit(c) {
Selector::create_tag_name(chars)
} else if c == '#' {
Selector::create_id(chars)
} else if c == '[' {
Selector::create_attribute(chars)
} else {
Err(UnexpectedTokenError(c))
}
}
fn create_tag_name(chars: &mut Peekable<Chars>) -> Result<Selector, UnexpectedTokenError> {
extract_valid_string(chars).map(|s| Selector::TagName(s))
}
fn create_id(chars: &mut Peekable<Chars>) -> Result<Selector, UnexpectedTokenError> {
match chars.next() {
Some('#') => return extract_valid_string(chars).map(|s| Selector::Id(s)),
Some(token) => return Err(UnexpectedTokenError(token)),
None => return Err(UnexpectedTokenError(' ')),
}
}
fn create_attribute(chars: &mut Peekable<Chars>) -> Result<Selector, UnexpectedTokenError> {
expect_token!(chars.next(), '[');
extract_valid_string_until_token(chars, '=')
.and_then(|attribute| Ok((attribute, MatchType::Equals)))
.and_then(|(attribute, match_type)| {
let result = if Some(&'"') == chars.peek() {
chars.next().unwrap();
let result = extract_valid_string_until_token(chars, '"');
expect_token!(chars.next(), ']');
result
} else {
extract_valid_string_until_token(chars, ']')
};
result.map(|value| Selector::Attribute(attribute, match_type, value))
})
}
}
struct SelectorParts<I: Iterator<Item = String>> {
inner_iter: I,
}
impl<I: Iterator<Item = String>> Iterator for SelectorParts<I> {
type Item = (Scope, String);
fn next(&mut self) -> Option<Self::Item> {
self.inner_iter.next().and_then(|next_part| {
if &next_part == ">" {
Some((Scope::DirectChild, self.inner_iter.next().unwrap()))
} else {
Some((Scope::IndirectChild, next_part))
}
})
}
}
impl CompoundSelector {
pub fn parse(selector: &str) -> Result<Vec<CompoundSelector>, UnexpectedTokenError> {
let normalized_selector = selector.split(">").collect::<Vec<&str>>().join(" > ");
let selector_parts = SelectorParts {
inner_iter: normalized_selector
.split_whitespace()
.into_iter()
.map(|s| s.to_string()),
};
selector_parts.fold(Ok(Vec::new()), |result_so_far, (scope, part)| {
if let Ok(mut compound_selectors) = result_so_far {
Selector::create_list(&part).map(|parts| {
compound_selectors.push(CompoundSelector {
scope: scope,
parts: parts,
});
compound_selectors
})
} else {
result_so_far
}
})
}
}