mod attributes;
mod generator;
mod nodes;
mod parser;
mod utils;
extern crate proc_macro;
use generator::generate_nodes;
use nodes::NodeCollection;
use proc_macro::TokenStream;
use quote::quote;
use syn::{ItemFn, parse_macro_input};
use utils::is_pascal_case;
#[proc_macro]
pub fn html(input: TokenStream) -> TokenStream {
let parsed_html_nodes = parse_macro_input!(input as NodeCollection);
let expanded = generate_nodes(parsed_html_nodes);
TokenStream::from(expanded)
}
#[proc_macro_attribute]
pub fn component(_attr: TokenStream, item: TokenStream) -> TokenStream {
let mut function: ItemFn = syn::parse(item.clone()).unwrap();
let fn_name = &function.sig.ident;
if !is_pascal_case(fn_name) {
return syn::Error::new(
function.sig.ident.span(),
"Component name must be in PascalCase",
)
.to_compile_error()
.into();
}
let vis = &function.vis;
fn type_contains_refs(ty: &syn::Type) -> bool {
match ty {
syn::Type::Reference(_) => true,
syn::Type::Path(type_path) => {
type_path.path.segments.iter().any(|segment| {
if let syn::PathArguments::AngleBracketed(args) = &segment.arguments {
args.args.iter().any(|arg| {
if let syn::GenericArgument::Type(inner_ty) = arg {
type_contains_refs(inner_ty)
} else {
false
}
})
} else {
false
}
})
}
syn::Type::Tuple(type_tuple) => type_tuple.elems.iter().any(type_contains_refs),
syn::Type::Array(type_array) => type_contains_refs(&type_array.elem),
syn::Type::Slice(type_slice) => type_contains_refs(&type_slice.elem),
syn::Type::Paren(type_paren) => type_contains_refs(&type_paren.elem),
syn::Type::Group(type_group) => type_contains_refs(&type_group.elem),
_ => false,
}
}
fn add_lifetime_to_refs(ty: &mut syn::Type, lifetime: &syn::Lifetime) {
match ty {
syn::Type::Reference(type_ref) => {
if type_ref.lifetime.is_none() {
type_ref.lifetime = Some(lifetime.clone());
}
add_lifetime_to_refs(&mut type_ref.elem, lifetime);
}
syn::Type::Path(type_path) => {
for segment in &mut type_path.path.segments {
if let syn::PathArguments::AngleBracketed(args) = &mut segment.arguments {
for arg in &mut args.args {
if let syn::GenericArgument::Type(inner_ty) = arg {
add_lifetime_to_refs(inner_ty, lifetime);
}
}
}
}
}
syn::Type::Tuple(type_tuple) => {
for elem in &mut type_tuple.elems {
add_lifetime_to_refs(elem, lifetime);
}
}
syn::Type::Array(type_array) => add_lifetime_to_refs(&mut type_array.elem, lifetime),
syn::Type::Slice(type_slice) => add_lifetime_to_refs(&mut type_slice.elem, lifetime),
syn::Type::Paren(type_paren) => add_lifetime_to_refs(&mut type_paren.elem, lifetime),
syn::Type::Group(type_group) => add_lifetime_to_refs(&mut type_group.elem, lifetime),
_ => {}
}
}
if function.sig.generics.lifetimes().count() == 0 {
let has_refs = function.sig.inputs.iter().any(|arg| {
if let syn::FnArg::Typed(pat_type) = arg {
type_contains_refs(&pat_type.ty)
} else {
false
}
});
if has_refs {
let lifetime: syn::Lifetime = syn::parse_quote!('a);
let lifetime_param = syn::GenericParam::Lifetime(syn::LifetimeParam {
attrs: vec![],
lifetime: lifetime.clone(),
colon_token: None,
bounds: syn::punctuated::Punctuated::new(),
});
function.sig.generics.params.push(lifetime_param);
for input in &mut function.sig.inputs {
if let syn::FnArg::Typed(pat_type) = input {
add_lifetime_to_refs(&mut pat_type.ty, &lifetime);
}
}
}
}
let props_name = quote::format_ident!("{}Props", fn_name);
let props_builder_name = quote::format_ident!("{}PropsBuilder", fn_name);
let has_slot = function.sig.inputs.first().is_some_and(|arg| {
if let syn::FnArg::Typed(pat_type) = arg {
if let syn::Type::Path(type_path) = &*pat_type.ty {
type_path
.path
.segments
.last()
.is_some_and(|seg| seg.ident == "HtmlFragment")
} else {
false
}
} else {
false
}
});
let params: Vec<_> = function
.sig
.inputs
.iter()
.skip(if has_slot { 1 } else { 0 })
.filter_map(|arg| {
if let syn::FnArg::Typed(pat_type) = arg {
Some(pat_type)
} else {
None
}
})
.collect();
let struct_fields = params.iter().map(|param| {
let pat = ¶m.pat;
let ty = ¶m.ty;
quote! {
#pat: #ty
}
});
let internal_fn_name = quote::format_ident!("__{}", fn_name);
let mut internal_function = function.clone();
internal_function.sig.ident = internal_fn_name.clone();
internal_function.vis = syn::Visibility::Inherited;
let allow_attr: syn::Attribute = syn::parse_quote!(#[allow(non_snake_case)]);
internal_function.attrs.push(allow_attr);
let generics = &internal_function.sig.generics;
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
let param_names: Vec<_> = params.iter().map(|param| ¶m.pat).collect();
let is_no_params = params.is_empty();
let wrapper_fn = if has_slot {
let slot_param = &function.sig.inputs[0];
let slot_param_name = if let syn::FnArg::Typed(pat_type) = slot_param {
if let syn::Pat::Ident(ident) = &*pat_type.pat {
&ident.ident
} else {
panic!("Slot parameter must be a simple identifier")
}
} else {
panic!("Slot parameter must be a typed parameter")
};
quote! {
#[allow(non_snake_case)]
#vis fn #fn_name #impl_generics(#slot_param, props: #props_name #ty_generics) -> hypersynthetic::HtmlFragment #where_clause {
let #props_name { #(#param_names),* } = props;
#internal_fn_name(#slot_param_name, #(#param_names),*)
}
}
} else if is_no_params {
let direct_fn_name = quote::format_ident!("__{}__direct", fn_name);
quote! {
#[allow(non_snake_case)]
#vis fn #fn_name #impl_generics(props: #props_name #ty_generics) -> hypersynthetic::HtmlFragment #where_clause {
let #props_name { #(#param_names),* } = props;
#internal_fn_name(#(#param_names),*)
}
#[allow(non_snake_case)]
#[doc(hidden)]
#vis fn #direct_fn_name #impl_generics() -> hypersynthetic::HtmlFragment #where_clause {
#internal_fn_name()
}
}
} else {
quote! {
#[allow(non_snake_case)]
#vis fn #fn_name #impl_generics(props: #props_name #ty_generics) -> hypersynthetic::HtmlFragment #where_clause {
let #props_name { #(#param_names),* } = props;
#internal_fn_name(#(#param_names),*)
}
}
};
let output = quote! {
#[derive(::hypersynthetic::typed_builder_macro::TypedBuilder)]
#vis struct #props_name #impl_generics #where_clause {
#(#struct_fields,)*
}
impl #impl_generics hypersynthetic::component::Props for #props_name #ty_generics #where_clause {
type Builder = #props_builder_name #ty_generics;
fn builder() -> Self::Builder {
#props_name::builder()
}
}
#internal_function
#wrapper_fn
};
output.into()
}