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 .parse_simple(input)?
22 .into_iter()
23 .map(expand_node)
24 .collect::<Result<Vec<TokenStream>>>()?;
25
26 Ok(quote! {{
27 use ::htmx::native::*;
28 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 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 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}