244 lines
7.5 KiB
Rust
244 lines
7.5 KiB
Rust
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<Self> {
|
|
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::<Element>()?));
|
|
}
|
|
|
|
// 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::<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(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::<BodyNode>(element).unwrap(),
|
|
BodyNode::Element(_)
|
|
));
|
|
|
|
let text = quote! { "Hello, world!" };
|
|
assert!(matches!(
|
|
syn::parse2::<BodyNode>(text).unwrap(),
|
|
BodyNode::Text(_)
|
|
));
|
|
|
|
let component = quote! { Component {} };
|
|
assert!(matches!(
|
|
syn::parse2::<BodyNode>(component).unwrap(),
|
|
BodyNode::Component(_)
|
|
));
|
|
|
|
let raw_expr = quote! { { 1 + 1 } };
|
|
assert!(matches!(
|
|
syn::parse2::<BodyNode>(raw_expr).unwrap(),
|
|
BodyNode::RawExpr(_)
|
|
));
|
|
|
|
let for_loop = quote! { for item in items {} };
|
|
assert!(matches!(
|
|
syn::parse2::<BodyNode>(for_loop).unwrap(),
|
|
BodyNode::ForLoop(_)
|
|
));
|
|
|
|
let if_chain = quote! { if cond {} else if cond {} };
|
|
assert!(matches!(
|
|
syn::parse2::<BodyNode>(if_chain).unwrap(),
|
|
BodyNode::IfChain(_)
|
|
));
|
|
|
|
let match_expr = quote! {
|
|
match blah {
|
|
val => rsx! { div {} },
|
|
other_val => rsx! { div {} }
|
|
}
|
|
};
|
|
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(_)
|
|
),);
|
|
}
|
|
}
|