diff --git a/src/html5_parser/element_class.rs b/src/html5_parser/element_class.rs new file mode 100644 index 000000000..b18afbf21 --- /dev/null +++ b/src/html5_parser/element_class.rs @@ -0,0 +1,157 @@ +use std::collections::HashMap; + +#[derive(Debug, Clone, PartialEq)] +pub struct ElementClass { + /// a map of classes applied to an HTML element. + /// key = name, value = is_active + /// the is_active is used to toggle a class (JavaScript API) + class_map: HashMap, +} + +impl Default for ElementClass { + fn default() -> Self { + Self::new() + } +} + +impl ElementClass { + /// Initialise a new (empty) ElementClass + pub fn new() -> Self { + ElementClass { + class_map: HashMap::new(), + } + } + + /// Initialize a class from a class string + /// with space-delimited class names + pub fn from_string(class_string: &str) -> Self { + let mut class_map_local = HashMap::new(); + let classes = class_string.split_whitespace(); + for class_name in classes { + class_map_local.insert(class_name.to_owned(), true); + } + + ElementClass { + class_map: class_map_local, + } + } + + /// Count the number of classes (active or inactive) + /// assigned to an element + pub fn len(&self) -> usize { + self.class_map.len() + } + + /// Check if any classes are present + pub fn is_empty(&self) -> bool { + self.class_map.is_empty() + } + + /// Check if class name exists + pub fn contains(&self, name: &str) -> bool { + self.class_map.contains_key(name) + } + + /// Add a new class (if already exists, does nothing) + pub fn add(&mut self, name: &str) { + // by default, adding a new class will be active. + // however, map.insert will update a key if it exists + // and we don't want to overwrite an inactive class to make it active unintentionally + // so we ignore this operation if the class already exists + if !self.contains(name) { + self.class_map.insert(name.to_owned(), true); + } + } + + /// Remove a class (does nothing if not exists) + pub fn remove(&mut self, name: &str) { + self.class_map.remove(name); + } + + /// Toggle a class active/inactive. Does nothing if class doesn't exist + pub fn toggle(&mut self, name: &str) { + if let Some(is_active) = self.class_map.get_mut(name) { + *is_active = !*is_active; + } + } + + /// Set explicitly if a class is active or not. Does nothing if class doesn't exist + pub fn set_active(&mut self, name: &str, is_active: bool) { + if let Some(is_active_item) = self.class_map.get_mut(name) { + *is_active_item = is_active; + } + } + + /// Check if a class is active. Returns false if class doesn't exist + pub fn is_active(&self, name: &str) -> bool { + if let Some(is_active) = self.class_map.get(name) { + return *is_active; + } + + false + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn is_empty() { + let mut classes = ElementClass::new(); + assert!(classes.is_empty()); + classes.add("one"); + assert!(!classes.is_empty()); + } + + #[test] + fn count_classes() { + let mut classes = ElementClass::new(); + classes.add("one"); + classes.add("two"); + assert_eq!(classes.len(), 2); + } + + #[test] + fn contains_nonexistant_class() { + let classes = ElementClass::new(); + assert!(!classes.contains("nope")); + } + + #[test] + fn contains_valid_class() { + let mut classes = ElementClass::new(); + classes.add("yep"); + assert!(classes.contains("yep")); + } + + #[test] + fn add_class() { + let mut classes = ElementClass::new(); + classes.add("yep"); + assert!(classes.is_active("yep")); + + classes.set_active("yep", false); + classes.add("yep"); // should be ignored + assert!(!classes.is_active("yep")); + } + + #[test] + fn remove_class() { + let mut classes = ElementClass::new(); + classes.add("yep"); + classes.remove("yep"); + assert!(!classes.contains("yep")); + } + + #[test] + fn toggle_class() { + let mut classes = ElementClass::new(); + classes.add("yep"); + assert!(classes.is_active("yep")); + classes.toggle("yep"); + assert!(!classes.is_active("yep")); + classes.toggle("yep"); + assert!(classes.is_active("yep")); + } +} diff --git a/src/html5_parser/mod.rs b/src/html5_parser/mod.rs index 1695877a1..1557d3f5f 100644 --- a/src/html5_parser/mod.rs +++ b/src/html5_parser/mod.rs @@ -7,4 +7,6 @@ pub mod dom; pub mod error_logger; pub mod input_stream; +pub mod element_class; + mod node_arena; diff --git a/src/html5_parser/node.rs b/src/html5_parser/node.rs index 75b05351f..8a5d86548 100644 --- a/src/html5_parser/node.rs +++ b/src/html5_parser/node.rs @@ -1,3 +1,4 @@ +use crate::html5_parser::element_class::ElementClass; use derive_more::Display; use std::collections::HashMap; @@ -98,6 +99,8 @@ pub struct Node { pub namespace: Option, /// actual data of the node pub data: NodeData, + /// CSS classes (only relevant for NodeType::Element, otherwise None) + pub classes: Option, } impl Node { @@ -117,6 +120,7 @@ impl Clone for Node { name: self.name.clone(), namespace: self.namespace.clone(), data: self.data.clone(), + classes: self.classes.clone(), } } } @@ -131,6 +135,7 @@ impl Node { data: NodeData::Document {}, name: "".to_string(), namespace: None, + classes: None, } } @@ -146,6 +151,7 @@ impl Node { }, name: name.to_string(), namespace: Some(namespace.into()), + classes: Some(ElementClass::new()), } } @@ -160,6 +166,7 @@ impl Node { }, name: "".to_string(), namespace: None, + classes: None, } } @@ -174,6 +181,7 @@ impl Node { }, name: "".to_string(), namespace: None, + classes: None, } } @@ -246,9 +254,9 @@ impl Node { /// Get a constant reference to the attribute value /// (or None if attribute doesn't exist) - pub fn get_attribute(&self, name: &str) -> Result, String> { + pub fn get_attribute(&self, name: &str) -> Option<&String> { if self.type_of() != NodeType::Element { - return Err(ATTRIBUTE_NODETYPE_ERR_MSG.into()); + return None; } let mut value: Option<&String> = None; @@ -256,14 +264,14 @@ impl Node { value = attributes.get(name); } - Ok(value) + value } /// Get a mutable reference to the attribute value /// (or None if the attribute doesn't exist) - pub fn get_mut_attribute(&mut self, name: &str) -> Result, String> { + pub fn get_mut_attribute(&mut self, name: &str) -> Option<&mut String> { if self.type_of() != NodeType::Element { - return Err(ATTRIBUTE_NODETYPE_ERR_MSG.into()); + return None; } let mut value: Option<&mut String> = None; @@ -271,7 +279,7 @@ impl Node { value = attributes.get_mut(name); } - Ok(value) + value } /// Remove all attributes @@ -649,7 +657,7 @@ mod tests { let mut node = Node::new_element("name", attr.clone(), HTML_NAMESPACE); assert!(node.insert_attribute("key", "value").is_ok()); - let value = node.get_attribute("key").unwrap().unwrap(); + let value = node.get_attribute("key").unwrap(); assert_eq!(value, "value"); } @@ -676,7 +684,7 @@ mod tests { fn get_attribute_non_element() { let node = Node::new_document(); let result = node.get_attribute("name"); - assert!(result.is_err()); + assert!(result.is_none()); } #[test] @@ -686,7 +694,7 @@ mod tests { let node = Node::new_element("name", attr.clone(), HTML_NAMESPACE); - let value = node.get_attribute("key").unwrap().unwrap(); + let value = node.get_attribute("key").unwrap(); assert_eq!(value, "value"); } @@ -694,7 +702,7 @@ mod tests { fn get_mut_attribute_non_element() { let mut node = Node::new_document(); let result = node.get_mut_attribute("key"); - assert!(result.is_err()); + assert!(result.is_none()); } #[test] @@ -704,10 +712,10 @@ mod tests { let mut node = Node::new_element("name", attr.clone(), HTML_NAMESPACE); - let value = node.get_mut_attribute("key").unwrap().unwrap(); + let value = node.get_mut_attribute("key").unwrap(); value.push_str(" appended"); - let value = node.get_attribute("key").unwrap().unwrap(); + let value = node.get_attribute("key").unwrap(); assert_eq!(value, "value appended"); } diff --git a/src/html5_parser/parser/mod.rs b/src/html5_parser/parser/mod.rs index 2307a9a31..32b9e528b 100644 --- a/src/html5_parser/parser/mod.rs +++ b/src/html5_parser/parser/mod.rs @@ -5,6 +5,7 @@ mod quirks; // ------------------------------------------------------------ use super::node::NodeId; +use crate::html5_parser::element_class::ElementClass; use crate::html5_parser::error_logger::{ErrorLogger, ParseError, ParserError}; use crate::html5_parser::input_stream::InputStream; use crate::html5_parser::node::{Node, NodeData, HTML_NAMESPACE, MATHML_NAMESPACE, SVG_NAMESPACE}; @@ -3323,7 +3324,19 @@ impl<'a> Html5Parser<'a> { let adjusted_insert_location = self.adjusted_insert_location(None); // let parent_id = current_node!(self).id; - let node = self.create_node(token, namespace.unwrap_or(HTML_NAMESPACE)); + let mut node = self.create_node(token, namespace.unwrap_or(HTML_NAMESPACE)); + + // add CSS classes from class attribute in element + // e.g.,
+ // NOTE: it seems in base rust, you can't really combine "if" and "if let" so I + // had to introduce more nesting... please suggest cleaner alternatives if any! + if let Ok(contains_class) = node.contains_attribute("class") { + if contains_class { + if let Some(class_string) = node.get_attribute("class") { + node.classes = Some(ElementClass::from_string(class_string)); + } + } + } // if parent_id is possible to insert element (for instance: document already has child element etc) // if parser not created as part of html fragmentparsing algorithm @@ -3679,4 +3692,68 @@ mod test { println!("{}", parser.document); } + + #[test] + fn element_no_classes() { + let mut stream = InputStream::new(); + stream.read_from_str("
", Some(Encoding::UTF8)); + + let mut parser = Html5Parser::new(&mut stream); + let (doc, _) = parser.parse(); + + assert!(doc.get_root().classes.is_none()); + } + + #[test] + fn element_with_classes() { + let mut stream = InputStream::new(); + stream.read_from_str("
", Some(Encoding::UTF8)); + + let mut parser = Html5Parser::new(&mut stream); + let (doc, _) = parser.parse(); + + // document -> html -> head -> body -> div + let div = doc.get_node_by_id(NodeId(4)).unwrap(); + assert!(div.classes.is_some()); + + if let Some(classes) = div.classes.as_ref() { + assert_eq!(classes.len(), 3); + + assert!(classes.contains("one")); + assert!(classes.contains("two")); + assert!(classes.contains("three")); + + assert!(classes.is_active("one")); + assert!(classes.is_active("two")); + assert!(classes.is_active("three")); + } + } + + #[test] + fn element_with_classes_extra_whitespace() { + let mut stream = InputStream::new(); + stream.read_from_str( + "
", + Some(Encoding::UTF8), + ); + + let mut parser = Html5Parser::new(&mut stream); + let (doc, _) = parser.parse(); + + // document -> html -> head -> body -> div + let div = doc.get_node_by_id(NodeId(4)).unwrap(); + assert!(div.classes.is_some()); + + if let Some(classes) = div.classes.as_ref() { + assert_eq!(classes.len(), 3); + + assert!(classes.contains("one")); + assert!(classes.contains("two")); + assert!(classes.contains("three")); + + assert!(classes.is_active("one")); + assert!(classes.is_active("two")); + assert!(classes.is_active("three")); + } + } }