extern crate proc_macro;
use quote::quote;
use syn::{spanned::Spanned, Data, DeriveInput, Fields, Ident, Result};
macro_rules! bail {
(callsite, $($tts:tt)*) => {
return Err(syn::Error::new(
proc_macro2::Span::call_site(),
format!("typst: {}", format!($($tts)*))
))
};
($item:expr, $($tts:tt)*) => {
return Err(syn::Error::new_spanned(
&$item,
format!("typst: {}", format!($($tts)*))
))
};
}
#[proc_macro_derive(IntoValue, attributes(rename))]
pub fn derive_into_value(
item: proc_macro::TokenStream,
) -> proc_macro::TokenStream {
let item = syn::parse_macro_input!(item as DeriveInput);
derive_intoval(item).unwrap_or_else(|err| err.to_compile_error().into())
}
#[proc_macro_derive(IntoDict, attributes(rename))]
pub fn derive_into_dict(
item: proc_macro::TokenStream,
) -> proc_macro::TokenStream {
let item = syn::parse_macro_input!(item as DeriveInput);
derive_intodict(item).unwrap_or_else(|err| err.to_compile_error().into())
}
fn gather_input(
item: &DeriveInput,
) -> Result<(&Ident, Vec<proc_macro2::TokenStream>)> {
let ty = &item.ident;
let Data::Struct(ref data) = &item.data else {
bail!(item, "only structs are supported");
};
let Fields::Named(ref fields) = data.fields else {
bail!(data.fields, "only named fields are supported");
};
let rename_attr = item
.attrs
.iter()
.find(|attr| attr.path().is_ident("rename"));
let rename = if let Some(attr) = rename_attr {
attr.parse_args::<syn::LitStr>()?.value()
} else {
String::new()
};
let mut fieldlist = vec![];
for field in &fields.named {
let Some(ref old_id) = field.ident else {
bail!(field, "only named fields are supported");
};
let new_id = if let Some(attr) = field
.attrs
.iter()
.find(|attr| attr.path().is_ident("rename"))
{
Some(attr.parse_args::<syn::LitStr>()?.value())
} else if rename.is_empty() {
Some(old_id.to_string())
} else {
None
};
fieldlist.push(Element {
new_id,
old_id: old_id.to_string(),
ident: old_id.clone(),
});
}
let dictentries: Vec<_> = fieldlist
.iter()
.map(
|Element {
new_id,
old_id,
ident,
}| {
if let Some(ref n) = new_id {
quote! {
#n => self.#ident.into_value()
}
} else {
let fun = syn::Ident::new(&rename, rename_attr.span());
quote! {
heck::#fun(#old_id).to_string() => self.#ident.into_value()
}
}
},
)
.collect();
Ok((ty, dictentries))
}
fn derive_intodict(item: DeriveInput) -> Result<proc_macro::TokenStream> {
let (ty, dictentries) = gather_input(&item)?;
Ok(
quote! {
impl #ty {
#[inline]
#[must_use]
pub fn into_dict(self) -> typst::foundations::Dict {
typst::foundations::dict!(
#(#dictentries),*
)
}
}
}
.into(),
)
}
fn derive_intoval(item: DeriveInput) -> Result<proc_macro::TokenStream> {
let (ty, dictentries) = gather_input(&item)?;
Ok(
quote! {
impl typst::foundations::IntoValue for #ty {
#[inline]
#[must_use]
fn into_value(self) -> typst::foundations::Value {
let d = typst::foundations::dict!(
#(#dictentries),*
);
typst::foundations::Value::Dict(d)
}
}
}
.into(),
)
}
struct Element {
new_id: Option<String>,
old_id: String,
ident: Ident,
}