use core::fmt; use std::{iter::Peekable, str::CharIndices}; 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, pub self_closing: bool, pub children: Vec, } 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>(&mut self, node: N) { let node = node.into(); self.children.push(node); } /// Replace all children with one [Node::Text] pub fn set_inner_text>(&mut self, txt: S) { self.children = vec![Node::Text(txt.into())]; } pub fn child_tags(&self) -> TagIterator { TagIterator { inner: self.children.iter(), } } pub fn child_tags_mut(&mut self) -> TagIteratorMut { TagIteratorMut { inner: self.children.iter_mut(), } } pub fn has_tag(&self, tag: &str) -> bool { self.child_tags().any(|t| t.name == tag) } pub fn by_tag_mut<'a>(&'a mut self, looking: &str) -> Option<&'a mut Tag> { for tag in self.child_tags_mut() { if tag.name == looking { return Some(tag); } if let Some(found) = tag.by_tag_mut(looking) { return Some(found); } } None } } 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, "") } } } pub struct TagIterator<'a> { pub(crate) inner: std::slice::Iter<'a, Node>, } impl<'a> Iterator for TagIterator<'a> { type Item = &'a Tag; fn next(&mut self) -> Option { loop { match self.inner.next() { None => break None, Some(Node::Tag(ref tag)) => break Some(tag), Some(_) => continue, } } } } pub struct TagIteratorMut<'a> { pub(crate) inner: std::slice::IterMut<'a, Node>, } impl<'a> Iterator for TagIteratorMut<'a> { type Item = &'a mut Tag; fn next(&mut self) -> Option { loop { match self.inner.next() { None => break None, Some(Node::Tag(ref mut tag)) => break Some(tag), Some(_) => continue, } } } } #[cfg(test)] mod test { use crate::Tag; use super::peek_skip_while; #[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()); } // yes, this is a bad name. #[test] fn peek_skip_while_works() { let str = "\t no whitespace"; let mut chari = str.char_indices().peekable(); peek_skip_while(&mut chari, |c| c.is_whitespace()); let next_idx = chari.next().unwrap().0; assert_eq!(&str[next_idx..], "no whitespace"); } } struct Attribute<'body> { name: &'body str, content: Option<&'body str>, start: usize, len: usize, } fn find_attribute<'body>(body: &'body str, attribute: &str) -> Option> { let mut chari = body.char_indices().peekable(); 'big: loop { // skip whitespace peek_skip_while(&mut chari, |c| c.is_whitespace()); let key_start = chari.next().unwrap(); // find end of key peek_skip_while(&mut chari, |c| c.is_alphanumeric()); let key_after = chari.next().unwrap(); } todo!() } fn peek_skip_while

(iter: &mut Peekable, mut predicate: P) where P: FnMut(&char) -> bool, { loop { match iter.peek() { Some((_, c)) if predicate(c) => { iter.next(); } _ => break, } } }