about summary refs log tree commit diff
path: root/src/tag.rs
diff options
context:
space:
mode:
authorgennyble <gen@nyble.dev>2023-12-28 01:07:53 -0600
committergennyble <gen@nyble.dev>2023-12-28 01:07:53 -0600
commita14a861d824230458333defd5cf43bafd2ca596d (patch)
treeb5e2108e38268b02417e83c009545ac714032218 /src/tag.rs
parenta567b9eb9930e3ec0d6da00f58d217843a732e2f (diff)
downloadcutie-a14a861d824230458333defd5cf43bafd2ca596d.tar.gz
cutie-a14a861d824230458333defd5cf43bafd2ca596d.zip
query parsing, but this is not what i wanted actually
Diffstat (limited to 'src/tag.rs')
-rw-r--r--src/tag.rs177
1 files changed, 177 insertions, 0 deletions
diff --git a/src/tag.rs b/src/tag.rs
new file mode 100644
index 0000000..e325b1f
--- /dev/null
+++ b/src/tag.rs
@@ -0,0 +1,177 @@
+use core::fmt;
+
+use crate::Node;
+
+#[derive(Clone, Debug, PartialEq)]
+pub struct Tag {
+	pub name: String,
+	/// Everything inside the tag that's not it's name. Includes a
+	/// self-close if there is one.
+	pub body: Option<String>,
+	pub self_closing: bool,
+	pub children: Vec<Node>,
+}
+
+impl Tag {
+	pub fn self_closing(&self) -> bool {
+		self.self_closing
+	}
+
+	pub fn get_attribute<'a>(&'a self, key: &str) -> Option<&'a str> {
+		let body = match self.body.as_deref() {
+			None => return None,
+			Some(body) => body,
+		};
+
+		// get rid of potential self-close
+		let trimmed = if let Some(suffix) = body.trim().strip_suffix('/') {
+			suffix
+		} else {
+			body.trim()
+		};
+
+		let mut wrk = trimmed;
+		loop {
+			let key_end_idx = wrk.find(|c: char| c == ' ' || c == '=');
+
+			match key_end_idx {
+				None => {
+					// boolean ends body
+					if wrk == key {
+						return Some("");
+					} else {
+						break;
+					}
+				}
+				Some(idx) => match &wrk[idx..idx + 1] {
+					" " => {
+						// boolean
+						if &wrk[..idx] == key {
+							return Some("");
+						} else {
+							wrk = &wrk[idx + 1..];
+						}
+					}
+					"=" => {
+						// key-value
+						let found_name = &wrk[..idx];
+
+						// we're just assuming the attributes are properly
+						// formed right now. Skips the `=` and the `"` that
+						// should be there but we don't check for
+						wrk = &wrk[idx + 2..];
+						let end = wrk.find('"').unwrap();
+						let value = &wrk[..end];
+						wrk = &wrk[end + 1..].trim_start();
+
+						if found_name == key {
+							return Some(value);
+						}
+					}
+					_ => unreachable!(),
+				},
+			}
+		}
+
+		None
+	}
+
+	pub fn id(&self) -> Option<&str> {
+		match self.get_attribute("id") {
+			None => None,
+			Some("") => None,
+			Some(id) => Some(id),
+		}
+	}
+
+	pub fn has_class(&self, name: &str) -> bool {
+		match self.get_attribute("class") {
+			None => false,
+			Some(classes) => {
+				for class in classes.split(' ') {
+					if class == name {
+						return true;
+					}
+				}
+
+				false
+			}
+		}
+	}
+
+	pub fn append_child<N: Into<Node>>(&mut self, node: N) {
+		let node = node.into();
+		self.children.push(node);
+	}
+
+	/// Replace all children with one [Node::Text]
+	pub fn set_inner_text<S: Into<String>>(&mut self, txt: S) {
+		self.children = vec![Node::Text(txt.into())];
+	}
+}
+
+impl fmt::Display for Tag {
+	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+		let Tag {
+			name,
+			body,
+			self_closing,
+			children,
+		} = self;
+
+		let formatted_body = if let Some(body) = body {
+			format!(" {body}")
+		} else {
+			String::from("")
+		};
+
+		if *self_closing {
+			// we ignore our children if we're self-closing.
+			write!(f, "<{name} {}/>", body.as_deref().unwrap_or_default())
+		} else {
+			write!(f, "<{name}{formatted_body}>")?;
+			for child in children {
+				write!(f, "{}", child)?;
+			}
+			write!(f, "</{name}>")
+		}
+	}
+}
+
+#[cfg(test)]
+mod test {
+	use crate::Tag;
+
+	#[test]
+	fn tag_finds_boolen_attribute() {
+		let tag = Tag {
+			name: "div".into(),
+			body: Some("contenteditable".into()),
+			self_closing: false,
+			children: vec![],
+		};
+		assert!(tag.get_attribute("contenteditable").is_some())
+	}
+
+	#[test]
+	fn tag_finds_kv_attribute() {
+		let tag = Tag {
+			name: "script".into(),
+			body: Some("src=\"script.js\"".into()),
+			self_closing: false,
+			children: vec![],
+		};
+		assert_eq!(tag.get_attribute("src"), Some("script.js"))
+	}
+
+	#[test]
+	fn tag_finds_boolean_in_centre() {
+		let tag = Tag {
+			name: "div".into(),
+			body: Some("id=\"divy\" contenteditable style=\"display: none;\"".into()),
+			self_closing: false,
+			children: vec![],
+		};
+		assert!(tag.get_attribute("contenteditable").is_some());
+	}
+}