use crate::parser::{RsxAttribute, RsxElement, RsxNode};
use proc_macro2::TokenStream;
use quote::{quote, ToTokens};
use std::sync::atomic::{AtomicUsize, Ordering};
static AUTO_ID_COUNTER: AtomicUsize = AtomicUsize::new(0);
const STATEFUL_EVENTS: &[&str] = &["onClick", "on_click"];
const EVENT_HANDLERS: &[(&str, &str, &str)] = &[
("onClick", "on_click", "on_click"),
("onMouseDown", "on_mouse_down", "on_mouse_down"),
("onMouseUp", "on_mouse_up", "on_mouse_up"),
("onMouseMove", "on_mouse_move", "on_mouse_move"),
("onKeyDown", "on_key_down", "on_key_down"),
("onKeyUp", "on_key_up", "on_key_up"),
("onFocus", "on_focus", "on_focus"),
("onBlur", "on_blur", "on_blur"),
];
const COLOR_MAP: &[(&str, u32)] = &[
("red_500", 0xef4444),
("red_600", 0xef4444),
("green_600", 0x22c55e),
("blue_500", 0x3b82f6),
("blue_600", 0x2563eb),
("gray_600", 0x6b7280),
];
const SPACING_PATTERNS: &[(&str, &str)] = &[
("gap_", "gap"),
("p_", "p"),
("px_", "px"),
("py_", "py"),
("pt_", "pt"),
("pb_", "pb"),
("pl_", "pl"),
("pr_", "pr"),
("m_", "m"),
("mx_", "mx"),
("my_", "my"),
("mt_", "mt"),
("mb_", "mb"),
("ml_", "ml"),
("mr_", "mr"),
("w_", "w"),
("h_", "h"),
];
pub fn generate_code(element: &RsxElement) -> TokenStream {
generate_element(element)
}
fn needs_stateful_id(attributes: &[RsxAttribute]) -> bool {
attributes.iter().any(|attr| {
if let RsxAttribute::Value { name, .. } = attr {
let n = name.to_string();
STATEFUL_EVENTS.iter().any(|&s| n == s)
} else {
false
}
})
}
fn next_auto_id() -> String {
let n = AUTO_ID_COUNTER.fetch_add(1, Ordering::Relaxed);
format!("__rsx_{n}")
}
fn generate_element(element: &RsxElement) -> TokenStream {
let base = generate_base(element);
let mut methods: Vec<TokenStream> = Vec::new();
for attr in &element.attributes {
methods.extend(generate_attr_methods(attr));
}
for child in &element.children {
let child_expr = match child {
RsxNode::Element(elem) => generate_element(elem),
RsxNode::Expr(expr) => expr.to_token_stream(),
};
methods.push(quote! { .child(#child_expr) });
}
quote! { #base #(#methods)* }
}
fn generate_base(element: &RsxElement) -> TokenStream {
let tag = generate_tag(&element.name);
let user_id = element.attributes.iter().find_map(|a| match a {
RsxAttribute::Value { name, value } if name == "id" => Some(value),
_ => None,
});
if let Some(id_value) = user_id {
quote! { #tag.id(#id_value) }
} else if needs_stateful_id(&element.attributes) {
let auto_id = next_auto_id();
quote! { #tag.id(#auto_id) }
} else {
tag
}
}
fn generate_tag(name: &syn::Ident) -> TokenStream {
match name.to_string().as_str() {
"div" | "span" | "section" | "article" | "header" | "footer" | "main" | "nav"
| "aside" | "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "p" | "label" | "a"
| "button" | "input" | "textarea" | "select" | "form" | "ul" | "ol" | "li" => {
quote! { div() }
}
_ => quote! { #name() },
}
}
fn generate_attr_methods(attr: &RsxAttribute) -> Vec<TokenStream> {
match attr {
RsxAttribute::Value { name, .. } if name == "id" => vec![],
RsxAttribute::Flag(name) => {
vec![quote! { .#name() }]
}
RsxAttribute::Value { name, value } => {
let method_name = name.to_string();
if method_name == "class" {
if let syn::Expr::Lit(syn::ExprLit {
lit: syn::Lit::Str(lit_str),
..
}) = value
{
return parse_class_string(&lit_str.value());
}
}
for &(camel, snake, method) in EVENT_HANDLERS {
if method_name == camel || method_name == snake {
let method_ident =
syn::Ident::new(method, proc_macro2::Span::call_site());
return vec![quote! { .#method_ident(#value) }];
}
}
vec![quote! { .#name(#value) }]
}
}
}
fn parse_class_string(class_str: &str) -> Vec<TokenStream> {
class_str
.split_whitespace()
.filter_map(parse_single_class)
.collect()
}
fn parse_single_class(class: &str) -> Option<TokenStream> {
let method_name = class.replace('-', "_");
for &(prefix, method) in SPACING_PATTERNS {
if let Some(value) = method_name.strip_prefix(prefix) {
if let Ok(num) = value.parse::<f32>() {
let method_ident = syn::Ident::new(method, proc_macro2::Span::call_site());
return Some(quote! { .#method_ident(px(#num)) });
}
}
}
if let Some(color_code) = parse_color_class(&method_name) {
return Some(color_code);
}
if let Some(size) = method_name.strip_prefix("text_") {
let size_ident =
syn::Ident::new(&format!("text_{size}"), proc_macro2::Span::call_site());
return Some(quote! { .#size_ident() });
}
let ident = syn::Ident::new(&method_name, proc_macro2::Span::call_site());
Some(quote! { .#ident() })
}
fn parse_color_class(class: &str) -> Option<TokenStream> {
if let Some(color) = class.strip_prefix("text_") {
for &(color_name, color_value) in COLOR_MAP {
if color == color_name {
return Some(quote! { .text_color(rgb(#color_value)) });
}
}
} else if let Some(color) = class.strip_prefix("bg_") {
for &(color_name, color_value) in COLOR_MAP {
if color == color_name {
return Some(quote! { .bg(rgb(#color_value)) });
}
}
}
None
}