about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--src/query.rs244
1 files changed, 126 insertions, 118 deletions
diff --git a/src/query.rs b/src/query.rs
index 796e372..26b02aa 100644
--- a/src/query.rs
+++ b/src/query.rs
@@ -1,17 +1,67 @@
+use core::fmt;
+use std::str::FromStr;
+
 #[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),
+	Element(QueryElement),
+	DirectElement(QueryElement),
+}
+
+#[derive(Clone, Debug, Default, PartialEq)]
+pub struct QueryElement {
+	pub tag: Option<String>,
+	pub class: Option<String>,
+	pub id: Option<String>,
+}
+
+#[rustfmt::skip] // leave me alone
+impl FromStr for QueryElement {
+	type Err = QueryParseError;
+
+	fn from_str(raw: &str) -> Result<Self, Self::Err> {
+		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<Vec<QueryComponent>, QueryParseError> {
@@ -33,6 +83,10 @@ pub fn parse_query(mut raw: &str) -> Result<Vec<QueryComponent>, QueryParseError
 				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 {
@@ -49,137 +103,91 @@ pub fn parse_query(mut raw: &str) -> Result<Vec<QueryComponent>, QueryParseError
 			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()));
-			}
+		let element: QueryElement = part.parse()?;
+		if next_direct {
+			components.push(QueryComponent::DirectElement(element));
+			next_direct = false;
 		} 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()));
-			}
+			components.push(QueryComponent::Element(element));
 		}
 	}
 }
 
-#[derive(Debug, thiserror::Error)]
+#[derive(Debug)]
 pub enum QueryParseError {
-	#[error("Query ends with '>' which does not make sense. Are you missing a selector?")]
+	StartsDirect,
 	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 },
+	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 super::parse_query;
-
-	macro_rules! qc {
-		($tag:expr) => {
-			$crate::query::QueryComponent::TagName(String::from($tag))
-		};
-
-		(>$tag:expr) => {
-			$crate::query::QueryComponent::DirectTagName(String::from($tag))
-		};
+	use crate::query::{QueryComponent, QueryElement};
 
-		(ID $tag:expr) => {
-			$crate::query::QueryComponent::Id(String::from($tag))
-		};
-
-		(>ID $tag:expr) => {
-			$crate::query::QueryComponent::DirectId(String::from($tag))
-		};
+	use super::parse_query;
 
-		(. $tag:expr) => {
-			$crate::query::QueryComponent::Class(String::from($tag))
+	macro_rules! txt {
+		() => {
+			None
 		};
 
-		(>. $tag:expr) => {
-			$crate::query::QueryComponent::DirectClass(String::from($tag))
+		($txt:expr) => {
+			Some(String::from($txt))
 		};
 	}
 
 	#[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";
+	fn parses_query_element() {
+		let raw = "main#wide.post";
 		let parse = parse_query(raw).unwrap();
-		assert_eq!(parse, vec![qc!("main"), qc!(>."post")])
+		assert_eq!(
+			parse,
+			vec![QueryComponent::Element(QueryElement {
+				tag: txt!("main"),
+				id: txt!("wide"),
+				class: txt!("post")
+			})]
+		)
 	}
 
 	#[test]
-	fn parses_complex() {
-		let raw = "main > article";
+	fn parses_direct_query_element() {
+		let raw = "> main#wide.post";
 		let parse = parse_query(raw).unwrap();
-		assert_eq!(parse, vec![qc!("main"), qc!(>."post")])
+		assert_eq!(
+			parse,
+			vec![QueryComponent::DirectElement(QueryElement {
+				tag: txt!("main"),
+				id: txt!("wide"),
+				class: txt!("post")
+			})]
+		)
 	}
 }