#[derive(Clone, Debug, PartialEq)] pub enum QueryComponent { /// Every child element of the tag TagName(String), /// Only direct children with the tag DirectTagName(String), /// The child element with the ID Id(String), /// Only direct children with the tag DirectId(String), /// Every child that has the class Class(String), /// Only direct children with the class DirectClass(String), } 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 next_direct { return Err(QueryParseError::DoubleDirect); } else { next_direct = true; } } raw = &raw[idx + 1..]; part } }; if part.is_empty() { continue; } if let Some(id) = part.strip_prefix('#') { if id.contains(['#', '.']) { return Err(QueryParseError::UnknownComponent { malformed: id.into(), }); } if next_direct { components.push(QueryComponent::DirectId(id.into())); next_direct = false; } else { components.push(QueryComponent::Id(id.into())); } } else if let Some(class) = part.strip_prefix('.') { if class.contains(['#', '.']) { return Err(QueryParseError::UnknownComponent { malformed: class.into(), }); } if next_direct { components.push(QueryComponent::DirectClass(class.into())); next_direct = false; } else { components.push(QueryComponent::Class(class.into())); } } else { if part.contains(['#', '.']) { return Err(QueryParseError::UnknownComponent { malformed: part.into(), }); } if next_direct { components.push(QueryComponent::DirectTagName(part.into())); next_direct = false; } else { components.push(QueryComponent::TagName(part.into())); } } } } #[derive(Debug, thiserror::Error)] pub enum QueryParseError { #[error("Query ends with '>' which does not make sense. Are you missing a selector?")] EndsInDirect, #[error("Two direct descendent selectors (>) appeard together")] DoubleDirect, #[error( "The component {malformed} does not make sense. Valid selectors are #id, .class, and tag" )] UnknownComponent { malformed: String }, } #[cfg(test)] mod test { use super::parse_query; macro_rules! qc { ($tag:expr) => { $crate::query::QueryComponent::TagName(String::from($tag)) }; (>$tag:expr) => { $crate::query::QueryComponent::DirectTagName(String::from($tag)) }; (ID $tag:expr) => { $crate::query::QueryComponent::Id(String::from($tag)) }; (>ID $tag:expr) => { $crate::query::QueryComponent::DirectId(String::from($tag)) }; (. $tag:expr) => { $crate::query::QueryComponent::Class(String::from($tag)) }; (>. $tag:expr) => { $crate::query::QueryComponent::DirectClass(String::from($tag)) }; } #[test] fn parses_tags() { let raw = "main section p"; let parse = parse_query(raw).unwrap(); assert_eq!(parse, vec![qc!("main"), qc!("section"), qc!("p")]) } #[test] fn parses_direct_tags() { let raw = "main > section > p"; let parse = parse_query(raw).unwrap(); assert_eq!(parse, vec![qc!("main"), qc!(> "section"), qc!(> "p")]) } #[test] fn parses_id() { let raw = "main #job"; let parse = parse_query(raw).unwrap(); assert_eq!(parse, vec![qc!("main"), qc!(ID "job")]) } #[test] fn parses_direct_id() { let raw = "main > #job"; let parse = parse_query(raw).unwrap(); assert_eq!(parse, vec![qc!("main"), qc!(>ID "job")]) } #[test] fn parses_class() { let raw = "main .post"; let parse = parse_query(raw).unwrap(); assert_eq!(parse, vec![qc!("main"), qc!(."post")]) } #[test] fn parses_direct_class() { let raw = "main > .post"; let parse = parse_query(raw).unwrap(); assert_eq!(parse, vec![qc!("main"), qc!(>."post")]) } #[test] fn parses_complex() { let raw = "main > article"; let parse = parse_query(raw).unwrap(); assert_eq!(parse, vec![qc!("main"), qc!(>."post")]) } }