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),
)
.parse_simple(input)?
.into_iter()
.map(expand_node)
.collect::<Result<Vec<TokenStream>>>()?;
Ok(quote! {{
use ::htmx::native::*;
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),
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::...`")
}),
NodeName::Punctuated(_) => {
todo!("handle data-...")
}
NodeName::Block(_) => {
bail!(
name,
"Only normal identifiers are allowd as attribute names"
)
}
}
}