1use react_rs_elements::attributes::AttributeValue;
2use react_rs_elements::node::Node;
3use react_rs_elements::Element;
4
5pub struct RenderOutput {
6 pub html: String,
7}
8
9pub fn render_to_string(node: &Node) -> RenderOutput {
10 RenderOutput {
11 html: render_node(node),
12 }
13}
14
15fn render_node(node: &Node) -> String {
16 match node {
17 Node::Element(element) => render_element(element),
18 Node::Text(text) => escape_html(text),
19 Node::ReactiveText(reactive) => escape_html(&reactive.get()),
20 Node::Fragment(children) => children
21 .iter()
22 .map(render_node)
23 .collect::<Vec<_>>()
24 .join(""),
25 }
26}
27
28fn render_element(element: &Element) -> String {
29 let tag = element.tag();
30 let attrs = render_attributes(element);
31 let children = element
32 .get_children()
33 .iter()
34 .map(render_node)
35 .collect::<Vec<_>>()
36 .join("");
37
38 if is_void_element(tag) {
39 format!("<{}{} />", tag, attrs)
40 } else {
41 format!("<{}{}>{}</{}>", tag, attrs, children, tag)
42 }
43}
44
45fn render_attributes(element: &Element) -> String {
46 let attrs: Vec<String> = element
47 .attributes()
48 .iter()
49 .filter_map(|attr| match &attr.value {
50 AttributeValue::String(s) => Some(format!(" {}=\"{}\"", attr.name, escape_attr(s))),
51 AttributeValue::Bool(b) => {
52 if *b {
53 Some(format!(" {}", attr.name))
54 } else {
55 None
56 }
57 }
58 AttributeValue::ReactiveString(reactive) => Some(format!(
59 " {}=\"{}\"",
60 attr.name,
61 escape_attr(&reactive.get())
62 )),
63 AttributeValue::ReactiveBool(reactive) => {
64 if reactive.get() {
65 Some(format!(" {}", attr.name))
66 } else {
67 None
68 }
69 }
70 })
71 .collect();
72
73 attrs.join("")
74}
75
76fn escape_html(s: &str) -> String {
77 s.replace('&', "&")
78 .replace('<', "<")
79 .replace('>', ">")
80}
81
82fn escape_attr(s: &str) -> String {
83 s.replace('&', "&")
84 .replace('"', """)
85 .replace('<', "<")
86 .replace('>', ">")
87}
88
89fn is_void_element(tag: &str) -> bool {
90 matches!(
91 tag,
92 "area"
93 | "base"
94 | "br"
95 | "col"
96 | "embed"
97 | "hr"
98 | "img"
99 | "input"
100 | "link"
101 | "meta"
102 | "param"
103 | "source"
104 | "track"
105 | "wbr"
106 )
107}
108
109#[cfg(test)]
110mod tests {
111 use super::*;
112 use react_rs_elements::html::*;
113 use react_rs_elements::node::IntoNode;
114
115 #[test]
116 fn test_render_simple_element() {
117 let element = div().class("container").text("Hello");
118 let output = render_to_string(&element.into_node());
119 assert_eq!(output.html, "<div class=\"container\">Hello</div>");
120 }
121
122 #[test]
123 fn test_render_nested_elements() {
124 let element = div()
125 .class("app")
126 .child(h1().text("Title"))
127 .child(p().text("Content"));
128 let output = render_to_string(&element.into_node());
129 assert_eq!(
130 output.html,
131 "<div class=\"app\"><h1>Title</h1><p>Content</p></div>"
132 );
133 }
134
135 #[test]
136 fn test_render_void_element() {
137 let element = input().type_("text").placeholder("Enter name");
138 let output = render_to_string(&element.into_node());
139 assert_eq!(
140 output.html,
141 "<input type=\"text\" placeholder=\"Enter name\" />"
142 );
143 }
144
145 #[test]
146 fn test_render_escapes_html() {
147 let element = p().text("<script>alert('xss')</script>");
148 let output = render_to_string(&element.into_node());
149 assert_eq!(
150 output.html,
151 "<p><script>alert('xss')</script></p>"
152 );
153 }
154
155 #[test]
156 fn test_render_boolean_attribute() {
157 let element = input().disabled(true);
158 let output = render_to_string(&element.into_node());
159 assert!(output.html.contains(" disabled"));
160
161 let element_enabled = input().disabled(false);
162 let output_enabled = render_to_string(&element_enabled.into_node());
163 assert!(!output_enabled.html.contains("disabled"));
164 }
165
166 #[test]
167 fn test_render_fragment() {
168 let fragment = vec![span().text("A"), span().text("B")];
169 let output = render_to_string(&fragment.into_node());
170 assert_eq!(output.html, "<span>A</span><span>B</span>");
171 }
172
173 #[test]
174 fn test_render_complex_structure() {
175 let view = html().child(head().child(title().text("My App"))).child(
176 body().child(
177 div()
178 .id("root")
179 .child(header().child(nav().child(a().href("/").text("Home"))))
180 .child(main_el().child(h1().text("Welcome")))
181 .child(footer().text("2024")),
182 ),
183 );
184 let output = render_to_string(&view.into_node());
185
186 assert!(output.html.contains("<html>"));
187 assert!(output.html.contains("<title>My App</title>"));
188 assert!(output.html.contains("<div id=\"root\">"));
189 assert!(output.html.contains("<a href=\"/\">Home</a>"));
190 assert!(output.html.contains("</html>"));
191 }
192}