ソースを参照

Add a ton of comments to rsx/hotreload, add snapshot testing, refactor a bit to simplify the crate (#2130)

Merge dynamic context and dynamic mapping, clean up the rsx hotreload logic, and add location data to the Rsx objects
Jonathan Kelley 1 年間 前
コミット
eb79e61642

+ 29 - 0
Cargo.lock

@@ -2469,6 +2469,7 @@ name = "dioxus-rsx"
 version = "0.5.0-alpha.2"
 dependencies = [
  "dioxus-core 0.5.0-alpha.2",
+ "insta",
  "internment",
  "krates",
  "proc-macro2",
@@ -4728,6 +4729,19 @@ dependencies = [
  "generic-array 0.14.7",
 ]
 
+[[package]]
+name = "insta"
+version = "1.36.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0a7c22c4d34ef4788c351e971c52bfdfe7ea2766f8c5466bc175dd46e52ac22e"
+dependencies = [
+ "console",
+ "lazy_static",
+ "linked-hash-map",
+ "similar",
+ "yaml-rust",
+]
+
 [[package]]
 name = "instant"
 version = "0.1.12"
@@ -7931,6 +7945,12 @@ version = "0.1.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f27f6278552951f1f2b8cf9da965d10969b2efdea95a6ec47987ab46edfe263a"
 
+[[package]]
+name = "similar"
+version = "2.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32fea41aca09ee824cc9724996433064c89f7777e60762749a4170a14abbfa21"
+
 [[package]]
 name = "simple_logger"
 version = "4.3.3"
@@ -10250,6 +10270,15 @@ version = "0.8.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "927da81e25be1e1a2901d59b81b37dd2efd1fc9c9345a55007f09bf5a2d3ee03"
 
+[[package]]
+name = "yaml-rust"
+version = "0.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85"
+dependencies = [
+ "linked-hash-map",
+]
+
 [[package]]
 name = "yansi"
 version = "0.5.1"

+ 13 - 0
Cargo.toml

@@ -114,6 +114,19 @@ axum-extra = "0.9.2"
 reqwest = "0.11.24"
 owo-colors = "4.0.0"
 
+[workspace.dev-dependencies]
+isnta = "1.36.1"
+
+# speed up some macros by optimizing them
+[profile.dev.package.insta]
+opt-level = 3
+
+[profile.dev.package.similar]
+opt-level = 3
+
+[profile.dev.package.dioxus-core-macro]
+opt-level = 3
+
 # Enable a small amount of optimization in debug mode
 [profile.cli-dev]
 inherits = "dev"

+ 1 - 0
packages/autofmt/src/writer.rs

@@ -262,6 +262,7 @@ impl<'a> Writer<'a> {
                 then_branch,
                 else_if_branch,
                 else_branch,
+                ..
             } = chain;
 
             write!(

+ 0 - 1
packages/cli/rustfmt.toml

@@ -1 +0,0 @@
-imports_granularity = "Crate"

+ 1 - 0
packages/rsx-rosetta/src/lib.rs

@@ -143,6 +143,7 @@ pub fn collect_svgs(children: &mut [BodyNode], out: &mut Vec<BodyNode>) {
                     manual_props: None,
                     key: None,
                     brace: Default::default(),
+                    location: Default::default(),
                 });
 
                 std::mem::swap(child, &mut new_comp);

+ 8 - 0
packages/rsx/.vscode/settings.json

@@ -0,0 +1,8 @@
+{
+    "rust-analyzer.check.workspace": false,
+    // "rust-analyzer.check.extraArgs": [
+    //     "--features",
+    //     "hot_reload"
+    // ],
+    "rust-analyzer.cargo.features": "all"
+}

+ 5 - 1
packages/rsx/Cargo.toml

@@ -23,7 +23,11 @@ krates = { version = "0.16.6", optional = true }
 tracing = { workspace = true }
 
 [features]
-default = ["html"]
+default = ["html", "hot_reload"]
 hot_reload = ["krates", "internment", "dioxus-core"]
 serde = ["dep:serde"]
 html = []
+
+
+[dev-dependencies]
+insta = "1.36.1"

+ 21 - 0
packages/rsx/src/attribute.rs

@@ -8,7 +8,10 @@ use syn::{parse_quote, spanned::Spanned, Expr, ExprIf, Ident, LitStr};
 
 #[derive(PartialEq, Eq, Clone, Debug, Hash)]
 pub enum AttributeType {
+    /// An attribute that is known
     Named(ElementAttrNamed),
+
+    /// An attribute that's being spread in via the `..` syntax
     Spread(Expr),
 }
 
@@ -68,6 +71,24 @@ impl AttributeType {
             }
         }
     }
+
+    pub fn as_static_str_literal(&self) -> Option<(&ElementAttrName, &IfmtInput)> {
+        match self {
+            AttributeType::Named(ElementAttrNamed {
+                attr:
+                    ElementAttr {
+                        value: ElementAttrValue::AttrLiteral(value),
+                        name,
+                    },
+                ..
+            }) if value.is_static() => Some((name, value)),
+            _ => None,
+        }
+    }
+
+    pub fn is_static_str_literal(&self) -> bool {
+        self.as_static_str_literal().is_some()
+    }
 }
 
 #[derive(Clone, Debug)]

+ 7 - 13
packages/rsx/src/component.rs

@@ -11,6 +11,8 @@
 //! - [ ] Keys
 //! - [ ] Properties spreading with with `..` syntax
 
+use self::{location::CallerLocation, renderer::TemplateRenderer};
+
 use super::*;
 
 use proc_macro2::TokenStream as TokenStream2;
@@ -32,6 +34,7 @@ pub struct Component {
     pub children: Vec<BodyNode>,
     pub manual_props: Option<Expr>,
     pub brace: syn::token::Brace,
+    pub location: CallerLocation,
 }
 
 impl Parse for Component {
@@ -88,6 +91,7 @@ impl Parse for Component {
         }
 
         Ok(Self {
+            location: CallerLocation::default(),
             name,
             prop_gen_args,
             fields,
@@ -179,20 +183,10 @@ impl Component {
             toks.append_all(quote! {#field})
         }
         if !self.children.is_empty() {
-            let renderer: TemplateRenderer = TemplateRenderer {
-                roots: &self.children,
-                location: None,
-            };
-
-            toks.append_all(quote! {
-                .children(
-                    Some({ #renderer })
-                )
-            });
+            let renderer = TemplateRenderer::as_tokens(&self.children, None);
+            toks.append_all(quote! { .children( Some({ #renderer }) ) });
         }
-        toks.append_all(quote! {
-            .build()
-        });
+        toks.append_all(quote! { .build() });
         toks
     }
 

+ 416 - 0
packages/rsx/src/context.rs

@@ -0,0 +1,416 @@
+use std::collections::HashMap;
+
+use crate::*;
+use proc_macro2::TokenStream as TokenStream2;
+use quote::quote;
+
+/// 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> {
+    pub dynamic_nodes: Vec<&'a BodyNode>,
+    pub dynamic_attributes: Vec<Vec<&'a AttributeType>>,
+    pub current_path: Vec<u8>,
+    pub node_paths: Vec<Vec<u8>>,
+    pub attr_paths: Vec<Vec<u8>>,
+
+    /// Mapping variables used to map the old template to the new template
+    ///
+    /// This tracks whether or not we're tracking some nodes or attributes
+    /// If we're tracking, then we'll attempt to use the old mapping
+    is_tracking: bool,
+
+    /// The mapping of node to its index in the dynamic_nodes list
+    /// We use the fact that BodyNode is Hash/PartialEq to track the nodes when we run into them
+    node_to_idx: HashMap<BodyNode, Vec<usize>>,
+    last_element_idx: usize,
+
+    /// The mapping of attribute to its index in the dynamic_attributes list
+    /// We use the fact that AttributeType is Hash/PartialEq to track the attributes when we run into them
+    attribute_to_idx: HashMap<AttributeType, Vec<usize>>,
+    last_attribute_idx: usize,
+}
+
+impl<'a> DynamicContext<'a> {
+    pub fn new_with_old(template: Option<CallBody>) -> Self {
+        let mut new = Self::default();
+
+        if let Some(call) = template {
+            for node in call.roots {
+                new.track_node(node);
+            }
+            new.is_tracking = true;
+        }
+
+        new
+    }
+
+    /// Populate the dynamic context with our own roots
+    ///
+    /// This will call update_node on each root, attempting to build us a list of TemplateNodes that
+    /// we can render out.
+    ///
+    /// These TemplateNodes are the same one used in Dioxus core! We just serialize them out and then
+    /// they'll get picked up after codegen for compilation. Cool stuff.
+    ///
+    /// If updating fails (IE the root is a dynamic node that has changed), then we return None.
+    pub fn populate_by_updating<Ctx: HotReloadingContext>(
+        &mut self,
+        roots: &'a [BodyNode],
+    ) -> Option<Vec<TemplateNode>> {
+        // Create a list of new roots that we'll spit out
+        let mut roots_ = Vec::new();
+
+        // Populate the dynamic context with our own roots
+        for (idx, root) in roots.iter().enumerate() {
+            self.current_path.push(idx as u8);
+            roots_.push(self.update_node::<Ctx>(root)?);
+            self.current_path.pop();
+        }
+
+        Some(roots_)
+    }
+
+    /// Render a portion of an rsx callbody to a TemplateNode call
+    ///
+    /// We're assembling the templatenodes
+    pub fn render_static_node(&mut self, root: &'a BodyNode) -> TokenStream2 {
+        match root {
+            BodyNode::Element(el) => self.render_static_element(el),
+
+            BodyNode::Text(text) if text.is_static() => {
+                let text = text.to_static().unwrap();
+                quote! { dioxus_core::TemplateNode::Text { text: #text } }
+            }
+
+            BodyNode::ForLoop(for_loop) => self.render_for_loop(root, for_loop),
+
+            BodyNode::RawExpr(_)
+            | BodyNode::Text(_)
+            | BodyNode::IfChain(_)
+            | BodyNode::Component(_) => self.render_dynamic_node(root),
+        }
+    }
+
+    /// Render a for loop to a token stream
+    ///
+    /// This is basically just rendering a dynamic node, but with some extra bookkepping to track the
+    /// contents of the for loop in case we want to hot reload it
+    fn render_for_loop(&mut self, root: &'a BodyNode, _for_loop: &ForLoop) -> TokenStream2 {
+        self.render_dynamic_node(root)
+    }
+
+    fn render_dynamic_node(&mut self, root: &'a BodyNode) -> TokenStream2 {
+        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 } },
+        }
+    }
+
+    fn render_static_element(&mut self, el: &'a Element) -> TokenStream2 {
+        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
+            .merged_attributes
+            .iter()
+            .map(|attr| self.render_merged_attributes(attr, ns, el_name))
+            .collect::<Vec<_>>();
+
+        let children = el
+            .children
+            .iter()
+            .enumerate()
+            .map(|(idx, root)| self.render_children_nodes(idx, root))
+            .collect::<Vec<_>>();
+
+        let ns = ns(quote!(NAME_SPACE));
+        let el_name = el_name.tag_name();
+
+        quote! {
+            dioxus_core::TemplateNode::Element {
+                tag: #el_name,
+                namespace: #ns,
+                attrs: &[ #(#static_attrs)* ],
+                children: &[ #(#children),* ],
+            }
+        }
+    }
+
+    pub fn render_children_nodes(&mut self, idx: usize, root: &'a BodyNode) -> TokenStream2 {
+        self.current_path.push(idx as u8);
+        let out = self.render_static_node(root);
+        self.current_path.pop();
+        out
+    }
+
+    /// Render the attributes of an element
+    fn render_merged_attributes(
+        &mut self,
+        attr: &'a AttributeType,
+        ns: impl Fn(TokenStream2) -> TokenStream2,
+        el_name: &ElementName,
+    ) -> TokenStream2 {
+        // Rendering static attributes requires a bit more work than just a dynamic attrs
+        match attr.as_static_str_literal() {
+            // If it's static, we'll take this little optimization
+            Some((name, value)) => Self::render_static_attr(value, name, ns, el_name),
+
+            // Otherwise, we'll just render it as a dynamic attribute
+            // This will also insert the attribute into the dynamic_attributes list to assemble the final template
+            _ => self.render_dynamic_attr(attr),
+        }
+    }
+
+    fn render_static_attr(
+        value: &IfmtInput,
+        name: &ElementAttrName,
+        ns: impl Fn(TokenStream2) -> TokenStream2,
+        el_name: &ElementName,
+    ) -> TokenStream2 {
+        let value = value.to_static().unwrap();
+
+        let ns = match name {
+            ElementAttrName::BuiltIn(name) => ns(quote!(#name.1)),
+            ElementAttrName::Custom(_) => quote!(None),
+        };
+
+        let name = match (el_name, name) {
+            (ElementName::Ident(_), ElementAttrName::BuiltIn(_)) => quote! { #el_name::#name.0 },
+            _ => {
+                //hmmmm I think we could just totokens this, but the to_string might be inserting quotes
+                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,
+            },
+        }
+    }
+
+    /// If the attr is dynamic, we save it to the tracked attributes list
+    /// This will let us use this context at a later point in time to update the template
+    fn render_dynamic_attr(&mut self, attr: &'a AttributeType) -> TokenStream2 {
+        let ct = self.dynamic_attributes.len();
+
+        self.dynamic_attributes.push(vec![attr]);
+        self.attr_paths.push(self.current_path.clone());
+
+        quote! { dioxus_core::TemplateAttribute::Dynamic { id: #ct }, }
+    }
+
+    #[cfg(feature = "hot_reload")]
+    pub fn update_node<Ctx: HotReloadingContext>(
+        &mut self,
+        root: &'a BodyNode,
+    ) -> Option<TemplateNode> {
+        match root {
+            // The user is moving a static node around in the template
+            // Check this is a bit more complex but we can likely handle it
+            BodyNode::Element(el) => self.update_element::<Ctx>(el),
+
+            BodyNode::Text(text) if text.is_static() => {
+                let text = text.source.as_ref().unwrap();
+                let text = intern(text.value().as_str());
+                Some(TemplateNode::Text { text })
+            }
+
+            // The user is moving a dynamic node around in the template
+            // We *might* be able to handle it, but you never really know
+            BodyNode::RawExpr(_)
+            | BodyNode::Text(_)
+            | BodyNode::ForLoop(_)
+            | BodyNode::IfChain(_)
+            | BodyNode::Component(_) => self.update_dynamic_node(root),
+        }
+    }
+
+    /// Attempt to update a dynamic node in the template
+    ///
+    /// If the change between the old and new template results in a mapping that doesn't exist, then we need to bail out.
+    /// Basically if we *had* a mapping of `[0, 1]` and the new template is `[1, 2]`, then we need to bail out, since
+    /// the new mapping doesn't exist in the original.
+    fn update_dynamic_node(&mut self, root: &'a BodyNode) -> Option<TemplateNode> {
+        let idx = match self.has_tracked_nodes() {
+            //    Bail out if the mapping doesn't exist
+            //    The user put it new code in the template, and that code is not hotreloadable
+            true => self.tracked_node_idx(root)?,
+            false => self.dynamic_nodes.len(),
+        };
+
+        // Put the node in the dynamic nodes list
+        self.dynamic_nodes.push(root);
+
+        // Fill in as many paths as we need - might have to fill in more since the old tempate shrunk some and let some paths be empty
+        if self.node_paths.len() <= idx {
+            self.node_paths.resize_with(idx + 1, Vec::new);
+        }
+
+        // And then set the path of this node to the current path (which we're hitting during traversal)
+        self.node_paths[idx] = self.current_path.clone();
+
+        Some(match root {
+            BodyNode::Text(_) => TemplateNode::DynamicText { id: idx },
+            _ => TemplateNode::Dynamic { id: idx },
+        })
+    }
+
+    fn update_element<Ctx: HotReloadingContext>(
+        &mut self,
+        el: &'a Element,
+    ) -> Option<TemplateNode> {
+        let rust_name = el.name.to_string();
+
+        let mut static_attr_array = Vec::new();
+
+        for attr in &el.merged_attributes {
+            let template_attr = match attr.as_static_str_literal() {
+                // For static attributes, we don't need to pull in any mapping or anything
+                // We can just build them directly
+                Some((name, value)) => Self::make_static_attribute::<Ctx>(value, name, &rust_name),
+
+                // For dynamic attributes, we need to check the mapping to see if that mapping exists
+                // todo: one day we could generate new dynamic attributes on the fly if they're a literal,
+                // or something sufficiently serializable
+                //  (ie `checked`` being a bool and bools being interpretable)
+                //
+                // For now, just give up if that attribute doesn't exist in the mapping
+                None => {
+                    let id = self.update_dynamic_attribute(attr)?;
+                    TemplateAttribute::Dynamic { id }
+                }
+            };
+
+            static_attr_array.push(template_attr);
+        }
+
+        let children = self.populate_by_updating::<Ctx>(el.children.as_slice())?;
+
+        let (tag, namespace) =
+            Ctx::map_element(&rust_name).unwrap_or((intern(rust_name.as_str()), None));
+
+        Some(TemplateNode::Element {
+            tag,
+            namespace,
+            attrs: intern(static_attr_array.into_boxed_slice()),
+            children: intern(children.as_slice()),
+        })
+    }
+
+    fn update_dynamic_attribute(&mut self, attr: &'a AttributeType) -> Option<usize> {
+        let idx = match self.has_tracked_nodes() {
+            true => self.tracked_attribute_idx(attr)?,
+            false => self.dynamic_attributes.len(),
+        };
+
+        self.dynamic_attributes.push(vec![attr]);
+        if self.attr_paths.len() <= idx {
+            self.attr_paths.resize_with(idx + 1, Vec::new);
+        }
+
+        self.attr_paths[idx] = self.current_path.clone();
+
+        Some(idx)
+    }
+
+    fn make_static_attribute<Ctx: HotReloadingContext>(
+        value: &IfmtInput,
+        name: &ElementAttrName,
+        element_name_rust: &str,
+    ) -> TemplateAttribute {
+        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));
+
+        let static_attr = TemplateAttribute::Static {
+            name,
+            namespace,
+            value: intern(value.value().as_str()),
+        };
+
+        static_attr
+    }
+
+    /// Check if we're tracking any nodes
+    ///
+    /// If we're tracking, then we'll attempt to use the old mapping
+    fn has_tracked_nodes(&self) -> bool {
+        self.is_tracking
+    }
+
+    /// Track a BodyNode
+    ///
+    /// This will save the any dynamic nodes that we find.
+    /// We need to be careful around saving if/for/components since we want to hotreload their contents
+    /// provided that their rust portions haven't changed.
+    pub(crate) fn track_node(&mut self, node: BodyNode) {
+        match node {
+            // If the node is a static element, we just want to merge its attributes into the dynamic mapping
+            BodyNode::Element(el) => self.track_element(el),
+
+            // We skip static nodes since they'll get written out by the template during the diffing phase
+            BodyNode::Text(text) if text.is_static() => {}
+
+            BodyNode::RawExpr(_)
+            | BodyNode::Text(_)
+            | BodyNode::ForLoop(_)
+            | BodyNode::IfChain(_)
+            | BodyNode::Component(_) => {
+                self.track_dynamic_node(node);
+            }
+        }
+    }
+
+    fn track_element(&mut self, el: Element) {
+        for attr in el.merged_attributes {
+            // If the attribute is a static string literal, we don't need to insert it since the attribute
+            // will be written out during the diffing phase (since it's static)
+            if !attr.is_static_str_literal() {
+                self.track_attribute(attr);
+            }
+        }
+
+        for child in el.children {
+            self.track_node(child);
+        }
+    }
+
+    pub(crate) fn track_attribute(&mut self, attr: AttributeType) -> usize {
+        let idx = self.last_attribute_idx;
+        self.last_attribute_idx += 1;
+        self.attribute_to_idx.entry(attr).or_default().push(idx);
+        idx
+    }
+
+    pub(crate) fn track_dynamic_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
+    }
+
+    pub(crate) fn tracked_attribute_idx(&mut self, attr: &AttributeType) -> Option<usize> {
+        self.attribute_to_idx
+            .get_mut(attr)
+            .and_then(|idxs| idxs.pop())
+    }
+
+    pub(crate) fn tracked_node_idx(&mut self, node: &BodyNode) -> Option<usize> {
+        self.node_to_idx.get_mut(node).and_then(|idxs| idxs.pop())
+    }
+}

+ 8 - 10
packages/rsx/src/hot_reload/hot_reloading_file_map.rs

@@ -1,3 +1,7 @@
+use super::{
+    hot_reload_diff::{diff_rsx, DiffResult},
+    ChangedRsx,
+};
 use crate::{CallBody, HotReloadingContext};
 use dioxus_core::{
     prelude::{TemplateAttribute, TemplateNode},
@@ -9,17 +13,11 @@ pub use proc_macro2::TokenStream;
 pub use std::collections::HashMap;
 pub use std::sync::Mutex;
 pub use std::time::SystemTime;
-use std::{collections::HashSet, ffi::OsStr, path::PathBuf};
+use std::{collections::HashSet, ffi::OsStr, marker::PhantomData, path::PathBuf};
 pub use std::{fs, io, path::Path};
 pub use std::{fs::File, io::Read};
-pub use syn::__private::ToTokens;
 use syn::spanned::Spanned;
 
-use super::{
-    hot_reload_diff::{diff_rsx, DiffResult},
-    ChangedRsx,
-};
-
 pub enum UpdateResult {
     UpdatedRsx(Vec<Template>),
 
@@ -40,7 +38,7 @@ pub struct FileMap<Ctx: HotReloadingContext> {
 
     in_workspace: HashMap<PathBuf, Option<PathBuf>>,
 
-    phantom: std::marker::PhantomData<Ctx>,
+    phantom: PhantomData<Ctx>,
 }
 
 /// A cached file that has been parsed
@@ -75,7 +73,7 @@ impl<Ctx: HotReloadingContext> FileMap<Ctx> {
         let mut map = Self {
             map,
             in_workspace: HashMap::new(),
-            phantom: std::marker::PhantomData,
+            phantom: PhantomData,
         };
 
         map.load_assets(crate_dir.as_path());
@@ -303,7 +301,7 @@ impl<Ctx: HotReloadingContext> FileMap<Ctx> {
     }
 }
 
-fn template_location(old_start: proc_macro2::LineColumn, file: &Path) -> String {
+pub fn template_location(old_start: proc_macro2::LineColumn, file: &Path) -> String {
     let line = old_start.line;
     let column = old_start.column + 1;
     let location = file.display().to_string()

+ 90 - 667
packages/rsx/src/lib.rs

@@ -19,143 +19,111 @@ mod errors;
 mod attribute;
 mod component;
 mod element;
-#[cfg(feature = "hot_reload")]
-pub mod hot_reload;
 mod ifmt;
+mod location;
 mod node;
+mod util;
 
-use std::{fmt::Debug, hash::Hash};
+pub(crate) mod context;
+pub(crate) mod renderer;
+mod sub_templates;
 
 // Re-export the namespaces into each other
 pub use attribute::*;
 pub use component::*;
+pub use context::DynamicContext;
+pub use element::*;
+pub use ifmt::*;
+pub use node::*;
+
+#[cfg(feature = "hot_reload")]
+pub mod hot_reload;
+
 #[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 renderer::TemplateRenderer;
+use std::{fmt::Debug, hash::Hash};
 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
+/// The Callbody is the contents of the rsx! macro
+///
+/// It is a list of BodyNodes, which are the different parts of the template.
+/// The Callbody contains no information about how the template will be rendered, only information about the parsed tokens.
+///
+/// Every callbody should be valid, so you can use it to build a template.
+/// To generate the code used to render the template, use the ToTokens impl on the Callbody, or with the `render_with_location` method.
 #[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> {
-        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),
-        };
-
         // Empty templates just are placeholders for "none"
         if self.roots.is_empty() {
             return quote! { None };
         }
 
-        quote! {
-            Some({ #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 })
-    }
-}
-
-impl ToTokens for CallBody {
-    fn to_tokens(&self, out_tokens: &mut TokenStream2) {
-        let body: TemplateRenderer = TemplateRenderer {
-            roots: &self.roots,
-            location: None,
-        };
-
-        // Empty templates just are placeholders for "none"
-        if self.roots.is_empty() {
-            return out_tokens.append_all(quote! { None });
-        }
-
-        out_tokens.append_all(quote! {
-            Some({ #body })
-        })
-    }
-}
-
-pub struct TemplateRenderer<'a> {
-    pub roots: &'a [BodyNode],
-    pub location: Option<String>,
-}
-
-impl<'a> TemplateRenderer<'a> {
+        let body = TemplateRenderer::as_tokens(&self.roots, Some(location));
+
+        quote! { Some({ #body }) }
+    }
+
+    /// 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.
+    ///
+    /// The previous_location is the location of the previous template at the time the template was originally compiled.
+    /// It's up to you the implementor to trace the template location back to the original source code. Generally you
+    /// can simply just match the location from the syn::File type to the template map living in the renderer.
+    ///
+    /// When you implement hotreloading, you're likely just going to parse the source code into the Syn::File type, which
+    /// should make retrieving the template location easy.
+    ///
+    /// ## Note:
+    ///
+    ///  - 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.
+    ///
+    /// ## Longer note about sub templates:
+    ///
+    ///    Sub templates when expanded in rustc use the same file/lin/col information as the parent template. This can
+    ///    be annoying when you're trying to get a location for a sub template and it's pretending that it's its parent.
+    ///    The new implementation of this aggregates all subtemplates into the TemplateRenderer and then assigns them
+    ///    unique IDs based on the byte index of the template, working around this issue.
+    ///
+    /// ## TODO:
+    ///
+    ///    A longer term goal would be to provide some sort of diagnostics to the user as to why the template was not
+    ///    updated, giving them an option to revert to the previous template as to not require a full rebuild.
     #[cfg(feature = "hot_reload")]
-    fn update_template<Ctx: HotReloadingContext>(
-        &mut self,
-        previous_call: Option<CallBody>,
+    pub fn update_template<Ctx: HotReloadingContext>(
+        &self,
+        old: Option<CallBody>,
         location: &'static str,
     ) -> Option<Template> {
-        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();
-        }
-
+        // Create a context that will be used to update the template
+        let mut context = DynamicContext::new_with_old(old);
+
+        // Force the template node to generate us TemplateNodes, and fill in the location information
+        let roots = context.populate_by_updating::<Ctx>(&self.roots)?;
+
+        // We've received the dioxus-core TemplateNodess, and need to assemble them into a Template
+        // We could just use them directly, but we want to intern them to do our best to avoid
+        // egregious memory leaks. We're sitll leaking memory, but at least we can blame it on
+        // the `Intern` crate and not just the fact that we call Box::leak.
+        //
+        // We should also note that order of these nodes could be all scrambeled
         Some(Template {
             name: location,
             roots: intern(roots.as_slice()),
@@ -179,586 +147,41 @@ impl<'a> TemplateRenderer<'a> {
     }
 }
 
-impl<'a> ToTokens for TemplateRenderer<'a> {
-    fn to_tokens(&self, out_tokens: &mut TokenStream2) {
-        let mut context = DynamicContext::default();
-
-        let key = match self.roots.first() {
-            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( #tok.to_string() ) },
-            None => quote! { None },
-        };
-
-        let root_col = match self.roots.first() {
-            Some(first_root) => {
-                let first_root_span = format!("{:?}", first_root.span());
-                first_root_span
-                    .rsplit_once("..")
-                    .and_then(|(_, after)| after.split_once(')').map(|(before, _)| before))
-                    .unwrap_or_default()
-                    .to_string()
-            }
-            _ => "0".to_string(),
-        };
-        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 node_printer = &context.dynamic_nodes;
-        let dyn_attr_printer = context
-            .dynamic_attributes
-            .iter()
-            .map(|attrs| AttributeType::merge_quote(attrs));
-        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),* ],
-            };
-
-            {
-                // NOTE: Allocating a temporary is important to make reads within rsx drop before the value is returned
-                let __vnodes = dioxus_core::VNode::new(
-                    #key_tokens,
-                    TEMPLATE,
-                    Box::new([ #( #node_printer),* ]),
-                    Box::new([ #(#dyn_attr_printer),* ]),
-                );
-                __vnodes
-            }
-        });
-    }
-}
-
-#[cfg(feature = "hot_reload")]
-#[derive(Default, Debug)]
-struct DynamicMapping {
-    attribute_to_idx: std::collections::HashMap<AttributeType, 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: &AttributeType) -> 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: AttributeType) -> 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.merged_attributes {
-                    match &attr {
-                        AttributeType::Named(ElementAttrNamed {
-                            attr:
-                                ElementAttr {
-                                    value: ElementAttrValue::AttrLiteral(input),
-                                    ..
-                                },
-                            ..
-                        }) if input.is_static() => {}
-                        _ => {
-                            self.insert_attribute(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<Vec<&'a AttributeType>>,
-    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> {
-        match root {
-            BodyNode::Element(el) => {
-                let element_name_rust = el.name.to_string();
-
-                let mut static_attrs = Vec::new();
-                for attr in &el.merged_attributes {
-                    match &attr {
-                        AttributeType::Named(ElementAttrNamed {
-                            attr:
-                                ElementAttr {
-                                    value: ElementAttrValue::AttrLiteral(value),
-                                    name,
-                                },
-                            ..
-                        }) 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()),
-                            })
-                        }
-
-                        _ => {
-                            let idx = match mapping {
-                                Some(mapping) => mapping.get_attribute_idx(attr)?,
-                                None => self.dynamic_attributes.len(),
-                            };
-                            self.dynamic_attributes.push(vec![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();
-                }
+impl Parse for CallBody {
+    fn parse(input: ParseStream) -> Result<Self> {
+        let mut roots = Vec::new();
 
-                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()),
-                })
-            }
+        while !input.is_empty() {
+            let node = input.parse::<BodyNode>()?;
 
-            BodyNode::Text(text) if text.is_static() => {
-                let text = text.source.as_ref().unwrap();
-                Some(TemplateNode::Text {
-                    text: intern(text.value().as_str()),
-                })
+            if input.peek(Token![,]) {
+                let _ = input.parse::<Token![,]>();
             }
 
-            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 },
-                })
-            }
+            roots.push(node);
         }
-    }
-
-    /// Render a portion of an rsx callbody to a token stream
-    pub 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.merged_attributes.iter().map(|attr| match attr {
-                    AttributeType::Named(ElementAttrNamed {
-                        attr:
-                            ElementAttr {
-                                value: ElementAttrValue::AttrLiteral(value),
-                                name,
-                            },
-                        ..
-                    }) if value.is_static() => {
-                        let value = value.to_static().unwrap();
-                        let ns = {
-                            match name {
-                                ElementAttrName::BuiltIn(name) => ns(quote!(#name.1)),
-                                ElementAttrName::Custom(_) => quote!(None),
-                            }
-                        };
-                        let name = match (el_name, name) {
-                            (ElementName::Ident(_), ElementAttrName::BuiltIn(_)) => {
-                                quote! { #el_name::#name.0 }
-                            }
-                            _ => {
-                                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,
-                            },
-                        }
-                    }
-
-                    _ => {
-                        let ct = self.dynamic_attributes.len();
-                        self.dynamic_attributes.push(vec![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 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 } },
-                }
-            }
-        }
+        Ok(CallBody { roots })
     }
 }
 
-#[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,
+impl ToTokens for CallBody {
+    fn to_tokens(&self, out_tokens: &mut TokenStream2) {
+        // Empty templates just are placeholders for "none"
+        match self.roots.is_empty() {
+            true => out_tokens.append_all(quote! { None }),
+            false => {
+                let body = TemplateRenderer::as_tokens(&self.roots, None);
+                out_tokens.append_all(quote! { Some({ #body }) })
             }
         }
     }
-
-    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() {
-    #[allow(unused, non_snake_case)]
-    fn Comp() -> 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]]
-        },
-    )
+// interns a object into a static object, resusing the value if it already exists
+pub(crate) fn intern<T: Eq + Hash + Send + Sync + ?Sized + 'static>(
+    s: impl Into<Intern<T>>,
+) -> &'static T {
+    s.into().as_ref()
 }

+ 8 - 0
packages/rsx/src/location.rs

@@ -0,0 +1,8 @@
+/// Information about the location of the call to a component
+///
+/// This will be filled in when the dynamiccontext is built, filling in the file:line:column:id format
+///
+#[derive(PartialEq, Eq, Clone, Debug, Hash, Default)]
+pub struct CallerLocation {
+    inner: Option<String>,
+}

+ 100 - 70
packages/rsx/src/node.rs

@@ -1,3 +1,5 @@
+use self::location::CallerLocation;
+
 use super::*;
 
 use proc_macro2::{Span, TokenStream as TokenStream2};
@@ -21,11 +23,12 @@ Parse
 #[derive(PartialEq, Eq, Clone, Debug, Hash)]
 pub enum BodyNode {
     Element(Element),
+    Text(IfmtInput),
+    RawExpr(Expr),
+
     Component(Component),
     ForLoop(ForLoop),
     IfChain(IfChain),
-    Text(IfmtInput),
-    RawExpr(Expr),
 }
 
 impl BodyNode {
@@ -138,92 +141,45 @@ impl Parse for BodyNode {
 
 impl ToTokens for BodyNode {
     fn to_tokens(&self, tokens: &mut TokenStream2) {
-        match &self {
+        match self {
             BodyNode::Element(_) => {
                 unimplemented!("Elements are statically created in the template")
             }
-            BodyNode::Component(comp) => comp.to_tokens(tokens),
+
+            // Text is simple, just write it out
             BodyNode::Text(txt) => tokens.append_all(quote! {
                 dioxus_core::DynamicNode::Text(dioxus_core::VText::new(#txt.to_string()))
             }),
+
+            // Expressons too
             BodyNode::RawExpr(exp) => tokens.append_all(quote! {
                 {
                     let ___nodes = (#exp).into_dyn_node();
                     ___nodes
                 }
             }),
-            BodyNode::ForLoop(exp) => {
-                let ForLoop {
-                    pat, expr, body, ..
-                } = exp;
-
-                let renderer: TemplateRenderer = TemplateRenderer {
-                    roots: body,
-                    location: None,
-                };
-
-                // Signals expose an issue with temporary lifetimes
-                // We need to directly render out the nodes first to collapse their lifetime to <'a>
-                // And then we can return them into the dyn loop
-                tokens.append_all(quote! {
-                    {
-                        let ___nodes = (#expr).into_iter().map(|#pat| { #renderer }).into_dyn_node();
-                        ___nodes
-                    }
-                })
-            }
-            BodyNode::IfChain(chain) => {
-                let mut body = TokenStream2::new();
-                let mut terminated = false;
-
-                let mut elif = Some(chain);
-
-                while let Some(chain) = elif {
-                    let IfChain {
-                        if_token,
-                        cond,
-                        then_branch,
-                        else_if_branch,
-                        else_branch,
-                    } = chain;
-
-                    let mut renderer: TemplateRenderer = TemplateRenderer {
-                        roots: then_branch,
-                        location: None,
-                    };
-
-                    body.append_all(quote! { #if_token #cond { Some({#renderer}) } });
-
-                    if let Some(next) = else_if_branch {
-                        body.append_all(quote! { else });
-                        elif = Some(next);
-                    } else if let Some(else_branch) = else_branch {
-                        renderer.roots = else_branch;
-                        body.append_all(quote! { else { Some({#renderer}) } });
-                        terminated = true;
-                        break;
-                    } else {
-                        elif = None;
-                    }
-                }
 
-                if !terminated {
-                    body.append_all(quote! {
-                        else { None }
-                    });
-                }
+            // todo:
+            //
+            // Component children should also participate in hotreloading
+            // This is a *little* hard since components might not be able to take children in the
+            // first place. I'm sure there's a hacky way to allow this... but it's not quite as
+            // straightforward as a for loop.
+            //
+            // It might involve always generating a `children` field on the component and always
+            // populating it with an empty template. This might lose the typesafety of whether
+            // or not a component can even accept children - essentially allowing childrne in
+            // every component - so it'd be breaking - but it would/could work.
+            BodyNode::Component(comp) => tokens.append_all(quote! { #comp }),
 
-                tokens.append_all(quote! {
-                    {
-                        let ___nodes = (#body).into_dyn_node();
-                        ___nodes
-                    }
-                });
-            }
+            BodyNode::ForLoop(exp) => tokens.append_all(quote! { #exp }),
+
+            BodyNode::IfChain(chain) => tokens.append_all(quote! { #chain }),
         }
     }
 }
 
+#[non_exhaustive]
 #[derive(PartialEq, Eq, Clone, Debug, Hash)]
 pub struct ForLoop {
     pub for_token: Token![for],
@@ -232,6 +188,7 @@ pub struct ForLoop {
     pub expr: Box<Expr>,
     pub body: Vec<BodyNode>,
     pub brace_token: token::Brace,
+    pub location: CallerLocation,
 }
 
 impl Parse for ForLoop {
@@ -251,11 +208,33 @@ impl Parse for ForLoop {
             in_token,
             body,
             brace_token,
+            location: CallerLocation::default(),
             expr: Box::new(expr),
         })
     }
 }
 
+impl ToTokens for ForLoop {
+    fn to_tokens(&self, tokens: &mut TokenStream2) {
+        let ForLoop {
+            pat, expr, body, ..
+        } = self;
+
+        let renderer = TemplateRenderer::as_tokens(body, None);
+
+        // Signals expose an issue with temporary lifetimes
+        // We need to directly render out the nodes first to collapse their lifetime to <'a>
+        // And then we can return them into the dyn loop
+        tokens.append_all(quote! {
+            {
+                let ___nodes = (#expr).into_iter().map(|#pat| { #renderer }).into_dyn_node();
+                ___nodes
+            }
+        })
+    }
+}
+
+#[non_exhaustive]
 #[derive(PartialEq, Eq, Clone, Debug, Hash)]
 pub struct IfChain {
     pub if_token: Token![if],
@@ -263,6 +242,7 @@ pub struct IfChain {
     pub then_branch: Vec<BodyNode>,
     pub else_if_branch: Option<Box<IfChain>>,
     pub else_branch: Option<Vec<BodyNode>>,
+    pub location: CallerLocation,
 }
 
 impl Parse for IfChain {
@@ -294,6 +274,56 @@ impl Parse for IfChain {
             then_branch,
             else_if_branch,
             else_branch,
+            location: CallerLocation::default(),
+        })
+    }
+}
+
+impl ToTokens for IfChain {
+    fn to_tokens(&self, tokens: &mut TokenStream2) {
+        let mut body = TokenStream2::new();
+        let mut terminated = false;
+
+        let mut elif = Some(self);
+
+        while let Some(chain) = elif {
+            let IfChain {
+                if_token,
+                cond,
+                then_branch,
+                else_if_branch,
+                else_branch,
+                ..
+            } = chain;
+
+            let renderer = TemplateRenderer::as_tokens(then_branch, None);
+
+            body.append_all(quote! { #if_token #cond { Some({#renderer}) } });
+
+            if let Some(next) = else_if_branch {
+                body.append_all(quote! { else });
+                elif = Some(next);
+            } else if let Some(else_branch) = else_branch {
+                let renderer = TemplateRenderer::as_tokens(else_branch, None);
+                body.append_all(quote! { else { Some({#renderer}) } });
+                terminated = true;
+                break;
+            } else {
+                elif = None;
+            }
+        }
+
+        if !terminated {
+            body.append_all(quote! {
+                else { None }
+            });
+        }
+
+        tokens.append_all(quote! {
+            {
+                let ___nodes = (#body).into_dyn_node();
+                ___nodes
+            }
         })
     }
 }

+ 126 - 0
packages/rsx/src/renderer.rs

@@ -0,0 +1,126 @@
+use crate::*;
+use proc_macro2::TokenStream as TokenStream2;
+use quote::quote;
+
+pub struct TemplateRenderer<'a> {
+    pub roots: &'a [BodyNode],
+    location: Option<String>,
+}
+
+impl<'a> TemplateRenderer<'a> {
+    /// Render the contents of the callbody out with a specific location
+    ///
+    /// This will cascade location information down the tree if it already hasn't been set
+    pub fn as_tokens(roots: &'a [BodyNode], location: Option<String>) -> TokenStream2 {
+        TemplateRenderer::render(Self { roots, location })
+    }
+
+    fn render(mut self) -> TokenStream2 {
+        // Create a new dynamic context that tracks the state of all the dynamic nodes
+        // We have no old template, to seed it with, so it'll just be used for rendering
+        let mut context = DynamicContext::default();
+
+        // If we have an implicit key, then we need to write its tokens
+        let key_tokens = match self.implicit_key() {
+            Some(tok) => quote! { Some( #tok.to_string() ) },
+            None => quote! { None },
+        };
+
+        // Get the tokens we'll use as the ID of the template
+        // This follows the file:line:column:id format
+        let name = self.get_template_id_tokens();
+
+        // Render the static nodes, generating the mapping of dynamic
+        // This will modify the bodynodes in place - sorry about that
+        let roots = self.render_body_nodes(&mut context);
+
+        let dynamic_nodes = &context.dynamic_nodes;
+        let dyn_attr_printer = context
+            .dynamic_attributes
+            .iter()
+            .map(|attrs| AttributeType::merge_quote(attrs));
+
+        let node_paths = context.node_paths.iter().map(|it| quote!(&[#(#it),*]));
+        let attr_paths = context.attr_paths.iter().map(|it| quote!(&[#(#it),*]));
+
+        quote! {
+            static TEMPLATE: dioxus_core::Template = dioxus_core::Template {
+                name: #name,
+                roots: #roots,
+                node_paths: &[ #(#node_paths),* ],
+                attr_paths: &[ #(#attr_paths),* ],
+            };
+
+            {
+                // NOTE: Allocating a temporary is important to make reads within rsx drop before the value is returned
+                let __vnodes = dioxus_core::VNode::new(
+                    #key_tokens,
+                    TEMPLATE,
+                    Box::new([ #( #dynamic_nodes),* ]),
+                    Box::new([ #(#dyn_attr_printer),* ]),
+                );
+                __vnodes
+            }
+        }
+    }
+
+    fn get_template_id_tokens(&self) -> TokenStream2 {
+        match self.location {
+            Some(ref loc) => quote! { #loc },
+            None => {
+                // Get the root:column:id tag we'll use as the ID of the template
+                let root_col = self.get_root_col_id();
+
+                quote! {
+                    concat!(
+                        file!(),
+                        ":",
+                        line!(),
+                        ":",
+                        column!(),
+                        ":",
+                        #root_col
+                    )
+                }
+            }
+        }
+    }
+
+    fn get_root_col_id(&self) -> String {
+        let root_col = match self.roots.first() {
+            Some(first_root) => {
+                let first_root_span = format!("{:?}", first_root.span());
+                first_root_span
+                    .rsplit_once("..")
+                    .and_then(|(_, after)| after.split_once(')').map(|(before, _)| before))
+                    .unwrap_or_default()
+                    .to_string()
+            }
+            _ => "0".to_string(),
+        };
+        root_col
+    }
+
+    fn implicit_key(&self) -> Option<IfmtInput> {
+        let key = match self.roots.first() {
+            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,
+        };
+        key
+    }
+
+    /// Render a list of BodyNodes as a static array (&[...])
+    pub fn render_body_nodes(&mut self, context: &mut DynamicContext<'a>) -> TokenStream2 {
+        let root_printer = self
+            .roots
+            .iter()
+            .enumerate()
+            .map(|(idx, root)| context.render_children_nodes(idx, root));
+
+        // Render the static nodes, generating the mapping of dynamic
+        quote! {
+            &[ #( #root_printer ),* ]
+        }
+    }
+}

+ 45 - 0
packages/rsx/src/sub_templates.rs

@@ -0,0 +1,45 @@
+//! Logic for handlng sub-templates
+//!
+//! These are things like for/if and component calls that are expanded by the contaning macro but turn into templates
+//! separate template calls in the final output.
+//!
+//! Instead of recursively expanding sub-templates in place - and thus losing location information - we collect them
+//! into a list and then expand them at the end.
+//!
+//! The goal here is to provide a stable index for each sub-template in a way that we can find the for/if statements later.
+//! This requires some sort of hashing + indexing of the sub-templates.
+//!
+//! When expanding the for/if statements, we still want to expand them in the correct order, but we defer the actual token
+//! expansion until the end. This way the `TEMPLATE` static that subtemplate references is available *before* we convert
+//! an `if` statement into an actual vnode.
+//!
+//! Expanding a template will generate two token streams - one for the vnode creation and one for list of templates.
+//!
+//! While semantically it might make sense for a macro call that sees its dynamic contents to own those static nodes,
+//! core is currently designed around the assumption that the list of Dynamic attributes matches 1:1 the template it
+//! belongs to. This means that whatever dynamic nodes are rendered need to match the template they are rendered in,
+//! hence why we need to spit out true subtemplates rather than just building a "super template".
+//!
+//! I would prefer we used the "super template" strategy since it's more flexible when diffing - no need to keep track
+//! of nested templates, and we can just diff the super template and the sub templates as a whole.
+//!
+//! However core would need to be redesigned to support this strategy, and I'm not sure if it's worth the effort.
+//! It's just conceptually easier to have the sub-templates be a part of the template they are rendered in, and then
+//! deal with the complicated rsx diffing logic in the rsx crate itself.
+//!
+//! /// The subtemplates that we've discovered that need to be updated
+//! /// These will be collected just by recursing the body nodes of various items like for/if/components etc
+//! ///
+//! /// The locations of these templates will be the same location as the original template, with
+//! /// the addition of the dynamic node index that the template belongs to.
+//! ///
+//! /// rsx! {                        <-------- this is the location of the template "file.rs:123:0:0"
+//! ///     div {
+//! ///         for i in 0..10 {      <-------- dyn_node(0)
+//! ///             "hi"              <-------- template with location  "file.rs:123:0:1" (original name, but with the dynamic node index)
+//! ///         }
+//! ///     }
+//! /// }
+//! ///
+//! /// We only track this when the template changes
+//! pub discovered_templates: Vec<Template>,

+ 1 - 0
packages/rsx/src/util.rs

@@ -0,0 +1 @@
+

+ 90 - 0
packages/rsx/tests/hotreload_pattern.rs

@@ -0,0 +1,90 @@
+#![allow(unused)]
+
+use dioxus_rsx::{
+    hot_reload::{diff_rsx, template_location, ChangedRsx, DiffResult},
+    CallBody, HotReloadingContext,
+};
+use quote::quote;
+use syn::{parse::Parse, spanned::Spanned, File};
+
+#[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,
+        }
+    }
+}
+#[test]
+fn testing_for_pattern() {
+    let old = quote! {
+        div {
+            for item in vec![1, 2, 3] {
+                div { "123" }
+                div { "asasddasdasd" }
+            }
+        }
+    };
+
+    let new = quote! {
+        div {
+            for item in vec![1, 2, 3] {
+                div { "asasddasdasd" }
+            }
+        }
+    };
+
+    let old: CallBody = syn::parse2(old).unwrap();
+    let new: CallBody = syn::parse2(new).unwrap();
+
+    let updated = old.update_template::<Mock>(Some(new), "testing");
+
+    // currently, modifying a for loop is not hot reloadable
+    // We want to change this...
+    assert!(updated.is_none());
+
+    // let updated = old.update_template::<Mock>(Some(new), "testing").unwrap();
+
+    // let old = include_str!(concat!("./valid/for_.old.rsx"));
+    // let new = include_str!(concat!("./valid/for_.new.rsx"));
+    // let (old, new) = load_files(old, new);
+
+    // let DiffResult::RsxChanged { rsx_calls } = diff_rsx(&new, &old) else {
+    //     panic!("Expected a rsx call to be changed")
+    // };
+
+    // for calls in rsx_calls {
+    //     let ChangedRsx { old, new } = calls;
+
+    //     let old_start = old.span().start();
+
+    //     let old_call_body = syn::parse2::<CallBody>(old.tokens).unwrap();
+    //     let new_call_body = syn::parse2::<CallBody>(new).unwrap();
+
+    //     let leaked_location = Box::leak(template_location(old_start, file).into_boxed_str());
+
+    //     let hotreloadable_template =
+    //         new_call_body.update_template::<Ctx>(Some(old_call_body), leaked_location);
+
+    //     dbg!(hotreloadable_template);
+    // }
+
+    // dbg!(rsx_calls);
+}

+ 39 - 43
packages/rsx/tests/hotreloads.rs

@@ -1,53 +1,49 @@
 use dioxus_rsx::hot_reload::{diff_rsx, DiffResult};
 use syn::File;
 
+macro_rules! assert_rsx_changed {
+    (
+        $( #[doc = $doc:expr] )*
+        $name:ident
+    ) => {
+        $( #[doc = $doc] )*
+        #[test]
+        fn $name() {
+            let old = include_str!(concat!("./valid/", stringify!($name), ".old.rsx"));
+            let new = include_str!(concat!("./valid/", stringify!($name), ".new.rsx"));
+            let (old, new) = load_files(old, new);
+            assert!(matches!( diff_rsx(&new, &old), DiffResult::RsxChanged { .. }));
+        }
+    };
+}
+
+macro_rules! assert_code_changed {
+    (
+        $( #[doc = $doc:expr] )*
+        $name:ident
+    ) => {
+        $( #[doc = $doc] )*
+        #[test]
+        fn $name() {
+            let old = include_str!(concat!("./invalid/", stringify!($name), ".old.rsx"));
+            let new = include_str!(concat!("./invalid/", stringify!($name), ".new.rsx"));
+            let (old, new) = load_files(old, new);
+            assert!(matches!(diff_rsx(&new, &old), DiffResult::CodeChanged(_) ));
+        }
+    };
+}
+
 fn load_files(old: &str, new: &str) -> (File, File) {
     let old = syn::parse_file(old).unwrap();
     let new = syn::parse_file(new).unwrap();
     (old, new)
 }
 
-#[test]
-fn hotreloads() {
-    let (old, new) = load_files(
-        include_str!("./valid/expr.old.rsx"),
-        include_str!("./valid/expr.new.rsx"),
-    );
-
-    assert!(matches!(
-        diff_rsx(&new, &old),
-        DiffResult::RsxChanged { .. }
-    ));
-
-    let (old, new) = load_files(
-        include_str!("./valid/let.old.rsx"),
-        include_str!("./valid/let.new.rsx"),
-    );
-
-    assert!(matches!(
-        diff_rsx(&new, &old),
-        DiffResult::RsxChanged { .. }
-    ));
-
-    let (old, new) = load_files(
-        include_str!("./valid/combo.old.rsx"),
-        include_str!("./valid/combo.new.rsx"),
-    );
-
-    assert!(matches!(
-        diff_rsx(&new, &old),
-        DiffResult::RsxChanged { .. }
-    ));
-}
-
-#[test]
-fn doesnt_hotreload() {
-    let (old, new) = load_files(
-        include_str!("./invalid/changedexpr.old.rsx"),
-        include_str!("./invalid/changedexpr.new.rsx"),
-    );
+assert_rsx_changed![combo];
+assert_rsx_changed![expr];
+assert_rsx_changed![for_];
+assert_rsx_changed![if_];
+assert_rsx_changed![let_];
+assert_rsx_changed![nested];
 
-    let res = diff_rsx(&new, &old);
-    dbg!(&res);
-    assert!(matches!(res, DiffResult::CodeChanged(_)));
-}
+assert_code_changed![changedexpr];

+ 112 - 0
packages/rsx/tests/rsx_diff.rs

@@ -0,0 +1,112 @@
+use dioxus_rsx::{CallBody, HotReloadingContext};
+use quote::quote;
+
+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,
+        }
+    }
+}
+
+#[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}"})}
+        }
+    };
+
+    let call_body: CallBody = syn::parse2(input).unwrap();
+    let new_template = call_body.update_template::<Mock>(None, "testing").unwrap();
+    insta::assert_debug_snapshot!(new_template);
+}
+
+#[test]
+fn diff_template() {
+    #[allow(unused, non_snake_case)]
+    fn Comp() -> 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 {}
+        }
+    };
+
+    let call_body1: CallBody = syn::parse2(input).unwrap();
+    let created_template = call_body1.update_template::<Mock>(None, "testing").unwrap();
+    insta::assert_debug_snapshot!(created_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 new_template = call_body2
+        .update_template::<Mock>(Some(call_body1), "testing")
+        .unwrap();
+
+    insta::assert_debug_snapshot!(new_template);
+}
+
+#[test]
+fn changing_forloops_is_okay() {
+    let input = quote! {
+        div {
+            for i in 0..10 {
+                div { "123" }
+                "asdasd"
+            }
+        }
+    };
+
+    let call_body: CallBody = syn::parse2(input).unwrap();
+    let new_template = call_body.update_template::<Mock>(None, "testing").unwrap();
+
+    dbg!(new_template);
+}

+ 64 - 0
packages/rsx/tests/snapshots/rsx_diff__create_template.snap

@@ -0,0 +1,64 @@
+---
+source: packages/rsx/tests/rsx_diff.rs
+expression: template
+---
+Template {
+    name: "testing",
+    roots: [
+        Element {
+            tag: "svg",
+            namespace: Some(
+                "svg",
+            ),
+            attrs: [
+                Dynamic {
+                    id: 0,
+                },
+                Static {
+                    name: "height",
+                    value: "100px",
+                    namespace: Some(
+                        "style",
+                    ),
+                },
+                Dynamic {
+                    id: 1,
+                },
+                Static {
+                    name: "height2",
+                    value: "100px",
+                    namespace: None,
+                },
+            ],
+            children: [
+                Element {
+                    tag: "p",
+                    namespace: None,
+                    attrs: [],
+                    children: [
+                        Text {
+                            text: "hello world",
+                        },
+                    ],
+                },
+                Dynamic {
+                    id: 0,
+                },
+            ],
+        },
+    ],
+    node_paths: [
+        [
+            0,
+            1,
+        ],
+    ],
+    attr_paths: [
+        [
+            0,
+        ],
+        [
+            0,
+        ],
+    ],
+}

+ 81 - 0
packages/rsx/tests/snapshots/rsx_diff__diff_template-2.snap

@@ -0,0 +1,81 @@
+---
+source: packages/rsx/tests/rsx_diff.rs
+expression: new_template
+---
+Template {
+    name: "testing",
+    roots: [
+        Element {
+            tag: "div",
+            namespace: None,
+            attrs: [
+                Dynamic {
+                    id: 1,
+                },
+                Static {
+                    name: "height",
+                    value: "100px",
+                    namespace: None,
+                },
+                Static {
+                    name: "height2",
+                    value: "100px",
+                    namespace: None,
+                },
+                Dynamic {
+                    id: 0,
+                },
+            ],
+            children: [
+                Dynamic {
+                    id: 3,
+                },
+                Dynamic {
+                    id: 2,
+                },
+                Dynamic {
+                    id: 1,
+                },
+                Dynamic {
+                    id: 0,
+                },
+                Element {
+                    tag: "p",
+                    namespace: None,
+                    attrs: [],
+                    children: [
+                        Text {
+                            text: "hello world",
+                        },
+                    ],
+                },
+            ],
+        },
+    ],
+    node_paths: [
+        [
+            0,
+            3,
+        ],
+        [
+            0,
+            2,
+        ],
+        [
+            0,
+            1,
+        ],
+        [
+            0,
+            0,
+        ],
+    ],
+    attr_paths: [
+        [
+            0,
+        ],
+        [
+            0,
+        ],
+    ],
+}

+ 85 - 0
packages/rsx/tests/snapshots/rsx_diff__diff_template.snap

@@ -0,0 +1,85 @@
+---
+source: packages/rsx/tests/rsx_diff.rs
+expression: created_template
+---
+Template {
+    name: "testing",
+    roots: [
+        Element {
+            tag: "svg",
+            namespace: Some(
+                "svg",
+            ),
+            attrs: [
+                Dynamic {
+                    id: 0,
+                },
+                Static {
+                    name: "height",
+                    value: "100px",
+                    namespace: Some(
+                        "style",
+                    ),
+                },
+                Dynamic {
+                    id: 1,
+                },
+                Static {
+                    name: "height2",
+                    value: "100px",
+                    namespace: None,
+                },
+            ],
+            children: [
+                Element {
+                    tag: "p",
+                    namespace: None,
+                    attrs: [],
+                    children: [
+                        Text {
+                            text: "hello world",
+                        },
+                    ],
+                },
+                Dynamic {
+                    id: 0,
+                },
+                Dynamic {
+                    id: 1,
+                },
+                Dynamic {
+                    id: 2,
+                },
+                Dynamic {
+                    id: 3,
+                },
+            ],
+        },
+    ],
+    node_paths: [
+        [
+            0,
+            1,
+        ],
+        [
+            0,
+            2,
+        ],
+        [
+            0,
+            3,
+        ],
+        [
+            0,
+            4,
+        ],
+    ],
+    attr_paths: [
+        [
+            0,
+        ],
+        [
+            0,
+        ],
+    ],
+}

+ 9 - 0
packages/rsx/tests/valid/for_.new.rsx

@@ -0,0 +1,9 @@
+use dioxus::prelude::*;
+
+pub fn CoolChild() -> Element {
+    rsx! {
+        for items in vec![1, 2, 3] {
+            div { "asasddasdasd" }
+        }
+    }
+}

+ 10 - 0
packages/rsx/tests/valid/for_.old.rsx

@@ -0,0 +1,10 @@
+use dioxus::prelude::*;
+
+pub fn CoolChild() -> Element {
+    rsx! {
+        for items in vec![1, 2, 3] {
+            div { "123" }
+            div { "asasddasdasd" }
+        }
+    }
+}

+ 10 - 0
packages/rsx/tests/valid/if_.new.rsx

@@ -0,0 +1,10 @@
+use dioxus::prelude::*;
+
+pub fn CoolChild() -> Element {
+    rsx! {
+        if cond() {
+            div { "123" }
+            div { "asasddasdasd" }
+        }
+    }
+}

+ 9 - 0
packages/rsx/tests/valid/if_.old.rsx

@@ -0,0 +1,9 @@
+use dioxus::prelude::*;
+
+pub fn CoolChild() -> Element {
+    rsx! {
+        if cond() {
+            div { "asasddasdasd" }
+        }
+    }
+}

+ 0 - 0
packages/rsx/tests/valid/let.new.rsx → packages/rsx/tests/valid/let_.new.rsx


+ 0 - 0
packages/rsx/tests/valid/let.old.rsx → packages/rsx/tests/valid/let_.old.rsx


+ 10 - 0
packages/rsx/tests/valid/nested.new.rsx

@@ -0,0 +1,10 @@
+use dioxus::prelude::*;
+
+pub fn CoolChild() -> Element {
+    rsx! {
+        ForLoop {
+            div { "123" }
+            div { "asasddasdasd" }
+        }
+    }
+}

+ 9 - 0
packages/rsx/tests/valid/nested.old.rsx

@@ -0,0 +1,9 @@
+use dioxus::prelude::*;
+
+pub fn CoolChild() -> Element {
+    rsx! {
+        ForLoop {
+            div { "asasddasdasd" }
+        }
+    }
+}