summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/layout/image.rs (renamed from src/library/image.rs)56
-rw-r--r--src/layout/mod.rs2
-rw-r--r--src/library/align.rs128
-rw-r--r--src/library/decorations.rs86
-rw-r--r--src/library/elements.rs (renamed from src/library/shapes.rs)82
-rw-r--r--src/library/grid.rs89
-rw-r--r--src/library/lang.rs44
-rw-r--r--src/library/layout.rs316
-rw-r--r--src/library/math.rs57
-rw-r--r--src/library/mod.rs78
-rw-r--r--src/library/pad.rs37
-rw-r--r--src/library/page.rs106
-rw-r--r--src/library/par.rs32
-rw-r--r--src/library/spacing.rs39
-rw-r--r--src/library/stack.rs40
-rw-r--r--src/library/text.rs (renamed from src/library/font.rs)146
-rw-r--r--src/library/utility.rs (renamed from src/library/basic.rs)63
-rw-r--r--src/syntax/node.rs62
18 files changed, 530 insertions, 933 deletions
diff --git a/src/library/image.rs b/src/layout/image.rs
index 54fa54c9..9ba8cd82 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::<Spanned<String>>(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<Linear>,
+ pub width: Option<Linear>,
/// The fixed height, if any.
- height: Option<Linear>,
+ pub height: Option<Linear>,
}
impl Layout for ImageNode {
fn layout(
&self,
- _: &mut LayoutContext,
+ ctx: &mut LayoutContext,
regions: &Regions,
) -> Vec<Constrained<Rc<Frame>>> {
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 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::<AlignValue>(ctx);
- let second = args.eat::<AlignValue>(ctx);
- let mut horizontal = args.named::<AlignValue>(ctx, "horizontal");
- let mut vertical = args.named::<AlignValue>(ctx, "vertical");
- let body = args.eat::<TemplateValue>(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<SpecAxis> {
- 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 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<Rc<LineState>>,
-) -> Value {
- let color = args.named(ctx, "color");
- let position = args.named(ctx, "position");
- let strength = args.named::<Linear>(ctx, "strength");
- let extent = args.named(ctx, "extent").unwrap_or_default();
- let body = args.eat::<TemplateValue>(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
index 213e2f4a..b1b5c1f9 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::<Spanned<String>>(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::<Length>(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::<Length>(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 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::<Tracks>(ctx, "columns").unwrap_or_default();
- let rows = args.named::<Tracks>(ctx, "rows").unwrap_or_default();
- let gutter = args
- .named::<Linear>(ctx, "gutter")
- .map(|v| vec![TrackSizing::Linear(v)])
- .unwrap_or_default();
- let gutter_columns = args.named::<Tracks>(ctx, "gutter-columns");
- let gutter_rows = args.named::<Tracks>(ctx, "gutter-rows");
- let column_dir = args.named(ctx, "column-dir");
- let row_dir = args.named(ctx, "row-dir");
- let children = args.all::<TemplateValue>(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<TrackSizing>;
-
-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 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::<String>(ctx).map(|s| lang_dir(&s));
- let dir = match args.named::<Spanned<Dir>>(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::<Spanned<String>>(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::<TemplateValue>(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<Linear> = 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::<AlignValue>(ctx);
+ let second = args.eat::<AlignValue>(ctx);
+ let mut horizontal = args.named::<AlignValue>(ctx, "horizontal");
+ let mut vertical = args.named::<AlignValue>(ctx, "vertical");
+ let body = args.eat::<TemplateValue>(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<SpecAxis> {
+ 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::<TemplateValue>(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::<Dir>(ctx, "dir").unwrap_or(Dir::TTB);
+ let children = args.all::<TemplateValue>(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::<Tracks>(ctx, "columns").unwrap_or_default();
+ let rows = args.named::<Tracks>(ctx, "rows").unwrap_or_default();
+ let gutter = args
+ .named::<Linear>(ctx, "gutter")
+ .map(|v| vec![TrackSizing::Linear(v)])
+ .unwrap_or_default();
+ let gutter_columns = args.named::<Tracks>(ctx, "gutter-columns");
+ let gutter_rows = args.named::<Tracks>(ctx, "gutter-rows");
+ let column_dir = args.named(ctx, "column-dir");
+ let row_dir = args.named(ctx, "row-dir");
+ let children = args.all::<TemplateValue>(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<TrackSizing>;
+
+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::<Value>(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::<Value>(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::<TemplateValue>(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::<Spanned<String>>(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::<TemplateValue>(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/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<Linear> = 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::<Dir>(ctx, "dir").unwrap_or(Dir::TTB);
- let children = args.all::<TemplateValue>(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
index 3f816c6d..f80b417c 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::<Linear>(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::<String>(ctx).map(|s| lang_dir(&s));
+ let dir = match args.named::<Spanned<Dir>>(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<Rc<LineState>>,
+) -> Value {
+ let color = args.named(ctx, "color");
+ let position = args.named(ctx, "position");
+ let strength = args.named::<Linear>(ctx, "strength");
+ let extent = args.named(ctx, "extent").unwrap_or_default();
+ let body = args.eat::<TemplateValue>(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
index e0e464ae..146fce9c 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::<Value>(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::<Value>(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::<Value>(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::<Value>(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.