summaryrefslogtreecommitdiff
path: root/crates/typst-macros/src
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2023-07-02 19:59:52 +0200
committerLaurenz <laurmaedje@gmail.com>2023-07-02 20:07:43 +0200
commitebfdb1dafa430786db10dad2ef7d5467c1bdbed1 (patch)
tree2bbc24ddb4124c4bb14dec0e536129d4de37b056 /crates/typst-macros/src
parent3ab19185093d7709f824b95b979060ce125389d8 (diff)
Move everything into `crates/` directory
Diffstat (limited to 'crates/typst-macros/src')
-rw-r--r--crates/typst-macros/src/castable.rs331
-rw-r--r--crates/typst-macros/src/element.rs554
-rw-r--r--crates/typst-macros/src/func.rs268
-rw-r--r--crates/typst-macros/src/lib.rs63
-rw-r--r--crates/typst-macros/src/symbols.rs78
-rw-r--r--crates/typst-macros/src/util.rs145
6 files changed, 1439 insertions, 0 deletions
diff --git a/crates/typst-macros/src/castable.rs b/crates/typst-macros/src/castable.rs
new file mode 100644
index 00000000..05c1b4d1
--- /dev/null
+++ b/crates/typst-macros/src/castable.rs
@@ -0,0 +1,331 @@
+use super::*;
+
+/// Expand the `#[derive(Cast)]` macro.
+pub fn derive_cast(item: &DeriveInput) -> Result<TokenStream> {
+ let ty = &item.ident;
+
+ let syn::Data::Enum(data) = &item.data else {
+ bail!(item, "only enums are supported");
+ };
+
+ let mut variants = vec![];
+ for variant in &data.variants {
+ if let Some((_, expr)) = &variant.discriminant {
+ bail!(expr, "explicit discriminant is not allowed");
+ }
+
+ let string = if let Some(attr) =
+ variant.attrs.iter().find(|attr| attr.path().is_ident("string"))
+ {
+ attr.parse_args::<syn::LitStr>()?.value()
+ } else {
+ kebab_case(&variant.ident)
+ };
+
+ variants.push(Variant {
+ ident: variant.ident.clone(),
+ string,
+ docs: documentation(&variant.attrs),
+ });
+ }
+
+ let strs_to_variants = variants.iter().map(|Variant { ident, string, docs }| {
+ quote! {
+ #[doc = #docs]
+ #string => Self::#ident
+ }
+ });
+
+ let variants_to_strs = variants.iter().map(|Variant { ident, string, .. }| {
+ quote! {
+ #ty::#ident => #string
+ }
+ });
+
+ Ok(quote! {
+ ::typst::eval::cast! {
+ #ty,
+ self => ::typst::eval::IntoValue::into_value(match self {
+ #(#variants_to_strs),*
+ }),
+ #(#strs_to_variants),*
+ }
+ })
+}
+
+/// An enum variant in a `derive(Cast)`.
+struct Variant {
+ ident: Ident,
+ string: String,
+ docs: String,
+}
+
+/// Expand the `cast!` macro.
+pub fn cast(stream: TokenStream) -> Result<TokenStream> {
+ let input: CastInput = syn::parse2(stream)?;
+ let ty = &input.ty;
+ let eval = quote! { ::typst::eval };
+
+ let castable_body = create_castable_body(&input);
+ let describe_body = create_describe_body(&input);
+ let into_value_body = create_into_value_body(&input);
+ let from_value_body = create_from_value_body(&input);
+
+ let reflect = (!input.from_value.is_empty() || input.name.is_some()).then(|| {
+ quote! {
+ impl #eval::Reflect for #ty {
+ fn describe() -> #eval::CastInfo {
+ #describe_body
+ }
+
+ fn castable(value: &#eval::Value) -> bool {
+ #castable_body
+ }
+ }
+ }
+ });
+
+ let into_value = (input.into_value.is_some() || input.name.is_some()).then(|| {
+ quote! {
+ impl #eval::IntoValue for #ty {
+ fn into_value(self) -> #eval::Value {
+ #into_value_body
+ }
+ }
+ }
+ });
+
+ let from_value = (!input.from_value.is_empty() || input.name.is_some()).then(|| {
+ quote! {
+ impl #eval::FromValue for #ty {
+ fn from_value(value: #eval::Value) -> ::typst::diag::StrResult<Self> {
+ #from_value_body
+ }
+ }
+ }
+ });
+
+ let ty = input.name.as_ref().map(|name| {
+ quote! {
+ impl #eval::Type for #ty {
+ const TYPE_NAME: &'static str = #name;
+ }
+ }
+ });
+
+ Ok(quote! {
+ #reflect
+ #into_value
+ #from_value
+ #ty
+ })
+}
+
+/// The input to `cast!`.
+struct CastInput {
+ ty: syn::Type,
+ name: Option<syn::LitStr>,
+ into_value: Option<syn::Expr>,
+ from_value: Punctuated<Cast, Token![,]>,
+}
+
+impl Parse for CastInput {
+ fn parse(input: ParseStream) -> Result<Self> {
+ let ty;
+ let mut name = None;
+ if input.peek(syn::Token![type]) {
+ let _: syn::Token![type] = input.parse()?;
+ ty = input.parse()?;
+ let _: syn::Token![:] = input.parse()?;
+ name = Some(input.parse()?);
+ } else {
+ ty = input.parse()?;
+ }
+
+ let _: syn::Token![,] = input.parse()?;
+
+ let mut into_value = None;
+ if input.peek(syn::Token![self]) {
+ let _: syn::Token![self] = input.parse()?;
+ let _: syn::Token![=>] = input.parse()?;
+ into_value = Some(input.parse()?);
+ let _: syn::Token![,] = input.parse()?;
+ }
+
+ let from_value = Punctuated::parse_terminated(input)?;
+ Ok(Self { ty, name, into_value, from_value })
+ }
+}
+
+impl Parse for Cast {
+ fn parse(input: ParseStream) -> Result<Self> {
+ let attrs = input.call(syn::Attribute::parse_outer)?;
+ let pattern = input.parse()?;
+ let _: syn::Token![=>] = input.parse()?;
+ let expr = input.parse()?;
+ Ok(Self { attrs, pattern, expr })
+ }
+}
+
+impl Parse for Pattern {
+ fn parse(input: ParseStream) -> Result<Self> {
+ if input.peek(syn::LitStr) {
+ Ok(Pattern::Str(input.parse()?))
+ } else {
+ let pat = syn::Pat::parse_single(input)?;
+ let _: syn::Token![:] = input.parse()?;
+ let ty = input.parse()?;
+ Ok(Pattern::Ty(pat, ty))
+ }
+ }
+}
+
+/// A single cast, e.g. `v: i64 => Self::Int(v)`.
+struct Cast {
+ attrs: Vec<syn::Attribute>,
+ pattern: Pattern,
+ expr: syn::Expr,
+}
+
+/// A pattern in a cast, e.g.`"ascender"` or `v: i64`.
+enum Pattern {
+ Str(syn::LitStr),
+ Ty(syn::Pat, syn::Type),
+}
+
+fn create_castable_body(input: &CastInput) -> TokenStream {
+ let mut strings = vec![];
+ let mut casts = vec![];
+
+ for cast in &input.from_value {
+ match &cast.pattern {
+ Pattern::Str(lit) => {
+ strings.push(quote! { #lit => return true });
+ }
+ Pattern::Ty(_, ty) => {
+ casts.push(quote! {
+ if <#ty as ::typst::eval::Reflect>::castable(value) {
+ return true;
+ }
+ });
+ }
+ }
+ }
+
+ let dynamic_check = input.name.is_some().then(|| {
+ quote! {
+ if let ::typst::eval::Value::Dyn(dynamic) = &value {
+ if dynamic.is::<Self>() {
+ return true;
+ }
+ }
+ }
+ });
+
+ let str_check = (!strings.is_empty()).then(|| {
+ quote! {
+ if let ::typst::eval::Value::Str(string) = &value {
+ match string.as_str() {
+ #(#strings,)*
+ _ => {}
+ }
+ }
+ }
+ });
+
+ quote! {
+ #dynamic_check
+ #str_check
+ #(#casts)*
+ false
+ }
+}
+
+fn create_describe_body(input: &CastInput) -> TokenStream {
+ let mut infos = vec![];
+
+ for cast in &input.from_value {
+ let docs = documentation(&cast.attrs);
+ infos.push(match &cast.pattern {
+ Pattern::Str(lit) => {
+ quote! {
+ ::typst::eval::CastInfo::Value(
+ ::typst::eval::IntoValue::into_value(#lit),
+ #docs,
+ )
+ }
+ }
+ Pattern::Ty(_, ty) => {
+ quote! { <#ty as ::typst::eval::Reflect>::describe() }
+ }
+ });
+ }
+
+ if let Some(name) = &input.name {
+ infos.push(quote! {
+ ::typst::eval::CastInfo::Type(#name)
+ });
+ }
+
+ quote! {
+ #(#infos)+*
+ }
+}
+
+fn create_into_value_body(input: &CastInput) -> TokenStream {
+ if let Some(expr) = &input.into_value {
+ quote! { #expr }
+ } else {
+ quote! { ::typst::eval::Value::dynamic(self) }
+ }
+}
+
+fn create_from_value_body(input: &CastInput) -> TokenStream {
+ let mut string_arms = vec![];
+ let mut cast_checks = vec![];
+
+ for cast in &input.from_value {
+ let expr = &cast.expr;
+ match &cast.pattern {
+ Pattern::Str(lit) => {
+ string_arms.push(quote! { #lit => return Ok(#expr) });
+ }
+ Pattern::Ty(binding, ty) => {
+ cast_checks.push(quote! {
+ if <#ty as ::typst::eval::Reflect>::castable(&value) {
+ let #binding = <#ty as ::typst::eval::FromValue>::from_value(value)?;
+ return Ok(#expr);
+ }
+ });
+ }
+ }
+ }
+
+ let dynamic_check = input.name.is_some().then(|| {
+ quote! {
+ if let ::typst::eval::Value::Dyn(dynamic) = &value {
+ if let Some(concrete) = dynamic.downcast::<Self>() {
+ return Ok(concrete.clone());
+ }
+ }
+ }
+ });
+
+ let str_check = (!string_arms.is_empty()).then(|| {
+ quote! {
+ if let ::typst::eval::Value::Str(string) = &value {
+ match string.as_str() {
+ #(#string_arms,)*
+ _ => {}
+ }
+ }
+ }
+ });
+
+ quote! {
+ #dynamic_check
+ #str_check
+ #(#cast_checks)*
+ Err(<Self as ::typst::eval::Reflect>::error(&value))
+ }
+}
diff --git a/crates/typst-macros/src/element.rs b/crates/typst-macros/src/element.rs
new file mode 100644
index 00000000..6ce91fcb
--- /dev/null
+++ b/crates/typst-macros/src/element.rs
@@ -0,0 +1,554 @@
+use super::*;
+
+/// Expand the `#[element]` macro.
+pub fn element(stream: TokenStream, body: &syn::ItemStruct) -> Result<TokenStream> {
+ let element = prepare(stream, body)?;
+ Ok(create(&element))
+}
+
+struct Elem {
+ name: String,
+ display: String,
+ category: String,
+ keywords: Option<String>,
+ docs: String,
+ vis: syn::Visibility,
+ ident: Ident,
+ capable: Vec<Ident>,
+ fields: Vec<Field>,
+ scope: Option<BlockWithReturn>,
+}
+
+struct Field {
+ name: String,
+ docs: String,
+ internal: bool,
+ external: bool,
+ positional: bool,
+ required: bool,
+ variadic: bool,
+ synthesized: bool,
+ fold: bool,
+ resolve: bool,
+ parse: Option<BlockWithReturn>,
+ default: syn::Expr,
+ vis: syn::Visibility,
+ ident: Ident,
+ ident_in: Ident,
+ with_ident: Ident,
+ push_ident: Ident,
+ set_ident: Ident,
+ ty: syn::Type,
+ output: syn::Type,
+}
+
+impl Field {
+ fn inherent(&self) -> bool {
+ self.required || self.variadic
+ }
+
+ fn settable(&self) -> bool {
+ !self.inherent()
+ }
+}
+
+/// Preprocess the element's definition.
+fn prepare(stream: TokenStream, body: &syn::ItemStruct) -> Result<Elem> {
+ let syn::Fields::Named(named) = &body.fields else {
+ bail!(body, "expected named fields");
+ };
+
+ let mut fields = vec![];
+ for field in &named.named {
+ let Some(ident) = field.ident.clone() else {
+ bail!(field, "expected named field");
+ };
+
+ let mut attrs = field.attrs.clone();
+ let variadic = has_attr(&mut attrs, "variadic");
+ let required = has_attr(&mut attrs, "required") || variadic;
+ let positional = has_attr(&mut attrs, "positional") || required;
+
+ if ident == "label" {
+ bail!(ident, "invalid field name");
+ }
+
+ let mut field = Field {
+ name: kebab_case(&ident),
+ docs: documentation(&attrs),
+ internal: has_attr(&mut attrs, "internal"),
+ external: has_attr(&mut attrs, "external"),
+ positional,
+ required,
+ variadic,
+ synthesized: has_attr(&mut attrs, "synthesized"),
+ fold: has_attr(&mut attrs, "fold"),
+ resolve: has_attr(&mut attrs, "resolve"),
+ parse: parse_attr(&mut attrs, "parse")?.flatten(),
+ default: parse_attr(&mut attrs, "default")?
+ .flatten()
+ .unwrap_or_else(|| parse_quote! { ::std::default::Default::default() }),
+ vis: field.vis.clone(),
+ ident: ident.clone(),
+ ident_in: Ident::new(&format!("{}_in", ident), ident.span()),
+ with_ident: Ident::new(&format!("with_{}", ident), ident.span()),
+ push_ident: Ident::new(&format!("push_{}", ident), ident.span()),
+ set_ident: Ident::new(&format!("set_{}", ident), ident.span()),
+ ty: field.ty.clone(),
+ output: field.ty.clone(),
+ };
+
+ if field.required && (field.fold || field.resolve) {
+ bail!(ident, "required fields cannot be folded or resolved");
+ }
+
+ if field.required && !field.positional {
+ bail!(ident, "only positional fields can be required");
+ }
+
+ if field.resolve {
+ let output = &field.output;
+ field.output = parse_quote! { <#output as ::typst::model::Resolve>::Output };
+ }
+ if field.fold {
+ let output = &field.output;
+ field.output = parse_quote! { <#output as ::typst::model::Fold>::Output };
+ }
+
+ validate_attrs(&attrs)?;
+ fields.push(field);
+ }
+
+ let capable = Punctuated::<Ident, Token![,]>::parse_terminated
+ .parse2(stream)?
+ .into_iter()
+ .collect();
+
+ 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();
+
+ let element = Elem {
+ name: body.ident.to_string().trim_end_matches("Elem").to_lowercase(),
+ display,
+ category,
+ keywords,
+ docs,
+ vis: body.vis.clone(),
+ ident: body.ident.clone(),
+ capable,
+ fields,
+ scope: parse_attr(&mut attrs, "scope")?.flatten(),
+ };
+
+ validate_attrs(&attrs)?;
+ Ok(element)
+}
+
+/// Produce the element's definition.
+fn create(element: &Elem) -> TokenStream {
+ let Elem { vis, ident, docs, .. } = element;
+ let all = element.fields.iter().filter(|field| !field.external);
+ let settable = all.clone().filter(|field| !field.synthesized && field.settable());
+
+ // Inherent methods and functions.
+ let new = create_new_func(element);
+ let field_methods = all.clone().map(create_field_method);
+ let field_in_methods = settable.clone().map(create_field_in_method);
+ let with_field_methods = all.clone().map(create_with_field_method);
+ let push_field_methods = all.map(create_push_field_method);
+ let field_style_methods = settable.map(create_set_field_method);
+
+ // Trait implementations.
+ let element_impl = create_pack_impl(element);
+ let construct_impl = element
+ .capable
+ .iter()
+ .all(|capability| capability != "Construct")
+ .then(|| create_construct_impl(element));
+ let set_impl = create_set_impl(element);
+ let locatable_impl = element
+ .capable
+ .iter()
+ .any(|capability| capability == "Locatable")
+ .then(|| quote! { impl ::typst::model::Locatable for #ident {} });
+
+ quote! {
+ #[doc = #docs]
+ #[derive(Debug, Clone, Hash)]
+ #[repr(transparent)]
+ #vis struct #ident(pub ::typst::model::Content);
+
+ impl #ident {
+ #new
+ #(#field_methods)*
+ #(#field_in_methods)*
+ #(#with_field_methods)*
+ #(#push_field_methods)*
+ #(#field_style_methods)*
+
+ /// The element's span.
+ pub fn span(&self) -> ::typst::syntax::Span {
+ self.0.span()
+ }
+
+ /// Set the element's span.
+ pub fn spanned(self, span: ::typst::syntax::Span) -> Self {
+ Self(self.0.spanned(span))
+ }
+ }
+
+ #element_impl
+ #construct_impl
+ #set_impl
+ #locatable_impl
+
+ impl ::typst::eval::IntoValue for #ident {
+ fn into_value(self) -> ::typst::eval::Value {
+ ::typst::eval::Value::Content(self.0)
+ }
+ }
+ }
+}
+
+/// Create the `new` function for the element.
+fn create_new_func(element: &Elem) -> TokenStream {
+ let relevant = element
+ .fields
+ .iter()
+ .filter(|field| !field.external && !field.synthesized && field.inherent());
+ let params = relevant.clone().map(|Field { ident, ty, .. }| {
+ quote! { #ident: #ty }
+ });
+ let builder_calls = relevant.map(|Field { ident, with_ident, .. }| {
+ quote! { .#with_ident(#ident) }
+ });
+ quote! {
+ /// Create a new element.
+ pub fn new(#(#params),*) -> Self {
+ Self(::typst::model::Content::new(
+ <Self as ::typst::model::Element>::func()
+ ))
+ #(#builder_calls)*
+ }
+ }
+}
+
+/// Create an accessor methods for a field.
+fn create_field_method(field: &Field) -> TokenStream {
+ let Field { vis, docs, ident, name, output, .. } = field;
+ if field.inherent() || field.synthesized {
+ quote! {
+ #[doc = #docs]
+ #[track_caller]
+ #vis fn #ident(&self) -> #output {
+ self.0.expect_field(#name)
+ }
+ }
+ } else {
+ let access = create_style_chain_access(field, quote! { self.0.field(#name) });
+ quote! {
+ #[doc = #docs]
+ #vis fn #ident(&self, styles: ::typst::model::StyleChain) -> #output {
+ #access
+ }
+ }
+ }
+}
+
+/// Create a style chain access method for a field.
+fn create_field_in_method(field: &Field) -> TokenStream {
+ let Field { vis, ident_in, name, output, .. } = field;
+ let doc = format!("Access the `{}` field in the given style chain.", name);
+ let access = create_style_chain_access(field, quote! { None });
+ quote! {
+ #[doc = #doc]
+ #vis fn #ident_in(styles: ::typst::model::StyleChain) -> #output {
+ #access
+ }
+ }
+}
+
+/// Create a style chain access method for a field.
+fn create_style_chain_access(field: &Field, inherent: TokenStream) -> TokenStream {
+ let Field { name, ty, default, .. } = field;
+ let getter = match (field.fold, field.resolve) {
+ (false, false) => quote! { get },
+ (false, true) => quote! { get_resolve },
+ (true, false) => quote! { get_fold },
+ (true, true) => quote! { get_resolve_fold },
+ };
+
+ quote! {
+ styles.#getter::<#ty>(
+ <Self as ::typst::model::Element>::func(),
+ #name,
+ #inherent,
+ || #default,
+ )
+ }
+}
+
+/// Create a builder pattern method for a field.
+fn create_with_field_method(field: &Field) -> TokenStream {
+ let Field { vis, ident, with_ident, name, ty, .. } = field;
+ let doc = format!("Set the [`{}`](Self::{}) field.", name, ident);
+ quote! {
+ #[doc = #doc]
+ #vis fn #with_ident(mut self, #ident: #ty) -> Self {
+ Self(self.0.with_field(#name, #ident))
+ }
+ }
+}
+
+/// Create a set-style method for a field.
+fn create_push_field_method(field: &Field) -> TokenStream {
+ let Field { vis, ident, push_ident, name, ty, .. } = field;
+ let doc = format!("Push the [`{}`](Self::{}) field.", name, ident);
+ quote! {
+ #[doc = #doc]
+ #vis fn #push_ident(&mut self, #ident: #ty) {
+ self.0.push_field(#name, #ident);
+ }
+ }
+}
+
+/// Create a setter method for a field.
+fn create_set_field_method(field: &Field) -> TokenStream {
+ let Field { vis, ident, set_ident, name, ty, .. } = field;
+ let doc = format!("Create a style property for the `{}` field.", name);
+ quote! {
+ #[doc = #doc]
+ #vis fn #set_ident(#ident: #ty) -> ::typst::model::Style {
+ ::typst::model::Style::Property(::typst::model::Property::new(
+ <Self as ::typst::model::Element>::func(),
+ #name,
+ #ident,
+ ))
+ }
+ }
+}
+
+/// Create the element's `Pack` implementation.
+fn create_pack_impl(element: &Elem) -> TokenStream {
+ let Elem { ident, name, display, keywords, category, docs, .. } = element;
+ let vtable_func = create_vtable_func(element);
+ let infos = element
+ .fields
+ .iter()
+ .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 {
+ self.0
+ }
+
+ fn unpack(content: &::typst::model::Content) -> ::std::option::Option<&Self> {
+ // Safety: Elements are #[repr(transparent)].
+ content.is::<Self>().then(|| unsafe {
+ ::std::mem::transmute(content)
+ })
+ }
+
+ fn func() -> ::typst::model::ElemFunc {
+ static NATIVE: ::typst::model::NativeElemFunc = ::typst::model::NativeElemFunc {
+ name: #name,
+ vtable: #vtable_func,
+ construct: <#ident as ::typst::model::Construct>::construct,
+ set: <#ident as ::typst::model::Set>::set,
+ info: ::typst::eval::Lazy::new(|| typst::eval::FuncInfo {
+ name: #name,
+ display: #display,
+ keywords: #keywords,
+ docs: #docs,
+ params: ::std::vec![#(#infos),*],
+ returns: ::typst::eval::CastInfo::Union(::std::vec![
+ ::typst::eval::CastInfo::Type("content")
+ ]),
+ category: #category,
+ scope: #scope,
+ }),
+ };
+ (&NATIVE).into()
+ }
+ }
+ }
+}
+
+/// Create the element's casting vtable.
+fn create_vtable_func(element: &Elem) -> TokenStream {
+ let ident = &element.ident;
+ let relevant = element.capable.iter().filter(|&ident| ident != "Construct");
+ let checks = relevant.map(|capability| {
+ quote! {
+ if id == ::std::any::TypeId::of::<dyn #capability>() {
+ return Some(unsafe {
+ ::typst::util::fat::vtable(&null as &dyn #capability)
+ });
+ }
+ }
+ });
+
+ quote! {
+ |id| {
+ let null = Self(::typst::model::Content::new(
+ <#ident as ::typst::model::Element>::func()
+ ));
+ #(#checks)*
+ None
+ }
+ }
+}
+
+/// Create a parameter info for a field.
+fn create_param_info(field: &Field) -> TokenStream {
+ let Field {
+ name,
+ docs,
+ positional,
+ variadic,
+ required,
+ default,
+ fold,
+ ty,
+ output,
+ ..
+ } = field;
+ let named = !positional;
+ let settable = field.settable();
+ let default_ty = if *fold { &output } else { &ty };
+ let default = quote_option(&settable.then(|| {
+ quote! {
+ || {
+ let typed: #default_ty = #default;
+ ::typst::eval::IntoValue::into_value(typed)
+ }
+ }
+ }));
+ let ty = if *variadic {
+ quote! { <#ty as ::typst::eval::Variadics>::Inner }
+ } else {
+ quote! { #ty }
+ };
+ quote! {
+ ::typst::eval::ParamInfo {
+ name: #name,
+ docs: #docs,
+ cast: <#ty as ::typst::eval::Reflect>::describe(),
+ default: #default,
+ positional: #positional,
+ named: #named,
+ variadic: #variadic,
+ required: #required,
+ settable: #settable,
+ }
+ }
+}
+
+/// Create the element's `Construct` implementation.
+fn create_construct_impl(element: &Elem) -> TokenStream {
+ let ident = &element.ident;
+ let handlers = element
+ .fields
+ .iter()
+ .filter(|field| {
+ !field.external
+ && !field.synthesized
+ && (!field.internal || field.parse.is_some())
+ })
+ .map(|field| {
+ let push_ident = &field.push_ident;
+ let (prefix, value) = create_field_parser(field);
+ if field.settable() {
+ quote! {
+ #prefix
+ if let Some(value) = #value {
+ element.#push_ident(value);
+ }
+ }
+ } else {
+ quote! {
+ #prefix
+ element.#push_ident(#value);
+ }
+ }
+ });
+
+ quote! {
+ impl ::typst::model::Construct for #ident {
+ fn construct(
+ vm: &mut ::typst::eval::Vm,
+ args: &mut ::typst::eval::Args,
+ ) -> ::typst::diag::SourceResult<::typst::model::Content> {
+ let mut element = Self(::typst::model::Content::new(
+ <Self as ::typst::model::Element>::func()
+ ));
+ #(#handlers)*
+ Ok(element.0)
+ }
+ }
+ }
+}
+
+/// Create the element's `Set` implementation.
+fn create_set_impl(element: &Elem) -> TokenStream {
+ let ident = &element.ident;
+ let handlers = element
+ .fields
+ .iter()
+ .filter(|field| {
+ !field.external
+ && !field.synthesized
+ && field.settable()
+ && (!field.internal || field.parse.is_some())
+ })
+ .map(|field| {
+ let set_ident = &field.set_ident;
+ let (prefix, value) = create_field_parser(field);
+ quote! {
+ #prefix
+ if let Some(value) = #value {
+ styles.set(Self::#set_ident(value));
+ }
+ }
+ });
+
+ quote! {
+ impl ::typst::model::Set for #ident {
+ fn set(
+ args: &mut ::typst::eval::Args,
+ ) -> ::typst::diag::SourceResult<::typst::model::Styles> {
+ let mut styles = ::typst::model::Styles::new();
+ #(#handlers)*
+ Ok(styles)
+ }
+ }
+ }
+}
+
+/// Create argument parsing code for a field.
+fn create_field_parser(field: &Field) -> (TokenStream, TokenStream) {
+ if let Some(BlockWithReturn { prefix, expr }) = &field.parse {
+ return (quote! { #(#prefix);* }, quote! { #expr });
+ }
+
+ let name = &field.name;
+ let value = if field.variadic {
+ quote! { args.all()? }
+ } else if field.required {
+ quote! { args.expect(#name)? }
+ } else if field.positional {
+ quote! { args.find()? }
+ } else {
+ quote! { args.named(#name)? }
+ };
+
+ (quote! {}, value)
+}
diff --git a/crates/typst-macros/src/func.rs b/crates/typst-macros/src/func.rs
new file mode 100644
index 00000000..4a68e846
--- /dev/null
+++ b/crates/typst-macros/src/func.rs
@@ -0,0 +1,268 @@
+use super::*;
+
+/// Expand the `#[func]` macro.
+pub fn func(stream: TokenStream, item: &syn::ItemFn) -> Result<TokenStream> {
+ let func = prepare(stream, item)?;
+ Ok(create(&func, item))
+}
+
+struct Func {
+ name: String,
+ display: String,
+ category: String,
+ keywords: Option<String>,
+ docs: String,
+ vis: syn::Visibility,
+ ident: Ident,
+ ident_func: Ident,
+ parent: Option<syn::Type>,
+ vm: bool,
+ vt: bool,
+ args: bool,
+ span: bool,
+ params: Vec<Param>,
+ returns: syn::Type,
+ scope: Option<BlockWithReturn>,
+}
+
+struct Param {
+ name: String,
+ docs: String,
+ external: bool,
+ named: bool,
+ variadic: bool,
+ default: Option<syn::Expr>,
+ ident: Ident,
+ ty: syn::Type,
+}
+
+fn prepare(stream: TokenStream, item: &syn::ItemFn) -> Result<Func> {
+ let sig = &item.sig;
+
+ let Parent(parent) = syn::parse2(stream)?;
+
+ let mut vm = false;
+ let mut vt = false;
+ let mut args = false;
+ let mut span = false;
+ let mut params = vec![];
+ for input in &sig.inputs {
+ let syn::FnArg::Typed(typed) = input else {
+ bail!(input, "self is not allowed here");
+ };
+
+ let syn::Pat::Ident(syn::PatIdent {
+ by_ref: None,
+ mutability: None,
+ ident,
+ ..
+ }) = &*typed.pat else {
+ bail!(typed.pat, "expected identifier");
+ };
+
+ match ident.to_string().as_str() {
+ "vm" => vm = true,
+ "vt" => vt = true,
+ "args" => args = true,
+ "span" => span = true,
+ _ => {
+ let mut attrs = typed.attrs.clone();
+ params.push(Param {
+ name: kebab_case(ident),
+ docs: documentation(&attrs),
+ external: has_attr(&mut attrs, "external"),
+ named: has_attr(&mut attrs, "named"),
+ variadic: has_attr(&mut attrs, "variadic"),
+ default: parse_attr(&mut attrs, "default")?.map(|expr| {
+ expr.unwrap_or_else(
+ || parse_quote! { ::std::default::Default::default() },
+ )
+ }),
+ ident: ident.clone(),
+ ty: (*typed.ty).clone(),
+ });
+
+ validate_attrs(&attrs)?;
+ }
+ }
+ }
+
+ let mut attrs = item.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();
+
+ let func = Func {
+ name: sig.ident.to_string().trim_end_matches('_').replace('_', "-"),
+ display,
+ category,
+ keywords,
+ docs,
+ vis: item.vis.clone(),
+ ident: sig.ident.clone(),
+ ident_func: Ident::new(
+ &format!("{}_func", sig.ident.to_string().trim_end_matches('_')),
+ sig.ident.span(),
+ ),
+ parent,
+ params,
+ returns: match &sig.output {
+ syn::ReturnType::Default => parse_quote! { () },
+ syn::ReturnType::Type(_, ty) => ty.as_ref().clone(),
+ },
+ scope: parse_attr(&mut attrs, "scope")?.flatten(),
+ vm,
+ vt,
+ args,
+ span,
+ };
+
+ Ok(func)
+}
+
+fn create(func: &Func, item: &syn::ItemFn) -> TokenStream {
+ let Func {
+ name,
+ display,
+ category,
+ docs,
+ vis,
+ ident,
+ ident_func,
+ returns,
+ ..
+ } = func;
+
+ let handlers = func
+ .params
+ .iter()
+ .filter(|param| !param.external)
+ .map(create_param_parser);
+
+ let args = func
+ .params
+ .iter()
+ .filter(|param| !param.external)
+ .map(|param| &param.ident);
+
+ let parent = func.parent.as_ref().map(|ty| quote! { #ty:: });
+ let vm_ = func.vm.then(|| quote! { vm, });
+ let vt_ = func.vt.then(|| quote! { &mut vm.vt, });
+ let args_ = func.args.then(|| quote! { args.take(), });
+ let span_ = func.span.then(|| quote! { args.span, });
+ let wrapper = quote! {
+ |vm, args| {
+ let __typst_func = #parent #ident;
+ #(#handlers)*
+ let output = __typst_func(#(#args,)* #vm_ #vt_ #args_ #span_);
+ ::typst::eval::IntoResult::into_result(output, args.span)
+ }
+ };
+
+ let mut item = item.clone();
+ item.attrs.clear();
+
+ let inputs = item.sig.inputs.iter().cloned().filter_map(|mut input| {
+ if let syn::FnArg::Typed(typed) = &mut input {
+ if typed.attrs.iter().any(|attr| attr.path().is_ident("external")) {
+ return None;
+ }
+ typed.attrs.clear();
+ }
+ Some(input)
+ });
+
+ item.sig.inputs = parse_quote! { #(#inputs),* };
+
+ let keywords = quote_option(&func.keywords);
+ let params = func.params.iter().map(create_param_info);
+ let scope = create_scope_builder(func.scope.as_ref());
+
+ quote! {
+ #[doc(hidden)]
+ #vis fn #ident_func() -> &'static ::typst::eval::NativeFunc {
+ static FUNC: ::typst::eval::NativeFunc = ::typst::eval::NativeFunc {
+ func: #wrapper,
+ info: ::typst::eval::Lazy::new(|| typst::eval::FuncInfo {
+ name: #name,
+ display: #display,
+ keywords: #keywords,
+ category: #category,
+ docs: #docs,
+ params: ::std::vec![#(#params),*],
+ returns: <#returns as ::typst::eval::Reflect>::describe(),
+ scope: #scope,
+ }),
+ };
+ &FUNC
+ }
+
+ #[doc = #docs]
+ #item
+ }
+}
+
+/// Create a parameter info for a field.
+fn create_param_info(param: &Param) -> TokenStream {
+ let Param { name, docs, named, variadic, ty, default, .. } = param;
+ let positional = !named;
+ let required = default.is_none();
+ let default = quote_option(&default.as_ref().map(|_default| {
+ quote! {
+ || {
+ let typed: #ty = #default;
+ ::typst::eval::IntoValue::into_value(typed)
+ }
+ }
+ }));
+ let ty = if *variadic {
+ quote! { <#ty as ::typst::eval::Variadics>::Inner }
+ } else {
+ quote! { #ty }
+ };
+ quote! {
+ ::typst::eval::ParamInfo {
+ name: #name,
+ docs: #docs,
+ cast: <#ty as ::typst::eval::Reflect>::describe(),
+ default: #default,
+ positional: #positional,
+ named: #named,
+ variadic: #variadic,
+ required: #required,
+ settable: false,
+ }
+ }
+}
+
+/// Create argument parsing code for a parameter.
+fn create_param_parser(param: &Param) -> TokenStream {
+ let Param { name, ident, ty, .. } = param;
+
+ let mut value = if param.variadic {
+ quote! { args.all()? }
+ } else if param.named {
+ quote! { args.named(#name)? }
+ } else if param.default.is_some() {
+ quote! { args.eat()? }
+ } else {
+ quote! { args.expect(#name)? }
+ };
+
+ if let Some(default) = &param.default {
+ value = quote! { #value.unwrap_or_else(|| #default) }
+ }
+
+ quote! { let mut #ident: #ty = #value; }
+}
+
+struct Parent(Option<syn::Type>);
+
+impl Parse for Parent {
+ fn parse(input: ParseStream) -> Result<Self> {
+ Ok(Self(if !input.is_empty() { Some(input.parse()?) } else { None }))
+ }
+}
diff --git a/crates/typst-macros/src/lib.rs b/crates/typst-macros/src/lib.rs
new file mode 100644
index 00000000..49840ef2
--- /dev/null
+++ b/crates/typst-macros/src/lib.rs
@@ -0,0 +1,63 @@
+//! Procedural macros for Typst.
+
+extern crate proc_macro;
+
+#[macro_use]
+mod util;
+mod castable;
+mod element;
+mod func;
+mod symbols;
+
+use proc_macro::TokenStream as BoundaryStream;
+use proc_macro2::TokenStream;
+use quote::quote;
+use syn::ext::IdentExt;
+use syn::parse::{Parse, ParseStream, Parser};
+use syn::punctuated::Punctuated;
+use syn::{parse_quote, DeriveInput, Ident, Result, Token};
+
+use self::util::*;
+
+/// Turns a function into a `NativeFunc`.
+#[proc_macro_attribute]
+pub fn func(stream: BoundaryStream, item: BoundaryStream) -> BoundaryStream {
+ let item = syn::parse_macro_input!(item as syn::ItemFn);
+ func::func(stream.into(), &item)
+ .unwrap_or_else(|err| err.to_compile_error())
+ .into()
+}
+
+/// Turns a type into an `Element`.
+#[proc_macro_attribute]
+pub fn element(stream: BoundaryStream, item: BoundaryStream) -> BoundaryStream {
+ let item = syn::parse_macro_input!(item as syn::ItemStruct);
+ element::element(stream.into(), &item)
+ .unwrap_or_else(|err| err.to_compile_error())
+ .into()
+}
+
+/// Implements `Reflect`, `FromValue`, and `IntoValue` for an enum.
+#[proc_macro_derive(Cast, attributes(string))]
+pub fn derive_cast(item: BoundaryStream) -> BoundaryStream {
+ let item = syn::parse_macro_input!(item as DeriveInput);
+ castable::derive_cast(&item)
+ .unwrap_or_else(|err| err.to_compile_error())
+ .into()
+}
+
+/// Implements `Reflect`, `FromValue`, and `IntoValue` for a type.
+#[proc_macro]
+pub fn cast(stream: BoundaryStream) -> BoundaryStream {
+ castable::cast(stream.into())
+ .unwrap_or_else(|err| err.to_compile_error())
+ .into()
+}
+
+/// Defines a list of `Symbol`s.
+#[proc_macro]
+pub fn symbols(stream: BoundaryStream) -> BoundaryStream {
+ symbols::symbols(stream.into())
+ .unwrap_or_else(|err| err.to_compile_error())
+ .into()
+}
diff --git a/crates/typst-macros/src/symbols.rs b/crates/typst-macros/src/symbols.rs
new file mode 100644
index 00000000..cdb7f5d7
--- /dev/null
+++ b/crates/typst-macros/src/symbols.rs
@@ -0,0 +1,78 @@
+use super::*;
+
+/// Expand the `symbols!` macro.
+pub fn symbols(stream: TokenStream) -> Result<TokenStream> {
+ let list: Punctuated<Symbol, Token![,]> =
+ Punctuated::parse_terminated.parse2(stream)?;
+ let pairs = list.iter().map(|symbol| {
+ let name = symbol.name.to_string();
+ let kind = match &symbol.kind {
+ Kind::Single(c) => quote! { typst::eval::Symbol::new(#c), },
+ Kind::Multiple(variants) => {
+ let variants = variants.iter().map(|variant| {
+ let name = &variant.name;
+ let c = &variant.c;
+ quote! { (#name, #c) }
+ });
+ quote! {
+ typst::eval::Symbol::list(&[#(#variants),*])
+ }
+ }
+ };
+ quote! { (#name, #kind) }
+ });
+ Ok(quote! { &[#(#pairs),*] })
+}
+
+struct Symbol {
+ name: syn::Ident,
+ kind: Kind,
+}
+
+enum Kind {
+ Single(syn::LitChar),
+ Multiple(Punctuated<Variant, Token![,]>),
+}
+
+struct Variant {
+ name: String,
+ c: syn::LitChar,
+}
+
+impl Parse for Symbol {
+ fn parse(input: ParseStream) -> Result<Self> {
+ let name = input.call(Ident::parse_any)?;
+ input.parse::<Token![:]>()?;
+ let kind = input.parse()?;
+ Ok(Self { name, kind })
+ }
+}
+
+impl Parse for Kind {
+ fn parse(input: ParseStream) -> Result<Self> {
+ if input.peek(syn::LitChar) {
+ Ok(Self::Single(input.parse()?))
+ } else {
+ let content;
+ syn::bracketed!(content in input);
+ Ok(Self::Multiple(Punctuated::parse_terminated(&content)?))
+ }
+ }
+}
+
+impl Parse for Variant {
+ fn parse(input: ParseStream) -> Result<Self> {
+ let mut name = String::new();
+ if input.peek(syn::Ident::peek_any) {
+ name.push_str(&input.call(Ident::parse_any)?.to_string());
+ while input.peek(Token![.]) {
+ input.parse::<Token![.]>()?;
+ name.push('.');
+ name.push_str(&input.call(Ident::parse_any)?.to_string());
+ }
+ input.parse::<Token![:]>()?;
+ }
+ let c = input.parse()?;
+ Ok(Self { name, c })
+ }
+}
diff --git a/crates/typst-macros/src/util.rs b/crates/typst-macros/src/util.rs
new file mode 100644
index 00000000..389fed06
--- /dev/null
+++ b/crates/typst-macros/src/util.rs
@@ -0,0 +1,145 @@
+use heck::ToKebabCase;
+use quote::ToTokens;
+
+use super::*;
+
+/// Return an error at the given item.
+macro_rules! bail {
+ (callsite, $($tts:tt)*) => {
+ return Err(syn::Error::new(
+ proc_macro2::Span::call_site(),
+ format!("typst: {}", format!($($tts)*))
+ ))
+ };
+ ($item:expr, $($tts:tt)*) => {
+ return Err(syn::Error::new_spanned(
+ &$item,
+ format!("typst: {}", format!($($tts)*))
+ ))
+ };
+}
+
+/// 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 })
+ }
+}
+
+/// Whether an attribute list has a specified attribute.
+pub fn has_attr(attrs: &mut Vec<syn::Attribute>, target: &str) -> bool {
+ take_attr(attrs, target).is_some()
+}
+
+/// Whether an attribute list has a specified attribute.
+pub fn parse_attr<T: Parse>(
+ attrs: &mut Vec<syn::Attribute>,
+ target: &str,
+) -> Result<Option<Option<T>>> {
+ take_attr(attrs, target)
+ .map(|attr| {
+ Ok(match attr.meta {
+ syn::Meta::Path(_) => None,
+ syn::Meta::List(list) => Some(list.parse_args()?),
+ syn::Meta::NameValue(meta) => bail!(meta, "not valid here"),
+ })
+ })
+ .transpose()
+}
+
+/// Whether an attribute list has a specified attribute.
+pub fn take_attr(
+ attrs: &mut Vec<syn::Attribute>,
+ target: &str,
+) -> Option<syn::Attribute> {
+ attrs
+ .iter()
+ .position(|attr| attr.path().is_ident(target))
+ .map(|i| attrs.remove(i))
+}
+
+/// Ensure that no unrecognized attributes remain.
+pub fn validate_attrs(attrs: &[syn::Attribute]) -> Result<()> {
+ for attr in attrs {
+ if !attr.path().is_ident("doc") && !attr.path().is_ident("derive") {
+ let ident = attr.path().get_ident().unwrap();
+ bail!(ident, "unrecognized attribute: {ident}");
+ }
+ }
+ Ok(())
+}
+
+/// Convert an identifier to a kebab-case string.
+pub fn kebab_case(name: &Ident) -> String {
+ name.to_string().to_kebab_case()
+}
+
+/// Extract documentation comments from an attribute list.
+pub fn documentation(attrs: &[syn::Attribute]) -> String {
+ let mut doc = String::new();
+
+ // 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()
+}
+
+/// 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())
+ }
+ None => bail!(callsite, "missing metadata key: {key}"),
+ }
+}
+
+/// 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() }
+ }
+}
+
+/// 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 }
+ }
+}