diff options
| author | Laurenz <laurmaedje@gmail.com> | 2023-09-11 14:38:54 +0200 |
|---|---|---|
| committer | Laurenz <laurmaedje@gmail.com> | 2023-09-11 14:38:54 +0200 |
| commit | 8f36fca68447a5d42a3d54b5fac7e5546ee244be (patch) | |
| tree | c6f12b205d0642eae33824e5d03593784c9ec111 /crates/typst-macros/src/util.rs | |
| parent | 921b40cf9cb75c6412e2421130671b08dcf1ee13 (diff) | |
Better proc macros
Diffstat (limited to 'crates/typst-macros/src/util.rs')
| -rw-r--r-- | crates/typst-macros/src/util.rs | 229 |
1 files changed, 167 insertions, 62 deletions
diff --git a/crates/typst-macros/src/util.rs b/crates/typst-macros/src/util.rs index 389fed06..890db779 100644 --- a/crates/typst-macros/src/util.rs +++ b/crates/typst-macros/src/util.rs @@ -1,5 +1,7 @@ -use heck::ToKebabCase; +use heck::{ToKebabCase, ToTitleCase}; use quote::ToTokens; +use syn::token::Token; +use syn::Attribute; use super::*; @@ -19,25 +21,27 @@ macro_rules! bail { }; } -/// For parsing attributes of the form: -/// #[attr( -/// statement; -/// statement; -/// returned_expression -/// )] -pub struct BlockWithReturn { - pub prefix: Vec<syn::Stmt>, - pub expr: syn::Stmt, -} +/// Extract documentation comments from an attribute list. +pub fn documentation(attrs: &[syn::Attribute]) -> String { + let mut doc = String::new(); -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 }) + // 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() } /// Whether an attribute list has a specified attribute. @@ -83,63 +87,164 @@ pub fn validate_attrs(attrs: &[syn::Attribute]) -> Result<()> { Ok(()) } -/// Convert an identifier to a kebab-case string. -pub fn kebab_case(name: &Ident) -> String { - name.to_string().to_kebab_case() +/// 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 } + } } -/// Extract documentation comments from an attribute list. -pub fn documentation(attrs: &[syn::Attribute]) -> String { - let mut doc = String::new(); +/// Parse a metadata key-value pair, separated by `=`. +pub fn parse_key_value<K: Token + Default + Parse, V: Parse>( + input: ParseStream, +) -> Result<Option<V>> { + if !input.peek(|_| K::default()) { + return Ok(None); + } - // 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'); - } - } - } - } + let _: K = input.parse()?; + let _: Token![=] = input.parse()?; + let value: V = input.parse::<V>()?; + eat_comma(input); + Ok(Some(value)) +} + +/// Parse a metadata key-array pair, separated by `=`. +pub fn parse_key_value_array<K: Token + Default + Parse, V: Parse>( + input: ParseStream, +) -> Result<Vec<V>> { + Ok(parse_key_value::<K, Array<V>>(input)?.map_or(vec![], |array| array.0)) +} + +/// Parse a metadata key-string pair, separated by `=`. +pub fn parse_string<K: Token + Default + Parse>( + input: ParseStream, +) -> Result<Option<String>> { + Ok(parse_key_value::<K, syn::LitStr>(input)?.map(|s| s.value())) +} + +/// Parse a metadata key-string pair, separated by `=`. +pub fn parse_string_array<K: Token + Default + Parse>( + input: ParseStream, +) -> Result<Vec<String>> { + Ok(parse_key_value_array::<K, syn::LitStr>(input)? + .into_iter() + .map(|lit| lit.value()) + .collect()) +} + +/// Parse a metadata flag that can be present or not. +pub fn parse_flag<K: Token + Default + Parse>(input: ParseStream) -> Result<bool> { + if input.peek(|_| K::default()) { + let _: K = input.parse()?; + eat_comma(input); + return Ok(true); } + Ok(false) +} - doc.trim().into() +/// Parse a comma if there is one. +pub fn eat_comma(input: ParseStream) { + if input.peek(Token![,]) { + let _: Token![,] = input.parse().unwrap(); + } } -/// 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()) +/// Determine the normal and title case name of a function, type, or element. +pub fn determine_name_and_title( + specified_name: Option<String>, + specified_title: Option<String>, + ident: &syn::Ident, + trim: Option<fn(&str) -> &str>, +) -> Result<(String, String)> { + let name = { + let trim = trim.unwrap_or(|s| s); + let default = trim(&ident.to_string()).to_kebab_case(); + if specified_name.as_ref() == Some(&default) { + bail!(ident, "name was specified unncessarily"); } - None => bail!(callsite, "missing metadata key: {key}"), + specified_name.unwrap_or(default) + }; + + let title = { + let default = name.to_title_case(); + if specified_title.as_ref() == Some(&default) { + bail!(ident, "title was specified unncessarily"); + } + specified_title.unwrap_or(default) + }; + + Ok((name, title)) +} + +/// A generic parseable array. +struct Array<T>(Vec<T>); + +impl<T: Parse> Parse for Array<T> { + fn parse(input: ParseStream) -> Result<Self> { + let content; + syn::bracketed!(content in input); + + let mut elems = Vec::new(); + while !content.is_empty() { + let first: T = content.parse()?; + elems.push(first); + if !content.is_empty() { + let _: Token![,] = content.parse()?; + } + } + + Ok(Self(elems)) } } -/// 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() } +/// 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 }) } } -/// 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 } +pub mod kw { + syn::custom_keyword!(name); + syn::custom_keyword!(title); + syn::custom_keyword!(scope); + syn::custom_keyword!(constructor); + syn::custom_keyword!(keywords); + syn::custom_keyword!(parent); +} + +/// Parse a bare `type Name;` item. +pub struct BareType { + pub attrs: Vec<Attribute>, + pub type_token: Token![type], + pub ident: Ident, + pub semi_token: Token![;], +} + +impl Parse for BareType { + fn parse(input: ParseStream) -> Result<Self> { + Ok(BareType { + attrs: input.call(Attribute::parse_outer)?, + type_token: input.parse()?, + ident: input.parse()?, + semi_token: input.parse()?, + }) } } |
