use super::cache::Segment;
use crate::cache::StringCache;
use dioxus_core::{prelude::*, AttributeValue, DynamicNode};
use rustc_hash::FxHashMap;
use std::fmt::Write;
use std::sync::Arc;
type ComponentRenderCallback = Arc<
dyn Fn(&mut Renderer, &mut dyn Write, &VirtualDom, ScopeId) -> std::fmt::Result + Send + Sync,
>;
#[derive(Default)]
pub struct Renderer {
pub pre_render: bool,
render_components: Option<ComponentRenderCallback>,
template_cache: FxHashMap<Template, Arc<StringCache>>,
dynamic_node_id: usize,
}
impl Renderer {
pub fn new() -> Self {
Self::default()
}
pub fn set_render_components(
&mut self,
callback: impl Fn(&mut Renderer, &mut dyn Write, &VirtualDom, ScopeId) -> std::fmt::Result
+ Send
+ Sync
+ 'static,
) {
self.render_components = Some(Arc::new(callback));
}
pub fn reset_render_components(&mut self) {
self.render_components = None;
}
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<W: Write + ?Sized>(
&mut self,
buf: &mut W,
dom: &VirtualDom,
) -> std::fmt::Result {
self.reset_hydration();
self.render_scope(buf, dom, ScopeId::ROOT)
}
pub fn render_element(&mut self, element: Element) -> String {
let mut buf = String::new();
self.render_element_to(&mut buf, element).unwrap();
buf
}
pub fn render_element_to<W: Write + ?Sized>(
&mut self,
buf: &mut W,
element: Element,
) -> std::fmt::Result {
fn lazy_app(props: Element) -> Element {
props
}
let mut dom = VirtualDom::new_with_props(lazy_app, element);
dom.rebuild_in_place();
self.render_to(buf, &dom)
}
pub fn reset_hydration(&mut self) {
self.dynamic_node_id = 0;
}
pub fn render_scope<W: Write + ?Sized>(
&mut self,
buf: &mut W,
dom: &VirtualDom,
scope: ScopeId,
) -> std::fmt::Result {
let node = dom.get_scope(scope).unwrap().root_node();
self.render_template(buf, dom, node)?;
Ok(())
}
fn render_template<W: Write + ?Sized>(
&mut self,
mut buf: &mut W,
dom: &VirtualDom,
template: &VNode,
) -> std::fmt::Result {
let entry = self
.template_cache
.entry(template.template)
.or_insert_with(move || Arc::new(StringCache::from_template(template).unwrap()))
.clone();
let mut inner_html = None;
let mut accumulated_dynamic_styles = Vec::new();
let mut accumulated_listeners = Vec::new();
let mut index = 0;
while let Some(segment) = entry.segments.get(index) {
match segment {
Segment::HydrationOnlySection(jump_to) => {
if !self.pre_render {
index = *jump_to;
continue;
}
}
Segment::Attr(idx) => {
let attrs = &*template.dynamic_attrs[*idx];
for attr in attrs {
if attr.name == "dangerous_inner_html" {
inner_html = Some(attr);
} else if attr.namespace == Some("style") {
accumulated_dynamic_styles.push(attr);
} else if BOOL_ATTRS.contains(&attr.name) {
if truthy(&attr.value) {
write_attribute(buf, attr)?;
}
} else {
write_attribute(buf, attr)?;
}
if self.pre_render {
if let AttributeValue::Listener(_) = &attr.value {
if attr.name != "onmounted" {
accumulated_listeners.push(attr.name);
}
}
}
}
}
Segment::Node(idx) => match &template.dynamic_nodes[*idx] {
DynamicNode::Component(node) => {
if let Some(render_components) = self.render_components.clone() {
let scope_id = node.mounted_scope_id(*idx, template, dom).unwrap();
render_components(self, &mut buf, dom, scope_id)?;
} else {
let scope = node.mounted_scope(*idx, template, dom).unwrap();
let node = scope.root_node();
self.render_template(buf, dom, node)?
}
}
DynamicNode::Text(text) => {
if self.pre_render {
write!(buf, "<!--node-id{}-->", self.dynamic_node_id)?;
self.dynamic_node_id += 1;
}
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(_) => {
if self.pre_render {
write!(buf, "<!--placeholder{}-->", self.dynamic_node_id)?;
self.dynamic_node_id += 1;
}
}
},
Segment::PreRendered(contents) => write!(buf, "{contents}")?,
Segment::StyleMarker { inside_style_tag } => {
if !accumulated_dynamic_styles.is_empty() {
if !*inside_style_tag {
write!(buf, " style=\"")?;
}
for attr in &accumulated_dynamic_styles {
write!(buf, "{}:", attr.name)?;
write_value_unquoted(buf, &attr.value)?;
write!(buf, ";")?;
}
if !*inside_style_tag {
write!(buf, "\"")?;
}
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)?,
_ => {}
}
}
}
Segment::AttributeNodeMarker => {
write!(buf, "{}", self.dynamic_node_id)?;
self.dynamic_node_id += 1;
for name in accumulated_listeners.drain(..) {
write!(buf, ",{}:", &name[2..])?;
write!(
buf,
"{}",
dioxus_core_types::event_bubbles(&name[2..]) as u8
)?;
}
}
Segment::RootNodeMarker => {
write!(buf, "{}", self.dynamic_node_id)?;
self.dynamic_node_id += 1
}
}
index += 1;
}
Ok(())
}
}
#[test]
fn to_string_works() {
use dioxus::prelude::*;
fn app() -> Element {
let dynamic = 123;
let dyn2 = "</diiiiiiiiv>"; rsx! {
div { class: "asdasdasd", class: "asdasdasd", id: "id-{dynamic}",
"Hello world 1 -->"
"{dynamic}"
"<-- Hello world 2"
div { "nest 1" }
div {}
div { "nest 2" }
"{dyn2}"
for i in (0..5) {
div { "finalize {i}" }
}
}
}
}
let mut dom = VirtualDom::new(app);
dom.rebuild(&mut dioxus_core::NoOpMutations);
let mut renderer = Renderer::new();
let out = renderer.render(&dom);
for item in renderer.template_cache.iter() {
if item.1.segments.len() > 10 {
assert_eq!(
item.1.segments,
vec![
PreRendered("<div class=\"asdasdasd asdasdasd\"".to_string()),
Attr(0),
StyleMarker {
inside_style_tag: false
},
HydrationOnlySection(7), PreRendered(" data-node-hydration=\"".to_string()),
AttributeNodeMarker,
PreRendered("\"".to_string()),
PreRendered(">".to_string()),
InnerHtmlMarker,
PreRendered("Hello world 1 -->".to_string()),
Node(0),
PreRendered(
"<-- Hello world 2<div>nest 1</div><div></div><div>nest 2</div>"
.to_string()
),
Node(1),
Node(2),
PreRendered("</div>".to_string())
]
);
}
}
use Segment::*;
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>");
}
#[test]
fn empty_for_loop_works() {
use dioxus::prelude::*;
fn app() -> Element {
rsx! {
div { class: "asdasdasd",
for _ in (0..5) {
}
}
}
}
let mut dom = VirtualDom::new(app);
dom.rebuild(&mut dioxus_core::NoOpMutations);
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("<div class=\"asdasdasd\"".to_string()),
HydrationOnlySection(5), PreRendered(" data-node-hydration=\"".to_string()),
RootNodeMarker,
PreRendered("\"".to_string()),
PreRendered(">".to_string()),
Node(0),
PreRendered("</div>".to_string())
]
);
}
}
use Segment::*;
assert_eq!(out, "<div class=\"asdasdasd\"></div>");
}
#[test]
fn empty_render_works() {
use dioxus::prelude::*;
fn app() -> Element {
rsx! {}
}
let mut dom = VirtualDom::new(app);
dom.rebuild(&mut dioxus_core::NoOpMutations);
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![]);
}
}
assert_eq!(out, "");
}
pub(crate) const BOOL_ATTRS: &[&str] = &[
"allowfullscreen",
"allowpaymentrequest",
"async",
"autofocus",
"autoplay",
"checked",
"controls",
"default",
"defer",
"disabled",
"formnovalidate",
"hidden",
"ismap",
"itemscope",
"loop",
"multiple",
"muted",
"nomodule",
"novalidate",
"open",
"playsinline",
"readonly",
"required",
"reversed",
"selected",
"truespeed",
"webkitdirectory",
];
pub(crate) fn str_truthy(value: &str) -> bool {
!value.is_empty() && value != "0" && value.to_lowercase() != "false"
}
pub(crate) fn truthy(value: &AttributeValue) -> bool {
match value {
AttributeValue::Text(value) => str_truthy(value),
AttributeValue::Bool(value) => *value,
AttributeValue::Int(value) => *value != 0,
AttributeValue::Float(value) => *value != 0.0,
_ => false,
}
}
pub(crate) fn write_attribute<W: Write + ?Sized>(
buf: &mut W,
attr: &Attribute,
) -> std::fmt::Result {
let name = &attr.name;
match &attr.value {
AttributeValue::Text(value) => write!(buf, " {name}=\"{value}\""),
AttributeValue::Bool(value) => write!(buf, " {name}={value}"),
AttributeValue::Int(value) => write!(buf, " {name}={value}"),
AttributeValue::Float(value) => write!(buf, " {name}={value}"),
_ => Ok(()),
}
}
pub(crate) fn write_value_unquoted<W: Write + ?Sized>(
buf: &mut W,
value: &AttributeValue,
) -> std::fmt::Result {
match value {
AttributeValue::Text(value) => write!(buf, "{}", value),
AttributeValue::Bool(value) => write!(buf, "{}", value),
AttributeValue::Int(value) => write!(buf, "{}", value),
AttributeValue::Float(value) => write!(buf, "{}", value),
_ => Ok(()),
}
}