workers_rsx/
simple_element.rs1use crate::html_escaping::escape_html;
2use crate::Render;
3use std::borrow::Cow;
4use std::collections::HashMap;
5use std::fmt::{Result, Write};
6
7#[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#[derive(Debug)]
18pub struct SimpleElement<'a, T: Render> {
19 pub tag_name: &'a str,
21 pub attributes: Attributes<'a>,
22 pub contents: Option<T>,
23}
24
25const 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}