Hotreloading of for/if/body, formatted strings, literals, component props, nested rsx, light CLI rewrite, cli TUI (#2258)
Hotreload the contents of for loops, if chains, component bodies, props, attributes, and any literals discovered in rsx! Add a TUI renderer to the CLI. Improve the CLI build system to be async and parallel. Refactor RSX to allow partial expansion of expressions. Merge autofmt implementations for consistency. Merge the representation of elements and components under the hood. Add a diagnostics system for rsx for improved error messages. Drop interprocess and move to websockets for communication between the CLI and the server. Assign IDs to nodes and attributes in a stable way to be used in non compiler contexts. Add hotreloading to any body of component/for loop/if chain/etc. --------- Co-authored-by: Evan Almloff <evanalmloff@gmail.com> Co-authored-by: Liam Mitchell <liamkarlmitchell@gmail.com>
This commit is contained in:
181 files changed
+12755
-10806
No files matched your search
+229
-399
@@ -1,188 +1,43 @@
|
||||
use self::location::CallerLocation;
|
||||
|
||||
use super::*;
|
||||
#[cfg(feature = "hot_reload")]
|
||||
use dioxus_core::TemplateNode;
|
||||
|
||||
use crate::innerlude::*;
|
||||
use proc_macro2::{Span, TokenStream as TokenStream2};
|
||||
use quote::quote;
|
||||
use quote::ToTokens;
|
||||
use syn::{
|
||||
braced,
|
||||
parse::ParseBuffer,
|
||||
parse::{Parse, ParseStream},
|
||||
spanned::Spanned,
|
||||
token::{self, Brace},
|
||||
Expr, ExprCall, ExprIf, Ident, LitStr, Pat,
|
||||
Ident, LitStr, Result, Token,
|
||||
};
|
||||
|
||||
/*
|
||||
Parse
|
||||
-> div {}
|
||||
-> Component {}
|
||||
-> component()
|
||||
-> "text {with_args}"
|
||||
-> {(0..10).map(|f| rsx!("asd"))} // <--- notice the curly braces
|
||||
*/
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(PartialEq, Eq, Clone, Debug)]
|
||||
pub enum BodyNode {
|
||||
/// div {}
|
||||
Element(Element),
|
||||
Text(IfmtInput),
|
||||
RawExpr(TokenStream2),
|
||||
|
||||
/// Component {}
|
||||
Component(Component),
|
||||
|
||||
/// "text {formatted}"
|
||||
Text(TextNode),
|
||||
|
||||
/// {expr}
|
||||
RawExpr(ExprNode),
|
||||
|
||||
/// for item in items {}
|
||||
ForLoop(ForLoop),
|
||||
|
||||
/// if cond {} else if cond {} (else {}?)
|
||||
IfChain(IfChain),
|
||||
}
|
||||
|
||||
impl PartialEq for BodyNode {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
match (self, other) {
|
||||
(Self::Element(l), Self::Element(r)) => l == r,
|
||||
(Self::Text(l), Self::Text(r)) => l == r,
|
||||
(Self::RawExpr(l), Self::RawExpr(r)) => l.to_string() == r.to_string(),
|
||||
(Self::Component(l), Self::Component(r)) => l == r,
|
||||
(Self::ForLoop(l), Self::ForLoop(r)) => l == r,
|
||||
(Self::IfChain(l), Self::IfChain(r)) => l == r,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for BodyNode {}
|
||||
|
||||
impl Hash for BodyNode {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
match self {
|
||||
Self::Element(el) => el.hash(state),
|
||||
Self::Text(text) => text.hash(state),
|
||||
Self::RawExpr(exp) => exp.to_string().hash(state),
|
||||
Self::Component(comp) => comp.hash(state),
|
||||
Self::ForLoop(for_loop) => for_loop.hash(state),
|
||||
Self::IfChain(if_chain) => if_chain.hash(state),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BodyNode {
|
||||
pub fn is_litstr(&self) -> bool {
|
||||
matches!(self, BodyNode::Text { .. })
|
||||
}
|
||||
|
||||
pub fn span(&self) -> Span {
|
||||
match self {
|
||||
BodyNode::Element(el) => el.name.span(),
|
||||
BodyNode::Component(component) => component.name.span(),
|
||||
BodyNode::Text(text) => text.source.span(),
|
||||
BodyNode::RawExpr(exp) => exp.span(),
|
||||
BodyNode::ForLoop(fl) => fl.for_token.span(),
|
||||
BodyNode::IfChain(f) => f.if_token.span(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn parse_with_options(
|
||||
stream: ParseStream,
|
||||
partial_completions: bool,
|
||||
) -> Result<Self> {
|
||||
// Make sure the next token is a brace if we're not in partial completion mode
|
||||
fn peek_brace(stream: &ParseBuffer, partial_completions: bool) -> bool {
|
||||
partial_completions || stream.peek(token::Brace)
|
||||
}
|
||||
|
||||
impl Parse for BodyNode {
|
||||
fn parse(stream: ParseStream) -> Result<Self> {
|
||||
if stream.peek(LitStr) {
|
||||
return Ok(BodyNode::Text(stream.parse()?));
|
||||
}
|
||||
|
||||
// if this is a dash-separated path, it's a web component (custom element)
|
||||
let body_stream = stream.fork();
|
||||
if let Ok(ElementName::Custom(name)) = body_stream.parse::<ElementName>() {
|
||||
if name.value().contains('-') && peek_brace(&body_stream, partial_completions) {
|
||||
return Ok(BodyNode::Element(stream.parse::<Element>()?));
|
||||
}
|
||||
}
|
||||
|
||||
let body_stream = stream.fork();
|
||||
|
||||
if let Ok(path) = body_stream.parse::<syn::Path>() {
|
||||
// this is an Element if path match of:
|
||||
// - one ident
|
||||
// - followed by `{`
|
||||
// - 1st char is lowercase
|
||||
// - no underscores (reserved for components)
|
||||
//
|
||||
// example:
|
||||
// div {}
|
||||
if let Some(ident) = path.get_ident() {
|
||||
if peek_brace(&body_stream, partial_completions)
|
||||
&& !ident_looks_like_component(ident)
|
||||
{
|
||||
return Ok(BodyNode::Element(Element::parse_with_options(
|
||||
stream,
|
||||
partial_completions,
|
||||
)?));
|
||||
}
|
||||
}
|
||||
|
||||
// If it is a single function call with a name that looks like a component, it should probably be a component
|
||||
// Eg, if we run into this:
|
||||
// ```rust
|
||||
// my_function(key, prop)
|
||||
// ```
|
||||
// We should tell the user that they need braces around props instead of turning the component call into an expression
|
||||
if let Ok(call) = stream.fork().parse::<ExprCall>() {
|
||||
if let Expr::Path(path) = call.func.as_ref() {
|
||||
if let Some(ident) = path.path.get_ident() {
|
||||
if ident_looks_like_component(ident) {
|
||||
let function_args: Vec<_> = call
|
||||
.args
|
||||
.iter()
|
||||
.map(|arg| arg.to_token_stream().to_string())
|
||||
.collect();
|
||||
let function_call = format!("{}({})", ident, function_args.join(", "));
|
||||
let component_call = if function_args.is_empty() {
|
||||
format!("{} {{}}", ident)
|
||||
} else {
|
||||
let component_args: Vec<_> = call
|
||||
.args
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(prop_count, arg)| {
|
||||
// Try to parse it as a shorthand field
|
||||
if let Ok(simple_ident) =
|
||||
syn::parse2::<Ident>(arg.to_token_stream())
|
||||
{
|
||||
format!("{}", simple_ident)
|
||||
} else {
|
||||
let ident = format!("prop{}", prop_count + 1);
|
||||
format!("{}: {}", ident, arg.to_token_stream())
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
format!("{} {{\n\t{}\n}}", ident, component_args.join(",\n\t"))
|
||||
};
|
||||
let error_text = format!(
|
||||
"Expected a valid body node found a function call. Did you forget to add braces around props?\nComponents should be called with braces instead of being called as expressions.\nInstead of:\n```rust\n{function_call}\n```\nTry:\n```rust\n{component_call}\n```\nIf you are trying to call a function, not a component, you need to wrap your expression in braces.",
|
||||
);
|
||||
return Err(syn::Error::new(call.span(), error_text));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise this should be Component, allowed syntax:
|
||||
// - syn::Path
|
||||
// - PathArguments can only appear in last segment
|
||||
// - followed by `{` or `(`, note `(` cannot be used with one ident
|
||||
//
|
||||
// example
|
||||
// Div {}
|
||||
// ::Div {}
|
||||
// crate::Div {}
|
||||
// component {} <-- already handled by elements
|
||||
// ::component {}
|
||||
// crate::component{}
|
||||
// Input::<InputProps<'_, i32> {}
|
||||
// crate::Input::<InputProps<'_, i32> {}
|
||||
if peek_brace(&body_stream, partial_completions) {
|
||||
return Ok(BodyNode::Component(stream.parse()?));
|
||||
}
|
||||
}
|
||||
|
||||
// Transform for loops into into_iter calls
|
||||
if stream.peek(Token![for]) {
|
||||
return Ok(BodyNode::ForLoop(stream.parse()?));
|
||||
@@ -194,273 +49,248 @@ impl BodyNode {
|
||||
}
|
||||
|
||||
// Match statements are special but have no special arm syntax
|
||||
// we could allow arm syntax if we wanted
|
||||
// we could allow arm syntax if we wanted.
|
||||
//
|
||||
// And it might even backwards compatible? - I think it is with the right fallback
|
||||
// -> parse as bodynode (BracedRawExpr will kick in on multiline arms)
|
||||
// -> if that fails parse as an expr, since that arm might be a one-liner
|
||||
//
|
||||
// ```
|
||||
// match {
|
||||
// val => div {}
|
||||
// other_val => div {}
|
||||
// match expr {
|
||||
// val => rsx! { div {} },
|
||||
// other_val => rsx! { div {} }
|
||||
// }
|
||||
// ```
|
||||
if stream.peek(Token![match]) {
|
||||
return Ok(BodyNode::RawExpr(stream.parse::<Expr>()?.to_token_stream()));
|
||||
return Ok(BodyNode::RawExpr(stream.parse()?));
|
||||
}
|
||||
|
||||
// Raw expressions need to be wrapped in braces - let RawBracedExpr handle partial expansion
|
||||
if stream.peek(token::Brace) {
|
||||
// If we are in strict mode, make sure thing inside the braces is actually a valid expression
|
||||
let combined = if !partial_completions {
|
||||
stream.parse::<Expr>()?.to_token_stream()
|
||||
} else {
|
||||
// otherwise, just take whatever is inside the braces. It might be invalid, but we still want to spit it out so we get completions
|
||||
let content;
|
||||
let brace = braced!(content in stream);
|
||||
let content: TokenStream2 = content.parse()?;
|
||||
let mut combined = TokenStream2::new();
|
||||
brace.surround(&mut combined, |inside_brace| {
|
||||
inside_brace.append_all(content);
|
||||
});
|
||||
combined
|
||||
};
|
||||
|
||||
return Ok(BodyNode::RawExpr(combined));
|
||||
return Ok(BodyNode::RawExpr(stream.parse()?));
|
||||
}
|
||||
|
||||
Err(syn::Error::new(
|
||||
stream.span(),
|
||||
"Expected a valid body node.\nExpressions must be wrapped in curly braces.",
|
||||
))
|
||||
}
|
||||
}
|
||||
// If there's an ident immediately followed by a dash, it's a web component
|
||||
// Web components support no namespacing, so just parse it as an element directly
|
||||
if stream.peek(Ident) && stream.peek2(Token![-]) {
|
||||
return Ok(BodyNode::Element(stream.parse::<Element>()?));
|
||||
}
|
||||
|
||||
// Checks if an ident looks like a component
|
||||
fn ident_looks_like_component(ident: &Ident) -> bool {
|
||||
let as_string = ident.to_string();
|
||||
let first_char = as_string.chars().next().unwrap();
|
||||
// Components either start with an uppercase letter or have an underscore in them
|
||||
first_char.is_ascii_uppercase() || as_string.contains('_')
|
||||
}
|
||||
// this is an Element if path match of:
|
||||
//
|
||||
// - one ident
|
||||
// - followed by `{`
|
||||
// - 1st char is lowercase
|
||||
// - no underscores (reserved for components)
|
||||
//
|
||||
// example:
|
||||
// div {}
|
||||
if stream.peek(Ident) && stream.peek2(Brace) {
|
||||
let ident = stream.fork().parse::<Ident>().unwrap();
|
||||
let el_name = ident.to_string();
|
||||
let first_char = el_name.chars().next().unwrap();
|
||||
|
||||
impl Parse for BodyNode {
|
||||
fn parse(stream: ParseStream) -> Result<Self> {
|
||||
Self::parse_with_options(stream, true)
|
||||
if first_char.is_ascii_lowercase() && !el_name.contains('_') {
|
||||
return Ok(BodyNode::Element(stream.parse::<Element>()?));
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise this should be Component, allowed syntax:
|
||||
// - syn::Path
|
||||
// - PathArguments can only apper in last segment
|
||||
// - followed by `{` or `(`, note `(` cannot be used with one ident
|
||||
//
|
||||
// example
|
||||
// Div {}
|
||||
// ::Div {}
|
||||
// crate::Div {}
|
||||
// component {} <-- already handled by elements
|
||||
// ::component {}
|
||||
// crate::component{}
|
||||
// Input::<InputProps<'_, i32> {}
|
||||
// crate::Input::<InputProps<'_, i32> {}
|
||||
Ok(BodyNode::Component(stream.parse()?))
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for BodyNode {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream2) {
|
||||
match self {
|
||||
BodyNode::Element(_) => {
|
||||
unimplemented!("Elements are statically created in the template")
|
||||
}
|
||||
BodyNode::Element(ela) => ela.to_tokens(tokens),
|
||||
BodyNode::RawExpr(exp) => exp.to_tokens(tokens),
|
||||
BodyNode::Text(txt) => txt.to_tokens(tokens),
|
||||
BodyNode::ForLoop(floop) => floop.to_tokens(tokens),
|
||||
BodyNode::Component(comp) => comp.to_tokens(tokens),
|
||||
BodyNode::IfChain(ifchain) => ifchain.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()))
|
||||
}),
|
||||
impl BodyNode {
|
||||
/// Convert this BodyNode into a TemplateNode.
|
||||
///
|
||||
/// dioxus-core uses this to understand templates at compiletime
|
||||
#[cfg(feature = "hot_reload")]
|
||||
pub fn to_template_node<Ctx: crate::HotReloadingContext>(&self) -> TemplateNode {
|
||||
match self {
|
||||
BodyNode::Element(el) => {
|
||||
let rust_name = el.name.to_string();
|
||||
|
||||
// Expressons too
|
||||
BodyNode::RawExpr(exp) => tokens.append_all(quote! {
|
||||
{
|
||||
#[allow(clippy::let_and_return)]
|
||||
let ___nodes = (#exp).into_dyn_node();
|
||||
___nodes
|
||||
let (tag, namespace) =
|
||||
Ctx::map_element(&rust_name).unwrap_or((intern(rust_name.as_str()), None));
|
||||
|
||||
TemplateNode::Element {
|
||||
tag,
|
||||
namespace,
|
||||
children: intern(
|
||||
el.children
|
||||
.iter()
|
||||
.map(|c| c.to_template_node::<Ctx>())
|
||||
.collect::<Vec<_>>(),
|
||||
),
|
||||
attrs: intern(
|
||||
el.merged_attributes
|
||||
.iter()
|
||||
.map(|attr| attr.to_template_attribute::<Ctx>(&rust_name))
|
||||
.collect::<Vec<_>>(),
|
||||
),
|
||||
}
|
||||
}),
|
||||
}
|
||||
BodyNode::Text(text) if text.is_static() => {
|
||||
let text = text.input.source.clone();
|
||||
let text = intern(text.value().as_str());
|
||||
TemplateNode::Text { text }
|
||||
}
|
||||
BodyNode::Text(text) => TemplateNode::Dynamic {
|
||||
id: text.dyn_idx.get(),
|
||||
},
|
||||
BodyNode::RawExpr(exp) => TemplateNode::Dynamic {
|
||||
id: exp.dyn_idx.get(),
|
||||
},
|
||||
BodyNode::Component(comp) => TemplateNode::Dynamic {
|
||||
id: comp.dyn_idx.get(),
|
||||
},
|
||||
BodyNode::ForLoop(floop) => TemplateNode::Dynamic {
|
||||
id: floop.dyn_idx.get(),
|
||||
},
|
||||
BodyNode::IfChain(chain) => TemplateNode::Dynamic {
|
||||
id: chain.dyn_idx.get(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// 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 }),
|
||||
pub fn get_dyn_idx(&self) -> usize {
|
||||
match self {
|
||||
BodyNode::Text(text) => text.dyn_idx.get(),
|
||||
BodyNode::RawExpr(exp) => exp.dyn_idx.get(),
|
||||
BodyNode::Component(comp) => comp.dyn_idx.get(),
|
||||
BodyNode::ForLoop(floop) => floop.dyn_idx.get(),
|
||||
BodyNode::IfChain(chain) => chain.dyn_idx.get(),
|
||||
BodyNode::Element(_) => panic!("Cannot get dyn_idx for this node"),
|
||||
}
|
||||
}
|
||||
|
||||
BodyNode::ForLoop(exp) => tokens.append_all(quote! { #exp }),
|
||||
pub fn set_dyn_idx(&self, idx: usize) {
|
||||
match self {
|
||||
BodyNode::Text(text) => text.dyn_idx.set(idx),
|
||||
BodyNode::RawExpr(exp) => exp.dyn_idx.set(idx),
|
||||
BodyNode::Component(comp) => comp.dyn_idx.set(idx),
|
||||
BodyNode::ForLoop(floop) => floop.dyn_idx.set(idx),
|
||||
BodyNode::IfChain(chain) => chain.dyn_idx.set(idx),
|
||||
BodyNode::Element(_) => panic!("Cannot set dyn_idx for this node"),
|
||||
}
|
||||
}
|
||||
|
||||
BodyNode::IfChain(chain) => tokens.append_all(quote! { #chain }),
|
||||
pub fn is_litstr(&self) -> bool {
|
||||
matches!(self, BodyNode::Text { .. })
|
||||
}
|
||||
|
||||
pub fn span(&self) -> Span {
|
||||
match self {
|
||||
BodyNode::Element(el) => el.name.span(),
|
||||
BodyNode::Component(component) => component.name.span(),
|
||||
BodyNode::Text(text) => text.input.source.span(),
|
||||
BodyNode::RawExpr(exp) => exp.span(),
|
||||
BodyNode::ForLoop(fl) => fl.for_token.span(),
|
||||
BodyNode::IfChain(f) => f.if_token.span(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn element_children(&self) -> &[BodyNode] {
|
||||
match self {
|
||||
BodyNode::Element(el) => &el.children,
|
||||
_ => panic!("Children not available for this node"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn el_name(&self) -> &ElementName {
|
||||
match self {
|
||||
BodyNode::Element(el) => &el.name,
|
||||
_ => panic!("Element name not available for this node"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[non_exhaustive]
|
||||
#[derive(PartialEq, Eq, Clone, Debug, Hash)]
|
||||
pub struct ForLoop {
|
||||
pub for_token: Token![for],
|
||||
pub pat: Pat,
|
||||
pub in_token: Token![in],
|
||||
pub expr: Box<Expr>,
|
||||
pub body: Vec<BodyNode>,
|
||||
pub brace_token: token::Brace,
|
||||
pub location: CallerLocation,
|
||||
}
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use quote::quote;
|
||||
|
||||
impl Parse for ForLoop {
|
||||
fn parse(input: ParseStream) -> Result<Self> {
|
||||
let for_token: Token![for] = input.parse()?;
|
||||
#[test]
|
||||
fn parsing_matches() {
|
||||
let element = quote! { div { class: "inline-block mr-4", icons::icon_14 {} } };
|
||||
assert!(matches!(
|
||||
syn::parse2::<BodyNode>(element).unwrap(),
|
||||
BodyNode::Element(_)
|
||||
));
|
||||
|
||||
let pat = Pat::parse_single(input)?;
|
||||
let text = quote! { "Hello, world!" };
|
||||
assert!(matches!(
|
||||
syn::parse2::<BodyNode>(text).unwrap(),
|
||||
BodyNode::Text(_)
|
||||
));
|
||||
|
||||
let in_token: Token![in] = input.parse()?;
|
||||
let expr: Expr = input.call(Expr::parse_without_eager_brace)?;
|
||||
let component = quote! { Component {} };
|
||||
assert!(matches!(
|
||||
syn::parse2::<BodyNode>(component).unwrap(),
|
||||
BodyNode::Component(_)
|
||||
));
|
||||
|
||||
let (brace_token, body) = parse_buffer_as_braced_children(input)?;
|
||||
let raw_expr = quote! { { 1 + 1 } };
|
||||
assert!(matches!(
|
||||
syn::parse2::<BodyNode>(raw_expr).unwrap(),
|
||||
BodyNode::RawExpr(_)
|
||||
));
|
||||
|
||||
Ok(Self {
|
||||
for_token,
|
||||
pat,
|
||||
in_token,
|
||||
body,
|
||||
brace_token,
|
||||
location: CallerLocation::default(),
|
||||
expr: Box::new(expr),
|
||||
})
|
||||
}
|
||||
}
|
||||
let for_loop = quote! { for item in items {} };
|
||||
assert!(matches!(
|
||||
syn::parse2::<BodyNode>(for_loop).unwrap(),
|
||||
BodyNode::ForLoop(_)
|
||||
));
|
||||
|
||||
impl ToTokens for ForLoop {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream2) {
|
||||
let ForLoop {
|
||||
pat, expr, body, ..
|
||||
} = self;
|
||||
let if_chain = quote! { if cond {} else if cond {} };
|
||||
assert!(matches!(
|
||||
syn::parse2::<BodyNode>(if_chain).unwrap(),
|
||||
BodyNode::IfChain(_)
|
||||
));
|
||||
|
||||
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! {
|
||||
{
|
||||
#[allow(clippy::let_and_return)]
|
||||
let ___nodes = (#expr).into_iter().map(|#pat| { #renderer }).into_dyn_node();
|
||||
___nodes
|
||||
let match_expr = quote! {
|
||||
match blah {
|
||||
val => rsx! { div {} },
|
||||
other_val => rsx! { div {} }
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[non_exhaustive]
|
||||
#[derive(PartialEq, Eq, Clone, Debug, Hash)]
|
||||
pub struct IfChain {
|
||||
pub if_token: Token![if],
|
||||
pub cond: Box<Expr>,
|
||||
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 {
|
||||
fn parse(input: ParseStream) -> Result<Self> {
|
||||
let if_token: Token![if] = input.parse()?;
|
||||
|
||||
// stolen from ExprIf
|
||||
let cond = Box::new(input.call(Expr::parse_without_eager_brace)?);
|
||||
|
||||
let (_, then_branch) = parse_buffer_as_braced_children(input)?;
|
||||
|
||||
let mut else_branch = None;
|
||||
let mut else_if_branch = None;
|
||||
|
||||
// if the next token is `else`, set the else branch as the next if chain
|
||||
if input.peek(Token![else]) {
|
||||
input.parse::<Token![else]>()?;
|
||||
if input.peek(Token![if]) {
|
||||
else_if_branch = Some(Box::new(input.parse::<IfChain>()?));
|
||||
} else {
|
||||
let (_, else_branch_nodes) = parse_buffer_as_braced_children(input)?;
|
||||
else_branch = Some(else_branch_nodes);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
cond,
|
||||
if_token,
|
||||
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 { {#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 { {#renderer} } });
|
||||
terminated = true;
|
||||
break;
|
||||
} else {
|
||||
elif = None;
|
||||
}
|
||||
}
|
||||
|
||||
if !terminated {
|
||||
body.append_all(quote! {
|
||||
else { dioxus_core::VNode::empty() }
|
||||
});
|
||||
}
|
||||
|
||||
tokens.append_all(quote! {
|
||||
{
|
||||
#[allow(clippy::let_and_return)]
|
||||
let ___nodes = (#body).into_dyn_node();
|
||||
___nodes
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_buffer_as_braced_children(
|
||||
input: &syn::parse::ParseBuffer<'_>,
|
||||
) -> Result<(Brace, Vec<BodyNode>)> {
|
||||
let content;
|
||||
let brace_token = braced!(content in input);
|
||||
let mut then_branch = vec![];
|
||||
while !content.is_empty() {
|
||||
then_branch.push(content.parse()?);
|
||||
}
|
||||
Ok((brace_token, then_branch))
|
||||
}
|
||||
|
||||
pub(crate) fn is_if_chain_terminated(chain: &ExprIf) -> bool {
|
||||
let mut current = chain;
|
||||
loop {
|
||||
if let Some((_, else_block)) = ¤t.else_branch {
|
||||
if let Expr::If(else_if) = else_block.as_ref() {
|
||||
current = else_if;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
assert!(matches!(
|
||||
syn::parse2::<BodyNode>(match_expr).unwrap(),
|
||||
BodyNode::RawExpr(_)
|
||||
),);
|
||||
|
||||
let incomplete_component = quote! {
|
||||
some::cool::Component
|
||||
};
|
||||
assert!(matches!(
|
||||
syn::parse2::<BodyNode>(incomplete_component).unwrap(),
|
||||
BodyNode::Component(_)
|
||||
),);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user