use proc_macro::TokenStream;
use proc_macro2::{Literal, TokenStream as TokenStream2};
use quote::quote;
mod parser;
use parser::{HtmlElement, HtmlNode, parse_html};
#[proc_macro]
pub fn hers(input: TokenStream) -> TokenStream {
let input = TokenStream2::from(input);
match parse_html(input) {
Ok(nodes) => {
let code = generate_code(&nodes);
TokenStream::from(quote! {
{
use ::hers::{HtmlString, ToHtml};
#code
}
})
}
Err(err) => TokenStream::from(err.to_compile_error()),
}
}
fn generate_code(nodes: &[HtmlNode]) -> TokenStream2 {
let mut parts = Vec::new();
for node in nodes {
match node {
HtmlNode::Element(element) => {
let element_code = generate_element_code(element);
parts.push(element_code);
}
HtmlNode::Text(text) => {
let text_literal = Literal::string(text);
parts.push(quote! { #text_literal.to_string() });
}
HtmlNode::Variable(expr) => {
parts.push(quote! { (#expr).to_html().into_string() });
}
HtmlNode::If(if_node) => {
let condition = &if_node.condition;
let then_code = generate_code(&if_node.then_branch);
let else_code = if let Some(else_branch) = &if_node.else_branch {
let else_gen = generate_code(else_branch);
quote! { #else_gen.into_string() }
} else {
quote! { String::new() }
};
parts.push(quote! {
if #condition {
#then_code.into_string()
} else {
#else_code
}
});
}
HtmlNode::For(for_node) => {
let pattern = &for_node.pattern;
let iterable = &for_node.iterable;
let body_code = generate_code(&for_node.body);
parts.push(quote! {
{
let mut result = String::new();
for #pattern in #iterable {
result.push_str(&#body_code.into_string());
}
result
}
});
}
}
}
if parts.is_empty() {
quote! { HtmlString::new(String::new()) }
} else if parts.len() == 1 {
let part = &parts[0];
quote! { HtmlString::new(#part) }
} else {
quote! {
HtmlString::new(format!("{}", [#(#parts),*].join("")))
}
}
}
fn generate_element_code(element: &HtmlElement) -> TokenStream2 {
let tag = &element.tag;
let children_code = if element.children.is_empty() {
quote! { String::new() }
} else {
let child_parts: Vec<_> = element
.children
.iter()
.map(|child| match child {
HtmlNode::Element(child_element) => generate_element_code(child_element),
HtmlNode::Text(text) => {
let text_literal = Literal::string(text);
quote! { #text_literal.to_string() }
}
HtmlNode::Variable(expr) => {
quote! { (#expr).to_html().into_string() }
}
HtmlNode::If(if_node) => {
let condition = &if_node.condition;
let then_code = generate_code(&if_node.then_branch);
let else_code = if let Some(else_branch) = &if_node.else_branch {
let else_gen = generate_code(else_branch);
quote! { #else_gen.into_string() }
} else {
quote! { String::new() }
};
quote! {
if #condition {
#then_code.into_string()
} else {
#else_code
}
}
}
HtmlNode::For(for_node) => {
let pattern = &for_node.pattern;
let iterable = &for_node.iterable;
let body_code = generate_code(&for_node.body);
quote! {
{
let mut result = String::new();
for #pattern in #iterable {
result.push_str(&#body_code.into_string());
}
result
}
}
}
})
.collect();
if child_parts.len() == 1 {
child_parts[0].clone()
} else {
quote! { [#(#child_parts),*].join("") }
}
};
let attrs_code =
if element.attributes.is_empty() {
quote! { String::new() }
} else {
let attr_parts: Vec<_> = element.attributes.iter().map(|(name, value)| {
let name_str = name.to_string();
match value {
Some(val_expr) => {
quote! { format!(" {}=\"{}\"", #name_str, (#val_expr).to_html().as_str()) }
}
None => {
quote! { format!(" {}", #name_str) }
}
}
}).collect();
if attr_parts.len() == 1 {
attr_parts[0].clone()
} else {
quote! { [#(#attr_parts),*].join("") }
}
};
let self_closing_tags = ["img", "br", "hr", "input", "meta", "link"];
let is_self_closing = self_closing_tags.contains(&tag.as_str());
if is_self_closing {
quote! {
format!("<{}{} />", #tag, #attrs_code)
}
} else {
quote! {
format!("<{}{}>{}</{}>", #tag, #attrs_code, #children_code, #tag)
}
}
}