1use proc_macro::TokenStream;
15use proc_macro2::TokenStream as TokenStream2;
16use quote::{quote, ToTokens};
17use syn::parse::discouraged::Speculative;
18use syn::parse::{Parse, ParseStream};
19use syn::{braced, token, Expr, Ident, LitStr, Result, Token};
20
21#[proc_macro]
25pub fn html(input: TokenStream) -> TokenStream {
26 let node = syn::parse_macro_input!(input as Node);
27 let expanded = node.to_token_stream();
28 expanded.into()
29}
30
31enum Node {
33 Element(ElementNode),
34 Text(LitStr),
35 Expr(Expr),
36 For(ForLoop),
37 If(IfNode),
38}
39
40impl Parse for Node {
41 fn parse(input: ParseStream) -> Result<Self> {
42 if input.peek(LitStr) {
43 Ok(Self::Text(input.parse()?))
44 } else if input.peek(Token![#]) {
45 input.parse::<Token![#]>()?;
46 Ok(Self::Expr(input.parse()?))
47 } else if input.peek(Token![for]) {
48 Ok(Self::For(input.parse()?))
49 } else if input.peek(Token![if]) {
50 Ok(Self::If(input.parse()?))
51 } else if input.peek(Ident) {
52 Ok(Self::Element(input.parse()?))
53 } else {
54 Err(input.error("expected element, text literal, or expression"))
55 }
56 }
57}
58
59impl ToTokens for Node {
60 fn to_tokens(&self, tokens: &mut TokenStream2) {
61 match self {
62 Self::Element(elem) => elem.to_tokens(tokens),
63 Self::Text(lit) => {
64 tokens.extend(quote! { .text(#lit) });
65 }
66 Self::Expr(expr) => {
67 tokens.extend(quote! { .text(#expr) });
68 }
69 Self::For(for_loop) => for_loop.to_tokens(tokens),
70 Self::If(if_node) => if_node.to_tokens(tokens),
71 }
72 }
73}
74
75struct ElementNode {
77 tag: Ident,
78 attrs: Vec<Attribute>,
79 children: Vec<Node>,
80}
81
82impl Parse for ElementNode {
83 fn parse(input: ParseStream) -> Result<Self> {
84 let tag: Ident = input.parse()?;
85
86 let mut attrs = Vec::new();
88 while input.peek(Token![.]) {
89 input.parse::<Token![.]>()?;
90 attrs.push(input.parse()?);
91 }
92
93 let children = if input.peek(token::Brace) {
95 let content;
96 braced!(content in input);
97 let mut children = Vec::new();
98 while !content.is_empty() {
99 children.push(content.parse()?);
100 }
101 children
102 } else {
103 Vec::new()
104 };
105
106 Ok(Self {
107 tag,
108 attrs,
109 children,
110 })
111 }
112}
113
114impl ToTokens for ElementNode {
115 fn to_tokens(&self, tokens: &mut TokenStream2) {
116 let tag = &self.tag;
117 let tag_pascal = to_pascal_case(&tag.to_string());
118 let tag_ident = Ident::new(&tag_pascal, tag.span());
119
120 let attr_calls: Vec<_> = self
122 .attrs
123 .iter()
124 .map(quote::ToTokens::to_token_stream)
125 .collect();
126
127 if self.children.is_empty() {
128 tokens.extend(quote! {
130 ::ironhtml::typed::Element::<::ironhtml_elements::#tag_ident>::new()
131 #(#attr_calls)*
132 });
133 } else {
134 let mut child_tokens = TokenStream2::new();
136
137 for child in &self.children {
138 match child {
139 Node::Element(elem) => {
140 let child_tag = &elem.tag;
141 let child_pascal = to_pascal_case(&child_tag.to_string());
142 let child_ident = Ident::new(&child_pascal, child_tag.span());
143
144 let child_attrs: Vec<_> = elem
145 .attrs
146 .iter()
147 .map(quote::ToTokens::to_token_stream)
148 .collect();
149
150 if elem.children.is_empty() {
151 child_tokens.extend(quote! {
152 .child::<::ironhtml_elements::#child_ident, _>(|e| e #(#child_attrs)*)
153 });
154 } else {
155 let nested = generate_children(&elem.children);
156 child_tokens.extend(quote! {
157 .child::<::ironhtml_elements::#child_ident, _>(|e| e #(#child_attrs)* #nested)
158 });
159 }
160 }
161 Node::Text(lit) => {
162 child_tokens.extend(quote! { .text(#lit) });
163 }
164 Node::Expr(expr) => {
165 child_tokens.extend(quote! { .text(#expr) });
166 }
167 Node::For(for_loop) => {
168 for_loop.to_tokens(&mut child_tokens);
169 }
170 Node::If(if_node) => {
171 if_node.to_tokens(&mut child_tokens);
172 }
173 }
174 }
175
176 tokens.extend(quote! {
177 ::ironhtml::typed::Element::<::ironhtml_elements::#tag_ident>::new()
178 #(#attr_calls)*
179 #child_tokens
180 });
181 }
182 }
183}
184
185fn generate_children(children: &[Node]) -> TokenStream2 {
187 let mut tokens = TokenStream2::new();
188
189 for child in children {
190 match child {
191 Node::Element(elem) => {
192 let child_tag = &elem.tag;
193 let child_pascal = to_pascal_case(&child_tag.to_string());
194 let child_ident = Ident::new(&child_pascal, child_tag.span());
195
196 let child_attrs: Vec<_> = elem
197 .attrs
198 .iter()
199 .map(quote::ToTokens::to_token_stream)
200 .collect();
201
202 if elem.children.is_empty() {
203 tokens.extend(quote! {
204 .child::<::ironhtml_elements::#child_ident, _>(|e| e #(#child_attrs)*)
205 });
206 } else {
207 let nested = generate_children(&elem.children);
208 tokens.extend(quote! {
209 .child::<::ironhtml_elements::#child_ident, _>(|e| e #(#child_attrs)* #nested)
210 });
211 }
212 }
213 Node::Text(lit) => {
214 tokens.extend(quote! { .text(#lit) });
215 }
216 Node::Expr(expr) => {
217 tokens.extend(quote! { .text(#expr) });
218 }
219 Node::For(for_loop) => {
220 for_loop.to_tokens(&mut tokens);
221 }
222 Node::If(if_node) => {
223 if_node.to_tokens(&mut tokens);
224 }
225 }
226 }
227
228 tokens
229}
230
231struct Attribute {
233 name: Ident,
234 value: Option<AttrValue>,
235}
236
237enum AttrValue {
238 Lit(LitStr),
239 Expr(Expr),
240}
241
242impl Parse for Attribute {
243 fn parse(input: ParseStream) -> Result<Self> {
244 let name: Ident = input.parse()?;
245
246 let value = if input.peek(token::Paren) {
247 let content;
248 syn::parenthesized!(content in input);
249
250 if content.peek(Token![#]) {
251 content.parse::<Token![#]>()?;
252 Some(AttrValue::Expr(content.parse()?))
253 } else if content.peek(LitStr) {
254 Some(AttrValue::Lit(content.parse()?))
255 } else {
256 Some(AttrValue::Expr(content.parse()?))
257 }
258 } else {
259 None
260 };
261
262 Ok(Self { name, value })
263 }
264}
265
266impl ToTokens for Attribute {
267 fn to_tokens(&self, tokens: &mut TokenStream2) {
268 let name = &self.name;
269 let name_str = name.to_string();
270
271 let method_name = match name_str.as_str() {
273 "class" | "id" => name.clone(),
274 _ => Ident::new("attr", name.span()),
275 };
276
277 let convert_attr_name = |s: &str| -> String { s.trim_end_matches('_').replace('_', "-") };
280
281 match &self.value {
282 Some(AttrValue::Lit(lit)) => {
283 if name_str == "class" || name_str == "id" {
284 tokens.extend(quote! { .#method_name(#lit) });
285 } else {
286 let attr_name = convert_attr_name(&name_str);
287 tokens.extend(quote! { .#method_name(#attr_name, #lit) });
288 }
289 }
290 Some(AttrValue::Expr(expr)) => {
291 if name_str == "class" || name_str == "id" {
292 tokens.extend(quote! { .#method_name(#expr) });
293 } else {
294 let attr_name = convert_attr_name(&name_str);
295 tokens.extend(quote! { .#method_name(#attr_name, #expr) });
296 }
297 }
298 None => {
299 let attr_name = convert_attr_name(&name_str);
301 tokens.extend(quote! { .bool_attr(#attr_name) });
302 }
303 }
304 }
305}
306
307struct ForLoop {
309 pat: syn::Pat,
310 expr: Expr,
311 children: Vec<Node>,
312}
313
314impl Parse for ForLoop {
315 fn parse(input: ParseStream) -> Result<Self> {
316 let for_token: Token![for] = input.parse()?;
317 let pat = syn::Pat::parse_single(input)?;
318 input.parse::<Token![in]>()?;
319 input.parse::<Token![#]>()?;
320
321 let expr = parse_expr_before_brace(input)?;
324
325 let content;
326 braced!(content in input);
327 let mut children = Vec::new();
328 while !content.is_empty() {
329 children.push(content.parse()?);
330 }
331
332 if children.len() != 1 || !matches!(children.first(), Some(Node::Element(_))) {
333 return Err(syn::Error::new(
334 for_token.span,
335 "for loop body must contain exactly one element",
336 ));
337 }
338
339 Ok(Self {
340 pat,
341 expr,
342 children,
343 })
344 }
345}
346
347fn parse_expr_before_brace(input: ParseStream) -> Result<Expr> {
349 let fork = input.fork();
351
352 if let Ok(path) = fork.parse::<syn::ExprPath>() {
354 if fork.peek(token::Brace) {
356 input.advance_to(&fork);
357 return Ok(Expr::Path(path));
358 }
359 }
360
361 input.parse()
363}
364
365impl ToTokens for ForLoop {
366 fn to_tokens(&self, tokens: &mut TokenStream2) {
367 let pat = &self.pat;
368 let expr = &self.expr;
369
370 if let Some(Node::Element(elem)) = self.children.first() {
373 let child_tag = &elem.tag;
374 let child_pascal = to_pascal_case(&child_tag.to_string());
375 let child_ident = Ident::new(&child_pascal, child_tag.span());
376
377 let child_attrs: Vec<_> = elem
378 .attrs
379 .iter()
380 .map(quote::ToTokens::to_token_stream)
381 .collect();
382 let nested = generate_children(&elem.children);
383
384 tokens.extend(quote! {
385 .children(#expr, |#pat, e: ::ironhtml::typed::Element<::ironhtml_elements::#child_ident>| {
386 e #(#child_attrs)* #nested
387 })
388 });
389 }
390 }
391}
392
393struct IfNode {
395 cond: Expr,
396 children: Vec<Node>,
397}
398
399impl Parse for IfNode {
400 fn parse(input: ParseStream) -> Result<Self> {
401 input.parse::<Token![if]>()?;
402 input.parse::<Token![#]>()?;
403
404 let cond = parse_expr_before_brace(input)?;
406
407 let content;
408 braced!(content in input);
409 let mut children = Vec::new();
410 while !content.is_empty() {
411 children.push(content.parse()?);
412 }
413
414 Ok(Self { cond, children })
415 }
416}
417
418impl ToTokens for IfNode {
419 fn to_tokens(&self, tokens: &mut TokenStream2) {
420 let cond = &self.cond;
421 let child_tokens = generate_children(&self.children);
422
423 tokens.extend(quote! {
424 .when(#cond, |e| e #child_tokens)
425 });
426 }
427}
428
429fn to_pascal_case(s: &str) -> String {
431 let mut result = String::new();
432 let mut capitalize_next = true;
433
434 for c in s.chars() {
435 if c == '_' {
436 capitalize_next = true;
437 } else if capitalize_next {
438 result.push(c.to_ascii_uppercase());
439 capitalize_next = false;
440 } else {
441 result.push(c);
442 }
443 }
444
445 match result.as_str() {
447 "A" => "A".to_string(),
448 "B" => "B".to_string(),
449 "I" => "I".to_string(),
450 "P" => "P".to_string(),
451 "Q" => "Q".to_string(),
452 "S" => "S".to_string(),
453 "U" => "U".to_string(),
454 "Br" => "Br".to_string(),
455 "Hr" => "Hr".to_string(),
456 "H1" => "H1".to_string(),
457 "H2" => "H2".to_string(),
458 "H3" => "H3".to_string(),
459 "H4" => "H4".to_string(),
460 "H5" => "H5".to_string(),
461 "H6" => "H6".to_string(),
462 "Dl" => "Dl".to_string(),
463 "Dt" => "Dt".to_string(),
464 "Dd" => "Dd".to_string(),
465 "Li" => "Li".to_string(),
466 "Ol" => "Ol".to_string(),
467 "Ul" => "Ul".to_string(),
468 "Td" => "Td".to_string(),
469 "Th" => "Th".to_string(),
470 "Tr" => "Tr".to_string(),
471 "Em" => "Em".to_string(),
472 "Rp" => "Rp".to_string(),
473 "Rt" => "Rt".to_string(),
474 "Wbr" => "Wbr".to_string(),
475 "Kbd" => "Kbd".to_string(),
476 "Pre" => "Pre".to_string(),
477 "Sub" => "Sub".to_string(),
478 "Sup" => "Sup".to_string(),
479 "Var" => "Var".to_string(),
480 "Bdi" => "Bdi".to_string(),
481 "Bdo" => "Bdo".to_string(),
482 "Col" => "Col".to_string(),
483 "Del" => "Del".to_string(),
484 "Dfn" => "Dfn".to_string(),
485 "Div" => "Div".to_string(),
486 "Img" => "Img".to_string(),
487 "Ins" => "Ins".to_string(),
488 "Map" => "Map".to_string(),
489 "Nav" => "Nav".to_string(),
490 "Svg" => "Svg".to_string(),
491 "Option" => "Option_".to_string(),
493 _ => result,
494 }
495}