dioxus_ssr/
renderer.rs

1use super::cache::Segment;
2use crate::cache::StringCache;
3
4use dioxus_core::{prelude::*, AttributeValue, DynamicNode};
5use rustc_hash::FxHashMap;
6use std::fmt::Write;
7use std::sync::Arc;
8
9type ComponentRenderCallback = Arc<
10    dyn Fn(&mut Renderer, &mut dyn Write, &VirtualDom, ScopeId) -> std::fmt::Result + Send + Sync,
11>;
12
13/// A virtualdom renderer that caches the templates it has seen for faster rendering
14#[derive(Default)]
15pub struct Renderer {
16    /// Choose to write ElementIDs into elements so the page can be re-hydrated later on
17    pub pre_render: bool,
18
19    /// A callback used to render components. You can set this callback to control what components are rendered and add wrappers around components that are not present in CSR
20    render_components: Option<ComponentRenderCallback>,
21
22    /// A cache of templates that have been rendered
23    template_cache: FxHashMap<Template, Arc<StringCache>>,
24
25    /// The current dynamic node id for hydration
26    dynamic_node_id: usize,
27}
28
29impl Renderer {
30    pub fn new() -> Self {
31        Self::default()
32    }
33
34    /// Set the callback that the renderer uses to render components
35    pub fn set_render_components(
36        &mut self,
37        callback: impl Fn(&mut Renderer, &mut dyn Write, &VirtualDom, ScopeId) -> std::fmt::Result
38            + Send
39            + Sync
40            + 'static,
41    ) {
42        self.render_components = Some(Arc::new(callback));
43    }
44
45    /// Reset the callback that the renderer uses to render components
46    pub fn reset_render_components(&mut self) {
47        self.render_components = None;
48    }
49
50    pub fn render(&mut self, dom: &VirtualDom) -> String {
51        let mut buf = String::new();
52        self.render_to(&mut buf, dom).unwrap();
53        buf
54    }
55
56    pub fn render_to<W: Write + ?Sized>(
57        &mut self,
58        buf: &mut W,
59        dom: &VirtualDom,
60    ) -> std::fmt::Result {
61        self.reset_hydration();
62        self.render_scope(buf, dom, ScopeId::ROOT)
63    }
64
65    /// Render an element to a string
66    pub fn render_element(&mut self, element: Element) -> String {
67        let mut buf = String::new();
68        self.render_element_to(&mut buf, element).unwrap();
69        buf
70    }
71
72    /// Render an element to the buffer
73    pub fn render_element_to<W: Write + ?Sized>(
74        &mut self,
75        buf: &mut W,
76        element: Element,
77    ) -> std::fmt::Result {
78        fn lazy_app(props: Element) -> Element {
79            props
80        }
81        let mut dom = VirtualDom::new_with_props(lazy_app, element);
82        dom.rebuild_in_place();
83        self.render_to(buf, &dom)
84    }
85
86    /// Reset the renderer hydration state
87    pub fn reset_hydration(&mut self) {
88        self.dynamic_node_id = 0;
89    }
90
91    pub fn render_scope<W: Write + ?Sized>(
92        &mut self,
93        buf: &mut W,
94        dom: &VirtualDom,
95        scope: ScopeId,
96    ) -> std::fmt::Result {
97        let node = dom.get_scope(scope).unwrap().root_node();
98        self.render_template(buf, dom, node)?;
99
100        Ok(())
101    }
102
103    fn render_template<W: Write + ?Sized>(
104        &mut self,
105        mut buf: &mut W,
106        dom: &VirtualDom,
107        template: &VNode,
108    ) -> std::fmt::Result {
109        let entry = self
110            .template_cache
111            .entry(template.template)
112            .or_insert_with(move || Arc::new(StringCache::from_template(template).unwrap()))
113            .clone();
114
115        let mut inner_html = None;
116
117        // We need to keep track of the dynamic styles so we can insert them into the right place
118        let mut accumulated_dynamic_styles = Vec::new();
119
120        // We need to keep track of the listeners so we can insert them into the right place
121        let mut accumulated_listeners = Vec::new();
122
123        // We keep track of the index we are on manually so that we can jump forward to a new section quickly without iterating every item
124        let mut index = 0;
125
126        while let Some(segment) = entry.segments.get(index) {
127            match segment {
128                Segment::HydrationOnlySection(jump_to) => {
129                    // If we are not prerendering, we don't need to write the content of the hydration only section
130                    // Instead we can jump to the next section
131                    if !self.pre_render {
132                        index = *jump_to;
133                        continue;
134                    }
135                }
136                Segment::Attr(idx) => {
137                    let attrs = &*template.dynamic_attrs[*idx];
138                    for attr in attrs {
139                        if attr.name == "dangerous_inner_html" {
140                            inner_html = Some(attr);
141                        } else if attr.namespace == Some("style") {
142                            accumulated_dynamic_styles.push(attr);
143                        } else if BOOL_ATTRS.contains(&attr.name) {
144                            if truthy(&attr.value) {
145                                write_attribute(buf, attr)?;
146                            }
147                        } else {
148                            write_attribute(buf, attr)?;
149                        }
150
151                        if self.pre_render {
152                            if let AttributeValue::Listener(_) = &attr.value {
153                                // The onmounted event doesn't need a DOM listener
154                                if attr.name != "onmounted" {
155                                    accumulated_listeners.push(attr.name);
156                                }
157                            }
158                        }
159                    }
160                }
161                Segment::Node(idx) => match &template.dynamic_nodes[*idx] {
162                    DynamicNode::Component(node) => {
163                        if let Some(render_components) = self.render_components.clone() {
164                            let scope_id = node.mounted_scope_id(*idx, template, dom).unwrap();
165
166                            render_components(self, &mut buf, dom, scope_id)?;
167                        } else {
168                            let scope = node.mounted_scope(*idx, template, dom).unwrap();
169                            let node = scope.root_node();
170                            self.render_template(buf, dom, node)?
171                        }
172                    }
173                    DynamicNode::Text(text) => {
174                        // in SSR, we are concerned that we can't hunt down the right text node since they might get merged
175                        if self.pre_render {
176                            write!(buf, "<!--node-id{}-->", self.dynamic_node_id)?;
177                            self.dynamic_node_id += 1;
178                        }
179
180                        write!(
181                            buf,
182                            "{}",
183                            askama_escape::escape(&text.value, askama_escape::Html)
184                        )?;
185
186                        if self.pre_render {
187                            write!(buf, "<!--#-->")?;
188                        }
189                    }
190                    DynamicNode::Fragment(nodes) => {
191                        for child in nodes {
192                            self.render_template(buf, dom, child)?;
193                        }
194                    }
195
196                    DynamicNode::Placeholder(_) => {
197                        if self.pre_render {
198                            write!(buf, "<!--placeholder{}-->", self.dynamic_node_id)?;
199                            self.dynamic_node_id += 1;
200                        }
201                    }
202                },
203
204                Segment::PreRendered(contents) => write!(buf, "{contents}")?,
205
206                Segment::StyleMarker { inside_style_tag } => {
207                    if !accumulated_dynamic_styles.is_empty() {
208                        // if we are inside a style tag, we don't need to write the style attribute
209                        if !*inside_style_tag {
210                            write!(buf, " style=\"")?;
211                        }
212                        for attr in &accumulated_dynamic_styles {
213                            write!(buf, "{}:", attr.name)?;
214                            write_value_unquoted(buf, &attr.value)?;
215                            write!(buf, ";")?;
216                        }
217                        if !*inside_style_tag {
218                            write!(buf, "\"")?;
219                        }
220
221                        // clear the accumulated styles
222                        accumulated_dynamic_styles.clear();
223                    }
224                }
225
226                Segment::InnerHtmlMarker => {
227                    if let Some(inner_html) = inner_html.take() {
228                        let inner_html = &inner_html.value;
229                        match inner_html {
230                            AttributeValue::Text(value) => write!(buf, "{}", value)?,
231                            AttributeValue::Bool(value) => write!(buf, "{}", value)?,
232                            AttributeValue::Float(f) => write!(buf, "{}", f)?,
233                            AttributeValue::Int(i) => write!(buf, "{}", i)?,
234                            _ => {}
235                        }
236                    }
237                }
238
239                Segment::AttributeNodeMarker => {
240                    // first write the id
241                    write!(buf, "{}", self.dynamic_node_id)?;
242                    self.dynamic_node_id += 1;
243                    // then write any listeners
244                    for name in accumulated_listeners.drain(..) {
245                        write!(buf, ",{}:", &name[2..])?;
246                        write!(
247                            buf,
248                            "{}",
249                            dioxus_core_types::event_bubbles(&name[2..]) as u8
250                        )?;
251                    }
252                }
253
254                Segment::RootNodeMarker => {
255                    write!(buf, "{}", self.dynamic_node_id)?;
256                    self.dynamic_node_id += 1
257                }
258            }
259
260            index += 1;
261        }
262
263        Ok(())
264    }
265}
266
267#[test]
268fn to_string_works() {
269    use dioxus::prelude::*;
270
271    fn app() -> Element {
272        let dynamic = 123;
273        let dyn2 = "</diiiiiiiiv>"; // this should be escaped
274
275        rsx! {
276            div { class: "asdasdasd", class: "asdasdasd", id: "id-{dynamic}",
277                "Hello world 1 -->"
278                "{dynamic}"
279                "<-- Hello world 2"
280                div { "nest 1" }
281                div {}
282                div { "nest 2" }
283                "{dyn2}"
284                for i in (0..5) {
285                    div { "finalize {i}" }
286                }
287            }
288        }
289    }
290
291    let mut dom = VirtualDom::new(app);
292    dom.rebuild(&mut dioxus_core::NoOpMutations);
293
294    let mut renderer = Renderer::new();
295    let out = renderer.render(&dom);
296
297    for item in renderer.template_cache.iter() {
298        if item.1.segments.len() > 10 {
299            assert_eq!(
300                item.1.segments,
301                vec![
302                    PreRendered("<div class=\"asdasdasd asdasdasd\"".to_string()),
303                    Attr(0),
304                    StyleMarker {
305                        inside_style_tag: false
306                    },
307                    HydrationOnlySection(7), // jump to `>` if we don't need to hydrate
308                    PreRendered(" data-node-hydration=\"".to_string()),
309                    AttributeNodeMarker,
310                    PreRendered("\"".to_string()),
311                    PreRendered(">".to_string()),
312                    InnerHtmlMarker,
313                    PreRendered("Hello world 1 --&gt;".to_string()),
314                    Node(0),
315                    PreRendered(
316                        "&lt;-- Hello world 2<div>nest 1</div><div></div><div>nest 2</div>"
317                            .to_string()
318                    ),
319                    Node(1),
320                    Node(2),
321                    PreRendered("</div>".to_string())
322                ]
323            );
324        }
325    }
326
327    use Segment::*;
328
329    assert_eq!(out, "<div class=\"asdasdasd asdasdasd\" id=\"id-123\">Hello world 1 --&gt;123&lt;-- Hello world 2<div>nest 1</div><div></div><div>nest 2</div>&lt;/diiiiiiiiv&gt;<div>finalize 0</div><div>finalize 1</div><div>finalize 2</div><div>finalize 3</div><div>finalize 4</div></div>");
330}
331
332#[test]
333fn empty_for_loop_works() {
334    use dioxus::prelude::*;
335
336    fn app() -> Element {
337        rsx! {
338            div { class: "asdasdasd",
339                for _ in (0..5) {
340
341                }
342            }
343        }
344    }
345
346    let mut dom = VirtualDom::new(app);
347    dom.rebuild(&mut dioxus_core::NoOpMutations);
348
349    let mut renderer = Renderer::new();
350    let out = renderer.render(&dom);
351
352    for item in renderer.template_cache.iter() {
353        if item.1.segments.len() > 5 {
354            assert_eq!(
355                item.1.segments,
356                vec![
357                    PreRendered("<div class=\"asdasdasd\"".to_string()),
358                    HydrationOnlySection(5), // jump to `>` if we don't need to hydrate
359                    PreRendered(" data-node-hydration=\"".to_string()),
360                    RootNodeMarker,
361                    PreRendered("\"".to_string()),
362                    PreRendered(">".to_string()),
363                    Node(0),
364                    PreRendered("</div>".to_string())
365                ]
366            );
367        }
368    }
369
370    use Segment::*;
371
372    assert_eq!(out, "<div class=\"asdasdasd\"></div>");
373}
374
375#[test]
376fn empty_render_works() {
377    use dioxus::prelude::*;
378
379    fn app() -> Element {
380        rsx! {}
381    }
382
383    let mut dom = VirtualDom::new(app);
384    dom.rebuild(&mut dioxus_core::NoOpMutations);
385
386    let mut renderer = Renderer::new();
387    let out = renderer.render(&dom);
388
389    for item in renderer.template_cache.iter() {
390        if item.1.segments.len() > 5 {
391            assert_eq!(item.1.segments, vec![]);
392        }
393    }
394    assert_eq!(out, "");
395}
396
397pub(crate) const BOOL_ATTRS: &[&str] = &[
398    "allowfullscreen",
399    "allowpaymentrequest",
400    "async",
401    "autofocus",
402    "autoplay",
403    "checked",
404    "controls",
405    "default",
406    "defer",
407    "disabled",
408    "formnovalidate",
409    "hidden",
410    "ismap",
411    "itemscope",
412    "loop",
413    "multiple",
414    "muted",
415    "nomodule",
416    "novalidate",
417    "open",
418    "playsinline",
419    "readonly",
420    "required",
421    "reversed",
422    "selected",
423    "truespeed",
424    "webkitdirectory",
425];
426
427pub(crate) fn str_truthy(value: &str) -> bool {
428    !value.is_empty() && value != "0" && value.to_lowercase() != "false"
429}
430
431pub(crate) fn truthy(value: &AttributeValue) -> bool {
432    match value {
433        AttributeValue::Text(value) => str_truthy(value),
434        AttributeValue::Bool(value) => *value,
435        AttributeValue::Int(value) => *value != 0,
436        AttributeValue::Float(value) => *value != 0.0,
437        _ => false,
438    }
439}
440
441pub(crate) fn write_attribute<W: Write + ?Sized>(
442    buf: &mut W,
443    attr: &Attribute,
444) -> std::fmt::Result {
445    let name = &attr.name;
446    match &attr.value {
447        AttributeValue::Text(value) => write!(buf, " {name}=\"{value}\""),
448        AttributeValue::Bool(value) => write!(buf, " {name}={value}"),
449        AttributeValue::Int(value) => write!(buf, " {name}={value}"),
450        AttributeValue::Float(value) => write!(buf, " {name}={value}"),
451        _ => Ok(()),
452    }
453}
454
455pub(crate) fn write_value_unquoted<W: Write + ?Sized>(
456    buf: &mut W,
457    value: &AttributeValue,
458) -> std::fmt::Result {
459    match value {
460        AttributeValue::Text(value) => write!(buf, "{}", value),
461        AttributeValue::Bool(value) => write!(buf, "{}", value),
462        AttributeValue::Int(value) => write!(buf, "{}", value),
463        AttributeValue::Float(value) => write!(buf, "{}", value),
464        _ => Ok(()),
465    }
466}