use react_rs_elements::attributes::AttributeValue;
use react_rs_elements::node::Node;
use react_rs_elements::Element;
pub struct RenderOutput {
pub html: String,
}
pub fn render_to_string(node: &Node) -> RenderOutput {
RenderOutput {
html: render_node(node),
}
}
fn render_node(node: &Node) -> String {
match node {
Node::Element(element) => render_element(element),
Node::Text(text) => escape_html(text),
Node::ReactiveText(reactive) => escape_html(&reactive.get()),
Node::Fragment(children) => children
.iter()
.map(render_node)
.collect::<Vec<_>>()
.join(""),
Node::Conditional(condition, then_node, else_node) => {
let show = condition.get();
let then_html = render_node(then_node);
let else_html = else_node
.as_ref()
.map(|n| render_node(n))
.unwrap_or_default();
let then_style = if show { "" } else { " style=\"display:none\"" };
let else_style = if show { " style=\"display:none\"" } else { "" };
if else_html.is_empty() {
format!(
"<span data-cond style=\"display:contents\"><span{}>{}</span></span>",
then_style, then_html
)
} else {
format!(
"<span data-cond style=\"display:contents\"><span{}>{}</span><span{}>{}</span></span>",
then_style, then_html, else_style, else_html
)
}
}
Node::ReactiveList(list_fn) => {
let items_html = list_fn()
.iter()
.map(render_node)
.collect::<Vec<_>>()
.join("");
format!(
"<span data-list style=\"display:contents\">{}</span>",
items_html
)
}
Node::Head(_) => String::new(),
Node::Suspense(sus) => {
if (sus.loading_signal)() {
render_node(&sus.fallback)
} else {
render_node(&sus.children)
}
}
Node::ErrorBoundary(eb) => {
if let Some(error) = (eb.error_signal)() {
render_node(&(eb.error_fallback)(error))
} else {
render_node(&eb.children)
}
}
}
}
fn render_element(element: &Element) -> String {
let tag = element.tag();
let attrs = render_attributes(element);
let children = element
.get_children()
.iter()
.map(render_node)
.collect::<Vec<_>>()
.join("");
if is_void_element(tag) {
format!("<{}{} />", tag, attrs)
} else {
format!("<{}{}>{}</{}>", tag, attrs, children, tag)
}
}
fn render_attributes(element: &Element) -> String {
let attrs: Vec<String> = element
.attributes()
.iter()
.filter_map(|attr| match &attr.value {
AttributeValue::String(s) => Some(format!(" {}=\"{}\"", attr.name, escape_attr(s))),
AttributeValue::Bool(b) => {
if *b {
Some(format!(" {}", attr.name))
} else {
None
}
}
AttributeValue::ReactiveString(reactive) => Some(format!(
" {}=\"{}\"",
attr.name,
escape_attr(&reactive.get())
)),
AttributeValue::ReactiveBool(reactive) => {
if reactive.get() {
Some(format!(" {}", attr.name))
} else {
None
}
}
})
.collect();
attrs.join("")
}
fn escape_html(s: &str) -> String {
s.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
}
fn escape_attr(s: &str) -> String {
s.replace('&', "&")
.replace('"', """)
.replace('<', "<")
.replace('>', ">")
}
fn is_void_element(tag: &str) -> bool {
matches!(
tag,
"area"
| "base"
| "br"
| "col"
| "embed"
| "hr"
| "img"
| "input"
| "link"
| "meta"
| "param"
| "source"
| "track"
| "wbr"
)
}
#[cfg(test)]
mod tests {
use super::*;
use react_rs_elements::html::*;
use react_rs_elements::node::IntoNode;
#[test]
fn test_render_simple_element() {
let element = div().class("container").text("Hello");
let output = render_to_string(&element.into_node());
assert_eq!(output.html, "<div class=\"container\">Hello</div>");
}
#[test]
fn test_render_nested_elements() {
let element = div()
.class("app")
.child(h1().text("Title"))
.child(p().text("Content"));
let output = render_to_string(&element.into_node());
assert_eq!(
output.html,
"<div class=\"app\"><h1>Title</h1><p>Content</p></div>"
);
}
#[test]
fn test_render_void_element() {
let element = input().type_("text").placeholder("Enter name");
let output = render_to_string(&element.into_node());
assert_eq!(
output.html,
"<input type=\"text\" placeholder=\"Enter name\" />"
);
}
#[test]
fn test_render_escapes_html() {
let element = p().text("<script>alert('xss')</script>");
let output = render_to_string(&element.into_node());
assert_eq!(
output.html,
"<p><script>alert('xss')</script></p>"
);
}
#[test]
fn test_render_boolean_attribute() {
let element = input().disabled(true);
let output = render_to_string(&element.into_node());
assert!(output.html.contains(" disabled"));
let element_enabled = input().disabled(false);
let output_enabled = render_to_string(&element_enabled.into_node());
assert!(!output_enabled.html.contains("disabled"));
}
#[test]
fn test_render_fragment() {
let fragment = vec![span().text("A"), span().text("B")];
let output = render_to_string(&fragment.into_node());
assert_eq!(output.html, "<span>A</span><span>B</span>");
}
#[test]
fn test_render_complex_structure() {
let view = html().child(head().child(title().text("My App"))).child(
body().child(
div()
.id("root")
.child(header().child(nav().child(a().href("/").text("Home"))))
.child(main_el().child(h1().text("Welcome")))
.child(footer().text("2024")),
),
);
let output = render_to_string(&view.into_node());
assert!(output.html.contains("<html>"));
assert!(output.html.contains("<title>My App</title>"));
assert!(output.html.contains("<div id=\"root\">"));
assert!(output.html.contains("<a href=\"/\">Home</a>"));
assert!(output.html.contains("</html>"));
}
}