From 285c2f617b74e182be69decea46bbd0afdb0f604 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Sat, 26 Jun 2021 13:06:37 +0200 Subject: [PATCH] Cleanse library - Remove doc-comments for Typst functions from library - Reduce number of library source files --- src/{library => layout}/image.rs | 56 +---- src/layout/mod.rs | 2 + src/library/align.rs | 128 ---------- src/library/decorations.rs | 86 ------- src/library/{shapes.rs => elements.rs} | 82 +++---- src/library/grid.rs | 89 ------- src/library/lang.rs | 44 ---- src/library/layout.rs | 316 +++++++++++++++++++++++++ src/library/math.rs | 57 ----- src/library/mod.rs | 82 +++---- src/library/pad.rs | 37 --- src/library/page.rs | 106 --------- src/library/par.rs | 32 --- src/library/spacing.rs | 39 --- src/library/stack.rs | 40 ---- src/library/{font.rs => text.rs} | 146 ++++++++---- src/library/{basic.rs => utility.rs} | 63 +++-- src/syntax/node.rs | 62 ----- 18 files changed, 532 insertions(+), 935 deletions(-) rename src/{library => layout}/image.rs (57%) delete mode 100644 src/library/align.rs delete mode 100644 src/library/decorations.rs rename src/library/{shapes.rs => elements.rs} (62%) delete mode 100644 src/library/grid.rs delete mode 100644 src/library/lang.rs create mode 100644 src/library/layout.rs delete mode 100644 src/library/math.rs delete mode 100644 src/library/pad.rs delete mode 100644 src/library/page.rs delete mode 100644 src/library/par.rs delete mode 100644 src/library/spacing.rs delete mode 100644 src/library/stack.rs rename src/library/{font.rs => text.rs} (58%) rename src/library/{basic.rs => utility.rs} (60%) diff --git a/src/library/image.rs b/src/layout/image.rs similarity index 57% rename from src/library/image.rs rename to src/layout/image.rs index 54fa54c9e..9ba8cd822 100644 --- a/src/library/image.rs +++ b/src/layout/image.rs @@ -1,62 +1,23 @@ -use ::image::GenericImageView; - use super::*; use crate::image::ImageId; -use crate::layout::{ - AnyNode, Constrained, Constraints, Element, Frame, Layout, LayoutContext, Regions, -}; -/// `image`: An image. -/// -/// Supports PNG and JPEG files. -/// -/// # Positional parameters -/// - Path to image file: of type `string`. -/// -/// # Return value -/// A template that inserts an image. -pub fn image(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { - let path = args.expect::>(ctx, "path to image file"); - let width = args.named(ctx, "width"); - let height = args.named(ctx, "height"); - - let mut node = None; - if let Some(path) = &path { - if let Some((resolved, _)) = ctx.resolve(&path.v, path.span) { - if let Some(id) = ctx.cache.image.load(ctx.loader, &resolved) { - let img = ctx.cache.image.get(id); - let dimensions = img.buf.dimensions(); - node = Some(ImageNode { id, dimensions, width, height }); - } else { - ctx.diag(error!(path.span, "failed to load image")); - } - } - } - - Value::template("image", move |ctx| { - if let Some(node) = node { - ctx.push_into_par(node); - } - }) -} +use ::image::GenericImageView; /// An image node. #[derive(Debug, Copy, Clone, PartialEq, Hash)] -struct ImageNode { +pub struct ImageNode { /// The id of the image file. - id: ImageId, - /// The pixel dimensions of the image. - dimensions: (u32, u32), + pub id: ImageId, /// The fixed width, if any. - width: Option, + pub width: Option, /// The fixed height, if any. - height: Option, + pub height: Option, } impl Layout for ImageNode { fn layout( &self, - _: &mut LayoutContext, + ctx: &mut LayoutContext, regions: &Regions, ) -> Vec>> { let Regions { current, base, .. } = regions; @@ -66,8 +27,9 @@ impl Layout for ImageNode { let width = self.width.map(|w| w.resolve(base.width)); let height = self.height.map(|w| w.resolve(base.height)); - let pixel_width = self.dimensions.0 as f64; - let pixel_height = self.dimensions.1 as f64; + let dimensions = ctx.cache.image.get(self.id).buf.dimensions(); + let pixel_width = dimensions.0 as f64; + let pixel_height = dimensions.1 as f64; let pixel_ratio = pixel_width / pixel_height; let size = match (width, height) { diff --git a/src/layout/mod.rs b/src/layout/mod.rs index f1ae3e2af..10c30f419 100644 --- a/src/layout/mod.rs +++ b/src/layout/mod.rs @@ -4,12 +4,14 @@ mod background; mod fixed; mod frame; mod grid; +mod image; mod incremental; mod pad; mod par; mod shaping; mod stack; +pub use self::image::*; pub use background::*; pub use fixed::*; pub use frame::*; diff --git a/src/library/align.rs b/src/library/align.rs deleted file mode 100644 index c0ed0416a..000000000 --- a/src/library/align.rs +++ /dev/null @@ -1,128 +0,0 @@ -use super::*; - -/// `align`: Configure the alignment along the layouting axes. -/// -/// # Positional parameters -/// - Alignments: variadic, of type `alignment`. -/// - Body: optional, of type `template`. -/// -/// # Named parameters -/// - Horizontal alignment: `horizontal`, of type `alignment`. -/// - Vertical alignment: `vertical`, of type `alignment`. -/// -/// # Return value -/// A template that changes the alignment along the layouting axes. The effect -/// is scoped to the body if present. -/// -/// # Relevant types and constants -/// - Type `alignment` -/// - `start` -/// - `center` -/// - `end` -/// - `left` -/// - `right` -/// - `top` -/// - `bottom` -pub fn align(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { - let first = args.eat::(ctx); - let second = args.eat::(ctx); - let mut horizontal = args.named::(ctx, "horizontal"); - let mut vertical = args.named::(ctx, "vertical"); - let body = args.eat::(ctx); - - for value in first.into_iter().chain(second) { - match value.axis() { - Some(SpecAxis::Horizontal) | None if horizontal.is_none() => { - horizontal = Some(value); - } - Some(SpecAxis::Vertical) | None if vertical.is_none() => { - vertical = Some(value); - } - _ => {} - } - } - - Value::template("align", move |ctx| { - let snapshot = ctx.state.clone(); - - if let Some(horizontal) = horizontal { - ctx.state.aligns.cross = horizontal.to_align(ctx.state.lang.dir); - } - - if let Some(vertical) = vertical { - ctx.state.aligns.main = vertical.to_align(Dir::TTB); - if ctx.state.aligns.main != snapshot.aligns.main { - ctx.parbreak(); - } - } - - if let Some(body) = &body { - body.exec(ctx); - ctx.state = snapshot; - } - }) -} - -/// An alignment specifier passed to `align`. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)] -pub(super) enum AlignValue { - Start, - Center, - End, - Left, - Right, - Top, - Bottom, -} - -impl AlignValue { - fn axis(self) -> Option { - match self { - Self::Start => None, - Self::Center => None, - Self::End => None, - Self::Left => Some(SpecAxis::Horizontal), - Self::Right => Some(SpecAxis::Horizontal), - Self::Top => Some(SpecAxis::Vertical), - Self::Bottom => Some(SpecAxis::Vertical), - } - } - - fn to_align(self, dir: Dir) -> Align { - let side = |is_at_positive_start| { - if dir.is_positive() == is_at_positive_start { - Align::Start - } else { - Align::End - } - }; - - match self { - Self::Start => Align::Start, - Self::Center => Align::Center, - Self::End => Align::End, - Self::Left => side(true), - Self::Right => side(false), - Self::Top => side(true), - Self::Bottom => side(false), - } - } -} - -impl Display for AlignValue { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - f.pad(match self { - Self::Start => "start", - Self::Center => "center", - Self::End => "end", - Self::Left => "left", - Self::Right => "right", - Self::Top => "top", - Self::Bottom => "bottom", - }) - } -} - -castable! { - AlignValue: "alignment", -} diff --git a/src/library/decorations.rs b/src/library/decorations.rs deleted file mode 100644 index b935f7073..000000000 --- a/src/library/decorations.rs +++ /dev/null @@ -1,86 +0,0 @@ -use crate::exec::{FontState, LineState}; -use crate::layout::Fill; - -use super::*; - -/// `strike`: Enable striken-through text. -/// -/// # Named parameters -/// - Color: `color`, of type `color`. -/// - Baseline offset: `position`, of type `linear`. -/// - Strength: `strength`, of type `linear`. -/// - Extent that is applied on either end of the line: `extent`, of type -/// `linear`. -/// -/// # Return value -/// A template that enables striken-through text. The effect is scoped to the -/// body if present. -pub fn strike(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { - line_impl("strike", ctx, args, |font| &mut font.strikethrough) -} - -/// `underline`: Enable underlined text. -/// -/// # Named parameters -/// - Color: `color`, of type `color`. -/// - Baseline offset: `position`, of type `linear`. -/// - Strength: `strength`, of type `linear`. -/// - Extent that is applied on either end of the line: `extent`, of type -/// `linear`. -/// -/// # Return value -/// A template that enables underlined text. The effect is scoped to the body if -/// present. -pub fn underline(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { - line_impl("underline", ctx, args, |font| &mut font.underline) -} - -/// `overline`: Add an overline above text. -/// -/// # Named parameters -/// - Color: `color`, of type `color`. -/// - Baseline offset: `position`, of type `linear`. -/// - Strength: `strength`, of type `linear`. -/// - Extent that is applied on either end of the line: `extent`, of type -/// `linear`. -/// -/// # Return value -/// A template that adds an overline above text. The effect is scoped to the -/// body if present. -pub fn overline(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { - line_impl("overline", ctx, args, |font| &mut font.overline) -} - -fn line_impl( - name: &str, - ctx: &mut EvalContext, - args: &mut FuncArgs, - substate: fn(&mut FontState) -> &mut Option>, -) -> Value { - let color = args.named(ctx, "color"); - let position = args.named(ctx, "position"); - let strength = args.named::(ctx, "strength"); - let extent = args.named(ctx, "extent").unwrap_or_default(); - let body = args.eat::(ctx); - - // Suppress any existing strikethrough if strength is explicitly zero. - let state = strength.map_or(true, |s| !s.is_zero()).then(|| { - Rc::new(LineState { - strength, - position, - extent, - fill: color.map(Fill::Color), - }) - }); - - Value::template(name, move |ctx| { - let snapshot = ctx.state.clone(); - - *substate(ctx.state.font_mut()) = state.clone(); - - if let Some(body) = &body { - body.exec(ctx); - ctx.state = snapshot; - } - }) -} diff --git a/src/library/shapes.rs b/src/library/elements.rs similarity index 62% rename from src/library/shapes.rs rename to src/library/elements.rs index 213e2f4ab..b1b5c1f9e 100644 --- a/src/library/shapes.rs +++ b/src/library/elements.rs @@ -4,20 +4,35 @@ use decorum::N64; use super::*; use crate::color::Color; -use crate::layout::{BackgroundNode, BackgroundShape, Fill, FixedNode, PadNode}; +use crate::layout::{ + BackgroundNode, BackgroundShape, Fill, FixedNode, ImageNode, PadNode, +}; + +/// `image`: An image. +pub fn image(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { + let path = args.expect::>(ctx, "path to image file"); + let width = args.named(ctx, "width"); + let height = args.named(ctx, "height"); + + let mut node = None; + if let Some(path) = &path { + if let Some((resolved, _)) = ctx.resolve(&path.v, path.span) { + if let Some(id) = ctx.cache.image.load(ctx.loader, &resolved) { + node = Some(ImageNode { id, width, height }); + } else { + ctx.diag(error!(path.span, "failed to load image")); + } + } + } + + Value::template("image", move |ctx| { + if let Some(node) = node { + ctx.push_into_par(node); + } + }) +} /// `rect`: A rectangle with optional content. -/// -/// # Positional parameters -/// - Body: optional, of type `template`. -/// -/// # Named parameters -/// - Width: `width`, of type `linear` relative to parent width. -/// - Height: `height`, of type `linear` relative to parent height. -/// - Fill color: `fill`, of type `color`. -/// -/// # Return value -/// A template that inserts a rectangle and sets the body into it. pub fn rect(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { let width = args.named(ctx, "width"); let height = args.named(ctx, "height"); @@ -27,22 +42,6 @@ pub fn rect(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { } /// `square`: A square with optional content. -/// -/// # Positional parameters -/// - Body: optional, of type `template`. -/// -/// # Named parameters -/// - Side length: `length`, of type `length`. -/// - Width: `width`, of type `linear` relative to parent width. -/// - Height: `height`, of type `linear` relative to parent height. -/// - Fill color: `fill`, of type `color`. -/// -/// Note that you can specify only one of `length`, `width` and `height`. The -/// width and height parameters exist so that you can size the square relative -/// to its parent's size, which isn't possible by setting the side length. -/// -/// # Return value -/// A template that inserts a square and sets the body into it. pub fn square(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { let length = args.named::(ctx, "length").map(Linear::from); let width = length.or_else(|| args.named(ctx, "width")); @@ -79,17 +78,6 @@ fn rect_impl( } /// `ellipse`: An ellipse with optional content. -/// -/// # Positional parameters -/// - Body: optional, of type `template`. -/// -/// # Named parameters -/// - Width: `width`, of type `linear` relative to parent width. -/// - Height: `height`, of type `linear` relative to parent height. -/// - Fill color: `fill`, of type `color`. -/// -/// # Return value -/// A template that inserts an ellipse and sets the body into it. pub fn ellipse(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { let width = args.named(ctx, "width"); let height = args.named(ctx, "height"); @@ -99,22 +87,6 @@ pub fn ellipse(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { } /// `circle`: A circle with optional content. -/// -/// # Positional parameters -/// - Body: optional, of type `template`. -/// -/// # Named parameters -/// - Radius: `radius`, of type `length`. -/// - Width: `width`, of type `linear` relative to parent width. -/// - Height: `height`, of type `linear` relative to parent height. -/// - Fill color: `fill`, of type `color`. -/// -/// Note that you can specify only one of `radius`, `width` and `height`. The -/// width and height parameters exist so that you can size the circle relative -/// to its parent's size, which isn't possible by setting the radius. -/// -/// # Return value -/// A template that inserts a circle and sets the body into it. pub fn circle(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { let radius = args.named::(ctx, "radius").map(|r| 2.0 * Linear::from(r)); let width = radius.or_else(|| args.named(ctx, "width")); diff --git a/src/library/grid.rs b/src/library/grid.rs deleted file mode 100644 index c2e765141..000000000 --- a/src/library/grid.rs +++ /dev/null @@ -1,89 +0,0 @@ -use crate::layout::{GridNode, TrackSizing}; - -use super::*; - -/// `grid`: Arrange children into a grid. -/// -/// # Positional parameters -/// - Children: variadic, of type `template`. -/// -/// # Named parameters -/// - Column sizing: `columns`, of type `tracks`. -/// - Row sizing: `rows`, of type `tracks`. -/// - Gutter: `gutter`, shorthand for equal gutter everywhere, of type `length`. -/// - Gutter for rows: `gutter-rows`, of type `tracks`. -/// - Gutter for columns: `gutter-columns`, of type `tracks`. -/// - Column direction: `column-dir`, of type `direction`. -/// - Row direction: `row-dir`, of type `direction`. -/// -/// # Return value -/// A template that arranges its children along the specified grid cells. -/// -/// # Relevant types and constants -/// - Type `tracks` -/// - coerces from `array` of `track-sizing` -/// - Type `track-sizing` -/// - `auto` -// - coerces from `length` -// - coerces from `relative` -// - coerces from `linear` -// - coerces from `fractional` -/// - Type `direction` -/// - `ltr` -/// - `rtl` -/// - `ttb` -/// - `btt` -pub fn grid(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { - let columns = args.named::(ctx, "columns").unwrap_or_default(); - let rows = args.named::(ctx, "rows").unwrap_or_default(); - let gutter = args - .named::(ctx, "gutter") - .map(|v| vec![TrackSizing::Linear(v)]) - .unwrap_or_default(); - let gutter_columns = args.named::(ctx, "gutter-columns"); - let gutter_rows = args.named::(ctx, "gutter-rows"); - let column_dir = args.named(ctx, "column-dir"); - let row_dir = args.named(ctx, "row-dir"); - let children = args.all::(ctx); - - Value::template("grid", move |ctx| { - let children = children - .iter() - .map(|child| ctx.exec_template_stack(child).into()) - .collect(); - - let cross_dir = column_dir.unwrap_or(ctx.state.lang.dir); - let main_dir = row_dir.unwrap_or(cross_dir.axis().other().dir(true)); - - ctx.push_into_stack(GridNode { - dirs: Gen::new(cross_dir, main_dir), - tracks: Gen::new(columns.clone(), rows.clone()), - gutter: Gen::new( - gutter_columns.as_ref().unwrap_or(&gutter).clone(), - gutter_rows.as_ref().unwrap_or(&gutter).clone(), - ), - children, - }) - }) -} - -/// Defines size of rows and columns in a grid. -type Tracks = Vec; - -castable! { - Tracks: "array of `auto`s, linears, and fractionals", - Value::Int(count) => vec![TrackSizing::Auto; count.max(0) as usize], - Value::Array(values) => values - .into_iter() - .filter_map(|v| v.cast().ok()) - .collect(), -} - -castable! { - TrackSizing: "`auto`, linear, or fractional", - Value::Auto => TrackSizing::Auto, - Value::Length(v) => TrackSizing::Linear(v.into()), - Value::Relative(v) => TrackSizing::Linear(v.into()), - Value::Linear(v) => TrackSizing::Linear(v), - Value::Fractional(v) => TrackSizing::Fractional(v), -} diff --git a/src/library/lang.rs b/src/library/lang.rs deleted file mode 100644 index 7a08001af..000000000 --- a/src/library/lang.rs +++ /dev/null @@ -1,44 +0,0 @@ -use super::*; - -/// `lang`: Configure the language. -/// -/// # Positional parameters -/// - Language: of type `string`. Has to be a valid ISO 639-1 code. -/// -/// # Named parameters -/// - Text direction: `dir`, of type `direction`, must be horizontal. -/// -/// # Return value -/// A template that configures language properties. -/// -/// # Relevant types and constants -/// - Type `direction` -/// - `ltr` -/// - `rtl` -pub fn lang(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { - let iso = args.eat::(ctx).map(|s| lang_dir(&s)); - let dir = match args.named::>(ctx, "dir") { - Some(dir) if dir.v.axis() == SpecAxis::Horizontal => Some(dir.v), - Some(dir) => { - ctx.diag(error!(dir.span, "must be horizontal")); - None - } - None => None, - }; - - Value::template("lang", move |ctx| { - if let Some(dir) = dir.or(iso) { - ctx.state.lang.dir = dir; - } - - ctx.parbreak(); - }) -} - -/// The default direction for the language identified by `iso`. -fn lang_dir(iso: &str) -> Dir { - match iso.to_ascii_lowercase().as_str() { - "ar" | "he" | "fa" | "ur" | "ps" | "yi" => Dir::RTL, - "en" | "fr" | "de" | _ => Dir::LTR, - } -} diff --git a/src/library/layout.rs b/src/library/layout.rs new file mode 100644 index 000000000..cba77c726 --- /dev/null +++ b/src/library/layout.rs @@ -0,0 +1,316 @@ +use super::*; +use crate::layout::{GridNode, PadNode, StackChild, StackNode, TrackSizing}; +use crate::paper::{Paper, PaperClass}; + +/// `page`: Configure pages. +pub fn page(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { + let paper = args.eat::>(ctx).and_then(|name| { + Paper::from_name(&name.v).or_else(|| { + ctx.diag(error!(name.span, "invalid paper name")); + None + }) + }); + + let width = args.named(ctx, "width"); + let height = args.named(ctx, "height"); + let margins = args.named(ctx, "margins"); + let left = args.named(ctx, "left"); + let top = args.named(ctx, "top"); + let right = args.named(ctx, "right"); + let bottom = args.named(ctx, "bottom"); + let flip = args.named(ctx, "flip"); + let body = args.eat::(ctx); + let span = args.span; + + 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(); + } + + if let Some(width) = width { + ctx.state.page.class = PaperClass::Custom; + ctx.state.page.size.width = width; + } + + if let Some(height) = height { + ctx.state.page.class = PaperClass::Custom; + ctx.state.page.size.height = height; + } + + if let Some(margins) = margins { + ctx.state.page.margins = Sides::splat(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); + } + + ctx.pagebreak(false, true, span); + + if let Some(body) = &body { + // TODO: Restrict body to a single page? + body.exec(ctx); + ctx.state = snapshot; + ctx.pagebreak(true, false, span); + } + }) +} + +/// `pagebreak`: Start a new page. +pub fn pagebreak(_: &mut EvalContext, args: &mut FuncArgs) -> Value { + let span = args.span; + Value::template("pagebreak", move |ctx| { + ctx.pagebreak(true, true, span); + }) +} + +/// `h`: Horizontal spacing. +pub fn h(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { + spacing_impl("h", ctx, args, GenAxis::Cross) +} + +/// `v`: Vertical spacing. +pub fn v(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { + spacing_impl("v", ctx, args, GenAxis::Main) +} + +fn spacing_impl( + name: &str, + ctx: &mut EvalContext, + args: &mut FuncArgs, + axis: GenAxis, +) -> Value { + let spacing: Option = args.expect(ctx, "spacing"); + Value::template(name, move |ctx| { + if let Some(linear) = spacing { + // TODO: Should this really always be font-size relative? + let amount = linear.resolve(ctx.state.font.size); + ctx.push_spacing(axis, amount); + } + }) +} + +/// `align`: Configure the alignment along the layouting axes. +pub fn align(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { + let first = args.eat::(ctx); + let second = args.eat::(ctx); + let mut horizontal = args.named::(ctx, "horizontal"); + let mut vertical = args.named::(ctx, "vertical"); + let body = args.eat::(ctx); + + for value in first.into_iter().chain(second) { + match value.axis() { + Some(SpecAxis::Horizontal) | None if horizontal.is_none() => { + horizontal = Some(value); + } + Some(SpecAxis::Vertical) | None if vertical.is_none() => { + vertical = Some(value); + } + _ => {} + } + } + + Value::template("align", move |ctx| { + let snapshot = ctx.state.clone(); + + if let Some(horizontal) = horizontal { + ctx.state.aligns.cross = horizontal.to_align(ctx.state.lang.dir); + } + + if let Some(vertical) = vertical { + ctx.state.aligns.main = vertical.to_align(Dir::TTB); + if ctx.state.aligns.main != snapshot.aligns.main { + ctx.parbreak(); + } + } + + if let Some(body) = &body { + body.exec(ctx); + ctx.state = snapshot; + } + }) +} + +/// An alignment specifier passed to `align`. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)] +pub(super) enum AlignValue { + Start, + Center, + End, + Left, + Right, + Top, + Bottom, +} + +impl AlignValue { + fn axis(self) -> Option { + match self { + Self::Start => None, + Self::Center => None, + Self::End => None, + Self::Left => Some(SpecAxis::Horizontal), + Self::Right => Some(SpecAxis::Horizontal), + Self::Top => Some(SpecAxis::Vertical), + Self::Bottom => Some(SpecAxis::Vertical), + } + } + + fn to_align(self, dir: Dir) -> Align { + let side = |is_at_positive_start| { + if dir.is_positive() == is_at_positive_start { + Align::Start + } else { + Align::End + } + }; + + match self { + Self::Start => Align::Start, + Self::Center => Align::Center, + Self::End => Align::End, + Self::Left => side(true), + Self::Right => side(false), + Self::Top => side(true), + Self::Bottom => side(false), + } + } +} + +impl Display for AlignValue { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.pad(match self { + Self::Start => "start", + Self::Center => "center", + Self::End => "end", + Self::Left => "left", + Self::Right => "right", + Self::Top => "top", + Self::Bottom => "bottom", + }) + } +} + +castable! { + AlignValue: "alignment", +} + +/// `pad`: Pad content at the sides. +pub fn pad(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { + let all = args.eat(ctx); + let left = args.named(ctx, "left"); + let top = args.named(ctx, "top"); + let right = args.named(ctx, "right"); + let bottom = args.named(ctx, "bottom"); + let body = args.expect::(ctx, "body").unwrap_or_default(); + + let padding = Sides::new( + left.or(all).unwrap_or_default(), + top.or(all).unwrap_or_default(), + right.or(all).unwrap_or_default(), + bottom.or(all).unwrap_or_default(), + ); + + Value::template("pad", move |ctx| { + let child = ctx.exec_template_stack(&body).into(); + ctx.push_into_stack(PadNode { padding, child }); + }) +} + +/// `stack`: Stack children along an axis. +pub fn stack(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { + let dir = args.named::(ctx, "dir").unwrap_or(Dir::TTB); + let children = args.all::(ctx); + + Value::template("stack", move |ctx| { + let children = children + .iter() + .map(|child| { + let child = ctx.exec_template_stack(child).into(); + StackChild::Any(child, ctx.state.aligns) + }) + .collect(); + + ctx.push_into_stack(StackNode { + dirs: Gen::new(ctx.state.lang.dir, dir), + aspect: None, + children, + }); + }) +} + +/// `grid`: Arrange children into a grid. +pub fn grid(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { + let columns = args.named::(ctx, "columns").unwrap_or_default(); + let rows = args.named::(ctx, "rows").unwrap_or_default(); + let gutter = args + .named::(ctx, "gutter") + .map(|v| vec![TrackSizing::Linear(v)]) + .unwrap_or_default(); + let gutter_columns = args.named::(ctx, "gutter-columns"); + let gutter_rows = args.named::(ctx, "gutter-rows"); + let column_dir = args.named(ctx, "column-dir"); + let row_dir = args.named(ctx, "row-dir"); + let children = args.all::(ctx); + + Value::template("grid", move |ctx| { + let children = children + .iter() + .map(|child| ctx.exec_template_stack(child).into()) + .collect(); + + let cross_dir = column_dir.unwrap_or(ctx.state.lang.dir); + let main_dir = row_dir.unwrap_or(cross_dir.axis().other().dir(true)); + + ctx.push_into_stack(GridNode { + dirs: Gen::new(cross_dir, main_dir), + tracks: Gen::new(columns.clone(), rows.clone()), + gutter: Gen::new( + gutter_columns.as_ref().unwrap_or(&gutter).clone(), + gutter_rows.as_ref().unwrap_or(&gutter).clone(), + ), + children, + }) + }) +} + +/// Defines size of rows and columns in a grid. +type Tracks = Vec; + +castable! { + Tracks: "array of `auto`s, linears, and fractionals", + Value::Int(count) => vec![TrackSizing::Auto; count.max(0) as usize], + Value::Array(values) => values + .into_iter() + .filter_map(|v| v.cast().ok()) + .collect(), +} + +castable! { + TrackSizing: "`auto`, linear, or fractional", + Value::Auto => TrackSizing::Auto, + Value::Length(v) => TrackSizing::Linear(v.into()), + Value::Relative(v) => TrackSizing::Linear(v.into()), + Value::Linear(v) => TrackSizing::Linear(v), + Value::Fractional(v) => TrackSizing::Fractional(v), +} diff --git a/src/library/math.rs b/src/library/math.rs deleted file mode 100644 index 4afb540df..000000000 --- a/src/library/math.rs +++ /dev/null @@ -1,57 +0,0 @@ -use std::cmp::Ordering; - -use super::*; - -/// `min`: The minimum of two values. -/// -/// # Positional parameters -/// - Values: variadic, must be comparable. -/// -/// # Return value -/// The minimum of the sequence of values. For equal elements, the first one is -/// returned. -pub fn min(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { - minmax(ctx, args, Ordering::Less) -} - -/// `max`: The maximum of two values. -/// -/// # Positional parameters -/// - Values: variadic, must be comparable. -/// -/// # Return value -/// The maximum of the sequence of values. For equal elements, the first one is -/// returned. -pub fn max(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { - minmax(ctx, args, Ordering::Greater) -} - -/// Find the minimum or maximum of a sequence of values. -fn minmax(ctx: &mut EvalContext, args: &mut FuncArgs, goal: Ordering) -> Value { - let mut extremum = None; - - while let Some(value) = args.eat::(ctx) { - if let Some(prev) = &extremum { - match value.cmp(&prev) { - Some(ordering) if ordering == goal => extremum = Some(value), - Some(_) => {} - None => { - ctx.diag(error!( - args.span, - "cannot compare {} with {}", - prev.type_name(), - value.type_name(), - )); - return Value::Error; - } - } - } else { - extremum = Some(value); - } - } - - extremum.unwrap_or_else(|| { - args.expect::(ctx, "value"); - Value::Error - }) -} diff --git a/src/library/mod.rs b/src/library/mod.rs index 4911e5c86..5f0430b2f 100644 --- a/src/library/mod.rs +++ b/src/library/mod.rs @@ -3,35 +3,15 @@ //! Call [`new`] to obtain a [`Scope`] containing all standard library //! definitions. -mod align; -mod basic; -mod decorations; -mod font; -mod grid; -mod image; -mod lang; -mod math; -mod pad; -mod page; -mod par; -mod shapes; -mod spacing; -mod stack; +mod elements; +mod layout; +mod text; +mod utility; -pub use self::image::*; -pub use align::*; -pub use basic::*; -pub use decorations::*; -pub use font::*; -pub use grid::*; -pub use lang::*; -pub use math::*; -pub use pad::*; -pub use page::*; -pub use par::*; -pub use shapes::*; -pub use spacing::*; -pub use stack::*; +pub use elements::*; +pub use layout::*; +pub use text::*; +pub use utility::*; use std::fmt::{self, Display, Formatter}; use std::rc::Rc; @@ -47,32 +27,38 @@ use crate::syntax::Spanned; pub fn new() -> Scope { let mut std = Scope::new(); - // Library functions. - std.def_func("align", align); - std.def_func("circle", circle); - std.def_func("ellipse", ellipse); + // Text. std.def_func("font", font); - std.def_func("grid", grid); - std.def_func("h", h); - std.def_func("image", image); + std.def_func("par", par); std.def_func("lang", lang); - std.def_func("len", len); - std.def_func("max", max); - std.def_func("min", min); + std.def_func("strike", strike); + std.def_func("underline", underline); std.def_func("overline", overline); - std.def_func("pad", pad); + + // Layout. std.def_func("page", page); std.def_func("pagebreak", pagebreak); - std.def_func("par", par); - std.def_func("rect", rect); - std.def_func("repr", repr); - std.def_func("rgb", rgb); - std.def_func("square", square); - std.def_func("stack", stack); - std.def_func("strike", strike); - std.def_func("type", type_); - std.def_func("underline", underline); + std.def_func("h", h); std.def_func("v", v); + std.def_func("align", align); + std.def_func("pad", pad); + std.def_func("stack", stack); + std.def_func("grid", grid); + + // Elements. + std.def_func("image", image); + std.def_func("rect", rect); + std.def_func("square", square); + std.def_func("ellipse", ellipse); + std.def_func("circle", circle); + + // Utility. + std.def_func("type", type_); + std.def_func("repr", repr); + std.def_func("len", len); + std.def_func("rgb", rgb); + std.def_func("min", min); + std.def_func("max", max); // Colors. std.def_const("white", RgbaColor::WHITE); diff --git a/src/library/pad.rs b/src/library/pad.rs deleted file mode 100644 index 4b68a4348..000000000 --- a/src/library/pad.rs +++ /dev/null @@ -1,37 +0,0 @@ -use super::*; -use crate::layout::PadNode; - -/// `pad`: Pad content at the sides. -/// -/// # Positional parameters -/// - Padding for all sides: `padding`, of type `linear` relative to sides. -/// - Body: of type `template`. -/// -/// # Named parameters -/// - Left padding: `left`, of type `linear` relative to parent width. -/// - Right padding: `right`, of type `linear` relative to parent width. -/// - Top padding: `top`, of type `linear` relative to parent height. -/// - Bottom padding: `bottom`, of type `linear` relative to parent height. -/// -/// # Return value -/// A template that pads its region and sets the body into it. -pub fn pad(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { - let all = args.eat(ctx); - let left = args.named(ctx, "left"); - let top = args.named(ctx, "top"); - let right = args.named(ctx, "right"); - let bottom = args.named(ctx, "bottom"); - let body = args.expect::(ctx, "body").unwrap_or_default(); - - let padding = Sides::new( - left.or(all).unwrap_or_default(), - top.or(all).unwrap_or_default(), - right.or(all).unwrap_or_default(), - bottom.or(all).unwrap_or_default(), - ); - - Value::template("pad", move |ctx| { - let child = ctx.exec_template_stack(&body).into(); - ctx.push_into_stack(PadNode { padding, child }); - }) -} diff --git a/src/library/page.rs b/src/library/page.rs deleted file mode 100644 index eb39fb9e2..000000000 --- a/src/library/page.rs +++ /dev/null @@ -1,106 +0,0 @@ -use super::*; -use crate::paper::{Paper, PaperClass}; - -/// `page`: Configure pages. -/// -/// # Positional parameters -/// - Paper name: optional, of type `string`, see [here](crate::paper) for a -/// full list of all paper names. -/// - Body: optional, of type `template`. -/// -/// # Named parameters -/// - 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`. -/// -/// # Return value -/// A template that configures page properties. The effect is scoped to the body -/// if present. -pub fn page(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { - let paper = args.eat::>(ctx).and_then(|name| { - Paper::from_name(&name.v).or_else(|| { - ctx.diag(error!(name.span, "invalid paper name")); - None - }) - }); - - let width = args.named(ctx, "width"); - let height = args.named(ctx, "height"); - let margins = args.named(ctx, "margins"); - let left = args.named(ctx, "left"); - let top = args.named(ctx, "top"); - let right = args.named(ctx, "right"); - let bottom = args.named(ctx, "bottom"); - let flip = args.named(ctx, "flip"); - let body = args.eat::(ctx); - let span = args.span; - - 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(); - } - - if let Some(width) = width { - ctx.state.page.class = PaperClass::Custom; - ctx.state.page.size.width = width; - } - - if let Some(height) = height { - ctx.state.page.class = PaperClass::Custom; - ctx.state.page.size.height = height; - } - - if let Some(margins) = margins { - ctx.state.page.margins = Sides::splat(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); - } - - ctx.pagebreak(false, true, span); - - if let Some(body) = &body { - // TODO: Restrict body to a single page? - body.exec(ctx); - ctx.state = snapshot; - ctx.pagebreak(true, false, span); - } - }) -} - -/// `pagebreak`: Start a new page. -/// -/// # Return value -/// A template that inserts a page break. -pub fn pagebreak(_: &mut EvalContext, args: &mut FuncArgs) -> Value { - let span = args.span; - Value::template("pagebreak", move |ctx| { - ctx.pagebreak(true, true, span); - }) -} diff --git a/src/library/par.rs b/src/library/par.rs deleted file mode 100644 index 1737133ba..000000000 --- a/src/library/par.rs +++ /dev/null @@ -1,32 +0,0 @@ -use super::*; - -/// `par`: Configure paragraphs. -/// -/// # Named parameters -/// - Paragraph spacing: `spacing`, of type `linear` relative to current font size. -/// - Line leading: `leading`, of type `linear` relative to current font size. -/// - Word spacing: `word-spacing`, of type `linear` relative to current font size. -/// -/// # Return value -/// A template that configures paragraph properties. -pub fn par(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { - let spacing = args.named(ctx, "spacing"); - let leading = args.named(ctx, "leading"); - let word_spacing = args.named(ctx, "word-spacing"); - - Value::template("par", move |ctx| { - if let Some(spacing) = spacing { - ctx.state.par.spacing = spacing; - } - - if let Some(leading) = leading { - ctx.state.par.leading = leading; - } - - if let Some(word_spacing) = word_spacing { - ctx.state.par.word_spacing = word_spacing; - } - - ctx.parbreak(); - }) -} diff --git a/src/library/spacing.rs b/src/library/spacing.rs deleted file mode 100644 index b32e97c1b..000000000 --- a/src/library/spacing.rs +++ /dev/null @@ -1,39 +0,0 @@ -use super::*; - -/// `h`: Horizontal spacing. -/// -/// # Positional parameters -/// - Amount of spacing: of type `linear` relative to current font size. -/// -/// # Return value -/// A template that inserts horizontal spacing. -pub fn h(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { - spacing_impl("h", ctx, args, GenAxis::Cross) -} - -/// `v`: Vertical spacing. -/// -/// # Positional parameters -/// - Amount of spacing: of type `linear` relative to current font size. -/// -/// # Return value -/// A template that inserts vertical spacing. -pub fn v(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { - spacing_impl("v", ctx, args, GenAxis::Main) -} - -fn spacing_impl( - name: &str, - ctx: &mut EvalContext, - args: &mut FuncArgs, - axis: GenAxis, -) -> Value { - let spacing: Option = args.expect(ctx, "spacing"); - Value::template(name, move |ctx| { - if let Some(linear) = spacing { - // TODO: Should this really always be font-size relative? - let amount = linear.resolve(ctx.state.font.size); - ctx.push_spacing(axis, amount); - } - }) -} diff --git a/src/library/stack.rs b/src/library/stack.rs deleted file mode 100644 index 21a0ac352..000000000 --- a/src/library/stack.rs +++ /dev/null @@ -1,40 +0,0 @@ -use super::*; -use crate::layout::{StackChild, StackNode}; - -/// `stack`: Stack children along an axis. -/// -/// # Positional parameters -/// - Children: variadic, of type `template`. -/// -/// # Named parameters -/// - Stacking direction: `dir`, of type `direction`. -/// -/// # Return value -/// A template that places its children along the specified layouting axis. -/// -/// # Relevant types and constants -/// - Type `direction` -/// - `ltr` -/// - `rtl` -/// - `ttb` -/// - `btt` -pub fn stack(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { - let dir = args.named::(ctx, "dir").unwrap_or(Dir::TTB); - let children = args.all::(ctx); - - Value::template("stack", move |ctx| { - let children = children - .iter() - .map(|child| { - let child = ctx.exec_template_stack(child).into(); - StackChild::Any(child, ctx.state.aligns) - }) - .collect(); - - ctx.push_into_stack(StackNode { - dirs: Gen::new(ctx.state.lang.dir, dir), - aspect: None, - children, - }); - }) -} diff --git a/src/library/font.rs b/src/library/text.rs similarity index 58% rename from src/library/font.rs rename to src/library/text.rs index 3f816c6dd..f80b417c2 100644 --- a/src/library/font.rs +++ b/src/library/text.rs @@ -1,53 +1,10 @@ +use crate::exec::{FontState, LineState}; use crate::font::{FontStretch, FontStyle, FontWeight}; use crate::layout::Fill; use super::*; /// `font`: Configure the font. -/// -/// # Positional parameters -/// - Body: optional, of type `template`. -/// -/// # Named parameters -/// - Font size: `size`, of type `linear` relative to current font size. -/// - Font families: `family`, `font-family`, `string` or `array`. -/// - Font Style: `style`, of type `font-style`. -/// - Font Weight: `weight`, of type `font-weight`. -/// - Font Stretch: `stretch`, of type `relative`, between 0.5 and 2.0. -/// - Top edge of the font: `top-edge`, of type `vertical-font-metric`. -/// - Bottom edge of the font: `bottom-edge`, of type `vertical-font-metric`. -/// - Color the glyphs: `color`, of type `color`. -/// - Serif family definition: `serif`, of type `family-def`. -/// - Sans-serif family definition: `sans-serif`, of type `family-def`. -/// - Monospace family definition: `monospace`, of type `family-def`. -/// -/// # Return value -/// A template that configures font properties. The effect is scoped to the body -/// if present. -/// -/// # Relevant types and constants -/// - Type `font-family` -/// - `serif` -/// - `sans-serif` -/// - `monospace` -/// - coerces from `string` -/// - Type `family-def` -/// - coerces from `string` -/// - coerces from `array` of `string` -/// - Type `font-style` -/// - `normal` -/// - `italic` -/// - `oblique` -/// - Type `font-weight` -/// - `regular` (400) -/// - `bold` (700) -/// - coerces from `integer`, between 100 and 900 -/// - Type `vertical-font-metric` -/// - `ascender` -/// - `cap-height` -/// - `x-height` -/// - `baseline` -/// - `descender` pub fn font(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { let list = args.named(ctx, "family"); let size = args.named::(ctx, "size"); @@ -198,3 +155,104 @@ castable! { castable! { VerticalFontMetric: "vertical font metric", } + +/// `par`: Configure paragraphs. +pub fn par(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { + let spacing = args.named(ctx, "spacing"); + let leading = args.named(ctx, "leading"); + let word_spacing = args.named(ctx, "word-spacing"); + + Value::template("par", move |ctx| { + if let Some(spacing) = spacing { + ctx.state.par.spacing = spacing; + } + + if let Some(leading) = leading { + ctx.state.par.leading = leading; + } + + if let Some(word_spacing) = word_spacing { + ctx.state.par.word_spacing = word_spacing; + } + + ctx.parbreak(); + }) +} + +/// `lang`: Configure the language. +pub fn lang(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { + let iso = args.eat::(ctx).map(|s| lang_dir(&s)); + let dir = match args.named::>(ctx, "dir") { + Some(dir) if dir.v.axis() == SpecAxis::Horizontal => Some(dir.v), + Some(dir) => { + ctx.diag(error!(dir.span, "must be horizontal")); + None + } + None => None, + }; + + Value::template("lang", move |ctx| { + if let Some(dir) = dir.or(iso) { + ctx.state.lang.dir = dir; + } + + ctx.parbreak(); + }) +} + +/// The default direction for the language identified by `iso`. +fn lang_dir(iso: &str) -> Dir { + match iso.to_ascii_lowercase().as_str() { + "ar" | "he" | "fa" | "ur" | "ps" | "yi" => Dir::RTL, + "en" | "fr" | "de" | _ => Dir::LTR, + } +} + +/// `strike`: Enable striken-through text. +pub fn strike(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { + line_impl("strike", ctx, args, |font| &mut font.strikethrough) +} + +/// `underline`: Enable underlined text. +pub fn underline(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { + line_impl("underline", ctx, args, |font| &mut font.underline) +} + +/// `overline`: Add an overline above text. +pub fn overline(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { + line_impl("overline", ctx, args, |font| &mut font.overline) +} + +fn line_impl( + name: &str, + ctx: &mut EvalContext, + args: &mut FuncArgs, + substate: fn(&mut FontState) -> &mut Option>, +) -> Value { + let color = args.named(ctx, "color"); + let position = args.named(ctx, "position"); + let strength = args.named::(ctx, "strength"); + let extent = args.named(ctx, "extent").unwrap_or_default(); + let body = args.eat::(ctx); + + // Suppress any existing strikethrough if strength is explicitly zero. + let state = strength.map_or(true, |s| !s.is_zero()).then(|| { + Rc::new(LineState { + strength, + position, + extent, + fill: color.map(Fill::Color), + }) + }); + + Value::template(name, move |ctx| { + let snapshot = ctx.state.clone(); + + *substate(ctx.state.font_mut()) = state.clone(); + + if let Some(body) = &body { + body.exec(ctx); + ctx.state = snapshot; + } + }) +} diff --git a/src/library/basic.rs b/src/library/utility.rs similarity index 60% rename from src/library/basic.rs rename to src/library/utility.rs index e0e464aed..146fce9c8 100644 --- a/src/library/basic.rs +++ b/src/library/utility.rs @@ -1,15 +1,11 @@ +use std::cmp::Ordering; + use crate::color::{Color, RgbaColor}; use crate::pretty::pretty; use super::*; /// `type`: The name of a value's type. -/// -/// # Positional parameters -/// - Any value. -/// -/// # Return value -/// The name of the value's type as a string. pub fn type_(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { match args.expect::(ctx, "value") { Some(value) => value.type_name().into(), @@ -18,12 +14,6 @@ pub fn type_(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { } /// `repr`: The string representation of a value. -/// -/// # Positional parameters -/// - Any value. -/// -/// # Return value -/// The string representation of the value. pub fn repr(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { match args.expect::(ctx, "value") { Some(value) => pretty(&value).into(), @@ -46,15 +36,6 @@ pub fn len(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { } /// `rgb`: Create an RGB(A) color. -/// -/// # Positional parameters -/// - 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. -/// -/// # Return value -/// The color with the given components. pub fn rgb(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { let r = args.expect(ctx, "red component"); let g = args.expect(ctx, "green component"); @@ -77,3 +58,43 @@ pub fn rgb(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { clamp(a, 255), ))) } + +/// `min`: The minimum of a sequence of values. +pub fn min(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { + minmax(ctx, args, Ordering::Less) +} + +/// `max`: The maximum of a sequence of values. +pub fn max(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { + minmax(ctx, args, Ordering::Greater) +} + +/// Find the minimum or maximum of a sequence of values. +fn minmax(ctx: &mut EvalContext, args: &mut FuncArgs, goal: Ordering) -> Value { + let mut extremum = None; + + while let Some(value) = args.eat::(ctx) { + if let Some(prev) = &extremum { + match value.cmp(&prev) { + Some(ordering) if ordering == goal => extremum = Some(value), + Some(_) => {} + None => { + ctx.diag(error!( + args.span, + "cannot compare {} with {}", + prev.type_name(), + value.type_name(), + )); + return Value::Error; + } + } + } else { + extremum = Some(value); + } + } + + extremum.unwrap_or_else(|| { + args.expect::(ctx, "value"); + Value::Error + }) +} diff --git a/src/syntax/node.rs b/src/syntax/node.rs index a97430b64..79b1e3522 100644 --- a/src/syntax/node.rs +++ b/src/syntax/node.rs @@ -30,68 +30,6 @@ pub enum Node { } /// A raw block with optional syntax highlighting: `` `...` ``. -/// -/// Raw blocks start with 1 or 3+ backticks and end with the same number of -/// backticks. -/// -/// When using at least three backticks, an optional language tag may follow -/// directly after the backticks. This tag defines which language to -/// syntax-highlight the text in. Apart from the language tag and some -/// whitespace trimming discussed below, everything inside a raw block is -/// rendered verbatim, in particular, there are no escape sequences. -/// -/// # Examples -/// - Raw text is surrounded by backticks. -/// ```typst -/// `raw` -/// ``` -/// - An optional language tag may follow directly at the start when the block -/// is surrounded by at least three backticks. -/// ````typst -/// ```rust println!("hello!")```; -/// ```` -/// - Blocks can span multiple lines. -/// ````typst -/// ```rust -/// loop { -/// find_yak().shave(); -/// } -/// ``` -/// ```` -/// - Start with a space to omit the language tag (the space will be trimmed -/// from the output). -/// `````typst -/// ```` This has no leading space.```` -/// ````` -/// - Use more backticks to allow backticks in the raw text. -/// `````typst -/// ```` This contains ```backticks```.```` -/// ````` -/// -/// # Trimming -/// If we would always render the raw text between the backticks exactly as -/// given, some things would become cumbersome/impossible to write: -/// - Typical multiline code blocks (like in the example above) would have an -/// additional newline before and after the code. -/// - Multi-line blocks would need to start with a space since a word would be -/// interpreted as a language tag. -/// - Text ending with a backtick would be impossible since the backtick would -/// be interpreted as belonging to the closing backticks. -/// -/// To fix these problems, we sometimes trim a bit of space from blocks with 3+ -/// backticks: -/// - At the start, we trim a single space or a sequence of whitespace followed -/// by a newline. -/// - At the end, we trim -/// - a single space if the raw text ends with a backtick followed only by -/// whitespace, -/// - a newline followed by a sequence of whitespace. -/// -/// You can thus produce a single backtick without surrounding spaces with the -/// sequence ```` ``` ` ``` ````. -/// -/// Note that with these rules you can always force leading or trailing -/// whitespace simply by adding more spaces. #[derive(Debug, Clone, PartialEq)] pub struct RawNode { /// The source code location.