use crate::innerlude::*; use proc_macro2::{Span, TokenStream as TokenStream2}; use quote::ToTokens; use syn::{ ext::IdentExt, parse::{Parse, ParseStream}, spanned::Spanned, token::{self}, Ident, LitStr, Result, Token, }; #[derive(PartialEq, Eq, Clone, Debug)] pub enum BodyNode { /// div {} Element(Element), /// 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 Parse for BodyNode { fn parse(stream: ParseStream) -> Result { if stream.peek(LitStr) { return Ok(BodyNode::Text(stream.parse()?)); } // Transform for loops into into_iter calls if stream.peek(Token![for]) { return Ok(BodyNode::ForLoop(stream.parse()?)); } // Transform unterminated if statements into terminated optional if statements if stream.peek(Token![if]) { return Ok(BodyNode::IfChain(stream.parse()?)); } // Match statements are special but have no special arm syntax // 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 expr { // val => rsx! { div {} }, // other_val => rsx! { div {} } // } // ``` if stream.peek(Token![match]) { return Ok(BodyNode::RawExpr(stream.parse()?)); } // Raw expressions need to be wrapped in braces - let RawBracedExpr handle partial expansion if stream.peek(token::Brace) { return Ok(BodyNode::RawExpr(stream.parse()?)); } // 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::peek_any) && stream.peek2(Token![-]) { return Ok(BodyNode::Element(stream.parse::()?)); } // this is an Element if the path is: // // - one ident // - 1st char is lowercase // - no underscores (reserved for components) // And it is not: // - the start of a path with components // // example: // div {} if stream.peek(Ident::peek_any) && !stream.peek2(Token![::]) { let ident = parse_raw_ident(&stream.fork()).unwrap(); let el_name = ident.to_string(); let first_char = el_name.chars().next().unwrap(); if first_char.is_ascii_lowercase() && !el_name.contains('_') { return Ok(BodyNode::Element(stream.parse::()?)); } } // 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:: {} // crate::Input:: {} Ok(BodyNode::Component(stream.parse()?)) } } impl ToTokens for BodyNode { fn to_tokens(&self, tokens: &mut TokenStream2) { match self { 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), } } } impl BodyNode { 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"), } } 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"), } } 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.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"), } } } #[cfg(test)] mod tests { use super::*; use quote::quote; #[test] fn parsing_matches() { let element = quote! { div { class: "inline-block mr-4", icons::icon_14 {} } }; assert!(matches!( syn::parse2::(element).unwrap(), BodyNode::Element(_) )); let text = quote! { "Hello, world!" }; assert!(matches!( syn::parse2::(text).unwrap(), BodyNode::Text(_) )); let component = quote! { Component {} }; assert!(matches!( syn::parse2::(component).unwrap(), BodyNode::Component(_) )); let raw_expr = quote! { { 1 + 1 } }; assert!(matches!( syn::parse2::(raw_expr).unwrap(), BodyNode::RawExpr(_) )); let for_loop = quote! { for item in items {} }; assert!(matches!( syn::parse2::(for_loop).unwrap(), BodyNode::ForLoop(_) )); let if_chain = quote! { if cond {} else if cond {} }; assert!(matches!( syn::parse2::(if_chain).unwrap(), BodyNode::IfChain(_) )); let match_expr = quote! { match blah { val => rsx! { div {} }, other_val => rsx! { div {} } } }; assert!(matches!( syn::parse2::(match_expr).unwrap(), BodyNode::RawExpr(_) ),); let incomplete_component = quote! { some::cool::Component }; assert!(matches!( syn::parse2::(incomplete_component).unwrap(), BodyNode::Component(_) ),); } }