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 Node::Conditional(condition, then_node, else_node) => {
26 let show = condition.get();
27 let then_html = render_node(then_node);
28 let else_html = else_node
29 .as_ref()
30 .map(|n| render_node(n))
31 .unwrap_or_default();
32
33 let then_style = if show { "" } else { " style=\"display:none\"" };
34 let else_style = if show { " style=\"display:none\"" } else { "" };
35
36 if else_html.is_empty() {
37 format!(
38 "<span data-cond style=\"display:contents\"><span{}>{}</span></span>",
39 then_style, then_html
40 )
41 } else {
42 format!(
43 "<span data-cond style=\"display:contents\"><span{}>{}</span><span{}>{}</span></span>",
44 then_style, then_html, else_style, else_html
45 )
46 }
47 }
48 Node::ReactiveList(list_fn) => {
49 let items_html = list_fn()
50 .iter()
51 .map(render_node)
52 .collect::<Vec<_>>()
53 .join("");
54 format!(
55 "<span data-list style=\"display:contents\">{}</span>",
56 items_html
57 )
58 }
59 Node::KeyedList(list_fn) => {
60 let items_html = list_fn()
61 .iter()
62 .map(|(_, node)| render_node(node))
63 .collect::<Vec<_>>()
64 .join("");
65 format!(
66 "<span data-list style=\"display:contents\">{}</span>",
67 items_html
68 )
69 }
70 Node::Head(_) => String::new(),
71 Node::Suspense(sus) => {
72 if (sus.loading_signal)() {
73 render_node(&sus.fallback)
74 } else {
75 render_node(&sus.children)
76 }
77 }
78 Node::ErrorBoundary(eb) => {
79 if let Some(error) = (eb.error_signal)() {
80 render_node(&(eb.error_fallback)(error))
81 } else {
82 render_node(&eb.children)
83 }
84 }
85 }
86}
87
88fn render_element(element: &Element) -> String {
89 let tag = element.tag();
90 let attrs = render_attributes(element);
91 let children = element
92 .get_children()
93 .iter()
94 .map(render_node)
95 .collect::<Vec<_>>()
96 .join("");
97
98 if is_void_element(tag) {
99 format!("<{}{} />", tag, attrs)
100 } else {
101 format!("<{}{}>{}</{}>", tag, attrs, children, tag)
102 }
103}
104
105fn render_attributes(element: &Element) -> String {
106 let attrs: Vec<String> = element
107 .attributes()
108 .iter()
109 .filter_map(|attr| match &attr.value {
110 AttributeValue::String(s) => Some(format!(" {}=\"{}\"", attr.name, escape_attr(s))),
111 AttributeValue::Bool(b) => {
112 if *b {
113 Some(format!(" {}", attr.name))
114 } else {
115 None
116 }
117 }
118 AttributeValue::ReactiveString(reactive) => Some(format!(
119 " {}=\"{}\"",
120 attr.name,
121 escape_attr(&reactive.get())
122 )),
123 AttributeValue::ReactiveBool(reactive) => {
124 if reactive.get() {
125 Some(format!(" {}", attr.name))
126 } else {
127 None
128 }
129 }
130 })
131 .collect();
132
133 attrs.join("")
134}
135
136fn escape_html(s: &str) -> String {
137 s.replace('&', "&")
138 .replace('<', "<")
139 .replace('>', ">")
140}
141
142fn escape_attr(s: &str) -> String {
143 s.replace('&', "&")
144 .replace('"', """)
145 .replace('<', "<")
146 .replace('>', ">")
147}
148
149fn is_void_element(tag: &str) -> bool {
150 matches!(
151 tag,
152 "area"
153 | "base"
154 | "br"
155 | "col"
156 | "embed"
157 | "hr"
158 | "img"
159 | "input"
160 | "link"
161 | "meta"
162 | "param"
163 | "source"
164 | "track"
165 | "wbr"
166 )
167}
168
169#[cfg(test)]
170mod tests {
171 use super::*;
172 use react_rs_elements::html::*;
173 use react_rs_elements::node::IntoNode;
174
175 #[test]
176 fn test_render_simple_element() {
177 let element = div().class("container").text("Hello");
178 let output = render_to_string(&element.into_node());
179 assert_eq!(output.html, "<div class=\"container\">Hello</div>");
180 }
181
182 #[test]
183 fn test_render_nested_elements() {
184 let element = div()
185 .class("app")
186 .child(h1().text("Title"))
187 .child(p().text("Content"));
188 let output = render_to_string(&element.into_node());
189 assert_eq!(
190 output.html,
191 "<div class=\"app\"><h1>Title</h1><p>Content</p></div>"
192 );
193 }
194
195 #[test]
196 fn test_render_void_element() {
197 let element = input().type_("text").placeholder("Enter name");
198 let output = render_to_string(&element.into_node());
199 assert_eq!(
200 output.html,
201 "<input type=\"text\" placeholder=\"Enter name\" />"
202 );
203 }
204
205 #[test]
206 fn test_render_escapes_html() {
207 let element = p().text("<script>alert('xss')</script>");
208 let output = render_to_string(&element.into_node());
209 assert_eq!(
210 output.html,
211 "<p><script>alert('xss')</script></p>"
212 );
213 }
214
215 #[test]
216 fn test_render_boolean_attribute() {
217 let element = input().disabled(true);
218 let output = render_to_string(&element.into_node());
219 assert!(output.html.contains(" disabled"));
220
221 let element_enabled = input().disabled(false);
222 let output_enabled = render_to_string(&element_enabled.into_node());
223 assert!(!output_enabled.html.contains("disabled"));
224 }
225
226 #[test]
227 fn test_render_fragment() {
228 let fragment = vec![span().text("A"), span().text("B")];
229 let output = render_to_string(&fragment.into_node());
230 assert_eq!(output.html, "<span>A</span><span>B</span>");
231 }
232
233 #[test]
234 fn test_render_complex_structure() {
235 let view = html().child(head().child(title().text("My App"))).child(
236 body().child(
237 div()
238 .id("root")
239 .child(header().child(nav().child(a().href("/").text("Home"))))
240 .child(main_el().child(h1().text("Welcome")))
241 .child(footer().text("2024")),
242 ),
243 );
244 let output = render_to_string(&view.into_node());
245
246 assert!(output.html.contains("<html>"));
247 assert!(output.html.contains("<title>My App</title>"));
248 assert!(output.html.contains("<div id=\"root\">"));
249 assert!(output.html.contains("<a href=\"/\">Home</a>"));
250 assert!(output.html.contains("</html>"));
251 }
252}