hypertext-macros 0.12.1

A blazing fast type-checked HTML macro crate.
Documentation
use proc_macro2::TokenStream;
use quote::quote;
use syn::{FnArg, Ident, ItemFn, Pat, PatType, Type, Visibility, parse::Parse};

use crate::html::generate::Generator;

pub struct ComponentArgs {
    visibility: Visibility,
    ident: Option<Ident>,
}

impl Parse for ComponentArgs {
    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
        Ok(Self {
            visibility: input.parse()?,
            ident: if input.peek(Ident) {
                Some(input.parse()?)
            } else {
                None
            },
        })
    }
}

pub fn generate(args: ComponentArgs, fn_item: &ItemFn) -> syn::Result<TokenStream> {
    let mut fields = Vec::new();
    let mut field_names = Vec::new();
    let mut field_refs = Vec::new();

    let vis = if args.visibility == Visibility::Inherited {
        fn_item.vis.clone()
    } else {
        args.visibility
    };

    for input in &fn_item.sig.inputs {
        if let FnArg::Typed(PatType { pat, ty, .. }) = input {
            let ident = match &**pat {
                Pat::Ident(pat_ident) => &pat_ident.ident,
                _ => {
                    return Err(syn::Error::new_spanned(
                        pat,
                        "component function parameters must be identifiers",
                    ));
                }
            };
            let (ty, ref_token) = match &**ty {
                Type::Reference(ty_ref) => {
                    if ty_ref.mutability.is_some() {
                        return Err(syn::Error::new_spanned(
                            ty_ref,
                            "component function parameters cannot be mutable references",
                        ));
                    }

                    if ty_ref.lifetime.is_some() {
                        (ty, None)
                    } else {
                        (&ty_ref.elem, Some(ty_ref.and_token))
                    }
                }
                _ => (ty, None),
            };
            fields.push(quote! {
                #vis #ident: #ty
            });
            field_names.push(ident.clone());
            field_refs.push(ref_token);
        } else {
            return Err(syn::Error::new_spanned(
                input,
                "component function parameters do not support `self` or `&self`",
            ));
        }
    }

    let fn_name = &fn_item.sig.ident;

    let struct_name = args
        .ident
        .unwrap_or_else(|| Ident::new(&to_pascal_case(&fn_name.to_string()), fn_name.span()));

    let (impl_generics, ty_generics, where_clause) = fn_item.sig.generics.split_for_impl();

    let struct_body = if fields.is_empty() {
        quote!(;)
    } else {
        quote! {
            { #(#fields),* }
        }
    };

    let buffer_ident = Generator::buffer_ident();

    let output = quote! {
        #[allow(clippy::needless_lifetimes)]
        #fn_item

        #vis struct #struct_name #ty_generics #struct_body

        #[automatically_derived]
        impl #impl_generics ::hypertext::Renderable for #struct_name #ty_generics #where_clause {
            fn render_to(&self, #buffer_ident: &mut ::hypertext::Buffer) {
                ::hypertext::Renderable::render_to(
                    &#fn_name(#(
                        #field_refs self.#field_names
                    ),*),
                    #buffer_ident,
                );
            }
        }
    };

    Ok(output)
}

fn to_pascal_case(s: &str) -> String {
    let mut result = String::new();
    let mut capitalize_next = true;

    for c in s.chars() {
        if c == '_' {
            capitalize_next = true;
        } else if capitalize_next {
            result.push(c.to_ascii_uppercase());
            capitalize_next = false;
        } else {
            result.push(c);
        }
    }

    result
}