Better proc macros
This commit is contained in:
parent
921b40cf9c
commit
8f36fca684
@ -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>() {
|
@ -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 {
|
||||
name: String,
|
||||
docs: String,
|
||||
internal: bool,
|
||||
external: bool,
|
||||
positional: bool,
|
||||
required: bool,
|
||||
variadic: bool,
|
||||
synthesized: bool,
|
||||
fold: bool,
|
||||
resolve: bool,
|
||||
parse: Option<BlockWithReturn>,
|
||||
default: syn::Expr,
|
||||
vis: syn::Visibility,
|
||||
ident: Ident,
|
||||
ident_in: Ident,
|
||||
with_ident: Ident,
|
||||
push_ident: Ident,
|
||||
set_ident: Ident,
|
||||
vis: syn::Visibility,
|
||||
ty: syn::Type,
|
||||
output: syn::Type,
|
||||
name: String,
|
||||
docs: String,
|
||||
positional: bool,
|
||||
required: bool,
|
||||
variadic: bool,
|
||||
resolve: bool,
|
||||
fold: bool,
|
||||
internal: bool,
|
||||
external: bool,
|
||||
synthesized: bool,
|
||||
parse: Option<BlockWithReturn>,
|
||||
default: syn::Expr,
|
||||
}
|
||||
|
||||
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<_>>()?;
|
||||
|
||||
let mut fields = vec![];
|
||||
for field in &named.named {
|
||||
let Some(ident) = field.ident.clone() else {
|
||||
bail!(field, "expected named field");
|
||||
};
|
||||
|
||||
let mut attrs = field.attrs.clone();
|
||||
let variadic = has_attr(&mut attrs, "variadic");
|
||||
let required = has_attr(&mut attrs, "required") || variadic;
|
||||
let positional = has_attr(&mut attrs, "positional") || required;
|
||||
|
||||
if ident == "label" {
|
||||
bail!(ident, "invalid field name");
|
||||
}
|
||||
|
||||
let mut field = Field {
|
||||
name: kebab_case(&ident),
|
||||
docs: documentation(&attrs),
|
||||
internal: has_attr(&mut attrs, "internal"),
|
||||
external: has_attr(&mut attrs, "external"),
|
||||
positional,
|
||||
required,
|
||||
variadic,
|
||||
synthesized: has_attr(&mut attrs, "synthesized"),
|
||||
fold: has_attr(&mut attrs, "fold"),
|
||||
resolve: has_attr(&mut attrs, "resolve"),
|
||||
parse: parse_attr(&mut attrs, "parse")?.flatten(),
|
||||
default: parse_attr(&mut attrs, "default")?
|
||||
.flatten()
|
||||
.unwrap_or_else(|| parse_quote! { ::std::default::Default::default() }),
|
||||
vis: field.vis.clone(),
|
||||
ident: ident.clone(),
|
||||
ident_in: Ident::new(&format!("{}_in", ident), ident.span()),
|
||||
with_ident: Ident::new(&format!("with_{}", ident), ident.span()),
|
||||
push_ident: Ident::new(&format!("push_{}", ident), ident.span()),
|
||||
set_ident: Ident::new(&format!("set_{}", ident), ident.span()),
|
||||
ty: field.ty.clone(),
|
||||
output: field.ty.clone(),
|
||||
};
|
||||
|
||||
if field.required && (field.fold || field.resolve) {
|
||||
bail!(ident, "required fields cannot be folded or resolved");
|
||||
}
|
||||
|
||||
if field.required && !field.positional {
|
||||
bail!(ident, "only positional fields can be required");
|
||||
}
|
||||
|
||||
if field.resolve {
|
||||
let output = &field.output;
|
||||
field.output = parse_quote! { <#output as ::typst::model::Resolve>::Output };
|
||||
}
|
||||
if field.fold {
|
||||
let output = &field.output;
|
||||
field.output = parse_quote! { <#output as ::typst::model::Fold>::Output };
|
||||
}
|
||||
|
||||
validate_attrs(&attrs)?;
|
||||
fields.push(field);
|
||||
}
|
||||
|
||||
let capable = Punctuated::<Ident, Token![,]>::parse_terminated
|
||||
.parse2(stream)?
|
||||
.into_iter()
|
||||
.collect();
|
||||
|
||||
let mut attrs = body.attrs.clone();
|
||||
let docs = documentation(&attrs);
|
||||
let mut lines = docs.split('\n').collect();
|
||||
let keywords = meta_line(&mut lines, "Keywords").ok().map(Into::into);
|
||||
let category = meta_line(&mut lines, "Category")?.into();
|
||||
let display = meta_line(&mut lines, "Display")?.into();
|
||||
let docs = lines.join("\n").trim().into();
|
||||
|
||||
let element = Elem {
|
||||
name: body.ident.to_string().trim_end_matches("Elem").to_lowercase(),
|
||||
display,
|
||||
category,
|
||||
keywords,
|
||||
Ok(Elem {
|
||||
name,
|
||||
title,
|
||||
scope: meta.scope,
|
||||
keywords: meta.keywords,
|
||||
docs,
|
||||
vis: body.vis.clone(),
|
||||
ident: body.ident.clone(),
|
||||
capable,
|
||||
capabilities: meta.capabilities,
|
||||
fields,
|
||||
scope: parse_attr(&mut attrs, "scope")?.flatten(),
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_field(field: &syn::Field) -> Result<Field> {
|
||||
let Some(ident) = field.ident.clone() else {
|
||||
bail!(field, "expected named field");
|
||||
};
|
||||
|
||||
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: 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.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)?;
|
||||
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)
|
@ -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 {
|
||||
name: String,
|
||||
docs: String,
|
||||
external: bool,
|
||||
named: bool,
|
||||
variadic: bool,
|
||||
default: Option<syn::Expr>,
|
||||
binding: Binding,
|
||||
ident: Ident,
|
||||
ty: syn::Type,
|
||||
name: String,
|
||||
docs: String,
|
||||
named: bool,
|
||||
variadic: bool,
|
||||
external: bool,
|
||||
default: Option<syn::Expr>,
|
||||
}
|
||||
|
||||
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");
|
||||
};
|
||||
for input in &item.sig.inputs {
|
||||
parse_param(&mut special, &mut params, meta.parent.as_ref(), input)?;
|
||||
}
|
||||
|
||||
let syn::Pat::Ident(syn::PatIdent {
|
||||
by_ref: None, mutability: None, ident, ..
|
||||
}) = &*typed.pat
|
||||
else {
|
||||
bail!(typed.pat, "expected identifier");
|
||||
};
|
||||
let returns = match &item.sig.output {
|
||||
syn::ReturnType::Default => parse_quote! { () },
|
||||
syn::ReturnType::Type(_, ty) => ty.as_ref().clone(),
|
||||
};
|
||||
|
||||
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(),
|
||||
});
|
||||
if meta.parent.is_some() && meta.scope {
|
||||
bail!(item, "scoped function cannot have a scope");
|
||||
}
|
||||
|
||||
validate_attrs(&attrs)?;
|
||||
}
|
||||
Ok(Func {
|
||||
name,
|
||||
title,
|
||||
scope: meta.scope,
|
||||
constructor: meta.constructor,
|
||||
keywords: meta.keywords,
|
||||
parent: meta.parent,
|
||||
docs,
|
||||
vis: item.vis.clone(),
|
||||
ident: item.sig.ident.clone(),
|
||||
special,
|
||||
params,
|
||||
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");
|
||||
};
|
||||
|
||||
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)?;
|
||||
}
|
||||
}
|
||||
|
||||
let mut attrs = item.attrs.clone();
|
||||
let docs = documentation(&attrs);
|
||||
let mut lines = docs.split('\n').collect();
|
||||
let keywords = meta_line(&mut lines, "Keywords").ok().map(Into::into);
|
||||
let category = meta_line(&mut lines, "Category")?.into();
|
||||
let display = meta_line(&mut lines, "Display")?.into();
|
||||
let docs = lines.join("\n").trim().into();
|
||||
|
||||
let func = Func {
|
||||
name: sig.ident.to_string().trim_end_matches('_').replace('_', "-"),
|
||||
display,
|
||||
category,
|
||||
keywords,
|
||||
docs,
|
||||
vis: item.vis.clone(),
|
||||
ident: sig.ident.clone(),
|
||||
ident_func: Ident::new(
|
||||
&format!("{}_func", sig.ident.to_string().trim_end_matches('_')),
|
||||
sig.ident.span(),
|
||||
),
|
||||
parent,
|
||||
params,
|
||||
returns: match &sig.output {
|
||||
syn::ReturnType::Default => parse_quote! { () },
|
||||
syn::ReturnType::Type(_, ty) => ty.as_ref().clone(),
|
||||
},
|
||||
scope: parse_attr(&mut attrs, "scope")?.flatten(),
|
||||
vm,
|
||||
vt,
|
||||
args,
|
||||
span,
|
||||
};
|
||||
|
||||
Ok(func)
|
||||
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 {
|
||||
name,
|
||||
display,
|
||||
category,
|
||||
docs,
|
||||
vis,
|
||||
ident,
|
||||
ident_func,
|
||||
name,
|
||||
title,
|
||||
docs,
|
||||
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| ¶m.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 }
|
||||
};
|
||||
|
||||
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()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a type that shadows the function.
|
||||
fn create_func_ty(func: &Func) -> Option<TokenStream> {
|
||||
if func.parent.is_some() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let Func { vis, ident, .. } = func;
|
||||
Some(quote! {
|
||||
#[doc(hidden)]
|
||||
#[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)*
|
||||
}
|
||||
};
|
||||
|
||||
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();
|
||||
// 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,)*)
|
||||
}
|
||||
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());
|
||||
};
|
||||
|
||||
// This is the whole wrapped closure.
|
||||
let ident = &func.ident;
|
||||
let parent = func.parent.as_ref().map(|ty| quote! { #ty:: });
|
||||
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
|
||||
|vm, args| {
|
||||
let __typst_func = #parent #ident;
|
||||
#handlers
|
||||
let output = #call;
|
||||
::typst::eval::IntoResult::into_result(output, args.span)
|
||||
}
|
||||
|
||||
#[doc = #docs]
|
||||
#item
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 = ¶m.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
|
||||
}
|
||||
|
@ -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 {
|
||||
let item = syn::parse_macro_input!(item as syn::ItemStruct);
|
||||
element::element(stream.into(), &item)
|
||||
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()
|
||||
}
|
||||
|
||||
/// 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)
|
||||
/// 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);
|
||||
elem::elem(stream.into(), item)
|
||||
.unwrap_or_else(|err| err.to_compile_error())
|
||||
.into()
|
||||
}
|
||||
|
||||
/// 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())
|
||||
|
153
crates/typst-macros/src/scope.rs
Normal file
153
crates/typst-macros/src/scope.rs
Normal file
@ -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)*
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
113
crates/typst-macros/src/ty.rs
Normal file
113
crates/typst-macros/src/ty.rs
Normal file
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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,58 +87,6 @@ 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()
|
||||
}
|
||||
|
||||
/// Extract documentation comments from an attribute list.
|
||||
pub fn documentation(attrs: &[syn::Attribute]) -> String {
|
||||
let mut doc = String::new();
|
||||
|
||||
// Parse doc comments.
|
||||
for attr in attrs {
|
||||
if let syn::Meta::NameValue(meta) = &attr.meta {
|
||||
if meta.path.is_ident("doc") {
|
||||
if let syn::Expr::Lit(lit) = &meta.value {
|
||||
if let syn::Lit::Str(string) = &lit.lit {
|
||||
let full = string.value();
|
||||
let line = full.strip_prefix(' ').unwrap_or(&full);
|
||||
doc.push_str(line);
|
||||
doc.push('\n');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
doc.trim().into()
|
||||
}
|
||||
|
||||
/// Extract a line of metadata from documentation.
|
||||
pub fn meta_line<'a>(lines: &mut Vec<&'a str>, key: &str) -> Result<&'a str> {
|
||||
match lines.last().and_then(|line| line.strip_prefix(&format!("{key}:"))) {
|
||||
Some(value) => {
|
||||
lines.pop();
|
||||
Ok(value.trim())
|
||||
}
|
||||
None => bail!(callsite, "missing metadata key: {key}"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a block responsible for building a `Scope`.
|
||||
pub fn create_scope_builder(scope_block: Option<&BlockWithReturn>) -> TokenStream {
|
||||
if let Some(BlockWithReturn { prefix, expr }) = scope_block {
|
||||
quote! { {
|
||||
let mut scope = ::typst::eval::Scope::deduplicating();
|
||||
#(#prefix);*
|
||||
#expr
|
||||
} }
|
||||
} else {
|
||||
quote! { ::typst::eval::Scope::new() }
|
||||
}
|
||||
}
|
||||
|
||||
/// Quotes an option literally.
|
||||
pub fn quote_option<T: ToTokens>(option: &Option<T>) -> TokenStream {
|
||||
if let Some(value) = option {
|
||||
@ -143,3 +95,156 @@ pub fn quote_option<T: ToTokens>(option: &Option<T>) -> TokenStream {
|
||||
quote! { None }
|
||||
}
|
||||
}
|
||||
|
||||
/// 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);
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
/// Parse a comma if there is one.
|
||||
pub fn eat_comma(input: ParseStream) {
|
||||
if input.peek(Token![,]) {
|
||||
let _: Token![,] = input.parse().unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
/// 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");
|
||||
}
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 })
|
||||
}
|
||||
}
|
||||
|
||||
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()?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user