Skip to main content

react_rs_dom/
render.rs

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('&', "&amp;")
78        .replace('<', "&lt;")
79        .replace('>', "&gt;")
80}
81
82fn escape_attr(s: &str) -> String {
83    s.replace('&', "&amp;")
84        .replace('"', "&quot;")
85        .replace('<', "&lt;")
86        .replace('>', "&gt;")
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>&lt;script&gt;alert('xss')&lt;/script&gt;</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}