typst-macros 0.15.1

Proc-macros for Typst.
Documentation
use heck::ToKebabCase;
use proc_macro2::TokenStream;
use quote::quote;
use syn::parse::{Parse, ParseStream};
use syn::punctuated::Punctuated;
use syn::{MetaNameValue, Result, Token, parse_quote};

use crate::util::{BareType, foundations, kw, parse_flag};

/// Expand the `#[scope]` macro.
pub fn scope(stream: TokenStream, item: syn::Item) -> Result<TokenStream> {
    let meta: Meta = syn::parse2(stream)?;
    let syn::Item::Impl(mut item) = item else {
        bail!(item, "expected module or impl item");
    };

    let self_ty = &item.self_ty;

    let mut primitive_ident_ext = None;
    if let syn::Type::Path(syn::TypePath { path, .. }) = self_ty.as_ref()
        && let Some(ident) = path.get_ident()
        && meta.ext
    {
        let ident_ext = quote::format_ident!("{ident}Ext");
        primitive_ident_ext = Some(ident_ext);
    }

    let self_ty_expr = match &primitive_ident_ext {
        None => quote! { #self_ty },
        Some(ident_ext) => quote! { <#self_ty as #ident_ext> },
    };

    let mut definitions = vec![];
    let mut constructor = quote! { None };
    for child in &mut item.items {
        let bare: BareType;
        let (mut def, attrs) = match child {
            syn::ImplItem::Const(item) => {
                (handle_const(&self_ty_expr, item)?, &item.attrs)
            }
            syn::ImplItem::Fn(item) => (
                match handle_fn(self_ty, item)? {
                    FnKind::Member(tokens) => tokens,
                    FnKind::Constructor(tokens) => {
                        constructor = tokens;
                        continue;
                    }
                },
                &item.attrs,
            ),
            syn::ImplItem::Verbatim(item) => {
                bare = syn::parse2(item.clone())?;
                (handle_type_or_elem(&bare)?, &bare.attrs)
            }
            _ => bail!(child, "unexpected item in scope"),
        };

        if let Some(attr) = attrs.iter().find(|attr| attr.path().is_ident("deprecated")) {
            match &attr.meta {
                syn::Meta::NameValue(pair) if pair.path.is_ident("deprecated") => {
                    let message = &pair.value;
                    def = quote! { #def.deprecated(#message) }
                }
                syn::Meta::List(list) if list.path.is_ident("deprecated") => {
                    let args = list.parse_args_with(
                        Punctuated::<MetaNameValue, Token![,]>::parse_separated_nonempty,
                    )?;

                    let mut deprecation =
                        quote! { crate::foundations::Deprecation::new() };

                    if let Some(message) = args.iter().find_map(|pair| {
                        pair.path.is_ident("message").then_some(&pair.value)
                    }) {
                        deprecation = quote! { #deprecation.with_message(#message) }
                    }

                    if let Some(version) = args.iter().find_map(|pair| {
                        pair.path.is_ident("until").then_some(&pair.value)
                    }) {
                        deprecation = quote! { #deprecation.with_until(#version) }
                    }

                    def = quote! { #def.deprecated(#deprecation) }
                }
                _ => {}
            }
        }

        definitions.push(def);
    }

    item.items.retain(|item| !matches!(item, syn::ImplItem::Verbatim(_)));

    let base = match &primitive_ident_ext {
        None => quote! { #item },
        Some(ident_ext) => rewrite_primitive_base(&item, ident_ext),
    };

    Ok(quote! {
        #base

        impl #foundations::NativeScope for #self_ty {
            fn constructor() -> ::std::option::Option<&'static #foundations::NativeFuncData> {
                #constructor
            }

            #[allow(deprecated)]
            fn scope() -> #foundations::Scope {
                let mut scope = #foundations::Scope::deduplicating();
                #(#definitions;)*
                scope
            }
        }
    })
}

/// The `..` in `#[scope(..)]`.
struct Meta {
    /// Whether this the scope should be implemented through an extension
    /// trait instead of an inherent impl.
    ext: bool,
}

impl Parse for Meta {
    fn parse(input: ParseStream) -> Result<Self> {
        Ok(Self { ext: parse_flag::<kw::ext>(input)? })
    }
}

/// Process a const item and returns its definition.
fn handle_const(self_ty: &TokenStream, item: &syn::ImplItemConst) -> Result<TokenStream> {
    let ident = &item.ident;
    let name = ident.to_string().to_kebab_case();
    Ok(quote! { scope.define(#name, #self_ty::#ident) })
}

/// Process a type item.
fn handle_type_or_elem(item: &BareType) -> Result<TokenStream> {
    let ident = &item.ident;
    let define = if item.attrs.iter().any(|attr| attr.path().is_ident("elem")) {
        quote! { define_elem }
    } else {
        quote! { define_type }
    };
    Ok(quote! { scope.#define::<#ident>() })
}

/// Process a function, return its definition, and register it as a constructor
/// if applicable.
fn handle_fn(self_ty: &syn::Type, item: &mut syn::ImplItemFn) -> Result<FnKind> {
    let Some(attr) = item.attrs.iter_mut().find(|attr| attr.meta.path().is_ident("func"))
    else {
        bail!(item, "scope function is missing #[func] attribute");
    };

    let ident_data = quote::format_ident!("{}_data", item.sig.ident);

    match &mut attr.meta {
        syn::Meta::Path(_) => {
            *attr = parse_quote! { #[func(parent = #self_ty)] };
        }
        syn::Meta::List(list) => {
            let tokens = &list.tokens;
            let meta: crate::func::Meta = syn::parse2(tokens.clone())?;
            list.tokens = quote! { #tokens, parent = #self_ty };
            if meta.constructor {
                return Ok(FnKind::Constructor(quote! { Some(#self_ty::#ident_data()) }));
            }
        }
        syn::Meta::NameValue(_) => bail!(attr.meta, "invalid func attribute"),
    }

    Ok(FnKind::Member(quote! { scope.define_func_with_data(#self_ty::#ident_data()) }))
}

enum FnKind {
    Constructor(TokenStream),
    Member(TokenStream),
}

/// Rewrite an impl block for a primitive into a trait + trait impl.
fn rewrite_primitive_base(item: &syn::ItemImpl, ident_ext: &syn::Ident) -> TokenStream {
    let mut sigs = vec![];
    let mut items = vec![];
    for sub in &item.items {
        match sub.clone() {
            syn::ImplItem::Fn(mut func) => {
                func.vis = syn::Visibility::Inherited;
                items.push(func.clone());

                let mut sig = func.sig;
                let inputs = sig.inputs.iter().cloned().map(|mut input| {
                    if let syn::FnArg::Typed(typed) = &mut input {
                        typed.attrs.clear();
                    }
                    input
                });
                sig.inputs = parse_quote! { #(#inputs),* };

                let ident_data = quote::format_ident!("{}_data", sig.ident);
                sigs.push(quote! { #sig; });
                sigs.push(quote! {
                    fn #ident_data() -> &'static #foundations::NativeFuncData;
                });
            }

            syn::ImplItem::Const(cons) => {
                sigs.push(quote! { #cons });
            }

            _ => {}
        }
    }

    let self_ty = &item.self_ty;
    quote! {
        #[allow(non_camel_case_types)]
        trait #ident_ext {
            #(#sigs)*
        }

        impl #ident_ext for #self_ty {
            #(#items)*
        }
    }
}