hypertext-macros 0.12.1

A blazing fast type-checked HTML macro crate.
Documentation
use proc_macro2::{Span, TokenStream};
use quote::quote;
use syn::{DeriveInput, Error, spanned::Spanned};

use crate::{
    AttributeValueNode, Context, Document, Maud, Nodes, Rsx,
    html::{self, generate::Generator},
};

#[allow(clippy::needless_pass_by_value)]
pub fn renderable(input: DeriveInput) -> syn::Result<TokenStream> {
    match (renderable_element(&input), attribute_renderable(&input)) {
        (Ok(None), Ok(None)) => Err(Error::new(
            Span::call_site(),
            "expected at least one of `maud`, `rsx`, or `attribute` attributes",
        )),
        (Ok(element), Ok(attribute)) => Ok(quote! {
            #element
            #attribute
        }),
        (Ok(_), Err(e)) | (Err(e), Ok(_)) => Err(e),
        (Err(mut e1), Err(e2)) => {
            e1.combine(e2);
            Err(e1)
        }
    }
}

fn renderable_element(input: &DeriveInput) -> syn::Result<Option<TokenStream>> {
    let mut attrs = input
        .attrs
        .iter()
        .filter_map(|attr| {
            if attr.path().is_ident("maud") {
                Some((
                    attr,
                    html::generate::lazy::<Document<Maud>>
                        as fn(TokenStream, bool) -> syn::Result<TokenStream>,
                ))
            } else if attr.path().is_ident("rsx") {
                Some((
                    attr,
                    html::generate::lazy::<Document<Rsx>>
                        as fn(TokenStream, bool) -> syn::Result<TokenStream>,
                ))
            } else {
                None
            }
        })
        .peekable();

    let (lazy_fn, tokens) = match (attrs.next(), attrs.peek()) {
        (Some((attr, f)), None) => (f, attr.meta.require_list()?.tokens.clone()),
        (Some((attr, _)), Some(_)) => {
            let mut error = Error::new(
                attr.span(),
                "cannot have multiple `maud` or `rsx` attributes",
            );
            for (attr, _) in attrs {
                error.combine(syn::Error::new(
                    attr.span(),
                    "cannot have multiple `maud` or `rsx` attributes",
                ));
            }
            return Err(error);
        }
        (None, _) => {
            return Ok(None);
        }
    };

    let lazy = lazy_fn(tokens, true)?;

    let name = &input.ident;
    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
    let buffer_ident = Generator::buffer_ident();
    let output = quote! {
        #[automatically_derived]
        impl #impl_generics ::hypertext::Renderable for #name #ty_generics #where_clause {
            fn render_to(&self, #buffer_ident: &mut ::hypertext::Buffer) {
                ::hypertext::Renderable::render_to(&#lazy, #buffer_ident);
            }
        }
    };
    Ok(Some(output))
}

fn attribute_renderable(input: &DeriveInput) -> syn::Result<Option<TokenStream>> {
    let mut attrs = input
        .attrs
        .iter()
        .filter(|attr| attr.path().is_ident("attribute"))
        .peekable();

    let tokens = match (attrs.next(), attrs.peek()) {
        (Some(attr), None) => attr.meta.require_list()?.tokens.clone(),
        (Some(_), Some(_)) => {
            let mut error = Error::new(
                Span::call_site(),
                "cannot have multiple `attribute` attributes",
            );
            for attr in attrs {
                error.combine(syn::Error::new(
                    attr.span(),
                    "cannot have multiple `attribute` attributes",
                ));
            }
            return Err(error);
        }
        (None, _) => {
            return Ok(None);
        }
    };

    let lazy = html::generate::lazy::<Nodes<AttributeValueNode>>(tokens, true)?;
    let name = &input.ident;
    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
    let buffer_ident = Generator::buffer_ident();
    let context_marker = Context::AttributeValue.marker_type();
    let output = quote! {
        #[automatically_derived]
        impl #impl_generics ::hypertext::Renderable<#context_marker> for #name #ty_generics
            #where_clause {
            fn render_to(
                &self,
                #buffer_ident: &mut ::hypertext::AttributeBuffer,
            ) {
                ::hypertext::Renderable::render_to(
                    &#lazy,
                    #buffer_ident,
                );
            }
        }
    };

    Ok(Some(output))
}