use super::attribute::{AttrHints, generate_attr_methods_with_mode, static_class_expr_needs_id};
use super::class::{ClassMode, parse_class_string_with_mode};
use super::tables::{
is_stateful_attr, is_stateful_class, lookup_attr_flag_method, lookup_tag_default,
};
use crate::diagnostics::{for_loop_missing_key_error, missing_required_attribute_error};
#[derive(Default)]
struct AttrAnalysis {
name: Option<String>,
static_class: Option<String>,
needs_id: bool,
}
impl AttrAnalysis {
fn hints(&self) -> AttrHints<'_> {
AttrHints {
name: self.name.as_deref(),
static_class: self.static_class.as_deref(),
}
}
}
fn analyze_attr(attr: &RsxAttribute) -> AttrAnalysis {
match attr {
RsxAttribute::Value { name, value } if name == "id" || name == "key" => {
AttrAnalysis::default()
}
RsxAttribute::Value { name, value } if name == "class" => {
let static_class = if let syn::Expr::Lit(syn::ExprLit {
lit: syn::Lit::Str(lit_str),
..
}) = value
{
Some(lit_str.value())
} else {
None
};
let needs_id = if let Some(class) = static_class.as_deref() {
class.split_ascii_whitespace().any(is_stateful_class)
} else {
static_class_expr_needs_id(value)
};
AttrAnalysis {
static_class,
needs_id,
..AttrAnalysis::default()
}
}
RsxAttribute::Value { name, .. } => {
let name = name.to_string();
let needs_id = is_stateful_attr(&name);
AttrAnalysis {
name: Some(name),
needs_id,
..AttrAnalysis::default()
}
}
RsxAttribute::Flag(name) if name == "styled" => AttrAnalysis::default(),
RsxAttribute::Flag(name) => {
let name = name.to_string();
let needs_id = is_stateful_attr(&name)
|| lookup_attr_flag_method(&name).is_some_and(is_stateful_attr);
AttrAnalysis {
name: Some(name),
needs_id,
..AttrAnalysis::default()
}
}
RsxAttribute::StateClass { method, .. } => {
let name = method.to_string();
let needs_id = is_stateful_attr(&name);
AttrAnalysis {
name: Some(name),
needs_id,
..AttrAnalysis::default()
}
}
_ => AttrAnalysis::default(),
}
}
use crate::parser::{RsxAttribute, RsxBody, RsxElement, RsxElementName, RsxNode};
use proc_macro2::TokenStream;
use quote::{ToTokens, quote};
type CodegenResult = Result<TokenStream, TokenStream>;
pub fn generate_body_with_mode(body: &RsxBody, mode: ClassMode) -> TokenStream {
generate_body_checked(body, mode).unwrap_or_else(|err| err)
}
pub fn generate_body_expansion_preview(body: &RsxBody, mode: ClassMode) -> String {
generate_body_with_mode(body, mode).to_string()
}
fn generate_body_checked(body: &RsxBody, mode: ClassMode) -> CodegenResult {
match body {
RsxBody::Single(element) => generate_element_checked(element, false, mode),
RsxBody::Fragment(children) => {
let child_exprs: Vec<TokenStream> = children
.iter()
.map(|node| generate_node_checked(node, false, mode))
.collect::<Result<_, _>>()?;
Ok(quote! { vec![#(#child_exprs),*] })
}
}
}
fn generate_node_checked(node: &RsxNode, require_loop_key: bool, mode: ClassMode) -> CodegenResult {
match node {
RsxNode::Element(elem) => generate_element_checked(elem, require_loop_key, mode),
RsxNode::Expr(expr) => Ok(expr.to_token_stream()),
RsxNode::Spread(expr) => Ok(expr.to_token_stream()),
RsxNode::For {
binding,
iter,
body,
} => generate_for_loop_checked(binding, iter, body, mode),
}
}
fn generate_for_loop_checked(
binding: &syn::Pat,
iter: &syn::Expr,
body: &[RsxNode],
mode: ClassMode,
) -> CodegenResult {
let body_exprs: Vec<TokenStream> = body
.iter()
.map(|node| generate_node_checked(node, true, mode))
.collect::<Result<_, _>>()?;
if body_exprs.len() == 1 {
let single = &body_exprs[0];
Ok(quote! { (#iter).into_iter().map(|#binding| #single) })
} else {
Ok(quote! {
(#iter).into_iter().flat_map(|#binding| [#((#body_exprs).into_any_element()),*])
})
}
}
fn generate_element_checked(
element: &RsxElement,
require_loop_key: bool,
mode: ClassMode,
) -> CodegenResult {
let tag_str = element.name.to_string();
if element.attributes.is_empty() && element.children.is_empty() {
return generate_tag(&tag_str, &element.name, None, None, None);
}
let mut user_id = None;
let mut user_key = None;
let mut base_expr = None;
let mut img_source = None;
let mut canvas_prepaint = None;
let mut canvas_paint = None;
let mut has_styled = false;
let mut needs_id = false;
let mut methods: Vec<TokenStream> =
Vec::with_capacity(element.attributes.len() * 2 + element.children.len());
for attr in &element.attributes {
match attr {
RsxAttribute::Value { name, value } if name == "id" => {
user_id = Some(value);
}
RsxAttribute::Value { name, value } if name == "key" => {
user_key = Some(value);
}
RsxAttribute::Value { name, value } if name == "base" => {
base_expr = Some(value);
}
RsxAttribute::Value { name, value }
if tag_str == "img" && (name == "src" || name == "source") =>
{
img_source = Some(value);
}
RsxAttribute::Value { name, value } if tag_str == "canvas" && name == "prepaint" => {
canvas_prepaint = Some(value);
}
RsxAttribute::Value { name, value } if tag_str == "canvas" && name == "paint" => {
canvas_paint = Some(value);
}
RsxAttribute::Value { name, value } if tag_str == "svg" && name == "src" => {
methods.push(quote! { .path(#value) });
}
RsxAttribute::Flag(name) if name == "styled" => {
has_styled = true;
}
_ => {
let analysis = analyze_attr(attr);
if !needs_id && analysis.needs_id {
needs_id = true;
}
generate_attr_methods_with_mode(attr, analysis.hints(), &mut methods, mode);
}
}
}
if require_loop_key && needs_id && user_id.is_none() && user_key.is_none() {
return Err(for_loop_missing_key_error(&element.name.path, &tag_str).to_compile_error());
}
let tag = if let Some(base) = base_expr {
quote! { #base }
} else {
generate_tag(
&tag_str,
&element.name,
img_source,
canvas_prepaint,
canvas_paint,
)?
};
let base = if let Some(id_value) = user_id {
quote! { #tag.id(#id_value) }
} else if needs_id {
if let Some(key_expr) = user_key {
let keyed_id = make_keyed_auto_id(&element.name, key_expr);
quote! { #tag.id(#keyed_id) }
} else {
let auto_id = make_auto_id(&element.name);
quote! { #tag.id(#auto_id) }
}
} else {
tag
};
let default_methods: Vec<TokenStream> =
if has_styled && let Some(class_str) = lookup_tag_default(&tag_str) {
parse_class_string_with_mode(class_str, mode).collect()
} else {
Vec::new()
};
generate_children_methods(&element.children, require_loop_key, &mut methods, mode)?;
Ok(quote! { #base #(#default_methods)* #(#methods)* })
}
fn generate_children_methods(
children: &[RsxNode],
require_loop_key: bool,
methods: &mut Vec<TokenStream>,
mode: ClassMode,
) -> Result<(), TokenStream> {
for node in children {
match node {
RsxNode::Expr(expr) => {
methods.push(quote! { .child(#expr) });
}
RsxNode::Element(elem) => {
let child_expr = generate_element_checked(elem, require_loop_key, mode)?;
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_checked(binding, iter, body, mode)?;
methods.push(quote! { .children(#for_expr) });
}
}
}
Ok(())
}
fn generate_tag(
tag_str: &str,
name: &RsxElementName,
img_source: Option<&syn::Expr>,
canvas_prepaint: Option<&syn::Expr>,
canvas_paint: Option<&syn::Expr>,
) -> CodegenResult {
if name.as_single_ident().is_none() {
let path = &name.path;
return Ok(quote! { #path() });
}
let path = &name.path;
Ok(match tag_str {
"svg" => quote! { svg() },
"img" => {
let Some(source) = img_source else {
return Err(missing_required_attribute_error(
&name.path,
"img",
"src",
r#"<img src={"path/to/image.png"} />"#,
)
.to_compile_error());
};
quote! { img(#source) }
}
"canvas" => {
let Some(prepaint) = canvas_prepaint else {
return Err(missing_required_attribute_error(
&name.path,
"canvas",
"prepaint",
r#"<canvas prepaint={|bounds, window, cx| state} paint={|bounds, state, window, cx| { ... }} />"#,
)
.to_compile_error());
};
let Some(paint) = canvas_paint else {
return Err(missing_required_attribute_error(
&name.path,
"canvas",
"paint",
r#"<canvas prepaint={|bounds, window, cx| state} paint={|bounds, state, window, cx| { ... }} />"#,
)
.to_compile_error());
};
quote! { canvas(#prepaint, #paint) }
}
"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! { #path() },
})
}
fn make_auto_id(tag_name: &RsxElementName) -> TokenStream {
let span = tag_name.span();
let loc = span.start(); let id_suffix = format!("__rsx_{}_L{}C{}", tag_name, loc.line, loc.column);
quote! { concat!(file!(), "::", #id_suffix) }
}
fn make_keyed_auto_id(tag_name: &RsxElementName, key_expr: &syn::Expr) -> TokenStream {
let span = tag_name.span();
let loc = span.start();
let prefix_suffix = format!("::__rsx_{}_L{}C{}_", tag_name, loc.line, loc.column);
if let Some(static_suffix) = static_key_suffix(key_expr) {
return quote! { concat!(file!(), #prefix_suffix, #static_suffix) };
}
quote! { format!(concat!(file!(), #prefix_suffix, "{}"), #key_expr) }
}
fn static_key_suffix(expr: &syn::Expr) -> Option<String> {
match expr {
syn::Expr::Lit(syn::ExprLit {
lit: syn::Lit::Str(lit),
..
}) => Some(lit.value()),
syn::Expr::Lit(syn::ExprLit {
lit: syn::Lit::Int(lit),
..
}) => lit
.base10_parse::<u128>()
.ok()
.map(|value| value.to_string()),
syn::Expr::Lit(syn::ExprLit {
lit: syn::Lit::Bool(lit),
..
}) => Some(lit.value.to_string()),
syn::Expr::Lit(syn::ExprLit {
lit: syn::Lit::Char(lit),
..
}) => Some(lit.value().to_string()),
syn::Expr::Unary(unary) if matches!(unary.op, syn::UnOp::Neg(_)) => match &*unary.expr {
syn::Expr::Lit(syn::ExprLit {
lit: syn::Lit::Int(lit),
..
}) => lit
.base10_parse::<u128>()
.ok()
.map(|value| format!("-{value}")),
_ => None,
},
syn::Expr::Paren(expr) => static_key_suffix(&expr.expr),
syn::Expr::Group(expr) => static_key_suffix(&expr.expr),
_ => None,
}
}