use super::cache::Segment; use crate::cache::StringCache; use dioxus_core::{prelude::*, AttributeValue, DynamicNode, RenderReturn}; use std::collections::HashMap; use std::fmt::Write; use std::sync::Arc; /// A virtualdom renderer that caches the templates it has seen for faster rendering #[derive(Default)] pub struct Renderer { /// should we do our best to prettify the output? pub pretty: bool, /// Control if elements are written onto a new line pub newline: bool, /// Should we sanitize text nodes? (escape HTML) pub sanitize: bool, /// Choose to write ElementIDs into elements so the page can be re-hydrated later on pub pre_render: bool, // Currently not implemented // Don't proceed onto new components. Instead, put the name of the component. pub skip_components: bool, /// A cache of templates that have been rendered template_cache: HashMap<&'static str, Arc>, } impl Renderer { pub fn new() -> Self { Self::default() } pub fn render(&mut self, dom: &VirtualDom) -> String { let mut buf = String::new(); self.render_to(&mut buf, dom).unwrap(); buf } pub fn render_to(&mut self, buf: &mut impl Write, dom: &VirtualDom) -> std::fmt::Result { self.render_scope(buf, dom, ScopeId(0)) } pub fn render_scope( &mut self, buf: &mut impl Write, dom: &VirtualDom, scope: ScopeId, ) -> std::fmt::Result { // We should never ever run into async or errored nodes in SSR // Error boundaries and suspense boundaries will convert these to sync if let RenderReturn::Ready(node) = dom.get_scope(scope).unwrap().root_node() { self.render_template(buf, dom, node)? }; Ok(()) } fn render_template( &mut self, buf: &mut impl Write, dom: &VirtualDom, template: &VNode, ) -> std::fmt::Result { let entry = self .template_cache .entry(template.template.get().name) .or_insert_with(|| Arc::new(StringCache::from_template(template).unwrap())) .clone(); let mut inner_html = None; // We need to keep track of the dynamic styles so we can insert them into the right place let mut accumulated_dynamic_styles = Vec::new(); for segment in entry.segments.iter() { match segment { Segment::Attr(idx) => { let attr = &template.dynamic_attrs[*idx]; attr.attribute_type().try_for_each(|attr| { if attr.name == "dangerous_inner_html" { inner_html = Some(attr); } else if attr.namespace == Some("style") { accumulated_dynamic_styles.push(attr); } else { match attr.value { AttributeValue::Text(value) => { write!(buf, " {}=\"{}\"", attr.name, value)? } AttributeValue::Bool(value) => { write!(buf, " {}={}", attr.name, value)? } AttributeValue::Int(value) => { write!(buf, " {}={}", attr.name, value)? } AttributeValue::Float(value) => { write!(buf, " {}={}", attr.name, value)? } _ => {} }; } Ok(()) })?; } Segment::Node(idx) => match &template.dynamic_nodes[*idx] { DynamicNode::Component(node) => { if self.skip_components { write!(buf, "<{}><{}/>", node.name, node.name)?; } else { let id = node.mounted_scope().unwrap(); let scope = dom.get_scope(id).unwrap(); let node = scope.root_node(); match node { RenderReturn::Ready(node) => { self.render_template(buf, dom, node)? } _ => todo!( "generally, scopes should be sync, only if being traversed" ), } } } DynamicNode::Text(text) => { // in SSR, we are concerned that we can't hunt down the right text node since they might get merged if self.pre_render { write!(buf, "")?; } write!( buf, "{}", askama_escape::escape(text.value, askama_escape::Html) )?; if self.pre_render { write!(buf, "")?; } } DynamicNode::Fragment(nodes) => { for child in *nodes { self.render_template(buf, dom, child)?; } } DynamicNode::Placeholder(_el) => { if self.pre_render { write!(buf, "
")?;
                        }
                    }
                },

                Segment::PreRendered(contents) => write!(buf, "{contents}")?,

                Segment::StyleMarker { inside_style_tag } => {
                    if !accumulated_dynamic_styles.is_empty() {
                        // if we are inside a style tag, we don't need to write the style attribute
                        if !*inside_style_tag {
                            write!(buf, " style=\"")?;
                        }
                        for attr in &accumulated_dynamic_styles {
                            match attr.value {
                                AttributeValue::Text(value) => {
                                    write!(buf, "{}:{};", attr.name, value)?
                                }
                                AttributeValue::Bool(value) => {
                                    write!(buf, "{}:{};", attr.name, value)?
                                }
                                AttributeValue::Float(f) => write!(buf, "{}:{};", attr.name, f)?,
                                AttributeValue::Int(i) => write!(buf, "{}:{};", attr.name, i)?,
                                _ => {}
                            };
                        }
                        if !*inside_style_tag {
                            write!(buf, "\"")?;
                        }

                        // clear the accumulated styles
                        accumulated_dynamic_styles.clear();
                    }
                }

                Segment::InnerHtmlMarker => {
                    if let Some(inner_html) = inner_html.take() {
                        let inner_html = &inner_html.value;
                        match inner_html {
                            AttributeValue::Text(value) => write!(buf, "{}", value)?,
                            AttributeValue::Bool(value) => write!(buf, "{}", value)?,
                            AttributeValue::Float(f) => write!(buf, "{}", f)?,
                            AttributeValue::Int(i) => write!(buf, "{}", i)?,
                            _ => {}
                        }
                    }
                }
            }
        }

        Ok(())
    }
}

#[test]
fn to_string_works() {
    use dioxus::prelude::*;

    fn app(cx: Scope) -> Element {
        let dynamic = 123;
        let dyn2 = ""; // this should be escaped

        render! {
            div { class: "asdasdasd", class: "asdasdasd", id: "id-{dynamic}",
                "Hello world 1 -->" "{dynamic}" "<-- Hello world 2"
                div { "nest 1" }
                div {}
                div { "nest 2" }
                "{dyn2}"
                (0..5).map(|i| rsx! { div { "finalize {i}" } })
            }
        }
    }

    let mut dom = VirtualDom::new(app);
    _ = dom.rebuild();

    let mut renderer = Renderer::new();
    let out = renderer.render(&dom);

    for item in renderer.template_cache.iter() {
        if item.1.segments.len() > 5 {
            assert_eq!(
                item.1.segments,
                vec![
                    PreRendered("
".into()), InnerHtmlMarker, PreRendered("Hello world 1 -->".into(),), Node(0,), PreRendered( "<-- Hello world 2
nest 1
nest 2
".into(), ), Node(1,), Node(2,), PreRendered("
".into(),), ] ); } } use Segment::*; assert_eq!(out, "
Hello world 1 -->123<-- Hello world 2
nest 1
nest 2
</diiiiiiiiv>
finalize 0
finalize 1
finalize 2
finalize 3
finalize 4
"); }