dioxus/packages/ssr/src/renderer.rs

419 lines
14 KiB
Rust
Raw Normal View History

2022-12-06 17:41:47 -08:00
use super::cache::Segment;
use crate::cache::StringCache;
2024-01-10 17:57:15 -06:00
use dioxus_core::RenderReturn;
2023-08-08 10:56:41 -07:00
2023-08-08 11:27:29 -07:00
use dioxus_core::Attribute;
2024-01-10 09:56:03 -06:00
use dioxus_core::{prelude::*, AttributeValue, DynamicNode};
2022-12-06 17:41:47 -08:00
use std::collections::HashMap;
use std::fmt::Write;
use std::sync::Arc;
2022-12-06 17:41:47 -08:00
/// A virtualdom renderer that caches the templates it has seen for faster rendering
#[derive(Default)]
pub struct Renderer {
/// should we do our best to prettify the output?
pub pretty: bool,
/// Control if elements are written onto a new line
pub newline: bool,
/// Should we sanitize text nodes? (escape HTML)
pub sanitize: bool,
/// Choose to write ElementIDs into elements so the page can be re-hydrated later on
pub pre_render: bool,
// Currently not implemented
// Don't proceed onto new components. Instead, put the name of the component.
pub skip_components: bool,
/// A cache of templates that have been rendered
template_cache: HashMap<&'static str, Arc<StringCache>>,
2023-12-27 10:17:18 -06:00
/// The current dynamic node id for hydration
dynamic_node_id: usize,
2022-12-06 17:41:47 -08:00
}
impl Renderer {
pub fn new() -> Self {
Self::default()
}
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(&mut self, buf: &mut impl Write, dom: &VirtualDom) -> std::fmt::Result {
self.render_scope(buf, dom, ScopeId::ROOT)
2022-12-06 17:41:47 -08:00
}
pub fn render_scope(
&mut self,
buf: &mut impl Write,
dom: &VirtualDom,
scope: ScopeId,
) -> std::fmt::Result {
// We should never ever run into async or errored nodes in SSR
// Error boundaries and suspense boundaries will convert these to sync
2022-12-22 14:45:48 -05:00
if let RenderReturn::Ready(node) = dom.get_scope(scope).unwrap().root_node() {
2023-12-27 10:17:18 -06:00
self.dynamic_node_id = 0;
2022-12-06 17:41:47 -08:00
self.render_template(buf, dom, node)?
};
Ok(())
}
fn render_template(
&mut self,
buf: &mut impl Write,
dom: &VirtualDom,
template: &VNode,
) -> std::fmt::Result {
let entry = self
.template_cache
2022-12-19 19:28:44 -06:00
.entry(template.template.get().name)
.or_insert_with({
let prerender = self.pre_render;
move || Arc::new(StringCache::from_template(template, prerender).unwrap())
})
2022-12-06 17:41:47 -08:00
.clone();
2024-01-15 11:06:27 -06:00
let mut inner_html = None;
// We need to keep track of the dynamic styles so we can insert them into the right place
let mut accumulated_dynamic_styles = Vec::new();
// We need to keep track of the listeners so we can insert them into the right place
let mut accumulated_listeners = Vec::new();
for segment in entry.segments.iter() {
match segment {
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 {
// The onmounted event doesn't need a DOM listener
if attr.name != "onmounted" {
accumulated_listeners.push(attr.name);
}
}
}
}
}
Segment::Node(idx) => match &template.dynamic_nodes[*idx] {
DynamicNode::Component(node) => {
if self.skip_components {
write!(buf, "<{}><{}/>", node.name, node.name)?;
} else {
let scope = node.mounted_scope(*idx, template, dom).unwrap();
let node = scope.root_node();
match node {
RenderReturn::Ready(node) => {
self.render_template(buf, dom, node)?
}
_ => todo!(
"generally, scopes should be sync, only if being traversed"
),
}
}
}
DynamicNode::Text(text) => {
// in SSR, we are concerned that we can't hunt down the right text node since they might get merged
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,
"<pre data-node-hydration={}></pre>",
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 we are inside a style tag, we don't need to write the style attribute
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, "\"")?;
}
// clear the accumulated styles
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 => {
// first write the id
write!(buf, "{}", self.dynamic_node_id)?;
self.dynamic_node_id += 1;
// then write any listeners
for name in accumulated_listeners.drain(..) {
write!(buf, ",{}:", &name[2..])?;
write!(buf, "{}", dioxus_html::event_bubbles(name) as u8)?;
}
}
Segment::RootNodeMarker => {
write!(buf, "{}", self.dynamic_node_id)?;
self.dynamic_node_id += 1
}
}
}
2022-12-06 17:41:47 -08:00
Ok(())
}
}
#[test]
fn to_string_works() {
use dioxus::prelude::*;
2024-01-15 11:12:24 -06:00
fn app() -> Element {
2022-12-06 17:41:47 -08:00
let dynamic = 123;
2023-02-14 09:06:59 -06:00
let dyn2 = "</diiiiiiiiv>"; // this should be escaped
2022-12-06 17:41:47 -08:00
2024-01-16 13:18:46 -06:00
rsx! {
2022-12-06 17:41:47 -08:00
div { class: "asdasdasd", class: "asdasdasd", id: "id-{dynamic}",
"Hello world 1 -->"
"{dynamic}"
"<-- Hello world 2"
2022-12-06 17:41:47 -08:00
div { "nest 1" }
div {}
div { "nest 2" }
"{dyn2}"
for i in (0..5) {
div { "finalize {i}" }
}
2022-12-06 17:41:47 -08:00
}
}
}
let mut dom = VirtualDom::new(app);
2024-01-10 17:57:15 -06:00
_ = dom.rebuild(&mut dioxus_core::NoOpMutations);
2022-12-06 17:41:47 -08:00
let mut renderer = Renderer::new();
let out = renderer.render(&dom);
2022-12-07 15:29:32 -08:00
for item in renderer.template_cache.iter() {
if item.1.segments.len() > 5 {
assert_eq!(
item.1.segments,
vec![
2023-09-19 10:23:36 -05:00
PreRendered("<div class=\"asdasdasd asdasdasd\"".into(),),
2022-12-07 15:29:32 -08:00
Attr(0,),
2023-03-22 10:10:18 -05:00
StyleMarker {
inside_style_tag: false,
},
2023-04-07 09:35:32 -05:00
PreRendered(">".into()),
InnerHtmlMarker,
PreRendered("Hello world 1 --&gt;".into(),),
2022-12-07 15:29:32 -08:00
Node(0,),
PreRendered(
2023-02-14 09:06:59 -06:00
"&lt;-- Hello world 2<div>nest 1</div><div></div><div>nest 2</div>".into(),
2022-12-07 15:29:32 -08:00
),
Node(1,),
Node(2,),
PreRendered("</div>".into(),),
]
);
}
}
2022-12-06 17:41:47 -08:00
use Segment::*;
2022-12-07 15:29:32 -08:00
2023-09-19 10:23:36 -05:00
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>");
2022-12-06 17:41:47 -08:00
}
2023-08-08 10:56:41 -07:00
#[test]
fn empty_for_loop_works() {
use dioxus::prelude::*;
2024-01-15 11:12:24 -06:00
fn app() -> Element {
2024-01-16 13:18:46 -06:00
rsx! {
div { class: "asdasdasd",
for _ in (0..5) {
}
}
}
}
let mut dom = VirtualDom::new(app);
2024-01-10 17:57:15 -06:00
_ = 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\"".into(),),
Attr(0,),
StyleMarker {
inside_style_tag: false,
},
PreRendered(">".into()),
InnerHtmlMarker,
PreRendered("</div>".into(),),
]
);
}
}
use Segment::*;
assert_eq!(out, "<div class=\"asdasdasd\"></div>");
}
#[test]
fn empty_render_works() {
use dioxus::prelude::*;
2024-01-15 11:12:24 -06:00
fn app() -> Element {
2024-01-16 13:18:46 -06:00
rsx! {}
}
let mut dom = VirtualDom::new(app);
2024-01-10 17:57:15 -06:00
_ = 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, "");
2022-12-06 17:41:47 -08:00
}
2023-08-08 10:56:41 -07:00
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,
}
}
2023-08-08 11:27:29 -07:00
pub(crate) fn write_attribute(buf: &mut impl Write, attr: &Attribute) -> std::fmt::Result {
let name = &attr.name;
2024-01-10 17:57:15 -06:00
match &attr.value {
2023-08-08 11:27:29 -07:00
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(()),
}
}
2023-08-08 10:56:41 -07:00
pub(crate) fn write_value_unquoted(
buf: &mut impl Write,
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(()),
}
}