Create easy-to-use argument parser 💎

This commit is contained in:
Laurenz 2019-11-07 19:02:13 +01:00
parent 271af7ed03
commit 1ece263579
10 changed files with 168 additions and 89 deletions

View File

@ -1,8 +1,9 @@
use std::iter::Peekable; //! Helper types and macros for creating custom functions.
use std::slice::Iter;
use super::prelude::*;
/// Implement the function trait more concisely. use super::prelude::*;
use Expression::*;
/// Lets you implement the function trait more concisely.
#[macro_export] #[macro_export]
macro_rules! function { macro_rules! function {
(data: $ident:ident, $($tts:tt)*) => ( (data: $ident:ident, $($tts:tt)*) => (
@ -15,15 +16,15 @@ macro_rules! function {
); );
(@parse $ident:ident, parse: plain, $($tts:tt)*) => ( (@parse $ident:ident, parse: plain, $($tts:tt)*) => (
fn parse(header: &FuncHeader, body: Option<&str>, _: ParseContext) fn parse(header: &FuncHeader, body: Option<&str>, _: ParseContext) -> ParseResult<Self>
-> ParseResult<Self> where Self: Sized where Self: Sized {
{ ArgParser::new(&header.args).done()?;
Arguments::new(header).done()?;
if body.is_some() { if body.is_some() {
err!("expected no body"); err!("expected no body");
} }
Ok($ident) Ok($ident)
} }
function!(@layout $($tts)*); function!(@layout $($tts)*);
); );
@ -33,14 +34,15 @@ macro_rules! function {
$block:block $block:block
$($tts:tt)* $($tts:tt)*
) => ( ) => (
fn parse(header: &FuncHeader, body: Option<&str>, ctx: ParseContext) fn parse(header: &FuncHeader, body: Option<&str>, ctx: ParseContext) -> ParseResult<Self>
-> ParseResult<Self> where Self: Sized where Self: Sized {
{ #[allow(unused_mut)]
#[allow(unused_mut)] let mut $args = Arguments::new(header); let mut $args = ArgParser::new(&header.args);
let $body = body; let $body = body;
let $ctx = ctx; let $ctx = ctx;
$block $block
} }
function!(@layout $($tts)*); 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_export]
macro_rules! err { macro_rules! err {
(@$($tts:tt)*) => ($crate::syntax::ParseError::new(format!($($tts)*))); (@$($tts:tt)*) => ($crate::syntax::ParseError::new(format!($($tts)*)));
($($tts:tt)*) => (return Err(err!(@$($tts)*));); ($($tts:tt)*) => (return Err(err!(@$($tts)*)););
} }
/// Convenient interface for parsing function arguments. /// Easy parsing of function arguments.
pub struct Arguments<'a> { pub struct ArgParser<'a> {
args: Peekable<Iter<'a, Spanned<Expression>>>, args: &'a FuncArgs,
positional_index: usize,
} }
impl<'a> Arguments<'a> { impl<'a> ArgParser<'a> {
pub fn new(header: &'a FuncHeader) -> Arguments<'a> { pub fn new(args: &'a FuncArgs) -> ArgParser<'a> {
Arguments { ArgParser {
args: header.args.positional.iter().peekable() args,
positional_index: 0,
} }
} }
pub fn get_expr(&mut self) -> ParseResult<&'a Spanned<Expression>> { /// Get the next positional argument of the given type.
self.args.next() ///
.ok_or_else(|| ParseError::new("expected expression")) /// 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>> { /// Get the next positional argument if there is any.
let expr = self.get_expr()?; ///
match &expr.val { /// If the argument is of the wrong type, this will return an error.
Expression::Ident(s) => Ok(Spanned::new(s.as_str(), expr.span)), pub fn get_pos_opt<T>(&mut self) -> ParseResult<Option<Spanned<T::Output>>>
_ => err!("expected identifier"), 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;
} }
pub fn get_ident_if_present(&mut self) -> ParseResult<Option<Spanned<&'a str>>> { arg
if self.args.peek().is_some() {
self.get_ident().map(|s| Some(s))
} else {
Ok(None)
}
} }
pub fn done(&mut self) -> ParseResult<()> { /// Get a keyword argument with the given key and type.
if self.args.peek().is_none() { 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))
}
/// 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(()) Ok(())
} else { } 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);

View File

@ -7,8 +7,7 @@ use std::fmt::{self, Debug, Formatter};
use self::prelude::*; use self::prelude::*;
#[macro_use] #[macro_use]
mod helpers; pub mod helpers;
pub use helpers::Arguments;
/// Useful imports for creating your own functions. /// Useful imports for creating your own functions.
pub mod prelude { pub mod prelude {
@ -24,11 +23,12 @@ pub mod prelude {
/// Typesetting function types. /// Typesetting function types.
/// ///
/// These types have to be able to parse tokens into themselves and store the /// These types have to be able to parse themselves from a string and build
/// relevant information from the parsing to do their role in typesetting later. /// a list of layouting commands corresponding to the parsed source.
/// ///
/// The trait `FunctionBounds` is automatically implemented for types which can /// This trait is a supertrait of `FunctionBounds` for technical reasons. The
/// be used as functions, that is they fulfill the bounds `Debug + PartialEq + /// trait `FunctionBounds` is automatically implemented for types which can
/// be used as functions, that is, all types which fulfill the bounds `Debug + PartialEq +
/// 'static`. /// 'static`.
pub trait Function: FunctionBounds { pub trait Function: FunctionBounds {
/// Parse the header and body into this function given a context. /// 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. /// A sequence of commands requested for execution by a function.
#[derive(Debug)] #[derive(Debug)]
pub struct CommandList<'a> { pub struct CommandList<'a> {
@ -130,19 +143,8 @@ impl<'a> IntoIterator for &'a CommandList<'a> {
} }
} }
/// Commands requested for execution by functions. /// Create a list of commands.
#[derive(Debug)] #[macro_export]
pub enum Command<'a> {
Layout(&'a SyntaxTree),
Add(Layout),
AddMany(MultiLayout),
AddFlex(Layout),
SetAlignment(Alignment),
SetStyle(TextStyle),
FinishLayout,
FinishFlexRun,
}
macro_rules! commands { macro_rules! commands {
($($x:expr),*$(,)*) => ( ($($x:expr),*$(,)*) => (
$crate::func::CommandList::from_vec(vec![$($x,)*]) $crate::func::CommandList::from_vec(vec![$($x,)*])

View File

@ -62,7 +62,7 @@ debug_display!(LayoutAction);
/// content is written. /// content is written.
/// ///
/// Furthermore, the action list can translate absolute position into a coordinate system /// 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 /// be added at a position, effectively translating all movement actions inside the layout
/// by the position. /// by the position.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]

View File

@ -2,7 +2,7 @@ use super::*;
/// Layouts boxes stack-like. /// 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)] #[derive(Debug, Clone)]
pub struct StackLayouter { pub struct StackLayouter {
ctx: StackContext, ctx: StackContext,

View File

@ -108,7 +108,7 @@ impl<'a, 'p> TreeLayouter<'a, 'p> {
for command in commands { for command in commands {
match command { match command {
Command::Layout(tree) => self.layout(tree)?, Command::LayoutTree(tree) => self.layout(tree)?,
Command::Add(layout) => { Command::Add(layout) => {
self.finish_flex()?; self.finish_flex()?;

View File

@ -1,6 +1,7 @@
use crate::func::prelude::*; use crate::func::prelude::*;
use Command::*;
/// Ends the current page. /// 📜 `page.break`: Ends the current page.
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq)]
pub struct Pagebreak; pub struct Pagebreak;
@ -9,11 +10,11 @@ function! {
parse: plain, parse: plain,
layout(_, _) { layout(_, _) {
Ok(commands![Command::FinishLayout]) Ok(commands![FinishLayout])
} }
} }
/// Ends the current line. /// 🔙 `line.break`, `n`: Ends the current line.
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq)]
pub struct Linebreak; pub struct Linebreak;
@ -22,11 +23,14 @@ function! {
parse: plain, parse: plain,
layout(_, _) { 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)] #[derive(Debug, PartialEq)]
pub struct Align { pub struct Align {
body: Option<SyntaxTree>, body: Option<SyntaxTree>,
@ -38,7 +42,7 @@ function! {
parse(args, body, ctx) { parse(args, body, ctx) {
let body = parse!(optional: body, ctx); let body = parse!(optional: body, ctx);
let arg = args.get_ident()?; let arg = args.get_pos::<ArgIdent>()?;
let alignment = match arg.val { let alignment = match arg.val {
"left" => Alignment::Left, "left" => Alignment::Left,
"right" => Alignment::Right, "right" => Alignment::Right,
@ -56,17 +60,22 @@ function! {
layout(this, ctx) { layout(this, ctx) {
Ok(commands![match &this.body { Ok(commands![match &this.body {
Some(body) => { Some(body) => {
Command::AddMany(layout_tree(body, LayoutContext { AddMany(layout_tree(body, LayoutContext {
alignment: this.alignment, alignment: this.alignment,
.. ctx .. 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)] #[derive(Debug, PartialEq)]
pub struct Boxed { pub struct Boxed {
body: SyntaxTree, body: SyntaxTree,
@ -80,7 +89,7 @@ function! {
let body = parse!(required: body, ctx); let body = parse!(required: body, ctx);
let mut flow = Flow::Vertical; 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 { flow = match ident.val {
"vertical" => Flow::Vertical, "vertical" => Flow::Vertical,
"horizontal" => Flow::Horizontal, "horizontal" => Flow::Horizontal,
@ -97,7 +106,7 @@ function! {
layout(this, ctx) { layout(this, ctx) {
Ok(commands![ Ok(commands![
Command::AddMany(layout_tree(&this.body, LayoutContext { AddMany(layout_tree(&this.body, LayoutContext {
flow: this.flow, flow: this.flow,
.. ctx .. ctx
})?) })?)
@ -106,8 +115,12 @@ function! {
} }
macro_rules! spacefunc { macro_rules! spacefunc {
($ident:ident, $name:expr, $var:ident => $command:expr) => ( ($ident:ident, $doc:expr, $var:ident => $command:expr) => (
/// Adds whitespace. #[doc = $doc]
///
/// **Positional arguments:**
/// - Spacing as a size or number, which is interpreted as a multiple
/// of the font size _(required)_.
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq)]
pub struct $ident(Spacing); pub struct $ident(Spacing);
@ -117,10 +130,10 @@ macro_rules! spacefunc {
parse(args, body, _ctx) { parse(args, body, _ctx) {
parse!(forbidden: body); parse!(forbidden: body);
let arg = args.get_expr()?; let arg = args.get_pos::<ArgExpr>()?;
let spacing = match arg.val { let spacing = match arg.val {
Expression::Size(s) => Spacing::Absolute(s), Expression::Size(s) => Spacing::Absolute(*s),
Expression::Number(f) => Spacing::Relative(f as f32), Expression::Num(f) => Spacing::Relative(*f as f32),
_ => err!("invalid spacing, expected size or number"), _ => err!("invalid spacing, expected size or number"),
}; };
@ -146,5 +159,8 @@ enum Spacing {
Relative(f32), Relative(f32),
} }
spacefunc!(HorizontalSpace, "h", space => Command::AddFlex(Layout::empty(space, Size::zero()))); spacefunc!(HorizontalSpace, "📖 `h`: Adds horizontal whitespace.",
spacefunc!(VerticalSpace, "v", space => Command::Add(Layout::empty(Size::zero(), space))); space => AddFlex(Layout::empty(space, Size::zero())));
spacefunc!(VerticalSpace, "📑 `v`: Adds vertical whitespace.",
space => Add(Layout::empty(Size::zero(), space)));

View File

@ -2,8 +2,8 @@ use crate::func::prelude::*;
use toddle::query::FontClass; use toddle::query::FontClass;
macro_rules! stylefunc { macro_rules! stylefunc {
($ident:ident) => ( ($ident:ident, $doc:expr) => (
/// Styles text. #[doc = $doc]
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq)]
pub struct $ident { pub struct $ident {
body: Option<SyntaxTree> body: Option<SyntaxTree>
@ -24,7 +24,7 @@ macro_rules! stylefunc {
Ok(match &this.body { Ok(match &this.body {
Some(body) => commands![ Some(body) => commands![
Command::SetStyle(new_style), Command::SetStyle(new_style),
Command::Layout(body), Command::LayoutTree(body),
Command::SetStyle(ctx.style.clone()), Command::SetStyle(ctx.style.clone()),
], ],
None => commands![Command::SetStyle(new_style)] None => commands![Command::SetStyle(new_style)]
@ -34,6 +34,6 @@ macro_rules! stylefunc {
); );
} }
stylefunc!(Italic); stylefunc!(Italic, "💡 `italic`: Sets text in _italics_.");
stylefunc!(Bold); stylefunc!(Bold, "🧱 `bold`: Sets text in **bold**.");
stylefunc!(Monospace); stylefunc!(Monospace, "👩‍💻 `mono`: Sets text in `monospace`.");

View File

@ -128,7 +128,7 @@ pub enum FuncArg {
pub enum Expression { pub enum Expression {
Ident(String), Ident(String),
Str(String), Str(String),
Number(f64), Num(f64),
Size(Size), Size(Size),
Bool(bool), Bool(bool),
} }
@ -145,7 +145,7 @@ impl Display for Expression {
match self { match self {
Ident(s) => write!(f, "{}", s), Ident(s) => write!(f, "{}", s),
Str(s) => write!(f, "{:?}", s), Str(s) => write!(f, "{:?}", s),
Number(n) => write!(f, "{}", n), Num(n) => write!(f, "{}", n),
Size(s) => write!(f, "{}", s), Size(s) => write!(f, "{}", s),
Bool(b) => write!(f, "{}", b), Bool(b) => write!(f, "{}", b),
} }

View File

@ -211,7 +211,7 @@ impl<'s> Parser<'s> {
if let Ok(b) = text.parse::<bool>() { if let Ok(b) = text.parse::<bool>() {
Expression::Bool(b) Expression::Bool(b)
} else if let Ok(num) = text.parse::<f64>() { } else if let Ok(num) = text.parse::<f64>() {
Expression::Number(num) Expression::Num(num)
} else if let Ok(size) = text.parse::<Size>() { } else if let Ok(size) = text.parse::<Size>() {
Expression::Size(size) Expression::Size(size)
} else { } else {
@ -499,7 +499,7 @@ mod tests {
mod args { mod args {
use super::Expression; 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 S(string: &str) -> Expression { Expression::Str(string.to_owned()) }
pub fn I(string: &str) -> Expression { Expression::Ident(string.to_owned()) } pub fn I(string: &str) -> Expression { Expression::Ident(string.to_owned()) }

View File

@ -1,6 +1,6 @@
{size:420pt*300pt} {size:420pt*300pt}
[box: horizontal][ [box: flow=horizontal][
*Technical University Berlin* [n] *Technical University Berlin* [n]
*Faculty II, Institute for Mathematics* [n] *Faculty II, Institute for Mathematics* [n]
Secretary Example [n] Secretary Example [n]
@ -17,7 +17,7 @@
*Alle Antworten sind zu beweisen.* *Alle Antworten sind zu beweisen.*
] ]
[box: horizontal][ [box: flow=horizontal][
*1. Aufgabe* [align: right][(1 + 1 + 2 Punkte)] *1. Aufgabe* [align: right][(1 + 1 + 2 Punkte)]
] ]