summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--crates/typst-macros/src/cast.rs (renamed from crates/typst-macros/src/castable.rs)74
-rw-r--r--crates/typst-macros/src/elem.rs (renamed from crates/typst-macros/src/element.rs)306
-rw-r--r--crates/typst-macros/src/func.rs459
-rw-r--r--crates/typst-macros/src/lib.rs295
-rw-r--r--crates/typst-macros/src/scope.rs153
-rw-r--r--crates/typst-macros/src/symbols.rs2
-rw-r--r--crates/typst-macros/src/ty.rs113
-rw-r--r--crates/typst-macros/src/util.rs229
8 files changed, 1228 insertions, 403 deletions
diff --git a/crates/typst-macros/src/castable.rs b/crates/typst-macros/src/cast.rs
index 05c1b4d1..74bdc590 100644
--- a/crates/typst-macros/src/castable.rs
+++ b/crates/typst-macros/src/cast.rs
@@ -1,7 +1,9 @@
+use heck::ToKebabCase;
+
use super::*;
/// Expand the `#[derive(Cast)]` macro.
-pub fn derive_cast(item: &DeriveInput) -> Result<TokenStream> {
+pub fn derive_cast(item: DeriveInput) -> Result<TokenStream> {
let ty = &item.ident;
let syn::Data::Enum(data) = &item.data else {
@@ -19,7 +21,7 @@ pub fn derive_cast(item: &DeriveInput) -> Result<TokenStream> {
{
attr.parse_args::<syn::LitStr>()?.value()
} else {
- kebab_case(&variant.ident)
+ variant.ident.to_string().to_kebab_case()
};
variants.push(Variant {
@@ -62,20 +64,25 @@ struct Variant {
/// 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 input: CastInput = syn::parse2(stream)?;
+ let ty = &input.ty;
let castable_body = create_castable_body(&input);
- let describe_body = create_describe_body(&input);
+ let input_body = create_input_body(&input);
+ let output_body = create_output_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(|| {
+ let reflect = (!input.from_value.is_empty() || input.dynamic).then(|| {
quote! {
impl #eval::Reflect for #ty {
- fn describe() -> #eval::CastInfo {
- #describe_body
+ fn input() -> #eval::CastInfo {
+ #input_body
+ }
+
+ fn output() -> #eval::CastInfo {
+ #output_body
}
fn castable(value: &#eval::Value) -> bool {
@@ -85,7 +92,7 @@ pub fn cast(stream: TokenStream) -> Result<TokenStream> {
}
});
- let into_value = (input.into_value.is_some() || input.name.is_some()).then(|| {
+ let into_value = (input.into_value.is_some() || input.dynamic).then(|| {
quote! {
impl #eval::IntoValue for #ty {
fn into_value(self) -> #eval::Value {
@@ -95,7 +102,7 @@ pub fn cast(stream: TokenStream) -> Result<TokenStream> {
}
});
- let from_value = (!input.from_value.is_empty() || input.name.is_some()).then(|| {
+ let from_value = (!input.from_value.is_empty() || input.dynamic).then(|| {
quote! {
impl #eval::FromValue for #ty {
fn from_value(value: #eval::Value) -> ::typst::diag::StrResult<Self> {
@@ -105,55 +112,42 @@ pub fn cast(stream: TokenStream) -> Result<TokenStream> {
}
});
- 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>,
+ dynamic: bool,
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;
+ let mut dynamic = false;
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()?;
+ dynamic = true;
}
+ let ty = input.parse()?;
let _: syn::Token![,] = input.parse()?;
- let mut into_value = None;
+ let mut to_value = None;
if input.peek(syn::Token![self]) {
let _: syn::Token![self] = input.parse()?;
let _: syn::Token![=>] = input.parse()?;
- into_value = Some(input.parse()?);
+ to_value = Some(input.parse()?);
let _: syn::Token![,] = input.parse()?;
}
let from_value = Punctuated::parse_terminated(input)?;
- Ok(Self { ty, name, into_value, from_value })
+ Ok(Self { ty, dynamic, into_value: to_value, from_value })
}
}
@@ -212,7 +206,7 @@ fn create_castable_body(input: &CastInput) -> TokenStream {
}
}
- let dynamic_check = input.name.is_some().then(|| {
+ let dynamic_check = input.dynamic.then(|| {
quote! {
if let ::typst::eval::Value::Dyn(dynamic) = &value {
if dynamic.is::<Self>() {
@@ -241,7 +235,7 @@ fn create_castable_body(input: &CastInput) -> TokenStream {
}
}
-fn create_describe_body(input: &CastInput) -> TokenStream {
+fn create_input_body(input: &CastInput) -> TokenStream {
let mut infos = vec![];
for cast in &input.from_value {
@@ -256,14 +250,14 @@ fn create_describe_body(input: &CastInput) -> TokenStream {
}
}
Pattern::Ty(_, ty) => {
- quote! { <#ty as ::typst::eval::Reflect>::describe() }
+ quote! { <#ty as ::typst::eval::Reflect>::input() }
}
});
}
- if let Some(name) = &input.name {
+ if input.dynamic {
infos.push(quote! {
- ::typst::eval::CastInfo::Type(#name)
+ ::typst::eval::CastInfo::Type(::typst::eval::Type::of::<Self>())
});
}
@@ -272,6 +266,14 @@ fn create_describe_body(input: &CastInput) -> TokenStream {
}
}
+fn create_output_body(input: &CastInput) -> TokenStream {
+ if input.dynamic {
+ quote! { ::typst::eval::CastInfo::Type(::typst::eval::Type::of::<Self>()) }
+ } else {
+ quote! { Self::input() }
+ }
+}
+
fn create_into_value_body(input: &CastInput) -> TokenStream {
if let Some(expr) = &input.into_value {
quote! { #expr }
@@ -301,7 +303,7 @@ fn create_from_value_body(input: &CastInput) -> TokenStream {
}
}
- let dynamic_check = input.name.is_some().then(|| {
+ let dynamic_check = input.dynamic.then(|| {
quote! {
if let ::typst::eval::Value::Dyn(dynamic) = &value {
if let Some(concrete) = dynamic.downcast::<Self>() {
diff --git a/crates/typst-macros/src/element.rs b/crates/typst-macros/src/elem.rs
index e047e606..5584bdb6 100644
--- a/crates/typst-macros/src/element.rs
+++ b/crates/typst-macros/src/elem.rs
@@ -1,152 +1,175 @@
+use heck::ToKebabCase;
+
use super::*;
-/// Expand the `#[element]` macro.
-pub fn element(stream: TokenStream, body: &syn::ItemStruct) -> Result<TokenStream> {
- let element = prepare(stream, body)?;
+/// Expand the `#[elem]` macro.
+pub fn elem(stream: TokenStream, body: syn::ItemStruct) -> Result<TokenStream> {
+ let element = parse(stream, &body)?;
Ok(create(&element))
}
+/// Details about an element.
struct Elem {
name: String,
- display: String,
- category: String,
- keywords: Option<String>,
+ title: String,
+ scope: bool,
+ keywords: Vec<String>,
docs: String,
vis: syn::Visibility,
ident: Ident,
- capable: Vec<Ident>,
+ capabilities: Vec<Ident>,
fields: Vec<Field>,
- scope: Option<BlockWithReturn>,
}
+/// Details about an element field.
struct Field {
+ ident: Ident,
+ ident_in: Ident,
+ with_ident: Ident,
+ push_ident: Ident,
+ set_ident: Ident,
+ vis: syn::Visibility,
+ ty: syn::Type,
+ output: syn::Type,
name: String,
docs: String,
- internal: bool,
- external: bool,
positional: bool,
required: bool,
variadic: bool,
- synthesized: bool,
- fold: bool,
resolve: bool,
+ fold: bool,
+ internal: bool,
+ external: bool,
+ synthesized: 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 {
+ /// Whether the field is present on every instance of the element.
fn inherent(&self) -> bool {
self.required || self.variadic
}
+ /// Whether the field can be used with set rules.
fn settable(&self) -> bool {
!self.inherent()
}
}
-/// Preprocess the element's definition.
-fn prepare(stream: TokenStream, body: &syn::ItemStruct) -> Result<Elem> {
+/// The `..` in `#[elem(..)]`.
+struct Meta {
+ scope: bool,
+ name: Option<String>,
+ title: Option<String>,
+ keywords: Vec<String>,
+ capabilities: Vec<Ident>,
+}
+
+impl Parse for Meta {
+ fn parse(input: ParseStream) -> Result<Self> {
+ Ok(Self {
+ scope: parse_flag::<kw::scope>(input)?,
+ name: parse_string::<kw::name>(input)?,
+ title: parse_string::<kw::title>(input)?,
+ keywords: parse_string_array::<kw::keywords>(input)?,
+ capabilities: Punctuated::<Ident, Token![,]>::parse_terminated(input)?
+ .into_iter()
+ .collect(),
+ })
+ }
+}
+
+/// Parse details about the element from its struct definition.
+fn parse(stream: TokenStream, body: &syn::ItemStruct) -> Result<Elem> {
+ let meta: Meta = syn::parse2(stream)?;
+ let (name, title) = determine_name_and_title(
+ meta.name,
+ meta.title,
+ &body.ident,
+ Some(|base| base.trim_end_matches("Elem")),
+ )?;
+
+ let docs = documentation(&body.attrs);
+
let syn::Fields::Named(named) = &body.fields else {
bail!(body, "expected named fields");
};
+ let fields = named.named.iter().map(parse_field).collect::<Result<_>>()?;
+
+ Ok(Elem {
+ name,
+ title,
+ scope: meta.scope,
+ keywords: meta.keywords,
+ docs,
+ vis: body.vis.clone(),
+ ident: body.ident.clone(),
+ capabilities: meta.capabilities,
+ fields,
+ })
+}
- let mut fields = vec![];
- for field in &named.named {
- let Some(ident) = field.ident.clone() else {
- bail!(field, "expected named field");
- };
+fn parse_field(field: &syn::Field) -> Result<Field> {
+ 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");
+ }
- if ident == "label" {
- bail!(ident, "invalid field name");
- }
+ 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;
- 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");
- }
+ let mut field = Field {
+ name: ident.to_string().to_kebab_case(),
+ 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.positional {
- bail!(ident, "only positional fields can be required");
- }
+ if field.required && (field.fold || field.resolve) {
+ bail!(ident, "required fields cannot be folded or resolved");
+ }
- 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 };
- }
+ if field.required && !field.positional {
+ bail!(ident, "only positional fields can be required");
+ }
- validate_attrs(&attrs)?;
- fields.push(field);
+ if field.resolve {
+ let output = &field.output;
+ field.output = parse_quote! { <#output as ::typst::model::Resolve>::Output };
}
- 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(),
- };
+ if field.fold {
+ let output = &field.output;
+ field.output = parse_quote! { <#output as ::typst::model::Fold>::Output };
+ }
validate_attrs(&attrs)?;
- Ok(element)
+
+ Ok(field)
}
/// Produce the element's definition.
@@ -166,13 +189,13 @@ fn create(element: &Elem) -> TokenStream {
// Trait implementations.
let element_impl = create_pack_impl(element);
let construct_impl = element
- .capable
+ .capabilities
.iter()
.all(|capability| capability != "Construct")
.then(|| create_construct_impl(element));
let set_impl = create_set_impl(element);
let locatable_impl = element
- .capable
+ .capabilities
.iter()
.any(|capability| capability == "Locatable")
.then(|| quote! { impl ::typst::model::Locatable for #ident {} });
@@ -231,7 +254,7 @@ fn create_new_func(element: &Elem) -> TokenStream {
/// Create a new element.
pub fn new(#(#params),*) -> Self {
Self(::typst::model::Content::new(
- <Self as ::typst::model::Element>::func()
+ <Self as ::typst::model::NativeElement>::elem()
))
#(#builder_calls)*
}
@@ -285,7 +308,7 @@ fn create_style_chain_access(field: &Field, inherent: TokenStream) -> TokenStrea
quote! {
styles.#getter::<#ty>(
- <Self as ::typst::model::Element>::func(),
+ <Self as ::typst::model::NativeElement>::elem(),
#name,
#inherent,
|| #default,
@@ -325,7 +348,7 @@ fn create_set_field_method(field: &Field) -> TokenStream {
#[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(),
+ <Self as ::typst::model::NativeElement>::elem(),
#name,
#ident,
))
@@ -335,49 +358,54 @@ 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, keywords, category, docs, .. } = element;
+ let eval = quote! { ::typst::eval };
+ let model = quote! { ::typst::model };
+
+ let Elem { name, ident, title, scope, keywords, docs, .. } = element;
let vtable_func = create_vtable_func(element);
- let infos = element
+ let params = 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);
+
+ let scope = if *scope {
+ quote! { <#ident as #eval::NativeScope>::scope() }
+ } else {
+ quote! { #eval::Scope::new() }
+ };
+
+ let data = quote! {
+ #model::NativeElementData {
+ name: #name,
+ title: #title,
+ docs: #docs,
+ keywords: &[#(#keywords),*],
+ construct: <#ident as #model::Construct>::construct,
+ set: <#ident as #model::Set>::set,
+ vtable: #vtable_func,
+ scope: #eval::Lazy::new(|| #scope),
+ params: #eval::Lazy::new(|| ::std::vec![#(#params),*])
+ }
+ };
+
quote! {
- impl ::typst::model::Element for #ident {
- fn pack(self) -> ::typst::model::Content {
+ impl #model::NativeElement for #ident {
+ fn data() -> &'static #model::NativeElementData {
+ static DATA: #model::NativeElementData = #data;
+ &DATA
+ }
+
+ fn pack(self) -> #model::Content {
self.0
}
- fn unpack(content: &::typst::model::Content) -> ::std::option::Option<&Self> {
+ fn unpack(content: &#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()
- }
}
}
}
@@ -385,7 +413,7 @@ fn create_pack_impl(element: &Elem) -> TokenStream {
/// 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 relevant = element.capabilities.iter().filter(|&ident| ident != "Construct");
let checks = relevant.map(|capability| {
quote! {
if id == ::std::any::TypeId::of::<dyn #capability>() {
@@ -399,7 +427,7 @@ fn create_vtable_func(element: &Elem) -> TokenStream {
quote! {
|id| {
let null = Self(::typst::model::Content::new(
- <#ident as ::typst::model::Element>::func()
+ <#ident as ::typst::model::NativeElement>::elem()
));
#(#checks)*
None
@@ -441,7 +469,7 @@ fn create_param_info(field: &Field) -> TokenStream {
::typst::eval::ParamInfo {
name: #name,
docs: #docs,
- cast: <#ty as ::typst::eval::Reflect>::describe(),
+ input: <#ty as ::typst::eval::Reflect>::input(),
default: #default,
positional: #positional,
named: #named,
@@ -488,7 +516,7 @@ fn create_construct_impl(element: &Elem) -> TokenStream {
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()
+ <Self as ::typst::model::NativeElement>::elem()
));
#(#handlers)*
Ok(element.0)
diff --git a/crates/typst-macros/src/func.rs b/crates/typst-macros/src/func.rs
index 13a6eb2d..87d57f19 100644
--- a/crates/typst-macros/src/func.rs
+++ b/crates/typst-macros/src/func.rs
@@ -1,206 +1,340 @@
use super::*;
+use heck::ToKebabCase;
+
/// Expand the `#[func]` macro.
pub fn func(stream: TokenStream, item: &syn::ItemFn) -> Result<TokenStream> {
- let func = prepare(stream, item)?;
+ let func = parse(stream, item)?;
Ok(create(&func, item))
}
+/// Details about a function.
struct Func {
name: String,
- display: String,
- category: String,
- keywords: Option<String>,
+ title: String,
+ scope: bool,
+ constructor: bool,
+ keywords: Vec<String>,
+ parent: Option<syn::Type>,
docs: String,
vis: syn::Visibility,
ident: Ident,
- ident_func: Ident,
- parent: Option<syn::Type>,
+ special: SpecialParams,
+ params: Vec<Param>,
+ returns: syn::Type,
+}
+
+/// Special parameters provided by the runtime.
+#[derive(Default)]
+struct SpecialParams {
+ self_: Option<Param>,
vm: bool,
vt: bool,
args: bool,
span: bool,
- params: Vec<Param>,
- returns: syn::Type,
- scope: Option<BlockWithReturn>,
}
+/// Details about a function parameter.
struct Param {
+ binding: Binding,
+ ident: Ident,
+ ty: syn::Type,
name: String,
docs: String,
- external: bool,
named: bool,
variadic: bool,
+ external: bool,
default: Option<syn::Expr>,
- ident: Ident,
- ty: syn::Type,
}
-fn prepare(stream: TokenStream, item: &syn::ItemFn) -> Result<Func> {
- let sig = &item.sig;
+/// How a parameter is bound.
+enum Binding {
+ /// Normal parameter.
+ Owned,
+ /// `&self`.
+ Ref,
+ /// `&mut self`.
+ RefMut,
+}
- let Parent(parent) = syn::parse2(stream)?;
+/// The `..` in `#[func(..)]`.
+pub struct Meta {
+ pub scope: bool,
+ pub name: Option<String>,
+ pub title: Option<String>,
+ pub constructor: bool,
+ pub keywords: Vec<String>,
+ pub parent: Option<syn::Type>,
+}
- let mut vm = false;
- let mut vt = false;
- let mut args = false;
- let mut span = false;
+impl Parse for Meta {
+ fn parse(input: ParseStream) -> Result<Self> {
+ Ok(Self {
+ scope: parse_flag::<kw::scope>(input)?,
+ name: parse_string::<kw::name>(input)?,
+ title: parse_string::<kw::title>(input)?,
+ constructor: parse_flag::<kw::constructor>(input)?,
+ keywords: parse_string_array::<kw::keywords>(input)?,
+ parent: parse_key_value::<kw::parent, _>(input)?,
+ })
+ }
+}
+
+/// Parse details about the function from the fn item.
+fn parse(stream: TokenStream, item: &syn::ItemFn) -> Result<Func> {
+ let meta: Meta = syn::parse2(stream)?;
+ let (name, title) =
+ determine_name_and_title(meta.name, meta.title, &item.sig.ident, None)?;
+
+ let docs = documentation(&item.attrs);
+
+ let mut special = SpecialParams::default();
let mut params = vec![];
- for input in &sig.inputs {
- let syn::FnArg::Typed(typed) = input else {
- println!("option a");
- 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)?;
- }
- }
+ for input in &item.sig.inputs {
+ parse_param(&mut special, &mut params, meta.parent.as_ref(), input)?;
}
- 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,
+ let returns = match &item.sig.output {
+ syn::ReturnType::Default => parse_quote! { () },
+ syn::ReturnType::Type(_, ty) => ty.as_ref().clone(),
+ };
+
+ if meta.parent.is_some() && meta.scope {
+ bail!(item, "scoped function cannot have a scope");
+ }
+
+ Ok(Func {
+ name,
+ title,
+ scope: meta.scope,
+ constructor: meta.constructor,
+ keywords: meta.keywords,
+ parent: meta.parent,
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,
+ ident: item.sig.ident.clone(),
+ special,
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,
+ returns,
+ })
+}
+
+/// Parse details about a functino parameter.
+fn parse_param(
+ special: &mut SpecialParams,
+ params: &mut Vec<Param>,
+ parent: Option<&syn::Type>,
+ input: &syn::FnArg,
+) -> Result<()> {
+ let typed = match input {
+ syn::FnArg::Receiver(recv) => {
+ let mut binding = Binding::Owned;
+ if recv.reference.is_some() {
+ if recv.mutability.is_some() {
+ binding = Binding::RefMut
+ } else {
+ binding = Binding::Ref
+ }
+ };
+
+ special.self_ = Some(Param {
+ binding,
+ ident: syn::Ident::new("self_", recv.self_token.span),
+ ty: match parent {
+ Some(ty) => ty.clone(),
+ None => bail!(recv, "explicit parent type required"),
+ },
+ name: "self".into(),
+ docs: documentation(&recv.attrs),
+ named: false,
+ variadic: false,
+ external: false,
+ default: None,
+ });
+ return Ok(());
+ }
+ syn::FnArg::Typed(typed) => typed,
+ };
+
+ let syn::Pat::Ident(syn::PatIdent { by_ref: None, mutability: None, ident, .. }) =
+ &*typed.pat
+ else {
+ bail!(typed.pat, "expected identifier");
};
- Ok(func)
+ match ident.to_string().as_str() {
+ "vm" => special.vm = true,
+ "vt" => special.vt = true,
+ "args" => special.args = true,
+ "span" => special.span = true,
+ _ => {
+ let mut attrs = typed.attrs.clone();
+ params.push(Param {
+ binding: Binding::Owned,
+ ident: ident.clone(),
+ ty: (*typed.ty).clone(),
+ name: ident.to_string().to_kebab_case(),
+ docs: documentation(&attrs),
+ named: has_attr(&mut attrs, "named"),
+ variadic: has_attr(&mut attrs, "variadic"),
+ external: has_attr(&mut attrs, "external"),
+ default: parse_attr(&mut attrs, "default")?.map(|expr| {
+ expr.unwrap_or_else(
+ || parse_quote! { ::std::default::Default::default() },
+ )
+ }),
+ });
+ validate_attrs(&attrs)?;
+ }
+ }
+
+ Ok(())
}
+/// Produce the function's definition.
fn create(func: &Func, item: &syn::ItemFn) -> TokenStream {
+ let eval = quote! { ::typst::eval };
+
+ let Func { docs, vis, ident, .. } = func;
+ let item = rewrite_fn_item(item);
+ let ty = create_func_ty(func);
+ let data = create_func_data(func);
+
+ let creator = if ty.is_some() {
+ quote! {
+ impl #eval::NativeFunc for #ident {
+ fn data() -> &'static #eval::NativeFuncData {
+ static DATA: #eval::NativeFuncData = #data;
+ &DATA
+ }
+ }
+ }
+ } else {
+ let ident_data = quote::format_ident!("{ident}_data");
+ quote! {
+ #[doc(hidden)]
+ #vis fn #ident_data() -> &'static #eval::NativeFuncData {
+ static DATA: #eval::NativeFuncData = #data;
+ &DATA
+ }
+ }
+ };
+
+ quote! {
+ #[doc = #docs]
+ #[allow(dead_code)]
+ #item
+
+ #[doc(hidden)]
+ #ty
+ #creator
+ }
+}
+
+/// Create native function data for the function.
+fn create_func_data(func: &Func) -> TokenStream {
+ let eval = quote! { ::typst::eval };
+
let Func {
+ ident,
name,
- display,
- category,
+ title,
docs,
- vis,
- ident,
- ident_func,
+ keywords,
returns,
+ scope,
+ parent,
+ constructor,
..
} = func;
- let handlers = func
- .params
- .iter()
- .filter(|param| !param.external)
- .map(create_param_parser);
+ let scope = if *scope {
+ quote! { <#ident as #eval::NativeScope>::scope() }
+ } else {
+ quote! { #eval::Scope::new() }
+ };
- let args = func
- .params
- .iter()
- .filter(|param| !param.external)
- .map(|param| &param.ident);
+ let closure = create_wrapper_closure(func);
+ let params = func.special.self_.iter().chain(&func.params).map(create_param_info);
- 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 name = if *constructor {
+ quote! { <#parent as #eval::NativeType>::NAME }
+ } else {
+ quote! { #name }
};
- 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();
+ quote! {
+ #eval::NativeFuncData {
+ function: #closure,
+ name: #name,
+ title: #title,
+ docs: #docs,
+ keywords: &[#(#keywords),*],
+ scope: #eval::Lazy::new(|| #scope),
+ params: #eval::Lazy::new(|| ::std::vec![#(#params),*]),
+ returns: #eval::Lazy::new(|| <#returns as #eval::Reflect>::output()),
}
- 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());
+/// Create a type that shadows the function.
+fn create_func_ty(func: &Func) -> Option<TokenStream> {
+ if func.parent.is_some() {
+ return None;
+ }
- quote! {
+ let Func { vis, ident, .. } = func;
+ Some(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
+ #[allow(non_camel_case_types)]
+ #vis enum #ident {}
+ })
+}
+
+/// Create the runtime-compatible wrapper closure that parses arguments.
+fn create_wrapper_closure(func: &Func) -> TokenStream {
+ // These handlers parse the arguments.
+ let handlers = {
+ let func_handlers = func
+ .params
+ .iter()
+ .filter(|param| !param.external)
+ .map(create_param_parser);
+ let self_handler = func.special.self_.as_ref().map(create_param_parser);
+ quote! {
+ #self_handler
+ #(#func_handlers)*
}
+ };
- #[doc = #docs]
- #item
+ // This is the actual function call.
+ let call = {
+ let self_ = func
+ .special
+ .self_
+ .as_ref()
+ .map(bind)
+ .map(|tokens| quote! { #tokens, });
+ let vm_ = func.special.vm.then(|| quote! { vm, });
+ let vt_ = func.special.vt.then(|| quote! { &mut vm.vt, });
+ let args_ = func.special.args.then(|| quote! { args.take(), });
+ let span_ = func.special.span.then(|| quote! { args.span, });
+ let forwarded = func.params.iter().filter(|param| !param.external).map(bind);
+ quote! {
+ __typst_func(#self_ #vm_ #vt_ #args_ #span_ #(#forwarded,)*)
+ }
+ };
+
+ // This is the whole wrapped closure.
+ let ident = &func.ident;
+ let parent = func.parent.as_ref().map(|ty| quote! { #ty:: });
+ quote! {
+ |vm, args| {
+ let __typst_func = #parent #ident;
+ #handlers
+ let output = #call;
+ ::typst::eval::IntoResult::into_result(output, args.span)
+ }
}
}
@@ -226,7 +360,7 @@ fn create_param_info(param: &Param) -> TokenStream {
::typst::eval::ParamInfo {
name: #name,
docs: #docs,
- cast: <#ty as ::typst::eval::Reflect>::describe(),
+ input: <#ty as ::typst::eval::Reflect>::input(),
default: #default,
positional: #positional,
named: #named,
@@ -258,10 +392,29 @@ fn create_param_parser(param: &Param) -> TokenStream {
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 }))
+/// Apply the binding to a parameter.
+fn bind(param: &Param) -> TokenStream {
+ let ident = &param.ident;
+ match param.binding {
+ Binding::Owned => quote! { #ident },
+ Binding::Ref => quote! { &#ident },
+ Binding::RefMut => quote! { &mut #ident },
}
}
+
+/// Removes attributes and so on from the native function.
+fn rewrite_fn_item(item: &syn::ItemFn) -> syn::ItemFn {
+ 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)
+ });
+ let mut item = item.clone();
+ item.attrs.clear();
+ item.sig.inputs = parse_quote! { #(#inputs),* };
+ item
+}
diff --git a/crates/typst-macros/src/lib.rs b/crates/typst-macros/src/lib.rs
index 49840ef2..52f3e237 100644
--- a/crates/typst-macros/src/lib.rs
+++ b/crates/typst-macros/src/lib.rs
@@ -4,10 +4,12 @@ extern crate proc_macro;
#[macro_use]
mod util;
-mod castable;
-mod element;
+mod cast;
+mod elem;
mod func;
+mod scope;
mod symbols;
+mod ty;
use proc_macro::TokenStream as BoundaryStream;
use proc_macro2::TokenStream;
@@ -19,7 +21,79 @@ use syn::{parse_quote, DeriveInput, Ident, Result, Token};
use self::util::*;
-/// Turns a function into a `NativeFunc`.
+/// Makes a native Rust function usable as a Typst function.
+///
+/// This implements `NativeFunction` for a freshly generated type with the same
+/// name as a function. (In Rust, functions and types live in separate
+/// namespace, so both can coexist.)
+///
+/// If the function is in an impl block annotated with `#[scope]`, things work a
+/// bit differently because the no type can be generated within the impl block.
+/// In that case, a function named `{name}_data` that returns `&'static
+/// NativeFuncData` is generated. You typically don't need to interact with this
+/// function though because the `#[scope]` macro hooks everything up for you.
+///
+/// ```ignore
+/// /// Doubles an integer.
+/// #[func]
+/// fn double(x: i64) -> i64 {
+/// 2 * x
+/// }
+/// ```
+///
+/// # Properties
+/// You can customize some properties of the resulting function:
+/// - `scope`: Indicates that the function has an associated scope defined by
+/// the `#[scope]` macro.
+/// - `name`: The functions's normal name (e.g. `min`). Defaults to the Rust
+/// name in kebab-case.
+/// - `title`: The functions's title case name (e.g. `Minimum`). Defaults to the
+/// normal name in title case.
+///
+/// # Arguments
+/// By default, function arguments are positional and required. You can use
+/// various attributes to configure their parsing behaviour:
+///
+/// - `#[named]`: Makes the argument named and optional. The argument type must
+/// either be `Option<_>` _or_ the `#[default]` attribute must be used. (If
+/// it's both `Option<_>` and `#[default]`, then the argument can be specified
+/// as `none` in Typst).
+/// - `#[default]`: Specifies the default value of the argument as
+/// `Default::default()`.
+/// - `#[default(..)]`: Specifies the default value of the argument as `..`.
+/// - `#[variadic]`: Parses a variable number of arguments. The argument type
+/// must be `Vec<_>`.
+/// - `#[external]`: The argument appears in documentation, but is otherwise
+/// ignored. Can be useful if you want to do something manually for more
+/// flexibility.
+///
+/// Defaults can be specified for positional and named arguments. This is in
+/// contrast to user-defined functions which currently cannot have optional
+/// positional arguments (except through argument sinks).
+///
+/// In the example below, we define a `min` function that could be called as
+/// `min(1, 2, 3, default: 0)` in Typst.
+///
+/// ```ignore
+/// /// Determines the minimum of a sequence of values.
+/// #[func(title = "Minimum")]
+/// fn min(
+/// /// The values to extract the minimum from.
+/// #[variadic]
+/// values: Vec<i64>,
+/// /// A default value to return if there are no values.
+/// #[named]
+/// #[default(0)]
+/// default: i64,
+/// ) -> i64 {
+/// self.values.iter().min().unwrap_or(default)
+/// }
+/// ```
+///
+/// As you can see, arguments can also have doc-comments, which will be rendered
+/// in the documentation. The first line of documentation should be concise and
+/// self-contained as it is the designated short description, which is used in
+/// overviews in the documentation (and for autocompletion).
#[proc_macro_attribute]
pub fn func(stream: BoundaryStream, item: BoundaryStream) -> BoundaryStream {
let item = syn::parse_macro_input!(item as syn::ItemFn);
@@ -28,33 +102,230 @@ pub fn func(stream: BoundaryStream, item: BoundaryStream) -> BoundaryStream {
.into()
}
-/// Turns a type into an `Element`.
+/// Makes a native Rust type usable as a Typst type.
+///
+/// This implements `NativeType` for the given type.
+///
+/// ```ignore
+/// /// A sequence of codepoints.
+/// #[ty(scope, title = "String")]
+/// struct Str(EcoString);
+///
+/// #[scope]
+/// impl Str {
+/// ...
+/// }
+/// ```
+///
+/// # Properties
+/// You can customize some properties of the resulting type:
+/// - `scope`: Indicates that the type has an associated scope defined by the
+/// `#[scope]` macro
+/// - `name`: The type's normal name (e.g. `str`). Defaults to the Rust name in
+/// kebab-case.
+/// - `title`: The type's title case name (e.g. `String`). Defaults to the
+/// normal name in title case.
#[proc_macro_attribute]
-pub fn element(stream: BoundaryStream, item: BoundaryStream) -> BoundaryStream {
+pub fn ty(stream: BoundaryStream, item: BoundaryStream) -> BoundaryStream {
+ let item = syn::parse_macro_input!(item as syn::Item);
+ ty::ty(stream.into(), item)
+ .unwrap_or_else(|err| err.to_compile_error())
+ .into()
+}
+
+/// Makes a native Rust type usable as a Typst element.
+///
+/// This implements `NativeElement` for the given type.
+///
+/// ```
+/// /// A section heading.
+/// #[elem(Show, Count)]
+/// struct HeadingElem {
+/// /// The logical nesting depth of the heading, starting from one.
+/// #[default(NonZeroUsize::ONE)]
+/// level: NonZeroUsize,
+///
+/// /// The heading's title.
+/// #[required]
+/// body: Content,
+/// }
+/// ```
+///
+/// # Properties
+/// You can customize some properties of the resulting type:
+/// - `scope`: Indicates that the type has an associated scope defined by the
+/// `#[scope]` macro
+/// - `name`: The element's normal name (e.g. `str`). Defaults to the Rust name
+/// in kebab-case.
+/// - `title`: The type's title case name (e.g. `String`). Defaults to the long
+/// name in title case.
+/// - The remaining entries in the `elem` macros list are traits the element
+/// is capable of. These can be dynamically accessed.
+///
+/// # Fields
+/// By default, element fields are named and optional (and thus settable). You
+/// can use various attributes to configure their parsing behaviour:
+///
+/// - `#[positional]`: Makes the argument positional (but still optional).
+/// - `#[required]`: Makes the argument positional and required.
+/// - `#[default(..)]`: Specifies the default value of the argument as `..`.
+/// - `#[variadic]`: Parses a variable number of arguments. The field type must
+/// be `Vec<_>`. The field will be exposed as an array.
+/// - `#[parse({ .. })]`: A block of code that parses the field manually.
+///
+/// In addition that there are a number of attributes that configure other
+/// aspects of the field than the parsing behaviour.
+/// - `#[resolve]`: When accessing the field, it will be automatically
+/// resolved through the `Resolve` trait. This, for instance, turns `Length`
+/// into `Abs`. It's just convenient.
+/// - `#[fold]`: When there are multiple set rules for the field, all values
+/// are folded together into one. E.g. `set rect(stroke: 2pt)` and
+/// `set rect(stroke: red)` are combined into the equivalent of
+/// `set rect(stroke: 2pt + red)` instead of having `red` override `2pt`.
+/// - `#[internal]`: The field does not appear in the documentation.
+/// - `#[external]`: The field appears in the documentation, but is otherwise
+/// ignored. Can be useful if you want to do something manually for more
+/// flexibility.
+/// - `#[synthesized]`: The field cannot be specified in a constructor or set
+/// rule. Instead, it is added to an element before its show rule runs
+/// through the `Synthesize` trait.
+#[proc_macro_attribute]
+pub fn elem(stream: BoundaryStream, item: BoundaryStream) -> BoundaryStream {
let item = syn::parse_macro_input!(item as syn::ItemStruct);
- element::element(stream.into(), &item)
+ elem::elem(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)
+/// Provides an associated scope to a native function, type, or element.
+///
+/// This implements `NativeScope` for the function's shadow type, the type, or
+/// the element.
+///
+/// The implementation block can contain four kinds of items:
+/// - constants, which will be defined through `scope.define`
+/// - functions, which will be defined through `scope.define_func`
+/// - types, which will be defined through `scope.define_type`
+/// - elements, which will be defined through `scope.define_elem`
+///
+/// ```ignore
+/// #[func(scope)]
+/// fn name() { .. }
+///
+/// #[scope]
+/// impl name {
+/// /// A simple constant.
+/// const VAL: u32 = 0;
+///
+/// /// A function.
+/// #[func]
+/// fn foo() -> EcoString {
+/// "foo!".into()
+/// }
+///
+/// /// A type.
+/// type Brr;
+///
+/// /// An element.
+/// #[elem]
+/// type NiceElem;
+/// }
+///
+/// #[ty]
+/// struct Brr;
+///
+/// #[elem]
+/// struct NiceElem {}
+/// ```
+#[proc_macro_attribute]
+pub fn scope(stream: BoundaryStream, item: BoundaryStream) -> BoundaryStream {
+ let item = syn::parse_macro_input!(item as syn::Item);
+ scope::scope(stream.into(), item)
.unwrap_or_else(|err| err.to_compile_error())
.into()
}
/// Implements `Reflect`, `FromValue`, and `IntoValue` for a type.
+///
+/// - `Reflect` makes Typst's runtime aware of the type's characteristics.
+/// It's important for autocompletion, error messages, etc.
+/// - `FromValue` defines how to cast from a value into this type.
+/// - `IntoValue` defines how to cast fromthis type into a value.
+///
+/// ```ignore
+/// /// An integer between 0 and 13.
+/// struct CoolInt(u8);
+///
+/// cast! {
+/// CoolInt,
+///
+/// // Defines how to turn a `CoolInt` into a value.
+/// self => self.0.into_value(),
+///
+/// // Defines "match arms" of types that can be cast into a `CoolInt`.
+/// // These types needn't be value primitives, they can themselves use
+/// // `cast!`.
+/// v: bool => Self(v as u8),
+/// v: i64 => if matches!(v, 0..=13) {
+/// Self(v as u8)
+/// } else {
+/// bail!("integer is not nice :/")
+/// },
+/// }
+/// ```
#[proc_macro]
pub fn cast(stream: BoundaryStream) -> BoundaryStream {
- castable::cast(stream.into())
+ cast::cast(stream.into())
+ .unwrap_or_else(|err| err.to_compile_error())
+ .into()
+}
+
+/// Implements `Reflect`, `FromValue`, and `IntoValue` for an enum.
+///
+/// The enum will become castable from kebab-case strings. The doc-comments will
+/// become user-facing documentation for each variant. The `#[string]` attribute
+/// can be used to override the string corresponding to a variant.
+///
+/// ```ignore
+/// /// A stringy enum of options.
+/// #[derive(Cast)]
+/// enum Niceness {
+/// /// Clearly nice (parses from `"nice"`).
+/// Nice,
+/// /// Not so nice (parses from `"not-nice"`).
+/// NotNice,
+/// /// Very much not nice (parses from `"❌"`).
+/// #[string("❌")]
+/// Unnice,
+/// }
+/// ```
+#[proc_macro_derive(Cast, attributes(string))]
+pub fn derive_cast(item: BoundaryStream) -> BoundaryStream {
+ let item = syn::parse_macro_input!(item as DeriveInput);
+ cast::derive_cast(item)
.unwrap_or_else(|err| err.to_compile_error())
.into()
}
/// Defines a list of `Symbol`s.
+///
+/// ```ignore
+/// const EMOJI: &[(&str, Symbol)] = symbols! {
+/// // A plain symbol without modifiers.
+/// abacus: '🧮',
+///
+/// // A symbol with a modifierless default and one modifier.
+/// alien: ['👽', monster: '👾'],
+///
+/// // A symbol where each variant has a modifier. The first one will be
+/// // the default.
+/// clock: [one: '🕐', two: '🕑', ...],
+/// }
+/// ```
+///
+/// _Note:_ While this could use `macro_rules!` instead of a proc-macro, it was
+/// horribly slow in rust-analyzer. The underlying cause might be
+/// [this issue](https://github.com/rust-lang/rust-analyzer/issues/11108).
#[proc_macro]
pub fn symbols(stream: BoundaryStream) -> BoundaryStream {
symbols::symbols(stream.into())
diff --git a/crates/typst-macros/src/scope.rs b/crates/typst-macros/src/scope.rs
new file mode 100644
index 00000000..4aa17c6c
--- /dev/null
+++ b/crates/typst-macros/src/scope.rs
@@ -0,0 +1,153 @@
+use heck::ToKebabCase;
+
+use super::*;
+
+/// Expand the `#[scope]` macro.
+pub fn scope(_: TokenStream, item: syn::Item) -> Result<TokenStream> {
+ let syn::Item::Impl(mut item) = item else {
+ bail!(item, "expected module or impl item");
+ };
+
+ let eval = quote! { ::typst::eval };
+ let self_ty = &item.self_ty;
+
+ let mut definitions = vec![];
+ let mut constructor = quote! { None };
+ for child in &mut item.items {
+ let def = match child {
+ syn::ImplItem::Const(item) => handle_const(self_ty, item)?,
+ syn::ImplItem::Fn(item) => match handle_fn(self_ty, item)? {
+ FnKind::Member(tokens) => tokens,
+ FnKind::Constructor(tokens) => {
+ constructor = tokens;
+ continue;
+ }
+ },
+ syn::ImplItem::Verbatim(item) => handle_type_or_elem(item)?,
+ _ => bail!(child, "unexpected item in scope"),
+ };
+ definitions.push(def);
+ }
+
+ item.items.retain(|item| !matches!(item, syn::ImplItem::Verbatim(_)));
+
+ let mut base = quote! { #item };
+ if let syn::Type::Path(syn::TypePath { path, .. }) = self_ty.as_ref() {
+ if let Some(ident) = path.get_ident() {
+ if is_primitive(ident) {
+ base = rewrite_primitive_base(&item, ident);
+ }
+ }
+ }
+
+ Ok(quote! {
+ #base
+
+ impl #eval::NativeScope for #self_ty {
+ fn constructor() -> ::std::option::Option<&'static #eval::NativeFuncData> {
+ #constructor
+ }
+
+ fn scope() -> #eval::Scope {
+ let mut scope = #eval::Scope::deduplicating();
+ #(#definitions;)*
+ scope
+ }
+ }
+ })
+}
+
+/// Process a const item and returns its definition.
+fn handle_const(self_ty: &syn::Type, 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: &TokenStream) -> Result<TokenStream> {
+ let item: BareType = syn::parse2(item.clone())?;
+ 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: super::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),
+}
+
+/// Whether the identifier describes a primitive type.
+fn is_primitive(ident: &syn::Ident) -> bool {
+ ident == "bool" || ident == "i64" || ident == "f64"
+}
+
+/// Rewrite an impl block for a primitive into a trait + trait impl.
+fn rewrite_primitive_base(item: &syn::ItemImpl, ident: &syn::Ident) -> TokenStream {
+ let mut sigs = vec![];
+ let mut items = vec![];
+ for sub in &item.items {
+ let syn::ImplItem::Fn(mut func) = sub.clone() else { continue };
+ 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 ::typst::eval::NativeFuncData;
+ });
+ }
+
+ let ident_ext = quote::format_ident!("{ident}Ext");
+ let self_ty = &item.self_ty;
+ quote! {
+ trait #ident_ext {
+ #(#sigs)*
+ }
+
+ impl #ident_ext for #self_ty {
+ #(#items)*
+ }
+ }
+}
diff --git a/crates/typst-macros/src/symbols.rs b/crates/typst-macros/src/symbols.rs
index cdb7f5d7..8ab47f08 100644
--- a/crates/typst-macros/src/symbols.rs
+++ b/crates/typst-macros/src/symbols.rs
@@ -7,7 +7,7 @@ pub fn symbols(stream: TokenStream) -> Result<TokenStream> {
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::Single(c) => quote! { typst::eval::Symbol::single(#c), },
Kind::Multiple(variants) => {
let variants = variants.iter().map(|variant| {
let name = &variant.name;
diff --git a/crates/typst-macros/src/ty.rs b/crates/typst-macros/src/ty.rs
new file mode 100644
index 00000000..df60e7bb
--- /dev/null
+++ b/crates/typst-macros/src/ty.rs
@@ -0,0 +1,113 @@
+use syn::Attribute;
+
+use super::*;
+
+/// Expand the `#[ty]` macro.
+pub fn ty(stream: TokenStream, item: syn::Item) -> Result<TokenStream> {
+ let meta: Meta = syn::parse2(stream)?;
+ let bare: BareType;
+ let (ident, attrs, keep) = match &item {
+ syn::Item::Struct(item) => (&item.ident, &item.attrs, true),
+ syn::Item::Type(item) => (&item.ident, &item.attrs, true),
+ syn::Item::Enum(item) => (&item.ident, &item.attrs, true),
+ syn::Item::Verbatim(item) => {
+ bare = syn::parse2(item.clone())?;
+ (&bare.ident, &bare.attrs, false)
+ }
+ _ => bail!(item, "invalid type item"),
+ };
+ let ty = parse(meta, ident.clone(), attrs)?;
+ Ok(create(&ty, keep.then_some(&item)))
+}
+
+/// Holds all relevant parsed data about a type.
+struct Type {
+ ident: Ident,
+ name: String,
+ long: String,
+ scope: bool,
+ title: String,
+ docs: String,
+ keywords: Vec<String>,
+}
+
+/// The `..` in `#[ty(..)]`.
+struct Meta {
+ scope: bool,
+ name: Option<String>,
+ title: Option<String>,
+ keywords: Vec<String>,
+}
+
+impl Parse for Meta {
+ fn parse(input: ParseStream) -> Result<Self> {
+ Ok(Self {
+ scope: parse_flag::<kw::scope>(input)?,
+ name: parse_string::<kw::name>(input)?,
+ title: parse_string::<kw::title>(input)?,
+ keywords: parse_string_array::<kw::keywords>(input)?,
+ })
+ }
+}
+
+/// Parse details about the type from its definition.
+fn parse(meta: Meta, ident: Ident, attrs: &[Attribute]) -> Result<Type> {
+ let docs = documentation(attrs);
+ let (name, title) = determine_name_and_title(meta.name, meta.title, &ident, None)?;
+ let long = title.to_lowercase();
+ Ok(Type {
+ ident,
+ name,
+ long,
+ scope: meta.scope,
+ keywords: meta.keywords,
+ title,
+ docs,
+ })
+}
+
+/// Produce the output of the macro.
+fn create(ty: &Type, item: Option<&syn::Item>) -> TokenStream {
+ let eval = quote! { ::typst::eval };
+
+ let Type {
+ ident, name, long, title, docs, keywords, scope, ..
+ } = ty;
+
+ let constructor = if *scope {
+ quote! { <#ident as #eval::NativeScope>::constructor() }
+ } else {
+ quote! { None }
+ };
+
+ let scope = if *scope {
+ quote! { <#ident as #eval::NativeScope>::scope() }
+ } else {
+ quote! { #eval::Scope::new() }
+ };
+
+ let data = quote! {
+ #eval::NativeTypeData {
+ name: #name,
+ long_name: #long,
+ title: #title,
+ docs: #docs,
+ keywords: &[#(#keywords),*],
+ constructor: #eval::Lazy::new(|| #constructor),
+ scope: #eval::Lazy::new(|| #scope),
+ }
+ };
+
+ quote! {
+ #item
+
+ impl #eval::NativeType for #ident {
+ const NAME: &'static str = #name;
+
+ fn data() -> &'static #eval::NativeTypeData {
+ static DATA: #eval::NativeTypeData = #data;
+ &DATA
+ }
+ }
+ }
+}
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()?,
+ })
}
}