hypertext_garnish/
html.rs

1use serde::Deserialize;
2
3#[derive(Debug, Clone, Eq, PartialEq, Deserialize)]
4pub struct Attribute {
5    name: String,
6    value: Option<String>,
7}
8
9impl Attribute {
10    pub fn new(name: String, value: String) -> Self {
11        Self { name, value: Some(value) }
12    }
13
14    pub fn toggle(name: String) -> Self {
15        Self { name, value: None }
16    }
17}
18
19impl ToString for Attribute {
20    fn to_string(&self) -> String {
21        match &self.value {
22            Some(value) => {
23                format!("{}=\"{}\"", self.name, value)
24            }
25            None => self.name.to_string()
26        }
27    }
28}
29
30#[derive(Debug, Clone, Eq, PartialEq, Deserialize)]
31pub enum Node {
32    Text(String),
33    Comment(String),
34    Element {
35        tag: String,
36        #[serde(default)]
37        attributes: Vec<Attribute>,
38        #[serde(default)]
39        children: Vec<Node>,
40    },
41}
42
43impl Node {
44    pub fn element(tag: String, attributes: Vec<Attribute>, children: Vec<Node>) -> Self {
45        Self::Element {
46            tag,
47            attributes,
48            children,
49        }
50    }
51
52    pub fn text(text: String) -> Self {
53        Self::Text(text)
54    }
55
56    pub fn comment(text: String) -> Self {
57        Self::Comment(text)
58    }
59}
60
61impl ToString for Node {
62    fn to_string(&self) -> String {
63        match self {
64            Node::Text(s) => s.clone(),
65            Node::Comment(s) => format!("<!-- {} -->", s),
66            Node::Element {
67                tag,
68                attributes,
69                children,
70            } => {
71                let child_text = children
72                    .iter()
73                    .map(|c| c.to_string())
74                    .collect::<Vec<String>>()
75                    .join("");
76
77                let open_tag = match attributes.is_empty() {
78                    true => format!("<{}>", tag),
79                    false => {
80                        let attribute_text = attributes
81                            .iter()
82                            .map(Attribute::to_string)
83                            .collect::<Vec<String>>()
84                            .join(" ");
85                        format!("<{} {}>", tag, attribute_text)
86                    }
87                };
88                format!("{}{}</{}>", open_tag, child_text, tag)
89            }
90        }
91    }
92}
93
94#[cfg(test)]
95mod to_string {
96    use crate::html::{Attribute, Node};
97
98    #[test]
99    fn single_element() {
100        let element = Node::element("body".to_string(), vec![], vec![]);
101
102        assert_eq!(element.to_string(), "<body></body>");
103    }
104
105    #[test]
106    fn single_element_with_text() {
107        let element = Node::element(
108            "body".to_string(),
109            vec![],
110            vec![Node::text("Some text".to_string())],
111        );
112
113        assert_eq!(element.to_string(), "<body>Some text</body>");
114    }
115
116    #[test]
117    fn attribute() {
118        let attr = Attribute::new("class".to_string(), "my-class".to_string());
119        assert_eq!(attr.to_string(), "class=\"my-class\"")
120    }
121
122    #[test]
123    fn attribute_no_value() {
124        let attr = Attribute::toggle("class".to_string());
125        assert_eq!(attr.to_string(), "class")
126    }
127
128    #[test]
129    fn single_element_with_attributes() {
130        let element = Node::element(
131            "body".to_string(),
132            vec![
133                Attribute::new("class".to_string(), "my-class".to_string()),
134                Attribute::new("width".to_string(), "100".to_string()),
135            ],
136            vec![],
137        );
138
139        assert_eq!(
140            element.to_string(),
141            "<body class=\"my-class\" width=\"100\"></body>"
142        );
143    }
144
145    #[test]
146    fn child_elements() {
147        let element = Node::element(
148            "body".to_string(),
149            vec![],
150            vec![Node::element(
151                "h1".to_string(),
152                vec![],
153                vec![Node::text("Heading".to_string())],
154            )],
155        );
156
157        assert_eq!(element.to_string(), "<body><h1>Heading</h1></body>");
158    }
159
160    #[test]
161    fn child_elements_then_text() {
162        let element = Node::element(
163            "body".to_string(),
164            vec![],
165            vec![
166                Node::element(
167                    "h1".to_string(),
168                    vec![],
169                    vec![Node::text("Heading".to_string())],
170                ),
171                Node::text("Some text".to_string()),
172            ],
173        );
174
175        assert_eq!(
176            element.to_string(),
177            "<body><h1>Heading</h1>Some text</body>"
178        );
179    }
180
181    #[test]
182    fn comment() {
183        let element = Node::comment("Some comments".to_string());
184
185        assert_eq!(element.to_string(), "<!-- Some comments -->");
186    }
187}