From 285c2f617b74e182be69decea46bbd0afdb0f604 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Sat, 26 Jun 2021 13:06:37 +0200 Subject: Cleanse library - Remove doc-comments for Typst functions from library - Reduce number of library source files --- src/layout/image.rs | 65 ++++++++++ src/layout/mod.rs | 2 + src/library/align.rs | 128 ------------------ src/library/basic.rs | 79 ------------ src/library/decorations.rs | 86 ------------ src/library/elements.rs | 135 +++++++++++++++++++ src/library/font.rs | 200 ---------------------------- src/library/grid.rs | 89 ------------- src/library/image.rs | 103 --------------- src/library/lang.rs | 44 ------- src/library/layout.rs | 316 +++++++++++++++++++++++++++++++++++++++++++++ src/library/math.rs | 57 -------- src/library/mod.rs | 78 +++++------ src/library/pad.rs | 37 ------ src/library/page.rs | 106 --------------- src/library/par.rs | 32 ----- src/library/shapes.rs | 163 ----------------------- src/library/spacing.rs | 39 ------ src/library/stack.rs | 40 ------ src/library/text.rs | 258 ++++++++++++++++++++++++++++++++++++ src/library/utility.rs | 100 ++++++++++++++ src/syntax/node.rs | 62 --------- 22 files changed, 908 insertions(+), 1311 deletions(-) create mode 100644 src/layout/image.rs delete mode 100644 src/library/align.rs delete mode 100644 src/library/basic.rs delete mode 100644 src/library/decorations.rs create mode 100644 src/library/elements.rs delete mode 100644 src/library/font.rs delete mode 100644 src/library/grid.rs delete mode 100644 src/library/image.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/shapes.rs delete mode 100644 src/library/spacing.rs delete mode 100644 src/library/stack.rs create mode 100644 src/library/text.rs create mode 100644 src/library/utility.rs (limited to 'src') diff --git a/src/layout/image.rs b/src/layout/image.rs new file mode 100644 index 00000000..9ba8cd82 --- /dev/null +++ b/src/layout/image.rs @@ -0,0 +1,65 @@ +use super::*; +use crate::image::ImageId; + +use ::image::GenericImageView; + +/// An image node. +#[derive(Debug, Copy, Clone, PartialEq, Hash)] +pub struct ImageNode { + /// The id of the image file. + pub id: ImageId, + /// The fixed width, if any. + pub width: Option, + /// The fixed height, if any. + pub height: Option, +} + +impl Layout for ImageNode { + fn layout( + &self, + ctx: &mut LayoutContext, + regions: &Regions, + ) -> Vec>> { + let Regions { current, base, .. } = regions; + let mut constraints = Constraints::new(regions.expand); + constraints.set_base_using_linears(Spec::new(self.width, self.height), regions); + + let width = self.width.map(|w| w.resolve(base.width)); + let height = self.height.map(|w| w.resolve(base.height)); + + 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) { + (Some(width), Some(height)) => Size::new(width, height), + (Some(width), None) => Size::new(width, width / pixel_ratio), + (None, Some(height)) => Size::new(height * pixel_ratio, height), + (None, None) => { + constraints.exact = current.to_spec().map(Some); + + let ratio = current.width / current.height; + if ratio < pixel_ratio && current.width.is_finite() { + Size::new(current.width, current.width / pixel_ratio) + } else if current.height.is_finite() { + // TODO: Fix issue with line spacing. + Size::new(current.height * pixel_ratio, current.height) + } else { + // Totally unbounded region, we have to make up something. + Size::new(Length::pt(pixel_width), Length::pt(pixel_height)) + } + } + }; + + let mut frame = Frame::new(size, size.height); + frame.push(Point::zero(), Element::Image(self.id, size)); + vec![frame.constrain(constraints)] + } +} + +impl From for AnyNode { + fn from(image: ImageNode) -> Self { + Self::new(image) + } +} diff --git a/src/layout/mod.rs b/src/layout/mod.rs index f1ae3e2a..10c30f41 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 c0ed0416..00000000 --- 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/basic.rs b/src/library/basic.rs deleted file mode 100644 index e0e464ae..00000000 --- a/src/library/basic.rs +++ /dev/null @@ -1,79 +0,0 @@ -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(), - None => Value::Error, - } -} - -/// `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(), - None => Value::Error, - } -} - -/// `len`: The length of a string, an array or a dictionary. -pub fn len(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { - match args.expect::>(ctx, "collection") { - Some(Spanned { v: Value::Str(v), .. }) => Value::Int(v.len() as i64), - Some(Spanned { v: Value::Array(v), .. }) => Value::Int(v.len() as i64), - Some(Spanned { v: Value::Dict(v), .. }) => Value::Int(v.len() as i64), - Some(other) if other.v != Value::Error => { - ctx.diag(error!(other.span, "expected string, array or dictionary")); - Value::Error - } - _ => Value::Error, - } -} - -/// `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"); - let b = args.expect(ctx, "blue component"); - let a = args.eat(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/decorations.rs b/src/library/decorations.rs deleted file mode 100644 index b935f707..00000000 --- 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/elements.rs b/src/library/elements.rs new file mode 100644 index 00000000..b1b5c1f9 --- /dev/null +++ b/src/library/elements.rs @@ -0,0 +1,135 @@ +use std::f64::consts::SQRT_2; + +use decorum::N64; + +use super::*; +use crate::color::Color; +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. +pub fn rect(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { + let width = args.named(ctx, "width"); + let height = args.named(ctx, "height"); + let fill = args.named(ctx, "fill"); + let body = args.eat::(ctx).unwrap_or_default(); + rect_impl("rect", width, height, None, fill, body) +} + +/// `square`: A square with optional content. +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")); + let height = width.is_none().then(|| args.named(ctx, "height")).flatten(); + let fill = args.named(ctx, "fill"); + let body = args.eat::(ctx).unwrap_or_default(); + rect_impl("square", width, height, Some(N64::from(1.0)), fill, body) +} + +fn rect_impl( + name: &str, + width: Option, + height: Option, + aspect: Option, + fill: Option, + body: TemplateValue, +) -> Value { + Value::template(name, move |ctx| { + let mut stack = ctx.exec_template_stack(&body); + stack.aspect = aspect; + + let fixed = FixedNode { width, height, child: stack.into() }; + + if let Some(color) = fill { + ctx.push_into_par(BackgroundNode { + shape: BackgroundShape::Rect, + fill: Fill::Color(color), + child: fixed.into(), + }); + } else { + ctx.push_into_par(fixed); + } + }) +} + +/// `ellipse`: An ellipse with optional content. +pub fn ellipse(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { + let width = args.named(ctx, "width"); + let height = args.named(ctx, "height"); + let fill = args.named(ctx, "fill"); + let body = args.eat::(ctx).unwrap_or_default(); + ellipse_impl("ellipse", width, height, None, fill, body) +} + +/// `circle`: A circle with optional content. +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")); + let height = width.is_none().then(|| args.named(ctx, "height")).flatten(); + let fill = args.named(ctx, "fill"); + let body = args.eat::(ctx).unwrap_or_default(); + ellipse_impl("circle", width, height, Some(N64::from(1.0)), fill, body) +} + +fn ellipse_impl( + name: &str, + width: Option, + height: Option, + aspect: Option, + fill: Option, + body: TemplateValue, +) -> Value { + Value::template(name, move |ctx| { + // This padding ratio ensures that the rectangular padded region fits + // perfectly into the ellipse. + const PAD: f64 = 0.5 - SQRT_2 / 4.0; + + let mut stack = ctx.exec_template_stack(&body); + stack.aspect = aspect; + + let fixed = FixedNode { + width, + height, + child: PadNode { + padding: Sides::splat(Relative::new(PAD).into()), + child: stack.into(), + } + .into(), + }; + + if let Some(color) = fill { + ctx.push_into_par(BackgroundNode { + shape: BackgroundShape::Ellipse, + fill: Fill::Color(color), + child: fixed.into(), + }); + } else { + ctx.push_into_par(fixed); + } + }) +} diff --git a/src/library/font.rs b/src/library/font.rs deleted file mode 100644 index 3f816c6d..00000000 --- a/src/library/font.rs +++ /dev/null @@ -1,200 +0,0 @@ -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"); - let style = args.named(ctx, "style"); - let weight = args.named(ctx, "weight"); - let stretch = args.named(ctx, "stretch"); - let top_edge = args.named(ctx, "top-edge"); - let bottom_edge = args.named(ctx, "bottom-edge"); - let color = args.named(ctx, "color"); - let serif = args.named(ctx, "serif"); - let sans_serif = args.named(ctx, "sans-serif"); - let monospace = args.named(ctx, "monospace"); - let body = args.eat::(ctx); - - Value::template("font", move |ctx| { - let snapshot = ctx.state.clone(); - let font = ctx.state.font_mut(); - - if let Some(linear) = size { - font.size = linear.resolve(font.size); - } - - if let Some(FontDef(list)) = &list { - font.families_mut().list = list.clone(); - } - - if let Some(style) = style { - font.variant.style = style; - } - - if let Some(weight) = weight { - font.variant.weight = weight; - } - - if let Some(stretch) = stretch { - font.variant.stretch = stretch; - } - - if let Some(top_edge) = top_edge { - font.top_edge = top_edge; - } - - if let Some(bottom_edge) = bottom_edge { - font.bottom_edge = bottom_edge; - } - - if let Some(color) = color { - font.fill = Fill::Color(color); - } - - if let Some(FamilyDef(serif)) = &serif { - font.families_mut().serif = serif.clone(); - } - - if let Some(FamilyDef(sans_serif)) = &sans_serif { - font.families_mut().sans_serif = sans_serif.clone(); - } - - if let Some(FamilyDef(monospace)) = &monospace { - font.families_mut().monospace = monospace.clone(); - } - - if let Some(body) = &body { - body.exec(ctx); - ctx.state = snapshot; - } - }) -} - -#[derive(Debug)] -struct FontDef(Vec); - -castable! { - FontDef: "font family or array of font families", - Value::Str(string) => Self(vec![FontFamily::Named(string.to_lowercase())]), - Value::Array(values) => Self(values - .into_iter() - .filter_map(|v| v.cast().ok()) - .collect() - ), - #(family: FontFamily) => Self(vec![family]), -} - -#[derive(Debug)] -struct FamilyDef(Vec); - -castable! { - FamilyDef: "string or array of strings", - Value::Str(string) => Self(vec![string.to_lowercase()]), - Value::Array(values) => Self(values - .into_iter() - .filter_map(|v| v.cast().ok()) - .map(|string: String| string.to_lowercase()) - .collect() - ), -} - -castable! { - FontFamily: "font family", - Value::Str(string) => Self::Named(string.to_lowercase()) -} - -castable! { - FontStyle: "font style", -} - -castable! { - FontWeight: "font weight", - Value::Int(number) => { - let [min, max] = [Self::THIN, Self::BLACK]; - let message = || format!( - "should be between {} and {}", - min.to_number(), - max.to_number(), - ); - - return if number < i64::from(min.to_number()) { - CastResult::Warn(min, message()) - } else if number > i64::from(max.to_number()) { - CastResult::Warn(max, message()) - } else { - CastResult::Ok(Self::from_number(number as u16)) - }; - }, -} - -castable! { - FontStretch: "font stretch", - Value::Relative(relative) => { - let [min, max] = [Self::ULTRA_CONDENSED, Self::ULTRA_EXPANDED]; - let message = || format!( - "should be between {} and {}", - Relative::new(min.to_ratio() as f64), - Relative::new(max.to_ratio() as f64), - ); - - let ratio = relative.get() as f32; - let value = Self::from_ratio(ratio); - - return if ratio < min.to_ratio() || ratio > max.to_ratio() { - CastResult::Warn(value, message()) - } else { - CastResult::Ok(value) - }; - }, -} - -castable! { - VerticalFontMetric: "vertical font metric", -} diff --git a/src/library/grid.rs b/src/library/grid.rs deleted file mode 100644 index c2e76514..00000000 --- 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/image.rs b/src/library/image.rs deleted file mode 100644 index 54fa54c9..00000000 --- a/src/library/image.rs +++ /dev/null @@ -1,103 +0,0 @@ -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); - } - }) -} - -/// An image node. -#[derive(Debug, Copy, Clone, PartialEq, Hash)] -struct ImageNode { - /// The id of the image file. - id: ImageId, - /// The pixel dimensions of the image. - dimensions: (u32, u32), - /// The fixed width, if any. - width: Option, - /// The fixed height, if any. - height: Option, -} - -impl Layout for ImageNode { - fn layout( - &self, - _: &mut LayoutContext, - regions: &Regions, - ) -> Vec>> { - let Regions { current, base, .. } = regions; - let mut constraints = Constraints::new(regions.expand); - constraints.set_base_using_linears(Spec::new(self.width, self.height), regions); - - 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 pixel_ratio = pixel_width / pixel_height; - - let size = match (width, height) { - (Some(width), Some(height)) => Size::new(width, height), - (Some(width), None) => Size::new(width, width / pixel_ratio), - (None, Some(height)) => Size::new(height * pixel_ratio, height), - (None, None) => { - constraints.exact = current.to_spec().map(Some); - - let ratio = current.width / current.height; - if ratio < pixel_ratio && current.width.is_finite() { - Size::new(current.width, current.width / pixel_ratio) - } else if current.height.is_finite() { - // TODO: Fix issue with line spacing. - Size::new(current.height * pixel_ratio, current.height) - } else { - // Totally unbounded region, we have to make up something. - Size::new(Length::pt(pixel_width), Length::pt(pixel_height)) - } - } - }; - - let mut frame = Frame::new(size, size.height); - frame.push(Point::zero(), Element::Image(self.id, size)); - vec![frame.constrain(constraints)] - } -} - -impl From for AnyNode { - fn from(image: ImageNode) -> Self { - Self::new(image) - } -} diff --git a/src/library/lang.rs b/src/library/lang.rs deleted file mode 100644 index 7a08001a..00000000 --- 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 00000000..cba77c72 --- /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 4afb540d..00000000 --- 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 4911e5c8..5f0430b2 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("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("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("ellipse", ellipse); + std.def_func("circle", circle); + + // Utility. std.def_func("type", type_); - std.def_func("underline", underline); - std.def_func("v", v); + 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 4b68a434..00000000 --- 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 eb39fb9e..00000000 --- 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 1737133b..00000000 --- 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/shapes.rs b/src/library/shapes.rs deleted file mode 100644 index 213e2f4a..00000000 --- a/src/library/shapes.rs +++ /dev/null @@ -1,163 +0,0 @@ -use std::f64::consts::SQRT_2; - -use decorum::N64; - -use super::*; -use crate::color::Color; -use crate::layout::{BackgroundNode, BackgroundShape, Fill, FixedNode, PadNode}; - -/// `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"); - let fill = args.named(ctx, "fill"); - let body = args.eat::(ctx).unwrap_or_default(); - rect_impl("rect", width, height, None, fill, body) -} - -/// `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")); - let height = width.is_none().then(|| args.named(ctx, "height")).flatten(); - let fill = args.named(ctx, "fill"); - let body = args.eat::(ctx).unwrap_or_default(); - rect_impl("square", width, height, Some(N64::from(1.0)), fill, body) -} - -fn rect_impl( - name: &str, - width: Option, - height: Option, - aspect: Option, - fill: Option, - body: TemplateValue, -) -> Value { - Value::template(name, move |ctx| { - let mut stack = ctx.exec_template_stack(&body); - stack.aspect = aspect; - - let fixed = FixedNode { width, height, child: stack.into() }; - - if let Some(color) = fill { - ctx.push_into_par(BackgroundNode { - shape: BackgroundShape::Rect, - fill: Fill::Color(color), - child: fixed.into(), - }); - } else { - ctx.push_into_par(fixed); - } - }) -} - -/// `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"); - let fill = args.named(ctx, "fill"); - let body = args.eat::(ctx).unwrap_or_default(); - ellipse_impl("ellipse", width, height, None, fill, body) -} - -/// `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")); - let height = width.is_none().then(|| args.named(ctx, "height")).flatten(); - let fill = args.named(ctx, "fill"); - let body = args.eat::(ctx).unwrap_or_default(); - ellipse_impl("circle", width, height, Some(N64::from(1.0)), fill, body) -} - -fn ellipse_impl( - name: &str, - width: Option, - height: Option, - aspect: Option, - fill: Option, - body: TemplateValue, -) -> Value { - Value::template(name, move |ctx| { - // This padding ratio ensures that the rectangular padded region fits - // perfectly into the ellipse. - const PAD: f64 = 0.5 - SQRT_2 / 4.0; - - let mut stack = ctx.exec_template_stack(&body); - stack.aspect = aspect; - - let fixed = FixedNode { - width, - height, - child: PadNode { - padding: Sides::splat(Relative::new(PAD).into()), - child: stack.into(), - } - .into(), - }; - - if let Some(color) = fill { - ctx.push_into_par(BackgroundNode { - shape: BackgroundShape::Ellipse, - fill: Fill::Color(color), - child: fixed.into(), - }); - } else { - ctx.push_into_par(fixed); - } - }) -} diff --git a/src/library/spacing.rs b/src/library/spacing.rs deleted file mode 100644 index b32e97c1..00000000 --- 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 21a0ac35..00000000 --- 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/text.rs b/src/library/text.rs new file mode 100644 index 00000000..f80b417c --- /dev/null +++ b/src/library/text.rs @@ -0,0 +1,258 @@ +use crate::exec::{FontState, LineState}; +use crate::font::{FontStretch, FontStyle, FontWeight}; +use crate::layout::Fill; + +use super::*; + +/// `font`: Configure the font. +pub fn font(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { + let list = args.named(ctx, "family"); + let size = args.named::(ctx, "size"); + let style = args.named(ctx, "style"); + let weight = args.named(ctx, "weight"); + let stretch = args.named(ctx, "stretch"); + let top_edge = args.named(ctx, "top-edge"); + let bottom_edge = args.named(ctx, "bottom-edge"); + let color = args.named(ctx, "color"); + let serif = args.named(ctx, "serif"); + let sans_serif = args.named(ctx, "sans-serif"); + let monospace = args.named(ctx, "monospace"); + let body = args.eat::(ctx); + + Value::template("font", move |ctx| { + let snapshot = ctx.state.clone(); + let font = ctx.state.font_mut(); + + if let Some(linear) = size { + font.size = linear.resolve(font.size); + } + + if let Some(FontDef(list)) = &list { + font.families_mut().list = list.clone(); + } + + if let Some(style) = style { + font.variant.style = style; + } + + if let Some(weight) = weight { + font.variant.weight = weight; + } + + if let Some(stretch) = stretch { + font.variant.stretch = stretch; + } + + if let Some(top_edge) = top_edge { + font.top_edge = top_edge; + } + + if let Some(bottom_edge) = bottom_edge { + font.bottom_edge = bottom_edge; + } + + if let Some(color) = color { + font.fill = Fill::Color(color); + } + + if let Some(FamilyDef(serif)) = &serif { + font.families_mut().serif = serif.clone(); + } + + if let Some(FamilyDef(sans_serif)) = &sans_serif { + font.families_mut().sans_serif = sans_serif.clone(); + } + + if let Some(FamilyDef(monospace)) = &monospace { + font.families_mut().monospace = monospace.clone(); + } + + if let Some(body) = &body { + body.exec(ctx); + ctx.state = snapshot; + } + }) +} + +#[derive(Debug)] +struct FontDef(Vec); + +castable! { + FontDef: "font family or array of font families", + Value::Str(string) => Self(vec![FontFamily::Named(string.to_lowercase())]), + Value::Array(values) => Self(values + .into_iter() + .filter_map(|v| v.cast().ok()) + .collect() + ), + #(family: FontFamily) => Self(vec![family]), +} + +#[derive(Debug)] +struct FamilyDef(Vec); + +castable! { + FamilyDef: "string or array of strings", + Value::Str(string) => Self(vec![string.to_lowercase()]), + Value::Array(values) => Self(values + .into_iter() + .filter_map(|v| v.cast().ok()) + .map(|string: String| string.to_lowercase()) + .collect() + ), +} + +castable! { + FontFamily: "font family", + Value::Str(string) => Self::Named(string.to_lowercase()) +} + +castable! { + FontStyle: "font style", +} + +castable! { + FontWeight: "font weight", + Value::Int(number) => { + let [min, max] = [Self::THIN, Self::BLACK]; + let message = || format!( + "should be between {} and {}", + min.to_number(), + max.to_number(), + ); + + return if number < i64::from(min.to_number()) { + CastResult::Warn(min, message()) + } else if number > i64::from(max.to_number()) { + CastResult::Warn(max, message()) + } else { + CastResult::Ok(Self::from_number(number as u16)) + }; + }, +} + +castable! { + FontStretch: "font stretch", + Value::Relative(relative) => { + let [min, max] = [Self::ULTRA_CONDENSED, Self::ULTRA_EXPANDED]; + let message = || format!( + "should be between {} and {}", + Relative::new(min.to_ratio() as f64), + Relative::new(max.to_ratio() as f64), + ); + + let ratio = relative.get() as f32; + let value = Self::from_ratio(ratio); + + return if ratio < min.to_ratio() || ratio > max.to_ratio() { + CastResult::Warn(value, message()) + } else { + CastResult::Ok(value) + }; + }, +} + +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/utility.rs b/src/library/utility.rs new file mode 100644 index 00000000..146fce9c --- /dev/null +++ b/src/library/utility.rs @@ -0,0 +1,100 @@ +use std::cmp::Ordering; + +use crate::color::{Color, RgbaColor}; +use crate::pretty::pretty; + +use super::*; + +/// `type`: The name of a value's type. +pub fn type_(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { + match args.expect::(ctx, "value") { + Some(value) => value.type_name().into(), + None => Value::Error, + } +} + +/// `repr`: The string representation of a value. +pub fn repr(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { + match args.expect::(ctx, "value") { + Some(value) => pretty(&value).into(), + None => Value::Error, + } +} + +/// `len`: The length of a string, an array or a dictionary. +pub fn len(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { + match args.expect::>(ctx, "collection") { + Some(Spanned { v: Value::Str(v), .. }) => Value::Int(v.len() as i64), + Some(Spanned { v: Value::Array(v), .. }) => Value::Int(v.len() as i64), + Some(Spanned { v: Value::Dict(v), .. }) => Value::Int(v.len() as i64), + Some(other) if other.v != Value::Error => { + ctx.diag(error!(other.span, "expected string, array or dictionary")); + Value::Error + } + _ => Value::Error, + } +} + +/// `rgb`: Create an RGB(A) color. +pub fn rgb(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { + let r = args.expect(ctx, "red component"); + let g = args.expect(ctx, "green component"); + let b = args.expect(ctx, "blue component"); + let a = args.eat(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), + ))) +} + +/// `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 a97430b6..79b1e352 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. -- cgit v1.2.3