htmx-macros 0.1.0

macros for htmx
Documentation
use manyhow::{bail, manyhow, Result};
use proc_macro2::TokenStream;
use quote::ToTokens;
use quote_use::quote_use as quote;
use rstml::atoms::OpenTag;
use rstml::node::{
    AttributeValueExpr, KeyedAttribute, KeyedAttributeValue, Node, NodeAttribute, NodeElement,
    NodeName,
};
use syn::ExprPath;

#[manyhow]
#[proc_macro]
pub fn htmx(input: TokenStream) -> Result {
    let nodes = rstml::Parser::new(
        rstml::ParserConfig::new()
            .recover_block(true)
            .element_close_use_default_wildcard_ident(false),
    )
    // TODO parse_recoverable
    .parse_simple(input)?
    .into_iter()
    .map(expand_node)
    .collect::<Result<Vec<TokenStream>>>()?;

    Ok(quote! {{
        use ::htmx::native::*;
        // use ::std::fmt::Write as _;
        // let mut $out = String::new();
        vec![#(#nodes),*]
    }})
}

fn expand_node(node: Node) -> Result {
    match node {
        Node::Comment(_) => todo!(),
        Node::Doctype(_) => todo!(),
        Node::Fragment(_) => todo!(),
        Node::Element(NodeElement {
            open_tag: OpenTag {
                name, attributes, ..
            },
            children,
            ..
        }) => {
            let name = name_to_struct(name)?;
            let attributes = attributes
                .into_iter()
                .map(|attribute| match attribute {
                    NodeAttribute::Block(_) => {
                        bail!(attribute, "dynamic attribute names not supported")
                    }
                    NodeAttribute::Attribute(KeyedAttribute {
                        key,
                        possible_value,
                    }) => match possible_value {
                        KeyedAttributeValue::Binding(_) => todo!(),
                        KeyedAttributeValue::Value(AttributeValueExpr { value, .. }) => {
                            attribute_key_to_fn(key, value)
                        }
                        KeyedAttributeValue::None => attribute_key_to_fn(key, true),
                    },
                })
                .collect::<Result<Vec<_>>>()?;
            let children = children
                .into_iter()
                .map(expand_node)
                .collect::<Result<Vec<_>>>()?;
            Ok(quote!(#name::builder() #(.#attributes)* #(.push(#children))*.build()))
        }
        Node::Block(_) => todo!(),
        Node::Text(_) => todo!(),
        Node::RawText(_) => todo!(),
    }
}

fn name_to_struct(name: NodeName) -> Result<ExprPath> {
    match name {
        NodeName::Path(path) => Ok(path),
        // This {...}
        NodeName::Punctuated(_) | NodeName::Block(_) => {
            bail!(name, "Only normal identifiers are allowd as node names")
        }
    }
}

fn attribute_key_to_fn(name: NodeName, value: impl ToTokens) -> Result {
    match name {
        NodeName::Path(ExprPath { path, .. }) => Ok(if let Some(ident) = path.get_ident() {
            let sident = ident.to_string();
            if let Some(sident) = sident.strip_prefix("data_") {
                quote!(data(#sident, #value))
            } else if sident.starts_with("hz_") {
                quote!(data(#sident, #value))
            } else {
                quote!(#ident(#value))
            }
        } else {
            todo!("handle `data::...` or `hz::...`")
        }),
        // This {...}
        NodeName::Punctuated(_) => {
            todo!("handle data-...")
        }
        NodeName::Block(_) => {
            bail!(
                name,
                "Only normal identifiers are allowd as attribute names"
            )
        }
    }
}