dioxus_ssr/
renderer.rs

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