summaryrefslogtreecommitdiff
path: root/macros/src/element.rs
diff options
context:
space:
mode:
Diffstat (limited to 'macros/src/element.rs')
-rw-r--r--macros/src/element.rs533
1 files changed, 533 insertions, 0 deletions
diff --git a/macros/src/element.rs b/macros/src/element.rs
new file mode 100644
index 00000000..9d6b5c88
--- /dev/null
+++ b/macros/src/element.rs
@@ -0,0 +1,533 @@
+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,
+ docs: String,
+ vis: syn::Visibility,
+ ident: Ident,
+ capable: Vec<Ident>,
+ fields: Vec<Field>,
+}
+
+struct Field {
+ name: String,
+ docs: String,
+ internal: bool,
+ external: bool,
+ positional: bool,
+ required: bool,
+ variadic: bool,
+ synthesized: bool,
+ fold: bool,
+ resolve: bool,
+ parse: Option<FieldParser>,
+ 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()
+ }
+}
+
+struct FieldParser {
+ prefix: Vec<syn::Stmt>,
+ expr: syn::Stmt,
+}
+
+impl Parse for FieldParser {
+ 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 on expression"));
+ };
+ Ok(Self { prefix: stmts, expr })
+ }
+}
+
+/// 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 docs = documentation(&body.attrs);
+ let mut lines = docs.split("\n").collect();
+ 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,
+ docs,
+ vis: body.vis.clone(),
+ ident: body.ident.clone(),
+ capable,
+ fields,
+ };
+
+ validate_attrs(&body.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()
+ }
+ }
+
+ #element_impl
+ #construct_impl
+ #set_impl
+ #locatable_impl
+
+ impl From<#ident> for ::typst::eval::Value {
+ fn from(value: #ident) -> Self {
+ value.0.into()
+ }
+ }
+ }
+}
+
+/// 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.into(),
+ #ident.into()
+ ))
+ }
+ }
+}
+
+/// Create the element's `Pack` implementation.
+fn create_pack_impl(element: &Elem) -> TokenStream {
+ let Elem { ident, name, display, 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);
+ 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,
+ docs: #docs,
+ params: ::std::vec![#(#infos),*],
+ returns: ::std::vec!["content"],
+ category: #category,
+ }),
+ };
+ (&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, ty, .. } = field;
+ let named = !positional;
+ let settable = field.settable();
+ 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::Cast<
+ ::typst::syntax::Spanned<::typst::eval::Value>
+ >>::describe(),
+ 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(FieldParser { 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)
+}