diff options
Diffstat (limited to 'src/tag.rs')
-rw-r--r-- | src/tag.rs | 177 |
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()); + } +} |