1use proc_macro::TokenStream;
2use proc_macro2::{Literal, TokenStream as TokenStream2};
3use quote::quote;
4
5mod parser;
6use parser::{HtmlElement, HtmlNode, parse_html};
7
8#[proc_macro]
10pub fn hers(input: TokenStream) -> TokenStream {
11 let input = TokenStream2::from(input);
12
13 match parse_html(input) {
14 Ok(nodes) => {
15 let code = generate_code(&nodes);
16 TokenStream::from(quote! {
17 {
18 use ::hers::{HtmlString, ToHtml};
19 #code
20 }
21 })
22 }
23 Err(err) => TokenStream::from(err.to_compile_error()),
24 }
25}
26
27fn generate_code(nodes: &[HtmlNode]) -> TokenStream2 {
29 let mut parts = Vec::new();
30
31 for node in nodes {
32 match node {
33 HtmlNode::Element(element) => {
34 let element_code = generate_element_code(element);
35 parts.push(element_code);
36 }
37 HtmlNode::Text(text) => {
38 let text_literal = Literal::string(text);
39 parts.push(quote! { #text_literal.to_string() });
40 }
41 HtmlNode::Variable(expr) => {
42 parts.push(quote! { (#expr).to_html().into_string() });
43 }
44 HtmlNode::If(if_node) => {
45 let condition = &if_node.condition;
46 let then_code = generate_code(&if_node.then_branch);
47
48 let else_code = if let Some(else_branch) = &if_node.else_branch {
49 let else_gen = generate_code(else_branch);
50 quote! { #else_gen.into_string() }
51 } else {
52 quote! { String::new() }
53 };
54
55 parts.push(quote! {
56 if #condition {
57 #then_code.into_string()
58 } else {
59 #else_code
60 }
61 });
62 }
63 HtmlNode::For(for_node) => {
64 let pattern = &for_node.pattern;
65 let iterable = &for_node.iterable;
66 let body_code = generate_code(&for_node.body);
67
68 parts.push(quote! {
69 {
70 let mut result = String::new();
71 for #pattern in #iterable {
72 result.push_str(&#body_code.into_string());
73 }
74 result
75 }
76 });
77 }
78 }
79 }
80
81 if parts.is_empty() {
82 quote! { HtmlString::new(String::new()) }
83 } else if parts.len() == 1 {
84 let part = &parts[0];
85 quote! { HtmlString::new(#part) }
86 } else {
87 quote! {
88 HtmlString::new(format!("{}", [#(#parts),*].join("")))
89 }
90 }
91}
92
93fn generate_element_code(element: &HtmlElement) -> TokenStream2 {
95 let tag = &element.tag;
96 let children_code = if element.children.is_empty() {
97 quote! { String::new() }
98 } else {
99 let child_parts: Vec<_> = element
100 .children
101 .iter()
102 .map(|child| match child {
103 HtmlNode::Element(child_element) => generate_element_code(child_element),
104 HtmlNode::Text(text) => {
105 let text_literal = Literal::string(text);
106 quote! { #text_literal.to_string() }
107 }
108 HtmlNode::Variable(expr) => {
109 quote! { (#expr).to_html().into_string() }
110 }
111 HtmlNode::If(if_node) => {
112 let condition = &if_node.condition;
113 let then_code = generate_code(&if_node.then_branch);
114
115 let else_code = if let Some(else_branch) = &if_node.else_branch {
116 let else_gen = generate_code(else_branch);
117 quote! { #else_gen.into_string() }
118 } else {
119 quote! { String::new() }
120 };
121
122 quote! {
123 if #condition {
124 #then_code.into_string()
125 } else {
126 #else_code
127 }
128 }
129 }
130 HtmlNode::For(for_node) => {
131 let pattern = &for_node.pattern;
132 let iterable = &for_node.iterable;
133 let body_code = generate_code(&for_node.body);
134
135 quote! {
136 {
137 let mut result = String::new();
138 for #pattern in #iterable {
139 result.push_str(&#body_code.into_string());
140 }
141 result
142 }
143 }
144 }
145 })
146 .collect();
147
148 if child_parts.len() == 1 {
149 child_parts[0].clone()
150 } else {
151 quote! { [#(#child_parts),*].join("") }
152 }
153 };
154
155 let attrs_code =
157 if element.attributes.is_empty() {
158 quote! { String::new() }
159 } else {
160 let attr_parts: Vec<_> = element.attributes.iter().map(|(name, value)| {
161 let name_str = name.to_string();
162 match value {
163 Some(val_expr) => {
164 quote! { format!(" {}=\"{}\"", #name_str, (#val_expr).to_html().as_str()) }
165 }
166 None => {
167 quote! { format!(" {}", #name_str) }
168 }
169 }
170 }).collect();
171
172 if attr_parts.len() == 1 {
173 attr_parts[0].clone()
174 } else {
175 quote! { [#(#attr_parts),*].join("") }
176 }
177 };
178
179 let self_closing_tags = ["img", "br", "hr", "input", "meta", "link"];
181 let is_self_closing = self_closing_tags.contains(&tag.as_str());
182
183 if is_self_closing {
184 quote! {
185 format!("<{}{} />", #tag, #attrs_code)
186 }
187 } else {
188 quote! {
189 format!("<{}{}>{}</{}>", #tag, #attrs_code, #children_code, #tag)
190 }
191 }
192}