workers-rsx-impl 0.1.0

Proc macros for workers-rsx
Documentation
use crate::children::Children;
use crate::element_attribute::{parse_attr_value, AttributeKey, ElementAttribute};
use proc_macro_error::emit_error;
use quote::{quote, ToTokens};
use std::collections::HashSet;
use syn::ext::IdentExt;
use syn::parse::{Parse, ParseStream, Result};
use syn::spanned::Spanned;

pub type Attributes = HashSet<ElementAttribute>;

#[derive(Default)]
pub struct ElementAttributes {
    pub attributes: Attributes,
}

impl ElementAttributes {
    pub fn new(attributes: Attributes) -> Self {
        Self { attributes }
    }

    pub fn for_custom_element<'c>(
        &self,
        children: &'c Children,
    ) -> CustomElementAttributes<'_, 'c> {
        CustomElementAttributes {
            attributes: &self.attributes,
            children,
        }
    }

    pub fn for_simple_element(&self) -> SimpleElementAttributes<'_> {
        SimpleElementAttributes {
            attributes: &self.attributes,
        }
    }

    pub fn parse(input: ParseStream, is_custom_element: bool) -> Result<Self> {
        let mut parsed_self = Self::parse_with_context(input, is_custom_element)?;

        let new_attributes: Attributes = parsed_self
            .attributes
            .drain()
            .filter_map(|attribute| match attribute.validate(is_custom_element) {
                Ok(x) => Some(x),
                Err(err) => {
                    emit_error!(err.span(), "Invalid attribute: {}", err);
                    None
                }
            })
            .collect();

        Ok(ElementAttributes::new(new_attributes))
    }

    fn parse_with_context(input: ParseStream, is_custom_element: bool) -> Result<Self> {
        let mut attributes: HashSet<ElementAttribute> = HashSet::new();
        while input.peek(syn::Ident::peek_any) || input.peek(syn::token::Brace) {
            if input.peek(syn::token::Brace) {
                // Prop shorthand: {name} → name={name}
                let content;
                syn::braced!(content in input);
                let ident = syn::Ident::parse_any(&content)?;
                let mut key = AttributeKey::new();
                key.push_value(ident);
                let attribute = ElementAttribute::Punned(key);
                let ident = attribute.ident();
                if attributes.contains(&attribute) {
                    emit_error!(
                        ident.span(),
                        "There is a previous definition of the {} attribute",
                        quote!(#ident)
                    );
                }
                attributes.insert(attribute);
            } else {
                let name =
                    AttributeKey::parse_separated_nonempty_with(input, syn::Ident::parse_any)?;
                let has_value = input.peek(syn::Token![=]);

                if has_value {
                    input.parse::<syn::Token![=]>()?;
                    let value = parse_attr_value(input)?;
                    let attribute = ElementAttribute::WithValue(name, value);
                    let ident = attribute.ident();
                    if attributes.contains(&attribute) {
                        emit_error!(
                            ident.span(),
                            "There is a previous definition of the {} attribute",
                            quote!(#ident)
                        );
                    }
                    attributes.insert(attribute);
                } else if is_custom_element {
                    // On custom elements, bare ident is punning: name → name={name}
                    let attribute = ElementAttribute::Punned(name);
                    let ident = attribute.ident();
                    if attributes.contains(&attribute) {
                        emit_error!(
                            ident.span(),
                            "There is a previous definition of the {} attribute",
                            quote!(#ident)
                        );
                    }
                    attributes.insert(attribute);
                } else {
                    // On simple HTML elements, bare ident is a boolean attribute: disabled → disabled
                    let attribute = ElementAttribute::BooleanAttribute(name);
                    let ident = attribute.ident();
                    if attributes.contains(&attribute) {
                        emit_error!(
                            ident.span(),
                            "There is a previous definition of the {} attribute",
                            quote!(#ident)
                        );
                    }
                    attributes.insert(attribute);
                }
            }
        }
        Ok(ElementAttributes::new(attributes))
    }
}

impl Parse for ElementAttributes {
    fn parse(input: ParseStream) -> Result<Self> {
        let mut attributes: HashSet<ElementAttribute> = HashSet::new();
        while input.peek(syn::Ident::peek_any) || input.peek(syn::token::Brace) {
            if input.peek(syn::token::Brace) {
                // Prop shorthand: {name} → name={name}
                let content;
                syn::braced!(content in input);
                let ident = syn::Ident::parse_any(&content)?;
                let mut key = AttributeKey::new();
                key.push_value(ident);
                let attribute = ElementAttribute::Punned(key);
                let ident = attribute.ident();
                if attributes.contains(&attribute) {
                    emit_error!(
                        ident.span(),
                        "There is a previous definition of the {} attribute",
                        quote!(#ident)
                    );
                }
                attributes.insert(attribute);
            } else {
                let attribute = input.parse::<ElementAttribute>()?;
                let ident = attribute.ident();
                if attributes.contains(&attribute) {
                    emit_error!(
                        ident.span(),
                        "There is a previous definition of the {} attribute",
                        quote!(#ident)
                    );
                }
                attributes.insert(attribute);
            }
        }
        Ok(ElementAttributes::new(attributes))
    }
}

pub struct CustomElementAttributes<'a, 'c> {
    attributes: &'a Attributes,
    children: &'c Children,
}

impl<'a, 'c> ToTokens for CustomElementAttributes<'a, 'c> {
    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
        let mut attrs: Vec<_> = self
            .attributes
            .iter()
            .map(|attribute| {
                let ident = attribute.ident();
                let value = attribute.value_tokens();

                quote! {
                    #ident: #value
                }
            })
            .collect();

        if self.children.len() > 0 {
            let children_tuple = self.children.as_option_of_tuples_tokens();
            attrs.push(quote! {
                children: #children_tuple
            });
        }

        let quoted = if attrs.len() == 0 {
            quote!()
        } else {
            quote!({ #(#attrs),* })
        };

        quoted.to_tokens(tokens);
    }
}

pub struct SimpleElementAttributes<'a> {
    attributes: &'a Attributes,
}

impl<'a> ToTokens for SimpleElementAttributes<'a> {
    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
        if self.attributes.is_empty() {
            quote!(None).to_tokens(tokens);
        } else {
            let attrs: Vec<_> = self
                .attributes
                .iter()
                .map(|attribute| {
                    let mut iter = attribute.ident().iter();
                    let first_word = iter.next().unwrap().unraw();
                    let ident = iter.fold(first_word.to_string(), |acc, curr| {
                        format!("{}-{}", acc, curr.unraw())
                    });

                    if attribute.is_boolean() {
                        quote! {
                            hm.insert(#ident, workers_rsx::simple_element::AttrValue::Boolean);
                        }
                    } else {
                        let value = attribute.value_tokens();
                        quote! {
                            hm.insert(#ident, workers_rsx::simple_element::AttrValue::Value(::std::borrow::Cow::from(#value)));
                        }
                    }
                })
                .collect();

            let hashmap_declaration = quote! {{
                let mut hm = std::collections::HashMap::<&str, workers_rsx::simple_element::AttrValue<'_>>::new();
                #(#attrs)*
                Some(hm)
            }};

            hashmap_declaration.to_tokens(tokens);
        }
    }
}