Heavily document component macro
This commit is contained in:
1 file changed
+164
-135
@@ -4,10 +4,6 @@ use syn::parse::{Parse, ParseStream};
|
|||||||
use syn::spanned::Spanned;
|
use syn::spanned::Spanned;
|
||||||
use syn::*;
|
use syn::*;
|
||||||
|
|
||||||
/// General struct for parsing a component body.
|
|
||||||
/// However, because it's ambiguous, it does not implement [`ToTokens`](quote::to_tokens::ToTokens).
|
|
||||||
///
|
|
||||||
/// Refer to the [module documentation](crate::component_body) for more.
|
|
||||||
pub struct ComponentBody {
|
pub struct ComponentBody {
|
||||||
pub item_fn: ItemFn,
|
pub item_fn: ItemFn,
|
||||||
}
|
}
|
||||||
@@ -24,8 +20,14 @@ impl ToTokens for ComponentBody {
|
|||||||
fn to_tokens(&self, tokens: &mut TokenStream) {
|
fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||||
let comp_fn = self.comp_fn();
|
let comp_fn = self.comp_fn();
|
||||||
|
|
||||||
|
// If there's no props declared, we simply omit the props argument
|
||||||
|
// This is basically so you can annotate the App component with #[component] and still be compatible with the
|
||||||
|
// launch signatures that take fn() -> Element
|
||||||
let props_struct = match self.item_fn.sig.inputs.is_empty() {
|
let props_struct = match self.item_fn.sig.inputs.is_empty() {
|
||||||
|
// No props declared, so we don't need to generate a props struct
|
||||||
true => quote! {},
|
true => quote! {},
|
||||||
|
|
||||||
|
// Props declared, so we generate a props struct and thatn also attach the doc attributes to it
|
||||||
false => {
|
false => {
|
||||||
let doc = format!("Properties for the [`{}`] component.", &comp_fn.sig.ident);
|
let doc = format!("Properties for the [`{}`] component.", &comp_fn.sig.ident);
|
||||||
let props_struct = self.props_struct();
|
let props_struct = self.props_struct();
|
||||||
@@ -64,19 +66,22 @@ impl ComponentBody {
|
|||||||
..
|
..
|
||||||
} = sig;
|
} = sig;
|
||||||
let Generics { where_clause, .. } = generics;
|
let Generics { where_clause, .. } = generics;
|
||||||
|
let (_, ty_generics, _) = generics.split_for_impl();
|
||||||
|
|
||||||
// We generate a struct with the same name as the component but called `Props`
|
// We generate a struct with the same name as the component but called `Props`
|
||||||
let struct_ident = Ident::new(&format!("{fn_ident}Props"), fn_ident.span());
|
let struct_ident = Ident::new(&format!("{fn_ident}Props"), fn_ident.span());
|
||||||
|
|
||||||
let struct_field_names = inputs.iter().filter_map(strip_mutability);
|
// We pull in the field names from the original function signature, but need to strip off the mutability
|
||||||
let (_, ty_generics, _) = generics.split_for_impl();
|
let struct_field_names = inputs.iter().filter_map(rebind_mutability);
|
||||||
|
|
||||||
let props_docs = self.props_docs(inputs.iter().skip(1).collect());
|
let props_docs = self.props_docs(inputs.iter().skip(1).collect());
|
||||||
|
|
||||||
|
// Don't generate the props argument if there are no inputs
|
||||||
|
// This means we need to skip adding the argument to the function signature, and also skip the expanded struct
|
||||||
let props_ident = match inputs.is_empty() {
|
let props_ident = match inputs.is_empty() {
|
||||||
true => quote! {},
|
true => quote! {},
|
||||||
false => quote! { mut __props: #struct_ident #ty_generics },
|
false => quote! { mut __props: #struct_ident #ty_generics },
|
||||||
};
|
};
|
||||||
|
|
||||||
let expanded_struct = match inputs.is_empty() {
|
let expanded_struct = match inputs.is_empty() {
|
||||||
true => quote! {},
|
true => quote! {},
|
||||||
false => quote! { let #struct_ident { #(#struct_field_names),* } = __props; },
|
false => quote! { let #struct_ident { #(#struct_field_names),* } = __props; },
|
||||||
@@ -92,10 +97,16 @@ impl ComponentBody {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build the props struct
|
/// Build an associated struct for the props of the component
|
||||||
|
///
|
||||||
|
/// This will expand to the typed-builder implementation that we have vendored in this crate.
|
||||||
|
/// TODO: don't vendor typed-builder and instead transform the tokens we give it before expansion.
|
||||||
|
/// TODO: cache these tokens since this codegen is rather expensive (lots of tokens)
|
||||||
|
///
|
||||||
|
/// We try our best to transfer over any declared doc attributes from the original function signature onto the
|
||||||
|
/// props struct fields.
|
||||||
fn props_struct(&self) -> ItemStruct {
|
fn props_struct(&self) -> ItemStruct {
|
||||||
let ComponentBody { item_fn, .. } = &self;
|
let ItemFn { vis, sig, .. } = &self.item_fn;
|
||||||
let ItemFn { vis, sig, .. } = item_fn;
|
|
||||||
let Signature {
|
let Signature {
|
||||||
inputs,
|
inputs,
|
||||||
ident,
|
ident,
|
||||||
@@ -103,17 +114,21 @@ impl ComponentBody {
|
|||||||
..
|
..
|
||||||
} = sig;
|
} = sig;
|
||||||
|
|
||||||
let struct_fields = inputs.iter().map(move |f| make_prop_struct_fields(f, vis));
|
let struct_fields = inputs.iter().map(move |f| make_prop_struct_field(f, vis));
|
||||||
let struct_ident = Ident::new(&format!("{ident}Props"), ident.span());
|
let struct_ident = Ident::new(&format!("{ident}Props"), ident.span());
|
||||||
|
|
||||||
parse_quote! {
|
parse_quote! {
|
||||||
#[derive(Props, Clone, PartialEq)]
|
#[derive(Props, Clone, PartialEq)]
|
||||||
#[allow(non_camel_case_types)]
|
#[allow(non_camel_case_types)]
|
||||||
#vis struct #struct_ident #generics
|
#vis struct #struct_ident #generics {
|
||||||
{ #(#struct_fields),* }
|
#(#struct_fields),*
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Convert a list of function arguments into a list of doc attributes for the props struct
|
||||||
|
///
|
||||||
|
/// This lets us generate set of attributes that we can apply to the props struct to give it a nice docstring.
|
||||||
fn props_docs(&self, inputs: Vec<&FnArg>) -> Vec<Attribute> {
|
fn props_docs(&self, inputs: Vec<&FnArg>) -> Vec<Attribute> {
|
||||||
let fn_ident = &self.item_fn.sig.ident;
|
let fn_ident = &self.item_fn.sig.ident;
|
||||||
|
|
||||||
@@ -123,58 +138,7 @@ impl ComponentBody {
|
|||||||
|
|
||||||
let arg_docs = inputs
|
let arg_docs = inputs
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|f| match f {
|
.filter_map(|f| build_doc_fields(f))
|
||||||
FnArg::Receiver(_) => unreachable!(), // ComponentBody prohibits receiver parameters.
|
|
||||||
FnArg::Typed(pt) => {
|
|
||||||
let arg_doc = pt
|
|
||||||
.attrs
|
|
||||||
.iter()
|
|
||||||
.filter_map(|attr| {
|
|
||||||
// TODO: Error reporting
|
|
||||||
// Check if the path of the attribute is "doc"
|
|
||||||
if !is_attr_doc(attr) {
|
|
||||||
return None;
|
|
||||||
};
|
|
||||||
|
|
||||||
let Meta::NameValue(meta_name_value) = &attr.meta else {
|
|
||||||
return None;
|
|
||||||
};
|
|
||||||
|
|
||||||
let Expr::Lit(doc_lit) = &meta_name_value.value else {
|
|
||||||
return None;
|
|
||||||
};
|
|
||||||
|
|
||||||
let Lit::Str(doc_lit_str) = &doc_lit.lit else {
|
|
||||||
return None;
|
|
||||||
};
|
|
||||||
|
|
||||||
Some(doc_lit_str.value())
|
|
||||||
})
|
|
||||||
.fold(String::new(), |mut doc, next_doc_line| {
|
|
||||||
doc.push('\n');
|
|
||||||
doc.push_str(&next_doc_line);
|
|
||||||
doc
|
|
||||||
});
|
|
||||||
|
|
||||||
Some((
|
|
||||||
&pt.pat,
|
|
||||||
&pt.ty,
|
|
||||||
pt.attrs.iter().find_map(|attr| {
|
|
||||||
if attr.path() != &parse_quote!(deprecated) {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let res = crate::utils::DeprecatedAttribute::from_meta(&attr.meta);
|
|
||||||
|
|
||||||
match res {
|
|
||||||
Err(e) => panic!("{}", e.to_string()),
|
|
||||||
Ok(v) => Some(v),
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
arg_doc,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
let mut props_docs = Vec::with_capacity(5);
|
let mut props_docs = Vec::with_capacity(5);
|
||||||
@@ -186,7 +150,14 @@ impl ComponentBody {
|
|||||||
#[doc = #header]
|
#[doc = #header]
|
||||||
});
|
});
|
||||||
|
|
||||||
for (arg_name, arg_type, deprecation, input_arg_doc) in arg_docs {
|
for arg in arg_docs {
|
||||||
|
let DocField {
|
||||||
|
arg_name,
|
||||||
|
arg_type,
|
||||||
|
deprecation,
|
||||||
|
input_arg_doc,
|
||||||
|
} = arg;
|
||||||
|
|
||||||
let arg_name = arg_name.into_token_stream().to_string();
|
let arg_name = arg_name.into_token_stream().to_string();
|
||||||
let arg_type = crate::utils::format_type_string(arg_type);
|
let arg_type = crate::utils::format_type_string(arg_type);
|
||||||
|
|
||||||
@@ -220,91 +191,70 @@ impl ComponentBody {
|
|||||||
arg_doc.push_str(&format!("<p>{input_arg_doc}</p>"));
|
arg_doc.push_str(&format!("<p>{input_arg_doc}</p>"));
|
||||||
}
|
}
|
||||||
|
|
||||||
props_docs.push(parse_quote! {
|
props_docs.push(parse_quote! { #[doc = #arg_doc] });
|
||||||
#[doc = #arg_doc]
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
props_docs
|
props_docs
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn make_prop_struct_fields(f: &FnArg, vis: &Visibility) -> TokenStream {
|
struct DocField<'a> {
|
||||||
match f {
|
arg_name: &'a Box<Pat>,
|
||||||
FnArg::Receiver(_) => unreachable!(), // Unreachable because of ComponentBody parsing
|
arg_type: &'a Box<Type>,
|
||||||
FnArg::Typed(pt) => {
|
deprecation: Option<crate::utils::DeprecatedAttribute>,
|
||||||
let arg_pat = match pt.pat.as_ref() {
|
input_arg_doc: String,
|
||||||
// rip off mutability
|
}
|
||||||
Pat::Ident(f) => {
|
|
||||||
let mut f = f.clone();
|
fn build_doc_fields(f: &FnArg) -> Option<DocField> {
|
||||||
f.mutability = None;
|
let FnArg::Typed(pt) = f else { unreachable!() };
|
||||||
quote! { #f }
|
|
||||||
}
|
let arg_doc = pt
|
||||||
a => quote! { #a },
|
.attrs
|
||||||
|
.iter()
|
||||||
|
.filter_map(|attr| {
|
||||||
|
// TODO: Error reporting
|
||||||
|
// Check if the path of the attribute is "doc"
|
||||||
|
if !is_attr_doc(attr) {
|
||||||
|
return None;
|
||||||
};
|
};
|
||||||
|
|
||||||
let arg_colon = &pt.colon_token;
|
let Meta::NameValue(meta_name_value) = &attr.meta else {
|
||||||
let arg_ty = &pt.ty; // Type
|
return None;
|
||||||
let arg_attrs = &pt.attrs; // Attributes
|
};
|
||||||
|
|
||||||
quote! {
|
let Expr::Lit(doc_lit) = &meta_name_value.value else {
|
||||||
#(#arg_attrs)
|
return None;
|
||||||
*
|
};
|
||||||
#vis #arg_pat #arg_colon #arg_ty
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn strip_mutability(f: &FnArg) -> Option<TokenStream> {
|
let Lit::Str(doc_lit_str) = &doc_lit.lit else {
|
||||||
match f {
|
return None;
|
||||||
FnArg::Receiver(_) => unreachable!(), // ComponentBody prohibits receiver parameters.
|
};
|
||||||
FnArg::Typed(pt) => {
|
|
||||||
let pat = &pt.pat;
|
|
||||||
|
|
||||||
let mut pat = pat.clone();
|
Some(doc_lit_str.value())
|
||||||
|
})
|
||||||
|
.fold(String::new(), |mut doc, next_doc_line| {
|
||||||
|
doc.push('\n');
|
||||||
|
doc.push_str(&next_doc_line);
|
||||||
|
doc
|
||||||
|
});
|
||||||
|
|
||||||
// rip off mutability, but still write it out eventually
|
Some(DocField {
|
||||||
if let Pat::Ident(ref mut pat_ident) = pat.as_mut() {
|
arg_name: &pt.pat,
|
||||||
pat_ident.mutability = None;
|
arg_type: &pt.ty,
|
||||||
|
deprecation: pt.attrs.iter().find_map(|attr| {
|
||||||
|
if attr.path() != &parse_quote!(deprecated) {
|
||||||
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
Some(quote!(mut #pat))
|
let res = crate::utils::DeprecatedAttribute::from_meta(&attr.meta);
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Checks if the attribute is a `#[doc]` attribute.
|
match res {
|
||||||
fn is_attr_doc(attr: &Attribute) -> bool {
|
Err(e) => panic!("{}", e.to_string()),
|
||||||
attr.path() == &parse_quote!(doc)
|
Ok(v) => Some(v),
|
||||||
}
|
|
||||||
|
|
||||||
fn keep_up_to_n_consecutive_chars(
|
|
||||||
input: &str,
|
|
||||||
n_of_consecutive_chars_allowed: usize,
|
|
||||||
target_char: char,
|
|
||||||
) -> String {
|
|
||||||
let mut output = String::new();
|
|
||||||
let mut prev_char: Option<char> = None;
|
|
||||||
let mut consecutive_count = 0;
|
|
||||||
|
|
||||||
for c in input.chars() {
|
|
||||||
match prev_char {
|
|
||||||
Some(prev) if c == target_char && prev == target_char => {
|
|
||||||
if consecutive_count < n_of_consecutive_chars_allowed {
|
|
||||||
output.push(c);
|
|
||||||
consecutive_count += 1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
_ => {
|
}),
|
||||||
output.push(c);
|
input_arg_doc: arg_doc,
|
||||||
prev_char = Some(c);
|
})
|
||||||
consecutive_count = 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
output
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validate_component_fn_signature(item_fn: &ItemFn) -> Result<()> {
|
fn validate_component_fn_signature(item_fn: &ItemFn) -> Result<()> {
|
||||||
@@ -356,3 +306,82 @@ fn validate_component_fn_signature(item_fn: &ItemFn) -> Result<()> {
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Convert a function arg with a given visibility (provided by the function) and then generate a field for the
|
||||||
|
/// associated props struct.
|
||||||
|
fn make_prop_struct_field(f: &FnArg, vis: &Visibility) -> TokenStream {
|
||||||
|
// There's no receivers (&self) allowed in the component body
|
||||||
|
let FnArg::Typed(pt) = f else { unreachable!() };
|
||||||
|
|
||||||
|
let arg_pat = match pt.pat.as_ref() {
|
||||||
|
// rip off mutability
|
||||||
|
// todo: we actually don't want any of the extra bits of the field pattern
|
||||||
|
Pat::Ident(f) => {
|
||||||
|
let mut f = f.clone();
|
||||||
|
f.mutability = None;
|
||||||
|
quote! { #f }
|
||||||
|
}
|
||||||
|
a => quote! { #a },
|
||||||
|
};
|
||||||
|
|
||||||
|
let PatType {
|
||||||
|
attrs,
|
||||||
|
ty,
|
||||||
|
colon_token,
|
||||||
|
..
|
||||||
|
} = pt;
|
||||||
|
|
||||||
|
quote! {
|
||||||
|
#(#attrs)*
|
||||||
|
#vis #arg_pat #colon_token #ty
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rebind_mutability(f: &FnArg) -> Option<TokenStream> {
|
||||||
|
// There's no receivers (&self) allowed in the component body
|
||||||
|
let FnArg::Typed(pt) = f else { unreachable!() };
|
||||||
|
|
||||||
|
let pat = &pt.pat;
|
||||||
|
|
||||||
|
let mut pat = pat.clone();
|
||||||
|
|
||||||
|
// rip off mutability, but still write it out eventually
|
||||||
|
if let Pat::Ident(ref mut pat_ident) = pat.as_mut() {
|
||||||
|
pat_ident.mutability = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(quote!(mut #pat))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks if the attribute is a `#[doc]` attribute.
|
||||||
|
fn is_attr_doc(attr: &Attribute) -> bool {
|
||||||
|
attr.path() == &parse_quote!(doc)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn keep_up_to_n_consecutive_chars(
|
||||||
|
input: &str,
|
||||||
|
n_of_consecutive_chars_allowed: usize,
|
||||||
|
target_char: char,
|
||||||
|
) -> String {
|
||||||
|
let mut output = String::new();
|
||||||
|
let mut prev_char: Option<char> = None;
|
||||||
|
let mut consecutive_count = 0;
|
||||||
|
|
||||||
|
for c in input.chars() {
|
||||||
|
match prev_char {
|
||||||
|
Some(prev) if c == target_char && prev == target_char => {
|
||||||
|
if consecutive_count < n_of_consecutive_chars_allowed {
|
||||||
|
output.push(c);
|
||||||
|
consecutive_count += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
output.push(c);
|
||||||
|
prev_char = Some(c);
|
||||||
|
consecutive_count = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
output
|
||||||
|
}
|
||||||
Reference in new issue
Block a user