dioxus_rsx/
node.rs

1use crate::innerlude::*;
2use proc_macro2::{Span, TokenStream as TokenStream2};
3use quote::ToTokens;
4use syn::{
5    ext::IdentExt,
6    parse::{Parse, ParseStream},
7    spanned::Spanned,
8    token::{self},
9    Ident, LitStr, Result, Token,
10};
11
12#[derive(PartialEq, Eq, Clone, Debug)]
13pub enum BodyNode {
14    /// div {}
15    Element(Element),
16
17    /// Component {}
18    Component(Component),
19
20    /// "text {formatted}"
21    Text(TextNode),
22
23    /// {expr}
24    RawExpr(ExprNode),
25
26    /// for item in items {}
27    ForLoop(ForLoop),
28
29    /// if cond {} else if cond {} (else {}?)
30    IfChain(IfChain),
31}
32
33impl Parse for BodyNode {
34    fn parse(stream: ParseStream) -> Result<Self> {
35        if stream.peek(LitStr) {
36            return Ok(BodyNode::Text(stream.parse()?));
37        }
38
39        // Transform for loops into into_iter calls
40        if stream.peek(Token![for]) {
41            return Ok(BodyNode::ForLoop(stream.parse()?));
42        }
43
44        // Transform unterminated if statements into terminated optional if statements
45        if stream.peek(Token![if]) {
46            return Ok(BodyNode::IfChain(stream.parse()?));
47        }
48
49        // Match statements are special but have no special arm syntax
50        // we could allow arm syntax if we wanted.
51        //
52        // And it might even backwards compatible? - I think it is with the right fallback
53        // -> parse as bodynode (BracedRawExpr will kick in on multiline arms)
54        // -> if that fails parse as an expr, since that arm might be a one-liner
55        //
56        // ```
57        // match expr {
58        //    val => rsx! { div {} },
59        //    other_val => rsx! { div {} }
60        // }
61        // ```
62        if stream.peek(Token![match]) {
63            return Ok(BodyNode::RawExpr(stream.parse()?));
64        }
65
66        // Raw expressions need to be wrapped in braces - let RawBracedExpr handle partial expansion
67        if stream.peek(token::Brace) {
68            return Ok(BodyNode::RawExpr(stream.parse()?));
69        }
70
71        // If there's an ident immediately followed by a dash, it's a web component
72        // Web components support no namespacing, so just parse it as an element directly
73        if stream.peek(Ident::peek_any) && stream.peek2(Token![-]) {
74            return Ok(BodyNode::Element(stream.parse::<Element>()?));
75        }
76
77        // this is an Element if the path is:
78        //
79        // - one ident
80        // - 1st char is lowercase
81        // - no underscores (reserved for components)
82        // And it is not:
83        // - the start of a path with components
84        //
85        // example:
86        // div {}
87        if stream.peek(Ident::peek_any) && !stream.peek2(Token![::]) {
88            let ident = parse_raw_ident(&stream.fork()).unwrap();
89            let el_name = ident.to_string();
90            let first_char = el_name.chars().next().unwrap();
91
92            if first_char.is_ascii_lowercase() && !el_name.contains('_') {
93                return Ok(BodyNode::Element(stream.parse::<Element>()?));
94            }
95        }
96
97        // Otherwise this should be Component, allowed syntax:
98        // - syn::Path
99        // - PathArguments can only apper in last segment
100        // - followed by `{` or `(`, note `(` cannot be used with one ident
101        //
102        // example
103        // Div {}
104        // ::Div {}
105        // crate::Div {}
106        // component {} <-- already handled by elements
107        // ::component {}
108        // crate::component{}
109        // Input::<InputProps<'_, i32> {}
110        // crate::Input::<InputProps<'_, i32> {}
111        Ok(BodyNode::Component(stream.parse()?))
112    }
113}
114
115impl ToTokens for BodyNode {
116    fn to_tokens(&self, tokens: &mut TokenStream2) {
117        match self {
118            BodyNode::Element(ela) => ela.to_tokens(tokens),
119            BodyNode::RawExpr(exp) => exp.to_tokens(tokens),
120            BodyNode::Text(txt) => txt.to_tokens(tokens),
121            BodyNode::ForLoop(floop) => floop.to_tokens(tokens),
122            BodyNode::Component(comp) => comp.to_tokens(tokens),
123            BodyNode::IfChain(ifchain) => ifchain.to_tokens(tokens),
124        }
125    }
126}
127
128impl BodyNode {
129    pub fn get_dyn_idx(&self) -> usize {
130        match self {
131            BodyNode::Text(text) => text.dyn_idx.get(),
132            BodyNode::RawExpr(exp) => exp.dyn_idx.get(),
133            BodyNode::Component(comp) => comp.dyn_idx.get(),
134            BodyNode::ForLoop(floop) => floop.dyn_idx.get(),
135            BodyNode::IfChain(chain) => chain.dyn_idx.get(),
136            BodyNode::Element(_) => panic!("Cannot get dyn_idx for this node"),
137        }
138    }
139
140    pub fn set_dyn_idx(&self, idx: usize) {
141        match self {
142            BodyNode::Text(text) => text.dyn_idx.set(idx),
143            BodyNode::RawExpr(exp) => exp.dyn_idx.set(idx),
144            BodyNode::Component(comp) => comp.dyn_idx.set(idx),
145            BodyNode::ForLoop(floop) => floop.dyn_idx.set(idx),
146            BodyNode::IfChain(chain) => chain.dyn_idx.set(idx),
147            BodyNode::Element(_) => panic!("Cannot set dyn_idx for this node"),
148        }
149    }
150
151    pub fn is_litstr(&self) -> bool {
152        matches!(self, BodyNode::Text { .. })
153    }
154
155    pub fn span(&self) -> Span {
156        match self {
157            BodyNode::Element(el) => el.name.span(),
158            BodyNode::Component(component) => component.name.span(),
159            BodyNode::Text(text) => text.input.span(),
160            BodyNode::RawExpr(exp) => exp.span(),
161            BodyNode::ForLoop(fl) => fl.for_token.span(),
162            BodyNode::IfChain(f) => f.if_token.span(),
163        }
164    }
165
166    pub fn element_children(&self) -> &[BodyNode] {
167        match self {
168            BodyNode::Element(el) => &el.children,
169            _ => panic!("Children not available for this node"),
170        }
171    }
172
173    pub fn el_name(&self) -> &ElementName {
174        match self {
175            BodyNode::Element(el) => &el.name,
176            _ => panic!("Element name not available for this node"),
177        }
178    }
179}
180
181#[cfg(test)]
182mod tests {
183    use super::*;
184    use quote::quote;
185
186    #[test]
187    fn parsing_matches() {
188        let element = quote! { div { class: "inline-block mr-4", icons::icon_14 {} } };
189        assert!(matches!(
190            syn::parse2::<BodyNode>(element).unwrap(),
191            BodyNode::Element(_)
192        ));
193
194        let text = quote! { "Hello, world!" };
195        assert!(matches!(
196            syn::parse2::<BodyNode>(text).unwrap(),
197            BodyNode::Text(_)
198        ));
199
200        let component = quote! { Component {} };
201        assert!(matches!(
202            syn::parse2::<BodyNode>(component).unwrap(),
203            BodyNode::Component(_)
204        ));
205
206        let raw_expr = quote! { { 1 + 1 } };
207        assert!(matches!(
208            syn::parse2::<BodyNode>(raw_expr).unwrap(),
209            BodyNode::RawExpr(_)
210        ));
211
212        let for_loop = quote! { for item in items {} };
213        assert!(matches!(
214            syn::parse2::<BodyNode>(for_loop).unwrap(),
215            BodyNode::ForLoop(_)
216        ));
217
218        let if_chain = quote! { if cond {} else if cond {} };
219        assert!(matches!(
220            syn::parse2::<BodyNode>(if_chain).unwrap(),
221            BodyNode::IfChain(_)
222        ));
223
224        let match_expr = quote! {
225            match blah {
226                val => rsx! { div {} },
227                other_val => rsx! { div {} }
228            }
229        };
230        assert!(matches!(
231            syn::parse2::<BodyNode>(match_expr).unwrap(),
232            BodyNode::RawExpr(_)
233        ),);
234
235        let incomplete_component = quote! {
236            some::cool::Component
237        };
238        assert!(matches!(
239            syn::parse2::<BodyNode>(incomplete_component).unwrap(),
240            BodyNode::Component(_)
241        ),);
242    }
243}