summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--library/src/text/mod.rs10
-rw-r--r--macros/src/capability.rs10
-rw-r--r--macros/src/lib.rs448
-rw-r--r--macros/src/node.rs516
-rw-r--r--src/model/content.rs12
-rw-r--r--src/model/mod.rs2
6 files changed, 564 insertions, 434 deletions
diff --git a/library/src/text/mod.rs b/library/src/text/mod.rs
index d36f8db5..a00510b6 100644
--- a/library/src/text/mod.rs
+++ b/library/src/text/mod.rs
@@ -114,22 +114,22 @@ impl TextNode {
/// Whether the font weight should be increased by 300.
#[property(skip, fold)]
- pub(super) const BOLD: Toggle = false;
+ const BOLD: Toggle = false;
/// Whether the font style should be inverted.
#[property(skip, fold)]
- pub(super) const ITALIC: Toggle = false;
+ const ITALIC: Toggle = false;
/// A case transformation that should be applied to the text.
#[property(skip)]
- pub(super) const CASE: Option<Case> = None;
+ const CASE: Option<Case> = None;
/// Whether small capital glyphs should be used. ("smcp")
#[property(skip)]
- pub(super) const SMALLCAPS: bool = false;
+ const SMALLCAPS: bool = false;
/// A destination the text should be linked to.
#[property(skip, referenced)]
pub(crate) const LINK: Option<Destination> = None;
/// Decorative lines.
#[property(skip, fold)]
- pub(super) const DECO: Decoration = vec![];
+ const DECO: Decoration = vec![];
fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> {
// The text constructor is special: It doesn't create a text node.
diff --git a/macros/src/capability.rs b/macros/src/capability.rs
new file mode 100644
index 00000000..7dd4c42a
--- /dev/null
+++ b/macros/src/capability.rs
@@ -0,0 +1,10 @@
+use super::*;
+
+/// Expand the `#[capability]` macro.
+pub fn expand(body: syn::ItemTrait) -> Result<TokenStream> {
+ let ident = &body.ident;
+ Ok(quote! {
+ #body
+ impl ::typst::model::Capability for dyn #ident {}
+ })
+}
diff --git a/macros/src/lib.rs b/macros/src/lib.rs
index 7e2caae5..699deee9 100644
--- a/macros/src/lib.rs
+++ b/macros/src/lib.rs
@@ -2,437 +2,39 @@
extern crate proc_macro;
-use proc_macro::TokenStream;
-use proc_macro2::{TokenStream as TokenStream2, TokenTree};
+/// Return an error at the given item.
+macro_rules! bail {
+ ($item:expr, $fmt:literal $($tts:tt)*) => {
+ return Err(Error::new_spanned(
+ &$item,
+ format!(concat!("typst: ", $fmt) $($tts)*)
+ ))
+ }
+}
+
+mod capability;
+mod node;
+
+use proc_macro::TokenStream as BoundaryStream;
+use proc_macro2::{TokenStream, TokenTree};
use quote::{quote, quote_spanned};
use syn::parse_quote;
-use syn::punctuated::Punctuated;
-use syn::spanned::Spanned;
use syn::{Error, Ident, Result};
-/// Implement `Capability` for a trait.
-#[proc_macro_attribute]
-pub fn capability(_: TokenStream, item: TokenStream) -> TokenStream {
- let item_trait = syn::parse_macro_input!(item as syn::ItemTrait);
- let name = &item_trait.ident;
- quote! {
- #item_trait
- impl ::typst::model::Capability for dyn #name {}
- }
- .into()
-}
-
/// Implement `Node` for a struct.
#[proc_macro_attribute]
-pub fn node(stream: TokenStream, item: TokenStream) -> TokenStream {
- let impl_block = syn::parse_macro_input!(item as syn::ItemImpl);
- expand_node(stream.into(), impl_block)
+pub fn node(stream: BoundaryStream, item: BoundaryStream) -> BoundaryStream {
+ let item = syn::parse_macro_input!(item as syn::ItemImpl);
+ node::expand(stream.into(), item)
.unwrap_or_else(|err| err.to_compile_error())
.into()
}
-/// Expand an impl block for a node.
-fn expand_node(
- stream: TokenStream2,
- mut impl_block: syn::ItemImpl,
-) -> Result<TokenStream2> {
- // Split the node type into name and generic type arguments.
- let params = &impl_block.generics.params;
- let self_ty = &*impl_block.self_ty;
- let (self_name, self_args) = parse_self(self_ty)?;
-
- let module = quote::format_ident!("{}_types", self_name);
-
- let mut key_modules = vec![];
- let mut properties = vec![];
- let mut construct = None;
- let mut set = None;
- let mut field = None;
-
- for item in std::mem::take(&mut impl_block.items) {
- match item {
- syn::ImplItem::Const(mut item) => {
- let (property, module) =
- process_const(&mut item, params, self_ty, &self_name, &self_args)?;
- properties.push(property);
- key_modules.push(module);
- impl_block.items.push(syn::ImplItem::Const(item));
- }
- syn::ImplItem::Method(method) => {
- match method.sig.ident.to_string().as_str() {
- "construct" => construct = Some(method),
- "set" => set = Some(method),
- "field" => field = Some(method),
- _ => return Err(Error::new(method.span(), "unexpected method")),
- }
- }
- _ => return Err(Error::new(item.span(), "unexpected item")),
- }
- }
-
- let construct = construct.unwrap_or_else(|| {
- parse_quote! {
- fn construct(
- _: &mut model::Vm,
- _: &mut model::Args,
- ) -> typst::diag::SourceResult<model::Content> {
- unimplemented!()
- }
- }
- });
-
- let set = generate_set(&properties, set);
-
- let field = field.unwrap_or_else(|| {
- parse_quote! {
- fn field(&self, name: &str) -> Option<Value> {
- None
- }
- }
- });
-
- let items: syn::punctuated::Punctuated<Ident, syn::Token![,]> =
- parse_quote! { #stream };
-
- let checks = items.iter().map(|cap| {
- quote! {
- if id == TypeId::of::<dyn #cap>() {
- return Some(unsafe { typst::util::fat::vtable(self as &dyn #cap) });
- }
- }
- });
-
- let vtable = quote! {
- fn vtable(&self, id: TypeId) -> Option<*const ()> {
- #(#checks)*
- None
- }
- };
-
- let name = self_name.trim_end_matches("Node").to_lowercase();
-
- // Put everything into a module with a hopefully unique type to isolate
- // it from the outside.
- Ok(quote! {
- #[allow(non_snake_case)]
- mod #module {
- use ::std::any::TypeId;
- use ::std::marker::PhantomData;
- use ::once_cell::sync::Lazy;
- use ::typst::model;
- use super::*;
-
- #impl_block
-
- impl<#params> model::Node for #self_ty {
- #construct
- #set
- #field
-
- fn id(&self) -> model::NodeId {
- model::NodeId::of::<Self>()
- }
-
- fn name(&self) -> &'static str {
- #name
- }
-
- #vtable
- }
-
- #(#key_modules)*
- }
- })
-}
-
-/// Parse the name and generic type arguments of the node type.
-fn parse_self(
- self_ty: &syn::Type,
-) -> Result<(String, Punctuated<syn::GenericArgument, syn::Token![,]>)> {
- // Extract the node type for which we want to generate properties.
- let path = match self_ty {
- syn::Type::Path(path) => path,
- ty => return Err(Error::new(ty.span(), "must be a path type")),
- };
-
- // Split up the type into its name and its generic type arguments.
- let last = path.path.segments.last().unwrap();
- let self_name = last.ident.to_string();
- let self_args = match &last.arguments {
- syn::PathArguments::AngleBracketed(args) => args.args.clone(),
- _ => Punctuated::new(),
- };
-
- Ok((self_name, self_args))
-}
-
-/// Process a single const item.
-fn process_const(
- item: &mut syn::ImplItemConst,
- params: &Punctuated<syn::GenericParam, syn::Token![,]>,
- self_ty: &syn::Type,
- self_name: &str,
- self_args: &Punctuated<syn::GenericArgument, syn::Token![,]>,
-) -> Result<(Property, syn::ItemMod)> {
- let property = parse_property(item)?;
-
- // The display name, e.g. `TextNode::BOLD`.
- let name = format!("{}::{}", self_name, &item.ident);
-
- // The type of the property's value is what the user of our macro wrote
- // as type of the const ...
- let value_ty = &item.ty;
- let output_ty = if property.referenced {
- parse_quote!(&'a #value_ty)
- } else if property.fold && property.resolve {
- parse_quote!(<<#value_ty as model::Resolve>::Output as model::Fold>::Output)
- } else if property.fold {
- parse_quote!(<#value_ty as model::Fold>::Output)
- } else if property.resolve {
- parse_quote!(<#value_ty as model::Resolve>::Output)
- } else {
- value_ty.clone()
- };
-
- // ... but the real type of the const becomes this ...
- let key = quote! { Key<#value_ty, #self_args> };
- let phantom_args = self_args.iter().filter(|arg| match arg {
- syn::GenericArgument::Type(syn::Type::Path(path)) => {
- params.iter().all(|param| match param {
- syn::GenericParam::Const(c) => !path.path.is_ident(&c.ident),
- _ => true,
- })
- }
- _ => true,
- });
-
- let default = &item.expr;
-
- // Ensure that the type is
- // - either `Copy`, or
- // - that the property is referenced, or
- // - that the property isn't copy but can't be referenced because it needs
- // folding.
- let get;
- let mut copy = None;
-
- if property.referenced {
- get = quote! {
- values.next().unwrap_or_else(|| {
- static LAZY: Lazy<#value_ty> = Lazy::new(|| #default);
- &*LAZY
- })
- };
- } else if property.resolve && property.fold {
- get = quote! {
- match values.next().cloned() {
- Some(value) => model::Fold::fold(
- model::Resolve::resolve(value, chain),
- Self::get(chain, values),
- ),
- None => #default,
- }
- };
- } else if property.resolve {
- get = quote! {
- let value = values.next().cloned().unwrap_or_else(|| #default);
- model::Resolve::resolve(value, chain)
- };
- } else if property.fold {
- get = quote! {
- match values.next().cloned() {
- Some(value) => model::Fold::fold(value, Self::get(chain, values)),
- None => #default,
- }
- };
- } else {
- get = quote! {
- values.next().copied().unwrap_or(#default)
- };
-
- copy = Some(quote_spanned! { item.ty.span() =>
- const _: fn() -> () = || {
- fn type_must_be_copy_or_fold_or_referenced<T: Copy>() {}
- type_must_be_copy_or_fold_or_referenced::<#value_ty>();
- };
- });
- }
-
- // Generate the module code.
- let module_name = &item.ident;
- let module = parse_quote! {
- #[allow(non_snake_case)]
- mod #module_name {
- use super::*;
-
- pub struct Key<VALUE, #params>(pub PhantomData<(VALUE, #(#phantom_args,)*)>);
-
- impl<#params> Copy for #key {}
- impl<#params> Clone for #key {
- fn clone(&self) -> Self {
- *self
- }
- }
-
- impl<'a, #params> model::Key<'a> for #key {
- type Value = #value_ty;
- type Output = #output_ty;
-
- const NAME: &'static str = #name;
-
- fn node() -> model::NodeId {
- model::NodeId::of::<#self_ty>()
- }
-
- fn get(
- chain: model::StyleChain<'a>,
- mut values: impl Iterator<Item = &'a Self::Value>,
- ) -> Self::Output {
- #get
- }
- }
-
- #copy
- }
- };
-
- // Replace type and initializer expression with the `Key`.
- item.ty = parse_quote! { #module_name::#key };
- item.expr = parse_quote! { #module_name::Key(PhantomData) };
-
- Ok((property, module))
-}
-
-/// A style property.
-struct Property {
- name: Ident,
- skip: bool,
- referenced: bool,
- shorthand: Option<Shorthand>,
- resolve: bool,
- fold: bool,
-}
-
-enum Shorthand {
- Positional,
- Named(Ident),
-}
-
-/// Parse a style property attribute.
-fn parse_property(item: &mut syn::ImplItemConst) -> Result<Property> {
- let mut property = Property {
- name: item.ident.clone(),
- skip: false,
- shorthand: None,
- referenced: false,
- resolve: false,
- fold: false,
- };
-
- if let Some(idx) = item
- .attrs
- .iter()
- .position(|attr| attr.path.get_ident().map_or(false, |name| name == "property"))
- {
- let attr = item.attrs.remove(idx);
- let mut stream = attr.parse_args::<TokenStream2>()?.into_iter().peekable();
- while let Some(token) = stream.next() {
- match token {
- TokenTree::Ident(ident) => match ident.to_string().as_str() {
- "skip" => property.skip = true,
- "shorthand" => {
- let short = if let Some(TokenTree::Group(group)) = stream.peek() {
- let span = group.span();
- let repr = group.to_string();
- let ident = repr.trim_matches(|c| matches!(c, '(' | ')'));
- if !ident.chars().all(|c| c.is_ascii_alphabetic()) {
- return Err(Error::new(span, "invalid args"));
- }
- stream.next();
- Shorthand::Named(Ident::new(ident, span))
- } else {
- Shorthand::Positional
- };
- property.shorthand = Some(short);
- }
- "referenced" => property.referenced = true,
- "resolve" => property.resolve = true,
- "fold" => property.fold = true,
- _ => return Err(Error::new(ident.span(), "invalid attribute")),
- },
- TokenTree::Punct(_) => {}
- _ => return Err(Error::new(token.span(), "invalid token")),
- }
- }
- }
-
- let span = property.name.span();
- if property.skip && property.shorthand.is_some() {
- return Err(Error::new(span, "skip and shorthand are mutually exclusive"));
- }
-
- if property.referenced && (property.fold || property.resolve) {
- return Err(Error::new(
- span,
- "referenced is mutually exclusive with fold and resolve",
- ));
- }
-
- Ok(property)
-}
-
-/// Auto-generate a `set` function from properties.
-fn generate_set(
- properties: &[Property],
- user: Option<syn::ImplItemMethod>,
-) -> syn::ImplItemMethod {
- let user = user.map(|method| {
- let block = &method.block;
- quote! { (|| -> typst::diag::SourceResult<()> { #block; Ok(()) } )()?; }
- });
-
- let mut shorthands = vec![];
- let sets: Vec<_> = properties
- .iter()
- .filter(|p| !p.skip)
- .map(|property| {
- let name = &property.name;
- let string = name.to_string().replace('_', "-").to_lowercase();
-
- let value = if let Some(short) = &property.shorthand {
- match short {
- Shorthand::Positional => quote! { args.named_or_find(#string)? },
- Shorthand::Named(named) => {
- shorthands.push(named);
- quote! { args.named(#string)?.or_else(|| #named.clone()) }
- }
- }
- } else {
- quote! { args.named(#string)? }
- };
-
- quote! { styles.set_opt(Self::#name, #value); }
- })
- .collect();
-
- shorthands.sort();
- shorthands.dedup_by_key(|ident| ident.to_string());
-
- let bindings = shorthands.into_iter().map(|ident| {
- let string = ident.to_string();
- quote! { let #ident = args.named(#string)?; }
- });
-
- parse_quote! {
- fn set(
- args: &mut model::Args,
- constructor: bool,
- ) -> typst::diag::SourceResult<model::StyleMap> {
- let mut styles = model::StyleMap::new();
- #user
- #(#bindings)*
- #(#sets)*
- Ok(styles)
- }
- }
+/// Implement `Capability` for a trait.
+#[proc_macro_attribute]
+pub fn capability(_: BoundaryStream, item: BoundaryStream) -> BoundaryStream {
+ let item = syn::parse_macro_input!(item as syn::ItemTrait);
+ capability::expand(item)
+ .unwrap_or_else(|err| err.to_compile_error())
+ .into()
}
diff --git a/macros/src/node.rs b/macros/src/node.rs
new file mode 100644
index 00000000..b92dabcc
--- /dev/null
+++ b/macros/src/node.rs
@@ -0,0 +1,516 @@
+use syn::parse::Parser;
+use syn::punctuated::Punctuated;
+use syn::spanned::Spanned;
+use syn::Token;
+
+use super::*;
+
+/// Expand the `#[node]` macro.
+pub fn expand(attr: TokenStream, body: syn::ItemImpl) -> Result<TokenStream> {
+ let node = prepare(attr, body)?;
+ let scope = create(&node)?;
+ Ok(quote! {
+ const _: () = { #scope };
+ })
+}
+
+/// Details about a node.
+struct Node {
+ body: syn::ItemImpl,
+ params: Punctuated<syn::GenericParam, Token![,]>,
+ self_ty: syn::Type,
+ self_name: String,
+ self_args: Punctuated<syn::GenericArgument, Token![,]>,
+ capabilities: Vec<syn::Ident>,
+ properties: Vec<Property>,
+ construct: Option<syn::ImplItemMethod>,
+ set: Option<syn::ImplItemMethod>,
+ field: Option<syn::ImplItemMethod>,
+}
+
+/// A style property.
+struct Property {
+ attrs: Vec<syn::Attribute>,
+ vis: syn::Visibility,
+ name: Ident,
+ value_ty: syn::Type,
+ output_ty: syn::Type,
+ default: syn::Expr,
+ skip: bool,
+ referenced: bool,
+ shorthand: Option<Shorthand>,
+ resolve: bool,
+ fold: bool,
+}
+
+/// The shorthand form of a style property.
+enum Shorthand {
+ Positional,
+ Named(Ident),
+}
+
+/// Preprocess the impl block of a node.
+fn prepare(attr: TokenStream, body: syn::ItemImpl) -> Result<Node> {
+ // Extract the generic type arguments.
+ let params = body.generics.params.clone();
+
+ // Extract the node type for which we want to generate properties.
+ let self_ty = (*body.self_ty).clone();
+ let self_path = match &self_ty {
+ syn::Type::Path(path) => path,
+ ty => bail!(ty, "must be a path type"),
+ };
+
+ // Split up the type into its name and its generic type arguments.
+ let last = self_path.path.segments.last().unwrap();
+ let self_name = last.ident.to_string();
+ let self_args = match &last.arguments {
+ syn::PathArguments::AngleBracketed(args) => args.args.clone(),
+ _ => Punctuated::new(),
+ };
+
+ // Parse the capabilities.
+ let capabilities: Vec<_> = Punctuated::<Ident, Token![,]>::parse_terminated
+ .parse2(attr)?
+ .into_iter()
+ .collect();
+
+ let mut properties = vec![];
+ let mut construct = None;
+ let mut set = None;
+ let mut field = None;
+
+ // Parse the properties and methods.
+ for item in &body.items {
+ match item {
+ syn::ImplItem::Const(item) => {
+ properties.push(prepare_property(item)?);
+ }
+ syn::ImplItem::Method(method) => {
+ match method.sig.ident.to_string().as_str() {
+ "construct" => construct = Some(method.clone()),
+ "set" => set = Some(method.clone()),
+ "field" => field = Some(method.clone()),
+ _ => bail!(method, "unexpected method"),
+ }
+ }
+ _ => bail!(item, "unexpected item"),
+ }
+ }
+
+ Ok(Node {
+ body,
+ params,
+ self_ty,
+ self_name,
+ self_args,
+ capabilities,
+ properties,
+ construct,
+ set,
+ field,
+ })
+}
+
+/// Preprocess and validate a property constant.
+fn prepare_property(item: &syn::ImplItemConst) -> Result<Property> {
+ let mut attrs = item.attrs.clone();
+ let tokens = match attrs
+ .iter()
+ .position(|attr| attr.path.is_ident("property"))
+ .map(|i| attrs.remove(i))
+ {
+ Some(attr) => attr.parse_args::<TokenStream>()?,
+ None => TokenStream::default(),
+ };
+
+ let mut skip = false;
+ let mut shorthand = None;
+ let mut referenced = false;
+ let mut resolve = false;
+ let mut fold = false;
+
+ // Parse the `#[property(..)]` attribute.
+ let mut stream = tokens.into_iter().peekable();
+ while let Some(token) = stream.next() {
+ let ident = match token {
+ TokenTree::Ident(ident) => ident,
+ TokenTree::Punct(_) => continue,
+ _ => bail!(token, "invalid token"),
+ };
+
+ let mut arg = None;
+ if let Some(TokenTree::Group(group)) = stream.peek() {
+ let span = group.span();
+ let string = group.to_string();
+ let ident = string.trim_start_matches('(').trim_end_matches(')');
+ if !ident.chars().all(|c| c.is_ascii_alphabetic()) {
+ bail!(group, "invalid arguments");
+ }
+ arg = Some(Ident::new(ident, span));
+ stream.next();
+ };
+
+ match ident.to_string().as_str() {
+ "skip" => skip = true,
+ "shorthand" => {
+ shorthand = Some(match arg {
+ Some(name) => Shorthand::Named(name),
+ None => Shorthand::Positional,
+ });
+ }
+ "referenced" => referenced = true,
+ "resolve" => resolve = true,
+ "fold" => fold = true,
+ _ => bail!(ident, "invalid attribute"),
+ }
+ }
+
+ if skip && shorthand.is_some() {
+ bail!(item.ident, "skip and shorthand are mutually exclusive");
+ }
+
+ if referenced && (fold || resolve) {
+ bail!(item.ident, "referenced is mutually exclusive with fold and resolve");
+ }
+
+ // The type of the property's value is what the user of our macro wrote as
+ // type of the const, but the real type of the const will be a unique `Key`
+ // type.
+ let value_ty = item.ty.clone();
+ let output_ty = if referenced {
+ parse_quote! { &'a #value_ty }
+ } else if fold && resolve {
+ parse_quote! {
+ <<#value_ty as ::typst::model::Resolve>::Output
+ as ::typst::model::Fold>::Output
+ }
+ } else if fold {
+ parse_quote! { <#value_ty as ::typst::model::Fold>::Output }
+ } else if resolve {
+ parse_quote! { <#value_ty as ::typst::model::Resolve>::Output }
+ } else {
+ value_ty.clone()
+ };
+
+ Ok(Property {
+ attrs,
+ vis: item.vis.clone(),
+ name: item.ident.clone(),
+ value_ty,
+ output_ty,
+ default: item.expr.clone(),
+ skip,
+ shorthand,
+ referenced,
+ resolve,
+ fold,
+ })
+}
+
+/// Produce the necessary items for a type to become a node.
+fn create(node: &Node) -> Result<TokenStream> {
+ let params = &node.params;
+ let self_ty = &node.self_ty;
+
+ let id_method = create_node_id_method();
+ let name_method = create_node_name_method(node);
+ let construct_func = create_node_construct_func(node);
+ let set_func = create_node_set_func(node);
+ let field_method = create_node_field_method(node);
+ let vtable_method = create_node_vtable_method(node);
+
+ let node_impl = quote! {
+ impl<#params> ::typst::model::Node for #self_ty {
+ #id_method
+ #name_method
+ #construct_func
+ #set_func
+ #field_method
+ #vtable_method
+ }
+ };
+
+ let mut modules: Vec<syn::ItemMod> = vec![];
+ let mut items: Vec<syn::ImplItem> = vec![];
+
+ for property in &node.properties {
+ let (key, module) = create_property_module(node, &property);
+ modules.push(module);
+
+ let name = &property.name;
+ let attrs = &property.attrs;
+ let vis = &property.vis;
+ items.push(parse_quote! {
+ #(#attrs)*
+ #vis const #name: #name::#key = #name::Key(::std::marker::PhantomData);
+ });
+ }
+
+ let mut body = node.body.clone();
+ body.items = items;
+
+ Ok(quote! {
+ #body
+ #node_impl
+ #(#modules)*
+ })
+}
+
+/// Create the node's id method.
+fn create_node_id_method() -> syn::ImplItemMethod {
+ parse_quote! {
+ fn id(&self) -> ::typst::model::NodeId {
+ ::typst::model::NodeId::of::<Self>()
+ }
+ }
+}
+
+/// Create the node's name method.
+fn create_node_name_method(node: &Node) -> syn::ImplItemMethod {
+ let name = node.self_name.trim_end_matches("Node").to_lowercase();
+ parse_quote! {
+ fn name(&self) -> &'static str {
+ #name
+ }
+ }
+}
+
+/// Create the node's `construct` function.
+fn create_node_construct_func(node: &Node) -> syn::ImplItemMethod {
+ node.construct.clone().unwrap_or_else(|| {
+ parse_quote! {
+ fn construct(
+ _: &mut ::typst::model::Vm,
+ _: &mut ::typst::model::Args,
+ ) -> ::typst::diag::SourceResult<::typst::model::Content> {
+ unimplemented!()
+ }
+ }
+ })
+}
+
+/// Create the node's `set` function.
+fn create_node_set_func(node: &Node) -> syn::ImplItemMethod {
+ let user = node.set.as_ref().map(|method| {
+ let block = &method.block;
+ quote! { (|| -> typst::diag::SourceResult<()> { #block; Ok(()) } )()?; }
+ });
+
+ let mut shorthands = vec![];
+ let sets: Vec<_> = node
+ .properties
+ .iter()
+ .filter(|p| !p.skip)
+ .map(|property| {
+ let name = &property.name;
+ let string = name.to_string().replace('_', "-").to_lowercase();
+ let value = match &property.shorthand {
+ Some(Shorthand::Positional) => quote! { args.named_or_find(#string)? },
+ Some(Shorthand::Named(named)) => {
+ shorthands.push(named);
+ quote! { args.named(#string)?.or_else(|| #named.clone()) }
+ }
+ None => quote! { args.named(#string)? },
+ };
+
+ quote! { styles.set_opt(Self::#name, #value); }
+ })
+ .collect();
+
+ shorthands.sort();
+ shorthands.dedup_by_key(|ident| ident.to_string());
+
+ let bindings = shorthands.into_iter().map(|ident| {
+ let string = ident.to_string();
+ quote! { let #ident = args.named(#string)?; }
+ });
+
+ parse_quote! {
+ fn set(
+ args: &mut ::typst::model::Args,
+ constructor: bool,
+ ) -> ::typst::diag::SourceResult<::typst::model::StyleMap> {
+ let mut styles = ::typst::model::StyleMap::new();
+ #user
+ #(#bindings)*
+ #(#sets)*
+ Ok(styles)
+ }
+ }
+}
+
+/// Create the node's `field` method.
+fn create_node_field_method(node: &Node) -> syn::ImplItemMethod {
+ node.field.clone().unwrap_or_else(|| {
+ parse_quote! {
+ fn field(
+ &self,
+ _: &str,
+ ) -> ::std::option::Option<::typst::model::Value> {
+ None
+ }
+ }
+ })
+}
+
+/// Create the node's capability accessor method.
+fn create_node_vtable_method(node: &Node) -> syn::ImplItemMethod {
+ let checks = node.capabilities.iter().map(|capability| {
+ quote! {
+ if id == ::std::any::TypeId::of::<dyn #capability>() {
+ return Some(unsafe {
+ ::typst::util::fat::vtable(self as &dyn #capability)
+ });
+ }
+ }
+ });
+
+ parse_quote! {
+ fn vtable(&self, id: ::std::any::TypeId) -> ::std::option::Option<*const ()> {
+ #(#checks)*
+ None
+ }
+ }
+}
+
+/// Process a single const item.
+fn create_property_module(node: &Node, property: &Property) -> (syn::Type, syn::ItemMod) {
+ let params = &node.params;
+ let self_args = &node.self_args;
+ let name = &property.name;
+ let value_ty = &property.value_ty;
+ let output_ty = &property.output_ty;
+
+ let key = parse_quote! { Key<#value_ty, #self_args> };
+ let phantom_args = self_args.iter().filter(|arg| match arg {
+ syn::GenericArgument::Type(syn::Type::Path(path)) => {
+ node.params.iter().all(|param| match param {
+ syn::GenericParam::Const(c) => !path.path.is_ident(&c.ident),
+ _ => true,
+ })
+ }
+ _ => true,
+ });
+
+ let name_const = create_property_name_const(node, property);
+ let node_func = create_property_node_func(node);
+ let get_method = create_property_get_method(property);
+ let copy_assertion = create_property_copy_assertion(property);
+
+ // Generate the contents of the module.
+ let scope = quote! {
+ use super::*;
+
+ pub struct Key<__T, #params>(
+ pub ::std::marker::PhantomData<(__T, #(#phantom_args,)*)>
+ );
+
+ impl<#params> ::std::marker::Copy for #key {}
+ impl<#params> ::std::clone::Clone for #key {
+ fn clone(&self) -> Self { *self }
+ }
+
+ impl<'a, #params> ::typst::model::Key<'a> for #key {
+ type Value = #value_ty;
+ type Output = #output_ty;
+ #name_const
+ #node_func
+ #get_method
+ }
+
+ #copy_assertion
+ };
+
+ // Generate the module code.
+ let module = parse_quote! {
+ #[allow(non_snake_case)]
+ mod #name { #scope }
+ };
+
+ (key, module)
+}
+
+/// Create the property's node method.
+fn create_property_name_const(node: &Node, property: &Property) -> syn::ImplItemConst {
+ // The display name, e.g. `TextNode::BOLD`.
+ let name = format!("{}::{}", node.self_name, &property.name);
+ parse_quote! {
+ const NAME: &'static str = #name;
+ }
+}
+
+/// Create the property's node method.
+fn create_property_node_func(node: &Node) -> syn::ImplItemMethod {
+ let self_ty = &node.self_ty;
+ parse_quote! {
+ fn node() -> ::typst::model::NodeId {
+ ::typst::model::NodeId::of::<#self_ty>()
+ }
+ }
+}
+
+/// Create the property's get method.
+fn create_property_get_method(property: &Property) -> syn::ImplItemMethod {
+ let default = &property.default;
+ let value_ty = &property.value_ty;
+
+ let value = if property.referenced {
+ quote! {
+ values.next().unwrap_or_else(|| {
+ static LAZY: ::typst::model::once_cell::sync::Lazy<#value_ty>
+ = ::typst::model::once_cell::sync::Lazy::new(|| #default);
+ &*LAZY
+ })
+ }
+ } else if property.resolve && property.fold {
+ quote! {
+ match values.next().cloned() {
+ Some(value) => ::typst::model::Fold::fold(
+ ::typst::model::Resolve::resolve(value, chain),
+ Self::get(chain, values),
+ ),
+ None => #default,
+ }
+ }
+ } else if property.resolve {
+ quote! {
+ let value = values.next().cloned().unwrap_or_else(|| #default);
+ ::typst::model::Resolve::resolve(value, chain)
+ }
+ } else if property.fold {
+ quote! {
+ match values.next().cloned() {
+ Some(value) => ::typst::model::Fold::fold(value, Self::get(chain, values)),
+ None => #default,
+ }
+ }
+ } else {
+ quote! {
+ values.next().copied().unwrap_or(#default)
+ }
+ };
+
+ parse_quote! {
+ fn get(
+ chain: ::typst::model::StyleChain<'a>,
+ mut values: impl ::std::iter::Iterator<Item = &'a Self::Value>,
+ ) -> Self::Output {
+ #value
+ }
+ }
+}
+
+/// Create the assertion if the property's value must be copyable.
+fn create_property_copy_assertion(property: &Property) -> Option<TokenStream> {
+ let value_ty = &property.value_ty;
+ let must_be_copy = !property.fold && !property.resolve && !property.referenced;
+ must_be_copy.then(|| {
+ quote_spanned! { value_ty.span() =>
+ const _: fn() -> () = || {
+ fn must_be_copy_fold_resolve_or_referenced<T: ::std::marker::Copy>() {}
+ must_be_copy_fold_resolve_or_referenced::<#value_ty>();
+ };
+ }
+ })
+}
diff --git a/src/model/content.rs b/src/model/content.rs
index 210b8bde..c28082c2 100644
--- a/src/model/content.rs
+++ b/src/model/content.rs
@@ -281,6 +281,12 @@ pub trait Node: 'static {
Content(Arc::new(self), vec![], None)
}
+ /// A unique identifier of the node type.
+ fn id(&self) -> NodeId;
+
+ /// The node's name.
+ fn name(&self) -> &'static str;
+
/// Construct a node from the arguments.
///
/// This is passed only the arguments that remain after execution of the
@@ -300,12 +306,6 @@ pub trait Node: 'static {
/// Access a field on this node.
fn field(&self, name: &str) -> Option<Value>;
- /// A unique identifier of the node type.
- fn id(&self) -> NodeId;
-
- /// The node's name.
- fn name(&self) -> &'static str;
-
/// Extract the pointer of the vtable of the trait object with the
/// given type `id` if this node implements that trait.
fn vtable(&self, id: TypeId) -> Option<*const ()>;
diff --git a/src/model/mod.rs b/src/model/mod.rs
index 20bca106..0f946a40 100644
--- a/src/model/mod.rs
+++ b/src/model/mod.rs
@@ -23,6 +23,8 @@ mod ops;
mod scope;
mod vm;
+#[doc(hidden)]
+pub use once_cell;
pub use typst_macros::{capability, node};
pub use self::args::*;