Docs in component macro

This commit is contained in:
Greg Johnston 2022-12-10 16:21:58 -05:00
parent 4a9f906571
commit 18bd2162cf
2 changed files with 225 additions and 142 deletions

View file

@ -12,6 +12,7 @@ proc-macro = true
[dependencies]
cfg-if = "1"
itertools = "0.10"
proc-macro-error = "1"
proc-macro2 = "1"
quote = "1"

View file

@ -1,167 +1,247 @@
// Credit to Dioxus: https://github.com/DioxusLabs/dioxus/blob/master/packages/core-macro/src/inlineprops.rs
// Based in large part on Dioxus: https://github.com/DioxusLabs/dioxus/blob/master/packages/core-macro/src/inlineprops.rs
use proc_macro2::{Span, TokenStream as TokenStream2};
use quote::{quote, ToTokens, TokenStreamExt};
#![allow(unstable_name_collisions)]
use std::collections::HashMap;
use proc_macro2::{Span, TokenStream as TokenStream2, TokenTree};
use quote::{quote, ToTokens, TokenStreamExt,};
use syn::{
parse::{Parse, ParseStream},
punctuated::Punctuated,
*,
parse::{Parse, ParseStream},
punctuated::Punctuated,
*,
};
use itertools::Itertools;
pub struct InlinePropsBody {
pub attrs: Vec<Attribute>,
pub vis: syn::Visibility,
pub fn_token: Token![fn],
pub ident: Ident,
pub cx_token: Box<Pat>,
pub generics: Generics,
pub paren_token: token::Paren,
pub inputs: Punctuated<FnArg, Token![,]>,
// pub fields: FieldsNamed,
pub output: ReturnType,
pub where_clause: Option<WhereClause>,
pub block: Box<Block>,
pub attrs: Vec<Attribute>,
pub vis: syn::Visibility,
pub fn_token: Token![fn],
pub ident: Ident,
pub cx_token: Box<Pat>,
pub generics: Generics,
pub paren_token: token::Paren,
pub inputs: Punctuated<FnArg, Token![,]>,
// pub fields: FieldsNamed,
pub output: ReturnType,
pub where_clause: Option<WhereClause>,
pub block: Box<Block>,
pub doc_comment: String
}
/// The custom rusty variant of parsing rsx!
impl Parse for InlinePropsBody {
fn parse(input: ParseStream) -> Result<Self> {
let attrs: Vec<Attribute> = input.call(Attribute::parse_outer)?;
let vis: Visibility = input.parse()?;
fn parse(input: ParseStream) -> Result<Self> {
let attrs: Vec<Attribute> = input.call(Attribute::parse_outer)?;
let vis: Visibility = input.parse()?;
let fn_token = input.parse()?;
let ident = input.parse()?;
let generics: Generics = input.parse()?;
let fn_token = input.parse()?;
let ident = input.parse()?;
let generics: Generics = input.parse()?;
let content;
let paren_token = syn::parenthesized!(content in input);
let content;
let paren_token = syn::parenthesized!(content in input);
let first_arg: FnArg = content.parse()?;
let cx_token = {
match first_arg {
FnArg::Receiver(_) => {
panic!("first argument must not be a receiver argument")
}
FnArg::Typed(f) => f.pat,
}
};
let first_arg: FnArg = content.parse()?;
let cx_token = {
match first_arg {
FnArg::Receiver(_) => panic!("first argument must not be a receiver argument"),
FnArg::Typed(f) => f.pat,
}
};
let _: Result<Token![,]> = content.parse();
let _: Result<Token![,]> = content.parse();
let inputs = syn::punctuated::Punctuated::parse_terminated(&content)?;
let inputs = syn::punctuated::Punctuated::parse_terminated(&content)?;
let output = input.parse()?;
let output = input.parse()?;
let where_clause = input
.peek(syn::token::Where)
.then(|| input.parse())
.transpose()?;
let where_clause = input
.peek(syn::token::Where)
.then(|| input.parse())
.transpose()?;
let block = input.parse()?;
let block = input.parse()?;
Ok(Self {
vis,
fn_token,
ident,
generics,
paren_token,
inputs,
output,
where_clause,
block,
cx_token,
attrs,
})
}
let doc_comment = attrs.iter().filter_map(|attr| if attr.path.segments[0].ident == "doc" {
Some(attr.clone().tokens.into_iter().filter_map(|token| if let TokenTree::Literal(_) = token {
// remove quotes
let chars = token.to_string();
let mut chars = chars.chars();
chars.next();
chars.next_back();
Some(chars.as_str().to_string())
} else {
None
}).collect::<String>())
} else {
None
})
.intersperse_with(|| "\n".to_string())
.collect();
Ok(Self {
vis,
fn_token,
ident,
generics,
paren_token,
inputs,
output,
where_clause,
block,
cx_token,
attrs,
doc_comment
})
}
}
/// Serialize the same way, regardless of flavor
impl ToTokens for InlinePropsBody {
fn to_tokens(&self, out_tokens: &mut TokenStream2) {
let Self {
vis,
ident,
generics,
inputs,
output,
where_clause,
block,
cx_token,
attrs,
..
} = self;
fn to_tokens(&self, out_tokens: &mut TokenStream2) {
let Self {
vis,
ident,
generics,
inputs,
output,
where_clause,
block,
cx_token,
attrs,
doc_comment,
..
} = self;
let fields = inputs.iter().map(|f| {
let typed_arg = match f {
FnArg::Receiver(_) => todo!(),
FnArg::Typed(t) => t,
};
if let Type::Path(pat) = &*typed_arg.ty {
if pat.path.segments[0].ident == "Option" {
quote! {
#[doc = "My comment."]
#[builder(default, setter(strip_option, into))]
#vis #f
}
} else {
quote! { #vis #f }
}
} else {
quote! { #vis #f }
}
});
let component_name_str = ident.to_string();
let struct_name = Ident::new(&format!("{}Props", ident), Span::call_site());
let field_names = inputs.iter().filter_map(|f| match f {
FnArg::Receiver(_) => todo!(),
FnArg::Typed(t) => Some(&t.pat),
});
let first_lifetime =
if let Some(GenericParam::Lifetime(lt)) = generics.params.first() {
Some(lt)
} else {
None
};
let modifiers = quote! { #[derive(leptos::TypedBuilder)] };
let (_scope_lifetime, fn_generics, struct_generics) =
if let Some(lt) = first_lifetime {
let struct_generics: Punctuated<_, token::Comma> = generics
.params
.iter()
.map(|it| match it {
GenericParam::Type(tp) => {
let mut tp = tp.clone();
tp.bounds.push(parse_quote!( 'a ));
GenericParam::Type(tp)
let field_docs: HashMap<String, String> = {
let mut map = HashMap::new();
let mut pieces = doc_comment.split("# Props");
pieces.next();
let rest = pieces.next().unwrap_or_default();
let mut current_field_name = String::new();
let mut current_field_value = String::new();
for line in rest.split('\n') {
if let Some(line) = line.strip_prefix(" - ") {
let mut pieces = line.split("**");
pieces.next();
let field_name = pieces.next();
let field_value = pieces.next().unwrap_or_default();
let field_value = if let Some((_ty, desc)) = field_value.split_once('-') {
desc
} else {
field_value
};
if let Some(field_name) = field_name {
if !current_field_name.is_empty() {
map.insert(current_field_name.clone(), current_field_value.clone());
}
current_field_name = field_name.to_string();
current_field_value = String::new();
current_field_value.push_str(field_value);
} else {
current_field_value.push_str(field_value);
}
} else {
current_field_value.push_str(line);
}
}
if !current_field_name.is_empty() {
map.insert(current_field_name, current_field_value.clone());
}
_ => it.clone(),
})
.collect();
(
quote! { #lt, },
generics.clone(),
quote! { <#struct_generics> },
)
} else {
let lifetime: LifetimeDef = parse_quote! { 'a };
map
};
let mut fn_generics = generics.clone();
fn_generics
.params
.insert(0, GenericParam::Lifetime(lifetime.clone()));
let fields = inputs.iter().map(|f| {
let typed_arg = match f {
FnArg::Receiver(_) => todo!(),
FnArg::Typed(t) => t,
};
let comment = if let Pat::Ident(ident) = &*typed_arg.pat {
field_docs.get(&ident.ident.to_string()).cloned()
} else {
None
}.unwrap_or_default();
let comment_macro = quote! {
#[doc = #comment]
};
if let Type::Path(pat) = &*typed_arg.ty {
if pat.path.segments[0].ident == "Option" {
quote! {
#comment_macro
#[builder(default, setter(strip_option, doc = #comment))]
pub #f
}
} else {
quote! {
#comment_macro
#[builder(setter(doc = #comment))]
pub #f
}
}
} else {
quote! {
#comment
#vis #f
}
}
});
(quote! { #lifetime, }, fn_generics, quote! { #generics })
};
let component_name_str = ident.to_string();
let struct_name = Ident::new(&format!("{}Props", ident), Span::call_site());
let prop_struct_comments = format!("Props for the [`{ident}`] component.");
out_tokens.append_all(quote! {
let field_names = inputs.iter().filter_map(|f| match f {
FnArg::Receiver(_) => todo!(),
FnArg::Typed(t) => Some(&t.pat),
});
let first_lifetime = if let Some(GenericParam::Lifetime(lt)) = generics.params.first() {
Some(lt)
} else {
None
};
//let modifiers = if first_lifetime.is_some() {
let modifiers = quote! {
#[derive(leptos::TypedBuilder)]
#[builder(doc)]
};
/* } else {
quote! { #[derive(Props, PartialEq, Eq)] }
}; */
let (_scope_lifetime, fn_generics, struct_generics) = if let Some(lt) = first_lifetime {
let struct_generics: Punctuated<_, token::Comma> = generics
.params
.iter()
.map(|it| match it {
GenericParam::Type(tp) => {
let mut tp = tp.clone();
tp.bounds.push(parse_quote!( 'a ));
GenericParam::Type(tp)
}
_ => it.clone(),
})
.collect();
(
quote! { #lt, },
generics.clone(),
quote! { <#struct_generics> },
)
} else {
let fn_generics = generics.clone();
(quote! { }, fn_generics, quote! { #generics })
};
out_tokens.append_all(quote! {
#modifiers
#[doc = #prop_struct_comments]
#[allow(non_camel_case_types)]
#vis struct #struct_name #struct_generics
#where_clause
@ -174,13 +254,15 @@ impl ToTokens for InlinePropsBody {
#vis fn #ident #fn_generics (#cx_token: Scope, props: #struct_name #struct_generics) #output
#where_clause
{
let #struct_name { #(#field_names),* } = props;
::leptos::Component::new(
#component_name_str,
move |#cx_token| #block
)
.into_view(#cx_token)
let #struct_name { #(#field_names),* } = props;
::leptos::Component::new(
#component_name_str,
move |#cx_token| #block
)
.into_view(#cx_token)
}
});
}
}
}