feat: Support generic components in rsx!() macro

This commit is contained in:
Muhannad Alrusayni 2022-04-29 02:00:12 +03:00
parent fe1279a1c5
commit a55b56b403
3 changed files with 126 additions and 34 deletions

View file

@ -18,19 +18,63 @@ use quote::{quote, ToTokens, TokenStreamExt};
use syn::{
ext::IdentExt,
parse::{Parse, ParseBuffer, ParseStream},
token, Expr, Ident, LitStr, Result, Token,
token, AngleBracketedGenericArguments, Expr, Ident, LitStr, PathArguments, Result, Token,
};
pub struct Component {
pub name: syn::Path,
pub prop_gen_args: Option<AngleBracketedGenericArguments>,
pub body: Vec<ComponentField>,
pub children: Vec<BodyNode>,
pub manual_props: Option<Expr>,
}
impl Component {
pub fn validate_component_path(path: &syn::Path) -> Result<()> {
// ensure path segments doesn't have PathArguments, only the last
// segment is allowed to have one.
if path
.segments
.iter()
.take(path.segments.len() - 1)
.any(|seg| seg.arguments != PathArguments::None)
{
component_path_cannot_have_arguments!(path);
}
// ensure last segment only have value of None or AngleBracketed
if !matches!(
path.segments.last().unwrap().arguments,
PathArguments::None | PathArguments::AngleBracketed(_)
) {
invalid_component_path!(path);
}
// if matches!(
// path.segments.last().unwrap().arguments,
// PathArguments::AngleBracketed(_)
// ) {
// proc_macro_error::abort!(path, "path: {}", path.to_token_stream().to_string());
// }
Ok(())
}
}
impl Parse for Component {
fn parse(stream: ParseStream) -> Result<Self> {
let name = syn::Path::parse_mod_style(stream)?;
let mut name = stream.parse::<syn::Path>()?;
Component::validate_component_path(&name)?;
// extract the path arguments from the path into prop_gen_args
let prop_gen_args = name.segments.last_mut().and_then(|seg| {
if let PathArguments::AngleBracketed(args) = seg.arguments.clone() {
seg.arguments = PathArguments::None;
Some(args)
} else {
None
}
});
let content: ParseBuffer;
@ -64,6 +108,7 @@ impl Parse for Component {
Ok(Self {
name,
prop_gen_args,
body,
children,
manual_props,
@ -74,6 +119,7 @@ impl Parse for Component {
impl ToTokens for Component {
fn to_tokens(&self, tokens: &mut TokenStream2) {
let name = &self.name;
let prop_gen_args = &self.prop_gen_args;
let mut has_key = None;
@ -101,7 +147,10 @@ impl ToTokens for Component {
}}
}
None => {
let mut toks = quote! { fc_to_builder(#name) };
let mut toks = match prop_gen_args {
Some(gen_args) => quote! { fc_to_builder::#gen_args(#name) },
None => quote! { fc_to_builder(#name) },
};
for field in &self.body {
match field.name.to_string().as_str() {
"key" => {

View file

@ -13,3 +13,29 @@ macro_rules! attr_after_element {
)
};
}
macro_rules! component_path_cannot_have_arguments {
($span:expr) => {
proc_macro_error::abort!(
$span,
"expected a path without arguments";
help = "try remove the path arguments"
)
};
}
macro_rules! component_ident_cannot_use_paren {
($span:expr, $com_name:ident) => {
proc_macro_error::abort!(
$span,
"Invalid component syntax";
help = "try replace {} (..) to {} {{..}}", $com_name, $com_name;
)
};
}
macro_rules! invalid_component_path {
($span:expr) => {
proc_macro_error::abort!($span, "Invalid component path syntax")
};
}

View file

@ -4,7 +4,7 @@ use proc_macro2::TokenStream as TokenStream2;
use quote::{quote, ToTokens, TokenStreamExt};
use syn::{
parse::{Parse, ParseStream},
token, Expr, LitStr, Result, Token,
token, Expr, LitStr, PathArguments, Result, Token,
};
/*
@ -28,39 +28,56 @@ impl Parse for BodyNode {
return Ok(BodyNode::Text(stream.parse()?));
}
// div {} -> el
// Div {} -> comp
if stream.peek(syn::Ident) && stream.peek2(token::Brace) {
if stream
.fork()
.parse::<Ident>()?
.to_string()
.chars()
.next()
.unwrap()
.is_ascii_uppercase()
{
return Ok(BodyNode::Component(stream.parse()?));
} else {
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 `{`
// - have no `PathArguments`
// - starts with lowercase
//
// example:
// div {}
if let Some(ident) = path.get_ident() {
if body_stream.peek(token::Brace)
&& ident
.to_string()
.chars()
.next()
.unwrap()
.is_ascii_lowercase()
{
return Ok(BodyNode::Element(stream.parse::<Element>()?));
}
}
}
// component() -> comp
// ::component {} -> comp
// ::component () -> comp
if (stream.peek(syn::Ident) && stream.peek2(token::Paren))
|| (stream.peek(Token![::]))
|| (stream.peek(Token![:]) && stream.peek2(Token![:]))
{
return Ok(BodyNode::Component(stream.parse::<Component>()?));
}
// 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()
// ::component {}
// ::component ()
// crate::component{}
// crate::component()
// Input<'_, String> {}
// crate::Input<'_, i32> {}
if body_stream.peek(token::Brace) || body_stream.peek(token::Paren) {
// this syntax is not allowd:
// Div () -> comp
if path.segments.len() == 1 && body_stream.peek(token::Paren) {
let com_ident = &path.segments.iter().next().unwrap().ident;
component_ident_cannot_use_paren!(path, com_ident);
}
// crate::component{} -> comp
// crate::component() -> comp
if let Ok(pat) = stream.fork().parse::<syn::Path>() {
if pat.segments.len() > 1 {
return Ok(BodyNode::Component(stream.parse::<Component>()?));
Component::validate_component_path(&path)?;
return Ok(BodyNode::Component(stream.parse()?));
}
}