Parcourir la source

feat: Support generic components in `rsx!()` macro

Muhannad Alrusayni il y a 3 ans
Parent
commit
a55b56b403

+ 52 - 3
packages/core-macro/src/rsx/component.rs

@@ -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" => {

+ 26 - 0
packages/core-macro/src/rsx/errors.rs

@@ -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")
+    };
+}

+ 48 - 31
packages/core-macro/src/rsx/node.rs

@@ -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()?));
             }
         }