dioxus/packages/rsx/src/lib.rs
2023-08-15 12:19:05 -05:00

772 lines
26 KiB
Rust

//! Parse the root tokens in the rsx!{} macro
//! =========================================
//!
//! This parsing path emerges directly from the macro call, with `RsxRender` being the primary entrance into parsing.
//! This feature must support:
//! - [x] Optionally rendering if the `in XYZ` pattern is present
//! - [x] Fragments as top-level element (through ambiguous)
//! - [x] Components as top-level element (through ambiguous)
//! - [x] Tags as top-level elements (through ambiguous)
//! - [x] Good errors if parsing fails
//!
//! Any errors in using rsx! will likely occur when people start using it, so the first errors must be really helpful.
#[macro_use]
mod errors;
mod component;
mod element;
#[cfg(feature = "hot_reload")]
pub mod hot_reload;
mod ifmt;
mod node;
use std::{fmt::Debug, hash::Hash};
// Re-export the namespaces into each other
pub use component::*;
#[cfg(feature = "hot_reload")]
use dioxus_core::{Template, TemplateAttribute, TemplateNode};
pub use element::*;
#[cfg(feature = "hot_reload")]
pub use hot_reload::HotReloadingContext;
pub use ifmt::*;
#[cfg(feature = "hot_reload")]
use internment::Intern;
pub use node::*;
// imports
use proc_macro2::TokenStream as TokenStream2;
use quote::{quote, ToTokens, TokenStreamExt};
use syn::{
parse::{Parse, ParseStream},
Result, Token,
};
#[cfg(feature = "hot_reload")]
// interns a object into a static object, resusing the value if it already exists
fn intern<T: Eq + Hash + Send + Sync + ?Sized + 'static>(s: impl Into<Intern<T>>) -> &'static T {
s.into().as_ref()
}
/// Fundametnally, every CallBody is a template
#[derive(Default, Debug)]
pub struct CallBody {
pub roots: Vec<BodyNode>,
}
impl CallBody {
#[cfg(feature = "hot_reload")]
/// This will try to create a new template from the current body and the previous body. This will return None if the rsx has some dynamic part that has changed.
/// This function intentionally leaks memory to create a static template.
/// Keeping the template static allows us to simplify the core of dioxus and leaking memory in dev mode is less of an issue.
/// the previous_location is the location of the previous template at the time the template was originally compiled.
pub fn update_template<Ctx: HotReloadingContext>(
&self,
template: Option<CallBody>,
location: &'static str,
) -> Option<Template<'static>> {
let mut renderer: TemplateRenderer = TemplateRenderer {
roots: &self.roots,
location: None,
};
renderer.update_template::<Ctx>(template, location)
}
/// Render the template with a manually set file location. This should be used when multiple rsx! calls are used in the same macro
pub fn render_with_location(&self, location: String) -> TokenStream2 {
let body = TemplateRenderer {
roots: &self.roots,
location: Some(location),
};
quote! {
::dioxus::core::LazyNodes::new( move | __cx: &::dioxus::core::ScopeState| -> ::dioxus::core::VNode {
#body
})
}
}
}
impl Parse for CallBody {
fn parse(input: ParseStream) -> Result<Self> {
let mut roots = Vec::new();
while !input.is_empty() {
let node = input.parse::<BodyNode>()?;
if input.peek(Token![,]) {
let _ = input.parse::<Token![,]>();
}
roots.push(node);
}
Ok(Self { roots })
}
}
/// Serialize the same way, regardless of flavor
impl ToTokens for CallBody {
fn to_tokens(&self, out_tokens: &mut TokenStream2) {
let body = TemplateRenderer {
roots: &self.roots,
location: None,
};
out_tokens.append_all(quote! {
::dioxus::core::LazyNodes::new( move | __cx: &::dioxus::core::ScopeState| -> ::dioxus::core::VNode {
#body
})
})
}
}
#[derive(Default, Debug)]
pub struct RenderCallBody(pub CallBody);
impl ToTokens for RenderCallBody {
fn to_tokens(&self, out_tokens: &mut TokenStream2) {
let body: TemplateRenderer = TemplateRenderer {
roots: &self.0.roots,
location: None,
};
out_tokens.append_all(quote! {
Some({
let __cx = cx;
#body
})
})
}
}
pub struct TemplateRenderer<'a> {
pub roots: &'a [BodyNode],
pub location: Option<String>,
}
impl<'a> TemplateRenderer<'a> {
#[cfg(feature = "hot_reload")]
fn update_template<Ctx: HotReloadingContext>(
&mut self,
previous_call: Option<CallBody>,
location: &'static str,
) -> Option<Template<'static>> {
let mut mapping = previous_call.map(|call| DynamicMapping::from(call.roots));
let mut context = DynamicContext::default();
let mut roots = Vec::new();
for (idx, root) in self.roots.iter().enumerate() {
context.current_path.push(idx as u8);
roots.push(context.update_node::<Ctx>(root, &mut mapping)?);
context.current_path.pop();
}
Some(Template {
name: location,
roots: intern(roots.as_slice()),
node_paths: intern(
context
.node_paths
.into_iter()
.map(|path| intern(path.as_slice()))
.collect::<Vec<_>>()
.as_slice(),
),
attr_paths: intern(
context
.attr_paths
.into_iter()
.map(|path| intern(path.as_slice()))
.collect::<Vec<_>>()
.as_slice(),
),
})
}
}
impl<'a> ToTokens for TemplateRenderer<'a> {
fn to_tokens(&self, out_tokens: &mut TokenStream2) {
let mut context = DynamicContext::default();
let key = match self.roots.get(0) {
Some(BodyNode::Element(el)) if self.roots.len() == 1 => el.key.clone(),
Some(BodyNode::Component(comp)) if self.roots.len() == 1 => comp.key().cloned(),
_ => None,
};
let key_tokens = match key {
Some(tok) => quote! { Some( __cx.raw_text(#tok) ) },
None => quote! { None },
};
let spndbg = format!("{:?}", self.roots[0].span());
let root_col = spndbg
.rsplit_once("..")
.and_then(|(_, after)| after.split_once(')').map(|(before, _)| before))
.unwrap_or_default();
let root_printer = self.roots.iter().enumerate().map(|(idx, root)| {
context.current_path.push(idx as u8);
let out = context.render_static_node(root);
context.current_path.pop();
out
});
let name = match self.location {
Some(ref loc) => quote! { #loc },
None => quote! {
concat!(
file!(),
":",
line!(),
":",
column!(),
":",
#root_col
)
},
};
// Render and release the mutable borrow on context
let roots = quote! { #( #root_printer ),* };
let root_count = self.roots.len();
let node_printer = &context.dynamic_nodes;
let dyn_attr_printer = &context.dynamic_attributes;
let node_paths = context.node_paths.iter().map(|it| quote!(&[#(#it),*]));
let attr_paths = context.attr_paths.iter().map(|it| quote!(&[#(#it),*]));
out_tokens.append_all(quote! {
static TEMPLATE: ::dioxus::core::Template = ::dioxus::core::Template {
name: #name,
roots: &[ #roots ],
node_paths: &[ #(#node_paths),* ],
attr_paths: &[ #(#attr_paths),* ],
};
::dioxus::core::VNode {
parent: None,
key: #key_tokens,
template: std::cell::Cell::new(TEMPLATE),
root_ids: dioxus::core::exports::bumpalo::collections::Vec::with_capacity_in(#root_count, __cx.bump()).into(),
dynamic_nodes: __cx.bump().alloc([ #( #node_printer ),* ]),
dynamic_attrs: __cx.bump().alloc([ #( #dyn_attr_printer ),* ]),
}
});
}
}
#[cfg(feature = "hot_reload")]
#[derive(Default, Debug)]
struct DynamicMapping {
attribute_to_idx: std::collections::HashMap<ElementAttr, Vec<usize>>,
last_attribute_idx: usize,
node_to_idx: std::collections::HashMap<BodyNode, Vec<usize>>,
last_element_idx: usize,
}
#[cfg(feature = "hot_reload")]
impl DynamicMapping {
fn from(nodes: Vec<BodyNode>) -> Self {
let mut new = Self::default();
for node in nodes {
new.add_node(node);
}
new
}
fn get_attribute_idx(&mut self, attr: &ElementAttr) -> Option<usize> {
self.attribute_to_idx
.get_mut(attr)
.and_then(|idxs| idxs.pop())
}
fn get_node_idx(&mut self, node: &BodyNode) -> Option<usize> {
self.node_to_idx.get_mut(node).and_then(|idxs| idxs.pop())
}
fn insert_attribute(&mut self, attr: ElementAttr) -> usize {
let idx = self.last_attribute_idx;
self.last_attribute_idx += 1;
self.attribute_to_idx.entry(attr).or_default().push(idx);
idx
}
fn insert_node(&mut self, node: BodyNode) -> usize {
let idx = self.last_element_idx;
self.last_element_idx += 1;
self.node_to_idx.entry(node).or_default().push(idx);
idx
}
fn add_node(&mut self, node: BodyNode) {
match node {
BodyNode::Element(el) => {
for attr in el.attributes {
match &attr.attr {
ElementAttr::CustomAttrText { value, .. }
| ElementAttr::AttrText { value, .. }
if value.is_static() => {}
ElementAttr::AttrExpression { .. }
| ElementAttr::AttrText { .. }
| ElementAttr::CustomAttrText { .. }
| ElementAttr::CustomAttrExpression { .. }
| ElementAttr::EventTokens { .. } => {
self.insert_attribute(attr.attr);
}
}
}
for child in el.children {
self.add_node(child);
}
}
BodyNode::Text(text) if text.is_static() => {}
BodyNode::RawExpr(_)
| BodyNode::Text(_)
| BodyNode::ForLoop(_)
| BodyNode::IfChain(_)
| BodyNode::Component(_) => {
self.insert_node(node);
}
}
}
}
// As we create the dynamic nodes, we want to keep track of them in a linear fashion
// We'll use the size of the vecs to determine the index of the dynamic node in the final output
#[derive(Default, Debug)]
pub struct DynamicContext<'a> {
dynamic_nodes: Vec<&'a BodyNode>,
dynamic_attributes: Vec<&'a ElementAttrNamed>,
current_path: Vec<u8>,
node_paths: Vec<Vec<u8>>,
attr_paths: Vec<Vec<u8>>,
}
impl<'a> DynamicContext<'a> {
#[cfg(feature = "hot_reload")]
fn update_node<Ctx: HotReloadingContext>(
&mut self,
root: &'a BodyNode,
mapping: &mut Option<DynamicMapping>,
) -> Option<TemplateNode<'static>> {
match root {
BodyNode::Element(el) => {
let element_name_rust = el.name.to_string();
let mut static_attrs = Vec::new();
for attr in &el.attributes {
match &attr.attr {
ElementAttr::AttrText { name, value } if value.is_static() => {
let value = value.source.as_ref().unwrap();
let attribute_name_rust = name.to_string();
let (name, namespace) =
Ctx::map_attribute(&element_name_rust, &attribute_name_rust)
.unwrap_or((intern(attribute_name_rust.as_str()), None));
static_attrs.push(TemplateAttribute::Static {
name,
namespace,
value: intern(value.value().as_str()),
})
}
ElementAttr::CustomAttrText { name, value } if value.is_static() => {
let value = value.source.as_ref().unwrap();
static_attrs.push(TemplateAttribute::Static {
name: intern(name.value().as_str()),
namespace: None,
value: intern(value.value().as_str()),
})
}
ElementAttr::AttrExpression { .. }
| ElementAttr::AttrText { .. }
| ElementAttr::CustomAttrText { .. }
| ElementAttr::CustomAttrExpression { .. }
| ElementAttr::EventTokens { .. } => {
let idx = match mapping {
Some(mapping) => mapping.get_attribute_idx(&attr.attr)?,
None => self.dynamic_attributes.len(),
};
self.dynamic_attributes.push(attr);
if self.attr_paths.len() <= idx {
self.attr_paths.resize_with(idx + 1, Vec::new);
}
self.attr_paths[idx] = self.current_path.clone();
static_attrs.push(TemplateAttribute::Dynamic { id: idx })
}
}
}
let mut children = Vec::new();
for (idx, root) in el.children.iter().enumerate() {
self.current_path.push(idx as u8);
children.push(self.update_node::<Ctx>(root, mapping)?);
self.current_path.pop();
}
let (tag, namespace) = Ctx::map_element(&element_name_rust)
.unwrap_or((intern(element_name_rust.as_str()), None));
Some(TemplateNode::Element {
tag,
namespace,
attrs: intern(static_attrs.into_boxed_slice()),
children: intern(children.as_slice()),
})
}
BodyNode::Text(text) if text.is_static() => {
let text = text.source.as_ref().unwrap();
Some(TemplateNode::Text {
text: intern(text.value().as_str()),
})
}
BodyNode::RawExpr(_)
| BodyNode::Text(_)
| BodyNode::ForLoop(_)
| BodyNode::IfChain(_)
| BodyNode::Component(_) => {
let idx = match mapping {
Some(mapping) => mapping.get_node_idx(root)?,
None => self.dynamic_nodes.len(),
};
self.dynamic_nodes.push(root);
if self.node_paths.len() <= idx {
self.node_paths.resize_with(idx + 1, Vec::new);
}
self.node_paths[idx] = self.current_path.clone();
Some(match root {
BodyNode::Text(_) => TemplateNode::DynamicText { id: idx },
_ => TemplateNode::Dynamic { id: idx },
})
}
}
}
fn render_static_node(&mut self, root: &'a BodyNode) -> TokenStream2 {
match root {
BodyNode::Element(el) => {
let el_name = &el.name;
let ns = |name| match el_name {
ElementName::Ident(i) => quote! { dioxus_elements::#i::#name },
ElementName::Custom(_) => quote! { None },
};
let static_attrs = el.attributes.iter().map(|attr| match &attr.attr {
ElementAttr::AttrText { name, value } if value.is_static() => {
let value = value.to_static().unwrap();
let ns = ns(quote!(#name.1));
let name = match el_name {
ElementName::Ident(_) => quote! { #el_name::#name.0 },
ElementName::Custom(_) => {
let as_string = name.to_string();
quote! { #as_string }
}
};
quote! {
::dioxus::core::TemplateAttribute::Static {
name: #name,
namespace: #ns,
value: #value,
// todo: we don't diff these so we never apply the volatile flag
// volatile: dioxus_elements::#el_name::#name.2,
}
}
}
ElementAttr::CustomAttrText { name, value } if value.is_static() => {
let value = value.to_static().unwrap();
quote! {
::dioxus::core::TemplateAttribute::Static {
name: #name,
namespace: None,
value: #value,
// todo: we don't diff these so we never apply the volatile flag
// volatile: dioxus_elements::#el_name::#name.2,
}
}
}
ElementAttr::AttrExpression { .. }
| ElementAttr::AttrText { .. }
| ElementAttr::CustomAttrText { .. }
| ElementAttr::CustomAttrExpression { .. }
| ElementAttr::EventTokens { .. } => {
let ct = self.dynamic_attributes.len();
self.dynamic_attributes.push(attr);
self.attr_paths.push(self.current_path.clone());
quote! { ::dioxus::core::TemplateAttribute::Dynamic { id: #ct } }
}
});
let attrs = quote! { #(#static_attrs),*};
let children = el.children.iter().enumerate().map(|(idx, root)| {
self.current_path.push(idx as u8);
let out = self.render_static_node(root);
self.current_path.pop();
out
});
let _opt = el.children.len() == 1;
let children = quote! { #(#children),* };
let ns = ns(quote!(NAME_SPACE));
let el_name = el_name.tag_name();
quote! {
::dioxus::core::TemplateNode::Element {
tag: #el_name,
namespace: #ns,
attrs: &[ #attrs ],
children: &[ #children ],
}
}
}
BodyNode::Text(text) if text.is_static() => {
let text = text.to_static().unwrap();
quote! { ::dioxus::core::TemplateNode::Text{ text: #text } }
}
BodyNode::RawExpr(_)
| BodyNode::Text(_)
| BodyNode::ForLoop(_)
| BodyNode::IfChain(_)
| BodyNode::Component(_) => {
let ct = self.dynamic_nodes.len();
self.dynamic_nodes.push(root);
self.node_paths.push(self.current_path.clone());
match root {
BodyNode::Text(_) => {
quote! { ::dioxus::core::TemplateNode::DynamicText { id: #ct } }
}
_ => quote! { ::dioxus::core::TemplateNode::Dynamic { id: #ct } },
}
}
}
}
}
#[cfg(feature = "hot_reload")]
#[test]
fn create_template() {
let input = quote! {
svg {
width: 100,
height: "100px",
"width2": 100,
"height2": "100px",
p {
"hello world"
}
(0..10).map(|i| rsx!{"{i}"})
}
};
struct Mock;
impl HotReloadingContext for Mock {
fn map_attribute(
element_name_rust: &str,
attribute_name_rust: &str,
) -> Option<(&'static str, Option<&'static str>)> {
match element_name_rust {
"svg" => match attribute_name_rust {
"width" => Some(("width", Some("style"))),
"height" => Some(("height", Some("style"))),
_ => None,
},
_ => None,
}
}
fn map_element(element_name_rust: &str) -> Option<(&'static str, Option<&'static str>)> {
match element_name_rust {
"svg" => Some(("svg", Some("svg"))),
_ => None,
}
}
}
let call_body: CallBody = syn::parse2(input).unwrap();
let template = call_body.update_template::<Mock>(None, "testing").unwrap();
dbg!(template);
assert_eq!(
template,
Template {
name: "testing",
roots: &[TemplateNode::Element {
tag: "svg",
namespace: Some("svg"),
attrs: &[
TemplateAttribute::Dynamic { id: 0 },
TemplateAttribute::Static {
name: "height",
namespace: Some("style"),
value: "100px",
},
TemplateAttribute::Dynamic { id: 1 },
TemplateAttribute::Static {
name: "height2",
namespace: None,
value: "100px",
},
],
children: &[
TemplateNode::Element {
tag: "p",
namespace: None,
attrs: &[],
children: &[TemplateNode::Text {
text: "hello world",
}],
},
TemplateNode::Dynamic { id: 0 }
],
}],
node_paths: &[&[0, 1,],],
attr_paths: &[&[0,], &[0,],],
},
)
}
#[cfg(feature = "hot_reload")]
#[test]
fn diff_template() {
use dioxus_core::Scope;
#[allow(unused, non_snake_case)]
fn Comp(_: Scope) -> dioxus_core::Element {
None
}
let input = quote! {
svg {
width: 100,
height: "100px",
"width2": 100,
"height2": "100px",
p {
"hello world"
}
(0..10).map(|i| rsx!{"{i}"}),
(0..10).map(|i| rsx!{"{i}"}),
(0..11).map(|i| rsx!{"{i}"}),
Comp{}
}
};
#[derive(Debug)]
struct Mock;
impl HotReloadingContext for Mock {
fn map_attribute(
element_name_rust: &str,
attribute_name_rust: &str,
) -> Option<(&'static str, Option<&'static str>)> {
match element_name_rust {
"svg" => match attribute_name_rust {
"width" => Some(("width", Some("style"))),
"height" => Some(("height", Some("style"))),
_ => None,
},
_ => None,
}
}
fn map_element(element_name_rust: &str) -> Option<(&'static str, Option<&'static str>)> {
match element_name_rust {
"svg" => Some(("svg", Some("svg"))),
_ => None,
}
}
}
let call_body1: CallBody = syn::parse2(input).unwrap();
let template = call_body1.update_template::<Mock>(None, "testing").unwrap();
dbg!(template);
// scrambling the attributes should not cause a full rebuild
let input = quote! {
div {
"width2": 100,
height: "100px",
"height2": "100px",
width: 100,
Comp{}
(0..11).map(|i| rsx!{"{i}"}),
(0..10).map(|i| rsx!{"{i}"}),
(0..10).map(|i| rsx!{"{i}"}),
p {
"hello world"
}
}
};
let call_body2: CallBody = syn::parse2(input).unwrap();
let template = call_body2
.update_template::<Mock>(Some(call_body1), "testing")
.unwrap();
dbg!(template);
assert_eq!(
template,
Template {
name: "testing",
roots: &[TemplateNode::Element {
tag: "div",
namespace: None,
attrs: &[
TemplateAttribute::Dynamic { id: 1 },
TemplateAttribute::Static {
name: "height",
namespace: None,
value: "100px",
},
TemplateAttribute::Static {
name: "height2",
namespace: None,
value: "100px",
},
TemplateAttribute::Dynamic { id: 0 },
],
children: &[
TemplateNode::Dynamic { id: 3 },
TemplateNode::Dynamic { id: 2 },
TemplateNode::Dynamic { id: 1 },
TemplateNode::Dynamic { id: 0 },
TemplateNode::Element {
tag: "p",
namespace: None,
attrs: &[],
children: &[TemplateNode::Text {
text: "hello world",
}],
},
],
}],
node_paths: &[&[0, 3], &[0, 2], &[0, 1], &[0, 0]],
attr_paths: &[&[0], &[0]]
},
)
}