diff --git a/src/func/helpers.rs b/src/func/helpers.rs new file mode 100644 index 000000000..d562284f8 --- /dev/null +++ b/src/func/helpers.rs @@ -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 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 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 { + 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>, +} + +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> { + 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")) + } + } +} diff --git a/src/func.rs b/src/func/mod.rs similarity index 89% rename from src/func.rs rename to src/func/mod.rs index 05bbe83df..9a6fcbd14 100644 --- a/src/func.rs +++ b/src/func/mod.rs @@ -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 diff --git a/src/library/align.rs b/src/library/align.rs deleted file mode 100644 index cc41f295d..000000000 --- a/src/library/align.rs +++ /dev/null @@ -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, -} - -impl Function for AlignFunc { - fn parse(header: &FuncHeader, body: Option<&str>, ctx: ParseContext) -> ParseResult - 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 { - 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)]) - } - } -} diff --git a/src/library/boxed.rs b/src/library/boxed.rs deleted file mode 100644 index 71a184a66..000000000 --- a/src/library/boxed.rs +++ /dev/null @@ -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 - 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 { - let layout = layout_tree(&self.body, LayoutContext { - flow: self.flow, - .. ctx - })?; - - Ok(commands![Command::AddMany(layout)]) - } -} diff --git a/src/library/breaks.rs b/src/library/breaks.rs deleted file mode 100644 index a622350f2..000000000 --- a/src/library/breaks.rs +++ /dev/null @@ -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 - 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 { - 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 - 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 { - Ok(commands![Command::FinishLayout]) - } -} diff --git a/src/library/mod.rs b/src/library/mod.rs index 784ef2041..74f77204d 100644 --- a/src/library/mod.rs +++ b/src/library/mod.rs @@ -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::("align"); - std.add::("box"); - std.add::("line.break"); - std.add::("n"); - std.add::("page.break"); + std.add::("align"); + std.add::("box"); - std.add::("h"); - std.add::("v"); + std.add::("line.break"); + std.add::("n"); + std.add::("page.break"); + + std.add::("h"); + std.add::("v"); + + std.add::("bold"); + std.add::("italic"); + std.add::("mono"); - std.add::("bold"); - std.add::("italic"); - std.add::("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> { - if let Some(body) = body { - Ok(Some(parse(body, ctx)?)) - } else { - Ok(None) - } - } - - pub fn err, T>(message: S) -> ParseResult { - Err(ParseError::new(message)) - } -} diff --git a/src/library/spacing.rs b/src/library/spacing.rs deleted file mode 100644 index 91288edcb..000000000 --- a/src/library/spacing.rs +++ /dev/null @@ -1,76 +0,0 @@ -use std::marker::PhantomData; -use super::prelude::*; -use crate::size::Size; - -/// Adds vertical space. -pub type VerticalSpaceFunc = SpaceFunc; - -/// Adds horizontal space. -pub type HorizontalSpaceFunc = SpaceFunc; - -/// Adds generic space. -#[derive(Debug, PartialEq)] -pub struct SpaceFunc { - spacing: Spacing, - _phantom: PhantomData, -} - -/// Absolute or font-relative spacing. -#[derive(Debug, PartialEq)] -enum Spacing { - Absolute(Size), - Relative(f32), -} - -impl Function for SpaceFunc { - fn parse(header: &FuncHeader, body: Option<&str>, _: ParseContext) -> ParseResult - 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 { - 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())) - } -} diff --git a/src/library/structure.rs b/src/library/structure.rs new file mode 100644 index 000000000..ae05a12be --- /dev/null +++ b/src/library/structure.rs @@ -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, + 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))); diff --git a/src/library/style.rs b/src/library/style.rs new file mode 100644 index 000000000..397375a48 --- /dev/null +++ b/src/library/style.rs @@ -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 + } + + 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); diff --git a/src/library/styles.rs b/src/library/styles.rs deleted file mode 100644 index bc84ac3b4..000000000 --- a/src/library/styles.rs +++ /dev/null @@ -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 - } - - impl Function for $struct { - fn parse(header: &FuncHeader, body: Option<&str>, ctx: ParseContext) - -> ParseResult 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 { - 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 -}