summaryrefslogtreecommitdiff
path: root/crates/typst-library/src/compute
diff options
context:
space:
mode:
Diffstat (limited to 'crates/typst-library/src/compute')
-rw-r--r--crates/typst-library/src/compute/calc.rs1024
-rw-r--r--crates/typst-library/src/compute/construct.rs743
-rw-r--r--crates/typst-library/src/compute/data.rs492
-rw-r--r--crates/typst-library/src/compute/foundations.rs215
-rw-r--r--crates/typst-library/src/compute/mod.rs39
5 files changed, 2513 insertions, 0 deletions
diff --git a/crates/typst-library/src/compute/calc.rs b/crates/typst-library/src/compute/calc.rs
new file mode 100644
index 00000000..81715007
--- /dev/null
+++ b/crates/typst-library/src/compute/calc.rs
@@ -0,0 +1,1024 @@
+//! Calculations and processing of numeric values.
+
+use std::cmp;
+use std::cmp::Ordering;
+use std::ops::{Div, Rem};
+
+use typst::eval::{Module, Scope};
+
+use crate::prelude::*;
+
+/// A module with computational functions.
+pub fn module() -> Module {
+ let mut scope = Scope::new();
+ scope.define("abs", abs_func());
+ scope.define("pow", pow_func());
+ scope.define("exp", exp_func());
+ scope.define("sqrt", sqrt_func());
+ scope.define("sin", sin_func());
+ scope.define("cos", cos_func());
+ scope.define("tan", tan_func());
+ scope.define("asin", asin_func());
+ scope.define("acos", acos_func());
+ scope.define("atan", atan_func());
+ scope.define("atan2", atan2_func());
+ scope.define("sinh", sinh_func());
+ scope.define("cosh", cosh_func());
+ scope.define("tanh", tanh_func());
+ scope.define("log", log_func());
+ scope.define("ln", ln_func());
+ scope.define("fact", fact_func());
+ scope.define("perm", perm_func());
+ scope.define("binom", binom_func());
+ scope.define("gcd", gcd_func());
+ scope.define("lcm", lcm_func());
+ scope.define("floor", floor_func());
+ scope.define("ceil", ceil_func());
+ scope.define("trunc", trunc_func());
+ scope.define("fract", fract_func());
+ scope.define("round", round_func());
+ scope.define("clamp", clamp_func());
+ scope.define("min", min_func());
+ scope.define("max", max_func());
+ scope.define("even", even_func());
+ scope.define("odd", odd_func());
+ scope.define("rem", rem_func());
+ scope.define("quo", quo_func());
+ scope.define("inf", f64::INFINITY);
+ scope.define("nan", f64::NAN);
+ scope.define("pi", std::f64::consts::PI);
+ scope.define("e", std::f64::consts::E);
+ Module::new("calc").with_scope(scope)
+}
+
+/// Calculates the absolute value of a numeric value.
+///
+/// ## Example { #example }
+/// ```example
+/// #calc.abs(-5) \
+/// #calc.abs(5pt - 2cm) \
+/// #calc.abs(2fr)
+/// ```
+///
+/// Display: Absolute
+/// Category: calculate
+#[func]
+pub fn abs(
+ /// The value whose absolute value to calculate.
+ value: ToAbs,
+) -> Value {
+ value.0
+}
+
+/// A value of which the absolute value can be taken.
+pub struct ToAbs(Value);
+
+cast! {
+ ToAbs,
+ v: i64 => Self(v.abs().into_value()),
+ v: f64 => Self(v.abs().into_value()),
+ v: Length => Self(Value::Length(v.try_abs()
+ .ok_or("cannot take absolute value of this length")?)),
+ v: Angle => Self(Value::Angle(v.abs())),
+ v: Ratio => Self(Value::Ratio(v.abs())),
+ v: Fr => Self(Value::Fraction(v.abs())),
+}
+
+/// Raises a value to some exponent.
+///
+/// ## Example { #example }
+/// ```example
+/// #calc.pow(2, 3)
+/// ```
+///
+/// Display: Power
+/// Category: calculate
+#[func]
+pub fn pow(
+ /// The base of the power.
+ base: Num,
+ /// The exponent of the power.
+ exponent: Spanned<Num>,
+ /// The callsite span.
+ span: Span,
+) -> SourceResult<Num> {
+ match exponent.v {
+ _ if exponent.v.float() == 0.0 && base.float() == 0.0 => {
+ bail!(span, "zero to the power of zero is undefined")
+ }
+ Num::Int(i) if i32::try_from(i).is_err() => {
+ bail!(exponent.span, "exponent is too large")
+ }
+ Num::Float(f) if !f.is_normal() && f != 0.0 => {
+ bail!(exponent.span, "exponent may not be infinite, subnormal, or NaN")
+ }
+ _ => {}
+ };
+
+ let result = match (base, exponent.v) {
+ (Num::Int(a), Num::Int(b)) if b >= 0 => a
+ .checked_pow(b as u32)
+ .map(Num::Int)
+ .ok_or("the result is too large")
+ .at(span)?,
+ (a, b) => Num::Float(if a.float() == std::f64::consts::E {
+ b.float().exp()
+ } else if a.float() == 2.0 {
+ b.float().exp2()
+ } else if let Num::Int(b) = b {
+ a.float().powi(b as i32)
+ } else {
+ a.float().powf(b.float())
+ }),
+ };
+
+ if result.float().is_nan() {
+ bail!(span, "the result is not a real number")
+ }
+
+ Ok(result)
+}
+
+/// Raises a value to some exponent of e.
+///
+/// ## Example { #example }
+/// ```example
+/// #calc.exp(1)
+/// ```
+///
+/// Display: Exponential
+/// Category: calculate
+#[func]
+pub fn exp(
+ /// The exponent of the power.
+ exponent: Spanned<Num>,
+ /// The callsite span.
+ span: Span,
+) -> SourceResult<f64> {
+ match exponent.v {
+ Num::Int(i) if i32::try_from(i).is_err() => {
+ bail!(exponent.span, "exponent is too large")
+ }
+ Num::Float(f) if !f.is_normal() && f != 0.0 => {
+ bail!(exponent.span, "exponent may not be infinite, subnormal, or NaN")
+ }
+ _ => {}
+ };
+
+ let result = exponent.v.float().exp();
+ if result.is_nan() {
+ bail!(span, "the result is not a real number")
+ }
+
+ Ok(result)
+}
+
+/// Extracts the square root of a number.
+///
+/// ## Example { #example }
+/// ```example
+/// #calc.sqrt(16) \
+/// #calc.sqrt(2.5)
+/// ```
+///
+/// Display: Square Root
+/// Category: calculate
+#[func]
+pub fn sqrt(
+ /// The number whose square root to calculate. Must be non-negative.
+ value: Spanned<Num>,
+) -> SourceResult<f64> {
+ if value.v.float() < 0.0 {
+ bail!(value.span, "cannot take square root of negative number");
+ }
+ Ok(value.v.float().sqrt())
+}
+
+/// Calculates the sine of an angle.
+///
+/// When called with an integer or a float, they will be interpreted as
+/// radians.
+///
+/// ## Example { #example }
+/// ```example
+/// #assert(calc.sin(90deg) == calc.sin(-270deg))
+/// #calc.sin(1.5) \
+/// #calc.sin(90deg)
+/// ```
+///
+/// Display: Sine
+/// Category: calculate
+#[func]
+pub fn sin(
+ /// The angle whose sine to calculate.
+ angle: AngleLike,
+) -> f64 {
+ match angle {
+ AngleLike::Angle(a) => a.sin(),
+ AngleLike::Int(n) => (n as f64).sin(),
+ AngleLike::Float(n) => n.sin(),
+ }
+}
+
+/// Calculates the cosine of an angle.
+///
+/// When called with an integer or a float, they will be interpreted as
+/// radians.
+///
+/// ## Example { #example }
+/// ```example
+/// #calc.cos(90deg) \
+/// #calc.cos(1.5) \
+/// #calc.cos(90deg)
+/// ```
+///
+/// Display: Cosine
+/// Category: calculate
+#[func]
+pub fn cos(
+ /// The angle whose cosine to calculate.
+ angle: AngleLike,
+) -> f64 {
+ match angle {
+ AngleLike::Angle(a) => a.cos(),
+ AngleLike::Int(n) => (n as f64).cos(),
+ AngleLike::Float(n) => n.cos(),
+ }
+}
+
+/// Calculates the tangent of an angle.
+///
+/// When called with an integer or a float, they will be interpreted as
+/// radians.
+///
+/// ## Example { #example }
+/// ```example
+/// #calc.tan(1.5) \
+/// #calc.tan(90deg)
+/// ```
+///
+/// Display: Tangent
+/// Category: calculate
+#[func]
+pub fn tan(
+ /// The angle whose tangent to calculate.
+ angle: AngleLike,
+) -> f64 {
+ match angle {
+ AngleLike::Angle(a) => a.tan(),
+ AngleLike::Int(n) => (n as f64).tan(),
+ AngleLike::Float(n) => n.tan(),
+ }
+}
+
+/// Calculates the arcsine of a number.
+///
+/// ## Example { #example }
+/// ```example
+/// #calc.asin(0) \
+/// #calc.asin(1)
+/// ```
+///
+/// Display: Arcsine
+/// Category: calculate
+#[func]
+pub fn asin(
+ /// The number whose arcsine to calculate. Must be between -1 and 1.
+ value: Spanned<Num>,
+) -> SourceResult<Angle> {
+ let val = value.v.float();
+ if val < -1.0 || val > 1.0 {
+ bail!(value.span, "value must be between -1 and 1");
+ }
+ Ok(Angle::rad(val.asin()))
+}
+
+/// Calculates the arccosine of a number.
+///
+/// ## Example { #example }
+/// ```example
+/// #calc.acos(0) \
+/// #calc.acos(1)
+/// ```
+///
+/// Display: Arccosine
+/// Category: calculate
+#[func]
+pub fn acos(
+ /// The number whose arcsine to calculate. Must be between -1 and 1.
+ value: Spanned<Num>,
+) -> SourceResult<Angle> {
+ let val = value.v.float();
+ if val < -1.0 || val > 1.0 {
+ bail!(value.span, "value must be between -1 and 1");
+ }
+ Ok(Angle::rad(val.acos()))
+}
+
+/// Calculates the arctangent of a number.
+///
+/// ## Example { #example }
+/// ```example
+/// #calc.atan(0) \
+/// #calc.atan(1)
+/// ```
+///
+/// Display: Arctangent
+/// Category: calculate
+#[func]
+pub fn atan(
+ /// The number whose arctangent to calculate.
+ value: Num,
+) -> Angle {
+ Angle::rad(value.float().atan())
+}
+
+/// Calculates the four-quadrant arctangent of a coordinate.
+///
+/// The arguments are `(x, y)`, not `(y, x)`.
+///
+/// ## Example { #example }
+/// ```example
+/// #calc.atan2(1, 1) \
+/// #calc.atan2(-2, -3)
+/// ```
+///
+/// Display: Four-quadrant Arctangent
+/// Category: calculate
+#[func]
+pub fn atan2(
+ /// The X coordinate.
+ x: Num,
+ /// The Y coordinate.
+ y: Num,
+) -> Angle {
+ Angle::rad(f64::atan2(y.float(), x.float()))
+}
+
+/// Calculates the hyperbolic sine of an angle.
+///
+/// When called with an integer or a float, they will be interpreted as radians.
+///
+/// ## Example { #example }
+/// ```example
+/// #calc.sinh(0) \
+/// #calc.sinh(45deg)
+/// ```
+///
+/// Display: Hyperbolic sine
+/// Category: calculate
+#[func]
+pub fn sinh(
+ /// The angle whose hyperbolic sine to calculate.
+ angle: AngleLike,
+) -> f64 {
+ match angle {
+ AngleLike::Angle(a) => a.to_rad().sinh(),
+ AngleLike::Int(n) => (n as f64).sinh(),
+ AngleLike::Float(n) => n.sinh(),
+ }
+}
+
+/// Calculates the hyperbolic cosine of an angle.
+///
+/// When called with an integer or a float, they will be interpreted as radians.
+///
+/// ## Example { #example }
+/// ```example
+/// #calc.cosh(0) \
+/// #calc.cosh(45deg)
+/// ```
+///
+/// Display: Hyperbolic cosine
+/// Category: calculate
+#[func]
+pub fn cosh(
+ /// The angle whose hyperbolic cosine to calculate.
+ angle: AngleLike,
+) -> f64 {
+ match angle {
+ AngleLike::Angle(a) => a.to_rad().cosh(),
+ AngleLike::Int(n) => (n as f64).cosh(),
+ AngleLike::Float(n) => n.cosh(),
+ }
+}
+
+/// Calculates the hyperbolic tangent of an angle.
+///
+/// When called with an integer or a float, they will be interpreted as radians.
+///
+/// ## Example { #example }
+/// ```example
+/// #calc.tanh(0) \
+/// #calc.tanh(45deg)
+/// ```
+///
+/// Display: Hyperbolic tangent
+/// Category: calculate
+#[func]
+pub fn tanh(
+ /// The angle whose hyperbolic tangent to calculate.
+ angle: AngleLike,
+) -> f64 {
+ match angle {
+ AngleLike::Angle(a) => a.to_rad().tanh(),
+ AngleLike::Int(n) => (n as f64).tanh(),
+ AngleLike::Float(n) => n.tanh(),
+ }
+}
+
+/// Calculates the logarithm of a number.
+///
+/// If the base is not specified, the logarithm is calculated in base 10.
+///
+/// ## Example { #example }
+/// ```example
+/// #calc.log(100)
+/// ```
+///
+/// Display: Logarithm
+/// Category: calculate
+#[func]
+pub fn log(
+ /// The number whose logarithm to calculate. Must be strictly positive.
+ value: Spanned<Num>,
+ /// The base of the logarithm. May not be zero.
+ #[named]
+ #[default(Spanned::new(10.0, Span::detached()))]
+ base: Spanned<f64>,
+ /// The callsite span.
+ span: Span,
+) -> SourceResult<f64> {
+ let number = value.v.float();
+ if number <= 0.0 {
+ bail!(value.span, "value must be strictly positive")
+ }
+
+ if !base.v.is_normal() {
+ bail!(base.span, "base may not be zero, NaN, infinite, or subnormal")
+ }
+
+ let result = if base.v == std::f64::consts::E {
+ number.ln()
+ } else if base.v == 2.0 {
+ number.log2()
+ } else if base.v == 10.0 {
+ number.log10()
+ } else {
+ number.log(base.v)
+ };
+
+ if result.is_infinite() || result.is_nan() {
+ bail!(span, "the result is not a real number")
+ }
+
+ Ok(result)
+}
+
+/// Calculates the natural logarithm of a number.
+///
+/// ## Example { #example }
+/// ```example
+/// #calc.ln(calc.e)
+/// ```
+///
+/// Display: Natural Logarithm
+/// Category: calculate
+#[func]
+pub fn ln(
+ /// The number whose logarithm to calculate. Must be strictly positive.
+ value: Spanned<Num>,
+ /// The callsite span.
+ span: Span,
+) -> SourceResult<f64> {
+ let number = value.v.float();
+ if number <= 0.0 {
+ bail!(value.span, "value must be strictly positive")
+ }
+
+ let result = number.ln();
+ if result.is_infinite() {
+ bail!(span, "result close to -inf")
+ }
+
+ Ok(result)
+}
+
+/// Calculates the factorial of a number.
+///
+/// ## Example { #example }
+/// ```example
+/// #calc.fact(5)
+/// ```
+///
+/// Display: Factorial
+/// Category: calculate
+#[func]
+pub fn fact(
+ /// The number whose factorial to calculate. Must be non-negative.
+ number: u64,
+) -> StrResult<i64> {
+ Ok(fact_impl(1, number).ok_or("the result is too large")?)
+}
+
+/// Calculates a permutation.
+///
+/// ## Example { #example }
+/// ```example
+/// #calc.perm(10, 5)
+/// ```
+///
+/// Display: Permutation
+/// Category: calculate
+#[func]
+pub fn perm(
+ /// The base number. Must be non-negative.
+ base: u64,
+ /// The number of permutations. Must be non-negative.
+ numbers: u64,
+) -> StrResult<i64> {
+ // By convention.
+ if base < numbers {
+ return Ok(0);
+ }
+
+ Ok(fact_impl(base - numbers + 1, base).ok_or("the result is too large")?)
+}
+
+/// Calculates the product of a range of numbers. Used to calculate
+/// permutations. Returns None if the result is larger than `i64::MAX`
+fn fact_impl(start: u64, end: u64) -> Option<i64> {
+ // By convention
+ if end + 1 < start {
+ return Some(0);
+ }
+
+ let real_start: u64 = cmp::max(1, start);
+ let mut count: u64 = 1;
+ for i in real_start..=end {
+ count = count.checked_mul(i)?;
+ }
+
+ count.try_into().ok()
+}
+
+/// Calculates a binomial coefficient.
+///
+/// ## Example { #example }
+/// ```example
+/// #calc.binom(10, 5)
+/// ```
+///
+/// Display: Binomial
+/// Category: calculate
+#[func]
+pub fn binom(
+ /// The upper coefficient. Must be non-negative.
+ n: u64,
+ /// The lower coefficient. Must be non-negative.
+ k: u64,
+) -> StrResult<i64> {
+ Ok(binom_impl(n, k).ok_or("the result is too large")?)
+}
+
+/// Calculates a binomial coefficient, with `n` the upper coefficient and `k`
+/// the lower coefficient. Returns `None` if the result is larger than
+/// `i64::MAX`
+fn binom_impl(n: u64, k: u64) -> Option<i64> {
+ if k > n {
+ return Some(0);
+ }
+
+ // By symmetry
+ let real_k = cmp::min(n - k, k);
+ if real_k == 0 {
+ return Some(1);
+ }
+
+ let mut result: u64 = 1;
+ for i in 0..real_k {
+ result = result.checked_mul(n - i)?.checked_div(i + 1)?;
+ }
+
+ result.try_into().ok()
+}
+
+/// Calculates the greatest common divisor of two integers.
+///
+/// ## Example { #example }
+/// ```example
+/// #calc.gcd(7, 42)
+/// ```
+///
+/// Display: Greatest Common Divisor
+/// Category: calculate
+#[func]
+pub fn gcd(
+ /// The first integer.
+ a: i64,
+ /// The second integer.
+ b: i64,
+) -> i64 {
+ let (mut a, mut b) = (a, b);
+ while b != 0 {
+ let temp = b;
+ b = a % b;
+ a = temp;
+ }
+
+ a.abs()
+}
+
+/// Calculates the least common multiple of two integers.
+///
+/// ## Example { #example }
+/// ```example
+/// #calc.lcm(96, 13)
+/// ```
+///
+/// Display: Least Common Multiple
+/// Category: calculate
+#[func]
+pub fn lcm(
+ /// The first integer.
+ a: i64,
+ /// The second integer.
+ b: i64,
+) -> StrResult<i64> {
+ if a == b {
+ return Ok(a.abs());
+ }
+
+ Ok(a.checked_div(gcd(a, b))
+ .and_then(|gcd| gcd.checked_mul(b))
+ .map(|v| v.abs())
+ .ok_or("the return value is too large")?)
+}
+
+/// Rounds a number down to the nearest integer.
+///
+/// If the number is already an integer, it is returned unchanged.
+///
+/// ## Example { #example }
+/// ```example
+/// #assert(calc.floor(3.14) == 3)
+/// #assert(calc.floor(3) == 3)
+/// #calc.floor(500.1)
+/// ```
+///
+/// Display: Round down
+/// Category: calculate
+#[func]
+pub fn floor(
+ /// The number to round down.
+ value: Num,
+) -> i64 {
+ match value {
+ Num::Int(n) => n,
+ Num::Float(n) => n.floor() as i64,
+ }
+}
+
+/// Rounds a number up to the nearest integer.
+///
+/// If the number is already an integer, it is returned unchanged.
+///
+/// ## Example { #example }
+/// ```example
+/// #assert(calc.ceil(3.14) == 4)
+/// #assert(calc.ceil(3) == 3)
+/// #calc.ceil(500.1)
+/// ```
+///
+/// Display: Round up
+/// Category: calculate
+#[func]
+pub fn ceil(
+ /// The number to round up.
+ value: Num,
+) -> i64 {
+ match value {
+ Num::Int(n) => n,
+ Num::Float(n) => n.ceil() as i64,
+ }
+}
+
+/// Returns the integer part of a number.
+///
+/// If the number is already an integer, it is returned unchanged.
+///
+/// ## Example { #example }
+/// ```example
+/// #assert(calc.trunc(3) == 3)
+/// #assert(calc.trunc(-3.7) == -3)
+/// #assert(calc.trunc(15.9) == 15)
+/// ```
+///
+/// Display: Truncate
+/// Category: calculate
+#[func]
+pub fn trunc(
+ /// The number to truncate.
+ value: Num,
+) -> i64 {
+ match value {
+ Num::Int(n) => n,
+ Num::Float(n) => n.trunc() as i64,
+ }
+}
+
+/// Returns the fractional part of a number.
+///
+/// If the number is an integer, returns `0`.
+///
+/// ## Example { #example }
+/// ```example
+/// #assert(calc.fract(3) == 0)
+/// #calc.fract(-3.1)
+/// ```
+///
+/// Display: Fractional
+/// Category: calculate
+#[func]
+pub fn fract(
+ /// The number to truncate.
+ value: Num,
+) -> Num {
+ match value {
+ Num::Int(_) => Num::Int(0),
+ Num::Float(n) => Num::Float(n.fract()),
+ }
+}
+
+/// Rounds a number to the nearest integer.
+///
+/// Optionally, a number of decimal places can be specified.
+///
+/// ## Example { #example }
+/// ```example
+/// #assert(calc.round(3.14) == 3)
+/// #assert(calc.round(3.5) == 4)
+/// #calc.round(3.1415, digits: 2)
+/// ```
+///
+/// Display: Round
+/// Category: calculate
+#[func]
+pub fn round(
+ /// The number to round.
+ value: Num,
+ /// The number of decimal places.
+ #[named]
+ #[default(0)]
+ digits: i64,
+) -> Num {
+ match value {
+ Num::Int(n) if digits == 0 => Num::Int(n),
+ _ => {
+ let n = value.float();
+ let factor = 10.0_f64.powi(digits as i32);
+ Num::Float((n * factor).round() / factor)
+ }
+ }
+}
+
+/// Clamps a number between a minimum and maximum value.
+///
+/// ## Example { #example }
+/// ```example
+/// #assert(calc.clamp(5, 0, 10) == 5)
+/// #assert(calc.clamp(5, 6, 10) == 6)
+/// #calc.clamp(5, 0, 4)
+/// ```
+///
+/// Display: Clamp
+/// Category: calculate
+#[func]
+pub fn clamp(
+ /// The number to clamp.
+ value: Num,
+ /// The inclusive minimum value.
+ min: Num,
+ /// The inclusive maximum value.
+ max: Spanned<Num>,
+) -> SourceResult<Num> {
+ if max.v.float() < min.float() {
+ bail!(max.span, "max must be greater than or equal to min")
+ }
+ Ok(value.apply3(min, max.v, i64::clamp, f64::clamp))
+}
+
+/// Determines the minimum of a sequence of values.
+///
+/// ## Example { #example }
+/// ```example
+/// #calc.min(1, -3, -5, 20, 3, 6) \
+/// #calc.min("typst", "in", "beta")
+/// ```
+///
+/// Display: Minimum
+/// Category: calculate
+#[func]
+pub fn min(
+ /// The sequence of values from which to extract the minimum.
+ /// Must not be empty.
+ #[variadic]
+ values: Vec<Spanned<Value>>,
+ /// The callsite span.
+ span: Span,
+) -> SourceResult<Value> {
+ minmax(span, values, Ordering::Less)
+}
+
+/// Determines the maximum of a sequence of values.
+///
+/// ## Example { #example }
+/// ```example
+/// #calc.max(1, -3, -5, 20, 3, 6) \
+/// #calc.max("typst", "in", "beta")
+/// ```
+///
+/// Display: Maximum
+/// Category: calculate
+#[func]
+pub fn max(
+ /// The sequence of values from which to extract the maximum.
+ /// Must not be empty.
+ #[variadic]
+ values: Vec<Spanned<Value>>,
+ /// The callsite span.
+ span: Span,
+) -> SourceResult<Value> {
+ minmax(span, values, Ordering::Greater)
+}
+
+/// Find the minimum or maximum of a sequence of values.
+fn minmax(
+ span: Span,
+ values: Vec<Spanned<Value>>,
+ goal: Ordering,
+) -> SourceResult<Value> {
+ let mut iter = values.into_iter();
+ let Some(Spanned { v: mut extremum, ..}) = iter.next() else {
+ bail!(span, "expected at least one value");
+ };
+
+ for Spanned { v, span } in iter {
+ let ordering = typst::eval::ops::compare(&v, &extremum).at(span)?;
+ if ordering == goal {
+ extremum = v;
+ }
+ }
+
+ Ok(extremum)
+}
+
+/// Determines whether an integer is even.
+///
+/// ## Example { #example }
+/// ```example
+/// #calc.even(4) \
+/// #calc.even(5) \
+/// #range(10).filter(calc.even)
+/// ```
+///
+/// Display: Even
+/// Category: calculate
+#[func]
+pub fn even(
+ /// The number to check for evenness.
+ value: i64,
+) -> bool {
+ value % 2 == 0
+}
+
+/// Determines whether an integer is odd.
+///
+/// ## Example { #example }
+/// ```example
+/// #calc.odd(4) \
+/// #calc.odd(5) \
+/// #range(10).filter(calc.odd)
+/// ```
+///
+/// Display: Odd
+/// Category: calculate
+#[func]
+pub fn odd(
+ /// The number to check for oddness.
+ value: i64,
+) -> bool {
+ value % 2 != 0
+}
+
+/// Calculates the remainder of two numbers.
+///
+/// ## Example { #example }
+/// ```example
+/// #calc.rem(20, 6) \
+/// #calc.rem(1.75, 0.5)
+/// ```
+///
+/// Display: Remainder
+/// Category: calculate
+#[func]
+pub fn rem(
+ /// The dividend of the remainder.
+ dividend: Num,
+ /// The divisor of the remainder.
+ divisor: Spanned<Num>,
+) -> SourceResult<Num> {
+ if divisor.v.float() == 0.0 {
+ bail!(divisor.span, "divisor must not be zero");
+ }
+ Ok(dividend.apply2(divisor.v, Rem::rem, Rem::rem))
+}
+
+/// Calculates the quotient of two numbers.
+///
+/// ## Example { #example }
+/// ```example
+/// #calc.quo(14, 5) \
+/// #calc.quo(3.46, 0.5)
+/// ```
+///
+/// Display: Quotient
+/// Category: calculate
+#[func]
+pub fn quo(
+ /// The dividend of the quotient.
+ dividend: Num,
+ /// The divisor of the quotient.
+ divisor: Spanned<Num>,
+) -> SourceResult<i64> {
+ if divisor.v.float() == 0.0 {
+ bail!(divisor.span, "divisor must not be zero");
+ }
+
+ Ok(floor(dividend.apply2(divisor.v, Div::div, Div::div)))
+}
+
+/// A value which can be passed to functions that work with integers and floats.
+#[derive(Debug, Copy, Clone)]
+pub enum Num {
+ Int(i64),
+ Float(f64),
+}
+
+impl Num {
+ pub fn apply2(
+ self,
+ other: Self,
+ int: impl FnOnce(i64, i64) -> i64,
+ float: impl FnOnce(f64, f64) -> f64,
+ ) -> Num {
+ match (self, other) {
+ (Self::Int(a), Self::Int(b)) => Num::Int(int(a, b)),
+ (a, b) => Num::Float(float(a.float(), b.float())),
+ }
+ }
+
+ pub fn apply3(
+ self,
+ other: Self,
+ third: Self,
+ int: impl FnOnce(i64, i64, i64) -> i64,
+ float: impl FnOnce(f64, f64, f64) -> f64,
+ ) -> Num {
+ match (self, other, third) {
+ (Self::Int(a), Self::Int(b), Self::Int(c)) => Num::Int(int(a, b, c)),
+ (a, b, c) => Num::Float(float(a.float(), b.float(), c.float())),
+ }
+ }
+
+ pub fn float(self) -> f64 {
+ match self {
+ Self::Int(v) => v as f64,
+ Self::Float(v) => v,
+ }
+ }
+}
+
+cast! {
+ Num,
+ self => match self {
+ Self::Int(v) => v.into_value(),
+ Self::Float(v) => v.into_value(),
+ },
+ v: i64 => Self::Int(v),
+ v: f64 => Self::Float(v),
+}
+
+/// A value that can be passed to a trigonometric function.
+pub enum AngleLike {
+ Int(i64),
+ Float(f64),
+ Angle(Angle),
+}
+
+cast! {
+ AngleLike,
+ v: i64 => Self::Int(v),
+ v: f64 => Self::Float(v),
+ v: Angle => Self::Angle(v),
+}
diff --git a/crates/typst-library/src/compute/construct.rs b/crates/typst-library/src/compute/construct.rs
new file mode 100644
index 00000000..956212ee
--- /dev/null
+++ b/crates/typst-library/src/compute/construct.rs
@@ -0,0 +1,743 @@
+use std::num::NonZeroI64;
+use std::str::FromStr;
+
+use time::{Month, PrimitiveDateTime};
+
+use typst::eval::{Datetime, Regex};
+
+use crate::prelude::*;
+
+/// Converts a value to an integer.
+///
+/// - Booleans are converted to `0` or `1`.
+/// - Floats are floored to the next 64-bit integer.
+/// - Strings are parsed in base 10.
+///
+/// ## Example { #example }
+/// ```example
+/// #int(false) \
+/// #int(true) \
+/// #int(2.7) \
+/// #{ int("27") + int("4") }
+/// ```
+///
+/// Display: Integer
+/// Category: construct
+#[func]
+pub fn int(
+ /// The value that should be converted to an integer.
+ value: ToInt,
+) -> i64 {
+ value.0
+}
+
+/// A value that can be cast to an integer.
+pub struct ToInt(i64);
+
+cast! {
+ ToInt,
+ v: bool => Self(v as i64),
+ v: i64 => Self(v),
+ v: f64 => Self(v as i64),
+ v: EcoString => Self(v.parse().map_err(|_| eco_format!("invalid integer: {}", v))?),
+}
+
+/// Converts a value to a float.
+///
+/// - Booleans are converted to `0.0` or `1.0`.
+/// - Integers are converted to the closest 64-bit float.
+/// - Ratios are divided by 100%.
+/// - Strings are parsed in base 10 to the closest 64-bit float.
+/// Exponential notation is supported.
+///
+/// ## Example { #example }
+/// ```example
+/// #float(false) \
+/// #float(true) \
+/// #float(4) \
+/// #float(40%) \
+/// #float("2.7") \
+/// #float("1e5")
+/// ```
+///
+/// Display: Float
+/// Category: construct
+#[func]
+pub fn float(
+ /// The value that should be converted to a float.
+ value: ToFloat,
+) -> f64 {
+ value.0
+}
+
+/// A value that can be cast to a float.
+pub struct ToFloat(f64);
+
+cast! {
+ ToFloat,
+ v: bool => Self(v as i64 as f64),
+ v: i64 => Self(v as f64),
+ v: f64 => Self(v),
+ v: Ratio => Self(v.get()),
+ v: EcoString => Self(v.parse().map_err(|_| eco_format!("invalid float: {}", v))?),
+}
+
+/// Creates a grayscale color.
+///
+/// ## Example { #example }
+/// ```example
+/// #for x in range(250, step: 50) {
+/// box(square(fill: luma(x)))
+/// }
+/// ```
+///
+/// Display: Luma
+/// Category: construct
+#[func]
+pub fn luma(
+ /// The gray component.
+ gray: Component,
+) -> Color {
+ LumaColor::new(gray.0).into()
+}
+
+/// Creates an RGB(A) color.
+///
+/// The color is specified in the sRGB color space.
+///
+/// _Note:_ While you can specify transparent colors and Typst's preview will
+/// render them correctly, the PDF export does not handle them properly at the
+/// moment. This will be fixed in the future.
+///
+/// ## Example { #example }
+/// ```example
+/// #square(fill: rgb("#b1f2eb"))
+/// #square(fill: rgb(87, 127, 230))
+/// #square(fill: rgb(25%, 13%, 65%))
+/// ```
+///
+/// Display: RGB
+/// Category: construct
+#[func]
+pub fn rgb(
+ /// The color in hexadecimal notation.
+ ///
+ /// Accepts three, four, six or eight hexadecimal digits and optionally
+ /// a leading hashtag.
+ ///
+ /// If this string is given, the individual components should not be given.
+ ///
+ /// ```example
+ /// #text(16pt, rgb("#239dad"))[
+ /// *Typst*
+ /// ]
+ /// ```
+ #[external]
+ hex: EcoString,
+ /// The red component.
+ #[external]
+ red: Component,
+ /// The green component.
+ #[external]
+ green: Component,
+ /// The blue component.
+ #[external]
+ blue: Component,
+ /// The alpha component.
+ #[external]
+ alpha: Component,
+ /// The arguments.
+ args: Args,
+) -> SourceResult<Color> {
+ let mut args = args;
+ Ok(if let Some(string) = args.find::<Spanned<EcoString>>()? {
+ match RgbaColor::from_str(&string.v) {
+ Ok(color) => color.into(),
+ Err(msg) => bail!(string.span, "{msg}"),
+ }
+ } else {
+ let Component(r) = args.expect("red component")?;
+ let Component(g) = args.expect("green component")?;
+ let Component(b) = args.expect("blue component")?;
+ let Component(a) = args.eat()?.unwrap_or(Component(255));
+ RgbaColor::new(r, g, b, a).into()
+ })
+}
+
+/// An integer or ratio component.
+pub struct Component(u8);
+
+cast! {
+ Component,
+ v: i64 => match v {
+ 0 ..= 255 => Self(v as u8),
+ _ => bail!("number must be between 0 and 255"),
+ },
+ v: Ratio => if (0.0 ..= 1.0).contains(&v.get()) {
+ Self((v.get() * 255.0).round() as u8)
+ } else {
+ bail!("ratio must be between 0% and 100%");
+ },
+}
+
+/// Creates a new datetime.
+///
+/// You can specify the [datetime]($type/datetime) using a year, month, day,
+/// hour, minute, and second. You can also get the current date with
+/// [`datetime.today`]($func/datetime.today).
+///
+/// ## Example
+/// ```example
+/// #let date = datetime(
+/// year: 2012,
+/// month: 8,
+/// day: 3,
+/// )
+///
+/// #date.display() \
+/// #date.display(
+/// "[day].[month].[year]"
+/// )
+/// ```
+///
+/// ## Format
+/// _Note_: Depending on which components of the datetime you specify, Typst
+/// will store it in one of the following three ways:
+/// * If you specify year, month and day, Typst will store just a date.
+/// * If you specify hour, minute and second, Typst will store just a time.
+/// * If you specify all of year, month, day, hour, minute and second, Typst
+/// will store a full datetime.
+///
+/// Depending on how it is stored, the [`display`]($type/datetime.display)
+/// method will choose a different formatting by default.
+///
+/// Display: Datetime
+/// Category: construct
+#[func]
+#[scope(
+ scope.define("today", datetime_today_func());
+ scope
+)]
+pub fn datetime(
+ /// The year of the datetime.
+ #[named]
+ year: Option<YearComponent>,
+ /// The month of the datetime.
+ #[named]
+ month: Option<MonthComponent>,
+ /// The day of the datetime.
+ #[named]
+ day: Option<DayComponent>,
+ /// The hour of the datetime.
+ #[named]
+ hour: Option<HourComponent>,
+ /// The minute of the datetime.
+ #[named]
+ minute: Option<MinuteComponent>,
+ /// The second of the datetime.
+ #[named]
+ second: Option<SecondComponent>,
+) -> StrResult<Datetime> {
+ let time = match (hour, minute, second) {
+ (Some(hour), Some(minute), Some(second)) => {
+ match time::Time::from_hms(hour.0, minute.0, second.0) {
+ Ok(time) => Some(time),
+ Err(_) => bail!("time is invalid"),
+ }
+ }
+ (None, None, None) => None,
+ _ => bail!("time is incomplete"),
+ };
+
+ let date = match (year, month, day) {
+ (Some(year), Some(month), Some(day)) => {
+ match time::Date::from_calendar_date(year.0, month.0, day.0) {
+ Ok(date) => Some(date),
+ Err(_) => bail!("date is invalid"),
+ }
+ }
+ (None, None, None) => None,
+ _ => bail!("date is incomplete"),
+ };
+
+ Ok(match (date, time) {
+ (Some(date), Some(time)) => {
+ Datetime::Datetime(PrimitiveDateTime::new(date, time))
+ }
+ (Some(date), None) => Datetime::Date(date),
+ (None, Some(time)) => Datetime::Time(time),
+ (None, None) => {
+ bail!("at least one of date or time must be fully specified")
+ }
+ })
+}
+
+pub struct YearComponent(i32);
+pub struct MonthComponent(Month);
+pub struct DayComponent(u8);
+pub struct HourComponent(u8);
+pub struct MinuteComponent(u8);
+pub struct SecondComponent(u8);
+
+cast! {
+ YearComponent,
+ v: i32 => Self(v),
+}
+
+cast! {
+ MonthComponent,
+ v: u8 => Self(Month::try_from(v).map_err(|_| "month is invalid")?)
+}
+
+cast! {
+ DayComponent,
+ v: u8 => Self(v),
+}
+
+cast! {
+ HourComponent,
+ v: u8 => Self(v),
+}
+
+cast! {
+ MinuteComponent,
+ v: u8 => Self(v),
+}
+
+cast! {
+ SecondComponent,
+ v: u8 => Self(v),
+}
+
+/// Returns the current date.
+///
+/// Refer to the documentation of the [`display`]($type/datetime.display) method
+/// for details on how to affect the formatting of the date.
+///
+/// ## Example
+/// ```example
+/// Today's date is
+/// #datetime.today().display().
+/// ```
+///
+/// Display: Today
+/// Category: construct
+#[func]
+pub fn datetime_today(
+ /// An offset to apply to the current UTC date. If set to `{auto}`, the
+ /// offset will be the local offset.
+ #[named]
+ #[default]
+ offset: Smart<i64>,
+ /// The virtual machine.
+ vt: &mut Vt,
+) -> StrResult<Datetime> {
+ Ok(vt
+ .world
+ .today(offset.as_custom())
+ .ok_or("unable to get the current date")?)
+}
+
+/// Creates a CMYK color.
+///
+/// This is useful if you want to target a specific printer. The conversion
+/// to RGB for display preview might differ from how your printer reproduces
+/// the color.
+///
+/// ## Example { #example }
+/// ```example
+/// #square(
+/// fill: cmyk(27%, 0%, 3%, 5%)
+/// )
+/// ````
+///
+/// Display: CMYK
+/// Category: construct
+#[func]
+pub fn cmyk(
+ /// The cyan component.
+ cyan: RatioComponent,
+ /// The magenta component.
+ magenta: RatioComponent,
+ /// The yellow component.
+ yellow: RatioComponent,
+ /// The key component.
+ key: RatioComponent,
+) -> Color {
+ CmykColor::new(cyan.0, magenta.0, yellow.0, key.0).into()
+}
+
+/// A component that must be a ratio.
+pub struct RatioComponent(u8);
+
+cast! {
+ RatioComponent,
+ v: Ratio => if (0.0 ..= 1.0).contains(&v.get()) {
+ Self((v.get() * 255.0).round() as u8)
+ } else {
+ bail!("ratio must be between 0% and 100%");
+ },
+}
+
+/// Creates a custom symbol with modifiers.
+///
+/// ## Example { #example }
+/// ```example
+/// #let envelope = symbol(
+/// "🖂",
+/// ("stamped", "🖃"),
+/// ("stamped.pen", "🖆"),
+/// ("lightning", "🖄"),
+/// ("fly", "🖅"),
+/// )
+///
+/// #envelope
+/// #envelope.stamped
+/// #envelope.stamped.pen
+/// #envelope.lightning
+/// #envelope.fly
+/// ```
+///
+/// Display: Symbol
+/// Category: construct
+#[func]
+pub fn symbol(
+ /// The variants of the symbol.
+ ///
+ /// Can be a just a string consisting of a single character for the
+ /// modifierless variant or an array with two strings specifying the modifiers
+ /// and the symbol. Individual modifiers should be separated by dots. When
+ /// displaying a symbol, Typst selects the first from the variants that have
+ /// all attached modifiers and the minimum number of other modifiers.
+ #[variadic]
+ variants: Vec<Spanned<Variant>>,
+ /// The callsite span.
+ span: Span,
+) -> SourceResult<Symbol> {
+ let mut list = Vec::new();
+ if variants.is_empty() {
+ bail!(span, "expected at least one variant");
+ }
+ for Spanned { v, span } in variants {
+ if list.iter().any(|(prev, _)| &v.0 == prev) {
+ bail!(span, "duplicate variant");
+ }
+ list.push((v.0, v.1));
+ }
+ Ok(Symbol::runtime(list.into_boxed_slice()))
+}
+
+/// A value that can be cast to a symbol.
+pub struct Variant(EcoString, char);
+
+cast! {
+ Variant,
+ c: char => Self(EcoString::new(), c),
+ array: Array => {
+ let mut iter = array.into_iter();
+ match (iter.next(), iter.next(), iter.next()) {
+ (Some(a), Some(b), None) => Self(a.cast()?, b.cast()?),
+ _ => bail!("point array must contain exactly two entries"),
+ }
+ },
+}
+
+/// Converts a value to a string.
+///
+/// - Integers are formatted in base 10. This can be overridden with the
+/// optional `base` parameter.
+/// - Floats are formatted in base 10 and never in exponential notation.
+/// - From labels the name is extracted.
+///
+/// If you wish to convert from and to Unicode code points, see
+/// [`str.to-unicode`]($func/str.to-unicode) and
+/// [`str.from-unicode`]($func/str.from-unicode).
+///
+/// ## Example { #example }
+/// ```example
+/// #str(10) \
+/// #str(4000, base: 16) \
+/// #str(2.7) \
+/// #str(1e8) \
+/// #str(<intro>)
+/// ```
+///
+/// Display: String
+/// Category: construct
+#[func]
+#[scope(
+ scope.define("to-unicode", str_to_unicode_func());
+ scope.define("from-unicode", str_from_unicode_func());
+ scope
+)]
+pub fn str(
+ /// The value that should be converted to a string.
+ value: ToStr,
+ /// The base (radix) to display integers in, between 2 and 36.
+ #[named]
+ #[default(Spanned::new(10, Span::detached()))]
+ base: Spanned<i64>,
+) -> SourceResult<Str> {
+ Ok(match value {
+ ToStr::Str(s) => {
+ if base.v != 10 {
+ bail!(base.span, "base is only supported for integers");
+ }
+ s
+ }
+ ToStr::Int(n) => {
+ if base.v < 2 || base.v > 36 {
+ bail!(base.span, "base must be between 2 and 36");
+ }
+ int_to_base(n, base.v).into()
+ }
+ })
+}
+
+/// A value that can be cast to a string.
+pub enum ToStr {
+ /// A string value ready to be used as-is.
+ Str(Str),
+ /// An integer about to be formatted in a given base.
+ Int(i64),
+}
+
+cast! {
+ ToStr,
+ v: i64 => Self::Int(v),
+ v: f64 => Self::Str(format_str!("{}", v)),
+ v: Label => Self::Str(v.0.into()),
+ v: Str => Self::Str(v),
+}
+
+/// Format an integer in a base.
+fn int_to_base(mut n: i64, base: i64) -> EcoString {
+ if n == 0 {
+ return "0".into();
+ }
+
+ // In Rust, `format!("{:x}", -14i64)` is not `-e` but `fffffffffffffff2`.
+ // So we can only use the built-in for decimal, not bin/oct/hex.
+ if base == 10 {
+ return eco_format!("{n}");
+ }
+
+ // The largest output is `to_base(i64::MIN, 2)`, which is 65 chars long.
+ const SIZE: usize = 65;
+ let mut digits = [b'\0'; SIZE];
+ let mut i = SIZE;
+
+ // It's tempting to take the absolute value, but this will fail for i64::MIN.
+ // Instead, we turn n negative, as -i64::MAX is perfectly representable.
+ let negative = n < 0;
+ if n > 0 {
+ n = -n;
+ }
+
+ while n != 0 {
+ let digit = char::from_digit(-(n % base) as u32, base as u32);
+ i -= 1;
+ digits[i] = digit.unwrap_or('?') as u8;
+ n /= base;
+ }
+
+ if negative {
+ i -= 1;
+ digits[i] = b'-';
+ }
+
+ std::str::from_utf8(&digits[i..]).unwrap_or_default().into()
+}
+
+/// Converts a character into its corresponding code point.
+///
+/// ## Example
+/// ```example
+/// #str.to-unicode("a") \
+/// #"a\u{0300}".codepoints().map(str.to-unicode)
+/// ```
+///
+/// Display: String To Unicode
+/// Category: construct
+#[func]
+pub fn str_to_unicode(
+ /// The character that should be converted.
+ value: char,
+) -> u32 {
+ value.into()
+}
+
+/// Converts a Unicode code point into its corresponding string.
+///
+/// ```example
+/// #str.from-unicode(97)
+/// ```
+///
+/// Display: String From Unicode
+/// Category: construct
+#[func]
+pub fn str_from_unicode(
+ /// The code point that should be converted.
+ value: CodePoint,
+) -> Str {
+ format_str!("{}", value.0)
+}
+
+/// The numeric representation of a single unicode code point.
+pub struct CodePoint(char);
+
+cast! {
+ CodePoint,
+ v: i64 => {
+ Self(v.try_into().ok().and_then(|v: u32| v.try_into().ok()).ok_or_else(
+ || eco_format!("{:#x} is not a valid codepoint", v),
+ )?)
+ },
+}
+
+/// Creates a label from a string.
+///
+/// Inserting a label into content attaches it to the closest previous element
+/// that is not a space. Then, the element can be [referenced]($func/ref) and
+/// styled through the label.
+///
+/// ## Example { #example }
+/// ```example
+/// #show <a>: set text(blue)
+/// #show label("b"): set text(red)
+///
+/// = Heading <a>
+/// *Strong* #label("b")
+/// ```
+///
+/// ## Syntax { #syntax }
+/// This function also has dedicated syntax: You can create a label by enclosing
+/// its name in angle brackets. This works both in markup and code.
+///
+/// Display: Label
+/// Category: construct
+#[func]
+pub fn label(
+ /// The name of the label.
+ name: EcoString,
+) -> Label {
+ Label(name)
+}
+
+/// Creates a regular expression from a string.
+///
+/// The result can be used as a
+/// [show rule selector]($styling/#show-rules) and with
+/// [string methods]($type/string) like `find`, `split`, and `replace`.
+///
+/// See [the specification of the supported syntax](https://docs.rs/regex/latest/regex/#syntax).
+///
+/// ## Example { #example }
+/// ```example
+/// // Works with show rules.
+/// #show regex("\d+"): set text(red)
+///
+/// The numbers 1 to 10.
+///
+/// // Works with string methods.
+/// #("a,b;c"
+/// .split(regex("[,;]")))
+/// ```
+///
+/// Display: Regex
+/// Category: construct
+#[func]
+pub fn regex(
+ /// The regular expression as a string.
+ ///
+ /// Most regex escape sequences just work because they are not valid Typst
+ /// escape sequences. To produce regex escape sequences that are also valid in
+ /// Typst (e.g. `[\\]`), you need to escape twice. Thus, to match a verbatim
+ /// backslash, you would need to write `{regex("\\\\")}`.
+ ///
+ /// If you need many escape sequences, you can also create a raw element
+ /// and extract its text to use it for your regular expressions:
+ /// ```{regex(`\d+\.\d+\.\d+`.text)}```.
+ regex: Spanned<EcoString>,
+) -> SourceResult<Regex> {
+ Regex::new(&regex.v).at(regex.span)
+}
+
+/// Creates an array consisting of consecutive integers.
+///
+/// If you pass just one positional parameter, it is interpreted as the `end` of
+/// the range. If you pass two, they describe the `start` and `end` of the
+/// range.
+///
+/// ## Example { #example }
+/// ```example
+/// #range(5) \
+/// #range(2, 5) \
+/// #range(20, step: 4) \
+/// #range(21, step: 4) \
+/// #range(5, 2, step: -1)
+/// ```
+///
+/// Display: Range
+/// Category: construct
+#[func]
+pub fn range(
+ /// The start of the range (inclusive).
+ #[external]
+ #[default]
+ start: i64,
+ /// The end of the range (exclusive).
+ #[external]
+ end: i64,
+ /// The distance between the generated numbers.
+ #[named]
+ #[default(NonZeroI64::new(1).unwrap())]
+ step: NonZeroI64,
+ /// The arguments.
+ args: Args,
+) -> SourceResult<Array> {
+ let mut args = args;
+ let first = args.expect::<i64>("end")?;
+ let (start, end) = match args.eat::<i64>()? {
+ Some(second) => (first, second),
+ None => (0, first),
+ };
+
+ let step = step.get();
+
+ let mut x = start;
+ let mut array = Array::new();
+
+ while x.cmp(&end) == 0.cmp(&step) {
+ array.push(Value::Int(x));
+ x += step;
+ }
+
+ Ok(array)
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_to_base() {
+ assert_eq!(&int_to_base(0, 10), "0");
+ assert_eq!(&int_to_base(0, 16), "0");
+ assert_eq!(&int_to_base(0, 36), "0");
+ assert_eq!(
+ &int_to_base(i64::MAX, 2),
+ "111111111111111111111111111111111111111111111111111111111111111"
+ );
+ assert_eq!(
+ &int_to_base(i64::MIN, 2),
+ "-1000000000000000000000000000000000000000000000000000000000000000"
+ );
+ assert_eq!(&int_to_base(i64::MAX, 10), "9223372036854775807");
+ assert_eq!(&int_to_base(i64::MIN, 10), "-9223372036854775808");
+ assert_eq!(&int_to_base(i64::MAX, 16), "7fffffffffffffff");
+ assert_eq!(&int_to_base(i64::MIN, 16), "-8000000000000000");
+ assert_eq!(&int_to_base(i64::MAX, 36), "1y2p0ij32e8e7");
+ assert_eq!(&int_to_base(i64::MIN, 36), "-1y2p0ij32e8e8");
+ }
+}
diff --git a/crates/typst-library/src/compute/data.rs b/crates/typst-library/src/compute/data.rs
new file mode 100644
index 00000000..6e3a298e
--- /dev/null
+++ b/crates/typst-library/src/compute/data.rs
@@ -0,0 +1,492 @@
+use typst::diag::{format_xml_like_error, FileError};
+use typst::eval::Datetime;
+
+use crate::prelude::*;
+
+/// Reads plain text from a file.
+///
+/// The file will be read and returned as a string.
+///
+/// ## Example { #example }
+/// ```example
+/// #let text = read("data.html")
+///
+/// An example for a HTML file:\
+/// #raw(text, lang: "html")
+/// ```
+///
+/// Display: Read
+/// Category: data-loading
+#[func]
+pub fn read(
+ /// Path to a file.
+ path: Spanned<EcoString>,
+ /// The virtual machine.
+ vm: &mut Vm,
+) -> SourceResult<Str> {
+ let Spanned { v: path, span } = path;
+ let id = vm.location().join(&path).at(span)?;
+ let data = vm.world().file(id).at(span)?;
+ let text = std::str::from_utf8(&data)
+ .map_err(|_| "file is not valid utf-8")
+ .at(span)?;
+ Ok(text.into())
+}
+
+/// Reads structured data from a CSV file.
+///
+/// The CSV file will be read and parsed into a 2-dimensional array of strings:
+/// Each row in the CSV file will be represented as an array of strings, and all
+/// rows will be collected into a single array. Header rows will not be
+/// stripped.
+///
+/// ## Example { #example }
+/// ```example
+/// #let results = csv("data.csv")
+///
+/// #table(
+/// columns: 2,
+/// [*Condition*], [*Result*],
+/// ..results.flatten(),
+/// )
+/// ```
+///
+/// Display: CSV
+/// Category: data-loading
+#[func]
+pub fn csv(
+ /// Path to a CSV file.
+ path: Spanned<EcoString>,
+ /// The delimiter that separates columns in the CSV file.
+ /// Must be a single ASCII character.
+ #[named]
+ #[default]
+ delimiter: Delimiter,
+ /// The virtual machine.
+ vm: &mut Vm,
+) -> SourceResult<Array> {
+ let Spanned { v: path, span } = path;
+ let id = vm.location().join(&path).at(span)?;
+ let data = vm.world().file(id).at(span)?;
+
+ let mut builder = csv::ReaderBuilder::new();
+ builder.has_headers(false);
+ builder.delimiter(delimiter.0 as u8);
+
+ let mut reader = builder.from_reader(data.as_slice());
+ let mut array = Array::new();
+
+ for (line, result) in reader.records().enumerate() {
+ // Original solution use line from error, but that is incorrect with
+ // `has_headers` set to `false`. See issue:
+ // https://github.com/BurntSushi/rust-csv/issues/184
+ let line = line + 1; // Counting lines from 1
+ let row = result.map_err(|err| format_csv_error(err, line)).at(span)?;
+ let sub = row.into_iter().map(|field| field.into_value()).collect();
+ array.push(Value::Array(sub))
+ }
+
+ Ok(array)
+}
+
+/// The delimiter to use when parsing CSV files.
+pub struct Delimiter(char);
+
+impl Default for Delimiter {
+ fn default() -> Self {
+ Self(',')
+ }
+}
+
+cast! {
+ Delimiter,
+ self => self.0.into_value(),
+ v: EcoString => {
+ let mut chars = v.chars();
+ let first = chars.next().ok_or("delimiter must not be empty")?;
+ if chars.next().is_some() {
+ bail!("delimiter must be a single character");
+ }
+
+ if !first.is_ascii() {
+ bail!("delimiter must be an ASCII character");
+ }
+
+ Self(first)
+ },
+}
+
+/// Format the user-facing CSV error message.
+fn format_csv_error(error: csv::Error, line: usize) -> EcoString {
+ match error.kind() {
+ csv::ErrorKind::Utf8 { .. } => "file is not valid utf-8".into(),
+ csv::ErrorKind::UnequalLengths { expected_len, len, .. } => {
+ eco_format!(
+ "failed to parse csv file: found {len} instead of {expected_len} fields in line {line}"
+ )
+ }
+ _ => "failed to parse csv file".into(),
+ }
+}
+
+/// Reads structured data from a JSON file.
+///
+/// The file must contain a valid JSON object or array. JSON objects will be
+/// converted into Typst dictionaries, and JSON arrays will be converted into
+/// Typst arrays. Strings and booleans will be converted into the Typst
+/// equivalents, `null` will be converted into `{none}`, and numbers will be
+/// converted to floats or integers depending on whether they are whole numbers.
+///
+/// The function returns a dictionary or an array, depending on the JSON file.
+///
+/// The JSON files in the example contain objects with the keys `temperature`,
+/// `unit`, and `weather`.
+///
+/// ## Example { #example }
+/// ```example
+/// #let forecast(day) = block[
+/// #box(square(
+/// width: 2cm,
+/// inset: 8pt,
+/// fill: if day.weather == "sunny" {
+/// yellow
+/// } else {
+/// aqua
+/// },
+/// align(
+/// bottom + right,
+/// strong(day.weather),
+/// ),
+/// ))
+/// #h(6pt)
+/// #set text(22pt, baseline: -8pt)
+/// #day.temperature °#day.unit
+/// ]
+///
+/// #forecast(json("monday.json"))
+/// #forecast(json("tuesday.json"))
+/// ```
+///
+/// Display: JSON
+/// Category: data-loading
+#[func]
+pub fn json(
+ /// Path to a JSON file.
+ path: Spanned<EcoString>,
+ /// The virtual machine.
+ vm: &mut Vm,
+) -> SourceResult<Value> {
+ let Spanned { v: path, span } = path;
+ let id = vm.location().join(&path).at(span)?;
+ let data = vm.world().file(id).at(span)?;
+ let value: serde_json::Value =
+ serde_json::from_slice(&data).map_err(format_json_error).at(span)?;
+ Ok(convert_json(value))
+}
+
+/// Convert a JSON value to a Typst value.
+fn convert_json(value: serde_json::Value) -> Value {
+ match value {
+ serde_json::Value::Null => Value::None,
+ serde_json::Value::Bool(v) => v.into_value(),
+ serde_json::Value::Number(v) => match v.as_i64() {
+ Some(int) => int.into_value(),
+ None => v.as_f64().unwrap_or(f64::NAN).into_value(),
+ },
+ serde_json::Value::String(v) => v.into_value(),
+ serde_json::Value::Array(v) => {
+ v.into_iter().map(convert_json).collect::<Array>().into_value()
+ }
+ serde_json::Value::Object(v) => v
+ .into_iter()
+ .map(|(key, value)| (key.into(), convert_json(value)))
+ .collect::<Dict>()
+ .into_value(),
+ }
+}
+
+/// Format the user-facing JSON error message.
+fn format_json_error(error: serde_json::Error) -> EcoString {
+ assert!(error.is_syntax() || error.is_eof());
+ eco_format!("failed to parse json file: syntax error in line {}", error.line())
+}
+
+/// Reads structured data from a TOML file.
+///
+/// The file must contain a valid TOML table. TOML tables will be
+/// converted into Typst dictionaries, and TOML arrays will be converted into
+/// Typst arrays. Strings and booleans will be converted into the Typst
+/// equivalents and numbers will be converted to floats or integers depending on
+/// whether they are whole numbers. For the time being, datetimes will be
+/// converted to strings as Typst does not have a built-in datetime yet.
+///
+/// The TOML file in the example consists of a table with the keys `title`,
+/// `version`, and `authors`.
+///
+/// ## Example { #example }
+/// ```example
+/// #let details = toml("details.toml")
+///
+/// Title: #details.title \
+/// Version: #details.version \
+/// Authors: #(details.authors
+/// .join(", ", last: " and "))
+/// ```
+///
+/// Display: TOML
+/// Category: data-loading
+#[func]
+pub fn toml(
+ /// Path to a TOML file.
+ path: Spanned<EcoString>,
+ /// The virtual machine.
+ vm: &mut Vm,
+) -> SourceResult<Value> {
+ let Spanned { v: path, span } = path;
+ let id = vm.location().join(&path).at(span)?;
+ let data = vm.world().file(id).at(span)?;
+
+ let raw = std::str::from_utf8(&data)
+ .map_err(|_| "file is not valid utf-8")
+ .at(span)?;
+
+ let value: toml::Value = toml::from_str(raw).map_err(format_toml_error).at(span)?;
+ Ok(convert_toml(value))
+}
+
+/// Convert a TOML value to a Typst value.
+fn convert_toml(value: toml::Value) -> Value {
+ match value {
+ toml::Value::String(v) => v.into_value(),
+ toml::Value::Integer(v) => v.into_value(),
+ toml::Value::Float(v) => v.into_value(),
+ toml::Value::Boolean(v) => v.into_value(),
+ toml::Value::Array(v) => {
+ v.into_iter().map(convert_toml).collect::<Array>().into_value()
+ }
+ toml::Value::Table(v) => v
+ .into_iter()
+ .map(|(key, value)| (key.into(), convert_toml(value)))
+ .collect::<Dict>()
+ .into_value(),
+ toml::Value::Datetime(v) => match (v.date, v.time) {
+ (None, None) => Value::None,
+ (Some(date), None) => {
+ Datetime::from_ymd(date.year as i32, date.month, date.day).into_value()
+ }
+ (None, Some(time)) => {
+ Datetime::from_hms(time.hour, time.minute, time.second).into_value()
+ }
+ (Some(date), Some(time)) => Datetime::from_ymd_hms(
+ date.year as i32,
+ date.month,
+ date.day,
+ time.hour,
+ time.minute,
+ time.second,
+ )
+ .into_value(),
+ },
+ }
+}
+
+/// Format the user-facing TOML error message.
+fn format_toml_error(error: toml::de::Error) -> EcoString {
+ if let Some(range) = error.span() {
+ eco_format!(
+ "failed to parse toml file: {}, index {}-{}",
+ error.message(),
+ range.start,
+ range.end
+ )
+ } else {
+ eco_format!("failed to parse toml file: {}", error.message())
+ }
+}
+
+/// Reads structured data from a YAML file.
+///
+/// The file must contain a valid YAML object or array. YAML mappings will be
+/// converted into Typst dictionaries, and YAML sequences will be converted into
+/// Typst arrays. Strings and booleans will be converted into the Typst
+/// equivalents, null-values (`null`, `~` or empty ``) will be converted into
+/// `{none}`, and numbers will be converted to floats or integers depending on
+/// whether they are whole numbers.
+///
+/// Note that mapping keys that are not a string cause the entry to be
+/// discarded.
+///
+/// Custom YAML tags are ignored, though the loaded value will still be
+/// present.
+///
+/// The function returns a dictionary or value or an array, depending on
+/// the YAML file.
+///
+/// The YAML files in the example contain objects with authors as keys,
+/// each with a sequence of their own submapping with the keys
+/// "title" and "published"
+///
+/// ## Example { #example }
+/// ```example
+/// #let bookshelf(contents) = {
+/// for (author, works) in contents {
+/// author
+/// for work in works [
+/// - #work.title (#work.published)
+/// ]
+/// }
+/// }
+///
+/// #bookshelf(
+/// yaml("scifi-authors.yaml")
+/// )
+/// ```
+///
+/// Display: YAML
+/// Category: data-loading
+#[func]
+pub fn yaml(
+ /// Path to a YAML file.
+ path: Spanned<EcoString>,
+ /// The virtual machine.
+ vm: &mut Vm,
+) -> SourceResult<Value> {
+ let Spanned { v: path, span } = path;
+ let id = vm.location().join(&path).at(span)?;
+ let data = vm.world().file(id).at(span)?;
+ let value: serde_yaml::Value =
+ serde_yaml::from_slice(&data).map_err(format_yaml_error).at(span)?;
+ Ok(convert_yaml(value))
+}
+
+/// Convert a YAML value to a Typst value.
+fn convert_yaml(value: serde_yaml::Value) -> Value {
+ match value {
+ serde_yaml::Value::Null => Value::None,
+ serde_yaml::Value::Bool(v) => v.into_value(),
+ serde_yaml::Value::Number(v) => match v.as_i64() {
+ Some(int) => int.into_value(),
+ None => v.as_f64().unwrap_or(f64::NAN).into_value(),
+ },
+ serde_yaml::Value::String(v) => v.into_value(),
+ serde_yaml::Value::Sequence(v) => {
+ v.into_iter().map(convert_yaml).collect::<Array>().into_value()
+ }
+ serde_yaml::Value::Mapping(v) => v
+ .into_iter()
+ .map(|(key, value)| (convert_yaml_key(key), convert_yaml(value)))
+ .filter_map(|(key, value)| key.map(|key| (key, value)))
+ .collect::<Dict>()
+ .into_value(),
+ }
+}
+
+/// Converts an arbitrary YAML mapping key into a Typst Dict Key.
+/// Currently it only does so for strings, everything else
+/// returns None
+fn convert_yaml_key(key: serde_yaml::Value) -> Option<Str> {
+ match key {
+ serde_yaml::Value::String(v) => Some(Str::from(v)),
+ _ => None,
+ }
+}
+
+/// Format the user-facing YAML error message.
+fn format_yaml_error(error: serde_yaml::Error) -> EcoString {
+ eco_format!("failed to parse yaml file: {}", error.to_string().trim())
+}
+
+/// Reads structured data from an XML file.
+///
+/// The XML file is parsed into an array of dictionaries and strings. XML nodes
+/// can be elements or strings. Elements are represented as dictionaries with
+/// the the following keys:
+///
+/// - `tag`: The name of the element as a string.
+/// - `attrs`: A dictionary of the element's attributes as strings.
+/// - `children`: An array of the element's child nodes.
+///
+/// The XML file in the example contains a root `news` tag with multiple
+/// `article` tags. Each article has a `title`, `author`, and `content` tag. The
+/// `content` tag contains one or more paragraphs, which are represented as `p`
+/// tags.
+///
+/// ## Example { #example }
+/// ```example
+/// #let find-child(elem, tag) = {
+/// elem.children
+/// .find(e => "tag" in e and e.tag == tag)
+/// }
+///
+/// #let article(elem) = {
+/// let title = find-child(elem, "title")
+/// let author = find-child(elem, "author")
+/// let pars = find-child(elem, "content")
+///
+/// heading(title.children.first())
+/// text(10pt, weight: "medium")[
+/// Published by
+/// #author.children.first()
+/// ]
+///
+/// for p in pars.children {
+/// if (type(p) == "dictionary") {
+/// parbreak()
+/// p.children.first()
+/// }
+/// }
+/// }
+///
+/// #let data = xml("example.xml")
+/// #for elem in data.first().children {
+/// if (type(elem) == "dictionary") {
+/// article(elem)
+/// }
+/// }
+/// ```
+///
+/// Display: XML
+/// Category: data-loading
+#[func]
+pub fn xml(
+ /// Path to an XML file.
+ path: Spanned<EcoString>,
+ /// The virtual machine.
+ vm: &mut Vm,
+) -> SourceResult<Value> {
+ let Spanned { v: path, span } = path;
+ let id = vm.location().join(&path).at(span)?;
+ let data = vm.world().file(id).at(span)?;
+ let text = std::str::from_utf8(&data).map_err(FileError::from).at(span)?;
+ let document = roxmltree::Document::parse(text).map_err(format_xml_error).at(span)?;
+ Ok(convert_xml(document.root()))
+}
+
+/// Convert an XML node to a Typst value.
+fn convert_xml(node: roxmltree::Node) -> Value {
+ if node.is_text() {
+ return node.text().unwrap_or_default().into_value();
+ }
+
+ let children: Array = node.children().map(convert_xml).collect();
+ if node.is_root() {
+ return Value::Array(children);
+ }
+
+ let tag: Str = node.tag_name().name().into();
+ let attrs: Dict = node
+ .attributes()
+ .map(|attr| (attr.name().into(), attr.value().into_value()))
+ .collect();
+
+ Value::Dict(dict! {
+ "tag" => tag,
+ "attrs" => attrs,
+ "children" => children,
+ })
+}
+
+/// Format the user-facing XML error message.
+fn format_xml_error(error: roxmltree::Error) -> EcoString {
+ format_xml_like_error("xml file", error)
+}
diff --git a/crates/typst-library/src/compute/foundations.rs b/crates/typst-library/src/compute/foundations.rs
new file mode 100644
index 00000000..f83d71a0
--- /dev/null
+++ b/crates/typst-library/src/compute/foundations.rs
@@ -0,0 +1,215 @@
+use crate::prelude::*;
+
+/// Determines the type of a value.
+///
+/// Returns the name of the value's type.
+///
+/// ## Example { #example }
+/// ```example
+/// #type(12) \
+/// #type(14.7) \
+/// #type("hello") \
+/// #type(none) \
+/// #type([Hi]) \
+/// #type(x => x + 1)
+/// ```
+///
+/// Display: Type
+/// Category: foundations
+#[func]
+pub fn type_(
+ /// The value whose type's to determine.
+ value: Value,
+) -> Str {
+ value.type_name().into()
+}
+
+/// Returns the string representation of a value.
+///
+/// When inserted into content, most values are displayed as this representation
+/// in monospace with syntax-highlighting. The exceptions are `{none}`,
+/// integers, floats, strings, content, and functions.
+///
+/// **Note:** This function is for debugging purposes. Its output should not be
+/// considered stable and may change at any time!
+///
+/// ## Example { #example }
+/// ```example
+/// #none vs #repr(none) \
+/// #"hello" vs #repr("hello") \
+/// #(1, 2) vs #repr((1, 2)) \
+/// #[*Hi*] vs #repr([*Hi*])
+/// ```
+///
+/// Display: Representation
+/// Category: foundations
+#[func]
+pub fn repr(
+ /// The value whose string representation to produce.
+ value: Value,
+) -> Str {
+ value.repr()
+}
+
+/// Fails with an error.
+///
+/// ## Example { #example }
+/// The code below produces the error `panicked with: "this is wrong"`.
+/// ```typ
+/// #panic("this is wrong")
+/// ```
+///
+/// Display: Panic
+/// Category: foundations
+#[func]
+pub fn panic(
+ /// The values to panic with.
+ #[variadic]
+ values: Vec<Value>,
+) -> StrResult<Never> {
+ let mut msg = EcoString::from("panicked");
+ if !values.is_empty() {
+ msg.push_str(" with: ");
+ for (i, value) in values.iter().enumerate() {
+ if i > 0 {
+ msg.push_str(", ");
+ }
+ msg.push_str(&value.repr());
+ }
+ }
+ Err(msg)
+}
+
+/// Ensures that a condition is fulfilled.
+///
+/// Fails with an error if the condition is not fulfilled. Does not
+/// produce any output in the document.
+///
+/// If you wish to test equality between two values, see
+/// [`assert.eq`]($func/assert.eq) and [`assert.ne`]($func/assert.ne).
+///
+/// ## Example { #example }
+/// ```typ
+/// #assert(1 < 2, message: "math broke")
+/// ```
+///
+/// Display: Assert
+/// Category: foundations
+#[func]
+#[scope(
+ scope.define("eq", assert_eq_func());
+ scope.define("ne", assert_ne_func());
+ scope
+)]
+pub fn assert(
+ /// The condition that must be true for the assertion to pass.
+ condition: bool,
+ /// The error message when the assertion fails.
+ #[named]
+ message: Option<EcoString>,
+) -> StrResult<NoneValue> {
+ if !condition {
+ if let Some(message) = message {
+ bail!("assertion failed: {message}");
+ } else {
+ bail!("assertion failed");
+ }
+ }
+ Ok(NoneValue)
+}
+
+/// Ensures that two values are equal.
+///
+/// Fails with an error if the first value is not equal to the second. Does not
+/// produce any output in the document.
+///
+/// ## Example { #example }
+/// ```typ
+/// #assert.eq(10, 10)
+/// ```
+///
+/// Display: Assert Equals
+/// Category: foundations
+#[func]
+pub fn assert_eq(
+ /// The first value to compare.
+ left: Value,
+
+ /// The second value to compare.
+ right: Value,
+
+ /// An optional message to display on error instead of the representations
+ /// of the compared values.
+ #[named]
+ message: Option<EcoString>,
+) -> StrResult<NoneValue> {
+ if left != right {
+ if let Some(message) = message {
+ bail!("equality assertion failed: {message}");
+ } else {
+ bail!("equality assertion failed: value {left:?} was not equal to {right:?}");
+ }
+ }
+ Ok(NoneValue)
+}
+
+/// Ensures that two values are not equal.
+///
+/// Fails with an error if the first value is equal to the second. Does not
+/// produce any output in the document.
+///
+/// ## Example { #example }
+/// ```typ
+/// #assert.ne(3, 4)
+/// ```
+///
+/// Display: Assert Not Equals
+/// Category: foundations
+#[func]
+pub fn assert_ne(
+ /// The first value to compare.
+ left: Value,
+
+ /// The second value to compare.
+ right: Value,
+
+ /// An optional message to display on error instead of the representations
+ /// of the compared values.
+ #[named]
+ message: Option<EcoString>,
+) -> StrResult<NoneValue> {
+ if left == right {
+ if let Some(message) = message {
+ bail!("inequality assertion failed: {message}");
+ } else {
+ bail!("inequality assertion failed: value {left:?} was equal to {right:?}");
+ }
+ }
+ Ok(NoneValue)
+}
+
+/// Evaluates a string as Typst code.
+///
+/// This function should only be used as a last resort.
+///
+/// ## Example { #example }
+/// ```example
+/// #eval("1 + 1") \
+/// #eval("(1, 2, 3, 4)").len() \
+/// #eval("[*Strong text*]")
+/// ```
+///
+/// Display: Evaluate
+/// Category: foundations
+#[func]
+pub fn eval(
+ /// A string of Typst code to evaluate.
+ ///
+ /// The code in the string cannot interact with the file system.
+ source: Spanned<String>,
+ /// The virtual machine.
+ vm: &mut Vm,
+) -> SourceResult<Value> {
+ let Spanned { v: text, span } = source;
+ typst::eval::eval_string(vm.world(), &text, span)
+}
diff --git a/crates/typst-library/src/compute/mod.rs b/crates/typst-library/src/compute/mod.rs
new file mode 100644
index 00000000..e9e4870c
--- /dev/null
+++ b/crates/typst-library/src/compute/mod.rs
@@ -0,0 +1,39 @@
+//! Computational functions.
+
+pub mod calc;
+mod construct;
+mod data;
+mod foundations;
+
+pub use self::construct::*;
+pub use self::data::*;
+pub use self::foundations::*;
+
+use crate::prelude::*;
+
+/// Hook up all compute definitions.
+pub(super) fn define(global: &mut Scope) {
+ global.define("type", type_func());
+ global.define("repr", repr_func());
+ global.define("panic", panic_func());
+ global.define("assert", assert_func());
+ global.define("eval", eval_func());
+ global.define("int", int_func());
+ global.define("float", float_func());
+ global.define("luma", luma_func());
+ global.define("rgb", rgb_func());
+ global.define("cmyk", cmyk_func());
+ global.define("datetime", datetime_func());
+ global.define("symbol", symbol_func());
+ global.define("str", str_func());
+ global.define("label", label_func());
+ global.define("regex", regex_func());
+ global.define("range", range_func());
+ global.define("read", read_func());
+ global.define("csv", csv_func());
+ global.define("json", json_func());
+ global.define("toml", toml_func());
+ global.define("yaml", yaml_func());
+ global.define("xml", xml_func());
+ global.define("calc", calc::module());
+}