diff options
| author | Laurenz <laurmaedje@gmail.com> | 2023-07-02 19:59:52 +0200 |
|---|---|---|
| committer | Laurenz <laurmaedje@gmail.com> | 2023-07-02 20:07:43 +0200 |
| commit | ebfdb1dafa430786db10dad2ef7d5467c1bdbed1 (patch) | |
| tree | 2bbc24ddb4124c4bb14dec0e536129d4de37b056 /library/src/compute | |
| parent | 3ab19185093d7709f824b95b979060ce125389d8 (diff) | |
Move everything into `crates/` directory
Diffstat (limited to 'library/src/compute')
| -rw-r--r-- | library/src/compute/calc.rs | 1024 | ||||
| -rw-r--r-- | library/src/compute/construct.rs | 743 | ||||
| -rw-r--r-- | library/src/compute/data.rs | 492 | ||||
| -rw-r--r-- | library/src/compute/foundations.rs | 215 | ||||
| -rw-r--r-- | library/src/compute/mod.rs | 39 |
5 files changed, 0 insertions, 2513 deletions
diff --git a/library/src/compute/calc.rs b/library/src/compute/calc.rs deleted file mode 100644 index 81715007..00000000 --- a/library/src/compute/calc.rs +++ /dev/null @@ -1,1024 +0,0 @@ -//! 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/library/src/compute/construct.rs b/library/src/compute/construct.rs deleted file mode 100644 index 956212ee..00000000 --- a/library/src/compute/construct.rs +++ /dev/null @@ -1,743 +0,0 @@ -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(®ex.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/library/src/compute/data.rs b/library/src/compute/data.rs deleted file mode 100644 index 6e3a298e..00000000 --- a/library/src/compute/data.rs +++ /dev/null @@ -1,492 +0,0 @@ -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/library/src/compute/foundations.rs b/library/src/compute/foundations.rs deleted file mode 100644 index f83d71a0..00000000 --- a/library/src/compute/foundations.rs +++ /dev/null @@ -1,215 +0,0 @@ -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/library/src/compute/mod.rs b/library/src/compute/mod.rs deleted file mode 100644 index e9e4870c..00000000 --- a/library/src/compute/mod.rs +++ /dev/null @@ -1,39 +0,0 @@ -//! 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()); -} |
