mirror of
https://github.com/typst/typst
synced 2025-05-13 20:46:23 +08:00
Create easy-to-use argument parser 💎
This commit is contained in:
parent
271af7ed03
commit
1ece263579
@ -1,8 +1,9 @@
|
||||
use std::iter::Peekable;
|
||||
use std::slice::Iter;
|
||||
use super::prelude::*;
|
||||
//! Helper types and macros for creating custom functions.
|
||||
|
||||
/// Implement the function trait more concisely.
|
||||
use super::prelude::*;
|
||||
use Expression::*;
|
||||
|
||||
/// Lets you implement the function trait more concisely.
|
||||
#[macro_export]
|
||||
macro_rules! function {
|
||||
(data: $ident:ident, $($tts:tt)*) => (
|
||||
@ -15,15 +16,15 @@ macro_rules! function {
|
||||
);
|
||||
|
||||
(@parse $ident:ident, parse: plain, $($tts:tt)*) => (
|
||||
fn parse(header: &FuncHeader, body: Option<&str>, _: ParseContext)
|
||||
-> ParseResult<Self> where Self: Sized
|
||||
{
|
||||
Arguments::new(header).done()?;
|
||||
fn parse(header: &FuncHeader, body: Option<&str>, _: ParseContext) -> ParseResult<Self>
|
||||
where Self: Sized {
|
||||
ArgParser::new(&header.args).done()?;
|
||||
if body.is_some() {
|
||||
err!("expected no body");
|
||||
}
|
||||
Ok($ident)
|
||||
}
|
||||
|
||||
function!(@layout $($tts)*);
|
||||
);
|
||||
|
||||
@ -33,14 +34,15 @@ macro_rules! function {
|
||||
$block:block
|
||||
$($tts:tt)*
|
||||
) => (
|
||||
fn parse(header: &FuncHeader, body: Option<&str>, ctx: ParseContext)
|
||||
-> ParseResult<Self> where Self: Sized
|
||||
{
|
||||
#[allow(unused_mut)] let mut $args = Arguments::new(header);
|
||||
fn parse(header: &FuncHeader, body: Option<&str>, ctx: ParseContext) -> ParseResult<Self>
|
||||
where Self: Sized {
|
||||
#[allow(unused_mut)]
|
||||
let mut $args = ArgParser::new(&header.args);
|
||||
let $body = body;
|
||||
let $ctx = ctx;
|
||||
$block
|
||||
}
|
||||
|
||||
function!(@layout $($tts)*);
|
||||
);
|
||||
|
||||
@ -82,51 +84,110 @@ macro_rules! parse {
|
||||
)
|
||||
}
|
||||
|
||||
/// Return a formatted parsing error.
|
||||
/// Early-return with a formatted parsing error or yield
|
||||
/// an error expression without returning when prefixed with `@`.
|
||||
#[macro_export]
|
||||
macro_rules! err {
|
||||
(@$($tts:tt)*) => ($crate::syntax::ParseError::new(format!($($tts)*)));
|
||||
($($tts:tt)*) => (return Err(err!(@$($tts)*)););
|
||||
}
|
||||
|
||||
/// Convenient interface for parsing function arguments.
|
||||
pub struct Arguments<'a> {
|
||||
args: Peekable<Iter<'a, Spanned<Expression>>>,
|
||||
/// Easy parsing of function arguments.
|
||||
pub struct ArgParser<'a> {
|
||||
args: &'a FuncArgs,
|
||||
positional_index: usize,
|
||||
}
|
||||
|
||||
impl<'a> Arguments<'a> {
|
||||
pub fn new(header: &'a FuncHeader) -> Arguments<'a> {
|
||||
Arguments {
|
||||
args: header.args.positional.iter().peekable()
|
||||
impl<'a> ArgParser<'a> {
|
||||
pub fn new(args: &'a FuncArgs) -> ArgParser<'a> {
|
||||
ArgParser {
|
||||
args,
|
||||
positional_index: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_expr(&mut self) -> ParseResult<&'a Spanned<Expression>> {
|
||||
self.args.next()
|
||||
.ok_or_else(|| ParseError::new("expected expression"))
|
||||
/// Get the next positional argument of the given type.
|
||||
///
|
||||
/// If there are no more arguments or the type is wrong,
|
||||
/// this will return an error.
|
||||
pub fn get_pos<T>(&mut self) -> ParseResult<Spanned<T::Output>> where T: Argument<'a> {
|
||||
self.get_pos_opt::<T>()?
|
||||
.ok_or_else(|| err!(@"expected {}", T::ERROR_MESSAGE))
|
||||
}
|
||||
|
||||
pub fn get_ident(&mut self) -> ParseResult<Spanned<&'a str>> {
|
||||
let expr = self.get_expr()?;
|
||||
match &expr.val {
|
||||
Expression::Ident(s) => Ok(Spanned::new(s.as_str(), expr.span)),
|
||||
_ => err!("expected identifier"),
|
||||
/// Get the next positional argument if there is any.
|
||||
///
|
||||
/// If the argument is of the wrong type, this will return an error.
|
||||
pub fn get_pos_opt<T>(&mut self) -> ParseResult<Option<Spanned<T::Output>>>
|
||||
where T: Argument<'a> {
|
||||
let arg = self.args.positional
|
||||
.get(self.positional_index)
|
||||
.map(T::from_expr)
|
||||
.transpose();
|
||||
|
||||
if let Ok(Some(_)) = arg {
|
||||
self.positional_index += 1;
|
||||
}
|
||||
|
||||
arg
|
||||
}
|
||||
|
||||
pub fn get_ident_if_present(&mut self) -> ParseResult<Option<Spanned<&'a str>>> {
|
||||
if self.args.peek().is_some() {
|
||||
self.get_ident().map(|s| Some(s))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
/// Get a keyword argument with the given key and type.
|
||||
pub fn get_key<T>(&mut self, key: &str) -> ParseResult<Spanned<T::Output>>
|
||||
where T: Argument<'a> {
|
||||
self.get_key_opt::<T>(key)?
|
||||
.ok_or_else(|| err!(@"expected {}", T::ERROR_MESSAGE))
|
||||
}
|
||||
|
||||
pub fn done(&mut self) -> ParseResult<()> {
|
||||
if self.args.peek().is_none() {
|
||||
/// Get a keyword argument with the given key and type if it is present.
|
||||
pub fn get_key_opt<T>(&mut self, key: &str) -> ParseResult<Option<Spanned<T::Output>>>
|
||||
where T: Argument<'a> {
|
||||
self.args.keyword.iter()
|
||||
.find(|entry| entry.val.0.val == key)
|
||||
.map(|entry| T::from_expr(&entry.val.1))
|
||||
.transpose()
|
||||
}
|
||||
|
||||
/// Assert that there are no positional arguments left. Returns an error, otherwise.
|
||||
pub fn done(&self) -> ParseResult<()> {
|
||||
if self.positional_index == self.args.positional.len() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(ParseError::new("unexpected argument"))
|
||||
err!("unexpected argument");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A kind of argument.
|
||||
pub trait Argument<'a> {
|
||||
type Output;
|
||||
const ERROR_MESSAGE: &'static str;
|
||||
|
||||
fn from_expr(expr: &'a Spanned<Expression>) -> ParseResult<Spanned<Self::Output>>;
|
||||
}
|
||||
|
||||
macro_rules! arg {
|
||||
($type:ident, $err:expr, $doc:expr, $output:ty, $wanted:pat => $converted:expr) => (
|
||||
#[doc = $doc]
|
||||
#[doc = " argument for use with the [`ArgParser`]."]
|
||||
pub struct $type;
|
||||
impl<'a> Argument<'a> for $type {
|
||||
type Output = $output;
|
||||
const ERROR_MESSAGE: &'static str = $err;
|
||||
|
||||
fn from_expr(expr: &'a Spanned<Expression>) -> ParseResult<Spanned<Self::Output>> {
|
||||
match &expr.val {
|
||||
$wanted => Ok(Spanned::new($converted, expr.span)),
|
||||
#[allow(unreachable_patterns)] _ => err!("expected {}", $err),
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
arg!(ArgExpr, "expression", "A generic expression", &'a Expression, expr => &expr);
|
||||
arg!(ArgIdent, "identifier", "An identifier (e.g. `horizontal`)", &'a str, Ident(s) => s.as_str());
|
||||
arg!(ArgStr, "string", "A string (e.g. `\"Hello\"`)", &'a str, Str(s) => s.as_str());
|
||||
arg!(ArgNum, "number", "A number (e.g. `5.4`)", f64, Num(n) => *n);
|
||||
arg!(ArgSize, "size", "A size (e.g. `12pt`)", crate::size::Size, Size(s) => *s);
|
||||
arg!(ArgBool, "bool", "A boolean (`true` or `false`)", bool, Bool(b) => *b);
|
||||
|
@ -7,8 +7,7 @@ use std::fmt::{self, Debug, Formatter};
|
||||
use self::prelude::*;
|
||||
|
||||
#[macro_use]
|
||||
mod helpers;
|
||||
pub use helpers::Arguments;
|
||||
pub mod helpers;
|
||||
|
||||
/// Useful imports for creating your own functions.
|
||||
pub mod prelude {
|
||||
@ -24,11 +23,12 @@ pub mod prelude {
|
||||
|
||||
/// Typesetting function types.
|
||||
///
|
||||
/// These types have to be able to parse tokens into themselves and store the
|
||||
/// relevant information from the parsing to do their role in typesetting later.
|
||||
/// These types have to be able to parse themselves from a string and build
|
||||
/// a list of layouting commands corresponding to the parsed source.
|
||||
///
|
||||
/// The trait `FunctionBounds` is automatically implemented for types which can
|
||||
/// be used as functions, that is they fulfill the bounds `Debug + PartialEq +
|
||||
/// This trait is a supertrait of `FunctionBounds` for technical reasons. The
|
||||
/// trait `FunctionBounds` is automatically implemented for types which can
|
||||
/// be used as functions, that is, all types which fulfill the bounds `Debug + PartialEq +
|
||||
/// 'static`.
|
||||
pub trait Function: FunctionBounds {
|
||||
/// Parse the header and body into this function given a context.
|
||||
@ -84,6 +84,19 @@ where T: Debug + PartialEq + 'static
|
||||
}
|
||||
}
|
||||
|
||||
/// Commands requested for execution by functions.
|
||||
#[derive(Debug)]
|
||||
pub enum Command<'a> {
|
||||
LayoutTree(&'a SyntaxTree),
|
||||
Add(Layout),
|
||||
AddMany(MultiLayout),
|
||||
AddFlex(Layout),
|
||||
SetAlignment(Alignment),
|
||||
SetStyle(TextStyle),
|
||||
FinishLayout,
|
||||
FinishFlexRun,
|
||||
}
|
||||
|
||||
/// A sequence of commands requested for execution by a function.
|
||||
#[derive(Debug)]
|
||||
pub struct CommandList<'a> {
|
||||
@ -130,19 +143,8 @@ impl<'a> IntoIterator for &'a CommandList<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Commands requested for execution by functions.
|
||||
#[derive(Debug)]
|
||||
pub enum Command<'a> {
|
||||
Layout(&'a SyntaxTree),
|
||||
Add(Layout),
|
||||
AddMany(MultiLayout),
|
||||
AddFlex(Layout),
|
||||
SetAlignment(Alignment),
|
||||
SetStyle(TextStyle),
|
||||
FinishLayout,
|
||||
FinishFlexRun,
|
||||
}
|
||||
|
||||
/// Create a list of commands.
|
||||
#[macro_export]
|
||||
macro_rules! commands {
|
||||
($($x:expr),*$(,)*) => (
|
||||
$crate::func::CommandList::from_vec(vec![$($x,)*])
|
||||
|
@ -62,7 +62,7 @@ debug_display!(LayoutAction);
|
||||
/// content is written.
|
||||
///
|
||||
/// Furthermore, the action list can translate absolute position into a coordinate system
|
||||
/// with a different. This is realized in the `add_box` method, which allows a layout to
|
||||
/// with a different origin. This is realized in the `add_box` method, which allows a layout to
|
||||
/// be added at a position, effectively translating all movement actions inside the layout
|
||||
/// by the position.
|
||||
#[derive(Debug, Clone)]
|
||||
|
@ -2,7 +2,7 @@ use super::*;
|
||||
|
||||
/// Layouts boxes stack-like.
|
||||
///
|
||||
/// The boxes are arranged vertically, each layout gettings it's own "line".
|
||||
/// The boxes are arranged along an axis, each layout gettings it's own "line".
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct StackLayouter {
|
||||
ctx: StackContext,
|
||||
|
@ -108,7 +108,7 @@ impl<'a, 'p> TreeLayouter<'a, 'p> {
|
||||
|
||||
for command in commands {
|
||||
match command {
|
||||
Command::Layout(tree) => self.layout(tree)?,
|
||||
Command::LayoutTree(tree) => self.layout(tree)?,
|
||||
|
||||
Command::Add(layout) => {
|
||||
self.finish_flex()?;
|
||||
|
@ -1,6 +1,7 @@
|
||||
use crate::func::prelude::*;
|
||||
use Command::*;
|
||||
|
||||
/// Ends the current page.
|
||||
/// 📜 `page.break`: Ends the current page.
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct Pagebreak;
|
||||
|
||||
@ -9,11 +10,11 @@ function! {
|
||||
parse: plain,
|
||||
|
||||
layout(_, _) {
|
||||
Ok(commands![Command::FinishLayout])
|
||||
Ok(commands![FinishLayout])
|
||||
}
|
||||
}
|
||||
|
||||
/// Ends the current line.
|
||||
/// 🔙 `line.break`, `n`: Ends the current line.
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct Linebreak;
|
||||
|
||||
@ -22,11 +23,14 @@ function! {
|
||||
parse: plain,
|
||||
|
||||
layout(_, _) {
|
||||
Ok(commands![Command::FinishFlexRun])
|
||||
Ok(commands![FinishFlexRun])
|
||||
}
|
||||
}
|
||||
|
||||
/// Aligns content in different ways.
|
||||
/// 📐 `align`: Aligns content in different ways.
|
||||
///
|
||||
/// **Positional arguments:**
|
||||
/// - `left`, `right` or `center` _(required)_.
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct Align {
|
||||
body: Option<SyntaxTree>,
|
||||
@ -38,7 +42,7 @@ function! {
|
||||
|
||||
parse(args, body, ctx) {
|
||||
let body = parse!(optional: body, ctx);
|
||||
let arg = args.get_ident()?;
|
||||
let arg = args.get_pos::<ArgIdent>()?;
|
||||
let alignment = match arg.val {
|
||||
"left" => Alignment::Left,
|
||||
"right" => Alignment::Right,
|
||||
@ -56,17 +60,22 @@ function! {
|
||||
layout(this, ctx) {
|
||||
Ok(commands![match &this.body {
|
||||
Some(body) => {
|
||||
Command::AddMany(layout_tree(body, LayoutContext {
|
||||
AddMany(layout_tree(body, LayoutContext {
|
||||
alignment: this.alignment,
|
||||
.. ctx
|
||||
})?)
|
||||
}
|
||||
None => Command::SetAlignment(this.alignment)
|
||||
None => SetAlignment(this.alignment)
|
||||
}])
|
||||
}
|
||||
}
|
||||
|
||||
/// Layouts content into a box.
|
||||
/// 📦 `box`: Layouts content into a box.
|
||||
///
|
||||
/// **Positional arguments:** None.
|
||||
///
|
||||
/// **Keyword arguments:**
|
||||
/// - flow: either `horizontal` or `vertical` _(optional)_.
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct Boxed {
|
||||
body: SyntaxTree,
|
||||
@ -80,7 +89,7 @@ function! {
|
||||
let body = parse!(required: body, ctx);
|
||||
|
||||
let mut flow = Flow::Vertical;
|
||||
if let Some(ident) = args.get_ident_if_present()? {
|
||||
if let Some(ident) = args.get_key_opt::<ArgIdent>("flow")? {
|
||||
flow = match ident.val {
|
||||
"vertical" => Flow::Vertical,
|
||||
"horizontal" => Flow::Horizontal,
|
||||
@ -97,7 +106,7 @@ function! {
|
||||
|
||||
layout(this, ctx) {
|
||||
Ok(commands![
|
||||
Command::AddMany(layout_tree(&this.body, LayoutContext {
|
||||
AddMany(layout_tree(&this.body, LayoutContext {
|
||||
flow: this.flow,
|
||||
.. ctx
|
||||
})?)
|
||||
@ -106,8 +115,12 @@ function! {
|
||||
}
|
||||
|
||||
macro_rules! spacefunc {
|
||||
($ident:ident, $name:expr, $var:ident => $command:expr) => (
|
||||
/// Adds whitespace.
|
||||
($ident:ident, $doc:expr, $var:ident => $command:expr) => (
|
||||
#[doc = $doc]
|
||||
///
|
||||
/// **Positional arguments:**
|
||||
/// - Spacing as a size or number, which is interpreted as a multiple
|
||||
/// of the font size _(required)_.
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct $ident(Spacing);
|
||||
|
||||
@ -117,10 +130,10 @@ macro_rules! spacefunc {
|
||||
parse(args, body, _ctx) {
|
||||
parse!(forbidden: body);
|
||||
|
||||
let arg = args.get_expr()?;
|
||||
let arg = args.get_pos::<ArgExpr>()?;
|
||||
let spacing = match arg.val {
|
||||
Expression::Size(s) => Spacing::Absolute(s),
|
||||
Expression::Number(f) => Spacing::Relative(f as f32),
|
||||
Expression::Size(s) => Spacing::Absolute(*s),
|
||||
Expression::Num(f) => Spacing::Relative(*f as f32),
|
||||
_ => err!("invalid spacing, expected size or number"),
|
||||
};
|
||||
|
||||
@ -146,5 +159,8 @@ enum Spacing {
|
||||
Relative(f32),
|
||||
}
|
||||
|
||||
spacefunc!(HorizontalSpace, "h", space => Command::AddFlex(Layout::empty(space, Size::zero())));
|
||||
spacefunc!(VerticalSpace, "v", space => Command::Add(Layout::empty(Size::zero(), space)));
|
||||
spacefunc!(HorizontalSpace, "📖 `h`: Adds horizontal whitespace.",
|
||||
space => AddFlex(Layout::empty(space, Size::zero())));
|
||||
|
||||
spacefunc!(VerticalSpace, "📑 `v`: Adds vertical whitespace.",
|
||||
space => Add(Layout::empty(Size::zero(), space)));
|
||||
|
@ -2,8 +2,8 @@ use crate::func::prelude::*;
|
||||
use toddle::query::FontClass;
|
||||
|
||||
macro_rules! stylefunc {
|
||||
($ident:ident) => (
|
||||
/// Styles text.
|
||||
($ident:ident, $doc:expr) => (
|
||||
#[doc = $doc]
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct $ident {
|
||||
body: Option<SyntaxTree>
|
||||
@ -24,7 +24,7 @@ macro_rules! stylefunc {
|
||||
Ok(match &this.body {
|
||||
Some(body) => commands![
|
||||
Command::SetStyle(new_style),
|
||||
Command::Layout(body),
|
||||
Command::LayoutTree(body),
|
||||
Command::SetStyle(ctx.style.clone()),
|
||||
],
|
||||
None => commands![Command::SetStyle(new_style)]
|
||||
@ -34,6 +34,6 @@ macro_rules! stylefunc {
|
||||
);
|
||||
}
|
||||
|
||||
stylefunc!(Italic);
|
||||
stylefunc!(Bold);
|
||||
stylefunc!(Monospace);
|
||||
stylefunc!(Italic, "💡 `italic`: Sets text in _italics_.");
|
||||
stylefunc!(Bold, "🧱 `bold`: Sets text in **bold**.");
|
||||
stylefunc!(Monospace, "👩💻 `mono`: Sets text in `monospace`.");
|
||||
|
@ -128,7 +128,7 @@ pub enum FuncArg {
|
||||
pub enum Expression {
|
||||
Ident(String),
|
||||
Str(String),
|
||||
Number(f64),
|
||||
Num(f64),
|
||||
Size(Size),
|
||||
Bool(bool),
|
||||
}
|
||||
@ -145,7 +145,7 @@ impl Display for Expression {
|
||||
match self {
|
||||
Ident(s) => write!(f, "{}", s),
|
||||
Str(s) => write!(f, "{:?}", s),
|
||||
Number(n) => write!(f, "{}", n),
|
||||
Num(n) => write!(f, "{}", n),
|
||||
Size(s) => write!(f, "{}", s),
|
||||
Bool(b) => write!(f, "{}", b),
|
||||
}
|
||||
|
@ -211,7 +211,7 @@ impl<'s> Parser<'s> {
|
||||
if let Ok(b) = text.parse::<bool>() {
|
||||
Expression::Bool(b)
|
||||
} else if let Ok(num) = text.parse::<f64>() {
|
||||
Expression::Number(num)
|
||||
Expression::Num(num)
|
||||
} else if let Ok(size) = text.parse::<Size>() {
|
||||
Expression::Size(size)
|
||||
} else {
|
||||
@ -499,7 +499,7 @@ mod tests {
|
||||
|
||||
mod args {
|
||||
use super::Expression;
|
||||
pub use Expression::{Number as N, Size as Z, Bool as B};
|
||||
pub use Expression::{Num as N, Size as Z, Bool as B};
|
||||
|
||||
pub fn S(string: &str) -> Expression { Expression::Str(string.to_owned()) }
|
||||
pub fn I(string: &str) -> Expression { Expression::Ident(string.to_owned()) }
|
||||
|
@ -1,6 +1,6 @@
|
||||
{size:420pt*300pt}
|
||||
|
||||
[box: horizontal][
|
||||
[box: flow=horizontal][
|
||||
*Technical University Berlin* [n]
|
||||
*Faculty II, Institute for Mathematics* [n]
|
||||
Secretary Example [n]
|
||||
@ -17,7 +17,7 @@
|
||||
*Alle Antworten sind zu beweisen.*
|
||||
]
|
||||
|
||||
[box: horizontal][
|
||||
[box: flow=horizontal][
|
||||
*1. Aufgabe* [align: right][(1 + 1 + 2 Punkte)]
|
||||
]
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user