about summary refs log tree commit diff
path: root/src/query.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/query.rs')
-rw-r--r--src/query.rs185
1 files changed, 185 insertions, 0 deletions
diff --git a/src/query.rs b/src/query.rs
new file mode 100644
index 0000000..796e372
--- /dev/null
+++ b/src/query.rs
@@ -0,0 +1,185 @@
+#[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<Vec<QueryComponent>, 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")])
+	}
+}