123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360 |
- use super::cache::Segment;
- use crate::cache::StringCache;
- use dioxus_core::Attribute;
- use dioxus_core::{prelude::*, AttributeValue, DynamicNode, RenderReturn};
- use std::collections::HashMap;
- use std::fmt::Write;
- use std::sync::Arc;
- /// 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>>,
- /// The current dynamic node id for hydration
- dynamic_node_id: usize,
- }
- 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)
- }
- 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
- if let RenderReturn::Ready(node) = dom.get_scope(scope).unwrap().root_node() {
- self.dynamic_node_id = 0;
- 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
- .entry(template.template.get().name)
- .or_insert_with({
- let prerender = self.pre_render;
- move || Arc::new(StringCache::from_template(template, prerender).unwrap())
- })
- .clone();
- 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 attr = &template.dynamic_attrs[*idx];
- 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!(buf, " {}=", attr.name)?;
- write_value(buf, &attr.value)?;
- }
- } 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 id = node.mounted_scope().unwrap();
- let scope = dom.get_scope(id).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
- }
- }
- }
- Ok(())
- }
- }
- #[test]
- fn to_string_works() {
- use dioxus::prelude::*;
- fn app(cx: Scope) -> Element {
- let dynamic = 123;
- let dyn2 = "</diiiiiiiiv>"; // this should be escaped
- render! {
- div { class: "asdasdasd", class: "asdasdasd", id: "id-{dynamic}",
- "Hello world 1 -->"
- "{dynamic}"
- "<-- Hello world 2"
- div { "nest 1" }
- div {}
- div { "nest 2" }
- "{dyn2}"
- (0..5).map(|i| rsx! { div { "finalize {i}" } })
- }
- }
- }
- let mut dom = VirtualDom::new(app);
- _ = dom.rebuild();
- 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\" class=\"asdasdasd\"".into(),),
- Attr(0,),
- StyleMarker {
- inside_style_tag: false,
- },
- PreRendered(">".into()),
- InnerHtmlMarker,
- PreRendered("Hello world 1 -->".into(),),
- Node(0,),
- PreRendered(
- "<-- Hello world 2<div>nest 1</div><div></div><div>nest 2</div>".into(),
- ),
- Node(1,),
- Node(2,),
- PreRendered("</div>".into(),),
- ]
- );
- }
- }
- use Segment::*;
- assert_eq!(out, "<div class=\"asdasdasd\" class=\"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>");
- }
- 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(buf: &mut impl Write, 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(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(()),
- }
- }
- 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(()),
- }
- }
|