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//! ```text
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::{Span, 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 key_warnings = self.check_for_duplicate_keys();
113
114 let roots = node.quote_roots();
115
116 // Print paths is easy - just print the paths
117 let node_paths = node.node_paths.iter().map(|it| quote!(&[#(#it),*]));
118 let attr_paths = node.attr_paths.iter().map(|(it, _)| quote!(&[#(#it),*]));
119
120 // For printing dynamic nodes, we rely on the ToTokens impl
121 // Elements have a weird ToTokens - they actually are the entrypoint for Template creation
122 let dynamic_nodes: Vec<_> = node.dynamic_nodes().collect();
123 let dynamic_nodes_len = dynamic_nodes.len();
124
125 // We could add a ToTokens for Attribute but since we use that for both components and elements
126 // They actually need to be different, so we just localize that here
127 let dyn_attr_printer: Vec<_> = node
128 .dynamic_attributes()
129 .map(|attr| attr.rendered_as_dynamic_attr())
130 .collect();
131 let dynamic_attr_len = dyn_attr_printer.len();
132
133 let dynamic_text = node.dynamic_text_segments.iter();
134
135 let diagnostics = &node.diagnostics;
136 let index = node.template_idx.get();
137 let hot_reload_mapping = node.hot_reload_mapping();
138
139 tokens.append_all(quote! {
140 dioxus_core::Element::Ok({
141 #diagnostics
142
143 #key_warnings
144
145 // Components pull in the dynamic literal pool and template in debug mode, so they need to be defined before dynamic nodes
146 #[cfg(debug_assertions)]
147 fn __original_template() -> &'static dioxus_core::internal::HotReloadedTemplate {
148 static __ORIGINAL_TEMPLATE: ::std::sync::OnceLock<dioxus_core::internal::HotReloadedTemplate> = ::std::sync::OnceLock::new();
149 if __ORIGINAL_TEMPLATE.get().is_none() {
150 _ = __ORIGINAL_TEMPLATE.set(#hot_reload_mapping);
151 }
152 __ORIGINAL_TEMPLATE.get().unwrap()
153 }
154 #[cfg(debug_assertions)]
155 let __template_read = {
156 use dioxus_signals::ReadableExt;
157
158 static __NORMALIZED_FILE: &'static str = {
159 const PATH: &str = dioxus_core::const_format::str_replace!(file!(), "\\\\", "/");
160 dioxus_core::const_format::str_replace!(PATH, '\\', "/")
161 };
162
163 // The key is important here - we're creating a new GlobalSignal each call to this
164 // But the key is what's keeping it stable
165 static __TEMPLATE: dioxus_signals::GlobalSignal<Option<dioxus_core::internal::HotReloadedTemplate>> = dioxus_signals::GlobalSignal::with_location(
166 || None::<dioxus_core::internal::HotReloadedTemplate>,
167 __NORMALIZED_FILE,
168 line!(),
169 column!(),
170 #index
171 );
172
173 dioxus_core::Runtime::try_current().map(|_| __TEMPLATE.read())
174 };
175 // If the template has not been hot reloaded, we always use the original template
176 // Templates nested within macros may be merged because they have the same file-line-column-index
177 // They cannot be hot reloaded, so this prevents incorrect rendering
178 #[cfg(debug_assertions)]
179 let __template_read = match __template_read.as_ref().map(|__template_read| __template_read.as_ref()) {
180 Some(Some(__template_read)) => &__template_read,
181 _ => __original_template(),
182 };
183 #[cfg(debug_assertions)]
184 let mut __dynamic_literal_pool = dioxus_core::internal::DynamicLiteralPool::new(
185 vec![ #( #dynamic_text.to_string() ),* ],
186 );
187
188 // The key needs to be created before the dynamic nodes as it might depend on a borrowed value which gets moved into the dynamic nodes
189 #[cfg(not(debug_assertions))]
190 let __key = #key_tokens;
191 // These items are used in both the debug and release expansions of rsx. Pulling them out makes the expansion
192 // slightly smaller and easier to understand. Rust analyzer also doesn't autocomplete well when it sees an ident show up twice in the expansion
193 let __dynamic_nodes: [dioxus_core::DynamicNode; #dynamic_nodes_len] = [ #( #dynamic_nodes ),* ];
194 let __dynamic_attributes: [Box<[dioxus_core::Attribute]>; #dynamic_attr_len] = [ #( #dyn_attr_printer ),* ];
195 #[doc(hidden)]
196 static __TEMPLATE_ROOTS: &[dioxus_core::TemplateNode] = &[ #( #roots ),* ];
197
198 #[cfg(debug_assertions)]
199 {
200 let mut __dynamic_value_pool = dioxus_core::internal::DynamicValuePool::new(
201 Vec::from(__dynamic_nodes),
202 Vec::from(__dynamic_attributes),
203 __dynamic_literal_pool
204 );
205 __dynamic_value_pool.render_with(__template_read)
206 }
207 #[cfg(not(debug_assertions))]
208 {
209 #[doc(hidden)] // vscode please stop showing these in symbol search
210 static ___TEMPLATE: dioxus_core::Template = dioxus_core::Template {
211 roots: __TEMPLATE_ROOTS,
212 node_paths: &[ #( #node_paths ),* ],
213 attr_paths: &[ #( #attr_paths ),* ],
214 };
215
216 // NOTE: Allocating a temporary is important to make reads within rsx drop before the value is returned
217 #[allow(clippy::let_and_return)]
218 let __vnodes = dioxus_core::VNode::new(
219 __key,
220 ___TEMPLATE,
221 Box::new(__dynamic_nodes),
222 Box::new(__dynamic_attributes),
223 );
224 __vnodes
225 }
226 })
227 });
228 }
229}
230
231impl TemplateBody {
232 /// Create a new TemplateBody from a set of nodes
233 ///
234 /// This will fill in all the necessary path information for the nodes in the template and will
235 /// overwrite data like dynamic indexes.
236 pub fn new(nodes: Vec<BodyNode>) -> Self {
237 let mut body = Self {
238 roots: vec![],
239 template_idx: DynIdx::default(),
240 node_paths: Vec::new(),
241 attr_paths: Vec::new(),
242 dynamic_text_segments: Vec::new(),
243 diagnostics: Diagnostics::new(),
244 };
245
246 // Assign paths to all nodes in the template
247 body.assign_paths_inner(&nodes);
248
249 // And then save the roots
250 body.roots = nodes;
251
252 // Finally, validate the key
253 body.validate_key();
254
255 body
256 }
257
258 /// Normalize the Template body for rendering. If the body is completely empty, insert a placeholder node
259 pub fn normalized(&self) -> Self {
260 // If the nodes are completely empty, insert a placeholder node
261 // Core expects at least one node in the template to make it easier to replace
262 if self.is_empty() {
263 // Create an empty template body with a placeholder and diagnostics + the template index from the original
264 let empty = Self::new(vec![BodyNode::RawExpr(parse_quote! {()})]);
265 let default = Self {
266 diagnostics: self.diagnostics.clone(),
267 template_idx: self.template_idx.clone(),
268 ..empty
269 };
270 return default;
271 }
272 self.clone()
273 }
274
275 pub fn is_empty(&self) -> bool {
276 self.roots.is_empty()
277 }
278
279 pub fn implicit_key(&self) -> Option<&AttributeValue> {
280 self.roots.first().and_then(BodyNode::key)
281 }
282
283 /// Ensure only one key and that the key is not a static str
284 ///
285 /// todo: we want to allow arbitrary exprs for keys provided they impl hash / eq
286 fn validate_key(&mut self) {
287 let key = self.implicit_key();
288
289 if let Some(attr) = key {
290 let diagnostic = match &attr {
291 AttributeValue::AttrLiteral(ifmt) => {
292 if ifmt.is_static() {
293 ifmt.span().error("Key must not be a static string. Make sure to use a formatted string like `key: \"{value}\"")
294 } else {
295 return;
296 }
297 }
298 _ => attr
299 .span()
300 .error("Key must be in the form of a formatted string like `key: \"{value}\""),
301 };
302
303 self.diagnostics.push(diagnostic);
304 }
305 }
306
307 fn check_for_duplicate_keys(&self) -> TokenStream2 {
308 let mut warnings = TokenStream2::new();
309
310 // Make sure there are not multiple keys or keys on nodes other than the first in the block
311 for root in self.roots.iter().skip(1) {
312 if let Some(key) = root.key() {
313 warnings.extend(new_diagnostics::warning_diagnostic(
314 key.span(),
315 "Keys are only allowed on the first node in the block.",
316 ));
317 }
318 }
319
320 warnings
321 }
322
323 pub fn get_dyn_node(&self, path: &[u8]) -> &BodyNode {
324 let mut node = self.roots.get(path[0] as usize).unwrap();
325 for idx in path.iter().skip(1) {
326 node = node.element_children().get(*idx as usize).unwrap();
327 }
328 node
329 }
330
331 pub fn get_dyn_attr(&self, path: &AttributePath, idx: usize) -> &Attribute {
332 match self.get_dyn_node(path) {
333 BodyNode::Element(el) => &el.merged_attributes[idx],
334 _ => unreachable!(),
335 }
336 }
337
338 pub fn dynamic_attributes(&self) -> impl DoubleEndedIterator<Item = &Attribute> {
339 self.attr_paths
340 .iter()
341 .map(|(path, idx)| self.get_dyn_attr(path, *idx))
342 }
343
344 pub fn dynamic_nodes(&self) -> impl DoubleEndedIterator<Item = &BodyNode> {
345 self.node_paths.iter().map(|path| self.get_dyn_node(path))
346 }
347
348 fn quote_roots(&self) -> impl Iterator<Item = TokenStream2> + '_ {
349 self.roots.iter().map(|node| match node {
350 BodyNode::Element(el) => quote! { #el },
351 BodyNode::Text(text) if text.is_static() => {
352 let text = text.input.to_static().unwrap();
353 quote! { dioxus_core::TemplateNode::Text { text: #text } }
354 }
355 _ => {
356 let id = node.get_dyn_idx();
357 quote! { dioxus_core::TemplateNode::Dynamic { id: #id } }
358 }
359 })
360 }
361
362 /// Iterate through the literal component properties of this rsx call in depth-first order
363 pub fn literal_component_properties(&self) -> impl Iterator<Item = &HotLiteral> + '_ {
364 self.dynamic_nodes()
365 .filter_map(|node| {
366 if let BodyNode::Component(component) = node {
367 Some(component)
368 } else {
369 None
370 }
371 })
372 .flat_map(|component| {
373 component.component_props().filter_map(|field| {
374 if let AttributeValue::AttrLiteral(literal) = &field.value {
375 Some(literal)
376 } else {
377 None
378 }
379 })
380 })
381 }
382
383 fn hot_reload_mapping(&self) -> TokenStream2 {
384 let key = if let Some(AttributeValue::AttrLiteral(HotLiteral::Fmted(key))) =
385 self.implicit_key()
386 {
387 quote! { Some(#key) }
388 } else {
389 quote! { None }
390 };
391 let dynamic_nodes = self.dynamic_nodes().map(|node| {
392 let id = node.get_dyn_idx();
393 quote! { dioxus_core::internal::HotReloadDynamicNode::Dynamic(#id) }
394 });
395 let dyn_attr_printer = self.dynamic_attributes().map(|attr| {
396 let id = attr.get_dyn_idx();
397 quote! { dioxus_core::internal::HotReloadDynamicAttribute::Dynamic(#id) }
398 });
399 let component_values = self
400 .literal_component_properties()
401 .map(|literal| literal.quote_as_hot_reload_literal());
402 quote! {
403 dioxus_core::internal::HotReloadedTemplate::new(
404 #key,
405 vec![ #( #dynamic_nodes ),* ],
406 vec![ #( #dyn_attr_printer ),* ],
407 vec![ #( #component_values ),* ],
408 __TEMPLATE_ROOTS,
409 )
410 }
411 }
412
413 /// Get the span of the first root of this template
414 pub(crate) fn first_root_span(&self) -> Span {
415 match self.roots.first() {
416 Some(root) => root.span(),
417 _ => Span::call_site(),
418 }
419 }
420}