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#[derive(Default)]
15pub struct Renderer {
16 pub pre_render: bool,
18
19 render_components: Option<ComponentRenderCallback>,
21
22 template_cache: FxHashMap<Template, Arc<StringCache>>,
24
25 dynamic_node_id: usize,
27}
28
29impl Renderer {
30 pub fn new() -> Self {
31 Self::default()
32 }
33
34 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 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 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 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 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 let mut accumulated_dynamic_styles = Vec::new();
119
120 let mut accumulated_listeners = Vec::new();
122
123 let mut index = 0;
125
126 while let Some(segment) = entry.segments.get(index) {
127 match segment {
128 Segment::HydrationOnlySection(jump_to) => {
129 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 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 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 !*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 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 write!(buf, "{}", self.dynamic_node_id)?;
242 self.dynamic_node_id += 1;
243 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>"; 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), PreRendered(" data-node-hydration=\"".to_string()),
309 AttributeNodeMarker,
310 PreRendered("\"".to_string()),
311 PreRendered(">".to_string()),
312 InnerHtmlMarker,
313 PreRendered("Hello world 1 -->".to_string()),
314 Node(0),
315 PreRendered(
316 "<-- 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 -->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>");
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), 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}