dioxus_rsx/
template_body.rs

1//! I'm so sorry this is so complicated. Here's my best to simplify and explain it:
2//!
3//! The `Callbody` is the contents of the rsx! macro - this contains all the information about every
4//! node that rsx! directly knows about. For loops, if statements, etc.
5//!
6//! However, there are multiple *templates* inside a callbody - due to how core clones templates and
7//! just generally rationalize the concept of a template, nested bodies like for loops and if statements
8//! and component children are all templates, contained within the same Callbody.
9//!
10//! This gets confusing fast since there's lots of IDs bouncing around.
11//!
12//! The IDs at play:
13//! - The id of the template itself so we can find it and apply it to the dom.
14//!   This is challenging since all calls to file/line/col/id are relative to the macro invocation,
15//!   so they will have to share the same base ID and we need to give each template a new ID.
16//!   The id of the template will be something like file!():line!():col!():ID where ID increases for
17//!   each nested template.
18//!
19//! - The IDs of dynamic nodes relative to the template they live in. This is somewhat easy to track
20//!   but needs to happen on a per-template basis.
21//!
22//! - The IDs of formatted strings in debug mode only. Any formatted segments like "{x:?}" get pulled out
23//!   into a pool so we can move them around during hot reloading on a per-template basis.
24//!
25//! - The IDs of component property literals in debug mode only. Any component property literals like
26//!   1234 get pulled into the pool so we can hot reload them with the context of the literal pool.
27//!
28//! We solve this by parsing the structure completely and then doing a second pass that fills in IDs
29//! by walking the structure.
30//!
31//! This means you can't query the ID of any node "in a vacuum" - these are assigned once - but at
32//! least they're stable enough for the purposes of hotreloading
33//!
34//! ```rust, ignore
35//! rsx! {
36//!     div {
37//!         class: "hello",
38//!         id: "node-{node_id}",    <--- {node_id} has the formatted segment id 0 in the literal pool
39//!         ..props,                 <--- spreads are not reloadable
40//!
41//!         "Hello, world!           <--- not tracked but reloadable in the template since it's just a string
42//!
43//!         for item in 0..10 {      <--- both 0 and 10 are technically reloadable, but we don't hot reload them today...
44//!             div { "cool-{item}" }     <--- {item} has the formatted segment id 1 in the literal pool
45//!         }
46//!
47//!         Link {
48//!             to: "/home", <-- hotreloadable since its a component prop literal (with component literal id 0)
49//!             class: "link {is_ready}", <-- {is_ready} has the formatted segment id 2 in the literal pool and the property has the component literal id 1
50//!             "Home" <-- hotreloadable since its a component child (via template)
51//!         }
52//!     }
53//! }
54//! ```
55
56use self::location::DynIdx;
57use crate::innerlude::Attribute;
58use crate::*;
59use proc_macro2::TokenStream as TokenStream2;
60use proc_macro2_diagnostics::SpanDiagnosticExt;
61use syn::parse_quote;
62
63type NodePath = Vec<u8>;
64type AttributePath = Vec<u8>;
65
66/// A set of nodes in a template position
67///
68/// this could be:
69/// - The root of a callbody
70/// - The children of a component
71/// - The children of a for loop
72/// - The children of an if chain
73///
74/// The TemplateBody when needs to be parsed into a surrounding `Body` to be correctly re-indexed
75/// By default every body has a `0` default index
76#[derive(PartialEq, Eq, Clone, Debug)]
77pub struct TemplateBody {
78    pub roots: Vec<BodyNode>,
79    pub template_idx: DynIdx,
80    pub node_paths: Vec<NodePath>,
81    pub attr_paths: Vec<(AttributePath, usize)>,
82    pub dynamic_text_segments: Vec<FormattedSegment>,
83    pub diagnostics: Diagnostics,
84}
85
86impl Parse for TemplateBody {
87    /// Parse the nodes of the callbody as `Body`.
88    fn parse(input: ParseStream) -> Result<Self> {
89        let children = RsxBlock::parse_children(input)?;
90        let mut myself = Self::new(children.children);
91        myself
92            .diagnostics
93            .extend(children.diagnostics.into_diagnostics());
94
95        Ok(myself)
96    }
97}
98
99/// Our ToTokens impl here just defers to rendering a template out like any other `Body`.
100/// This is because the parsing phase filled in all the additional metadata we need
101impl ToTokens for TemplateBody {
102    fn to_tokens(&self, tokens: &mut TokenStream2) {
103        // First normalize the template body for rendering
104        let node = self.normalized();
105
106        // If we have an implicit key, then we need to write its tokens
107        let key_tokens = match node.implicit_key() {
108            Some(tok) => quote! { Some( #tok.to_string() ) },
109            None => quote! { None },
110        };
111
112        let roots = node.quote_roots();
113
114        // Print paths is easy - just print the paths
115        let node_paths = node.node_paths.iter().map(|it| quote!(&[#(#it),*]));
116        let attr_paths = node.attr_paths.iter().map(|(it, _)| quote!(&[#(#it),*]));
117
118        // For printing dynamic nodes, we rely on the ToTokens impl
119        // Elements have a weird ToTokens - they actually are the entrypoint for Template creation
120        let dynamic_nodes: Vec<_> = node.dynamic_nodes().collect();
121        let dynamic_nodes_len = dynamic_nodes.len();
122
123        // We could add a ToTokens for Attribute but since we use that for both components and elements
124        // They actually need to be different, so we just localize that here
125        let dyn_attr_printer: Vec<_> = node
126            .dynamic_attributes()
127            .map(|attr| attr.rendered_as_dynamic_attr())
128            .collect();
129        let dynamic_attr_len = dyn_attr_printer.len();
130
131        let dynamic_text = node.dynamic_text_segments.iter();
132
133        let diagnostics = &node.diagnostics;
134        let index = node.template_idx.get();
135        let hot_reload_mapping = node.hot_reload_mapping();
136
137        tokens.append_all(quote! {
138            dioxus_core::Element::Ok({
139                #diagnostics
140
141                // Components pull in the dynamic literal pool and template in debug mode, so they need to be defined before dynamic nodes
142                #[cfg(debug_assertions)]
143                fn __original_template() -> &'static dioxus_core::internal::HotReloadedTemplate {
144                    static __ORIGINAL_TEMPLATE: ::std::sync::OnceLock<dioxus_core::internal::HotReloadedTemplate> = ::std::sync::OnceLock::new();
145                    if __ORIGINAL_TEMPLATE.get().is_none() {
146                        _ = __ORIGINAL_TEMPLATE.set(#hot_reload_mapping);
147                    }
148                    __ORIGINAL_TEMPLATE.get().unwrap()
149                }
150                #[cfg(debug_assertions)]
151                let __template_read = {
152                    static __NORMALIZED_FILE: &'static str = {
153                        const PATH: &str = dioxus_core::const_format::str_replace!(file!(), "\\\\", "/");
154                        dioxus_core::const_format::str_replace!(PATH, '\\', "/")
155                    };
156
157                    // The key is important here - we're creating a new GlobalSignal each call to this
158                    // But the key is what's keeping it stable
159                    static __TEMPLATE: GlobalSignal<Option<dioxus_core::internal::HotReloadedTemplate>> = GlobalSignal::with_location(
160                        || None::<dioxus_core::internal::HotReloadedTemplate>,
161                        __NORMALIZED_FILE,
162                        line!(),
163                        column!(),
164                        #index
165                    );
166
167                    dioxus_core::Runtime::current().ok().map(|_| __TEMPLATE.read())
168                };
169                // If the template has not been hot reloaded, we always use the original template
170                // Templates nested within macros may be merged because they have the same file-line-column-index
171                // They cannot be hot reloaded, so this prevents incorrect rendering
172                #[cfg(debug_assertions)]
173                let __template_read = match __template_read.as_ref().map(|__template_read| __template_read.as_ref()) {
174                    Some(Some(__template_read)) => &__template_read,
175                    _ => __original_template(),
176                };
177                #[cfg(debug_assertions)]
178                let mut __dynamic_literal_pool = dioxus_core::internal::DynamicLiteralPool::new(
179                    vec![ #( #dynamic_text.to_string() ),* ],
180                );
181
182                // These items are used in both the debug and release expansions of rsx. Pulling them out makes the expansion
183                // slightly smaller and easier to understand. Rust analyzer also doesn't autocomplete well when it sees an ident show up twice in the expansion
184                let __dynamic_nodes: [dioxus_core::DynamicNode; #dynamic_nodes_len] = [ #( #dynamic_nodes ),* ];
185                let __dynamic_attributes: [Box<[dioxus_core::Attribute]>; #dynamic_attr_len] = [ #( #dyn_attr_printer ),* ];
186                #[doc(hidden)]
187                static __TEMPLATE_ROOTS: &[dioxus_core::TemplateNode] = &[ #( #roots ),* ];
188
189                #[cfg(debug_assertions)]
190                {
191                    let mut __dynamic_value_pool = dioxus_core::internal::DynamicValuePool::new(
192                        Vec::from(__dynamic_nodes),
193                        Vec::from(__dynamic_attributes),
194                        __dynamic_literal_pool
195                    );
196                    __dynamic_value_pool.render_with(__template_read)
197                }
198                #[cfg(not(debug_assertions))]
199                {
200                    #[doc(hidden)] // vscode please stop showing these in symbol search
201                    static ___TEMPLATE: dioxus_core::Template = dioxus_core::Template {
202                        roots: __TEMPLATE_ROOTS,
203                        node_paths: &[ #( #node_paths ),* ],
204                        attr_paths: &[ #( #attr_paths ),* ],
205                    };
206
207                    // NOTE: Allocating a temporary is important to make reads within rsx drop before the value is returned
208                    #[allow(clippy::let_and_return)]
209                    let __vnodes = dioxus_core::VNode::new(
210                        #key_tokens,
211                        ___TEMPLATE,
212                        Box::new(__dynamic_nodes),
213                        Box::new(__dynamic_attributes),
214                    );
215                    __vnodes
216                }
217            })
218        });
219    }
220}
221
222impl TemplateBody {
223    /// Create a new TemplateBody from a set of nodes
224    ///
225    /// This will fill in all the necessary path information for the nodes in the template and will
226    /// overwrite data like dynamic indexes.
227    pub fn new(nodes: Vec<BodyNode>) -> Self {
228        let mut body = Self {
229            roots: vec![],
230            template_idx: DynIdx::default(),
231            node_paths: Vec::new(),
232            attr_paths: Vec::new(),
233            dynamic_text_segments: Vec::new(),
234            diagnostics: Diagnostics::new(),
235        };
236
237        // Assign paths to all nodes in the template
238        body.assign_paths_inner(&nodes);
239        body.validate_key();
240
241        // And then save the roots
242        body.roots = nodes;
243
244        body
245    }
246
247    /// Normalize the Template body for rendering. If the body is completely empty, insert a placeholder node
248    pub fn normalized(&self) -> Self {
249        // If the nodes are completely empty, insert a placeholder node
250        // Core expects at least one node in the template to make it easier to replace
251        if self.is_empty() {
252            // Create an empty template body with a placeholder and diagnostics + the template index from the original
253            let empty = Self::new(vec![BodyNode::RawExpr(parse_quote! {()})]);
254            let default = Self {
255                diagnostics: self.diagnostics.clone(),
256                template_idx: self.template_idx.clone(),
257                ..empty
258            };
259            return default;
260        }
261        self.clone()
262    }
263
264    pub fn is_empty(&self) -> bool {
265        self.roots.is_empty()
266    }
267
268    pub fn implicit_key(&self) -> Option<&AttributeValue> {
269        match self.roots.first() {
270            Some(BodyNode::Element(el)) => el.key(),
271            Some(BodyNode::Component(comp)) => comp.get_key(),
272            _ => None,
273        }
274    }
275
276    /// Ensure only one key and that the key is not a static str
277    ///
278    /// todo: we want to allow arbitrary exprs for keys provided they impl hash / eq
279    fn validate_key(&mut self) {
280        let key = self.implicit_key();
281
282        if let Some(attr) = key {
283            let diagnostic = match &attr {
284                AttributeValue::AttrLiteral(ifmt) => {
285                    if ifmt.is_static() {
286                        ifmt.span().error("Key must not be a static string. Make sure to use a formatted string like `key: \"{value}\"")
287                    } else {
288                        return;
289                    }
290                }
291                _ => attr
292                    .span()
293                    .error("Key must be in the form of a formatted string like `key: \"{value}\""),
294            };
295
296            self.diagnostics.push(diagnostic);
297        }
298    }
299
300    pub fn get_dyn_node(&self, path: &[u8]) -> &BodyNode {
301        let mut node = self.roots.get(path[0] as usize).unwrap();
302        for idx in path.iter().skip(1) {
303            node = node.element_children().get(*idx as usize).unwrap();
304        }
305        node
306    }
307
308    pub fn get_dyn_attr(&self, path: &AttributePath, idx: usize) -> &Attribute {
309        match self.get_dyn_node(path) {
310            BodyNode::Element(el) => &el.merged_attributes[idx],
311            _ => unreachable!(),
312        }
313    }
314
315    pub fn dynamic_attributes(&self) -> impl DoubleEndedIterator<Item = &Attribute> {
316        self.attr_paths
317            .iter()
318            .map(|(path, idx)| self.get_dyn_attr(path, *idx))
319    }
320
321    pub fn dynamic_nodes(&self) -> impl DoubleEndedIterator<Item = &BodyNode> {
322        self.node_paths.iter().map(|path| self.get_dyn_node(path))
323    }
324
325    fn quote_roots(&self) -> impl Iterator<Item = TokenStream2> + '_ {
326        self.roots.iter().map(|node| match node {
327            BodyNode::Element(el) => quote! { #el },
328            BodyNode::Text(text) if text.is_static() => {
329                let text = text.input.to_static().unwrap();
330                quote! { dioxus_core::TemplateNode::Text { text: #text } }
331            }
332            _ => {
333                let id = node.get_dyn_idx();
334                quote! { dioxus_core::TemplateNode::Dynamic { id: #id } }
335            }
336        })
337    }
338
339    /// Iterate through the literal component properties of this rsx call in depth-first order
340    pub fn literal_component_properties(&self) -> impl Iterator<Item = &HotLiteral> + '_ {
341        self.dynamic_nodes()
342            .filter_map(|node| {
343                if let BodyNode::Component(component) = node {
344                    Some(component)
345                } else {
346                    None
347                }
348            })
349            .flat_map(|component| {
350                component.component_props().filter_map(|field| {
351                    if let AttributeValue::AttrLiteral(literal) = &field.value {
352                        Some(literal)
353                    } else {
354                        None
355                    }
356                })
357            })
358    }
359
360    fn hot_reload_mapping(&self) -> TokenStream2 {
361        let key = if let Some(AttributeValue::AttrLiteral(HotLiteral::Fmted(key))) =
362            self.implicit_key()
363        {
364            quote! { Some(#key) }
365        } else {
366            quote! { None }
367        };
368        let dynamic_nodes = self.dynamic_nodes().map(|node| {
369            let id = node.get_dyn_idx();
370            quote! { dioxus_core::internal::HotReloadDynamicNode::Dynamic(#id) }
371        });
372        let dyn_attr_printer = self.dynamic_attributes().map(|attr| {
373            let id = attr.get_dyn_idx();
374            quote! { dioxus_core::internal::HotReloadDynamicAttribute::Dynamic(#id) }
375        });
376        let component_values = self
377            .literal_component_properties()
378            .map(|literal| literal.quote_as_hot_reload_literal());
379        quote! {
380            dioxus_core::internal::HotReloadedTemplate::new(
381                #key,
382                vec![ #( #dynamic_nodes ),* ],
383                vec![ #( #dyn_attr_printer ),* ],
384                vec![ #( #component_values ),* ],
385                __TEMPLATE_ROOTS,
386            )
387        }
388    }
389}