diff --git a/src/library/align.rs b/src/library/align.rs new file mode 100644 index 000000000..d16e697da --- /dev/null +++ b/src/library/align.rs @@ -0,0 +1,170 @@ +use super::*; + +/// `align`: Align content along the layouting axes. +/// +/// Which axis an alignment should apply to (main or cross) is inferred from +/// either the argument itself (for anything other than `center`) or from the +/// second argument if present, defaulting to the cross axis for a single +/// `center` alignment. +/// +/// # Positional arguments +/// - Alignments: variadic, of type `alignment`. +/// - Body: optional, of type `template`. +/// +/// # Named arguments +/// - Horizontal alignment: `horizontal`, of type `alignment`. +/// - Vertical alignment: `vertical`, of type `alignment`. +/// +/// # Relevant types and constants +/// - Type `alignment` +/// - `left` +/// - `right` +/// - `top` +/// - `bottom` +/// - `center` +pub fn align(ctx: &mut EvalContext, args: &mut ValueArgs) -> Value { + let first = args.find(ctx); + let second = args.find(ctx); + let hor = args.get(ctx, "horizontal"); + let ver = args.get(ctx, "vertical"); + let body = args.find::(ctx); + + Value::template("align", move |ctx| { + let snapshot = ctx.state.clone(); + + let mut had = Gen::uniform(false); + let mut had_center = false; + + // Infer the axes alignments belong to. + for (axis, Spanned { v: arg, span }) in first + .into_iter() + .chain(second.into_iter()) + .map(|arg: Spanned| (arg.v.axis(), arg)) + .chain(hor.into_iter().map(|arg| (Some(SpecAxis::Horizontal), arg))) + .chain(ver.into_iter().map(|arg| (Some(SpecAxis::Vertical), arg))) + { + // Check whether we know which axis this alignment belongs to. + if let Some(axis) = axis { + // We know the axis. + let gen_axis = axis.switch(ctx.state.dirs); + let gen_align = arg.switch(ctx.state.dirs); + + if arg.axis().map_or(false, |a| a != axis) { + ctx.diag(error!(span, "invalid alignment for {} axis", axis)); + } else if had.get(gen_axis) { + ctx.diag(error!(span, "duplicate alignment for {} axis", axis)); + } else { + *ctx.state.align.get_mut(gen_axis) = gen_align; + *had.get_mut(gen_axis) = true; + } + } else { + // We don't know the axis: This has to be a `center` alignment for a + // positional argument. + debug_assert_eq!(arg, Alignment::Center); + + if had.main && had.cross { + ctx.diag(error!(span, "duplicate alignment")); + } else if had_center { + // Both this and the previous one are unspecified `center` + // alignments. Both axes should be centered. + ctx.state.align.main = Align::Center; + ctx.state.align.cross = Align::Center; + had = Gen::uniform(true); + } else { + had_center = true; + } + } + + // If we we know the other alignment, we can handle the unspecified + // `center` alignment. + if had_center && (had.main || had.cross) { + if had.main { + ctx.state.align.cross = Align::Center; + had.cross = true; + } else { + ctx.state.align.main = Align::Center; + had.main = true; + } + had_center = false; + } + } + + // If `had_center` wasn't flushed by now, it's the only argument and then we + // default to applying it to the cross axis. + if had_center { + ctx.state.align.cross = Align::Center; + } + + if ctx.state.align.main != snapshot.align.main { + ctx.end_par_group(); + ctx.start_par_group(); + } + + if let Some(body) = &body { + body.exec(ctx); + ctx.state = snapshot; + } + }) +} + +/// An alignment argument. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)] +pub(super) enum Alignment { + Left, + Center, + Right, + Top, + Bottom, +} + +impl Alignment { + /// The specific axis this alignment refers to. + fn axis(self) -> Option { + match self { + Self::Left => Some(SpecAxis::Horizontal), + Self::Right => Some(SpecAxis::Horizontal), + Self::Top => Some(SpecAxis::Vertical), + Self::Bottom => Some(SpecAxis::Vertical), + Self::Center => None, + } + } +} + +impl Switch for Alignment { + type Other = Align; + + fn switch(self, dirs: LayoutDirs) -> Self::Other { + let get = |dir: Dir, at_positive_start| { + if dir.is_positive() == at_positive_start { + Align::Start + } else { + Align::End + } + }; + + let dirs = dirs.switch(dirs); + match self { + Self::Left => get(dirs.horizontal, true), + Self::Right => get(dirs.horizontal, false), + Self::Top => get(dirs.vertical, true), + Self::Bottom => get(dirs.vertical, false), + Self::Center => Align::Center, + } + } +} + +impl Display for Alignment { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.pad(match self { + Self::Left => "left", + Self::Center => "center", + Self::Right => "right", + Self::Top => "top", + Self::Bottom => "bottom", + }) + } +} + +typify! { + Alignment: "alignment", +} diff --git a/src/library/base.rs b/src/library/base.rs new file mode 100644 index 000000000..3d067faa8 --- /dev/null +++ b/src/library/base.rs @@ -0,0 +1,62 @@ +use crate::color::{Color, RgbaColor}; +use crate::pretty::pretty; + +use super::*; + +/// `repr`: Get the string representation of a value. +/// +/// # Positional arguments +/// - Any value. +/// +/// # Return value +/// The string representation of the value. +pub fn repr(ctx: &mut EvalContext, args: &mut ValueArgs) -> Value { + match args.require::(ctx, "value") { + Some(value) => pretty(&value).into(), + None => Value::Error, + } +} + +/// `rgb`: Create an RGB(A) color. +/// +/// # Positional arguments +/// - Red component: of type `float`, between 0.0 and 1.0. +/// - Green component: of type `float`, between 0.0 and 1.0. +/// - Blue component: of type `float`, between 0.0 and 1.0. +/// - Alpha component: optional, of type `float`, between 0.0 and 1.0. +pub fn rgb(ctx: &mut EvalContext, args: &mut ValueArgs) -> Value { + let r = args.require(ctx, "red component"); + let g = args.require(ctx, "green component"); + let b = args.require(ctx, "blue component"); + let a = args.find(ctx); + + let mut clamp = |component: Option>, default| { + component.map_or(default, |c| { + if c.v < 0.0 || c.v > 1.0 { + ctx.diag(warning!(c.span, "should be between 0.0 and 1.0")); + } + (c.v.max(0.0).min(1.0) * 255.0).round() as u8 + }) + }; + + Value::Color(Color::Rgba(RgbaColor::new( + clamp(r, 0), + clamp(g, 0), + clamp(b, 0), + clamp(a, 255), + ))) +} + +/// `type`: Find out the name of a value's type. +/// +/// # Positional arguments +/// - Any value. +/// +/// # Return value +/// The name of the value's type as a string. +pub fn type_(ctx: &mut EvalContext, args: &mut ValueArgs) -> Value { + match args.require::(ctx, "value") { + Some(value) => value.type_name().into(), + None => Value::Error, + } +} diff --git a/src/library/extend.rs b/src/library/extend.rs deleted file mode 100644 index cf69dbd0d..000000000 --- a/src/library/extend.rs +++ /dev/null @@ -1,30 +0,0 @@ -use crate::prelude::*; -use crate::pretty::pretty; - -/// `type`: Find out the name of a value's type. -/// -/// # Positional arguments -/// - Any value. -/// -/// # Return value -/// The name of the value's type as a string. -pub fn type_(ctx: &mut EvalContext, args: &mut ValueArgs) -> Value { - match args.require::(ctx, "value") { - Some(value) => value.type_name().into(), - None => Value::Error, - } -} - -/// `repr`: Get the string representation of a value. -/// -/// # Positional arguments -/// - Any value. -/// -/// # Return value -/// The string representation of the value. -pub fn repr(ctx: &mut EvalContext, args: &mut ValueArgs) -> Value { - match args.require::(ctx, "value") { - Some(value) => pretty(&value).into(), - None => Value::Error, - } -} diff --git a/src/library/style.rs b/src/library/font.rs similarity index 81% rename from src/library/style.rs rename to src/library/font.rs index 357275157..439d98055 100644 --- a/src/library/style.rs +++ b/src/library/font.rs @@ -1,15 +1,13 @@ -use std::fmt::{self, Display, Formatter}; - use fontdock::{FontStretch, FontStyle, FontWeight}; -use crate::color::{Color, RgbaColor}; -use crate::prelude::*; +use super::*; /// `font`: Configure the font. /// /// # Positional arguments /// - Font size: optional, of type `linear` relative to current font size. /// - Font families: variadic, of type `font-family`. +/// - Body: optional, of type `template`. /// /// # Named arguments /// - Font Style: `style`, of type `font-style`. @@ -121,7 +119,7 @@ struct FontFamilies(Vec); /// A single font family. #[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)] -pub(crate) enum FontFamily { +pub(super) enum FontFamily { Serif, SansSerif, Monospace, @@ -183,33 +181,3 @@ typify! { typify! { FontStretch: "font stretch" } - -/// `rgb`: Create an RGB(A) color. -/// -/// # Positional arguments -/// - Red component: of type `float`, between 0.0 and 1.0. -/// - Green component: of type `float`, between 0.0 and 1.0. -/// - Blue component: of type `float`, between 0.0 and 1.0. -/// - Alpha component: optional, of type `float`, between 0.0 and 1.0. -pub fn rgb(ctx: &mut EvalContext, args: &mut ValueArgs) -> Value { - let r = args.require(ctx, "red component"); - let g = args.require(ctx, "green component"); - let b = args.require(ctx, "blue component"); - let a = args.find(ctx); - - let mut clamp = |component: Option>, default| { - component.map_or(default, |c| { - if c.v < 0.0 || c.v > 1.0 { - ctx.diag(warning!(c.span, "should be between 0.0 and 1.0")); - } - (c.v.max(0.0).min(1.0) * 255.0).round() as u8 - }) - }; - - Value::Color(Color::Rgba(RgbaColor::new( - clamp(r, 0), - clamp(g, 0), - clamp(b, 0), - clamp(a, 255), - ))) -} diff --git a/src/library/insert.rs b/src/library/image.rs similarity index 98% rename from src/library/insert.rs rename to src/library/image.rs index 4f0f64898..06908ce8d 100644 --- a/src/library/insert.rs +++ b/src/library/image.rs @@ -1,8 +1,8 @@ -use image::GenericImageView; +use ::image::GenericImageView; +use super::*; use crate::env::{ImageResource, ResourceId}; use crate::layout::*; -use crate::prelude::*; /// `image`: Insert an image. /// diff --git a/src/library/layout.rs b/src/library/layout.rs deleted file mode 100644 index 7fc7154ff..000000000 --- a/src/library/layout.rs +++ /dev/null @@ -1,378 +0,0 @@ -use std::fmt::{self, Display, Formatter}; - -use crate::exec::Softness; -use crate::layout::{Expansion, Fill, NodeBackground, NodeFixed, NodeSpacing, NodeStack}; -use crate::paper::{Paper, PaperClass}; -use crate::prelude::*; - -/// `align`: Align content along the layouting axes. -/// -/// Which axis an alignment should apply to (main or cross) is inferred from -/// either the argument itself (for anything other than `center`) or from the -/// second argument if present, defaulting to the cross axis for a single -/// `center` alignment. -/// -/// # Positional arguments -/// - Alignments: variadic, of type `alignment`. -/// -/// # Named arguments -/// - Horizontal alignment: `horizontal`, of type `alignment`. -/// - Vertical alignment: `vertical`, of type `alignment`. -/// -/// # Relevant types and constants -/// - Type `alignment` -/// - `left` -/// - `right` -/// - `top` -/// - `bottom` -/// - `center` -pub fn align(ctx: &mut EvalContext, args: &mut ValueArgs) -> Value { - let first = args.find(ctx); - let second = args.find(ctx); - let hor = args.get(ctx, "horizontal"); - let ver = args.get(ctx, "vertical"); - let body = args.find::(ctx); - - Value::template("align", move |ctx| { - let snapshot = ctx.state.clone(); - - let mut had = Gen::uniform(false); - let mut had_center = false; - - // Infer the axes alignments belong to. - for (axis, Spanned { v: arg, span }) in first - .into_iter() - .chain(second.into_iter()) - .map(|arg: Spanned| (arg.v.axis(), arg)) - .chain(hor.into_iter().map(|arg| (Some(SpecAxis::Horizontal), arg))) - .chain(ver.into_iter().map(|arg| (Some(SpecAxis::Vertical), arg))) - { - // Check whether we know which axis this alignment belongs to. - if let Some(axis) = axis { - // We know the axis. - let gen_axis = axis.switch(ctx.state.dirs); - let gen_align = arg.switch(ctx.state.dirs); - - if arg.axis().map_or(false, |a| a != axis) { - ctx.diag(error!(span, "invalid alignment for {} axis", axis)); - } else if had.get(gen_axis) { - ctx.diag(error!(span, "duplicate alignment for {} axis", axis)); - } else { - *ctx.state.align.get_mut(gen_axis) = gen_align; - *had.get_mut(gen_axis) = true; - } - } else { - // We don't know the axis: This has to be a `center` alignment for a - // positional argument. - debug_assert_eq!(arg, Alignment::Center); - - if had.main && had.cross { - ctx.diag(error!(span, "duplicate alignment")); - } else if had_center { - // Both this and the previous one are unspecified `center` - // alignments. Both axes should be centered. - ctx.state.align.main = Align::Center; - ctx.state.align.cross = Align::Center; - had = Gen::uniform(true); - } else { - had_center = true; - } - } - - // If we we know the other alignment, we can handle the unspecified - // `center` alignment. - if had_center && (had.main || had.cross) { - if had.main { - ctx.state.align.cross = Align::Center; - had.cross = true; - } else { - ctx.state.align.main = Align::Center; - had.main = true; - } - had_center = false; - } - } - - // If `had_center` wasn't flushed by now, it's the only argument and then we - // default to applying it to the cross axis. - if had_center { - ctx.state.align.cross = Align::Center; - } - - if ctx.state.align.main != snapshot.align.main { - ctx.end_par_group(); - ctx.start_par_group(); - } - - if let Some(body) = &body { - body.exec(ctx); - ctx.state = snapshot; - } - }) -} - -/// An alignment argument. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)] -pub(crate) enum Alignment { - Left, - Center, - Right, - Top, - Bottom, -} - -impl Alignment { - /// The specific axis this alignment refers to. - fn axis(self) -> Option { - match self { - Self::Left => Some(SpecAxis::Horizontal), - Self::Right => Some(SpecAxis::Horizontal), - Self::Top => Some(SpecAxis::Vertical), - Self::Bottom => Some(SpecAxis::Vertical), - Self::Center => None, - } - } -} - -impl Switch for Alignment { - type Other = Align; - - fn switch(self, dirs: LayoutDirs) -> Self::Other { - let get = |dir: Dir, at_positive_start| { - if dir.is_positive() == at_positive_start { - Align::Start - } else { - Align::End - } - }; - - let dirs = dirs.switch(dirs); - match self { - Self::Left => get(dirs.horizontal, true), - Self::Right => get(dirs.horizontal, false), - Self::Top => get(dirs.vertical, true), - Self::Bottom => get(dirs.vertical, false), - Self::Center => Align::Center, - } - } -} - -typify! { - Alignment: "alignment", -} - -impl Display for Alignment { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - f.pad(match self { - Self::Left => "left", - Self::Center => "center", - Self::Right => "right", - Self::Top => "top", - Self::Bottom => "bottom", - }) - } -} - -/// `box`: Layout content into a box. -/// -/// # Named arguments -/// - Width of the box: `width`, of type `linear` relative to parent width. -/// - Height of the box: `height`, of type `linear` relative to parent height. -/// - Main layouting direction: `main-dir`, of type `direction`. -/// - Cross layouting direction: `cross-dir`, of type `direction`. -/// - Background color of the box: `color`, of type `color`. -/// -/// # Relevant types and constants -/// - Type `direction` -/// - `ltr` (left to right) -/// - `rtl` (right to left) -/// - `ttb` (top to bottom) -/// - `btt` (bottom to top) -pub fn box_(ctx: &mut EvalContext, args: &mut ValueArgs) -> Value { - let width = args.get(ctx, "width"); - let height = args.get(ctx, "height"); - let main = args.get(ctx, "main-dir"); - let cross = args.get(ctx, "cross-dir"); - let color = args.get(ctx, "color"); - let body = args.find::(ctx); - - Value::template("box", move |ctx| { - let snapshot = ctx.state.clone(); - - ctx.set_dirs(Gen::new(main, cross)); - let dirs = ctx.state.dirs; - let align = ctx.state.align; - - ctx.start_content_group(); - if let Some(body) = &body { - body.exec(ctx); - } - let children = ctx.end_content_group(); - - let fill_if = |c| if c { Expansion::Fill } else { Expansion::Fit }; - let expand = Spec::new(fill_if(width.is_some()), fill_if(height.is_some())); - let fixed = NodeFixed { - width, - height, - child: NodeStack { dirs, align, expand, children }.into(), - }; - - if let Some(color) = color { - ctx.push(NodeBackground { - fill: Fill::Color(color), - child: fixed.into(), - }); - } else { - ctx.push(fixed); - } - - ctx.state = snapshot; - }) -} - -typify! { - Dir: "direction" -} - -/// `h`: Add horizontal spacing. -/// -/// # Positional arguments -/// - Amount of spacing: of type `linear` relative to current font size. -pub fn h(ctx: &mut EvalContext, args: &mut ValueArgs) -> Value { - spacing(ctx, args, SpecAxis::Horizontal) -} - -/// `v`: Add vertical spacing. -/// -/// # Positional arguments -/// - Amount of spacing: of type `linear` relative to current font size. -pub fn v(ctx: &mut EvalContext, args: &mut ValueArgs) -> Value { - spacing(ctx, args, SpecAxis::Vertical) -} - -/// Apply spacing along a specific axis. -fn spacing(ctx: &mut EvalContext, args: &mut ValueArgs, axis: SpecAxis) -> Value { - let spacing: Option = args.require(ctx, "spacing"); - - Value::template("spacing", move |ctx| { - if let Some(linear) = spacing { - let amount = linear.resolve(ctx.state.font.font_size()); - let spacing = NodeSpacing { amount, softness: Softness::Hard }; - if axis == ctx.state.dirs.main.axis() { - ctx.end_par_group(); - ctx.push(spacing); - ctx.start_par_group(); - } else { - ctx.push(spacing); - } - } - }) -} - -/// `page`: Configure pages. -/// -/// # Positional arguments -/// - Paper name: optional, of type `string`, see [here](crate::paper) for a -/// full list of all paper names. -/// -/// # Named arguments -/// - Width of the page: `width`, of type `length`. -/// - Height of the page: `height`, of type `length`. -/// - Margins for all sides: `margins`, of type `linear` relative to sides. -/// - Left margin: `left`, of type `linear` relative to width. -/// - Right margin: `right`, of type `linear` relative to width. -/// - Top margin: `top`, of type `linear` relative to height. -/// - Bottom margin: `bottom`, of type `linear` relative to height. -/// - Flip width and height: `flip`, of type `bool`. -/// - Main layouting direction: `main-dir`, of type `direction`. -/// - Cross layouting direction: `cross-dir`, of type `direction`. -pub fn page(ctx: &mut EvalContext, args: &mut ValueArgs) -> Value { - let paper = args.find::>(ctx).and_then(|name| { - Paper::from_name(&name.v).or_else(|| { - ctx.diag(error!(name.span, "invalid paper name")); - None - }) - }); - - let width = args.get(ctx, "width"); - let height = args.get(ctx, "height"); - let margins = args.get(ctx, "margins"); - let left = args.get(ctx, "left"); - let top = args.get(ctx, "top"); - let right = args.get(ctx, "right"); - let bottom = args.get(ctx, "bottom"); - let flip = args.get(ctx, "flip"); - let main = args.get(ctx, "main-dir"); - let cross = args.get(ctx, "cross-dir"); - let body = args.find::(ctx); - - Value::template("page", move |ctx| { - let snapshot = ctx.state.clone(); - - if let Some(paper) = paper { - ctx.state.page.class = paper.class; - ctx.state.page.size = paper.size(); - ctx.state.page.expand = Spec::uniform(Expansion::Fill); - } - - if let Some(width) = width { - ctx.state.page.class = PaperClass::Custom; - ctx.state.page.size.width = width; - ctx.state.page.expand.horizontal = Expansion::Fill; - } - - if let Some(height) = height { - ctx.state.page.class = PaperClass::Custom; - ctx.state.page.size.height = height; - ctx.state.page.expand.vertical = Expansion::Fill; - } - - if let Some(margins) = margins { - ctx.state.page.margins = Sides::uniform(Some(margins)); - } - - if let Some(left) = left { - ctx.state.page.margins.left = Some(left); - } - - if let Some(top) = top { - ctx.state.page.margins.top = Some(top); - } - - if let Some(right) = right { - ctx.state.page.margins.right = Some(right); - } - - if let Some(bottom) = bottom { - ctx.state.page.margins.bottom = Some(bottom); - } - - if flip.unwrap_or(false) { - let page = &mut ctx.state.page; - std::mem::swap(&mut page.size.width, &mut page.size.height); - std::mem::swap(&mut page.expand.horizontal, &mut page.expand.vertical); - } - - ctx.set_dirs(Gen::new(main, cross)); - - let mut softness = ctx.end_page_group(|_| false); - if let Some(body) = &body { - // TODO: Restrict body to a single page? - ctx.start_page_group(Softness::Hard); - body.exec(ctx); - ctx.end_page_group(|s| s == Softness::Hard); - softness = Softness::Soft; - ctx.state = snapshot; - } - - ctx.start_page_group(softness); - }) -} - -/// `pagebreak`: Start a new page. -pub fn pagebreak(_: &mut EvalContext, _: &mut ValueArgs) -> Value { - Value::template("pagebreak", move |ctx| { - ctx.end_page_group(|_| true); - ctx.start_page_group(Softness::Hard); - }) -} diff --git a/src/library/mod.rs b/src/library/mod.rs index d34b338ca..f846e6ee5 100644 --- a/src/library/mod.rs +++ b/src/library/mod.rs @@ -3,20 +3,30 @@ //! Call [`new`] to obtain a [`Scope`] containing all standard library //! definitions. -mod extend; -mod insert; -mod layout; -mod style; +mod align; +mod base; +mod font; +mod image; +mod page; +mod shapes; +mod spacing; -pub use extend::*; -pub use insert::*; -pub use layout::*; -pub use style::*; +pub use self::image::*; +pub use align::*; +pub use base::*; +pub use font::*; +pub use page::*; +pub use shapes::*; +pub use spacing::*; + +use std::fmt::{self, Display, Formatter}; use fontdock::{FontStretch, FontStyle, FontWeight}; use crate::eval::{Scope, ValueAny, ValueFunc}; -use crate::geom::Dir; +use crate::exec::Softness; +use crate::layout::*; +use crate::prelude::*; /// Construct a scope containing all standard library definitions. pub fn new() -> Scope { @@ -80,3 +90,7 @@ pub fn new() -> Scope { std } + +typify! { + Dir: "direction" +} diff --git a/src/library/page.rs b/src/library/page.rs new file mode 100644 index 000000000..1fdf4d3f4 --- /dev/null +++ b/src/library/page.rs @@ -0,0 +1,111 @@ +use super::*; +use crate::paper::{Paper, PaperClass}; + +/// `page`: Configure pages. +/// +/// # Positional arguments +/// - Paper name: optional, of type `string`, see [here](crate::paper) for a +/// full list of all paper names. +/// - Body: optional, of type `template`. +/// +/// # Named arguments +/// - Width of the page: `width`, of type `length`. +/// - Height of the page: `height`, of type `length`. +/// - Margins for all sides: `margins`, of type `linear` relative to sides. +/// - Left margin: `left`, of type `linear` relative to width. +/// - Right margin: `right`, of type `linear` relative to width. +/// - Top margin: `top`, of type `linear` relative to height. +/// - Bottom margin: `bottom`, of type `linear` relative to height. +/// - Flip width and height: `flip`, of type `bool`. +/// - Main layouting direction: `main-dir`, of type `direction`. +/// - Cross layouting direction: `cross-dir`, of type `direction`. +pub fn page(ctx: &mut EvalContext, args: &mut ValueArgs) -> Value { + let paper = args.find::>(ctx).and_then(|name| { + Paper::from_name(&name.v).or_else(|| { + ctx.diag(error!(name.span, "invalid paper name")); + None + }) + }); + + let width = args.get(ctx, "width"); + let height = args.get(ctx, "height"); + let margins = args.get(ctx, "margins"); + let left = args.get(ctx, "left"); + let top = args.get(ctx, "top"); + let right = args.get(ctx, "right"); + let bottom = args.get(ctx, "bottom"); + let flip = args.get(ctx, "flip"); + let main = args.get(ctx, "main-dir"); + let cross = args.get(ctx, "cross-dir"); + let body = args.find::(ctx); + + Value::template("page", move |ctx| { + let snapshot = ctx.state.clone(); + + if let Some(paper) = paper { + ctx.state.page.class = paper.class; + ctx.state.page.size = paper.size(); + ctx.state.page.expand = Spec::uniform(Expansion::Fill); + } + + if let Some(width) = width { + ctx.state.page.class = PaperClass::Custom; + ctx.state.page.size.width = width; + ctx.state.page.expand.horizontal = Expansion::Fill; + } + + if let Some(height) = height { + ctx.state.page.class = PaperClass::Custom; + ctx.state.page.size.height = height; + ctx.state.page.expand.vertical = Expansion::Fill; + } + + if let Some(margins) = margins { + ctx.state.page.margins = Sides::uniform(Some(margins)); + } + + if let Some(left) = left { + ctx.state.page.margins.left = Some(left); + } + + if let Some(top) = top { + ctx.state.page.margins.top = Some(top); + } + + if let Some(right) = right { + ctx.state.page.margins.right = Some(right); + } + + if let Some(bottom) = bottom { + ctx.state.page.margins.bottom = Some(bottom); + } + + if flip.unwrap_or(false) { + let page = &mut ctx.state.page; + std::mem::swap(&mut page.size.width, &mut page.size.height); + std::mem::swap(&mut page.expand.horizontal, &mut page.expand.vertical); + } + + ctx.set_dirs(Gen::new(main, cross)); + + let mut softness = ctx.end_page_group(|_| false); + if let Some(body) = &body { + // TODO: Restrict body to a single page? + ctx.start_page_group(Softness::Hard); + body.exec(ctx); + ctx.end_page_group(|s| s == Softness::Hard); + softness = Softness::Soft; + ctx.state = snapshot; + } + + ctx.start_page_group(softness); + }) +} + +/// `pagebreak`: Start a new page. +pub fn pagebreak(_: &mut EvalContext, _: &mut ValueArgs) -> Value { + Value::template("pagebreak", move |ctx| { + ctx.end_page_group(|_| true); + ctx.start_page_group(Softness::Hard); + }) +} diff --git a/src/library/shapes.rs b/src/library/shapes.rs new file mode 100644 index 000000000..f9685fceb --- /dev/null +++ b/src/library/shapes.rs @@ -0,0 +1,61 @@ +use super::*; + +/// `box`: Create a rectangular box. +/// +/// # Positional arguments +/// - Body: optional, of type `template`. +/// +/// # Named arguments +/// - Width of the box: `width`, of type `linear` relative to parent width. +/// - Height of the box: `height`, of type `linear` relative to parent height. +/// - Main layouting direction: `main-dir`, of type `direction`. +/// - Cross layouting direction: `cross-dir`, of type `direction`. +/// - Background color of the box: `color`, of type `color`. +/// +/// # Relevant types and constants +/// - Type `direction` +/// - `ltr` (left to right) +/// - `rtl` (right to left) +/// - `ttb` (top to bottom) +/// - `btt` (bottom to top) +pub fn box_(ctx: &mut EvalContext, args: &mut ValueArgs) -> Value { + let width = args.get(ctx, "width"); + let height = args.get(ctx, "height"); + let main = args.get(ctx, "main-dir"); + let cross = args.get(ctx, "cross-dir"); + let color = args.get(ctx, "color"); + let body = args.find::(ctx); + + Value::template("box", move |ctx| { + let snapshot = ctx.state.clone(); + + ctx.set_dirs(Gen::new(main, cross)); + let dirs = ctx.state.dirs; + let align = ctx.state.align; + + ctx.start_content_group(); + if let Some(body) = &body { + body.exec(ctx); + } + let children = ctx.end_content_group(); + + let fill_if = |c| if c { Expansion::Fill } else { Expansion::Fit }; + let expand = Spec::new(fill_if(width.is_some()), fill_if(height.is_some())); + let fixed = NodeFixed { + width, + height, + child: NodeStack { dirs, align, expand, children }.into(), + }; + + if let Some(color) = color { + ctx.push(NodeBackground { + fill: Fill::Color(color), + child: fixed.into(), + }); + } else { + ctx.push(fixed); + } + + ctx.state = snapshot; + }) +} diff --git a/src/library/spacing.rs b/src/library/spacing.rs new file mode 100644 index 000000000..624890e93 --- /dev/null +++ b/src/library/spacing.rs @@ -0,0 +1,36 @@ +use super::*; + +/// `h`: Add horizontal spacing. +/// +/// # Positional arguments +/// - Amount of spacing: of type `linear` relative to current font size. +pub fn h(ctx: &mut EvalContext, args: &mut ValueArgs) -> Value { + spacing(ctx, args, SpecAxis::Horizontal) +} + +/// `v`: Add vertical spacing. +/// +/// # Positional arguments +/// - Amount of spacing: of type `linear` relative to current font size. +pub fn v(ctx: &mut EvalContext, args: &mut ValueArgs) -> Value { + spacing(ctx, args, SpecAxis::Vertical) +} + +/// Apply spacing along a specific axis. +fn spacing(ctx: &mut EvalContext, args: &mut ValueArgs, axis: SpecAxis) -> Value { + let spacing: Option = args.require(ctx, "spacing"); + + Value::template("spacing", move |ctx| { + if let Some(linear) = spacing { + let amount = linear.resolve(ctx.state.font.font_size()); + let spacing = NodeSpacing { amount, softness: Softness::Hard }; + if axis == ctx.state.dirs.main.axis() { + ctx.end_par_group(); + ctx.push(spacing); + ctx.start_par_group(); + } else { + ctx.push(spacing); + } + } + }) +} diff --git a/tests/ref/library/box.png b/tests/ref/library/shapes.png similarity index 100% rename from tests/ref/library/box.png rename to tests/ref/library/shapes.png diff --git a/tests/ref/library/hv.png b/tests/ref/library/spacing.png similarity index 100% rename from tests/ref/library/hv.png rename to tests/ref/library/spacing.png diff --git a/tests/typ/library/base.typ b/tests/typ/library/base.typ new file mode 100644 index 000000000..000a80ea3 --- /dev/null +++ b/tests/typ/library/base.typ @@ -0,0 +1,25 @@ +// Test the base functions. +// Ref: false + +--- +#test(type("hi"), "string") +#test(repr([Hi #box[there]]), "[Hi []]") + +--- +// Check the output. +#test(rgb(0.0, 0.3, 0.7), #004db3) + +// Alpha channel. +#test(rgb(1.0, 0.0, 0.0, 0.5), #ff000080) + +// Warning: 2:11-2:14 should be between 0.0 and 1.0 +// Warning: 1:16-1:20 should be between 0.0 and 1.0 +#test(rgb(-30, 15.5, 0.5), #00ff80) + +// Error: 11-15 missing argument: blue component +#test(rgb(0, 1), #00ff00) + +// Error: 3:11-3:11 missing argument: red component +// Error: 2:11-2:11 missing argument: green component +// Error: 1:11-1:11 missing argument: blue component +#test(rgb(), #000000) diff --git a/tests/typ/library/font.typ b/tests/typ/library/font.typ index 157557fed..566f2e877 100644 --- a/tests/typ/library/font.typ +++ b/tests/typ/library/font.typ @@ -1,4 +1,4 @@ -// Test the font function. +// Test the `font` function. --- // Test configuring font properties. diff --git a/tests/typ/library/image.typ b/tests/typ/library/image.typ index d3e6c78da..a5737f4fe 100644 --- a/tests/typ/library/image.typ +++ b/tests/typ/library/image.typ @@ -1,4 +1,4 @@ -// Test the image function. +// Test the `image` function. --- // Test loading different image formats. diff --git a/tests/typ/library/page.typ b/tests/typ/library/page.typ index 1f16f009d..ac222e043 100644 --- a/tests/typ/library/page.typ +++ b/tests/typ/library/page.typ @@ -1,4 +1,4 @@ -// Test the page function. +// Test the `page` function. --- // Test configuring page sizes and margins. diff --git a/tests/typ/library/pagebreak.typ b/tests/typ/library/pagebreak.typ index 22af95126..37a544cfa 100644 --- a/tests/typ/library/pagebreak.typ +++ b/tests/typ/library/pagebreak.typ @@ -1,4 +1,4 @@ -// Test the pagebreak function. +// Test the `pagebreak` function. --- First of two diff --git a/tests/typ/library/rgb.typ b/tests/typ/library/rgb.typ deleted file mode 100644 index 62a1627cf..000000000 --- a/tests/typ/library/rgb.typ +++ /dev/null @@ -1,23 +0,0 @@ -// Test the rgb function. -// Ref: false - ---- -{ - // Check the output. - test(rgb(0.0, 0.3, 0.7), #004db3) - - // Alpha channel. - test(rgb(1.0, 0.0, 0.0, 0.5), #ff000080) - - // Warning: 2:14-2:17 should be between 0.0 and 1.0 - // Warning: 1:19-1:23 should be between 0.0 and 1.0 - test(rgb(-30, 15.5, 0.5), #00ff80) - - // Error: 14-18 missing argument: blue component - test(rgb(0, 1), #00ff00) - - // Error: 3:14-3:14 missing argument: red component - // Error: 2:14-2:14 missing argument: green component - // Error: 1:14-1:14 missing argument: blue component - test(rgb(), #000000) -} diff --git a/tests/typ/library/box.typ b/tests/typ/library/shapes.typ similarity index 95% rename from tests/typ/library/box.typ rename to tests/typ/library/shapes.typ index 7ce568592..ceaa0148c 100644 --- a/tests/typ/library/box.typ +++ b/tests/typ/library/shapes.typ @@ -1,4 +1,4 @@ -// Test the box function. +// Test shapes. --- #page("a8", flip: true) diff --git a/tests/typ/library/hv.typ b/tests/typ/library/spacing.typ similarity index 92% rename from tests/typ/library/hv.typ rename to tests/typ/library/spacing.typ index 09132a60b..a34840c67 100644 --- a/tests/typ/library/hv.typ +++ b/tests/typ/library/spacing.typ @@ -1,4 +1,4 @@ -// Test the h and v functions. +// Test the `h` and `v` functions. --- // Ends paragraphs.