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;
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);

View File

@ -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,)*])

View File

@ -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)]

View File

@ -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,

View File

@ -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()?;

View File

@ -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)));

View File

@ -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`.");

View File

@ -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),
}

View File

@ -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()) }

View File

@ -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)]
]