Skip to main content

workers_rsx/
simple_element.rs

1use crate::html_escaping::escape_html;
2use crate::Render;
3use std::borrow::Cow;
4use std::collections::HashMap;
5use std::fmt::{Result, Write};
6
7/// Attribute value: either a key-value pair or a boolean attribute (no value)
8#[derive(Debug)]
9pub enum AttrValue<'a> {
10    Value(Cow<'a, str>),
11    Boolean,
12}
13
14type Attributes<'a> = Option<HashMap<&'a str, AttrValue<'a>>>;
15
16/// Simple HTML element tag
17#[derive(Debug)]
18pub struct SimpleElement<'a, T: Render> {
19    /// the HTML tag name, like `html`, `head`, `body`, `link`...
20    pub tag_name: &'a str,
21    pub attributes: Attributes<'a>,
22    pub contents: Option<T>,
23}
24
25/// HTML void elements that must not have a closing tag
26const VOID_ELEMENTS: &[&str] = &[
27    "area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param",
28    "source", "track", "wbr",
29];
30
31fn is_void_element(tag: &str) -> bool {
32    VOID_ELEMENTS.contains(&tag)
33}
34
35fn write_attributes<'a, W: Write>(maybe_attributes: Attributes<'a>, writer: &mut W) -> Result {
36    match maybe_attributes {
37        None => Ok(()),
38        Some(mut attributes) => {
39            for (key, value) in attributes.drain() {
40                match value {
41                    AttrValue::Boolean => {
42                        write!(writer, " {}", key)?;
43                    }
44                    AttrValue::Value(v) => {
45                        write!(writer, " {}=\"", key)?;
46                        escape_html(&v, writer)?;
47                        write!(writer, "\"")?;
48                    }
49                }
50            }
51            Ok(())
52        }
53    }
54}
55
56impl<T: Render> Render for SimpleElement<'_, T> {
57    fn render_into<W: Write>(self, writer: &mut W) -> Result {
58        let is_void = is_void_element(self.tag_name);
59
60        match self.contents {
61            None => {
62                write!(writer, "<{}", self.tag_name)?;
63                write_attributes(self.attributes, writer)?;
64                if is_void {
65                    write!(writer, ">")
66                } else {
67                    write!(writer, "></{}>", self.tag_name)
68                }
69            }
70            Some(renderable) => {
71                write!(writer, "<{}", self.tag_name)?;
72                write_attributes(self.attributes, writer)?;
73                write!(writer, ">")?;
74                renderable.render_into(writer)?;
75                if !is_void {
76                    write!(writer, "</{}>", self.tag_name)?;
77                }
78                Ok(())
79            }
80        }
81    }
82}
83
84#[cfg(test)]
85mod tests {
86    use super::*;
87    use std::borrow::Cow;
88
89    #[test]
90    fn script_empty_renders_open_and_close_tag() {
91        let el: SimpleElement<'_, ()> = SimpleElement {
92            tag_name: "script",
93            attributes: {
94                let mut attrs = HashMap::new();
95                attrs.insert("src", AttrValue::Value(Cow::Borrowed("app.js")));
96                Some(attrs)
97            },
98            contents: None,
99        };
100        let result = el.render();
101        assert!(
102            result.contains("></script>"),
103            "Expected closing tag, got: {}",
104            result
105        );
106        assert!(
107            !result.contains("/>"),
108            "Should not self-close script, got: {}",
109            result
110        );
111    }
112
113    #[test]
114    fn script_with_content_renders_open_and_close_tag() {
115        let el = SimpleElement {
116            tag_name: "script",
117            attributes: None,
118            contents: Some("console.log(1)"),
119        };
120        let result = el.render();
121        assert_eq!(result, "<script>console.log(1)</script>");
122    }
123
124    #[test]
125    fn div_empty_renders_open_and_close_tag() {
126        let el: SimpleElement<'_, ()> = SimpleElement {
127            tag_name: "div",
128            attributes: None,
129            contents: None,
130        };
131        assert_eq!(el.render(), "<div></div>");
132    }
133
134    #[test]
135    fn void_element_no_closing_tag() {
136        let el: SimpleElement<'_, ()> = SimpleElement {
137            tag_name: "br",
138            attributes: None,
139            contents: None,
140        };
141        assert_eq!(el.render(), "<br>");
142    }
143
144    #[test]
145    fn void_element_input_no_closing_tag() {
146        let el: SimpleElement<'_, ()> = SimpleElement {
147            tag_name: "input",
148            attributes: {
149                let mut attrs = HashMap::new();
150                attrs.insert("type", AttrValue::Value(Cow::Borrowed("text")));
151                Some(attrs)
152            },
153            contents: None,
154        };
155        let result = el.render();
156        assert!(result.starts_with("<input"));
157        assert!(result.ends_with(">"));
158        assert!(!result.contains("</input>"));
159    }
160}