use proc_macro::TokenStream;
use quote::quote;
use syn::{
parse::{Parse, ParseStream},
parse_macro_input,
punctuated::Punctuated,
Expr, ExprBlock, ExprIf, ExprTuple, Token,
};
struct ClassNamesInput {
exprs: Vec<Expr>,
}
impl Parse for ClassNamesInput {
fn parse(input: ParseStream) -> syn::Result<Self> {
let exprs = Punctuated::<Expr, Token![,]>::parse_terminated(input)?;
Ok(ClassNamesInput {
exprs: exprs.into_iter().collect(),
})
}
}
#[proc_macro]
pub fn classnames(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as ClassNamesInput);
let mut tokens = Vec::new();
for expr in input.exprs {
tokens.push(parse_expr(expr));
}
quote! {
{
let mut classes = Vec::new();
#(#tokens)*
classes.into_iter()
.filter(|s| !s.is_empty())
.collect::<Vec<_>>()
.join(" ")
}
}
.into()
}
#[inline]
#[allow(dead_code)]
fn normalize_classname(input: &str) -> String {
input
.split_whitespace()
.filter(|s| !s.is_empty())
.collect::<Vec<_>>()
.join(" ")
}
fn parse_expr(expr: Expr) -> proc_macro2::TokenStream {
match expr {
Expr::Path(path) => {
quote! {
{
let class_str = #path;
let normalized = class_str.split_whitespace()
.filter(|s| !s.is_empty())
.collect::<Vec<_>>()
.join(" ");
classes.push(normalized);
}
}
}
Expr::Reference(expr_ref) => {
quote! {
{
let class_str = #expr_ref;
classes.push(class_str.to_string());
}
}
}
Expr::Lit(syn::ExprLit {
lit: syn::Lit::Str(s),
..
}) => {
let value = s.value();
quote! {
classes.push(
(#value).split_whitespace()
.filter(|s| !s.is_empty())
.collect::<Vec<_>>()
.join(" ")
);
}
}
Expr::Tuple(ExprTuple { elems, .. }) if elems.len() == 2 => {
let cond = &elems[0];
let class = &elems[1];
quote! {
if #cond {
let class = #class.to_string();
if !class.is_empty() { classes.push(class); }
}
}
}
Expr::If(ExprIf {
cond,
then_branch,
else_branch,
..
}) => {
if let Some((_, else_expr)) = else_branch {
quote! {
{
let value = if #cond {
#then_branch
} else {
#else_expr
};
let class = value.to_string();
if !class.is_empty() {
classes.push(class);
}
}
}
} else {
quote! {
if #cond {
let class = #then_branch.to_string();
if !class.is_empty() {
classes.push(class);
}
}
}
}
}
Expr::Block(ExprBlock { block, .. }) => {
quote! {
{
let result = #block;
if let Some(class) = result {
let class = class.to_string();
if !class.is_empty() { classes.push(class); }
}
}
}
}
Expr::Tuple(ExprTuple { elems, .. }) if elems.len() == 3 => {
let cond = &elems[0];
let true_val = &elems[1];
let false_val = &elems[2];
quote! {
{
let class = if #cond { #true_val } else { #false_val };
let class = class.to_string();
if !class.is_empty() { classes.push(class); }
}
}
}
_ => {
quote! {
{
let class = #expr.to_string();
if !class.is_empty() { classes.push(class); }
}
}
}
}
}
#[proc_macro]
pub fn choose(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as ClassNamesInput);
let exprs: Vec<_> = input.exprs.into_iter().collect();
if exprs.len() != 3 {
return syn::Error::new(
proc_macro2::Span::call_site(),
"choose! macro requires exactly three arguments: condition, true_value, false_value",
)
.to_compile_error()
.into();
}
let cond = &exprs[0];
let true_val = &exprs[1];
let false_val = &exprs[2];
quote! {
({
let result = if #cond {
let raw = #true_val.to_string();
raw.split_whitespace()
.filter(|s| !s.is_empty())
.collect::<Vec<_>>()
.join(" ")
} else {
let raw = #false_val.to_string();
raw.split_whitespace()
.filter(|s| !s.is_empty())
.collect::<Vec<_>>()
.join(" ")
};
result
})
}
.into()
}
#[proc_macro]
pub fn maybe(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as ClassNamesInput);
let exprs: Vec<_> = input.exprs.into_iter().collect();
if exprs.len() != 1 {
return syn::Error::new(
proc_macro2::Span::call_site(),
"maybe! macro requires exactly one argument",
)
.to_compile_error()
.into();
}
let value = &exprs[0];
quote! {
({
match #value {
Some(value) => {
let raw = value.to_string();
raw.split_whitespace()
.filter(|s| !s.is_empty())
.collect::<Vec<_>>()
.join(" ")
},
None => String::new()
}
})
}
.into()
}
#[proc_macro]
pub fn when(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as ClassNamesInput);
let exprs: Vec<_> = input.exprs.into_iter().collect();
if exprs.len() != 2 {
return syn::Error::new(
proc_macro2::Span::call_site(),
"when! macro requires exactly two arguments: condition and value",
)
.to_compile_error()
.into();
}
let cond = &exprs[0];
let value = &exprs[1];
quote! {
({
if #cond {
let raw = #value.to_string();
raw.split_whitespace()
.filter(|s| !s.is_empty())
.collect::<Vec<_>>()
.join(" ")
} else {
String::new()
}
})
}
.into()
}
#[proc_macro]
pub fn pretty_classname(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as ClassNamesInput);
let expr = &input.exprs[0];
quote! {
{
let raw = #expr.to_string();
raw.split_whitespace()
.filter(|s| !s.is_empty())
.collect::<Vec<_>>()
.join(" ")
}
}
.into()
}