hypertext_garnish 0.3.0

Serialization/deserialization of garnish data to HTML/CSS.
Documentation
use serde::Deserialize;

#[derive(Debug, Clone, Eq, PartialEq, Deserialize)]
pub struct Attribute {
    name: String,
    value: Option<String>,
}

impl Attribute {
    pub fn new(name: String, value: String) -> Self {
        Self { name, value: Some(value) }
    }

    pub fn toggle(name: String) -> Self {
        Self { name, value: None }
    }
}

impl ToString for Attribute {
    fn to_string(&self) -> String {
        match &self.value {
            Some(value) => {
                format!("{}=\"{}\"", self.name, value)
            }
            None => self.name.to_string()
        }
    }
}

#[derive(Debug, Clone, Eq, PartialEq, Deserialize)]
pub enum Node {
    Text(String),
    Comment(String),
    Element {
        tag: String,
        #[serde(default)]
        attributes: Vec<Attribute>,
        #[serde(default)]
        children: Vec<Node>,
    },
}

impl Node {
    pub fn element(tag: String, attributes: Vec<Attribute>, children: Vec<Node>) -> Self {
        Self::Element {
            tag,
            attributes,
            children,
        }
    }

    pub fn text(text: String) -> Self {
        Self::Text(text)
    }

    pub fn comment(text: String) -> Self {
        Self::Comment(text)
    }
}

impl ToString for Node {
    fn to_string(&self) -> String {
        match self {
            Node::Text(s) => s.clone(),
            Node::Comment(s) => format!("<!-- {} -->", s),
            Node::Element {
                tag,
                attributes,
                children,
            } => {
                let child_text = children
                    .iter()
                    .map(|c| c.to_string())
                    .collect::<Vec<String>>()
                    .join("");

                let open_tag = match attributes.is_empty() {
                    true => format!("<{}>", tag),
                    false => {
                        let attribute_text = attributes
                            .iter()
                            .map(Attribute::to_string)
                            .collect::<Vec<String>>()
                            .join(" ");
                        format!("<{} {}>", tag, attribute_text)
                    }
                };
                format!("{}{}</{}>", open_tag, child_text, tag)
            }
        }
    }
}

#[cfg(test)]
mod to_string {
    use crate::html::{Attribute, Node};

    #[test]
    fn single_element() {
        let element = Node::element("body".to_string(), vec![], vec![]);

        assert_eq!(element.to_string(), "<body></body>");
    }

    #[test]
    fn single_element_with_text() {
        let element = Node::element(
            "body".to_string(),
            vec![],
            vec![Node::text("Some text".to_string())],
        );

        assert_eq!(element.to_string(), "<body>Some text</body>");
    }

    #[test]
    fn attribute() {
        let attr = Attribute::new("class".to_string(), "my-class".to_string());
        assert_eq!(attr.to_string(), "class=\"my-class\"")
    }

    #[test]
    fn attribute_no_value() {
        let attr = Attribute::toggle("class".to_string());
        assert_eq!(attr.to_string(), "class")
    }

    #[test]
    fn single_element_with_attributes() {
        let element = Node::element(
            "body".to_string(),
            vec![
                Attribute::new("class".to_string(), "my-class".to_string()),
                Attribute::new("width".to_string(), "100".to_string()),
            ],
            vec![],
        );

        assert_eq!(
            element.to_string(),
            "<body class=\"my-class\" width=\"100\"></body>"
        );
    }

    #[test]
    fn child_elements() {
        let element = Node::element(
            "body".to_string(),
            vec![],
            vec![Node::element(
                "h1".to_string(),
                vec![],
                vec![Node::text("Heading".to_string())],
            )],
        );

        assert_eq!(element.to_string(), "<body><h1>Heading</h1></body>");
    }

    #[test]
    fn child_elements_then_text() {
        let element = Node::element(
            "body".to_string(),
            vec![],
            vec![
                Node::element(
                    "h1".to_string(),
                    vec![],
                    vec![Node::text("Heading".to_string())],
                ),
                Node::text("Some text".to_string()),
            ],
        );

        assert_eq!(
            element.to_string(),
            "<body><h1>Heading</h1>Some text</body>"
        );
    }

    #[test]
    fn comment() {
        let element = Node::comment("Some comments".to_string());

        assert_eq!(element.to_string(), "<!-- Some comments -->");
    }
}