diff --git a/docs/src/lib.rs b/docs/src/lib.rs index 30501e224..2a7b7c5f1 100644 --- a/docs/src/lib.rs +++ b/docs/src/lib.rs @@ -359,6 +359,7 @@ fn category_outline(kind: &str) -> Vec { pub struct FuncModel { pub path: Vec<&'static str>, pub display: &'static str, + pub keywords: Option<&'static str>, pub oneliner: &'static str, pub element: bool, pub details: Html, @@ -431,6 +432,7 @@ fn func_model( FuncModel { path, display: info.display, + keywords: info.keywords, oneliner: oneliner(docs), element: func.element().is_some(), details: Html::markdown_with_id_base(resolver, docs, id_base), diff --git a/library/src/meta/outline.rs b/library/src/meta/outline.rs index 2c51539d8..a45b2a346 100644 --- a/library/src/meta/outline.rs +++ b/library/src/meta/outline.rs @@ -49,6 +49,7 @@ use crate::text::{LinebreakElem, SpaceElem, TextElem}; /// /// Display: Outline /// Category: meta +/// Keywords: Table of Contents #[element(Show, Finalize, LocalName)] pub struct OutlineElem { /// The title of the outline. diff --git a/macros/src/element.rs b/macros/src/element.rs index 403af103a..618309392 100644 --- a/macros/src/element.rs +++ b/macros/src/element.rs @@ -10,6 +10,7 @@ struct Elem { name: String, display: String, category: String, + keywords: Option, docs: String, vis: syn::Visibility, ident: Ident, @@ -126,6 +127,7 @@ fn prepare(stream: TokenStream, body: &syn::ItemStruct) -> Result { let mut attrs = body.attrs.clone(); let docs = documentation(&attrs); let mut lines = docs.split('\n').collect(); + let keywords = meta_line(&mut lines, "Keywords").ok().map(Into::into); let category = meta_line(&mut lines, "Category")?.into(); let display = meta_line(&mut lines, "Display")?.into(); let docs = lines.join("\n").trim().into(); @@ -134,6 +136,7 @@ fn prepare(stream: TokenStream, body: &syn::ItemStruct) -> Result { name: body.ident.to_string().trim_end_matches("Elem").to_lowercase(), display, category, + keywords, docs, vis: body.vis.clone(), ident: body.ident.clone(), @@ -332,7 +335,7 @@ fn create_set_field_method(field: &Field) -> TokenStream { /// Create the element's `Pack` implementation. fn create_pack_impl(element: &Elem) -> TokenStream { - let Elem { ident, name, display, category, docs, .. } = element; + let Elem { ident, name, display, keywords, category, docs, .. } = element; let vtable_func = create_vtable_func(element); let infos = element .fields @@ -340,6 +343,7 @@ fn create_pack_impl(element: &Elem) -> TokenStream { .filter(|field| !field.internal && !field.synthesized) .map(create_param_info); let scope = create_scope_builder(element.scope.as_ref()); + let keywords = quote_option(keywords); quote! { impl ::typst::model::Element for #ident { fn pack(self) -> ::typst::model::Content { @@ -362,6 +366,7 @@ fn create_pack_impl(element: &Elem) -> TokenStream { info: ::typst::eval::Lazy::new(|| typst::eval::FuncInfo { name: #name, display: #display, + keywords: #keywords, docs: #docs, params: ::std::vec![#(#infos),*], returns: ::std::vec!["content"], diff --git a/macros/src/func.rs b/macros/src/func.rs index f3de68229..2d38c01a4 100644 --- a/macros/src/func.rs +++ b/macros/src/func.rs @@ -12,6 +12,7 @@ struct Func { name: String, display: String, category: String, + keywords: Option, docs: String, vis: syn::Visibility, ident: Ident, @@ -80,6 +81,7 @@ fn prepare(item: &syn::ItemFn) -> Result { .split(" or ") .map(Into::into) .collect(); + let keywords = meta_line(&mut lines, "Keywords").ok().map(Into::into); let category = meta_line(&mut lines, "Category")?.into(); let display = meta_line(&mut lines, "Display")?.into(); let docs = lines.join("\n").trim().into(); @@ -88,6 +90,7 @@ fn prepare(item: &syn::ItemFn) -> Result { name: sig.ident.to_string().replace('_', ""), display, category, + keywords, docs, vis: item.vis.clone(), ident: sig.ident.clone(), @@ -105,6 +108,7 @@ fn create(func: &Func) -> TokenStream { let Func { name, display, + keywords, category, docs, vis, @@ -117,6 +121,7 @@ fn create(func: &Func) -> TokenStream { let handlers = params.iter().filter(|param| !param.external).map(create_param_parser); let params = params.iter().map(create_param_info); let scope = create_scope_builder(func.scope.as_ref()); + let keywords = quote_option(keywords); quote! { #[doc = #docs] #vis fn #ident() -> &'static ::typst::eval::NativeFunc { @@ -129,6 +134,7 @@ fn create(func: &Func) -> TokenStream { info: ::typst::eval::Lazy::new(|| typst::eval::FuncInfo { name: #name, display: #display, + keywords: #keywords, docs: #docs, params: ::std::vec![#(#params),*], returns: ::std::vec![#(#returns),*], diff --git a/macros/src/util.rs b/macros/src/util.rs index 6b683e5d2..2e12ef17c 100644 --- a/macros/src/util.rs +++ b/macros/src/util.rs @@ -1,4 +1,5 @@ use heck::ToKebabCase; +use quote::ToTokens; use super::*; @@ -104,13 +105,16 @@ pub fn documentation(attrs: &[syn::Attribute]) -> String { /// Extract a line of metadata from documentation. pub fn meta_line<'a>(lines: &mut Vec<&'a str>, key: &str) -> Result<&'a str> { - match lines.pop().and_then(|line| line.strip_prefix(&format!("{key}:"))) { - Some(value) => Ok(value.trim()), + 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. +/// 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! { { @@ -122,3 +126,12 @@ pub fn create_scope_builder(scope_block: Option<&BlockWithReturn>) -> TokenStrea 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 } + } +} diff --git a/src/eval/func.rs b/src/eval/func.rs index a8afa36d3..75231fc97 100644 --- a/src/eval/func.rs +++ b/src/eval/func.rs @@ -249,6 +249,8 @@ pub struct FuncInfo { pub name: &'static str, /// The display name of the function. pub display: &'static str, + /// A string of keywords. + pub keywords: Option<&'static str>, /// Documentation for the function. pub docs: &'static str, /// Details about the function's parameters.