diff options
| author | Laurenz <laurmaedje@gmail.com> | 2023-07-02 19:59:52 +0200 |
|---|---|---|
| committer | Laurenz <laurmaedje@gmail.com> | 2023-07-02 20:07:43 +0200 |
| commit | ebfdb1dafa430786db10dad2ef7d5467c1bdbed1 (patch) | |
| tree | 2bbc24ddb4124c4bb14dec0e536129d4de37b056 /crates/typst-macros/src/util.rs | |
| parent | 3ab19185093d7709f824b95b979060ce125389d8 (diff) | |
Move everything into `crates/` directory
Diffstat (limited to 'crates/typst-macros/src/util.rs')
| -rw-r--r-- | crates/typst-macros/src/util.rs | 145 |
1 files changed, 145 insertions, 0 deletions
diff --git a/crates/typst-macros/src/util.rs b/crates/typst-macros/src/util.rs new file mode 100644 index 00000000..389fed06 --- /dev/null +++ b/crates/typst-macros/src/util.rs @@ -0,0 +1,145 @@ +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<syn::Stmt>, + pub expr: syn::Stmt, +} + +impl Parse for BlockWithReturn { + fn parse(input: ParseStream) -> Result<Self> { + 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<syn::Attribute>, target: &str) -> bool { + take_attr(attrs, target).is_some() +} + +/// Whether an attribute list has a specified attribute. +pub fn parse_attr<T: Parse>( + attrs: &mut Vec<syn::Attribute>, + target: &str, +) -> Result<Option<Option<T>>> { + 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<syn::Attribute>, + target: &str, +) -> Option<syn::Attribute> { + 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<T: ToTokens>(option: &Option<T>) -> TokenStream { + if let Some(value) = option { + quote! { Some(#value) } + } else { + quote! { None } + } +} |
