Improve value casting infrastructure
This commit is contained in:
parent
168bdf35bd
commit
fd417da04f
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -2488,7 +2488,7 @@ dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
"syn 2.0.16",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -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,
|
||||
}
|
||||
|
@ -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),
|
||||
|
@ -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))),
|
||||
(None, None) => {
|
||||
bail!(args.span, "at least one of date or time must be fully specified")
|
||||
Ok(match (date, time) {
|
||||
(Some(date), Some(time)) => {
|
||||
Datetime::Datetime(PrimitiveDateTime::new(date, time))
|
||||
}
|
||||
}
|
||||
(Some(date), None) => Datetime::Date(date),
|
||||
(None, Some(time)) => Datetime::Time(time),
|
||||
(None, None) => {
|
||||
bail!("at least one of date or time must be fully specified")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
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)]
|
||||
|
@ -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! {
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
@ -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>;
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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 })
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
self => match self {
|
||||
Self::Rel(rel) => {
|
||||
if rel.rel.is_zero() {
|
||||
rel.abs.into_value()
|
||||
} else if rel.abs.is_zero() {
|
||||
rel.rel.into_value()
|
||||
} else {
|
||||
rel.into_value()
|
||||
}
|
||||
}
|
||||
Self::Fr(fr) => fr.into_value(),
|
||||
},
|
||||
v: Rel<Length> => Self::Rel(v),
|
||||
v: Fr => Self::Fr(v),
|
||||
}
|
||||
|
||||
cast_to_value! {
|
||||
v: Spacing => match v {
|
||||
Spacing::Rel(rel) => {
|
||||
if rel.rel.is_zero() {
|
||||
Value::Length(rel.abs)
|
||||
} else if rel.abs.is_zero() {
|
||||
Value::Ratio(rel.rel)
|
||||
} else {
|
||||
Value::Relative(rel)
|
||||
}
|
||||
}
|
||||
Spacing::Fr(fr) => Value::Fraction(fr),
|
||||
}
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
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),
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Reflect> Reflect for Celled<T> {
|
||||
fn describe() -> CastInfo {
|
||||
T::describe() + CastInfo::Type("array") + CastInfo::Type("function")
|
||||
T::describe() + Array::describe() + Func::describe()
|
||||
}
|
||||
|
||||
fn castable(value: &Value) -> bool {
|
||||
Array::castable(value) || Func::castable(value) || T::castable(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: 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: 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)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -147,7 +147,7 @@ pub struct TermItem {
|
||||
pub description: Content,
|
||||
}
|
||||
|
||||
cast_from_value! {
|
||||
cast! {
|
||||
TermItem,
|
||||
array: Array => {
|
||||
let mut iter = array.into_iter();
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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("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);
|
||||
math.define("inline", inline);
|
||||
math.define("script", script);
|
||||
math.define("sscript", sscript);
|
||||
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));
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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")?,
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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())),
|
||||
}
|
||||
|
@ -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(),
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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.
|
||||
|
@ -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.
|
||||
|
@ -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),
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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),
|
||||
}
|
||||
|
@ -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::*;
|
||||
|
@ -251,8 +251,8 @@ impl Fold for Decoration {
|
||||
}
|
||||
}
|
||||
|
||||
cast_from_value! {
|
||||
Decoration: "decoration",
|
||||
cast! {
|
||||
type Decoration: "decoration",
|
||||
}
|
||||
|
||||
/// A kind of decorative line.
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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::*;
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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])
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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"] }
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
#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 expr = &cast.expr;
|
||||
Ok(quote! {
|
||||
impl ::std::convert::From<#ty> for ::typst::eval::Value {
|
||||
fn from(#pat: #ty) -> Self {
|
||||
#expr
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
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 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![,]>,
|
||||
into_value: Option<syn::Expr>,
|
||||
from_value: Punctuated<Cast, Token![,]>,
|
||||
}
|
||||
|
||||
struct Cast {
|
||||
attrs: Vec<syn::Attribute>,
|
||||
pattern: Pattern,
|
||||
expr: syn::Expr,
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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(),
|
||||
});
|
||||
|
||||
validate_attrs(&attrs)?;
|
||||
}
|
||||
}
|
||||
|
||||
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)?;
|
||||
}
|
||||
|
||||
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 }))
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
castable::derive_cast(&item)
|
||||
.unwrap_or_else(|err| err.to_compile_error())
|
||||
.into()
|
||||
}
|
||||
|
||||
/// Implement `Cast` and optionally `Type` for a type.
|
||||
/// Implements `Reflect`, `FromValue`, and `IntoValue` for a type.
|
||||
#[proc_macro]
|
||||
pub fn cast_from_value(stream: BoundaryStream) -> BoundaryStream {
|
||||
castable::cast_from_value(stream.into())
|
||||
pub fn cast(stream: BoundaryStream) -> BoundaryStream {
|
||||
castable::cast(stream.into())
|
||||
.unwrap_or_else(|err| err.to_compile_error())
|
||||
.into()
|
||||
}
|
||||
|
||||
/// Implement `From<T> for Value` for a type `T`.
|
||||
#[proc_macro]
|
||||
pub fn cast_to_value(stream: BoundaryStream) -> BoundaryStream {
|
||||
castable::cast_to_value(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())
|
||||
|
@ -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}"),
|
||||
}
|
||||
}
|
||||
|
||||
|
32
src/diag.rs
32
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)]))
|
||||
}
|
||||
}
|
||||
|
||||
|
56
src/doc.rs
56
src/doc.rs
@ -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)]
|
||||
|
@ -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(())
|
||||
}
|
||||
|
@ -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
|
||||
)
|
||||
}
|
||||
|
39
src/eval/auto.rs
Normal file
39
src/eval/auto.rs
Normal file
@ -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")
|
||||
}
|
||||
}
|
447
src/eval/cast.rs
447
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
|
||||
}
|
||||
|
||||
fn castable(_: &Value) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
impl<T: Reflect> Reflect for Spanned<T> {
|
||||
fn describe() -> CastInfo {
|
||||
T::describe()
|
||||
}
|
||||
|
||||
fn castable(value: &Value) -> bool {
|
||||
T::castable(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Cast> Cast<Spanned<Value>> for Spanned<T> {
|
||||
fn is(value: &Spanned<Value>) -> bool {
|
||||
T::is(&value.v)
|
||||
impl<T: Reflect> Reflect for StrResult<T> {
|
||||
fn describe() -> CastInfo {
|
||||
T::describe()
|
||||
}
|
||||
|
||||
fn cast(value: Spanned<Value>) -> StrResult<Self> {
|
||||
fn castable(value: &Value) -> bool {
|
||||
T::castable(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Reflect> Reflect for SourceResult<T> {
|
||||
fn describe() -> CastInfo {
|
||||
T::describe()
|
||||
}
|
||||
|
||||
fn castable(value: &Value) -> bool {
|
||||
T::castable(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Reflect> Reflect for &T {
|
||||
fn describe() -> CastInfo {
|
||||
T::describe()
|
||||
}
|
||||
|
||||
fn castable(value: &Value) -> bool {
|
||||
T::castable(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Reflect> Reflect for &mut T {
|
||||
fn describe() -> CastInfo {
|
||||
T::describe()
|
||||
}
|
||||
|
||||
fn castable(value: &Value) -> bool {
|
||||
T::castable(value)
|
||||
}
|
||||
}
|
||||
|
||||
/// 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;
|
||||
}
|
||||
|
||||
impl IntoValue for Value {
|
||||
fn into_value(self) -> Value {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: IntoValue> IntoValue for Spanned<T> {
|
||||
fn into_value(self) -> Value {
|
||||
self.v.into_value()
|
||||
}
|
||||
}
|
||||
|
||||
/// 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: IntoValue> IntoResult for T {
|
||||
fn into_result(self, _: Span) -> SourceResult<Value> {
|
||||
Ok(self.into_value())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: IntoValue> IntoResult for StrResult<T> {
|
||||
fn into_result(self, span: Span) -> SourceResult<Value> {
|
||||
self.map(IntoValue::into_value).at(span)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: IntoValue> IntoResult for SourceResult<T> {
|
||||
fn into_result(self, _: Span) -> SourceResult<Value> {
|
||||
self.map(IntoValue::into_value)
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 FromValue for Value {
|
||||
fn from_value(value: Value) -> StrResult<Self> {
|
||||
Ok(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: FromValue> FromValue<Spanned<Value>> for T {
|
||||
fn from_value(value: Spanned<Value>) -> StrResult<Self> {
|
||||
T::from_value(value.v)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: FromValue> FromValue<Spanned<Value>> for Spanned<T> {
|
||||
fn from_value(value: Spanned<Value>) -> StrResult<Self> {
|
||||
let span = value.span;
|
||||
T::cast(value.v).map(|t| Spanned::new(t, span))
|
||||
T::from_value(value.v).map(|t| Spanned::new(t, span))
|
||||
}
|
||||
|
||||
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"
|
||||
})?,
|
||||
}
|
||||
|
||||
cast_to_value! {
|
||||
v: NonZeroU64 => Value::Int(v.get() as i64)
|
||||
}
|
||||
|
||||
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"
|
||||
})?,
|
||||
}
|
||||
|
||||
cast_to_value! {
|
||||
v: NonZeroUsize => Value::Int(v.get() as i64)
|
||||
}
|
||||
|
||||
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")?,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
cast_to_value! {
|
||||
v: char => Value::Str(v.into())
|
||||
}
|
||||
|
||||
cast_to_value! {
|
||||
v: &str => Value::Str(v.into())
|
||||
}
|
||||
|
||||
cast_from_value! {
|
||||
EcoString,
|
||||
v: Str => v.into(),
|
||||
}
|
||||
|
||||
cast_to_value! {
|
||||
v: EcoString => Value::Str(v.into())
|
||||
}
|
||||
|
||||
cast_from_value! {
|
||||
String,
|
||||
v: Str => v.into(),
|
||||
}
|
||||
|
||||
cast_to_value! {
|
||||
v: String => Value::Str(v.into())
|
||||
}
|
||||
|
||||
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: Into<Value>> From<Spanned<T>> for Value {
|
||||
fn from(spanned: Spanned<T>) -> Self {
|
||||
spanned.v.into()
|
||||
}
|
||||
}
|
||||
|
||||
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: 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()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Into<Value>> From<Vec<T>> for Value {
|
||||
fn from(v: Vec<T>) -> Self {
|
||||
Value::Array(v.into_iter().map(Into::into).collect())
|
||||
}
|
||||
}
|
||||
|
||||
/// A container for a variadic argument.
|
||||
pub trait Variadics {
|
||||
/// The contained type.
|
||||
type Inner;
|
||||
}
|
||||
|
||||
impl<T> Variadics for Vec<T> {
|
||||
type Inner = T;
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
false
|
||||
}
|
||||
|
||||
fn cast(value: Value) -> StrResult<Self> {
|
||||
<Self as Cast>::error(value)
|
||||
}
|
||||
|
||||
impl Reflect for Never {
|
||||
fn describe() -> CastInfo {
|
||||
CastInfo::Union(vec![])
|
||||
}
|
||||
|
||||
fn castable(_: &Value) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoValue for Never {
|
||||
fn into_value(self) -> Value {
|
||||
match self {}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromValue for Never {
|
||||
fn from_value(value: Value) -> StrResult<Self> {
|
||||
Err(Self::error(&value))
|
||||
}
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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 {
|
||||
|
@ -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.
|
||||
|
81
src/eval/int.rs
Normal file
81
src/eval/int.rs
Normal file
@ -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
|
||||
}
|
@ -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 {
|
||||
|
@ -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::library::LANG_ITEMS,
|
||||
ecow::{eco_format, eco_vec},
|
||||
indexmap::IndexMap,
|
||||
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::*;
|
||||
#[doc(inline)]
|
||||
pub use typst_macros::{func, symbols};
|
||||
|
||||
pub(crate) use self::methods::methods_on;
|
||||
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,
|
||||
|
74
src/eval/none.rs
Normal file
74
src/eval/none.rs
Normal file
@ -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)),
|
||||
}
|
||||
}
|
||||
}
|
@ -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),
|
||||
})
|
||||
|
@ -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.
|
||||
|
@ -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)
|
||||
|
@ -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);
|
||||
|
@ -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]
|
||||
|
@ -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;
|
||||
|
@ -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::*;
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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>;
|
||||
|
||||
|
386
src/geom/color.rs
Normal file
386
src/geom/color.rs
Normal file
@ -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");
|
||||
}
|
||||
}
|
@ -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>>
|
||||
where
|
||||
T: Cast + Clone,
|
||||
{
|
||||
fn is(value: &Value) -> bool {
|
||||
matches!(value, Value::Dict(_)) || T::is(value)
|
||||
impl<T: Reflect> Reflect for Corners<Option<T>> {
|
||||
fn describe() -> CastInfo {
|
||||
T::describe() + Dict::describe()
|
||||
}
|
||||
|
||||
fn cast(mut value: Value) -> StrResult<Self> {
|
||||
fn castable(value: &Value) -> bool {
|
||||
Dict::castable(value) || T::castable(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> IntoValue for Corners<T>
|
||||
where
|
||||
T: PartialEq + IntoValue,
|
||||
{
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
@ -74,6 +74,6 @@ impl Debug for Dir {
|
||||
}
|
||||
}
|
||||
|
||||
cast_from_value! {
|
||||
Dir: "direction",
|
||||
cast! {
|
||||
type Dir: "direction",
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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.
|
||||
|
@ -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.
|
||||
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
@ -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(),
|
||||
}
|
||||
|
@ -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>>
|
||||
where
|
||||
T: Default + Cast + Clone,
|
||||
{
|
||||
fn is(value: &Value) -> bool {
|
||||
matches!(value, Value::Dict(_)) || T::is(value)
|
||||
impl<T: Reflect> Reflect for Sides<Option<T>> {
|
||||
fn describe() -> CastInfo {
|
||||
T::describe() + Dict::describe()
|
||||
}
|
||||
|
||||
fn cast(mut value: Value) -> StrResult<Self> {
|
||||
fn castable(value: &Value) -> bool {
|
||||
Dict::castable(value) || T::castable(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> IntoValue for Sides<T>
|
||||
where
|
||||
T: PartialEq + IntoValue,
|
||||
{
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
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>;
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
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),
|
||||
}
|
||||
impl<T: Reflect> Reflect for Smart<T> {
|
||||
fn castable(value: &Value) -> bool {
|
||||
AutoValue::castable(value) || T::castable(value)
|
||||
}
|
||||
|
||||
fn describe() -> CastInfo {
|
||||
T::describe() + CastInfo::Type("auto")
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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,6 +307,46 @@ impl<T: Default> From<Vec<DashLength<T>>> for DashPattern<T> {
|
||||
}
|
||||
}
|
||||
|
||||
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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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(),
|
||||
"loosely-dotted" => vec![DashLength::LineWidth, Abs::pt(4.0).into()].into(),
|
||||
"dashed" => vec![Abs::pt(3.0).into(), Abs::pt(3.0).into()].into(),
|
||||
"densely-dashed" => vec![Abs::pt(3.0).into(), Abs::pt(2.0).into()].into(),
|
||||
"loosely-dashed" => vec![Abs::pt(3.0).into(), Abs::pt(6.0).into()].into(),
|
||||
"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() },
|
||||
mut dict: Dict => {
|
||||
let array: Vec<DashLength> = dict.take("array")?.cast()?;
|
||||
let phase = dict.take("phase").ok().map(Value::cast)
|
||||
.transpose()?.unwrap_or(Length::zero());
|
||||
dict.finish(&["array", "phase"])?;
|
||||
Self {
|
||||
array,
|
||||
phase,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
/// The length of a dash in a line dash pattern
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||
pub enum DashLength<T = Length> {
|
||||
@ -257,133 +369,19 @@ impl<T> DashLength<T> {
|
||||
}
|
||||
}
|
||||
|
||||
cast_from_value! {
|
||||
DashLength: "dash length",
|
||||
"dot" => Self::LineWidth,
|
||||
l: Length => Self::Length(l),
|
||||
}
|
||||
|
||||
impl Resolve for DashLength {
|
||||
type Output = DashLength<Abs>;
|
||||
|
||||
fn resolve(self, styles: StyleChain) -> Self::Output {
|
||||
match self {
|
||||
Self::LineWidth => DashLength::LineWidth,
|
||||
Self::Length(l) => DashLength::Length(l.resolve(styles)),
|
||||
Self::Length(v) => DashLength::Length(v.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
|
||||
"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(),
|
||||
"loosely-dotted" => vec![DashLength::LineWidth, Abs::pt(4.0).into()].into(),
|
||||
"dashed" => vec![Abs::pt(3.0).into(), Abs::pt(3.0).into()].into(),
|
||||
"densely-dashed" => vec![Abs::pt(3.0).into(), Abs::pt(2.0).into()].into(),
|
||||
"loosely-dashed" => vec![Abs::pt(3.0).into(), Abs::pt(6.0).into()].into(),
|
||||
"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(),
|
||||
}
|
||||
},
|
||||
mut dict: Dict => {
|
||||
let array: Vec<DashLength> = dict.take("array")?.cast()?;
|
||||
let phase = dict.take("phase").ok().map(Length::cast)
|
||||
.transpose()?.unwrap_or(Length::zero());
|
||||
|
||||
dict.finish(&["array", "phase"])?;
|
||||
|
||||
Self {
|
||||
array,
|
||||
phase,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 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_to_value! {
|
||||
v: PartialStroke<Abs> => v.map(Length::from).into()
|
||||
cast! {
|
||||
DashLength,
|
||||
"dot" => Self::LineWidth,
|
||||
v: Length => Self::Length(v),
|
||||
}
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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.
|
||||
|
16
src/model/label.rs
Normal file
16
src/model/label.rs
Normal file
@ -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 {}
|
@ -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;
|
||||
|
||||
|
291
src/model/selector.rs
Normal file
291
src/model/selector.rs
Normal file
@ -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))
|
||||
}
|
||||
}
|
@ -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,
|
||||
|
@ -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));
|
||||
|
@ -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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user