diff --git a/src/lib.rs b/src/lib.rs index 5488aac..e8f2628 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -51,11 +51,7 @@ pub enum Doctype { /// Node of DOM #[derive(Debug, Clone)] pub enum Node { - Element { - name: String, - attrs: Vec<(String, String)>, - children: Vec, - }, + Element(Element), Text(String), Comment(String), Doctype(Doctype), @@ -74,34 +70,53 @@ impl Node { matches!(self, Node::Element { .. }) } + #[deprecated(note = "Please use `is_element` instead")] + pub fn into_element(self) -> Element { + match self { + Node::Element(element) => element, + _ => panic!("{:?} is not an element", self), + } + } + /// Convert the node into an element. /// - /// Warning: The program will panic if it fails to convert. - /// So take care to use this method unless you are sure. + /// Returns `None` if the node is not an element. /// /// Example: /// ``` /// use html_editor::{Node, Element}; /// /// let a: Node = Node::new_element("div", vec![("id", "app")], vec![]); - /// let a: Element = a.into_element(); + /// assert!(a.as_element().is_some()); /// /// let b: Node = Node::Text("hello".to_string()); - /// // The next line will panic at 'Text("hello") is not an element' - /// // let b: Element = a.into_element(); + /// assert!(b.as_element().is_none()); /// ``` - pub fn into_element(self) -> Element { + pub fn as_element(&self) -> Option<&Element> { match self { - Node::Element { - name, - attrs, - children, - } => Element { - name, - attrs, - children, - }, - _ => panic!("{:?} is not an element", self), + Node::Element(element) => Some(element), + _ => None, + } + } + + /// Convert the node into a mutable element. + /// + /// Returns `None` if the node is not an element. + /// + /// Example: + /// ``` + /// use html_editor::{Node, Element}; + /// + /// let mut a: Node = Node::new_element("div", vec![("id", "app")], vec![]); + /// assert!(a.as_element_mut().is_some()); + /// + /// let mut b: Node = Node::Text("hello".to_string()); + /// assert!(b.as_element_mut().is_none()); + /// ``` + pub fn as_element_mut(&mut self) -> Option<&mut Element> { + match self { + Node::Element(element) => Some(element), + _ => None, } } @@ -119,7 +134,7 @@ impl Node { /// ); /// ``` pub fn new_element(name: &str, attrs: Vec<(&str, &str)>, children: Vec) -> Node { - Node::Element { + Element { name: name.to_string(), attrs: attrs .into_iter() @@ -127,11 +142,12 @@ impl Node { .collect(), children, } + .into_node() } } /// HTML Element -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct Element { pub name: String, pub attrs: Vec<(String, String)>, @@ -151,3 +167,15 @@ impl Element { } } } + +impl Element { + pub fn into_node(self) -> Node { + Node::Element(self) + } +} + +impl From for Node { + fn from(element: Element) -> Self { + Node::Element(element) + } +} diff --git a/src/operation/edit.rs b/src/operation/edit.rs index 2fe7f2a..c09127b 100644 --- a/src/operation/edit.rs +++ b/src/operation/edit.rs @@ -76,8 +76,8 @@ impl Editable for Vec { Node::Element { .. } => true, }); for node in self.iter_mut() { - if let Node::Element { children, .. } = node { - children.trim(); + if let Node::Element(el) = node { + el.children.trim(); } } self @@ -85,19 +85,14 @@ impl Editable for Vec { fn insert_to(&mut self, selector: &Selector, target: Node) -> &mut Self { for node in self.iter_mut() { - if let Node::Element { - name, - attrs, - children, - } = node - { - children.insert_to(selector, target.clone()); + if let Node::Element(el) = node { + el.children.insert_to(selector, target.clone()); if selector.matches(&Element { - name: name.clone(), - attrs: attrs.clone(), + name: el.name.clone(), + attrs: el.attrs.clone(), children: vec![], }) { - children.push(target.clone()); + el.children.push(target.clone()); } } } @@ -106,10 +101,10 @@ impl Editable for Vec { fn remove_by(&mut self, selector: &Selector) -> &mut Self { self.retain(|node| { - if let Node::Element { name, attrs, .. } = node { + if let Node::Element(el) = node { let element = Element { - name: name.clone(), - attrs: attrs.clone(), + name: el.name.clone(), + attrs: el.attrs.clone(), children: vec![], }; return !selector.matches(&element); @@ -117,8 +112,8 @@ impl Editable for Vec { true }); for node in self.iter_mut() { - if let Node::Element { children, .. } = node { - children.remove_by(selector); + if let Node::Element(el) = node { + el.remove_by(selector); } } self diff --git a/src/operation/html.rs b/src/operation/html.rs index 5247602..de70bee 100644 --- a/src/operation/html.rs +++ b/src/operation/html.rs @@ -66,7 +66,7 @@ impl Htmlifiable for Element { impl Htmlifiable for Node { fn html(&self) -> String { match self { - Node::Element { .. } => self.clone().into_element().html(), + Node::Element(element) => element.html(), Node::Text(text) => text.to_string(), Node::Comment(comment) => format!("", comment), Node::Doctype(doctype) => match &doctype { diff --git a/src/operation/query.rs b/src/operation/query.rs index 08fa830..1e61110 100644 --- a/src/operation/query.rs +++ b/src/operation/query.rs @@ -22,9 +22,9 @@ pub trait Queryable { /// "#; /// /// let selector: Selector = Selector::from("#app"); - /// let app: Element = parse(html).unwrap().query(&selector).unwrap(); + /// let app: &Element = parse(html).unwrap().query(&selector).unwrap(); /// ``` - fn query(&self, selector: &Selector) -> Option; + fn query(&self, selector: &Selector) -> Option<&Element>; /// Query all the nodes in `self` for the given selector. /// @@ -47,20 +47,94 @@ pub trait Queryable { /// "#; /// /// let selector: Selector = Selector::from(".btn"); - /// let app: Vec = parse(html).unwrap().query_all(&selector); + /// let buttons: Vec<&Element> = parse(html).unwrap().query_all(&selector); /// ``` - fn query_all(&self, selector: &Selector) -> Vec; + fn query_all(&self, selector: &Selector) -> Vec<&Element>; + + /// Query the node in `self` as mutable for the given selector. + /// + /// ``` + /// use html_editor::{parse, Element}; + /// use html_editor::operation::*; + /// + /// let html = r#" + /// + /// + /// + /// + /// App + /// + /// + ///
+ /// + /// "#; + /// + /// let selector: Selector = Selector::from("#app"); + /// let app: &mut Element = parse(html).unwrap().query_mut(&selector).unwrap(); + /// ``` + fn query_mut(&mut self, selector: &Selector) -> Option<&mut Element>; + + /// Executes a given function for the node in `self` for the given selector. + /// + /// ``` + /// use html_editor::{parse, Element, Node}; + /// use html_editor::operation::*; + /// + /// let html = r#" + /// + /// + /// + /// + /// App + /// + /// + /// + /// + /// + /// + /// "#; + /// + /// // Add a class to all the input elements + /// let selector: Selector = Selector::from("input"); + /// let mut doc: Vec = parse(html).unwrap(); + /// doc.execute_for(&selector, |elem| { + /// elem.attrs.push(("class".to_string(), "input".to_string())); + /// }); + /// ``` + fn execute_for(&mut self, selector: &Selector, f: impl FnMut(&mut Element)); +} + +// We meed this function to allow the trait interface to use `impl FnMut(&mut Element)` instead of `&mut impl FnMut(&mut Element)` +fn nodes_execute_for_internal( + nodes: &mut Vec, + selector: &Selector, + f: &mut impl FnMut(&mut Element), +) { + for node in nodes { + if let Some(element) = node.as_element_mut() { + // Recursively traverse the descendants nodes + element_execute_for_internal(element, selector, f); + } + } +} + +// We meed this function to allow the trait interface to use `impl FnMut(&mut Element)` instead of `&mut impl FnMut(&mut Element)` +fn element_execute_for_internal( + element: &mut Element, + selector: &Selector, + f: &mut impl FnMut(&mut Element), +) { + if selector.matches(element) { + f(element); + } + nodes_execute_for_internal(&mut element.children, selector, f); } impl Queryable for Vec { - fn query(&self, selector: &Selector) -> Option { + fn query(&self, selector: &Selector) -> Option<&Element> { for node in self { - if node.is_element() { - let element = node.clone().into_element(); - - if selector.matches(&element) { - return Some(element); - } else if let Some(elem) = element.query(selector) { + if let Some(element) = node.as_element() { + if let Some(elem) = element.query(selector) { return Some(elem); } } @@ -68,30 +142,92 @@ impl Queryable for Vec { None } - fn query_all(&self, selector: &Selector) -> Vec { + fn query_all(&self, selector: &Selector) -> Vec<&Element> { let mut elements = Vec::new(); for node in self { - if node.is_element() { - let element = node.clone().into_element(); + if let Some(element) = node.as_element() { // Recursively traverse the descendants nodes let sub_elements = element.query_all(selector); elements.extend(sub_elements); - // Check if this element matches. If so, push it to the `elements` - if selector.matches(&element) { - elements.push(element); - } } } elements } + + fn query_mut(&mut self, selector: &Selector) -> Option<&mut Element> { + for node in self { + if let Some(element) = node.as_element_mut() { + if let Some(elem) = element.query_mut(selector) { + return Some(elem); + } + } + } + None + } + + fn execute_for(&mut self, selector: &Selector, mut f: impl FnMut(&mut Element)) { + nodes_execute_for_internal(self, selector, &mut f); + } } impl Queryable for Element { - fn query(&self, selector: &Selector) -> Option { - self.children.query(selector) + fn query(&self, selector: &Selector) -> Option<&Element> { + if selector.matches(self) { + Some(self) + } else { + self.children.query(selector) + } + } + + fn query_all(&self, selector: &Selector) -> Vec<&Element> { + let mut elements = self.children.query_all(selector); + if selector.matches(self) { + elements.push(self); + } + elements } - fn query_all(&self, selector: &Selector) -> Vec { - self.children.query_all(selector) + fn query_mut(&mut self, selector: &Selector) -> Option<&mut Element> { + if selector.matches(self) { + Some(self) + } else { + self.children.query_mut(selector) + } + } + + fn execute_for(&mut self, selector: &Selector, mut f: impl FnMut(&mut Element)) { + element_execute_for_internal(self, selector, &mut f); + } +} + +impl Queryable for Node { + fn query(&self, selector: &Selector) -> Option<&Element> { + if let Some(element) = self.as_element() { + element.query(selector) + } else { + None + } + } + + fn query_all(&self, selector: &Selector) -> Vec<&Element> { + if let Some(element) = self.as_element() { + element.query_all(selector) + } else { + Vec::new() + } + } + + fn query_mut(&mut self, selector: &Selector) -> Option<&mut Element> { + if let Some(element) = self.as_element_mut() { + element.query_mut(selector) + } else { + None + } + } + + fn execute_for(&mut self, selector: &Selector, f: impl FnMut(&mut Element)) { + if let Some(element) = self.as_element_mut() { + element.execute_for(selector, f); + } } } diff --git a/src/parse.rs b/src/parse.rs index 55f0edd..ce09245 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -9,7 +9,7 @@ mod attrs; mod token; -use crate::{data::VOID_TAGS, Node}; +use crate::{data::VOID_TAGS, Element, Node}; use token::Token; fn html_to_stack(html: &str) -> Result, String> { @@ -135,11 +135,14 @@ fn stack_to_dom(token_stack: Vec) -> Result, String> { let is_void_tag = VOID_TAGS.contains(&tag.as_str()); if start_tags_stack.is_empty() { if is_void_tag { - nodes.push(Node::Element { - name: tag.clone(), - attrs: attrs.clone(), - children: Vec::new(), - }); + nodes.push( + Element { + name: tag.clone(), + attrs: attrs.clone(), + children: Vec::new(), + } + .into_node(), + ); } else { start_tag_index = i; start_tags_stack.push(Token::Start(tag.clone(), attrs.clone())); @@ -155,7 +158,7 @@ fn stack_to_dom(token_stack: Vec) -> Result, String> { } Token::End(tag) => { let start_tag = match start_tags_stack.pop() { - Some(token) => token.into_node().into_element(), + Some(token) => token.into_element(), None => return Err(format!("No start tag matches ", tag)), }; if tag != &start_tag.name { @@ -165,11 +168,14 @@ fn stack_to_dom(token_stack: Vec) -> Result, String> { )); } if start_tags_stack.is_empty() { - nodes.push(Node::Element { - name: start_tag.name, - attrs: start_tag.attrs, - children: stack_to_dom(token_stack[start_tag_index + 1..i].to_vec())?, - }) + nodes.push( + Element { + name: start_tag.name, + attrs: start_tag.attrs, + children: stack_to_dom(token_stack[start_tag_index + 1..i].to_vec())?, + } + .into_node(), + ) } } _ => { @@ -182,7 +188,7 @@ fn stack_to_dom(token_stack: Vec) -> Result, String> { match start_tags_stack.pop() { Some(token) => { - let start_tag_name = token.into_node().into_element().name; + let start_tag_name = token.into_element().name; Err(format!("<{}> is not closed", start_tag_name)) } None => Ok(nodes), @@ -200,11 +206,14 @@ fn try_stack_to_dom(token_stack: Vec) -> Vec { let is_void_tag = VOID_TAGS.contains(&tag.as_str()); if start_tags_stack.is_empty() { if is_void_tag { - nodes.push(Node::Element { - name: tag.clone(), - attrs: attrs.clone(), - children: Vec::new(), - }); + nodes.push( + Element { + name: tag.clone(), + attrs: attrs.clone(), + children: Vec::new(), + } + .into_node(), + ); } else { start_tag_index = i; start_tags_stack.push(Token::Start(tag.clone(), attrs.clone())); @@ -220,7 +229,7 @@ fn try_stack_to_dom(token_stack: Vec) -> Vec { } Token::End(tag) => { let start_tag = match start_tags_stack.pop() { - Some(token) => token.into_node().into_element(), + Some(token) => token.into_element(), // It means the end tag is redundant, so we will omit // it and just start the next loop. None => continue, @@ -235,11 +244,16 @@ fn try_stack_to_dom(token_stack: Vec) -> Vec { } if start_tags_stack.is_empty() { - nodes.push(Node::Element { - name: start_tag.name, - attrs: start_tag.attrs, - children: try_stack_to_dom(token_stack[start_tag_index + 1..i].to_vec()), - }) + nodes.push( + Element { + name: start_tag.name, + attrs: start_tag.attrs, + children: try_stack_to_dom( + token_stack[start_tag_index + 1..i].to_vec(), + ), + } + .into_node(), + ) } } _ => { @@ -252,11 +266,12 @@ fn try_stack_to_dom(token_stack: Vec) -> Vec { while let Some(token) = start_tags_stack.pop() { let node = match token { - Token::Start(name, attrs) => Node::Element { + Token::Start(name, attrs) => Element { name, attrs, children: try_stack_to_dom(token_stack[start_tag_index + 1..].to_vec()), - }, + } + .into_node(), _ => unreachable!(), }; nodes = vec![node]; diff --git a/src/parse/token.rs b/src/parse/token.rs index d18270a..4fe9753 100644 --- a/src/parse/token.rs +++ b/src/parse/token.rs @@ -1,5 +1,5 @@ use crate::parse::attrs; -use crate::{Doctype, Node}; +use crate::{Doctype, Element, Node}; #[derive(Debug, Clone)] pub enum Token { @@ -89,24 +89,51 @@ impl Token { pub fn into_node(self) -> Node { match self { - Self::Start(name, attrs) => Node::Element { + Self::Start(name, attrs) => Element { name, attrs, children: Vec::new(), - }, - Self::End(name) => Node::Element { + } + .into_node(), + + Self::End(name) => Element { name, attrs: Vec::new(), children: Vec::new(), - }, - Self::Closing(name, attrs) => Node::Element { + } + .into_node(), + + Self::Closing(name, attrs) => Element { name, attrs, children: Vec::new(), - }, + } + .into_node(), + Self::Doctype(doctype) => Node::Doctype(doctype), Self::Comment(comment) => Node::Comment(comment), Self::Text(text) => Node::Text(text), } } + + pub fn into_element(self) -> Element { + match self { + Self::Start(name, attrs) => Element { + name, + attrs, + children: Vec::new(), + }, + Self::End(name) => Element { + name, + attrs: Vec::new(), + children: Vec::new(), + }, + Self::Closing(name, attrs) => Element { + name, + attrs, + children: Vec::new(), + }, + _ => panic!("Cannot convert token to element"), + } + } } diff --git a/tests/query.rs b/tests/query.rs index 3ee0ed5..db41eaf 100644 --- a/tests/query.rs +++ b/tests/query.rs @@ -24,14 +24,21 @@ fn nodes_query_all() { fn element_query() { let nodes = parse(HTML).unwrap(); let node = nodes.into_iter().nth(1).unwrap(); - let _element = node.into_element().query(&Selector::from("span")).unwrap(); + let _element = node + .as_element() + .unwrap() + .query(&Selector::from("span")) + .unwrap(); } #[test] fn element_query_all() { let nodes = parse(HTML).unwrap(); let node = nodes.into_iter().nth(1).unwrap(); - let _elements = node.into_element().query_all(&Selector::from("span")); + let _elements = node + .as_element() + .unwrap() + .query_all(&Selector::from("span")); } #[test]