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#[derive(Default)]
17pub struct Renderer {
18 pub pre_render: bool,
20
21 render_components: Option<ComponentRenderCallback>,
23
24 template_cache: FxHashMap<Template, Arc<StringCache>>,
26
27 dynamic_node_id: usize,
29}
30
31impl Renderer {
32 pub fn new() -> Self {
33 Self::default()
34 }
35
36 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 pub fn clear(&mut self) {
49 self.template_cache.clear();
50 self.dynamic_node_id = 0;
51 self.render_components = None;
52 }
53
54 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 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 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 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 let mut accumulated_dynamic_styles = Vec::new();
129
130 let mut accumulated_listeners = Vec::new();
132
133 let mut index = 0;
135
136 while let Some(segment) = entry.segments.get(index) {
137 match segment {
138 Segment::HydrationOnlySection(jump_to) => {
139 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 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 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 !*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 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 write!(buf, "{}", self.dynamic_node_id)?;
268 self.dynamic_node_id += 1;
269 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>"; 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), PreRendered(" data-node-hydration=\"".to_string()),
336 AttributeNodeMarker,
337 PreRendered("\"".to_string()),
338 PreRendered(">".to_string()),
339 InnerHtmlMarker,
340 PreRendered("Hello world 1 -->".to_string()),
341 Node {
342 index: 0,
343 escape_text: EscapeText::Escape
344 },
345 PreRendered(
346 "<-- 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 -->123<-- Hello world 2<div>nest 1</div><div></div><div>nest 2</div></diiiiiiiiv><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), 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}