use heck::ToKebabCase; use quote::ToTokens; use super::*; /// Return an error at the given item. 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)*)) )) }; } /// For parsing attributes of the form: /// #[attr( /// statement; /// statement; /// returned_expression /// )] pub struct BlockWithReturn { pub prefix: Vec, pub expr: syn::Stmt, } impl Parse for BlockWithReturn { fn parse(input: ParseStream) -> Result { let mut stmts = syn::Block::parse_within(input)?; let Some(expr) = stmts.pop() else { return Err(input.error("expected at least one expression")); }; Ok(Self { prefix: stmts, expr }) } } /// Whether an attribute list has a specified attribute. pub fn has_attr(attrs: &mut Vec, target: &str) -> bool { take_attr(attrs, target).is_some() } /// Whether an attribute list has a specified attribute. pub fn parse_attr( attrs: &mut Vec, target: &str, ) -> Result>> { take_attr(attrs, target) .map(|attr| { Ok(match attr.meta { syn::Meta::Path(_) => None, syn::Meta::List(list) => Some(list.parse_args()?), syn::Meta::NameValue(meta) => bail!(meta, "not valid here"), }) }) .transpose() } /// Whether an attribute list has a specified attribute. pub fn take_attr( attrs: &mut Vec, target: &str, ) -> Option { attrs .iter() .position(|attr| attr.path().is_ident(target)) .map(|i| attrs.remove(i)) } /// Ensure that no unrecognized attributes remain. pub fn validate_attrs(attrs: &[syn::Attribute]) -> Result<()> { for attr in attrs { if !attr.path().is_ident("doc") && !attr.path().is_ident("derive") { let ident = attr.path().get_ident().unwrap(); bail!(ident, "unrecognized attribute: {ident}"); } } Ok(()) } /// Convert an identifier to a kebab-case string. pub fn kebab_case(name: &Ident) -> String { name.to_string().to_kebab_case() } /// Extract documentation comments from an attribute list. pub fn documentation(attrs: &[syn::Attribute]) -> String { let mut doc = String::new(); // Parse doc comments. for attr in attrs { if let syn::Meta::NameValue(meta) = &attr.meta { if meta.path.is_ident("doc") { if let syn::Expr::Lit(lit) = &meta.value { if let syn::Lit::Str(string) = &lit.lit { let full = string.value(); let line = full.strip_prefix(' ').unwrap_or(&full); doc.push_str(line); doc.push('\n'); } } } } } doc.trim().into() } /// Extract a line of metadata from documentation. pub fn meta_line<'a>(lines: &mut Vec<&'a str>, key: &str) -> Result<&'a str> { match lines.last().and_then(|line| line.strip_prefix(&format!("{key}:"))) { Some(value) => { lines.pop(); Ok(value.trim()) } None => bail!(callsite, "missing metadata key: {key}"), } } /// Creates a block responsible for building a `Scope`. pub fn create_scope_builder(scope_block: Option<&BlockWithReturn>) -> TokenStream { if let Some(BlockWithReturn { prefix, expr }) = scope_block { quote! { { let mut scope = ::typst::eval::Scope::deduplicating(); #(#prefix);* #expr } } } else { quote! { ::typst::eval::Scope::new() } } } /// Quotes an option literally. pub fn quote_option(option: &Option) -> TokenStream { if let Some(value) = option { quote! { Some(#value) } } else { quote! { None } } }