use unscanny::Scanner; use super::*; /// Expand the `#[func]` macro. pub fn func(item: syn::Item) -> Result { let docs = match &item { syn::Item::Struct(item) => documentation(&item.attrs), syn::Item::Enum(item) => documentation(&item.attrs), syn::Item::Fn(item) => documentation(&item.attrs), _ => String::new(), }; let first = docs.lines().next().unwrap(); let display = first.strip_prefix("# ").unwrap(); let display = display.trim(); let mut docs = docs[first.len()..].to_string(); let example = example(&mut docs, 2); let (params, returns) = params(&mut docs)?; let syntax = quote_option(section(&mut docs, "Syntax", 2)); let category = section(&mut docs, "Category", 2).expect("missing category"); let example = quote_option(example); let docs = docs.trim(); if docs.contains("# ") { bail!(item, "unrecognized heading"); } let info = quote! { ::typst::model::FuncInfo { name, display: #display, category: #category, docs: #docs, example: #example, syntax: #syntax, params: ::std::vec![#(#params),*], returns: ::std::vec![#(#returns),*] } }; if let syn::Item::Fn(item) = &item { let vis = &item.vis; let ident = &item.sig.ident; let s = ident.to_string(); let mut chars = s.trim_end_matches('_').chars(); let ty = quote::format_ident!( "{}{}Func", chars.next().unwrap().to_ascii_uppercase(), chars.as_str() ); let full = if item.sig.inputs.len() == 1 { quote! { |_, args| #ident(args) } } else { quote! { #ident } }; Ok(quote! { #item #[doc(hidden)] #vis enum #ty {} impl::typst::model::FuncType for #ty { fn create_func(name: &'static str) -> ::typst::model::Func { ::typst::model::Func::from_fn(#full, #info) } } }) } else { let (ident, generics) = match &item { syn::Item::Struct(s) => (&s.ident, &s.generics), syn::Item::Enum(s) => (&s.ident, &s.generics), _ => bail!(item, "only structs, enums, and functions are supported"), }; let (params, args, clause) = generics.split_for_impl(); Ok(quote! { #item impl #params ::typst::model::FuncType for #ident #args #clause { fn create_func(name: &'static str) -> ::typst::model::Func { ::typst::model::Func::from_node::(#info) } } }) } } /// Extract a section. pub fn section(docs: &mut String, title: &str, level: usize) -> Option { let hashtags = "#".repeat(level); let needle = format!("\n{hashtags} {title}\n"); let start = docs.find(&needle)?; let rest = &docs[start..]; let len = rest[1..] .find("\n# ") .or_else(|| rest[1..].find("\n## ")) .or_else(|| rest[1..].find("\n### ")) .map(|x| 1 + x) .unwrap_or(rest.len()); let end = start + len; let section = docs[start + needle.len()..end].trim().to_owned(); docs.replace_range(start..end, ""); Some(section) } /// Parse the example section. pub fn example(docs: &mut String, level: usize) -> Option { let section = section(docs, "Example", level)?; let mut s = unscanny::Scanner::new(§ion); let count = s.eat_while('`').len(); let term = "`".repeat(count); let text = s.eat_until(term.as_str()).trim(); s.expect(term.as_str()); Some(text.into()) } /// Parse the parameter section. fn params(docs: &mut String) -> Result<(Vec, Vec)> { let Some(section) = section(docs, "Parameters", 2) else { return Ok((vec![], vec![])); }; let mut s = Scanner::new(§ion); let mut infos = vec![]; let mut returns = vec![]; while s.eat_if('-') { let mut named = false; let mut positional = false; let mut required = false; let mut variadic = false; let mut settable = false; s.eat_whitespace(); let name = s.eat_until(':'); s.expect(": "); if name == "returns" { returns = s .eat_until('\n') .split(" or ") .map(str::trim) .map(Into::into) .collect(); s.eat_whitespace(); continue; } let ty: syn::Type = syn::parse_str(s.eat_until(char::is_whitespace))?; s.eat_whitespace(); s.expect('('); for part in s.eat_until(')').split(',').map(str::trim).filter(|s| !s.is_empty()) { match part { "named" => named = true, "positional" => positional = true, "required" => required = true, "variadic" => variadic = true, "settable" => settable = true, _ => bail!(callsite, "unknown parameter flag {:?}", part), } } if (!named && !positional) || (variadic && !positional) || (named && variadic) || (required && variadic) { bail!(callsite, "invalid combination of parameter flags"); } s.expect(')'); let mut docs = dedent(s.eat_until("\n-").trim()); let example = quote_option(example(&mut docs, 3)); let docs = docs.trim(); infos.push(quote! { ::typst::model::ParamInfo { name: #name, docs: #docs, example: #example, cast: <#ty as ::typst::model::Cast< ::typst::syntax::Spanned<::typst::model::Value> >>::describe(), named: #named, positional: #positional, required: #required, variadic: #variadic, settable: #settable, } }); s.eat_whitespace(); } Ok((infos, returns)) }