diff options
96 files changed, 2869 insertions, 2830 deletions
@@ -2488,7 +2488,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.16", ] [[package]] diff --git a/docs/src/lib.rs b/docs/src/lib.rs index 4cb47283..bc3be0cc 100644 --- a/docs/src/lib.rs +++ b/docs/src/lib.rs @@ -415,7 +415,8 @@ fn func_model( name = name .strip_prefix(parent) .or(name.strip_prefix(parent.strip_suffix('s').unwrap_or(parent))) - .unwrap_or(name); + .unwrap_or(name) + .trim_matches('-'); } path.push(name); @@ -429,6 +430,13 @@ fn func_model( }) .collect(); + let mut returns = vec![]; + casts(resolver, &mut returns, &mut vec![], &info.returns); + returns.sort_by_key(|ty| type_index(ty)); + if returns == ["none"] { + returns.clear(); + } + FuncModel { path, display: info.display, @@ -437,7 +445,7 @@ fn func_model( element: func.element().is_some(), details: Html::markdown_with_id_base(resolver, docs, id_base), params: info.params.iter().map(|param| param_model(resolver, param)).collect(), - returns: info.returns.clone(), + returns, methods: method_models(resolver, info.docs), scope, } diff --git a/library/src/compute/calc.rs b/library/src/compute/calc.rs index f485f817..0fc45891 100644 --- a/library/src/compute/calc.rs +++ b/library/src/compute/calc.rs @@ -11,38 +11,38 @@ use crate::prelude::*; /// A module with computational functions. pub fn module() -> Module { let mut scope = Scope::new(); - scope.define("abs", abs); - scope.define("pow", pow); - scope.define("sqrt", sqrt); - scope.define("sin", sin); - scope.define("cos", cos); - scope.define("tan", tan); - scope.define("asin", asin); - scope.define("acos", acos); - scope.define("atan", atan); - scope.define("atan2", atan2); - scope.define("sinh", sinh); - scope.define("cosh", cosh); - scope.define("tanh", tanh); - scope.define("log", log); - scope.define("fact", fact); - scope.define("perm", perm); - scope.define("binom", binom); - scope.define("gcd", gcd); - scope.define("lcm", lcm); - scope.define("floor", floor); - scope.define("ceil", ceil); - scope.define("trunc", trunc); - scope.define("fract", fract); - scope.define("round", round); - scope.define("clamp", clamp); - scope.define("min", min); - scope.define("max", max); - scope.define("even", even); - scope.define("odd", odd); - scope.define("rem", rem); - scope.define("mod", mod_); - scope.define("quo", quo); + scope.define("abs", abs_func()); + scope.define("pow", pow_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("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("mod", mod_func()); + scope.define("quo", quo_func()); scope.define("inf", Value::Float(f64::INFINITY)); scope.define("nan", Value::Float(f64::NAN)); scope.define("pi", Value::Float(std::f64::consts::PI)); @@ -61,7 +61,6 @@ pub fn module() -> Module { /// /// Display: Absolute /// Category: calculate -/// Returns: any #[func] pub fn abs( /// The value whose absolute value to calculate. @@ -71,12 +70,12 @@ pub fn abs( } /// A value of which the absolute value can be taken. -struct ToAbs(Value); +pub struct ToAbs(Value); -cast_from_value! { +cast! { ToAbs, - v: i64 => Self(Value::Int(v.abs())), - v: f64 => Self(Value::Float(v.abs())), + 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())), @@ -93,17 +92,18 @@ cast_from_value! { /// /// Display: Power /// Category: calculate -/// Returns: integer or float #[func] pub fn pow( /// The base of the power. base: Num, /// The exponent of the power. Must be non-negative. exponent: Spanned<Num>, -) -> Value { + /// The callsite span. + span: Span, +) -> SourceResult<Num> { match exponent.v { _ if exponent.v.float() == 0.0 && base.float() == 0.0 => { - bail!(args.span, "zero to the power of zero is undefined") + 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") @@ -119,16 +119,16 @@ pub fn pow( .checked_pow(b as u32) .map(Num::Int) .ok_or("the result is too large") - .at(args.span)?, + .at(span)?, (a, Num::Int(b)) => Num::Float(a.float().powi(b as i32)), (a, b) => Num::Float(a.float().powf(b.float())), }; if result.float().is_nan() { - bail!(args.span, "the result is not a real number") + bail!(span, "the result is not a real number") } - result.value() + Ok(result) } /// Calculate the square root of a number. @@ -141,16 +141,15 @@ pub fn pow( /// /// Display: Square Root /// Category: calculate -/// Returns: float #[func] pub fn sqrt( /// The number whose square root to calculate. Must be non-negative. value: Spanned<Num>, -) -> Value { +) -> SourceResult<f64> { if value.v.float() < 0.0 { bail!(value.span, "cannot take square root of negative number"); } - Value::Float(value.v.float().sqrt()) + Ok(value.v.float().sqrt()) } /// Calculate the sine of an angle. @@ -167,17 +166,16 @@ pub fn sqrt( /// /// Display: Sine /// Category: calculate -/// Returns: float #[func] pub fn sin( /// The angle whose sine to calculate. angle: AngleLike, -) -> Value { - Value::Float(match angle { +) -> f64 { + match angle { AngleLike::Angle(a) => a.sin(), AngleLike::Int(n) => (n as f64).sin(), AngleLike::Float(n) => n.sin(), - }) + } } /// Calculate the cosine of an angle. @@ -194,17 +192,16 @@ pub fn sin( /// /// Display: Cosine /// Category: calculate -/// Returns: float #[func] pub fn cos( /// The angle whose cosine to calculate. angle: AngleLike, -) -> Value { - Value::Float(match angle { +) -> f64 { + match angle { AngleLike::Angle(a) => a.cos(), AngleLike::Int(n) => (n as f64).cos(), AngleLike::Float(n) => n.cos(), - }) + } } /// Calculate the tangent of an angle. @@ -220,17 +217,16 @@ pub fn cos( /// /// Display: Tangent /// Category: calculate -/// Returns: float #[func] pub fn tan( /// The angle whose tangent to calculate. angle: AngleLike, -) -> Value { - Value::Float(match angle { +) -> f64 { + match angle { AngleLike::Angle(a) => a.tan(), AngleLike::Int(n) => (n as f64).tan(), AngleLike::Float(n) => n.tan(), - }) + } } /// Calculate the arcsine of a number. @@ -243,17 +239,16 @@ pub fn tan( /// /// Display: Arcsine /// Category: calculate -/// Returns: angle #[func] pub fn asin( /// The number whose arcsine to calculate. Must be between -1 and 1. value: Spanned<Num>, -) -> Value { +) -> SourceResult<Angle> { let val = value.v.float(); if val < -1.0 || val > 1.0 { bail!(value.span, "value must be between -1 and 1"); } - Value::Angle(Angle::rad(val.asin())) + Ok(Angle::rad(val.asin())) } /// Calculate the arccosine of a number. @@ -266,17 +261,16 @@ pub fn asin( /// /// Display: Arccosine /// Category: calculate -/// Returns: angle #[func] pub fn acos( /// The number whose arcsine to calculate. Must be between -1 and 1. value: Spanned<Num>, -) -> Value { +) -> SourceResult<Angle> { let val = value.v.float(); if val < -1.0 || val > 1.0 { bail!(value.span, "value must be between -1 and 1"); } - Value::Angle(Angle::rad(val.acos())) + Ok(Angle::rad(val.acos())) } /// Calculate the arctangent of a number. @@ -289,13 +283,12 @@ pub fn acos( /// /// Display: Arctangent /// Category: calculate -/// Returns: angle #[func] pub fn atan( /// The number whose arctangent to calculate. value: Num, -) -> Value { - Value::Angle(Angle::rad(value.float().atan())) +) -> Angle { + Angle::rad(value.float().atan()) } /// Calculate the four-quadrant arctangent of a coordinate. @@ -310,15 +303,14 @@ pub fn atan( /// /// Display: Four-quadrant Arctangent /// Category: calculate -/// Returns: angle #[func] pub fn atan2( /// The X coordinate. x: Num, /// The Y coordinate. y: Num, -) -> Value { - Value::Angle(Angle::rad(f64::atan2(y.float(), x.float()))) +) -> Angle { + Angle::rad(f64::atan2(y.float(), x.float())) } /// Calculate the hyperbolic sine of an angle. @@ -333,17 +325,16 @@ pub fn atan2( /// /// Display: Hyperbolic sine /// Category: calculate -/// Returns: float #[func] pub fn sinh( /// The angle whose hyperbolic sine to calculate. angle: AngleLike, -) -> Value { - Value::Float(match angle { +) -> f64 { + match angle { AngleLike::Angle(a) => a.to_rad().sinh(), AngleLike::Int(n) => (n as f64).sinh(), AngleLike::Float(n) => n.sinh(), - }) + } } /// Calculate the hyperbolic cosine of an angle. @@ -358,17 +349,16 @@ pub fn sinh( /// /// Display: Hyperbolic cosine /// Category: calculate -/// Returns: float #[func] pub fn cosh( /// The angle whose hyperbolic cosine to calculate. angle: AngleLike, -) -> Value { - Value::Float(match angle { +) -> f64 { + match angle { AngleLike::Angle(a) => a.to_rad().cosh(), AngleLike::Int(n) => (n as f64).cosh(), AngleLike::Float(n) => n.cosh(), - }) + } } /// Calculate the hyperbolic tangent of an angle. @@ -383,17 +373,16 @@ pub fn cosh( /// /// Display: Hyperbolic tangent /// Category: calculate -/// Returns: float #[func] pub fn tanh( /// The angle whose hyperbolic tangent to calculate. angle: AngleLike, -) -> Value { - Value::Float(match angle { +) -> f64 { + match angle { AngleLike::Angle(a) => a.to_rad().tanh(), AngleLike::Int(n) => (n as f64).tanh(), AngleLike::Float(n) => n.tanh(), - }) + } } /// Calculate the logarithm of a number. @@ -407,7 +396,6 @@ pub fn tanh( /// /// Display: Logarithm /// Category: calculate -/// Returns: float #[func] pub fn log( /// The number whose logarithm to calculate. Must be strictly positive. @@ -416,7 +404,9 @@ pub fn log( #[named] #[default(Spanned::new(10.0, Span::detached()))] base: Spanned<f64>, -) -> Value { + /// The callsite span. + span: Span, +) -> SourceResult<f64> { let number = value.v.float(); if number <= 0.0 { bail!(value.span, "value must be strictly positive") @@ -435,10 +425,10 @@ pub fn log( }; if result.is_infinite() || result.is_nan() { - bail!(args.span, "the result is not a real number") + bail!(span, "the result is not a real number") } - Value::Float(result) + Ok(result) } /// Calculate the factorial of a number. @@ -450,33 +440,12 @@ pub fn log( /// /// Display: Factorial /// Category: calculate -/// Returns: integer #[func] pub fn fact( /// The number whose factorial to calculate. Must be non-negative. number: u64, -) -> Value { - factorial_range(1, number) - .map(Value::Int) - .ok_or("the result is too large") - .at(args.span)? -} - -/// Calculates the product of a range of numbers. Used to calculate -/// permutations. Returns None if the result is larger than `i64::MAX` -fn factorial_range(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)?; - } - - i64::try_from(count).ok() +) -> StrResult<i64> { + Ok(fact_impl(1, number).ok_or("the result is too large")?) } /// Calculate a permutation. @@ -488,23 +457,36 @@ fn factorial_range(start: u64, end: u64) -> Option<i64> { /// /// Display: Permutation /// Category: calculate -/// Returns: integer #[func] pub fn perm( /// The base number. Must be non-negative. base: u64, /// The number of permutations. Must be non-negative. numbers: u64, -) -> Value { +) -> StrResult<i64> { // By convention. if base < numbers { - return Ok(Value::Int(0)); + return Ok(0); } - factorial_range(base - numbers + 1, base) - .map(Value::Int) - .ok_or("the result is too large") - .at(args.span)? + 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() } /// Calculate a binomial coefficient. @@ -516,24 +498,20 @@ pub fn perm( /// /// Display: Binomial /// Category: calculate -/// Returns: integer #[func] pub fn binom( /// The upper coefficient. Must be non-negative. n: u64, /// The lower coefficient. Must be non-negative. k: u64, -) -> Value { - binomial(n, k) - .map(Value::Int) - .ok_or("the result is too large") - .at(args.span)? +) -> 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 binomial(n: u64, k: u64) -> Option<i64> { +fn binom_impl(n: u64, k: u64) -> Option<i64> { if k > n { return Some(0); } @@ -549,7 +527,7 @@ fn binomial(n: u64, k: u64) -> Option<i64> { result = result.checked_mul(n - i)?.checked_div(i + 1)?; } - i64::try_from(result).ok() + result.try_into().ok() } /// Calculate the greatest common divisor of two integers. @@ -561,20 +539,14 @@ fn binomial(n: u64, k: u64) -> Option<i64> { /// /// Display: Greatest Common Divisor /// Category: calculate -/// Returns: integer #[func] pub fn gcd( /// The first integer. a: i64, /// The second integer. b: i64, -) -> Value { - Value::Int(calculate_gcd(a, b)) -} - -/// Calculates the greatest common divisor of two integers -/// It is always non-negative. -fn calculate_gcd(mut a: i64, mut b: i64) -> i64 { +) -> i64 { + let (mut a, mut b) = (a, b); while b != 0 { let temp = b; b = a % b; @@ -593,29 +565,21 @@ fn calculate_gcd(mut a: i64, mut b: i64) -> i64 { /// /// Display: Least Common Multiple /// Category: calculate -/// Returns: integer #[func] pub fn lcm( /// The first integer. a: i64, /// The second integer. b: i64, -) -> Value { - calculate_lcm(a, b) - .map(Value::Int) - .ok_or("the return value is too large") - .at(args.span)? -} - -/// Calculates the least common multiple between two non-zero integers -/// Returns None if the value cannot be computed. -/// It is always non-negative. -fn calculate_lcm(a: i64, b: i64) -> Option<i64> { +) -> StrResult<i64> { if a == b { - return Some(a.abs()); + return Ok(a.abs()); } - a.checked_div(calculate_gcd(a, b))?.checked_mul(b).map(|v| v.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")?) } /// Round a number down to the nearest integer. @@ -631,15 +595,14 @@ fn calculate_lcm(a: i64, b: i64) -> Option<i64> { /// /// Display: Round down /// Category: calculate -/// Returns: integer #[func] pub fn floor( /// The number to round down. value: Num, -) -> Value { +) -> i64 { match value { - Num::Int(n) => Value::Int(n), - Num::Float(n) => Value::Int(n.floor() as i64), + Num::Int(n) => n, + Num::Float(n) => n.floor() as i64, } } @@ -656,15 +619,14 @@ pub fn floor( /// /// Display: Round up /// Category: calculate -/// Returns: integer #[func] pub fn ceil( /// The number to round up. value: Num, -) -> Value { +) -> i64 { match value { - Num::Int(n) => Value::Int(n), - Num::Float(n) => Value::Int(n.ceil() as i64), + Num::Int(n) => n, + Num::Float(n) => n.ceil() as i64, } } @@ -681,16 +643,15 @@ pub fn ceil( /// /// Display: Truncate /// Category: calculate -/// Returns: integer #[func] pub fn trunc( /// The number to truncate. value: Num, -) -> Value { - Value::Int(match value { +) -> i64 { + match value { Num::Int(n) => n, Num::Float(n) => n.trunc() as i64, - }) + } } /// Returns the fractional part of a number. @@ -705,15 +666,14 @@ pub fn trunc( /// /// Display: Fractional /// Category: calculate -/// Returns: integer or float #[func] pub fn fract( /// The number to truncate. value: Num, -) -> Value { +) -> Num { match value { - Num::Int(_) => Value::Int(0), - Num::Float(n) => Value::Float(n.fract()), + Num::Int(_) => Num::Int(0), + Num::Float(n) => Num::Float(n.fract()), } } @@ -730,7 +690,6 @@ pub fn fract( /// /// Display: Round /// Category: calculate -/// Returns: integer or float #[func] pub fn round( /// The number to round. @@ -739,13 +698,13 @@ pub fn round( #[named] #[default(0)] digits: i64, -) -> Value { +) -> Num { match value { - Num::Int(n) if digits == 0 => Value::Int(n), + Num::Int(n) if digits == 0 => Num::Int(n), _ => { let n = value.float(); let factor = 10.0_f64.powi(digits as i32); - Value::Float((n * factor).round() / factor) + Num::Float((n * factor).round() / factor) } } } @@ -761,7 +720,6 @@ pub fn round( /// /// Display: Clamp /// Category: calculate -/// Returns: integer or float #[func] pub fn clamp( /// The number to clamp. @@ -770,11 +728,11 @@ pub fn clamp( min: Num, /// The inclusive maximum value. max: Spanned<Num>, -) -> Value { +) -> SourceResult<Num> { if max.v.float() < min.float() { bail!(max.span, "max must be greater than or equal to min") } - value.apply3(min, max.v, i64::clamp, f64::clamp).value() + Ok(value.apply3(min, max.v, i64::clamp, f64::clamp)) } /// Determine the minimum of a sequence of values. @@ -787,15 +745,16 @@ pub fn clamp( /// /// Display: Minimum /// Category: calculate -/// Returns: any #[func] pub fn min( /// The sequence of values from which to extract the minimum. /// Must not be empty. #[variadic] values: Vec<Spanned<Value>>, -) -> Value { - minmax(args.span, values, Ordering::Less)? + /// The callsite span. + span: Span, +) -> SourceResult<Value> { + minmax(span, values, Ordering::Less) } /// Determine the maximum of a sequence of values. @@ -808,15 +767,16 @@ pub fn min( /// /// Display: Maximum /// Category: calculate -/// Returns: any #[func] pub fn max( /// The sequence of values from which to extract the maximum. /// Must not be empty. #[variadic] values: Vec<Spanned<Value>>, -) -> Value { - minmax(args.span, values, Ordering::Greater)? + /// The callsite span. + span: Span, +) -> SourceResult<Value> { + minmax(span, values, Ordering::Greater) } /// Find the minimum or maximum of a sequence of values. @@ -851,13 +811,12 @@ fn minmax( /// /// Display: Even /// Category: calculate -/// Returns: boolean #[func] pub fn even( /// The number to check for evenness. value: i64, -) -> Value { - Value::Bool(value % 2 == 0) +) -> bool { + value % 2 == 0 } /// Determine whether an integer is odd. @@ -871,13 +830,12 @@ pub fn even( /// /// Display: Odd /// Category: calculate -/// Returns: boolean #[func] pub fn odd( /// The number to check for oddness. value: i64, -) -> Value { - Value::Bool(value % 2 != 0) +) -> bool { + value % 2 != 0 } /// Calculate the remainder of two numbers. @@ -890,18 +848,17 @@ pub fn odd( /// /// Display: Remainder /// Category: calculate -/// Returns: integer or float #[func] pub fn rem( /// The dividend of the remainder. dividend: Num, /// The divisor of the remainder. divisor: Spanned<Num>, -) -> Value { +) -> SourceResult<Num> { if divisor.v.float() == 0.0 { bail!(divisor.span, "divisor must not be zero"); } - dividend.apply2(divisor.v, Rem::rem, Rem::rem).value() + Ok(dividend.apply2(divisor.v, Rem::rem, Rem::rem)) } /// Calculate the modulus of two numbers. (Deprecated) @@ -911,18 +868,17 @@ pub fn rem( /// /// Display: Modulus /// Category: calculate -/// Returns: integer or float #[func] pub fn mod_( /// The dividend of the remainder. dividend: Num, /// The divisor of the remainder. divisor: Spanned<Num>, -) -> Value { +) -> SourceResult<Num> { if divisor.v.float() == 0.0 { bail!(divisor.span, "divisor must not be zero"); } - dividend.apply2(divisor.v, Rem::rem, Rem::rem).value() + Ok(dividend.apply2(divisor.v, Rem::rem, Rem::rem)) } /// Calculate the quotient of two numbers. @@ -935,33 +891,29 @@ pub fn mod_( /// /// Display: Quotient /// Category: calculate -/// Returns: integer or float #[func] pub fn quo( /// The dividend of the quotient. dividend: Num, /// The divisor of the quotient. divisor: Spanned<Num>, -) -> Value { +) -> SourceResult<i64> { if divisor.v.float() == 0.0 { bail!(divisor.span, "divisor must not be zero"); } - Value::Int(match dividend.apply2(divisor.v, Div::div, Div::div) { - Num::Int(i) => i, - Num::Float(f) => f.floor() as i64, // Note: the result should be an integer but floats doesn't have the same precision as i64. - }) + 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)] -enum Num { +pub enum Num { Int(i64), Float(f64), } impl Num { - fn apply2( + pub fn apply2( self, other: Self, int: impl FnOnce(i64, i64) -> i64, @@ -973,7 +925,7 @@ impl Num { } } - fn apply3( + pub fn apply3( self, other: Self, third: Self, @@ -986,35 +938,32 @@ impl Num { } } - fn float(self) -> f64 { + pub fn float(self) -> f64 { match self { Self::Int(v) => v as f64, Self::Float(v) => v, } } - - fn value(self) -> Value { - match self { - Self::Int(v) => Value::Int(v), - Self::Float(v) => Value::Float(v), - } - } } -cast_from_value! { +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. -enum AngleLike { +pub enum AngleLike { Int(i64), Float(f64), Angle(Angle), } -cast_from_value! { +cast! { AngleLike, v: i64 => Self::Int(v), v: f64 => Self::Float(v), diff --git a/library/src/compute/construct.rs b/library/src/compute/construct.rs index d11eb398..5d2d35ae 100644 --- a/library/src/compute/construct.rs +++ b/library/src/compute/construct.rs @@ -3,7 +3,7 @@ use std::str::FromStr; use time::{Month, PrimitiveDateTime}; -use typst::eval::{Datetime, Dynamic, Regex}; +use typst::eval::{Datetime, Regex}; use crate::prelude::*; @@ -23,19 +23,18 @@ use crate::prelude::*; /// /// Display: Integer /// Category: construct -/// Returns: integer #[func] pub fn int( /// The value that should be converted to an integer. value: ToInt, -) -> Value { - Value::Int(value.0) +) -> i64 { + value.0 } /// A value that can be cast to an integer. -struct ToInt(i64); +pub struct ToInt(i64); -cast_from_value! { +cast! { ToInt, v: bool => Self(v as i64), v: i64 => Self(v), @@ -63,19 +62,18 @@ cast_from_value! { /// /// Display: Float /// Category: construct -/// Returns: float #[func] pub fn float( /// The value that should be converted to a float. value: ToFloat, -) -> Value { - Value::Float(value.0) +) -> f64 { + value.0 } /// A value that can be cast to a float. -struct ToFloat(f64); +pub struct ToFloat(f64); -cast_from_value! { +cast! { ToFloat, v: bool => Self(v as i64 as f64), v: i64 => Self(v as f64), @@ -95,13 +93,12 @@ cast_from_value! { /// /// Display: Luma /// Category: construct -/// Returns: color #[func] pub fn luma( /// The gray component. gray: Component, -) -> Value { - Value::Color(LumaColor::new(gray.0).into()) +) -> Color { + LumaColor::new(gray.0).into() } /// Create an RGB(A) color. @@ -121,7 +118,6 @@ pub fn luma( /// /// Display: RGB /// Category: construct -/// Returns: color #[func] pub fn rgb( /// The color in hexadecimal notation. @@ -150,11 +146,14 @@ pub fn rgb( /// The alpha component. #[external] alpha: Component, -) -> Value { - Value::Color(if let Some(string) = args.find::<Spanned<EcoString>>()? { + /// 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), + Err(msg) => bail!(string.span, "{msg}"), } } else { let Component(r) = args.expect("red component")?; @@ -166,9 +165,9 @@ pub fn rgb( } /// An integer or ratio component. -struct Component(u8); +pub struct Component(u8); -cast_from_value! { +cast! { Component, v: i64 => match v { 0 ..= 255 => Self(v as u8), @@ -208,10 +207,9 @@ cast_from_value! { /// /// Display: Datetime /// Category: construct -/// Returns: datetime #[func] #[scope( - scope.define("today", datetime_today); + scope.define("today", datetime_today_func()); scope )] pub fn datetime( @@ -233,95 +231,77 @@ pub fn datetime( /// The second of the datetime. #[named] second: Option<SecondComponent>, -) -> Value { +) -> 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!(args.span, "time is invalid"), + Err(_) => bail!("time is invalid"), } } (None, None, None) => None, - _ => bail!(args.span, "time is incomplete"), + _ => 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!(args.span, "date is invalid"), + Err(_) => bail!("date is invalid"), } } (None, None, None) => None, - _ => bail!(args.span, "date is incomplete"), + _ => bail!("date is incomplete"), }; - match (date, time) { - (Some(date), Some(time)) => Value::Dyn(Dynamic::new(Datetime::Datetime( - PrimitiveDateTime::new(date, time), - ))), - (Some(date), None) => Value::Dyn(Dynamic::new(Datetime::Date(date))), - (None, Some(time)) => Value::Dyn(Dynamic::new(Datetime::Time(time))), + 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!(args.span, "at least one of date or time must be fully specified") + bail!("at least one of date or time must be fully specified") } - } + }) } -struct YearComponent(i32); -struct MonthComponent(Month); -struct DayComponent(u8); -struct HourComponent(u8); -struct MinuteComponent(u8); -struct SecondComponent(u8); +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_from_value!( +cast! { YearComponent, - v: i64 => match i32::try_from(v) { - Ok(n) => Self(n), - _ => Err("year is invalid")? - } -); + v: i32 => Self(v), +} -cast_from_value!( +cast! { MonthComponent, - v: i64 => match u8::try_from(v).ok().and_then(|n1| Month::try_from(n1).ok()).map(Self) { - Some(m) => m, - _ => Err("month is invalid")? - } -); + v: u8 => Self(Month::try_from(v).map_err(|_| "month is invalid")?) +} -cast_from_value!( +cast! { DayComponent, - v: i64 => match u8::try_from(v) { - Ok(n) => Self(n), - _ => Err("day is invalid")? - } -); + v: u8 => Self(v), +} -cast_from_value!( +cast! { HourComponent, - v: i64 => match u8::try_from(v) { - Ok(n) => Self(n), - _ => Err("hour is invalid")? - } -); + v: u8 => Self(v), +} -cast_from_value!( +cast! { MinuteComponent, - v: i64 => match u8::try_from(v) { - Ok(n) => Self(n), - _ => Err("minute is invalid")? - } -); + v: u8 => Self(v), +} -cast_from_value!( +cast! { SecondComponent, - v: i64 => match u8::try_from(v) { - Ok(n) => Self(n), - _ => Err("second is invalid")? - } -); + v: u8 => Self(v), +} /// Returns the current date. /// @@ -333,7 +313,6 @@ cast_from_value!( /// /// Display: Today /// Category: construct -/// Returns: datetime #[func] pub fn datetime_today( /// An offset to apply to the current UTC date. If set to `{auto}`, the @@ -341,13 +320,13 @@ pub fn datetime_today( #[named] #[default] offset: Smart<i64>, -) -> Value { - let current_date = match vm.vt.world.today(offset.as_custom()) { - Some(d) => d, - None => bail!(args.span, "unable to get the current date"), - }; - - Value::Dyn(Dynamic::new(current_date)) + /// The virtual machine. + vt: &mut Vt, +) -> StrResult<Datetime> { + Ok(vt + .world + .today(offset.as_custom()) + .ok_or("unable to get the current date")?) } /// Create a CMYK color. @@ -365,7 +344,6 @@ pub fn datetime_today( /// /// Display: CMYK /// Category: construct -/// Returns: color #[func] pub fn cmyk( /// The cyan component. @@ -376,14 +354,14 @@ pub fn cmyk( yellow: RatioComponent, /// The key component. key: RatioComponent, -) -> Value { - Value::Color(CmykColor::new(cyan.0, magenta.0, yellow.0, key.0).into()) +) -> Color { + CmykColor::new(cyan.0, magenta.0, yellow.0, key.0).into() } /// A component that must be a ratio. -struct RatioComponent(u8); +pub struct RatioComponent(u8); -cast_from_value! { +cast! { RatioComponent, v: Ratio => if (0.0 ..= 1.0).contains(&v.get()) { Self((v.get() * 255.0).round() as u8) @@ -413,7 +391,6 @@ cast_from_value! { /// /// Display: Symbol /// Category: construct -/// Returns: symbol #[func] pub fn symbol( /// The variants of the symbol. @@ -425,10 +402,12 @@ pub fn symbol( /// all attached modifiers and the minimum number of other modifiers. #[variadic] variants: Vec<Spanned<Variant>>, -) -> Value { + /// The callsite span. + span: Span, +) -> SourceResult<Symbol> { let mut list = Vec::new(); if variants.is_empty() { - bail!(args.span, "expected at least one variant"); + bail!(span, "expected at least one variant"); } for Spanned { v, span } in variants { if list.iter().any(|(prev, _)| &v.0 == prev) { @@ -436,13 +415,13 @@ pub fn symbol( } list.push((v.0, v.1)); } - Value::Symbol(Symbol::runtime(list.into_boxed_slice())) + Ok(Symbol::runtime(list.into_boxed_slice())) } /// A value that can be cast to a symbol. -struct Variant(EcoString, char); +pub struct Variant(EcoString, char); -cast_from_value! { +cast! { Variant, c: char => Self(EcoString::new(), c), array: Array => { @@ -476,11 +455,10 @@ cast_from_value! { /// /// Display: String /// Category: construct -/// Returns: string #[func] #[scope( - scope.define("to-unicode", to_unicode); - scope.define("from-unicode", from_unicode); + scope.define("to-unicode", str_to_unicode_func()); + scope.define("from-unicode", str_from_unicode_func()); scope )] pub fn str( @@ -490,13 +468,13 @@ pub fn str( #[named] #[default(Spanned::new(10, Span::detached()))] base: Spanned<i64>, -) -> Value { - match value { +) -> SourceResult<Str> { + Ok(match value { ToStr::Str(s) => { if base.v != 10 { bail!(base.span, "base is only supported for integers"); } - Value::Str(s) + s } ToStr::Int(n) => { if base.v < 2 || base.v > 36 { @@ -504,18 +482,18 @@ pub fn str( } int_to_base(n, base.v).into() } - } + }) } /// A value that can be cast to a string. -enum ToStr { +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_from_value! { +cast! { ToStr, v: i64 => Self::Int(v), v: f64 => Self::Str(format_str!("{}", v)), @@ -572,16 +550,14 @@ fn int_to_base(mut n: i64, base: i64) -> EcoString { /// /// Display: String To Unicode /// Category: construct -/// Returns: int #[func] -pub fn to_unicode( +pub fn str_to_unicode( /// The character that should be converted. value: char, -) -> Value { - Value::Int(From::<u32>::from(value.into())) +) -> u32 { + value.into() } -#[func] /// Converts a unicode code point into its corresponding string. /// /// ```example @@ -590,18 +566,18 @@ pub fn to_unicode( /// /// Display: Sting From Unicode /// Category: construct -/// Returns: string -pub fn from_unicode( +#[func] +pub fn str_from_unicode( /// The code point that should be converted. value: CodePoint, -) -> Value { - Value::Str(format_str!("{}", value.0)) +) -> Str { + format_str!("{}", value.0) } /// The numeric representation of a single unicode code point. -struct CodePoint(char); +pub struct CodePoint(char); -cast_from_value! { +cast! { CodePoint, v: i64 => { Self(v.try_into().ok().and_then(|v: u32| v.try_into().ok()).ok_or_else( @@ -631,13 +607,12 @@ cast_from_value! { /// /// Display: Label /// Category: construct -/// Returns: label #[func] pub fn label( /// The name of the label. name: EcoString, -) -> Value { - Value::Label(Label(name)) +) -> Label { + Label(name) } /// Create a regular expression from a string. @@ -663,7 +638,6 @@ pub fn label( /// /// Display: Regex /// Category: construct -/// Returns: regex #[func] pub fn regex( /// The regular expression as a string. @@ -677,8 +651,8 @@ pub fn regex( /// and extract its text to use it for your regular expressions: /// ```{regex(`\d+\.\d+\.\d+`.text)}```. regex: Spanned<EcoString>, -) -> Value { - Regex::new(®ex.v).at(regex.span)?.into() +) -> SourceResult<Regex> { + Regex::new(®ex.v).at(regex.span) } /// Create an array consisting of a sequence of numbers. @@ -698,7 +672,6 @@ pub fn regex( /// /// Display: Range /// Category: construct -/// Returns: array #[func] pub fn range( /// The start of the range (inclusive). @@ -712,7 +685,10 @@ pub fn range( #[named] #[default(NonZeroI64::new(1).unwrap())] step: NonZeroI64, -) -> Value { + /// 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), @@ -729,7 +705,7 @@ pub fn range( x += step; } - Value::Array(array) + Ok(array) } #[cfg(test)] diff --git a/library/src/compute/data.rs b/library/src/compute/data.rs index e3918458..b236536d 100644 --- a/library/src/compute/data.rs +++ b/library/src/compute/data.rs @@ -14,21 +14,22 @@ use crate::prelude::*; /// #raw(text, lang: "html") /// ``` /// -/// Display: Plain text +/// Display: Read /// Category: data-loading -/// Returns: string #[func] pub fn read( /// Path to a file. path: Spanned<EcoString>, -) -> Value { + /// The virtual machine. + vm: &mut Vm, +) -> SourceResult<Str> { let Spanned { v: path, span } = path; let path = vm.locate(&path).at(span)?; let data = vm.world().file(&path).at(span)?; let text = std::str::from_utf8(&data) .map_err(|_| "file is not valid utf-8") .at(span)?; - Value::Str(text.into()) + Ok(text.into()) } /// Read structured data from a CSV file. @@ -51,7 +52,6 @@ pub fn read( /// /// Display: CSV /// Category: data-loading -/// Returns: array #[func] pub fn csv( /// Path to a CSV file. @@ -61,7 +61,9 @@ pub fn csv( #[named] #[default] delimiter: Delimiter, -) -> Value { + /// The virtual machine. + vm: &mut Vm, +) -> SourceResult<Array> { let Spanned { v: path, span } = path; let path = vm.locate(&path).at(span)?; let data = vm.world().file(&path).at(span)?; @@ -74,22 +76,30 @@ pub fn csv( 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 + // 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| Value::Str(field.into())).collect(); + let sub = row.into_iter().map(|field| field.into_value()).collect(); array.push(Value::Array(sub)) } - Value::Array(array) + Ok(array) } /// The delimiter to use when parsing CSV files. -struct Delimiter(char); +pub struct Delimiter(char); -cast_from_value! { +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")?; @@ -105,16 +115,6 @@ cast_from_value! { }, } -cast_to_value! { - v: Delimiter => v.0.into() -} - -impl Default for Delimiter { - fn default() -> Self { - Self(',') - } -} - /// Format the user-facing CSV error message. fn format_csv_error(error: csv::Error, line: usize) -> EcoString { match error.kind() { @@ -168,38 +168,39 @@ fn format_csv_error(error: csv::Error, line: usize) -> EcoString { /// /// Display: JSON /// Category: data-loading -/// Returns: array or dictionary #[func] pub fn json( /// Path to a JSON file. path: Spanned<EcoString>, -) -> Value { + /// The virtual machine. + vm: &mut Vm, +) -> SourceResult<Value> { let Spanned { v: path, span } = path; let path = vm.locate(&path).at(span)?; let data = vm.world().file(&path).at(span)?; let value: serde_json::Value = serde_json::from_slice(&data).map_err(format_json_error).at(span)?; - convert_json(value) + 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) => Value::Bool(v), + serde_json::Value::Bool(v) => v.into_value(), serde_json::Value::Number(v) => match v.as_i64() { - Some(int) => Value::Int(int), - None => Value::Float(v.as_f64().unwrap_or(f64::NAN)), + Some(int) => int.into_value(), + None => v.as_f64().unwrap_or(f64::NAN).into_value(), }, - serde_json::Value::String(v) => Value::Str(v.into()), + serde_json::Value::String(v) => v.into_value(), serde_json::Value::Array(v) => { - Value::Array(v.into_iter().map(convert_json).collect()) + v.into_iter().map(convert_json).collect::<Array>().into_value() } - serde_json::Value::Object(v) => Value::Dict( - v.into_iter() - .map(|(key, value)| (key.into(), convert_json(value))) - .collect(), - ), + serde_json::Value::Object(v) => v + .into_iter() + .map(|(key, value)| (key.into(), convert_json(value))) + .collect::<Dict>() + .into_value(), } } @@ -233,12 +234,13 @@ fn format_json_error(error: serde_json::Error) -> EcoString { /// /// Display: TOML /// Category: data-loading -/// Returns: dictionary #[func] pub fn toml( /// Path to a TOML file. path: Spanned<EcoString>, -) -> Value { + /// The virtual machine. + vm: &mut Vm, +) -> SourceResult<Value> { let Spanned { v: path, span } = path; let path = vm.locate(&path).at(span)?; let data = vm.world().file(&path).at(span)?; @@ -248,24 +250,26 @@ pub fn toml( .at(span)?; let value: toml::Value = toml::from_str(raw).map_err(format_toml_error).at(span)?; - convert_toml(value) + 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) => Value::Str(v.into()), - toml::Value::Integer(v) => Value::Int(v), - toml::Value::Float(v) => Value::Float(v), - toml::Value::Boolean(v) => Value::Bool(v), - toml::Value::Array(v) => Value::Array(v.into_iter().map(convert_toml).collect()), - toml::Value::Table(v) => Value::Dict( - v.into_iter() - .map(|(key, value)| (key.into(), convert_toml(value))) - .collect(), - ), + 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(), // TODO: Make it use native date/time object(s) once it is implemented. - toml::Value::Datetime(v) => Value::Str(v.to_string().into()), + toml::Value::Datetime(v) => v.to_string().into_value(), } } @@ -323,39 +327,40 @@ fn format_toml_error(error: toml::de::Error) -> EcoString { /// /// Display: YAML /// Category: data-loading -/// Returns: array or dictionary or none or boolean or integer or float or string #[func] pub fn yaml( /// Path to a YAML file. path: Spanned<EcoString>, -) -> Value { + /// The virtual machine. + vm: &mut Vm, +) -> SourceResult<Value> { let Spanned { v: path, span } = path; let path = vm.locate(&path).at(span)?; let data = vm.world().file(&path).at(span)?; let value: serde_yaml::Value = serde_yaml::from_slice(&data).map_err(format_yaml_error).at(span)?; - convert_yaml(value) + 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) => Value::Bool(v), + serde_yaml::Value::Bool(v) => v.into_value(), serde_yaml::Value::Number(v) => match v.as_i64() { - Some(int) => Value::Int(int), - None => Value::Float(v.as_f64().unwrap_or(f64::NAN)), + Some(int) => int.into_value(), + None => v.as_f64().unwrap_or(f64::NAN).into_value(), }, - serde_yaml::Value::String(v) => Value::Str(v.into()), + serde_yaml::Value::String(v) => v.into_value(), serde_yaml::Value::Sequence(v) => { - Value::Array(v.into_iter().map(convert_yaml).collect()) + v.into_iter().map(convert_yaml).collect::<Array>().into_value() } - serde_yaml::Value::Mapping(v) => Value::Dict( - v.into_iter() - .map(|(key, value)| (convert_yaml_key(key), convert_yaml(value))) - .filter_map(|(key, value)| key.map(|key| (key, value))) - .collect(), - ), + 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(), } } @@ -425,24 +430,25 @@ fn format_yaml_error(error: serde_yaml::Error) -> EcoString { /// /// Display: XML /// Category: data-loading -/// Returns: array #[func] pub fn xml( /// Path to an XML file. path: Spanned<EcoString>, -) -> Value { + /// The virtual machine. + vm: &mut Vm, +) -> SourceResult<Value> { let Spanned { v: path, span } = path; let path = vm.locate(&path).at(span)?; let data = vm.world().file(&path).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)?; - convert_xml(document.root()) + 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 Value::Str(node.text().unwrap_or_default().into()); + return node.text().unwrap_or_default().into_value(); } let children: Array = node.children().map(convert_xml).collect(); @@ -453,7 +459,7 @@ fn convert_xml(node: roxmltree::Node) -> Value { let tag: Str = node.tag_name().name().into(); let attrs: Dict = node .attributes() - .map(|attr| (attr.name().into(), attr.value().into())) + .map(|attr| (attr.name().into(), attr.value().into_value())) .collect(); Value::Dict(dict! { diff --git a/library/src/compute/foundations.rs b/library/src/compute/foundations.rs index 6a92c2a8..7964ac45 100644 --- a/library/src/compute/foundations.rs +++ b/library/src/compute/foundations.rs @@ -16,12 +16,11 @@ use crate::prelude::*; /// /// Display: Type /// Category: foundations -/// Returns: string #[func] pub fn type_( /// The value whose type's to determine. value: Value, -) -> Value { +) -> Str { value.type_name().into() } @@ -44,13 +43,12 @@ pub fn type_( /// /// Display: Representation /// Category: foundations -/// Returns: string #[func] pub fn repr( /// The value whose string representation to produce. value: Value, -) -> Value { - value.repr().into() +) -> Str { + value.repr() } /// Fail with an error. @@ -63,13 +61,12 @@ pub fn repr( /// /// Display: Panic /// Category: foundations -/// Returns: #[func] pub fn panic( /// The values to panic with. #[variadic] values: Vec<Value>, -) -> Value { +) -> StrResult<Never> { let mut msg = EcoString::from("panicked"); if !values.is_empty() { msg.push_str(" with: "); @@ -80,7 +77,7 @@ pub fn panic( msg.push_str(&value.repr()); } } - bail!(args.span, msg); + Err(msg) } /// Ensure that a condition is fulfilled. @@ -98,11 +95,10 @@ pub fn panic( /// /// Display: Assert /// Category: foundations -/// Returns: #[func] #[scope( - scope.define("eq", assert_eq); - scope.define("ne", assert_ne); + scope.define("eq", assert_eq_func()); + scope.define("ne", assert_ne_func()); scope )] pub fn assert( @@ -110,17 +106,16 @@ pub fn assert( condition: bool, /// The error message when the assertion fails. #[named] - #[default] message: Option<EcoString>, -) -> Value { +) -> StrResult<NoneValue> { if !condition { if let Some(message) = message { - bail!(args.span, "assertion failed: {}", message); + bail!("assertion failed: {message}"); } else { - bail!(args.span, "assertion failed"); + bail!("assertion failed"); } } - Value::None + Ok(NoneValue) } /// Ensure that two values are equal. @@ -135,7 +130,6 @@ pub fn assert( /// /// Display: Assert Equals /// Category: foundations -/// Returns: #[func] pub fn assert_eq( /// The first value to compare. @@ -147,22 +141,16 @@ pub fn assert_eq( /// An optional message to display on error instead of the representations /// of the compared values. #[named] - #[default] message: Option<EcoString>, -) -> Value { +) -> StrResult<NoneValue> { if left != right { if let Some(message) = message { - bail!(args.span, "equality assertion failed: {}", message); + bail!("equality assertion failed: {message}"); } else { - bail!( - args.span, - "equality assertion failed: value {:?} was not equal to {:?}", - left, - right - ); + bail!("equality assertion failed: value {left:?} was not equal to {right:?}"); } } - Value::None + Ok(NoneValue) } /// Ensure that two values are not equal. @@ -177,7 +165,6 @@ pub fn assert_eq( /// /// Display: Assert Not Equals /// Category: foundations -/// Returns: #[func] pub fn assert_ne( /// The first value to compare. @@ -189,22 +176,16 @@ pub fn assert_ne( /// An optional message to display on error instead of the representations /// of the compared values. #[named] - #[default] message: Option<EcoString>, -) -> Value { +) -> StrResult<NoneValue> { if left == right { if let Some(message) = message { - bail!(args.span, "inequality assertion failed: {}", message); + bail!("inequality assertion failed: {message}"); } else { - bail!( - args.span, - "inequality assertion failed: value {:?} was equal to {:?}", - left, - right - ); + bail!("inequality assertion failed: value {left:?} was equal to {right:?}"); } } - Value::None + Ok(NoneValue) } /// Evaluate a string as Typst code. @@ -220,14 +201,15 @@ pub fn assert_ne( /// /// Display: Evaluate /// Category: foundations -/// Returns: any #[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>, -) -> Value { + /// The virtual machine. + vm: &mut Vm, +) -> SourceResult<Value> { let Spanned { v: text, span } = source; - typst::eval::eval_string(vm.world(), &text, span)? + typst::eval::eval_string(vm.world(), &text, span) } diff --git a/library/src/compute/mod.rs b/library/src/compute/mod.rs index 90730e02..e9e4870c 100644 --- a/library/src/compute/mod.rs +++ b/library/src/compute/mod.rs @@ -13,27 +13,27 @@ use crate::prelude::*; /// Hook up all compute definitions. pub(super) fn define(global: &mut Scope) { - global.define("type", type_); - global.define("repr", repr); - global.define("panic", panic); - global.define("assert", assert); - global.define("eval", eval); - global.define("int", int); - global.define("float", float); - global.define("luma", luma); - global.define("rgb", rgb); - global.define("cmyk", cmyk); - global.define("datetime", datetime); - global.define("symbol", symbol); - global.define("str", str); - global.define("label", label); - global.define("regex", regex); - global.define("range", range); - global.define("read", read); - global.define("csv", csv); - global.define("json", json); - global.define("toml", toml); - global.define("yaml", yaml); - global.define("xml", xml); + 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()); } diff --git a/library/src/layout/container.rs b/library/src/layout/container.rs index b7d0ba2f..d28c8b5e 100644 --- a/library/src/layout/container.rs +++ b/library/src/layout/container.rs @@ -1,3 +1,5 @@ +use typst::eval::AutoValue; + use super::VElem; use crate::layout::Spacing; use crate::prelude::*; @@ -482,17 +484,14 @@ impl<T: Into<Spacing>> From<T> for Sizing { } } -cast_from_value! { +cast! { Sizing, - _: Smart<Never> => Self::Auto, + self => match self { + Self::Auto => Value::Auto, + Self::Rel(rel) => rel.into_value(), + Self::Fr(fr) => fr.into_value(), + }, + _: AutoValue => Self::Auto, v: Rel<Length> => Self::Rel(v), v: Fr => Self::Fr(v), } - -cast_to_value! { - v: Sizing => match v { - Sizing::Auto => Value::Auto, - Sizing::Rel(rel) => Value::Relative(rel), - Sizing::Fr(fr) => Value::Fraction(fr), - } -} diff --git a/library/src/layout/enum.rs b/library/src/layout/enum.rs index 0fd0ebb2..62f4c351 100644 --- a/library/src/layout/enum.rs +++ b/library/src/layout/enum.rs @@ -283,7 +283,7 @@ pub struct EnumItem { pub body: Content, } -cast_from_value! { +cast! { EnumItem, array: Array => { let mut iter = array.into_iter(); @@ -298,15 +298,12 @@ cast_from_value! { struct Parent(usize); -cast_from_value! { +cast! { Parent, + self => self.0.into_value(), v: usize => Self(v), } -cast_to_value! { - v: Parent => v.0.into() -} - impl Fold for Parent { type Output = Vec<usize>; diff --git a/library/src/layout/grid.rs b/library/src/layout/grid.rs index 3c299eb0..ea155acd 100644 --- a/library/src/layout/grid.rs +++ b/library/src/layout/grid.rs @@ -129,17 +129,14 @@ impl Layout for GridElem { #[derive(Debug, Default, Clone, Eq, PartialEq, Hash)] pub struct TrackSizings(pub Vec<Sizing>); -cast_from_value! { +cast! { TrackSizings, + self => self.0.into_value(), sizing: Sizing => Self(vec![sizing]), count: NonZeroUsize => Self(vec![Sizing::Auto; count.get()]), values: Array => Self(values.into_iter().map(Value::cast).collect::<StrResult<_>>()?), } -cast_to_value! { - v: TrackSizings => v.0.into() -} - /// Performs grid layout. pub struct GridLayouter<'a, 'v> { /// The core context. diff --git a/library/src/layout/list.rs b/library/src/layout/list.rs index 1e42d51b..b308c2ce 100644 --- a/library/src/layout/list.rs +++ b/library/src/layout/list.rs @@ -174,7 +174,7 @@ pub struct ListItem { pub body: Content, } -cast_from_value! { +cast! { ListItem, v: Content => v.to::<Self>().cloned().unwrap_or_else(|| Self::new(v.clone())), } @@ -193,13 +193,21 @@ impl ListMarker { Self::Content(list) => { list.get(depth).or(list.last()).cloned().unwrap_or_default() } - Self::Func(func) => func.call_vt(vt, [Value::Int(depth as i64)])?.display(), + Self::Func(func) => func.call_vt(vt, [depth])?.display(), }) } } -cast_from_value! { +cast! { ListMarker, + self => match self { + Self::Content(vec) => if vec.len() == 1 { + vec.into_iter().next().unwrap().into_value() + } else { + vec.into_value() + }, + Self::Func(func) => func.into_value(), + }, v: Content => Self::Content(vec![v]), array: Array => { if array.is_empty() { @@ -210,28 +218,14 @@ cast_from_value! { v: Func => Self::Func(v), } -cast_to_value! { - v: ListMarker => match v { - ListMarker::Content(vec) => if vec.len() == 1 { - vec.into_iter().next().unwrap().into() - } else { - vec.into() - }, - ListMarker::Func(func) => func.into(), - } -} - struct Depth; -cast_from_value! { +cast! { Depth, + self => Value::None, _: Value => Self, } -cast_to_value! { - _: Depth => Value::None -} - impl Fold for Depth { type Output = usize; diff --git a/library/src/layout/measure.rs b/library/src/layout/measure.rs index c7a19243..4cbd1698 100644 --- a/library/src/layout/measure.rs +++ b/library/src/layout/measure.rs @@ -39,17 +39,18 @@ use crate::prelude::*; /// /// Display: Measure /// Category: layout -/// Returns: dictionary #[func] pub fn measure( /// The content whose size to measure. content: Content, /// The styles with which to layout the content. styles: Styles, -) -> Value { + /// The virtual machine. + vm: &mut Vm, +) -> SourceResult<Dict> { let pod = Regions::one(Axes::splat(Abs::inf()), Axes::splat(false)); let styles = StyleChain::new(&styles); let frame = content.measure(&mut vm.vt, styles, pod)?.into_frame(); let Size { x, y } = frame.size(); - dict! { "width" => x, "height" => y }.into() + Ok(dict! { "width" => x, "height" => y }) } diff --git a/library/src/layout/mod.rs b/library/src/layout/mod.rs index 2d5b18c8..9b6ba204 100644 --- a/library/src/layout/mod.rs +++ b/library/src/layout/mod.rs @@ -87,7 +87,19 @@ pub(super) fn define(global: &mut Scope) { global.define("scale", ScaleElem::func()); global.define("rotate", RotateElem::func()); global.define("hide", HideElem::func()); - global.define("measure", measure); + global.define("measure", measure_func()); + global.define("ltr", Dir::LTR); + global.define("rtl", Dir::RTL); + global.define("ttb", Dir::TTB); + global.define("btt", Dir::BTT); + global.define("start", GenAlign::Start); + global.define("end", GenAlign::End); + global.define("left", GenAlign::Specific(Align::Left)); + global.define("center", GenAlign::Specific(Align::Center)); + global.define("right", GenAlign::Specific(Align::Right)); + global.define("top", GenAlign::Specific(Align::Top)); + global.define("horizon", GenAlign::Specific(Align::Horizon)); + global.define("bottom", GenAlign::Specific(Align::Bottom)); } /// Root-level layout. diff --git a/library/src/layout/page.rs b/library/src/layout/page.rs index 6510bd58..26ebbc53 100644 --- a/library/src/layout/page.rs +++ b/library/src/layout/page.rs @@ -341,7 +341,7 @@ impl PageElem { let footer_descent = self.footer_descent(styles); let numbering_meta = FrameItem::Meta( - Meta::PageNumbering(self.numbering(styles).into()), + Meta::PageNumbering(self.numbering(styles).into_value()), Size::zero(), ); @@ -451,24 +451,21 @@ impl Marginal { pub fn resolve(&self, vt: &mut Vt, page: usize) -> SourceResult<Content> { Ok(match self { Self::Content(content) => content.clone(), - Self::Func(func) => func.call_vt(vt, [Value::Int(page as i64)])?.display(), + Self::Func(func) => func.call_vt(vt, [page])?.display(), }) } } -cast_from_value! { +cast! { Marginal, + self => match self { + Self::Content(v) => v.into_value(), + Self::Func(v) => v.into_value(), + }, v: Content => Self::Content(v), v: Func => Self::Func(v), } -cast_to_value! { - v: Marginal => match v { - Marginal::Content(v) => v.into(), - Marginal::Func(v) => v.into(), - } -} - /// Specification of a paper. #[derive(Debug, Copy, Clone, Hash)] pub struct Paper { @@ -517,17 +514,14 @@ macro_rules! papers { } } - cast_from_value! { + cast! { Paper, + self => self.name.into_value(), $( /// Produces a paper of the respective size. $name => Self::$var, )* } - - cast_to_value! { - v: Paper => v.name.into() - } }; } diff --git a/library/src/layout/spacing.rs b/library/src/layout/spacing.rs index 588f9f29..0cbf6641 100644 --- a/library/src/layout/spacing.rs +++ b/library/src/layout/spacing.rs @@ -157,7 +157,7 @@ impl Behave for VElem { } } -cast_from_value! { +cast! { VElem, v: Content => v.to::<Self>().cloned().ok_or("expected `v` element")?, } @@ -221,23 +221,20 @@ impl PartialOrd for Spacing { } } -cast_from_value! { +cast! { Spacing, - v: Rel<Length> => Self::Rel(v), - v: Fr => Self::Fr(v), -} - -cast_to_value! { - v: Spacing => match v { - Spacing::Rel(rel) => { + self => match self { + Self::Rel(rel) => { if rel.rel.is_zero() { - Value::Length(rel.abs) + rel.abs.into_value() } else if rel.abs.is_zero() { - Value::Ratio(rel.rel) + rel.rel.into_value() } else { - Value::Relative(rel) + rel.into_value() } } - Spacing::Fr(fr) => Value::Fraction(fr), - } + Self::Fr(fr) => fr.into_value(), + }, + v: Rel<Length> => Self::Rel(v), + v: Fr => Self::Fr(v), } diff --git a/library/src/layout/stack.rs b/library/src/layout/stack.rs index 2ce2cc28..f8670026 100644 --- a/library/src/layout/stack.rs +++ b/library/src/layout/stack.rs @@ -90,19 +90,16 @@ impl Debug for StackChild { } } -cast_from_value! { +cast! { StackChild, + self => match self { + Self::Spacing(spacing) => spacing.into_value(), + Self::Block(content) => content.into_value(), + }, v: Spacing => Self::Spacing(v), v: Content => Self::Block(v), } -cast_to_value! { - v: StackChild => match v { - StackChild::Spacing(spacing) => spacing.into(), - StackChild::Block(content) => content.into(), - } -} - /// Performs stack layout. struct StackLayouter<'a> { /// The stacking direction. diff --git a/library/src/layout/table.rs b/library/src/layout/table.rs index 9d5a83d2..74370cb2 100644 --- a/library/src/layout/table.rs +++ b/library/src/layout/table.rs @@ -1,3 +1,5 @@ +use typst::eval::{CastInfo, Reflect}; + use crate::layout::{AlignElem, GridLayouter, TrackSizings}; use crate::meta::{Figurable, LocalName}; use crate::prelude::*; @@ -243,15 +245,12 @@ pub enum Celled<T> { Array(Vec<T>), } -impl<T: Cast + Clone + Default> Celled<T> { +impl<T: Default + Clone + FromValue> Celled<T> { /// Resolve the value based on the cell position. pub fn resolve(&self, vt: &mut Vt, x: usize, y: usize) -> SourceResult<T> { Ok(match self { Self::Value(value) => value.clone(), - Self::Func(func) => func - .call_vt(vt, [Value::Int(x as i64), Value::Int(y as i64)])? - .cast() - .at(func.span())?, + Self::Func(func) => func.call_vt(vt, [x, y])?.cast().at(func.span())?, Self::Array(array) => x .checked_rem(array.len()) .and_then(|i| array.get(i)) @@ -267,33 +266,35 @@ impl<T: Default> Default for Celled<T> { } } -impl<T: Cast> Cast for Celled<T> { - fn is(value: &Value) -> bool { - matches!(value, Value::Array(_) | Value::Func(_)) || T::is(value) +impl<T: Reflect> Reflect for Celled<T> { + fn describe() -> CastInfo { + T::describe() + Array::describe() + Func::describe() } - fn cast(value: Value) -> StrResult<Self> { - match value { - Value::Func(v) => Ok(Self::Func(v)), - Value::Array(array) => { - Ok(Self::Array(array.into_iter().map(T::cast).collect::<StrResult<_>>()?)) - } - v if T::is(&v) => Ok(Self::Value(T::cast(v)?)), - v => <Self as Cast>::error(v), - } + fn castable(value: &Value) -> bool { + Array::castable(value) || Func::castable(value) || T::castable(value) } +} - fn describe() -> CastInfo { - T::describe() + CastInfo::Type("array") + CastInfo::Type("function") +impl<T: IntoValue> IntoValue for Celled<T> { + fn into_value(self) -> Value { + match self { + Self::Value(value) => value.into_value(), + Self::Func(func) => func.into_value(), + Self::Array(arr) => arr.into_value(), + } } } -impl<T: Into<Value>> From<Celled<T>> for Value { - fn from(celled: Celled<T>) -> Self { - match celled { - Celled::Value(value) => value.into(), - Celled::Func(func) => func.into(), - Celled::Array(arr) => arr.into(), +impl<T: FromValue> FromValue for Celled<T> { + fn from_value(value: Value) -> StrResult<Self> { + match value { + Value::Func(v) => Ok(Self::Func(v)), + Value::Array(array) => Ok(Self::Array( + array.into_iter().map(T::from_value).collect::<StrResult<_>>()?, + )), + v if T::castable(&v) => Ok(Self::Value(T::from_value(v)?)), + v => Err(Self::error(&v)), } } } diff --git a/library/src/layout/terms.rs b/library/src/layout/terms.rs index 43127d97..0cfe98d9 100644 --- a/library/src/layout/terms.rs +++ b/library/src/layout/terms.rs @@ -147,7 +147,7 @@ pub struct TermItem { pub description: Content, } -cast_from_value! { +cast! { TermItem, array: Array => { let mut iter = array.into_iter(); diff --git a/library/src/lib.rs b/library/src/lib.rs index 0f8346f0..0bd88501 100644 --- a/library/src/lib.rs +++ b/library/src/lib.rs @@ -1,6 +1,7 @@ //! Typst's standard library. #![allow(clippy::wildcard_in_or_patterns)] +#![allow(clippy::manual_range_contains)] #![allow(clippy::comparison_chain)] pub mod compute; @@ -15,7 +16,7 @@ pub mod visualize; use typst::diag::At; use typst::eval::{LangItems, Library, Module, Scope}; -use typst::geom::{Align, Color, Dir, GenAlign, Smart}; +use typst::geom::Smart; use typst::model::{Element, Styles}; use self::layout::LayoutRoot; @@ -41,40 +42,6 @@ fn global(math: Module) -> Module { symbols::define(&mut global); global.define("math", math); - // Colors. - global.define("black", Color::BLACK); - global.define("gray", Color::GRAY); - global.define("silver", Color::SILVER); - global.define("white", Color::WHITE); - global.define("navy", Color::NAVY); - global.define("blue", Color::BLUE); - global.define("aqua", Color::AQUA); - global.define("teal", Color::TEAL); - global.define("eastern", Color::EASTERN); - global.define("purple", Color::PURPLE); - global.define("fuchsia", Color::FUCHSIA); - global.define("maroon", Color::MAROON); - global.define("red", Color::RED); - global.define("orange", Color::ORANGE); - global.define("yellow", Color::YELLOW); - global.define("olive", Color::OLIVE); - global.define("green", Color::GREEN); - global.define("lime", Color::LIME); - - // Other constants. - global.define("ltr", Dir::LTR); - global.define("rtl", Dir::RTL); - global.define("ttb", Dir::TTB); - global.define("btt", Dir::BTT); - global.define("start", GenAlign::Start); - global.define("end", GenAlign::End); - global.define("left", GenAlign::Specific(Align::Left)); - global.define("center", GenAlign::Specific(Align::Center)); - global.define("right", GenAlign::Specific(Align::Right)); - global.define("top", GenAlign::Specific(Align::Top)); - global.define("horizon", GenAlign::Specific(Align::Horizon)); - global.define("bottom", GenAlign::Specific(Align::Bottom)); - Module::new("global").with_scope(global) } diff --git a/library/src/math/accent.rs b/library/src/math/accent.rs index 7878782b..4a12f5c5 100644 --- a/library/src/math/accent.rs +++ b/library/src/math/accent.rs @@ -128,15 +128,12 @@ impl Accent { } } -cast_from_value! { +cast! { Accent, + self => self.0.into_value(), v: char => Self::new(v), v: Content => match v.to::<TextElem>() { Some(elem) => Value::Str(elem.text().into()).cast()?, None => Err("expected text")?, }, } - -cast_to_value! { - v: Accent => v.0.into() -} diff --git a/library/src/math/delimited.rs b/library/src/math/delimited.rs index 403f7922..08d18a5e 100644 --- a/library/src/math/delimited.rs +++ b/library/src/math/delimited.rs @@ -113,12 +113,11 @@ fn scale( /// /// Display: Floor /// Category: math -/// Returns: content #[func] pub fn floor( /// The expression to floor. body: Content, -) -> Value { +) -> Content { delimited(body, '⌊', '⌋') } @@ -131,12 +130,11 @@ pub fn floor( /// /// Display: Ceil /// Category: math -/// Returns: content #[func] pub fn ceil( /// The expression to ceil. body: Content, -) -> Value { +) -> Content { delimited(body, '⌈', '⌉') } @@ -149,12 +147,11 @@ pub fn ceil( /// /// Display: Round /// Category: math -/// Returns: content #[func] pub fn round( /// The expression to round. body: Content, -) -> Value { +) -> Content { delimited(body, '⌊', '⌉') } @@ -168,12 +165,11 @@ pub fn round( /// /// Display: Abs /// Category: math -/// Returns: content #[func] pub fn abs( /// The expression to take the absolute value of. body: Content, -) -> Value { +) -> Content { delimited(body, '|', '|') } @@ -186,21 +182,19 @@ pub fn abs( /// /// Display: Norm /// Category: math -/// Returns: content #[func] pub fn norm( /// The expression to take the norm of. body: Content, -) -> Value { +) -> Content { delimited(body, '‖', '‖') } -fn delimited(body: Content, left: char, right: char) -> Value { +fn delimited(body: Content, left: char, right: char) -> Content { LrElem::new(Content::sequence([ TextElem::packed(left), body, TextElem::packed(right), ])) .pack() - .into() } diff --git a/library/src/math/mod.rs b/library/src/math/mod.rs index 3ea96cfa..4ab23f23 100644 --- a/library/src/math/mod.rs +++ b/library/src/math/mod.rs @@ -59,11 +59,11 @@ pub fn module() -> Module { // Grouping. math.define("lr", LrElem::func()); - math.define("abs", abs); - math.define("norm", norm); - math.define("floor", floor); - math.define("ceil", ceil); - math.define("round", round); + math.define("abs", abs_func()); + math.define("norm", norm_func()); + math.define("floor", floor_func()); + math.define("ceil", ceil_func()); + math.define("round", round_func()); // Attachments and accents. math.define("attach", AttachElem::func()); @@ -86,24 +86,24 @@ pub fn module() -> Module { math.define("cases", CasesElem::func()); // Roots. - math.define("sqrt", sqrt); + math.define("sqrt", sqrt_func()); math.define("root", RootElem::func()); // Styles. - math.define("upright", upright); - math.define("bold", bold); - math.define("italic", italic); - math.define("serif", serif); - math.define("sans", sans); - math.define("cal", cal); - math.define("frak", frak); - math.define("mono", mono); - math.define("bb", bb); - - math.define("display", display); - math.define("inline", inline); - math.define("script", script); - math.define("sscript", sscript); + math.define("upright", upright_func()); + math.define("bold", bold_func()); + math.define("italic", italic_func()); + math.define("serif", serif_func()); + math.define("sans", sans_func()); + math.define("cal", cal_func()); + math.define("frak", frak_func()); + math.define("mono", mono_func()); + math.define("bb", bb_func()); + + math.define("display", display_func()); + math.define("inline", inline_func()); + math.define("script", script_func()); + math.define("sscript", sscript_func()); // Text operators. math.define("op", OpElem::func()); @@ -197,9 +197,7 @@ impl Synthesize for EquationElem { let supplement = match self.supplement(styles) { Smart::Auto => TextElem::packed(self.local_name_in(styles)), Smart::Custom(None) => Content::empty(), - Smart::Custom(Some(supplement)) => { - supplement.resolve(vt, [self.clone().into()])? - } + Smart::Custom(Some(supplement)) => supplement.resolve(vt, [self.clone()])?, }; self.push_block(self.block(styles)); diff --git a/library/src/math/root.rs b/library/src/math/root.rs index 05f3a2a7..d1c5f46a 100644 --- a/library/src/math/root.rs +++ b/library/src/math/root.rs @@ -9,13 +9,12 @@ use super::*; /// /// Display: Square Root /// Category: math -/// Returns: content #[func] pub fn sqrt( /// The expression to take the square root of. radicand: Content, -) -> Value { - RootElem::new(radicand).pack().into() +) -> Content { + RootElem::new(radicand).pack() } /// A general root. diff --git a/library/src/math/row.rs b/library/src/math/row.rs index 9b693bc1..6e666e89 100644 --- a/library/src/math/row.rs +++ b/library/src/math/row.rs @@ -233,3 +233,22 @@ impl<T: Into<MathFragment>> From<T> for MathRow { Self(vec![fragment.into()]) } } + +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +enum LeftRightAlternator { + Left, + Right, +} + +impl Iterator for LeftRightAlternator { + type Item = LeftRightAlternator; + + fn next(&mut self) -> Option<Self::Item> { + let r = Some(*self); + match self { + Self::Left => *self = Self::Right, + Self::Right => *self = Self::Left, + } + r + } +} diff --git a/library/src/math/style.rs b/library/src/math/style.rs index e0e1ccad..05ff64dc 100644 --- a/library/src/math/style.rs +++ b/library/src/math/style.rs @@ -9,13 +9,12 @@ use super::*; /// /// Display: Bold /// Category: math -/// Returns: content #[func] pub fn bold( /// The content to style. body: Content, -) -> Value { - MathStyleElem::new(body).with_bold(Some(true)).pack().into() +) -> Content { + MathStyleElem::new(body).with_bold(Some(true)).pack() } /// Upright (non-italic) font style in math. @@ -27,13 +26,12 @@ pub fn bold( /// /// Display: Upright /// Category: math -/// Returns: content #[func] pub fn upright( /// The content to style. body: Content, -) -> Value { - MathStyleElem::new(body).with_italic(Some(false)).pack().into() +) -> Content { + MathStyleElem::new(body).with_italic(Some(false)).pack() } /// Italic font style in math. @@ -42,13 +40,12 @@ pub fn upright( /// /// Display: Italic /// Category: math -/// Returns: content #[func] pub fn italic( /// The content to style. body: Content, -) -> Value { - MathStyleElem::new(body).with_italic(Some(true)).pack().into() +) -> Content { + MathStyleElem::new(body).with_italic(Some(true)).pack() } /// Serif (roman) font style in math. /// @@ -56,16 +53,12 @@ pub fn italic( /// /// Display: Serif /// Category: math -/// Returns: content #[func] pub fn serif( /// The content to style. body: Content, -) -> Value { - MathStyleElem::new(body) - .with_variant(Some(MathVariant::Serif)) - .pack() - .into() +) -> Content { + MathStyleElem::new(body).with_variant(Some(MathVariant::Serif)).pack() } /// Sans-serif font style in math. @@ -77,16 +70,12 @@ pub fn serif( /// /// Display: Sans-serif /// Category: math -/// Returns: content #[func] pub fn sans( /// The content to style. body: Content, -) -> Value { - MathStyleElem::new(body) - .with_variant(Some(MathVariant::Sans)) - .pack() - .into() +) -> Content { + MathStyleElem::new(body).with_variant(Some(MathVariant::Sans)).pack() } /// Calligraphic font style in math. @@ -98,16 +87,12 @@ pub fn sans( /// /// Display: Calligraphic /// Category: math -/// Returns: content #[func] pub fn cal( /// The content to style. body: Content, -) -> Value { - MathStyleElem::new(body) - .with_variant(Some(MathVariant::Cal)) - .pack() - .into() +) -> Content { + MathStyleElem::new(body).with_variant(Some(MathVariant::Cal)).pack() } /// Fraktur font style in math. @@ -119,16 +104,12 @@ pub fn cal( /// /// Display: Fraktur /// Category: math -/// Returns: content #[func] pub fn frak( /// The content to style. body: Content, -) -> Value { - MathStyleElem::new(body) - .with_variant(Some(MathVariant::Frak)) - .pack() - .into() +) -> Content { + MathStyleElem::new(body).with_variant(Some(MathVariant::Frak)).pack() } /// Monospace font style in math. @@ -140,16 +121,12 @@ pub fn frak( /// /// Display: Monospace /// Category: math -/// Returns: content #[func] pub fn mono( /// The content to style. body: Content, -) -> Value { - MathStyleElem::new(body) - .with_variant(Some(MathVariant::Mono)) - .pack() - .into() +) -> Content { + MathStyleElem::new(body).with_variant(Some(MathVariant::Mono)).pack() } /// Blackboard bold (double-struck) font style in math. @@ -166,16 +143,12 @@ pub fn mono( /// /// Display: Blackboard Bold /// Category: math -/// Returns: content #[func] pub fn bb( /// The content to style. body: Content, -) -> Value { - MathStyleElem::new(body) - .with_variant(Some(MathVariant::Bb)) - .pack() - .into() +) -> Content { + MathStyleElem::new(body).with_variant(Some(MathVariant::Bb)).pack() } /// Forced display style in math. @@ -189,7 +162,6 @@ pub fn bb( /// /// Display: Display Size /// Category: math -/// Returns: content #[func] pub fn display( /// The content to size. @@ -199,12 +171,11 @@ pub fn display( #[named] #[default(false)] cramp: bool, -) -> Value { +) -> Content { MathStyleElem::new(body) .with_size(Some(MathSize::Display)) .with_cramp(Some(cramp)) .pack() - .into() } /// Forced inline (text) style in math. @@ -219,7 +190,6 @@ pub fn display( /// /// Display: Inline Size /// Category: math -/// Returns: content #[func] pub fn inline( /// The content to size. @@ -229,12 +199,11 @@ pub fn inline( #[named] #[default(false)] cramp: bool, -) -> Value { +) -> Content { MathStyleElem::new(body) .with_size(Some(MathSize::Text)) .with_cramp(Some(cramp)) .pack() - .into() } /// Forced script style in math. @@ -248,7 +217,6 @@ pub fn inline( /// /// Display: Script Size /// Category: math -/// Returns: content #[func] pub fn script( /// The content to size. @@ -258,12 +226,11 @@ pub fn script( #[named] #[default(true)] cramp: bool, -) -> Value { +) -> Content { MathStyleElem::new(body) .with_size(Some(MathSize::Script)) .with_cramp(Some(cramp)) .pack() - .into() } /// Forced second script style in math. @@ -278,7 +245,6 @@ pub fn script( /// /// Display: Script-Script Size /// Category: math -/// Returns: content #[func] pub fn sscript( /// The content to size. @@ -288,12 +254,11 @@ pub fn sscript( #[named] #[default(true)] cramp: bool, -) -> Value { +) -> Content { MathStyleElem::new(body) .with_size(Some(MathSize::ScriptScript)) .with_cramp(Some(cramp)) .pack() - .into() } /// A font variant in math. diff --git a/library/src/meta/bibliography.rs b/library/src/meta/bibliography.rs index a2491bb5..5e5145b1 100644 --- a/library/src/meta/bibliography.rs +++ b/library/src/meta/bibliography.rs @@ -84,16 +84,13 @@ pub struct BibliographyElem { #[derive(Debug, Default, Clone, Hash)] pub struct BibPaths(Vec<EcoString>); -cast_from_value! { +cast! { BibPaths, + self => self.0.into_value(), v: EcoString => Self(vec![v]), v: Array => Self(v.into_iter().map(Value::cast).collect::<StrResult<_>>()?), } -cast_to_value! { - v: BibPaths => v.0.into() -} - impl BibliographyElem { /// Find the document's bibliography. pub fn find(introspector: Tracked<Introspector>) -> StrResult<Self> { @@ -374,7 +371,7 @@ impl Show for CiteElem { } } -cast_from_value! { +cast! { CiteElem, v: Content => v.to::<Self>().cloned().ok_or("expected citation")?, } diff --git a/library/src/meta/context.rs b/library/src/meta/context.rs index ad466305..d599c63e 100644 --- a/library/src/meta/context.rs +++ b/library/src/meta/context.rs @@ -46,7 +46,6 @@ use crate::prelude::*; /// /// Display: Locate /// Category: meta -/// Returns: content #[func] pub fn locate( /// A function that receives a `location`. Its return value is displayed @@ -56,8 +55,8 @@ pub fn locate( /// `locate` appears in the document. That makes it possible to generate /// content that depends on its own location in the document. func: Func, -) -> Value { - LocateElem::new(func).pack().into() +) -> Content { + LocateElem::new(func).pack() } /// Executes a `locate` call. @@ -79,7 +78,7 @@ impl Show for LocateElem { } let location = self.0.location().unwrap(); - Ok(self.func().call_vt(vt, [location.into()])?.display()) + Ok(self.func().call_vt(vt, [location])?.display()) } } @@ -102,7 +101,6 @@ impl Show for LocateElem { /// /// Display: Style /// Category: meta -/// Returns: content #[func] pub fn style( /// A function to call with the styles. Its return value is displayed @@ -112,8 +110,8 @@ pub fn style( /// `style` appears in the document. That makes it possible to generate /// content that depends on the style context it appears in. func: Func, -) -> Value { - StyleElem::new(func).pack().into() +) -> Content { + StyleElem::new(func).pack() } /// Executes a style access. @@ -130,7 +128,7 @@ struct StyleElem { impl Show for StyleElem { #[tracing::instrument(name = "StyleElem::show", skip_all)] fn show(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Content> { - Ok(self.func().call_vt(vt, [styles.to_map().into()])?.display()) + Ok(self.func().call_vt(vt, [styles.to_map()])?.display()) } } @@ -177,7 +175,6 @@ impl Show for StyleElem { /// /// Display: Layout /// Category: meta -/// Returns: content #[func] pub fn layout( /// A function to call with the outer container's size. Its return value is @@ -190,8 +187,8 @@ pub fn layout( /// `layout` appears in the document. That makes it possible to generate /// content that depends on the size of the container it is inside of. func: Func, -) -> Value { - LayoutElem::new(func).pack().into() +) -> Content { + LayoutElem::new(func).pack() } /// Executes a `layout` call. @@ -213,16 +210,13 @@ impl Layout for LayoutElem { styles: StyleChain, regions: Regions, ) -> SourceResult<Fragment> { - // Gets the current region's base size, which will be the size of the outer container, - // or of the page if there is no such container. + // Gets the current region's base size, which will be the size of the + // outer container, or of the page if there is no such container. let Size { x, y } = regions.base(); - let size_dict = dict! { "width" => x, "height" => y }.into(); - let result = self .func() - .call_vt(vt, [size_dict])? // calls func(size) + .call_vt(vt, [dict! { "width" => x, "height" => y }])? .display(); - result.layout(vt, styles, regions) } } diff --git a/library/src/meta/counter.rs b/library/src/meta/counter.rs index 46992d38..ef4646ab 100644 --- a/library/src/meta/counter.rs +++ b/library/src/meta/counter.rs @@ -277,7 +277,6 @@ use crate::prelude::*; /// /// Display: Counter /// Category: meta -/// Returns: counter #[func] pub fn counter( /// The key that identifies this counter. @@ -288,8 +287,8 @@ pub fn counter( /// - If this is an element function or selector, counts through its elements, /// - If this is the [`page`]($func/page) function, counts through pages. key: CounterKey, -) -> Value { - Value::dynamic(Counter::new(key)) +) -> Counter { + Counter::new(key) } /// Counts through pages, elements, and more. @@ -317,17 +316,17 @@ impl Counter { span: Span, ) -> SourceResult<Value> { let value = match method { - "display" => { - self.display(args.eat()?, args.named("both")?.unwrap_or(false)).into() - } + "display" => self + .display(args.eat()?, args.named("both")?.unwrap_or(false)) + .into_value(), "step" => self .update(CounterUpdate::Step( args.named("level")?.unwrap_or(NonZeroUsize::ONE), )) - .into(), - "update" => self.update(args.expect("value or function")?).into(), - "at" => self.at(&mut vm.vt, args.expect("location")?)?.into(), - "final" => self.final_(&mut vm.vt, args.expect("location")?)?.into(), + .into_value(), + "update" => self.update(args.expect("value or function")?).into_value(), + "at" => self.at(&mut vm.vt, args.expect("location")?)?.into_value(), + "final" => self.final_(&mut vm.vt, args.expect("location")?)?.into_value(), _ => bail!(span, "type counter has no method `{}`", method), }; args.finish()?; @@ -342,10 +341,7 @@ impl Counter { /// Get the value of the state at the given location. pub fn at(&self, vt: &mut Vt, location: Location) -> SourceResult<CounterState> { let sequence = self.sequence(vt)?; - let offset = vt - .introspector - .query(&Selector::before(self.selector(), location, true)) - .len(); + let offset = vt.introspector.query(&self.selector().before(location, true)).len(); let (mut state, page) = sequence[offset].clone(); if self.is_page() { let delta = vt.introspector.page(location).get().saturating_sub(page.get()); @@ -479,8 +475,8 @@ impl Debug for Counter { } } -cast_from_value! { - Counter: "counter", +cast! { + type Counter: "counter", } /// Identifies a counter. @@ -495,7 +491,7 @@ pub enum CounterKey { Str(Str), } -cast_from_value! { +cast! { CounterKey, v: Str => Self::Str(v), label: Label => Self::Selector(Selector::Label(label)), @@ -503,7 +499,7 @@ cast_from_value! { if v == PageElem::func() { Self::Page } else { - Self::Selector(LocatableSelector::cast(Value::from(v))?.0) + Self::Selector(LocatableSelector::from_value(v.into_value())?.0) } }, selector: LocatableSelector => Self::Selector(selector.0), @@ -536,8 +532,8 @@ impl Debug for CounterUpdate { } } -cast_from_value! { - CounterUpdate: "counter update", +cast! { + type CounterUpdate: "counter update", v: CounterState => Self::Set(v), v: Func => Self::Func(v), } @@ -559,10 +555,7 @@ impl CounterState { CounterUpdate::Set(state) => *self = state, CounterUpdate::Step(level) => self.step(level, 1), CounterUpdate::Func(func) => { - *self = func - .call_vt(vt, self.0.iter().copied().map(Into::into))? - .cast() - .at(func.span())? + *self = func.call_vt(vt, self.0.iter().copied())?.cast().at(func.span())? } } Ok(()) @@ -593,8 +586,9 @@ impl CounterState { } } -cast_from_value! { +cast! { CounterState, + self => Value::Array(self.0.into_iter().map(IntoValue::into_value).collect()), num: usize => Self(smallvec![num]), array: Array => Self(array .into_iter() @@ -602,10 +596,6 @@ cast_from_value! { .collect::<StrResult<_>>()?), } -cast_to_value! { - v: CounterState => Value::Array(v.0.into_iter().map(Into::into).collect()) -} - /// Executes a display of a state. /// /// Display: State diff --git a/library/src/meta/document.rs b/library/src/meta/document.rs index 1ce900ed..db036e0a 100644 --- a/library/src/meta/document.rs +++ b/library/src/meta/document.rs @@ -78,12 +78,9 @@ impl LayoutRoot for DocumentElem { #[derive(Debug, Default, Clone, Hash)] pub struct Author(Vec<EcoString>); -cast_from_value! { +cast! { Author, + self => self.0.into_value(), v: EcoString => Self(vec![v]), v: Array => Self(v.into_iter().map(Value::cast).collect::<StrResult<_>>()?), } - -cast_to_value! { - v: Author => v.0.into() -} diff --git a/library/src/meta/figure.rs b/library/src/meta/figure.rs index 67087832..0d218770 100644 --- a/library/src/meta/figure.rs +++ b/library/src/meta/figure.rs @@ -209,7 +209,7 @@ impl Synthesize for FigureElem { }; let target = descendant.unwrap_or_else(|| self.body()); - supplement.resolve(vt, [target.into()])? + supplement.resolve(vt, [target])? } }; @@ -312,8 +312,8 @@ impl FigureElem { self.counter(), self.numbering(StyleChain::default()), ) { - let numbers = - counter.at(vt, self.0.location().unwrap())?.display(vt, &numbering)?; + let loc = self.0.location().unwrap(); + let numbers = counter.at(vt, loc)?.display(vt, &numbering)?; if !supplement.is_empty() { supplement += TextElem::packed("\u{a0}"); @@ -335,19 +335,16 @@ pub enum FigureKind { Name(EcoString), } -cast_from_value! { +cast! { FigureKind, + self => match self { + Self::Elem(v) => v.into_value(), + Self::Name(v) => v.into_value(), + }, v: ElemFunc => Self::Elem(v), v: EcoString => Self::Name(v), } -cast_to_value! { - v: FigureKind => match v { - FigureKind::Elem(v) => v.into(), - FigureKind::Name(v) => v.into(), - } -} - /// An element that can be auto-detected in a figure. /// /// This trait is used to determine the type of a figure. diff --git a/library/src/meta/footnote.rs b/library/src/meta/footnote.rs index 22de91c3..1c95716c 100644 --- a/library/src/meta/footnote.rs +++ b/library/src/meta/footnote.rs @@ -211,7 +211,7 @@ impl Finalize for FootnoteEntry { } } -cast_from_value! { +cast! { FootnoteElem, v: Content => v.to::<Self>().cloned().unwrap_or_else(|| Self::new(v.clone())), } diff --git a/library/src/meta/heading.rs b/library/src/meta/heading.rs index d6ad7044..7a333e36 100644 --- a/library/src/meta/heading.rs +++ b/library/src/meta/heading.rs @@ -104,9 +104,7 @@ impl Synthesize for HeadingElem { let supplement = match self.supplement(styles) { Smart::Auto => TextElem::packed(self.local_name_in(styles)), Smart::Custom(None) => Content::empty(), - Smart::Custom(Some(supplement)) => { - supplement.resolve(vt, [self.clone().into()])? - } + Smart::Custom(Some(supplement)) => supplement.resolve(vt, [self.clone()])?, }; self.push_level(self.level(styles)); @@ -164,7 +162,7 @@ impl Count for HeadingElem { } } -cast_from_value! { +cast! { HeadingElem, v: Content => v.to::<Self>().ok_or("expected heading")?.clone(), } diff --git a/library/src/meta/link.rs b/library/src/meta/link.rs index 93b9999d..43f6a34d 100644 --- a/library/src/meta/link.rs +++ b/library/src/meta/link.rs @@ -125,19 +125,16 @@ pub enum LinkTarget { Label(Label), } -cast_from_value! { +cast! { LinkTarget, + self => match self { + Self::Dest(v) => v.into_value(), + Self::Label(v) => v.into_value(), + }, v: Destination => Self::Dest(v), v: Label => Self::Label(v), } -cast_to_value! { - v: LinkTarget => match v { - LinkTarget::Dest(v) => v.into(), - LinkTarget::Label(v) => v.into(), - } -} - impl From<Destination> for LinkTarget { fn from(dest: Destination) -> Self { Self::Dest(dest) diff --git a/library/src/meta/mod.rs b/library/src/meta/mod.rs index 724e5d20..dcac6379 100644 --- a/library/src/meta/mod.rs +++ b/library/src/meta/mod.rs @@ -42,14 +42,14 @@ pub(super) fn define(global: &mut Scope) { global.define("footnote", FootnoteElem::func()); global.define("cite", CiteElem::func()); global.define("bibliography", BibliographyElem::func()); - global.define("locate", locate); - global.define("style", style); - global.define("layout", layout); - global.define("counter", counter); - global.define("numbering", numbering); - global.define("state", state); - global.define("query", query); - global.define("selector", selector); + global.define("locate", locate_func()); + global.define("style", style_func()); + global.define("layout", layout_func()); + global.define("counter", counter_func()); + global.define("numbering", numbering_func()); + global.define("state", state_func()); + global.define("query", query_func()); + global.define("selector", selector_func()); } /// The named with which an element is referenced. diff --git a/library/src/meta/numbering.rs b/library/src/meta/numbering.rs index fbe7306c..ef0d0f70 100644 --- a/library/src/meta/numbering.rs +++ b/library/src/meta/numbering.rs @@ -32,14 +32,13 @@ use crate::text::Case; /// /// Display: Numbering /// Category: meta -/// Returns: any #[func] pub fn numbering( /// Defines how the numbering works. /// - /// **Counting symbols** are `1`, `a`, `A`, `i`, `I`, `い`, `イ`, - /// `א`, `가`, `ㄱ`, and `*`. They are replaced by the number in the sequence, - /// in the given case. + /// **Counting symbols** are `1`, `a`, `A`, `i`, `I`, `い`, `イ`, `א`, `가`, + /// `ㄱ`, and `*`. They are replaced by the number in the sequence, in the + /// given case. /// /// The `*` character means that symbols should be used to count, in the /// order of `*`, `†`, `‡`, `§`, `¶`, and `‖`. If there are more than six @@ -52,9 +51,9 @@ pub fn numbering( /// suffixes. They are repeated as-is at in front of their rendered /// equivalent of their counting symbol. /// - /// This parameter can also be an arbitrary function that gets each number as - /// an individual argument. When given a function, the `numbering` function - /// just forwards the arguments to that function. While this is not + /// This parameter can also be an arbitrary function that gets each number + /// as an individual argument. When given a function, the `numbering` + /// function just forwards the arguments to that function. While this is not /// particularly useful in itself, it means that you can just give arbitrary /// numberings to the `numbering` function without caring whether they are /// defined as a pattern or function. @@ -65,8 +64,10 @@ pub fn numbering( /// given, the last counting symbol with its prefix is repeated. #[variadic] numbers: Vec<usize>, -) -> Value { - numbering.apply_vm(vm, &numbers)? + /// The virtual machine. + vm: &mut Vm, +) -> SourceResult<Value> { + numbering.apply_vm(vm, &numbers) } /// How to number a sequence of things. @@ -84,8 +85,7 @@ impl Numbering { Ok(match self { Self::Pattern(pattern) => Value::Str(pattern.apply(numbers).into()), Self::Func(func) => { - let args = - Args::new(func.span(), numbers.iter().map(|&n| Value::Int(n as i64))); + let args = Args::new(func.span(), numbers.iter().copied()); func.call_vm(vm, args)? } }) @@ -95,9 +95,7 @@ impl Numbering { pub fn apply_vt(&self, vt: &mut Vt, numbers: &[usize]) -> SourceResult<Value> { Ok(match self { Self::Pattern(pattern) => Value::Str(pattern.apply(numbers).into()), - Self::Func(func) => { - func.call_vt(vt, numbers.iter().map(|&n| Value::Int(n as i64)))? - } + Self::Func(func) => func.call_vt(vt, numbers.iter().copied())?, }) } @@ -116,19 +114,16 @@ impl From<NumberingPattern> for Numbering { } } -cast_from_value! { +cast! { Numbering, + self => match self { + Self::Pattern(pattern) => pattern.into_value(), + Self::Func(func) => func.into_value(), + }, v: NumberingPattern => Self::Pattern(v), v: Func => Self::Func(v), } -cast_to_value! { - v: Numbering => match v { - Numbering::Pattern(pattern) => pattern.into(), - Numbering::Func(func) => func.into(), - } -} - /// How to turn a number into text. /// /// A pattern consists of a prefix, followed by one of `1`, `a`, `A`, `i`, @@ -230,15 +225,11 @@ impl FromStr for NumberingPattern { } } -cast_from_value! { +cast! { NumberingPattern, - v: Str => v.parse()?, -} - -cast_to_value! { - v: NumberingPattern => { + self => { let mut pat = EcoString::new(); - for (prefix, kind, case) in &v.pieces { + for (prefix, kind, case) in &self.pieces { pat.push_str(prefix); let mut c = kind.to_char(); if *case == Case::Upper { @@ -246,9 +237,10 @@ cast_to_value! { } pat.push(c); } - pat.push_str(&v.suffix); - pat.into() - } + pat.push_str(&self.suffix); + pat.into_value() + }, + v: Str => v.parse()?, } /// Different kinds of numberings. diff --git a/library/src/meta/outline.rs b/library/src/meta/outline.rs index 089eceb2..894c3b8b 100644 --- a/library/src/meta/outline.rs +++ b/library/src/meta/outline.rs @@ -367,19 +367,14 @@ impl OutlineIndent { // Length => indent with some fixed spacing per level Some(Smart::Custom(OutlineIndent::Length(length))) => { - let Ok(depth): Result<i64, _> = ancestors.len().try_into() else { - bail!(span, "outline element depth too large"); - }; - - let hspace = HElem::new(*length).pack().repeat(depth).unwrap(); - seq.push(hspace); + seq.push(HElem::new(*length).pack().repeat(ancestors.len())); } // Function => call function with the current depth and take // the returned content Some(Smart::Custom(OutlineIndent::Function(func))) => { let depth = ancestors.len(); - let returned = func.call_vt(vt, [depth.into()])?; + let returned = func.call_vt(vt, [depth])?; let Ok(returned) = returned.cast::<Content>() else { bail!( span, @@ -396,17 +391,14 @@ impl OutlineIndent { } } -cast_from_value! { +cast! { OutlineIndent, - b: bool => OutlineIndent::Bool(b), - s: Spacing => OutlineIndent::Length(s), - f: Func => OutlineIndent::Function(f), -} - -cast_to_value! { - v: OutlineIndent => match v { - OutlineIndent::Bool(b) => b.into(), - OutlineIndent::Length(s) => s.into(), - OutlineIndent::Function(f) => f.into() - } + self => match self { + Self::Bool(v) => v.into_value(), + Self::Length(v) => v.into_value(), + Self::Function(v) => v.into_value() + }, + v: bool => OutlineIndent::Bool(v), + v: Spacing => OutlineIndent::Length(v), + v: Func => OutlineIndent::Function(v), } diff --git a/library/src/meta/query.rs b/library/src/meta/query.rs index ad7274f1..7f839f97 100644 --- a/library/src/meta/query.rs +++ b/library/src/meta/query.rs @@ -98,7 +98,6 @@ use crate::prelude::*; /// /// Display: Query /// Category: meta -/// Returns: array #[func] pub fn query( /// Can be an element function like a `heading` or `figure`, a `{<label>}` @@ -111,7 +110,6 @@ pub fn query( /// have an explicit label attached to them. This limitation will be /// resolved in the future. target: LocatableSelector, - /// Can be any location. Why is it required then? As noted before, Typst has /// to evaluate parts of your code multiple times to determine the values of /// all state. By only allowing this function within @@ -120,14 +118,14 @@ pub fn query( /// level of a module, the evaluation of the whole module and its exports /// could depend on the query's result. location: Location, -) -> Value { + /// The virtual machine. + vm: &mut Vm, +) -> Array { let _ = location; let vec = vm.vt.introspector.query(&target.0); - Value::Array( - vec.into_iter() - .map(|elem| Value::Content(elem.into_inner())) - .collect(), - ) + vec.into_iter() + .map(|elem| Value::Content(elem.into_inner())) + .collect() } /// Turns a value into a selector. The following values are accepted: @@ -137,12 +135,11 @@ pub fn query( /// /// Display: Selector /// Category: meta -/// Returns: content #[func] pub fn selector( /// Can be an element function like a `heading` or `figure`, a `{<label>}` /// or a more complex selector like `{heading.where(level: 1)}`. target: Selector, -) -> Value { - target.into() +) -> Selector { + target } diff --git a/library/src/meta/reference.rs b/library/src/meta/reference.rs index c538b696..42450b97 100644 --- a/library/src/meta/reference.rs +++ b/library/src/meta/reference.rs @@ -193,7 +193,7 @@ impl Show for RefElem { Smart::Auto => refable.supplement(), Smart::Custom(None) => Content::empty(), Smart::Custom(Some(supplement)) => { - supplement.resolve(vt, [(*elem).clone().into()])? + supplement.resolve(vt, [(*elem).clone()])? } }; @@ -229,10 +229,10 @@ pub enum Supplement { impl Supplement { /// Tries to resolve the supplement into its content. - pub fn resolve( + pub fn resolve<T: IntoValue>( &self, vt: &mut Vt, - args: impl IntoIterator<Item = Value>, + args: impl IntoIterator<Item = T>, ) -> SourceResult<Content> { Ok(match self { Supplement::Content(content) => content.clone(), @@ -241,19 +241,16 @@ impl Supplement { } } -cast_from_value! { +cast! { Supplement, + self => match self { + Self::Content(v) => v.into_value(), + Self::Func(v) => v.into_value(), + }, v: Content => Self::Content(v), v: Func => Self::Func(v), } -cast_to_value! { - v: Supplement => match v { - Supplement::Content(v) => v.into(), - Supplement::Func(v) => v.into(), - } -} - /// Marks an element as being able to be referenced. This is used to implement /// the `@ref` element. pub trait Refable { diff --git a/library/src/meta/state.rs b/library/src/meta/state.rs index 284daf54..231852e3 100644 --- a/library/src/meta/state.rs +++ b/library/src/meta/state.rs @@ -233,7 +233,6 @@ use crate::prelude::*; /// /// Display: State /// Category: meta -/// Returns: state #[func] pub fn state( /// The key that identifies this state. @@ -241,8 +240,8 @@ pub fn state( /// The initial value of the state. #[default] init: Value, -) -> Value { - Value::dynamic(State { key, init }) +) -> State { + State { key, init } } /// A state. @@ -265,10 +264,10 @@ impl State { span: Span, ) -> SourceResult<Value> { let value = match method { - "display" => self.display(args.eat()?).into(), + "display" => self.display(args.eat()?).into_value(), "at" => self.at(&mut vm.vt, args.expect("location")?)?, "final" => self.final_(&mut vm.vt, args.expect("location")?)?, - "update" => self.update(args.expect("value or function")?).into(), + "update" => self.update(args.expect("value or function")?).into_value(), _ => bail!(span, "type state has no method `{}`", method), }; args.finish()?; @@ -284,10 +283,7 @@ impl State { #[tracing::instrument(skip(self, vt))] pub fn at(self, vt: &mut Vt, location: Location) -> SourceResult<Value> { let sequence = self.sequence(vt)?; - let offset = vt - .introspector - .query(&Selector::before(self.selector(), location, true)) - .len(); + let offset = vt.introspector.query(&self.selector().before(location, true)).len(); Ok(sequence[offset].clone()) } @@ -358,8 +354,8 @@ impl Debug for State { } } -cast_from_value! { - State: "state", +cast! { + type State: "state", } /// An update to perform on a state. @@ -377,8 +373,8 @@ impl Debug for StateUpdate { } } -cast_from_value! { - StateUpdate: "state update", +cast! { + type StateUpdate: "state update", v: Func => Self::Func(v), v: Value => Self::Set(v), } diff --git a/library/src/prelude.rs b/library/src/prelude.rs index 59b8a0a0..f0dda5c8 100644 --- a/library/src/prelude.rs +++ b/library/src/prelude.rs @@ -15,8 +15,8 @@ pub use typst::diag::{bail, error, At, SourceResult, StrResult}; pub use typst::doc::*; #[doc(no_inline)] pub use typst::eval::{ - array, cast_from_value, cast_to_value, dict, format_str, func, Args, Array, Cast, - CastInfo, Dict, Func, Never, Scope, Str, Symbol, Value, Vm, + array, cast, dict, format_str, func, Args, Array, AutoValue, Cast, Dict, FromValue, + Func, IntoValue, Never, NoneValue, Scope, Str, Symbol, Type, Value, Vm, }; #[doc(no_inline)] pub use typst::geom::*; diff --git a/library/src/text/deco.rs b/library/src/text/deco.rs index cfb06956..329d3f85 100644 --- a/library/src/text/deco.rs +++ b/library/src/text/deco.rs @@ -251,8 +251,8 @@ impl Fold for Decoration { } } -cast_from_value! { - Decoration: "decoration", +cast! { + type Decoration: "decoration", } /// A kind of decorative line. diff --git a/library/src/text/misc.rs b/library/src/text/misc.rs index 5dafe4ac..2af7a212 100644 --- a/library/src/text/misc.rs +++ b/library/src/text/misc.rs @@ -115,15 +115,12 @@ impl Show for StrongElem { #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] pub struct Delta(pub i64); -cast_from_value! { +cast! { Delta, + self => self.0.into_value(), v: i64 => Self(v), } -cast_to_value! { - v: Delta => v.0.into() -} - impl Fold for Delta { type Output = i64; @@ -176,15 +173,12 @@ impl Show for EmphElem { #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] pub struct Toggle; -cast_from_value! { +cast! { Toggle, + self => Value::None, _: Value => Self, } -cast_to_value! { - _: Toggle => Value::None -} - impl Fold for Toggle { type Output = bool; @@ -204,12 +198,11 @@ impl Fold for Toggle { /// /// Display: Lowercase /// Category: text -/// Returns: string or content #[func] pub fn lower( /// The text to convert to lowercase. - text: ToCase, -) -> Value { + text: Caseable, +) -> Caseable { case(text, Case::Lower) } @@ -224,31 +217,36 @@ pub fn lower( /// /// Display: Uppercase /// Category: text -/// Returns: string or content #[func] pub fn upper( /// The text to convert to uppercase. - text: ToCase, -) -> Value { + text: Caseable, +) -> Caseable { case(text, Case::Upper) } /// Change the case of text. -fn case(text: ToCase, case: Case) -> Value { +fn case(text: Caseable, case: Case) -> Caseable { match text { - ToCase::Str(v) => Value::Str(case.apply(&v).into()), - ToCase::Content(v) => Value::Content(v.styled(TextElem::set_case(Some(case)))), + Caseable::Str(v) => Caseable::Str(case.apply(&v).into()), + Caseable::Content(v) => { + Caseable::Content(v.styled(TextElem::set_case(Some(case)))) + } } } /// A value whose case can be changed. -enum ToCase { +pub enum Caseable { Str(Str), Content(Content), } -cast_from_value! { - ToCase, +cast! { + Caseable, + self => match self { + Self::Str(v) => v.into_value(), + Self::Content(v) => v.into_value(), + }, v: Str => Self::Str(v), v: Content => Self::Content(v), } @@ -297,13 +295,12 @@ impl Case { /// /// Display: Small Capitals /// Category: text -/// Returns: content #[func] pub fn smallcaps( /// The text to display to small capitals. body: Content, -) -> Value { - Value::Content(body.styled(TextElem::set_smallcaps(true))) +) -> Content { + body.styled(TextElem::set_smallcaps(true)) } /// Create blind text. @@ -324,11 +321,10 @@ pub fn smallcaps( /// /// Display: Blind Text /// Category: text -/// Returns: string #[func] pub fn lorem( /// The length of the blind text in words. words: usize, -) -> Value { - Value::Str(lipsum::lipsum(words).replace("--", "–").into()) +) -> Str { + lipsum::lipsum(words).replace("--", "–").into() } diff --git a/library/src/text/mod.rs b/library/src/text/mod.rs index e86bc168..6090094a 100644 --- a/library/src/text/mod.rs +++ b/library/src/text/mod.rs @@ -14,8 +14,6 @@ pub use self::raw::*; pub use self::shaping::*; pub use self::shift::*; -use std::borrow::Cow; - use rustybuzz::Tag; use typst::font::{FontMetrics, FontStretch, FontStyle, FontWeight, VerticalFontMetric}; @@ -29,16 +27,16 @@ pub(super) fn define(global: &mut Scope) { global.define("smartquote", SmartQuoteElem::func()); global.define("strong", StrongElem::func()); global.define("emph", EmphElem::func()); - global.define("lower", lower); - global.define("upper", upper); - global.define("smallcaps", smallcaps); + global.define("lower", lower_func()); + global.define("upper", upper_func()); + global.define("smallcaps", smallcaps_func()); global.define("sub", SubElem::func()); global.define("super", SuperElem::func()); global.define("underline", UnderlineElem::func()); global.define("strike", StrikeElem::func()); global.define("overline", OverlineElem::func()); global.define("raw", RawElem::func()); - global.define("lorem", lorem); + global.define("lorem", lorem_func()); } /// Customize the look and layout of text in a variety of ways. @@ -560,15 +558,12 @@ impl Debug for FontFamily { } } -cast_from_value! { +cast! { FontFamily, + self => self.0.into_value(), string: EcoString => Self::new(&string), } -cast_to_value! { - v: FontFamily => v.0.into() -} - /// Font family fallback list. #[derive(Debug, Default, Clone, Eq, PartialEq, Hash)] pub struct FontList(pub Vec<FontFamily>); @@ -582,20 +577,17 @@ impl IntoIterator for FontList { } } -cast_from_value! { +cast! { FontList, + self => if self.0.len() == 1 { + self.0.into_iter().next().unwrap().0.into_value() + } else { + self.0.into_value() + }, family: FontFamily => Self(vec![family]), values: Array => Self(values.into_iter().map(|v| v.cast()).collect::<StrResult<_>>()?), } -cast_to_value! { - v: FontList => if v.0.len() == 1 { - v.0.into_iter().next().unwrap().0.into() - } else { - v.0.into() - } -} - /// The size of text. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] pub struct TextSize(pub Length); @@ -608,15 +600,12 @@ impl Fold for TextSize { } } -cast_from_value! { +cast! { TextSize, + self => self.0.into_value(), v: Length => Self(v), } -cast_to_value! { - v: TextSize => v.0.into() -} - /// Specifies the bottom or top edge of text. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] pub enum TextEdge { @@ -636,25 +625,23 @@ impl TextEdge { } } -cast_from_value! { +cast! { TextEdge, + self => match self { + Self::Metric(metric) => metric.into_value(), + Self::Length(length) => length.into_value(), + }, v: VerticalFontMetric => Self::Metric(v), v: Length => Self::Length(v), } -cast_to_value! { - v: TextEdge => match v { - TextEdge::Metric(metric) => metric.into(), - TextEdge::Length(length) => length.into(), - } -} - /// The direction of text and inline objects in their line. #[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash)] pub struct TextDir(pub Smart<Dir>); -cast_from_value! { +cast! { TextDir, + self => self.0.into_value(), v: Smart<Dir> => { if v.map_or(false, |dir| dir.axis() == Axis::Y) { Err("text direction must be horizontal")?; @@ -663,10 +650,6 @@ cast_from_value! { }, } -cast_to_value! { - v: TextDir => v.0.into() -} - impl Resolve for TextDir { type Output = Dir; @@ -682,15 +665,12 @@ impl Resolve for TextDir { #[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash)] pub struct Hyphenate(pub Smart<bool>); -cast_from_value! { +cast! { Hyphenate, + self => self.0.into_value(), v: Smart<bool> => Self(v), } -cast_to_value! { - v: Hyphenate => v.0.into() -} - impl Resolve for Hyphenate { type Output = bool; @@ -718,18 +698,15 @@ impl StylisticSet { } } -cast_from_value! { +cast! { StylisticSet, + self => self.0.into_value(), v: i64 => match v { 1 ..= 20 => Self::new(v as u8), _ => Err("stylistic set must be between 1 and 20")?, }, } -cast_to_value! { - v: StylisticSet => v.0.into() -} - /// Which kind of numbers / figures to select. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)] pub enum NumberType { @@ -754,8 +731,17 @@ pub enum NumberWidth { #[derive(Debug, Default, Clone, Eq, PartialEq, Hash)] pub struct FontFeatures(pub Vec<(Tag, u32)>); -cast_from_value! { +cast! { FontFeatures, + self => self.0 + .into_iter() + .map(|(tag, num)| { + let bytes = tag.to_bytes(); + let key = std::str::from_utf8(&bytes).unwrap_or_default(); + (key.into(), num.into_value()) + }) + .collect::<Dict>() + .into_value(), values: Array => Self(values .into_iter() .map(|v| { @@ -773,18 +759,6 @@ cast_from_value! { .collect::<StrResult<_>>()?), } -cast_to_value! { - v: FontFeatures => Value::Dict( - v.0.into_iter() - .map(|(tag, num)| { - let bytes = tag.to_bytes(); - let key = std::str::from_utf8(&bytes).unwrap_or_default(); - (key.into(), num.into()) - }) - .collect(), - ) -} - impl Fold for FontFeatures { type Output = Self; diff --git a/library/src/text/shaping.rs b/library/src/text/shaping.rs index 3610e0c0..5a1724ab 100644 --- a/library/src/text/shaping.rs +++ b/library/src/text/shaping.rs @@ -1,13 +1,14 @@ +use std::borrow::Cow; use std::ops::Range; use std::str::FromStr; use az::SaturatingAs; use rustybuzz::{Feature, Tag, UnicodeBuffer}; -use typst::font::{Font, FontVariant}; +use typst::font::{Font, FontStyle, FontVariant}; use typst::util::SliceExt; use unicode_script::{Script, UnicodeScript}; -use super::*; +use super::{decorate, FontFamily, NumberType, NumberWidth, TextElem}; use crate::layout::SpanMapper; use crate::prelude::*; diff --git a/library/src/visualize/mod.rs b/library/src/visualize/mod.rs index 3adf4e77..ea873f44 100644 --- a/library/src/visualize/mod.rs +++ b/library/src/visualize/mod.rs @@ -24,4 +24,22 @@ pub(super) fn define(global: &mut Scope) { global.define("circle", CircleElem::func()); global.define("polygon", PolygonElem::func()); global.define("path", PathElem::func()); + global.define("black", Color::BLACK); + global.define("gray", Color::GRAY); + global.define("silver", Color::SILVER); + global.define("white", Color::WHITE); + global.define("navy", Color::NAVY); + global.define("blue", Color::BLUE); + global.define("aqua", Color::AQUA); + global.define("teal", Color::TEAL); + global.define("eastern", Color::EASTERN); + global.define("purple", Color::PURPLE); + global.define("fuchsia", Color::FUCHSIA); + global.define("maroon", Color::MAROON); + global.define("red", Color::RED); + global.define("orange", Color::ORANGE); + global.define("yellow", Color::YELLOW); + global.define("olive", Color::OLIVE); + global.define("green", Color::GREEN); + global.define("lime", Color::LIME); } diff --git a/library/src/visualize/path.rs b/library/src/visualize/path.rs index c9b596c7..641095c5 100644 --- a/library/src/visualize/path.rs +++ b/library/src/visualize/path.rs @@ -1,6 +1,9 @@ -use self::PathVertex::{AllControlPoints, MirroredControlPoint, Vertex}; -use crate::prelude::*; use kurbo::{CubicBez, ParamCurveExtrema}; +use typst::eval::Reflect; + +use crate::prelude::*; + +use PathVertex::{AllControlPoints, MirroredControlPoint, Vertex}; /// A path through a list of points, connected by Bezier curves. /// @@ -179,8 +182,13 @@ impl PathVertex { } } -cast_from_value! { +cast! { PathVertex, + self => match self { + Vertex(x) => x.into_value(), + MirroredControlPoint(x, c) => array![x, c].into_value(), + AllControlPoints(x, c1, c2) => array![x, c1, c2].into_value(), + }, array: Array => { let mut iter = array.into_iter(); match (iter.next(), iter.next(), iter.next(), iter.next()) { @@ -188,7 +196,7 @@ cast_from_value! { Vertex(a.cast()?) }, (Some(a), Some(b), None, None) => { - if Axes::<Rel<Length>>::is(&a) { + if Axes::<Rel<Length>>::castable(&a) { MirroredControlPoint(a.cast()?, b.cast()?) } else { Vertex(Axes::new(a.cast()?, b.cast()?)) @@ -201,19 +209,3 @@ cast_from_value! { } }, } - -cast_to_value! { - v: PathVertex => { - match v { - PathVertex::Vertex(x) => { - Value::from(x) - }, - PathVertex::MirroredControlPoint(x, c) => { - Value::Array(array![x, c]) - }, - PathVertex::AllControlPoints(x, c1, c2) => { - Value::Array(array![x, c1, c2]) - }, - } - } -} diff --git a/macros/Cargo.toml b/macros/Cargo.toml index 179666d8..15f2b2bd 100644 --- a/macros/Cargo.toml +++ b/macros/Cargo.toml @@ -19,4 +19,4 @@ bench = false heck = "0.4" proc-macro2 = "1" quote = "1" -syn = { version = "1", features = ["full", "extra-traits"] } +syn = { version = "2", features = ["full", "extra-traits"] } diff --git a/macros/src/castable.rs b/macros/src/castable.rs index cd05ed2d..05c1b4d1 100644 --- a/macros/src/castable.rs +++ b/macros/src/castable.rs @@ -1,7 +1,7 @@ use super::*; /// Expand the `#[derive(Cast)]` macro. -pub fn cast(item: DeriveInput) -> Result<TokenStream> { +pub fn derive_cast(item: &DeriveInput) -> Result<TokenStream> { let ty = &item.ident; let syn::Data::Enum(data) = &item.data else { @@ -15,7 +15,7 @@ pub fn cast(item: DeriveInput) -> Result<TokenStream> { } let string = if let Some(attr) = - variant.attrs.iter().find(|attr| attr.path.is_ident("string")) + variant.attrs.iter().find(|attr| attr.path().is_ident("string")) { attr.parse_args::<syn::LitStr>()?.value() } else { @@ -43,107 +43,117 @@ pub fn cast(item: DeriveInput) -> Result<TokenStream> { }); Ok(quote! { - ::typst::eval::cast_from_value! { + ::typst::eval::cast! { #ty, - #(#strs_to_variants),* - } - - ::typst::eval::cast_to_value! { - v: #ty => ::typst::eval::Value::from(match v { + self => ::typst::eval::IntoValue::into_value(match self { #(#variants_to_strs),* - }) + }), + #(#strs_to_variants),* } }) } +/// An enum variant in a `derive(Cast)`. struct Variant { ident: Ident, string: String, docs: String, } -/// Expand the `cast_from_value!` macro. -pub fn cast_from_value(stream: TokenStream) -> Result<TokenStream> { - let castable: Castable = syn::parse2(stream)?; - let ty = &castable.ty; +/// Expand the `cast!` macro. +pub fn cast(stream: TokenStream) -> Result<TokenStream> { + let input: CastInput = syn::parse2(stream)?; + let ty = &input.ty; + let eval = quote! { ::typst::eval }; - if castable.casts.is_empty() && castable.name.is_none() { - bail!(castable.ty, "expected at least one pattern"); - } + let castable_body = create_castable_body(&input); + let describe_body = create_describe_body(&input); + let into_value_body = create_into_value_body(&input); + let from_value_body = create_from_value_body(&input); - let is_func = create_is_func(&castable); - let cast_func = create_cast_func(&castable); - let describe_func = create_describe_func(&castable); - let dynamic_impls = castable.name.as_ref().map(|name| { + let reflect = (!input.from_value.is_empty() || input.name.is_some()).then(|| { quote! { - impl ::typst::eval::Type for #ty { - const TYPE_NAME: &'static str = #name; - } + impl #eval::Reflect for #ty { + fn describe() -> #eval::CastInfo { + #describe_body + } - impl From<#ty> for ::typst::eval::Value { - fn from(v: #ty) -> Self { - ::typst::eval::Value::Dyn(::typst::eval::Dynamic::new(v)) + fn castable(value: &#eval::Value) -> bool { + #castable_body } } } }); - Ok(quote! { - impl ::typst::eval::Cast for #ty { - #is_func - #cast_func - #describe_func + let into_value = (input.into_value.is_some() || input.name.is_some()).then(|| { + quote! { + impl #eval::IntoValue for #ty { + fn into_value(self) -> #eval::Value { + #into_value_body + } + } } + }); - #dynamic_impls - }) -} - -/// Expand the `cast_to_value!` macro. -pub fn cast_to_value(stream: TokenStream) -> Result<TokenStream> { - let cast: Cast = syn::parse2(stream)?; - let Pattern::Ty(pat, ty) = &cast.pattern else { - bail!(callsite, "expected pattern"); - }; + let from_value = (!input.from_value.is_empty() || input.name.is_some()).then(|| { + quote! { + impl #eval::FromValue for #ty { + fn from_value(value: #eval::Value) -> ::typst::diag::StrResult<Self> { + #from_value_body + } + } + } + }); - let expr = &cast.expr; - Ok(quote! { - impl ::std::convert::From<#ty> for ::typst::eval::Value { - fn from(#pat: #ty) -> Self { - #expr + let ty = input.name.as_ref().map(|name| { + quote! { + impl #eval::Type for #ty { + const TYPE_NAME: &'static str = #name; } } + }); + + Ok(quote! { + #reflect + #into_value + #from_value + #ty }) } -struct Castable { +/// The input to `cast!`. +struct CastInput { ty: syn::Type, name: Option<syn::LitStr>, - casts: Punctuated<Cast, Token![,]>, -} - -struct Cast { - attrs: Vec<syn::Attribute>, - pattern: Pattern, - expr: syn::Expr, + into_value: Option<syn::Expr>, + from_value: Punctuated<Cast, Token![,]>, } -enum Pattern { - Str(syn::LitStr), - Ty(syn::Pat, syn::Type), -} - -impl Parse for Castable { +impl Parse for CastInput { fn parse(input: ParseStream) -> Result<Self> { - let ty = input.parse()?; + let ty; let mut name = None; - if input.peek(Token![:]) { + if input.peek(syn::Token![type]) { + let _: syn::Token![type] = input.parse()?; + ty = input.parse()?; let _: syn::Token![:] = input.parse()?; name = Some(input.parse()?); + } else { + ty = input.parse()?; } + let _: syn::Token![,] = input.parse()?; - let casts = Punctuated::parse_terminated(input)?; - Ok(Self { ty, name, casts }) + + let mut into_value = None; + if input.peek(syn::Token![self]) { + let _: syn::Token![self] = input.parse()?; + let _: syn::Token![=>] = input.parse()?; + into_value = Some(input.parse()?); + let _: syn::Token![,] = input.parse()?; + } + + let from_value = Punctuated::parse_terminated(input)?; + Ok(Self { ty, name, into_value, from_value }) } } @@ -162,7 +172,7 @@ impl Parse for Pattern { if input.peek(syn::LitStr) { Ok(Pattern::Str(input.parse()?)) } else { - let pat = input.parse()?; + let pat = syn::Pat::parse_single(input)?; let _: syn::Token![:] = input.parse()?; let ty = input.parse()?; Ok(Pattern::Ty(pat, ty)) @@ -170,19 +180,31 @@ impl Parse for Pattern { } } -/// Create the castable's `is` function. -fn create_is_func(castable: &Castable) -> TokenStream { - let mut string_arms = vec![]; - let mut cast_checks = vec![]; +/// A single cast, e.g. `v: i64 => Self::Int(v)`. +struct Cast { + attrs: Vec<syn::Attribute>, + pattern: Pattern, + expr: syn::Expr, +} - for cast in &castable.casts { +/// A pattern in a cast, e.g.`"ascender"` or `v: i64`. +enum Pattern { + Str(syn::LitStr), + Ty(syn::Pat, syn::Type), +} + +fn create_castable_body(input: &CastInput) -> TokenStream { + let mut strings = vec![]; + let mut casts = vec![]; + + for cast in &input.from_value { match &cast.pattern { Pattern::Str(lit) => { - string_arms.push(quote! { #lit => return true }); + strings.push(quote! { #lit => return true }); } Pattern::Ty(_, ty) => { - cast_checks.push(quote! { - if <#ty as ::typst::eval::Cast>::is(value) { + casts.push(quote! { + if <#ty as ::typst::eval::Reflect>::castable(value) { return true; } }); @@ -190,7 +212,7 @@ fn create_is_func(castable: &Castable) -> TokenStream { } } - let dynamic_check = castable.name.is_some().then(|| { + let dynamic_check = input.name.is_some().then(|| { quote! { if let ::typst::eval::Value::Dyn(dynamic) = &value { if dynamic.is::<Self>() { @@ -200,11 +222,11 @@ fn create_is_func(castable: &Castable) -> TokenStream { } }); - let str_check = (!string_arms.is_empty()).then(|| { + let str_check = (!strings.is_empty()).then(|| { quote! { if let ::typst::eval::Value::Str(string) = &value { match string.as_str() { - #(#string_arms,)* + #(#strings,)* _ => {} } } @@ -212,21 +234,57 @@ fn create_is_func(castable: &Castable) -> TokenStream { }); quote! { - fn is(value: &::typst::eval::Value) -> bool { - #dynamic_check - #str_check - #(#cast_checks)* - false - } + #dynamic_check + #str_check + #(#casts)* + false } } -/// Create the castable's `cast` function. -fn create_cast_func(castable: &Castable) -> TokenStream { +fn create_describe_body(input: &CastInput) -> TokenStream { + let mut infos = vec![]; + + for cast in &input.from_value { + let docs = documentation(&cast.attrs); + infos.push(match &cast.pattern { + Pattern::Str(lit) => { + quote! { + ::typst::eval::CastInfo::Value( + ::typst::eval::IntoValue::into_value(#lit), + #docs, + ) + } + } + Pattern::Ty(_, ty) => { + quote! { <#ty as ::typst::eval::Reflect>::describe() } + } + }); + } + + if let Some(name) = &input.name { + infos.push(quote! { + ::typst::eval::CastInfo::Type(#name) + }); + } + + quote! { + #(#infos)+* + } +} + +fn create_into_value_body(input: &CastInput) -> TokenStream { + if let Some(expr) = &input.into_value { + quote! { #expr } + } else { + quote! { ::typst::eval::Value::dynamic(self) } + } +} + +fn create_from_value_body(input: &CastInput) -> TokenStream { let mut string_arms = vec![]; let mut cast_checks = vec![]; - for cast in &castable.casts { + for cast in &input.from_value { let expr = &cast.expr; match &cast.pattern { Pattern::Str(lit) => { @@ -234,8 +292,8 @@ fn create_cast_func(castable: &Castable) -> TokenStream { } Pattern::Ty(binding, ty) => { cast_checks.push(quote! { - if <#ty as ::typst::eval::Cast>::is(&value) { - let #binding = <#ty as ::typst::eval::Cast>::cast(value)?; + if <#ty as ::typst::eval::Reflect>::castable(&value) { + let #binding = <#ty as ::typst::eval::FromValue>::from_value(value)?; return Ok(#expr); } }); @@ -243,7 +301,7 @@ fn create_cast_func(castable: &Castable) -> TokenStream { } } - let dynamic_check = castable.name.is_some().then(|| { + let dynamic_check = input.name.is_some().then(|| { quote! { if let ::typst::eval::Value::Dyn(dynamic) = &value { if let Some(concrete) = dynamic.downcast::<Self>() { @@ -265,40 +323,9 @@ fn create_cast_func(castable: &Castable) -> TokenStream { }); quote! { - fn cast(value: ::typst::eval::Value) -> ::typst::diag::StrResult<Self> { - #dynamic_check - #str_check - #(#cast_checks)* - <Self as ::typst::eval::Cast>::error(value) - } - } -} - -/// Create the castable's `describe` function. -fn create_describe_func(castable: &Castable) -> TokenStream { - let mut infos = vec![]; - - for cast in &castable.casts { - let docs = documentation(&cast.attrs); - infos.push(match &cast.pattern { - Pattern::Str(lit) => { - quote! { ::typst::eval::CastInfo::Value(#lit.into(), #docs) } - } - Pattern::Ty(_, ty) => { - quote! { <#ty as ::typst::eval::Cast>::describe() } - } - }); - } - - if let Some(name) = &castable.name { - infos.push(quote! { - ::typst::eval::CastInfo::Type(#name) - }); - } - - quote! { - fn describe() -> ::typst::eval::CastInfo { - #(#infos)+* - } + #dynamic_check + #str_check + #(#cast_checks)* + Err(<Self as ::typst::eval::Reflect>::error(&value)) } } diff --git a/macros/src/element.rs b/macros/src/element.rs index d5882666..6ce91fcb 100644 --- a/macros/src/element.rs +++ b/macros/src/element.rs @@ -1,8 +1,8 @@ use super::*; /// Expand the `#[element]` macro. -pub fn element(stream: TokenStream, body: syn::ItemStruct) -> Result<TokenStream> { - let element = prepare(stream, &body)?; +pub fn element(stream: TokenStream, body: &syn::ItemStruct) -> Result<TokenStream> { + let element = prepare(stream, body)?; Ok(create(&element)) } @@ -207,9 +207,9 @@ fn create(element: &Elem) -> TokenStream { #set_impl #locatable_impl - impl From<#ident> for ::typst::eval::Value { - fn from(value: #ident) -> Self { - value.0.into() + impl ::typst::eval::IntoValue for #ident { + fn into_value(self) -> ::typst::eval::Value { + ::typst::eval::Value::Content(self.0) } } } @@ -326,8 +326,8 @@ fn create_set_field_method(field: &Field) -> TokenStream { #vis fn #set_ident(#ident: #ty) -> ::typst::model::Style { ::typst::model::Style::Property(::typst::model::Property::new( <Self as ::typst::model::Element>::func(), - #name.into(), - #ident.into() + #name, + #ident, )) } } @@ -369,7 +369,9 @@ fn create_pack_impl(element: &Elem) -> TokenStream { keywords: #keywords, docs: #docs, params: ::std::vec![#(#infos),*], - returns: ::std::vec!["content"], + returns: ::typst::eval::CastInfo::Union(::std::vec![ + ::typst::eval::CastInfo::Type("content") + ]), category: #category, scope: #scope, }), @@ -426,7 +428,7 @@ fn create_param_info(field: &Field) -> TokenStream { quote! { || { let typed: #default_ty = #default; - ::typst::eval::Value::from(typed) + ::typst::eval::IntoValue::into_value(typed) } } })); @@ -439,9 +441,7 @@ fn create_param_info(field: &Field) -> TokenStream { ::typst::eval::ParamInfo { name: #name, docs: #docs, - cast: <#ty as ::typst::eval::Cast< - ::typst::syntax::Spanned<::typst::eval::Value> - >>::describe(), + cast: <#ty as ::typst::eval::Reflect>::describe(), default: #default, positional: #positional, named: #named, diff --git a/macros/src/func.rs b/macros/src/func.rs index 2e63ee75..4a68e846 100644 --- a/macros/src/func.rs +++ b/macros/src/func.rs @@ -1,11 +1,9 @@ -use quote::ToTokens; - use super::*; /// Expand the `#[func]` macro. -pub fn func(item: syn::ItemFn) -> Result<TokenStream> { - let func = prepare(&item)?; - Ok(create(&func)) +pub fn func(stream: TokenStream, item: &syn::ItemFn) -> Result<TokenStream> { + let func = prepare(stream, item)?; + Ok(create(&func, item)) } struct Func { @@ -16,9 +14,14 @@ struct Func { docs: String, vis: syn::Visibility, ident: Ident, + ident_func: Ident, + parent: Option<syn::Type>, + vm: bool, + vt: bool, + args: bool, + span: bool, params: Vec<Param>, - returns: Vec<String>, - body: syn::Block, + returns: syn::Type, scope: Option<BlockWithReturn>, } @@ -33,9 +36,15 @@ struct Param { ty: syn::Type, } -fn prepare(item: &syn::ItemFn) -> Result<Func> { +fn prepare(stream: TokenStream, item: &syn::ItemFn) -> Result<Func> { let sig = &item.sig; + let Parent(parent) = syn::parse2(stream)?; + + let mut vm = false; + let mut vt = false; + let mut args = false; + let mut span = false; let mut params = vec![]; for input in &sig.inputs { let syn::FnArg::Typed(typed) = input else { @@ -51,99 +60,148 @@ fn prepare(item: &syn::ItemFn) -> Result<Func> { bail!(typed.pat, "expected identifier"); }; - if sig.output.to_token_stream().to_string() != "-> Value" { - bail!(sig.output, "must return `Value`"); - } + match ident.to_string().as_str() { + "vm" => vm = true, + "vt" => vt = true, + "args" => args = true, + "span" => span = true, + _ => { + let mut attrs = typed.attrs.clone(); + params.push(Param { + name: kebab_case(ident), + docs: documentation(&attrs), + external: has_attr(&mut attrs, "external"), + named: has_attr(&mut attrs, "named"), + variadic: has_attr(&mut attrs, "variadic"), + default: parse_attr(&mut attrs, "default")?.map(|expr| { + expr.unwrap_or_else( + || parse_quote! { ::std::default::Default::default() }, + ) + }), + ident: ident.clone(), + ty: (*typed.ty).clone(), + }); - let mut attrs = typed.attrs.clone(); - params.push(Param { - name: kebab_case(ident), - docs: documentation(&attrs), - external: has_attr(&mut attrs, "external"), - named: has_attr(&mut attrs, "named"), - variadic: has_attr(&mut attrs, "variadic"), - default: parse_attr(&mut attrs, "default")?.map(|expr| { - expr.unwrap_or_else( - || parse_quote! { ::std::default::Default::default() }, - ) - }), - ident: ident.clone(), - ty: (*typed.ty).clone(), - }); - - validate_attrs(&attrs)?; + validate_attrs(&attrs)?; + } + } } let mut attrs = item.attrs.clone(); let docs = documentation(&attrs); let mut lines = docs.split('\n').collect(); - let returns = meta_line(&mut lines, "Returns")? - .split(" or ") - .map(Into::into) - .collect(); let keywords = meta_line(&mut lines, "Keywords").ok().map(Into::into); let category = meta_line(&mut lines, "Category")?.into(); let display = meta_line(&mut lines, "Display")?.into(); let docs = lines.join("\n").trim().into(); let func = Func { - name: sig.ident.to_string().replace('_', ""), + name: sig.ident.to_string().trim_end_matches('_').replace('_', "-"), display, category, keywords, docs, vis: item.vis.clone(), ident: sig.ident.clone(), + ident_func: Ident::new( + &format!("{}_func", sig.ident.to_string().trim_end_matches('_')), + sig.ident.span(), + ), + parent, params, - returns, - body: (*item.block).clone(), + returns: match &sig.output { + syn::ReturnType::Default => parse_quote! { () }, + syn::ReturnType::Type(_, ty) => ty.as_ref().clone(), + }, scope: parse_attr(&mut attrs, "scope")?.flatten(), + vm, + vt, + args, + span, }; - validate_attrs(&attrs)?; Ok(func) } -fn create(func: &Func) -> TokenStream { +fn create(func: &Func, item: &syn::ItemFn) -> TokenStream { let Func { name, display, - keywords, category, docs, vis, ident, - params, + ident_func, returns, - body, .. } = func; - let handlers = params.iter().filter(|param| !param.external).map(create_param_parser); - let params = params.iter().map(create_param_info); + + let handlers = func + .params + .iter() + .filter(|param| !param.external) + .map(create_param_parser); + + let args = func + .params + .iter() + .filter(|param| !param.external) + .map(|param| ¶m.ident); + + let parent = func.parent.as_ref().map(|ty| quote! { #ty:: }); + let vm_ = func.vm.then(|| quote! { vm, }); + let vt_ = func.vt.then(|| quote! { &mut vm.vt, }); + let args_ = func.args.then(|| quote! { args.take(), }); + let span_ = func.span.then(|| quote! { args.span, }); + let wrapper = quote! { + |vm, args| { + let __typst_func = #parent #ident; + #(#handlers)* + let output = __typst_func(#(#args,)* #vm_ #vt_ #args_ #span_); + ::typst::eval::IntoResult::into_result(output, args.span) + } + }; + + let mut item = item.clone(); + item.attrs.clear(); + + let inputs = item.sig.inputs.iter().cloned().filter_map(|mut input| { + if let syn::FnArg::Typed(typed) = &mut input { + if typed.attrs.iter().any(|attr| attr.path().is_ident("external")) { + return None; + } + typed.attrs.clear(); + } + Some(input) + }); + + item.sig.inputs = parse_quote! { #(#inputs),* }; + + let keywords = quote_option(&func.keywords); + let params = func.params.iter().map(create_param_info); let scope = create_scope_builder(func.scope.as_ref()); - let keywords = quote_option(keywords); + quote! { - #[doc = #docs] - #vis fn #ident() -> &'static ::typst::eval::NativeFunc { + #[doc(hidden)] + #vis fn #ident_func() -> &'static ::typst::eval::NativeFunc { static FUNC: ::typst::eval::NativeFunc = ::typst::eval::NativeFunc { - func: |vm, args| { - #(#handlers)* - #[allow(unreachable_code)] - Ok(#body) - }, + func: #wrapper, info: ::typst::eval::Lazy::new(|| typst::eval::FuncInfo { name: #name, display: #display, keywords: #keywords, + category: #category, docs: #docs, params: ::std::vec![#(#params),*], - returns: ::std::vec![#(#returns),*], - category: #category, + returns: <#returns as ::typst::eval::Reflect>::describe(), scope: #scope, }), }; &FUNC } + + #[doc = #docs] + #item } } @@ -156,7 +214,7 @@ fn create_param_info(param: &Param) -> TokenStream { quote! { || { let typed: #ty = #default; - ::typst::eval::Value::from(typed) + ::typst::eval::IntoValue::into_value(typed) } } })); @@ -169,9 +227,7 @@ fn create_param_info(param: &Param) -> TokenStream { ::typst::eval::ParamInfo { name: #name, docs: #docs, - cast: <#ty as ::typst::eval::Cast< - ::typst::syntax::Spanned<::typst::eval::Value> - >>::describe(), + cast: <#ty as ::typst::eval::Reflect>::describe(), default: #default, positional: #positional, named: #named, @@ -200,5 +256,13 @@ fn create_param_parser(param: &Param) -> TokenStream { value = quote! { #value.unwrap_or_else(|| #default) } } - quote! { let #ident: #ty = #value; } + quote! { let mut #ident: #ty = #value; } +} + +struct Parent(Option<syn::Type>); + +impl Parse for Parent { + fn parse(input: ParseStream) -> Result<Self> { + Ok(Self(if !input.is_empty() { Some(input.parse()?) } else { None })) + } } diff --git a/macros/src/lib.rs b/macros/src/lib.rs index 945bbcd0..49840ef2 100644 --- a/macros/src/lib.rs +++ b/macros/src/lib.rs @@ -21,46 +21,40 @@ use self::util::*; /// Turns a function into a `NativeFunc`. #[proc_macro_attribute] -pub fn func(_: BoundaryStream, item: BoundaryStream) -> BoundaryStream { +pub fn func(stream: BoundaryStream, item: BoundaryStream) -> BoundaryStream { let item = syn::parse_macro_input!(item as syn::ItemFn); - func::func(item).unwrap_or_else(|err| err.to_compile_error()).into() + func::func(stream.into(), &item) + .unwrap_or_else(|err| err.to_compile_error()) + .into() } -/// Turns a struct into an element. +/// Turns a type into an `Element`. #[proc_macro_attribute] pub fn element(stream: BoundaryStream, item: BoundaryStream) -> BoundaryStream { let item = syn::parse_macro_input!(item as syn::ItemStruct); - element::element(stream.into(), item) + element::element(stream.into(), &item) .unwrap_or_else(|err| err.to_compile_error()) .into() } -/// Implement `Cast` for an enum. +/// Implements `Reflect`, `FromValue`, and `IntoValue` for an enum. #[proc_macro_derive(Cast, attributes(string))] -pub fn cast(item: BoundaryStream) -> BoundaryStream { +pub fn derive_cast(item: BoundaryStream) -> BoundaryStream { let item = syn::parse_macro_input!(item as DeriveInput); - castable::cast(item) - .unwrap_or_else(|err| err.to_compile_error()) - .into() -} - -/// Implement `Cast` and optionally `Type` for a type. -#[proc_macro] -pub fn cast_from_value(stream: BoundaryStream) -> BoundaryStream { - castable::cast_from_value(stream.into()) + castable::derive_cast(&item) .unwrap_or_else(|err| err.to_compile_error()) .into() } -/// Implement `From<T> for Value` for a type `T`. +/// Implements `Reflect`, `FromValue`, and `IntoValue` for a type. #[proc_macro] -pub fn cast_to_value(stream: BoundaryStream) -> BoundaryStream { - castable::cast_to_value(stream.into()) +pub fn cast(stream: BoundaryStream) -> BoundaryStream { + castable::cast(stream.into()) .unwrap_or_else(|err| err.to_compile_error()) .into() } -/// Define a list of symbols. +/// Defines a list of `Symbol`s. #[proc_macro] pub fn symbols(stream: BoundaryStream) -> BoundaryStream { symbols::symbols(stream.into()) diff --git a/macros/src/util.rs b/macros/src/util.rs index 2e12ef17..389fed06 100644 --- a/macros/src/util.rs +++ b/macros/src/util.rs @@ -5,16 +5,16 @@ use super::*; /// Return an error at the given item. macro_rules! bail { - (callsite, $fmt:literal $($tts:tt)*) => { + (callsite, $($tts:tt)*) => { return Err(syn::Error::new( proc_macro2::Span::call_site(), - format!(concat!("typst: ", $fmt) $($tts)*) + format!("typst: {}", format!($($tts)*)) )) }; - ($item:expr, $fmt:literal $($tts:tt)*) => { + ($item:expr, $($tts:tt)*) => { return Err(syn::Error::new_spanned( &$item, - format!(concat!("typst: ", $fmt) $($tts)*) + format!("typst: {}", format!($($tts)*)) )) }; } @@ -51,7 +51,13 @@ pub fn parse_attr<T: Parse>( target: &str, ) -> Result<Option<Option<T>>> { take_attr(attrs, target) - .map(|attr| (!attr.tokens.is_empty()).then(|| attr.parse_args()).transpose()) + .map(|attr| { + Ok(match attr.meta { + syn::Meta::Path(_) => None, + syn::Meta::List(list) => Some(list.parse_args()?), + syn::Meta::NameValue(meta) => bail!(meta, "not valid here"), + }) + }) .transpose() } @@ -62,16 +68,16 @@ pub fn take_attr( ) -> Option<syn::Attribute> { attrs .iter() - .position(|attr| attr.path.is_ident(target)) + .position(|attr| attr.path().is_ident(target)) .map(|i| attrs.remove(i)) } /// Ensure that no unrecognized attributes remain. pub fn validate_attrs(attrs: &[syn::Attribute]) -> Result<()> { for attr in attrs { - if !attr.path.is_ident("doc") { - let ident = attr.path.get_ident().unwrap(); - bail!(ident, "unrecognized attribute: {:?}", ident.to_string()); + if !attr.path().is_ident("doc") && !attr.path().is_ident("derive") { + let ident = attr.path().get_ident().unwrap(); + bail!(ident, "unrecognized attribute: {ident}"); } } Ok(()) @@ -88,13 +94,15 @@ pub fn documentation(attrs: &[syn::Attribute]) -> String { // Parse doc comments. for attr in attrs { - if let Ok(syn::Meta::NameValue(meta)) = attr.parse_meta() { + if let syn::Meta::NameValue(meta) = &attr.meta { if meta.path.is_ident("doc") { - if let syn::Lit::Str(string) = &meta.lit { - let full = string.value(); - let line = full.strip_prefix(' ').unwrap_or(&full); - doc.push_str(line); - doc.push('\n'); + if let syn::Expr::Lit(lit) = &meta.value { + if let syn::Lit::Str(string) = &lit.lit { + let full = string.value(); + let line = full.strip_prefix(' ').unwrap_or(&full); + doc.push_str(line); + doc.push('\n'); + } } } } @@ -110,7 +118,7 @@ pub fn meta_line<'a>(lines: &mut Vec<&'a str>, key: &str) -> Result<&'a str> { lines.pop(); Ok(value.trim()) } - None => bail!(callsite, "missing metadata key: {}", key), + None => bail!(callsite, "missing metadata key: {key}"), } } diff --git a/src/diag.rs b/src/diag.rs index a122fe1f..88924310 100644 --- a/src/diag.rs +++ b/src/diag.rs @@ -8,44 +8,53 @@ use std::str::Utf8Error; use std::string::FromUtf8Error; use comemo::Tracked; -use ecow::EcoString; use crate::syntax::{ErrorPos, Span, Spanned}; use crate::World; -/// Early-return with a [`SourceError`]. +/// Early-return with a [`StrError`] or [`SourceError`]. #[macro_export] #[doc(hidden)] macro_rules! __bail { + ($fmt:literal $(, $arg:expr)* $(,)?) => { + return Err($crate::diag::eco_format!($fmt, $($arg),*)) + }; + ($error:expr) => { return Err(Box::new(vec![$error])) }; - ($($tts:tt)*) => { - $crate::diag::bail!($crate::diag::error!($($tts)*)) + ($span:expr, $fmt:literal $(, $arg:expr)* $(,)?) => { + return Err(Box::new(vec![$crate::diag::SourceError::new( + $span, + $crate::diag::eco_format!($fmt, $($arg),*), + )])) }; } #[doc(inline)] pub use crate::__bail as bail; -/// Construct a [`SourceError`]. +/// Construct a [`StrError`] or [`SourceError`]. #[macro_export] #[doc(hidden)] macro_rules! __error { - ($span:expr, $message:expr $(,)?) => { - $crate::diag::SourceError::new($span, $message) + ($fmt:literal $(, $arg:expr)* $(,)?) => { + $crate::diag::eco_format!($fmt, $($arg),*) }; - ($span:expr, $fmt:expr, $($arg:expr),+ $(,)?) => { - $crate::diag::error!($span, $crate::diag::eco_format!($fmt, $($arg),+)) + ($span:expr, $fmt:literal $(, $arg:expr)* $(,)?) => { + $crate::diag::SourceError::new( + $span, + $crate::diag::eco_format!($fmt, $($arg),*), + ) }; } #[doc(inline)] pub use crate::__error as error; #[doc(hidden)] -pub use ecow::eco_format; +pub use ecow::{eco_format, EcoString}; /// A result that can carry multiple source errors. pub type SourceResult<T> = Result<T, Box<Vec<SourceError>>>; @@ -68,7 +77,6 @@ pub struct SourceError { impl SourceError { /// Create a new, bare error. - #[track_caller] pub fn new(span: Span, message: impl Into<EcoString>) -> Self { Self { span, @@ -173,7 +181,7 @@ where S: Into<EcoString>, { fn at(self, span: Span) -> SourceResult<T> { - self.map_err(|message| Box::new(vec![error!(span, message)])) + self.map_err(|message| Box::new(vec![SourceError::new(span, message)])) } } @@ -8,7 +8,7 @@ use std::sync::Arc; use ecow::EcoString; -use crate::eval::{cast_from_value, cast_to_value, dict, Dict, Value}; +use crate::eval::{cast, dict, Dict, Value}; use crate::font::Font; use crate::geom::{ self, rounded_rect, Abs, Align, Axes, Color, Corners, Dir, Em, Geometry, Length, @@ -567,15 +567,12 @@ impl FromStr for Lang { } } -cast_from_value! { +cast! { Lang, + self => self.as_str().into_value(), string: EcoString => Self::from_str(&string)?, } -cast_to_value! { - v: Lang => v.as_str().into() -} - /// An identifier for a region somewhere in the world. #[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] pub struct Region([u8; 2]); @@ -608,15 +605,12 @@ impl FromStr for Region { } } -cast_from_value! { +cast! { Region, + self => self.as_str().into_value(), string: EcoString => Self::from_str(&string)?, } -cast_to_value! { - v: Region => v.as_str().into() -} - /// Meta information that isn't visible or renderable. #[derive(Clone, PartialEq, Hash)] pub enum Meta { @@ -633,6 +627,10 @@ pub enum Meta { Hide, } +cast! { + type Meta: "meta", +} + impl Debug for Meta { fn fmt(&self, f: &mut Formatter) -> fmt::Result { match self { @@ -644,10 +642,6 @@ impl Debug for Meta { } } -cast_from_value! { - Meta: "meta", -} - /// A link destination. #[derive(Debug, Clone, Eq, PartialEq, Hash)] pub enum Destination { @@ -659,21 +653,18 @@ pub enum Destination { Location(Location), } -cast_from_value! { +cast! { Destination, + self => match self { + Self::Url(v) => v.into_value(), + Self::Position(v) => v.into_value(), + Self::Location(v) => v.into_value(), + }, v: EcoString => Self::Url(v), v: Position => Self::Position(v), v: Location => Self::Location(v), } -cast_to_value! { - v: Destination => match v { - Destination::Url(v) => v.into(), - Destination::Position(v) => v.into(), - Destination::Location(v) => v.into(), - } -} - /// A physical position in a document. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] pub struct Position { @@ -683,8 +674,9 @@ pub struct Position { pub point: Point, } -cast_from_value! { +cast! { Position, + self => Value::Dict(self.into()), mut dict: Dict => { let page = dict.take("page")?.cast()?; let x: Length = dict.take("x")?.cast()?; @@ -694,12 +686,14 @@ cast_from_value! { }, } -cast_to_value! { - v: Position => Value::Dict(dict! { - "page" => Value::Int(v.page.get() as i64), - "x" => Value::Length(v.point.x.into()), - "y" => Value::Length(v.point.y.into()), - }) +impl From<Position> for Dict { + fn from(pos: Position) -> Self { + dict! { + "page" => pos.page, + "x" => pos.point.x, + "y" => pos.point.y, + } + } } #[cfg(test)] diff --git a/src/eval/args.rs b/src/eval/args.rs index bea2baa1..da29eeaf 100644 --- a/src/eval/args.rs +++ b/src/eval/args.rs @@ -2,7 +2,7 @@ use std::fmt::{self, Debug, Formatter}; use ecow::{eco_format, EcoVec}; -use super::{Array, Cast, Dict, Str, Value}; +use super::{Array, Dict, FromValue, IntoValue, Str, Value}; use crate::diag::{bail, At, SourceResult}; use crate::syntax::{Span, Spanned}; use crate::util::pretty_array_like; @@ -29,10 +29,14 @@ pub struct Arg { impl Args { /// Create positional arguments from a span and values. - pub fn new(span: Span, values: impl IntoIterator<Item = Value>) -> Self { + pub fn new<T: IntoValue>(span: Span, values: impl IntoIterator<Item = T>) -> Self { let items = values .into_iter() - .map(|value| Arg { span, name: None, value: Spanned::new(value, span) }) + .map(|value| Arg { + span, + name: None, + value: Spanned::new(value.into_value(), span), + }) .collect(); Self { span, items } } @@ -49,13 +53,13 @@ impl Args { /// Consume and cast the first positional argument if there is one. pub fn eat<T>(&mut self) -> SourceResult<Option<T>> where - T: Cast<Spanned<Value>>, + T: FromValue<Spanned<Value>>, { for (i, slot) in self.items.iter().enumerate() { if slot.name.is_none() { let value = self.items.remove(i).value; let span = value.span; - return T::cast(value).at(span).map(Some); + return T::from_value(value).at(span).map(Some); } } Ok(None) @@ -87,24 +91,24 @@ impl Args { /// left. pub fn expect<T>(&mut self, what: &str) -> SourceResult<T> where - T: Cast<Spanned<Value>>, + T: FromValue<Spanned<Value>>, { match self.eat()? { Some(v) => Ok(v), - None => bail!(self.span, "missing argument: {}", what), + None => bail!(self.span, "missing argument: {what}"), } } /// Find and consume the first castable positional argument. pub fn find<T>(&mut self) -> SourceResult<Option<T>> where - T: Cast<Spanned<Value>>, + T: FromValue<Spanned<Value>>, { for (i, slot) in self.items.iter().enumerate() { - if slot.name.is_none() && T::is(&slot.value) { + if slot.name.is_none() && T::castable(&slot.value.v) { let value = self.items.remove(i).value; let span = value.span; - return T::cast(value).at(span).map(Some); + return T::from_value(value).at(span).map(Some); } } Ok(None) @@ -113,7 +117,7 @@ impl Args { /// Find and consume all castable positional arguments. pub fn all<T>(&mut self) -> SourceResult<Vec<T>> where - T: Cast<Spanned<Value>>, + T: FromValue<Spanned<Value>>, { let mut list = vec![]; while let Some(value) = self.find()? { @@ -126,7 +130,7 @@ impl Args { /// error if the conversion fails. pub fn named<T>(&mut self, name: &str) -> SourceResult<Option<T>> where - T: Cast<Spanned<Value>>, + T: FromValue<Spanned<Value>>, { // We don't quit once we have a match because when multiple matches // exist, we want to remove all of them and use the last one. @@ -136,7 +140,7 @@ impl Args { if self.items[i].name.as_deref() == Some(name) { let value = self.items.remove(i).value; let span = value.span; - found = Some(T::cast(value).at(span)?); + found = Some(T::from_value(value).at(span)?); } else { i += 1; } @@ -147,7 +151,7 @@ impl Args { /// Same as named, but with fallback to find. pub fn named_or_find<T>(&mut self, name: &str) -> SourceResult<Option<T>> where - T: Cast<Spanned<Value>>, + T: FromValue<Spanned<Value>>, { match self.named(name)? { Some(value) => Ok(Some(value)), @@ -167,13 +171,10 @@ impl Args { /// argument. pub fn finish(self) -> SourceResult<()> { if let Some(arg) = self.items.first() { - bail!( - arg.span, - match &arg.name { - Some(name) => eco_format!("unexpected argument: {}", name), - _ => eco_format!("unexpected argument"), - } - ) + match &arg.name { + Some(name) => bail!(arg.span, "unexpected argument: {name}"), + _ => bail!(arg.span, "unexpected argument"), + } } Ok(()) } diff --git a/src/eval/array.rs b/src/eval/array.rs index d0b63b35..a7a1387b 100644 --- a/src/eval/array.rs +++ b/src/eval/array.rs @@ -4,7 +4,7 @@ use std::ops::{Add, AddAssign}; use ecow::{eco_format, EcoString, EcoVec}; -use super::{ops, Args, Func, Value, Vm}; +use super::{ops, Args, CastInfo, FromValue, Func, IntoValue, Reflect, Value, Vm}; use crate::diag::{At, SourceResult, StrResult}; use crate::syntax::Span; use crate::util::pretty_array_like; @@ -14,11 +14,16 @@ use crate::util::pretty_array_like; #[doc(hidden)] macro_rules! __array { ($value:expr; $count:expr) => { - $crate::eval::Array::from_vec($crate::eval::eco_vec![$value.into(); $count]) + $crate::eval::Array::from($crate::eval::eco_vec![ + $crate::eval::IntoValue::into_value($value); + $count + ]) }; ($($value:expr),* $(,)?) => { - $crate::eval::Array::from_vec($crate::eval::eco_vec![$($value.into()),*]) + $crate::eval::Array::from($crate::eval::eco_vec![$( + $crate::eval::IntoValue::into_value($value) + ),*]) }; } @@ -38,19 +43,14 @@ impl Array { Self::default() } - /// Create a new array from an eco vector of values. - pub fn from_vec(vec: EcoVec<Value>) -> Self { - Self(vec) - } - /// Return `true` if the length is 0. pub fn is_empty(&self) -> bool { self.0.len() == 0 } /// The length of the array. - pub fn len(&self) -> i64 { - self.0.len() as i64 + pub fn len(&self) -> usize { + self.0.len() } /// The first value in the array. @@ -134,14 +134,14 @@ impl Array { .filter(|&start| start <= self.0.len()) .ok_or_else(|| out_of_bounds(start, len))?; - let end = end.unwrap_or(self.len()); + let end = end.unwrap_or(self.len() as i64); let end = self .locate(end) .filter(|&end| end <= self.0.len()) .ok_or_else(|| out_of_bounds(end, len))? .max(start); - Ok(Self::from_vec(self.0[start..end].into())) + Ok(self.0[start..end].into()) } /// Whether the array contains a specific value. @@ -182,7 +182,7 @@ impl Array { kept.push(item.clone()) } } - Ok(Self::from_vec(kept)) + Ok(kept.into()) } /// Transform each item in the array with a function. @@ -273,7 +273,7 @@ impl Array { flat.push(item.clone()); } } - Self::from_vec(flat) + flat.into() } /// Returns a new array with reversed order. @@ -317,9 +317,7 @@ impl Array { pub fn zip(&self, other: Array) -> Array { self.iter() .zip(other) - .map(|(first, second)| { - Value::Array(Array::from_vec(eco_vec![first.clone(), second])) - }) + .map(|(first, second)| array![first.clone(), second].into_value()) .collect() } @@ -360,7 +358,7 @@ impl Array { } } }); - result.map(|_| Self::from_vec(vec)) + result.map(|_| vec.into()) } /// Repeat this array `n` times. @@ -385,19 +383,20 @@ impl Array { /// Resolve an index. fn locate(&self, index: i64) -> Option<usize> { - usize::try_from(if index >= 0 { index } else { self.len().checked_add(index)? }) - .ok() + usize::try_from(if index >= 0 { + index + } else { + (self.len() as i64).checked_add(index)? + }) + .ok() } /// Enumerate all items in the array. pub fn enumerate(&self) -> Self { - let v = self - .iter() + self.iter() .enumerate() - .map(|(i, value)| array![i, value.clone()]) - .map(Value::Array) - .collect(); - Self::from_vec(v) + .map(|(i, value)| array![i, value.clone()].into_value()) + .collect() } } @@ -453,6 +452,40 @@ impl<'a> IntoIterator for &'a Array { } } +impl From<EcoVec<Value>> for Array { + fn from(v: EcoVec<Value>) -> Self { + Array(v) + } +} + +impl From<&[Value]> for Array { + fn from(v: &[Value]) -> Self { + Array(v.into()) + } +} + +impl<T> Reflect for Vec<T> { + fn describe() -> CastInfo { + Array::describe() + } + + fn castable(value: &Value) -> bool { + Array::castable(value) + } +} + +impl<T: IntoValue> IntoValue for Vec<T> { + fn into_value(self) -> Value { + Value::Array(self.into_iter().map(IntoValue::into_value).collect()) + } +} + +impl<T: FromValue> FromValue for Vec<T> { + fn from_value(value: Value) -> StrResult<Self> { + value.cast::<Array>()?.into_iter().map(Value::cast).collect() + } +} + /// The error message when the array is empty. #[cold] fn array_is_empty() -> EcoString { @@ -461,17 +494,15 @@ fn array_is_empty() -> EcoString { /// The out of bounds access error message. #[cold] -fn out_of_bounds(index: i64, len: i64) -> EcoString { - eco_format!("array index out of bounds (index: {}, len: {})", index, len) +fn out_of_bounds(index: i64, len: usize) -> EcoString { + eco_format!("array index out of bounds (index: {index}, len: {len})") } /// The out of bounds access error message when no default value was given. #[cold] -fn out_of_bounds_no_default(index: i64, len: i64) -> EcoString { +fn out_of_bounds_no_default(index: i64, len: usize) -> EcoString { eco_format!( - "array index out of bounds (index: {}, len: {}) \ + "array index out of bounds (index: {index}, len: {len}) \ and no default value was specified", - index, - len ) } diff --git a/src/eval/auto.rs b/src/eval/auto.rs new file mode 100644 index 00000000..e73b3f33 --- /dev/null +++ b/src/eval/auto.rs @@ -0,0 +1,39 @@ +use std::fmt::{self, Debug, Formatter}; + +use super::{CastInfo, FromValue, IntoValue, Reflect, Value}; +use crate::diag::StrResult; + +/// A value that indicates a smart default. +#[derive(Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub struct AutoValue; + +impl IntoValue for AutoValue { + fn into_value(self) -> Value { + Value::Auto + } +} + +impl FromValue for AutoValue { + fn from_value(value: Value) -> StrResult<Self> { + match value { + Value::Auto => Ok(Self), + _ => Err(Self::error(&value)), + } + } +} + +impl Reflect for AutoValue { + fn describe() -> CastInfo { + CastInfo::Type("auto") + } + + fn castable(value: &Value) -> bool { + matches!(value, Value::Auto) + } +} + +impl Debug for AutoValue { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.pad("auto") + } +} diff --git a/src/eval/cast.rs b/src/eval/cast.rs index 7ef2f1d0..29cf5f71 100644 --- a/src/eval/cast.rs +++ b/src/eval/cast.rs @@ -1,278 +1,185 @@ -pub use typst_macros::{cast_from_value, cast_to_value, Cast}; +pub use typst_macros::{cast, Cast}; -use std::num::{NonZeroI64, NonZeroU64, NonZeroUsize}; +use std::fmt::Write; use std::ops::Add; use ecow::EcoString; -use super::{Array, Str, Value}; -use crate::diag::StrResult; -use crate::eval::Type; -use crate::geom::Length; -use crate::syntax::Spanned; +use super::Value; +use crate::diag::{At, SourceResult, StrResult}; +use crate::syntax::{Span, Spanned}; use crate::util::separated_list; -/// Cast from a value to a specific type. -pub trait Cast<V = Value>: Sized { - /// Check whether the value is castable to `Self`. - fn is(value: &V) -> bool; - - /// Try to cast the value into an instance of `Self`. - fn cast(value: V) -> StrResult<Self>; - - /// Describe the acceptable values. +/// Determine details of a type. +/// +/// Type casting works as follows: +/// - [`Reflect for T`](Reflect) describes the possible Typst values for `T` +/// (for documentation and autocomplete). +/// - [`IntoValue for T`](IntoValue) is for conversion from `T -> Value` +/// (infallible) +/// - [`FromValue for T`](FromValue) is for conversion from `Value -> T` +/// (fallible). +/// +/// We can't use `TryFrom<Value>` due to conflicting impls. We could use +/// `From<T> for Value`, but that inverses the impl and leads to tons of +/// `.into()` all over the place that become hard to decipher. +pub trait Reflect { + /// Describe the acceptable values for this type. fn describe() -> CastInfo; - /// Produce an error for an inacceptable value. - fn error(value: Value) -> StrResult<Self> { - Err(Self::describe().error(&value)) + /// Whether the given value can be converted to `T`. + /// + /// This exists for performance. The check could also be done through the + /// [`CastInfo`], but it would be much more expensive (heap allocation + + /// dynamic checks instead of optimized machine code for each type). + fn castable(value: &Value) -> bool; + + /// Produce an error message for an inacceptable value. + /// + /// ``` + /// # use typst::eval::{Int, Reflect, Value}; + /// assert_eq!( + /// <Int as Reflect>::error(Value::None), + /// "expected integer, found none", + /// ); + /// ``` + fn error(found: &Value) -> EcoString { + Self::describe().error(found) } } -impl Cast for Value { - fn is(_: &Value) -> bool { - true - } - - fn cast(value: Value) -> StrResult<Self> { - Ok(value) - } - +impl Reflect for Value { fn describe() -> CastInfo { CastInfo::Any } -} -impl<T: Cast> Cast<Spanned<Value>> for T { - fn is(value: &Spanned<Value>) -> bool { - T::is(&value.v) - } - - fn cast(value: Spanned<Value>) -> StrResult<Self> { - T::cast(value.v) + fn castable(_: &Value) -> bool { + true } +} +impl<T: Reflect> Reflect for Spanned<T> { fn describe() -> CastInfo { T::describe() } -} - -impl<T: Cast> Cast<Spanned<Value>> for Spanned<T> { - fn is(value: &Spanned<Value>) -> bool { - T::is(&value.v) - } - fn cast(value: Spanned<Value>) -> StrResult<Self> { - let span = value.span; - T::cast(value.v).map(|t| Spanned::new(t, span)) + fn castable(value: &Value) -> bool { + T::castable(value) } +} +impl<T: Reflect> Reflect for StrResult<T> { fn describe() -> CastInfo { T::describe() } -} - -cast_to_value! { - v: u8 => Value::Int(v as i64) -} - -cast_to_value! { - v: u16 => Value::Int(v as i64) -} - -cast_from_value! { - u32, - int: i64 => int.try_into().map_err(|_| { - if int < 0 { - "number must be at least zero" - } else { - "number too large" - } - })?, -} -cast_to_value! { - v: u32 => Value::Int(v as i64) -} - -cast_from_value! { - u64, - int: i64 => int.try_into().map_err(|_| { - if int < 0 { - "number must be at least zero" - } else { - "number too large" - } - })?, -} - -cast_to_value! { - v: u64 => Value::Int(v as i64) -} - -cast_from_value! { - usize, - int: i64 => int.try_into().map_err(|_| { - if int < 0 { - "number must be at least zero" - } else { - "number too large" - } - })?, -} - -cast_to_value! { - v: usize => Value::Int(v as i64) -} - -cast_to_value! { - v: i32 => Value::Int(v as i64) -} - -cast_from_value! { - NonZeroI64, - int: i64 => int.try_into() - .map_err(|_| if int == 0 { - "number must not be zero" - } else { - "number too large" - })?, -} - -cast_to_value! { - v: NonZeroI64 => Value::Int(v.get()) -} - -cast_from_value! { - NonZeroU64, - int: i64 => int - .try_into() - .and_then(|int: u64| int.try_into()) - .map_err(|_| if int <= 0 { - "number must be positive" - } else { - "number too large" - })?, + fn castable(value: &Value) -> bool { + T::castable(value) + } } -cast_to_value! { - v: NonZeroU64 => Value::Int(v.get() as i64) -} +impl<T: Reflect> Reflect for SourceResult<T> { + fn describe() -> CastInfo { + T::describe() + } -cast_from_value! { - NonZeroUsize, - int: i64 => int - .try_into() - .and_then(|int: usize| int.try_into()) - .map_err(|_| if int <= 0 { - "number must be positive" - } else { - "number too large" - })?, + fn castable(value: &Value) -> bool { + T::castable(value) + } } -cast_to_value! { - v: NonZeroUsize => Value::Int(v.get() as i64) -} +impl<T: Reflect> Reflect for &T { + fn describe() -> CastInfo { + T::describe() + } -cast_from_value! { - char, - string: Str => { - let mut chars = string.chars(); - match (chars.next(), chars.next()) { - (Some(c), None) => c, - _ => Err("expected exactly one character")?, - } - }, + fn castable(value: &Value) -> bool { + T::castable(value) + } } -cast_to_value! { - v: char => Value::Str(v.into()) -} +impl<T: Reflect> Reflect for &mut T { + fn describe() -> CastInfo { + T::describe() + } -cast_to_value! { - v: &str => Value::Str(v.into()) + fn castable(value: &Value) -> bool { + T::castable(value) + } } -cast_from_value! { - EcoString, - v: Str => v.into(), +/// Cast a Rust type into a Typst [`Value`]. +/// +/// See also: [`Reflect`]. +pub trait IntoValue { + /// Cast this type into a value. + fn into_value(self) -> Value; } -cast_to_value! { - v: EcoString => Value::Str(v.into()) +impl IntoValue for Value { + fn into_value(self) -> Value { + self + } } -cast_from_value! { - String, - v: Str => v.into(), +impl<T: IntoValue> IntoValue for Spanned<T> { + fn into_value(self) -> Value { + self.v.into_value() + } } -cast_to_value! { - v: String => Value::Str(v.into()) +/// Cast a Rust type or result into a [`SourceResult<Value>`]. +/// +/// Converts `T`, [`StrResult<T>`], or [`SourceResult<T>`] into +/// [`SourceResult<Value>`] by `Ok`-wrapping or adding span information. +pub trait IntoResult { + /// Cast this type into a value. + fn into_result(self, span: Span) -> SourceResult<Value>; } -impl<T: Cast> Cast for Option<T> { - fn is(value: &Value) -> bool { - matches!(value, Value::None) || T::is(value) - } - - fn cast(value: Value) -> StrResult<Self> { - match value { - Value::None => Ok(None), - v if T::is(&v) => Ok(Some(T::cast(v)?)), - _ => <Self as Cast>::error(value), - } - } - - fn describe() -> CastInfo { - T::describe() + CastInfo::Type("none") +impl<T: IntoValue> IntoResult for T { + fn into_result(self, _: Span) -> SourceResult<Value> { + Ok(self.into_value()) } } -impl<T: Into<Value>> From<Spanned<T>> for Value { - fn from(spanned: Spanned<T>) -> Self { - spanned.v.into() +impl<T: IntoValue> IntoResult for StrResult<T> { + fn into_result(self, span: Span) -> SourceResult<Value> { + self.map(IntoValue::into_value).at(span) } } -impl<T: Into<Value>> From<Option<T>> for Value { - fn from(v: Option<T>) -> Self { - match v { - Some(v) => v.into(), - None => Value::None, - } +impl<T: IntoValue> IntoResult for SourceResult<T> { + fn into_result(self, _: Span) -> SourceResult<Value> { + self.map(IntoValue::into_value) } } -impl<T: Cast> Cast for Vec<T> { - fn is(value: &Value) -> bool { - Array::is(value) - } - - fn cast(value: Value) -> StrResult<Self> { - value.cast::<Array>()?.into_iter().map(Value::cast).collect() - } - - fn describe() -> CastInfo { - <Array as Cast>::describe() - } +/// Try to cast a Typst [`Value`] into a Rust type. +/// +/// See also: [`Reflect`]. +pub trait FromValue<V = Value>: Sized + Reflect { + /// Try to cast the value into an instance of `Self`. + fn from_value(value: V) -> StrResult<Self>; } -impl<T: Into<Value>> From<Vec<T>> for Value { - fn from(v: Vec<T>) -> Self { - Value::Array(v.into_iter().map(Into::into).collect()) +impl FromValue for Value { + fn from_value(value: Value) -> StrResult<Self> { + Ok(value) } } -/// A container for a variadic argument. -pub trait Variadics { - /// The contained type. - type Inner; +impl<T: FromValue> FromValue<Spanned<Value>> for T { + fn from_value(value: Spanned<Value>) -> StrResult<Self> { + T::from_value(value.v) + } } -impl<T> Variadics for Vec<T> { - type Inner = T; +impl<T: FromValue> FromValue<Spanned<Value>> for Spanned<T> { + fn from_value(value: Spanned<Value>) -> StrResult<Self> { + let span = value.span; + T::from_value(value.v).map(|t| Spanned::new(t, span)) + } } /// Describes a possible value for a cast. @@ -332,10 +239,10 @@ impl CastInfo { } if_chain::if_chain! { if let Value::Int(i) = found; - if parts.iter().any(|p| p == Length::TYPE_NAME); + if parts.iter().any(|p| p == "length"); if !matching_type; then { - msg.push_str(&format!(": a length needs a unit – did you mean {i}pt?")); + write!(msg, ": a length needs a unit – did you mean {i}pt?").unwrap(); } }; @@ -373,19 +280,37 @@ impl Add for CastInfo { } } -/// Castable from nothing. +/// A container for a variadic argument. +pub trait Variadics { + /// The contained type. + type Inner; +} + +impl<T> Variadics for Vec<T> { + type Inner = T; +} + +/// An uninhabitable type. pub enum Never {} -impl Cast for Never { - fn is(_: &Value) -> bool { +impl Reflect for Never { + fn describe() -> CastInfo { + CastInfo::Union(vec![]) + } + + fn castable(_: &Value) -> bool { false } +} - fn cast(value: Value) -> StrResult<Self> { - <Self as Cast>::error(value) +impl IntoValue for Never { + fn into_value(self) -> Value { + match self {} } +} - fn describe() -> CastInfo { - CastInfo::Union(vec![]) +impl FromValue for Never { + fn from_value(value: Value) -> StrResult<Self> { + Err(Self::error(&value)) } } diff --git a/src/eval/datetime.rs b/src/eval/datetime.rs index 47574bae..57d0683d 100644 --- a/src/eval/datetime.rs +++ b/src/eval/datetime.rs @@ -6,7 +6,7 @@ use ecow::{eco_format, EcoString, EcoVec}; use time::error::{Format, InvalidFormatDescription}; use time::{format_description, PrimitiveDateTime}; -use crate::eval::cast_from_value; +use crate::eval::cast; use crate::util::pretty_array_like; /// A datetime object that represents either a date, a time or a combination of @@ -153,8 +153,8 @@ impl Debug for Datetime { } } -cast_from_value! { - Datetime: "datetime", +cast! { + type Datetime: "datetime", } /// Format the `Format` error of the time crate in an appropriate way. diff --git a/src/eval/dict.rs b/src/eval/dict.rs index 49a60147..3e6233ae 100644 --- a/src/eval/dict.rs +++ b/src/eval/dict.rs @@ -17,8 +17,8 @@ macro_rules! __dict { ($($key:expr => $value:expr),* $(,)?) => {{ #[allow(unused_mut)] let mut map = $crate::eval::IndexMap::new(); - $(map.insert($key.into(), $value.into());)* - $crate::eval::Dict::from_map(map) + $(map.insert($key.into(), $crate::eval::IntoValue::into_value($value));)* + $crate::eval::Dict::from(map) }}; } @@ -38,19 +38,14 @@ impl Dict { Self::default() } - /// Create a new dictionary from a mapping of strings to values. - pub fn from_map(map: IndexMap<Str, Value>) -> Self { - Self(Arc::new(map)) - } - /// Whether the dictionary is empty. pub fn is_empty(&self) -> bool { self.0.is_empty() } /// The number of pairs in the dictionary. - pub fn len(&self) -> i64 { - self.0.len() as i64 + pub fn len(&self) -> usize { + self.0.len() } /// Borrow the value the given `key` maps to, @@ -217,6 +212,12 @@ impl<'a> IntoIterator for &'a Dict { } } +impl From<IndexMap<Str, Value>> for Dict { + fn from(map: IndexMap<Str, Value>) -> Self { + Self(Arc::new(map)) + } +} + /// The missing key access error message. #[cold] fn missing_key(key: &str) -> EcoString { diff --git a/src/eval/func.rs b/src/eval/func.rs index a224c5b8..86070fc7 100644 --- a/src/eval/func.rs +++ b/src/eval/func.rs @@ -1,5 +1,3 @@ -pub use typst_macros::func; - use std::fmt::{self, Debug, Formatter}; use std::hash::{Hash, Hasher}; use std::sync::Arc; @@ -9,7 +7,7 @@ use ecow::eco_format; use once_cell::sync::Lazy; use super::{ - cast_to_value, Args, CastInfo, Eval, Flow, Route, Scope, Scopes, Tracer, Value, Vm, + cast, Args, CastInfo, Eval, Flow, IntoValue, Route, Scope, Scopes, Tracer, Value, Vm, }; use crate::diag::{bail, SourceResult, StrResult}; use crate::model::{ElemFunc, Introspector, Locator, Vt}; @@ -119,10 +117,10 @@ impl Func { /// Call the function with a Vt. #[tracing::instrument(skip_all)] - pub fn call_vt( + pub fn call_vt<T: IntoValue>( &self, vt: &mut Vt, - args: impl IntoIterator<Item = Value>, + args: impl IntoIterator<Item = T>, ) -> SourceResult<Value> { let route = Route::default(); let id = SourceId::detached(); @@ -233,13 +231,9 @@ impl From<&'static NativeFunc> for Func { } } -impl<F> From<F> for Value -where - F: Fn() -> &'static NativeFunc, -{ - fn from(f: F) -> Self { - Value::Func(f().into()) - } +cast! { + &'static NativeFunc, + self => Value::Func(self.into()), } /// Details about a function. @@ -249,16 +243,16 @@ pub struct FuncInfo { pub name: &'static str, /// The display name of the function. pub display: &'static str, - /// A string of keywords. + /// A string of search keywords. pub keywords: Option<&'static str>, + /// Which category the function is part of. + pub category: &'static str, /// Documentation for the function. pub docs: &'static str, /// Details about the function's parameters. pub params: Vec<ParamInfo>, - /// Valid types for the return value. - pub returns: Vec<&'static str>, - /// Which category the function is part of. - pub category: &'static str, + /// Valid values for the return value. + pub returns: CastInfo, /// The function's own scope of fields and sub-functions. pub scope: Scope, } @@ -311,6 +305,7 @@ pub(super) struct Closure { pub body: Expr, } +/// A closure parameter. #[derive(Hash)] pub enum Param { /// A positional parameter: `x`. @@ -362,7 +357,7 @@ impl Closure { // Parse the arguments according to the parameter list. let num_pos_params = closure.params.iter().filter(|p| matches!(p, Param::Pos(_))).count(); - let num_pos_args = args.to_pos().len() as usize; + let num_pos_args = args.to_pos().len(); let sink_size = num_pos_args.checked_sub(num_pos_params); let mut sink = None; @@ -425,8 +420,9 @@ impl From<Closure> for Func { } } -cast_to_value! { - v: Closure => Value::Func(v.into()) +cast! { + Closure, + self => Value::Func(self.into()), } /// A visitor that determines which variables to capture for a closure. diff --git a/src/eval/int.rs b/src/eval/int.rs new file mode 100644 index 00000000..4e081617 --- /dev/null +++ b/src/eval/int.rs @@ -0,0 +1,81 @@ +use std::num::{NonZeroI64, NonZeroIsize, NonZeroU64, NonZeroUsize}; + +use super::{cast, Value}; + +macro_rules! signed_int { + ($($ty:ty)*) => { + $(cast! { + $ty, + self => Value::Int(self as i64), + v: i64 => v.try_into().map_err(|_| "number too large")?, + })* + } +} + +macro_rules! unsigned_int { + ($($ty:ty)*) => { + $(cast! { + $ty, + self => Value::Int(self as i64), + v: i64 => v.try_into().map_err(|_| { + if v < 0 { + "number must be at least zero" + } else { + "number too large" + } + })?, + })* + } +} + +macro_rules! signed_nonzero { + ($($ty:ty)*) => { + $(cast! { + $ty, + self => Value::Int(self.get() as i64), + v: i64 => v + .try_into() + .ok() + .and_then($ty::new) + .ok_or_else(|| if v == 0 { + "number must not be zero" + } else { + "number too large" + })?, + })* + } +} + +macro_rules! unsigned_nonzero { + ($($ty:ty)*) => { + $(cast! { + $ty, + self => Value::Int(self.get() as i64), + v: i64 => v + .try_into() + .ok() + .and_then($ty::new) + .ok_or_else(|| if v <= 0 { + "number must be positive" + } else { + "number too large" + })?, + })* + } +} + +signed_int! { + i8 i16 i32 isize +} + +unsigned_int! { + u8 u16 u32 u64 usize +} + +signed_nonzero! { + NonZeroI64 NonZeroIsize +} + +unsigned_nonzero! { + NonZeroU64 NonZeroUsize +} diff --git a/src/eval/methods.rs b/src/eval/methods.rs index f57bf84d..62ac4095 100644 --- a/src/eval/methods.rs +++ b/src/eval/methods.rs @@ -2,7 +2,7 @@ use ecow::EcoString; -use super::{Args, Str, Value, Vm}; +use super::{Args, IntoValue, Str, Value, Vm}; use crate::diag::{At, SourceResult}; use crate::eval::Datetime; use crate::model::{Location, Selector}; @@ -21,20 +21,20 @@ pub fn call( let output = match value { Value::Color(color) => match method { - "lighten" => Value::Color(color.lighten(args.expect("amount")?)), - "darken" => Value::Color(color.darken(args.expect("amount")?)), - "negate" => Value::Color(color.negate()), + "lighten" => color.lighten(args.expect("amount")?).into_value(), + "darken" => color.darken(args.expect("amount")?).into_value(), + "negate" => color.negate().into_value(), _ => return missing(), }, Value::Str(string) => match method { - "len" => Value::Int(string.len()), - "first" => Value::Str(string.first().at(span)?), - "last" => Value::Str(string.last().at(span)?), + "len" => string.len().into_value(), + "first" => string.first().at(span)?.into_value(), + "last" => string.last().at(span)?.into_value(), "at" => { let index = args.expect("index")?; let default = args.named::<EcoString>("default")?; - Value::Str(string.at(index, default.as_deref()).at(span)?) + string.at(index, default.as_deref()).at(span)?.into_value() } "slice" => { let start = args.expect("start")?; @@ -42,56 +42,50 @@ pub fn call( if end.is_none() { end = args.named("count")?.map(|c: i64| start + c); } - Value::Str(string.slice(start, end).at(span)?) + string.slice(start, end).at(span)?.into_value() } - "clusters" => Value::Array(string.clusters()), - "codepoints" => Value::Array(string.codepoints()), - "contains" => Value::Bool(string.contains(args.expect("pattern")?)), - "starts-with" => Value::Bool(string.starts_with(args.expect("pattern")?)), - "ends-with" => Value::Bool(string.ends_with(args.expect("pattern")?)), - "find" => { - string.find(args.expect("pattern")?).map_or(Value::None, Value::Str) - } - "position" => string - .position(args.expect("pattern")?) - .map_or(Value::None, Value::Int), - "match" => string - .match_(args.expect("pattern")?) - .map_or(Value::None, Value::Dict), - "matches" => Value::Array(string.matches(args.expect("pattern")?)), + "clusters" => string.clusters().into_value(), + "codepoints" => string.codepoints().into_value(), + "contains" => string.contains(args.expect("pattern")?).into_value(), + "starts-with" => string.starts_with(args.expect("pattern")?).into_value(), + "ends-with" => string.ends_with(args.expect("pattern")?).into_value(), + "find" => string.find(args.expect("pattern")?).into_value(), + "position" => string.position(args.expect("pattern")?).into_value(), + "match" => string.match_(args.expect("pattern")?).into_value(), + "matches" => string.matches(args.expect("pattern")?).into_value(), "replace" => { let pattern = args.expect("pattern")?; let with = args.expect("string or function")?; let count = args.named("count")?; - Value::Str(string.replace(vm, pattern, with, count)?) + string.replace(vm, pattern, with, count)?.into_value() } "trim" => { let pattern = args.eat()?; let at = args.named("at")?; let repeat = args.named("repeat")?.unwrap_or(true); - Value::Str(string.trim(pattern, at, repeat)) + string.trim(pattern, at, repeat).into_value() } - "split" => Value::Array(string.split(args.eat()?)), + "split" => string.split(args.eat()?).into_value(), _ => return missing(), }, Value::Content(content) => match method { - "func" => content.func().into(), - "has" => Value::Bool(content.has(&args.expect::<EcoString>("field")?)), + "func" => content.func().into_value(), + "has" => content.has(&args.expect::<EcoString>("field")?).into_value(), "at" => content .at(&args.expect::<EcoString>("field")?, args.named("default")?) .at(span)?, - "fields" => Value::Dict(content.dict()), + "fields" => content.dict().into_value(), "location" => content .location() .ok_or("this method can only be called on content returned by query(..)") .at(span)? - .into(), + .into_value(), _ => return missing(), }, Value::Array(array) => match method { - "len" => Value::Int(array.len()), + "len" => array.len().into_value(), "first" => array.first().at(span)?.clone(), "last" => array.last().at(span)?.clone(), "at" => array @@ -104,117 +98,104 @@ pub fn call( if end.is_none() { end = args.named("count")?.map(|c: i64| start + c); } - Value::Array(array.slice(start, end).at(span)?) + array.slice(start, end).at(span)?.into_value() } - "contains" => Value::Bool(array.contains(&args.expect("value")?)), - "find" => array.find(vm, args.expect("function")?)?.unwrap_or(Value::None), - "position" => array - .position(vm, args.expect("function")?)? - .map_or(Value::None, Value::Int), - "filter" => Value::Array(array.filter(vm, args.expect("function")?)?), - "map" => Value::Array(array.map(vm, args.expect("function")?)?), + "contains" => array.contains(&args.expect("value")?).into_value(), + "find" => array.find(vm, args.expect("function")?)?.into_value(), + "position" => array.position(vm, args.expect("function")?)?.into_value(), + "filter" => array.filter(vm, args.expect("function")?)?.into_value(), + "map" => array.map(vm, args.expect("function")?)?.into_value(), "fold" => { array.fold(vm, args.expect("initial value")?, args.expect("function")?)? } "sum" => array.sum(args.named("default")?, span)?, "product" => array.product(args.named("default")?, span)?, - "any" => Value::Bool(array.any(vm, args.expect("function")?)?), - "all" => Value::Bool(array.all(vm, args.expect("function")?)?), - "flatten" => Value::Array(array.flatten()), - "rev" => Value::Array(array.rev()), - "split" => Value::Array(array.split(args.expect("separator")?)), + "any" => array.any(vm, args.expect("function")?)?.into_value(), + "all" => array.all(vm, args.expect("function")?)?.into_value(), + "flatten" => array.flatten().into_value(), + "rev" => array.rev().into_value(), + "split" => array.split(args.expect("separator")?).into_value(), "join" => { let sep = args.eat()?; let last = args.named("last")?; array.join(sep, last).at(span)? } - "sorted" => Value::Array(array.sorted(vm, span, args.named("key")?)?), - "zip" => Value::Array(array.zip(args.expect("other")?)), - "enumerate" => Value::Array(array.enumerate()), + "sorted" => array.sorted(vm, span, args.named("key")?)?.into_value(), + "zip" => array.zip(args.expect("other")?).into_value(), + "enumerate" => array.enumerate().into_value(), _ => return missing(), }, Value::Dict(dict) => match method { - "len" => Value::Int(dict.len()), + "len" => dict.len().into_value(), "at" => dict .at(&args.expect::<Str>("key")?, args.named("default")?.as_ref()) .at(span)? .clone(), - "keys" => Value::Array(dict.keys()), - "values" => Value::Array(dict.values()), - "pairs" => Value::Array(dict.pairs()), + "keys" => dict.keys().into_value(), + "values" => dict.values().into_value(), + "pairs" => dict.pairs().into_value(), _ => return missing(), }, Value::Func(func) => match method { - "with" => Value::Func(func.with(args.take())), + "with" => func.with(args.take()).into_value(), "where" => { let fields = args.to_named(); args.items.retain(|arg| arg.name.is_none()); - Value::dynamic( - func.element() - .ok_or("`where()` can only be called on element functions") - .at(span)? - .where_(fields), - ) + func.element() + .ok_or("`where()` can only be called on element functions") + .at(span)? + .where_(fields) + .into_value() } _ => return missing(), }, Value::Args(args) => match method { - "pos" => Value::Array(args.to_pos()), - "named" => Value::Dict(args.to_named()), + "pos" => args.to_pos().into_value(), + "named" => args.to_named().into_value(), _ => return missing(), }, Value::Dyn(dynamic) => { if let Some(location) = dynamic.downcast::<Location>() { match method { - "page" => vm.vt.introspector.page(*location).into(), - "position" => vm.vt.introspector.position(*location).into(), + "page" => vm.vt.introspector.page(*location).into_value(), + "position" => vm.vt.introspector.position(*location).into_value(), "page-numbering" => vm.vt.introspector.page_numbering(*location), _ => return missing(), } } else if let Some(selector) = dynamic.downcast::<Selector>() { match method { - "or" => selector.clone().or(args.all::<Selector>()?).into(), - "and" => selector.clone().and(args.all::<Selector>()?).into(), + "or" => selector.clone().or(args.all::<Selector>()?).into_value(), + "and" => selector.clone().and(args.all::<Selector>()?).into_value(), "before" => { let location = args.expect::<Selector>("selector")?; let inclusive = args.named_or_find::<bool>("inclusive")?.unwrap_or(true); - selector.clone().before(location, inclusive).into() + selector.clone().before(location, inclusive).into_value() } "after" => { let location = args.expect::<Selector>("selector")?; let inclusive = args.named_or_find::<bool>("inclusive")?.unwrap_or(true); - selector.clone().after(location, inclusive).into() + selector.clone().after(location, inclusive).into_value() } _ => return missing(), } } else if let Some(&datetime) = dynamic.downcast::<Datetime>() { match method { - "display" => datetime.display(args.eat()?).at(args.span)?.into(), - "year" => { - datetime.year().map_or(Value::None, |y| Value::Int(y.into())) - } - "month" => { - datetime.month().map_or(Value::None, |m| Value::Int(m.into())) - } - "weekday" => { - datetime.weekday().map_or(Value::None, |w| Value::Int(w.into())) - } - "day" => datetime.day().map_or(Value::None, |d| Value::Int(d.into())), - "hour" => { - datetime.hour().map_or(Value::None, |h| Value::Int(h.into())) - } - "minute" => { - datetime.minute().map_or(Value::None, |m| Value::Int(m.into())) - } - "second" => { - datetime.second().map_or(Value::None, |s| Value::Int(s.into())) + "display" => { + datetime.display(args.eat()?).at(args.span)?.into_value() } + "year" => datetime.year().into_value(), + "month" => datetime.month().into_value(), + "weekday" => datetime.weekday().into_value(), + "day" => datetime.day().into_value(), + "hour" => datetime.hour().into_value(), + "minute" => datetime.minute().into_value(), + "second" => datetime.second().into_value(), _ => return missing(), } } else { diff --git a/src/eval/mod.rs b/src/eval/mod.rs index 2499ae22..ab75bfb4 100644 --- a/src/eval/mod.rs +++ b/src/eval/mod.rs @@ -13,31 +13,45 @@ mod str; #[macro_use] mod value; mod args; +mod auto; mod datetime; mod func; +mod int; mod methods; mod module; +mod none; pub mod ops; mod scope; mod symbol; #[doc(hidden)] -pub use once_cell::sync::Lazy; - -pub use self::args::*; -pub use self::array::*; -pub use self::cast::*; -pub use self::datetime::*; -pub use self::dict::*; -pub use self::func::*; -pub use self::library::*; -pub use self::module::*; -pub use self::scope::*; -pub use self::str::*; -pub use self::symbol::*; -pub use self::value::*; - -pub(crate) use self::methods::methods_on; +pub use { + self::library::LANG_ITEMS, + ecow::{eco_format, eco_vec}, + indexmap::IndexMap, + once_cell::sync::Lazy, +}; + +#[doc(inline)] +pub use typst_macros::{func, symbols}; + +pub use self::args::{Arg, Args}; +pub use self::array::{array, Array}; +pub use self::auto::AutoValue; +pub use self::cast::{ + cast, Cast, CastInfo, FromValue, IntoResult, IntoValue, Never, Reflect, Variadics, +}; +pub use self::datetime::Datetime; +pub use self::dict::{dict, Dict}; +pub use self::func::{Func, FuncInfo, NativeFunc, Param, ParamInfo}; +pub use self::library::{set_lang_items, LangItems, Library}; +pub use self::methods::methods_on; +pub use self::module::Module; +pub use self::none::NoneValue; +pub use self::scope::{Scope, Scopes}; +pub use self::str::{format_str, Regex, Str}; +pub use self::symbol::Symbol; +pub use self::value::{Dynamic, Type, Value}; use std::collections::HashSet; use std::mem; @@ -47,6 +61,7 @@ use comemo::{Track, Tracked, TrackedMut, Validate}; use ecow::{EcoString, EcoVec}; use unicode_segmentation::UnicodeSegmentation; +use self::func::{CapturesVisitor, Closure}; use crate::diag::{ bail, error, At, SourceError, SourceResult, StrResult, Trace, Tracepoint, }; @@ -214,8 +229,8 @@ impl<'a> Vm<'a> { /// Define a variable in the current scope. #[tracing::instrument(skip_all)] - pub fn define(&mut self, var: ast::Ident, value: impl Into<Value>) { - let value = value.into(); + pub fn define(&mut self, var: ast::Ident, value: impl IntoValue) { + let value = value.into_value(); if self.traced == Some(var.span()) { self.vt.tracer.trace(value.clone()); } @@ -934,7 +949,7 @@ impl Eval for ast::Array { } } - Ok(Array::from_vec(vec)) + Ok(vec.into()) } } @@ -965,7 +980,7 @@ impl Eval for ast::Dict { } } - Ok(Dict::from_map(map)) + Ok(map.into()) } } @@ -1285,23 +1300,23 @@ impl ast::Pattern { for p in destruct.bindings() { match p { ast::DestructuringKind::Normal(expr) => { - let Ok(v) = value.at(i, None) else { + let Ok(v) = value.at(i as i64, None) else { bail!(expr.span(), "not enough elements to destructure"); }; f(vm, expr, v.clone())?; i += 1; } ast::DestructuringKind::Sink(spread) => { - let sink_size = (1 + value.len() as usize) - .checked_sub(destruct.bindings().count()); - let sink = - sink_size.and_then(|s| value.slice(i, Some(i + s as i64)).ok()); + let sink_size = + (1 + value.len()).checked_sub(destruct.bindings().count()); + let sink = sink_size + .and_then(|s| value.slice(i as i64, Some((i + s) as i64)).ok()); if let (Some(sink_size), Some(sink)) = (sink_size, sink) { if let Some(expr) = spread.expr() { f(vm, expr, Value::Array(sink.clone()))?; } - i += sink_size as i64; + i += sink_size; } else { bail!(self.span(), "not enough elements to destructure") } @@ -1590,7 +1605,7 @@ impl Eval for ast::ForLoop { #[allow(unused_parens)] for value in $iter { - $pat.define(vm, Value::from(value))?; + $pat.define(vm, value.into_value())?; let body = self.body(); let value = body.eval(vm)?; @@ -1644,7 +1659,7 @@ impl Eval for ast::ForLoop { } /// Applies imports from `import` to the current scope. -fn apply_imports<V: Into<Value>>( +fn apply_imports<V: IntoValue>( imports: Option<ast::Imports>, vm: &mut Vm, source_value: V, diff --git a/src/eval/none.rs b/src/eval/none.rs new file mode 100644 index 00000000..ab7644a7 --- /dev/null +++ b/src/eval/none.rs @@ -0,0 +1,74 @@ +use std::fmt::{self, Debug, Formatter}; + +use super::{cast, CastInfo, FromValue, IntoValue, Reflect, Value}; +use crate::diag::StrResult; + +/// A value that indicates the absence of any other value. +#[derive(Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub struct NoneValue; + +impl Reflect for NoneValue { + fn describe() -> CastInfo { + CastInfo::Type("none") + } + + fn castable(value: &Value) -> bool { + matches!(value, Value::None) + } +} + +impl IntoValue for NoneValue { + fn into_value(self) -> Value { + Value::None + } +} + +impl FromValue for NoneValue { + fn from_value(value: Value) -> StrResult<Self> { + match value { + Value::None => Ok(Self), + _ => Err(Self::error(&value)), + } + } +} + +impl Debug for NoneValue { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.pad("none") + } +} + +cast! { + (), + self => Value::None, + _: NoneValue => (), +} + +impl<T: Reflect> Reflect for Option<T> { + fn describe() -> CastInfo { + T::describe() + NoneValue::describe() + } + + fn castable(value: &Value) -> bool { + NoneValue::castable(value) || T::castable(value) + } +} + +impl<T: IntoValue> IntoValue for Option<T> { + fn into_value(self) -> Value { + match self { + Some(v) => v.into_value(), + None => Value::None, + } + } +} + +impl<T: FromValue> FromValue for Option<T> { + fn from_value(value: Value) -> StrResult<Self> { + match value { + Value::None => Ok(None), + v if T::castable(&v) => Ok(Some(T::from_value(v)?)), + _ => Err(Self::error(&value)), + } + } +} diff --git a/src/eval/ops.rs b/src/eval/ops.rs index 3e397c30..bea2ea82 100644 --- a/src/eval/ops.rs +++ b/src/eval/ops.rs @@ -209,8 +209,8 @@ pub fn mul(lhs: Value, rhs: Value) -> StrResult<Value> { (Int(a), Str(b)) => Str(b.repeat(a)?), (Array(a), Int(b)) => Array(a.repeat(b)?), (Int(a), Array(b)) => Array(b.repeat(a)?), - (Content(a), Int(b)) => Content(a.repeat(b)?), - (Int(a), Content(b)) => Content(b.repeat(a)?), + (Content(a), b @ Int(_)) => Content(a.repeat(b.cast()?)), + (a @ Int(_), Content(b)) => Content(b.repeat(a.cast()?)), (a, b) => mismatch!("cannot multiply {} with {}", a, b), }) diff --git a/src/eval/scope.rs b/src/eval/scope.rs index 83d1703e..bc62feb1 100644 --- a/src/eval/scope.rs +++ b/src/eval/scope.rs @@ -4,7 +4,7 @@ use std::hash::Hash; use ecow::{eco_format, EcoString}; -use super::{Library, Value}; +use super::{IntoValue, Library, Value}; use crate::diag::StrResult; /// A stack of scopes. @@ -95,7 +95,7 @@ impl Scope { /// Bind a value to a name. #[track_caller] - pub fn define(&mut self, name: impl Into<EcoString>, value: impl Into<Value>) { + pub fn define(&mut self, name: impl Into<EcoString>, value: impl IntoValue) { let name = name.into(); #[cfg(debug_assertions)] @@ -103,16 +103,13 @@ impl Scope { panic!("duplicate definition: {name}"); } - self.0.insert(name, Slot::new(value.into(), Kind::Normal)); + self.0.insert(name, Slot::new(value.into_value(), Kind::Normal)); } /// Define a captured, immutable binding. - pub fn define_captured( - &mut self, - var: impl Into<EcoString>, - value: impl Into<Value>, - ) { - self.0.insert(var.into(), Slot::new(value.into(), Kind::Captured)); + pub fn define_captured(&mut self, var: impl Into<EcoString>, value: impl IntoValue) { + self.0 + .insert(var.into(), Slot::new(value.into_value(), Kind::Captured)); } /// Try to access a variable immutably. diff --git a/src/eval/str.rs b/src/eval/str.rs index 567ee5bd..48ba8311 100644 --- a/src/eval/str.rs +++ b/src/eval/str.rs @@ -6,9 +6,8 @@ use std::ops::{Add, AddAssign, Deref, Range}; use ecow::EcoString; use unicode_segmentation::UnicodeSegmentation; -use super::{cast_from_value, dict, Array, Dict, Func, Value, Vm}; +use super::{cast, dict, Args, Array, Dict, Func, IntoValue, Value, Vm}; use crate::diag::{At, SourceResult, StrResult}; -use crate::eval::Args; use crate::geom::GenAlign; /// Create a new [`Str`] from a format string. @@ -41,8 +40,8 @@ impl Str { } /// The length of the string in bytes. - pub fn len(&self) -> i64 { - self.0.len() as i64 + pub fn len(&self) -> usize { + self.0.len() } /// A string slice containing the entire string. @@ -82,7 +81,7 @@ impl Str { /// Extract a contiguous substring. pub fn slice(&self, start: i64, end: Option<i64>) -> StrResult<Self> { let start = self.locate(start)?; - let end = self.locate(end.unwrap_or(self.len()))?.max(start); + let end = self.locate(end.unwrap_or(self.len() as i64))?.max(start); Ok(self.0[start..end].into()) } @@ -283,7 +282,7 @@ impl Str { match &with { Replacement::Str(s) => output.push_str(s), Replacement::Func(func) => { - let args = Args::new(func.span(), [dict.into()]); + let args = Args::new(func.span(), [dict]); let piece = func.call_vm(vm, args)?.cast::<Str>().at(func.span())?; output.push_str(&piece); } @@ -329,7 +328,7 @@ impl Str { /// Errors on invalid char boundaries. fn locate_opt(&self, index: i64) -> StrResult<Option<usize>> { let wrapped = - if index >= 0 { Some(index) } else { self.len().checked_add(index) }; + if index >= 0 { Some(index) } else { (self.len() as i64).checked_add(index) }; let resolved = wrapped .and_then(|v| usize::try_from(v).ok()) @@ -351,13 +350,13 @@ impl Str { /// The out of bounds access error message. #[cold] -fn out_of_bounds(index: i64, len: i64) -> EcoString { +fn out_of_bounds(index: i64, len: usize) -> EcoString { eco_format!("string index out of bounds (index: {}, len: {})", index, len) } /// The out of bounds access error message when no default value was given. #[cold] -fn no_default_and_out_of_bounds(index: i64, len: i64) -> EcoString { +fn no_default_and_out_of_bounds(index: i64, len: usize) -> EcoString { eco_format!("no default value was specified and string index out of bounds (index: {}, len: {})", index, len) } @@ -376,10 +375,10 @@ fn string_is_empty() -> EcoString { /// Convert an item of std's `match_indices` to a dictionary. fn match_to_dict((start, text): (usize, &str)) -> Dict { dict! { - "start" => Value::Int(start as i64), - "end" => Value::Int((start + text.len()) as i64), - "text" => Value::Str(text.into()), - "captures" => Value::Array(Array::new()), + "start" => start, + "end" => start + text.len(), + "text" => text, + "captures" => Array::new(), } } @@ -387,15 +386,13 @@ fn match_to_dict((start, text): (usize, &str)) -> Dict { fn captures_to_dict(cap: regex::Captures) -> Dict { let m = cap.get(0).expect("missing first match"); dict! { - "start" => Value::Int(m.start() as i64), - "end" => Value::Int(m.end() as i64), - "text" => Value::Str(m.as_str().into()), - "captures" => Value::Array( - cap.iter() - .skip(1) - .map(|opt| opt.map_or(Value::None, |m| m.as_str().into())) - .collect(), - ), + "start" => m.start(), + "end" => m.end(), + "text" => m.as_str(), + "captures" => cap.iter() + .skip(1) + .map(|opt| opt.map_or(Value::None, |m| m.as_str().into_value())) + .collect::<Array>(), } } @@ -503,6 +500,35 @@ impl From<Str> for String { } } +cast! { + char, + self => Value::Str(self.into()), + string: Str => { + let mut chars = string.chars(); + match (chars.next(), chars.next()) { + (Some(c), None) => c, + _ => Err("expected exactly one character")?, + } + }, +} + +cast! { + &str, + self => Value::Str(self.into()), +} + +cast! { + EcoString, + self => Value::Str(self.into()), + v: Str => v.into(), +} + +cast! { + String, + self => Value::Str(self.into()), + v: Str => v.into(), +} + /// A regular expression. #[derive(Clone)] pub struct Regex(regex::Regex); @@ -540,8 +566,8 @@ impl Hash for Regex { } } -cast_from_value! { - Regex: "regex", +cast! { + type Regex: "regex", } /// A pattern which can be searched for in a string. @@ -553,7 +579,7 @@ pub enum StrPattern { Regex(Regex), } -cast_from_value! { +cast! { StrPattern, text: Str => Self::Str(text), regex: Regex => Self::Regex(regex), @@ -569,7 +595,7 @@ pub enum StrSide { End, } -cast_from_value! { +cast! { StrSide, align: GenAlign => match align { GenAlign::Start => Self::Start, @@ -587,7 +613,7 @@ pub enum Replacement { Func(Func), } -cast_from_value! { +cast! { Replacement, text: Str => Self::Str(text), func: Func => Self::Func(func) diff --git a/src/eval/symbol.rs b/src/eval/symbol.rs index 5c8951b1..8f4c93f8 100644 --- a/src/eval/symbol.rs +++ b/src/eval/symbol.rs @@ -7,9 +7,6 @@ use ecow::EcoString; use crate::diag::StrResult; -#[doc(inline)] -pub use typst_macros::symbols; - /// A symbol, possibly with variants. #[derive(Clone, Eq, PartialEq, Hash)] pub struct Symbol(Repr); diff --git a/src/eval/value.rs b/src/eval/value.rs index 6d947866..91fdadbe 100644 --- a/src/eval/value.rs +++ b/src/eval/value.rs @@ -8,12 +8,12 @@ use ecow::eco_format; use siphasher::sip128::{Hasher128, SipHasher13}; use super::{ - cast_to_value, format_str, ops, Args, Array, Cast, CastInfo, Content, Dict, Func, - Label, Module, Str, Symbol, + cast, format_str, ops, Args, Array, CastInfo, Content, Dict, FromValue, Func, + IntoValue, Module, Reflect, Str, Symbol, }; use crate::diag::StrResult; use crate::geom::{Abs, Angle, Color, Em, Fr, Length, Ratio, Rel}; -use crate::model::Styles; +use crate::model::{Label, Styles}; use crate::syntax::{ast, Span}; /// A computational value. @@ -79,11 +79,11 @@ impl Value { pub fn numeric(pair: (f64, ast::Unit)) -> Self { let (v, unit) = pair; match unit { - ast::Unit::Length(unit) => Abs::with_unit(v, unit).into(), - ast::Unit::Angle(unit) => Angle::with_unit(v, unit).into(), - ast::Unit::Em => Em::new(v).into(), - ast::Unit::Fr => Fr::new(v).into(), - ast::Unit::Percent => Ratio::new(v / 100.0).into(), + ast::Unit::Length(unit) => Abs::with_unit(v, unit).into_value(), + ast::Unit::Angle(unit) => Angle::with_unit(v, unit).into_value(), + ast::Unit::Em => Em::new(v).into_value(), + ast::Unit::Fr => Fr::new(v).into_value(), + ast::Unit::Percent => Ratio::new(v / 100.0).into_value(), } } @@ -116,8 +116,8 @@ impl Value { } /// Try to cast the value into a specific type. - pub fn cast<T: Cast>(self) -> StrResult<T> { - T::cast(self) + pub fn cast<T: FromValue>(self) -> StrResult<T> { + T::from_value(self) } /// Try to access a field on the value. @@ -283,8 +283,9 @@ impl PartialEq for Dynamic { } } -cast_to_value! { - v: Dynamic => Value::Dyn(v) +cast! { + Dynamic, + self => Value::Dyn(self), } trait Bounds: Debug + Sync + Send + 'static { @@ -337,20 +338,32 @@ pub trait Type { /// Implement traits for primitives. macro_rules! primitive { ( - $type:ty: $name:literal, $variant:ident + $ty:ty: $name:literal, $variant:ident $(, $other:ident$(($binding:ident))? => $out:expr)* ) => { - impl Type for $type { + impl Type for $ty { const TYPE_NAME: &'static str = $name; } - impl Cast for $type { - fn is(value: &Value) -> bool { + impl Reflect for $ty { + fn describe() -> CastInfo { + CastInfo::Type(Self::TYPE_NAME) + } + + fn castable(value: &Value) -> bool { matches!(value, Value::$variant(_) $(| primitive!(@$other $(($binding))?))*) } + } - fn cast(value: Value) -> StrResult<Self> { + impl IntoValue for $ty { + fn into_value(self) -> Value { + Value::$variant(self) + } + } + + impl FromValue for $ty { + fn from_value(value: Value) -> StrResult<Self> { match value { Value::$variant(v) => Ok(v), $(Value::$other$(($binding))? => Ok($out),)* @@ -361,16 +374,6 @@ macro_rules! primitive { )), } } - - fn describe() -> CastInfo { - CastInfo::Type(Self::TYPE_NAME) - } - } - - impl From<$type> for Value { - fn from(v: $type) -> Self { - Value::$variant(v) - } } }; @@ -408,8 +411,8 @@ primitive! { Styles: "styles", Styles } primitive! { Array: "array", Array } primitive! { Dict: "dictionary", Dict } primitive! { Func: "function", Func } -primitive! { Module: "module", Module } primitive! { Args: "arguments", Args } +primitive! { Module: "module", Module } #[cfg(test)] mod tests { @@ -418,8 +421,8 @@ mod tests { use crate::geom::RgbaColor; #[track_caller] - fn test(value: impl Into<Value>, exp: &str) { - assert_eq!(format!("{:?}", value.into()), exp); + fn test(value: impl IntoValue, exp: &str) { + assert_eq!(format!("{:?}", value.into_value()), exp); } #[test] diff --git a/src/font/mod.rs b/src/font/mod.rs index bfb790bb..dbd7dd60 100644 --- a/src/font/mod.rs +++ b/src/font/mod.rs @@ -3,8 +3,8 @@ mod book; mod variant; -pub use self::book::*; -pub use self::variant::*; +pub use self::book::{FontBook, FontFlags, FontInfo}; +pub use self::variant::{FontStretch, FontStyle, FontVariant, FontWeight}; use std::fmt::{self, Debug, Formatter}; use std::hash::{Hash, Hasher}; @@ -12,6 +12,7 @@ use std::sync::Arc; use ttf_parser::GlyphId; +use self::book::find_name; use crate::eval::Cast; use crate::geom::Em; use crate::util::Buffer; diff --git a/src/font/variant.rs b/src/font/variant.rs index 9391aae1..d4508a59 100644 --- a/src/font/variant.rs +++ b/src/font/variant.rs @@ -2,7 +2,7 @@ use std::fmt::{self, Debug, Formatter}; use serde::{Deserialize, Serialize}; -use crate::eval::{cast_from_value, cast_to_value, Cast, Value}; +use crate::eval::{cast, Cast, IntoValue}; use crate::geom::Ratio; /// Properties that distinguish a font from other fonts in the same family. @@ -130,8 +130,20 @@ impl Debug for FontWeight { } } -cast_from_value! { +cast! { FontWeight, + self => IntoValue::into_value(match self { + FontWeight::THIN => "thin", + FontWeight::EXTRALIGHT => "extralight", + FontWeight::LIGHT => "light", + FontWeight::REGULAR => "regular", + FontWeight::MEDIUM => "medium", + FontWeight::SEMIBOLD => "semibold", + FontWeight::BOLD => "bold", + FontWeight::EXTRABOLD => "extrabold", + FontWeight::BLACK => "black", + _ => return self.to_number().into_value(), + }), v: i64 => Self::from_number(v.clamp(0, u16::MAX as i64) as u16), /// Thin weight (100). "thin" => Self::THIN, @@ -153,21 +165,6 @@ cast_from_value! { "black" => Self::BLACK, } -cast_to_value! { - v: FontWeight => Value::from(match v { - FontWeight::THIN => "thin", - FontWeight::EXTRALIGHT => "extralight", - FontWeight::LIGHT => "light", - FontWeight::REGULAR => "regular", - FontWeight::MEDIUM => "medium", - FontWeight::SEMIBOLD => "semibold", - FontWeight::BOLD => "bold", - FontWeight::EXTRABOLD => "extrabold", - FontWeight::BLACK => "black", - _ => return v.to_number().into(), - }) -} - /// The width of a font. #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] #[derive(Serialize, Deserialize)] @@ -247,15 +244,12 @@ impl Debug for FontStretch { } } -cast_from_value! { +cast! { FontStretch, + self => self.to_ratio().into_value(), v: Ratio => Self::from_ratio(v), } -cast_to_value! { - v: FontStretch => v.to_ratio().into() -} - #[cfg(test)] mod tests { use super::*; diff --git a/src/geom/abs.rs b/src/geom/abs.rs index 34c3d010..4ca3a9a1 100644 --- a/src/geom/abs.rs +++ b/src/geom/abs.rs @@ -214,8 +214,9 @@ impl<'a> Sum<&'a Self> for Abs { } } -cast_to_value! { - v: Abs => Value::Length(v.into()) +cast! { + Abs, + self => Value::Length(self.into()), } /// Different units of absolute measurement. diff --git a/src/geom/align.rs b/src/geom/align.rs index 42fc493e..dca35891 100644 --- a/src/geom/align.rs +++ b/src/geom/align.rs @@ -128,16 +128,27 @@ impl Debug for GenAlign { } } -cast_from_value! { - GenAlign: "alignment", +cast! { + type GenAlign: "alignment", } -cast_from_value! { - Axes<GenAlign>: "2d alignment", +cast! { + type Axes<GenAlign>: "2d alignment", } -cast_from_value! { +cast! { + Axes<Align>, + self => self.map(GenAlign::from).into_value(), +} + +cast! { Axes<Option<GenAlign>>, + self => match (self.x, self.y) { + (Some(x), Some(y)) => Axes::new(x, y).into_value(), + (Some(x), None) => x.into_value(), + (None, Some(y)) => y.into_value(), + (None, None) => Value::None, + }, align: GenAlign => { let mut aligns = Axes::default(); aligns.set(align.axis(), Some(align)); @@ -146,19 +157,6 @@ cast_from_value! { aligns: Axes<GenAlign> => aligns.map(Some), } -cast_to_value! { - v: Axes<Align> => v.map(GenAlign::from).into() -} - -cast_to_value! { - v: Axes<Option<GenAlign>> => match (v.x, v.y) { - (Some(x), Some(y)) => Axes::new(x, y).into(), - (Some(x), None) => x.into(), - (None, Some(y)) => y.into(), - (None, None) => Value::None, - } -} - impl From<Axes<GenAlign>> for Axes<Option<GenAlign>> { fn from(axes: Axes<GenAlign>) -> Self { axes.map(Some) @@ -213,8 +211,9 @@ impl Fold for Align { #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] pub struct HorizontalAlign(pub GenAlign); -cast_from_value! { +cast! { HorizontalAlign, + self => self.0.into_value(), align: GenAlign => { if align.axis() != Axis::X { Err("alignment must be horizontal")?; @@ -223,17 +222,14 @@ cast_from_value! { }, } -cast_to_value! { - v: HorizontalAlign => v.0.into() -} - /// Utility struct to restrict a passed alignment value to the vertical axis on /// cast. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] pub struct VerticalAlign(pub GenAlign); -cast_from_value! { +cast! { VerticalAlign, + self => self.0.into_value(), align: GenAlign => { if align.axis() != Axis::Y { Err("alignment must be vertical")?; @@ -241,26 +237,3 @@ cast_from_value! { Self(align) }, } - -cast_to_value! { - v: VerticalAlign => v.0.into() -} - -#[derive(Debug, Copy, Clone, Eq, PartialEq)] -pub enum LeftRightAlternator { - Left, - Right, -} - -impl Iterator for LeftRightAlternator { - type Item = LeftRightAlternator; - - fn next(&mut self) -> Option<Self::Item> { - let r = Some(*self); - match self { - Self::Left => *self = Self::Right, - Self::Right => *self = Self::Left, - } - r - } -} diff --git a/src/geom/axes.rs b/src/geom/axes.rs index 511e6ff5..35c94129 100644 --- a/src/geom/axes.rs +++ b/src/geom/axes.rs @@ -2,7 +2,6 @@ use std::any::Any; use std::ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign, Not}; use super::*; -use crate::eval::Array; /// A container with a horizontal and vertical component. #[derive(Default, Copy, Clone, Eq, PartialEq, Hash)] @@ -274,8 +273,9 @@ impl BitAndAssign for Axes<bool> { } } -cast_from_value! { +cast! { Axes<Rel<Length>>, + self => array![self.x, self.y].into_value(), array: Array => { let mut iter = array.into_iter(); match (iter.next(), iter.next(), iter.next()) { @@ -285,10 +285,6 @@ cast_from_value! { }, } -cast_to_value! { - v: Axes<Rel<Length>> => Value::Array(array![v.x, v.y]) -} - impl<T: Resolve> Resolve for Axes<T> { type Output = Axes<T::Output>; diff --git a/src/geom/color.rs b/src/geom/color.rs new file mode 100644 index 00000000..c7676c2b --- /dev/null +++ b/src/geom/color.rs @@ -0,0 +1,386 @@ +use std::str::FromStr; + +use super::*; + +/// A color in a dynamic format. +#[derive(Copy, Clone, Eq, PartialEq, Hash)] +pub enum Color { + /// An 8-bit luma color. + Luma(LumaColor), + /// An 8-bit RGBA color. + Rgba(RgbaColor), + /// An 8-bit CMYK color. + Cmyk(CmykColor), +} + +impl Color { + pub const BLACK: Self = Self::Rgba(RgbaColor::new(0x00, 0x00, 0x00, 0xFF)); + pub const GRAY: Self = Self::Rgba(RgbaColor::new(0xAA, 0xAA, 0xAA, 0xFF)); + pub const SILVER: Self = Self::Rgba(RgbaColor::new(0xDD, 0xDD, 0xDD, 0xFF)); + pub const WHITE: Self = Self::Rgba(RgbaColor::new(0xFF, 0xFF, 0xFF, 0xFF)); + pub const NAVY: Self = Self::Rgba(RgbaColor::new(0x00, 0x1f, 0x3f, 0xFF)); + pub const BLUE: Self = Self::Rgba(RgbaColor::new(0x00, 0x74, 0xD9, 0xFF)); + pub const AQUA: Self = Self::Rgba(RgbaColor::new(0x7F, 0xDB, 0xFF, 0xFF)); + pub const TEAL: Self = Self::Rgba(RgbaColor::new(0x39, 0xCC, 0xCC, 0xFF)); + pub const EASTERN: Self = Self::Rgba(RgbaColor::new(0x23, 0x9D, 0xAD, 0xFF)); + pub const PURPLE: Self = Self::Rgba(RgbaColor::new(0xB1, 0x0D, 0xC9, 0xFF)); + pub const FUCHSIA: Self = Self::Rgba(RgbaColor::new(0xF0, 0x12, 0xBE, 0xFF)); + pub const MAROON: Self = Self::Rgba(RgbaColor::new(0x85, 0x14, 0x4b, 0xFF)); + pub const RED: Self = Self::Rgba(RgbaColor::new(0xFF, 0x41, 0x36, 0xFF)); + pub const ORANGE: Self = Self::Rgba(RgbaColor::new(0xFF, 0x85, 0x1B, 0xFF)); + pub const YELLOW: Self = Self::Rgba(RgbaColor::new(0xFF, 0xDC, 0x00, 0xFF)); + pub const OLIVE: Self = Self::Rgba(RgbaColor::new(0x3D, 0x99, 0x70, 0xFF)); + pub const GREEN: Self = Self::Rgba(RgbaColor::new(0x2E, 0xCC, 0x40, 0xFF)); + pub const LIME: Self = Self::Rgba(RgbaColor::new(0x01, 0xFF, 0x70, 0xFF)); + + /// Convert this color to RGBA. + pub fn to_rgba(self) -> RgbaColor { + match self { + Self::Luma(luma) => luma.to_rgba(), + Self::Rgba(rgba) => rgba, + Self::Cmyk(cmyk) => cmyk.to_rgba(), + } + } + + /// Lighten this color by the given factor. + pub fn lighten(self, factor: Ratio) -> Self { + match self { + Self::Luma(luma) => Self::Luma(luma.lighten(factor)), + Self::Rgba(rgba) => Self::Rgba(rgba.lighten(factor)), + Self::Cmyk(cmyk) => Self::Cmyk(cmyk.lighten(factor)), + } + } + + /// Darken this color by the given factor. + pub fn darken(self, factor: Ratio) -> Self { + match self { + Self::Luma(luma) => Self::Luma(luma.darken(factor)), + Self::Rgba(rgba) => Self::Rgba(rgba.darken(factor)), + Self::Cmyk(cmyk) => Self::Cmyk(cmyk.darken(factor)), + } + } + + /// Negate this color. + pub fn negate(self) -> Self { + match self { + Self::Luma(luma) => Self::Luma(luma.negate()), + Self::Rgba(rgba) => Self::Rgba(rgba.negate()), + Self::Cmyk(cmyk) => Self::Cmyk(cmyk.negate()), + } + } +} + +impl Debug for Color { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match self { + Self::Luma(c) => Debug::fmt(c, f), + Self::Rgba(c) => Debug::fmt(c, f), + Self::Cmyk(c) => Debug::fmt(c, f), + } + } +} + +/// An 8-bit grayscale color. +#[derive(Copy, Clone, Eq, PartialEq, Hash)] +pub struct LumaColor(pub u8); + +impl LumaColor { + /// Construct a new luma color. + pub const fn new(luma: u8) -> Self { + Self(luma) + } + + /// Convert to an opque RGBA color. + pub const fn to_rgba(self) -> RgbaColor { + RgbaColor::new(self.0, self.0, self.0, u8::MAX) + } + + /// Convert to CMYK as a fraction of true black. + pub fn to_cmyk(self) -> CmykColor { + CmykColor::new( + round_u8(self.0 as f64 * 0.75), + round_u8(self.0 as f64 * 0.68), + round_u8(self.0 as f64 * 0.67), + round_u8(self.0 as f64 * 0.90), + ) + } + + /// Lighten this color by a factor. + pub fn lighten(self, factor: Ratio) -> Self { + let inc = round_u8((u8::MAX - self.0) as f64 * factor.get()); + Self(self.0.saturating_add(inc)) + } + + /// Darken this color by a factor. + pub fn darken(self, factor: Ratio) -> Self { + let dec = round_u8(self.0 as f64 * factor.get()); + Self(self.0.saturating_sub(dec)) + } + + /// Negate this color. + pub fn negate(self) -> Self { + Self(u8::MAX - self.0) + } +} + +impl Debug for LumaColor { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + write!(f, "luma({})", self.0) + } +} + +impl From<LumaColor> for Color { + fn from(luma: LumaColor) -> Self { + Self::Luma(luma) + } +} + +/// An 8-bit RGBA color. +#[derive(Copy, Clone, Eq, PartialEq, Hash)] +pub struct RgbaColor { + /// Red channel. + pub r: u8, + /// Green channel. + pub g: u8, + /// Blue channel. + pub b: u8, + /// Alpha channel. + pub a: u8, +} + +impl RgbaColor { + /// Construct a new RGBA color. + pub const fn new(r: u8, g: u8, b: u8, a: u8) -> Self { + Self { r, g, b, a } + } + + /// Lighten this color by a factor. + /// + /// The alpha channel is not affected. + pub fn lighten(self, factor: Ratio) -> Self { + let lighten = + |c: u8| c.saturating_add(round_u8((u8::MAX - c) as f64 * factor.get())); + Self { + r: lighten(self.r), + g: lighten(self.g), + b: lighten(self.b), + a: self.a, + } + } + + /// Darken this color by a factor. + /// + /// The alpha channel is not affected. + pub fn darken(self, factor: Ratio) -> Self { + let darken = |c: u8| c.saturating_sub(round_u8(c as f64 * factor.get())); + Self { + r: darken(self.r), + g: darken(self.g), + b: darken(self.b), + a: self.a, + } + } + + /// Negate this color. + /// + /// The alpha channel is not affected. + pub fn negate(self) -> Self { + Self { + r: u8::MAX - self.r, + g: u8::MAX - self.g, + b: u8::MAX - self.b, + a: self.a, + } + } +} + +impl FromStr for RgbaColor { + type Err = &'static str; + + /// Constructs a new color from hex strings like the following: + /// - `#aef` (shorthand, with leading hashtag), + /// - `7a03c2` (without alpha), + /// - `abcdefff` (with alpha). + /// + /// The hashtag is optional and both lower and upper case are fine. + fn from_str(hex_str: &str) -> Result<Self, Self::Err> { + let hex_str = hex_str.strip_prefix('#').unwrap_or(hex_str); + if hex_str.chars().any(|c| !c.is_ascii_hexdigit()) { + return Err("color string contains non-hexadecimal letters"); + } + + let len = hex_str.len(); + let long = len == 6 || len == 8; + let short = len == 3 || len == 4; + let alpha = len == 4 || len == 8; + if !long && !short { + return Err("color string has wrong length"); + } + + let mut values: [u8; 4] = [u8::MAX; 4]; + for elem in if alpha { 0..4 } else { 0..3 } { + let item_len = if long { 2 } else { 1 }; + let pos = elem * item_len; + + let item = &hex_str[pos..(pos + item_len)]; + values[elem] = u8::from_str_radix(item, 16).unwrap(); + + if short { + // Duplicate number for shorthand notation, i.e. `a` -> `aa` + values[elem] += values[elem] * 16; + } + } + + Ok(Self::new(values[0], values[1], values[2], values[3])) + } +} + +impl Debug for RgbaColor { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + if f.alternate() { + write!(f, "rgba({}, {}, {}, {})", self.r, self.g, self.b, self.a,)?; + } else { + write!(f, "rgb(\"#{:02x}{:02x}{:02x}", self.r, self.g, self.b)?; + if self.a != 255 { + write!(f, "{:02x}", self.a)?; + } + write!(f, "\")")?; + } + Ok(()) + } +} + +impl<T: Into<RgbaColor>> From<T> for Color { + fn from(rgba: T) -> Self { + Self::Rgba(rgba.into()) + } +} + +cast! { + RgbaColor, + self => Value::Color(self.into()), +} + +/// An 8-bit CMYK color. +#[derive(Copy, Clone, Eq, PartialEq, Hash)] +pub struct CmykColor { + /// The cyan component. + pub c: u8, + /// The magenta component. + pub m: u8, + /// The yellow component. + pub y: u8, + /// The key (black) component. + pub k: u8, +} + +impl CmykColor { + /// Construct a new CMYK color. + pub const fn new(c: u8, m: u8, y: u8, k: u8) -> Self { + Self { c, m, y, k } + } + + /// Convert this color to RGBA. + pub fn to_rgba(self) -> RgbaColor { + let k = self.k as f64 / 255.0; + let f = |c| { + let c = c as f64 / 255.0; + round_u8(255.0 * (1.0 - c) * (1.0 - k)) + }; + + RgbaColor { r: f(self.c), g: f(self.m), b: f(self.y), a: 255 } + } + + /// Lighten this color by a factor. + pub fn lighten(self, factor: Ratio) -> Self { + let lighten = |c: u8| c.saturating_sub(round_u8(c as f64 * factor.get())); + Self { + c: lighten(self.c), + m: lighten(self.m), + y: lighten(self.y), + k: lighten(self.k), + } + } + + /// Darken this color by a factor. + pub fn darken(self, factor: Ratio) -> Self { + let darken = + |c: u8| c.saturating_add(round_u8((u8::MAX - c) as f64 * factor.get())); + Self { + c: darken(self.c), + m: darken(self.m), + y: darken(self.y), + k: darken(self.k), + } + } + + /// Negate this color. + /// + /// Does not affect the key component. + pub fn negate(self) -> Self { + Self { + c: u8::MAX - self.c, + m: u8::MAX - self.m, + y: u8::MAX - self.y, + k: self.k, + } + } +} + +impl Debug for CmykColor { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + let g = |c| 100.0 * (c as f64 / 255.0); + write!( + f, + "cmyk({:.1}%, {:.1}%, {:.1}%, {:.1}%)", + g(self.c), + g(self.m), + g(self.y), + g(self.k), + ) + } +} + +impl From<CmykColor> for Color { + fn from(cmyk: CmykColor) -> Self { + Self::Cmyk(cmyk) + } +} + +/// Convert to the closest u8. +fn round_u8(value: f64) -> u8 { + value.round() as u8 +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_color_strings() { + #[track_caller] + fn test(hex: &str, r: u8, g: u8, b: u8, a: u8) { + assert_eq!(RgbaColor::from_str(hex), Ok(RgbaColor::new(r, g, b, a))); + } + + test("f61243ff", 0xf6, 0x12, 0x43, 0xff); + test("b3d8b3", 0xb3, 0xd8, 0xb3, 0xff); + test("fCd2a9AD", 0xfc, 0xd2, 0xa9, 0xad); + test("233", 0x22, 0x33, 0x33, 0xff); + test("111b", 0x11, 0x11, 0x11, 0xbb); + } + + #[test] + fn test_parse_invalid_colors() { + #[track_caller] + fn test(hex: &str, message: &str) { + assert_eq!(RgbaColor::from_str(hex), Err(message)); + } + + test("a5", "color string has wrong length"); + test("12345", "color string has wrong length"); + test("f075ff011", "color string has wrong length"); + test("hmmm", "color string contains non-hexadecimal letters"); + test("14B2AH", "color string contains non-hexadecimal letters"); + } +} diff --git a/src/geom/corners.rs b/src/geom/corners.rs index 20e9bed0..5ee1e063 100644 --- a/src/geom/corners.rs +++ b/src/geom/corners.rs @@ -1,3 +1,5 @@ +use crate::eval::{CastInfo, FromValue, IntoValue, Reflect}; + use super::*; /// A container with components for the four corners of a rectangle. @@ -108,15 +110,47 @@ pub enum Corner { BottomLeft, } -impl<T> Cast for Corners<Option<T>> +impl<T: Reflect> Reflect for Corners<Option<T>> { + fn describe() -> CastInfo { + T::describe() + Dict::describe() + } + + fn castable(value: &Value) -> bool { + Dict::castable(value) || T::castable(value) + } +} + +impl<T> IntoValue for Corners<T> where - T: Cast + Clone, + T: PartialEq + IntoValue, { - fn is(value: &Value) -> bool { - matches!(value, Value::Dict(_)) || T::is(value) + fn into_value(self) -> Value { + if self.is_uniform() { + return self.top_left.into_value(); + } + + let mut dict = Dict::new(); + let mut handle = |key: &str, component: T| { + let value = component.into_value(); + if value != Value::None { + dict.insert(key.into(), value); + } + }; + + handle("top-left", self.top_left); + handle("top-right", self.top_right); + handle("bottom-right", self.bottom_right); + handle("bottom-left", self.bottom_left); + + Value::Dict(dict) } +} - fn cast(mut value: Value) -> StrResult<Self> { +impl<T> FromValue for Corners<Option<T>> +where + T: FromValue + Clone, +{ + fn from_value(mut value: Value) -> StrResult<Self> { let keys = [ "top-left", "top-right", @@ -131,7 +165,7 @@ where if let Value::Dict(dict) = &mut value { if dict.iter().any(|(key, _)| keys.contains(&key.as_str())) { - let mut take = |key| dict.take(key).ok().map(T::cast).transpose(); + let mut take = |key| dict.take(key).ok().map(T::from_value).transpose(); let rest = take("rest")?; let left = take("left")?.or_else(|| rest.clone()); let top = take("top")?.or_else(|| rest.clone()); @@ -157,16 +191,12 @@ where } } - if T::is(&value) { - Ok(Self::splat(Some(T::cast(value)?))) + if T::castable(&value) { + Ok(Self::splat(Some(T::from_value(value)?))) } else { - <Self as Cast>::error(value) + Err(Self::error(&value)) } } - - fn describe() -> CastInfo { - T::describe() + CastInfo::Type("dictionary") - } } impl<T: Resolve> Resolve for Corners<T> { @@ -187,29 +217,3 @@ impl<T: Fold> Fold for Corners<Option<T>> { }) } } - -impl<T> From<Corners<T>> for Value -where - T: PartialEq + Into<Value>, -{ - fn from(corners: Corners<T>) -> Self { - if corners.is_uniform() { - return corners.top_left.into(); - } - - let mut dict = Dict::new(); - let mut handle = |key: &str, component: T| { - let value = component.into(); - if value != Value::None { - dict.insert(key.into(), value); - } - }; - - handle("top-left", corners.top_left); - handle("top-right", corners.top_right); - handle("bottom-right", corners.bottom_right); - handle("bottom-left", corners.bottom_left); - - Value::Dict(dict) - } -} diff --git a/src/geom/dir.rs b/src/geom/dir.rs index bc4d66e1..48915471 100644 --- a/src/geom/dir.rs +++ b/src/geom/dir.rs @@ -74,6 +74,6 @@ impl Debug for Dir { } } -cast_from_value! { - Dir: "direction", +cast! { + type Dir: "direction", } diff --git a/src/geom/em.rs b/src/geom/em.rs index 2c63c81d..8dda9ff6 100644 --- a/src/geom/em.rs +++ b/src/geom/em.rs @@ -135,8 +135,9 @@ impl Sum for Em { } } -cast_to_value! { - v: Em => Value::Length(v.into()) +cast! { + Em, + self => Value::Length(self.into()), } impl Resolve for Em { diff --git a/src/geom/length.rs b/src/geom/length.rs index 9d6552da..7d0a9841 100644 --- a/src/geom/length.rs +++ b/src/geom/length.rs @@ -1,6 +1,6 @@ use super::*; -/// A length, possibly expressed with contextual units. +/// A size or distance, possibly expressed with contextual units. /// /// Currently supports absolute and font-relative units, but support could quite /// easily be extended to other units. diff --git a/src/geom/mod.rs b/src/geom/mod.rs index 8896c24c..4a9ecfe1 100644 --- a/src/geom/mod.rs +++ b/src/geom/mod.rs @@ -6,6 +6,7 @@ mod abs; mod align; mod angle; mod axes; +mod color; mod corners; mod dir; mod ellipse; @@ -26,29 +27,32 @@ mod smart; mod stroke; mod transform; -pub use self::abs::*; -pub use self::align::*; -pub use self::angle::*; -pub use self::axes::*; -pub use self::corners::*; -pub use self::dir::*; -pub use self::ellipse::*; -pub use self::em::*; -pub use self::fr::*; -pub use self::length::*; -pub use self::paint::*; -pub use self::path::*; -pub use self::point::*; -pub use self::ratio::*; -pub use self::rel::*; -pub use self::rounded::*; -pub use self::scalar::*; -pub use self::shape::*; -pub use self::sides::*; -pub use self::size::*; -pub use self::smart::*; -pub use self::stroke::*; -pub use self::transform::*; +pub use self::abs::{Abs, AbsUnit}; +pub use self::align::{Align, GenAlign, HorizontalAlign, VerticalAlign}; +pub use self::angle::{Angle, AngleUnit}; +pub use self::axes::{Axes, Axis}; +pub use self::color::{CmykColor, Color, LumaColor, RgbaColor}; +pub use self::corners::{Corner, Corners}; +pub use self::dir::Dir; +pub use self::ellipse::ellipse; +pub use self::em::Em; +pub use self::fr::Fr; +pub use self::length::Length; +pub use self::paint::Paint; +pub use self::path::{Path, PathItem}; +pub use self::point::Point; +pub use self::ratio::Ratio; +pub use self::rel::Rel; +pub use self::rounded::rounded_rect; +pub use self::scalar::Scalar; +pub use self::shape::{Geometry, Shape}; +pub use self::sides::{Side, Sides}; +pub use self::size::Size; +pub use self::smart::Smart; +pub use self::stroke::{ + DashLength, DashPattern, LineCap, LineJoin, PartialStroke, Stroke, +}; +pub use self::transform::Transform; use std::cmp::Ordering; use std::f64::consts::PI; @@ -58,7 +62,7 @@ use std::iter::Sum; use std::ops::*; use crate::diag::StrResult; -use crate::eval::{array, cast_from_value, cast_to_value, Cast, CastInfo, Dict, Value}; +use crate::eval::{array, cast, Array, Dict, Value}; use crate::model::{Fold, Resolve, StyleChain}; /// Generic access to a structure's components. diff --git a/src/geom/paint.rs b/src/geom/paint.rs index e9bd3a2e..10fa9fde 100644 --- a/src/geom/paint.rs +++ b/src/geom/paint.rs @@ -1,5 +1,3 @@ -use std::str::FromStr; - use super::*; /// How a fill or stroke should be painted. @@ -23,393 +21,10 @@ impl Debug for Paint { } } -cast_from_value! { +cast! { Paint, + self => match self { + Self::Solid(color) => Value::Color(color), + }, color: Color => Self::Solid(color), } - -cast_to_value! { - Paint::Solid(color): Paint => Value::Color(color) -} - -/// A color in a dynamic format. -#[derive(Copy, Clone, Eq, PartialEq, Hash)] -pub enum Color { - /// An 8-bit luma color. - Luma(LumaColor), - /// An 8-bit RGBA color. - Rgba(RgbaColor), - /// An 8-bit CMYK color. - Cmyk(CmykColor), -} - -impl Color { - pub const BLACK: Self = Self::Rgba(RgbaColor::new(0x00, 0x00, 0x00, 0xFF)); - pub const GRAY: Self = Self::Rgba(RgbaColor::new(0xAA, 0xAA, 0xAA, 0xFF)); - pub const SILVER: Self = Self::Rgba(RgbaColor::new(0xDD, 0xDD, 0xDD, 0xFF)); - pub const WHITE: Self = Self::Rgba(RgbaColor::new(0xFF, 0xFF, 0xFF, 0xFF)); - pub const NAVY: Self = Self::Rgba(RgbaColor::new(0x00, 0x1f, 0x3f, 0xFF)); - pub const BLUE: Self = Self::Rgba(RgbaColor::new(0x00, 0x74, 0xD9, 0xFF)); - pub const AQUA: Self = Self::Rgba(RgbaColor::new(0x7F, 0xDB, 0xFF, 0xFF)); - pub const TEAL: Self = Self::Rgba(RgbaColor::new(0x39, 0xCC, 0xCC, 0xFF)); - pub const EASTERN: Self = Self::Rgba(RgbaColor::new(0x23, 0x9D, 0xAD, 0xFF)); - pub const PURPLE: Self = Self::Rgba(RgbaColor::new(0xB1, 0x0D, 0xC9, 0xFF)); - pub const FUCHSIA: Self = Self::Rgba(RgbaColor::new(0xF0, 0x12, 0xBE, 0xFF)); - pub const MAROON: Self = Self::Rgba(RgbaColor::new(0x85, 0x14, 0x4b, 0xFF)); - pub const RED: Self = Self::Rgba(RgbaColor::new(0xFF, 0x41, 0x36, 0xFF)); - pub const ORANGE: Self = Self::Rgba(RgbaColor::new(0xFF, 0x85, 0x1B, 0xFF)); - pub const YELLOW: Self = Self::Rgba(RgbaColor::new(0xFF, 0xDC, 0x00, 0xFF)); - pub const OLIVE: Self = Self::Rgba(RgbaColor::new(0x3D, 0x99, 0x70, 0xFF)); - pub const GREEN: Self = Self::Rgba(RgbaColor::new(0x2E, 0xCC, 0x40, 0xFF)); - pub const LIME: Self = Self::Rgba(RgbaColor::new(0x01, 0xFF, 0x70, 0xFF)); - - /// Convert this color to RGBA. - pub fn to_rgba(self) -> RgbaColor { - match self { - Self::Luma(luma) => luma.to_rgba(), - Self::Rgba(rgba) => rgba, - Self::Cmyk(cmyk) => cmyk.to_rgba(), - } - } - - /// Lighten this color by the given factor. - pub fn lighten(self, factor: Ratio) -> Self { - match self { - Self::Luma(luma) => Self::Luma(luma.lighten(factor)), - Self::Rgba(rgba) => Self::Rgba(rgba.lighten(factor)), - Self::Cmyk(cmyk) => Self::Cmyk(cmyk.lighten(factor)), - } - } - - /// Darken this color by the given factor. - pub fn darken(self, factor: Ratio) -> Self { - match self { - Self::Luma(luma) => Self::Luma(luma.darken(factor)), - Self::Rgba(rgba) => Self::Rgba(rgba.darken(factor)), - Self::Cmyk(cmyk) => Self::Cmyk(cmyk.darken(factor)), - } - } - - /// Negate this color. - pub fn negate(self) -> Self { - match self { - Self::Luma(luma) => Self::Luma(luma.negate()), - Self::Rgba(rgba) => Self::Rgba(rgba.negate()), - Self::Cmyk(cmyk) => Self::Cmyk(cmyk.negate()), - } - } -} - -impl Debug for Color { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - match self { - Self::Luma(c) => Debug::fmt(c, f), - Self::Rgba(c) => Debug::fmt(c, f), - Self::Cmyk(c) => Debug::fmt(c, f), - } - } -} - -/// An 8-bit grayscale color. -#[derive(Copy, Clone, Eq, PartialEq, Hash)] -pub struct LumaColor(pub u8); - -impl LumaColor { - /// Construct a new luma color. - pub const fn new(luma: u8) -> Self { - Self(luma) - } - - /// Convert to an opque RGBA color. - pub const fn to_rgba(self) -> RgbaColor { - RgbaColor::new(self.0, self.0, self.0, u8::MAX) - } - - /// Convert to CMYK as a fraction of true black. - pub fn to_cmyk(self) -> CmykColor { - CmykColor::new( - round_u8(self.0 as f64 * 0.75), - round_u8(self.0 as f64 * 0.68), - round_u8(self.0 as f64 * 0.67), - round_u8(self.0 as f64 * 0.90), - ) - } - - /// Lighten this color by a factor. - pub fn lighten(self, factor: Ratio) -> Self { - let inc = round_u8((u8::MAX - self.0) as f64 * factor.get()); - Self(self.0.saturating_add(inc)) - } - - /// Darken this color by a factor. - pub fn darken(self, factor: Ratio) -> Self { - let dec = round_u8(self.0 as f64 * factor.get()); - Self(self.0.saturating_sub(dec)) - } - - /// Negate this color. - pub fn negate(self) -> Self { - Self(u8::MAX - self.0) - } -} - -impl Debug for LumaColor { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - write!(f, "luma({})", self.0) - } -} - -impl From<LumaColor> for Color { - fn from(luma: LumaColor) -> Self { - Self::Luma(luma) - } -} - -/// An 8-bit RGBA color. -#[derive(Copy, Clone, Eq, PartialEq, Hash)] -pub struct RgbaColor { - /// Red channel. - pub r: u8, - /// Green channel. - pub g: u8, - /// Blue channel. - pub b: u8, - /// Alpha channel. - pub a: u8, -} - -impl RgbaColor { - /// Construct a new RGBA color. - pub const fn new(r: u8, g: u8, b: u8, a: u8) -> Self { - Self { r, g, b, a } - } - - /// Lighten this color by a factor. - /// - /// The alpha channel is not affected. - pub fn lighten(self, factor: Ratio) -> Self { - let lighten = - |c: u8| c.saturating_add(round_u8((u8::MAX - c) as f64 * factor.get())); - Self { - r: lighten(self.r), - g: lighten(self.g), - b: lighten(self.b), - a: self.a, - } - } - - /// Darken this color by a factor. - /// - /// The alpha channel is not affected. - pub fn darken(self, factor: Ratio) -> Self { - let darken = |c: u8| c.saturating_sub(round_u8(c as f64 * factor.get())); - Self { - r: darken(self.r), - g: darken(self.g), - b: darken(self.b), - a: self.a, - } - } - - /// Negate this color. - /// - /// The alpha channel is not affected. - pub fn negate(self) -> Self { - Self { - r: u8::MAX - self.r, - g: u8::MAX - self.g, - b: u8::MAX - self.b, - a: self.a, - } - } -} - -impl FromStr for RgbaColor { - type Err = &'static str; - - /// Constructs a new color from hex strings like the following: - /// - `#aef` (shorthand, with leading hashtag), - /// - `7a03c2` (without alpha), - /// - `abcdefff` (with alpha). - /// - /// The hashtag is optional and both lower and upper case are fine. - fn from_str(hex_str: &str) -> Result<Self, Self::Err> { - let hex_str = hex_str.strip_prefix('#').unwrap_or(hex_str); - if hex_str.chars().any(|c| !c.is_ascii_hexdigit()) { - return Err("color string contains non-hexadecimal letters"); - } - - let len = hex_str.len(); - let long = len == 6 || len == 8; - let short = len == 3 || len == 4; - let alpha = len == 4 || len == 8; - if !long && !short { - return Err("color string has wrong length"); - } - - let mut values: [u8; 4] = [u8::MAX; 4]; - for elem in if alpha { 0..4 } else { 0..3 } { - let item_len = if long { 2 } else { 1 }; - let pos = elem * item_len; - - let item = &hex_str[pos..(pos + item_len)]; - values[elem] = u8::from_str_radix(item, 16).unwrap(); - - if short { - // Duplicate number for shorthand notation, i.e. `a` -> `aa` - values[elem] += values[elem] * 16; - } - } - - Ok(Self::new(values[0], values[1], values[2], values[3])) - } -} - -impl Debug for RgbaColor { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - if f.alternate() { - write!(f, "rgba({}, {}, {}, {})", self.r, self.g, self.b, self.a,)?; - } else { - write!(f, "rgb(\"#{:02x}{:02x}{:02x}", self.r, self.g, self.b)?; - if self.a != 255 { - write!(f, "{:02x}", self.a)?; - } - write!(f, "\")")?; - } - Ok(()) - } -} - -impl<T: Into<RgbaColor>> From<T> for Color { - fn from(rgba: T) -> Self { - Self::Rgba(rgba.into()) - } -} - -cast_to_value! { - v: RgbaColor => Value::Color(v.into()) -} - -/// An 8-bit CMYK color. -#[derive(Copy, Clone, Eq, PartialEq, Hash)] -pub struct CmykColor { - /// The cyan component. - pub c: u8, - /// The magenta component. - pub m: u8, - /// The yellow component. - pub y: u8, - /// The key (black) component. - pub k: u8, -} - -impl CmykColor { - /// Construct a new CMYK color. - pub const fn new(c: u8, m: u8, y: u8, k: u8) -> Self { - Self { c, m, y, k } - } - - /// Convert this color to RGBA. - pub fn to_rgba(self) -> RgbaColor { - let k = self.k as f64 / 255.0; - let f = |c| { - let c = c as f64 / 255.0; - round_u8(255.0 * (1.0 - c) * (1.0 - k)) - }; - - RgbaColor { r: f(self.c), g: f(self.m), b: f(self.y), a: 255 } - } - - /// Lighten this color by a factor. - pub fn lighten(self, factor: Ratio) -> Self { - let lighten = |c: u8| c.saturating_sub(round_u8(c as f64 * factor.get())); - Self { - c: lighten(self.c), - m: lighten(self.m), - y: lighten(self.y), - k: lighten(self.k), - } - } - - /// Darken this color by a factor. - pub fn darken(self, factor: Ratio) -> Self { - let darken = - |c: u8| c.saturating_add(round_u8((u8::MAX - c) as f64 * factor.get())); - Self { - c: darken(self.c), - m: darken(self.m), - y: darken(self.y), - k: darken(self.k), - } - } - - /// Negate this color. - /// - /// Does not affect the key component. - pub fn negate(self) -> Self { - Self { - c: u8::MAX - self.c, - m: u8::MAX - self.m, - y: u8::MAX - self.y, - k: self.k, - } - } -} - -impl Debug for CmykColor { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - let g = |c| 100.0 * (c as f64 / 255.0); - write!( - f, - "cmyk({:.1}%, {:.1}%, {:.1}%, {:.1}%)", - g(self.c), - g(self.m), - g(self.y), - g(self.k), - ) - } -} - -impl From<CmykColor> for Color { - fn from(cmyk: CmykColor) -> Self { - Self::Cmyk(cmyk) - } -} - -/// Convert to the closest u8. -fn round_u8(value: f64) -> u8 { - value.round() as u8 -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_parse_color_strings() { - #[track_caller] - fn test(hex: &str, r: u8, g: u8, b: u8, a: u8) { - assert_eq!(RgbaColor::from_str(hex), Ok(RgbaColor::new(r, g, b, a))); - } - - test("f61243ff", 0xf6, 0x12, 0x43, 0xff); - test("b3d8b3", 0xb3, 0xd8, 0xb3, 0xff); - test("fCd2a9AD", 0xfc, 0xd2, 0xa9, 0xad); - test("233", 0x22, 0x33, 0x33, 0xff); - test("111b", 0x11, 0x11, 0x11, 0xbb); - } - - #[test] - fn test_parse_invalid_colors() { - #[track_caller] - fn test(hex: &str, message: &str) { - assert_eq!(RgbaColor::from_str(hex), Err(message)); - } - - test("a5", "color string has wrong length"); - test("12345", "color string has wrong length"); - test("f075ff011", "color string has wrong length"); - test("hmmm", "color string contains non-hexadecimal letters"); - test("14B2AH", "color string contains non-hexadecimal letters"); - } -} diff --git a/src/geom/rel.rs b/src/geom/rel.rs index cf1e73ef..88972222 100644 --- a/src/geom/rel.rs +++ b/src/geom/rel.rs @@ -240,6 +240,7 @@ impl Fold for Rel<Length> { } } -cast_to_value! { - v: Rel<Abs> => v.map(Length::from).into() +cast! { + Rel<Abs>, + self => self.map(Length::from).into_value(), } diff --git a/src/geom/sides.rs b/src/geom/sides.rs index a905a5f8..d4b72a9d 100644 --- a/src/geom/sides.rs +++ b/src/geom/sides.rs @@ -1,3 +1,5 @@ +use crate::eval::{CastInfo, FromValue, IntoValue, Reflect}; + use super::*; /// A container with left, top, right and bottom components. @@ -178,19 +180,51 @@ impl Side { } } -impl<T> Cast for Sides<Option<T>> +impl<T: Reflect> Reflect for Sides<Option<T>> { + fn describe() -> CastInfo { + T::describe() + Dict::describe() + } + + fn castable(value: &Value) -> bool { + Dict::castable(value) || T::castable(value) + } +} + +impl<T> IntoValue for Sides<T> where - T: Default + Cast + Clone, + T: PartialEq + IntoValue, { - fn is(value: &Value) -> bool { - matches!(value, Value::Dict(_)) || T::is(value) + fn into_value(self) -> Value { + if self.is_uniform() { + return self.left.into_value(); + } + + let mut dict = Dict::new(); + let mut handle = |key: &str, component: T| { + let value = component.into_value(); + if value != Value::None { + dict.insert(key.into(), value); + } + }; + + handle("left", self.left); + handle("top", self.top); + handle("right", self.right); + handle("bottom", self.bottom); + + Value::Dict(dict) } +} - fn cast(mut value: Value) -> StrResult<Self> { +impl<T> FromValue for Sides<Option<T>> +where + T: Default + FromValue + Clone, +{ + fn from_value(mut value: Value) -> StrResult<Self> { let keys = ["left", "top", "right", "bottom", "x", "y", "rest"]; if let Value::Dict(dict) = &mut value { if dict.iter().any(|(key, _)| keys.contains(&key.as_str())) { - let mut take = |key| dict.take(key).ok().map(T::cast).transpose(); + let mut take = |key| dict.take(key).ok().map(T::from_value).transpose(); let rest = take("rest")?; let x = take("x")?.or_else(|| rest.clone()); let y = take("y")?.or_else(|| rest.clone()); @@ -206,43 +240,14 @@ where } } - if T::is(&value) { - Ok(Self::splat(Some(T::cast(value)?))) + if T::castable(&value) { + Ok(Self::splat(Some(T::from_value(value)?))) } else { - <Self as Cast>::error(value) + Err(Self::error(&value)) } } - - fn describe() -> CastInfo { - T::describe() + CastInfo::Type("dictionary") - } } -impl<T> From<Sides<T>> for Value -where - T: PartialEq + Into<Value>, -{ - fn from(sides: Sides<T>) -> Self { - if sides.is_uniform() { - return sides.left.into(); - } - - let mut dict = Dict::new(); - let mut handle = |key: &str, component: T| { - let value = component.into(); - if value != Value::None { - dict.insert(key.into(), value); - } - }; - - handle("left", sides.left); - handle("top", sides.top); - handle("right", sides.right); - handle("bottom", sides.bottom); - - Value::Dict(dict) - } -} impl<T: Resolve> Resolve for Sides<T> { type Output = Sides<T::Output>; diff --git a/src/geom/smart.rs b/src/geom/smart.rs index d9f8fd16..a6271c20 100644 --- a/src/geom/smart.rs +++ b/src/geom/smart.rs @@ -1,3 +1,5 @@ +use crate::eval::{AutoValue, CastInfo, FromValue, IntoValue, Reflect}; + use super::*; /// A value that can be automatically determined. @@ -94,21 +96,32 @@ impl<T> Default for Smart<T> { } } -impl<T: Cast> Cast for Smart<T> { - fn is(value: &Value) -> bool { - matches!(value, Value::Auto) || T::is(value) +impl<T: Reflect> Reflect for Smart<T> { + fn castable(value: &Value) -> bool { + AutoValue::castable(value) || T::castable(value) } - fn cast(value: Value) -> StrResult<Self> { - match value { - Value::Auto => Ok(Self::Auto), - v if T::is(&v) => Ok(Self::Custom(T::cast(v)?)), - _ => <Self as Cast>::error(value), + fn describe() -> CastInfo { + T::describe() + AutoValue::describe() + } +} + +impl<T: IntoValue> IntoValue for Smart<T> { + fn into_value(self) -> Value { + match self { + Smart::Custom(v) => v.into_value(), + Smart::Auto => Value::Auto, } } +} - fn describe() -> CastInfo { - T::describe() + CastInfo::Type("auto") +impl<T: FromValue> FromValue for Smart<T> { + fn from_value(value: Value) -> StrResult<Self> { + match value { + Value::Auto => Ok(Self::Auto), + v if T::castable(&v) => Ok(Self::Custom(T::from_value(v)?)), + _ => Err(Self::error(&value)), + } } } @@ -131,12 +144,3 @@ where self.map(|inner| inner.fold(outer.unwrap_or_default())) } } - -impl<T: Into<Value>> From<Smart<T>> for Value { - fn from(v: Smart<T>) -> Self { - match v { - Smart::Custom(v) => v.into(), - Smart::Auto => Value::Auto, - } - } -} diff --git a/src/geom/stroke.rs b/src/geom/stroke.rs index 6539922c..66264d5d 100644 --- a/src/geom/stroke.rs +++ b/src/geom/stroke.rs @@ -1,3 +1,5 @@ +use crate::eval::{Cast, FromValue}; + use super::*; /// A stroke of a geometric shape. @@ -169,8 +171,78 @@ impl<T: Debug> Debug for PartialStroke<T> { } } +impl Resolve for PartialStroke { + type Output = PartialStroke<Abs>; + + fn resolve(self, styles: StyleChain) -> Self::Output { + PartialStroke { + paint: self.paint, + thickness: self.thickness.resolve(styles), + line_cap: self.line_cap, + line_join: self.line_join, + dash_pattern: self.dash_pattern.resolve(styles), + miter_limit: self.miter_limit, + } + } +} + +impl Fold for PartialStroke<Abs> { + type Output = Self; + + fn fold(self, outer: Self::Output) -> Self::Output { + Self { + paint: self.paint.or(outer.paint), + thickness: self.thickness.or(outer.thickness), + line_cap: self.line_cap.or(outer.line_cap), + line_join: self.line_join.or(outer.line_join), + dash_pattern: self.dash_pattern.or(outer.dash_pattern), + miter_limit: self.miter_limit.or(outer.miter_limit), + } + } +} + +cast! { + type PartialStroke: "stroke", + thickness: Length => Self { + thickness: Smart::Custom(thickness), + ..Default::default() + }, + color: Color => Self { + paint: Smart::Custom(color.into()), + ..Default::default() + }, + mut dict: Dict => { + fn take<T: FromValue>(dict: &mut Dict, key: &str) -> StrResult<Smart<T>> { + Ok(dict.take(key).ok().map(T::from_value) + .transpose()?.map(Smart::Custom).unwrap_or(Smart::Auto)) + } + + let paint = take::<Paint>(&mut dict, "paint")?; + let thickness = take::<Length>(&mut dict, "thickness")?; + let line_cap = take::<LineCap>(&mut dict, "cap")?; + let line_join = take::<LineJoin>(&mut dict, "join")?; + let dash_pattern = take::<Option<DashPattern>>(&mut dict, "dash")?; + let miter_limit = take::<f64>(&mut dict, "miter-limit")?; + dict.finish(&["paint", "thickness", "cap", "join", "dash", "miter-limit"])?; + + Self { + paint, + thickness, + line_cap, + line_join, + dash_pattern, + miter_limit: miter_limit.map(Scalar), + } + }, +} + +cast! { + PartialStroke<Abs>, + self => self.map(Length::from).into_value(), +} + /// The line cap of a stroke -#[derive(Cast, Clone, Eq, PartialEq, Hash)] +#[derive(Copy, Clone, Eq, PartialEq, Hash, Cast)] pub enum LineCap { Butt, Round, @@ -188,7 +260,7 @@ impl Debug for LineCap { } /// The line join of a stroke -#[derive(Cast, Clone, Eq, PartialEq, Hash)] +#[derive(Copy, Clone, Eq, PartialEq, Hash, Cast)] pub enum LineJoin { Miter, Round, @@ -235,49 +307,22 @@ impl<T: Default> From<Vec<DashLength<T>>> for DashPattern<T> { } } -/// The length of a dash in a line dash pattern -#[derive(Debug, Clone, Eq, PartialEq, Hash)] -pub enum DashLength<T = Length> { - LineWidth, - Length(T), -} - -impl From<Abs> for DashLength { - fn from(l: Abs) -> Self { - DashLength::Length(l.into()) - } -} - -impl<T> DashLength<T> { - fn finish(self, line_width: T) -> T { - match self { - Self::LineWidth => line_width, - Self::Length(l) => l, - } - } -} - -cast_from_value! { - DashLength: "dash length", - "dot" => Self::LineWidth, - l: Length => Self::Length(l), -} - -impl Resolve for DashLength { - type Output = DashLength<Abs>; +impl Resolve for DashPattern { + type Output = DashPattern<Abs>; fn resolve(self, styles: StyleChain) -> Self::Output { - match self { - Self::LineWidth => DashLength::LineWidth, - Self::Length(l) => DashLength::Length(l.resolve(styles)), + DashPattern { + array: self.array.into_iter().map(|l| l.resolve(styles)).collect(), + phase: self.phase.resolve(styles), } } } -cast_from_value! { - DashPattern: "dash pattern", - // Use same names as tikz: - // https://tex.stackexchange.com/questions/45275/tikz-get-values-for-predefined-dash-patterns +// Same names as tikz: +// https://tex.stackexchange.com/questions/45275/tikz-get-values-for-predefined-dash-patterns +cast! { + DashPattern, + "solid" => Vec::new().into(), "dotted" => vec![DashLength::LineWidth, Abs::pt(2.0).into()].into(), "densely-dotted" => vec![DashLength::LineWidth, Abs::pt(1.0).into()].into(), @@ -288,19 +333,13 @@ cast_from_value! { "dash-dotted" => vec![Abs::pt(3.0).into(), Abs::pt(2.0).into(), DashLength::LineWidth, Abs::pt(2.0).into()].into(), "densely-dash-dotted" => vec![Abs::pt(3.0).into(), Abs::pt(1.0).into(), DashLength::LineWidth, Abs::pt(1.0).into()].into(), "loosely-dash-dotted" => vec![Abs::pt(3.0).into(), Abs::pt(4.0).into(), DashLength::LineWidth, Abs::pt(4.0).into()].into(), - array: Vec<DashLength> => { - Self { - array, - phase: Length::zero(), - } - }, + + array: Vec<DashLength> => Self { array, phase: Length::zero() }, mut dict: Dict => { let array: Vec<DashLength> = dict.take("array")?.cast()?; - let phase = dict.take("phase").ok().map(Length::cast) + let phase = dict.take("phase").ok().map(Value::cast) .transpose()?.unwrap_or(Length::zero()); - dict.finish(&["array", "phase"])?; - Self { array, phase, @@ -308,82 +347,41 @@ cast_from_value! { }, } -impl Resolve for DashPattern { - type Output = DashPattern<Abs>; - - fn resolve(self, styles: StyleChain) -> Self::Output { - DashPattern { - array: self.array.into_iter().map(|l| l.resolve(styles)).collect(), - phase: self.phase.resolve(styles), - } - } +/// The length of a dash in a line dash pattern +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub enum DashLength<T = Length> { + LineWidth, + Length(T), } -cast_from_value! { - PartialStroke: "stroke", - thickness: Length => Self { - thickness: Smart::Custom(thickness), - ..Default::default() - }, - color: Color => Self { - paint: Smart::Custom(color.into()), - ..Default::default() - }, - mut dict: Dict => { - fn take<T: Cast<Value>>(dict: &mut Dict, key: &str) -> StrResult<Smart<T>> { - Ok(dict.take(key).ok().map(T::cast) - .transpose()?.map(Smart::Custom).unwrap_or(Smart::Auto)) - } - - let paint = take::<Paint>(&mut dict, "paint")?; - let thickness = take::<Length>(&mut dict, "thickness")?; - let line_cap = take::<LineCap>(&mut dict, "cap")?; - let line_join = take::<LineJoin>(&mut dict, "join")?; - let dash_pattern = take::<Option<DashPattern>>(&mut dict, "dash")?; - let miter_limit = take::<f64>(&mut dict, "miter-limit")?; - dict.finish(&["paint", "thickness", "cap", "join", "dash", "miter-limit"])?; - - Self { - paint, - thickness, - line_cap, - line_join, - dash_pattern, - miter_limit: miter_limit.map(Scalar), - } - }, +impl From<Abs> for DashLength { + fn from(l: Abs) -> Self { + DashLength::Length(l.into()) + } } -impl Resolve for PartialStroke { - type Output = PartialStroke<Abs>; - - fn resolve(self, styles: StyleChain) -> Self::Output { - PartialStroke { - paint: self.paint, - thickness: self.thickness.resolve(styles), - line_cap: self.line_cap, - line_join: self.line_join, - dash_pattern: self.dash_pattern.resolve(styles), - miter_limit: self.miter_limit, +impl<T> DashLength<T> { + fn finish(self, line_width: T) -> T { + match self { + Self::LineWidth => line_width, + Self::Length(l) => l, } } } -impl Fold for PartialStroke<Abs> { - type Output = Self; +impl Resolve for DashLength { + type Output = DashLength<Abs>; - fn fold(self, outer: Self::Output) -> Self::Output { - Self { - paint: self.paint.or(outer.paint), - thickness: self.thickness.or(outer.thickness), - line_cap: self.line_cap.or(outer.line_cap), - line_join: self.line_join.or(outer.line_join), - dash_pattern: self.dash_pattern.or(outer.dash_pattern), - miter_limit: self.miter_limit.or(outer.miter_limit), + fn resolve(self, styles: StyleChain) -> Self::Output { + match self { + Self::LineWidth => DashLength::LineWidth, + Self::Length(v) => DashLength::Length(v.resolve(styles)), } } } -cast_to_value! { - v: PartialStroke<Abs> => v.map(Length::from).into() +cast! { + DashLength, + "dot" => Self::LineWidth, + v: Length => Self::Length(v), } diff --git a/src/ide/mod.rs b/src/ide/mod.rs index 38bede0b..4b08b66b 100644 --- a/src/ide/mod.rs +++ b/src/ide/mod.rs @@ -7,10 +7,10 @@ mod jump; mod tooltip; pub use self::analyze::analyze_labels; -pub use self::complete::*; -pub use self::highlight::*; -pub use self::jump::*; -pub use self::tooltip::*; +pub use self::complete::{autocomplete, Completion, CompletionKind}; +pub use self::highlight::{highlight, highlight_html, Tag}; +pub use self::jump::{jump_from_click, jump_from_cursor, Jump}; +pub use self::tooltip::{tooltip, Tooltip}; use std::fmt::Write; diff --git a/src/model/content.rs b/src/model/content.rs index f262d027..015f8b76 100644 --- a/src/model/content.rs +++ b/src/model/content.rs @@ -8,11 +8,11 @@ use ecow::{eco_format, EcoString, EcoVec}; use super::{ element, Behave, Behaviour, ElemFunc, Element, Fold, Guard, Label, Locatable, - Location, PlainText, Recipe, Selector, Style, Styles, Synthesize, + Location, Recipe, Selector, Style, Styles, Synthesize, }; use crate::diag::{SourceResult, StrResult}; use crate::doc::Meta; -use crate::eval::{Cast, Dict, Str, Value, Vm}; +use crate::eval::{Dict, FromValue, IntoValue, Str, Value, Vm}; use crate::syntax::Span; use crate::util::pretty_array_like; @@ -153,23 +153,24 @@ impl Content { pub fn with_field( mut self, name: impl Into<EcoString>, - value: impl Into<Value>, + value: impl IntoValue, ) -> Self { self.push_field(name, value); self } /// Attach a field to the content. - pub fn push_field(&mut self, name: impl Into<EcoString>, value: impl Into<Value>) { + pub fn push_field(&mut self, name: impl Into<EcoString>, value: impl IntoValue) { let name = name.into(); if let Some(i) = self.attrs.iter().position(|attr| match attr { Attr::Field(field) => *field == name, _ => false, }) { - self.attrs.make_mut()[i + 1] = Attr::Value(Prehashed::new(value.into())); + self.attrs.make_mut()[i + 1] = + Attr::Value(Prehashed::new(value.into_value())); } else { self.attrs.push(Attr::Field(name)); - self.attrs.push(Attr::Value(Prehashed::new(value.into()))); + self.attrs.push(Attr::Value(Prehashed::new(value.into_value()))); } } @@ -226,7 +227,7 @@ impl Content { } /// Try to access a field on the content as a specified type. - pub fn cast_field<T: Cast>(&self, name: &str) -> Option<T> { + pub fn cast_field<T: FromValue>(&self, name: &str) -> Option<T> { match self.field(name) { Some(value) => value.cast().ok(), None => None, @@ -235,7 +236,7 @@ impl Content { /// Expect a field on the content to exist as a specified type. #[track_caller] - pub fn expect_field<T: Cast>(&self, name: &str) -> T { + pub fn expect_field<T: FromValue>(&self, name: &str) -> T { self.field(name).unwrap().cast().unwrap() } @@ -311,12 +312,9 @@ impl Content { } } - /// Repeat this content `n` times. - pub fn repeat(&self, n: i64) -> StrResult<Self> { - let count = usize::try_from(n) - .map_err(|_| format!("cannot repeat this content {} times", n))?; - - Ok(Self::sequence(vec![self.clone(); count])) + /// Repeat this content `count` times. + pub fn repeat(&self, count: usize) -> Self { + Self::sequence(vec![self.clone(); count]) } /// Disable a show rule recipe. @@ -599,6 +597,12 @@ impl Fold for Vec<Meta> { } } +/// Tries to extract the plain-text representation of the element. +pub trait PlainText { + /// Write this element's plain text into the given buffer. + fn plain_text(&self, text: &mut EcoString); +} + /// The missing key access error message when no default value was given. #[cold] fn missing_field_no_default(key: &str) -> EcoString { diff --git a/src/model/element.rs b/src/model/element.rs index e26848b1..c673ee41 100644 --- a/src/model/element.rs +++ b/src/model/element.rs @@ -2,14 +2,11 @@ use std::any::TypeId; use std::fmt::{self, Debug, Formatter}; use std::hash::{Hash, Hasher}; -use ecow::EcoString; use once_cell::sync::Lazy; use super::{Content, Selector, Styles}; use crate::diag::SourceResult; -use crate::eval::{ - cast_from_value, cast_to_value, Args, Dict, Func, FuncInfo, Value, Vm, -}; +use crate::eval::{cast, Args, Dict, Func, FuncInfo, Value, Vm}; /// A document element. pub trait Element: Construct + Set + Sized + 'static { @@ -110,15 +107,12 @@ impl Hash for ElemFunc { } } -cast_from_value! { +cast! { ElemFunc, + self => Value::Func(self.into()), v: Func => v.element().ok_or("expected element function")?, } -cast_to_value! { - v: ElemFunc => Value::Func(v.into()) -} - impl From<&'static NativeElemFunc> for ElemFunc { fn from(native: &'static NativeElemFunc) -> Self { Self(native) @@ -138,22 +132,3 @@ pub struct NativeElemFunc { /// Details about the function. pub info: Lazy<FuncInfo>, } - -/// A label for an element. -#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] -pub struct Label(pub EcoString); - -impl Debug for Label { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - write!(f, "<{}>", self.0) - } -} - -/// Indicates that an element cannot be labelled. -pub trait Unlabellable {} - -/// Tries to extract the plain-text representation of the element. -pub trait PlainText { - /// Write this element's plain text into the given buffer. - fn plain_text(&self, text: &mut EcoString); -} diff --git a/src/model/introspect.rs b/src/model/introspect.rs index 87a17a8e..f00f89f5 100644 --- a/src/model/introspect.rs +++ b/src/model/introspect.rs @@ -11,12 +11,12 @@ use indexmap::IndexMap; use super::{Content, Selector}; use crate::diag::StrResult; use crate::doc::{Frame, FrameItem, Meta, Position}; -use crate::eval::{cast_from_value, Value}; +use crate::eval::{cast, Value}; use crate::geom::{Point, Transform}; use crate::model::Label; use crate::util::NonZeroExt; -/// Uniquely identifies an element in the document across multiple layout passes. +/// Identifies the location of an element in the document. /// /// This struct is created by [`Locator::locate`]. #[derive(Copy, Clone, Eq, PartialEq, Hash)] @@ -46,8 +46,8 @@ impl Debug for Location { } } -cast_from_value! { - Location: "location", +cast! { + type Location: "location", } /// Provides locations for elements in the document. diff --git a/src/model/label.rs b/src/model/label.rs new file mode 100644 index 00000000..ef8f3edd --- /dev/null +++ b/src/model/label.rs @@ -0,0 +1,16 @@ +use std::fmt::{self, Debug, Formatter}; + +use ecow::EcoString; + +/// A label for an element. +#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub struct Label(pub EcoString); + +impl Debug for Label { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + write!(f, "<{}>", self.0) + } +} + +/// Indicates that an element cannot be labelled. +pub trait Unlabellable {} diff --git a/src/model/mod.rs b/src/model/mod.rs index e541cd01..632b691f 100644 --- a/src/model/mod.rs +++ b/src/model/mod.rs @@ -3,16 +3,26 @@ mod content; mod element; mod introspect; +mod label; mod realize; +mod selector; mod styles; +#[doc(inline)] pub use typst_macros::element; -pub use self::content::*; -pub use self::element::*; -pub use self::introspect::*; -pub use self::realize::*; -pub use self::styles::*; +pub use self::content::{Content, MetaElem, PlainText}; +pub use self::element::{Construct, ElemFunc, Element, NativeElemFunc, Set}; +pub use self::introspect::{Introspector, Location, Locator}; +pub use self::label::{Label, Unlabellable}; +pub use self::realize::{ + applicable, realize, Behave, Behaviour, Finalize, Guard, Locatable, Show, Synthesize, +}; +pub use self::selector::{LocatableSelector, Selector, ShowableSelector}; +pub use self::styles::{ + Fold, Property, Recipe, Resolve, Style, StyleChain, StyleVec, StyleVecBuilder, + Styles, Transform, +}; use std::mem::ManuallyDrop; diff --git a/src/model/selector.rs b/src/model/selector.rs new file mode 100644 index 00000000..c1528c0a --- /dev/null +++ b/src/model/selector.rs @@ -0,0 +1,291 @@ +use std::any::{Any, TypeId}; +use std::fmt::{self, Debug, Formatter, Write}; +use std::sync::Arc; + +use ecow::{eco_format, EcoString, EcoVec}; + +use super::{Content, ElemFunc, Label, Location}; +use crate::diag::StrResult; +use crate::eval::{ + cast, CastInfo, Dict, FromValue, Func, IntoValue, Reflect, Regex, Value, +}; +use crate::model::Locatable; +use crate::util::pretty_array_like; + +/// A selector in a show rule. +#[derive(Clone, PartialEq, Hash)] +pub enum Selector { + /// Matches a specific type of element. + /// + /// If there is a dictionary, only elements with the fields from the + /// dictionary match. + Elem(ElemFunc, Option<Dict>), + /// Matches the element at the specified location. + Location(Location), + /// Matches elements with a specific label. + Label(Label), + /// Matches text elements through a regular expression. + Regex(Regex), + /// Matches elements with a specific capability. + Can(TypeId), + /// Matches if any of the subselectors match. + Or(EcoVec<Self>), + /// Matches if all of the subselectors match. + And(EcoVec<Self>), + /// Matches all matches of `selector` before `end`. + Before { selector: Arc<Self>, end: Arc<Self>, inclusive: bool }, + /// Matches all matches of `selector` after `start`. + After { selector: Arc<Self>, start: Arc<Self>, inclusive: bool }, +} + +impl Selector { + /// Define a simple text selector. + pub fn text(text: &str) -> Self { + Self::Regex(Regex::new(®ex::escape(text)).unwrap()) + } + + /// Define a simple [`Selector::Can`] selector. + pub fn can<T: ?Sized + Any>() -> Self { + Self::Can(TypeId::of::<T>()) + } + + /// Transforms this selector and an iterator of other selectors into a + /// [`Selector::Or`] selector. + pub fn and(self, others: impl IntoIterator<Item = Self>) -> Self { + Self::And(others.into_iter().chain(Some(self)).collect()) + } + + /// Transforms this selector and an iterator of other selectors into a + /// [`Selector::And`] selector. + pub fn or(self, others: impl IntoIterator<Item = Self>) -> Self { + Self::Or(others.into_iter().chain(Some(self)).collect()) + } + + /// Transforms this selector into a [`Selector::Before`] selector. + pub fn before(self, location: impl Into<Self>, inclusive: bool) -> Self { + Self::Before { + selector: Arc::new(self), + end: Arc::new(location.into()), + inclusive, + } + } + + /// Transforms this selector into a [`Selector::After`] selector. + pub fn after(self, location: impl Into<Self>, inclusive: bool) -> Self { + Self::After { + selector: Arc::new(self), + start: Arc::new(location.into()), + inclusive, + } + } + + /// Whether the selector matches for the target. + pub fn matches(&self, target: &Content) -> bool { + match self { + Self::Elem(element, dict) => { + target.func() == *element + && dict + .iter() + .flat_map(|dict| dict.iter()) + .all(|(name, value)| target.field_ref(name) == Some(value)) + } + Self::Label(label) => target.label() == Some(label), + Self::Regex(regex) => { + target.func() == item!(text_func) + && item!(text_str)(target).map_or(false, |text| regex.is_match(&text)) + } + Self::Can(cap) => target.can_type_id(*cap), + Self::Or(selectors) => selectors.iter().any(move |sel| sel.matches(target)), + Self::And(selectors) => selectors.iter().all(move |sel| sel.matches(target)), + Self::Location(location) => target.location() == Some(*location), + // Not supported here. + Self::Before { .. } | Self::After { .. } => false, + } + } +} + +impl From<Location> for Selector { + fn from(value: Location) -> Self { + Self::Location(value) + } +} + +impl Debug for Selector { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match self { + Self::Elem(elem, dict) => { + f.write_str(elem.name())?; + if let Some(dict) = dict { + f.write_str(".where")?; + dict.fmt(f)?; + } + Ok(()) + } + Self::Label(label) => label.fmt(f), + Self::Regex(regex) => regex.fmt(f), + Self::Can(cap) => cap.fmt(f), + Self::Or(selectors) | Self::And(selectors) => { + f.write_str(if matches!(self, Self::Or(_)) { "or" } else { "and" })?; + let pieces: Vec<_> = + selectors.iter().map(|sel| eco_format!("{sel:?}")).collect(); + f.write_str(&pretty_array_like(&pieces, false)) + } + Self::Location(loc) => loc.fmt(f), + Self::Before { selector, end: split, inclusive } + | Self::After { selector, start: split, inclusive } => { + selector.fmt(f)?; + + if matches!(self, Self::Before { .. }) { + f.write_str(".before(")?; + } else { + f.write_str(".after(")?; + } + + split.fmt(f)?; + if !*inclusive { + f.write_str(", inclusive: false")?; + } + f.write_char(')') + } + } + } +} + +cast! { + type Selector: "selector", + func: Func => func + .element() + .ok_or("only element functions can be used as selectors")? + .select(), + label: Label => Self::Label(label), + text: EcoString => Self::text(&text), + regex: Regex => Self::Regex(regex), + location: Location => Self::Location(location), +} + +/// A selector that can be used with `query`. +/// +/// Hopefully, this is made obsolete by a more powerful query mechanism in the +/// future. +#[derive(Clone, PartialEq, Hash)] +pub struct LocatableSelector(pub Selector); + +impl Reflect for LocatableSelector { + fn describe() -> CastInfo { + CastInfo::Union(vec![ + CastInfo::Type("function"), + CastInfo::Type("label"), + CastInfo::Type("selector"), + ]) + } + + fn castable(value: &Value) -> bool { + matches!(value.type_name(), "function" | "label" | "selector") + } +} + +impl IntoValue for LocatableSelector { + fn into_value(self) -> Value { + self.0.into_value() + } +} + +impl FromValue for LocatableSelector { + fn from_value(value: Value) -> StrResult<Self> { + fn validate(selector: &Selector) -> StrResult<()> { + match selector { + Selector::Elem(elem, _) => { + if !elem.can::<dyn Locatable>() { + Err(eco_format!("{} is not locatable", elem.name()))? + } + } + Selector::Location(_) => {} + Selector::Label(_) => {} + Selector::Regex(_) => Err("text is not locatable")?, + Selector::Can(_) => Err("capability is not locatable")?, + Selector::Or(list) | Selector::And(list) => { + for selector in list { + validate(selector)?; + } + } + Selector::Before { selector, end: split, .. } + | Selector::After { selector, start: split, .. } => { + for selector in [selector, split] { + validate(selector)?; + } + } + } + Ok(()) + } + + if !Self::castable(&value) { + return Err(Self::error(&value)); + } + + let selector = Selector::from_value(value)?; + validate(&selector)?; + Ok(Self(selector)) + } +} + +/// A selector that can be used with show rules. +/// +/// Hopefully, this is made obsolete by a more powerful showing mechanism in the +/// future. +#[derive(Clone, PartialEq, Hash)] +pub struct ShowableSelector(pub Selector); + +impl Reflect for ShowableSelector { + fn describe() -> CastInfo { + CastInfo::Union(vec![ + CastInfo::Type("function"), + CastInfo::Type("label"), + CastInfo::Type("string"), + CastInfo::Type("regex"), + CastInfo::Type("symbol"), + CastInfo::Type("selector"), + ]) + } + + fn castable(value: &Value) -> bool { + matches!( + value.type_name(), + "symbol" | "string" | "label" | "function" | "regex" | "selector" + ) + } +} + +impl IntoValue for ShowableSelector { + fn into_value(self) -> Value { + self.0.into_value() + } +} + +impl FromValue for ShowableSelector { + fn from_value(value: Value) -> StrResult<Self> { + fn validate(selector: &Selector) -> StrResult<()> { + match selector { + Selector::Elem(_, _) => {} + Selector::Label(_) => {} + Selector::Regex(_) => {} + Selector::Or(_) + | Selector::And(_) + | Selector::Location(_) + | Selector::Can(_) + | Selector::Before { .. } + | Selector::After { .. } => { + Err("this selector cannot be used with show")? + } + } + Ok(()) + } + + if !Self::castable(&value) { + return Err(Self::error(&value)); + } + + let selector = Selector::from_value(value)?; + validate(&selector)?; + Ok(Self(selector)) + } +} diff --git a/src/model/styles.rs b/src/model/styles.rs index e30a8a92..5b6430c2 100644 --- a/src/model/styles.rs +++ b/src/model/styles.rs @@ -1,19 +1,15 @@ -use std::any::{Any, TypeId}; use std::fmt::{self, Debug, Formatter, Write}; use std::iter; use std::mem; use std::ptr; -use std::sync::Arc; use comemo::Prehashed; -use ecow::{eco_format, eco_vec, EcoString, EcoVec}; +use ecow::{eco_vec, EcoString, EcoVec}; -use super::{Content, ElemFunc, Element, Label, Location, Vt}; -use crate::diag::{SourceResult, StrResult, Trace, Tracepoint}; -use crate::eval::{cast_from_value, Args, Cast, CastInfo, Dict, Func, Regex, Value, Vm}; -use crate::model::Locatable; +use super::{Content, ElemFunc, Element, Selector, Vt}; +use crate::diag::{SourceResult, Trace, Tracepoint}; +use crate::eval::{cast, Args, FromValue, Func, IntoValue, Value, Vm}; use crate::syntax::Span; -use crate::util::pretty_array_like; /// A list of style properties. #[derive(Default, PartialEq, Clone, Hash)] @@ -158,8 +154,17 @@ pub struct Property { impl Property { /// Create a new property from a key-value pair. - pub fn new(element: ElemFunc, name: EcoString, value: Value) -> Self { - Self { element, name, value, span: None } + pub fn new( + element: ElemFunc, + name: impl Into<EcoString>, + value: impl IntoValue, + ) -> Self { + Self { + element, + name: name.into(), + value: value.into_value(), + span: None, + } } /// Whether this property is the given one. @@ -254,282 +259,6 @@ impl Debug for Recipe { } } -/// A selector in a show rule. -#[derive(Clone, PartialEq, Hash)] -pub enum Selector { - /// Matches a specific type of element. - /// - /// If there is a dictionary, only elements with the fields from the - /// dictionary match. - Elem(ElemFunc, Option<Dict>), - /// Matches the element at the specified location. - Location(Location), - /// Matches elements with a specific label. - Label(Label), - /// Matches text elements through a regular expression. - Regex(Regex), - /// Matches elements with a specific capability. - Can(TypeId), - /// Matches if any of the subselectors match. - Or(EcoVec<Self>), - /// Matches if all of the subselectors match. - And(EcoVec<Self>), - /// Matches all matches of `selector` before `end`. - Before { selector: Arc<Self>, end: Arc<Self>, inclusive: bool }, - /// Matches all matches of `selector` after `start`. - After { selector: Arc<Self>, start: Arc<Self>, inclusive: bool }, -} - -impl Selector { - /// Define a simple text selector. - pub fn text(text: &str) -> Self { - Self::Regex(Regex::new(®ex::escape(text)).unwrap()) - } - - /// Define a simple [`Selector::Can`] selector. - pub fn can<T: ?Sized + Any>() -> Self { - Self::Can(TypeId::of::<T>()) - } - - /// Transforms this selector and an iterator of other selectors into a - /// [`Selector::Or`] selector. - pub fn and(self, others: impl IntoIterator<Item = Self>) -> Self { - Self::And(others.into_iter().chain(Some(self)).collect()) - } - - /// Transforms this selector and an iterator of other selectors into a - /// [`Selector::And`] selector. - pub fn or(self, others: impl IntoIterator<Item = Self>) -> Self { - Self::Or(others.into_iter().chain(Some(self)).collect()) - } - - /// Transforms this selector into a [`Selector::Before`] selector. - pub fn before(self, location: impl Into<Self>, inclusive: bool) -> Self { - Self::Before { - selector: Arc::new(self), - end: Arc::new(location.into()), - inclusive, - } - } - - /// Transforms this selector into a [`Selector::After`] selector. - pub fn after(self, location: impl Into<Self>, inclusive: bool) -> Self { - Self::After { - selector: Arc::new(self), - start: Arc::new(location.into()), - inclusive, - } - } - - /// Whether the selector matches for the target. - pub fn matches(&self, target: &Content) -> bool { - match self { - Self::Elem(element, dict) => { - target.func() == *element - && dict - .iter() - .flat_map(|dict| dict.iter()) - .all(|(name, value)| target.field_ref(name) == Some(value)) - } - Self::Label(label) => target.label() == Some(label), - Self::Regex(regex) => { - target.func() == item!(text_func) - && item!(text_str)(target).map_or(false, |text| regex.is_match(&text)) - } - Self::Can(cap) => target.can_type_id(*cap), - Self::Or(selectors) => selectors.iter().any(move |sel| sel.matches(target)), - Self::And(selectors) => selectors.iter().all(move |sel| sel.matches(target)), - Self::Location(location) => target.location() == Some(*location), - // Not supported here. - Self::Before { .. } | Self::After { .. } => false, - } - } -} - -impl From<Location> for Selector { - fn from(value: Location) -> Self { - Self::Location(value) - } -} - -impl Debug for Selector { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - match self { - Self::Elem(elem, dict) => { - f.write_str(elem.name())?; - if let Some(dict) = dict { - f.write_str(".where")?; - dict.fmt(f)?; - } - Ok(()) - } - Self::Label(label) => label.fmt(f), - Self::Regex(regex) => regex.fmt(f), - Self::Can(cap) => cap.fmt(f), - Self::Or(selectors) | Self::And(selectors) => { - f.write_str(if matches!(self, Self::Or(_)) { "or" } else { "and" })?; - let pieces: Vec<_> = - selectors.iter().map(|sel| eco_format!("{sel:?}")).collect(); - f.write_str(&pretty_array_like(&pieces, false)) - } - Self::Location(loc) => loc.fmt(f), - Self::Before { selector, end: split, inclusive } - | Self::After { selector, start: split, inclusive } => { - selector.fmt(f)?; - - if matches!(self, Self::Before { .. }) { - f.write_str(".before(")?; - } else { - f.write_str(".after(")?; - } - - split.fmt(f)?; - if !*inclusive { - f.write_str(", inclusive: false")?; - } - f.write_char(')') - } - } - } -} - -cast_from_value! { - Selector: "selector", - func: Func => func - .element() - .ok_or("only element functions can be used as selectors")? - .select(), - label: Label => Self::Label(label), - text: EcoString => Self::text(&text), - regex: Regex => Self::Regex(regex), - location: Location => Self::Location(location), -} - -/// A selector that can be used with `query`. -/// -/// Hopefully, this is made obsolete by a more powerful query mechanism in the -/// future. -#[derive(Clone, PartialEq, Hash)] -pub struct LocatableSelector(pub Selector); - -impl Cast for LocatableSelector { - fn is(value: &Value) -> bool { - matches!(value, Value::Label(_) | Value::Func(_)) - || value.type_name() == "selector" - } - - fn cast(value: Value) -> StrResult<Self> { - fn validate(selector: &Selector) -> StrResult<()> { - match selector { - Selector::Elem(elem, _) => { - if !elem.can::<dyn Locatable>() { - Err(eco_format!("{} is not locatable", elem.name()))? - } - } - Selector::Location(_) => {} - Selector::Label(_) => {} - Selector::Regex(_) => Err("text is not locatable")?, - Selector::Can(_) => Err("capability is not locatable")?, - Selector::Or(list) | Selector::And(list) => { - for selector in list { - validate(selector)?; - } - } - Selector::Before { selector, end: split, .. } - | Selector::After { selector, start: split, .. } => { - for selector in [selector, split] { - validate(selector)?; - } - } - } - Ok(()) - } - - if !Self::is(&value) { - return <Self as Cast>::error(value); - } - - let selector = Selector::cast(value)?; - validate(&selector)?; - Ok(Self(selector)) - } - - fn describe() -> CastInfo { - CastInfo::Union(vec![ - CastInfo::Type("label"), - CastInfo::Type("function"), - CastInfo::Type("selector"), - ]) - } -} - -impl From<LocatableSelector> for Value { - fn from(value: LocatableSelector) -> Self { - value.0.into() - } -} - -/// A selector that can be used with show rules. -/// -/// Hopefully, this is made obsolete by a more powerful showing mechanism in the -/// future. -#[derive(Clone, PartialEq, Hash)] -pub struct ShowableSelector(pub Selector); - -impl Cast for ShowableSelector { - fn is(value: &Value) -> bool { - matches!( - value, - Value::Symbol(_) | Value::Str(_) | Value::Label(_) | Value::Func(_) - ) || value.type_name() == "regex" - || value.type_name() == "selector" - } - - fn cast(value: Value) -> StrResult<Self> { - fn validate(selector: &Selector) -> StrResult<()> { - match selector { - Selector::Elem(_, _) => {} - Selector::Label(_) => {} - Selector::Regex(_) => {} - Selector::Or(_) - | Selector::And(_) - | Selector::Location(_) - | Selector::Can(_) - | Selector::Before { .. } - | Selector::After { .. } => { - Err("this selector cannot be used with show")? - } - } - Ok(()) - } - - if !Self::is(&value) { - return <Self as Cast>::error(value); - } - - let selector = Selector::cast(value)?; - validate(&selector)?; - Ok(Self(selector)) - } - - fn describe() -> CastInfo { - CastInfo::Union(vec![ - CastInfo::Type("function"), - CastInfo::Type("label"), - CastInfo::Type("string"), - CastInfo::Type("regex"), - CastInfo::Type("symbol"), - CastInfo::Type("selector"), - ]) - } -} - -impl From<ShowableSelector> for Value { - fn from(value: ShowableSelector) -> Self { - value.0.into() - } -} - /// A show rule transformation that can be applied to a match. #[derive(Clone, PartialEq, Hash)] pub enum Transform { @@ -551,7 +280,7 @@ impl Debug for Transform { } } -cast_from_value! { +cast! { Transform, content: Content => Self::Content(content), func: Func => Self::Func(func), @@ -592,7 +321,7 @@ impl<'a> StyleChain<'a> { } /// Cast the first value for the given property in the chain. - pub fn get<T: Cast>( + pub fn get<T: FromValue>( self, func: ElemFunc, name: &'a str, @@ -605,7 +334,7 @@ impl<'a> StyleChain<'a> { } /// Cast the first value for the given property in the chain. - pub fn get_resolve<T: Cast + Resolve>( + pub fn get_resolve<T: FromValue + Resolve>( self, func: ElemFunc, name: &'a str, @@ -616,7 +345,7 @@ impl<'a> StyleChain<'a> { } /// Cast the first value for the given property in the chain. - pub fn get_fold<T: Cast + Fold>( + pub fn get_fold<T: FromValue + Fold>( self, func: ElemFunc, name: &'a str, @@ -645,7 +374,7 @@ impl<'a> StyleChain<'a> { default: impl Fn() -> <T::Output as Fold>::Output, ) -> <T::Output as Fold>::Output where - T: Cast + Resolve, + T: FromValue + Resolve, T::Output: Fold, { fn next<T>( @@ -671,7 +400,7 @@ impl<'a> StyleChain<'a> { } /// Iterate over all values for the given property in the chain. - pub fn properties<T: Cast + 'a>( + pub fn properties<T: FromValue + 'a>( self, func: ElemFunc, name: &'a str, diff --git a/tests/src/tests.rs b/tests/src/tests.rs index 2c94af6c..bf3d19c1 100644 --- a/tests/src/tests.rs +++ b/tests/src/tests.rs @@ -20,9 +20,9 @@ use tiny_skia as sk; use unscanny::Scanner; use walkdir::WalkDir; -use typst::diag::{bail, FileError, FileResult}; +use typst::diag::{bail, FileError, FileResult, StrResult}; use typst::doc::{Document, Frame, FrameItem, Meta}; -use typst::eval::{func, Datetime, Library, Value}; +use typst::eval::{func, Datetime, Library, NoneValue, Value}; use typst::font::{Font, FontBook}; use typst::geom::{Abs, Color, RgbaColor, Sides, Smart}; use typst::syntax::{Source, SourceId, Span, SyntaxNode}; @@ -145,20 +145,18 @@ fn main() { fn library() -> Library { /// Display: Test /// Category: test - /// Returns: #[func] - fn test(lhs: Value, rhs: Value) -> Value { + fn test(lhs: Value, rhs: Value) -> StrResult<NoneValue> { if lhs != rhs { - bail!(args.span, "Assertion failed: {:?} != {:?}", lhs, rhs,); + bail!("Assertion failed: {lhs:?} != {rhs:?}"); } - Value::None + Ok(NoneValue) } /// Display: Print /// Category: test - /// Returns: #[func] - fn print(#[variadic] values: Vec<Value>) -> Value { + fn print(#[variadic] values: Vec<Value>) -> NoneValue { let mut stdout = io::stdout().lock(); write!(stdout, "> ").unwrap(); for (i, value) in values.into_iter().enumerate() { @@ -168,7 +166,7 @@ fn library() -> Library { write!(stdout, "{value:?}").unwrap(); } writeln!(stdout).unwrap(); - Value::None + NoneValue } let mut lib = typst_library::build(); @@ -185,8 +183,8 @@ fn library() -> Library { lib.styles.set(TextElem::set_size(TextSize(Abs::pt(10.0).into()))); // Hook up helpers into the global scope. - lib.global.scope_mut().define("test", test); - lib.global.scope_mut().define("print", print); + lib.global.scope_mut().define("test", test_func()); + lib.global.scope_mut().define("print", print_func()); lib.global .scope_mut() .define("conifer", RgbaColor::new(0x9f, 0xEB, 0x52, 0xFF)); diff --git a/tests/typ/visualize/stroke.typ b/tests/typ/visualize/stroke.typ index 844cd8c2..88185403 100644 --- a/tests/typ/visualize/stroke.typ +++ b/tests/typ/visualize/stroke.typ @@ -56,18 +56,15 @@ (0pt, 20pt), (15pt, 0pt), (0pt, 40pt), (15pt, 45pt)), ) --- - // Error: 29-56 unexpected key "thicknes", valid keys are "paint", "thickness", "cap", "join", "dash", and "miter-limit" #line(length: 60pt, stroke: (paint: red, thicknes: 1pt)) --- - -// Error: 29-55 expected "solid", "dotted", "densely-dotted", "loosely-dotted", "dashed", "densely-dashed", "loosely-dashed", "dash-dotted", "densely-dash-dotted", "loosely-dash-dotted", array, dictionary, dash pattern, or none +// Error: 29-55 expected "solid", "dotted", "densely-dotted", "loosely-dotted", "dashed", "densely-dashed", "loosely-dashed", "dash-dotted", "densely-dash-dotted", "loosely-dash-dotted", array, dictionary, or none #line(length: 60pt, stroke: (paint: red, dash: "dash")) --- // 0pt strokes must function exactly like 'none' strokes and not draw anything - #rect(width: 10pt, height: 10pt, stroke: none) #rect(width: 10pt, height: 10pt, stroke: 0pt) #rect(width: 10pt, height: 10pt, stroke: none, fill: blue) |
