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