typst/src/func.rs

198 lines
6.7 KiB
Rust

//! Trait and prelude for custom functions.
use crate::Pass;
use crate::syntax::ParseContext;
use crate::syntax::func::FuncHeader;
use crate::syntax::span::Spanned;
/// Types that are useful for creating your own functions.
pub mod prelude {
pub use crate::{function, body, err};
pub use crate::layout::prelude::*;
pub use crate::layout::Command::{self, *};
pub use crate::style::{LayoutStyle, PageStyle, TextStyle};
pub use crate::syntax::SyntaxModel;
pub use crate::syntax::expr::*;
pub use crate::syntax::func::*;
pub use crate::syntax::span::{Span, Spanned};
}
/// Parse a function from source code.
pub trait ParseFunc {
/// A metadata type whose value is passed into the function parser. This
/// allows a single function to do different things depending on the value
/// that needs to be given when inserting the function into a
/// [scope](crate::syntax::Scope).
///
/// For example, the functions `word.spacing`, `line.spacing` and
/// `par.spacing` are actually all the same function
/// [`ContentSpacingFunc`](crate::library::ContentSpacingFunc) with the
/// metadata specifiy which content should be spaced.
type Meta: Clone;
/// Parse the header and body into this function given a context.
fn parse(
header: FuncHeader,
body: Option<Spanned<&str>>,
ctx: ParseContext,
metadata: Self::Meta,
) -> Pass<Self> where Self: Sized;
}
/// Allows to implement a function type concisely.
///
/// # Example
/// A function that hides its body depending on a boolean argument.
/// ```
/// use typstc::func::prelude::*;
///
/// function! {
/// #[derive(Debug, Clone, PartialEq)]
/// pub struct HiderFunc {
/// body: Option<SyntaxModel>,
/// }
///
/// parse(header, body, ctx, errors, decos) {
/// let body = body!(opt: body, ctx, errors, decos);
/// let hidden = header.args.pos.get::<bool>(errors)
/// .or_missing(errors, header.name.span, "hidden")
/// .unwrap_or(false);
///
/// HiderFunc { body: if hidden { None } else { body } }
/// }
///
/// layout(self, ctx, errors) {
/// match &self.body {
/// Some(model) => vec![LayoutSyntaxModel(model)],
/// None => vec![],
/// }
/// }
/// }
/// ```
/// This function can be used as follows:
/// ```typst
/// [hider: true][Hi, you.] => Nothing
/// [hider: false][Hi, you.] => Text: "Hi, you."
///
/// [hider][Hi, you.] => Text: "Hi, you."
/// ^^^^^
/// missing argument: hidden
/// ```
///
/// # More examples
/// Look at the source code of the [`library`](crate::library) module for more
/// examples on how the macro works.
#[macro_export]
macro_rules! function {
// Entry point.
($(#[$outer:meta])* $v:vis $storage:ident $name:ident $($r:tt)*) => {
function!(@def($name) $(#[$outer])* $v $storage $name $($r)*);
};
(@def($name:ident) $definition:item $($r:tt)*) => {
$definition
function!(@meta($name) $($r)*);
};
// Metadata.
(@meta($name:ident) type Meta = $meta:ty; $($r:tt)*) => {
function!(@parse($name, $meta) $($r)*);
};
(@meta($name:ident) $($r:tt)*) => {
function!(@parse($name, ()) $($r)*);
};
// Parse trait.
(@parse($($a:tt)*) parse(default) $($r:tt)*) => {
function!(@parse($($a)*) parse(_h, _b, _c, _f, _m) {Default::default() } $($r)*);
};
(@parse($($a:tt)*) parse($h:ident, $b:ident, $c:ident, $f:ident) $($r:tt)* ) => {
function!(@parse($($a)*) parse($h, $b, $c, $f, _metadata) $($r)*);
};
(@parse($name:ident, $meta:ty) parse(
$header:ident,
$body:ident,
$ctx:ident,
$feedback:ident,
$metadata:ident
) $code:block $($r:tt)*) => {
impl $crate::func::ParseFunc for $name {
type Meta = $meta;
fn parse(
#[allow(unused)] mut header: $crate::syntax::func::FuncHeader,
#[allow(unused)] $body: Option<$crate::syntax::span::Spanned<&str>>,
#[allow(unused)] $ctx: $crate::syntax::ParseContext,
#[allow(unused)] $metadata: Self::Meta,
) -> $crate::Pass<Self> where Self: Sized {
let mut feedback = $crate::Feedback::new();
#[allow(unused)] let $header = &mut header;
#[allow(unused)] let $feedback = &mut feedback;
let func = $code;
for arg in header.args.into_iter() {
feedback.errors.push(err!(arg.span(); "unexpected argument"));
}
$crate::Pass::new(func, feedback)
}
}
function!(@layout($name) $($r)*);
};
(@layout($name:ident) layout($this:ident, $ctx:ident, $feedback:ident) $code:block) => {
impl $crate::syntax::Model for $name {
fn layout<'a, 'b, 't>(
#[allow(unused)] &'a $this,
#[allow(unused)] mut $ctx: $crate::layout::LayoutContext<'b>,
) -> $crate::layout::DynFuture<'t, $crate::Pass<$crate::layout::Commands<'a>>>
where
'a: 't,
'b: 't,
Self: 't,
{
Box::pin(async move {
let mut feedback = $crate::Feedback::new();
#[allow(unused)] let $feedback = &mut feedback;
let commands = $code;
$crate::Pass::new(commands, feedback)
})
}
}
};
}
/// Parse the body of a function.
///
/// - If the function does not expect a body, use `body!(nope: body, errors)`.
/// - If the function can have a body, use `body!(opt: body, ctx, errors, decos)`.
///
/// # Arguments
/// - The `$body` should be of type `Option<Spanned<&str>>`.
/// - The `$ctx` is the [`ParseContext`](crate::syntax::ParseContext) to use for parsing.
/// - The `$errors` and `$decos` should be mutable references to vectors of spanned
/// errors / decorations which are filled with the errors and decorations arising
/// from parsing.
#[macro_export]
macro_rules! body {
(opt: $body:expr, $ctx:expr, $feedback:expr) => ({
$body.map(|body| {
// Since the body span starts at the opening bracket of the body, we
// need to add 1 column to find out the start position of body
// content.
let start = body.span.start + $crate::syntax::span::Position::new(0, 1);
let parsed = $crate::syntax::parse(start, body.v, $ctx);
$feedback.extend(parsed.feedback);
parsed.output
})
});
(nope: $body:expr, $feedback:expr) => {
if let Some(body) = $body {
$feedback.errors.push($crate::err!(body.span; "unexpected body"));
}
};
}