htmx_macros/
lib.rs

1use manyhow::{bail, manyhow, Result};
2use proc_macro2::TokenStream;
3use quote::ToTokens;
4use quote_use::quote_use as quote;
5use rstml::atoms::OpenTag;
6use rstml::node::{
7    AttributeValueExpr, KeyedAttribute, KeyedAttributeValue, Node, NodeAttribute, NodeElement,
8    NodeName,
9};
10use syn::ExprPath;
11
12#[manyhow]
13#[proc_macro]
14pub fn htmx(input: TokenStream) -> Result {
15    let nodes = rstml::Parser::new(
16        rstml::ParserConfig::new()
17            .recover_block(true)
18            .element_close_use_default_wildcard_ident(false),
19    )
20    // TODO parse_recoverable
21    .parse_simple(input)?
22    .into_iter()
23    .map(expand_node)
24    .collect::<Result<Vec<TokenStream>>>()?;
25
26    Ok(quote! {{
27        use ::htmx::native::*;
28        // use ::std::fmt::Write as _;
29        // let mut $out = String::new();
30        vec![#(#nodes),*]
31    }})
32}
33
34fn expand_node(node: Node) -> Result {
35    match node {
36        Node::Comment(_) => todo!(),
37        Node::Doctype(_) => todo!(),
38        Node::Fragment(_) => todo!(),
39        Node::Element(NodeElement {
40            open_tag: OpenTag {
41                name, attributes, ..
42            },
43            children,
44            ..
45        }) => {
46            let name = name_to_struct(name)?;
47            let attributes = attributes
48                .into_iter()
49                .map(|attribute| match attribute {
50                    NodeAttribute::Block(_) => {
51                        bail!(attribute, "dynamic attribute names not supported")
52                    }
53                    NodeAttribute::Attribute(KeyedAttribute {
54                        key,
55                        possible_value,
56                    }) => match possible_value {
57                        KeyedAttributeValue::Binding(_) => todo!(),
58                        KeyedAttributeValue::Value(AttributeValueExpr { value, .. }) => {
59                            attribute_key_to_fn(key, value)
60                        }
61                        KeyedAttributeValue::None => attribute_key_to_fn(key, true),
62                    },
63                })
64                .collect::<Result<Vec<_>>>()?;
65            let children = children
66                .into_iter()
67                .map(expand_node)
68                .collect::<Result<Vec<_>>>()?;
69            Ok(quote!(#name::builder() #(.#attributes)* #(.push(#children))*.build()))
70        }
71        Node::Block(_) => todo!(),
72        Node::Text(_) => todo!(),
73        Node::RawText(_) => todo!(),
74    }
75}
76
77fn name_to_struct(name: NodeName) -> Result<ExprPath> {
78    match name {
79        NodeName::Path(path) => Ok(path),
80        // This {...}
81        NodeName::Punctuated(_) | NodeName::Block(_) => {
82            bail!(name, "Only normal identifiers are allowd as node names")
83        }
84    }
85}
86
87fn attribute_key_to_fn(name: NodeName, value: impl ToTokens) -> Result {
88    match name {
89        NodeName::Path(ExprPath { path, .. }) => Ok(if let Some(ident) = path.get_ident() {
90            let sident = ident.to_string();
91            if let Some(sident) = sident.strip_prefix("data_") {
92                quote!(data(#sident, #value))
93            } else if sident.starts_with("hz_") {
94                quote!(data(#sident, #value))
95            } else {
96                quote!(#ident(#value))
97            }
98        } else {
99            todo!("handle `data::...` or `hz::...`")
100        }),
101        // This {...}
102        NodeName::Punctuated(_) => {
103            todo!("handle data-...")
104        }
105        NodeName::Block(_) => {
106            bail!(
107                name,
108                "Only normal identifiers are allowd as attribute names"
109            )
110        }
111    }
112}