blob: 995b344e3c7cd9efb8587fe564510128561a3c6a [file] [log] [blame]
use convert_case::Casing;
use proc_macro::{self, TokenStream};
use proc_macro2::{Literal, Span, TokenStream as TokenStream2};
use quote::quote;
use syn::{
parse::Parse, parse_macro_input, parse_quote, Attribute, Data, DataEnum, DeriveInput, Fields, Ident, Token,
};
pub fn derive_parse(input: TokenStream) -> TokenStream {
let DeriveInput {
ident,
data,
mut generics,
attrs,
..
} = parse_macro_input!(input);
let opts = CssOptions::parse_attributes(&attrs).unwrap();
let cloned_generics = generics.clone();
let (_, ty_generics, _) = cloned_generics.split_for_impl();
if generics.lifetimes().next().is_none() {
generics.params.insert(0, parse_quote! { 'i })
}
let lifetime = generics.lifetimes().next().unwrap().clone();
let (impl_generics, _, where_clause) = generics.split_for_impl();
let imp = match &data {
Data::Enum(data) => derive_enum(&data, &ident, &opts),
_ => todo!(),
};
let output = quote! {
impl #impl_generics Parse<#lifetime> for #ident #ty_generics #where_clause {
fn parse<'t>(input: &mut Parser<#lifetime, 't>) -> Result<Self, ParseError<#lifetime, ParserError<#lifetime>>> {
#imp
}
}
};
output.into()
}
fn derive_enum(data: &DataEnum, ident: &Ident, opts: &CssOptions) -> TokenStream2 {
let mut idents = Vec::new();
let mut non_idents = Vec::new();
for (index, variant) in data.variants.iter().enumerate() {
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::<Vec<_>>();
let mut expr = match &variant.fields {
Fields::Unit => {
idents.push((
Literal::string(&variant.ident.to_string().to_case(opts.case)),
name.clone(),
));
continue;
}
Fields::Named(_) => {
quote! {
return Ok(#ident::#name { #(#fields),* })
}
}
Fields::Unnamed(_) => {
quote! {
return Ok(#ident::#name(#(#fields),*))
}
}
};
// Group multiple ident branches together.
if !idents.is_empty() {
if idents.len() == 1 {
let (s, name) = idents.remove(0);
non_idents.push(quote! {
if input.try_parse(|input| input.expect_ident_matching(#s)).is_ok() {
return Ok(#ident::#name)
}
});
} else {
let matches = idents
.iter()
.map(|(s, name)| {
quote! {
#s => return Ok(#ident::#name),
}
})
.collect::<Vec<_>>();
non_idents.push(quote! {
{
let state = input.state();
if let Ok(ident) = input.try_parse(|input| input.expect_ident_cloned()) {
cssparser::match_ignore_ascii_case! { &*ident,
#(#matches)*
_ => {}
}
input.reset(&state);
}
}
});
idents.clear();
}
}
let is_last = index == data.variants.len() - 1;
for (index, field) in variant.fields.iter().enumerate().rev() {
let ty = &field.ty;
let field_name = field.ident.as_ref().map_or_else(
|| Ident::new(&format!("_{}", index), Span::call_site()),
|ident| ident.clone(),
);
if is_last {
expr = quote! {
let #field_name = <#ty>::parse(input)?;
#expr
};
} else {
expr = quote! {
if let Ok(#field_name) = input.try_parse(<#ty>::parse) {
#expr
}
};
}
}
non_idents.push(expr);
}
let idents = if idents.is_empty() {
quote! {}
} else if idents.len() == 1 {
let (s, name) = idents.remove(0);
quote! {
input.expect_ident_matching(#s)?;
Ok(#ident::#name)
}
} else {
let idents = idents
.into_iter()
.map(|(s, name)| {
quote! {
#s => Ok(#ident::#name),
}
})
.collect::<Vec<_>>();
quote! {
let location = input.current_source_location();
let ident = input.expect_ident()?;
cssparser::match_ignore_ascii_case! { &*ident,
#(#idents)*
_ => Err(location.new_unexpected_token_error(
cssparser::Token::Ident(ident.clone())
))
}
}
};
let output = quote! {
#(#non_idents)*
#idents
};
output.into()
}
pub struct CssOptions {
pub case: convert_case::Case,
}
impl CssOptions {
pub fn parse_attributes(attrs: &Vec<Attribute>) -> syn::Result<Self> {
for attr in attrs {
if attr.path.is_ident("css") {
let opts: CssOptions = attr.parse_args()?;
return Ok(opts);
}
}
Ok(CssOptions {
case: convert_case::Case::Kebab,
})
}
}
impl Parse for CssOptions {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let mut case = convert_case::Case::Kebab;
while !input.is_empty() {
let k: Ident = input.parse()?;
let _: Token![=] = input.parse()?;
let v: Ident = input.parse()?;
if k == "case" {
if v == "lower" {
case = convert_case::Case::Flat;
}
}
}
Ok(Self { case })
}
}