mirror of
https://github.com/typst/typst
synced 2025-05-13 20:46:23 +08:00
Refactor proc macros
This commit is contained in:
parent
7c7b830225
commit
36ea0b05c9
@ -114,22 +114,22 @@ impl TextNode {
|
|||||||
|
|
||||||
/// Whether the font weight should be increased by 300.
|
/// Whether the font weight should be increased by 300.
|
||||||
#[property(skip, fold)]
|
#[property(skip, fold)]
|
||||||
pub(super) const BOLD: Toggle = false;
|
const BOLD: Toggle = false;
|
||||||
/// Whether the font style should be inverted.
|
/// Whether the font style should be inverted.
|
||||||
#[property(skip, fold)]
|
#[property(skip, fold)]
|
||||||
pub(super) const ITALIC: Toggle = false;
|
const ITALIC: Toggle = false;
|
||||||
/// A case transformation that should be applied to the text.
|
/// A case transformation that should be applied to the text.
|
||||||
#[property(skip)]
|
#[property(skip)]
|
||||||
pub(super) const CASE: Option<Case> = None;
|
const CASE: Option<Case> = None;
|
||||||
/// Whether small capital glyphs should be used. ("smcp")
|
/// Whether small capital glyphs should be used. ("smcp")
|
||||||
#[property(skip)]
|
#[property(skip)]
|
||||||
pub(super) const SMALLCAPS: bool = false;
|
const SMALLCAPS: bool = false;
|
||||||
/// A destination the text should be linked to.
|
/// A destination the text should be linked to.
|
||||||
#[property(skip, referenced)]
|
#[property(skip, referenced)]
|
||||||
pub(crate) const LINK: Option<Destination> = None;
|
pub(crate) const LINK: Option<Destination> = None;
|
||||||
/// Decorative lines.
|
/// Decorative lines.
|
||||||
#[property(skip, fold)]
|
#[property(skip, fold)]
|
||||||
pub(super) const DECO: Decoration = vec![];
|
const DECO: Decoration = vec![];
|
||||||
|
|
||||||
fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> {
|
fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> {
|
||||||
// The text constructor is special: It doesn't create a text node.
|
// The text constructor is special: It doesn't create a text node.
|
||||||
|
10
macros/src/capability.rs
Normal file
10
macros/src/capability.rs
Normal file
@ -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 {}
|
||||||
|
})
|
||||||
|
}
|
@ -2,437 +2,39 @@
|
|||||||
|
|
||||||
extern crate proc_macro;
|
extern crate proc_macro;
|
||||||
|
|
||||||
use proc_macro::TokenStream;
|
/// Return an error at the given item.
|
||||||
use proc_macro2::{TokenStream as TokenStream2, TokenTree};
|
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 quote::{quote, quote_spanned};
|
||||||
use syn::parse_quote;
|
use syn::parse_quote;
|
||||||
use syn::punctuated::Punctuated;
|
|
||||||
use syn::spanned::Spanned;
|
|
||||||
use syn::{Error, Ident, Result};
|
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.
|
/// Implement `Node` for a struct.
|
||||||
#[proc_macro_attribute]
|
#[proc_macro_attribute]
|
||||||
pub fn node(stream: TokenStream, item: TokenStream) -> TokenStream {
|
pub fn node(stream: BoundaryStream, item: BoundaryStream) -> BoundaryStream {
|
||||||
let impl_block = syn::parse_macro_input!(item as syn::ItemImpl);
|
let item = syn::parse_macro_input!(item as syn::ItemImpl);
|
||||||
expand_node(stream.into(), impl_block)
|
node::expand(stream.into(), item)
|
||||||
.unwrap_or_else(|err| err.to_compile_error())
|
.unwrap_or_else(|err| err.to_compile_error())
|
||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Expand an impl block for a node.
|
/// Implement `Capability` for a trait.
|
||||||
fn expand_node(
|
#[proc_macro_attribute]
|
||||||
stream: TokenStream2,
|
pub fn capability(_: BoundaryStream, item: BoundaryStream) -> BoundaryStream {
|
||||||
mut impl_block: syn::ItemImpl,
|
let item = syn::parse_macro_input!(item as syn::ItemTrait);
|
||||||
) -> Result<TokenStream2> {
|
capability::expand(item)
|
||||||
// Split the node type into name and generic type arguments.
|
.unwrap_or_else(|err| err.to_compile_error())
|
||||||
let params = &impl_block.generics.params;
|
.into()
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
516
macros/src/node.rs
Normal file
516
macros/src/node.rs
Normal file
@ -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>();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
@ -281,6 +281,12 @@ pub trait Node: 'static {
|
|||||||
Content(Arc::new(self), vec![], None)
|
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.
|
/// Construct a node from the arguments.
|
||||||
///
|
///
|
||||||
/// This is passed only the arguments that remain after execution of the
|
/// 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.
|
/// Access a field on this node.
|
||||||
fn field(&self, name: &str) -> Option<Value>;
|
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
|
/// Extract the pointer of the vtable of the trait object with the
|
||||||
/// given type `id` if this node implements that trait.
|
/// given type `id` if this node implements that trait.
|
||||||
fn vtable(&self, id: TypeId) -> Option<*const ()>;
|
fn vtable(&self, id: TypeId) -> Option<*const ()>;
|
||||||
|
@ -23,6 +23,8 @@ mod ops;
|
|||||||
mod scope;
|
mod scope;
|
||||||
mod vm;
|
mod vm;
|
||||||
|
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub use once_cell;
|
||||||
pub use typst_macros::{capability, node};
|
pub use typst_macros::{capability, node};
|
||||||
|
|
||||||
pub use self::args::*;
|
pub use self::args::*;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user