Introduce a set of macros for writing functions more concisely 🎁

This commit is contained in:
Laurenz 2019-10-23 00:14:43 +02:00
parent cff325b520
commit ecf0ff4d05
10 changed files with 347 additions and 339 deletions

129
src/func/helpers.rs Normal file
View File

@ -0,0 +1,129 @@
use super::prelude::*;
use std::iter::Peekable;
use std::slice::Iter;
/// Implement the function trait more concisely.
#[macro_export]
macro_rules! function {
(data: $ident:ident, $($tts:tt)*) => {
impl Function for $ident {
function!(@parse $ident, $($tts)*);
}
};
(@parse $ident:ident, parse: plain, $($tts:tt)*) => {
fn parse(header: &FuncHeader, body: Option<&str>, _: ParseContext)
-> ParseResult<Self> where Self: Sized
{
Arguments::new(header).done()?;
if body.is_some() {
err!("expected no body");
}
Ok($ident)
}
function!(@layout $($tts)*);
};
(
@parse $ident:ident,
parse($args:ident, $body:ident, $ctx:ident)
$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);
let $body = body;
let $ctx = ctx;
$block
}
function!(@layout $($tts)*);
};
(@layout layout($this:pat, $ctx:pat) $block:block) => {
fn layout(&self, ctx: LayoutContext) -> LayoutResult<CommandList> {
let $ctx = ctx;
let $this = self;
$block
}
};
}
/// Parse the body of a function.
/// - If the function does not expect a body, use `forbidden`.
/// - If the function can have a body, use `optional`.
/// - If the function must have a body, use `required`.
#[macro_export]
macro_rules! parse {
(forbidden: $body:expr) => {
if $body.is_some() {
err!("unexpected body");
}
};
(optional: $body:expr, $ctx:expr) => {
if let Some(body) = $body {
Some($crate::parsing::parse(body, $ctx)?)
} else {
None
}
};
(required: $body:expr, $ctx:expr) => {
if let Some(body) = $body {
$crate::parsing::parse(body, $ctx)?
} else {
err!("expected body");
}
}
}
/// Return a formatted parsing error.
#[macro_export]
macro_rules! err {
($($tts:tt)*) => {
return Err(ParseError::new(format!($($tts)*)));
};
}
/// Convenient interface for parsing function arguments.
pub struct Arguments<'a> {
args: Peekable<Iter<'a, Expression>>,
}
impl<'a> Arguments<'a> {
pub fn new(header: &'a FuncHeader) -> Arguments<'a> {
Arguments {
args: header.args.iter().peekable()
}
}
pub fn get_expr(&mut self) -> ParseResult<&'a Expression> {
self.args.next()
.ok_or_else(|| ParseError::new("expected expression"))
}
pub fn get_ident(&mut self) -> ParseResult<&'a str> {
match self.get_expr()? {
Expression::Ident(s) => Ok(s.as_str()),
_ => Err(ParseError::new("expected identifier")),
}
}
pub fn get_ident_if_present(&mut self) -> ParseResult<Option<&'a str>> {
if self.args.peek().is_some() {
self.get_ident().map(|s| Some(s))
} else {
Ok(None)
}
}
pub fn done(&mut self) -> ParseResult<()> {
if self.args.peek().is_none() {
Ok(())
} else {
Err(ParseError::new("unexpected argument"))
}
}
}

View File

@ -9,6 +9,21 @@ use crate::parsing::{ParseContext, ParseResult};
use crate::style::TextStyle;
use crate::syntax::{FuncHeader, SyntaxTree};
#[macro_use]
mod helpers;
pub use helpers::Arguments;
/// Useful imports for creating your own functions.
pub mod prelude {
pub use crate::func::{Command, CommandList, Function};
pub use crate::layout::{layout_tree, Layout, LayoutContext, MultiLayout};
pub use crate::layout::{Flow, Alignment, LayoutError, LayoutResult};
pub use crate::parsing::{parse, ParseContext, ParseError, ParseResult};
pub use crate::syntax::{Expression, FuncHeader, SyntaxTree};
pub use crate::size::{Size, Size2D, SizeBox};
pub use super::helpers::*;
}
/// Typesetting function types.
///
/// These types have to be able to parse tokens into themselves and store the

View File

@ -1,49 +0,0 @@
use super::prelude::*;
use crate::layout::Alignment;
/// Allows to align content in different ways.
#[derive(Debug, PartialEq)]
pub struct AlignFunc {
alignment: Alignment,
body: Option<SyntaxTree>,
}
impl Function for AlignFunc {
fn parse(header: &FuncHeader, body: Option<&str>, ctx: ParseContext) -> ParseResult<Self>
where Self: Sized {
if header.args.len() != 1 || !header.kwargs.is_empty() {
return err("align: expected exactly one positional argument");
}
let alignment = if let Expression::Ident(ident) = &header.args[0] {
match ident.as_str() {
"left" => Alignment::Left,
"right" => Alignment::Right,
"center" => Alignment::Center,
s => return err(format!("invalid alignment specifier: '{}'", s)),
}
} else {
return err(format!(
"expected alignment specifier, found: '{}'",
header.args[0]
));
};
let body = parse_maybe_body(body, ctx)?;
Ok(AlignFunc { alignment, body })
}
fn layout(&self, ctx: LayoutContext) -> LayoutResult<CommandList> {
if let Some(body) = &self.body {
let layouts = layout_tree(body, LayoutContext {
alignment: self.alignment,
.. ctx
})?;
Ok(commands![Command::AddMany(layouts)])
} else {
Ok(commands![Command::SetAlignment(self.alignment)])
}
}
}

View File

@ -1,51 +0,0 @@
use super::prelude::*;
use crate::layout::Flow;
/// Wraps content into a box.
#[derive(Debug, PartialEq)]
pub struct BoxFunc {
body: SyntaxTree,
flow: Flow,
}
impl Function for BoxFunc {
fn parse(header: &FuncHeader, body: Option<&str>, ctx: ParseContext) -> ParseResult<Self>
where Self: Sized {
let flow = if header.args.is_empty() {
Flow::Vertical
} else if header.args.len() == 1 {
if let Expression::Ident(ident) = &header.args[0] {
match ident.as_str() {
"vertical" => Flow::Vertical,
"horizontal" => Flow::Horizontal,
f => return err(format!("invalid flow specifier: '{}'", f)),
}
} else {
return err(format!(
"expected alignment specifier, found: '{}'",
header.args[0]
));
}
} else {
return err("box: expected flow specifier or no arguments");
};
if let Some(body) = body {
Ok(BoxFunc {
body: parse(body, ctx)?,
flow,
})
} else {
err("box: expected body")
}
}
fn layout(&self, ctx: LayoutContext) -> LayoutResult<CommandList> {
let layout = layout_tree(&self.body, LayoutContext {
flow: self.flow,
.. ctx
})?;
Ok(commands![Command::AddMany(layout)])
}
}

View File

@ -1,47 +0,0 @@
use super::prelude::*;
/// Ends the current line.
#[derive(Debug, PartialEq)]
pub struct LinebreakFunc;
impl Function for LinebreakFunc {
fn parse(header: &FuncHeader, body: Option<&str>, _: ParseContext) -> ParseResult<Self>
where Self: Sized {
if has_arguments(header) {
return err("linebreak: expected no arguments");
}
if body.is_some() {
return err("linebreak: expected no body");
}
Ok(LinebreakFunc)
}
fn layout(&self, _: LayoutContext) -> LayoutResult<CommandList> {
Ok(commands![Command::FinishFlexRun])
}
}
/// Ends the current page.
#[derive(Debug, PartialEq)]
pub struct PagebreakFunc;
impl Function for PagebreakFunc {
fn parse(header: &FuncHeader, body: Option<&str>, _: ParseContext) -> ParseResult<Self>
where Self: Sized {
if has_arguments(header) {
return err("pagebreak: expected no arguments");
}
if body.is_some() {
return err("pagebreak: expected no body");
}
Ok(PagebreakFunc)
}
fn layout(&self, _: LayoutContext) -> LayoutResult<CommandList> {
Ok(commands![Command::FinishLayout])
}
}

View File

@ -2,64 +2,29 @@
use crate::func::Scope;
mod align;
mod boxed;
mod breaks;
mod spacing;
mod styles;
mod structure;
mod style;
/// Useful imports for creating your own functions.
pub mod prelude {
pub use crate::func::{Command, CommandList, Function};
pub use crate::layout::{layout_tree, Layout, LayoutContext, MultiLayout};
pub use crate::layout::{LayoutError, LayoutResult};
pub use crate::parsing::{parse, ParseContext, ParseError, ParseResult};
pub use crate::syntax::{Expression, FuncHeader, SyntaxTree};
pub use super::helpers::*;
}
pub use align::AlignFunc;
pub use boxed::BoxFunc;
pub use breaks::{LinebreakFunc, PagebreakFunc};
pub use spacing::{HorizontalSpaceFunc, VerticalSpaceFunc};
pub use styles::{BoldFunc, ItalicFunc, MonospaceFunc};
pub use structure::*;
pub use style::*;
/// Create a scope with all standard functions.
pub fn std() -> Scope {
let mut std = Scope::new();
std.add::<AlignFunc>("align");
std.add::<BoxFunc>("box");
std.add::<LinebreakFunc>("line.break");
std.add::<LinebreakFunc>("n");
std.add::<PagebreakFunc>("page.break");
std.add::<Align>("align");
std.add::<Boxed>("box");
std.add::<HorizontalSpaceFunc>("h");
std.add::<VerticalSpaceFunc>("v");
std.add::<Linebreak>("line.break");
std.add::<Linebreak>("n");
std.add::<Pagebreak>("page.break");
std.add::<HorizontalSpace>("h");
std.add::<VerticalSpace>("v");
std.add::<Bold>("bold");
std.add::<Italic>("italic");
std.add::<Monospace>("mono");
std.add::<BoldFunc>("bold");
std.add::<ItalicFunc>("italic");
std.add::<MonospaceFunc>("mono");
std
}
/// Helpers for writing custom functions.
pub mod helpers {
use super::prelude::*;
pub fn has_arguments(header: &FuncHeader) -> bool {
!header.args.is_empty() || !header.kwargs.is_empty()
}
pub fn parse_maybe_body(body: Option<&str>, ctx: ParseContext) -> ParseResult<Option<SyntaxTree>> {
if let Some(body) = body {
Ok(Some(parse(body, ctx)?))
} else {
Ok(None)
}
}
pub fn err<S: Into<String>, T>(message: S) -> ParseResult<T> {
Err(ParseError::new(message))
}
}

View File

@ -1,76 +0,0 @@
use std::marker::PhantomData;
use super::prelude::*;
use crate::size::Size;
/// Adds vertical space.
pub type VerticalSpaceFunc = SpaceFunc<SpaceVertical>;
/// Adds horizontal space.
pub type HorizontalSpaceFunc = SpaceFunc<SpaceHorizontal>;
/// Adds generic space.
#[derive(Debug, PartialEq)]
pub struct SpaceFunc<F: SpaceFlow> {
spacing: Spacing,
_phantom: PhantomData<F>,
}
/// Absolute or font-relative spacing.
#[derive(Debug, PartialEq)]
enum Spacing {
Absolute(Size),
Relative(f32),
}
impl<F: SpaceFlow> Function for SpaceFunc<F> {
fn parse(header: &FuncHeader, body: Option<&str>, _: ParseContext) -> ParseResult<Self>
where Self: Sized {
if header.args.len() != 1 || !header.kwargs.is_empty() {
return err("align: expected exactly one positional argument");
}
let spacing = match header.args[0] {
Expression::Size(s) => Spacing::Absolute(s),
Expression::Number(f) => Spacing::Relative(f as f32),
_ => return err("space: expected size or number"),
};
if body.is_some() {
return err("space: expected no body");
}
Ok(SpaceFunc {
spacing,
_phantom: PhantomData,
})
}
fn layout(&self, ctx: LayoutContext) -> LayoutResult<CommandList> {
let space = match self.spacing {
Spacing::Absolute(s) => s,
Spacing::Relative(f) => Size::pt(f * ctx.style.font_size),
};
Ok(commands![F::cmd(space)])
}
}
pub trait SpaceFlow: std::fmt::Debug + PartialEq + 'static {
fn cmd(space: Size) -> Command<'static>;
}
#[derive(Debug, PartialEq)]
pub struct SpaceVertical;
impl SpaceFlow for SpaceVertical {
fn cmd(space: Size) -> Command<'static> {
Command::Add(Layout::empty(Size::zero(), space))
}
}
#[derive(Debug, PartialEq)]
pub struct SpaceHorizontal;
impl SpaceFlow for SpaceHorizontal {
fn cmd(space: Size) -> Command<'static> {
Command::AddFlex(Layout::empty(space, Size::zero()))
}
}

148
src/library/structure.rs Normal file
View File

@ -0,0 +1,148 @@
use crate::func::prelude::*;
/// Ends the current page.
#[derive(Debug, PartialEq)]
pub struct Pagebreak;
function! {
data: Pagebreak,
parse: plain,
layout(_, _) {
Ok(commands![Command::FinishLayout])
}
}
/// Ends the current line.
#[derive(Debug, PartialEq)]
pub struct Linebreak;
function! {
data: Linebreak,
parse: plain,
layout(_, _) {
Ok(commands![Command::FinishFlexRun])
}
}
/// Aligns content in different ways.
#[derive(Debug, PartialEq)]
pub struct Align {
body: Option<SyntaxTree>,
alignment: Alignment,
}
function! {
data: Align,
parse(args, body, ctx) {
let body = parse!(optional: body, ctx);
let alignment = match args.get_ident()? {
"left" => Alignment::Left,
"right" => Alignment::Right,
"center" => Alignment::Center,
s => err!("invalid alignment specifier: {}", s),
};
args.done()?;
Ok(Align {
body,
alignment,
})
}
layout(this, ctx) {
Ok(commands![match &this.body {
Some(body) => {
Command::AddMany(layout_tree(body, LayoutContext {
alignment: this.alignment,
.. ctx
})?)
}
None => Command::SetAlignment(this.alignment)
}])
}
}
/// Layouts content into a box.
#[derive(Debug, PartialEq)]
pub struct Boxed {
body: SyntaxTree,
flow: Flow,
}
function! {
data: Boxed,
parse(args, body, ctx) {
let body = parse!(required: body, ctx);
let mut flow = Flow::Vertical;
if let Some(ident) = args.get_ident_if_present()? {
flow = match ident {
"vertical" => Flow::Vertical,
"horizontal" => Flow::Horizontal,
f => err!("invalid flow specifier: {}", f),
};
}
args.done()?;
Ok(Boxed {
body,
flow,
})
}
layout(this, ctx) {
Ok(commands![
Command::AddMany(layout_tree(&this.body, LayoutContext {
flow: this.flow,
.. ctx
})?)
])
}
}
macro_rules! spacefunc {
($ident:ident, $name:expr, $var:ident => $command:expr) => {
/// Adds whitespace.
#[derive(Debug, PartialEq)]
pub struct $ident(Spacing);
function! {
data: $ident,
parse(args, body, _ctx) {
parse!(forbidden: body);
let spacing = match args.get_expr()? {
Expression::Size(s) => Spacing::Absolute(*s),
Expression::Number(f) => Spacing::Relative(*f as f32),
_ => err!("invalid spacing, expected size or number"),
};
Ok($ident(spacing))
}
layout(this, ctx) {
let $var = match this.0 {
Spacing::Absolute(s) => s,
Spacing::Relative(f) => Size::pt(f * ctx.style.font_size),
};
Ok(commands![$command])
}
}
};
}
/// Absolute or font-relative spacing.
#[derive(Debug, PartialEq)]
enum Spacing {
Absolute(Size),
Relative(f32),
}
spacefunc!(HorizontalSpace, "h", space => Command::AddFlex(Layout::empty(space, Size::zero())));
spacefunc!(VerticalSpace, "v", space => Command::Add(Layout::empty(Size::zero(), space)));

39
src/library/style.rs Normal file
View File

@ -0,0 +1,39 @@
use crate::func::prelude::*;
use toddle::query::FontClass;
macro_rules! stylefunc {
($ident:ident) => {
/// Styles text.
#[derive(Debug, PartialEq)]
pub struct $ident {
body: Option<SyntaxTree>
}
function! {
data: $ident,
parse(args, body, ctx) {
args.done()?;
Ok($ident { body: parse!(optional: body, ctx) })
}
layout(this, ctx) {
let mut new_style = ctx.style.clone();
new_style.toggle_class(FontClass::$ident);
Ok(match &this.body {
Some(body) => commands![
Command::SetStyle(new_style),
Command::Layout(body),
Command::SetStyle(ctx.style.clone()),
],
None => commands![Command::SetStyle(new_style)]
})
}
}
};
}
stylefunc!(Italic);
stylefunc!(Bold);
stylefunc!(Monospace);

View File

@ -1,65 +0,0 @@
use toddle::query::FontClass;
use super::prelude::*;
macro_rules! style_func {
(
$(#[$outer:meta])*
pub struct $struct:ident { $name:expr },
$style:ident => $class:ident
) => {
$(#[$outer])*
#[derive(Debug, PartialEq)]
pub struct $struct {
body: Option<SyntaxTree>
}
impl Function for $struct {
fn parse(header: &FuncHeader, body: Option<&str>, ctx: ParseContext)
-> ParseResult<Self> where Self: Sized {
// Accept only invocations without arguments and with body.
if has_arguments(header) {
return err(format!("{}: expected no arguments", $name));
}
let body = parse_maybe_body(body, ctx)?;
Ok($struct { body })
}
fn layout(&self, ctx: LayoutContext) -> LayoutResult<CommandList> {
let mut new_style = ctx.style.clone();
new_style.toggle_class(FontClass::$class);
if let Some(body) = &self.body {
let saved_style = ctx.style.clone();
Ok(commands![
Command::SetStyle(new_style),
Command::Layout(body),
Command::SetStyle(saved_style),
])
} else {
Ok(commands![Command::SetStyle(new_style)])
}
}
}
};
}
style_func! {
/// Typesets text in bold.
pub struct BoldFunc { "bold" },
style => Bold
}
style_func! {
/// Typesets text in italics.
pub struct ItalicFunc { "italic" },
style => Italic
}
style_func! {
/// Typesets text in monospace.
pub struct MonospaceFunc { "mono" },
style => Monospace
}