summaryrefslogtreecommitdiff
path: root/crates/typst-macros/src/util.rs
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2023-09-11 14:38:54 +0200
committerLaurenz <laurmaedje@gmail.com>2023-09-11 14:38:54 +0200
commit8f36fca68447a5d42a3d54b5fac7e5546ee244be (patch)
treec6f12b205d0642eae33824e5d03593784c9ec111 /crates/typst-macros/src/util.rs
parent921b40cf9cb75c6412e2421130671b08dcf1ee13 (diff)
Better proc macros
Diffstat (limited to 'crates/typst-macros/src/util.rs')
-rw-r--r--crates/typst-macros/src/util.rs229
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()?,
+ })
}
}