use super::attribute::generate_attr_methods;
use super::class::parse_class_string;
use super::tables::{is_stateful_attr, lookup_tag_default};
use crate::parser::{RsxAttribute, RsxBody, RsxElement, RsxNode};
use proc_macro2::TokenStream;
use quote::{ToTokens, quote};
use std::cell::Cell;
thread_local! {
static AUTO_ID_COUNTER: Cell<usize> = const { Cell::new(0) };
}
pub fn generate_body(body: &RsxBody) -> TokenStream {
match body {
RsxBody::Single(element) => generate_element(element),
RsxBody::Fragment(children) => {
let child_exprs: Vec<TokenStream> = children.iter().map(generate_node).collect();
quote! { vec![#(#child_exprs),*] }
}
}
}
fn generate_node(node: &RsxNode) -> TokenStream {
match node {
RsxNode::Element(elem) => generate_element(elem),
RsxNode::Expr(expr) => expr.to_token_stream(),
RsxNode::Spread(expr) => expr.to_token_stream(),
RsxNode::For {
binding,
iter,
body,
} => generate_for_loop(binding, iter, body),
}
}
fn generate_for_loop(binding: &syn::Pat, iter: &syn::Expr, body: &[RsxNode]) -> TokenStream {
let body_exprs: Vec<TokenStream> = body.iter().map(generate_node).collect();
if body_exprs.len() == 1 {
let single = &body_exprs[0];
quote! { (#iter).into_iter().map(|#binding| #single) }
} else {
quote! { (#iter).into_iter().flat_map(|#binding| vec![#(#body_exprs),*]) }
}
}
pub(crate) fn generate_element(element: &RsxElement) -> TokenStream {
let tag_str = element.name.to_string();
if element.attributes.is_empty() && element.children.is_empty() {
return generate_tag(&tag_str, &element.name);
}
let mut user_id = None;
let mut has_styled = false;
let mut needs_id = false;
for attr in &element.attributes {
match attr {
RsxAttribute::Value { name, value } if name == "id" => {
user_id = Some(value);
}
RsxAttribute::Flag(name) if name == "styled" => {
has_styled = true;
}
RsxAttribute::Value { name, .. } | RsxAttribute::Flag(name) => {
if !needs_id {
let attr_str = name.to_string();
needs_id = is_stateful_attr(&attr_str);
}
}
_ => {}
}
}
let tag = generate_tag(&tag_str, &element.name);
let base = if let Some(id_value) = user_id {
quote! { #tag.id(#id_value) }
} else if needs_id {
let auto_id = next_auto_id(&tag_str);
quote! { #tag.id(#auto_id) }
} else {
tag
};
let mut methods: Vec<TokenStream> =
Vec::with_capacity(element.attributes.len() * 2 + element.children.len());
if has_styled {
if let Some(class_str) = lookup_tag_default(&tag_str) {
methods.extend(parse_class_string(class_str));
}
}
for attr in &element.attributes {
generate_attr_methods(attr, &mut methods);
}
generate_children_methods(&element.children, &mut methods);
quote! { #base #(#methods)* }
}
fn generate_children_methods(children: &[RsxNode], methods: &mut Vec<TokenStream>) {
let mut i = 0;
let mut consecutive_exprs: Vec<TokenStream> = Vec::with_capacity(4);
while i < children.len() {
consecutive_exprs.clear();
while i < children.len() {
if let RsxNode::Expr(expr) = &children[i] {
consecutive_exprs.push(expr.to_token_stream());
i += 1;
} else {
break;
}
}
if consecutive_exprs.len() >= 2 {
methods.push(quote! { .children([#(#consecutive_exprs),*]) });
} else {
for expr in &consecutive_exprs {
methods.push(quote! { .child(#expr) });
}
}
if i < children.len() {
match &children[i] {
RsxNode::Element(elem) => {
let child_expr = generate_element(elem);
methods.push(quote! { .child(#child_expr) });
}
RsxNode::Spread(expr) => {
methods.push(quote! { .children(#expr) });
}
RsxNode::For {
binding,
iter,
body,
} => {
let for_expr = generate_for_loop(binding, iter, body);
methods.push(quote! { .children(#for_expr) });
}
RsxNode::Expr(_) => {
unreachable!(
"BUG in gpui-rsx codegen: Expr node should have been consumed above"
)
}
}
i += 1;
}
}
}
fn generate_tag(tag_str: &str, name: &syn::Ident) -> TokenStream {
match tag_str {
"svg" => quote! { svg() },
"img" => quote! { img() },
"canvas" => quote! { canvas() },
"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 next_auto_id(tag: &str) -> String {
AUTO_ID_COUNTER.with(|c| {
let n = c.get();
c.set(n + 1);
format!("__rsx_{tag}_{n}")
})
}