use convert_case::Casing; use proc_macro::{self, TokenStream}; use proc_macro2::{Literal, Span, TokenStream as TokenStream2}; use quote::quote; use syn::{parse_macro_input, Data, DataEnum, DeriveInput, Fields, Ident, Type}; use crate::parse::CssOptions; pub fn derive_to_css(input: TokenStream) -> TokenStream { let DeriveInput { ident, data, generics, attrs, .. } = parse_macro_input!(input); let opts = CssOptions::parse_attributes(&attrs).unwrap(); let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); let imp = match &data { Data::Enum(data) => derive_enum(&data, &opts), _ => todo!(), }; let output = quote! { impl #impl_generics ToCss for #ident #ty_generics #where_clause { fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> where W: std::fmt::Write, { #imp } } }; output.into() } fn derive_enum(data: &DataEnum, opts: &CssOptions) -> TokenStream2 { let variants = data .variants .iter() .map(|variant| { let name = &variant.ident; let fields = variant .fields .iter() .enumerate() .map(|(index, field)| { field.ident.as_ref().map_or_else( || Ident::new(&format!("_{}", index), Span::call_site()), |ident| ident.clone(), ) }) .collect::>(); #[derive(PartialEq)] enum NeedsSpace { Yes, No, Maybe, } let mut needs_space = NeedsSpace::No; let mut fields_iter = variant.fields.iter().zip(fields.iter()).peekable(); let mut writes = Vec::new(); let mut has_needs_space = false; while let Some((field, name)) = fields_iter.next() { writes.push(if fields.len() > 1 { let space = match needs_space { NeedsSpace::Yes => quote! { dest.write_char(' ')?; }, NeedsSpace::No => quote! {}, NeedsSpace::Maybe => { has_needs_space = true; quote! { if needs_space { dest.write_char(' ')?; } } } }; if is_option(&field.ty) { needs_space = NeedsSpace::Maybe; let after_space = if matches!(fields_iter.peek(), Some((field, _)) if !is_option(&field.ty)) { // If the next field is non-optional, just insert the space here. needs_space = NeedsSpace::No; quote! { dest.write_char(' ')?; } } else { quote! {} }; quote! { if let Some(v) = #name { #space v.to_css(dest)?; #after_space } } } else { needs_space = NeedsSpace::Yes; quote! { #space #name.to_css(dest)?; } } } else { quote! { #name.to_css(dest) } }); } if writes.len() > 1 { writes.push(quote! { Ok(()) }); } if has_needs_space { writes.insert(0, quote! { let mut needs_space = false }); } match variant.fields { Fields::Unit => { let s = Literal::string(&variant.ident.to_string().to_case(opts.case)); quote! { Self::#name => dest.write_str(#s) } } Fields::Named(_) => { quote! { Self::#name { #(#fields),* } => { #(#writes)* } } } Fields::Unnamed(_) => { quote! { Self::#name(#(#fields),*) => { #(#writes)* } } } } }) .collect::>(); let output = quote! { match self { #(#variants),* } }; output.into() } fn is_option(ty: &Type) -> bool { matches!(&ty, Type::Path(p) if p.qself.is_none() && p.path.segments.iter().next().unwrap().ident == "Option") }