use super::string_search::AttributeSelectionKind;
use crate::Reader;
use crate::query::compiler::SelectorParseError;
#[derive(Debug, PartialEq, Clone, Copy)]
enum QuoteKind {
DoubleQuoted,
SingleQuoted,
}
#[derive(Debug, PartialEq, Clone)]
pub struct Attribute<'html> {
pub key: &'html str,
pub value: Option<&'html str>,
}
#[derive(Debug, PartialEq, Clone)]
pub struct AttributeSelection<'query> {
pub name: &'query str,
pub value: Option<&'query str>,
pub kind: AttributeSelectionKind,
}
impl<'query> AttributeSelection<'query> {
pub const fn new_const(
name: &'query str,
value: Option<&'query str>,
kind: AttributeSelectionKind,
) -> Self {
Self { name, value, kind }
}
}
#[derive(Debug, PartialEq, Clone)]
pub enum AttributeSelections<'query> {
Static(&'query [AttributeSelection<'query>]),
Owned(Box<[AttributeSelection<'query>]>),
}
impl<'query> AttributeSelections<'query> {
pub const fn from_static(attributes: &'query [AttributeSelection<'query>]) -> Self {
Self::Static(attributes)
}
pub fn as_slice(&self) -> &[AttributeSelection<'query>] {
match self {
Self::Static(attributes) => attributes,
Self::Owned(attributes) => attributes,
}
}
}
impl<'query> Default for AttributeSelections<'query> {
fn default() -> Self {
Self::Static(&[])
}
}
impl<'query> From<Vec<AttributeSelection<'query>>> for AttributeSelections<'query> {
fn from(value: Vec<AttributeSelection<'query>>) -> Self {
Self::Owned(value.into_boxed_slice())
}
}
#[derive(Debug, Clone)]
pub enum ClassSelections<'query> {
Static(&'query [&'query str]),
Owned(Box<[&'query str]>),
}
impl<'query> ClassSelections<'query> {
pub const fn from_static(classes: &'query [&'query str]) -> Self {
Self::Static(classes)
}
pub fn as_slice(&self) -> &[&'query str] {
match self {
Self::Static(classes) => classes,
Self::Owned(classes) => classes,
}
}
}
impl<'query> Default for ClassSelections<'query> {
fn default() -> Self {
Self::Static(&[])
}
}
impl<'query> From<Vec<&'query str>> for ClassSelections<'query> {
fn from(value: Vec<&'query str>) -> Self {
Self::Owned(value.into_boxed_slice())
}
}
impl<'query> PartialEq for ClassSelections<'query> {
fn eq(&self, other: &Self) -> bool {
self.as_slice() == other.as_slice()
}
}
pub trait IElement<'html> {
fn name(&self) -> &'html str;
fn id(&self) -> Option<&'html str>;
fn class(&self) -> Option<&'html str>;
fn attributes(&self) -> &[Attribute<'html>];
}
struct KeyValueAttributeSelection<'query> {
name: Option<&'query str>,
selection_kind: AttributeSelectionKind,
value: Option<&'query str>,
}
impl<'query> KeyValueAttributeSelection<'query> {
fn push(&mut self, content_inside_quotes: &'query str) {
if self.name.is_none() {
self.name = Some(content_inside_quotes);
} else if self.value.is_none() {
self.value = Some(content_inside_quotes);
} else {
unreachable!();
}
}
fn refresh_equal(&mut self) {
if self.selection_kind == AttributeSelectionKind::Presence && self.value.is_some() {
self.selection_kind = AttributeSelectionKind::Exact;
}
}
}
impl<'query> From<&mut Reader<'query>> for AttributeSelection<'query> {
fn from(reader: &mut Reader<'query>) -> Self {
Self::try_from(reader).unwrap()
}
}
impl<'query> AttributeSelection<'query> {
fn try_from(reader: &mut Reader<'query>) -> Result<Self, SelectorParseError> {
let mut position = reader.get_position();
let mut opened_quote: Option<QuoteKind> = None;
let mut equal = false;
let mut operator_requires_equal = false;
let mut kv = KeyValueAttributeSelection {
name: None,
selection_kind: AttributeSelectionKind::Presence,
value: None,
};
while let Some(token) = SelectionAttributeToken::next(reader)? {
if operator_requires_equal && !matches!(token, SelectionAttributeToken::Equal) {
return Err(SelectorParseError::new(
"attribute match operator requires '='",
reader.get_position(),
));
}
match token {
SelectionAttributeToken::Quote(kind) => {
if opened_quote.is_none() {
opened_quote = Some(kind);
position = reader.get_position();
continue;
}
if let Some(quote_kind) = &opened_quote
&& *quote_kind != kind
{
continue;
}
opened_quote = None;
const SIZE_OF_QUOTE: usize = 1;
let end_position = reader.get_position() - SIZE_OF_QUOTE;
let content_inside_quotes = reader.slice(position..end_position);
kv.push(content_inside_quotes);
}
SelectionAttributeToken::String(string_value) => {
if opened_quote.is_some() {
continue;
}
kv.push(string_value);
}
SelectionAttributeToken::StringMatchSelector(equal_selector) => {
kv.selection_kind = equal_selector;
operator_requires_equal = true;
}
SelectionAttributeToken::Equal => {
operator_requires_equal = false;
if kv.name.is_none() {
return Err(SelectorParseError::new(
"attribute selector is missing a key",
reader.get_position(),
));
}
if kv.value.is_some() {
return Err(SelectorParseError::new(
"attribute selector has multiple values",
reader.get_position(),
));
}
if equal {
return Err(SelectorParseError::new(
"attribute selector has multiple '=' tokens",
reader.get_position(),
));
}
equal = true;
}
}
}
if operator_requires_equal {
return Err(SelectorParseError::new(
"attribute match operator requires '='",
reader.get_position(),
));
}
if kv.name.is_none() {
return Err(SelectorParseError::new(
"attribute selector is missing a key",
reader.get_position(),
));
}
if !is_valid_attribute_name(kv.name.unwrap()) {
return Err(SelectorParseError::new(
"attribute selector key is invalid",
reader.get_position(),
));
}
if opened_quote.is_some() {
return Err(SelectorParseError::new(
"attribute selector has an unclosed quoted value",
reader.get_position(),
));
}
if equal && kv.value.is_none() {
return Err(SelectorParseError::new(
"attribute selector is missing a value",
reader.get_position(),
));
}
kv.refresh_equal();
Ok(AttributeSelection {
name: kv.name.unwrap(),
value: kv.value,
kind: kv.selection_kind,
})
}
}
enum SelectionKeyWords<'query> {
String(&'query str),
ID,
Class,
Quote,
OpenAttribute,
CloseAttribute,
}
impl<'a> SelectionKeyWords<'a> {
pub fn next(reader: &mut Reader<'a>) -> Option<Self> {
let start_pos = reader.get_position();
if let Some(token) = reader.peek()
&& matches!(token, b'>' | b' ' | b'+' | b'~' | b'|')
{
return None;
}
match reader.next()? {
b'#' => Some(Self::ID),
b'.' => Some(Self::Class),
b'"' => Some(Self::Quote),
b'\'' => Some(Self::Quote),
b'[' => Some(Self::OpenAttribute),
b']' => Some(Self::CloseAttribute),
_ => {
reader.next_until_list(&[b' ', b'#', b'.', b'[']);
Some(Self::String(reader.slice(start_pos..reader.get_position())))
}
}
}
}
enum SelectionAttributeToken<'a> {
String(&'a str),
Quote(QuoteKind),
Equal,
StringMatchSelector(AttributeSelectionKind),
}
impl<'a> SelectionAttributeToken<'a> {
pub fn next(reader: &mut Reader<'a>) -> Result<Option<Self>, SelectorParseError> {
reader.next_while(b' ');
let start_pos = reader.get_position();
let token = match reader.next() {
None => {
return Err(SelectorParseError::new(
"attribute selector is missing a closing ']'",
reader.get_position(),
));
}
Some(token) => token,
};
Ok(match token {
b'"' => Some(Self::Quote(QuoteKind::DoubleQuoted)),
b'\'' => Some(Self::Quote(QuoteKind::SingleQuoted)),
b'=' => Some(Self::Equal),
b'~' => Some(Self::StringMatchSelector(
AttributeSelectionKind::WhitespaceSeparated,
)),
b'|' => Some(Self::StringMatchSelector(
AttributeSelectionKind::HyphenSeparated,
)),
b'^' => Some(Self::StringMatchSelector(AttributeSelectionKind::Prefix)),
b'$' => Some(Self::StringMatchSelector(AttributeSelectionKind::Suffix)),
b'*' => Some(Self::StringMatchSelector(AttributeSelectionKind::Substring)),
b']' => None,
_ => {
reader.next_until_list(&[
b' ', b'"', b'\'', b'=', b']', b'~', b'|', b'^', b'$', b'*',
]);
Some(Self::String(reader.slice(start_pos..reader.get_position())))
}
})
}
}
#[derive(Debug, PartialEq, Clone)]
pub struct ElementPredicate<'a> {
pub name: Option<&'a str>,
pub id: Option<&'a str>,
pub classes: ClassSelections<'a>,
pub attributes: AttributeSelections<'a>,
}
impl<'a> ElementPredicate<'a> {
pub const fn new_const(
name: Option<&'a str>,
id: Option<&'a str>,
classes: ClassSelections<'a>,
attributes: AttributeSelections<'a>,
) -> Self {
Self {
name,
id,
classes,
attributes,
}
}
fn push_class(&mut self, class_name: &'a str) {
let mut classes = self.classes.as_slice().to_vec();
classes.push(class_name);
self.classes = ClassSelections::from(classes);
}
fn try_parse_attribute(&mut self, reader: &mut Reader<'a>) -> Result<(), SelectorParseError> {
let attribute = AttributeSelection::try_from(reader)?;
let mut attributes = self.attributes.as_slice().to_vec();
attributes.push(attribute);
self.attributes = AttributeSelections::from(attributes);
Ok(())
}
pub fn try_from(reader: &mut Reader<'a>) -> Result<Self, SelectorParseError> {
let mut element = Self {
name: None,
id: None,
classes: ClassSelections::default(),
attributes: AttributeSelections::default(),
};
let mut previous: Option<SelectionKeyWords> = None;
while let Some(word) = SelectionKeyWords::next(reader) {
match (previous, &word) {
(Option::None, SelectionKeyWords::String(name)) => {
if !is_valid_selector_name(name) {
return Err(SelectorParseError::new(
"illegal selector token",
reader.get_position().saturating_sub(name.len()),
));
}
if element.name.is_some() {
return Err(SelectorParseError::new(
"selector has multiple element names",
reader.get_position().saturating_sub(name.len()),
));
}
element.name = Some(*name);
}
(Some(SelectionKeyWords::ID), SelectionKeyWords::String(id_name)) => {
if !is_valid_selector_name(id_name) {
return Err(SelectorParseError::new(
"missing id string",
reader.get_position().saturating_sub(id_name.len()),
));
}
if element.id.is_none() {
element.id = Some(*id_name);
}
}
(Some(SelectionKeyWords::Class), SelectionKeyWords::String(class_name)) => {
if !is_valid_selector_name(class_name) {
return Err(SelectorParseError::new(
"missing class string",
reader.get_position().saturating_sub(class_name.len()),
));
}
element.push_class(class_name);
}
(_, SelectionKeyWords::OpenAttribute) => element.try_parse_attribute(reader)?,
(Some(SelectionKeyWords::ID), _) => {
return Err(SelectorParseError::new(
"missing id string",
reader.get_position(),
));
}
(Some(SelectionKeyWords::Class), _) => {
return Err(SelectorParseError::new(
"missing class string",
reader.get_position(),
));
}
(_, _) => (),
}
previous = Some(word);
}
match previous {
Some(SelectionKeyWords::ID) => Err(SelectorParseError::new(
"missing id string",
reader.get_position(),
)),
Some(SelectionKeyWords::Class) => Err(SelectorParseError::new(
"missing class string",
reader.get_position(),
)),
_ if element.name.is_none()
&& element.id.is_none()
&& element.classes.as_slice().is_empty()
&& element.attributes.as_slice().is_empty() =>
{
Err(SelectorParseError::new(
"missing selector element",
reader.get_position(),
))
}
_ => Ok(element),
}
}
}
impl<'a> From<&mut Reader<'a>> for ElementPredicate<'a> {
fn from(reader: &mut Reader<'a>) -> Self {
Self::try_from(reader).unwrap()
}
}
fn is_valid_selector_name(value: &str) -> bool {
!value.is_empty()
&& value
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_'))
}
fn is_valid_attribute_name(value: &str) -> bool {
let mut bytes = value.bytes();
match bytes.next() {
Some(first) if first.is_ascii_alphabetic() || first == b'_' => (),
_ => return false,
}
bytes.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_'))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_basic_element_selection() {
let mut reader = Reader::new("element#id.class");
let element = ElementPredicate::from(&mut reader);
assert_eq!(
element,
ElementPredicate {
name: Some("element"),
id: Some("id"),
classes: ClassSelections::from_static(&["class"]),
attributes: AttributeSelections::from_static(&[]),
}
);
}
#[test]
fn test_fully_detailed_element_selection() {
let mut reader = Reader::new("element#id.class[selected=true]");
let element = ElementPredicate::from(&mut reader);
assert_eq!(
element,
ElementPredicate {
name: Some("element"),
id: Some("id"),
classes: ClassSelections::from_static(&["class"]),
attributes: AttributeSelections::from(vec![AttributeSelection {
name: "selected",
value: Some("true"),
kind: AttributeSelectionKind::Exact
}]),
}
);
}
#[test]
fn test_two_fully_detailed_element_selection() {
let mut reader = Reader::new("element#id.class[href~=\"_blank\"][selected=true]");
let element = ElementPredicate::from(&mut reader);
assert_eq!(
element,
ElementPredicate {
name: Some("element"),
id: Some("id"),
classes: ClassSelections::from_static(&["class"]),
attributes: AttributeSelections::from(vec![
AttributeSelection {
name: "href",
value: Some("_blank"),
kind: AttributeSelectionKind::WhitespaceSeparated
},
AttributeSelection {
name: "selected",
value: Some("true"),
kind: AttributeSelectionKind::Exact
}
]),
}
);
}
#[test]
fn test_handle_duplicates_in_element_definition() {
let mut reader = Reader::new("element#id.class[selected=true]#id#notid");
let element = ElementPredicate::from(&mut reader);
assert_eq!(
element,
ElementPredicate {
name: Some("element"),
id: Some("id"),
classes: ClassSelections::from_static(&["class"]),
attributes: AttributeSelections::from(vec![AttributeSelection {
name: "selected",
value: Some("true"),
kind: AttributeSelectionKind::Exact
}]),
}
);
}
#[test]
fn test_multiple_classes_are_preserved() {
let mut reader = Reader::new("a.blue.exit");
let element = ElementPredicate::from(&mut reader);
assert_eq!(
element,
ElementPredicate {
name: Some("a"),
id: None,
classes: ClassSelections::from_static(&["blue", "exit"]),
attributes: AttributeSelections::from_static(&[]),
}
);
}
}