use core::fmt; use std::str::FromStr; #[derive(Clone, Debug, PartialEq)] pub enum QueryComponent { /// Every child element of the tag Element(QueryElement), DirectElement(QueryElement), } #[derive(Clone, Debug, Default, PartialEq)] pub struct QueryElement { pub tag: Option, pub class: Option, pub id: Option, } #[rustfmt::skip] // leave me alone impl FromStr for QueryElement { type Err = QueryParseError; fn from_str(raw: &str) -> Result { let mut element = QueryElement::default(); if let Some(idx) = raw.find(' ') { return Err(QueryParseError::MalformedComponent { raw: raw.into(), idx, error: "spaces not allowed in element queries".into() }); } let mut wrk = raw; loop { // Guards the match from a panic if wrk.is_empty() { break; } let part = match wrk[1..].find(['#', '.']) { None => { // Last part let part = wrk; wrk = &wrk[wrk.len()..]; part }, Some(idx) => { let part = &wrk[..idx+1]; wrk = &wrk[idx+1..]; part } }; if let Some(id) = part.strip_prefix('#') { println!("id {id}"); element.id = Some(id.into()); } else if let Some(class) = part.strip_prefix('.') { println!("class {class}"); element.class = Some(class.into()); } else { element.tag = Some(part.into()); } } Ok(element) } } pub fn parse_query(mut raw: &str) -> Result, QueryParseError> { let mut components = vec![]; let mut next_direct = false; loop { if raw.is_empty() { break Ok(components); } let part = match raw.find(['>', ' ']) { None => { let part = raw; raw = &raw[raw.len()..raw.len()]; part } Some(idx) => { let part = &raw[..idx]; if &raw[idx..idx + 1] == ">" { if components.is_empty() { return Err(QueryParseError::StartsDirect); } if next_direct { return Err(QueryParseError::DoubleDirect); } else { next_direct = true; } } raw = &raw[idx + 1..]; part } }; if part.is_empty() { continue; } let element: QueryElement = part.parse()?; if next_direct { components.push(QueryComponent::DirectElement(element)); next_direct = false; } else { components.push(QueryComponent::Element(element)); } } } #[derive(Debug)] pub enum QueryParseError { StartsDirect, EndsInDirect, DoubleDirect, MalformedComponent { raw: String, idx: usize, error: String, }, } impl fmt::Display for QueryParseError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::StartsDirect => { write!(f, "cannot start query with a direct relation") } Self::EndsInDirect => { write!( f, "Query ends with '>' which does not make sense. Are you missing a selector?" ) } Self::DoubleDirect => { write!(f, "Two direct descendent selectors (>) appeard together") } Self::MalformedComponent { raw, idx, error } => { write!(f, "{error}\n{raw}\n{:>width$}^", width = idx - 1) } } } } #[cfg(test)] mod test { use crate::query::{QueryComponent, QueryElement}; use super::parse_query; macro_rules! txt { () => { None }; ($txt:expr) => { Some(String::from($txt)) }; } #[test] fn parses_query_element() { let raw = "main#wide.post"; let parse = parse_query(raw).unwrap(); assert_eq!( parse, vec![QueryComponent::Element(QueryElement { tag: txt!("main"), id: txt!("wide"), class: txt!("post") })] ) } #[test] fn parses_direct_query_element() { let raw = "> main#wide.post"; let parse = parse_query(raw).unwrap(); assert_eq!( parse, vec![QueryComponent::DirectElement(QueryElement { tag: txt!("main"), id: txt!("wide"), class: txt!("post") })] ) } }