mirror of
https://github.com/DioxusLabs/dioxus
synced 2024-11-27 14:40:44 +00:00
772 lines
26 KiB
Rust
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]]
|
|
},
|
|
)
|
|
}
|