First-class types
Makes types first-class values.
This commit is contained in:
parent
8f36fca684
commit
b471ac7d59
4
Cargo.lock
generated
4
Cargo.lock
generated
@ -625,9 +625,9 @@ checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650"
|
||||
|
||||
[[package]]
|
||||
name = "ecow"
|
||||
version = "0.1.1"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c5c5051925c54d9a42c8652313b5358a7432eed209466b443ed5220431243a14"
|
||||
checksum = "1d1990d053cf6edf3f030682dba3b0eb65ef01fabb2686072765d8a17d6728e8"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
@ -22,7 +22,7 @@ az = "1.2"
|
||||
chinese-number = { version = "0.7.2", default-features = false, features = ["number-to-chinese"] }
|
||||
comemo = "0.3"
|
||||
csv = "1"
|
||||
ecow = "0.1"
|
||||
ecow = { version = "0.1.2", features = ["serde"] }
|
||||
hayagriva = "0.3.2"
|
||||
hypher = "0.1.3"
|
||||
icu_properties = { version = "1.2.0", features = ["serde"] }
|
||||
|
@ -8,62 +8,65 @@ use typst::eval::{Module, Scope};
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
/// A module with computational functions.
|
||||
pub fn module() -> Module {
|
||||
/// Hook up all calculation definitions.
|
||||
pub(super) fn define(global: &mut Scope) {
|
||||
global.category("calculate");
|
||||
global.define_module(module());
|
||||
}
|
||||
|
||||
/// A module with calculation definitions.
|
||||
fn module() -> Module {
|
||||
let mut scope = Scope::new();
|
||||
scope.define("abs", abs_func());
|
||||
scope.define("pow", pow_func());
|
||||
scope.define("exp", exp_func());
|
||||
scope.define("sqrt", sqrt_func());
|
||||
scope.define("sin", sin_func());
|
||||
scope.define("cos", cos_func());
|
||||
scope.define("tan", tan_func());
|
||||
scope.define("asin", asin_func());
|
||||
scope.define("acos", acos_func());
|
||||
scope.define("atan", atan_func());
|
||||
scope.define("atan2", atan2_func());
|
||||
scope.define("sinh", sinh_func());
|
||||
scope.define("cosh", cosh_func());
|
||||
scope.define("tanh", tanh_func());
|
||||
scope.define("log", log_func());
|
||||
scope.define("ln", ln_func());
|
||||
scope.define("fact", fact_func());
|
||||
scope.define("perm", perm_func());
|
||||
scope.define("binom", binom_func());
|
||||
scope.define("gcd", gcd_func());
|
||||
scope.define("lcm", lcm_func());
|
||||
scope.define("floor", floor_func());
|
||||
scope.define("ceil", ceil_func());
|
||||
scope.define("trunc", trunc_func());
|
||||
scope.define("fract", fract_func());
|
||||
scope.define("round", round_func());
|
||||
scope.define("clamp", clamp_func());
|
||||
scope.define("min", min_func());
|
||||
scope.define("max", max_func());
|
||||
scope.define("even", even_func());
|
||||
scope.define("odd", odd_func());
|
||||
scope.define("rem", rem_func());
|
||||
scope.define("quo", quo_func());
|
||||
scope.category("calculate");
|
||||
scope.define_func::<abs>();
|
||||
scope.define_func::<pow>();
|
||||
scope.define_func::<exp>();
|
||||
scope.define_func::<sqrt>();
|
||||
scope.define_func::<sin>();
|
||||
scope.define_func::<cos>();
|
||||
scope.define_func::<tan>();
|
||||
scope.define_func::<asin>();
|
||||
scope.define_func::<acos>();
|
||||
scope.define_func::<atan>();
|
||||
scope.define_func::<atan2>();
|
||||
scope.define_func::<sinh>();
|
||||
scope.define_func::<cosh>();
|
||||
scope.define_func::<tanh>();
|
||||
scope.define_func::<log>();
|
||||
scope.define_func::<ln>();
|
||||
scope.define_func::<fact>();
|
||||
scope.define_func::<perm>();
|
||||
scope.define_func::<binom>();
|
||||
scope.define_func::<gcd>();
|
||||
scope.define_func::<lcm>();
|
||||
scope.define_func::<floor>();
|
||||
scope.define_func::<ceil>();
|
||||
scope.define_func::<trunc>();
|
||||
scope.define_func::<fract>();
|
||||
scope.define_func::<round>();
|
||||
scope.define_func::<clamp>();
|
||||
scope.define_func::<min>();
|
||||
scope.define_func::<max>();
|
||||
scope.define_func::<even>();
|
||||
scope.define_func::<odd>();
|
||||
scope.define_func::<rem>();
|
||||
scope.define_func::<quo>();
|
||||
scope.define("inf", f64::INFINITY);
|
||||
scope.define("nan", f64::NAN);
|
||||
scope.define("pi", std::f64::consts::PI);
|
||||
scope.define("tau", std::f64::consts::TAU);
|
||||
scope.define("e", std::f64::consts::E);
|
||||
Module::new("calc").with_scope(scope)
|
||||
Module::new("calc", scope)
|
||||
}
|
||||
|
||||
/// Calculates the absolute value of a numeric value.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// ```example
|
||||
/// #calc.abs(-5) \
|
||||
/// #calc.abs(5pt - 2cm) \
|
||||
/// #calc.abs(2fr)
|
||||
/// ```
|
||||
///
|
||||
/// Display: Absolute
|
||||
/// Category: calculate
|
||||
#[func]
|
||||
#[func(title = "Absolute")]
|
||||
pub fn abs(
|
||||
/// The value whose absolute value to calculate.
|
||||
value: ToAbs,
|
||||
@ -87,21 +90,17 @@ cast! {
|
||||
|
||||
/// Raises a value to some exponent.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// ```example
|
||||
/// #calc.pow(2, 3)
|
||||
/// ```
|
||||
///
|
||||
/// Display: Power
|
||||
/// Category: calculate
|
||||
#[func]
|
||||
#[func(title = "Power")]
|
||||
pub fn pow(
|
||||
/// The callsite span.
|
||||
span: Span,
|
||||
/// The base of the power.
|
||||
base: Num,
|
||||
/// The exponent of the power.
|
||||
exponent: Spanned<Num>,
|
||||
/// The callsite span.
|
||||
span: Span,
|
||||
) -> SourceResult<Num> {
|
||||
match exponent.v {
|
||||
_ if exponent.v.float() == 0.0 && base.float() == 0.0 => {
|
||||
@ -142,19 +141,15 @@ pub fn pow(
|
||||
|
||||
/// Raises a value to some exponent of e.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// ```example
|
||||
/// #calc.exp(1)
|
||||
/// ```
|
||||
///
|
||||
/// Display: Exponential
|
||||
/// Category: calculate
|
||||
#[func]
|
||||
#[func(title = "Exponential")]
|
||||
pub fn exp(
|
||||
/// The exponent of the power.
|
||||
exponent: Spanned<Num>,
|
||||
/// The callsite span.
|
||||
span: Span,
|
||||
/// The exponent of the power.
|
||||
exponent: Spanned<Num>,
|
||||
) -> SourceResult<f64> {
|
||||
match exponent.v {
|
||||
Num::Int(i) if i32::try_from(i).is_err() => {
|
||||
@ -176,15 +171,11 @@ pub fn exp(
|
||||
|
||||
/// Extracts the square root of a number.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// ```example
|
||||
/// #calc.sqrt(16) \
|
||||
/// #calc.sqrt(2.5)
|
||||
/// ```
|
||||
///
|
||||
/// Display: Square Root
|
||||
/// Category: calculate
|
||||
#[func]
|
||||
#[func(title = "Square Root")]
|
||||
pub fn sqrt(
|
||||
/// The number whose square root to calculate. Must be non-negative.
|
||||
value: Spanned<Num>,
|
||||
@ -200,16 +191,12 @@ pub fn sqrt(
|
||||
/// When called with an integer or a float, they will be interpreted as
|
||||
/// radians.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// ```example
|
||||
/// #assert(calc.sin(90deg) == calc.sin(-270deg))
|
||||
/// #calc.sin(1.5) \
|
||||
/// #calc.sin(90deg)
|
||||
/// ```
|
||||
///
|
||||
/// Display: Sine
|
||||
/// Category: calculate
|
||||
#[func]
|
||||
#[func(title = "Sine")]
|
||||
pub fn sin(
|
||||
/// The angle whose sine to calculate.
|
||||
angle: AngleLike,
|
||||
@ -226,16 +213,12 @@ pub fn sin(
|
||||
/// When called with an integer or a float, they will be interpreted as
|
||||
/// radians.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// ```example
|
||||
/// #calc.cos(90deg) \
|
||||
/// #calc.cos(1.5) \
|
||||
/// #calc.cos(90deg)
|
||||
/// ```
|
||||
///
|
||||
/// Display: Cosine
|
||||
/// Category: calculate
|
||||
#[func]
|
||||
#[func(title = "Cosine")]
|
||||
pub fn cos(
|
||||
/// The angle whose cosine to calculate.
|
||||
angle: AngleLike,
|
||||
@ -252,15 +235,11 @@ pub fn cos(
|
||||
/// When called with an integer or a float, they will be interpreted as
|
||||
/// radians.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// ```example
|
||||
/// #calc.tan(1.5) \
|
||||
/// #calc.tan(90deg)
|
||||
/// ```
|
||||
///
|
||||
/// Display: Tangent
|
||||
/// Category: calculate
|
||||
#[func]
|
||||
#[func(title = "Tangent")]
|
||||
pub fn tan(
|
||||
/// The angle whose tangent to calculate.
|
||||
angle: AngleLike,
|
||||
@ -274,15 +253,11 @@ pub fn tan(
|
||||
|
||||
/// Calculates the arcsine of a number.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// ```example
|
||||
/// #calc.asin(0) \
|
||||
/// #calc.asin(1)
|
||||
/// ```
|
||||
///
|
||||
/// Display: Arcsine
|
||||
/// Category: calculate
|
||||
#[func]
|
||||
#[func(title = "Arcsine")]
|
||||
pub fn asin(
|
||||
/// The number whose arcsine to calculate. Must be between -1 and 1.
|
||||
value: Spanned<Num>,
|
||||
@ -296,15 +271,11 @@ pub fn asin(
|
||||
|
||||
/// Calculates the arccosine of a number.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// ```example
|
||||
/// #calc.acos(0) \
|
||||
/// #calc.acos(1)
|
||||
/// ```
|
||||
///
|
||||
/// Display: Arccosine
|
||||
/// Category: calculate
|
||||
#[func]
|
||||
#[func(title = "Arccosine")]
|
||||
pub fn acos(
|
||||
/// The number whose arcsine to calculate. Must be between -1 and 1.
|
||||
value: Spanned<Num>,
|
||||
@ -318,15 +289,11 @@ pub fn acos(
|
||||
|
||||
/// Calculates the arctangent of a number.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// ```example
|
||||
/// #calc.atan(0) \
|
||||
/// #calc.atan(1)
|
||||
/// ```
|
||||
///
|
||||
/// Display: Arctangent
|
||||
/// Category: calculate
|
||||
#[func]
|
||||
#[func(title = "Arctangent")]
|
||||
pub fn atan(
|
||||
/// The number whose arctangent to calculate.
|
||||
value: Num,
|
||||
@ -338,15 +305,11 @@ pub fn atan(
|
||||
///
|
||||
/// The arguments are `(x, y)`, not `(y, x)`.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// ```example
|
||||
/// #calc.atan2(1, 1) \
|
||||
/// #calc.atan2(-2, -3)
|
||||
/// ```
|
||||
///
|
||||
/// Display: Four-quadrant Arctangent
|
||||
/// Category: calculate
|
||||
#[func]
|
||||
#[func(title = "Four-quadrant Arctangent")]
|
||||
pub fn atan2(
|
||||
/// The X coordinate.
|
||||
x: Num,
|
||||
@ -358,15 +321,11 @@ pub fn atan2(
|
||||
|
||||
/// Calculates the hyperbolic sine of a hyperbolic angle.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// ```example
|
||||
/// #calc.sinh(0) \
|
||||
/// #calc.sinh(1.5)
|
||||
/// ```
|
||||
///
|
||||
/// Display: Hyperbolic sine
|
||||
/// Category: calculate
|
||||
#[func]
|
||||
#[func(title = "Hyperbolic Sine")]
|
||||
pub fn sinh(
|
||||
/// The hyperbolic angle whose hyperbolic sine to calculate.
|
||||
value: f64,
|
||||
@ -376,15 +335,11 @@ pub fn sinh(
|
||||
|
||||
/// Calculates the hyperbolic cosine of a hyperbolic angle.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// ```example
|
||||
/// #calc.cosh(0) \
|
||||
/// #calc.cosh(1.5)
|
||||
/// ```
|
||||
///
|
||||
/// Display: Hyperbolic cosine
|
||||
/// Category: calculate
|
||||
#[func]
|
||||
#[func(title = "Hyperbolic Cosine")]
|
||||
pub fn cosh(
|
||||
/// The hyperbolic angle whose hyperbolic cosine to calculate.
|
||||
value: f64,
|
||||
@ -394,15 +349,11 @@ pub fn cosh(
|
||||
|
||||
/// Calculates the hyperbolic tangent of an hyperbolic angle.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// ```example
|
||||
/// #calc.tanh(0) \
|
||||
/// #calc.tanh(1.5)
|
||||
/// ```
|
||||
///
|
||||
/// Display: Hyperbolic tangent
|
||||
/// Category: calculate
|
||||
#[func]
|
||||
#[func(title = "Hyperbolic Tangent")]
|
||||
pub fn tanh(
|
||||
/// The hyperbolic angle whose hyperbolic tangent to calculate.
|
||||
value: f64,
|
||||
@ -414,23 +365,19 @@ pub fn tanh(
|
||||
///
|
||||
/// If the base is not specified, the logarithm is calculated in base 10.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// ```example
|
||||
/// #calc.log(100)
|
||||
/// ```
|
||||
///
|
||||
/// Display: Logarithm
|
||||
/// Category: calculate
|
||||
#[func]
|
||||
#[func(title = "Logarithm")]
|
||||
pub fn log(
|
||||
/// The callsite span.
|
||||
span: Span,
|
||||
/// The number whose logarithm to calculate. Must be strictly positive.
|
||||
value: Spanned<Num>,
|
||||
/// The base of the logarithm. May not be zero.
|
||||
#[named]
|
||||
#[default(Spanned::new(10.0, Span::detached()))]
|
||||
base: Spanned<f64>,
|
||||
/// The callsite span.
|
||||
span: Span,
|
||||
) -> SourceResult<f64> {
|
||||
let number = value.v.float();
|
||||
if number <= 0.0 {
|
||||
@ -460,19 +407,15 @@ pub fn log(
|
||||
|
||||
/// Calculates the natural logarithm of a number.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// ```example
|
||||
/// #calc.ln(calc.e)
|
||||
/// ```
|
||||
///
|
||||
/// Display: Natural Logarithm
|
||||
/// Category: calculate
|
||||
#[func]
|
||||
#[func(title = "Natural Logarithm")]
|
||||
pub fn ln(
|
||||
/// The number whose logarithm to calculate. Must be strictly positive.
|
||||
value: Spanned<Num>,
|
||||
/// The callsite span.
|
||||
span: Span,
|
||||
/// The number whose logarithm to calculate. Must be strictly positive.
|
||||
value: Spanned<Num>,
|
||||
) -> SourceResult<f64> {
|
||||
let number = value.v.float();
|
||||
if number <= 0.0 {
|
||||
@ -489,14 +432,10 @@ pub fn ln(
|
||||
|
||||
/// Calculates the factorial of a number.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// ```example
|
||||
/// #calc.fact(5)
|
||||
/// ```
|
||||
///
|
||||
/// Display: Factorial
|
||||
/// Category: calculate
|
||||
#[func]
|
||||
#[func(title = "Factorial")]
|
||||
pub fn fact(
|
||||
/// The number whose factorial to calculate. Must be non-negative.
|
||||
number: u64,
|
||||
@ -506,14 +445,10 @@ pub fn fact(
|
||||
|
||||
/// Calculates a permutation.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// ```example
|
||||
/// #calc.perm(10, 5)
|
||||
/// ```
|
||||
///
|
||||
/// Display: Permutation
|
||||
/// Category: calculate
|
||||
#[func]
|
||||
#[func(title = "Permutation")]
|
||||
pub fn perm(
|
||||
/// The base number. Must be non-negative.
|
||||
base: u64,
|
||||
@ -547,14 +482,10 @@ fn fact_impl(start: u64, end: u64) -> Option<i64> {
|
||||
|
||||
/// Calculates a binomial coefficient.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// ```example
|
||||
/// #calc.binom(10, 5)
|
||||
/// ```
|
||||
///
|
||||
/// Display: Binomial
|
||||
/// Category: calculate
|
||||
#[func]
|
||||
#[func(title = "Binomial")]
|
||||
pub fn binom(
|
||||
/// The upper coefficient. Must be non-negative.
|
||||
n: u64,
|
||||
@ -588,14 +519,10 @@ fn binom_impl(n: u64, k: u64) -> Option<i64> {
|
||||
|
||||
/// Calculates the greatest common divisor of two integers.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// ```example
|
||||
/// #calc.gcd(7, 42)
|
||||
/// ```
|
||||
///
|
||||
/// Display: Greatest Common Divisor
|
||||
/// Category: calculate
|
||||
#[func]
|
||||
#[func(title = "Greatest Common Divisor")]
|
||||
pub fn gcd(
|
||||
/// The first integer.
|
||||
a: i64,
|
||||
@ -614,14 +541,10 @@ pub fn gcd(
|
||||
|
||||
/// Calculates the least common multiple of two integers.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// ```example
|
||||
/// #calc.lcm(96, 13)
|
||||
/// ```
|
||||
///
|
||||
/// Display: Least Common Multiple
|
||||
/// Category: calculate
|
||||
#[func]
|
||||
#[func(title = "Least Common Multiple")]
|
||||
pub fn lcm(
|
||||
/// The first integer.
|
||||
a: i64,
|
||||
@ -642,15 +565,11 @@ pub fn lcm(
|
||||
///
|
||||
/// If the number is already an integer, it is returned unchanged.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// ```example
|
||||
/// #assert(calc.floor(3.14) == 3)
|
||||
/// #assert(calc.floor(3) == 3)
|
||||
/// #calc.floor(500.1)
|
||||
/// ```
|
||||
///
|
||||
/// Display: Round down
|
||||
/// Category: calculate
|
||||
#[func]
|
||||
pub fn floor(
|
||||
/// The number to round down.
|
||||
@ -666,15 +585,11 @@ pub fn floor(
|
||||
///
|
||||
/// If the number is already an integer, it is returned unchanged.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// ```example
|
||||
/// #assert(calc.ceil(3.14) == 4)
|
||||
/// #assert(calc.ceil(3) == 3)
|
||||
/// #calc.ceil(500.1)
|
||||
/// ```
|
||||
///
|
||||
/// Display: Round up
|
||||
/// Category: calculate
|
||||
#[func]
|
||||
pub fn ceil(
|
||||
/// The number to round up.
|
||||
@ -690,16 +605,12 @@ pub fn ceil(
|
||||
///
|
||||
/// If the number is already an integer, it is returned unchanged.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// ```example
|
||||
/// #assert(calc.trunc(3) == 3)
|
||||
/// #assert(calc.trunc(-3.7) == -3)
|
||||
/// #assert(calc.trunc(15.9) == 15)
|
||||
/// ```
|
||||
///
|
||||
/// Display: Truncate
|
||||
/// Category: calculate
|
||||
#[func]
|
||||
#[func(title = "Truncate")]
|
||||
pub fn trunc(
|
||||
/// The number to truncate.
|
||||
value: Num,
|
||||
@ -714,15 +625,11 @@ pub fn trunc(
|
||||
///
|
||||
/// If the number is an integer, returns `0`.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// ```example
|
||||
/// #assert(calc.fract(3) == 0)
|
||||
/// #calc.fract(-3.1)
|
||||
/// ```
|
||||
///
|
||||
/// Display: Fractional
|
||||
/// Category: calculate
|
||||
#[func]
|
||||
#[func(title = "Fractional")]
|
||||
pub fn fract(
|
||||
/// The number to truncate.
|
||||
value: Num,
|
||||
@ -737,15 +644,11 @@ pub fn fract(
|
||||
///
|
||||
/// Optionally, a number of decimal places can be specified.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// ```example
|
||||
/// #assert(calc.round(3.14) == 3)
|
||||
/// #assert(calc.round(3.5) == 4)
|
||||
/// #calc.round(3.1415, digits: 2)
|
||||
/// ```
|
||||
///
|
||||
/// Display: Round
|
||||
/// Category: calculate
|
||||
#[func]
|
||||
pub fn round(
|
||||
/// The number to round.
|
||||
@ -767,15 +670,11 @@ pub fn round(
|
||||
|
||||
/// Clamps a number between a minimum and maximum value.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// ```example
|
||||
/// #assert(calc.clamp(5, 0, 10) == 5)
|
||||
/// #assert(calc.clamp(5, 6, 10) == 6)
|
||||
/// #calc.clamp(5, 0, 4)
|
||||
/// ```
|
||||
///
|
||||
/// Display: Clamp
|
||||
/// Category: calculate
|
||||
#[func]
|
||||
pub fn clamp(
|
||||
/// The number to clamp.
|
||||
@ -793,44 +692,36 @@ pub fn clamp(
|
||||
|
||||
/// Determines the minimum of a sequence of values.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// ```example
|
||||
/// #calc.min(1, -3, -5, 20, 3, 6) \
|
||||
/// #calc.min("typst", "in", "beta")
|
||||
/// ```
|
||||
///
|
||||
/// Display: Minimum
|
||||
/// Category: calculate
|
||||
#[func]
|
||||
#[func(title = "Minimum")]
|
||||
pub fn min(
|
||||
/// The callsite span.
|
||||
span: Span,
|
||||
/// The sequence of values from which to extract the minimum.
|
||||
/// Must not be empty.
|
||||
#[variadic]
|
||||
values: Vec<Spanned<Value>>,
|
||||
/// The callsite span.
|
||||
span: Span,
|
||||
) -> SourceResult<Value> {
|
||||
minmax(span, values, Ordering::Less)
|
||||
}
|
||||
|
||||
/// Determines the maximum of a sequence of values.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// ```example
|
||||
/// #calc.max(1, -3, -5, 20, 3, 6) \
|
||||
/// #calc.max("typst", "in", "beta")
|
||||
/// ```
|
||||
///
|
||||
/// Display: Maximum
|
||||
/// Category: calculate
|
||||
#[func]
|
||||
#[func(title = "Maximum")]
|
||||
pub fn max(
|
||||
/// The callsite span.
|
||||
span: Span,
|
||||
/// The sequence of values from which to extract the maximum.
|
||||
/// Must not be empty.
|
||||
#[variadic]
|
||||
values: Vec<Spanned<Value>>,
|
||||
/// The callsite span.
|
||||
span: Span,
|
||||
) -> SourceResult<Value> {
|
||||
minmax(span, values, Ordering::Greater)
|
||||
}
|
||||
@ -858,15 +749,11 @@ fn minmax(
|
||||
|
||||
/// Determines whether an integer is even.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// ```example
|
||||
/// #calc.even(4) \
|
||||
/// #calc.even(5) \
|
||||
/// #range(10).filter(calc.even)
|
||||
/// ```
|
||||
///
|
||||
/// Display: Even
|
||||
/// Category: calculate
|
||||
#[func]
|
||||
pub fn even(
|
||||
/// The number to check for evenness.
|
||||
@ -877,15 +764,11 @@ pub fn even(
|
||||
|
||||
/// Determines whether an integer is odd.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// ```example
|
||||
/// #calc.odd(4) \
|
||||
/// #calc.odd(5) \
|
||||
/// #range(10).filter(calc.odd)
|
||||
/// ```
|
||||
///
|
||||
/// Display: Odd
|
||||
/// Category: calculate
|
||||
#[func]
|
||||
pub fn odd(
|
||||
/// The number to check for oddness.
|
||||
@ -896,15 +779,11 @@ pub fn odd(
|
||||
|
||||
/// Calculates the remainder of two numbers.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// ```example
|
||||
/// #calc.rem(20, 6) \
|
||||
/// #calc.rem(1.75, 0.5)
|
||||
/// ```
|
||||
///
|
||||
/// Display: Remainder
|
||||
/// Category: calculate
|
||||
#[func]
|
||||
#[func(title = "Remainder")]
|
||||
pub fn rem(
|
||||
/// The dividend of the remainder.
|
||||
dividend: Num,
|
||||
@ -919,15 +798,11 @@ pub fn rem(
|
||||
|
||||
/// Calculates the quotient of two numbers.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// ```example
|
||||
/// #calc.quo(14, 5) \
|
||||
/// #calc.quo(3.46, 0.5)
|
||||
/// ```
|
||||
///
|
||||
/// Display: Quotient
|
||||
/// Category: calculate
|
||||
#[func]
|
||||
#[func(title = "Quotient")]
|
||||
pub fn quo(
|
||||
/// The dividend of the quotient.
|
||||
dividend: Num,
|
||||
@ -949,7 +824,7 @@ pub enum Num {
|
||||
}
|
||||
|
||||
impl Num {
|
||||
pub fn apply2(
|
||||
fn apply2(
|
||||
self,
|
||||
other: Self,
|
||||
int: impl FnOnce(i64, i64) -> i64,
|
||||
@ -961,7 +836,7 @@ impl Num {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn apply3(
|
||||
fn apply3(
|
||||
self,
|
||||
other: Self,
|
||||
third: Self,
|
||||
@ -974,7 +849,7 @@ impl Num {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn float(self) -> f64 {
|
||||
fn float(self) -> f64 {
|
||||
match self {
|
||||
Self::Int(v) => v as f64,
|
||||
Self::Float(v) => v,
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -4,15 +4,25 @@ use typst::syntax::is_newline;
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
/// Hook up all data loading definitions.
|
||||
pub(super) fn define(global: &mut Scope) {
|
||||
global.category("data-loading");
|
||||
global.define_func::<read>();
|
||||
global.define_func::<csv>();
|
||||
global.define_func::<json>();
|
||||
global.define_func::<toml>();
|
||||
global.define_func::<yaml>();
|
||||
global.define_func::<cbor>();
|
||||
global.define_func::<xml>();
|
||||
}
|
||||
|
||||
/// Reads plain text or data from a file.
|
||||
///
|
||||
/// By default, the file will be read as UTF-8 and returned as a
|
||||
/// [string]($type/string).
|
||||
/// By default, the file will be read as UTF-8 and returned as a [string]($str).
|
||||
///
|
||||
/// If you specify `{encoding: none}`, this returns raw [bytes]($type/bytes)
|
||||
/// instead.
|
||||
/// If you specify `{encoding: none}`, this returns raw [bytes]($bytes) instead.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// # Example
|
||||
/// ```example
|
||||
/// An example for a HTML file: \
|
||||
/// #let text = read("data.html")
|
||||
@ -21,11 +31,10 @@ use crate::prelude::*;
|
||||
/// Raw bytes:
|
||||
/// #read("tiger.jpg", encoding: none)
|
||||
/// ```
|
||||
///
|
||||
/// Display: Read
|
||||
/// Category: data-loading
|
||||
#[func]
|
||||
pub fn read(
|
||||
/// The virtual machine.
|
||||
vm: &mut Vm,
|
||||
/// Path to a file.
|
||||
path: Spanned<EcoString>,
|
||||
/// The encoding to read the file with.
|
||||
@ -34,8 +43,6 @@ pub fn read(
|
||||
#[named]
|
||||
#[default(Some(Encoding::Utf8))]
|
||||
encoding: Option<Encoding>,
|
||||
/// The virtual machine.
|
||||
vm: &mut Vm,
|
||||
) -> SourceResult<Readable> {
|
||||
let Spanned { v: path, span } = path;
|
||||
let id = vm.resolve_path(&path).at(span)?;
|
||||
@ -101,7 +108,7 @@ impl From<Readable> for Bytes {
|
||||
/// rows will be collected into a single array. Header rows will not be
|
||||
/// stripped.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// # Example
|
||||
/// ```example
|
||||
/// #let results = csv("data.csv")
|
||||
///
|
||||
@ -111,15 +118,10 @@ impl From<Readable> for Bytes {
|
||||
/// ..results.flatten(),
|
||||
/// )
|
||||
/// ```
|
||||
///
|
||||
/// Display: CSV
|
||||
/// Category: data-loading
|
||||
#[func]
|
||||
#[scope(
|
||||
scope.define("decode", csv_decode_func());
|
||||
scope
|
||||
)]
|
||||
#[func(scope, title = "CSV")]
|
||||
pub fn csv(
|
||||
/// The virtual machine.
|
||||
vm: &mut Vm,
|
||||
/// Path to a CSV file.
|
||||
path: Spanned<EcoString>,
|
||||
/// The delimiter that separates columns in the CSV file.
|
||||
@ -127,47 +129,45 @@ pub fn csv(
|
||||
#[named]
|
||||
#[default]
|
||||
delimiter: Delimiter,
|
||||
/// The virtual machine.
|
||||
vm: &mut Vm,
|
||||
) -> SourceResult<Array> {
|
||||
let Spanned { v: path, span } = path;
|
||||
let id = vm.resolve_path(&path).at(span)?;
|
||||
let data = vm.world().file(id).at(span)?;
|
||||
csv_decode(Spanned::new(Readable::Bytes(data), span), delimiter)
|
||||
self::csv::decode(Spanned::new(Readable::Bytes(data), span), delimiter)
|
||||
}
|
||||
|
||||
/// Reads structured data from a CSV string/bytes.
|
||||
///
|
||||
/// Display: Decode CSV
|
||||
/// Category: data-loading
|
||||
#[func]
|
||||
pub fn csv_decode(
|
||||
/// CSV data.
|
||||
data: Spanned<Readable>,
|
||||
/// The delimiter that separates columns in the CSV file.
|
||||
/// Must be a single ASCII character.
|
||||
#[named]
|
||||
#[default]
|
||||
delimiter: Delimiter,
|
||||
) -> SourceResult<Array> {
|
||||
let Spanned { v: data, span } = data;
|
||||
let mut builder = csv::ReaderBuilder::new();
|
||||
builder.has_headers(false);
|
||||
builder.delimiter(delimiter.0 as u8);
|
||||
let mut reader = builder.from_reader(data.as_slice());
|
||||
let mut array = Array::new();
|
||||
#[scope]
|
||||
impl csv {
|
||||
/// Reads structured data from a CSV string/bytes.
|
||||
#[func(title = "Decode CSV")]
|
||||
pub fn decode(
|
||||
/// CSV data.
|
||||
data: Spanned<Readable>,
|
||||
/// The delimiter that separates columns in the CSV file.
|
||||
/// Must be a single ASCII character.
|
||||
#[named]
|
||||
#[default]
|
||||
delimiter: Delimiter,
|
||||
) -> SourceResult<Array> {
|
||||
let Spanned { v: data, span } = data;
|
||||
let mut builder = ::csv::ReaderBuilder::new();
|
||||
builder.has_headers(false);
|
||||
builder.delimiter(delimiter.0 as u8);
|
||||
let mut reader = builder.from_reader(data.as_slice());
|
||||
let mut array = Array::new();
|
||||
|
||||
for (line, result) in reader.records().enumerate() {
|
||||
// Original solution use line from error, but that is incorrect with
|
||||
// `has_headers` set to `false`. See issue:
|
||||
// https://github.com/BurntSushi/rust-csv/issues/184
|
||||
let line = line + 1; // Counting lines from 1
|
||||
let row = result.map_err(|err| format_csv_error(err, line)).at(span)?;
|
||||
let sub = row.into_iter().map(|field| field.into_value()).collect();
|
||||
array.push(Value::Array(sub))
|
||||
for (line, result) in reader.records().enumerate() {
|
||||
// Original solution use line from error, but that is incorrect with
|
||||
// `has_headers` set to `false`. See issue:
|
||||
// https://github.com/BurntSushi/rust-csv/issues/184
|
||||
let line = line + 1; // Counting lines from 1
|
||||
let row = result.map_err(|err| format_csv_error(err, line)).at(span)?;
|
||||
let sub = row.into_iter().map(|field| field.into_value()).collect();
|
||||
array.push(Value::Array(sub))
|
||||
}
|
||||
|
||||
Ok(array)
|
||||
}
|
||||
|
||||
Ok(array)
|
||||
}
|
||||
|
||||
/// The delimiter to use when parsing CSV files.
|
||||
@ -198,10 +198,10 @@ cast! {
|
||||
}
|
||||
|
||||
/// Format the user-facing CSV error message.
|
||||
fn format_csv_error(err: csv::Error, line: usize) -> EcoString {
|
||||
fn format_csv_error(err: ::csv::Error, line: usize) -> EcoString {
|
||||
match err.kind() {
|
||||
csv::ErrorKind::Utf8 { .. } => "file is not valid utf-8".into(),
|
||||
csv::ErrorKind::UnequalLengths { expected_len, len, .. } => {
|
||||
::csv::ErrorKind::Utf8 { .. } => "file is not valid utf-8".into(),
|
||||
::csv::ErrorKind::UnequalLengths { expected_len, len, .. } => {
|
||||
eco_format!(
|
||||
"failed to parse CSV (found {len} instead of \
|
||||
{expected_len} fields in line {line})"
|
||||
@ -224,7 +224,7 @@ fn format_csv_error(err: csv::Error, line: usize) -> EcoString {
|
||||
/// The JSON files in the example contain objects with the keys `temperature`,
|
||||
/// `unit`, and `weather`.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// # Example
|
||||
/// ```example
|
||||
/// #let forecast(day) = block[
|
||||
/// #box(square(
|
||||
@ -248,64 +248,53 @@ fn format_csv_error(err: csv::Error, line: usize) -> EcoString {
|
||||
/// #forecast(json("monday.json"))
|
||||
/// #forecast(json("tuesday.json"))
|
||||
/// ```
|
||||
///
|
||||
/// Display: JSON
|
||||
/// Category: data-loading
|
||||
#[func]
|
||||
#[scope(
|
||||
scope.define("decode", json_decode_func());
|
||||
scope.define("encode", json_encode_func());
|
||||
scope
|
||||
)]
|
||||
#[func(scope, title = "JSON")]
|
||||
pub fn json(
|
||||
/// Path to a JSON file.
|
||||
path: Spanned<EcoString>,
|
||||
/// The virtual machine.
|
||||
vm: &mut Vm,
|
||||
/// Path to a JSON file.
|
||||
path: Spanned<EcoString>,
|
||||
) -> SourceResult<Value> {
|
||||
let Spanned { v: path, span } = path;
|
||||
let id = vm.resolve_path(&path).at(span)?;
|
||||
let data = vm.world().file(id).at(span)?;
|
||||
json_decode(Spanned::new(Readable::Bytes(data), span))
|
||||
json::decode(Spanned::new(Readable::Bytes(data), span))
|
||||
}
|
||||
|
||||
/// Reads structured data from a JSON string/bytes.
|
||||
///
|
||||
/// Display: JSON
|
||||
/// Category: data-loading
|
||||
#[func]
|
||||
pub fn json_decode(
|
||||
/// JSON data.
|
||||
data: Spanned<Readable>,
|
||||
) -> SourceResult<Value> {
|
||||
let Spanned { v: data, span } = data;
|
||||
serde_json::from_slice(data.as_slice())
|
||||
.map_err(|err| eco_format!("failed to parse JSON ({err})"))
|
||||
.at(span)
|
||||
}
|
||||
|
||||
/// Encodes structured data into a JSON string.
|
||||
///
|
||||
/// Display: Encode JSON
|
||||
/// Category: data-loading
|
||||
#[func]
|
||||
pub fn json_encode(
|
||||
/// Value to be encoded.
|
||||
value: Spanned<Value>,
|
||||
/// Whether to pretty print the JSON with newlines and indentation.
|
||||
#[named]
|
||||
#[default(true)]
|
||||
pretty: bool,
|
||||
) -> SourceResult<Str> {
|
||||
let Spanned { v: value, span } = value;
|
||||
if pretty {
|
||||
serde_json::to_string_pretty(&value)
|
||||
} else {
|
||||
serde_json::to_string(&value)
|
||||
#[scope]
|
||||
impl json {
|
||||
/// Reads structured data from a JSON string/bytes.
|
||||
#[func(title = "Decode JSON")]
|
||||
pub fn decode(
|
||||
/// JSON data.
|
||||
data: Spanned<Readable>,
|
||||
) -> SourceResult<Value> {
|
||||
let Spanned { v: data, span } = data;
|
||||
serde_json::from_slice(data.as_slice())
|
||||
.map_err(|err| eco_format!("failed to parse JSON ({err})"))
|
||||
.at(span)
|
||||
}
|
||||
|
||||
/// Encodes structured data into a JSON string.
|
||||
#[func(title = "Encode JSON")]
|
||||
pub fn encode(
|
||||
/// Value to be encoded.
|
||||
value: Spanned<Value>,
|
||||
/// Whether to pretty print the JSON with newlines and indentation.
|
||||
#[named]
|
||||
#[default(true)]
|
||||
pretty: bool,
|
||||
) -> SourceResult<Str> {
|
||||
let Spanned { v: value, span } = value;
|
||||
if pretty {
|
||||
serde_json::to_string_pretty(&value)
|
||||
} else {
|
||||
serde_json::to_string(&value)
|
||||
}
|
||||
.map(|v| v.into())
|
||||
.map_err(|err| eco_format!("failed to encode value as JSON ({err})"))
|
||||
.at(span)
|
||||
}
|
||||
.map(|v| v.into())
|
||||
.map_err(|err| eco_format!("failed to encode value as JSON ({err})"))
|
||||
.at(span)
|
||||
}
|
||||
|
||||
/// Reads structured data from a TOML file.
|
||||
@ -319,7 +308,7 @@ pub fn json_encode(
|
||||
/// The TOML file in the example consists of a table with the keys `title`,
|
||||
/// `version`, and `authors`.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// # Example
|
||||
/// ```example
|
||||
/// #let details = toml("details.toml")
|
||||
///
|
||||
@ -328,67 +317,56 @@ pub fn json_encode(
|
||||
/// Authors: #(details.authors
|
||||
/// .join(", ", last: " and "))
|
||||
/// ```
|
||||
///
|
||||
/// Display: TOML
|
||||
/// Category: data-loading
|
||||
#[func]
|
||||
#[scope(
|
||||
scope.define("decode", toml_decode_func());
|
||||
scope.define("encode", toml_encode_func());
|
||||
scope
|
||||
)]
|
||||
#[func(scope, title = "TOML")]
|
||||
pub fn toml(
|
||||
/// Path to a TOML file.
|
||||
path: Spanned<EcoString>,
|
||||
/// The virtual machine.
|
||||
vm: &mut Vm,
|
||||
/// Path to a TOML file.
|
||||
path: Spanned<EcoString>,
|
||||
) -> SourceResult<Value> {
|
||||
let Spanned { v: path, span } = path;
|
||||
let id = vm.resolve_path(&path).at(span)?;
|
||||
let data = vm.world().file(id).at(span)?;
|
||||
toml_decode(Spanned::new(Readable::Bytes(data), span))
|
||||
toml::decode(Spanned::new(Readable::Bytes(data), span))
|
||||
}
|
||||
|
||||
/// Reads structured data from a TOML string/bytes.
|
||||
///
|
||||
/// Display: Decode TOML
|
||||
/// Category: data-loading
|
||||
#[func]
|
||||
pub fn toml_decode(
|
||||
/// TOML data.
|
||||
data: Spanned<Readable>,
|
||||
) -> SourceResult<Value> {
|
||||
let Spanned { v: data, span } = data;
|
||||
let raw = std::str::from_utf8(data.as_slice())
|
||||
.map_err(|_| "file is not valid utf-8")
|
||||
.at(span)?;
|
||||
toml::from_str(raw)
|
||||
.map_err(|err| format_toml_error(err, raw))
|
||||
.at(span)
|
||||
}
|
||||
#[scope]
|
||||
impl toml {
|
||||
/// Reads structured data from a TOML string/bytes.
|
||||
#[func(title = "Decode TOML")]
|
||||
pub fn decode(
|
||||
/// TOML data.
|
||||
data: Spanned<Readable>,
|
||||
) -> SourceResult<Value> {
|
||||
let Spanned { v: data, span } = data;
|
||||
let raw = std::str::from_utf8(data.as_slice())
|
||||
.map_err(|_| "file is not valid utf-8")
|
||||
.at(span)?;
|
||||
::toml::from_str(raw)
|
||||
.map_err(|err| format_toml_error(err, raw))
|
||||
.at(span)
|
||||
}
|
||||
|
||||
/// Encodes structured data into a TOML string.
|
||||
///
|
||||
/// Display: Encode TOML
|
||||
/// Category: data-loading
|
||||
#[func]
|
||||
pub fn toml_encode(
|
||||
/// Value to be encoded.
|
||||
value: Spanned<Value>,
|
||||
/// Whether to pretty-print the resulting TOML.
|
||||
#[named]
|
||||
#[default(true)]
|
||||
pretty: bool,
|
||||
) -> SourceResult<Str> {
|
||||
let Spanned { v: value, span } = value;
|
||||
if pretty { toml::to_string_pretty(&value) } else { toml::to_string(&value) }
|
||||
.map(|v| v.into())
|
||||
.map_err(|err| eco_format!("failed to encode value as TOML ({err})"))
|
||||
.at(span)
|
||||
/// Encodes structured data into a TOML string.
|
||||
#[func(title = "Encode TOML")]
|
||||
pub fn encode(
|
||||
/// Value to be encoded.
|
||||
value: Spanned<Value>,
|
||||
/// Whether to pretty-print the resulting TOML.
|
||||
#[named]
|
||||
#[default(true)]
|
||||
pretty: bool,
|
||||
) -> SourceResult<Str> {
|
||||
let Spanned { v: value, span } = value;
|
||||
if pretty { ::toml::to_string_pretty(&value) } else { ::toml::to_string(&value) }
|
||||
.map(|v| v.into())
|
||||
.map_err(|err| eco_format!("failed to encode value as TOML ({err})"))
|
||||
.at(span)
|
||||
}
|
||||
}
|
||||
|
||||
/// Format the user-facing TOML error message.
|
||||
fn format_toml_error(error: toml::de::Error, raw: &str) -> EcoString {
|
||||
fn format_toml_error(error: ::toml::de::Error, raw: &str) -> EcoString {
|
||||
if let Some(head) = error.span().and_then(|range| raw.get(..range.start)) {
|
||||
let line = head.lines().count();
|
||||
let column = 1 + head.chars().rev().take_while(|&c| !is_newline(c)).count();
|
||||
@ -415,7 +393,7 @@ fn format_toml_error(error: toml::de::Error, raw: &str) -> EcoString {
|
||||
/// each with a sequence of their own submapping with the keys
|
||||
/// "title" and "published"
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// # Example
|
||||
/// ```example
|
||||
/// #let bookshelf(contents) = {
|
||||
/// for (author, works) in contents {
|
||||
@ -430,56 +408,45 @@ fn format_toml_error(error: toml::de::Error, raw: &str) -> EcoString {
|
||||
/// yaml("scifi-authors.yaml")
|
||||
/// )
|
||||
/// ```
|
||||
///
|
||||
/// Display: YAML
|
||||
/// Category: data-loading
|
||||
#[func]
|
||||
#[scope(
|
||||
scope.define("decode", yaml_decode_func());
|
||||
scope.define("encode", yaml_encode_func());
|
||||
scope
|
||||
)]
|
||||
#[func(scope, title = "YAML")]
|
||||
pub fn yaml(
|
||||
/// Path to a YAML file.
|
||||
path: Spanned<EcoString>,
|
||||
/// The virtual machine.
|
||||
vm: &mut Vm,
|
||||
/// Path to a YAML file.
|
||||
path: Spanned<EcoString>,
|
||||
) -> SourceResult<Value> {
|
||||
let Spanned { v: path, span } = path;
|
||||
let id = vm.resolve_path(&path).at(span)?;
|
||||
let data = vm.world().file(id).at(span)?;
|
||||
yaml_decode(Spanned::new(Readable::Bytes(data), span))
|
||||
yaml::decode(Spanned::new(Readable::Bytes(data), span))
|
||||
}
|
||||
|
||||
/// Reads structured data from a YAML string/bytes.
|
||||
///
|
||||
/// Display: Decode YAML
|
||||
/// Category: data-loading
|
||||
#[func]
|
||||
pub fn yaml_decode(
|
||||
/// YAML data.
|
||||
data: Spanned<Readable>,
|
||||
) -> SourceResult<Value> {
|
||||
let Spanned { v: data, span } = data;
|
||||
serde_yaml::from_slice(data.as_slice())
|
||||
.map_err(|err| eco_format!("failed to parse YAML ({err})"))
|
||||
.at(span)
|
||||
}
|
||||
#[scope]
|
||||
impl yaml {
|
||||
/// Reads structured data from a YAML string/bytes.
|
||||
#[func(title = "Decode YAML")]
|
||||
pub fn decode(
|
||||
/// YAML data.
|
||||
data: Spanned<Readable>,
|
||||
) -> SourceResult<Value> {
|
||||
let Spanned { v: data, span } = data;
|
||||
serde_yaml::from_slice(data.as_slice())
|
||||
.map_err(|err| eco_format!("failed to parse YAML ({err})"))
|
||||
.at(span)
|
||||
}
|
||||
|
||||
/// Encode structured data into a YAML string.
|
||||
///
|
||||
/// Display: Encode YAML
|
||||
/// Category: data-loading
|
||||
#[func]
|
||||
pub fn yaml_encode(
|
||||
/// Value to be encoded.
|
||||
value: Spanned<Value>,
|
||||
) -> SourceResult<Str> {
|
||||
let Spanned { v: value, span } = value;
|
||||
serde_yaml::to_string(&value)
|
||||
.map(|v| v.into())
|
||||
.map_err(|err| eco_format!("failed to encode value as YAML ({err})"))
|
||||
.at(span)
|
||||
/// Encode structured data into a YAML string.
|
||||
#[func(title = "Encode YAML")]
|
||||
pub fn encode(
|
||||
/// Value to be encoded.
|
||||
value: Spanned<Value>,
|
||||
) -> SourceResult<Str> {
|
||||
let Spanned { v: value, span } = value;
|
||||
serde_yaml::to_string(&value)
|
||||
.map(|v| v.into())
|
||||
.map_err(|err| eco_format!("failed to encode value as YAML ({err})"))
|
||||
.at(span)
|
||||
}
|
||||
}
|
||||
|
||||
/// Reads structured data from a CBOR file.
|
||||
@ -490,57 +457,46 @@ pub fn yaml_encode(
|
||||
/// equivalents, null-values (`null`, `~` or empty ``) will be converted into
|
||||
/// `{none}`, and numbers will be converted to floats or integers depending on
|
||||
/// whether they are whole numbers.
|
||||
///
|
||||
/// Display: CBOR
|
||||
/// Category: data-loading
|
||||
#[func]
|
||||
#[scope(
|
||||
scope.define("decode", cbor_decode_func());
|
||||
scope.define("encode", cbor_encode_func());
|
||||
scope
|
||||
)]
|
||||
#[func(scope, title = "CBOR")]
|
||||
pub fn cbor(
|
||||
/// Path to a CBOR file.
|
||||
path: Spanned<EcoString>,
|
||||
/// The virtual machine.
|
||||
vm: &mut Vm,
|
||||
/// Path to a CBOR file.
|
||||
path: Spanned<EcoString>,
|
||||
) -> SourceResult<Value> {
|
||||
let Spanned { v: path, span } = path;
|
||||
let id = vm.resolve_path(&path).at(span)?;
|
||||
let data = vm.world().file(id).at(span)?;
|
||||
cbor_decode(Spanned::new(data, span))
|
||||
cbor::decode(Spanned::new(data, span))
|
||||
}
|
||||
|
||||
/// Reads structured data from CBOR bytes.
|
||||
///
|
||||
/// Display: Decode CBOR
|
||||
/// Category: data-loading
|
||||
#[func]
|
||||
pub fn cbor_decode(
|
||||
/// cbor data.
|
||||
data: Spanned<Bytes>,
|
||||
) -> SourceResult<Value> {
|
||||
let Spanned { v: data, span } = data;
|
||||
ciborium::from_reader(data.as_slice())
|
||||
.map_err(|err| eco_format!("failed to parse CBOR ({err})"))
|
||||
.at(span)
|
||||
}
|
||||
#[scope]
|
||||
impl cbor {
|
||||
/// Reads structured data from CBOR bytes.
|
||||
#[func(title = "Decode CBOR")]
|
||||
pub fn decode(
|
||||
/// cbor data.
|
||||
data: Spanned<Bytes>,
|
||||
) -> SourceResult<Value> {
|
||||
let Spanned { v: data, span } = data;
|
||||
ciborium::from_reader(data.as_slice())
|
||||
.map_err(|err| eco_format!("failed to parse CBOR ({err})"))
|
||||
.at(span)
|
||||
}
|
||||
|
||||
/// Encode structured data into CBOR bytes.
|
||||
///
|
||||
/// Display: Encode CBOR
|
||||
/// Category: data-loading
|
||||
#[func]
|
||||
pub fn cbor_encode(
|
||||
/// Value to be encoded.
|
||||
value: Spanned<Value>,
|
||||
) -> SourceResult<Bytes> {
|
||||
let Spanned { v: value, span } = value;
|
||||
let mut res = Vec::new();
|
||||
ciborium::into_writer(&value, &mut res)
|
||||
.map(|_| res.into())
|
||||
.map_err(|err| eco_format!("failed to encode value as CBOR ({err})"))
|
||||
.at(span)
|
||||
/// Encode structured data into CBOR bytes.
|
||||
#[func(title = "Encode CBOR")]
|
||||
pub fn encode(
|
||||
/// Value to be encoded.
|
||||
value: Spanned<Value>,
|
||||
) -> SourceResult<Bytes> {
|
||||
let Spanned { v: value, span } = value;
|
||||
let mut res = Vec::new();
|
||||
ciborium::into_writer(&value, &mut res)
|
||||
.map(|_| res.into())
|
||||
.map_err(|err| eco_format!("failed to encode value as CBOR ({err})"))
|
||||
.at(span)
|
||||
}
|
||||
}
|
||||
|
||||
/// Reads structured data from an XML file.
|
||||
@ -558,7 +514,7 @@ pub fn cbor_encode(
|
||||
/// `content` tag contains one or more paragraphs, which are represented as `p`
|
||||
/// tags.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// # Example
|
||||
/// ```example
|
||||
/// #let find-child(elem, tag) = {
|
||||
/// elem.children
|
||||
@ -591,41 +547,35 @@ pub fn cbor_encode(
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Display: XML
|
||||
/// Category: data-loading
|
||||
#[func]
|
||||
#[scope(
|
||||
scope.define("decode", xml_decode_func());
|
||||
scope
|
||||
)]
|
||||
#[func(scope, title = "XML")]
|
||||
pub fn xml(
|
||||
/// Path to an XML file.
|
||||
path: Spanned<EcoString>,
|
||||
/// The virtual machine.
|
||||
vm: &mut Vm,
|
||||
/// Path to an XML file.
|
||||
path: Spanned<EcoString>,
|
||||
) -> SourceResult<Value> {
|
||||
let Spanned { v: path, span } = path;
|
||||
let id = vm.resolve_path(&path).at(span)?;
|
||||
let data = vm.world().file(id).at(span)?;
|
||||
xml_decode(Spanned::new(Readable::Bytes(data), span))
|
||||
xml::decode(Spanned::new(Readable::Bytes(data), span))
|
||||
}
|
||||
|
||||
/// Reads structured data from an XML string/bytes.
|
||||
///
|
||||
/// Display: Decode XML
|
||||
/// Category: data-loading
|
||||
#[func]
|
||||
pub fn xml_decode(
|
||||
/// XML data.
|
||||
data: Spanned<Readable>,
|
||||
) -> SourceResult<Value> {
|
||||
let Spanned { v: data, span } = data;
|
||||
let text = std::str::from_utf8(data.as_slice())
|
||||
.map_err(FileError::from)
|
||||
.at(span)?;
|
||||
let document = roxmltree::Document::parse(text).map_err(format_xml_error).at(span)?;
|
||||
Ok(convert_xml(document.root()))
|
||||
#[scope]
|
||||
impl xml {
|
||||
/// Reads structured data from an XML string/bytes.
|
||||
#[func(title = "Decode XML")]
|
||||
pub fn decode(
|
||||
/// XML data.
|
||||
data: Spanned<Readable>,
|
||||
) -> SourceResult<Value> {
|
||||
let Spanned { v: data, span } = data;
|
||||
let text = std::str::from_utf8(data.as_slice())
|
||||
.map_err(FileError::from)
|
||||
.at(span)?;
|
||||
let document =
|
||||
roxmltree::Document::parse(text).map_err(format_xml_error).at(span)?;
|
||||
Ok(convert_xml(document.root()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert an XML node to a Typst value.
|
||||
|
@ -1,29 +1,32 @@
|
||||
use typst::eval::EvalMode;
|
||||
use typst::eval::{
|
||||
Datetime, Duration, EvalMode, Module, Never, NoneValue, Plugin, Regex,
|
||||
};
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
/// Determines the type of a value.
|
||||
///
|
||||
/// Returns the name of the value's type.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// ```example
|
||||
/// #type(12) \
|
||||
/// #type(14.7) \
|
||||
/// #type("hello") \
|
||||
/// #type(none) \
|
||||
/// #type([Hi]) \
|
||||
/// #type(x => x + 1)
|
||||
/// ```
|
||||
///
|
||||
/// Display: Type
|
||||
/// Category: foundations
|
||||
#[func]
|
||||
pub fn type_(
|
||||
/// The value whose type's to determine.
|
||||
value: Value,
|
||||
) -> Str {
|
||||
value.type_name().into()
|
||||
/// Hook up all foundational definitions.
|
||||
pub(super) fn define(global: &mut Scope) {
|
||||
global.category("foundations");
|
||||
global.define_type::<bool>();
|
||||
global.define_type::<i64>();
|
||||
global.define_type::<f64>();
|
||||
global.define_type::<Str>();
|
||||
global.define_type::<Bytes>();
|
||||
global.define_type::<Content>();
|
||||
global.define_type::<Array>();
|
||||
global.define_type::<Dict>();
|
||||
global.define_type::<Func>();
|
||||
global.define_type::<Args>();
|
||||
global.define_type::<Type>();
|
||||
global.define_type::<Module>();
|
||||
global.define_type::<Regex>();
|
||||
global.define_type::<Datetime>();
|
||||
global.define_type::<Duration>();
|
||||
global.define_type::<Plugin>();
|
||||
global.define_func::<repr>();
|
||||
global.define_func::<panic>();
|
||||
global.define_func::<assert>();
|
||||
global.define_func::<eval>();
|
||||
}
|
||||
|
||||
/// Returns the string representation of a value.
|
||||
@ -35,17 +38,14 @@ pub fn type_(
|
||||
/// **Note:** This function is for debugging purposes. Its output should not be
|
||||
/// considered stable and may change at any time!
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// # Example
|
||||
/// ```example
|
||||
/// #none vs #repr(none) \
|
||||
/// #"hello" vs #repr("hello") \
|
||||
/// #(1, 2) vs #repr((1, 2)) \
|
||||
/// #[*Hi*] vs #repr([*Hi*])
|
||||
/// ```
|
||||
///
|
||||
/// Display: Representation
|
||||
/// Category: foundations
|
||||
#[func]
|
||||
#[func(title = "Representation")]
|
||||
pub fn repr(
|
||||
/// The value whose string representation to produce.
|
||||
value: Value,
|
||||
@ -55,16 +55,12 @@ pub fn repr(
|
||||
|
||||
/// Fails with an error.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// # Example
|
||||
/// The code below produces the error `panicked with: "this is wrong"`.
|
||||
/// ```typ
|
||||
/// #panic("this is wrong")
|
||||
/// ```
|
||||
///
|
||||
/// Display: Panic
|
||||
/// Category: foundations
|
||||
/// Keywords: error
|
||||
#[func]
|
||||
#[func(keywords = ["error"])]
|
||||
pub fn panic(
|
||||
/// The values to panic with.
|
||||
#[variadic]
|
||||
@ -89,21 +85,13 @@ pub fn panic(
|
||||
/// produce any output in the document.
|
||||
///
|
||||
/// If you wish to test equality between two values, see
|
||||
/// [`assert.eq`]($func/assert.eq) and [`assert.ne`]($func/assert.ne).
|
||||
/// [`assert.eq`]($assert.eq) and [`assert.ne`]($assert.ne).
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// # Example
|
||||
/// ```typ
|
||||
/// #assert(1 < 2, message: "math broke")
|
||||
/// ```
|
||||
///
|
||||
/// Display: Assert
|
||||
/// Category: foundations
|
||||
#[func]
|
||||
#[scope(
|
||||
scope.define("eq", assert_eq_func());
|
||||
scope.define("ne", assert_ne_func());
|
||||
scope
|
||||
)]
|
||||
#[func(scope)]
|
||||
pub fn assert(
|
||||
/// The condition that must be true for the assertion to pass.
|
||||
condition: bool,
|
||||
@ -121,91 +109,83 @@ pub fn assert(
|
||||
Ok(NoneValue)
|
||||
}
|
||||
|
||||
/// Ensures that two values are equal.
|
||||
///
|
||||
/// Fails with an error if the first value is not equal to the second. Does not
|
||||
/// produce any output in the document.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// ```typ
|
||||
/// #assert.eq(10, 10)
|
||||
/// ```
|
||||
///
|
||||
/// Display: Assert Equals
|
||||
/// Category: foundations
|
||||
#[func]
|
||||
pub fn assert_eq(
|
||||
/// The first value to compare.
|
||||
left: Value,
|
||||
|
||||
/// The second value to compare.
|
||||
right: Value,
|
||||
|
||||
/// An optional message to display on error instead of the representations
|
||||
/// of the compared values.
|
||||
#[named]
|
||||
message: Option<EcoString>,
|
||||
) -> StrResult<NoneValue> {
|
||||
if left != right {
|
||||
if let Some(message) = message {
|
||||
bail!("equality assertion failed: {message}");
|
||||
} else {
|
||||
bail!("equality assertion failed: value {left:?} was not equal to {right:?}");
|
||||
#[scope]
|
||||
impl assert {
|
||||
/// Ensures that two values are equal.
|
||||
///
|
||||
/// Fails with an error if the first value is not equal to the second. Does not
|
||||
/// produce any output in the document.
|
||||
///
|
||||
/// ```typ
|
||||
/// #assert.eq(10, 10)
|
||||
/// ```
|
||||
#[func(title = "Assert Equal")]
|
||||
pub fn eq(
|
||||
/// The first value to compare.
|
||||
left: Value,
|
||||
/// The second value to compare.
|
||||
right: Value,
|
||||
/// An optional message to display on error instead of the representations
|
||||
/// of the compared values.
|
||||
#[named]
|
||||
message: Option<EcoString>,
|
||||
) -> StrResult<NoneValue> {
|
||||
if left != right {
|
||||
if let Some(message) = message {
|
||||
bail!("equality assertion failed: {message}");
|
||||
} else {
|
||||
bail!("equality assertion failed: value {left:?} was not equal to {right:?}");
|
||||
}
|
||||
}
|
||||
Ok(NoneValue)
|
||||
}
|
||||
Ok(NoneValue)
|
||||
}
|
||||
|
||||
/// Ensures that two values are not equal.
|
||||
///
|
||||
/// Fails with an error if the first value is equal to the second. Does not
|
||||
/// produce any output in the document.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// ```typ
|
||||
/// #assert.ne(3, 4)
|
||||
/// ```
|
||||
///
|
||||
/// Display: Assert Not Equals
|
||||
/// Category: foundations
|
||||
#[func]
|
||||
pub fn assert_ne(
|
||||
/// The first value to compare.
|
||||
left: Value,
|
||||
|
||||
/// The second value to compare.
|
||||
right: Value,
|
||||
|
||||
/// An optional message to display on error instead of the representations
|
||||
/// of the compared values.
|
||||
#[named]
|
||||
message: Option<EcoString>,
|
||||
) -> StrResult<NoneValue> {
|
||||
if left == right {
|
||||
if let Some(message) = message {
|
||||
bail!("inequality assertion failed: {message}");
|
||||
} else {
|
||||
bail!("inequality assertion failed: value {left:?} was equal to {right:?}");
|
||||
/// Ensures that two values are not equal.
|
||||
///
|
||||
/// Fails with an error if the first value is equal to the second. Does not
|
||||
/// produce any output in the document.
|
||||
///
|
||||
/// ```typ
|
||||
/// #assert.ne(3, 4)
|
||||
/// ```
|
||||
#[func(title = "Assert Not Equal")]
|
||||
pub fn ne(
|
||||
/// The first value to compare.
|
||||
left: Value,
|
||||
/// The second value to compare.
|
||||
right: Value,
|
||||
/// An optional message to display on error instead of the representations
|
||||
/// of the compared values.
|
||||
#[named]
|
||||
message: Option<EcoString>,
|
||||
) -> StrResult<NoneValue> {
|
||||
if left == right {
|
||||
if let Some(message) = message {
|
||||
bail!("inequality assertion failed: {message}");
|
||||
} else {
|
||||
bail!(
|
||||
"inequality assertion failed: value {left:?} was equal to {right:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
Ok(NoneValue)
|
||||
}
|
||||
Ok(NoneValue)
|
||||
}
|
||||
|
||||
/// Evaluates a string as Typst code.
|
||||
///
|
||||
/// This function should only be used as a last resort.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// # Example
|
||||
/// ```example
|
||||
/// #eval("1 + 1") \
|
||||
/// #eval("(1, 2, 3, 4)").len() \
|
||||
/// #eval("*Markup!*", mode: "markup") \
|
||||
/// ```
|
||||
///
|
||||
/// Display: Evaluate
|
||||
/// Category: foundations
|
||||
#[func]
|
||||
#[func(title = "Evaluate")]
|
||||
pub fn eval(
|
||||
/// The virtual machine.
|
||||
vm: &mut Vm,
|
||||
/// A string of Typst code to evaluate.
|
||||
///
|
||||
/// The code in the string cannot interact with the file system.
|
||||
@ -235,8 +215,6 @@ pub fn eval(
|
||||
#[named]
|
||||
#[default]
|
||||
scope: Dict,
|
||||
/// The virtual machine.
|
||||
vm: &mut Vm,
|
||||
) -> SourceResult<Value> {
|
||||
let Spanned { v: text, span } = source;
|
||||
let dict = scope;
|
||||
|
@ -1,11 +1,10 @@
|
||||
//! Computational functions.
|
||||
|
||||
pub mod calc;
|
||||
mod construct;
|
||||
|
||||
mod data;
|
||||
mod foundations;
|
||||
|
||||
pub use self::construct::*;
|
||||
pub use self::data::*;
|
||||
pub use self::foundations::*;
|
||||
|
||||
@ -13,33 +12,7 @@ use crate::prelude::*;
|
||||
|
||||
/// Hook up all compute definitions.
|
||||
pub(super) fn define(global: &mut Scope) {
|
||||
global.define("type", type_func());
|
||||
global.define("repr", repr_func());
|
||||
global.define("panic", panic_func());
|
||||
global.define("assert", assert_func());
|
||||
global.define("eval", eval_func());
|
||||
global.define("int", int_func());
|
||||
global.define("float", float_func());
|
||||
global.define("luma", luma_func());
|
||||
global.define("rgb", rgb_func());
|
||||
global.define("cmyk", cmyk_func());
|
||||
global.define("color", color_module());
|
||||
global.define("datetime", datetime_func());
|
||||
global.define("duration", duration_func());
|
||||
global.define("symbol", symbol_func());
|
||||
global.define("str", str_func());
|
||||
global.define("bytes", bytes_func());
|
||||
global.define("label", label_func());
|
||||
global.define("regex", regex_func());
|
||||
global.define("array", array_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("cbor", cbor_func());
|
||||
global.define("xml", xml_func());
|
||||
global.define("calc", calc::module());
|
||||
global.define("plugin", plugin_func());
|
||||
self::foundations::define(global);
|
||||
self::data::define(global);
|
||||
self::calc::define(global);
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ use crate::prelude::*;
|
||||
|
||||
/// Aligns content horizontally and vertically.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// # Example
|
||||
/// ```example
|
||||
/// #set align(center)
|
||||
///
|
||||
@ -11,43 +11,9 @@ use crate::prelude::*;
|
||||
/// Not left nor right, it stands alone \
|
||||
/// A work of art, a visual throne
|
||||
/// ```
|
||||
///
|
||||
/// Display: Align
|
||||
/// Category: layout
|
||||
#[element(Show)]
|
||||
#[elem(Show)]
|
||||
pub struct AlignElem {
|
||||
/// The alignment along both axes.
|
||||
///
|
||||
/// Possible values for horizontal alignments are:
|
||||
/// - `start`
|
||||
/// - `end`
|
||||
/// - `left`
|
||||
/// - `center`
|
||||
/// - `right`
|
||||
///
|
||||
/// The `start` and `end` alignments are relative to the current [text
|
||||
/// direction]($func/text.dir).
|
||||
///
|
||||
/// Possible values for vertical alignments are:
|
||||
/// - `top`
|
||||
/// - `horizon`
|
||||
/// - `bottom`
|
||||
///
|
||||
/// You can use the `axis` method on a single-axis alignment to obtain
|
||||
/// whether it is `{"horizontal"}` or `{"vertical"}`. You can also use the
|
||||
/// `inv` method to obtain its inverse alignment. For example,
|
||||
/// `{top.axis()}` is `{"vertical"}`, while `{top.inv()}` is equal to
|
||||
/// `{bottom}`.
|
||||
///
|
||||
/// To align along both axes at the same time, add the two alignments using
|
||||
/// the `+` operator to get a `2d alignment`. For example, `top + right`
|
||||
/// aligns the content to the top right corner.
|
||||
///
|
||||
/// For 2d alignments, the `x` and `y` fields hold their horizontal and
|
||||
/// vertical components, respectively. Additionally, you can use the `inv`
|
||||
/// method to obtain a 2d alignment with both components inverted. For
|
||||
/// instance, `{(top + right).x}` is `right`, `{(top + right).y}` is `top`,
|
||||
/// and `{(top + right).inv()}` is equal to `bottom + left`.
|
||||
/// The [alignment]($alignment) along both axes.
|
||||
///
|
||||
/// ```example
|
||||
/// #set page(height: 6cm)
|
||||
@ -61,8 +27,8 @@ pub struct AlignElem {
|
||||
/// ```
|
||||
#[positional]
|
||||
#[fold]
|
||||
#[default(Axes::new(GenAlign::Start, GenAlign::Specific(Align::Top)))]
|
||||
pub alignment: Axes<Option<GenAlign>>,
|
||||
#[default]
|
||||
pub alignment: Align,
|
||||
|
||||
/// The content to align.
|
||||
#[required]
|
||||
@ -72,8 +38,6 @@ pub struct AlignElem {
|
||||
impl Show for AlignElem {
|
||||
#[tracing::instrument(name = "AlignElem::show", skip_all)]
|
||||
fn show(&self, _: &mut Vt, styles: StyleChain) -> SourceResult<Content> {
|
||||
Ok(self
|
||||
.body()
|
||||
.styled(Self::set_alignment(self.alignment(styles).map(Some))))
|
||||
Ok(self.body().styled(Self::set_alignment(self.alignment(styles))))
|
||||
}
|
||||
}
|
||||
|
@ -10,9 +10,9 @@ use crate::text::TextElem;
|
||||
/// necessary.
|
||||
///
|
||||
/// If you need to insert columns across your whole document, you can use the
|
||||
/// [`{page}` function's `columns` parameter]($func/page.columns) instead.
|
||||
/// [`{page}` function's `columns` parameter]($page.columns) instead.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// # Example
|
||||
/// ```example
|
||||
/// = Towards Advanced Deep Learning
|
||||
///
|
||||
@ -32,10 +32,7 @@ use crate::text::TextElem;
|
||||
/// increasingly been used to solve a
|
||||
/// variety of problems.
|
||||
/// ```
|
||||
///
|
||||
/// Display: Columns
|
||||
/// Category: layout
|
||||
#[element(Layout)]
|
||||
#[elem(Layout)]
|
||||
pub struct ColumnsElem {
|
||||
/// The number of columns.
|
||||
#[positional]
|
||||
@ -132,11 +129,11 @@ impl Layout for ColumnsElem {
|
||||
|
||||
/// Forces a column break.
|
||||
///
|
||||
/// The function will behave like a [page break]($func/pagebreak) when used in a
|
||||
/// The function will behave like a [page break]($pagebreak) when used in a
|
||||
/// single column layout or the last column on a page. Otherwise, content after
|
||||
/// the column break will be placed in the next column.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// # Example
|
||||
/// ```example
|
||||
/// #set page(columns: 2)
|
||||
/// Preliminary findings from our
|
||||
@ -153,10 +150,7 @@ impl Layout for ColumnsElem {
|
||||
/// understanding of the fundamental
|
||||
/// laws of nature.
|
||||
/// ```
|
||||
///
|
||||
/// Display: Column Break
|
||||
/// Category: layout
|
||||
#[element(Behave)]
|
||||
#[elem(title = "Column Break", Behave)]
|
||||
pub struct ColbreakElem {
|
||||
/// If `{true}`, the column break is skipped if the current column is
|
||||
/// already empty.
|
||||
|
@ -11,7 +11,7 @@ use crate::prelude::*;
|
||||
/// elements into a paragraph. Boxes take the size of their contents by default
|
||||
/// but can also be sized explicitly.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// # Example
|
||||
/// ```example
|
||||
/// Refer to the docs
|
||||
/// #box(
|
||||
@ -20,15 +20,12 @@ use crate::prelude::*;
|
||||
/// )
|
||||
/// for more information.
|
||||
/// ```
|
||||
///
|
||||
/// Display: Box
|
||||
/// Category: layout
|
||||
#[element(Layout)]
|
||||
#[elem(Layout)]
|
||||
pub struct BoxElem {
|
||||
/// The width of the box.
|
||||
///
|
||||
/// Boxes can have [fractional]($type/fraction) widths, as the example
|
||||
/// below demonstrates.
|
||||
/// Boxes can have [fractional]($fraction) widths, as the example below
|
||||
/// demonstrates.
|
||||
///
|
||||
/// _Note:_ Currently, only boxes and only their widths might be fractionally
|
||||
/// sized within paragraphs. Support for fractionally sized images, shapes,
|
||||
@ -51,23 +48,29 @@ pub struct BoxElem {
|
||||
pub baseline: Rel<Length>,
|
||||
|
||||
/// The box's background color. See the
|
||||
/// [rectangle's documentation]($func/rect.fill) for more details.
|
||||
/// [rectangle's documentation]($rect.fill) for more details.
|
||||
pub fill: Option<Paint>,
|
||||
|
||||
/// The box's border color. See the
|
||||
/// [rectangle's documentation]($func/rect.stroke) for more details.
|
||||
/// [rectangle's documentation]($rect.stroke) for more details.
|
||||
#[resolve]
|
||||
#[fold]
|
||||
pub stroke: Sides<Option<Option<PartialStroke>>>,
|
||||
pub stroke: Sides<Option<Option<Stroke>>>,
|
||||
|
||||
/// How much to round the box's corners. See the [rectangle's
|
||||
/// documentation]($func/rect.radius) for more details.
|
||||
/// How much to round the box's corners. See the
|
||||
/// [rectangle's documentation]($rect.radius) for more details.
|
||||
#[resolve]
|
||||
#[fold]
|
||||
pub radius: Corners<Option<Rel<Length>>>,
|
||||
|
||||
/// How much to pad the box's content. See the [rectangle's
|
||||
/// documentation]($func/rect.inset) for more details.
|
||||
/// How much to pad the box's content.
|
||||
///
|
||||
/// _Note:_ When the box contains text, its exact size depends on the
|
||||
/// current [text edges]($text.top-edge).
|
||||
///
|
||||
/// ```example
|
||||
/// #rect(inset: 0pt)[Tight]
|
||||
/// ```
|
||||
#[resolve]
|
||||
#[fold]
|
||||
pub inset: Sides<Option<Rel<Length>>>,
|
||||
@ -76,7 +79,7 @@ pub struct BoxElem {
|
||||
///
|
||||
/// This is useful to prevent padding from affecting line layout. For a
|
||||
/// generalized version of the example below, see the documentation for the
|
||||
/// [raw text's block parameter]($func/raw.block).
|
||||
/// [raw text's block parameter]($raw.block).
|
||||
///
|
||||
/// ```example
|
||||
/// An inline
|
||||
@ -119,8 +122,7 @@ impl Layout for BoxElem {
|
||||
let expand = sizing.as_ref().map(Smart::is_custom);
|
||||
let size = sizing
|
||||
.resolve(styles)
|
||||
.zip(regions.base())
|
||||
.map(|(s, b)| s.map(|v| v.relative_to(b)))
|
||||
.zip_map(regions.base(), |s, b| s.map(|v| v.relative_to(b)))
|
||||
.unwrap_or(regions.base());
|
||||
|
||||
// Apply inset.
|
||||
@ -151,7 +153,7 @@ impl Layout for BoxElem {
|
||||
|
||||
// Prepare fill and stroke.
|
||||
let fill = self.fill(styles);
|
||||
let stroke = self.stroke(styles).map(|s| s.map(PartialStroke::unwrap_or_default));
|
||||
let stroke = self.stroke(styles).map(|s| s.map(Stroke::unwrap_or_default));
|
||||
|
||||
// Add fill and/or stroke.
|
||||
if fill.is_some() || stroke.iter().any(Option::is_some) {
|
||||
@ -172,7 +174,7 @@ impl Layout for BoxElem {
|
||||
/// Such a container can be used to separate content, size it, and give it a
|
||||
/// background or border.
|
||||
///
|
||||
/// ## Examples { #examples }
|
||||
/// # Examples
|
||||
/// With a block, you can give a background to content while still allowing it
|
||||
/// to break across multiple pages.
|
||||
/// ```example
|
||||
@ -196,10 +198,7 @@ impl Layout for BoxElem {
|
||||
/// = Blocky
|
||||
/// More text.
|
||||
/// ```
|
||||
///
|
||||
/// Display: Block
|
||||
/// Category: layout
|
||||
#[element(Layout)]
|
||||
#[elem(Layout)]
|
||||
pub struct BlockElem {
|
||||
/// The block's width.
|
||||
///
|
||||
@ -215,7 +214,7 @@ pub struct BlockElem {
|
||||
pub width: Smart<Rel<Length>>,
|
||||
|
||||
/// The block's height. When the height is larger than the remaining space
|
||||
/// on a page and [`breakable`]($func/block.breakable) is `{true}`, the
|
||||
/// on a page and [`breakable`]($block.breakable) is `{true}`, the
|
||||
/// block will continue on the next page with the remaining height.
|
||||
///
|
||||
/// ```example
|
||||
@ -244,29 +243,29 @@ pub struct BlockElem {
|
||||
pub breakable: bool,
|
||||
|
||||
/// The block's background color. See the
|
||||
/// [rectangle's documentation]($func/rect.fill) for more details.
|
||||
/// [rectangle's documentation]($rect.fill) for more details.
|
||||
pub fill: Option<Paint>,
|
||||
|
||||
/// The block's border color. See the
|
||||
/// [rectangle's documentation]($func/rect.stroke) for more details.
|
||||
/// [rectangle's documentation]($rect.stroke) for more details.
|
||||
#[resolve]
|
||||
#[fold]
|
||||
pub stroke: Sides<Option<Option<PartialStroke>>>,
|
||||
pub stroke: Sides<Option<Option<Stroke>>>,
|
||||
|
||||
/// How much to round the block's corners. See the [rectangle's
|
||||
/// documentation]($func/rect.radius) for more details.
|
||||
/// How much to round the block's corners. See the
|
||||
/// [rectangle's documentation]($rect.radius) for more details.
|
||||
#[resolve]
|
||||
#[fold]
|
||||
pub radius: Corners<Option<Rel<Length>>>,
|
||||
|
||||
/// How much to pad the block's content. See the [rectangle's
|
||||
/// documentation]($func/rect.inset) for more details.
|
||||
/// How much to pad the block's content. See the
|
||||
/// [box's documentation]($box.inset) for more details.
|
||||
#[resolve]
|
||||
#[fold]
|
||||
pub inset: Sides<Option<Rel<Length>>>,
|
||||
|
||||
/// How much to expand the block's size without affecting the layout. See
|
||||
/// the [rectangle's documentation]($func/rect.outset) for more details.
|
||||
/// the [box's documentation]($box.outset) for more details.
|
||||
#[resolve]
|
||||
#[fold]
|
||||
pub outset: Sides<Option<Rel<Length>>>,
|
||||
@ -352,8 +351,7 @@ impl Layout for BlockElem {
|
||||
let mut expand = sizing.as_ref().map(Smart::is_custom);
|
||||
let mut size = sizing
|
||||
.resolve(styles)
|
||||
.zip(regions.base())
|
||||
.map(|(s, b)| s.map(|v| v.relative_to(b)))
|
||||
.zip_map(regions.base(), |s, b| s.map(|v| v.relative_to(b)))
|
||||
.unwrap_or(regions.base());
|
||||
|
||||
// Layout the child.
|
||||
@ -418,7 +416,7 @@ impl Layout for BlockElem {
|
||||
|
||||
// Prepare fill and stroke.
|
||||
let fill = self.fill(styles);
|
||||
let stroke = self.stroke(styles).map(|s| s.map(PartialStroke::unwrap_or_default));
|
||||
let stroke = self.stroke(styles).map(|s| s.map(Stroke::unwrap_or_default));
|
||||
|
||||
// Add fill and/or stroke.
|
||||
if fill.is_some() || stroke.iter().any(Option::is_some) {
|
||||
|
@ -11,7 +11,7 @@ use super::GridLayouter;
|
||||
///
|
||||
/// Displays a sequence of items vertically and numbers them consecutively.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// # Example
|
||||
/// ```example
|
||||
/// Automatically numbered:
|
||||
/// + Preparations
|
||||
@ -41,8 +41,8 @@ use super::GridLayouter;
|
||||
/// + Don't forget step two
|
||||
/// ```
|
||||
///
|
||||
/// You can also use [`enum.item`]($func/enum.item) to programmatically
|
||||
/// customize the number of each item in the enumeration:
|
||||
/// You can also use [`enum.item`]($enum.item) to programmatically customize the
|
||||
/// number of each item in the enumeration:
|
||||
///
|
||||
/// ```example
|
||||
/// #enum(
|
||||
@ -52,7 +52,7 @@ use super::GridLayouter;
|
||||
/// )
|
||||
/// ```
|
||||
///
|
||||
/// ## Syntax { #syntax }
|
||||
/// # Syntax
|
||||
/// This functions also has dedicated syntax:
|
||||
///
|
||||
/// - Starting a line with a plus sign creates an automatically numbered
|
||||
@ -63,18 +63,11 @@ use super::GridLayouter;
|
||||
/// Enumeration items can contain multiple paragraphs and other block-level
|
||||
/// content. All content that is indented more than an item's marker becomes
|
||||
/// part of that item.
|
||||
///
|
||||
/// Display: Numbered List
|
||||
/// Category: layout
|
||||
#[element(Layout)]
|
||||
#[scope(
|
||||
scope.define("item", EnumItem::func());
|
||||
scope
|
||||
)]
|
||||
#[elem(scope, title = "Numbered List", Layout)]
|
||||
pub struct EnumElem {
|
||||
/// If this is `{false}`, the items are spaced apart with
|
||||
/// [enum spacing]($func/enum.spacing). If it is `{true}`, they use normal
|
||||
/// [leading]($func/par.leading) instead. This makes the enumeration more
|
||||
/// [enum spacing]($enum.spacing). If it is `{true}`, they use normal
|
||||
/// [leading]($par.leading) instead. This makes the enumeration more
|
||||
/// compact, which can look better if the items are short.
|
||||
///
|
||||
/// In markup mode, the value of this parameter is determined based on
|
||||
@ -95,7 +88,7 @@ pub struct EnumElem {
|
||||
pub tight: bool,
|
||||
|
||||
/// How to number the enumeration. Accepts a
|
||||
/// [numbering pattern or function]($func/numbering).
|
||||
/// [numbering pattern or function]($numbering).
|
||||
///
|
||||
/// If the numbering pattern contains multiple counting symbols, they apply
|
||||
/// to nested enums. If given a function, the function receives one argument
|
||||
@ -153,7 +146,7 @@ pub struct EnumElem {
|
||||
|
||||
/// The spacing between the items of a wide (non-tight) enumeration.
|
||||
///
|
||||
/// If set to `{auto}`, uses the spacing [below blocks]($func/block.below).
|
||||
/// If set to `{auto}`, uses the spacing [below blocks]($block.below).
|
||||
pub spacing: Smart<Spacing>,
|
||||
|
||||
/// The horizontal alignment that enum numbers should have.
|
||||
@ -177,8 +170,8 @@ pub struct EnumElem {
|
||||
/// 16. Sixteen
|
||||
/// 32. Thirty two
|
||||
/// ````
|
||||
#[default(HorizontalAlign(GenAlign::End))]
|
||||
pub number_align: HorizontalAlign,
|
||||
#[default(HAlign::End)]
|
||||
pub number_align: HAlign,
|
||||
|
||||
/// The numbered list's items.
|
||||
///
|
||||
@ -201,6 +194,12 @@ pub struct EnumElem {
|
||||
parents: Parent,
|
||||
}
|
||||
|
||||
#[scope]
|
||||
impl EnumElem {
|
||||
#[elem]
|
||||
type EnumItem;
|
||||
}
|
||||
|
||||
impl Layout for EnumElem {
|
||||
#[tracing::instrument(name = "EnumElem::layout", skip_all)]
|
||||
fn layout(
|
||||
@ -225,11 +224,10 @@ impl Layout for EnumElem {
|
||||
let full = self.full(styles);
|
||||
|
||||
// Horizontally align based on the given respective parameter.
|
||||
// Vertically align to the top to avoid inheriting 'horizon' or
|
||||
// 'bottom' alignment from the context and having the number be
|
||||
// displaced in relation to the item it refers to.
|
||||
let number_align: Axes<Option<GenAlign>> =
|
||||
Axes::new(self.number_align(styles).into(), Align::Top.into()).map(Some);
|
||||
// Vertically align to the top to avoid inheriting `horizon` or `bottom`
|
||||
// alignment from the context and having the number be displaced in
|
||||
// relation to the item it refers to.
|
||||
let number_align = self.number_align(styles) + VAlign::Top;
|
||||
|
||||
for item in self.children() {
|
||||
number = item.number(styles).unwrap_or(number);
|
||||
@ -278,10 +276,7 @@ impl Layout for EnumElem {
|
||||
}
|
||||
|
||||
/// An enumeration item.
|
||||
///
|
||||
/// Display: Numbered List Item
|
||||
/// Category: layout
|
||||
#[element]
|
||||
#[elem(name = "item", title = "Numbered List Item")]
|
||||
pub struct EnumItem {
|
||||
/// The item's number.
|
||||
#[positional]
|
||||
|
@ -14,10 +14,7 @@ use crate::visualize::{
|
||||
///
|
||||
/// This element is responsible for layouting both the top-level content flow
|
||||
/// and the contents of boxes.
|
||||
///
|
||||
/// Display: Flow
|
||||
/// Category: layout
|
||||
#[element(Layout)]
|
||||
#[elem(Layout)]
|
||||
pub struct FlowElem {
|
||||
/// The children that will be arranges into a flow.
|
||||
#[variadic]
|
||||
@ -62,7 +59,7 @@ impl Layout for FlowElem {
|
||||
frame.meta(styles, true);
|
||||
layouter.items.push(FlowItem::Frame {
|
||||
frame,
|
||||
aligns: Axes::new(Align::Top, Align::Left),
|
||||
align: Axes::splat(FixedAlign::Start),
|
||||
sticky: true,
|
||||
movable: false,
|
||||
});
|
||||
@ -128,12 +125,12 @@ enum FlowItem {
|
||||
/// A frame for a layouted block, how to align it, whether it sticks to the
|
||||
/// item after it (for orphan prevention), and whether it is movable
|
||||
/// (to keep it together with its footnotes).
|
||||
Frame { frame: Frame, aligns: Axes<Align>, sticky: bool, movable: bool },
|
||||
Frame { frame: Frame, align: Axes<FixedAlign>, sticky: bool, movable: bool },
|
||||
/// An absolutely placed frame.
|
||||
Placed {
|
||||
frame: Frame,
|
||||
x_align: Align,
|
||||
y_align: Smart<Option<Align>>,
|
||||
x_align: FixedAlign,
|
||||
y_align: Smart<Option<FixedAlign>>,
|
||||
delta: Axes<Rel<Abs>>,
|
||||
float: bool,
|
||||
clearance: Abs,
|
||||
@ -209,7 +206,7 @@ impl<'a> FlowLayouter<'a> {
|
||||
par: &ParElem,
|
||||
styles: StyleChain,
|
||||
) -> SourceResult<()> {
|
||||
let aligns = AlignElem::alignment_in(styles).resolve(styles);
|
||||
let align = AlignElem::alignment_in(styles).resolve(styles);
|
||||
let leading = ParElem::leading_in(styles);
|
||||
let consecutive = self.last_was_par;
|
||||
let lines = par
|
||||
@ -242,7 +239,7 @@ impl<'a> FlowLayouter<'a> {
|
||||
|
||||
self.layout_item(
|
||||
vt,
|
||||
FlowItem::Frame { frame, aligns, sticky: false, movable: true },
|
||||
FlowItem::Frame { frame, align, sticky: false, movable: true },
|
||||
)?;
|
||||
}
|
||||
|
||||
@ -258,11 +255,11 @@ impl<'a> FlowLayouter<'a> {
|
||||
content: &dyn Layout,
|
||||
styles: StyleChain,
|
||||
) -> SourceResult<()> {
|
||||
let aligns = AlignElem::alignment_in(styles).resolve(styles);
|
||||
let align = AlignElem::alignment_in(styles).resolve(styles);
|
||||
let sticky = BlockElem::sticky_in(styles);
|
||||
let pod = Regions::one(self.regions.base(), Axes::splat(false));
|
||||
let frame = content.layout(vt, styles, pod)?.into_frame();
|
||||
self.layout_item(vt, FlowItem::Frame { frame, aligns, sticky, movable: true })?;
|
||||
self.layout_item(vt, FlowItem::Frame { frame, align, sticky, movable: true })?;
|
||||
self.last_was_par = false;
|
||||
Ok(())
|
||||
}
|
||||
@ -278,10 +275,10 @@ impl<'a> FlowLayouter<'a> {
|
||||
let clearance = placed.clearance(styles);
|
||||
let alignment = placed.alignment(styles);
|
||||
let delta = Axes::new(placed.dx(styles), placed.dy(styles)).resolve(styles);
|
||||
let x_align = alignment.map_or(Align::Center, |aligns| {
|
||||
aligns.x.unwrap_or(GenAlign::Start).resolve(styles)
|
||||
let x_align = alignment.map_or(FixedAlign::Center, |align| {
|
||||
align.x().unwrap_or_default().resolve(styles)
|
||||
});
|
||||
let y_align = alignment.map(|align| align.y.resolve(styles));
|
||||
let y_align = alignment.map(|align| align.y().map(VAlign::fix));
|
||||
let frame = placed.layout(vt, styles, self.regions)?.into_frame();
|
||||
let item = FlowItem::Placed { frame, x_align, y_align, delta, float, clearance };
|
||||
self.layout_item(vt, item)
|
||||
@ -309,7 +306,7 @@ impl<'a> FlowLayouter<'a> {
|
||||
}
|
||||
|
||||
// How to align the block.
|
||||
let aligns = if let Some(align) = block.to::<AlignElem>() {
|
||||
let align = if let Some(align) = block.to::<AlignElem>() {
|
||||
align.alignment(styles)
|
||||
} else if let Some((_, local)) = block.to_styled() {
|
||||
AlignElem::alignment_in(styles.chain(local))
|
||||
@ -332,7 +329,7 @@ impl<'a> FlowLayouter<'a> {
|
||||
self.finish_region(vt)?;
|
||||
}
|
||||
|
||||
let item = FlowItem::Frame { frame, aligns, sticky, movable: false };
|
||||
let item = FlowItem::Frame { frame, align, sticky, movable: false };
|
||||
self.layout_item(vt, item)?;
|
||||
}
|
||||
|
||||
@ -404,14 +401,14 @@ impl<'a> FlowLayouter<'a> {
|
||||
- (frame.height() + clearance) / 2.0)
|
||||
/ self.regions.full;
|
||||
let better_align =
|
||||
if ratio <= 0.5 { Align::Bottom } else { Align::Top };
|
||||
if ratio <= 0.5 { FixedAlign::End } else { FixedAlign::Start };
|
||||
*y_align = Smart::Custom(Some(better_align));
|
||||
}
|
||||
|
||||
// Add some clearance so that the float doesn't touch the main
|
||||
// content.
|
||||
frame.size_mut().y += clearance;
|
||||
if *y_align == Smart::Custom(Some(Align::Bottom)) {
|
||||
if *y_align == Smart::Custom(Some(FixedAlign::End)) {
|
||||
frame.translate(Point::with_y(clearance));
|
||||
}
|
||||
|
||||
@ -459,8 +456,10 @@ impl<'a> FlowLayouter<'a> {
|
||||
}
|
||||
FlowItem::Placed { float: false, .. } => {}
|
||||
FlowItem::Placed { frame, float: true, y_align, .. } => match y_align {
|
||||
Smart::Custom(Some(Align::Top)) => float_top_height += frame.height(),
|
||||
Smart::Custom(Some(Align::Bottom)) => {
|
||||
Smart::Custom(Some(FixedAlign::Start)) => {
|
||||
float_top_height += frame.height()
|
||||
}
|
||||
Smart::Custom(Some(FixedAlign::End)) => {
|
||||
float_bottom_height += frame.height()
|
||||
}
|
||||
_ => {}
|
||||
@ -486,7 +485,7 @@ impl<'a> FlowLayouter<'a> {
|
||||
}
|
||||
|
||||
let mut output = Frame::new(size);
|
||||
let mut ruler = Align::Top;
|
||||
let mut ruler = FixedAlign::Start;
|
||||
let mut float_top_offset = Abs::zero();
|
||||
let mut offset = float_top_height;
|
||||
let mut float_bottom_offset = Abs::zero();
|
||||
@ -502,9 +501,9 @@ impl<'a> FlowLayouter<'a> {
|
||||
let remaining = self.initial.y - used.y;
|
||||
offset += v.share(fr, remaining);
|
||||
}
|
||||
FlowItem::Frame { frame, aligns, .. } => {
|
||||
ruler = ruler.max(aligns.y);
|
||||
let x = aligns.x.position(size.x - frame.width());
|
||||
FlowItem::Frame { frame, align, .. } => {
|
||||
ruler = ruler.max(align.y);
|
||||
let x = align.x.position(size.x - frame.width());
|
||||
let y = offset + ruler.position(size.y - used.y);
|
||||
let pos = Point::new(x, y);
|
||||
offset += frame.height();
|
||||
@ -514,12 +513,12 @@ impl<'a> FlowLayouter<'a> {
|
||||
let x = x_align.position(size.x - frame.width());
|
||||
let y = if float {
|
||||
match y_align {
|
||||
Smart::Custom(Some(Align::Top)) => {
|
||||
Smart::Custom(Some(FixedAlign::Start)) => {
|
||||
let y = float_top_offset;
|
||||
float_top_offset += frame.height();
|
||||
y
|
||||
}
|
||||
Smart::Custom(Some(Align::Bottom)) => {
|
||||
Smart::Custom(Some(FixedAlign::End)) => {
|
||||
let y = size.y - footnote_height - float_bottom_height
|
||||
+ float_bottom_offset;
|
||||
float_bottom_offset += frame.height();
|
||||
@ -537,7 +536,7 @@ impl<'a> FlowLayouter<'a> {
|
||||
};
|
||||
|
||||
let pos = Point::new(x, y)
|
||||
+ delta.zip(size).map(|(d, s)| d.relative_to(s)).to_point();
|
||||
+ delta.zip_map(size, Rel::relative_to).to_point();
|
||||
|
||||
output.push_frame(pos, frame);
|
||||
}
|
||||
|
@ -34,7 +34,7 @@ use super::Sizing;
|
||||
/// instead of an array. For example, `columns:` `{3}` is equivalent to
|
||||
/// `columns:` `{(auto, auto, auto)}`.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// # Example
|
||||
/// ```example
|
||||
/// #set text(10pt, style: "italic")
|
||||
/// #let cell = rect.with(
|
||||
@ -58,10 +58,7 @@ use super::Sizing;
|
||||
/// cell[One more thing...],
|
||||
/// )
|
||||
/// ```
|
||||
///
|
||||
/// Display: Grid
|
||||
/// Category: layout
|
||||
#[element(Layout)]
|
||||
#[elem(Layout)]
|
||||
pub struct GridElem {
|
||||
/// The column sizes.
|
||||
///
|
||||
|
@ -7,15 +7,12 @@ use crate::prelude::*;
|
||||
/// content. It may also be useful to redact content because its arguments are
|
||||
/// not included in the output.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// # Example
|
||||
/// ```example
|
||||
/// Hello Jane \
|
||||
/// #hide[Hello] Joe
|
||||
/// ```
|
||||
///
|
||||
/// Display: Hide
|
||||
/// Category: layout
|
||||
#[element(Show)]
|
||||
#[elem(Show)]
|
||||
pub struct HideElem {
|
||||
/// The content to hide.
|
||||
#[required]
|
||||
|
@ -9,7 +9,7 @@ use super::GridLayouter;
|
||||
/// Displays a sequence of items vertically, with each item introduced by a
|
||||
/// marker.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// # Example
|
||||
/// ```example
|
||||
/// Normal list.
|
||||
/// - Text
|
||||
@ -30,24 +30,17 @@ use super::GridLayouter;
|
||||
/// )
|
||||
/// ```
|
||||
///
|
||||
/// ## Syntax { #syntax }
|
||||
/// # Syntax
|
||||
/// This functions also has dedicated syntax: Start a line with a hyphen,
|
||||
/// followed by a space to create a list item. A list item can contain multiple
|
||||
/// paragraphs and other block-level content. All content that is indented
|
||||
/// more than an item's marker becomes part of that item.
|
||||
///
|
||||
/// Display: Bullet List
|
||||
/// Category: layout
|
||||
#[element(Layout)]
|
||||
#[scope(
|
||||
scope.define("item", ListItem::func());
|
||||
scope
|
||||
)]
|
||||
#[elem(scope, title = "Bullet List", Layout)]
|
||||
pub struct ListElem {
|
||||
/// If this is `{false}`, the items are spaced apart with [list
|
||||
/// spacing]($func/list.spacing). If it is `{true}`, they use normal
|
||||
/// [leading]($func/par.leading) instead. This makes the list more compact,
|
||||
/// which can look better if the items are short.
|
||||
/// If this is `{false}`, the items are spaced apart with
|
||||
/// [list spacing]($list.spacing). If it is `{true}`, they use normal
|
||||
/// [leading]($par.leading) instead. This makes the list more compact, which
|
||||
/// can look better if the items are short.
|
||||
///
|
||||
/// In markup mode, the value of this parameter is determined based on
|
||||
/// whether items are separated with a blank line. If items directly follow
|
||||
@ -98,7 +91,7 @@ pub struct ListElem {
|
||||
|
||||
/// The spacing between the items of a wide (non-tight) list.
|
||||
///
|
||||
/// If set to `{auto}`, uses the spacing [below blocks]($func/block.below).
|
||||
/// If set to `{auto}`, uses the spacing [below blocks]($block.below).
|
||||
pub spacing: Smart<Spacing>,
|
||||
|
||||
/// The bullet list's children.
|
||||
@ -120,6 +113,12 @@ pub struct ListElem {
|
||||
depth: Depth,
|
||||
}
|
||||
|
||||
#[scope]
|
||||
impl ListElem {
|
||||
#[elem]
|
||||
type ListItem;
|
||||
}
|
||||
|
||||
impl Layout for ListElem {
|
||||
#[tracing::instrument(name = "ListElem::layout", skip_all)]
|
||||
fn layout(
|
||||
@ -142,7 +141,7 @@ impl Layout for ListElem {
|
||||
.marker(styles)
|
||||
.resolve(vt, depth)?
|
||||
// avoid '#set align' interference with the list
|
||||
.aligned(Align::LEFT_TOP.into());
|
||||
.aligned(HAlign::Start + VAlign::Top);
|
||||
|
||||
let mut cells = vec![];
|
||||
for item in self.children() {
|
||||
@ -170,10 +169,7 @@ impl Layout for ListElem {
|
||||
}
|
||||
|
||||
/// A bullet list item.
|
||||
///
|
||||
/// Display: Bullet List Item
|
||||
/// Category: layout
|
||||
#[element]
|
||||
#[elem(name = "item", title = "Bullet List Item")]
|
||||
pub struct ListItem {
|
||||
/// The item's body.
|
||||
#[required]
|
||||
|
@ -2,13 +2,13 @@ use crate::prelude::*;
|
||||
|
||||
/// Measures the layouted size of content.
|
||||
///
|
||||
/// The `measure` function lets you determine the layouted size of content.
|
||||
/// Note that an infinite space is assumed, therefore the measured height/width
|
||||
/// may not necessarily match the final height/width of the measured content.
|
||||
/// If you want to measure in the current layout dimensions, you can combined
|
||||
/// `measure` and [`layout`]($func/layout).
|
||||
/// The `measure` function lets you determine the layouted size of content. Note
|
||||
/// that an infinite space is assumed, therefore the measured height/width may
|
||||
/// not necessarily match the final height/width of the measured content. If you
|
||||
/// want to measure in the current layout dimensions, you can combine `measure`
|
||||
/// and [`layout`]($layout).
|
||||
///
|
||||
/// # Example { #example }
|
||||
/// # Example
|
||||
/// The same content can have a different size depending on the styles that
|
||||
/// are active when it is layouted. For example, in the example below
|
||||
/// `[#content]` is of course bigger when we increase the font size.
|
||||
@ -21,8 +21,8 @@ use crate::prelude::*;
|
||||
/// ```
|
||||
///
|
||||
/// To do a meaningful measurement, you therefore first need to retrieve the
|
||||
/// active styles with the [`style`]($func/style) function. You can then pass
|
||||
/// them to the `measure` function.
|
||||
/// active styles with the [`style`]($style) function. You can then pass them to
|
||||
/// the `measure` function.
|
||||
///
|
||||
/// ```example
|
||||
/// #let thing(body) = style(styles => {
|
||||
@ -35,18 +35,15 @@ use crate::prelude::*;
|
||||
/// ```
|
||||
///
|
||||
/// The measure function returns a dictionary with the entries `width` and
|
||||
/// `height`, both of type [`length`]($type/length).
|
||||
///
|
||||
/// Display: Measure
|
||||
/// Category: layout
|
||||
/// `height`, both of type [`length`]($length).
|
||||
#[func]
|
||||
pub fn measure(
|
||||
/// The virtual machine.
|
||||
vm: &mut Vm,
|
||||
/// The content whose size to measure.
|
||||
content: Content,
|
||||
/// The styles with which to layout the content.
|
||||
styles: Styles,
|
||||
/// The virtual machine.
|
||||
vm: &mut Vm,
|
||||
) -> SourceResult<Dict> {
|
||||
let pod = Regions::one(Axes::splat(Abs::inf()), Axes::splat(false));
|
||||
let styles = StyleChain::new(&styles);
|
||||
|
@ -10,7 +10,8 @@ mod fragment;
|
||||
mod grid;
|
||||
mod hide;
|
||||
mod list;
|
||||
mod measure;
|
||||
#[path = "measure.rs"]
|
||||
mod measure_;
|
||||
mod pad;
|
||||
mod page;
|
||||
mod par;
|
||||
@ -32,7 +33,7 @@ pub use self::fragment::*;
|
||||
pub use self::grid::*;
|
||||
pub use self::hide::*;
|
||||
pub use self::list::*;
|
||||
pub use self::measure::*;
|
||||
pub use self::measure_::*;
|
||||
pub use self::pad::*;
|
||||
pub use self::page::*;
|
||||
pub use self::par::*;
|
||||
@ -57,7 +58,7 @@ use crate::math::{EquationElem, LayoutMath};
|
||||
use crate::meta::DocumentElem;
|
||||
use crate::prelude::*;
|
||||
use crate::shared::BehavedBuilder;
|
||||
use crate::text::{LinebreakElem, SmartQuoteElem, SpaceElem, TextElem};
|
||||
use crate::text::{LinebreakElem, SmartquoteElem, SpaceElem, TextElem};
|
||||
use crate::visualize::{
|
||||
CircleElem, EllipseElem, ImageElem, LineElem, PathElem, PolygonElem, RectElem,
|
||||
SquareElem,
|
||||
@ -65,43 +66,39 @@ use crate::visualize::{
|
||||
|
||||
/// Hook up all layout definitions.
|
||||
pub(super) fn define(global: &mut Scope) {
|
||||
global.define("page", PageElem::func());
|
||||
global.define("pagebreak", PagebreakElem::func());
|
||||
global.define("v", VElem::func());
|
||||
global.define("par", ParElem::func());
|
||||
global.define("parbreak", ParbreakElem::func());
|
||||
global.define("h", HElem::func());
|
||||
global.define("box", BoxElem::func());
|
||||
global.define("block", BlockElem::func());
|
||||
global.define("list", ListElem::func());
|
||||
global.define("enum", EnumElem::func());
|
||||
global.define("terms", TermsElem::func());
|
||||
global.define("table", TableElem::func());
|
||||
global.define("stack", StackElem::func());
|
||||
global.define("grid", GridElem::func());
|
||||
global.define("columns", ColumnsElem::func());
|
||||
global.define("colbreak", ColbreakElem::func());
|
||||
global.define("place", PlaceElem::func());
|
||||
global.define("align", AlignElem::func());
|
||||
global.define("pad", PadElem::func());
|
||||
global.define("repeat", RepeatElem::func());
|
||||
global.define("move", MoveElem::func());
|
||||
global.define("scale", ScaleElem::func());
|
||||
global.define("rotate", RotateElem::func());
|
||||
global.define("hide", HideElem::func());
|
||||
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));
|
||||
global.category("layout");
|
||||
global.define_type::<Length>();
|
||||
global.define_type::<Angle>();
|
||||
global.define_type::<Ratio>();
|
||||
global.define_type::<Rel<Length>>();
|
||||
global.define_type::<Fr>();
|
||||
global.define_type::<Dir>();
|
||||
global.define_type::<Align>();
|
||||
global.define_elem::<PageElem>();
|
||||
global.define_elem::<PagebreakElem>();
|
||||
global.define_elem::<VElem>();
|
||||
global.define_elem::<ParElem>();
|
||||
global.define_elem::<ParbreakElem>();
|
||||
global.define_elem::<HElem>();
|
||||
global.define_elem::<BoxElem>();
|
||||
global.define_elem::<BlockElem>();
|
||||
global.define_elem::<ListElem>();
|
||||
global.define_elem::<EnumElem>();
|
||||
global.define_elem::<TermsElem>();
|
||||
global.define_elem::<TableElem>();
|
||||
global.define_elem::<StackElem>();
|
||||
global.define_elem::<GridElem>();
|
||||
global.define_elem::<ColumnsElem>();
|
||||
global.define_elem::<ColbreakElem>();
|
||||
global.define_elem::<PlaceElem>();
|
||||
global.define_elem::<AlignElem>();
|
||||
global.define_elem::<PadElem>();
|
||||
global.define_elem::<RepeatElem>();
|
||||
global.define_elem::<MoveElem>();
|
||||
global.define_elem::<ScaleElem>();
|
||||
global.define_elem::<RotateElem>();
|
||||
global.define_elem::<HideElem>();
|
||||
global.define_func::<measure>();
|
||||
}
|
||||
|
||||
/// Root-level layout.
|
||||
@ -598,7 +595,7 @@ impl<'a> ParBuilder<'a> {
|
||||
|| content.is::<TextElem>()
|
||||
|| content.is::<HElem>()
|
||||
|| content.is::<LinebreakElem>()
|
||||
|| content.is::<SmartQuoteElem>()
|
||||
|| content.is::<SmartquoteElem>()
|
||||
|| content.to::<EquationElem>().map_or(false, |elem| !elem.block(styles))
|
||||
|| content.is::<BoxElem>()
|
||||
{
|
||||
|
@ -5,7 +5,7 @@ use crate::prelude::*;
|
||||
/// The spacing can be specified for each side individually, or for all sides at
|
||||
/// once by specifying a positional argument.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// # Example
|
||||
/// ```example
|
||||
/// #set align(center)
|
||||
///
|
||||
@ -13,10 +13,7 @@ use crate::prelude::*;
|
||||
/// _Typing speeds can be
|
||||
/// measured in words per minute._
|
||||
/// ```
|
||||
///
|
||||
/// Display: Padding
|
||||
/// Category: layout
|
||||
#[element(Layout)]
|
||||
#[elem(title = "Padding", Layout)]
|
||||
pub struct PadElem {
|
||||
/// The padding at the left side.
|
||||
#[parse(
|
||||
@ -120,6 +117,5 @@ fn shrink(size: Size, padding: Sides<Rel<Abs>>) -> Size {
|
||||
/// <=> (1 - p.rel) * w = s + p.abs
|
||||
/// <=> w = (s + p.abs) / (1 - p.rel)
|
||||
fn grow(size: Size, padding: Sides<Rel<Abs>>) -> Size {
|
||||
size.zip(padding.sum_by_axis())
|
||||
.map(|(s, p)| (s + p.abs).safe_div(1.0 - p.rel.get()))
|
||||
size.zip_map(padding.sum_by_axis(), |s, p| (s + p.abs).safe_div(1.0 - p.rel.get()))
|
||||
}
|
||||
|
@ -1,6 +1,8 @@
|
||||
use std::ptr;
|
||||
use std::str::FromStr;
|
||||
|
||||
use typst::eval::AutoValue;
|
||||
|
||||
use super::{AlignElem, ColumnsElem};
|
||||
use crate::meta::{Counter, CounterKey, Numbering};
|
||||
use crate::prelude::*;
|
||||
@ -18,17 +20,14 @@ use crate::text::TextElem;
|
||||
/// The [Guide for Page Setup]($guides/page-setup-guide) explains how to use
|
||||
/// this and related functions to set up a document with many examples.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// # Example
|
||||
/// ```example
|
||||
/// >>> #set page(margin: auto)
|
||||
/// #set page("us-letter")
|
||||
///
|
||||
/// There you go, US friends!
|
||||
/// ```
|
||||
///
|
||||
/// Display: Page
|
||||
/// Category: layout
|
||||
#[element]
|
||||
#[elem]
|
||||
pub struct PageElem {
|
||||
/// A standard paper size to set width and height.
|
||||
#[external]
|
||||
@ -59,9 +58,9 @@ pub struct PageElem {
|
||||
/// The height of the page.
|
||||
///
|
||||
/// If this is set to `{auto}`, page breaks can only be triggered manually
|
||||
/// by inserting a [page break]($func/pagebreak). Most examples throughout
|
||||
/// this documentation use `{auto}` for the height of the page to
|
||||
/// dynamically grow and shrink to fit their content.
|
||||
/// by inserting a [page break]($pagebreak). Most examples throughout this
|
||||
/// documentation use `{auto}` for the height of the page to dynamically
|
||||
/// grow and shrink to fit their content.
|
||||
#[resolve]
|
||||
#[parse(
|
||||
args.named("height")?
|
||||
@ -103,9 +102,9 @@ pub struct PageElem {
|
||||
/// - `bottom`: The bottom margin.
|
||||
/// - `left`: The left margin.
|
||||
/// - `inside`: The margin at the inner side of the page (where the
|
||||
/// [binding]($func/page.binding) is).
|
||||
/// [binding]($page.binding) is).
|
||||
/// - `outside`: The margin at the outer side of the page (opposite to the
|
||||
/// [binding]($func/page.binding)).
|
||||
/// [binding]($page.binding)).
|
||||
/// - `x`: The horizontal margins.
|
||||
/// - `y`: The vertical margins.
|
||||
/// - `rest`: The margins on all sides except those for which the
|
||||
@ -132,7 +131,7 @@ pub struct PageElem {
|
||||
|
||||
/// On which side the pages will be bound.
|
||||
///
|
||||
/// - `{auto}`: Equivalent to `left` if the [text direction]($func/text.dir)
|
||||
/// - `{auto}`: Equivalent to `left` if the [text direction]($text.dir)
|
||||
/// is left-to-right and `right` if it is right-to-left.
|
||||
/// - `left`: Bound on the left side.
|
||||
/// - `right`: Bound on the right side.
|
||||
@ -144,7 +143,7 @@ pub struct PageElem {
|
||||
/// How many columns the page has.
|
||||
///
|
||||
/// If you need to insert columns into a page or other container, you can
|
||||
/// also use the [`columns` function]($func/columns).
|
||||
/// also use the [`columns` function]($columns).
|
||||
///
|
||||
/// ```example:single
|
||||
/// #set page(columns: 2, height: 4.8cm)
|
||||
@ -175,7 +174,7 @@ pub struct PageElem {
|
||||
/// ```
|
||||
pub fill: Option<Paint>,
|
||||
|
||||
/// How to [number]($func/numbering) the pages.
|
||||
/// How to [number]($numbering) the pages.
|
||||
///
|
||||
/// If an explicit `footer` (or `header` for top-aligned numbering) is
|
||||
/// given, the numbering is ignored.
|
||||
@ -207,17 +206,17 @@ pub struct PageElem {
|
||||
///
|
||||
/// #lorem(30)
|
||||
/// ```
|
||||
#[default(Align::Center.into())]
|
||||
#[default(HAlign::Center + VAlign::Bottom)]
|
||||
#[parse({
|
||||
let spanned: Option<Spanned<Axes<_>>> = args.named("number-align")?;
|
||||
if let Some(Spanned { v, span }) = spanned {
|
||||
if matches!(v.y, Some(GenAlign::Specific(Align::Horizon))) {
|
||||
let option: Option<Spanned<Align>> = args.named("number-align")?;
|
||||
if let Some(Spanned { v: align, span }) = option {
|
||||
if align.y() == Some(VAlign::Horizon) {
|
||||
bail!(span, "page number cannot be `horizon`-aligned");
|
||||
}
|
||||
}
|
||||
spanned.map(|s| s.v)
|
||||
option.map(|spanned| spanned.v)
|
||||
})]
|
||||
pub number_align: Axes<Option<GenAlign>>,
|
||||
pub number_align: Align,
|
||||
|
||||
/// The page's header. Fills the top margin of each page.
|
||||
///
|
||||
@ -245,7 +244,7 @@ pub struct PageElem {
|
||||
///
|
||||
/// For just a page number, the `numbering` property, typically suffices. If
|
||||
/// you want to create a custom footer, but still display the page number,
|
||||
/// you can directly access the [page counter]($func/counter).
|
||||
/// you can directly access the [page counter]($counter).
|
||||
///
|
||||
/// ```example
|
||||
/// #set par(justify: true)
|
||||
@ -406,14 +405,14 @@ impl PageElem {
|
||||
|
||||
// We interpret the Y alignment as selecting header or footer
|
||||
// and then ignore it for aligning the actual number.
|
||||
if let Some(x) = number_align.x {
|
||||
counter = counter.aligned(Axes::with_x(Some(x)));
|
||||
if let Some(x) = number_align.x() {
|
||||
counter = counter.aligned(x.into());
|
||||
}
|
||||
|
||||
counter
|
||||
});
|
||||
|
||||
if matches!(number_align.y, Some(GenAlign::Specific(Align::Top))) {
|
||||
if matches!(number_align.y(), Some(VAlign::Top)) {
|
||||
header = header.or(numbering_marginal);
|
||||
} else {
|
||||
footer = footer.or(numbering_marginal);
|
||||
@ -461,16 +460,16 @@ impl PageElem {
|
||||
let ascent = header_ascent.relative_to(margin.top);
|
||||
pos = Point::with_x(margin.left);
|
||||
area = Size::new(pw, margin.top - ascent);
|
||||
align = Align::Bottom.into();
|
||||
align = Align::BOTTOM;
|
||||
} else if ptr::eq(marginal, &footer) {
|
||||
let descent = footer_descent.relative_to(margin.bottom);
|
||||
pos = Point::new(margin.left, size.y - margin.bottom + descent);
|
||||
area = Size::new(pw, margin.bottom - descent);
|
||||
align = Align::Top.into();
|
||||
align = Align::TOP;
|
||||
} else {
|
||||
pos = Point::zero();
|
||||
area = size;
|
||||
align = Align::CENTER_HORIZON.into();
|
||||
align = HAlign::Center + VAlign::Horizon;
|
||||
};
|
||||
|
||||
let pod = Regions::one(area, Axes::splat(true));
|
||||
@ -626,12 +625,12 @@ impl Binding {
|
||||
cast! {
|
||||
Binding,
|
||||
self => match self {
|
||||
Self::Left => GenAlign::Specific(Align::Left).into_value(),
|
||||
Self::Right => GenAlign::Specific(Align::Right).into_value(),
|
||||
Self::Left => Align::LEFT.into_value(),
|
||||
Self::Right => Align::RIGHT.into_value(),
|
||||
},
|
||||
v: GenAlign => match v {
|
||||
GenAlign::Specific(Align::Left) => Self::Left,
|
||||
GenAlign::Specific(Align::Right) => Self::Right,
|
||||
v: Align => match v {
|
||||
Align::LEFT => Self::Left,
|
||||
Align::RIGHT => Self::Right,
|
||||
_ => bail!("must be `left` or `right`"),
|
||||
},
|
||||
}
|
||||
@ -669,7 +668,7 @@ cast! {
|
||||
///
|
||||
/// Must not be used inside any containers.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// # Example
|
||||
/// ```example
|
||||
/// The next page contains
|
||||
/// more details on compound theory.
|
||||
@ -678,10 +677,7 @@ cast! {
|
||||
/// == Compound Theory
|
||||
/// In 1984, the first ...
|
||||
/// ```
|
||||
///
|
||||
/// Display: Page Break
|
||||
/// Category: layout
|
||||
#[element]
|
||||
#[elem(title = "Page Break")]
|
||||
pub struct PagebreakElem {
|
||||
/// If `{true}`, the page break is skipped if the current page is already
|
||||
/// empty.
|
||||
|
@ -16,7 +16,7 @@ use crate::layout::AlignElem;
|
||||
use crate::math::EquationElem;
|
||||
use crate::prelude::*;
|
||||
use crate::text::{
|
||||
is_gb_style, shape, LinebreakElem, Quoter, Quotes, ShapedText, SmartQuoteElem,
|
||||
is_gb_style, shape, LinebreakElem, Quoter, Quotes, ShapedText, SmartquoteElem,
|
||||
SpaceElem, TextElem,
|
||||
};
|
||||
|
||||
@ -26,7 +26,7 @@ use crate::text::{
|
||||
/// properties, it can also be used to explicitly render its argument onto a
|
||||
/// paragraph of its own.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// # Example
|
||||
/// ```example
|
||||
/// #show par: set block(spacing: 0.65em)
|
||||
/// #set par(
|
||||
@ -45,10 +45,7 @@ use crate::text::{
|
||||
/// let $a$ be the smallest of the
|
||||
/// three integers. Then, we ...
|
||||
/// ```
|
||||
///
|
||||
/// Display: Paragraph
|
||||
/// Category: layout
|
||||
#[element(Construct)]
|
||||
#[elem(title = "Paragraph", Construct)]
|
||||
pub struct ParElem {
|
||||
/// The spacing between lines.
|
||||
#[resolve]
|
||||
@ -57,13 +54,13 @@ pub struct ParElem {
|
||||
|
||||
/// Whether to justify text in its line.
|
||||
///
|
||||
/// Hyphenation will be enabled for justified paragraphs if the [text
|
||||
/// property hyphenate]($func/text.hyphenate) is set to `{auto}` and the
|
||||
/// current language is known.
|
||||
/// Hyphenation will be enabled for justified paragraphs if the
|
||||
/// [text function's `hyphenate` property]($text.hyphenate) is set to
|
||||
/// `{auto}` and the current language is known.
|
||||
///
|
||||
/// Note that the current [alignment]($func/align) still has an effect on
|
||||
/// the placement of the last line except if it ends with a [justified line
|
||||
/// break]($func/linebreak.justify).
|
||||
/// Note that the current [alignment]($align) still has an effect on the
|
||||
/// placement of the last line except if it ends with a
|
||||
/// [justified line break]($linebreak.justify).
|
||||
#[default(false)]
|
||||
pub justify: bool,
|
||||
|
||||
@ -88,7 +85,6 @@ pub struct ParElem {
|
||||
/// challenging to break in a visually
|
||||
/// pleasing way.
|
||||
/// ```
|
||||
#[default]
|
||||
pub linebreaks: Smart<Linebreaks>,
|
||||
|
||||
/// The indent the first line of a paragraph should have.
|
||||
@ -98,7 +94,7 @@ pub struct ParElem {
|
||||
///
|
||||
/// By typographic convention, paragraph breaks are indicated either by some
|
||||
/// space between paragraphs or by indented first lines. Consider reducing
|
||||
/// the [paragraph spacing]($func/block.spacing) to the [`leading`] when
|
||||
/// the [paragraph spacing]($block.spacing) to the [`leading`] when
|
||||
/// using this property (e.g. using
|
||||
/// `[#show par: set block(spacing: 0.65em)]`).
|
||||
pub first_line_indent: Length,
|
||||
@ -219,7 +215,7 @@ pub enum Linebreaks {
|
||||
/// [for loops]($scripting/#loops). Multiple consecutive
|
||||
/// paragraph breaks collapse into a single one.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// # Example
|
||||
/// ```example
|
||||
/// #for i in range(3) {
|
||||
/// [Blind text #i: ]
|
||||
@ -228,13 +224,10 @@ pub enum Linebreaks {
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// ## Syntax { #syntax }
|
||||
/// # Syntax
|
||||
/// Instead of calling this function, you can insert a blank line into your
|
||||
/// markup to create a paragraph break.
|
||||
///
|
||||
/// Display: Paragraph Break
|
||||
/// Category: layout
|
||||
#[element(Unlabellable)]
|
||||
#[elem(title = "Paragraph Break", Unlabellable)]
|
||||
pub struct ParbreakElem {}
|
||||
|
||||
impl Unlabellable for ParbreakElem {}
|
||||
@ -266,8 +259,8 @@ struct Preparation<'a> {
|
||||
hyphenate: Option<bool>,
|
||||
/// The text language if it's the same for all children.
|
||||
lang: Option<Lang>,
|
||||
/// The paragraph's resolved alignment.
|
||||
align: Align,
|
||||
/// The paragraph's resolved horizontal alignment.
|
||||
align: FixedAlign,
|
||||
/// Whether to justify the paragraph.
|
||||
justify: bool,
|
||||
/// The paragraph's hanging indent.
|
||||
@ -550,7 +543,7 @@ fn collect<'a>(
|
||||
let first_line_indent = ParElem::first_line_indent_in(*styles);
|
||||
if !first_line_indent.is_zero()
|
||||
&& consecutive
|
||||
&& AlignElem::alignment_in(*styles).x.resolve(*styles)
|
||||
&& AlignElem::alignment_in(*styles).resolve(*styles).x
|
||||
== TextElem::dir_in(*styles).start().into()
|
||||
{
|
||||
full.push(SPACING_REPLACE);
|
||||
@ -593,15 +586,15 @@ fn collect<'a>(
|
||||
let c = if elem.justify(styles) { '\u{2028}' } else { '\n' };
|
||||
full.push(c);
|
||||
Segment::Text(c.len_utf8())
|
||||
} else if let Some(elem) = child.to::<SmartQuoteElem>() {
|
||||
} else if let Some(elem) = child.to::<SmartquoteElem>() {
|
||||
let prev = full.len();
|
||||
if SmartQuoteElem::enabled_in(styles) {
|
||||
if SmartquoteElem::enabled_in(styles) {
|
||||
let lang = TextElem::lang_in(styles);
|
||||
let region = TextElem::region_in(styles);
|
||||
let quotes = Quotes::from_lang(
|
||||
lang,
|
||||
region,
|
||||
SmartQuoteElem::alternative_in(styles),
|
||||
SmartquoteElem::alternative_in(styles),
|
||||
);
|
||||
let peeked = iter.peek().and_then(|child| {
|
||||
let child = if let Some((child, _)) = child.to_styled() {
|
||||
@ -611,7 +604,7 @@ fn collect<'a>(
|
||||
};
|
||||
if let Some(elem) = child.to::<TextElem>() {
|
||||
elem.text().chars().next()
|
||||
} else if child.is::<SmartQuoteElem>() {
|
||||
} else if child.is::<SmartquoteElem>() {
|
||||
Some('"')
|
||||
} else if child.is::<SpaceElem>()
|
||||
|| child.is::<HElem>()
|
||||
@ -642,7 +635,7 @@ fn collect<'a>(
|
||||
};
|
||||
|
||||
if let Some(last) = full.chars().last() {
|
||||
quoter.last(last, child.is::<SmartQuoteElem>());
|
||||
quoter.last(last, child.is::<SmartquoteElem>());
|
||||
}
|
||||
|
||||
spans.push(segment.len(), child.span());
|
||||
@ -673,9 +666,10 @@ fn prepare<'a>(
|
||||
styles: StyleChain<'a>,
|
||||
region: Size,
|
||||
) -> SourceResult<Preparation<'a>> {
|
||||
let dir = TextElem::dir_in(styles);
|
||||
let bidi = BidiInfo::new(
|
||||
text,
|
||||
match TextElem::dir_in(styles) {
|
||||
match dir {
|
||||
Dir::LTR => Some(BidiLevel::ltr()),
|
||||
Dir::RTL => Some(BidiLevel::rtl()),
|
||||
_ => None,
|
||||
@ -734,7 +728,7 @@ fn prepare<'a>(
|
||||
styles,
|
||||
hyphenate: shared_get(styles, children, TextElem::hyphenate_in),
|
||||
lang: shared_get(styles, children, TextElem::lang_in),
|
||||
align: AlignElem::alignment_in(styles).x.resolve(styles),
|
||||
align: AlignElem::alignment_in(styles).resolve(styles).x,
|
||||
justify: ParElem::justify_in(styles),
|
||||
hang: ParElem::hanging_indent_in(styles),
|
||||
})
|
||||
|
@ -7,7 +7,7 @@ use crate::prelude::*;
|
||||
/// other content in the container. Page margins will be respected.
|
||||
///
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// # Example
|
||||
/// ```example
|
||||
/// #set page(height: 60pt)
|
||||
/// Hello, world!
|
||||
@ -20,10 +20,7 @@ use crate::prelude::*;
|
||||
/// ),
|
||||
/// )
|
||||
/// ```
|
||||
///
|
||||
/// Display: Place
|
||||
/// Category: layout
|
||||
#[element(Layout, Behave)]
|
||||
#[elem(Layout, Behave)]
|
||||
pub struct PlaceElem {
|
||||
/// Relative to which position in the parent container to place the content.
|
||||
///
|
||||
@ -34,8 +31,8 @@ pub struct PlaceElem {
|
||||
/// that axis will be ignored, instead, the item will be placed in the
|
||||
/// origin of the axis.
|
||||
#[positional]
|
||||
#[default(Smart::Custom(Axes::with_x(Some(GenAlign::Start))))]
|
||||
pub alignment: Smart<Axes<Option<GenAlign>>>,
|
||||
#[default(Smart::Custom(Align::START))]
|
||||
pub alignment: Smart<Align>,
|
||||
|
||||
/// Whether the placed element has floating layout.
|
||||
///
|
||||
@ -98,16 +95,7 @@ impl Layout for PlaceElem {
|
||||
let float = self.float(styles);
|
||||
let alignment = self.alignment(styles);
|
||||
|
||||
if float
|
||||
&& !matches!(
|
||||
alignment,
|
||||
Smart::Auto
|
||||
| Smart::Custom(Axes {
|
||||
y: Some(GenAlign::Specific(Align::Top | Align::Bottom)),
|
||||
..
|
||||
})
|
||||
)
|
||||
{
|
||||
if float && alignment.map_or(false, |align| align.y() == Some(VAlign::Horizon)) {
|
||||
bail!(self.span(), "floating placement must be `auto`, `top`, or `bottom`");
|
||||
} else if !float && alignment.is_auto() {
|
||||
return Err("automatic positioning is only available for floating placement")
|
||||
@ -115,9 +103,7 @@ impl Layout for PlaceElem {
|
||||
.at(self.span());
|
||||
}
|
||||
|
||||
let child = self.body().aligned(
|
||||
alignment.unwrap_or_else(|| Axes::with_x(Some(Align::Center.into()))),
|
||||
);
|
||||
let child = self.body().aligned(alignment.unwrap_or_else(|| Align::CENTER));
|
||||
|
||||
let pod = Regions::one(base, Axes::splat(false));
|
||||
let frame = child.layout(vt, styles, pod)?.into_frame();
|
||||
|
@ -12,7 +12,7 @@ use super::AlignElem;
|
||||
/// Errors if there no bounds on the available space, as it would create
|
||||
/// infinite content.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// # Example
|
||||
/// ```example
|
||||
/// Sign on the dotted line:
|
||||
/// #box(width: 1fr, repeat[.])
|
||||
@ -23,10 +23,7 @@ use super::AlignElem;
|
||||
/// Berlin, the 22nd of December, 2022
|
||||
/// ]
|
||||
/// ```
|
||||
///
|
||||
/// Display: Repeat
|
||||
/// Category: layout
|
||||
#[element(Layout)]
|
||||
#[elem(Layout)]
|
||||
pub struct RepeatElem {
|
||||
/// The content to repeat.
|
||||
#[required]
|
||||
@ -43,7 +40,7 @@ impl Layout for RepeatElem {
|
||||
) -> SourceResult<Fragment> {
|
||||
let pod = Regions::one(regions.size, Axes::new(false, false));
|
||||
let piece = self.body().layout(vt, styles, pod)?.into_frame();
|
||||
let align = AlignElem::alignment_in(styles).x.resolve(styles);
|
||||
let align = AlignElem::alignment_in(styles).resolve(styles);
|
||||
|
||||
let fill = regions.size.x;
|
||||
let width = piece.width();
|
||||
@ -64,7 +61,7 @@ impl Layout for RepeatElem {
|
||||
|
||||
let mut offset = Abs::zero();
|
||||
if count == 1.0 {
|
||||
offset += align.position(remaining);
|
||||
offset += align.x.position(remaining);
|
||||
}
|
||||
|
||||
if width > Abs::zero() {
|
||||
|
@ -8,20 +8,17 @@ use crate::prelude::*;
|
||||
/// remaining space on the line is distributed among all fractional spacings
|
||||
/// according to their relative fractions.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// # Example
|
||||
/// ```example
|
||||
/// First #h(1cm) Second \
|
||||
/// First #h(30%) Second \
|
||||
/// First #h(2fr) Second #h(1fr) Third
|
||||
/// ```
|
||||
///
|
||||
/// ## Mathematical Spacing { #math-spacing }
|
||||
/// # Mathematical Spacing { #math-spacing }
|
||||
/// In [mathematical formulas]($category/math), you can additionally use these
|
||||
/// constants to add spacing between elements: `thin`, `med`, `thick`, `quad`.
|
||||
///
|
||||
/// Display: Spacing (H)
|
||||
/// Category: layout
|
||||
#[element(Behave)]
|
||||
#[elem(title = "Spacing (H)", Behave)]
|
||||
pub struct HElem {
|
||||
/// How much spacing to insert.
|
||||
#[required]
|
||||
@ -79,7 +76,7 @@ impl Behave for HElem {
|
||||
/// the remaining space on the page is distributed among all fractional spacings
|
||||
/// according to their relative fractions.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// # Example
|
||||
/// ```example
|
||||
/// #grid(
|
||||
/// rows: 3cm,
|
||||
@ -93,10 +90,7 @@ impl Behave for HElem {
|
||||
/// [A #v(1fr) B],
|
||||
/// )
|
||||
/// ```
|
||||
///
|
||||
/// Display: Spacing (V)
|
||||
/// Category: layout
|
||||
#[element(Behave)]
|
||||
#[elem(title = "Spacing (V)", Behave)]
|
||||
pub struct VElem {
|
||||
/// How much spacing to insert.
|
||||
#[required]
|
||||
|
@ -6,7 +6,7 @@ use crate::prelude::*;
|
||||
/// The stack places a list of items along an axis, with optional spacing
|
||||
/// between each item.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// # Example
|
||||
/// ```example
|
||||
/// #stack(
|
||||
/// dir: ttb,
|
||||
@ -15,10 +15,7 @@ use crate::prelude::*;
|
||||
/// rect(width: 90pt),
|
||||
/// )
|
||||
/// ```
|
||||
///
|
||||
/// Display: Stack
|
||||
/// Category: layout
|
||||
#[element(Layout)]
|
||||
#[elem(Layout)]
|
||||
pub struct StackElem {
|
||||
/// The direction along which the items are stacked. Possible values are:
|
||||
///
|
||||
@ -27,7 +24,7 @@ pub struct StackElem {
|
||||
/// - `{ttb}`: Top to bottom.
|
||||
/// - `{btt}`: Bottom to top.
|
||||
///
|
||||
/// You cab use the `start` and `end` methods to obtain the initial and
|
||||
/// You can use the `start` and `end` methods to obtain the initial and
|
||||
/// final points (respectively) of a direction, as `alignment`. You can also
|
||||
/// use the `axis` method to determine whether a direction is
|
||||
/// `{"horizontal"}` or `{"vertical"}`. The `inv` method returns a
|
||||
@ -141,7 +138,7 @@ enum StackItem {
|
||||
/// Fractional spacing between other items.
|
||||
Fractional(Fr),
|
||||
/// A frame for a layouted block.
|
||||
Frame(Frame, Axes<Align>),
|
||||
Frame(Frame, Axes<FixedAlign>),
|
||||
}
|
||||
|
||||
impl<'a> StackLayouter<'a> {
|
||||
@ -204,7 +201,7 @@ impl<'a> StackLayouter<'a> {
|
||||
}
|
||||
|
||||
// Block-axis alignment of the `AlignElement` is respected by stacks.
|
||||
let aligns = if let Some(align) = block.to::<AlignElem>() {
|
||||
let align = if let Some(align) = block.to::<AlignElem>() {
|
||||
align.alignment(styles)
|
||||
} else if let Some((_, local)) = block.to_styled() {
|
||||
AlignElem::alignment_in(styles.chain(local))
|
||||
@ -230,7 +227,7 @@ impl<'a> StackLayouter<'a> {
|
||||
self.used.main += gen.main;
|
||||
self.used.cross.set_max(gen.cross);
|
||||
|
||||
self.items.push(StackItem::Frame(frame, aligns));
|
||||
self.items.push(StackItem::Frame(frame, align));
|
||||
|
||||
if i + 1 < len {
|
||||
self.finish_region();
|
||||
@ -259,18 +256,18 @@ impl<'a> StackLayouter<'a> {
|
||||
|
||||
let mut output = Frame::new(size);
|
||||
let mut cursor = Abs::zero();
|
||||
let mut ruler: Align = self.dir.start().into();
|
||||
let mut ruler: FixedAlign = self.dir.start().into();
|
||||
|
||||
// Place all frames.
|
||||
for item in self.items.drain(..) {
|
||||
match item {
|
||||
StackItem::Absolute(v) => cursor += v,
|
||||
StackItem::Fractional(v) => cursor += v.share(self.fr, remaining),
|
||||
StackItem::Frame(frame, aligns) => {
|
||||
StackItem::Frame(frame, align) => {
|
||||
if self.dir.is_positive() {
|
||||
ruler = ruler.max(aligns.get(self.axis));
|
||||
ruler = ruler.max(align.get(self.axis));
|
||||
} else {
|
||||
ruler = ruler.min(aligns.get(self.axis));
|
||||
ruler = ruler.min(align.get(self.axis));
|
||||
}
|
||||
|
||||
// Align along the main axis.
|
||||
@ -285,7 +282,7 @@ impl<'a> StackLayouter<'a> {
|
||||
|
||||
// Align along the cross axis.
|
||||
let other = self.axis.other();
|
||||
let cross = aligns
|
||||
let cross = align
|
||||
.get(other)
|
||||
.position(size.get(other) - frame.size().get(other));
|
||||
|
||||
|
@ -9,13 +9,13 @@ use crate::prelude::*;
|
||||
/// Tables are used to arrange content in cells. Cells can contain arbitrary
|
||||
/// content, including multiple paragraphs and are specified in row-major order.
|
||||
/// Because tables are just grids with configurable cell properties, refer to
|
||||
/// the [grid documentation]($func/grid) for more information on how to size the
|
||||
/// the [grid documentation]($grid) for more information on how to size the
|
||||
/// table tracks.
|
||||
///
|
||||
/// To give a table a caption and make it [referenceable]($func/ref), put it
|
||||
/// into a [figure]($func/figure).
|
||||
/// To give a table a caption and make it [referenceable]($ref), put it into a
|
||||
/// [figure]($figure).
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// # Example
|
||||
/// ```example
|
||||
/// #table(
|
||||
/// columns: (1fr, auto, auto),
|
||||
@ -34,34 +34,31 @@ use crate::prelude::*;
|
||||
/// [$a$: edge length]
|
||||
/// )
|
||||
/// ```
|
||||
///
|
||||
/// Display: Table
|
||||
/// Category: layout
|
||||
#[element(Layout, LocalName, Figurable)]
|
||||
#[elem(Layout, LocalName, Figurable)]
|
||||
pub struct TableElem {
|
||||
/// The column sizes. See the [grid documentation]($func/grid) for more
|
||||
/// The column sizes. See the [grid documentation]($grid) for more
|
||||
/// information on track sizing.
|
||||
pub columns: TrackSizings,
|
||||
|
||||
/// The row sizes. See the [grid documentation]($func/grid) for more
|
||||
/// information on track sizing.
|
||||
/// The row sizes. See the [grid documentation]($grid) for more information
|
||||
/// on track sizing.
|
||||
pub rows: TrackSizings,
|
||||
|
||||
/// The gaps between rows & columns. See the [grid
|
||||
/// documentation]($func/grid) for more information on gutters.
|
||||
/// The gaps between rows & columns. See the [grid documentation]($grid) for
|
||||
/// more information on gutters.
|
||||
#[external]
|
||||
pub gutter: TrackSizings,
|
||||
|
||||
/// The gaps between columns. Takes precedence over `gutter`. See the [grid
|
||||
/// documentation]($func/grid) for more information on gutters.
|
||||
/// The gaps between columns. Takes precedence over `gutter`. See the
|
||||
/// [grid documentation]($grid) for more information on gutters.
|
||||
#[parse(
|
||||
let gutter = args.named("gutter")?;
|
||||
args.named("column-gutter")?.or_else(|| gutter.clone())
|
||||
)]
|
||||
pub column_gutter: TrackSizings,
|
||||
|
||||
/// The gaps between rows. Takes precedence over `gutter`. See the [grid
|
||||
/// documentation]($func/grid) for more information on gutters.
|
||||
/// The gaps between rows. Takes precedence over `gutter`. See the
|
||||
/// [grid documentation]($grid) for more information on gutters.
|
||||
#[parse(args.named("row-gutter")?.or_else(|| gutter.clone()))]
|
||||
pub row_gutter: TrackSizings,
|
||||
|
||||
@ -102,20 +99,19 @@ pub struct TableElem {
|
||||
/// [A], [B], [C],
|
||||
/// )
|
||||
/// ```
|
||||
pub align: Celled<Smart<Axes<Option<GenAlign>>>>,
|
||||
pub align: Celled<Smart<Align>>,
|
||||
|
||||
/// How to stroke the cells.
|
||||
/// How to [stroke]($stroke) the cells.
|
||||
///
|
||||
/// See the [line's documentation]($func/line.stroke) for more details.
|
||||
/// Strokes can be disabled by setting this to `{none}`.
|
||||
///
|
||||
/// _Note:_ Richer stroke customization for individual cells is not yet
|
||||
/// implemented, but will be in the future. In the meantime, you can use
|
||||
/// the third-party [tablex library](https://github.com/PgBiel/typst-tablex/).
|
||||
/// implemented, but will be in the future. In the meantime, you can use the
|
||||
/// third-party [tablex library](https://github.com/PgBiel/typst-tablex/).
|
||||
#[resolve]
|
||||
#[fold]
|
||||
#[default(Some(PartialStroke::default()))]
|
||||
pub stroke: Option<PartialStroke>,
|
||||
#[default(Some(Stroke::default()))]
|
||||
pub stroke: Option<Stroke>,
|
||||
|
||||
/// How much to pad the cells' content.
|
||||
#[default(Abs::pt(5.0).into())]
|
||||
@ -158,7 +154,7 @@ impl Layout for TableElem {
|
||||
.collect::<SourceResult<_>>()?;
|
||||
|
||||
let fill = self.fill(styles);
|
||||
let stroke = self.stroke(styles).map(PartialStroke::unwrap_or_default);
|
||||
let stroke = self.stroke(styles).map(Stroke::unwrap_or_default);
|
||||
|
||||
// Prepare grid layout by unifying content and gutter tracks.
|
||||
let layouter = GridLayouter::new(
|
||||
@ -268,8 +264,12 @@ impl<T: Default> Default for Celled<T> {
|
||||
}
|
||||
|
||||
impl<T: Reflect> Reflect for Celled<T> {
|
||||
fn describe() -> CastInfo {
|
||||
T::describe() + Array::describe() + Func::describe()
|
||||
fn input() -> CastInfo {
|
||||
T::input() + Array::input() + Func::input()
|
||||
}
|
||||
|
||||
fn output() -> CastInfo {
|
||||
T::output() + Array::output() + Func::output()
|
||||
}
|
||||
|
||||
fn castable(value: &Value) -> bool {
|
||||
|
@ -8,29 +8,22 @@ use crate::prelude::*;
|
||||
/// descriptions span over multiple lines, they use hanging indent to
|
||||
/// communicate the visual hierarchy.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// # Example
|
||||
/// ```example
|
||||
/// / Ligature: A merged glyph.
|
||||
/// / Kerning: A spacing adjustment
|
||||
/// between two adjacent letters.
|
||||
/// ```
|
||||
///
|
||||
/// ## Syntax { #syntax }
|
||||
/// # Syntax
|
||||
/// This function also has dedicated syntax: Starting a line with a slash,
|
||||
/// followed by a term, a colon and a description creates a term list item.
|
||||
///
|
||||
/// Display: Term List
|
||||
/// Category: layout
|
||||
#[element(Layout)]
|
||||
#[scope(
|
||||
scope.define("item", TermItem::func());
|
||||
scope
|
||||
)]
|
||||
#[elem(scope, title = "Term List", Layout)]
|
||||
pub struct TermsElem {
|
||||
/// If this is `{false}`, the items are spaced apart with [term list
|
||||
/// spacing]($func/terms.spacing). If it is `{true}`, they use normal
|
||||
/// [leading]($func/par.leading) instead. This makes the term list more
|
||||
/// compact, which can look better if the items are short.
|
||||
/// If this is `{false}`, the items are spaced apart with
|
||||
/// [term list spacing]($terms.spacing). If it is `{true}`, they use normal
|
||||
/// [leading]($par.leading) instead. This makes the term list more compact,
|
||||
/// which can look better if the items are short.
|
||||
///
|
||||
/// In markup mode, the value of this parameter is determined based on
|
||||
/// whether items are separated with a blank line. If items directly follow
|
||||
@ -81,7 +74,7 @@ pub struct TermsElem {
|
||||
|
||||
/// The spacing between the items of a wide (non-tight) term list.
|
||||
///
|
||||
/// If set to `{auto}`, uses the spacing [below blocks]($func/block.below).
|
||||
/// If set to `{auto}`, uses the spacing [below blocks]($block.below).
|
||||
pub spacing: Smart<Spacing>,
|
||||
|
||||
/// The term list's children.
|
||||
@ -100,6 +93,12 @@ pub struct TermsElem {
|
||||
pub children: Vec<TermItem>,
|
||||
}
|
||||
|
||||
#[scope]
|
||||
impl TermsElem {
|
||||
#[elem]
|
||||
type TermItem;
|
||||
}
|
||||
|
||||
impl Layout for TermsElem {
|
||||
#[tracing::instrument(name = "TermsElem::layout", skip_all)]
|
||||
fn layout(
|
||||
@ -138,10 +137,7 @@ impl Layout for TermsElem {
|
||||
}
|
||||
|
||||
/// A term list item.
|
||||
///
|
||||
/// Display: Term List Item
|
||||
/// Category: layout
|
||||
#[element]
|
||||
#[elem(name = "item", title = "Term List Item")]
|
||||
pub struct TermItem {
|
||||
/// The term described by the list item.
|
||||
#[required]
|
||||
|
@ -8,7 +8,7 @@ use crate::prelude::*;
|
||||
/// it at the original positions. Containers will still be sized as if the
|
||||
/// content was not moved.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// # Example
|
||||
/// ```example
|
||||
/// #rect(inset: 0pt, move(
|
||||
/// dx: 6pt, dy: 6pt,
|
||||
@ -20,10 +20,7 @@ use crate::prelude::*;
|
||||
/// )
|
||||
/// ))
|
||||
/// ```
|
||||
///
|
||||
/// Display: Move
|
||||
/// Category: layout
|
||||
#[element(Layout)]
|
||||
#[elem(Layout)]
|
||||
pub struct MoveElem {
|
||||
/// The horizontal displacement of the content.
|
||||
pub dx: Rel<Length>,
|
||||
@ -47,7 +44,7 @@ impl Layout for MoveElem {
|
||||
let pod = Regions::one(regions.base(), Axes::splat(false));
|
||||
let mut frame = self.body().layout(vt, styles, pod)?.into_frame();
|
||||
let delta = Axes::new(self.dx(styles), self.dy(styles)).resolve(styles);
|
||||
let delta = delta.zip(regions.base()).map(|(d, s)| d.relative_to(s));
|
||||
let delta = delta.zip_map(regions.base(), Rel::relative_to);
|
||||
frame.translate(delta.to_point());
|
||||
Ok(Fragment::frame(frame))
|
||||
}
|
||||
@ -58,7 +55,7 @@ impl Layout for MoveElem {
|
||||
/// Rotates an element by a given angle. The layout will act as if the element
|
||||
/// was not rotated.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// # Example
|
||||
/// ```example
|
||||
/// #stack(
|
||||
/// dir: ltr,
|
||||
@ -67,10 +64,7 @@ impl Layout for MoveElem {
|
||||
/// .map(i => rotate(24deg * i)[X]),
|
||||
/// )
|
||||
/// ```
|
||||
///
|
||||
/// Display: Rotate
|
||||
/// Category: layout
|
||||
#[element(Layout)]
|
||||
#[elem(Layout)]
|
||||
pub struct RotateElem {
|
||||
/// The amount of rotation.
|
||||
///
|
||||
@ -96,10 +90,9 @@ pub struct RotateElem {
|
||||
/// #box(rotate(30deg, origin: top + left, square()))
|
||||
/// #box(rotate(30deg, origin: bottom + right, square()))
|
||||
/// ```
|
||||
#[resolve]
|
||||
#[fold]
|
||||
#[default(Align::CENTER_HORIZON)]
|
||||
pub origin: Axes<Option<GenAlign>>,
|
||||
#[default(HAlign::Center + VAlign::Horizon)]
|
||||
pub origin: Align,
|
||||
|
||||
/// The content to rotate.
|
||||
#[required]
|
||||
@ -116,8 +109,10 @@ impl Layout for RotateElem {
|
||||
) -> SourceResult<Fragment> {
|
||||
let pod = Regions::one(regions.base(), Axes::splat(false));
|
||||
let mut frame = self.body().layout(vt, styles, pod)?.into_frame();
|
||||
let Axes { x, y } =
|
||||
self.origin(styles).zip(frame.size()).map(|(o, s)| o.position(s));
|
||||
let Axes { x, y } = self
|
||||
.origin(styles)
|
||||
.resolve(styles)
|
||||
.zip_map(frame.size(), FixedAlign::position);
|
||||
let ts = Transform::translate(x, y)
|
||||
.pre_concat(Transform::rotate(self.angle(styles)))
|
||||
.pre_concat(Transform::translate(-x, -y));
|
||||
@ -130,15 +125,12 @@ impl Layout for RotateElem {
|
||||
///
|
||||
/// Lets you mirror content by specifying a negative scale on a single axis.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// # Example
|
||||
/// ```example
|
||||
/// #set align(center)
|
||||
/// #scale(x: -100%)[This is mirrored.]
|
||||
/// ```
|
||||
///
|
||||
/// Display: Scale
|
||||
/// Category: layout
|
||||
#[element(Layout)]
|
||||
#[elem(Layout)]
|
||||
pub struct ScaleElem {
|
||||
/// The horizontal scaling factor.
|
||||
///
|
||||
@ -163,10 +155,9 @@ pub struct ScaleElem {
|
||||
/// A#box(scale(75%)[A])A \
|
||||
/// B#box(scale(75%, origin: bottom + left)[B])B
|
||||
/// ```
|
||||
#[resolve]
|
||||
#[fold]
|
||||
#[default(Align::CENTER_HORIZON)]
|
||||
pub origin: Axes<Option<GenAlign>>,
|
||||
#[default(HAlign::Center + VAlign::Horizon)]
|
||||
pub origin: Align,
|
||||
|
||||
/// The content to scale.
|
||||
#[required]
|
||||
@ -183,8 +174,10 @@ impl Layout for ScaleElem {
|
||||
) -> SourceResult<Fragment> {
|
||||
let pod = Regions::one(regions.base(), Axes::splat(false));
|
||||
let mut frame = self.body().layout(vt, styles, pod)?.into_frame();
|
||||
let Axes { x, y } =
|
||||
self.origin(styles).zip(frame.size()).map(|(o, s)| o.position(s));
|
||||
let Axes { x, y } = self
|
||||
.origin(styles)
|
||||
.resolve(styles)
|
||||
.zip_map(frame.size(), FixedAlign::position);
|
||||
let transform = Transform::translate(x, y)
|
||||
.pre_concat(Transform::scale(self.x(styles), self.y(styles)))
|
||||
.pre_concat(Transform::translate(-x, -y));
|
||||
|
@ -14,10 +14,9 @@ pub mod symbols;
|
||||
pub mod text;
|
||||
pub mod visualize;
|
||||
|
||||
use typst::diag::At;
|
||||
use typst::eval::{LangItems, Library, Module, Scope};
|
||||
use typst::geom::Smart;
|
||||
use typst::model::{Element, Styles};
|
||||
use typst::eval::{Array, LangItems, Library, Module, Scope};
|
||||
use typst::geom::{Align, Color, Dir, Smart};
|
||||
use typst::model::{NativeElement, Styles};
|
||||
|
||||
use self::layout::LayoutRoot;
|
||||
|
||||
@ -32,17 +31,53 @@ pub fn build() -> Library {
|
||||
#[tracing::instrument(skip_all)]
|
||||
fn global(math: Module) -> Module {
|
||||
let mut global = Scope::deduplicating();
|
||||
|
||||
// Categories.
|
||||
text::define(&mut global);
|
||||
global.define_module(math);
|
||||
layout::define(&mut global);
|
||||
visualize::define(&mut global);
|
||||
meta::define(&mut global);
|
||||
compute::define(&mut global);
|
||||
symbols::define(&mut global);
|
||||
global.define("math", math);
|
||||
compute::define(&mut global);
|
||||
prelude(&mut global);
|
||||
Module::new("global", global)
|
||||
}
|
||||
|
||||
Module::new("global").with_scope(global)
|
||||
/// Defines scoped values that are globally available, too.
|
||||
fn prelude(global: &mut Scope) {
|
||||
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);
|
||||
global.define("luma", Color::luma_data());
|
||||
global.define("rgb", Color::rgb_data());
|
||||
global.define("cmyk", Color::cmyk_data());
|
||||
global.define("range", Array::range_data());
|
||||
global.define("ltr", Dir::LTR);
|
||||
global.define("rtl", Dir::RTL);
|
||||
global.define("ttb", Dir::TTB);
|
||||
global.define("btt", Dir::BTT);
|
||||
global.define("start", Align::START);
|
||||
global.define("left", Align::LEFT);
|
||||
global.define("center", Align::CENTER);
|
||||
global.define("right", Align::RIGHT);
|
||||
global.define("end", Align::END);
|
||||
global.define("top", Align::TOP);
|
||||
global.define("horizon", Align::HORIZON);
|
||||
global.define("bottom", Align::BOTTOM);
|
||||
}
|
||||
|
||||
/// Construct the standard style map.
|
||||
@ -59,9 +94,9 @@ fn items() -> LangItems {
|
||||
space: || text::SpaceElem::new().pack(),
|
||||
linebreak: || text::LinebreakElem::new().pack(),
|
||||
text: |text| text::TextElem::new(text).pack(),
|
||||
text_func: text::TextElem::func(),
|
||||
text_elem: text::TextElem::elem(),
|
||||
text_str: |content| Some(content.to::<text::TextElem>()?.text()),
|
||||
smart_quote: |double| text::SmartQuoteElem::new().with_double(double).pack(),
|
||||
smart_quote: |double| text::SmartquoteElem::new().with_double(double).pack(),
|
||||
parbreak: || layout::ParbreakElem::new().pack(),
|
||||
strong: |body| text::StrongElem::new(body).pack(),
|
||||
emph: |body| text::EmphElem::new(body).pack(),
|
||||
@ -85,7 +120,7 @@ fn items() -> LangItems {
|
||||
},
|
||||
bibliography_keys: meta::BibliographyElem::keys,
|
||||
heading: |level, title| meta::HeadingElem::new(title).with_level(level).pack(),
|
||||
heading_func: meta::HeadingElem::func(),
|
||||
heading_elem: meta::HeadingElem::elem(),
|
||||
list_item: |body| layout::ListItem::new(body).pack(),
|
||||
enum_item: |number, body| {
|
||||
let mut elem = layout::EnumItem::new(body);
|
||||
@ -95,9 +130,6 @@ fn items() -> LangItems {
|
||||
elem.pack()
|
||||
},
|
||||
term_item: |term, description| layout::TermItem::new(term, description).pack(),
|
||||
rgb_func: compute::rgb_func(),
|
||||
cmyk_func: compute::cmyk_func(),
|
||||
luma_func: compute::luma_func(),
|
||||
equation: |body, block| math::EquationElem::new(body).with_block(block).pack(),
|
||||
math_align_point: || math::AlignPointElem::new().pack(),
|
||||
math_delimited: |open, body, close| math::LrElem::new(open + body + close).pack(),
|
||||
@ -131,15 +163,5 @@ fn items() -> LangItems {
|
||||
math_root: |index, radicand| {
|
||||
math::RootElem::new(radicand).with_index(index).pack()
|
||||
},
|
||||
library_method: |vm, dynamic, method, args, span| {
|
||||
if let Some(counter) = dynamic.downcast::<meta::Counter>().cloned() {
|
||||
counter.call_method(vm, method, args, span)
|
||||
} else if let Some(state) = dynamic.downcast::<meta::State>().cloned() {
|
||||
state.call_method(vm, method, args, span)
|
||||
} else {
|
||||
Err(format!("type {} has no method `{method}`", dynamic.type_name()))
|
||||
.at(span)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -5,16 +5,13 @@ const ACCENT_SHORT_FALL: Em = Em::new(0.5);
|
||||
|
||||
/// Attaches an accent to a base.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// # Example
|
||||
/// ```example
|
||||
/// $grave(a) = accent(a, `)$ \
|
||||
/// $arrow(a) = accent(a, arrow)$ \
|
||||
/// $tilde(a) = accent(a, \u{0303})$
|
||||
/// ```
|
||||
///
|
||||
/// Display: Accent
|
||||
/// Category: math
|
||||
#[element(LayoutMath)]
|
||||
#[elem(LayoutMath)]
|
||||
pub struct AccentElem {
|
||||
/// The base to which the accent is applied.
|
||||
/// May consist of multiple letters.
|
||||
|
@ -1,10 +1,7 @@
|
||||
use super::*;
|
||||
|
||||
/// A math alignment point: `&`, `&&`.
|
||||
///
|
||||
/// Display: Alignment Point
|
||||
/// Category: math
|
||||
#[element(LayoutMath)]
|
||||
#[elem(title = "Alignment Point", LayoutMath)]
|
||||
pub struct AlignPointElem {}
|
||||
|
||||
impl LayoutMath for AlignPointElem {
|
||||
|
@ -2,26 +2,13 @@ use super::*;
|
||||
|
||||
/// A base with optional attachments.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// ```example
|
||||
/// // With syntax.
|
||||
/// $ sum_(i=0)^n a_i = 2^(1+i) $
|
||||
///
|
||||
/// // With function call.
|
||||
/// $ attach(
|
||||
/// Pi, t: alpha, b: beta,
|
||||
/// tl: 1, tr: 2+3, bl: 4+5, br: 6,
|
||||
/// ) $
|
||||
/// ```
|
||||
///
|
||||
/// ## Syntax { #syntax }
|
||||
/// This function also has dedicated syntax for attachments after the base: Use
|
||||
/// the underscore (`_`) to indicate a subscript i.e. bottom attachment and the
|
||||
/// hat (`^`) to indicate a superscript i.e. top attachment.
|
||||
///
|
||||
/// Display: Attachment
|
||||
/// Category: math
|
||||
#[element(LayoutMath)]
|
||||
#[elem(LayoutMath)]
|
||||
pub struct AttachElem {
|
||||
/// The base to which things are attached.
|
||||
#[required]
|
||||
@ -86,19 +73,15 @@ impl LayoutMath for AttachElem {
|
||||
|
||||
/// Grouped primes.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// ```example
|
||||
/// $ a'''_b = a^'''_b $
|
||||
/// ```
|
||||
///
|
||||
/// ## Syntax
|
||||
/// # Syntax
|
||||
/// This function has dedicated syntax: use apostrophes instead of primes. They
|
||||
/// will automatically attach to the previous element, moving superscripts to
|
||||
/// the next level.
|
||||
///
|
||||
/// Display: Attachment
|
||||
/// Category: math
|
||||
#[element(LayoutMath)]
|
||||
#[elem(LayoutMath)]
|
||||
pub struct PrimesElem {
|
||||
/// The number of grouped primes.
|
||||
#[required]
|
||||
@ -141,14 +124,10 @@ impl LayoutMath for PrimesElem {
|
||||
|
||||
/// Forces a base to display attachments as scripts.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// ```example
|
||||
/// $ scripts(sum)_1^2 != sum_1^2 $
|
||||
/// ```
|
||||
///
|
||||
/// Display: Scripts
|
||||
/// Category: math
|
||||
#[element(LayoutMath)]
|
||||
#[elem(LayoutMath)]
|
||||
pub struct ScriptsElem {
|
||||
/// The base to attach the scripts to.
|
||||
#[required]
|
||||
@ -167,14 +146,10 @@ impl LayoutMath for ScriptsElem {
|
||||
|
||||
/// Forces a base to display attachments as limits.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// ```example
|
||||
/// $ limits(A)_1^2 != A_1^2 $
|
||||
/// ```
|
||||
///
|
||||
/// Display: Limits
|
||||
/// Category: math
|
||||
#[element(LayoutMath)]
|
||||
#[elem(LayoutMath)]
|
||||
pub struct LimitsElem {
|
||||
/// The base to attach the limits to.
|
||||
#[required]
|
||||
|
@ -4,17 +4,14 @@ use super::*;
|
||||
///
|
||||
/// This is commonly used to show the elimination of a term.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// # Example
|
||||
/// ```example
|
||||
/// >>> #set page(width: 140pt)
|
||||
/// Here, we can simplify:
|
||||
/// $ (a dot b dot cancel(x)) /
|
||||
/// cancel(x) $
|
||||
/// ```
|
||||
///
|
||||
/// Display: Cancel
|
||||
/// Category: math
|
||||
#[element(LayoutMath)]
|
||||
#[elem(LayoutMath)]
|
||||
pub struct CancelElem {
|
||||
/// The content over which the line should be placed.
|
||||
#[required]
|
||||
@ -53,8 +50,8 @@ pub struct CancelElem {
|
||||
#[default(false)]
|
||||
pub cross: bool,
|
||||
|
||||
/// How to rotate the cancel line. See the [line's
|
||||
/// documentation]($func/line.angle) for more details.
|
||||
/// How to rotate the cancel line. See the
|
||||
/// [line's documentation]($line.angle) for more details.
|
||||
///
|
||||
/// ```example
|
||||
/// >>> #set page(width: 140pt)
|
||||
@ -63,8 +60,7 @@ pub struct CancelElem {
|
||||
#[default(Angle::zero())]
|
||||
pub rotation: Angle,
|
||||
|
||||
/// How to stroke the cancel line. See the
|
||||
/// [line's documentation]($func/line.stroke) for more details.
|
||||
/// How to [stroke]($stroke) the cancel line.
|
||||
///
|
||||
/// ```example
|
||||
/// >>> #set page(width: 140pt)
|
||||
@ -79,12 +75,12 @@ pub struct CancelElem {
|
||||
/// ```
|
||||
#[resolve]
|
||||
#[fold]
|
||||
#[default(PartialStroke {
|
||||
#[default(Stroke {
|
||||
// Default stroke has 0.5pt for better visuals.
|
||||
thickness: Smart::Custom(Abs::pt(0.5)),
|
||||
..Default::default()
|
||||
})]
|
||||
pub stroke: PartialStroke,
|
||||
pub stroke: Stroke,
|
||||
}
|
||||
|
||||
impl LayoutMath for CancelElem {
|
||||
@ -99,7 +95,7 @@ impl LayoutMath for CancelElem {
|
||||
let span = self.span();
|
||||
let length = self.length(styles).resolve(styles);
|
||||
|
||||
let stroke = self.stroke(styles).unwrap_or(Stroke {
|
||||
let stroke = self.stroke(styles).unwrap_or(FixedStroke {
|
||||
paint: TextElem::fill_in(styles),
|
||||
..Default::default()
|
||||
});
|
||||
@ -139,7 +135,7 @@ impl LayoutMath for CancelElem {
|
||||
/// Draws a cancel line.
|
||||
fn draw_cancel_line(
|
||||
length: Rel<Abs>,
|
||||
stroke: Stroke,
|
||||
stroke: FixedStroke,
|
||||
invert: bool,
|
||||
angle: Angle,
|
||||
body_size: Size,
|
||||
@ -172,8 +168,8 @@ fn draw_cancel_line(
|
||||
// (-width / 2, height / 2) with length components (width, -height) (sign is
|
||||
// inverted in the y-axis). After applying the scale, the line will have the
|
||||
// correct length and orientation (inverted if needed).
|
||||
let start = Axes::new(-mid.x, mid.y).zip(scales).map(|(l, s)| l * s);
|
||||
let delta = Axes::new(width, -height).zip(scales).map(|(l, s)| l * s);
|
||||
let start = Axes::new(-mid.x, mid.y).zip_map(scales, |l, s| l * s);
|
||||
let delta = Axes::new(width, -height).zip_map(scales, |l, s| l * s);
|
||||
|
||||
let mut frame = Frame::new(body_size);
|
||||
frame.push(
|
||||
|
@ -5,7 +5,7 @@ use super::*;
|
||||
/// This is useful to treat certain symbols as if they were of a different
|
||||
/// class, e.g. to make a symbol behave like a relation.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// # Example
|
||||
/// ```example
|
||||
/// #let loves = math.class(
|
||||
/// "relation",
|
||||
@ -14,10 +14,7 @@ use super::*;
|
||||
///
|
||||
/// $x loves y and y loves 5$
|
||||
/// ```
|
||||
///
|
||||
/// Display: Class
|
||||
/// Category: math
|
||||
#[element(LayoutMath)]
|
||||
#[elem(LayoutMath)]
|
||||
pub struct ClassElem {
|
||||
/// The class to apply to the content.
|
||||
#[required]
|
||||
|
@ -4,21 +4,18 @@ const FRAC_AROUND: Em = Em::new(0.1);
|
||||
|
||||
/// A mathematical fraction.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// # Example
|
||||
/// ```example
|
||||
/// $ 1/2 < (x+1)/2 $
|
||||
/// $ ((x+1)) / 2 = frac(a, b) $
|
||||
/// ```
|
||||
///
|
||||
/// ## Syntax { #syntax }
|
||||
/// # Syntax
|
||||
/// This function also has dedicated syntax: Use a slash to turn neighbouring
|
||||
/// expressions into a fraction. Multiple atoms can be grouped into a single
|
||||
/// expression using round grouping parenthesis. Such parentheses are removed
|
||||
/// from the output, but you can nest multiple to force them.
|
||||
///
|
||||
/// Display: Fraction
|
||||
/// Category: math
|
||||
#[element(LayoutMath)]
|
||||
#[elem(title = "Fraction", LayoutMath)]
|
||||
pub struct FracElem {
|
||||
/// The fraction's numerator.
|
||||
#[required]
|
||||
@ -38,14 +35,11 @@ impl LayoutMath for FracElem {
|
||||
|
||||
/// A binomial expression.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// # Example
|
||||
/// ```example
|
||||
/// $ binom(n, k) $
|
||||
/// ```
|
||||
///
|
||||
/// Display: Binomial
|
||||
/// Category: math
|
||||
#[element(LayoutMath)]
|
||||
#[elem(title = "Binomial", LayoutMath)]
|
||||
pub struct BinomElem {
|
||||
/// The binomial's upper index.
|
||||
#[required]
|
||||
@ -135,10 +129,10 @@ fn layout(
|
||||
frame.push(
|
||||
line_pos,
|
||||
FrameItem::Shape(
|
||||
Geometry::Line(Point::with_x(line_width)).stroked(Stroke {
|
||||
Geometry::Line(Point::with_x(line_width)).stroked(FixedStroke {
|
||||
paint: TextElem::fill_in(ctx.styles()),
|
||||
thickness,
|
||||
..Stroke::default()
|
||||
..FixedStroke::default()
|
||||
}),
|
||||
span,
|
||||
),
|
||||
|
@ -7,16 +7,7 @@ pub(super) const DELIM_SHORT_FALL: Em = Em::new(0.1);
|
||||
///
|
||||
/// While matched delimiters scale by default, this can be used to scale
|
||||
/// unmatched delimiters and to control the delimiter scaling more precisely.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// ```example
|
||||
/// $ lr(]a, b/2]) $
|
||||
/// $ lr(]sum_(x=1)^n] x, size: #50%) $
|
||||
/// ```
|
||||
///
|
||||
/// Display: Left/Right
|
||||
/// Category: math
|
||||
#[element(LayoutMath)]
|
||||
#[elem(title = "Left/Right", LayoutMath)]
|
||||
pub struct LrElem {
|
||||
/// The size of the brackets, relative to the height of the wrapped content.
|
||||
pub size: Smart<Rel<Length>>,
|
||||
@ -107,13 +98,9 @@ fn scale(
|
||||
|
||||
/// Floors an expression.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// ```example
|
||||
/// $ floor(x/2) $
|
||||
/// ```
|
||||
///
|
||||
/// Display: Floor
|
||||
/// Category: math
|
||||
#[func]
|
||||
pub fn floor(
|
||||
/// The expression to floor.
|
||||
@ -124,13 +111,9 @@ pub fn floor(
|
||||
|
||||
/// Ceils an expression.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// ```example
|
||||
/// $ ceil(x/2) $
|
||||
/// ```
|
||||
///
|
||||
/// Display: Ceil
|
||||
/// Category: math
|
||||
#[func]
|
||||
pub fn ceil(
|
||||
/// The expression to ceil.
|
||||
@ -141,13 +124,9 @@ pub fn ceil(
|
||||
|
||||
/// Rounds an expression.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// ```example
|
||||
/// $ round(x/2) $
|
||||
/// ```
|
||||
///
|
||||
/// Display: Round
|
||||
/// Category: math
|
||||
#[func]
|
||||
pub fn round(
|
||||
/// The expression to round.
|
||||
@ -158,14 +137,9 @@ pub fn round(
|
||||
|
||||
/// Takes the absolute value of an expression.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// ```example
|
||||
/// $ abs(x/2) $
|
||||
/// ```
|
||||
///
|
||||
///
|
||||
/// Display: Abs
|
||||
/// Category: math
|
||||
#[func]
|
||||
pub fn abs(
|
||||
/// The expression to take the absolute value of.
|
||||
@ -176,13 +150,9 @@ pub fn abs(
|
||||
|
||||
/// Takes the norm of an expression.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// ```example
|
||||
/// $ norm(x/2) $
|
||||
/// ```
|
||||
///
|
||||
/// Display: Norm
|
||||
/// Category: math
|
||||
#[func]
|
||||
pub fn norm(
|
||||
/// The expression to take the norm of.
|
@ -12,15 +12,12 @@ const DEFAULT_STROKE_THICKNESS: Em = Em::new(0.05);
|
||||
///
|
||||
/// Content in the vector's elements can be aligned with the `&` symbol.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// # Example
|
||||
/// ```example
|
||||
/// $ vec(a, b, c) dot vec(1, 2, 3)
|
||||
/// = a + 2b + 3c $
|
||||
/// ```
|
||||
///
|
||||
/// Display: Vector
|
||||
/// Category: math
|
||||
#[element(LayoutMath)]
|
||||
#[elem(title = "Vector", LayoutMath)]
|
||||
pub struct VecElem {
|
||||
/// The delimiter to use.
|
||||
///
|
||||
@ -40,7 +37,7 @@ impl LayoutMath for VecElem {
|
||||
#[tracing::instrument(skip(ctx))]
|
||||
fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
|
||||
let delim = self.delim(ctx.styles());
|
||||
let frame = layout_vec_body(ctx, &self.children(), Align::Center)?;
|
||||
let frame = layout_vec_body(ctx, &self.children(), FixedAlign::Center)?;
|
||||
layout_delimiters(
|
||||
ctx,
|
||||
frame,
|
||||
@ -61,7 +58,7 @@ impl LayoutMath for VecElem {
|
||||
///
|
||||
/// Content in cells that are in the same row can be aligned with the `&` symbol.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// # Example
|
||||
/// ```example
|
||||
/// $ mat(
|
||||
/// 1, 2, ..., 10;
|
||||
@ -70,10 +67,7 @@ impl LayoutMath for VecElem {
|
||||
/// 10, 10, ..., 10;
|
||||
/// ) $
|
||||
/// ```
|
||||
///
|
||||
/// Display: Matrix
|
||||
/// Category: math
|
||||
#[element(LayoutMath)]
|
||||
#[elem(title = "Matrix", LayoutMath)]
|
||||
pub struct MatElem {
|
||||
/// The delimiter to use.
|
||||
///
|
||||
@ -102,10 +96,8 @@ pub struct MatElem {
|
||||
/// drawn after the second column of the matrix. Accepts either an
|
||||
/// integer for a single line, or an array of integers
|
||||
/// for multiple lines.
|
||||
/// - `stroke`: How to stroke the line. See the
|
||||
/// [line's documentation]($func/line.stroke)
|
||||
/// for more details. If set to `{auto}`, takes on a thickness of
|
||||
/// 0.05em and square line caps.
|
||||
/// - `stroke`: How to [stroke]($stroke) the line. If set to `{auto}`,
|
||||
/// takes on a thickness of 0.05em and square line caps.
|
||||
///
|
||||
/// ```example
|
||||
/// $ mat(1, 0, 1; 0, 1, 2; augment: #2) $
|
||||
@ -204,7 +196,7 @@ impl LayoutMath for MatElem {
|
||||
///
|
||||
/// Content across different branches can be aligned with the `&` symbol.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// # Example
|
||||
/// ```example
|
||||
/// $ f(x, y) := cases(
|
||||
/// 1 "if" (x dot y)/2 <= 0,
|
||||
@ -213,10 +205,7 @@ impl LayoutMath for MatElem {
|
||||
/// 4 "else",
|
||||
/// ) $
|
||||
/// ```
|
||||
///
|
||||
/// Display: Cases
|
||||
/// Category: math
|
||||
#[element(LayoutMath)]
|
||||
#[elem(LayoutMath)]
|
||||
pub struct CasesElem {
|
||||
/// The delimiter to use.
|
||||
///
|
||||
@ -236,7 +225,7 @@ impl LayoutMath for CasesElem {
|
||||
#[tracing::instrument(skip(ctx))]
|
||||
fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
|
||||
let delim = self.delim(ctx.styles());
|
||||
let frame = layout_vec_body(ctx, &self.children(), Align::Left)?;
|
||||
let frame = layout_vec_body(ctx, &self.children(), FixedAlign::Start)?;
|
||||
layout_delimiters(ctx, frame, Some(delim.open()), None, self.span())
|
||||
}
|
||||
}
|
||||
@ -289,7 +278,7 @@ impl Delimiter {
|
||||
fn layout_vec_body(
|
||||
ctx: &mut MathContext,
|
||||
column: &[Content],
|
||||
align: Align,
|
||||
align: FixedAlign,
|
||||
) -> SourceResult<Frame> {
|
||||
let gap = ROW_GAP.scaled(ctx);
|
||||
ctx.style(ctx.style.for_denominator());
|
||||
@ -319,7 +308,7 @@ fn layout_mat_body(
|
||||
// look correct by default at all matrix sizes.
|
||||
// The line cap is also set to square because it looks more "correct".
|
||||
let default_stroke_thickness = DEFAULT_STROKE_THICKNESS.scaled(ctx);
|
||||
let default_stroke = Stroke {
|
||||
let default_stroke = FixedStroke {
|
||||
thickness: default_stroke_thickness,
|
||||
line_cap: LineCap::Square,
|
||||
..Default::default()
|
||||
@ -383,7 +372,7 @@ fn layout_mat_body(
|
||||
let mut y = Abs::zero();
|
||||
|
||||
for (cell, &(ascent, descent)) in col.into_iter().zip(&heights) {
|
||||
let cell = cell.into_aligned_frame(ctx, &points, Align::Center);
|
||||
let cell = cell.into_aligned_frame(ctx, &points, FixedAlign::Center);
|
||||
let pos = Point::new(
|
||||
if points.is_empty() { x + (rcol - cell.width()) / 2.0 } else { x },
|
||||
y + ascent - cell.ascent(),
|
||||
@ -429,7 +418,7 @@ fn layout_mat_body(
|
||||
Ok(frame)
|
||||
}
|
||||
|
||||
fn line_item(length: Abs, vertical: bool, stroke: Stroke, span: Span) -> FrameItem {
|
||||
fn line_item(length: Abs, vertical: bool, stroke: FixedStroke, span: Span) -> FrameItem {
|
||||
let line_geom = if vertical {
|
||||
Geometry::Line(Point::with_y(length))
|
||||
} else {
|
||||
@ -482,14 +471,14 @@ fn layout_delimiters(
|
||||
/// Parameters specifying how augmentation lines
|
||||
/// should be drawn on a matrix.
|
||||
#[derive(Default, Clone, Hash)]
|
||||
pub struct Augment<T = Length> {
|
||||
pub struct Augment<T: Numeric = Length> {
|
||||
pub hline: Offsets,
|
||||
pub vline: Offsets,
|
||||
pub stroke: Smart<PartialStroke<T>>,
|
||||
pub stroke: Smart<Stroke<T>>,
|
||||
}
|
||||
|
||||
impl Augment<Abs> {
|
||||
fn stroke_or(&self, fallback: Stroke) -> Stroke {
|
||||
fn stroke_or(&self, fallback: FixedStroke) -> FixedStroke {
|
||||
match &self.stroke {
|
||||
Smart::Custom(v) => v.clone().unwrap_or(fallback),
|
||||
_ => fallback,
|
||||
@ -543,7 +532,7 @@ cast! {
|
||||
let vline = dict.take("vline").ok().map(Offsets::from_value)
|
||||
.transpose().unwrap_or_default().unwrap_or_default();
|
||||
|
||||
let stroke = dict.take("stroke").ok().map(PartialStroke::from_value)
|
||||
let stroke = dict.take("stroke").ok().map(Stroke::from_value)
|
||||
.transpose()?.map(Smart::Custom).unwrap_or(Smart::Auto);
|
||||
|
||||
Augment { hline, vline, stroke }
|
||||
|
@ -7,9 +7,9 @@ mod align;
|
||||
mod attach;
|
||||
mod cancel;
|
||||
mod class;
|
||||
mod delimited;
|
||||
mod frac;
|
||||
mod fragment;
|
||||
mod lr;
|
||||
mod matrix;
|
||||
mod op;
|
||||
mod root;
|
||||
@ -24,8 +24,8 @@ pub use self::align::*;
|
||||
pub use self::attach::*;
|
||||
pub use self::cancel::*;
|
||||
pub use self::class::*;
|
||||
pub use self::delimited::*;
|
||||
pub use self::frac::*;
|
||||
pub use self::lr::*;
|
||||
pub use self::matrix::*;
|
||||
pub use self::op::*;
|
||||
pub use self::root::*;
|
||||
@ -57,79 +57,64 @@ use crate::text::{
|
||||
/// Create a module with all math definitions.
|
||||
pub fn module() -> Module {
|
||||
let mut math = Scope::deduplicating();
|
||||
math.define("equation", EquationElem::func());
|
||||
math.define("text", TextElem::func());
|
||||
math.category("math");
|
||||
math.define_elem::<EquationElem>();
|
||||
math.define_elem::<TextElem>();
|
||||
math.define_elem::<LrElem>();
|
||||
math.define_elem::<AttachElem>();
|
||||
math.define_elem::<ScriptsElem>();
|
||||
math.define_elem::<LimitsElem>();
|
||||
math.define_elem::<AccentElem>();
|
||||
math.define_elem::<UnderlineElem>();
|
||||
math.define_elem::<OverlineElem>();
|
||||
math.define_elem::<UnderbraceElem>();
|
||||
math.define_elem::<OverbraceElem>();
|
||||
math.define_elem::<UnderbracketElem>();
|
||||
math.define_elem::<OverbracketElem>();
|
||||
math.define_elem::<CancelElem>();
|
||||
math.define_elem::<FracElem>();
|
||||
math.define_elem::<BinomElem>();
|
||||
math.define_elem::<VecElem>();
|
||||
math.define_elem::<MatElem>();
|
||||
math.define_elem::<CasesElem>();
|
||||
math.define_elem::<RootElem>();
|
||||
math.define_elem::<ClassElem>();
|
||||
math.define_elem::<OpElem>();
|
||||
math.define_func::<abs>();
|
||||
math.define_func::<norm>();
|
||||
math.define_func::<floor>();
|
||||
math.define_func::<ceil>();
|
||||
math.define_func::<round>();
|
||||
math.define_func::<sqrt>();
|
||||
math.define_func::<upright>();
|
||||
math.define_func::<bold>();
|
||||
math.define_func::<italic>();
|
||||
math.define_func::<serif>();
|
||||
math.define_func::<sans>();
|
||||
math.define_func::<cal>();
|
||||
math.define_func::<frak>();
|
||||
math.define_func::<mono>();
|
||||
math.define_func::<bb>();
|
||||
math.define_func::<display>();
|
||||
math.define_func::<inline>();
|
||||
math.define_func::<script>();
|
||||
math.define_func::<sscript>();
|
||||
|
||||
// Grouping.
|
||||
math.define("lr", LrElem::func());
|
||||
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());
|
||||
math.define("scripts", ScriptsElem::func());
|
||||
math.define("limits", LimitsElem::func());
|
||||
math.define("accent", AccentElem::func());
|
||||
math.define("underline", UnderlineElem::func());
|
||||
math.define("overline", OverlineElem::func());
|
||||
math.define("underbrace", UnderbraceElem::func());
|
||||
math.define("overbrace", OverbraceElem::func());
|
||||
math.define("underbracket", UnderbracketElem::func());
|
||||
math.define("overbracket", OverbracketElem::func());
|
||||
math.define("cancel", CancelElem::func());
|
||||
|
||||
// Fractions and matrix-likes.
|
||||
math.define("frac", FracElem::func());
|
||||
math.define("binom", BinomElem::func());
|
||||
math.define("vec", VecElem::func());
|
||||
math.define("mat", MatElem::func());
|
||||
math.define("cases", CasesElem::func());
|
||||
|
||||
// Roots.
|
||||
math.define("sqrt", sqrt_func());
|
||||
math.define("root", RootElem::func());
|
||||
|
||||
// Styles.
|
||||
math.define("upright", upright_func());
|
||||
math.define("bold", bold_func());
|
||||
math.define("italic", italic_func());
|
||||
math.define("serif", serif_func());
|
||||
math.define("sans", sans_func());
|
||||
math.define("cal", cal_func());
|
||||
math.define("frak", frak_func());
|
||||
math.define("mono", mono_func());
|
||||
math.define("bb", bb_func());
|
||||
|
||||
math.define("display", display_func());
|
||||
math.define("inline", inline_func());
|
||||
math.define("script", script_func());
|
||||
math.define("sscript", sscript_func());
|
||||
|
||||
math.define("class", ClassElem::func());
|
||||
|
||||
// Text operators.
|
||||
math.define("op", OpElem::func());
|
||||
// Text operators, spacings, and symbols.
|
||||
op::define(&mut math);
|
||||
|
||||
// Spacings.
|
||||
spacing::define(&mut math);
|
||||
|
||||
// Symbols.
|
||||
for (name, symbol) in crate::symbols::SYM {
|
||||
math.define(*name, symbol.clone());
|
||||
}
|
||||
|
||||
Module::new("math").with_scope(math)
|
||||
Module::new("math", math)
|
||||
}
|
||||
|
||||
/// A mathematical equation.
|
||||
///
|
||||
/// Can be displayed inline with text or as a separate block.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// # Example
|
||||
/// ```example
|
||||
/// #set text(font: "New Computer Modern")
|
||||
///
|
||||
@ -142,16 +127,13 @@ pub fn module() -> Module {
|
||||
/// $ sum_(k=1)^n k = (n(n+1)) / 2 $
|
||||
/// ```
|
||||
///
|
||||
/// ## Syntax { #syntax }
|
||||
/// # Syntax
|
||||
/// This function also has dedicated syntax: Write mathematical markup within
|
||||
/// dollar signs to create an equation. Starting and ending the equation with at
|
||||
/// least one space lifts it into a separate block that is centered
|
||||
/// horizontally. For more details about math syntax, see the
|
||||
/// [main math page]($category/math).
|
||||
///
|
||||
/// Display: Equation
|
||||
/// Category: math
|
||||
#[element(
|
||||
#[elem(
|
||||
Locatable, Synthesize, Show, Finalize, Layout, LayoutMath, Count, LocalName, Refable,
|
||||
Outlinable
|
||||
)]
|
||||
@ -160,7 +142,7 @@ pub struct EquationElem {
|
||||
#[default(false)]
|
||||
pub block: bool,
|
||||
|
||||
/// How to [number]($func/numbering) block-level equations.
|
||||
/// How to [number]($numbering) block-level equations.
|
||||
///
|
||||
/// ```example
|
||||
/// #set math.equation(numbering: "(1)")
|
||||
@ -216,9 +198,9 @@ impl Synthesize for EquationElem {
|
||||
impl Show for EquationElem {
|
||||
#[tracing::instrument(name = "EquationElem::show", skip_all)]
|
||||
fn show(&self, _: &mut Vt, styles: StyleChain) -> SourceResult<Content> {
|
||||
let mut realized = self.clone().pack().guarded(Guard::Base(Self::func()));
|
||||
let mut realized = self.clone().pack().guarded(Guard::Base(Self::elem()));
|
||||
if self.block(styles) {
|
||||
realized = realized.aligned(Axes::with_x(Some(Align::Center.into())))
|
||||
realized = realized.aligned(Align::CENTER);
|
||||
}
|
||||
Ok(realized)
|
||||
}
|
||||
@ -264,7 +246,7 @@ impl Layout for EquationElem {
|
||||
if block {
|
||||
if let Some(numbering) = self.numbering(styles) {
|
||||
let pod = Regions::one(regions.base(), Axes::splat(false));
|
||||
let counter = Counter::of(Self::func())
|
||||
let counter = Counter::of(Self::elem())
|
||||
.display(Some(numbering), false)
|
||||
.layout(vt, styles, pod)?
|
||||
.into_frame();
|
||||
@ -277,7 +259,7 @@ impl Layout for EquationElem {
|
||||
};
|
||||
|
||||
let height = frame.height().max(counter.height());
|
||||
frame.resize(Size::new(width, height), Align::CENTER_HORIZON);
|
||||
frame.resize(Size::new(width, height), Axes::splat(FixedAlign::Center));
|
||||
|
||||
let x = if TextElem::dir_in(styles).is_positive() {
|
||||
frame.width() - counter.width()
|
||||
@ -358,7 +340,7 @@ impl Refable for EquationElem {
|
||||
}
|
||||
|
||||
fn counter(&self) -> Counter {
|
||||
Counter::of(Self::func())
|
||||
Counter::of(Self::elem())
|
||||
}
|
||||
|
||||
fn numbering(&self) -> Option<Numbering> {
|
||||
|
@ -4,23 +4,20 @@ use super::*;
|
||||
|
||||
/// A text operator in an equation.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// # Example
|
||||
/// ```example
|
||||
/// $ tan x = (sin x)/(cos x) $
|
||||
/// $ op("custom",
|
||||
/// limits: #true)_(n->oo) n $
|
||||
/// ```
|
||||
///
|
||||
/// ## Predefined Operators { #predefined }
|
||||
/// # Predefined Operators { #predefined }
|
||||
/// Typst predefines the operators `arccos`, `arcsin`, `arctan`, `arg`, `cos`,
|
||||
/// `cosh`, `cot`, `coth`, `csc`, `ctg`, `deg`, `det`, `dim`, `exp`, `gcd`,
|
||||
/// `hom`, `id`, `im`, `inf`, `ker`, `lg`, `lim`, `liminf`, `limsup`, `ln`,
|
||||
/// `log`, `max`, `min`, `mod`, `Pr`, `sec`, `sin`, `sinc`, `sinh`, `sup`,
|
||||
/// `tan`, `tanh`, `tg` and `tr`.
|
||||
///
|
||||
/// Display: Text Operator
|
||||
/// Category: math
|
||||
#[element(LayoutMath)]
|
||||
#[elem(title = "Text Operator", LayoutMath)]
|
||||
pub struct OpElem {
|
||||
/// The operator's text.
|
||||
#[required]
|
||||
|
@ -2,14 +2,10 @@ use super::*;
|
||||
|
||||
/// A square root.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// ```example
|
||||
/// $ sqrt(3 - 2 sqrt(2)) = sqrt(2) - 1 $
|
||||
/// ```
|
||||
///
|
||||
/// Display: Square Root
|
||||
/// Category: math
|
||||
#[func]
|
||||
#[func(title = "Square Root")]
|
||||
pub fn sqrt(
|
||||
/// The expression to take the square root of.
|
||||
radicand: Content,
|
||||
@ -19,14 +15,10 @@ pub fn sqrt(
|
||||
|
||||
/// A general root.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// ```example
|
||||
/// $ root(3, x) $
|
||||
/// ```
|
||||
///
|
||||
/// Display: Root
|
||||
/// Category: math
|
||||
#[element(LayoutMath)]
|
||||
#[elem(LayoutMath)]
|
||||
pub struct RootElem {
|
||||
/// Which root of the radicand to take.
|
||||
#[positional]
|
||||
@ -129,10 +121,10 @@ fn layout(
|
||||
frame.push(
|
||||
line_pos,
|
||||
FrameItem::Shape(
|
||||
Geometry::Line(Point::with_x(radicand.width())).stroked(Stroke {
|
||||
Geometry::Line(Point::with_x(radicand.width())).stroked(FixedStroke {
|
||||
paint: TextElem::fill_in(ctx.styles()),
|
||||
thickness,
|
||||
..Stroke::default()
|
||||
..FixedStroke::default()
|
||||
}),
|
||||
span,
|
||||
),
|
||||
|
@ -121,7 +121,7 @@ impl MathRow {
|
||||
|
||||
pub fn into_frame(self, ctx: &MathContext) -> Frame {
|
||||
let styles = ctx.styles();
|
||||
let align = AlignElem::alignment_in(styles).x.resolve(styles);
|
||||
let align = AlignElem::alignment_in(styles).resolve(styles).x;
|
||||
self.into_aligned_frame(ctx, &[], align)
|
||||
}
|
||||
|
||||
@ -137,53 +137,54 @@ impl MathRow {
|
||||
self,
|
||||
ctx: &MathContext,
|
||||
points: &[Abs],
|
||||
align: Align,
|
||||
align: FixedAlign,
|
||||
) -> Frame {
|
||||
if self.iter().any(|frag| matches!(frag, MathFragment::Linebreak)) {
|
||||
let leading = if ctx.style.size >= MathSize::Text {
|
||||
ParElem::leading_in(ctx.styles())
|
||||
} else {
|
||||
TIGHT_LEADING.scaled(ctx)
|
||||
};
|
||||
|
||||
let mut rows: Vec<_> = self.rows();
|
||||
|
||||
if matches!(rows.last(), Some(row) if row.0.is_empty()) {
|
||||
rows.pop();
|
||||
}
|
||||
|
||||
let AlignmentResult { points, width } = alignments(&rows);
|
||||
let mut frame = Frame::new(Size::zero());
|
||||
|
||||
for (i, row) in rows.into_iter().enumerate() {
|
||||
let sub = row.into_line_frame(&points, align);
|
||||
let size = frame.size_mut();
|
||||
if i > 0 {
|
||||
size.y += leading;
|
||||
}
|
||||
|
||||
let mut pos = Point::with_y(size.y);
|
||||
if points.is_empty() {
|
||||
pos.x = align.position(width - sub.width());
|
||||
}
|
||||
size.y += sub.height();
|
||||
size.x.set_max(sub.width());
|
||||
frame.push_frame(pos, sub);
|
||||
}
|
||||
frame
|
||||
} else {
|
||||
self.into_line_frame(points, align)
|
||||
if !self.iter().any(|frag| matches!(frag, MathFragment::Linebreak)) {
|
||||
return self.into_line_frame(points, align);
|
||||
}
|
||||
|
||||
let leading = if ctx.style.size >= MathSize::Text {
|
||||
ParElem::leading_in(ctx.styles())
|
||||
} else {
|
||||
TIGHT_LEADING.scaled(ctx)
|
||||
};
|
||||
|
||||
let mut rows: Vec<_> = self.rows();
|
||||
|
||||
if matches!(rows.last(), Some(row) if row.0.is_empty()) {
|
||||
rows.pop();
|
||||
}
|
||||
|
||||
let AlignmentResult { points, width } = alignments(&rows);
|
||||
let mut frame = Frame::new(Size::zero());
|
||||
|
||||
for (i, row) in rows.into_iter().enumerate() {
|
||||
let sub = row.into_line_frame(&points, align);
|
||||
let size = frame.size_mut();
|
||||
if i > 0 {
|
||||
size.y += leading;
|
||||
}
|
||||
|
||||
let mut pos = Point::with_y(size.y);
|
||||
if points.is_empty() {
|
||||
pos.x = align.position(width - sub.width());
|
||||
}
|
||||
size.y += sub.height();
|
||||
size.x.set_max(sub.width());
|
||||
frame.push_frame(pos, sub);
|
||||
}
|
||||
|
||||
frame
|
||||
}
|
||||
|
||||
fn into_line_frame(self, points: &[Abs], align: Align) -> Frame {
|
||||
fn into_line_frame(self, points: &[Abs], align: FixedAlign) -> Frame {
|
||||
let ascent = self.ascent();
|
||||
let mut frame = Frame::new(Size::new(Abs::zero(), ascent + self.descent()));
|
||||
frame.set_baseline(ascent);
|
||||
|
||||
let mut next_x = {
|
||||
let mut widths = Vec::new();
|
||||
if !points.is_empty() && align != Align::Left {
|
||||
if !points.is_empty() && align != FixedAlign::Start {
|
||||
let mut width = Abs::zero();
|
||||
for fragment in self.iter() {
|
||||
if matches!(fragment, MathFragment::Align) {
|
||||
@ -201,8 +202,10 @@ impl MathRow {
|
||||
let mut point_widths = points.iter().copied().zip(widths);
|
||||
let mut alternator = LeftRightAlternator::Right;
|
||||
move || match align {
|
||||
Align::Left => prev_points.next(),
|
||||
Align::Right => point_widths.next().map(|(point, width)| point - width),
|
||||
FixedAlign::Start => prev_points.next(),
|
||||
FixedAlign::End => {
|
||||
point_widths.next().map(|(point, width)| point - width)
|
||||
}
|
||||
_ => point_widths
|
||||
.next()
|
||||
.zip(prev_points.next())
|
||||
|
@ -2,13 +2,9 @@ use super::*;
|
||||
|
||||
/// Bold font style in math.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// ```example
|
||||
/// $ bold(A) := B^+ $
|
||||
/// ```
|
||||
///
|
||||
/// Display: Bold
|
||||
/// Category: math
|
||||
#[func]
|
||||
pub fn bold(
|
||||
/// The content to style.
|
||||
@ -19,13 +15,9 @@ pub fn bold(
|
||||
|
||||
/// Upright (non-italic) font style in math.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// ```example
|
||||
/// $ upright(A) != A $
|
||||
/// ```
|
||||
///
|
||||
/// Display: Upright
|
||||
/// Category: math
|
||||
#[func]
|
||||
pub fn upright(
|
||||
/// The content to style.
|
||||
@ -37,9 +29,6 @@ pub fn upright(
|
||||
/// Italic font style in math.
|
||||
///
|
||||
/// For roman letters and greek lowercase letters, this is already the default.
|
||||
///
|
||||
/// Display: Italic
|
||||
/// Category: math
|
||||
#[func]
|
||||
pub fn italic(
|
||||
/// The content to style.
|
||||
@ -50,9 +39,6 @@ pub fn italic(
|
||||
/// Serif (roman) font style in math.
|
||||
///
|
||||
/// This is already the default.
|
||||
///
|
||||
/// Display: Serif
|
||||
/// Category: math
|
||||
#[func]
|
||||
pub fn serif(
|
||||
/// The content to style.
|
||||
@ -63,14 +49,10 @@ pub fn serif(
|
||||
|
||||
/// Sans-serif font style in math.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// ```example
|
||||
/// $ sans(A B C) $
|
||||
/// ```
|
||||
///
|
||||
/// Display: Sans-serif
|
||||
/// Category: math
|
||||
#[func]
|
||||
#[func(title = "Sans Serif")]
|
||||
pub fn sans(
|
||||
/// The content to style.
|
||||
body: Content,
|
||||
@ -80,14 +62,10 @@ pub fn sans(
|
||||
|
||||
/// Calligraphic font style in math.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// ```example
|
||||
/// Let $cal(P)$ be the set of ...
|
||||
/// ```
|
||||
///
|
||||
/// Display: Calligraphic
|
||||
/// Category: math
|
||||
#[func]
|
||||
#[func(title = "Calligraphic")]
|
||||
pub fn cal(
|
||||
/// The content to style.
|
||||
body: Content,
|
||||
@ -97,14 +75,10 @@ pub fn cal(
|
||||
|
||||
/// Fraktur font style in math.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// ```example
|
||||
/// $ frak(P) $
|
||||
/// ```
|
||||
///
|
||||
/// Display: Fraktur
|
||||
/// Category: math
|
||||
#[func]
|
||||
#[func(title = "Fraktur")]
|
||||
pub fn frak(
|
||||
/// The content to style.
|
||||
body: Content,
|
||||
@ -114,14 +88,10 @@ pub fn frak(
|
||||
|
||||
/// Monospace font style in math.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// ```example
|
||||
/// $ mono(x + y = z) $
|
||||
/// ```
|
||||
///
|
||||
/// Display: Monospace
|
||||
/// Category: math
|
||||
#[func]
|
||||
#[func(title = "Monospace")]
|
||||
pub fn mono(
|
||||
/// The content to style.
|
||||
body: Content,
|
||||
@ -134,16 +104,12 @@ pub fn mono(
|
||||
/// For uppercase latin letters, blackboard bold is additionally available
|
||||
/// through [symbols]($category/symbols/sym) of the form `NN` and `RR`.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// ```example
|
||||
/// $ bb(b) $
|
||||
/// $ bb(N) = NN $
|
||||
/// $ f: NN -> RR $
|
||||
/// ```
|
||||
///
|
||||
/// Display: Blackboard Bold
|
||||
/// Category: math
|
||||
#[func]
|
||||
#[func(title = "Blackboard Bold")]
|
||||
pub fn bb(
|
||||
/// The content to style.
|
||||
body: Content,
|
||||
@ -155,14 +121,10 @@ pub fn bb(
|
||||
///
|
||||
/// This is the normal size for block equations.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// ```example
|
||||
/// $sum_i x_i/2 = display(sum_i x_i/2)$
|
||||
/// ```
|
||||
///
|
||||
/// Display: Display Size
|
||||
/// Category: math
|
||||
#[func]
|
||||
#[func(title = "Display Size")]
|
||||
pub fn display(
|
||||
/// The content to size.
|
||||
body: Content,
|
||||
@ -182,15 +144,11 @@ pub fn display(
|
||||
///
|
||||
/// This is the normal size for inline equations.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// ```example
|
||||
/// $ sum_i x_i/2
|
||||
/// = inline(sum_i x_i/2) $
|
||||
/// ```
|
||||
///
|
||||
/// Display: Inline Size
|
||||
/// Category: math
|
||||
#[func]
|
||||
#[func(title = "Inline Size")]
|
||||
pub fn inline(
|
||||
/// The content to size.
|
||||
body: Content,
|
||||
@ -210,14 +168,10 @@ pub fn inline(
|
||||
///
|
||||
/// This is the smaller size used in powers or sub- or superscripts.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// ```example
|
||||
/// $sum_i x_i/2 = script(sum_i x_i/2)$
|
||||
/// ```
|
||||
///
|
||||
/// Display: Script Size
|
||||
/// Category: math
|
||||
#[func]
|
||||
#[func(title = "Script Size")]
|
||||
pub fn script(
|
||||
/// The content to size.
|
||||
body: Content,
|
||||
@ -238,14 +192,10 @@ pub fn script(
|
||||
/// This is the smallest size, used in second-level sub- and superscripts
|
||||
/// (script of the script).
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// ```example
|
||||
/// $sum_i x_i/2 = sscript(sum_i x_i/2)$
|
||||
/// ```
|
||||
///
|
||||
/// Display: Script-Script Size
|
||||
/// Category: math
|
||||
#[func]
|
||||
#[func(title = "Script-Script Size")]
|
||||
pub fn sscript(
|
||||
/// The content to size.
|
||||
body: Content,
|
||||
@ -262,10 +212,7 @@ pub fn sscript(
|
||||
}
|
||||
|
||||
/// A font variant in math.
|
||||
///
|
||||
/// Display: Bold
|
||||
/// Category: math
|
||||
#[element(LayoutMath)]
|
||||
#[elem(LayoutMath)]
|
||||
pub struct MathStyleElem {
|
||||
/// The content to style.
|
||||
#[required]
|
||||
|
@ -11,14 +11,10 @@ enum LineKind {
|
||||
|
||||
/// A horizontal line under content.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// ```example
|
||||
/// $ underline(1 + 2 + ... + 5) $
|
||||
/// ```
|
||||
///
|
||||
/// Display: Underline
|
||||
/// Category: math
|
||||
#[element(LayoutMath)]
|
||||
#[elem(LayoutMath)]
|
||||
pub struct UnderlineElem {
|
||||
/// The content above the line.
|
||||
#[required]
|
||||
@ -34,14 +30,10 @@ impl LayoutMath for UnderlineElem {
|
||||
|
||||
/// A horizontal line over content.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// ```example
|
||||
/// $ overline(1 + 2 + ... + 5) $
|
||||
/// ```
|
||||
///
|
||||
/// Display: Overline
|
||||
/// Category: math
|
||||
#[element(LayoutMath)]
|
||||
#[elem(LayoutMath)]
|
||||
pub struct OverlineElem {
|
||||
/// The content below the line.
|
||||
#[required]
|
||||
@ -103,10 +95,10 @@ fn layout_underoverline(
|
||||
frame.push(
|
||||
line_pos,
|
||||
FrameItem::Shape(
|
||||
Geometry::Line(Point::with_x(width)).stroked(Stroke {
|
||||
Geometry::Line(Point::with_x(width)).stroked(FixedStroke {
|
||||
paint: TextElem::fill_in(ctx.styles()),
|
||||
thickness: bar_height,
|
||||
..Stroke::default()
|
||||
..FixedStroke::default()
|
||||
}),
|
||||
span,
|
||||
),
|
||||
@ -119,14 +111,10 @@ fn layout_underoverline(
|
||||
|
||||
/// A horizontal brace under content, with an optional annotation below.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// ```example
|
||||
/// $ underbrace(1 + 2 + ... + 5, "numbers") $
|
||||
/// ```
|
||||
///
|
||||
/// Display: Underbrace
|
||||
/// Category: math
|
||||
#[element(LayoutMath)]
|
||||
#[elem(LayoutMath)]
|
||||
pub struct UnderbraceElem {
|
||||
/// The content above the brace.
|
||||
#[required]
|
||||
@ -154,14 +142,10 @@ impl LayoutMath for UnderbraceElem {
|
||||
|
||||
/// A horizontal brace over content, with an optional annotation above.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// ```example
|
||||
/// $ overbrace(1 + 2 + ... + 5, "numbers") $
|
||||
/// ```
|
||||
///
|
||||
/// Display: Overbrace
|
||||
/// Category: math
|
||||
#[element(LayoutMath)]
|
||||
#[elem(LayoutMath)]
|
||||
pub struct OverbraceElem {
|
||||
/// The content below the brace.
|
||||
#[required]
|
||||
@ -189,14 +173,10 @@ impl LayoutMath for OverbraceElem {
|
||||
|
||||
/// A horizontal bracket under content, with an optional annotation below.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// ```example
|
||||
/// $ underbracket(1 + 2 + ... + 5, "numbers") $
|
||||
/// ```
|
||||
///
|
||||
/// Display: Underbracket
|
||||
/// Category: math
|
||||
#[element(LayoutMath)]
|
||||
#[elem(LayoutMath)]
|
||||
pub struct UnderbracketElem {
|
||||
/// The content above the bracket.
|
||||
#[required]
|
||||
@ -224,14 +204,10 @@ impl LayoutMath for UnderbracketElem {
|
||||
|
||||
/// A horizontal bracket over content, with an optional annotation above.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// ```example
|
||||
/// $ overbracket(1 + 2 + ... + 5, "numbers") $
|
||||
/// ```
|
||||
///
|
||||
/// Display: Overbracket
|
||||
/// Category: math
|
||||
#[element(LayoutMath)]
|
||||
#[elem(LayoutMath)]
|
||||
pub struct OverbracketElem {
|
||||
/// The content below the bracket.
|
||||
#[required]
|
||||
@ -294,7 +270,7 @@ fn layout_underoverspreader(
|
||||
baseline = rows.len() - 1;
|
||||
}
|
||||
|
||||
let frame = stack(ctx, rows, Align::Center, gap, baseline);
|
||||
let frame = stack(ctx, rows, FixedAlign::Center, gap, baseline);
|
||||
ctx.push(FrameFragment::new(ctx, frame).with_class(body_class));
|
||||
|
||||
Ok(())
|
||||
@ -307,7 +283,7 @@ fn layout_underoverspreader(
|
||||
pub(super) fn stack(
|
||||
ctx: &MathContext,
|
||||
rows: Vec<MathRow>,
|
||||
align: Align,
|
||||
align: FixedAlign,
|
||||
gap: Abs,
|
||||
baseline: usize,
|
||||
) -> Frame {
|
||||
|
@ -30,8 +30,8 @@ use crate::text::TextElem;
|
||||
///
|
||||
/// As soon as you add a bibliography somewhere in your document, you can start
|
||||
/// citing things with reference syntax (`[@key]`) or explicit calls to the
|
||||
/// [citation]($func/cite) function (`[#cite("key")]`). The bibliography will
|
||||
/// only show entries for works that were referenced in the document.
|
||||
/// [citation]($cite) function (`[#cite("key")]`). The bibliography will only
|
||||
/// show entries for works that were referenced in the document.
|
||||
///
|
||||
/// # Example
|
||||
/// ```example
|
||||
@ -43,10 +43,7 @@ use crate::text::TextElem;
|
||||
///
|
||||
/// #bibliography("works.bib")
|
||||
/// ```
|
||||
///
|
||||
/// Display: Bibliography
|
||||
/// Category: meta
|
||||
#[element(Locatable, Synthesize, Show, Finalize, LocalName)]
|
||||
#[elem(Locatable, Synthesize, Show, Finalize, LocalName)]
|
||||
pub struct BibliographyElem {
|
||||
/// Path to a Hayagriva `.yml` or BibLaTeX `.bib` file.
|
||||
#[required]
|
||||
@ -78,8 +75,8 @@ pub struct BibliographyElem {
|
||||
|
||||
/// The title of the bibliography.
|
||||
///
|
||||
/// - When set to `{auto}`, an appropriate title for the [text
|
||||
/// language]($func/text.lang) will be used. This is the default.
|
||||
/// - When set to `{auto}`, an appropriate title for the
|
||||
/// [text language]($text.lang) will be used. This is the default.
|
||||
/// - When set to `{none}`, the bibliography will not have a title.
|
||||
/// - A custom title can be set by passing content.
|
||||
///
|
||||
@ -109,7 +106,7 @@ cast! {
|
||||
impl BibliographyElem {
|
||||
/// Find the document's bibliography.
|
||||
pub fn find(introspector: Tracked<Introspector>) -> StrResult<Self> {
|
||||
let mut iter = introspector.query(&Self::func().select()).into_iter();
|
||||
let mut iter = introspector.query(&Self::elem().select()).into_iter();
|
||||
let Some(elem) = iter.next() else {
|
||||
bail!("the document does not contain a bibliography");
|
||||
};
|
||||
@ -124,7 +121,7 @@ impl BibliographyElem {
|
||||
/// Whether the bibliography contains the given key.
|
||||
pub fn has(vt: &Vt, key: &str) -> bool {
|
||||
vt.introspector
|
||||
.query(&Self::func().select())
|
||||
.query(&Self::elem().select())
|
||||
.into_iter()
|
||||
.flat_map(|elem| {
|
||||
let elem = elem.to::<Self>().unwrap();
|
||||
@ -289,8 +286,8 @@ impl BibliographyStyle {
|
||||
|
||||
/// Cite a work from the bibliography.
|
||||
///
|
||||
/// Before you starting citing, you need to add a
|
||||
/// [bibliography]($func/bibliography) somewhere in your document.
|
||||
/// Before you starting citing, you need to add a [bibliography]($bibliography)
|
||||
/// somewhere in your document.
|
||||
///
|
||||
/// # Example
|
||||
/// ```example
|
||||
@ -304,13 +301,10 @@ impl BibliographyStyle {
|
||||
/// ```
|
||||
///
|
||||
/// # Syntax
|
||||
/// This function indirectly has dedicated syntax. [References]($func/ref)
|
||||
/// can be used to cite works from the bibliography. The label then
|
||||
/// corresponds to the citation key.
|
||||
///
|
||||
/// Display: Citation
|
||||
/// Category: meta
|
||||
#[element(Locatable, Synthesize, Show)]
|
||||
/// This function indirectly has dedicated syntax. [References]($ref) can be
|
||||
/// used to cite works from the bibliography. The label then corresponds to the
|
||||
/// citation key.
|
||||
#[elem(Locatable, Synthesize, Show)]
|
||||
pub struct CiteElem {
|
||||
/// The citation keys that identify the elements that shall be cited in
|
||||
/// the bibliography.
|
||||
@ -329,7 +323,6 @@ pub struct CiteElem {
|
||||
///
|
||||
/// #bibliography("works.bib")
|
||||
/// ```
|
||||
#[positional]
|
||||
pub supplement: Option<Content>,
|
||||
|
||||
/// Whether the citation should include brackets.
|
||||
@ -435,8 +428,8 @@ impl Works {
|
||||
let citations = vt
|
||||
.introspector
|
||||
.query(&Selector::Or(eco_vec![
|
||||
RefElem::func().select(),
|
||||
CiteElem::func().select(),
|
||||
RefElem::elem().select(),
|
||||
CiteElem::elem().select(),
|
||||
]))
|
||||
.into_iter()
|
||||
.map(|elem| match elem.to::<RefElem>() {
|
||||
|
@ -2,9 +2,9 @@ use crate::prelude::*;
|
||||
|
||||
/// Provides access to the location of content.
|
||||
///
|
||||
/// This is useful in combination with [queries]($func/query),
|
||||
/// [counters]($func/counter), [state]($func/state), and [links]($func/link).
|
||||
/// See their documentation for more details.
|
||||
/// This is useful in combination with [queries]($query), [counters]($counter),
|
||||
/// [state]($state), and [links]($link). See their documentation for more
|
||||
/// details.
|
||||
///
|
||||
/// ```example
|
||||
/// #locate(loc => [
|
||||
@ -12,44 +12,10 @@ use crate::prelude::*;
|
||||
/// #loc.position()!
|
||||
/// ])
|
||||
/// ```
|
||||
///
|
||||
/// ## Methods
|
||||
/// ### page()
|
||||
/// Returns the page number for this location.
|
||||
///
|
||||
/// Note that this does not return the value of the [page counter]($func/counter)
|
||||
/// at this location, but the true page number (starting from one).
|
||||
///
|
||||
/// If you want to know the value of the page counter, use
|
||||
/// `{counter(page).at(loc)}` instead.
|
||||
///
|
||||
/// - returns: integer
|
||||
///
|
||||
/// ### position()
|
||||
/// Returns a dictionary with the page number and the x, y position for this
|
||||
/// location. The page number starts at one and the coordinates are measured
|
||||
/// from the top-left of the page.
|
||||
///
|
||||
/// If you only need the page number, use `page()` instead as it allows Typst
|
||||
/// to skip unnecessary work.
|
||||
///
|
||||
/// - returns: dictionary
|
||||
///
|
||||
/// ### page-numbering()
|
||||
/// Returns the page numbering pattern of the page at this location. This can be
|
||||
/// used when displaying the page counter in order to obtain the local numbering.
|
||||
/// This is useful if you are building custom indices or outlines.
|
||||
///
|
||||
/// If the page numbering is set to `none` at that location, this function returns `none`.
|
||||
///
|
||||
/// - returns: string or function or none
|
||||
///
|
||||
/// Display: Locate
|
||||
/// Category: meta
|
||||
#[func]
|
||||
pub fn locate(
|
||||
/// A function that receives a `location`. Its return value is displayed
|
||||
/// in the document.
|
||||
/// A function that receives a [`location`]($location). Its return value is
|
||||
/// displayed in the document.
|
||||
///
|
||||
/// This function is called once for each time the content returned by
|
||||
/// `locate` appears in the document. That makes it possible to generate
|
||||
@ -60,10 +26,7 @@ pub fn locate(
|
||||
}
|
||||
|
||||
/// Executes a `locate` call.
|
||||
///
|
||||
/// Display: Locate
|
||||
/// Category: special
|
||||
#[element(Locatable, Show)]
|
||||
#[elem(Locatable, Show)]
|
||||
struct LocateElem {
|
||||
/// The function to call with the location.
|
||||
#[required]
|
||||
@ -83,9 +46,9 @@ impl Show for LocateElem {
|
||||
/// Provides access to active styles.
|
||||
///
|
||||
/// The styles are currently opaque and only useful in combination with the
|
||||
/// [`measure`]($func/measure) function. See its documentation for more details.
|
||||
/// In the future, the provided styles might also be directly accessed to look
|
||||
/// up styles defined by [set rules]($styling/#set-rules).
|
||||
/// [`measure`]($measure) function. See its documentation for more details. In
|
||||
/// the future, the provided styles might also be directly accessed to look up
|
||||
/// styles defined by [set rules]($styling/#set-rules).
|
||||
///
|
||||
/// ```example
|
||||
/// #let thing(body) = style(styles => {
|
||||
@ -96,9 +59,6 @@ impl Show for LocateElem {
|
||||
/// #thing[Hey] \
|
||||
/// #thing[Welcome]
|
||||
/// ```
|
||||
///
|
||||
/// Display: Style
|
||||
/// Category: meta
|
||||
#[func]
|
||||
pub fn style(
|
||||
/// A function to call with the styles. Its return value is displayed
|
||||
@ -113,10 +73,7 @@ pub fn style(
|
||||
}
|
||||
|
||||
/// Executes a style access.
|
||||
///
|
||||
/// Display: Style
|
||||
/// Category: special
|
||||
#[element(Show)]
|
||||
#[elem(Show)]
|
||||
struct StyleElem {
|
||||
/// The function to call with the styles.
|
||||
#[required]
|
||||
@ -134,10 +91,8 @@ impl Show for StyleElem {
|
||||
/// (width and height).
|
||||
///
|
||||
/// The given function must accept a single parameter, `size`, which is a
|
||||
/// dictionary with keys `width` and `height`, both of type
|
||||
/// [`length`]($type/length).
|
||||
/// dictionary with keys `width` and `height`, both of type [`length`]($length).
|
||||
///
|
||||
|
||||
/// ```example
|
||||
/// #let text = lorem(30)
|
||||
/// #layout(size => style(styles => [
|
||||
@ -155,9 +110,9 @@ impl Show for StyleElem {
|
||||
/// and a height of `{400pt}`, then the specified function will be given the
|
||||
/// parameter `{(width: 800pt, height: 400pt)}`. If it placed directly into the
|
||||
/// page it receives the page's dimensions minus its margins. This is mostly
|
||||
/// useful in combination with [measurement]($func/measure).
|
||||
/// useful in combination with [measurement]($measure).
|
||||
///
|
||||
/// You can also use this function to resolve [`ratio`]($type/ratio) to fixed
|
||||
/// You can also use this function to resolve [`ratio`]($ratio) to fixed
|
||||
/// lengths. This might come in handy if you're building your own layout
|
||||
/// abstractions.
|
||||
///
|
||||
@ -170,16 +125,13 @@ impl Show for StyleElem {
|
||||
///
|
||||
/// Note that this function will provide an infinite width or height if one of
|
||||
/// the page width or height is `auto`, respectively.
|
||||
///
|
||||
/// Display: Layout
|
||||
/// Category: meta
|
||||
#[func]
|
||||
pub fn layout(
|
||||
/// A function to call with the outer container's size. Its return value is
|
||||
/// displayed in the document.
|
||||
///
|
||||
/// The container's size is given as a [dictionary]($type/dictionary) with
|
||||
/// the keys `width` and `height`.
|
||||
/// The container's size is given as a [dictionary]($dictionary) with the
|
||||
/// keys `width` and `height`.
|
||||
///
|
||||
/// This function is called once for each time the content returned by
|
||||
/// `layout` appears in the document. That makes it possible to generate
|
||||
@ -190,10 +142,7 @@ pub fn layout(
|
||||
}
|
||||
|
||||
/// Executes a `layout` call.
|
||||
///
|
||||
/// Display: Layout
|
||||
/// Category: special
|
||||
#[element(Layout)]
|
||||
#[elem(Layout)]
|
||||
struct LayoutElem {
|
||||
/// The function to call with the outer container's (or page's) size.
|
||||
#[required]
|
||||
|
@ -17,15 +17,14 @@ use crate::prelude::*;
|
||||
/// headings, figures, and more. Moreover, you can define custom counters for
|
||||
/// other things you want to count.
|
||||
///
|
||||
/// ## Displaying a counter { #displaying }
|
||||
/// # Displaying a counter { #displaying }
|
||||
/// To display the current value of the heading counter, you call the `counter`
|
||||
/// function with the `key` set to `heading` and then call the `display` method
|
||||
/// on the counter. To see any output, you also have to enable heading
|
||||
/// [numbering]($func/heading.numbering).
|
||||
/// [numbering]($heading.numbering).
|
||||
///
|
||||
/// The `display` method optionally takes an argument telling it how to format
|
||||
/// the counter. This can be a [numbering pattern or a
|
||||
/// function]($func/numbering).
|
||||
/// the counter. This can be a [numbering pattern or a function]($numbering).
|
||||
///
|
||||
/// ```example
|
||||
/// #set heading(numbering: "1.")
|
||||
@ -41,7 +40,7 @@ use crate::prelude::*;
|
||||
/// #counter(heading).display("I")
|
||||
/// ```
|
||||
///
|
||||
/// ## Modifying a counter { #modifying }
|
||||
/// # Modifying a counter { #modifying }
|
||||
/// To modify a counter, you can use the `step` and `update` methods:
|
||||
///
|
||||
/// - The `step` method increases the value of the counter by one. Because
|
||||
@ -76,7 +75,6 @@ use crate::prelude::*;
|
||||
/// Still at #counter(heading).display().
|
||||
/// ```
|
||||
///
|
||||
/// ## Custom counters { #custom-counters }
|
||||
/// To define your own counter, call the `counter` function with a string as a
|
||||
/// key. This key identifies the counter globally.
|
||||
///
|
||||
@ -89,7 +87,7 @@ use crate::prelude::*;
|
||||
/// #mine.display() \
|
||||
/// ```
|
||||
///
|
||||
/// ## How to step { #how-to-step }
|
||||
/// # How to step
|
||||
/// When you define and use a custom counter, in general, you should first step
|
||||
/// the counter and then display it. This way, the stepping behaviour of a
|
||||
/// counter can depend on the element it is stepped for. If you were writing a
|
||||
@ -118,7 +116,7 @@ use crate::prelude::*;
|
||||
/// they always start at zero. This way, they are at one for the first display
|
||||
/// (which happens after the first step).
|
||||
///
|
||||
/// ## Page counter { #page-counter }
|
||||
/// # Page counter
|
||||
/// The page counter is special. It is automatically stepped at each pagebreak.
|
||||
/// But like other counters, you can also step it manually. For example, you
|
||||
/// could have Roman page numbers for your preface, then switch to Arabic page
|
||||
@ -145,7 +143,7 @@ use crate::prelude::*;
|
||||
/// Arabic numbers.
|
||||
/// ```
|
||||
///
|
||||
/// ## Time travel { #time-travel }
|
||||
/// # Time travel
|
||||
/// Counters can travel through time! You can find out the final value of the
|
||||
/// counter before it is reached and even determine what the value was at any
|
||||
/// particular location in the document.
|
||||
@ -177,16 +175,16 @@ use crate::prelude::*;
|
||||
///
|
||||
/// Let's dissect what happens in the example above:
|
||||
///
|
||||
/// - We call [`locate`]($func/locate) to get access to the current location in
|
||||
/// the document. We then pass this location to our counter's `at` method to
|
||||
/// get its value at the current location. The `at` method always returns an
|
||||
/// array because counters can have multiple levels. As the counter starts at
|
||||
/// zero, the first value is thus `{(0,)}`.
|
||||
/// - We call [`locate`]($locate) to get access to the current location in the
|
||||
/// document. We then pass this location to our counter's `at` method to get
|
||||
/// its value at the current location. The `at` method always returns an array
|
||||
/// because counters can have multiple levels. As the counter starts at zero,
|
||||
/// the first value is thus `{(0,)}`.
|
||||
///
|
||||
/// - We now [`query`]($func/query) the document for all elements with the
|
||||
/// - We now [`query`]($query) the document for all elements with the
|
||||
/// `{<intro>}` label. The result is an array from which we extract the first
|
||||
/// (and only) element's [location]($type/content.location). We then look up
|
||||
/// the value of the counter at that location. The first update to the counter
|
||||
/// (and only) element's [location]($content.location). We then look up the
|
||||
/// value of the counter at that location. The first update to the counter
|
||||
/// sets it to `{0 + 3 = 3}`. At the introduction heading, the value is thus
|
||||
/// `{(3,)}`.
|
||||
///
|
||||
@ -196,180 +194,31 @@ use crate::prelude::*;
|
||||
/// which one doesn't matter. After the heading follow two calls to `step()`,
|
||||
/// so the final value is `{(5,)}`.
|
||||
///
|
||||
/// ## Other kinds of state { #other-state }
|
||||
/// The `counter` function is closely related to [state]($func/state) function.
|
||||
/// Read its documentation for more details on state management in Typst and
|
||||
/// why it doesn't just use normal variables for counters.
|
||||
///
|
||||
/// ## Methods
|
||||
/// ### display()
|
||||
/// Displays the value of the counter.
|
||||
///
|
||||
/// - numbering: string or function (positional)
|
||||
/// A [numbering pattern or a function]($func/numbering), which specifies how
|
||||
/// to display the counter. If given a function, that function receives each
|
||||
/// number of the counter as a separate argument. If the amount of numbers
|
||||
/// varies, e.g. for the heading argument, you can use an
|
||||
/// [argument sink]($type/arguments).
|
||||
///
|
||||
/// If this is omitted, displays the counter with the numbering style for the
|
||||
/// counted element or with the pattern `{"1.1"}` if no such style exists.
|
||||
///
|
||||
/// - both: boolean (named)
|
||||
/// If enabled, displays the current and final top-level count together. Both
|
||||
/// can be styled through a single numbering pattern. This is used by the page
|
||||
/// numbering property to display the current and total number of pages when a
|
||||
/// pattern like `{"1 / 1"}` is given.
|
||||
///
|
||||
/// - returns: content
|
||||
///
|
||||
/// ### step()
|
||||
/// Increases the value of the counter by one.
|
||||
///
|
||||
/// The update will be in effect at the position where the returned content is
|
||||
/// inserted into the document. If you don't put the output into the document,
|
||||
/// nothing happens! This would be the case, for example, if you write
|
||||
/// `{let _ = counter(page).step()}`. Counter updates are always applied in
|
||||
/// layout order and in that case, Typst wouldn't know when to step the counter.
|
||||
///
|
||||
/// - level: integer (named)
|
||||
/// The depth at which to step the counter. Defaults to `{1}`.
|
||||
///
|
||||
/// - returns: content
|
||||
///
|
||||
/// ### update()
|
||||
/// Updates the value of the counter.
|
||||
///
|
||||
/// Just like with `step`, the update only occurs if you put the resulting
|
||||
/// content into the document.
|
||||
///
|
||||
/// - value: integer or array or function (positional, required)
|
||||
/// If given an integer or array of integers, sets the counter to that value.
|
||||
/// If given a function, that function receives the previous counter value
|
||||
/// (with each number as a separate argument) and has to return the new
|
||||
/// value (integer or array).
|
||||
///
|
||||
/// - returns: content
|
||||
///
|
||||
/// ### at()
|
||||
/// Gets the value of the counter at the given location. Always returns an
|
||||
/// array of integers, even if the counter has just one number.
|
||||
///
|
||||
/// - location: location (positional, required)
|
||||
/// The location at which the counter value should be retrieved. A suitable
|
||||
/// location can be retrieved from [`locate`]($func/locate) or
|
||||
/// [`query`]($func/query).
|
||||
///
|
||||
/// - returns: array
|
||||
///
|
||||
/// ### final()
|
||||
/// Gets the value of the counter at the end of the document. Always returns an
|
||||
/// array of integers, even if the counter has just one number.
|
||||
///
|
||||
/// - location: location (positional, required)
|
||||
/// Can be an arbitrary location, as its value is irrelevant for the method's
|
||||
/// return value. Why is it required then? Typst has to evaluate parts of your
|
||||
/// code multiple times to determine all counter values. By only allowing this
|
||||
/// method within [`locate`]($func/locate) calls, the amount of code that can
|
||||
/// depend on the method's result is reduced. If you could call `final`
|
||||
/// directly at the top level of a module, the evaluation of the whole module
|
||||
/// and its exports could depend on the counter's value.
|
||||
///
|
||||
/// - returns: array
|
||||
///
|
||||
/// Display: Counter
|
||||
/// Category: meta
|
||||
#[func]
|
||||
pub fn counter(
|
||||
/// The key that identifies this counter.
|
||||
///
|
||||
/// - If it is a string, creates a custom counter that is only affected by
|
||||
/// manual updates,
|
||||
/// - If this is a `{<label>}`, counts through all elements with that label,
|
||||
/// - 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,
|
||||
) -> Counter {
|
||||
Counter::new(key)
|
||||
}
|
||||
|
||||
/// Counts through pages, elements, and more.
|
||||
/// # Other kinds of state { #other-state }
|
||||
/// The `counter` type is closely related to [state]($state) type. Read its
|
||||
/// documentation for more details on state management in Typst and why it
|
||||
/// doesn't just use normal variables for counters.
|
||||
#[ty(scope)]
|
||||
#[derive(Clone, PartialEq, Hash)]
|
||||
pub struct Counter(CounterKey);
|
||||
|
||||
impl Counter {
|
||||
/// Create a new counter from a key.
|
||||
pub fn new(key: CounterKey) -> Self {
|
||||
/// Create a new counter identified by a key.
|
||||
pub fn new(key: CounterKey) -> Counter {
|
||||
Self(key)
|
||||
}
|
||||
|
||||
/// The counter for the given element.
|
||||
pub fn of(func: ElemFunc) -> Self {
|
||||
Self::new(CounterKey::Selector(Selector::Elem(func, None)))
|
||||
pub fn of(func: Element) -> Self {
|
||||
Self::construct(CounterKey::Selector(Selector::Elem(func, None)))
|
||||
}
|
||||
|
||||
/// Call a method on counter.
|
||||
#[tracing::instrument(skip(vm))]
|
||||
pub fn call_method(
|
||||
self,
|
||||
vm: &mut Vm,
|
||||
method: &str,
|
||||
mut args: Args,
|
||||
span: Span,
|
||||
) -> SourceResult<Value> {
|
||||
let value = match method {
|
||||
"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_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()?;
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
/// Display the current value of the counter.
|
||||
pub fn display(self, numbering: Option<Numbering>, both: bool) -> Content {
|
||||
DisplayElem::new(self, numbering, both).pack()
|
||||
}
|
||||
|
||||
/// 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(&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());
|
||||
state.step(NonZeroUsize::ONE, delta);
|
||||
}
|
||||
|
||||
Ok(state)
|
||||
}
|
||||
|
||||
/// Get the value of the state at the final location.
|
||||
pub fn final_(&self, vt: &mut Vt, _: Location) -> SourceResult<CounterState> {
|
||||
let sequence = self.sequence(vt)?;
|
||||
let (mut state, page) = sequence.last().unwrap().clone();
|
||||
if self.is_page() {
|
||||
let delta = vt.introspector.pages().get().saturating_sub(page.get());
|
||||
state.step(NonZeroUsize::ONE, delta);
|
||||
}
|
||||
Ok(state)
|
||||
}
|
||||
|
||||
/// Get the current and final value of the state combined in one state.
|
||||
/// Gets the current and final value of the state combined in one state.
|
||||
pub fn both(&self, vt: &mut Vt, location: Location) -> SourceResult<CounterState> {
|
||||
let sequence = self.sequence(vt)?;
|
||||
let offset = vt
|
||||
.introspector
|
||||
.query(&Selector::before(self.selector(), location, true))
|
||||
.query(&self.selector().before(location.into(), true))
|
||||
.len();
|
||||
let (mut at_state, at_page) = sequence[offset].clone();
|
||||
let (mut final_state, final_page) = sequence.last().unwrap().clone();
|
||||
@ -384,12 +233,7 @@ impl Counter {
|
||||
Ok(CounterState(smallvec![at_state.first(), final_state.first()]))
|
||||
}
|
||||
|
||||
/// Produce content that performs a state update.
|
||||
pub fn update(self, update: CounterUpdate) -> Content {
|
||||
UpdateElem::new(self.0, update).pack()
|
||||
}
|
||||
|
||||
/// Produce the whole sequence of counter states.
|
||||
/// Produces the whole sequence of counter states.
|
||||
///
|
||||
/// This has to happen just once for all counters, cutting down the number
|
||||
/// of counter updates from quadratic to linear.
|
||||
@ -462,7 +306,7 @@ impl Counter {
|
||||
/// The selector relevant for this counter's updates.
|
||||
fn selector(&self) -> Selector {
|
||||
let mut selector =
|
||||
Selector::Elem(UpdateElem::func(), Some(dict! { "key" => self.0.clone() }));
|
||||
Selector::Elem(UpdateElem::elem(), Some(dict! { "key" => self.0.clone() }));
|
||||
|
||||
if let CounterKey::Selector(key) = &self.0 {
|
||||
selector = Selector::Or(eco_vec![selector, key.clone()]);
|
||||
@ -477,6 +321,140 @@ impl Counter {
|
||||
}
|
||||
}
|
||||
|
||||
#[scope]
|
||||
impl Counter {
|
||||
/// Create a new counter identified by a key.
|
||||
#[func(constructor)]
|
||||
pub fn construct(
|
||||
/// The key that identifies this counter.
|
||||
///
|
||||
/// - If it is a string, creates a custom counter that is only affected
|
||||
/// by manual updates,
|
||||
/// - If this is a `{<label>}`, counts through all elements with that
|
||||
/// label,
|
||||
/// - If this is an element function or selector, counts through its
|
||||
/// elements,
|
||||
/// - If this is the [`page`]($page) function, counts through pages.
|
||||
key: CounterKey,
|
||||
) -> Counter {
|
||||
Self(key)
|
||||
}
|
||||
|
||||
/// Displays the current value of the counter.
|
||||
#[func]
|
||||
pub fn display(
|
||||
self,
|
||||
/// A [numbering pattern or a function]($numbering), which specifies how
|
||||
/// to display the counter. If given a function, that function receives
|
||||
/// each number of the counter as a separate argument. If the amount of
|
||||
/// numbers varies, e.g. for the heading argument, you can use an
|
||||
/// [argument sink]($arguments).
|
||||
///
|
||||
/// If this is omitted, displays the counter with the numbering style
|
||||
/// for the counted element or with the pattern `{"1.1"}` if no such
|
||||
/// style exists.
|
||||
#[default]
|
||||
numbering: Option<Numbering>,
|
||||
/// If enabled, displays the current and final top-level count together.
|
||||
/// Both can be styled through a single numbering pattern. This is used
|
||||
/// by the page numbering property to display the current and total
|
||||
/// number of pages when a pattern like `{"1 / 1"}` is given.
|
||||
#[named]
|
||||
#[default(false)]
|
||||
both: bool,
|
||||
) -> Content {
|
||||
DisplayElem::new(self, numbering, both).pack()
|
||||
}
|
||||
|
||||
/// Increases the value of the counter by one.
|
||||
///
|
||||
/// The update will be in effect at the position where the returned content
|
||||
/// is inserted into the document. If you don't put the output into the
|
||||
/// document, nothing happens! This would be the case, for example, if you
|
||||
/// write `{let _ = counter(page).step()}`. Counter updates are always
|
||||
/// applied in layout order and in that case, Typst wouldn't know when to
|
||||
/// step the counter.
|
||||
#[func]
|
||||
pub fn step(
|
||||
self,
|
||||
/// The depth at which to step the counter. Defaults to `{1}`.
|
||||
#[named]
|
||||
#[default(NonZeroUsize::ONE)]
|
||||
level: NonZeroUsize,
|
||||
) -> Content {
|
||||
self.update(CounterUpdate::Step(level))
|
||||
}
|
||||
|
||||
/// Updates the value of the counter.
|
||||
///
|
||||
/// Just like with `step`, the update only occurs if you put the resulting
|
||||
/// content into the document.
|
||||
#[func]
|
||||
pub fn update(
|
||||
self,
|
||||
/// If given an integer or array of integers, sets the counter to that
|
||||
/// value. If given a function, that function receives the previous
|
||||
/// counter value (with each number as a separate argument) and has to
|
||||
/// return the new value (integer or array).
|
||||
update: CounterUpdate,
|
||||
) -> Content {
|
||||
UpdateElem::new(self.0, update).pack()
|
||||
}
|
||||
|
||||
/// Gets the value of the counter at the given location. Always returns an
|
||||
/// array of integers, even if the counter has just one number.
|
||||
#[func]
|
||||
pub fn at(
|
||||
&self,
|
||||
/// The virtual typesetter.
|
||||
vt: &mut Vt,
|
||||
/// The location at which the counter value should be retrieved. A
|
||||
/// suitable location can be retrieved from [`locate`]($locate) or
|
||||
/// [`query`]($query).
|
||||
location: Location,
|
||||
) -> SourceResult<CounterState> {
|
||||
let sequence = self.sequence(vt)?;
|
||||
let offset = vt
|
||||
.introspector
|
||||
.query(&self.selector().before(location.into(), true))
|
||||
.len();
|
||||
let (mut state, page) = sequence[offset].clone();
|
||||
if self.is_page() {
|
||||
let delta = vt.introspector.page(location).get().saturating_sub(page.get());
|
||||
state.step(NonZeroUsize::ONE, delta);
|
||||
}
|
||||
|
||||
Ok(state)
|
||||
}
|
||||
|
||||
/// Gets the value of the counter at the end of the document. Always returns
|
||||
/// an array of integers, even if the counter has just one number.
|
||||
#[func]
|
||||
pub fn final_(
|
||||
&self,
|
||||
/// The virtual typesetter.
|
||||
vt: &mut Vt,
|
||||
/// Can be an arbitrary location, as its value is irrelevant for the
|
||||
/// method's return value. Why is it required then? Typst has to
|
||||
/// evaluate parts of your code multiple times to determine all counter
|
||||
/// values. By only allowing this method within [`locate`]($locate)
|
||||
/// calls, the amount of code that can depend on the method's result is
|
||||
/// reduced. If you could call `final` directly at the top level of a
|
||||
/// module, the evaluation of the whole module and its exports could
|
||||
/// depend on the counter's value.
|
||||
location: Location,
|
||||
) -> SourceResult<CounterState> {
|
||||
let _ = location;
|
||||
let sequence = self.sequence(vt)?;
|
||||
let (mut state, page) = sequence.last().unwrap().clone();
|
||||
if self.is_page() {
|
||||
let delta = vt.introspector.pages().get().saturating_sub(page.get());
|
||||
state.step(NonZeroUsize::ONE, delta);
|
||||
}
|
||||
Ok(state)
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for Counter {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
f.write_str("counter(")?;
|
||||
@ -486,7 +464,7 @@ impl Debug for Counter {
|
||||
}
|
||||
|
||||
cast! {
|
||||
type Counter: "counter",
|
||||
type Counter,
|
||||
}
|
||||
|
||||
/// Identifies a counter.
|
||||
@ -504,14 +482,14 @@ pub enum CounterKey {
|
||||
cast! {
|
||||
CounterKey,
|
||||
self => match self {
|
||||
Self::Page => PageElem::func().into_value(),
|
||||
Self::Page => PageElem::elem().into_value(),
|
||||
Self::Selector(v) => v.into_value(),
|
||||
Self::Str(v) => v.into_value(),
|
||||
},
|
||||
v: Str => Self::Str(v),
|
||||
v: Label => Self::Selector(Selector::Label(v)),
|
||||
v: ElemFunc => {
|
||||
if v == PageElem::func() {
|
||||
v: Element => {
|
||||
if v == PageElem::elem() {
|
||||
Self::Page
|
||||
} else {
|
||||
Self::Selector(LocatableSelector::from_value(v.into_value())?.0)
|
||||
@ -531,6 +509,7 @@ impl Debug for CounterKey {
|
||||
}
|
||||
|
||||
/// An update to perform on a counter.
|
||||
#[ty]
|
||||
#[derive(Clone, PartialEq, Hash)]
|
||||
pub enum CounterUpdate {
|
||||
/// Set the counter to the specified state.
|
||||
@ -548,7 +527,7 @@ impl Debug for CounterUpdate {
|
||||
}
|
||||
|
||||
cast! {
|
||||
type CounterUpdate: "counter update",
|
||||
type CounterUpdate,
|
||||
v: CounterState => Self::Set(v),
|
||||
v: Func => Self::Func(v),
|
||||
}
|
||||
@ -612,10 +591,7 @@ cast! {
|
||||
}
|
||||
|
||||
/// Executes a display of a state.
|
||||
///
|
||||
/// Display: State
|
||||
/// Category: special
|
||||
#[element(Locatable, Show)]
|
||||
#[elem(Locatable, Show)]
|
||||
struct DisplayElem {
|
||||
/// The counter.
|
||||
#[required]
|
||||
@ -643,11 +619,11 @@ impl Show for DisplayElem {
|
||||
return None;
|
||||
};
|
||||
|
||||
if func == HeadingElem::func() {
|
||||
if func == HeadingElem::elem() {
|
||||
HeadingElem::numbering_in(styles)
|
||||
} else if func == FigureElem::func() {
|
||||
} else if func == FigureElem::elem() {
|
||||
FigureElem::numbering_in(styles)
|
||||
} else if func == EquationElem::func() {
|
||||
} else if func == EquationElem::elem() {
|
||||
EquationElem::numbering_in(styles)
|
||||
} else {
|
||||
None
|
||||
@ -667,10 +643,7 @@ impl Show for DisplayElem {
|
||||
}
|
||||
|
||||
/// Executes a display of a state.
|
||||
///
|
||||
/// Display: State
|
||||
/// Category: special
|
||||
#[element(Locatable, Show)]
|
||||
#[elem(Locatable, Show)]
|
||||
struct UpdateElem {
|
||||
/// The key that identifies the counter.
|
||||
#[required]
|
||||
|
@ -17,10 +17,7 @@ use crate::prelude::*;
|
||||
///
|
||||
/// Note that metadata set with this function is not rendered within the
|
||||
/// document. Instead, it is embedded in the compiled PDF file.
|
||||
///
|
||||
/// Display: Document
|
||||
/// Category: meta
|
||||
#[element(Construct, LayoutRoot)]
|
||||
#[elem(Construct, LayoutRoot)]
|
||||
pub struct DocumentElem {
|
||||
/// The document's title. This is often rendered as the title of the
|
||||
/// PDF viewer window.
|
||||
|
@ -15,7 +15,7 @@ use crate::visualize::ImageElem;
|
||||
/// For example, figures containing images will be numbered separately from
|
||||
/// figures containing tables.
|
||||
///
|
||||
/// ## Examples { #examples }
|
||||
/// # Examples
|
||||
/// The example below shows a basic figure with an image:
|
||||
/// ```example
|
||||
/// @glacier shows a glacier. Glaciers
|
||||
@ -27,9 +27,8 @@ use crate::visualize::ImageElem;
|
||||
/// ) <glacier>
|
||||
/// ```
|
||||
///
|
||||
/// You can also insert [tables]($func/table) into figures to give them a
|
||||
/// caption. The figure will detect this and automatically use a separate
|
||||
/// counter.
|
||||
/// You can also insert [tables]($table) into figures to give them a caption.
|
||||
/// The figure will detect this and automatically use a separate counter.
|
||||
///
|
||||
/// ```example
|
||||
/// #figure(
|
||||
@ -45,7 +44,7 @@ use crate::visualize::ImageElem;
|
||||
/// This behaviour can be overridden by explicitly specifying the figure's
|
||||
/// `kind`. All figures of the same kind share a common counter.
|
||||
///
|
||||
/// ## Modifying the appearance { #modifying-appearance }
|
||||
/// # Modifying the appearance { #modifying-appearance }
|
||||
/// You can completely customize the look of your figures with a [show
|
||||
/// rule]($styling/#show-rules). In the example below, we show the figure's
|
||||
/// caption above its body and display its supplement and counter after the
|
||||
@ -73,13 +72,10 @@ use crate::visualize::ImageElem;
|
||||
/// If your figure is too large and its contents are breakable across pages
|
||||
/// (e.g. if it contains a large table), then you can make the figure breakable
|
||||
/// across pages as well by using `[#show figure: set block(breakable: true)]`
|
||||
/// (see the [block]($func/block) documentation for more information).
|
||||
///
|
||||
/// Display: Figure
|
||||
/// Category: meta
|
||||
#[element(Locatable, Synthesize, Count, Show, Finalize, Refable, Outlinable)]
|
||||
/// (see the [block]($block) documentation for more information).
|
||||
#[elem(Locatable, Synthesize, Count, Show, Finalize, Refable, Outlinable)]
|
||||
pub struct FigureElem {
|
||||
/// The content of the figure. Often, an [image]($func/image).
|
||||
/// The content of the figure. Often, an [image]($image).
|
||||
#[required]
|
||||
pub body: Content,
|
||||
|
||||
@ -103,7 +99,7 @@ pub struct FigureElem {
|
||||
/// )
|
||||
/// #lorem(60)
|
||||
/// ```
|
||||
pub placement: Option<Smart<VerticalAlign>>,
|
||||
pub placement: Option<Smart<VAlign>>,
|
||||
|
||||
/// The figure's caption.
|
||||
pub caption: Option<Content>,
|
||||
@ -122,8 +118,17 @@ pub struct FigureElem {
|
||||
/// caption: [I'm down here],
|
||||
/// )
|
||||
/// ```
|
||||
#[default(VerticalAlign(GenAlign::Specific(Align::Bottom)))]
|
||||
pub caption_pos: VerticalAlign,
|
||||
#[default(VAlign::Bottom)]
|
||||
#[parse({
|
||||
let option: Option<Spanned<VAlign>> = args.named("caption-pos")?;
|
||||
if let Some(Spanned { v: align, span }) = option {
|
||||
if align == VAlign::Horizon {
|
||||
bail!(span, "expected `top` or `bottom`");
|
||||
}
|
||||
}
|
||||
option.map(|spanned| spanned.v)
|
||||
})]
|
||||
pub caption_pos: VAlign,
|
||||
|
||||
/// The kind of figure this is.
|
||||
///
|
||||
@ -133,7 +138,7 @@ pub struct FigureElem {
|
||||
/// Setting this to something other than `{auto}` will override the
|
||||
/// automatic detection. This can be useful if
|
||||
/// - you wish to create a custom figure type that is not an
|
||||
/// [image]($func/image), a [table]($func/table) or [code]($func/raw),
|
||||
/// [image]($image), a [table]($table) or [code]($raw),
|
||||
/// - you want to force the figure to use a specific counter regardless of
|
||||
/// its content.
|
||||
///
|
||||
@ -155,8 +160,8 @@ pub struct FigureElem {
|
||||
/// The figure's supplement.
|
||||
///
|
||||
/// If set to `{auto}`, the figure will try to automatically determine the
|
||||
/// correct supplement based on the `kind` and the active [text
|
||||
/// language]($func/text.lang). If you are using a custom figure type, you
|
||||
/// correct supplement based on the `kind` and the active
|
||||
/// [text language]($text.lang). If you are using a custom figure type, you
|
||||
/// will need to manually specify the supplement.
|
||||
///
|
||||
/// If a function is specified, it is passed the first descendant of the
|
||||
@ -174,7 +179,7 @@ pub struct FigureElem {
|
||||
pub supplement: Smart<Option<Supplement>>,
|
||||
|
||||
/// How to number the figure. Accepts a
|
||||
/// [numbering pattern or function]($func/numbering).
|
||||
/// [numbering pattern or function]($numbering).
|
||||
#[default(Some(NumberingPattern::from_str("1").unwrap().into()))]
|
||||
pub numbering: Option<Numbering>,
|
||||
|
||||
@ -182,16 +187,15 @@ pub struct FigureElem {
|
||||
#[default(Em::new(0.65).into())]
|
||||
pub gap: Length,
|
||||
|
||||
/// Whether the figure should appear in an [`outline`]($func/outline)
|
||||
/// of figures.
|
||||
/// Whether the figure should appear in an [`outline`]($outline) of figures.
|
||||
#[default(true)]
|
||||
pub outlined: bool,
|
||||
|
||||
/// Convenience field to get access to the counter for this figure.
|
||||
///
|
||||
/// The counter only depends on the `kind`:
|
||||
/// - For (tables)[$func/table]: `{counter(figure.where(kind: table))}`
|
||||
/// - For (images)[$func/image]: `{counter(figure.where(kind: image))}`
|
||||
/// - For (tables)[@table]: `{counter(figure.where(kind: table))}`
|
||||
/// - For (images)[@image]: `{counter(figure.where(kind: image))}`
|
||||
/// - For a custom kind: `{counter(figure.where(kind: kind))}`
|
||||
///
|
||||
/// These are the counters you'll need to modify if you want to skip a
|
||||
@ -210,16 +214,9 @@ impl Synthesize for FigureElem {
|
||||
.query_first(Selector::can::<dyn Figurable>())
|
||||
.cloned()
|
||||
.map(|elem| FigureKind::Elem(elem.func()))
|
||||
.unwrap_or_else(|| FigureKind::Elem(ImageElem::func()))
|
||||
.unwrap_or_else(|| FigureKind::Elem(ImageElem::elem()))
|
||||
});
|
||||
|
||||
let caption_pos =
|
||||
VerticalAlign(GenAlign::Specific(match self.caption_pos(styles) {
|
||||
VerticalAlign(GenAlign::Specific(Align::Top)) => Align::Top,
|
||||
VerticalAlign(GenAlign::Specific(Align::Bottom)) => Align::Bottom,
|
||||
_ => bail!(self.span(), "caption-pos can only be top or bottom"),
|
||||
}));
|
||||
|
||||
// Resolve the supplement.
|
||||
let supplement = match self.supplement(styles) {
|
||||
Smart::Auto => {
|
||||
@ -261,14 +258,14 @@ impl Synthesize for FigureElem {
|
||||
|
||||
// Construct the figure's counter.
|
||||
let counter = Counter::new(CounterKey::Selector(Selector::Elem(
|
||||
Self::func(),
|
||||
Self::elem(),
|
||||
Some(dict! {
|
||||
"kind" => kind.clone(),
|
||||
}),
|
||||
)));
|
||||
|
||||
self.push_placement(self.placement(styles));
|
||||
self.push_caption_pos(caption_pos);
|
||||
self.push_caption_pos(self.caption_pos(styles));
|
||||
self.push_caption(self.caption(styles));
|
||||
self.push_kind(Smart::Custom(kind));
|
||||
self.push_supplement(Smart::Custom(Some(Supplement::Content(supplement))));
|
||||
@ -288,10 +285,7 @@ impl Show for FigureElem {
|
||||
// Build the caption, if any.
|
||||
if let Some(caption) = self.full_caption(vt)? {
|
||||
let v = VElem::weak(self.gap(styles).into()).pack();
|
||||
realized = if matches!(
|
||||
self.caption_pos(styles),
|
||||
VerticalAlign(GenAlign::Specific(Align::Bottom))
|
||||
) {
|
||||
realized = if self.caption_pos(styles) == VAlign::Bottom {
|
||||
realized + v + caption
|
||||
} else {
|
||||
caption + v + realized
|
||||
@ -302,15 +296,13 @@ impl Show for FigureElem {
|
||||
realized = BlockElem::new()
|
||||
.with_body(Some(realized))
|
||||
.pack()
|
||||
.aligned(Axes::with_x(Some(Align::Center.into())));
|
||||
.aligned(Align::CENTER);
|
||||
|
||||
// Wrap in a float.
|
||||
if let Some(align) = self.placement(styles) {
|
||||
realized = PlaceElem::new(realized)
|
||||
.with_float(true)
|
||||
.with_alignment(align.map(|VerticalAlign(align)| {
|
||||
Axes::new(Some(Align::Center.into()), Some(align))
|
||||
}))
|
||||
.with_alignment(align.map(|align| HAlign::Center + align))
|
||||
.pack();
|
||||
}
|
||||
|
||||
@ -345,7 +337,7 @@ impl Refable for FigureElem {
|
||||
}
|
||||
|
||||
fn counter(&self) -> Counter {
|
||||
self.counter().unwrap_or_else(|| Counter::of(Self::func()))
|
||||
self.counter().unwrap_or_else(|| Counter::of(Self::elem()))
|
||||
}
|
||||
|
||||
fn numbering(&self) -> Option<Numbering> {
|
||||
@ -379,8 +371,8 @@ impl FigureElem {
|
||||
self.counter(),
|
||||
self.numbering(StyleChain::default()),
|
||||
) {
|
||||
let loc = self.0.location().unwrap();
|
||||
let numbers = counter.at(vt, loc)?.display(vt, &numbering)?;
|
||||
let location = self.0.location().unwrap();
|
||||
let numbers = counter.at(vt, location)?.display(vt, &numbering)?;
|
||||
|
||||
if !supplement.is_empty() {
|
||||
supplement += TextElem::packed("\u{a0}");
|
||||
@ -397,7 +389,7 @@ impl FigureElem {
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum FigureKind {
|
||||
/// The kind is an element function.
|
||||
Elem(ElemFunc),
|
||||
Elem(Element),
|
||||
/// The kind is a name.
|
||||
Name(EcoString),
|
||||
}
|
||||
@ -408,7 +400,7 @@ cast! {
|
||||
Self::Elem(v) => v.into_value(),
|
||||
Self::Name(v) => v.into_value(),
|
||||
},
|
||||
v: ElemFunc => Self::Elem(v),
|
||||
v: Element => Self::Elem(v),
|
||||
v: EcoString => Self::Name(v),
|
||||
}
|
||||
|
||||
|
@ -34,11 +34,11 @@ cast! {
|
||||
/// and can break across multiple pages.
|
||||
///
|
||||
/// To customize the appearance of the entry in the footnote listing, see
|
||||
/// [`footnote.entry`]($func/footnote.entry). The footnote itself is realized as
|
||||
/// a normal superscript, so you can use a set rule on the
|
||||
/// [`super`]($func/super) function to customize it.
|
||||
/// [`footnote.entry`]($footnote.entry). The footnote itself is realized as a
|
||||
/// normal superscript, so you can use a set rule on the [`super`]($super)
|
||||
/// function to customize it.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// # Example
|
||||
/// ```example
|
||||
/// Check the docs for more details.
|
||||
/// #footnote[https://typst.app/docs]
|
||||
@ -46,7 +46,7 @@ cast! {
|
||||
///
|
||||
/// The footnote automatically attaches itself to the preceding word, even if
|
||||
/// there is a space before it in the markup. To force space, you can use the
|
||||
/// string `[#" "]` or explicit [horizontal spacing]($func/h).
|
||||
/// string `[#" "]` or explicit [horizontal spacing]($h).
|
||||
///
|
||||
/// By giving a label to a footnote, you can have multiple references to it.
|
||||
///
|
||||
@ -61,21 +61,14 @@ cast! {
|
||||
/// apply to the footnote's content. See [here][issue] for more information.
|
||||
///
|
||||
/// [issue]: https://github.com/typst/typst/issues/1467#issuecomment-1588799440
|
||||
///
|
||||
/// Display: Footnote
|
||||
/// Category: meta
|
||||
#[element(Locatable, Synthesize, Show, Count)]
|
||||
#[scope(
|
||||
scope.define("entry", FootnoteEntry::func());
|
||||
scope
|
||||
)]
|
||||
#[elem(scope, Locatable, Synthesize, Show, Count)]
|
||||
pub struct FootnoteElem {
|
||||
/// How to number footnotes.
|
||||
///
|
||||
/// By default, the footnote numbering continues throughout your document.
|
||||
/// If you prefer per-page footnote numbering, you can reset the footnote
|
||||
/// [counter]($func/counter) in the page [header]($func/page.header). In the
|
||||
/// future, there might be a simpler way to achieve this.
|
||||
/// [counter]($counter) in the page [header]($page.header). In the future,
|
||||
/// there might be a simpler way to achieve this.
|
||||
///
|
||||
/// ```example
|
||||
/// #set footnote(numbering: "*")
|
||||
@ -93,6 +86,12 @@ pub struct FootnoteElem {
|
||||
pub body: FootnoteBody,
|
||||
}
|
||||
|
||||
#[scope]
|
||||
impl FootnoteElem {
|
||||
#[elem]
|
||||
type FootnoteEntry;
|
||||
}
|
||||
|
||||
impl FootnoteElem {
|
||||
/// Creates a new footnote that the passed content as its body.
|
||||
pub fn with_content(content: Content) -> Self {
|
||||
@ -145,7 +144,7 @@ impl Show for FootnoteElem {
|
||||
Ok(vt.delayed(|vt| {
|
||||
let loc = self.declaration_location(vt).at(self.span())?;
|
||||
let numbering = self.numbering(styles);
|
||||
let counter = Counter::of(Self::func());
|
||||
let counter = Counter::of(Self::elem());
|
||||
let num = counter.at(vt, loc)?.display(vt, &numbering)?;
|
||||
let sup = SuperElem::new(num).pack();
|
||||
let hole = HElem::new(Abs::zero().into()).with_weak(true).pack();
|
||||
@ -168,11 +167,9 @@ impl Count for FootnoteElem {
|
||||
///
|
||||
/// _Note:_ Set and show rules for `footnote.entry` must be defined at the
|
||||
/// beginning of the document in order to work correctly.
|
||||
/// See [here][issue] for more information.
|
||||
/// See [here](https://github.com/typst/typst/issues/1348#issuecomment-1566316463)
|
||||
/// for more information.
|
||||
///
|
||||
/// [issue]: https://github.com/typst/typst/issues/1348#issuecomment-1566316463
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// ```example
|
||||
/// #show footnote.entry: set text(red)
|
||||
///
|
||||
@ -180,10 +177,7 @@ impl Count for FootnoteElem {
|
||||
/// #footnote[It's down here]
|
||||
/// has red text!
|
||||
/// ```
|
||||
///
|
||||
/// Display: Footnote Entry
|
||||
/// Category: meta
|
||||
#[element(Show, Finalize)]
|
||||
#[elem(name = "entry", title = "Footnote Entry", Show, Finalize)]
|
||||
pub struct FootnoteEntry {
|
||||
/// The footnote for this entry. It's location can be used to determine
|
||||
/// the footnote counter state.
|
||||
@ -220,7 +214,7 @@ pub struct FootnoteEntry {
|
||||
#[default(
|
||||
LineElem::new()
|
||||
.with_length(Ratio::new(0.3).into())
|
||||
.with_stroke(PartialStroke {
|
||||
.with_stroke(Stroke {
|
||||
thickness: Smart::Custom(Abs::pt(0.5).into()),
|
||||
..Default::default()
|
||||
})
|
||||
@ -273,7 +267,7 @@ impl Show for FootnoteEntry {
|
||||
let note = self.note();
|
||||
let number_gap = Em::new(0.05);
|
||||
let numbering = note.numbering(StyleChain::default());
|
||||
let counter = Counter::of(FootnoteElem::func());
|
||||
let counter = Counter::of(FootnoteElem::elem());
|
||||
let loc = note.0.location().unwrap();
|
||||
let num = counter.at(vt, loc)?.display(vt, &numbering)?;
|
||||
let sup = SuperElem::new(num)
|
||||
|
@ -17,14 +17,13 @@ use crate::text::{SpaceElem, TextElem, TextSize};
|
||||
///
|
||||
/// Typst can automatically number your headings for you. To enable numbering,
|
||||
/// specify how you want your headings to be numbered with a
|
||||
/// [numbering pattern or function]($func/numbering).
|
||||
/// [numbering pattern or function]($numbering).
|
||||
///
|
||||
/// Independently from the numbering, Typst can also automatically generate an
|
||||
/// [outline]($func/outline) of all headings for you. To exclude one or more
|
||||
/// headings from this outline, you can set the `outlined` parameter to
|
||||
/// `{false}`.
|
||||
/// [outline]($outline) of all headings for you. To exclude one or more headings
|
||||
/// from this outline, you can set the `outlined` parameter to `{false}`.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// # Example
|
||||
/// ```example
|
||||
/// #set heading(numbering: "1.a)")
|
||||
///
|
||||
@ -35,21 +34,18 @@ use crate::text::{SpaceElem, TextElem, TextSize};
|
||||
/// To start, ...
|
||||
/// ```
|
||||
///
|
||||
/// ## Syntax { #syntax }
|
||||
/// # Syntax
|
||||
/// Headings have dedicated syntax: They can be created by starting a line with
|
||||
/// one or multiple equals signs, followed by a space. The number of equals
|
||||
/// signs determines the heading's logical nesting depth.
|
||||
///
|
||||
/// Display: Heading
|
||||
/// Category: meta
|
||||
#[element(Locatable, Synthesize, Count, Show, Finalize, LocalName, Refable, Outlinable)]
|
||||
#[elem(Locatable, Synthesize, Count, Show, Finalize, LocalName, Refable, Outlinable)]
|
||||
pub struct HeadingElem {
|
||||
/// The logical nesting depth of the heading, starting from one.
|
||||
#[default(NonZeroUsize::ONE)]
|
||||
pub level: NonZeroUsize,
|
||||
|
||||
/// How to number the heading. Accepts a
|
||||
/// [numbering pattern or function]($func/numbering).
|
||||
/// [numbering pattern or function]($numbering).
|
||||
///
|
||||
/// ```example
|
||||
/// #set heading(numbering: "1.a.")
|
||||
@ -78,11 +74,11 @@ pub struct HeadingElem {
|
||||
/// ```
|
||||
pub supplement: Smart<Option<Supplement>>,
|
||||
|
||||
/// Whether the heading should appear in the [outline]($func/outline).
|
||||
/// Whether the heading should appear in the [outline]($outline).
|
||||
///
|
||||
/// Note that this property, if set to `{true}`, ensures the heading is
|
||||
/// also shown as a bookmark in the exported PDF's outline (when exporting
|
||||
/// to PDF). To change that behavior, use the `bookmarked` property.
|
||||
/// Note that this property, if set to `{true}`, ensures the heading is also
|
||||
/// shown as a bookmark in the exported PDF's outline (when exporting to
|
||||
/// PDF). To change that behavior, use the `bookmarked` property.
|
||||
///
|
||||
/// ```example
|
||||
/// #outline()
|
||||
@ -103,9 +99,8 @@ pub struct HeadingElem {
|
||||
/// The default value of `{auto}` indicates that the heading will only
|
||||
/// appear in the exported PDF's outline if its `outlined` property is set
|
||||
/// to `{true}`, that is, if it would also be listed in Typst's
|
||||
/// [outline]($func/outline). Setting this property to either
|
||||
/// `{true}` (bookmark) or `{false}` (don't bookmark) bypasses that
|
||||
/// behavior.
|
||||
/// [outline]($outline). Setting this property to either `{true}` (bookmark)
|
||||
/// or `{false}` (don't bookmark) bypasses that behavior.
|
||||
///
|
||||
/// ```example
|
||||
/// #heading[Normal heading]
|
||||
@ -149,7 +144,7 @@ impl Show for HeadingElem {
|
||||
fn show(&self, _: &mut Vt, styles: StyleChain) -> SourceResult<Content> {
|
||||
let mut realized = self.body();
|
||||
if let Some(numbering) = self.numbering(styles) {
|
||||
realized = Counter::of(Self::func())
|
||||
realized = Counter::of(Self::elem())
|
||||
.display(Some(numbering), false)
|
||||
.spanned(self.span())
|
||||
+ HElem::new(Em::new(0.3).into()).with_weak(true).pack()
|
||||
@ -205,7 +200,7 @@ impl Refable for HeadingElem {
|
||||
}
|
||||
|
||||
fn counter(&self) -> Counter {
|
||||
Counter::of(Self::func())
|
||||
Counter::of(Self::elem())
|
||||
}
|
||||
|
||||
fn numbering(&self) -> Option<Numbering> {
|
||||
@ -221,7 +216,7 @@ impl Outlinable for HeadingElem {
|
||||
|
||||
let mut content = self.body();
|
||||
if let Some(numbering) = self.numbering(StyleChain::default()) {
|
||||
let numbers = Counter::of(Self::func())
|
||||
let numbers = Counter::of(Self::elem())
|
||||
.at(vt, self.0.location().unwrap())?
|
||||
.display(vt, &numbering)?;
|
||||
content = numbers + SpaceElem::new().pack() + content;
|
||||
|
@ -6,7 +6,7 @@ use crate::text::{Hyphenate, TextElem};
|
||||
/// By default, links are not styled any different from normal text. However,
|
||||
/// you can easily apply a style of your choice with a show rule.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// # Example
|
||||
/// ```example
|
||||
/// #show link: underline
|
||||
///
|
||||
@ -18,13 +18,10 @@ use crate::text::{Hyphenate, TextElem};
|
||||
/// ]
|
||||
/// ```
|
||||
///
|
||||
/// ## Syntax { #syntax }
|
||||
/// # Syntax
|
||||
/// This function also has dedicated syntax: Text that starts with `http://` or
|
||||
/// `https://` is automatically turned into a link.
|
||||
///
|
||||
/// Display: Link
|
||||
/// Category: meta
|
||||
#[element(Show)]
|
||||
#[elem(Show)]
|
||||
pub struct LinkElem {
|
||||
/// The destination the link points to.
|
||||
///
|
||||
@ -35,17 +32,16 @@ pub struct LinkElem {
|
||||
///
|
||||
/// - To link to another part of the document, `dest` can take one of three
|
||||
/// forms:
|
||||
/// - A [label]($func/label) attached to an element. If you also want
|
||||
/// automatic text for the link based on the element, consider using
|
||||
/// a [reference]($func/ref) instead.
|
||||
/// - A [label]($label) attached to an element. If you also want automatic
|
||||
/// text for the link based on the element, consider using a
|
||||
/// [reference]($ref) instead.
|
||||
///
|
||||
/// - A [location]($func/locate) resulting from a [`locate`]($func/locate)
|
||||
/// call or [`query`]($func/query).
|
||||
/// - A [location]($locate) resulting from a [`locate`]($locate) call or
|
||||
/// [`query`]($query).
|
||||
///
|
||||
/// - A dictionary with a `page` key of type [integer]($type/integer) and
|
||||
/// `x` and `y` coordinates of type [length]($type/length). Pages are
|
||||
/// counted from one, and the coordinates are relative to the page's top
|
||||
/// left corner.
|
||||
/// - A dictionary with a `page` key of type [integer]($int) and `x` and
|
||||
/// `y` coordinates of type [length]($length). Pages are counted from
|
||||
/// one, and the coordinates are relative to the page's top left corner.
|
||||
///
|
||||
/// ```example
|
||||
/// = Introduction <intro>
|
||||
|
@ -2,11 +2,11 @@ use crate::prelude::*;
|
||||
|
||||
/// Exposes a value to the query system without producing visible content.
|
||||
///
|
||||
/// This element can be retrieved with the [`query`]($func/query) function and
|
||||
/// from the command with [`typst query`]($reference/meta/query/#cli-queries).
|
||||
/// Its purpose is to expose an arbitrary value to the introspection system. To
|
||||
/// identify a metadata value among others, you can attach a
|
||||
/// [`label`]($func/label) to it and query for that label.
|
||||
/// This element can be retrieved with the [`query`]($query) function and from
|
||||
/// the command with [`typst query`]($reference/meta/query/#cli-queries). Its
|
||||
/// purpose is to expose an arbitrary value to the introspection system. To
|
||||
/// identify a metadata value among others, you can attach a [`label`]($label)
|
||||
/// to it and query for that label.
|
||||
///
|
||||
/// The `metadata` element is especially useful for command line queries because
|
||||
/// it allows you to expose arbitrary values to the outside world.
|
||||
@ -20,10 +20,7 @@ use crate::prelude::*;
|
||||
/// query(<note>, loc).first().value
|
||||
/// })
|
||||
/// ```
|
||||
///
|
||||
/// Display: Metadata
|
||||
/// Category: meta
|
||||
#[element(Behave, Show, Locatable)]
|
||||
#[elem(Behave, Show, Locatable)]
|
||||
pub struct MetadataElem {
|
||||
/// The value to embed into the document.
|
||||
#[required]
|
||||
|
@ -9,9 +9,11 @@ mod footnote;
|
||||
mod heading;
|
||||
mod link;
|
||||
mod metadata;
|
||||
mod numbering;
|
||||
#[path = "numbering.rs"]
|
||||
mod numbering_;
|
||||
mod outline;
|
||||
mod query;
|
||||
#[path = "query.rs"]
|
||||
mod query_;
|
||||
mod reference;
|
||||
mod state;
|
||||
|
||||
@ -24,9 +26,9 @@ pub use self::footnote::*;
|
||||
pub use self::heading::*;
|
||||
pub use self::link::*;
|
||||
pub use self::metadata::*;
|
||||
pub use self::numbering::*;
|
||||
pub use self::numbering_::*;
|
||||
pub use self::outline::*;
|
||||
pub use self::query::*;
|
||||
pub use self::query_::*;
|
||||
pub use self::reference::*;
|
||||
pub use self::state::*;
|
||||
|
||||
@ -35,24 +37,27 @@ use crate::text::TextElem;
|
||||
|
||||
/// Hook up all meta definitions.
|
||||
pub(super) fn define(global: &mut Scope) {
|
||||
global.define("document", DocumentElem::func());
|
||||
global.define("ref", RefElem::func());
|
||||
global.define("link", LinkElem::func());
|
||||
global.define("outline", OutlineElem::func());
|
||||
global.define("heading", HeadingElem::func());
|
||||
global.define("figure", FigureElem::func());
|
||||
global.define("footnote", FootnoteElem::func());
|
||||
global.define("cite", CiteElem::func());
|
||||
global.define("bibliography", BibliographyElem::func());
|
||||
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());
|
||||
global.define("metadata", MetadataElem::func());
|
||||
global.category("meta");
|
||||
global.define_type::<Label>();
|
||||
global.define_type::<Selector>();
|
||||
global.define_type::<Location>();
|
||||
global.define_type::<Counter>();
|
||||
global.define_type::<State>();
|
||||
global.define_elem::<DocumentElem>();
|
||||
global.define_elem::<RefElem>();
|
||||
global.define_elem::<LinkElem>();
|
||||
global.define_elem::<OutlineElem>();
|
||||
global.define_elem::<HeadingElem>();
|
||||
global.define_elem::<FigureElem>();
|
||||
global.define_elem::<FootnoteElem>();
|
||||
global.define_elem::<CiteElem>();
|
||||
global.define_elem::<BibliographyElem>();
|
||||
global.define_elem::<MetadataElem>();
|
||||
global.define_func::<locate>();
|
||||
global.define_func::<style>();
|
||||
global.define_func::<layout>();
|
||||
global.define_func::<numbering>();
|
||||
global.define_func::<query>();
|
||||
}
|
||||
|
||||
/// The named with which an element is referenced.
|
||||
|
@ -16,7 +16,7 @@ use crate::text::Case;
|
||||
/// number is substituted, their prefixes, and one suffix. The prefixes and the
|
||||
/// suffix are repeated as-is.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// # Example
|
||||
/// ```example
|
||||
/// #numbering("1.1)", 1, 2, 3) \
|
||||
/// #numbering("1.a.i", 1, 2) \
|
||||
@ -29,11 +29,10 @@ use crate::text::Case;
|
||||
/// 1, 2, 3,
|
||||
/// )
|
||||
/// ```
|
||||
///
|
||||
/// Display: Numbering
|
||||
/// Category: meta
|
||||
#[func]
|
||||
pub fn numbering(
|
||||
/// The virtual machine.
|
||||
vm: &mut Vm,
|
||||
/// Defines how the numbering works.
|
||||
///
|
||||
/// **Counting symbols** are `1`, `a`, `A`, `i`, `I`, `い`, `イ`, `א`, `가`,
|
||||
@ -64,8 +63,6 @@ pub fn numbering(
|
||||
/// given, the last counting symbol with its prefix is repeated.
|
||||
#[variadic]
|
||||
numbers: Vec<usize>,
|
||||
/// The virtual machine.
|
||||
vm: &mut Vm,
|
||||
) -> SourceResult<Value> {
|
||||
numbering.apply_vm(vm, &numbers)
|
||||
}
|
||||
|
@ -16,7 +16,7 @@ use crate::text::{LinebreakElem, SpaceElem, TextElem};
|
||||
/// be displayed in the outline alongside its title or caption. By default this
|
||||
/// generates a table of contents.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// # Example
|
||||
/// ```example
|
||||
/// #outline()
|
||||
///
|
||||
@ -27,13 +27,13 @@ use crate::text::{LinebreakElem, SpaceElem, TextElem};
|
||||
/// #lorem(10)
|
||||
/// ```
|
||||
///
|
||||
/// ## Alternative outlines { #alternative-outlines }
|
||||
/// # Alternative outlines
|
||||
/// By setting the `target` parameter, the outline can be used to generate a
|
||||
/// list of other kinds of elements than headings. In the example below, we list
|
||||
/// all figures containing images by setting `target` to `{figure.where(kind:
|
||||
/// image)}`. We could have also set it to just `figure`, but then the list
|
||||
/// would also include figures containing tables or other material. For more
|
||||
/// details on the `where` selector, [see here]($type/content.where).
|
||||
/// details on the `where` selector, [see here]($function.where).
|
||||
///
|
||||
/// ```example
|
||||
/// #outline(
|
||||
@ -47,25 +47,17 @@ use crate::text::{LinebreakElem, SpaceElem, TextElem};
|
||||
/// )
|
||||
/// ```
|
||||
///
|
||||
/// ## Styling the outline { #styling-the-outline }
|
||||
/// # Styling the outline
|
||||
/// The outline element has several options for customization, such as its
|
||||
/// `title` and `indent` parameters. If desired, however, it is possible to
|
||||
/// have more control over the outline's look and style through the
|
||||
/// [`outline.entry`]($func/outline.entry) element.
|
||||
///
|
||||
/// Display: Outline
|
||||
/// Category: meta
|
||||
/// Keywords: Table of Contents
|
||||
#[element(Show, Finalize, LocalName)]
|
||||
#[scope(
|
||||
scope.define("entry", OutlineEntry::func());
|
||||
scope
|
||||
)]
|
||||
/// `title` and `indent` parameters. If desired, however, it is possible to have
|
||||
/// more control over the outline's look and style through the
|
||||
/// [`outline.entry`]($outline.entry) element.
|
||||
#[elem(scope, keywords = ["Table of Contents"], Show, Finalize, LocalName)]
|
||||
pub struct OutlineElem {
|
||||
/// The title of the outline.
|
||||
///
|
||||
/// - When set to `{auto}`, an appropriate title for the
|
||||
/// [text language]($func/text.lang) will be used. This is the default.
|
||||
/// [text language]($text.lang) will be used. This is the default.
|
||||
/// - When set to `{none}`, the outline will not have a title.
|
||||
/// - A custom title can be set by passing content.
|
||||
///
|
||||
@ -97,7 +89,7 @@ pub struct OutlineElem {
|
||||
/// )
|
||||
/// ```
|
||||
#[default(LocatableSelector(Selector::Elem(
|
||||
HeadingElem::func(),
|
||||
HeadingElem::elem(),
|
||||
Some(dict! { "outlined" => true })
|
||||
)))]
|
||||
pub target: LocatableSelector,
|
||||
@ -125,19 +117,18 @@ pub struct OutlineElem {
|
||||
/// - `{none}`: No indent
|
||||
/// - `{auto}`: Indents the numbering of the nested entry with the title of
|
||||
/// its parent entry. This only has an effect if the entries are numbered
|
||||
/// (e.g., via [heading numbering]($func/heading.numbering)).
|
||||
/// - [Relative length]($type/relative-length): Indents the item by this length
|
||||
/// (e.g., via [heading numbering]($heading.numbering)).
|
||||
/// - [Relative length]($relative): Indents the item by this length
|
||||
/// multiplied by its nesting level. Specifying `{2em}`, for instance,
|
||||
/// would indent top-level headings (not nested) by `{0em}`, second level
|
||||
/// headings by `{2em}` (nested once), third-level headings by `{4em}`
|
||||
/// (nested twice) and so on.
|
||||
/// - [Function]($type/function): You can completely customize this setting
|
||||
/// with a function. That function receives the nesting level as a
|
||||
/// parameter (starting at 0 for top-level headings/elements) and can
|
||||
/// return a relative length or content making up the indent. For example,
|
||||
/// `{n => n * 2em}` would be equivalent to just specifying `{2em}`,
|
||||
/// while `{n => [→ ] * n}` would indent with one arrow per nesting
|
||||
/// level.
|
||||
/// - [Function]($function): You can completely customize this setting with
|
||||
/// a function. That function receives the nesting level as a parameter
|
||||
/// (starting at 0 for top-level headings/elements) and can return a
|
||||
/// relative length or content making up the indent. For example,
|
||||
/// `{n => n * 2em}` would be equivalent to just specifying `{2em}`, while
|
||||
/// `{n => [→ ] * n}` would indent with one arrow per nesting level.
|
||||
///
|
||||
/// *Migration hints:* Specifying `{true}` (equivalent to `{auto}`) or
|
||||
/// `{false}` (equivalent to `{none}`) for this option is deprecated and
|
||||
@ -184,6 +175,12 @@ pub struct OutlineElem {
|
||||
pub fill: Option<Content>,
|
||||
}
|
||||
|
||||
#[scope]
|
||||
impl OutlineElem {
|
||||
#[elem]
|
||||
type OutlineEntry;
|
||||
}
|
||||
|
||||
impl Show for OutlineElem {
|
||||
#[tracing::instrument(name = "OutlineElem::show", skip_all)]
|
||||
fn show(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Content> {
|
||||
@ -391,11 +388,9 @@ cast! {
|
||||
/// outlined element, its page number, and the filler content between both.
|
||||
///
|
||||
/// This element is intended for use with show rules to control the appearance
|
||||
/// of outlines.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// The example below shows how to style entries for top-level sections to make
|
||||
/// them stand out.
|
||||
/// of outlines. To customize an entry's line, you can build it from scratch by
|
||||
/// accessing the `level`, `element`, `body`, `fill` and `page` fields on the
|
||||
/// entry.
|
||||
///
|
||||
/// ```example
|
||||
/// #set heading(numbering: "1.")
|
||||
@ -416,13 +411,7 @@ cast! {
|
||||
/// = Analysis
|
||||
/// == Setup
|
||||
/// ```
|
||||
///
|
||||
/// To completely customize an entry's line, you can also build it from scratch
|
||||
/// by accessing the `level`, `element`, `body`, `fill` and `page` fields on the entry.
|
||||
///
|
||||
/// Display: Outline Entry
|
||||
/// Category: meta
|
||||
#[element(Show)]
|
||||
#[elem(name = "entry", title = "Outline Entry", Show)]
|
||||
pub struct OutlineEntry {
|
||||
/// The nesting level of this outline entry. Starts at `{1}` for top-level
|
||||
/// entries.
|
||||
@ -430,8 +419,8 @@ pub struct OutlineEntry {
|
||||
pub level: NonZeroUsize,
|
||||
|
||||
/// The element this entry refers to. Its location will be available
|
||||
/// through the [`location`]($type/content.location) method on content
|
||||
/// and can be [linked]($func/link) to.
|
||||
/// through the [`location`]($content.location) method on content
|
||||
/// and can be [linked]($link) to.
|
||||
#[required]
|
||||
pub element: Content,
|
||||
|
||||
@ -446,7 +435,7 @@ pub struct OutlineEntry {
|
||||
/// located in. When `{none}`, empty space is inserted in that gap instead.
|
||||
///
|
||||
/// Note that, when using show rules to override outline entries, it is
|
||||
/// recommended to wrap the filling content in a [`box`]($func/box) with
|
||||
/// recommended to wrap the filling content in a [`box`]($box) with
|
||||
/// fractional width. For example, `{box(width: 1fr, repeat[-])}` would show
|
||||
/// precisely as many `-` characters as necessary to fill a particular gap.
|
||||
#[required]
|
||||
|
@ -4,10 +4,10 @@ use crate::prelude::*;
|
||||
///
|
||||
/// The `query` functions lets you search your document for elements of a
|
||||
/// particular type or with a particular label. To use it, you first need to
|
||||
/// retrieve the current document location with the [`locate`]($func/locate)
|
||||
/// retrieve the current document location with the [`locate`]($locate)
|
||||
/// function.
|
||||
///
|
||||
/// ## Finding elements { #finding-elements }
|
||||
/// # Finding elements
|
||||
/// In the example below, we create a custom page header that displays the text
|
||||
/// "Typst Academy" in small capitals and the current section title. On the
|
||||
/// first page, the section title is omitted because the header is before the
|
||||
@ -59,7 +59,7 @@ use crate::prelude::*;
|
||||
/// #lorem(15)
|
||||
/// ```
|
||||
///
|
||||
/// ## A word of caution { #caution }
|
||||
/// # A word of caution { #caution }
|
||||
/// To resolve all your queries, Typst evaluates and layouts parts of the
|
||||
/// document multiple times. However, there is no guarantee that your queries
|
||||
/// can actually be completely resolved. If you aren't careful a query can
|
||||
@ -73,9 +73,9 @@ use crate::prelude::*;
|
||||
/// on. As we can see, the output has five headings. This is because Typst
|
||||
/// simply gives up after five attempts.
|
||||
///
|
||||
/// In general, you should try not to write queries that affect themselves.
|
||||
/// The same words of caution also apply to other introspection features like
|
||||
/// [counters]($func/counter) and [state]($func/state).
|
||||
/// In general, you should try not to write queries that affect themselves. The
|
||||
/// same words of caution also apply to other introspection features like
|
||||
/// [counters]($counter) and [state]($state).
|
||||
///
|
||||
/// ```example
|
||||
/// = Real
|
||||
@ -86,11 +86,11 @@ use crate::prelude::*;
|
||||
/// })
|
||||
/// ```
|
||||
///
|
||||
/// ## Command line queries { #command-line-queries }
|
||||
/// # Command line queries
|
||||
/// You can also perform queries from the command line with the `typst query`
|
||||
/// command. This command executes an arbitrary query on the document and
|
||||
/// returns the resulting elements in serialized form. Consider the following
|
||||
/// `example.typ` file which contains some invisible [metadata]($func/metadata):
|
||||
/// `example.typ` file which contains some invisible [metadata]($metadata):
|
||||
///
|
||||
/// ```typ
|
||||
/// #metadata("This is a note") <note>
|
||||
@ -125,32 +125,29 @@ use crate::prelude::*;
|
||||
/// $ typst query example.typ "<note>" --field value --one
|
||||
/// "This is a note"
|
||||
/// ```
|
||||
///
|
||||
/// Display: Query
|
||||
/// Category: meta
|
||||
#[func]
|
||||
pub fn query(
|
||||
/// The virtual machine.
|
||||
vm: &mut Vm,
|
||||
/// Can be an element function like a `heading` or `figure`, a `{<label>}`
|
||||
/// or a more complex selector like `{heading.where(level: 1)}`.
|
||||
///
|
||||
/// Currently, only a subset of element functions is supported. Aside from
|
||||
/// headings and figures, this includes equations, references and all
|
||||
/// elements with an explicit label. As a result, you _can_ query for e.g.
|
||||
/// [`strong`]($func/strong) elements, but you will find only those that
|
||||
/// have an explicit label attached to them. This limitation will be
|
||||
/// resolved in the future.
|
||||
/// [`strong`]($strong) elements, but you will find only those that have an
|
||||
/// explicit label attached to them. This limitation will be resolved in the
|
||||
/// future.
|
||||
target: LocatableSelector,
|
||||
/// Can be an arbitrary location, as its value is irrelevant for the
|
||||
/// function's return value. 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
|
||||
/// [`locate`]($func/locate) calls, the amount of code that can depend on
|
||||
/// the query's result is reduced. If you could call it directly at the top
|
||||
/// [`locate`]($locate) calls, the amount of code that can depend on the
|
||||
/// query's result is reduced. If you could call it directly at the top
|
||||
/// level of a module, the evaluation of the whole module and its exports
|
||||
/// could depend on the query's result.
|
||||
location: Location,
|
||||
/// The virtual machine.
|
||||
vm: &mut Vm,
|
||||
) -> Array {
|
||||
let _ = location;
|
||||
let vec = vm.vt.introspector.query(&target.0);
|
||||
@ -158,19 +155,3 @@ pub fn query(
|
||||
.map(|elem| Value::Content(elem.into_inner()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Turns a value into a selector. The following values are accepted:
|
||||
/// - An element function like a `heading` or `figure`.
|
||||
/// - A `{<label>}`.
|
||||
/// - A more complex selector like `{heading.where(level: 1)}`.
|
||||
///
|
||||
/// Display: Selector
|
||||
/// Category: meta
|
||||
#[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,
|
||||
) -> Selector {
|
||||
target
|
||||
}
|
||||
|
@ -9,20 +9,19 @@ use crate::text::TextElem;
|
||||
/// Produces a textual reference to a label. For example, a reference to a
|
||||
/// heading will yield an appropriate string such as "Section 1" for a reference
|
||||
/// to the first heading. The references are also links to the respective
|
||||
/// element. Reference syntax can also be used to [cite]($func/cite) from a
|
||||
/// element. Reference syntax can also be used to [cite]($cite) from a
|
||||
/// bibliography.
|
||||
///
|
||||
/// Referenceable elements include [headings]($func/heading),
|
||||
/// [figures]($func/figure), [equations]($func/math.equation), and
|
||||
/// [footnotes]($func/footnote). To create a custom referenceable element like a
|
||||
/// theorem, you can create a figure of a custom [`kind`]($func/figure.kind) and
|
||||
/// write a show rule for it. In the future, there might be a more direct way to
|
||||
/// define a custom referenceable element.
|
||||
/// Referenceable elements include [headings]($heading), [figures]($figure),
|
||||
/// [equations]($math.equation), and [footnotes]($footnote). To create a custom
|
||||
/// referenceable element like a theorem, you can create a figure of a custom
|
||||
/// [`kind`]($figure.kind) and write a show rule for it. In the future, there
|
||||
/// might be a more direct way to define a custom referenceable element.
|
||||
///
|
||||
/// If you just want to link to a labelled element and not get an automatic
|
||||
/// textual reference, consider using the [`link`]($func/link) function instead.
|
||||
/// textual reference, consider using the [`link`]($link) function instead.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// # Example
|
||||
/// ```example
|
||||
/// #set heading(numbering: "1.")
|
||||
/// #set math.equation(numbering: "(1)")
|
||||
@ -46,7 +45,7 @@ use crate::text::TextElem;
|
||||
/// #bibliography("works.bib")
|
||||
/// ```
|
||||
///
|
||||
/// ## Syntax { #syntax }
|
||||
/// # Syntax
|
||||
/// This function also has dedicated syntax: A reference to a label can be
|
||||
/// created by typing an `@` followed by the name of the label (e.g.
|
||||
/// `[= Introduction <intro>]` can be referenced by typing `[@intro]`).
|
||||
@ -54,7 +53,7 @@ use crate::text::TextElem;
|
||||
/// To customize the supplement, add content in square brackets after the
|
||||
/// reference: `[@intro[Chapter]]`.
|
||||
///
|
||||
/// ## Customization { #customization }
|
||||
/// # Customization
|
||||
/// If you write a show rule for references, you can access the referenced
|
||||
/// element through the `element` field of the reference. The `element` may
|
||||
/// be `{none}` even if it exists if Typst hasn't discovered it yet, so you
|
||||
@ -83,10 +82,7 @@ use crate::text::TextElem;
|
||||
/// In @beginning we prove @pythagoras.
|
||||
/// $ a^2 + b^2 = c^2 $ <pythagoras>
|
||||
/// ```
|
||||
///
|
||||
/// Display: Reference
|
||||
/// Category: meta
|
||||
#[element(Synthesize, Locatable, Show)]
|
||||
#[elem(title = "Reference", Synthesize, Locatable, Show)]
|
||||
pub struct RefElem {
|
||||
/// The target label that should be referenced.
|
||||
#[required]
|
||||
@ -163,7 +159,7 @@ impl Show for RefElem {
|
||||
|
||||
let elem = elem.at(span)?;
|
||||
|
||||
if elem.func() == FootnoteElem::func() {
|
||||
if elem.func() == FootnoteElem::elem() {
|
||||
return Ok(FootnoteElem::with_label(target).pack().spanned(span));
|
||||
}
|
||||
|
||||
@ -192,7 +188,7 @@ impl Show for RefElem {
|
||||
.hint(eco_format!(
|
||||
"you can enable {} numbering with `#set {}(numbering: \"1.\")`",
|
||||
elem.func().name(),
|
||||
if elem.func() == EquationElem::func() {
|
||||
if elem.func() == EquationElem::elem() {
|
||||
"math.equation"
|
||||
} else {
|
||||
elem.func().name()
|
||||
|
@ -30,7 +30,7 @@ use crate::prelude::*;
|
||||
/// #compute("x - 5")
|
||||
/// ```
|
||||
///
|
||||
/// ## State and document markup { #state-and-markup }
|
||||
/// # State and document markup { #state-and-markup }
|
||||
/// Why does it do that? Because, in general, this kind of computation with side
|
||||
/// effects is problematic in document markup and Typst is upfront about that.
|
||||
/// For the results to make sense, the computation must proceed in the same
|
||||
@ -61,7 +61,7 @@ use crate::prelude::*;
|
||||
/// `template` function and only then sees the `Outline`. Just counting up would
|
||||
/// number the `Introduction` with `1` and the `Outline` with `2`.
|
||||
///
|
||||
/// ## Managing state in Typst { #state-in-typst }
|
||||
/// # Managing state in Typst { #state-in-typst }
|
||||
/// So what do we do instead? We use Typst's state management system. Calling
|
||||
/// the `state` function with an identifying string key and an optional initial
|
||||
/// value gives you a state value which exposes a few methods. The two most
|
||||
@ -122,14 +122,14 @@ use crate::prelude::*;
|
||||
///
|
||||
/// This example is of course a bit silly, but in practice this is often exactly
|
||||
/// what you want! A good example are heading counters, which is why Typst's
|
||||
/// [counting system]($func/counter) is very similar to its state system.
|
||||
/// [counting system]($counter) is very similar to its state system.
|
||||
///
|
||||
/// ## Time Travel { #time-travel }
|
||||
/// # Time Travel
|
||||
/// By using Typst's state management system you also get time travel
|
||||
/// capabilities! By combining the state system with [`locate`]($func/locate)
|
||||
/// and [`query`]($func/query), we can find out what the value of the state will
|
||||
/// be at any position in the document from anywhere else. In particular, the
|
||||
/// `at` method gives us the value of the state at any location and the `final`
|
||||
/// capabilities! By combining the state system with [`locate`]($locate) and
|
||||
/// [`query`]($query), we can find out what the value of the state will be at
|
||||
/// any position in the document from anywhere else. In particular, the `at`
|
||||
/// method gives us the value of the state at any location and the `final`
|
||||
/// methods gives us the value of the state at the end of the document.
|
||||
///
|
||||
/// ```example
|
||||
@ -156,7 +156,7 @@ use crate::prelude::*;
|
||||
/// #compute("x - 5")
|
||||
/// ```
|
||||
///
|
||||
/// ## A word of caution { #caution }
|
||||
/// # A word of caution { #caution }
|
||||
/// To resolve the values of all states, Typst evaluates parts of your code
|
||||
/// multiple times. However, there is no guarantee that your state manipulation
|
||||
/// can actually be completely resolved.
|
||||
@ -180,73 +180,7 @@ use crate::prelude::*;
|
||||
/// `locate` calls or `display` calls of state or counters. Instead, pass a
|
||||
/// function to `update` that determines the value of the state based on its
|
||||
/// previous value.
|
||||
///
|
||||
/// ## Methods
|
||||
/// ### display()
|
||||
/// Displays the value of the state.
|
||||
///
|
||||
/// - format: function (positional)
|
||||
/// A function which receives the value of the state and can return arbitrary
|
||||
/// content which is then displayed. If this is omitted, the value is directly
|
||||
/// displayed.
|
||||
///
|
||||
/// - returns: content
|
||||
///
|
||||
/// ### update()
|
||||
/// Updates the value of the state.
|
||||
///
|
||||
/// The update will be in effect at the position where the returned content is
|
||||
/// inserted into the document. If you don't put the output into the document,
|
||||
/// nothing happens! This would be the case, for example, if you write
|
||||
/// `{let _ = state("key").update(7)}`. State updates are always applied in
|
||||
/// layout order and in that case, Typst wouldn't know when to update the state.
|
||||
///
|
||||
/// - value: any or function (positional, required)
|
||||
/// If given a non function-value, sets the state to that value. If given a
|
||||
/// function, that function receives the previous state and has to return the
|
||||
/// new state.
|
||||
///
|
||||
/// - returns: content
|
||||
///
|
||||
/// ### at()
|
||||
/// Gets the value of the state at the given location.
|
||||
///
|
||||
/// - location: location (positional, required)
|
||||
/// The location at which the state's value should be retrieved. A suitable
|
||||
/// location can be retrieved from [`locate`]($func/locate) or
|
||||
/// [`query`]($func/query).
|
||||
///
|
||||
/// - returns: any
|
||||
///
|
||||
/// ### final()
|
||||
/// Gets the value of the state at the end of the document.
|
||||
///
|
||||
/// - location: location (positional, required)
|
||||
/// Can be an arbitrary location, as its value is irrelevant for the method's
|
||||
/// return value. 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 method within [`locate`]($func/locate) calls,
|
||||
/// the amount of code that can depend on the method's result is reduced. If
|
||||
/// you could call `final` directly at the top level of a module, the
|
||||
/// evaluation of the whole module and its exports could depend on the state's
|
||||
/// value.
|
||||
///
|
||||
/// - returns: any
|
||||
///
|
||||
/// Display: State
|
||||
/// Category: meta
|
||||
#[func]
|
||||
pub fn state(
|
||||
/// The key that identifies this state.
|
||||
key: Str,
|
||||
/// The initial value of the state.
|
||||
#[default]
|
||||
init: Value,
|
||||
) -> State {
|
||||
State { key, init }
|
||||
}
|
||||
|
||||
/// A state.
|
||||
#[ty(scope)]
|
||||
#[derive(Clone, PartialEq, Hash)]
|
||||
pub struct State {
|
||||
/// The key that identifies the state.
|
||||
@ -256,49 +190,9 @@ pub struct State {
|
||||
}
|
||||
|
||||
impl State {
|
||||
/// Call a method on a state.
|
||||
#[tracing::instrument(skip(vm))]
|
||||
pub fn call_method(
|
||||
self,
|
||||
vm: &mut Vm,
|
||||
method: &str,
|
||||
mut args: Args,
|
||||
span: Span,
|
||||
) -> SourceResult<Value> {
|
||||
let value = match method {
|
||||
"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_value(),
|
||||
_ => bail!(span, "type state has no method `{}`", method),
|
||||
};
|
||||
args.finish()?;
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
/// Display the current value of the state.
|
||||
pub fn display(self, func: Option<Func>) -> Content {
|
||||
DisplayElem::new(self, func).pack()
|
||||
}
|
||||
|
||||
/// Get the value of the state at the given location.
|
||||
#[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(&self.selector().before(location, true)).len();
|
||||
Ok(sequence[offset].clone())
|
||||
}
|
||||
|
||||
/// Get the value of the state at the final location.
|
||||
#[tracing::instrument(skip(self, vt))]
|
||||
pub fn final_(self, vt: &mut Vt, _: Location) -> SourceResult<Value> {
|
||||
let sequence = self.sequence(vt)?;
|
||||
Ok(sequence.last().unwrap().clone())
|
||||
}
|
||||
|
||||
/// Produce content that performs a state update.
|
||||
pub fn update(self, update: StateUpdate) -> Content {
|
||||
UpdateElem::new(self.key, update).pack()
|
||||
/// Create a new state identified by a key.
|
||||
pub fn new(key: Str, init: Value) -> State {
|
||||
Self { key, init }
|
||||
}
|
||||
|
||||
/// Produce the whole sequence of states.
|
||||
@ -350,7 +244,94 @@ impl State {
|
||||
|
||||
/// The selector for this state's updates.
|
||||
fn selector(&self) -> Selector {
|
||||
Selector::Elem(UpdateElem::func(), Some(dict! { "key" => self.key.clone() }))
|
||||
Selector::Elem(UpdateElem::elem(), Some(dict! { "key" => self.key.clone() }))
|
||||
}
|
||||
}
|
||||
|
||||
#[scope]
|
||||
impl State {
|
||||
/// Create a new state identified by a key.
|
||||
#[func(constructor)]
|
||||
pub fn construct(
|
||||
/// The key that identifies this state.
|
||||
key: Str,
|
||||
/// The initial value of the state.
|
||||
#[default]
|
||||
init: Value,
|
||||
) -> State {
|
||||
Self::new(key, init)
|
||||
}
|
||||
|
||||
/// Displays the current value of the state.
|
||||
#[func]
|
||||
pub fn display(
|
||||
self,
|
||||
/// A function which receives the value of the state and can return
|
||||
/// arbitrary content which is then displayed. If this is omitted, the
|
||||
/// value is directly displayed.
|
||||
#[default]
|
||||
func: Option<Func>,
|
||||
) -> Content {
|
||||
DisplayElem::new(self, func).pack()
|
||||
}
|
||||
|
||||
/// Update the value of the state.
|
||||
///
|
||||
/// The update will be in effect at the position where the returned content
|
||||
/// is inserted into the document. If you don't put the output into the
|
||||
/// document, nothing happens! This would be the case, for example, if you
|
||||
/// write `{let _ = state("key").update(7)}`. State updates are always
|
||||
/// applied in layout order and in that case, Typst wouldn't know when to
|
||||
/// update the state.
|
||||
#[func]
|
||||
pub fn update(
|
||||
self,
|
||||
/// If given a non function-value, sets the state to that value. If
|
||||
/// given a function, that function receives the previous state and has
|
||||
/// to return the new state.
|
||||
update: StateUpdate,
|
||||
) -> Content {
|
||||
UpdateElem::new(self.key, update).pack()
|
||||
}
|
||||
|
||||
/// Get the value of the state at the given location.
|
||||
#[func]
|
||||
pub fn at(
|
||||
&self,
|
||||
/// The virtual typesetter.
|
||||
vt: &mut Vt,
|
||||
/// The location at which the state's value should be retrieved. A
|
||||
/// suitable location can be retrieved from [`locate`]($locate) or
|
||||
/// [`query`]($query).
|
||||
location: Location,
|
||||
) -> SourceResult<Value> {
|
||||
let sequence = self.sequence(vt)?;
|
||||
let offset = vt
|
||||
.introspector
|
||||
.query(&self.selector().before(location.into(), true))
|
||||
.len();
|
||||
Ok(sequence[offset].clone())
|
||||
}
|
||||
|
||||
/// Get the value of the state at the end of the document.
|
||||
#[func]
|
||||
pub fn final_(
|
||||
&self,
|
||||
/// The virtual typesetter.
|
||||
vt: &mut Vt,
|
||||
/// Can be an arbitrary location, as its value is irrelevant for the
|
||||
/// method's return value. 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 method within
|
||||
/// [`locate`]($locate) calls, the amount of code that can depend on the
|
||||
/// method's result is reduced. If you could call `final` directly at
|
||||
/// the top level of a module, the evaluation of the whole module and
|
||||
/// its exports could depend on the state's value.
|
||||
location: Location,
|
||||
) -> SourceResult<Value> {
|
||||
let _ = location;
|
||||
let sequence = self.sequence(vt)?;
|
||||
Ok(sequence.last().unwrap().clone())
|
||||
}
|
||||
}
|
||||
|
||||
@ -365,10 +346,11 @@ impl Debug for State {
|
||||
}
|
||||
|
||||
cast! {
|
||||
type State: "state",
|
||||
type State,
|
||||
}
|
||||
|
||||
/// An update to perform on a state.
|
||||
#[ty]
|
||||
#[derive(Clone, PartialEq, Hash)]
|
||||
pub enum StateUpdate {
|
||||
/// Set the state to the specified value.
|
||||
@ -384,16 +366,13 @@ impl Debug for StateUpdate {
|
||||
}
|
||||
|
||||
cast! {
|
||||
type StateUpdate: "state update",
|
||||
type StateUpdate,
|
||||
v: Func => Self::Func(v),
|
||||
v: Value => Self::Set(v),
|
||||
}
|
||||
|
||||
/// Executes a display of a state.
|
||||
///
|
||||
/// Display: State
|
||||
/// Category: special
|
||||
#[element(Locatable, Show)]
|
||||
#[elem(Locatable, Show)]
|
||||
struct DisplayElem {
|
||||
/// The state.
|
||||
#[required]
|
||||
@ -419,10 +398,7 @@ impl Show for DisplayElem {
|
||||
}
|
||||
|
||||
/// Executes a display of a state.
|
||||
///
|
||||
/// Display: State
|
||||
/// Category: special
|
||||
#[element(Locatable, Show)]
|
||||
#[elem(Locatable, Show)]
|
||||
struct UpdateElem {
|
||||
/// The key that identifies the state.
|
||||
#[required]
|
||||
|
@ -15,15 +15,15 @@ pub use typst::diag::{bail, error, At, Hint, SourceResult, StrResult};
|
||||
pub use typst::doc::*;
|
||||
#[doc(no_inline)]
|
||||
pub use typst::eval::{
|
||||
array, cast, dict, format_str, func, Args, Array, AutoValue, Cast, Dict, FromValue,
|
||||
Func, IntoValue, Never, NoneValue, Scope, Str, Symbol, Type, Value, Vm,
|
||||
array, cast, dict, format_str, func, scope, ty, Args, Array, Bytes, Cast, Dict,
|
||||
FromValue, Func, IntoValue, Scope, Str, Symbol, Type, Value, Vm,
|
||||
};
|
||||
#[doc(no_inline)]
|
||||
pub use typst::geom::*;
|
||||
#[doc(no_inline)]
|
||||
pub use typst::model::{
|
||||
element, Behave, Behaviour, Construct, Content, ElemFunc, Element, Finalize, Fold,
|
||||
Introspector, Label, Locatable, LocatableSelector, Location, Locator, MetaElem,
|
||||
elem, Behave, Behaviour, Construct, Content, Element, Finalize, Fold, Introspector,
|
||||
Label, Locatable, LocatableSelector, Location, Locator, MetaElem, NativeElement,
|
||||
PlainText, Resolve, Selector, Set, Show, StyleChain, StyleVec, Styles, Synthesize,
|
||||
Unlabellable, Vt,
|
||||
};
|
||||
|
@ -24,7 +24,7 @@ pub trait ContentExt {
|
||||
fn backlinked(self, loc: Location) -> Self;
|
||||
|
||||
/// Set alignments for this content.
|
||||
fn aligned(self, aligns: Axes<Option<GenAlign>>) -> Self;
|
||||
fn aligned(self, align: Align) -> Self;
|
||||
|
||||
/// Pad this content at the sides.
|
||||
fn padded(self, padding: Sides<Rel<Length>>) -> Self;
|
||||
@ -56,8 +56,8 @@ impl ContentExt for Content {
|
||||
self.styled(MetaElem::set_data(vec![Meta::Elem(backlink)]))
|
||||
}
|
||||
|
||||
fn aligned(self, aligns: Axes<Option<GenAlign>>) -> Self {
|
||||
self.styled(AlignElem::set_alignment(aligns))
|
||||
fn aligned(self, align: Align) -> Self {
|
||||
self.styled(AlignElem::set_alignment(align))
|
||||
}
|
||||
|
||||
fn padded(self, padding: Sides<Rel<Length>>) -> Self {
|
||||
|
@ -6,7 +6,7 @@ pub fn emoji() -> Module {
|
||||
for (name, symbol) in EMOJI {
|
||||
scope.define(*name, symbol.clone());
|
||||
}
|
||||
Module::new("emoji").with_scope(scope)
|
||||
Module::new("emoji", scope)
|
||||
}
|
||||
|
||||
/// A list of named emoji.
|
||||
|
@ -10,6 +10,8 @@ use crate::prelude::*;
|
||||
|
||||
/// Hook up all symbol definitions.
|
||||
pub(super) fn define(global: &mut Scope) {
|
||||
global.define("sym", sym());
|
||||
global.define("emoji", emoji());
|
||||
global.category("symbols");
|
||||
global.define_type::<Symbol>();
|
||||
global.define_module(sym());
|
||||
global.define_module(emoji());
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ pub fn sym() -> Module {
|
||||
for (name, symbol) in SYM {
|
||||
scope.define(*name, symbol.clone());
|
||||
}
|
||||
Module::new("sym").with_scope(scope)
|
||||
Module::new("sym", scope)
|
||||
}
|
||||
|
||||
/// The list of general symbols.
|
||||
|
@ -6,19 +6,15 @@ use crate::prelude::*;
|
||||
|
||||
/// Underlines text.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// # Example
|
||||
/// ```example
|
||||
/// This is #underline[important].
|
||||
/// ```
|
||||
///
|
||||
/// Display: Underline
|
||||
/// Category: text
|
||||
#[element(Show)]
|
||||
#[elem(Show)]
|
||||
pub struct UnderlineElem {
|
||||
/// How to stroke the line.
|
||||
/// How to [stroke]($stroke) the line.
|
||||
///
|
||||
/// See the [line's documentation]($func/line.stroke) for more details. If
|
||||
/// set to `{auto}`, takes on the text's color and a thickness defined in
|
||||
/// If set to `{auto}`, takes on the text's color and a thickness defined in
|
||||
/// the current font.
|
||||
///
|
||||
/// ```example
|
||||
@ -30,7 +26,7 @@ pub struct UnderlineElem {
|
||||
/// ```
|
||||
#[resolve]
|
||||
#[fold]
|
||||
pub stroke: Smart<PartialStroke>,
|
||||
pub stroke: Smart<Stroke>,
|
||||
|
||||
/// The position of the line relative to the baseline, read from the font
|
||||
/// tables if `{auto}`.
|
||||
@ -85,19 +81,15 @@ impl Show for UnderlineElem {
|
||||
|
||||
/// Adds a line over text.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// # Example
|
||||
/// ```example
|
||||
/// #overline[A line over text.]
|
||||
/// ```
|
||||
///
|
||||
/// Display: Overline
|
||||
/// Category: text
|
||||
#[element(Show)]
|
||||
#[elem(Show)]
|
||||
pub struct OverlineElem {
|
||||
/// How to stroke the line.
|
||||
/// How to [stroke]($stroke) the line.
|
||||
///
|
||||
/// See the [line's documentation]($func/line.stroke) for more details. If
|
||||
/// set to `{auto}`, takes on the text's color and a thickness defined in
|
||||
/// If set to `{auto}`, takes on the text's color and a thickness defined in
|
||||
/// the current font.
|
||||
///
|
||||
/// ```example
|
||||
@ -110,7 +102,7 @@ pub struct OverlineElem {
|
||||
/// ```
|
||||
#[resolve]
|
||||
#[fold]
|
||||
pub stroke: Smart<PartialStroke>,
|
||||
pub stroke: Smart<Stroke>,
|
||||
|
||||
/// The position of the line relative to the baseline. Read from the font
|
||||
/// tables if `{auto}`.
|
||||
@ -170,23 +162,19 @@ impl Show for OverlineElem {
|
||||
|
||||
/// Strikes through text.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// # Example
|
||||
/// ```example
|
||||
/// This is #strike[not] relevant.
|
||||
/// ```
|
||||
///
|
||||
/// Display: Strikethrough
|
||||
/// Category: text
|
||||
#[element(Show)]
|
||||
#[elem(title = "Strikethrough", Show)]
|
||||
pub struct StrikeElem {
|
||||
/// How to stroke the line.
|
||||
/// How to [stroke]($stroke) the line.
|
||||
///
|
||||
/// See the [line's documentation]($func/line.stroke) for more details. If
|
||||
/// set to `{auto}`, takes on the text's color and a thickness defined in
|
||||
/// If set to `{auto}`, takes on the text's color and a thickness defined in
|
||||
/// the current font.
|
||||
///
|
||||
/// _Note:_ Please don't use this for real redaction as you can still
|
||||
/// copy paste the text.
|
||||
/// _Note:_ Please don't use this for real redaction as you can still copy
|
||||
/// paste the text.
|
||||
///
|
||||
/// ```example
|
||||
/// This is #strike(stroke: 1.5pt + red)[very stricken through]. \
|
||||
@ -194,7 +182,7 @@ pub struct StrikeElem {
|
||||
/// ```
|
||||
#[resolve]
|
||||
#[fold]
|
||||
pub stroke: Smart<PartialStroke>,
|
||||
pub stroke: Smart<Stroke>,
|
||||
|
||||
/// The position of the line relative to the baseline. Read from the font
|
||||
/// tables if `{auto}`.
|
||||
@ -240,14 +228,11 @@ impl Show for StrikeElem {
|
||||
|
||||
/// Highlights text with a background color.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// # Example
|
||||
/// ```example
|
||||
/// This is #highlight[important].
|
||||
/// ```
|
||||
///
|
||||
/// Display: Highlight
|
||||
/// Category: text
|
||||
#[element(Show)]
|
||||
#[elem(Show)]
|
||||
pub struct HighlightElem {
|
||||
/// The color to highlight the text with.
|
||||
/// (Default: 0xffff5f)
|
||||
@ -316,6 +301,7 @@ impl Show for HighlightElem {
|
||||
|
||||
/// Defines a line-based decoration that is positioned over, under or on top of text,
|
||||
/// or highlights the text with a background.
|
||||
#[ty]
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct Decoration {
|
||||
line: DecoLine,
|
||||
@ -332,15 +318,15 @@ impl Fold for Decoration {
|
||||
}
|
||||
|
||||
cast! {
|
||||
type Decoration: "decoration",
|
||||
type Decoration,
|
||||
}
|
||||
|
||||
/// A kind of decorative line.
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||
enum DecoLine {
|
||||
Underline { stroke: PartialStroke<Abs>, offset: Smart<Abs>, evade: bool },
|
||||
Strikethrough { stroke: PartialStroke<Abs>, offset: Smart<Abs> },
|
||||
Overline { stroke: PartialStroke<Abs>, offset: Smart<Abs>, evade: bool },
|
||||
Underline { stroke: Stroke<Abs>, offset: Smart<Abs>, evade: bool },
|
||||
Strikethrough { stroke: Stroke<Abs>, offset: Smart<Abs> },
|
||||
Overline { stroke: Stroke<Abs>, offset: Smart<Abs>, evade: bool },
|
||||
Highlight { fill: Paint, top_edge: TopEdge, bottom_edge: BottomEdge },
|
||||
}
|
||||
|
||||
@ -378,10 +364,10 @@ pub(super) fn decorate(
|
||||
};
|
||||
|
||||
let offset = offset.unwrap_or(-metrics.position.at(text.size)) - shift;
|
||||
let stroke = stroke.clone().unwrap_or(Stroke {
|
||||
let stroke = stroke.clone().unwrap_or(FixedStroke {
|
||||
paint: text.fill.clone(),
|
||||
thickness: metrics.thickness.at(text.size),
|
||||
..Stroke::default()
|
||||
..FixedStroke::default()
|
||||
});
|
||||
|
||||
let gap_padding = 0.08 * text.size;
|
||||
|
@ -2,10 +2,7 @@ use super::TextElem;
|
||||
use crate::prelude::*;
|
||||
|
||||
/// A text space.
|
||||
///
|
||||
/// Display: Space
|
||||
/// Category: text
|
||||
#[element(Behave, Unlabellable, PlainText)]
|
||||
#[elem(Behave, Unlabellable, PlainText)]
|
||||
pub struct SpaceElem {}
|
||||
|
||||
impl Behave for SpaceElem {
|
||||
@ -28,21 +25,18 @@ impl PlainText for SpaceElem {
|
||||
/// end of a paragraph is ignored, but more than one creates additional empty
|
||||
/// lines.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// # Example
|
||||
/// ```example
|
||||
/// *Date:* 26.12.2022 \
|
||||
/// *Topic:* Infrastructure Test \
|
||||
/// *Severity:* High \
|
||||
/// ```
|
||||
///
|
||||
/// ## Syntax { #syntax }
|
||||
/// # Syntax
|
||||
/// This function also has dedicated syntax: To insert a line break, simply write
|
||||
/// a backslash followed by whitespace. This always creates an unjustified
|
||||
/// break.
|
||||
///
|
||||
/// Display: Line Break
|
||||
/// Category: text
|
||||
#[element(Behave)]
|
||||
#[elem(title = "Line Break", Behave)]
|
||||
pub struct LinebreakElem {
|
||||
/// Whether to justify the line before the break.
|
||||
///
|
||||
@ -71,7 +65,7 @@ impl Behave for LinebreakElem {
|
||||
///
|
||||
/// Increases the current font weight by a given `delta`.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// # Example
|
||||
/// ```example
|
||||
/// This is *strong.* \
|
||||
/// This is #strong[too.] \
|
||||
@ -80,15 +74,12 @@ impl Behave for LinebreakElem {
|
||||
/// And this is *evermore.*
|
||||
/// ```
|
||||
///
|
||||
/// ## Syntax { #syntax }
|
||||
/// # Syntax
|
||||
/// This function also has dedicated syntax: To strongly emphasize content,
|
||||
/// simply enclose it in stars/asterisks (`*`). Note that this only works at
|
||||
/// word boundaries. To strongly emphasize part of a word, you have to use the
|
||||
/// function.
|
||||
///
|
||||
/// Display: Strong Emphasis
|
||||
/// Category: text
|
||||
#[element(Show)]
|
||||
#[elem(title = "Strong Emphasis", Show)]
|
||||
pub struct StrongElem {
|
||||
/// The delta to apply on the font weight.
|
||||
///
|
||||
@ -131,12 +122,12 @@ impl Fold for Delta {
|
||||
|
||||
/// Emphasizes content by setting it in italics.
|
||||
///
|
||||
/// - If the current [text style]($func/text.style) is `{"normal"}`,
|
||||
/// this turns it into `{"italic"}`.
|
||||
/// - If it is already `{"italic"}` or `{"oblique"}`,
|
||||
/// it turns it back to `{"normal"}`.
|
||||
/// - If the current [text style]($text.style) is `{"normal"}`, this turns it
|
||||
/// into `{"italic"}`.
|
||||
/// - If it is already `{"italic"}` or `{"oblique"}`, it turns it back to
|
||||
/// `{"normal"}`.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// # Example
|
||||
/// ```example
|
||||
/// This is _emphasized._ \
|
||||
/// This is #emph[too.]
|
||||
@ -148,14 +139,11 @@ impl Fold for Delta {
|
||||
/// This is _emphasized_ differently.
|
||||
/// ```
|
||||
///
|
||||
/// ## Syntax { #syntax }
|
||||
/// # Syntax
|
||||
/// This function also has dedicated syntax: To emphasize content, simply
|
||||
/// enclose it in underscores (`_`). Note that this only works at word
|
||||
/// boundaries. To emphasize part of a word, you have to use the function.
|
||||
///
|
||||
/// Display: Emphasis
|
||||
/// Category: text
|
||||
#[element(Show)]
|
||||
#[elem(title = "Emphasis", Show)]
|
||||
pub struct EmphElem {
|
||||
/// The content to emphasize.
|
||||
#[required]
|
||||
@ -189,16 +177,13 @@ impl Fold for Toggle {
|
||||
|
||||
/// Converts text or content to lowercase.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// # Example
|
||||
/// ```example
|
||||
/// #lower("ABC") \
|
||||
/// #lower[*My Text*] \
|
||||
/// #lower[already low]
|
||||
/// ```
|
||||
///
|
||||
/// Display: Lowercase
|
||||
/// Category: text
|
||||
#[func]
|
||||
#[func(title = "Lowercase")]
|
||||
pub fn lower(
|
||||
/// The text to convert to lowercase.
|
||||
text: Caseable,
|
||||
@ -208,16 +193,13 @@ pub fn lower(
|
||||
|
||||
/// Converts text or content to uppercase.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// # Example
|
||||
/// ```example
|
||||
/// #upper("abc") \
|
||||
/// #upper[*my text*] \
|
||||
/// #upper[ALREADY HIGH]
|
||||
/// ```
|
||||
///
|
||||
/// Display: Uppercase
|
||||
/// Category: text
|
||||
#[func]
|
||||
#[func(title = "Uppercase")]
|
||||
pub fn upper(
|
||||
/// The text to convert to uppercase.
|
||||
text: Caseable,
|
||||
@ -278,7 +260,7 @@ impl Case {
|
||||
/// support selecting a dedicated smallcaps font as well as synthesizing
|
||||
/// smallcaps from normal letters, but this is not yet implemented.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// # Example
|
||||
/// ```example
|
||||
/// #set par(justify: true)
|
||||
/// #set heading(numbering: "I.")
|
||||
@ -292,10 +274,7 @@ impl Case {
|
||||
/// = Introduction
|
||||
/// #lorem(40)
|
||||
/// ```
|
||||
///
|
||||
/// Display: Small Capitals
|
||||
/// Category: text
|
||||
#[func]
|
||||
#[func(title = "Small Capitals")]
|
||||
pub fn smallcaps(
|
||||
/// The text to display to small capitals.
|
||||
body: Content,
|
||||
@ -310,7 +289,7 @@ pub fn smallcaps(
|
||||
/// the same but randomly chosen. As usual for blind texts, it does not make any
|
||||
/// sense. Use it as a placeholder to try layouts.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// # Example
|
||||
/// ```example
|
||||
/// = Blind Text
|
||||
/// #lorem(30)
|
||||
@ -318,10 +297,7 @@ pub fn smallcaps(
|
||||
/// = More Blind Text
|
||||
/// #lorem(15)
|
||||
/// ```
|
||||
///
|
||||
/// Display: Blind Text
|
||||
/// Category: text
|
||||
#[func]
|
||||
#[func(keywords = ["Blind Text"])]
|
||||
pub fn lorem(
|
||||
/// The length of the blind text in words.
|
||||
words: usize,
|
||||
|
@ -23,22 +23,23 @@ use crate::prelude::*;
|
||||
|
||||
/// Hook up all text definitions.
|
||||
pub(super) fn define(global: &mut Scope) {
|
||||
global.define("text", TextElem::func());
|
||||
global.define("linebreak", LinebreakElem::func());
|
||||
global.define("smartquote", SmartQuoteElem::func());
|
||||
global.define("strong", StrongElem::func());
|
||||
global.define("emph", EmphElem::func());
|
||||
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("highlight", HighlightElem::func());
|
||||
global.define("overline", OverlineElem::func());
|
||||
global.define("raw", RawElem::func());
|
||||
global.define("lorem", lorem_func());
|
||||
global.category("text");
|
||||
global.define_elem::<TextElem>();
|
||||
global.define_elem::<LinebreakElem>();
|
||||
global.define_elem::<SmartquoteElem>();
|
||||
global.define_elem::<StrongElem>();
|
||||
global.define_elem::<EmphElem>();
|
||||
global.define_elem::<SubElem>();
|
||||
global.define_elem::<SuperElem>();
|
||||
global.define_elem::<UnderlineElem>();
|
||||
global.define_elem::<OverlineElem>();
|
||||
global.define_elem::<StrikeElem>();
|
||||
global.define_elem::<HighlightElem>();
|
||||
global.define_elem::<RawElem>();
|
||||
global.define_func::<lower>();
|
||||
global.define_func::<upper>();
|
||||
global.define_func::<smallcaps>();
|
||||
global.define_func::<lorem>();
|
||||
}
|
||||
|
||||
/// Customizes the look and layout of text in a variety of ways.
|
||||
@ -47,7 +48,7 @@ pub(super) fn define(global: &mut Scope) {
|
||||
/// the set rule is often the simpler choice, calling the `text` function
|
||||
/// directly can be useful when passing text as an argument to another function.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// # Example
|
||||
/// ```example
|
||||
/// #set text(18pt)
|
||||
/// With a set rule.
|
||||
@ -56,10 +57,7 @@ pub(super) fn define(global: &mut Scope) {
|
||||
/// With a function call.
|
||||
/// ])
|
||||
/// ```
|
||||
///
|
||||
/// Display: Text
|
||||
/// Category: text
|
||||
#[element(Construct, PlainText)]
|
||||
#[elem(Construct, PlainText)]
|
||||
pub struct TextElem {
|
||||
/// A prioritized sequence of font families.
|
||||
///
|
||||
@ -111,8 +109,8 @@ pub struct TextElem {
|
||||
/// italic and oblique style is rarely observable.
|
||||
///
|
||||
/// If you want to emphasize your text, you should do so using the
|
||||
/// [emph]($func/emph) function instead. This makes it easy to adapt the
|
||||
/// style later if you change your mind about how to signify the emphasis.
|
||||
/// [emph]($emph) function instead. This makes it easy to adapt the style
|
||||
/// later if you change your mind about how to signify the emphasis.
|
||||
///
|
||||
/// ```example
|
||||
/// #text(font: "Linux Libertine", style: "italic")[Italic]
|
||||
@ -126,7 +124,7 @@ pub struct TextElem {
|
||||
/// that is closest in weight.
|
||||
///
|
||||
/// If you want to strongly emphasize your text, you should do so using the
|
||||
/// [strong]($func/strong) function instead. This makes it easy to adapt the
|
||||
/// [strong]($strong) function instead. This makes it easy to adapt the
|
||||
/// style later if you change your mind about how to signify the strong
|
||||
/// emphasis.
|
||||
///
|
||||
@ -147,7 +145,7 @@ pub struct TextElem {
|
||||
/// the text if a condensed or expanded version of the font is available.
|
||||
///
|
||||
/// If you want to adjust the amount of space between characters instead of
|
||||
/// stretching the glyphs itself, use the [`tracking`]($func/text.tracking)
|
||||
/// stretching the glyphs itself, use the [`tracking`]($text.tracking)
|
||||
/// property instead.
|
||||
///
|
||||
/// ```example
|
||||
@ -196,7 +194,7 @@ pub struct TextElem {
|
||||
/// the space character in the font.
|
||||
///
|
||||
/// If you want to adjust the amount of space between characters rather than
|
||||
/// words, use the [`tracking`]($func/text.tracking) property instead.
|
||||
/// words, use the [`tracking`]($text.tracking) property instead.
|
||||
///
|
||||
/// ```example
|
||||
/// #set text(spacing: 200%)
|
||||
@ -272,7 +270,7 @@ pub struct TextElem {
|
||||
///
|
||||
/// - The text processing pipeline can make more informed choices.
|
||||
/// - Hyphenation will use the correct patterns for the language.
|
||||
/// - [Smart quotes]($func/smartquote) turns into the correct quotes for the
|
||||
/// - [Smart quotes]($smartquote) turns into the correct quotes for the
|
||||
/// language.
|
||||
/// - And all other things which are language-aware.
|
||||
///
|
||||
@ -327,13 +325,13 @@ pub struct TextElem {
|
||||
/// - `{rtl}`: Layout text from right to left.
|
||||
///
|
||||
/// When writing in right-to-left scripts like Arabic or Hebrew, you should
|
||||
/// set the [text language]($func/text.lang) or direction. While individual
|
||||
/// runs of text are automatically layouted in the correct direction,
|
||||
/// setting the dominant direction gives the bidirectional reordering
|
||||
/// algorithm the necessary information to correctly place punctuation and
|
||||
/// inline objects. Furthermore, setting the direction affects the alignment
|
||||
/// values `start` and `end`, which are equivalent to `left` and `right` in
|
||||
/// `ltr` text and the other way around in `rtl` text.
|
||||
/// set the [text language]($text.lang) or direction. While individual runs
|
||||
/// of text are automatically layouted in the correct direction, setting the
|
||||
/// dominant direction gives the bidirectional reordering algorithm the
|
||||
/// necessary information to correctly place punctuation and inline objects.
|
||||
/// Furthermore, setting the direction affects the alignment values `start`
|
||||
/// and `end`, which are equivalent to `left` and `right` in `ltr` text and
|
||||
/// the other way around in `rtl` text.
|
||||
///
|
||||
/// If you set this to `rtl` and experience bugs or in some way bad looking
|
||||
/// output, please do get in touch with us through the
|
||||
@ -350,7 +348,7 @@ pub struct TextElem {
|
||||
/// Whether to hyphenate text to improve line breaking. When `{auto}`, text
|
||||
/// will be hyphenated if and only if justification is enabled.
|
||||
///
|
||||
/// Setting the [text language]($func/text.lang) ensures that the correct
|
||||
/// Setting the [text language]($text.lang) ensures that the correct
|
||||
/// hyphenation patterns are used.
|
||||
///
|
||||
/// ```example
|
||||
|
@ -5,9 +5,9 @@ use crate::prelude::*;
|
||||
/// A language-aware quote that reacts to its context.
|
||||
///
|
||||
/// Automatically turns into an appropriate opening or closing quote based on
|
||||
/// the active [text language]($func/text.lang).
|
||||
/// the active [text language]($text.lang).
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// # Example
|
||||
/// ```example
|
||||
/// "This is in quotes."
|
||||
///
|
||||
@ -18,14 +18,11 @@ use crate::prelude::*;
|
||||
/// "C'est entre guillemets."
|
||||
/// ```
|
||||
///
|
||||
/// ## Syntax { #syntax }
|
||||
/// # Syntax
|
||||
/// This function also has dedicated syntax: The normal quote characters
|
||||
/// (`'` and `"`). Typst automatically makes your quotes smart.
|
||||
///
|
||||
/// Display: Smart Quote
|
||||
/// Category: text
|
||||
#[element]
|
||||
pub struct SmartQuoteElem {
|
||||
#[elem]
|
||||
pub struct SmartquoteElem {
|
||||
/// Whether this should be a double quote.
|
||||
#[default(true)]
|
||||
pub double: bool,
|
||||
|
@ -12,7 +12,7 @@ use typst::util::option_eq;
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
|
||||
use super::{
|
||||
FontFamily, FontList, Hyphenate, LinebreakElem, SmartQuoteElem, TextElem, TextSize,
|
||||
FontFamily, FontList, Hyphenate, LinebreakElem, SmartquoteElem, TextElem, TextSize,
|
||||
};
|
||||
use crate::layout::BlockElem;
|
||||
use crate::meta::{Figurable, LocalName};
|
||||
@ -23,7 +23,7 @@ use crate::prelude::*;
|
||||
/// Displays the text verbatim and in a monospace font. This is typically used
|
||||
/// to embed computer code into your document.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// # Example
|
||||
/// ````example
|
||||
/// Adding `rbx` to `rcx` gives
|
||||
/// the desired result.
|
||||
@ -43,7 +43,7 @@ use crate::prelude::*;
|
||||
/// also trimmed.
|
||||
/// ````
|
||||
///
|
||||
/// ## Syntax { #syntax }
|
||||
/// # Syntax
|
||||
/// This function also has dedicated syntax. You can enclose text in 1 or 3+
|
||||
/// backticks (`` ` ``) to make it raw. Two backticks produce empty raw text.
|
||||
/// When you use three or more backticks, you can additionally specify a
|
||||
@ -57,10 +57,15 @@ use crate::prelude::*;
|
||||
/// needed, start the text with a single space (which will be trimmed) or use
|
||||
/// the single backtick syntax. If your text should start or end with a
|
||||
/// backtick, put a space before or after it (it will be trimmed).
|
||||
///
|
||||
/// Display: Raw Text / Code
|
||||
/// Category: text
|
||||
#[element(Synthesize, Show, Finalize, LocalName, Figurable, PlainText)]
|
||||
#[elem(
|
||||
title = "Raw Text / Code",
|
||||
Synthesize,
|
||||
Show,
|
||||
Finalize,
|
||||
LocalName,
|
||||
Figurable,
|
||||
PlainText
|
||||
)]
|
||||
pub struct RawElem {
|
||||
/// The raw text.
|
||||
///
|
||||
@ -153,8 +158,8 @@ pub struct RawElem {
|
||||
/// code = "centered"
|
||||
/// ```
|
||||
/// ````
|
||||
#[default(HorizontalAlign(GenAlign::Start))]
|
||||
pub align: HorizontalAlign,
|
||||
#[default(HAlign::Start)]
|
||||
pub align: HAlign,
|
||||
|
||||
/// One or multiple additional syntax definitions to load. The syntax
|
||||
/// definitions should be in the
|
||||
@ -190,10 +195,10 @@ pub struct RawElem {
|
||||
/// Applying a theme only affects the color of specifically highlighted
|
||||
/// text. It does not consider the theme's foreground and background
|
||||
/// properties, so that you retain control over the color of raw text. You
|
||||
/// can apply the foreground color yourself with the [`text`]($func/text)
|
||||
/// function and the background with a [filled block]($func/block.fill). You
|
||||
/// could also use the [`xml`]($func/xml) function to extract these
|
||||
/// properties from the theme.
|
||||
/// can apply the foreground color yourself with the [`text`]($text)
|
||||
/// function and the background with a [filled block]($block.fill). You
|
||||
/// could also use the [`xml`]($xml) function to extract these properties
|
||||
/// from the theme.
|
||||
///
|
||||
/// ````example
|
||||
/// #set raw(theme: "halcyon.tmTheme")
|
||||
@ -340,7 +345,7 @@ impl Show for RawElem {
|
||||
|
||||
if self.block(styles) {
|
||||
// Align the text before inserting it into the block.
|
||||
realized = realized.aligned(Axes::with_x(Some(self.align(styles).into())));
|
||||
realized = realized.aligned(self.align(styles).into());
|
||||
realized = BlockElem::new().with_body(Some(realized)).pack();
|
||||
}
|
||||
|
||||
@ -356,7 +361,7 @@ impl Finalize for RawElem {
|
||||
styles.set(TextElem::set_size(TextSize(Em::new(0.8).into())));
|
||||
styles
|
||||
.set(TextElem::set_font(FontList(vec![FontFamily::new("DejaVu Sans Mono")])));
|
||||
styles.set(SmartQuoteElem::set_enabled(false));
|
||||
styles.set(SmartquoteElem::set_enabled(false));
|
||||
realized.styled_with_map(styles)
|
||||
}
|
||||
}
|
||||
|
@ -5,14 +5,11 @@ use crate::prelude::*;
|
||||
///
|
||||
/// The text is rendered smaller and its baseline is lowered.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// # Example
|
||||
/// ```example
|
||||
/// Revenue#sub[yearly]
|
||||
/// ```
|
||||
///
|
||||
/// Display: Subscript
|
||||
/// Category: text
|
||||
#[element(Show)]
|
||||
#[elem(title = "Subscript", Show)]
|
||||
pub struct SubElem {
|
||||
/// Whether to prefer the dedicated subscript characters of the font.
|
||||
///
|
||||
@ -68,14 +65,11 @@ impl Show for SubElem {
|
||||
///
|
||||
/// The text is rendered smaller and its baseline is raised.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// # Example
|
||||
/// ```example
|
||||
/// 1#super[st] try!
|
||||
/// ```
|
||||
///
|
||||
/// Display: Superscript
|
||||
/// Category: text
|
||||
#[element(Show)]
|
||||
#[elem(title = "Superscript", Show)]
|
||||
pub struct SuperElem {
|
||||
/// Whether to prefer the dedicated superscript characters of the font.
|
||||
///
|
||||
|
@ -18,7 +18,7 @@ use crate::text::families;
|
||||
/// in the resulting PDF. Make sure to double-check embedded SVG images. If you
|
||||
/// have an issue, also feel free to report it on [GitHub][gh-svg].
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// # Example
|
||||
/// ```example
|
||||
/// #figure(
|
||||
/// image("molecular.jpg", width: 80%),
|
||||
@ -30,14 +30,7 @@ use crate::text::families;
|
||||
/// ```
|
||||
///
|
||||
/// [gh-svg]: https://github.com/typst/typst/issues?q=is%3Aopen+is%3Aissue+label%3Asvg
|
||||
///
|
||||
/// Display: Image
|
||||
/// Category: visualize
|
||||
#[element(Layout, LocalName, Figurable)]
|
||||
#[scope(
|
||||
scope.define("decode", image_decode_func());
|
||||
scope
|
||||
)]
|
||||
#[elem(scope, Layout, LocalName, Figurable)]
|
||||
pub struct ImageElem {
|
||||
/// Path to an image file.
|
||||
#[required]
|
||||
@ -73,59 +66,58 @@ pub struct ImageElem {
|
||||
pub fit: ImageFit,
|
||||
}
|
||||
|
||||
/// Decode a raster or vector graphic from bytes or a string.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// ```example
|
||||
/// #let original = read("diagram.svg")
|
||||
/// #let changed = original.replace(
|
||||
/// "#2B80FF", // blue
|
||||
/// green.hex(),
|
||||
/// )
|
||||
///
|
||||
/// #image.decode(original)
|
||||
/// #image.decode(changed)
|
||||
/// ```
|
||||
///
|
||||
/// Display: Decode Image
|
||||
/// Category: visualize
|
||||
#[func]
|
||||
pub fn image_decode(
|
||||
/// The data to decode as an image. Can be a string for SVGs.
|
||||
data: Readable,
|
||||
/// The image's format. Detected automatically by default.
|
||||
#[named]
|
||||
format: Option<Smart<ImageFormat>>,
|
||||
/// The width of the image.
|
||||
#[named]
|
||||
width: Option<Smart<Rel<Length>>>,
|
||||
/// The height of the image.
|
||||
#[named]
|
||||
height: Option<Smart<Rel<Length>>>,
|
||||
/// A text describing the image.
|
||||
#[named]
|
||||
alt: Option<Option<EcoString>>,
|
||||
/// How the image should adjust itself to a given area.
|
||||
#[named]
|
||||
fit: Option<ImageFit>,
|
||||
) -> StrResult<Content> {
|
||||
let mut elem = ImageElem::new(EcoString::new(), data);
|
||||
if let Some(format) = format {
|
||||
elem.push_format(format);
|
||||
#[scope]
|
||||
impl ImageElem {
|
||||
/// Decode a raster or vector graphic from bytes or a string.
|
||||
///
|
||||
/// ```example
|
||||
/// #let original = read("diagram.svg")
|
||||
/// #let changed = original.replace(
|
||||
/// "#2B80FF", // blue
|
||||
/// green.to-hex(),
|
||||
/// )
|
||||
///
|
||||
/// #image.decode(original)
|
||||
/// #image.decode(changed)
|
||||
/// ```
|
||||
#[func(title = "Decode Image")]
|
||||
pub fn decode(
|
||||
/// The data to decode as an image. Can be a string for SVGs.
|
||||
data: Readable,
|
||||
/// The image's format. Detected automatically by default.
|
||||
#[named]
|
||||
format: Option<Smart<ImageFormat>>,
|
||||
/// The width of the image.
|
||||
#[named]
|
||||
width: Option<Smart<Rel<Length>>>,
|
||||
/// The height of the image.
|
||||
#[named]
|
||||
height: Option<Smart<Rel<Length>>>,
|
||||
/// A text describing the image.
|
||||
#[named]
|
||||
alt: Option<Option<EcoString>>,
|
||||
/// How the image should adjust itself to a given area.
|
||||
#[named]
|
||||
fit: Option<ImageFit>,
|
||||
) -> StrResult<Content> {
|
||||
let mut elem = ImageElem::new(EcoString::new(), data);
|
||||
if let Some(format) = format {
|
||||
elem.push_format(format);
|
||||
}
|
||||
if let Some(width) = width {
|
||||
elem.push_width(width);
|
||||
}
|
||||
if let Some(height) = height {
|
||||
elem.push_height(height);
|
||||
}
|
||||
if let Some(alt) = alt {
|
||||
elem.push_alt(alt);
|
||||
}
|
||||
if let Some(fit) = fit {
|
||||
elem.push_fit(fit);
|
||||
}
|
||||
Ok(elem.pack())
|
||||
}
|
||||
if let Some(width) = width {
|
||||
elem.push_width(width);
|
||||
}
|
||||
if let Some(height) = height {
|
||||
elem.push_height(height);
|
||||
}
|
||||
if let Some(alt) = alt {
|
||||
elem.push_alt(alt);
|
||||
}
|
||||
if let Some(fit) = fit {
|
||||
elem.push_fit(fit);
|
||||
}
|
||||
Ok(elem.pack())
|
||||
}
|
||||
|
||||
impl Layout for ImageElem {
|
||||
@ -175,8 +167,7 @@ impl Layout for ImageElem {
|
||||
|
||||
let sizing = Axes::new(self.width(styles), self.height(styles));
|
||||
let region = sizing
|
||||
.zip(regions.base())
|
||||
.map(|(s, r)| s.map(|v| v.resolve(styles).relative_to(r)))
|
||||
.zip_map(regions.base(), |s, r| s.map(|v| v.resolve(styles).relative_to(r)))
|
||||
.unwrap_or(regions.base());
|
||||
|
||||
let expand = sizing.as_ref().map(Smart::is_custom) | regions.expand;
|
||||
@ -217,7 +208,7 @@ impl Layout for ImageElem {
|
||||
// process.
|
||||
let mut frame = Frame::new(fitted);
|
||||
frame.push(Point::zero(), FrameItem::Image(image, fitted, self.span()));
|
||||
frame.resize(target, Align::CENTER_HORIZON);
|
||||
frame.resize(target, Axes::splat(FixedAlign::Center));
|
||||
|
||||
// Create a clipping group if only part of the image should be visible.
|
||||
if fit == ImageFit::Cover && !target.fits(fitted) {
|
||||
|
@ -2,7 +2,7 @@ use crate::prelude::*;
|
||||
|
||||
/// A line from one point to another.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// # Example
|
||||
/// ```example
|
||||
/// #set page(height: 100pt)
|
||||
///
|
||||
@ -13,10 +13,7 @@ use crate::prelude::*;
|
||||
/// stroke: 2pt + maroon,
|
||||
/// )
|
||||
/// ```
|
||||
///
|
||||
/// Display: Line
|
||||
/// Category: visualize
|
||||
#[element(Layout)]
|
||||
#[elem(Layout)]
|
||||
pub struct LineElem {
|
||||
/// The start point of the line.
|
||||
///
|
||||
@ -37,42 +34,7 @@ pub struct LineElem {
|
||||
/// respected if `end` is `none`.
|
||||
pub angle: Angle,
|
||||
|
||||
/// How to stroke the line. This can be:
|
||||
///
|
||||
/// - A length specifying the stroke's thickness. The color is inherited,
|
||||
/// defaulting to black.
|
||||
/// - A color to use for the stroke. The thickness is inherited, defaulting
|
||||
/// to `{1pt}`.
|
||||
/// - A stroke combined from color and thickness using the `+` operator as
|
||||
/// in `{2pt + red}`.
|
||||
/// - A stroke described by a dictionary with any of the following keys:
|
||||
/// - `paint`: The [color]($type/color) to use for the stroke.
|
||||
/// - `thickness`: The stroke's thickness as a [length]($type/length).
|
||||
/// - `cap`: How the line terminates. One of `{"butt"}`, `{"round"}`, or
|
||||
/// `{"square"}`.
|
||||
/// - `join`: How sharp turns of a contour are rendered. One of
|
||||
/// `{"miter"}`, `{"round"}`, or `{"bevel"}`. Not applicable to lines
|
||||
/// but to [polygons]($func/polygon) or [paths]($func/path).
|
||||
/// - `miter-limit`: Number at which protruding sharp angles are rendered
|
||||
/// with a bevel instead. The higher the number, the sharper an angle
|
||||
/// can be before it is bevelled. Only applicable if `join` is
|
||||
/// `{"miter"}`. Defaults to `{4.0}`.
|
||||
/// - `dash`: The dash pattern to use. Can be any of the following:
|
||||
/// - One of the predefined patterns `{"solid"}`, `{"dotted"}`,
|
||||
/// `{"densely-dotted"}`, `{"loosely-dotted"}`, `{"dashed"}`,
|
||||
/// `{"densely-dashed"}`, `{"loosely-dashed"}`, `{"dash-dotted"}`,
|
||||
/// `{"densely-dash-dotted"}` or `{"loosely-dash-dotted"}`
|
||||
/// - An [array]($type/array) with alternating lengths for dashes and
|
||||
/// gaps. You can also use the string `{"dot"}` for a length equal to
|
||||
/// the line thickness.
|
||||
/// - A [dictionary]($type/dictionary) with the keys `array` (same as
|
||||
/// the array above), and `phase` (of type [length]($type/length)),
|
||||
/// which defines where in the pattern to start drawing.
|
||||
///
|
||||
/// On a `stroke` object, you can access any of the fields mentioned in the
|
||||
/// dictionary format above. For example, `{(2pt + blue).thickness}` is
|
||||
/// `{2pt}`, `{(2pt + blue).miter-limit}` is `{4.0}` (the default), and so
|
||||
/// on.
|
||||
/// How to [stroke]($stroke) the line.
|
||||
///
|
||||
/// ```example
|
||||
/// #set line(length: 100%)
|
||||
@ -86,7 +48,7 @@ pub struct LineElem {
|
||||
/// ```
|
||||
#[resolve]
|
||||
#[fold]
|
||||
pub stroke: PartialStroke,
|
||||
pub stroke: Stroke,
|
||||
}
|
||||
|
||||
impl Layout for LineElem {
|
||||
@ -97,10 +59,8 @@ impl Layout for LineElem {
|
||||
styles: StyleChain,
|
||||
regions: Regions,
|
||||
) -> SourceResult<Fragment> {
|
||||
let resolve = |axes: Axes<Rel<Abs>>| {
|
||||
axes.zip(regions.base()).map(|(l, b)| l.relative_to(b))
|
||||
};
|
||||
|
||||
let resolve =
|
||||
|axes: Axes<Rel<Abs>>| axes.zip_map(regions.base(), Rel::relative_to);
|
||||
let start = resolve(self.start(styles));
|
||||
let delta =
|
||||
self.end(styles).map(|end| resolve(end) - start).unwrap_or_else(|| {
|
||||
|
@ -16,30 +16,15 @@ use crate::prelude::*;
|
||||
|
||||
/// Hook up all visualize definitions.
|
||||
pub(super) fn define(global: &mut Scope) {
|
||||
global.define("image", ImageElem::func());
|
||||
global.define("line", LineElem::func());
|
||||
global.define("rect", RectElem::func());
|
||||
global.define("square", SquareElem::func());
|
||||
global.define("ellipse", EllipseElem::func());
|
||||
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);
|
||||
global.category("visualize");
|
||||
global.define_type::<Color>();
|
||||
global.define_type::<Stroke>();
|
||||
global.define_elem::<ImageElem>();
|
||||
global.define_elem::<LineElem>();
|
||||
global.define_elem::<RectElem>();
|
||||
global.define_elem::<SquareElem>();
|
||||
global.define_elem::<EllipseElem>();
|
||||
global.define_elem::<CircleElem>();
|
||||
global.define_elem::<PolygonElem>();
|
||||
global.define_elem::<PathElem>();
|
||||
}
|
||||
|
@ -7,7 +7,7 @@ use PathVertex::{AllControlPoints, MirroredControlPoint, Vertex};
|
||||
|
||||
/// A path through a list of points, connected by Bezier curves.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// # Example
|
||||
/// ```example
|
||||
/// #path(
|
||||
/// fill: blue.lighten(80%),
|
||||
@ -18,26 +18,24 @@ use PathVertex::{AllControlPoints, MirroredControlPoint, Vertex};
|
||||
/// ((50%, 0pt), (40pt, 0pt)),
|
||||
/// )
|
||||
/// ```
|
||||
///
|
||||
/// Display: Path
|
||||
/// Category: visualize
|
||||
#[element(Layout)]
|
||||
#[elem(Layout)]
|
||||
pub struct PathElem {
|
||||
/// How to fill the path. See the
|
||||
/// [rectangle's documentation]($func/rect.fill) for more details.
|
||||
/// How to fill the path.
|
||||
///
|
||||
/// Currently all paths are filled according to the
|
||||
/// [non-zero winding rule](https://en.wikipedia.org/wiki/Nonzero-rule).
|
||||
/// When setting a fill, the default stroke disappears. To create a
|
||||
/// rectangle with both fill and stroke, you have to configure both.
|
||||
///
|
||||
/// Currently all paths are filled according to the [non-zero winding
|
||||
/// rule](https://en.wikipedia.org/wiki/Nonzero-rule).
|
||||
pub fill: Option<Paint>,
|
||||
|
||||
/// How to stroke the path. This can be:
|
||||
/// How to [stroke]($stroke) the path. This can be:
|
||||
///
|
||||
/// See the [line's documentation]($func/line.stroke) for more details. Can
|
||||
/// be set to `{none}` to disable the stroke or to `{auto}` for a stroke of
|
||||
/// `{1pt}` black if and if only if no fill is given.
|
||||
/// Can be set to `{none}` to disable the stroke or to `{auto}` for a
|
||||
/// stroke of `{1pt}` black if and if only if no fill is given.
|
||||
#[resolve]
|
||||
#[fold]
|
||||
pub stroke: Smart<Option<PartialStroke>>,
|
||||
pub stroke: Smart<Option<Stroke>>,
|
||||
|
||||
/// Whether to close this path with one last bezier curve. This curve will
|
||||
/// takes into account the adjacent control points. If you want to close
|
||||
@ -50,8 +48,8 @@ pub struct PathElem {
|
||||
///
|
||||
/// Each vertex can be defined in 3 ways:
|
||||
///
|
||||
/// - A regular point, as given to the [`line`]($func/line) or
|
||||
/// [`polygon`]($func/polygon) function.
|
||||
/// - A regular point, as given to the [`line`]($line) or
|
||||
/// [`polygon`]($polygon) function.
|
||||
/// - An array of two points, the first being the vertex and the second
|
||||
/// being the control point. The control point is expressed relative to
|
||||
/// the vertex and is mirrored to get the second control point. The given
|
||||
@ -60,7 +58,7 @@ pub struct PathElem {
|
||||
/// the curve going out of this vertex.
|
||||
/// - An array of three points, the first being the vertex and the next
|
||||
/// being the control points (control point for curves coming in and out,
|
||||
/// respectively)
|
||||
/// respectively).
|
||||
#[variadic]
|
||||
pub vertices: Vec<PathVertex>,
|
||||
}
|
||||
@ -75,8 +73,7 @@ impl Layout for PathElem {
|
||||
) -> SourceResult<Fragment> {
|
||||
let resolve = |axes: Axes<Rel<Length>>| {
|
||||
axes.resolve(styles)
|
||||
.zip(regions.base())
|
||||
.map(|(l, b)| l.relative_to(b))
|
||||
.zip_map(regions.base(), Rel::relative_to)
|
||||
.to_point()
|
||||
};
|
||||
|
||||
@ -136,9 +133,9 @@ impl Layout for PathElem {
|
||||
// Prepare fill and stroke.
|
||||
let fill = self.fill(styles);
|
||||
let stroke = match self.stroke(styles) {
|
||||
Smart::Auto if fill.is_none() => Some(Stroke::default()),
|
||||
Smart::Auto if fill.is_none() => Some(FixedStroke::default()),
|
||||
Smart::Auto => None,
|
||||
Smart::Custom(stroke) => stroke.map(PartialStroke::unwrap_or_default),
|
||||
Smart::Custom(stroke) => stroke.map(Stroke::unwrap_or_default),
|
||||
};
|
||||
|
||||
let mut frame = Frame::new(size);
|
||||
|
@ -6,7 +6,7 @@ use crate::prelude::*;
|
||||
///
|
||||
/// The polygon is defined by its corner points and is closed automatically.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// # Example
|
||||
/// ```example
|
||||
/// #polygon(
|
||||
/// fill: blue.lighten(80%),
|
||||
@ -17,37 +17,102 @@ use crate::prelude::*;
|
||||
/// (0%, 2cm),
|
||||
/// )
|
||||
/// ```
|
||||
///
|
||||
/// Display: Polygon
|
||||
/// Category: visualize
|
||||
#[element(Layout)]
|
||||
#[scope(
|
||||
scope.define("regular", polygon_regular_func());
|
||||
scope
|
||||
)]
|
||||
#[elem(scope, Layout)]
|
||||
pub struct PolygonElem {
|
||||
/// How to fill the polygon. See the
|
||||
/// [rectangle's documentation]($func/rect.fill) for more details.
|
||||
/// How to fill the polygon.
|
||||
///
|
||||
/// When setting a fill, the default stroke disappears. To create a
|
||||
/// rectangle with both fill and stroke, you have to configure both.
|
||||
///
|
||||
/// Currently all polygons are filled according to the
|
||||
/// [non-zero winding rule](https://en.wikipedia.org/wiki/Nonzero-rule).
|
||||
pub fill: Option<Paint>,
|
||||
|
||||
/// How to stroke the polygon. This can be:
|
||||
/// How to [stroke]($stroke) the polygon. This can be:
|
||||
///
|
||||
/// See the [line's documentation]($func/line.stroke) for more details. Can
|
||||
/// be set to `{none}` to disable the stroke or to `{auto}` for a stroke of
|
||||
/// `{1pt}` black if and if only if no fill is given.
|
||||
/// Can be set to `{none}` to disable the stroke or to `{auto}` for a
|
||||
/// stroke of `{1pt}` black if and if only if no fill is given.
|
||||
#[resolve]
|
||||
#[fold]
|
||||
pub stroke: Smart<Option<PartialStroke>>,
|
||||
pub stroke: Smart<Option<Stroke>>,
|
||||
|
||||
/// The vertices of the polygon. Each point is specified as an array of two
|
||||
/// [relative lengths]($type/relative-length).
|
||||
/// [relative lengths]($relative).
|
||||
#[variadic]
|
||||
pub vertices: Vec<Axes<Rel<Length>>>,
|
||||
}
|
||||
|
||||
#[scope]
|
||||
impl PolygonElem {
|
||||
/// A regular polygon, defined by its size and number of vertices.
|
||||
///
|
||||
/// ```example
|
||||
/// #polygon.regular(
|
||||
/// fill: blue.lighten(80%),
|
||||
/// stroke: blue,
|
||||
/// size: 30pt,
|
||||
/// vertices: 3,
|
||||
/// )
|
||||
/// ```
|
||||
#[func(title = "Regular Polygon")]
|
||||
pub fn regular(
|
||||
/// How to fill the polygon. See the general
|
||||
/// [polygon's documentation]($polygon.fill) for more details.
|
||||
#[named]
|
||||
fill: Option<Option<Paint>>,
|
||||
|
||||
/// How to stroke the polygon. See the general
|
||||
/// [polygon's documentation]($polygon.stroke) for more details.
|
||||
#[named]
|
||||
stroke: Option<Smart<Option<Stroke>>>,
|
||||
|
||||
/// The diameter of the [circumcircle](https://en.wikipedia.org/wiki/Circumcircle)
|
||||
/// of the regular polygon.
|
||||
#[named]
|
||||
#[default(Em::one().into())]
|
||||
size: Length,
|
||||
|
||||
/// The number of vertices in the polygon.
|
||||
#[named]
|
||||
#[default(3)]
|
||||
vertices: u64,
|
||||
) -> Content {
|
||||
let radius = size / 2.0;
|
||||
let angle = |i: f64| {
|
||||
2.0 * PI * i / (vertices as f64) + PI * (1.0 / 2.0 - 1.0 / vertices as f64)
|
||||
};
|
||||
let (horizontal_offset, vertical_offset) = (0..=vertices)
|
||||
.map(|v| {
|
||||
(
|
||||
(radius * angle(v as f64).cos()) + radius,
|
||||
(radius * angle(v as f64).sin()) + radius,
|
||||
)
|
||||
})
|
||||
.fold((radius, radius), |(min_x, min_y), (v_x, v_y)| {
|
||||
(
|
||||
if min_x < v_x { min_x } else { v_x },
|
||||
if min_y < v_y { min_y } else { v_y },
|
||||
)
|
||||
});
|
||||
let vertices = (0..=vertices)
|
||||
.map(|v| {
|
||||
let x = (radius * angle(v as f64).cos()) + radius - horizontal_offset;
|
||||
let y = (radius * angle(v as f64).sin()) + radius - vertical_offset;
|
||||
Axes::new(x, y).map(Rel::from)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut elem = PolygonElem::new(vertices);
|
||||
if let Some(fill) = fill {
|
||||
elem.push_fill(fill);
|
||||
}
|
||||
if let Some(stroke) = stroke {
|
||||
elem.push_stroke(stroke);
|
||||
}
|
||||
elem.pack()
|
||||
}
|
||||
}
|
||||
|
||||
impl Layout for PolygonElem {
|
||||
#[tracing::instrument(name = "PolygonElem::layout", skip_all)]
|
||||
fn layout(
|
||||
@ -60,10 +125,7 @@ impl Layout for PolygonElem {
|
||||
.vertices()
|
||||
.iter()
|
||||
.map(|c| {
|
||||
c.resolve(styles)
|
||||
.zip(regions.base())
|
||||
.map(|(l, b)| l.relative_to(b))
|
||||
.to_point()
|
||||
c.resolve(styles).zip_map(regions.base(), Rel::relative_to).to_point()
|
||||
})
|
||||
.collect();
|
||||
|
||||
@ -78,9 +140,9 @@ impl Layout for PolygonElem {
|
||||
// Prepare fill and stroke.
|
||||
let fill = self.fill(styles);
|
||||
let stroke = match self.stroke(styles) {
|
||||
Smart::Auto if fill.is_none() => Some(Stroke::default()),
|
||||
Smart::Auto if fill.is_none() => Some(FixedStroke::default()),
|
||||
Smart::Auto => None,
|
||||
Smart::Custom(stroke) => stroke.map(PartialStroke::unwrap_or_default),
|
||||
Smart::Custom(stroke) => stroke.map(Stroke::unwrap_or_default),
|
||||
};
|
||||
|
||||
// Construct a closed path given all points.
|
||||
@ -97,71 +159,3 @@ impl Layout for PolygonElem {
|
||||
Ok(Fragment::frame(frame))
|
||||
}
|
||||
}
|
||||
|
||||
/// A regular polygon, defined by its size and number of vertices.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// ```example
|
||||
/// #polygon.regular(
|
||||
/// fill: blue.lighten(80%),
|
||||
/// stroke: blue,
|
||||
/// size: 30pt,
|
||||
/// vertices: 3,
|
||||
/// )
|
||||
/// ```
|
||||
///
|
||||
/// Display: Regular Polygon
|
||||
/// Category: visualize
|
||||
#[func]
|
||||
pub fn polygon_regular(
|
||||
/// How to fill the polygon. See the general
|
||||
/// [polygon's documentation]($func/polygon.fill) for more details.
|
||||
#[named]
|
||||
fill: Option<Option<Paint>>,
|
||||
|
||||
/// How to stroke the polygon. See the general
|
||||
/// [polygon's documentation]($func/polygon.stroke) for more details.
|
||||
#[named]
|
||||
stroke: Option<Smart<Option<PartialStroke>>>,
|
||||
|
||||
/// The diameter of the circumcircle of the regular polygon (https://en.wikipedia.org/wiki/Circumcircle).
|
||||
#[named]
|
||||
#[default(Em::one().into())]
|
||||
size: Length,
|
||||
|
||||
/// The number of vertices in the polygon.
|
||||
#[named]
|
||||
#[default(3)]
|
||||
vertices: u64,
|
||||
) -> Content {
|
||||
let radius = size / 2.0;
|
||||
let angle = |i: f64| {
|
||||
2.0 * PI * i / (vertices as f64) + PI * (1.0 / 2.0 - 1.0 / vertices as f64)
|
||||
};
|
||||
let (horizontal_offset, vertical_offset) = (0..=vertices)
|
||||
.map(|v| {
|
||||
(
|
||||
(radius * angle(v as f64).cos()) + radius,
|
||||
(radius * angle(v as f64).sin()) + radius,
|
||||
)
|
||||
})
|
||||
.fold((radius, radius), |(min_x, min_y), (v_x, v_y)| {
|
||||
(if min_x < v_x { min_x } else { v_x }, if min_y < v_y { min_y } else { v_y })
|
||||
});
|
||||
let vertices = (0..=vertices)
|
||||
.map(|v| {
|
||||
let x = (radius * angle(v as f64).cos()) + radius - horizontal_offset;
|
||||
let y = (radius * angle(v as f64).sin()) + radius - vertical_offset;
|
||||
Axes::new(x, y).map(Rel::from)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut elem = PolygonElem::new(vertices);
|
||||
if let Some(fill) = fill {
|
||||
elem.push_fill(fill);
|
||||
}
|
||||
if let Some(stroke) = stroke {
|
||||
elem.push_stroke(stroke);
|
||||
}
|
||||
elem.pack()
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ use crate::prelude::*;
|
||||
|
||||
/// A rectangle with optional content.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// # Example
|
||||
/// ```example
|
||||
/// // Without content.
|
||||
/// #rect(width: 35%, height: 30pt)
|
||||
@ -15,10 +15,7 @@ use crate::prelude::*;
|
||||
/// to fit the content.
|
||||
/// ]
|
||||
/// ```
|
||||
///
|
||||
/// Display: Rectangle
|
||||
/// Category: visualize
|
||||
#[element(Layout)]
|
||||
#[elem(title = "Rectangle", Layout)]
|
||||
pub struct RectElem {
|
||||
/// The rectangle's width, relative to its parent container.
|
||||
pub width: Smart<Rel<Length>>,
|
||||
@ -41,8 +38,7 @@ pub struct RectElem {
|
||||
/// - `{none}` to disable stroking
|
||||
/// - `{auto}` for a stroke of `{1pt + black}` if and if only if no fill is
|
||||
/// given.
|
||||
/// - Any kind of stroke that can also be used for
|
||||
/// [lines]($func/line.stroke).
|
||||
/// - Any kind of [stroke]($stroke)
|
||||
/// - A dictionary describing the stroke for each side inidvidually. The
|
||||
/// dictionary can contain the following keys in order of precedence:
|
||||
/// - `top`: The top stroke.
|
||||
@ -65,7 +61,7 @@ pub struct RectElem {
|
||||
/// ```
|
||||
#[resolve]
|
||||
#[fold]
|
||||
pub stroke: Smart<Sides<Option<Option<PartialStroke>>>>,
|
||||
pub stroke: Smart<Sides<Option<Option<Stroke>>>>,
|
||||
|
||||
/// How much to round the rectangle's corners, relative to the minimum of
|
||||
/// the width and height divided by two. This can be:
|
||||
@ -106,20 +102,14 @@ pub struct RectElem {
|
||||
pub radius: Corners<Option<Rel<Length>>>,
|
||||
|
||||
/// How much to pad the rectangle's content.
|
||||
///
|
||||
/// _Note:_ When the rectangle contains text, its exact size depends on the
|
||||
/// current [text edges]($func/text.top-edge).
|
||||
///
|
||||
/// ```example
|
||||
/// #rect(inset: 0pt)[Tight]
|
||||
/// ```
|
||||
/// See the [box's documentation]($box.outset) for more details.
|
||||
#[resolve]
|
||||
#[fold]
|
||||
#[default(Sides::splat(Abs::pt(5.0).into()))]
|
||||
pub inset: Sides<Option<Rel<Length>>>,
|
||||
|
||||
/// How much to expand the rectangle's size without affecting the layout.
|
||||
/// See the [box's documentation]($func/box.outset) for more details.
|
||||
/// See the [box's documentation]($box.outset) for more details.
|
||||
#[resolve]
|
||||
#[fold]
|
||||
pub outset: Sides<Option<Rel<Length>>>,
|
||||
@ -159,7 +149,7 @@ impl Layout for RectElem {
|
||||
|
||||
/// A square with optional content.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// # Example
|
||||
/// ```example
|
||||
/// // Without content.
|
||||
/// #square(size: 40pt)
|
||||
@ -170,10 +160,7 @@ impl Layout for RectElem {
|
||||
/// sized to fit.
|
||||
/// ]
|
||||
/// ```
|
||||
///
|
||||
/// Display: Square
|
||||
/// Category: visualize
|
||||
#[element(Layout)]
|
||||
#[elem(Layout)]
|
||||
pub struct SquareElem {
|
||||
/// The square's side length. This is mutually exclusive with `width` and
|
||||
/// `height`.
|
||||
@ -203,31 +190,31 @@ pub struct SquareElem {
|
||||
})]
|
||||
pub height: Smart<Rel<Length>>,
|
||||
|
||||
/// How to fill the square. See the
|
||||
/// [rectangle's documentation]($func/rect.fill) for more details.
|
||||
/// How to fill the square. See the [rectangle's documentation]($rect.fill)
|
||||
/// for more details.
|
||||
pub fill: Option<Paint>,
|
||||
|
||||
/// How to stroke the square. See the [rectangle's
|
||||
/// documentation]($func/rect.stroke) for more details.
|
||||
/// How to stroke the square. See the
|
||||
/// [rectangle's documentation]($rect.stroke) for more details.
|
||||
#[resolve]
|
||||
#[fold]
|
||||
pub stroke: Smart<Sides<Option<Option<PartialStroke>>>>,
|
||||
pub stroke: Smart<Sides<Option<Option<Stroke>>>>,
|
||||
|
||||
/// How much to round the square's corners. See the [rectangle's
|
||||
/// documentation]($func/rect.radius) for more details.
|
||||
/// How much to round the square's corners. See the
|
||||
/// [rectangle's documentation]($rect.radius) for more details.
|
||||
#[resolve]
|
||||
#[fold]
|
||||
pub radius: Corners<Option<Rel<Length>>>,
|
||||
|
||||
/// How much to pad the square's content. See the [rectangle's
|
||||
/// documentation]($func/rect.inset) for more details.
|
||||
/// How much to pad the square's content. See the
|
||||
/// [box's documentation]($box.inset) for more details.
|
||||
#[resolve]
|
||||
#[fold]
|
||||
#[default(Sides::splat(Abs::pt(5.0).into()))]
|
||||
pub inset: Sides<Option<Rel<Length>>>,
|
||||
|
||||
/// How much to expand the square's size without affecting the layout. See
|
||||
/// the [rectangle's documentation]($func/rect.outset) for more details.
|
||||
/// the [box's documentation]($box.outset) for more details.
|
||||
#[resolve]
|
||||
#[fold]
|
||||
pub outset: Sides<Option<Rel<Length>>>,
|
||||
@ -268,7 +255,7 @@ impl Layout for SquareElem {
|
||||
|
||||
/// An ellipse with optional content.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// # Example
|
||||
/// ```example
|
||||
/// // Without content.
|
||||
/// #ellipse(width: 35%, height: 30pt)
|
||||
@ -280,10 +267,7 @@ impl Layout for SquareElem {
|
||||
/// to fit the content.
|
||||
/// ]
|
||||
/// ```
|
||||
///
|
||||
/// Display: Ellipse
|
||||
/// Category: visualize
|
||||
#[element(Layout)]
|
||||
#[elem(Layout)]
|
||||
pub struct EllipseElem {
|
||||
/// The ellipse's width, relative to its parent container.
|
||||
pub width: Smart<Rel<Length>>,
|
||||
@ -291,25 +275,25 @@ pub struct EllipseElem {
|
||||
/// The ellipse's height, relative to its parent container.
|
||||
pub height: Smart<Rel<Length>>,
|
||||
|
||||
/// How to fill the ellipse. See the
|
||||
/// [rectangle's documentation]($func/rect.fill) for more details.
|
||||
/// How to fill the ellipse. See the [rectangle's documentation]($rect.fill)
|
||||
/// for more details.
|
||||
pub fill: Option<Paint>,
|
||||
|
||||
/// How to stroke the ellipse. See the [rectangle's
|
||||
/// documentation]($func/rect.stroke) for more details.
|
||||
/// How to stroke the ellipse. See the
|
||||
/// [rectangle's documentation]($rect.stroke) for more details.
|
||||
#[resolve]
|
||||
#[fold]
|
||||
pub stroke: Smart<Option<PartialStroke>>,
|
||||
pub stroke: Smart<Option<Stroke>>,
|
||||
|
||||
/// How much to pad the ellipse's content. See the [rectangle's
|
||||
/// documentation]($func/rect.inset) for more details.
|
||||
/// How much to pad the ellipse's content. See the
|
||||
/// [box's documentation]($box.inset) for more details.
|
||||
#[resolve]
|
||||
#[fold]
|
||||
#[default(Sides::splat(Abs::pt(5.0).into()))]
|
||||
pub inset: Sides<Option<Rel<Length>>>,
|
||||
|
||||
/// How much to expand the ellipse's size without affecting the layout. See
|
||||
/// the [rectangle's documentation]($func/rect.outset) for more details.
|
||||
/// the [box's documentation]($box.outset) for more details.
|
||||
#[resolve]
|
||||
#[fold]
|
||||
pub outset: Sides<Option<Rel<Length>>>,
|
||||
@ -349,7 +333,7 @@ impl Layout for EllipseElem {
|
||||
|
||||
/// A circle with optional content.
|
||||
///
|
||||
/// ## Example { #example }
|
||||
/// # Example
|
||||
/// ```example
|
||||
/// // Without content.
|
||||
/// #circle(radius: 25pt)
|
||||
@ -361,10 +345,7 @@ impl Layout for EllipseElem {
|
||||
/// sized to fit.
|
||||
/// ]
|
||||
/// ```
|
||||
///
|
||||
/// Display: Circle
|
||||
/// Category: visualize
|
||||
#[element(Layout)]
|
||||
#[elem(Layout)]
|
||||
pub struct CircleElem {
|
||||
/// The circle's radius. This is mutually exclusive with `width` and
|
||||
/// `height`.
|
||||
@ -398,26 +379,26 @@ pub struct CircleElem {
|
||||
})]
|
||||
pub height: Smart<Rel<Length>>,
|
||||
|
||||
/// How to fill the circle. See the
|
||||
/// [rectangle's documentation]($func/rect.fill) for more details.
|
||||
/// How to fill the circle. See the [rectangle's documentation]($rect.fill)
|
||||
/// for more details.
|
||||
pub fill: Option<Paint>,
|
||||
|
||||
/// How to stroke the circle. See the [rectangle's
|
||||
/// documentation]($func/rect.stroke) for more details.
|
||||
/// How to stroke the circle. See the
|
||||
/// [rectangle's documentation]($rect.stroke) for more details.
|
||||
#[resolve]
|
||||
#[fold]
|
||||
#[default(Smart::Auto)]
|
||||
pub stroke: Smart<Option<PartialStroke>>,
|
||||
pub stroke: Smart<Option<Stroke>>,
|
||||
|
||||
/// How much to pad the circle's content. See the [rectangle's
|
||||
/// documentation]($func/rect.inset) for more details.
|
||||
/// How much to pad the circle's content. See the
|
||||
/// [box's documentation]($box.inset) for more details.
|
||||
#[resolve]
|
||||
#[fold]
|
||||
#[default(Sides::splat(Abs::pt(5.0).into()))]
|
||||
pub inset: Sides<Option<Rel<Length>>>,
|
||||
|
||||
/// How much to expand the circle's size without affecting the layout. See
|
||||
/// the [rectangle's documentation]($func/rect.outset) for more details.
|
||||
/// the [box's documentation]($box.outset) for more details.
|
||||
#[resolve]
|
||||
#[fold]
|
||||
pub outset: Sides<Option<Rel<Length>>>,
|
||||
@ -464,15 +445,14 @@ fn layout(
|
||||
body: &Option<Content>,
|
||||
sizing: Axes<Smart<Rel<Length>>>,
|
||||
fill: Option<Paint>,
|
||||
stroke: Smart<Sides<Option<PartialStroke<Abs>>>>,
|
||||
stroke: Smart<Sides<Option<Stroke<Abs>>>>,
|
||||
mut inset: Sides<Rel<Abs>>,
|
||||
outset: Sides<Rel<Abs>>,
|
||||
radius: Corners<Rel<Abs>>,
|
||||
span: Span,
|
||||
) -> SourceResult<Fragment> {
|
||||
let resolved = sizing
|
||||
.zip(regions.base())
|
||||
.map(|(s, r)| s.map(|v| v.resolve(styles).relative_to(r)));
|
||||
.zip_map(regions.base(), |s, r| s.map(|v| v.resolve(styles).relative_to(r)));
|
||||
|
||||
let mut frame;
|
||||
if let Some(child) = body {
|
||||
@ -517,11 +497,9 @@ fn layout(
|
||||
|
||||
// Prepare stroke.
|
||||
let stroke = match stroke {
|
||||
Smart::Auto if fill.is_none() => Sides::splat(Some(Stroke::default())),
|
||||
Smart::Auto if fill.is_none() => Sides::splat(Some(FixedStroke::default())),
|
||||
Smart::Auto => Sides::splat(None),
|
||||
Smart::Custom(strokes) => {
|
||||
strokes.map(|s| s.map(PartialStroke::unwrap_or_default))
|
||||
}
|
||||
Smart::Custom(strokes) => strokes.map(|s| s.map(Stroke::unwrap_or_default)),
|
||||
};
|
||||
|
||||
// Add fill and/or stroke.
|
||||
|
@ -17,7 +17,7 @@ bench = false
|
||||
|
||||
[dependencies]
|
||||
comemo = "0.3"
|
||||
ecow = { version = "0.1.1", features = ["serde"] }
|
||||
ecow = { version = "0.1.2", features = ["serde"] }
|
||||
once_cell = "1"
|
||||
serde = { version = "1.0.184", features = ["derive"] }
|
||||
tracing = "0.1.37"
|
||||
|
@ -22,7 +22,7 @@ base64 = "0.21.2"
|
||||
bitflags = { version = "2", features = ["serde"] }
|
||||
bytemuck = "1"
|
||||
comemo = "0.3"
|
||||
ecow = { version = "0.1.1", features = ["serde"] }
|
||||
ecow = { version = "0.1.2", features = ["serde"] }
|
||||
flate2 = "1"
|
||||
fontdb = "0.13"
|
||||
if_chain = "1"
|
||||
|
@ -41,7 +41,10 @@ macro_rules! __bail {
|
||||
}
|
||||
|
||||
#[doc(inline)]
|
||||
pub use crate::__bail as bail;
|
||||
pub use crate::{__bail as bail, __error as error, __warning as warning};
|
||||
|
||||
#[doc(hidden)]
|
||||
pub use ecow::{eco_format, EcoString};
|
||||
|
||||
/// Construct an [`EcoString`] or [`SourceDiagnostic`] with severity `Error`.
|
||||
#[macro_export]
|
||||
@ -71,13 +74,6 @@ macro_rules! __warning {
|
||||
};
|
||||
}
|
||||
|
||||
#[doc(inline)]
|
||||
pub use crate::__error as error;
|
||||
#[doc(inline)]
|
||||
pub use crate::__warning as warning;
|
||||
#[doc(hidden)]
|
||||
pub use ecow::{eco_format, EcoString};
|
||||
|
||||
/// A result that can carry multiple source errors.
|
||||
pub type SourceResult<T> = Result<T, Box<Vec<SourceDiagnostic>>>;
|
||||
|
||||
|
@ -8,11 +8,12 @@ use std::sync::Arc;
|
||||
|
||||
use ecow::EcoString;
|
||||
|
||||
use crate::eval::{cast, dict, Dict, Value};
|
||||
use crate::eval::{cast, dict, ty, Dict, Value};
|
||||
use crate::font::Font;
|
||||
use crate::geom::{
|
||||
self, rounded_rect, Abs, Align, Axes, Color, Corners, Dir, Em, Geometry, Length,
|
||||
Numeric, Paint, Point, Rel, RgbaColor, Shape, Sides, Size, Stroke, Transform,
|
||||
self, rounded_rect, Abs, Axes, Color, Corners, Dir, Em, FixedAlign, FixedStroke,
|
||||
Geometry, Length, Numeric, Paint, Point, Rel, RgbaColor, Shape, Sides, Size,
|
||||
Transform,
|
||||
};
|
||||
use crate::image::Image;
|
||||
use crate::model::{Content, Location, MetaElem, StyleChain};
|
||||
@ -231,14 +232,11 @@ impl Frame {
|
||||
|
||||
/// Resize the frame to a new size, distributing new space according to the
|
||||
/// given alignments.
|
||||
pub fn resize(&mut self, target: Size, aligns: Axes<Align>) {
|
||||
pub fn resize(&mut self, target: Size, align: Axes<FixedAlign>) {
|
||||
if self.size != target {
|
||||
let offset = Point::new(
|
||||
aligns.x.position(target.x - self.size.x),
|
||||
aligns.y.position(target.y - self.size.y),
|
||||
);
|
||||
let offset = align.zip_map(target - self.size, FixedAlign::position);
|
||||
self.size = target;
|
||||
self.translate(offset);
|
||||
self.translate(offset.to_point());
|
||||
}
|
||||
}
|
||||
|
||||
@ -290,7 +288,7 @@ impl Frame {
|
||||
pub fn fill_and_stroke(
|
||||
&mut self,
|
||||
fill: Option<Paint>,
|
||||
stroke: Sides<Option<Stroke>>,
|
||||
stroke: Sides<Option<FixedStroke>>,
|
||||
outset: Sides<Rel<Abs>>,
|
||||
radius: Corners<Rel<Abs>>,
|
||||
span: Span,
|
||||
@ -357,10 +355,10 @@ impl Frame {
|
||||
1,
|
||||
Point::with_y(self.baseline()),
|
||||
FrameItem::Shape(
|
||||
Geometry::Line(Point::with_x(self.size.x)).stroked(Stroke {
|
||||
Geometry::Line(Point::with_x(self.size.x)).stroked(FixedStroke {
|
||||
paint: Color::RED.into(),
|
||||
thickness: Abs::pt(1.0),
|
||||
..Stroke::default()
|
||||
..FixedStroke::default()
|
||||
}),
|
||||
Span::detached(),
|
||||
),
|
||||
@ -384,10 +382,10 @@ impl Frame {
|
||||
self.push(
|
||||
Point::with_y(y),
|
||||
FrameItem::Shape(
|
||||
Geometry::Line(Point::with_x(self.size.x)).stroked(Stroke {
|
||||
Geometry::Line(Point::with_x(self.size.x)).stroked(FixedStroke {
|
||||
paint: Color::GREEN.into(),
|
||||
thickness: Abs::pt(1.0),
|
||||
..Stroke::default()
|
||||
..FixedStroke::default()
|
||||
}),
|
||||
Span::detached(),
|
||||
),
|
||||
@ -660,6 +658,7 @@ cast! {
|
||||
}
|
||||
|
||||
/// Meta information that isn't visible or renderable.
|
||||
#[ty]
|
||||
#[derive(Clone, PartialEq, Hash)]
|
||||
pub enum Meta {
|
||||
/// An internal or external link to a destination.
|
||||
@ -676,7 +675,7 @@ pub enum Meta {
|
||||
}
|
||||
|
||||
cast! {
|
||||
type Meta: "meta",
|
||||
type Meta,
|
||||
}
|
||||
|
||||
impl Debug for Meta {
|
||||
|
@ -2,12 +2,42 @@ use std::fmt::{self, Debug, Formatter};
|
||||
|
||||
use ecow::{eco_format, EcoVec};
|
||||
|
||||
use super::{Array, Dict, FromValue, IntoValue, Str, Value};
|
||||
use crate::diag::{bail, At, SourceResult};
|
||||
use super::{func, scope, ty, Array, Dict, FromValue, IntoValue, Str, Value};
|
||||
use crate::diag::{bail, At, SourceDiagnostic, SourceResult};
|
||||
use crate::syntax::{Span, Spanned};
|
||||
use crate::util::pretty_array_like;
|
||||
|
||||
/// Evaluated arguments to a function.
|
||||
/// Captured arguments to a function.
|
||||
///
|
||||
/// # Argument Sinks
|
||||
/// Like built-in functions, custom functions can also take a variable number of
|
||||
/// arguments. You can specify an _argument sink_ which collects all excess
|
||||
/// arguments as `..sink`. The resulting `sink` value is of the `arguments`
|
||||
/// type. It exposes methods to access the positional and named arguments.
|
||||
///
|
||||
/// ```example
|
||||
/// #let format(title, ..authors) = {
|
||||
/// let by = authors
|
||||
/// .pos()
|
||||
/// .join(", ", last: " and ")
|
||||
///
|
||||
/// [*#title* \ _Written by #by;_]
|
||||
/// }
|
||||
///
|
||||
/// #format("ArtosFlow", "Jane", "Joe")
|
||||
/// ```
|
||||
///
|
||||
/// # Spreading
|
||||
/// Inversely to an argument sink, you can _spread_ arguments, arrays and
|
||||
/// dictionaries into a function call with the `..spread` operator:
|
||||
///
|
||||
/// ```example
|
||||
/// #let array = (2, 3, 5)
|
||||
/// #calc.min(..array)
|
||||
/// #let dict = (fill: blue)
|
||||
/// #text(..dict)[Hello]
|
||||
/// ```
|
||||
#[ty(scope, name = "arguments")]
|
||||
#[derive(Clone, PartialEq, Hash)]
|
||||
pub struct Args {
|
||||
/// The span of the whole argument list.
|
||||
@ -125,8 +155,21 @@ impl Args {
|
||||
T: FromValue<Spanned<Value>>,
|
||||
{
|
||||
let mut list = vec![];
|
||||
while let Some(value) = self.find()? {
|
||||
list.push(value);
|
||||
let mut errors = vec![];
|
||||
self.items.retain(|item| {
|
||||
if item.name.is_some() {
|
||||
return true;
|
||||
};
|
||||
let span = item.value.span;
|
||||
let spanned = Spanned::new(std::mem::take(&mut item.value.v), span);
|
||||
match T::from_value(spanned) {
|
||||
Ok(val) => list.push(val),
|
||||
Err(err) => errors.push(SourceDiagnostic::error(span, err)),
|
||||
}
|
||||
false
|
||||
});
|
||||
if !errors.is_empty() {
|
||||
return Err(Box::new(errors));
|
||||
}
|
||||
Ok(list)
|
||||
}
|
||||
@ -183,8 +226,12 @@ impl Args {
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract the positional arguments as an array.
|
||||
#[scope]
|
||||
impl Args {
|
||||
/// Returns the captured positional arguments as an array.
|
||||
#[func(name = "pos", title = "Positional")]
|
||||
pub fn to_pos(&self) -> Array {
|
||||
self.items
|
||||
.iter()
|
||||
@ -193,7 +240,8 @@ impl Args {
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Extract the named arguments as a dictionary.
|
||||
/// Returns the captured named arguments as a dictionary.
|
||||
#[func(name = "named")]
|
||||
pub fn to_named(&self) -> Dict {
|
||||
self.items
|
||||
.iter()
|
||||
|
@ -1,11 +1,15 @@
|
||||
use std::cmp::Ordering;
|
||||
use std::fmt::{self, Debug, Formatter};
|
||||
use std::num::NonZeroI64;
|
||||
use std::ops::{Add, AddAssign};
|
||||
|
||||
use ecow::{eco_format, EcoString, EcoVec};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::{ops, Args, CastInfo, FromValue, Func, IntoValue, Reflect, Value, Vm};
|
||||
use super::{
|
||||
cast, func, ops, scope, ty, Args, Bytes, CastInfo, FromValue, Func, IntoValue,
|
||||
Reflect, Value, Vm,
|
||||
};
|
||||
use crate::diag::{At, SourceResult, StrResult};
|
||||
use crate::eval::ops::{add, mul};
|
||||
use crate::syntax::Span;
|
||||
@ -31,10 +35,41 @@ macro_rules! __array {
|
||||
|
||||
#[doc(inline)]
|
||||
pub use crate::__array as array;
|
||||
|
||||
#[doc(hidden)]
|
||||
pub use ecow::eco_vec;
|
||||
|
||||
/// A reference counted array with value semantics.
|
||||
/// A sequence of values.
|
||||
///
|
||||
/// You can construct an array by enclosing a comma-separated sequence of values
|
||||
/// in parentheses. The values do not have to be of the same type.
|
||||
///
|
||||
/// You can access and update array items with the `.at()` method. Indices are
|
||||
/// zero-based and negative indices wrap around to the end of the array. You can
|
||||
/// iterate over an array using a [for loop]($scripting/#loops). Arrays can be
|
||||
/// added together with the `+` operator, [joined together]($scripting/#blocks)
|
||||
/// and multiplied with integers.
|
||||
///
|
||||
/// **Note:** An array of length one needs a trailing comma, as in `{(1,)}`.
|
||||
/// This is to disambiguate from a simple parenthesized expressions like `{(1 +
|
||||
/// 2) * 3}`. An empty array is written as `{()}`.
|
||||
///
|
||||
/// # Example
|
||||
/// ```example
|
||||
/// #let values = (1, 7, 4, -3, 2)
|
||||
///
|
||||
/// #values.at(0) \
|
||||
/// #(values.at(0) = 3)
|
||||
/// #values.at(-1) \
|
||||
/// #values.find(calc.even) \
|
||||
/// #values.filter(calc.odd) \
|
||||
/// #values.map(calc.abs) \
|
||||
/// #values.rev() \
|
||||
/// #(1, (2, 3)).flatten() \
|
||||
/// #(("A", "B", "C")
|
||||
/// .join(", ", last: " and "))
|
||||
/// ```
|
||||
#[ty(scope)]
|
||||
#[derive(Default, Clone, PartialEq, Hash, Serialize, Deserialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct Array(EcoVec<Value>);
|
||||
@ -52,17 +87,17 @@ impl Array {
|
||||
|
||||
/// Return `true` if the length is 0.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.0.len() == 0
|
||||
self.0.is_empty()
|
||||
}
|
||||
|
||||
/// The length of the array.
|
||||
pub fn len(&self) -> usize {
|
||||
self.0.len()
|
||||
/// Extract a slice of the whole array.
|
||||
pub fn as_slice(&self) -> &[Value] {
|
||||
self.0.as_slice()
|
||||
}
|
||||
|
||||
/// The first value in the array.
|
||||
pub fn first(&self) -> StrResult<&Value> {
|
||||
self.0.first().ok_or_else(array_is_empty)
|
||||
/// Iterate over references to the contained values.
|
||||
pub fn iter(&self) -> std::slice::Iter<Value> {
|
||||
self.0.iter()
|
||||
}
|
||||
|
||||
/// Mutably borrow the first value in the array.
|
||||
@ -70,24 +105,11 @@ impl Array {
|
||||
self.0.make_mut().first_mut().ok_or_else(array_is_empty)
|
||||
}
|
||||
|
||||
/// The last value in the array.
|
||||
pub fn last(&self) -> StrResult<&Value> {
|
||||
self.0.last().ok_or_else(array_is_empty)
|
||||
}
|
||||
|
||||
/// Mutably borrow the last value in the array.
|
||||
pub fn last_mut(&mut self) -> StrResult<&mut Value> {
|
||||
self.0.make_mut().last_mut().ok_or_else(array_is_empty)
|
||||
}
|
||||
|
||||
/// Borrow the value at the given index.
|
||||
pub fn at(&self, index: i64, default: Option<Value>) -> StrResult<Value> {
|
||||
self.locate_opt(index, false)
|
||||
.and_then(|i| self.0.get(i).cloned())
|
||||
.or(default)
|
||||
.ok_or_else(|| out_of_bounds_no_default(index, self.len()))
|
||||
}
|
||||
|
||||
/// Mutably borrow the value at the given index.
|
||||
pub fn at_mut(&mut self, index: i64) -> StrResult<&mut Value> {
|
||||
let len = self.len();
|
||||
@ -96,57 +118,210 @@ impl Array {
|
||||
.ok_or_else(|| out_of_bounds_no_default(index, len))
|
||||
}
|
||||
|
||||
/// Push a value to the end of the array.
|
||||
pub fn push(&mut self, value: Value) {
|
||||
/// Resolve an index or throw an out of bounds error.
|
||||
fn locate(&self, index: i64, end_ok: bool) -> StrResult<usize> {
|
||||
self.locate_opt(index, end_ok)
|
||||
.ok_or_else(|| out_of_bounds(index, self.len()))
|
||||
}
|
||||
|
||||
/// Resolve an index, if it is within bounds.
|
||||
///
|
||||
/// `index == len` is considered in bounds if and only if `end_ok` is true.
|
||||
fn locate_opt(&self, index: i64, end_ok: bool) -> Option<usize> {
|
||||
let wrapped =
|
||||
if index >= 0 { Some(index) } else { (self.len() as i64).checked_add(index) };
|
||||
|
||||
wrapped
|
||||
.and_then(|v| usize::try_from(v).ok())
|
||||
.filter(|&v| v < self.0.len() + end_ok as usize)
|
||||
}
|
||||
|
||||
/// Repeat this array `n` times.
|
||||
pub fn repeat(&self, n: usize) -> StrResult<Self> {
|
||||
let count = self
|
||||
.len()
|
||||
.checked_mul(n)
|
||||
.ok_or_else(|| format!("cannot repeat this array {} times", n))?;
|
||||
|
||||
Ok(self.iter().cloned().cycle().take(count).collect())
|
||||
}
|
||||
}
|
||||
|
||||
#[scope]
|
||||
impl Array {
|
||||
/// Converts a value to an array.
|
||||
///
|
||||
/// Note that this function is only intended for conversion of a collection-like
|
||||
/// value to an array, not for creation of an array from individual items. Use
|
||||
/// the array syntax `(1, 2, 3)` (or `(1,)` for a single-element array) instead.
|
||||
///
|
||||
/// ```example
|
||||
/// #let hi = "Hello 😃"
|
||||
/// #array(bytes(hi))
|
||||
/// ```
|
||||
#[func(constructor)]
|
||||
pub fn construct(
|
||||
/// The value that should be converted to an array.
|
||||
value: ToArray,
|
||||
) -> Array {
|
||||
value.0
|
||||
}
|
||||
|
||||
/// The number of values in the array.
|
||||
#[func(title = "Length")]
|
||||
pub fn len(&self) -> usize {
|
||||
self.0.len()
|
||||
}
|
||||
|
||||
/// Returns the first item in the array. May be used on the left-hand side
|
||||
/// of an assignment. Fails with an error if the array is empty.
|
||||
#[func]
|
||||
pub fn first(&self) -> StrResult<Value> {
|
||||
self.0.first().cloned().ok_or_else(array_is_empty)
|
||||
}
|
||||
|
||||
/// Returns the last item in the array. May be used on the left-hand side of
|
||||
/// an assignment. Fails with an error if the array is empty.
|
||||
#[func]
|
||||
pub fn last(&self) -> StrResult<Value> {
|
||||
self.0.last().cloned().ok_or_else(array_is_empty)
|
||||
}
|
||||
|
||||
/// Returns the item at the specified index in the array. May be used on the
|
||||
/// left-hand side of an assignment. Returns the default value if the index
|
||||
/// is out of bounds or fails with an error if no default value was
|
||||
/// specified.
|
||||
#[func]
|
||||
pub fn at(
|
||||
&self,
|
||||
/// The index at which to retrieve the item. If negative, indexes from
|
||||
/// the back.
|
||||
index: i64,
|
||||
/// A default value to return if the index is out of bounds.
|
||||
#[named]
|
||||
default: Option<Value>,
|
||||
) -> StrResult<Value> {
|
||||
self.locate_opt(index, false)
|
||||
.and_then(|i| self.0.get(i).cloned())
|
||||
.or(default)
|
||||
.ok_or_else(|| out_of_bounds_no_default(index, self.len()))
|
||||
}
|
||||
|
||||
/// Add a value to the end of the array.
|
||||
#[func]
|
||||
pub fn push(
|
||||
&mut self,
|
||||
/// The value to insert at the end of the array.
|
||||
value: Value,
|
||||
) {
|
||||
self.0.push(value);
|
||||
}
|
||||
|
||||
/// Remove the last value in the array.
|
||||
/// Remove the last item from the array and return it. Fails with an error
|
||||
/// if the array is empty.
|
||||
#[func]
|
||||
pub fn pop(&mut self) -> StrResult<Value> {
|
||||
self.0.pop().ok_or_else(array_is_empty)
|
||||
}
|
||||
|
||||
/// Insert a value at the specified index.
|
||||
pub fn insert(&mut self, index: i64, value: Value) -> StrResult<()> {
|
||||
/// Insert a value into the array at the specified index. Fails with an
|
||||
/// error if the index is out of bounds.
|
||||
#[func]
|
||||
pub fn insert(
|
||||
&mut self,
|
||||
/// The index at which to insert the item. If negative, indexes from
|
||||
/// the back.
|
||||
index: i64,
|
||||
/// The value to insert into the array.
|
||||
value: Value,
|
||||
) -> StrResult<()> {
|
||||
let i = self.locate(index, true)?;
|
||||
self.0.insert(i, value);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Remove and return the value at the specified index.
|
||||
pub fn remove(&mut self, index: i64) -> StrResult<Value> {
|
||||
/// Remove the value at the specified index from the array and return it.
|
||||
#[func]
|
||||
pub fn remove(
|
||||
&mut self,
|
||||
/// The index at which to remove the item. If negative, indexes from
|
||||
/// the back.
|
||||
index: i64,
|
||||
) -> StrResult<Value> {
|
||||
let i = self.locate(index, false)?;
|
||||
Ok(self.0.remove(i))
|
||||
}
|
||||
|
||||
/// Extract a contiguous subregion of the array.
|
||||
pub fn slice(&self, start: i64, end: Option<i64>) -> StrResult<Self> {
|
||||
/// Extract a subslice of the array. Fails with an error if the start or
|
||||
/// index is out of bounds.
|
||||
#[func]
|
||||
pub fn slice(
|
||||
&self,
|
||||
/// The start index (inclusive). If negative, indexes from the back.
|
||||
start: i64,
|
||||
/// The end index (exclusive). If omitted, the whole slice until the end
|
||||
/// of the array is extracted. If negative, indexes from the back.
|
||||
#[default]
|
||||
end: Option<i64>,
|
||||
/// The number of items to extract. This is equivalent to passing
|
||||
/// `start + count` as the `end` position. Mutually exclusive with `end`.
|
||||
#[named]
|
||||
count: Option<i64>,
|
||||
) -> StrResult<Array> {
|
||||
let mut end = end;
|
||||
if end.is_none() {
|
||||
end = count.map(|c: i64| start + c);
|
||||
}
|
||||
let start = self.locate(start, true)?;
|
||||
let end = self.locate(end.unwrap_or(self.len() as i64), true)?.max(start);
|
||||
Ok(self.0[start..end].into())
|
||||
}
|
||||
|
||||
/// Whether the array contains a specific value.
|
||||
pub fn contains(&self, value: &Value) -> bool {
|
||||
self.0.contains(value)
|
||||
/// Whether the array contains the specified value.
|
||||
///
|
||||
/// This method also has dedicated syntax: You can write `{2 in (1, 2, 3)}`
|
||||
/// instead of `{(1, 2, 3).contains(2)}`.
|
||||
#[func]
|
||||
pub fn contains(
|
||||
&self,
|
||||
/// The value to search for.
|
||||
value: Value,
|
||||
) -> bool {
|
||||
self.0.contains(&value)
|
||||
}
|
||||
|
||||
/// Return the first matching item.
|
||||
pub fn find(&self, vm: &mut Vm, func: Func) -> SourceResult<Option<Value>> {
|
||||
/// Searches for an item for which the given function returns `{true}` and
|
||||
/// returns the first match or `{none}` if there is no match.
|
||||
#[func]
|
||||
pub fn find(
|
||||
&self,
|
||||
/// The virtual machine.
|
||||
vm: &mut Vm,
|
||||
/// The function to apply to each item. Must return a boolean.
|
||||
searcher: Func,
|
||||
) -> SourceResult<Option<Value>> {
|
||||
for item in self.iter() {
|
||||
let args = Args::new(func.span(), [item.clone()]);
|
||||
if func.call_vm(vm, args)?.cast::<bool>().at(func.span())? {
|
||||
let args = Args::new(searcher.span(), [item.clone()]);
|
||||
if searcher.call_vm(vm, args)?.cast::<bool>().at(searcher.span())? {
|
||||
return Ok(Some(item.clone()));
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Return the index of the first matching item.
|
||||
pub fn position(&self, vm: &mut Vm, func: Func) -> SourceResult<Option<i64>> {
|
||||
/// Searches for an item for which the given function returns `{true}` and
|
||||
/// returns the index of the first match or `{none}` if there is no match.
|
||||
#[func]
|
||||
pub fn position(
|
||||
&self,
|
||||
/// The virtual machine.
|
||||
vm: &mut Vm,
|
||||
/// The function to apply to each item. Must return a boolean.
|
||||
searcher: Func,
|
||||
) -> SourceResult<Option<i64>> {
|
||||
for (i, item) in self.iter().enumerate() {
|
||||
let args = Args::new(func.span(), [item.clone()]);
|
||||
if func.call_vm(vm, args)?.cast::<bool>().at(func.span())? {
|
||||
let args = Args::new(searcher.span(), [item.clone()]);
|
||||
if searcher.call_vm(vm, args)?.cast::<bool>().at(searcher.span())? {
|
||||
return Ok(Some(i as i64));
|
||||
}
|
||||
}
|
||||
@ -154,180 +329,153 @@ impl Array {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Return a new array with only those items for which the function returns
|
||||
/// true.
|
||||
pub fn filter(&self, vm: &mut Vm, func: Func) -> SourceResult<Self> {
|
||||
/// Create an array consisting of a sequence of numbers.
|
||||
///
|
||||
/// If you pass just one positional parameter, it is interpreted as the
|
||||
/// `end` of the range. If you pass two, they describe the `start` and `end`
|
||||
/// of the range.
|
||||
///
|
||||
/// This function is available both in the array function's scope and
|
||||
/// globally.
|
||||
///
|
||||
/// ```example
|
||||
/// #range(5) \
|
||||
/// #range(2, 5) \
|
||||
/// #range(20, step: 4) \
|
||||
/// #range(21, step: 4) \
|
||||
/// #range(5, 2, step: -1)
|
||||
/// ```
|
||||
#[func]
|
||||
pub fn range(
|
||||
/// The real arguments (the other arguments are just for the docs, this
|
||||
/// function is a bit involved, so we parse the arguments manually).
|
||||
args: Args,
|
||||
/// The start of the range (inclusive).
|
||||
#[external]
|
||||
#[default]
|
||||
start: i64,
|
||||
/// The end of the range (exclusive).
|
||||
#[external]
|
||||
end: i64,
|
||||
/// The distance between the generated numbers.
|
||||
#[named]
|
||||
#[default(NonZeroI64::new(1).unwrap())]
|
||||
step: NonZeroI64,
|
||||
) -> SourceResult<Array> {
|
||||
let mut args = args;
|
||||
let first = args.expect::<i64>("end")?;
|
||||
let (start, end) = match args.eat::<i64>()? {
|
||||
Some(second) => (first, second),
|
||||
None => (0, first),
|
||||
};
|
||||
args.finish()?;
|
||||
|
||||
let step = step.get();
|
||||
|
||||
let mut x = start;
|
||||
let mut array = Self::new();
|
||||
|
||||
while x.cmp(&end) == 0.cmp(&step) {
|
||||
array.push(x.into_value());
|
||||
x += step;
|
||||
}
|
||||
|
||||
Ok(array)
|
||||
}
|
||||
|
||||
/// Produces a new array with only the items from the original one for which
|
||||
/// the given function returns true.
|
||||
#[func]
|
||||
pub fn filter(
|
||||
&self,
|
||||
/// The virtual machine.
|
||||
vm: &mut Vm,
|
||||
/// The function to apply to each item. Must return a boolean.
|
||||
test: Func,
|
||||
) -> SourceResult<Array> {
|
||||
let mut kept = EcoVec::new();
|
||||
for item in self.iter() {
|
||||
let args = Args::new(func.span(), [item.clone()]);
|
||||
if func.call_vm(vm, args)?.cast::<bool>().at(func.span())? {
|
||||
let args = Args::new(test.span(), [item.clone()]);
|
||||
if test.call_vm(vm, args)?.cast::<bool>().at(test.span())? {
|
||||
kept.push(item.clone())
|
||||
}
|
||||
}
|
||||
Ok(kept.into())
|
||||
}
|
||||
|
||||
/// Transform each item in the array with a function.
|
||||
pub fn map(&self, vm: &mut Vm, func: Func) -> SourceResult<Self> {
|
||||
/// Produces a new array in which all items from the original one were
|
||||
/// transformed with the given function.
|
||||
#[func]
|
||||
pub fn map(
|
||||
&self,
|
||||
/// The virtual machine.
|
||||
vm: &mut Vm,
|
||||
/// The function to apply to each item.
|
||||
mapper: Func,
|
||||
) -> SourceResult<Array> {
|
||||
self.iter()
|
||||
.map(|item| {
|
||||
let args = Args::new(func.span(), [item.clone()]);
|
||||
func.call_vm(vm, args)
|
||||
let args = Args::new(mapper.span(), [item.clone()]);
|
||||
mapper.call_vm(vm, args)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Fold all of the array's items into one with a function.
|
||||
pub fn fold(&self, vm: &mut Vm, init: Value, func: Func) -> SourceResult<Value> {
|
||||
let mut acc = init;
|
||||
for item in self.iter() {
|
||||
let args = Args::new(func.span(), [acc, item.clone()]);
|
||||
acc = func.call_vm(vm, args)?;
|
||||
}
|
||||
Ok(acc)
|
||||
}
|
||||
|
||||
/// Calculates the sum of the array's items
|
||||
pub fn sum(&self, default: Option<Value>, span: Span) -> SourceResult<Value> {
|
||||
let mut acc = self
|
||||
.first()
|
||||
.map(|x| x.clone())
|
||||
.or_else(|_| {
|
||||
default.ok_or_else(|| {
|
||||
eco_format!("cannot calculate sum of empty array with no default")
|
||||
})
|
||||
/// Returns a new array with the values alongside their indices.
|
||||
///
|
||||
/// The returned array consists of `(index, value)` pairs in the form of
|
||||
/// length-2 arrays. These can be [destructured]($scripting/#bindings) with
|
||||
/// a let binding or for loop.
|
||||
#[func]
|
||||
pub fn enumerate(
|
||||
&self,
|
||||
/// The index returned for the first pair of the returned list.
|
||||
#[named]
|
||||
#[default(0)]
|
||||
start: i64,
|
||||
) -> StrResult<Array> {
|
||||
self.iter()
|
||||
.enumerate()
|
||||
.map(|(i, value)| {
|
||||
Ok(array![
|
||||
start
|
||||
.checked_add_unsigned(i as u64)
|
||||
.ok_or("array index is too large")?,
|
||||
value.clone()
|
||||
]
|
||||
.into_value())
|
||||
})
|
||||
.at(span)?;
|
||||
for i in self.iter().skip(1) {
|
||||
acc = add(acc, i.clone()).at(span)?;
|
||||
}
|
||||
Ok(acc)
|
||||
}
|
||||
|
||||
/// Calculates the product of the array's items
|
||||
pub fn product(&self, default: Option<Value>, span: Span) -> SourceResult<Value> {
|
||||
let mut acc = self
|
||||
.first()
|
||||
.map(|x| x.clone())
|
||||
.or_else(|_| {
|
||||
default.ok_or_else(|| {
|
||||
eco_format!("cannot calculate product of empty array with no default")
|
||||
})
|
||||
})
|
||||
.at(span)?;
|
||||
for i in self.iter().skip(1) {
|
||||
acc = mul(acc, i.clone()).at(span)?;
|
||||
}
|
||||
Ok(acc)
|
||||
}
|
||||
|
||||
/// Whether any item matches.
|
||||
pub fn any(&self, vm: &mut Vm, func: Func) -> SourceResult<bool> {
|
||||
for item in self.iter() {
|
||||
let args = Args::new(func.span(), [item.clone()]);
|
||||
if func.call_vm(vm, args)?.cast::<bool>().at(func.span())? {
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
/// Whether all items match.
|
||||
pub fn all(&self, vm: &mut Vm, func: Func) -> SourceResult<bool> {
|
||||
for item in self.iter() {
|
||||
let args = Args::new(func.span(), [item.clone()]);
|
||||
if !func.call_vm(vm, args)?.cast::<bool>().at(func.span())? {
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
/// Return a new array with all items from this and nested arrays.
|
||||
pub fn flatten(&self) -> Self {
|
||||
let mut flat = EcoVec::with_capacity(self.0.len());
|
||||
for item in self.iter() {
|
||||
if let Value::Array(nested) = item {
|
||||
flat.extend(nested.flatten().into_iter());
|
||||
} else {
|
||||
flat.push(item.clone());
|
||||
}
|
||||
}
|
||||
flat.into()
|
||||
}
|
||||
|
||||
/// Returns a new array with reversed order.
|
||||
pub fn rev(&self) -> Self {
|
||||
self.0.iter().cloned().rev().collect()
|
||||
}
|
||||
|
||||
/// Split all values in the array.
|
||||
pub fn split(&self, at: Value) -> Array {
|
||||
self.as_slice()
|
||||
.split(|value| *value == at)
|
||||
.map(|subslice| Value::Array(subslice.iter().cloned().collect()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Join all values in the array, optionally with separator and last
|
||||
/// separator (between the final two items).
|
||||
pub fn join(&self, sep: Option<Value>, mut last: Option<Value>) -> StrResult<Value> {
|
||||
let len = self.0.len();
|
||||
let sep = sep.unwrap_or(Value::None);
|
||||
|
||||
let mut result = Value::None;
|
||||
for (i, value) in self.iter().cloned().enumerate() {
|
||||
if i > 0 {
|
||||
if i + 1 == len && last.is_some() {
|
||||
result = ops::join(result, last.take().unwrap())?;
|
||||
} else {
|
||||
result = ops::join(result, sep.clone())?;
|
||||
}
|
||||
}
|
||||
|
||||
result = ops::join(result, value)?;
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Returns an array with a copy of the separator value placed between
|
||||
/// adjacent elements.
|
||||
pub fn intersperse(&self, sep: Value) -> Array {
|
||||
// TODO: Use https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.intersperse
|
||||
// once it is stabilized.
|
||||
let size = match self.len() {
|
||||
0 => return Array::new(),
|
||||
n => (2 * n) - 1,
|
||||
};
|
||||
let mut vec = EcoVec::with_capacity(size);
|
||||
let mut iter = self.iter().cloned();
|
||||
|
||||
if let Some(first) = iter.next() {
|
||||
vec.push(first);
|
||||
}
|
||||
|
||||
for value in iter {
|
||||
vec.push(sep.clone());
|
||||
vec.push(value);
|
||||
}
|
||||
|
||||
Array(vec)
|
||||
}
|
||||
|
||||
/// The method `array.zip`, depending on the arguments, it automatically
|
||||
/// detects whether it should use the single zip operator, which depends
|
||||
/// on the standard library's implementation and can therefore be faster.
|
||||
/// Or it zips using a manual implementation which allows for zipping more
|
||||
/// than two arrays at once.
|
||||
pub fn zip(&self, args: &mut Args) -> SourceResult<Self> {
|
||||
/// Zips the array with other arrays. If the arrays are of unequal length,
|
||||
/// it will only zip up until the last element of the shortest array and the
|
||||
/// remaining elements will be ignored. The return value is an array where
|
||||
/// each element is yet another array, the size of each of those is the
|
||||
/// number of zipped arrays.
|
||||
///
|
||||
/// This function is variadic, meaning that you can zip multiple arrays
|
||||
/// together at once: `{(1, 2, 3).zip((3, 4, 5), (6, 7, 8))}` yields
|
||||
/// `{((1, 3, 6), (2, 4, 7), (3, 5, 8))}`.
|
||||
#[func]
|
||||
pub fn zip(
|
||||
&self,
|
||||
/// The real arguments (the other arguments are just for the docs, this
|
||||
/// function is a bit involved, so we parse the arguments manually).
|
||||
args: Args,
|
||||
/// The arrays to zip with.
|
||||
#[external]
|
||||
#[variadic]
|
||||
others: Vec<Array>,
|
||||
) -> SourceResult<Array> {
|
||||
// Fast path for just two arrays.
|
||||
let mut args = args;
|
||||
if args.remaining() <= 1 {
|
||||
let other = args.expect::<Array>("others")?;
|
||||
args.finish()?;
|
||||
return Ok(self
|
||||
.iter()
|
||||
.zip(args.expect::<Array>("others")?)
|
||||
.zip(other)
|
||||
.map(|(first, second)| array![first.clone(), second].into_value())
|
||||
.collect());
|
||||
}
|
||||
@ -339,6 +487,7 @@ impl Array {
|
||||
.into_iter()
|
||||
.map(|i| i.into_iter())
|
||||
.collect::<Vec<_>>();
|
||||
args.finish()?;
|
||||
|
||||
for this in self.iter() {
|
||||
let mut row = Self::with_capacity(1 + iterators.len());
|
||||
@ -358,16 +507,213 @@ impl Array {
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Return a sorted version of this array, optionally by a given key function.
|
||||
/// Folds all items into a single value using an accumulator function.
|
||||
#[func]
|
||||
pub fn fold(
|
||||
&self,
|
||||
/// The virtual machine.
|
||||
vm: &mut Vm,
|
||||
/// The initial value to start with.
|
||||
init: Value,
|
||||
/// The folding function. Must have two parameters: One for the
|
||||
/// accumulated value and one for an item.
|
||||
folder: Func,
|
||||
) -> SourceResult<Value> {
|
||||
let mut acc = init;
|
||||
for item in self.iter() {
|
||||
let args = Args::new(folder.span(), [acc, item.clone()]);
|
||||
acc = folder.call_vm(vm, args)?;
|
||||
}
|
||||
Ok(acc)
|
||||
}
|
||||
|
||||
/// Sums all items (works for all types that can be added).
|
||||
#[func]
|
||||
pub fn sum(
|
||||
&self,
|
||||
/// What to return if the array is empty. Must be set if the array can
|
||||
/// be empty.
|
||||
#[named]
|
||||
default: Option<Value>,
|
||||
) -> StrResult<Value> {
|
||||
let mut acc = self
|
||||
.0
|
||||
.first()
|
||||
.cloned()
|
||||
.or(default)
|
||||
.ok_or("cannot calculate sum of empty array with no default")?;
|
||||
for i in self.iter().skip(1) {
|
||||
acc = add(acc, i.clone())?;
|
||||
}
|
||||
Ok(acc)
|
||||
}
|
||||
|
||||
/// Calculates the product all items (works for all types that can be
|
||||
/// multiplied).
|
||||
#[func]
|
||||
pub fn product(
|
||||
&self,
|
||||
/// What to return if the array is empty. Must be set if the array can
|
||||
/// be empty.
|
||||
#[named]
|
||||
default: Option<Value>,
|
||||
) -> StrResult<Value> {
|
||||
let mut acc = self
|
||||
.0
|
||||
.first()
|
||||
.cloned()
|
||||
.or(default)
|
||||
.ok_or("cannot calculate product of empty array with no default")?;
|
||||
for i in self.iter().skip(1) {
|
||||
acc = mul(acc, i.clone())?;
|
||||
}
|
||||
Ok(acc)
|
||||
}
|
||||
|
||||
/// Whether the given function returns `{true}` for any item in the array.
|
||||
#[func]
|
||||
pub fn any(
|
||||
&self,
|
||||
/// The virtual machine.
|
||||
vm: &mut Vm,
|
||||
/// The function to apply to each item. Must return a boolean.
|
||||
test: Func,
|
||||
) -> SourceResult<bool> {
|
||||
for item in self.iter() {
|
||||
let args = Args::new(test.span(), [item.clone()]);
|
||||
if test.call_vm(vm, args)?.cast::<bool>().at(test.span())? {
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
/// Whether the given function returns `{true}` for all items in the array.
|
||||
#[func]
|
||||
pub fn all(
|
||||
&self,
|
||||
/// The virtual machine.
|
||||
vm: &mut Vm,
|
||||
/// The function to apply to each item. Must return a boolean.
|
||||
test: Func,
|
||||
) -> SourceResult<bool> {
|
||||
for item in self.iter() {
|
||||
let args = Args::new(test.span(), [item.clone()]);
|
||||
if !test.call_vm(vm, args)?.cast::<bool>().at(test.span())? {
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
/// Combine all nested arrays into a single flat one.
|
||||
#[func]
|
||||
pub fn flatten(&self) -> Array {
|
||||
let mut flat = EcoVec::with_capacity(self.0.len());
|
||||
for item in self.iter() {
|
||||
if let Value::Array(nested) = item {
|
||||
flat.extend(nested.flatten().into_iter());
|
||||
} else {
|
||||
flat.push(item.clone());
|
||||
}
|
||||
}
|
||||
flat.into()
|
||||
}
|
||||
|
||||
/// Return a new array with the same items, but in reverse order.
|
||||
#[func(title = "Reverse")]
|
||||
pub fn rev(&self) -> Array {
|
||||
self.0.iter().cloned().rev().collect()
|
||||
}
|
||||
|
||||
/// Split the array at occurrences of the specified value.
|
||||
#[func]
|
||||
pub fn split(
|
||||
&self,
|
||||
/// The value to split at.
|
||||
at: Value,
|
||||
) -> Array {
|
||||
self.as_slice()
|
||||
.split(|value| *value == at)
|
||||
.map(|subslice| Value::Array(subslice.iter().cloned().collect()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Combine all items in the array into one.
|
||||
#[func]
|
||||
pub fn join(
|
||||
&self,
|
||||
/// A value to insert between each item of the array.
|
||||
#[default]
|
||||
separator: Option<Value>,
|
||||
/// An alternative separator between the last two items
|
||||
#[named]
|
||||
last: Option<Value>,
|
||||
) -> StrResult<Value> {
|
||||
let len = self.0.len();
|
||||
let separator = separator.unwrap_or(Value::None);
|
||||
|
||||
let mut last = last;
|
||||
let mut result = Value::None;
|
||||
for (i, value) in self.iter().cloned().enumerate() {
|
||||
if i > 0 {
|
||||
if i + 1 == len && last.is_some() {
|
||||
result = ops::join(result, last.take().unwrap())?;
|
||||
} else {
|
||||
result = ops::join(result, separator.clone())?;
|
||||
}
|
||||
}
|
||||
|
||||
result = ops::join(result, value)?;
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Returns an array with a copy of the separator value placed between
|
||||
/// adjacent elements.
|
||||
#[func]
|
||||
pub fn intersperse(&self, sep: Value) -> Array {
|
||||
// TODO: Use once stabilized:
|
||||
// https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.intersperse
|
||||
let size = match self.len() {
|
||||
0 => return Array::new(),
|
||||
n => (2 * n) - 1,
|
||||
};
|
||||
let mut vec = EcoVec::with_capacity(size);
|
||||
let mut iter = self.iter().cloned();
|
||||
|
||||
if let Some(first) = iter.next() {
|
||||
vec.push(first);
|
||||
}
|
||||
|
||||
for value in iter {
|
||||
vec.push(sep.clone());
|
||||
vec.push(value);
|
||||
}
|
||||
|
||||
Array(vec)
|
||||
}
|
||||
|
||||
/// Return a sorted version of this array, optionally by a given key
|
||||
/// function.
|
||||
///
|
||||
/// Returns an error if two values could not be compared or if the key function (if given)
|
||||
/// yields an error.
|
||||
/// Returns an error if two values could not be compared or if the key
|
||||
/// function (if given) yields an error.
|
||||
#[func]
|
||||
pub fn sorted(
|
||||
&self,
|
||||
/// The virtual machine.
|
||||
vm: &mut Vm,
|
||||
/// The callsite span.
|
||||
span: Span,
|
||||
/// If given, applies this function to the elements in the array to
|
||||
/// determine the keys to sort by.
|
||||
#[named]
|
||||
key: Option<Func>,
|
||||
) -> SourceResult<Self> {
|
||||
) -> SourceResult<Array> {
|
||||
let mut result = Ok(());
|
||||
let mut vec = self.0.clone();
|
||||
let mut key_of = |x: Value| match &key {
|
||||
@ -398,34 +744,24 @@ impl Array {
|
||||
result.map(|_| vec.into())
|
||||
}
|
||||
|
||||
/// Repeat this array `n` times.
|
||||
pub fn repeat(&self, n: i64) -> StrResult<Self> {
|
||||
let count = usize::try_from(n)
|
||||
.ok()
|
||||
.and_then(|n| self.0.len().checked_mul(n))
|
||||
.ok_or_else(|| format!("cannot repeat this array {} times", n))?;
|
||||
|
||||
Ok(self.iter().cloned().cycle().take(count).collect())
|
||||
}
|
||||
|
||||
/// Enumerate all items in the array.
|
||||
pub fn enumerate(&self, start: i64) -> StrResult<Self> {
|
||||
self.iter()
|
||||
.enumerate()
|
||||
.map(|(i, value)| {
|
||||
Ok(array![
|
||||
start
|
||||
.checked_add_unsigned(i as u64)
|
||||
.ok_or_else(|| "array index is too large".to_string())?,
|
||||
value.clone()
|
||||
]
|
||||
.into_value())
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Deduplicates all items in the array.
|
||||
pub fn dedup(&self, vm: &mut Vm, key: Option<Func>) -> SourceResult<Self> {
|
||||
///
|
||||
/// Returns a new array with all duplicate items removed. Only the first
|
||||
/// element of each duplicate is kept.
|
||||
///
|
||||
/// ```example
|
||||
/// #(1, 1, 2, 3, 1).dedup()
|
||||
/// ```
|
||||
#[func(title = "Deduplicate")]
|
||||
pub fn dedup(
|
||||
&self,
|
||||
/// The virtual machine.
|
||||
vm: &mut Vm,
|
||||
/// If given, applies this function to the elements in the array to
|
||||
/// determine the keys to deduplicate by.
|
||||
#[named]
|
||||
key: Option<Func>,
|
||||
) -> SourceResult<Array> {
|
||||
let mut out = EcoVec::with_capacity(self.0.len());
|
||||
let mut key_of = |x: Value| match &key {
|
||||
// NOTE: We are relying on `comemo`'s memoization of function
|
||||
@ -455,34 +791,15 @@ impl Array {
|
||||
|
||||
Ok(Self(out))
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract a slice of the whole array.
|
||||
pub fn as_slice(&self) -> &[Value] {
|
||||
self.0.as_slice()
|
||||
}
|
||||
/// A value that can be cast to bytes.
|
||||
pub struct ToArray(Array);
|
||||
|
||||
/// Iterate over references to the contained values.
|
||||
pub fn iter(&self) -> std::slice::Iter<Value> {
|
||||
self.0.iter()
|
||||
}
|
||||
|
||||
/// Resolve an index or throw an out of bounds error.
|
||||
fn locate(&self, index: i64, end_ok: bool) -> StrResult<usize> {
|
||||
self.locate_opt(index, end_ok)
|
||||
.ok_or_else(|| out_of_bounds(index, self.len()))
|
||||
}
|
||||
|
||||
/// Resolve an index, if it is within bounds.
|
||||
///
|
||||
/// `index == len` is considered in bounds if and only if `end_ok` is true.
|
||||
fn locate_opt(&self, index: i64, end_ok: bool) -> Option<usize> {
|
||||
let wrapped =
|
||||
if index >= 0 { Some(index) } else { (self.len() as i64).checked_add(index) };
|
||||
|
||||
wrapped
|
||||
.and_then(|v| usize::try_from(v).ok())
|
||||
.filter(|&v| v < self.0.len() + end_ok as usize)
|
||||
}
|
||||
cast! {
|
||||
ToArray,
|
||||
v: Bytes => Self(v.iter().map(|&b| Value::Int(b.into())).collect()),
|
||||
v: Array => Self(v),
|
||||
}
|
||||
|
||||
impl Debug for Array {
|
||||
@ -555,8 +872,12 @@ impl From<&[Value]> for Array {
|
||||
}
|
||||
|
||||
impl<T> Reflect for Vec<T> {
|
||||
fn describe() -> CastInfo {
|
||||
Array::describe()
|
||||
fn input() -> CastInfo {
|
||||
Array::input()
|
||||
}
|
||||
|
||||
fn output() -> CastInfo {
|
||||
Array::output()
|
||||
}
|
||||
|
||||
fn castable(value: &Value) -> bool {
|
||||
|
@ -1,9 +1,17 @@
|
||||
use std::fmt::{self, Debug, Formatter};
|
||||
|
||||
use super::{CastInfo, FromValue, IntoValue, Reflect, Value};
|
||||
use super::{ty, CastInfo, FromValue, IntoValue, Reflect, Type, Value};
|
||||
use crate::diag::StrResult;
|
||||
|
||||
/// A value that indicates a smart default.
|
||||
///
|
||||
/// The auto type has exactly one value: `{auto}`.
|
||||
///
|
||||
/// Parameters that support the `{auto}` value have some smart default or
|
||||
/// contextual behaviour. A good example is the [text direction]($text.dir)
|
||||
/// parameter. Setting it to `{auto}` lets Typst automatically determine the
|
||||
/// direction from the [text language]($text.lang).
|
||||
#[ty(name = "auto")]
|
||||
#[derive(Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
|
||||
pub struct AutoValue;
|
||||
|
||||
@ -23,8 +31,12 @@ impl FromValue for AutoValue {
|
||||
}
|
||||
|
||||
impl Reflect for AutoValue {
|
||||
fn describe() -> CastInfo {
|
||||
CastInfo::Type("auto")
|
||||
fn input() -> CastInfo {
|
||||
CastInfo::Type(Type::of::<Self>())
|
||||
}
|
||||
|
||||
fn output() -> CastInfo {
|
||||
CastInfo::Type(Type::of::<Self>())
|
||||
}
|
||||
|
||||
fn castable(value: &Value) -> bool {
|
||||
|
15
crates/typst/src/eval/bool.rs
Normal file
15
crates/typst/src/eval/bool.rs
Normal file
@ -0,0 +1,15 @@
|
||||
use super::ty;
|
||||
|
||||
/// A type with two states.
|
||||
///
|
||||
/// The boolean type has two values: `{true}` and `{false}`. It denotes whether
|
||||
/// something is active or enabled.
|
||||
///
|
||||
/// # Example
|
||||
/// ```example
|
||||
/// #false \
|
||||
/// #true \
|
||||
/// #(1 < 2)
|
||||
/// ```
|
||||
#[ty(title = "Boolean")]
|
||||
type bool;
|
@ -7,11 +7,37 @@ use comemo::Prehashed;
|
||||
use ecow::{eco_format, EcoString};
|
||||
use serde::{Serialize, Serializer};
|
||||
|
||||
use crate::diag::StrResult;
|
||||
use super::{cast, func, scope, ty, Array, Reflect, Str, Value};
|
||||
use crate::diag::{bail, StrResult};
|
||||
|
||||
use super::Value;
|
||||
|
||||
/// A shared byte buffer that is cheap to clone and hash.
|
||||
/// A sequence of bytes.
|
||||
///
|
||||
/// This is conceptually similar to an array of [integers]($int) between `{0}`
|
||||
/// and `{255}`, but represented much more efficiently.
|
||||
///
|
||||
/// You can convert
|
||||
/// - a [string]($str) or an [array]($array) of integers to bytes with the
|
||||
/// [`bytes`]($bytes) constructor
|
||||
/// - bytes to a string with the [`str`]($str) constructor
|
||||
/// - bytes to an array of integers with the [`array`]($array) constructor
|
||||
///
|
||||
/// When [reading]($read) data from a file, you can decide whether to load it
|
||||
/// as a string or as raw bytes.
|
||||
///
|
||||
/// ```example
|
||||
/// #bytes((123, 160, 22, 0)) \
|
||||
/// #bytes("Hello 😃")
|
||||
///
|
||||
/// #let data = read(
|
||||
/// "rhino.png",
|
||||
/// encoding: none,
|
||||
/// )
|
||||
///
|
||||
/// // Magic bytes.
|
||||
/// #array(data.slice(0, 4)) \
|
||||
/// #str(data.slice(1, 4))
|
||||
/// ```
|
||||
#[ty(scope)]
|
||||
#[derive(Clone, Hash, Eq, PartialEq)]
|
||||
pub struct Bytes(Arc<Prehashed<Cow<'static, [u8]>>>);
|
||||
|
||||
@ -21,19 +47,9 @@ impl Bytes {
|
||||
Self(Arc::new(Prehashed::new(Cow::Borrowed(slice))))
|
||||
}
|
||||
|
||||
/// Get the byte at the given index.
|
||||
pub fn at(&self, index: i64, default: Option<Value>) -> StrResult<Value> {
|
||||
self.locate_opt(index)
|
||||
.and_then(|i| self.0.get(i).map(|&b| Value::Int(b as i64)))
|
||||
.or(default)
|
||||
.ok_or_else(|| out_of_bounds_no_default(index, self.len()))
|
||||
}
|
||||
|
||||
/// Extract a contiguous subregion of the bytes.
|
||||
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() as i64))?.max(start);
|
||||
Ok(self.0[start..end].into())
|
||||
/// Return `true` if the length is 0.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.0.is_empty()
|
||||
}
|
||||
|
||||
/// Return a view into the buffer.
|
||||
@ -64,6 +80,79 @@ impl Bytes {
|
||||
}
|
||||
}
|
||||
|
||||
#[scope]
|
||||
impl Bytes {
|
||||
/// Converts a value to bytes.
|
||||
///
|
||||
/// - Strings are encoded in UTF-8.
|
||||
/// - Arrays of integers between `{0}` and `{255}` are converted directly. The
|
||||
/// dedicated byte representation is much more efficient than the array
|
||||
/// representation and thus typically used for large byte buffers (e.g. image
|
||||
/// data).
|
||||
///
|
||||
/// ```example
|
||||
/// #bytes("Hello 😃") \
|
||||
/// #bytes((123, 160, 22, 0))
|
||||
/// ```
|
||||
#[func(constructor)]
|
||||
pub fn construct(
|
||||
/// The value that should be converted to bytes.
|
||||
value: ToBytes,
|
||||
) -> Bytes {
|
||||
value.0
|
||||
}
|
||||
|
||||
/// The length in bytes.
|
||||
#[func(title = "Length")]
|
||||
pub fn len(&self) -> usize {
|
||||
self.0.len()
|
||||
}
|
||||
|
||||
/// Returns the byte at the specified index. Returns the default value if
|
||||
/// the index is out of bounds or fails with an error if no default value
|
||||
/// was specified.
|
||||
#[func]
|
||||
pub fn at(
|
||||
&self,
|
||||
/// The index at which to retrieve the byte.
|
||||
index: i64,
|
||||
/// A default value to return if the index is out of bounds.
|
||||
#[named]
|
||||
default: Option<Value>,
|
||||
) -> StrResult<Value> {
|
||||
self.locate_opt(index)
|
||||
.and_then(|i| self.0.get(i).map(|&b| Value::Int(b.into())))
|
||||
.or(default)
|
||||
.ok_or_else(|| out_of_bounds_no_default(index, self.len()))
|
||||
}
|
||||
|
||||
/// Extracts a subslice of the bytes. Fails with an error if the start or
|
||||
/// index is out of bounds.
|
||||
#[func]
|
||||
pub fn slice(
|
||||
&self,
|
||||
/// The start index (inclusive).
|
||||
start: i64,
|
||||
/// The end index (exclusive). If omitted, the whole slice until the end
|
||||
/// is extracted.
|
||||
#[default]
|
||||
end: Option<i64>,
|
||||
/// The number of items to extract. This is equivalent to passing
|
||||
/// `start + count` as the `end` position. Mutually exclusive with
|
||||
/// `end`.
|
||||
#[named]
|
||||
count: Option<i64>,
|
||||
) -> StrResult<Bytes> {
|
||||
let mut end = end;
|
||||
if end.is_none() {
|
||||
end = count.map(|c: i64| start + c);
|
||||
}
|
||||
let start = self.locate(start)?;
|
||||
let end = self.locate(end.unwrap_or(self.len() as i64))?.max(start);
|
||||
Ok(self.0[start..end].into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&[u8]> for Bytes {
|
||||
fn from(slice: &[u8]) -> Self {
|
||||
Self(Arc::new(Prehashed::new(slice.to_vec().into())))
|
||||
@ -134,6 +223,24 @@ impl Serialize for Bytes {
|
||||
}
|
||||
}
|
||||
|
||||
/// A value that can be cast to bytes.
|
||||
pub struct ToBytes(Bytes);
|
||||
|
||||
cast! {
|
||||
ToBytes,
|
||||
v: Str => Self(v.as_bytes().into()),
|
||||
v: Array => Self(v.iter()
|
||||
.map(|item| match item {
|
||||
Value::Int(byte @ 0..=255) => Ok(*byte as u8),
|
||||
Value::Int(_) => bail!("number must be between 0 and 255"),
|
||||
value => Err(<u8 as Reflect>::error(value)),
|
||||
})
|
||||
.collect::<Result<Vec<u8>, _>>()?
|
||||
.into()
|
||||
),
|
||||
v: Bytes => Self(v),
|
||||
}
|
||||
|
||||
/// The out of bounds access error message.
|
||||
#[cold]
|
||||
fn out_of_bounds(index: i64, len: usize) -> EcoString {
|
||||
|
@ -4,9 +4,9 @@ use unicode_math_class::MathClass;
|
||||
use std::fmt::Write;
|
||||
use std::ops::Add;
|
||||
|
||||
use ecow::EcoString;
|
||||
use ecow::{eco_format, EcoString};
|
||||
|
||||
use super::Value;
|
||||
use super::{Type, Value};
|
||||
use crate::diag::{At, SourceResult, StrResult};
|
||||
use crate::syntax::{Span, Spanned};
|
||||
use crate::util::separated_list;
|
||||
@ -25,8 +25,11 @@ use crate::util::separated_list;
|
||||
/// `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;
|
||||
/// Describe what can be cast into this value.
|
||||
fn input() -> CastInfo;
|
||||
|
||||
/// Describe what this value can be cast into.
|
||||
fn output() -> CastInfo;
|
||||
|
||||
/// Whether the given value can be converted to `T`.
|
||||
///
|
||||
@ -45,12 +48,16 @@ pub trait Reflect {
|
||||
/// );
|
||||
/// ```
|
||||
fn error(found: &Value) -> EcoString {
|
||||
Self::describe().error(found)
|
||||
Self::input().error(found)
|
||||
}
|
||||
}
|
||||
|
||||
impl Reflect for Value {
|
||||
fn describe() -> CastInfo {
|
||||
fn input() -> CastInfo {
|
||||
CastInfo::Any
|
||||
}
|
||||
|
||||
fn output() -> CastInfo {
|
||||
CastInfo::Any
|
||||
}
|
||||
|
||||
@ -60,8 +67,12 @@ impl Reflect for Value {
|
||||
}
|
||||
|
||||
impl<T: Reflect> Reflect for Spanned<T> {
|
||||
fn describe() -> CastInfo {
|
||||
T::describe()
|
||||
fn input() -> CastInfo {
|
||||
T::input()
|
||||
}
|
||||
|
||||
fn output() -> CastInfo {
|
||||
T::output()
|
||||
}
|
||||
|
||||
fn castable(value: &Value) -> bool {
|
||||
@ -70,8 +81,12 @@ impl<T: Reflect> Reflect for Spanned<T> {
|
||||
}
|
||||
|
||||
impl<T: Reflect> Reflect for StrResult<T> {
|
||||
fn describe() -> CastInfo {
|
||||
T::describe()
|
||||
fn input() -> CastInfo {
|
||||
T::input()
|
||||
}
|
||||
|
||||
fn output() -> CastInfo {
|
||||
T::output()
|
||||
}
|
||||
|
||||
fn castable(value: &Value) -> bool {
|
||||
@ -80,8 +95,12 @@ impl<T: Reflect> Reflect for StrResult<T> {
|
||||
}
|
||||
|
||||
impl<T: Reflect> Reflect for SourceResult<T> {
|
||||
fn describe() -> CastInfo {
|
||||
T::describe()
|
||||
fn input() -> CastInfo {
|
||||
T::input()
|
||||
}
|
||||
|
||||
fn output() -> CastInfo {
|
||||
T::output()
|
||||
}
|
||||
|
||||
fn castable(value: &Value) -> bool {
|
||||
@ -90,8 +109,12 @@ impl<T: Reflect> Reflect for SourceResult<T> {
|
||||
}
|
||||
|
||||
impl<T: Reflect> Reflect for &T {
|
||||
fn describe() -> CastInfo {
|
||||
T::describe()
|
||||
fn input() -> CastInfo {
|
||||
T::input()
|
||||
}
|
||||
|
||||
fn output() -> CastInfo {
|
||||
T::output()
|
||||
}
|
||||
|
||||
fn castable(value: &Value) -> bool {
|
||||
@ -100,8 +123,12 @@ impl<T: Reflect> Reflect for &T {
|
||||
}
|
||||
|
||||
impl<T: Reflect> Reflect for &mut T {
|
||||
fn describe() -> CastInfo {
|
||||
T::describe()
|
||||
fn input() -> CastInfo {
|
||||
T::input()
|
||||
}
|
||||
|
||||
fn output() -> CastInfo {
|
||||
T::output()
|
||||
}
|
||||
|
||||
fn castable(value: &Value) -> bool {
|
||||
@ -191,7 +218,7 @@ pub enum CastInfo {
|
||||
/// A specific value, plus short documentation for that value.
|
||||
Value(Value, &'static str),
|
||||
/// Any value of a type.
|
||||
Type(&'static str),
|
||||
Type(Type),
|
||||
/// Multiple alternatives.
|
||||
Union(Vec<Self>),
|
||||
}
|
||||
@ -200,32 +227,20 @@ impl CastInfo {
|
||||
/// Produce an error message describing what was expected and what was
|
||||
/// found.
|
||||
pub fn error(&self, found: &Value) -> EcoString {
|
||||
fn accumulate(
|
||||
info: &CastInfo,
|
||||
found: &Value,
|
||||
parts: &mut Vec<EcoString>,
|
||||
matching_type: &mut bool,
|
||||
) {
|
||||
match info {
|
||||
CastInfo::Any => parts.push("anything".into()),
|
||||
CastInfo::Value(value, _) => {
|
||||
parts.push(value.repr().into());
|
||||
if value.type_name() == found.type_name() {
|
||||
*matching_type = true;
|
||||
}
|
||||
}
|
||||
CastInfo::Type(ty) => parts.push((*ty).into()),
|
||||
CastInfo::Union(options) => {
|
||||
for option in options {
|
||||
accumulate(option, found, parts, matching_type);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut matching_type = false;
|
||||
let mut parts = vec![];
|
||||
accumulate(self, found, &mut parts, &mut matching_type);
|
||||
|
||||
self.walk(|info| match info {
|
||||
CastInfo::Any => parts.push("anything".into()),
|
||||
CastInfo::Value(value, _) => {
|
||||
parts.push(value.repr().into());
|
||||
if value.ty() == found.ty() {
|
||||
matching_type = true;
|
||||
}
|
||||
}
|
||||
CastInfo::Type(ty) => parts.push(eco_format!("{ty}")),
|
||||
CastInfo::Union(_) => {}
|
||||
});
|
||||
|
||||
let mut msg = String::from("expected ");
|
||||
if parts.is_empty() {
|
||||
@ -236,7 +251,7 @@ impl CastInfo {
|
||||
|
||||
if !matching_type {
|
||||
msg.push_str(", found ");
|
||||
msg.push_str(found.type_name());
|
||||
write!(msg, "{}", found.ty()).unwrap();
|
||||
}
|
||||
if_chain::if_chain! {
|
||||
if let Value::Int(i) = found;
|
||||
@ -249,6 +264,27 @@ impl CastInfo {
|
||||
|
||||
msg.into()
|
||||
}
|
||||
|
||||
/// Walk all contained non-union infos.
|
||||
pub fn walk<F>(&self, mut f: F)
|
||||
where
|
||||
F: FnMut(&Self),
|
||||
{
|
||||
fn inner<F>(info: &CastInfo, f: &mut F)
|
||||
where
|
||||
F: FnMut(&CastInfo),
|
||||
{
|
||||
if let CastInfo::Union(infos) = info {
|
||||
for child in infos {
|
||||
inner(child, f);
|
||||
}
|
||||
} else {
|
||||
f(info);
|
||||
}
|
||||
}
|
||||
|
||||
inner(self, &mut f)
|
||||
}
|
||||
}
|
||||
|
||||
impl Add for CastInfo {
|
||||
@ -299,7 +335,11 @@ impl<T> Container for Vec<T> {
|
||||
pub enum Never {}
|
||||
|
||||
impl Reflect for Never {
|
||||
fn describe() -> CastInfo {
|
||||
fn input() -> CastInfo {
|
||||
CastInfo::Union(vec![])
|
||||
}
|
||||
|
||||
fn output() -> CastInfo {
|
||||
CastInfo::Union(vec![])
|
||||
}
|
||||
|
||||
|
@ -6,15 +6,114 @@ use std::ops::{Add, Sub};
|
||||
|
||||
use ecow::{eco_format, EcoString, EcoVec};
|
||||
use time::error::{Format, InvalidFormatDescription};
|
||||
use time::{format_description, PrimitiveDateTime};
|
||||
use time::macros::format_description;
|
||||
use time::{format_description, Month, PrimitiveDateTime};
|
||||
|
||||
use crate::diag::bail;
|
||||
use crate::eval::{Duration, StrResult};
|
||||
use super::{cast, func, scope, ty, Dict, Duration, Str, Value, Vm};
|
||||
use crate::diag::{bail, StrResult};
|
||||
use crate::geom::Smart;
|
||||
use crate::util::pretty_array_like;
|
||||
use crate::World;
|
||||
|
||||
/// A datetime object that represents either a date, a time or a combination of
|
||||
/// both.
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Hash)]
|
||||
/// Represents a date, a time, or a combination of both.
|
||||
///
|
||||
/// Can be created by either specifying a custom datetime using this type's
|
||||
/// constructor function or getting the current date with
|
||||
/// [`datetime.today`]($datetime.today).
|
||||
///
|
||||
/// # Example
|
||||
/// ```example
|
||||
/// #let date = datetime(
|
||||
/// year: 2020,
|
||||
/// month: 10,
|
||||
/// day: 4,
|
||||
/// )
|
||||
///
|
||||
/// #date.display() \
|
||||
/// #date.display(
|
||||
/// "y:[year repr:last_two]"
|
||||
/// )
|
||||
///
|
||||
/// #let time = datetime(
|
||||
/// hour: 18,
|
||||
/// minute: 2,
|
||||
/// second: 23,
|
||||
/// )
|
||||
///
|
||||
/// #time.display() \
|
||||
/// #time.display(
|
||||
/// "h:[hour repr:12][period]"
|
||||
/// )
|
||||
/// ```
|
||||
///
|
||||
/// # Format
|
||||
/// You can specify a customized formatting using the
|
||||
/// [`display`]($datetime.display) method. The format of a datetime is
|
||||
/// specified by providing _components_ with a specified number of _modifiers_.
|
||||
/// A component represents a certain part of the datetime that you want to
|
||||
/// display, and with the help of modifiers you can define how you want to
|
||||
/// display that component. In order to display a component, you wrap the name
|
||||
/// of the component in square brackets (e.g. `[[year]]` will display the year).
|
||||
/// In order to add modifiers, you add a space after the component name followed
|
||||
/// by the name of the modifier, a colon and the value of the modifier (e.g.
|
||||
/// `[[month repr:short]]` will display the short representation of the month).
|
||||
///
|
||||
/// The possible combination of components and their respective modifiers is as
|
||||
/// follows:
|
||||
///
|
||||
/// - `year`: Displays the year of the datetime.
|
||||
/// - `padding`: Can be either `zero`, `space` or `none`. Specifies how the
|
||||
/// year is padded.
|
||||
/// - `repr` Can be either `full` in which case the full year is displayed or
|
||||
/// `last_two` in which case only the last two digits are displayed.
|
||||
/// - `sign`: Can be either `automatic` or `mandatory`. Specifies when the
|
||||
/// sign should be displayed.
|
||||
/// - `month`: Displays the month of the datetime.
|
||||
/// - `padding`: Can be either `zero`, `space` or `none`. Specifies how the
|
||||
/// month is padded.
|
||||
/// - `repr`: Can be either `numerical`, `long` or `short`. Specifies if the
|
||||
/// month should be displayed as a number or a word. Unfortunately, when
|
||||
/// choosing the word representation, it can currently only display the
|
||||
/// English version. In the future, it is planned to support localization.
|
||||
/// - `day`: Displays the day of the datetime.
|
||||
/// - `padding`: Can be either `zero`, `space` or `none`. Specifies how the
|
||||
/// day is padded.
|
||||
/// - `week_number`: Displays the week number of the datetime.
|
||||
/// - `padding`: Can be either `zero`, `space` or `none`. Specifies how the
|
||||
/// week number is padded.
|
||||
/// - `repr`: Can be either `ISO`, `sunday` or `monday`. In the case of `ISO`,
|
||||
/// week numbers are between 1 and 53, while the other ones are between 0
|
||||
/// and 53.
|
||||
/// - `weekday`: Displays the weekday of the date.
|
||||
/// - `repr` Can be either `long`, `short`, `sunday` or `monday`. In the case
|
||||
/// of `long` and `short`, the corresponding English name will be displayed
|
||||
/// (same as for the month, other languages are currently not supported). In
|
||||
/// the case of `sunday` and `monday`, the numerical value will be displayed
|
||||
/// (assuming Sunday and Monday as the first day of the week, respectively).
|
||||
/// - `one_indexed`: Can be either `true` or `false`. Defines whether the
|
||||
/// numerical representation of the week starts with 0 or 1.
|
||||
/// - `hour`: Displays the hour of the date.
|
||||
/// - `padding`: Can be either `zero`, `space` or `none`. Specifies how the
|
||||
/// hour is padded.
|
||||
/// - `repr`: Can be either `24` or `12`. Changes whether the hour is
|
||||
/// displayed in the 24-hour or 12-hour format.
|
||||
/// - `period`: The AM/PM part of the hour
|
||||
/// - `case`: Can be `lower` to display it in lower case and `upper` to
|
||||
/// display it in upper case.
|
||||
/// - `minute`: Displays the minute of the date.
|
||||
/// - `padding`: Can be either `zero`, `space` or `none`. Specifies how the
|
||||
/// minute is padded.
|
||||
/// - `second`: Displays the second of the date.
|
||||
/// - `padding`: Can be either `zero`, `space` or `none`. Specifies how the
|
||||
/// second is padded.
|
||||
///
|
||||
/// Keep in mind that not always all components can be used. For example, if you
|
||||
/// create a new datetime with `{datetime(year: 2023, month: 10, day: 13)}`, it
|
||||
/// will be stored as a plain date internally, meaning that you cannot use
|
||||
/// components such as `hour` or `minute`, which would only work on datetimes
|
||||
/// that have a specified time.
|
||||
#[ty(scope)]
|
||||
#[derive(Clone, Copy, PartialEq, Hash)]
|
||||
pub enum Datetime {
|
||||
/// Representation as a date.
|
||||
Date(time::Date),
|
||||
@ -25,108 +124,6 @@ pub enum Datetime {
|
||||
}
|
||||
|
||||
impl Datetime {
|
||||
/// Which kind of variant this datetime stores.
|
||||
pub fn kind(&self) -> &'static str {
|
||||
match self {
|
||||
Datetime::Datetime(_) => "datetime",
|
||||
Datetime::Date(_) => "date",
|
||||
Datetime::Time(_) => "time",
|
||||
}
|
||||
}
|
||||
|
||||
/// Display the date and/or time in a certain format.
|
||||
pub fn display(&self, pattern: Option<EcoString>) -> Result<EcoString, EcoString> {
|
||||
let pattern = pattern.as_ref().map(EcoString::as_str).unwrap_or(match self {
|
||||
Datetime::Date(_) => "[year]-[month]-[day]",
|
||||
Datetime::Time(_) => "[hour]:[minute]:[second]",
|
||||
Datetime::Datetime(_) => "[year]-[month]-[day] [hour]:[minute]:[second]",
|
||||
});
|
||||
|
||||
let format = format_description::parse(pattern)
|
||||
.map_err(format_time_invalid_format_description_error)?;
|
||||
|
||||
let formatted_result = match self {
|
||||
Datetime::Date(date) => date.format(&format),
|
||||
Datetime::Time(time) => time.format(&format),
|
||||
Datetime::Datetime(datetime) => datetime.format(&format),
|
||||
}
|
||||
.map(EcoString::from);
|
||||
|
||||
formatted_result.map_err(format_time_format_error)
|
||||
}
|
||||
|
||||
/// Return the year of the datetime, if existing.
|
||||
pub fn year(&self) -> Option<i32> {
|
||||
match self {
|
||||
Datetime::Date(date) => Some(date.year()),
|
||||
Datetime::Time(_) => None,
|
||||
Datetime::Datetime(datetime) => Some(datetime.year()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the month of the datetime, if existing.
|
||||
pub fn month(&self) -> Option<u8> {
|
||||
match self {
|
||||
Datetime::Date(date) => Some(date.month().into()),
|
||||
Datetime::Time(_) => None,
|
||||
Datetime::Datetime(datetime) => Some(datetime.month().into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the weekday of the datetime, if existing.
|
||||
pub fn weekday(&self) -> Option<u8> {
|
||||
match self {
|
||||
Datetime::Date(date) => Some(date.weekday().number_from_monday()),
|
||||
Datetime::Time(_) => None,
|
||||
Datetime::Datetime(datetime) => Some(datetime.weekday().number_from_monday()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the day of the datetime, if existing.
|
||||
pub fn day(&self) -> Option<u8> {
|
||||
match self {
|
||||
Datetime::Date(date) => Some(date.day()),
|
||||
Datetime::Time(_) => None,
|
||||
Datetime::Datetime(datetime) => Some(datetime.day()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the hour of the datetime, if existing.
|
||||
pub fn hour(&self) -> Option<u8> {
|
||||
match self {
|
||||
Datetime::Date(_) => None,
|
||||
Datetime::Time(time) => Some(time.hour()),
|
||||
Datetime::Datetime(datetime) => Some(datetime.hour()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the minute of the datetime, if existing.
|
||||
pub fn minute(&self) -> Option<u8> {
|
||||
match self {
|
||||
Datetime::Date(_) => None,
|
||||
Datetime::Time(time) => Some(time.minute()),
|
||||
Datetime::Datetime(datetime) => Some(datetime.minute()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the second of the datetime, if existing.
|
||||
pub fn second(&self) -> Option<u8> {
|
||||
match self {
|
||||
Datetime::Date(_) => None,
|
||||
Datetime::Time(time) => Some(time.second()),
|
||||
Datetime::Datetime(datetime) => Some(datetime.second()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the ordinal (day of the year), if existing.
|
||||
pub fn ordinal(&self) -> Option<u16> {
|
||||
match self {
|
||||
Datetime::Datetime(datetime) => Some(datetime.ordinal()),
|
||||
Datetime::Date(date) => Some(date.ordinal()),
|
||||
Datetime::Time(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a datetime from year, month, and day.
|
||||
pub fn from_ymd(year: i32, month: u8, day: u8) -> Option<Self> {
|
||||
Some(Datetime::Date(
|
||||
@ -155,54 +152,276 @@ impl Datetime {
|
||||
let time = time::Time::from_hms(hour, minute, second).ok()?;
|
||||
Some(Datetime::Datetime(PrimitiveDateTime::new(date, time)))
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for Datetime {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
match (self, other) {
|
||||
(Datetime::Datetime(a), Datetime::Datetime(b)) => a.partial_cmp(b),
|
||||
(Datetime::Date(a), Datetime::Date(b)) => a.partial_cmp(b),
|
||||
(Datetime::Time(a), Datetime::Time(b)) => a.partial_cmp(b),
|
||||
_ => None,
|
||||
/// Try to parse a dictionary as a TOML date.
|
||||
pub fn from_toml_dict(dict: &Dict) -> Option<Self> {
|
||||
if dict.len() != 1 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let Ok(Value::Str(string)) = dict.get("$__toml_private_datetime") else {
|
||||
return None;
|
||||
};
|
||||
|
||||
if let Ok(d) = time::PrimitiveDateTime::parse(
|
||||
string,
|
||||
&format_description!("[year]-[month]-[day]T[hour]:[minute]:[second]Z"),
|
||||
) {
|
||||
Self::from_ymd_hms(
|
||||
d.year(),
|
||||
d.month() as u8,
|
||||
d.day(),
|
||||
d.hour(),
|
||||
d.minute(),
|
||||
d.second(),
|
||||
)
|
||||
} else if let Ok(d) = time::PrimitiveDateTime::parse(
|
||||
string,
|
||||
&format_description!("[year]-[month]-[day]T[hour]:[minute]:[second]"),
|
||||
) {
|
||||
Self::from_ymd_hms(
|
||||
d.year(),
|
||||
d.month() as u8,
|
||||
d.day(),
|
||||
d.hour(),
|
||||
d.minute(),
|
||||
d.second(),
|
||||
)
|
||||
} else if let Ok(d) =
|
||||
time::Date::parse(string, &format_description!("[year]-[month]-[day]"))
|
||||
{
|
||||
Self::from_ymd(d.year(), d.month() as u8, d.day())
|
||||
} else if let Ok(d) =
|
||||
time::Time::parse(string, &format_description!("[hour]:[minute]:[second]"))
|
||||
{
|
||||
Self::from_hms(d.hour(), d.minute(), d.second())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Add<Duration> for Datetime {
|
||||
type Output = Datetime;
|
||||
|
||||
fn add(self, rhs: Duration) -> Self::Output {
|
||||
let rhs: time::Duration = rhs.into();
|
||||
/// Which kind of variant this datetime stores.
|
||||
pub fn kind(&self) -> &'static str {
|
||||
match self {
|
||||
Datetime::Datetime(datetime) => Self::Datetime(datetime + rhs),
|
||||
Datetime::Date(date) => Self::Date(date + rhs),
|
||||
Datetime::Time(time) => Self::Time(time + rhs),
|
||||
Datetime::Datetime(_) => "datetime",
|
||||
Datetime::Date(_) => "date",
|
||||
Datetime::Time(_) => "time",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Sub<Duration> for Datetime {
|
||||
type Output = Datetime;
|
||||
#[scope]
|
||||
impl Datetime {
|
||||
/// Creates a new datetime.
|
||||
///
|
||||
/// You can specify the [datetime]($datetime) using a year, month, day,
|
||||
/// hour, minute, and second.
|
||||
///
|
||||
/// _Note_: Depending on which components of the datetime you specify, Typst
|
||||
/// will store it in one of the following three ways:
|
||||
/// * If you specify year, month and day, Typst will store just a date.
|
||||
/// * If you specify hour, minute and second, Typst will store just a time.
|
||||
/// * If you specify all of year, month, day, hour, minute and second, Typst
|
||||
/// will store a full datetime.
|
||||
///
|
||||
/// Depending on how it is stored, the [`display`]($datetime.display) method
|
||||
/// will choose a different formatting by default.
|
||||
///
|
||||
/// ```example
|
||||
/// #datetime(
|
||||
/// year: 2012,
|
||||
/// month: 8,
|
||||
/// day: 3,
|
||||
/// ).display()
|
||||
/// ```
|
||||
#[func(constructor)]
|
||||
pub fn construct(
|
||||
/// The year of the datetime.
|
||||
#[named]
|
||||
year: Option<YearComponent>,
|
||||
/// The month of the datetime.
|
||||
#[named]
|
||||
month: Option<MonthComponent>,
|
||||
/// The day of the datetime.
|
||||
#[named]
|
||||
day: Option<DayComponent>,
|
||||
/// The hour of the datetime.
|
||||
#[named]
|
||||
hour: Option<HourComponent>,
|
||||
/// The minute of the datetime.
|
||||
#[named]
|
||||
minute: Option<MinuteComponent>,
|
||||
/// The second of the datetime.
|
||||
#[named]
|
||||
second: Option<SecondComponent>,
|
||||
) -> StrResult<Datetime> {
|
||||
let time = match (hour, minute, second) {
|
||||
(Some(hour), Some(minute), Some(second)) => {
|
||||
match time::Time::from_hms(hour.0, minute.0, second.0) {
|
||||
Ok(time) => Some(time),
|
||||
Err(_) => bail!("time is invalid"),
|
||||
}
|
||||
}
|
||||
(None, None, None) => None,
|
||||
_ => bail!("time is incomplete"),
|
||||
};
|
||||
|
||||
fn sub(self, rhs: Duration) -> Self::Output {
|
||||
let rhs: time::Duration = rhs.into();
|
||||
let date = match (year, month, day) {
|
||||
(Some(year), Some(month), Some(day)) => {
|
||||
match time::Date::from_calendar_date(year.0, month.0, day.0) {
|
||||
Ok(date) => Some(date),
|
||||
Err(_) => bail!("date is invalid"),
|
||||
}
|
||||
}
|
||||
(None, None, None) => None,
|
||||
_ => bail!("date is incomplete"),
|
||||
};
|
||||
|
||||
Ok(match (date, time) {
|
||||
(Some(date), Some(time)) => {
|
||||
Datetime::Datetime(PrimitiveDateTime::new(date, time))
|
||||
}
|
||||
(Some(date), None) => Datetime::Date(date),
|
||||
(None, Some(time)) => Datetime::Time(time),
|
||||
(None, None) => {
|
||||
bail!("at least one of date or time must be fully specified")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns the current date.
|
||||
///
|
||||
/// ```example
|
||||
/// Today's date is
|
||||
/// #datetime.today().display().
|
||||
/// ```
|
||||
#[func]
|
||||
pub fn today(
|
||||
/// The virtual machine.
|
||||
vm: &mut Vm,
|
||||
/// An offset to apply to the current UTC date. If set to `{auto}`, the
|
||||
/// offset will be the local offset.
|
||||
#[named]
|
||||
#[default]
|
||||
offset: Smart<i64>,
|
||||
) -> StrResult<Datetime> {
|
||||
Ok(vm
|
||||
.vt
|
||||
.world
|
||||
.today(offset.as_custom())
|
||||
.ok_or("unable to get the current date")?)
|
||||
}
|
||||
|
||||
/// Displays the datetime in a specified format.
|
||||
///
|
||||
/// Depending on whether you have defined just a date, a time or both, the
|
||||
/// default format will be different. If you specified a date, it will be
|
||||
/// `[[year]-[month]-[day]]`. If you specified a time, it will be
|
||||
/// `[[hour]:[minute]:[second]]`. In the case of a datetime, it will be
|
||||
/// `[[year]-[month]-[day] [hour]:[minute]:[second]]`.
|
||||
#[func]
|
||||
pub fn display(
|
||||
&self,
|
||||
/// The format used to display the datetime.
|
||||
#[default]
|
||||
pattern: Smart<DisplayPattern>,
|
||||
) -> StrResult<EcoString> {
|
||||
let pat = |s| format_description::parse_borrowed::<2>(s).unwrap();
|
||||
let result = match pattern {
|
||||
Smart::Auto => match self {
|
||||
Self::Date(date) => date.format(&pat("[year]-[month]-[day]")),
|
||||
Self::Time(time) => time.format(&pat("[hour]:[minute]:[second]")),
|
||||
Self::Datetime(datetime) => {
|
||||
datetime.format(&pat("[year]-[month]-[day] [hour]:[minute]:[second]"))
|
||||
}
|
||||
},
|
||||
|
||||
Smart::Custom(DisplayPattern(_, format)) => match self {
|
||||
Self::Date(date) => date.format(&format),
|
||||
Self::Time(time) => time.format(&format),
|
||||
Self::Datetime(datetime) => datetime.format(&format),
|
||||
},
|
||||
};
|
||||
result.map(EcoString::from).map_err(format_time_format_error)
|
||||
}
|
||||
|
||||
/// The year if it was specified or `{none}`, otherwise.
|
||||
#[func]
|
||||
pub fn year(&self) -> Option<i32> {
|
||||
match self {
|
||||
Datetime::Datetime(datetime) => Self::Datetime(datetime - rhs),
|
||||
Datetime::Date(date) => Self::Date(date - rhs),
|
||||
Datetime::Time(time) => Self::Time(time - rhs),
|
||||
Self::Date(date) => Some(date.year()),
|
||||
Self::Time(_) => None,
|
||||
Self::Datetime(datetime) => Some(datetime.year()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Sub for Datetime {
|
||||
type Output = StrResult<Duration>;
|
||||
/// The month if it was specified or `{none}`, otherwise.
|
||||
#[func]
|
||||
pub fn month(&self) -> Option<u8> {
|
||||
match self {
|
||||
Self::Date(date) => Some(date.month().into()),
|
||||
Self::Time(_) => None,
|
||||
Self::Datetime(datetime) => Some(datetime.month().into()),
|
||||
}
|
||||
}
|
||||
|
||||
fn sub(self, rhs: Self) -> Self::Output {
|
||||
match (self, rhs) {
|
||||
(Datetime::Datetime(a), Datetime::Datetime(b)) => Ok((a - b).into()),
|
||||
(Datetime::Date(a), Datetime::Date(b)) => Ok((a - b).into()),
|
||||
(Datetime::Time(a), Datetime::Time(b)) => Ok((a - b).into()),
|
||||
(a, b) => bail!("cannot subtract {} from {}", b.kind(), a.kind()),
|
||||
/// The weekday if it was specified or `{none}`, otherwise.
|
||||
#[func]
|
||||
pub fn weekday(&self) -> Option<u8> {
|
||||
match self {
|
||||
Self::Date(date) => Some(date.weekday().number_from_monday()),
|
||||
Self::Time(_) => None,
|
||||
Self::Datetime(datetime) => Some(datetime.weekday().number_from_monday()),
|
||||
}
|
||||
}
|
||||
|
||||
/// The day if it was specified or `{none}`, otherwise.
|
||||
#[func]
|
||||
pub fn day(&self) -> Option<u8> {
|
||||
match self {
|
||||
Self::Date(date) => Some(date.day()),
|
||||
Self::Time(_) => None,
|
||||
Self::Datetime(datetime) => Some(datetime.day()),
|
||||
}
|
||||
}
|
||||
|
||||
/// The hour if it was specified or `{none}`, otherwise.
|
||||
#[func]
|
||||
pub fn hour(&self) -> Option<u8> {
|
||||
match self {
|
||||
Self::Date(_) => None,
|
||||
Self::Time(time) => Some(time.hour()),
|
||||
Self::Datetime(datetime) => Some(datetime.hour()),
|
||||
}
|
||||
}
|
||||
|
||||
/// The minute if it was specified or `{none}`, otherwise.
|
||||
#[func]
|
||||
pub fn minute(&self) -> Option<u8> {
|
||||
match self {
|
||||
Self::Date(_) => None,
|
||||
Self::Time(time) => Some(time.minute()),
|
||||
Self::Datetime(datetime) => Some(datetime.minute()),
|
||||
}
|
||||
}
|
||||
|
||||
/// The second if it was specified or `{none}`, otherwise.
|
||||
#[func]
|
||||
pub fn second(&self) -> Option<u8> {
|
||||
match self {
|
||||
Datetime::Date(_) => None,
|
||||
Self::Time(time) => Some(time.second()),
|
||||
Self::Datetime(datetime) => Some(datetime.second()),
|
||||
}
|
||||
}
|
||||
|
||||
/// The ordinal (day of the year), if it exists.
|
||||
#[func]
|
||||
pub fn ordinal(&self) -> Option<u16> {
|
||||
match self {
|
||||
Self::Datetime(datetime) => Some(datetime.ordinal()),
|
||||
Self::Date(date) => Some(date.ordinal()),
|
||||
Self::Time(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -224,6 +443,106 @@ impl Debug for Datetime {
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for Datetime {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
match (self, other) {
|
||||
(Self::Datetime(a), Self::Datetime(b)) => a.partial_cmp(b),
|
||||
(Self::Date(a), Self::Date(b)) => a.partial_cmp(b),
|
||||
(Self::Time(a), Self::Time(b)) => a.partial_cmp(b),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Add<Duration> for Datetime {
|
||||
type Output = Self;
|
||||
|
||||
fn add(self, rhs: Duration) -> Self::Output {
|
||||
let rhs: time::Duration = rhs.into();
|
||||
match self {
|
||||
Self::Datetime(datetime) => Self::Datetime(datetime + rhs),
|
||||
Self::Date(date) => Self::Date(date + rhs),
|
||||
Self::Time(time) => Self::Time(time + rhs),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Sub<Duration> for Datetime {
|
||||
type Output = Self;
|
||||
|
||||
fn sub(self, rhs: Duration) -> Self::Output {
|
||||
let rhs: time::Duration = rhs.into();
|
||||
match self {
|
||||
Self::Datetime(datetime) => Self::Datetime(datetime - rhs),
|
||||
Self::Date(date) => Self::Date(date - rhs),
|
||||
Self::Time(time) => Self::Time(time - rhs),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Sub for Datetime {
|
||||
type Output = StrResult<Duration>;
|
||||
|
||||
fn sub(self, rhs: Self) -> Self::Output {
|
||||
match (self, rhs) {
|
||||
(Self::Datetime(a), Self::Datetime(b)) => Ok((a - b).into()),
|
||||
(Self::Date(a), Self::Date(b)) => Ok((a - b).into()),
|
||||
(Self::Time(a), Self::Time(b)) => Ok((a - b).into()),
|
||||
(a, b) => bail!("cannot subtract {} from {}", b.kind(), a.kind()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct YearComponent(i32);
|
||||
pub struct MonthComponent(Month);
|
||||
pub struct DayComponent(u8);
|
||||
pub struct HourComponent(u8);
|
||||
pub struct MinuteComponent(u8);
|
||||
pub struct SecondComponent(u8);
|
||||
|
||||
cast! {
|
||||
YearComponent,
|
||||
v: i32 => Self(v),
|
||||
}
|
||||
|
||||
cast! {
|
||||
MonthComponent,
|
||||
v: u8 => Self(Month::try_from(v).map_err(|_| "month is invalid")?)
|
||||
}
|
||||
|
||||
cast! {
|
||||
DayComponent,
|
||||
v: u8 => Self(v),
|
||||
}
|
||||
|
||||
cast! {
|
||||
HourComponent,
|
||||
v: u8 => Self(v),
|
||||
}
|
||||
|
||||
cast! {
|
||||
MinuteComponent,
|
||||
v: u8 => Self(v),
|
||||
}
|
||||
|
||||
cast! {
|
||||
SecondComponent,
|
||||
v: u8 => Self(v),
|
||||
}
|
||||
|
||||
/// A format in which a datetime can be displayed.
|
||||
pub struct DisplayPattern(Str, format_description::OwnedFormatItem);
|
||||
|
||||
cast! {
|
||||
DisplayPattern,
|
||||
self => self.0.into_value(),
|
||||
v: Str => {
|
||||
let item = format_description::parse_owned::<2>(&v)
|
||||
.map_err(format_time_invalid_format_description_error)?;
|
||||
Self(v, item)
|
||||
}
|
||||
}
|
||||
|
||||
/// Format the `Format` error of the time crate in an appropriate way.
|
||||
fn format_time_format_error(error: Format) -> EcoString {
|
||||
match error {
|
||||
|
@ -4,9 +4,10 @@ use std::ops::{Add, AddAssign};
|
||||
use std::sync::Arc;
|
||||
|
||||
use ecow::{eco_format, EcoString};
|
||||
use indexmap::IndexMap;
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
|
||||
use super::{array, Array, Str, Value};
|
||||
use super::{array, func, scope, ty, Array, Str, Value};
|
||||
use crate::diag::StrResult;
|
||||
use crate::syntax::is_ident;
|
||||
use crate::util::{pretty_array_like, separated_list, ArcExt};
|
||||
@ -26,10 +27,42 @@ macro_rules! __dict {
|
||||
#[doc(inline)]
|
||||
pub use crate::__dict as dict;
|
||||
|
||||
#[doc(inline)]
|
||||
pub use indexmap::IndexMap;
|
||||
|
||||
/// A reference-counted dictionary with value semantics.
|
||||
/// A map from string keys to values.
|
||||
///
|
||||
/// You can construct a dictionary by enclosing comma-separated `key: value`
|
||||
/// pairs in parentheses. The values do not have to be of the same type. Since
|
||||
/// empty parentheses already yield an empty array, you have to use the special
|
||||
/// `(:)` syntax to create an empty dictionary.
|
||||
///
|
||||
/// A dictionary is conceptually similar to an array, but it is indexed by
|
||||
/// strings instead of integers. You can access and create dictionary entries
|
||||
/// with the `.at()` method. If you know the key statically, you can
|
||||
/// alternatively use [field access notation]($scripting/#fields) (`.key`) to
|
||||
/// access the value. Dictionaries can be added with the `+` operator and
|
||||
/// [joined together]($scripting/#blocks). To check whether a key is present in
|
||||
/// the dictionary, use the `in` keyword.
|
||||
///
|
||||
/// You can iterate over the pairs in a dictionary using a [for
|
||||
/// loop]($scripting/#loops). This will iterate in the order the pairs were
|
||||
/// inserted / declared.
|
||||
///
|
||||
/// # Example
|
||||
/// ```example
|
||||
/// #let dict = (
|
||||
/// name: "Typst",
|
||||
/// born: 2019,
|
||||
/// )
|
||||
///
|
||||
/// #dict.name \
|
||||
/// #(dict.launch = 20)
|
||||
/// #dict.len() \
|
||||
/// #dict.keys() \
|
||||
/// #dict.values() \
|
||||
/// #dict.at("born") \
|
||||
/// #dict.insert("city", "Berlin ")
|
||||
/// #("name" in dict)
|
||||
/// ```
|
||||
#[ty(scope, name = "dictionary")]
|
||||
#[derive(Default, Clone, PartialEq)]
|
||||
pub struct Dict(Arc<IndexMap<Str, Value>>);
|
||||
|
||||
@ -44,18 +77,14 @@ impl Dict {
|
||||
self.0.is_empty()
|
||||
}
|
||||
|
||||
/// The number of pairs in the dictionary.
|
||||
pub fn len(&self) -> usize {
|
||||
self.0.len()
|
||||
/// Borrow the value at the given key.
|
||||
pub fn get(&self, key: &str) -> StrResult<&Value> {
|
||||
self.0.get(key).ok_or_else(|| missing_key(key))
|
||||
}
|
||||
|
||||
/// Borrow the value the given `key` maps to,
|
||||
pub fn at(&self, key: &str, default: Option<Value>) -> StrResult<Value> {
|
||||
self.0
|
||||
.get(key)
|
||||
.cloned()
|
||||
.or(default)
|
||||
.ok_or_else(|| missing_key_no_default(key))
|
||||
/// Remove the value if the dictionary contains the given key.
|
||||
pub fn take(&mut self, key: &str) -> StrResult<Value> {
|
||||
Arc::make_mut(&mut self.0).remove(key).ok_or_else(|| missing_key(key))
|
||||
}
|
||||
|
||||
/// Mutably borrow the value the given `key` maps to.
|
||||
@ -65,31 +94,11 @@ impl Dict {
|
||||
.ok_or_else(|| missing_key_no_default(key))
|
||||
}
|
||||
|
||||
/// Remove the value if the dictionary contains the given key.
|
||||
pub fn take(&mut self, key: &str) -> StrResult<Value> {
|
||||
Arc::make_mut(&mut self.0)
|
||||
.remove(key)
|
||||
.ok_or_else(|| eco_format!("missing key: {:?}", Str::from(key)))
|
||||
}
|
||||
|
||||
/// Whether the dictionary contains a specific key.
|
||||
pub fn contains(&self, key: &str) -> bool {
|
||||
self.0.contains_key(key)
|
||||
}
|
||||
|
||||
/// Insert a mapping from the given `key` to the given `value`.
|
||||
pub fn insert(&mut self, key: Str, value: Value) {
|
||||
Arc::make_mut(&mut self.0).insert(key, value);
|
||||
}
|
||||
|
||||
/// Remove a mapping by `key` and return the value.
|
||||
pub fn remove(&mut self, key: &str) -> StrResult<Value> {
|
||||
match Arc::make_mut(&mut self.0).shift_remove(key) {
|
||||
Some(value) => Ok(value),
|
||||
None => Err(missing_key(key)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Clear the dictionary.
|
||||
pub fn clear(&mut self) {
|
||||
if Arc::strong_count(&self.0) == 1 {
|
||||
@ -99,25 +108,6 @@ impl Dict {
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the keys of the dictionary as an array.
|
||||
pub fn keys(&self) -> Array {
|
||||
self.0.keys().cloned().map(Value::Str).collect()
|
||||
}
|
||||
|
||||
/// Return the values of the dictionary as an array.
|
||||
pub fn values(&self) -> Array {
|
||||
self.0.values().cloned().collect()
|
||||
}
|
||||
|
||||
/// Return the values of the dictionary as an array of pairs (arrays of
|
||||
/// length two).
|
||||
pub fn pairs(&self) -> Array {
|
||||
self.0
|
||||
.iter()
|
||||
.map(|(k, v)| Value::Array(array![k.clone(), v.clone()]))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Iterate over pairs of references to the contained keys and values.
|
||||
pub fn iter(&self) -> indexmap::map::Iter<Str, Value> {
|
||||
self.0.iter()
|
||||
@ -135,6 +125,80 @@ impl Dict {
|
||||
}
|
||||
}
|
||||
|
||||
#[scope]
|
||||
impl Dict {
|
||||
/// The number of pairs in the dictionary.
|
||||
#[func(title = "Length")]
|
||||
pub fn len(&self) -> usize {
|
||||
self.0.len()
|
||||
}
|
||||
|
||||
/// Returns the value associated with the specified key in the dictionary.
|
||||
/// May be used on the left-hand side of an assignment if the key is already
|
||||
/// present in the dictionary. Returns the default value if the key is not
|
||||
/// part of the dictionary or fails with an error if no default value was
|
||||
/// specified.
|
||||
#[func]
|
||||
pub fn at(
|
||||
&self,
|
||||
/// The key at which to retrieve the item.
|
||||
key: Str,
|
||||
/// A default value to return if the key is not part of the dictionary.
|
||||
#[named]
|
||||
default: Option<Value>,
|
||||
) -> StrResult<Value> {
|
||||
self.0
|
||||
.get(&key)
|
||||
.cloned()
|
||||
.or(default)
|
||||
.ok_or_else(|| missing_key_no_default(&key))
|
||||
}
|
||||
|
||||
/// Insert a new pair into the dictionary and return the value. If the
|
||||
/// dictionary already contains this key, the value is updated.
|
||||
#[func]
|
||||
pub fn insert(
|
||||
&mut self,
|
||||
/// The key of the pair that should be inserted.
|
||||
key: Str,
|
||||
/// The value of the pair that should be inserted.
|
||||
value: Value,
|
||||
) {
|
||||
Arc::make_mut(&mut self.0).insert(key, value);
|
||||
}
|
||||
|
||||
/// Remove a pair from the dictionary by key and return the value.
|
||||
#[func]
|
||||
pub fn remove(&mut self, key: Str) -> StrResult<Value> {
|
||||
match Arc::make_mut(&mut self.0).shift_remove(&key) {
|
||||
Some(value) => Ok(value),
|
||||
None => Err(missing_key(&key)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the keys of the dictionary as an array in insertion order.
|
||||
#[func]
|
||||
pub fn keys(&self) -> Array {
|
||||
self.0.keys().cloned().map(Value::Str).collect()
|
||||
}
|
||||
|
||||
/// Returns the values of the dictionary as an array in insertion order.
|
||||
#[func]
|
||||
pub fn values(&self) -> Array {
|
||||
self.0.values().cloned().collect()
|
||||
}
|
||||
|
||||
/// Returns the keys and values of the dictionary as an array of pairs. Each
|
||||
/// pair is represented as an array of length two.
|
||||
#[func]
|
||||
pub fn pairs(&self) -> Array {
|
||||
self.0
|
||||
.iter()
|
||||
.map(|(k, v)| Value::Array(array![k.clone(), v.clone()]))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for Dict {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
if self.is_empty() {
|
||||
|
@ -1,11 +1,14 @@
|
||||
use crate::util::pretty_array_like;
|
||||
use ecow::eco_format;
|
||||
use std::fmt;
|
||||
use std::fmt::{Debug, Formatter};
|
||||
use std::ops::{Add, Div, Mul, Neg, Sub};
|
||||
use time::ext::NumericalDuration;
|
||||
|
||||
use super::{func, scope, ty};
|
||||
use crate::util::pretty_array_like;
|
||||
|
||||
/// Represents a positive or negative span of time.
|
||||
#[ty(scope)]
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
|
||||
pub struct Duration(time::Duration);
|
||||
|
||||
@ -14,39 +17,100 @@ impl Duration {
|
||||
pub fn is_zero(&self) -> bool {
|
||||
self.0.is_zero()
|
||||
}
|
||||
}
|
||||
|
||||
#[scope]
|
||||
impl Duration {
|
||||
/// Creates a new duration.
|
||||
///
|
||||
/// You can specify the [duration]($duration) using weeks, days, hours,
|
||||
/// minutes and seconds. You can also get a duration by subtracting two
|
||||
/// [datetimes]($datetime).
|
||||
///
|
||||
/// ```example
|
||||
/// #duration(
|
||||
/// days: 3,
|
||||
/// hours: 12,
|
||||
/// ).hours()
|
||||
/// ```
|
||||
#[func(constructor)]
|
||||
pub fn construct(
|
||||
/// The number of seconds.
|
||||
#[named]
|
||||
#[default(0)]
|
||||
seconds: i64,
|
||||
/// The number of minutes.
|
||||
#[named]
|
||||
#[default(0)]
|
||||
minutes: i64,
|
||||
/// The number of hours.
|
||||
#[named]
|
||||
#[default(0)]
|
||||
hours: i64,
|
||||
/// The number of days.
|
||||
#[named]
|
||||
#[default(0)]
|
||||
days: i64,
|
||||
/// The number of weeks.
|
||||
#[named]
|
||||
#[default(0)]
|
||||
weeks: i64,
|
||||
) -> Duration {
|
||||
Duration::from(
|
||||
time::Duration::seconds(seconds)
|
||||
+ time::Duration::minutes(minutes)
|
||||
+ time::Duration::hours(hours)
|
||||
+ time::Duration::days(days)
|
||||
+ time::Duration::weeks(weeks),
|
||||
)
|
||||
}
|
||||
|
||||
/// The duration expressed in seconds.
|
||||
///
|
||||
/// This function returns the total duration represented in seconds as a
|
||||
/// floating-point number rather than the second component of the duration.
|
||||
#[func]
|
||||
pub fn seconds(&self) -> f64 {
|
||||
self.0.as_seconds_f64()
|
||||
}
|
||||
|
||||
/// The duration expressed in minutes.
|
||||
///
|
||||
/// This function returns the total duration represented in minutes as a
|
||||
/// floating-point number rather than the second component of the duration.
|
||||
#[func]
|
||||
pub fn minutes(&self) -> f64 {
|
||||
self.seconds() / 60.0
|
||||
}
|
||||
|
||||
/// The duration expressed in hours.
|
||||
///
|
||||
/// This function returns the total duration represented in hours as a
|
||||
/// floating-point number rather than the second component of the duration.
|
||||
#[func]
|
||||
pub fn hours(&self) -> f64 {
|
||||
self.seconds() / 3_600.0
|
||||
}
|
||||
|
||||
/// The duration expressed in days.
|
||||
///
|
||||
/// This function returns the total duration represented in days as a
|
||||
/// floating-point number rather than the second component of the duration.
|
||||
#[func]
|
||||
pub fn days(&self) -> f64 {
|
||||
self.seconds() / 86_400.0
|
||||
}
|
||||
|
||||
/// The duration expressed in weeks.
|
||||
///
|
||||
/// This function returns the total duration represented in weeks as a
|
||||
/// floating-point number rather than the second component of the duration.
|
||||
#[func]
|
||||
pub fn weeks(&self) -> f64 {
|
||||
self.seconds() / 604_800.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<time::Duration> for Duration {
|
||||
fn from(value: time::Duration) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for Duration {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
let mut tmp = self.0;
|
||||
@ -85,6 +149,12 @@ impl Debug for Duration {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<time::Duration> for Duration {
|
||||
fn from(value: time::Duration) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Duration> for time::Duration {
|
||||
fn from(value: Duration) -> Self {
|
||||
value.0
|
||||
|
@ -1,22 +1,23 @@
|
||||
use ecow::{eco_format, EcoString};
|
||||
|
||||
use crate::diag::StrResult;
|
||||
use crate::geom::{Axes, GenAlign, PartialStroke, Stroke};
|
||||
use crate::geom::{Align, Length, Rel, Stroke};
|
||||
|
||||
use super::{IntoValue, Value};
|
||||
use super::{IntoValue, Type, Value};
|
||||
|
||||
/// Try to access a field on a value.
|
||||
/// This function is exclusively for types which have
|
||||
/// predefined fields, such as stroke and length.
|
||||
///
|
||||
/// This function is exclusively for types which have predefined fields, such as
|
||||
/// stroke and length.
|
||||
pub(crate) fn field(value: &Value, field: &str) -> StrResult<Value> {
|
||||
let name = value.type_name();
|
||||
let not_supported = || Err(no_fields(name));
|
||||
let missing = || Err(missing_field(name, field));
|
||||
let ty = value.ty();
|
||||
let nope = || Err(no_fields(ty));
|
||||
let missing = || Err(missing_field(ty, field));
|
||||
|
||||
// Special cases, such as module and dict, are handled by Value itself
|
||||
let result = match value {
|
||||
Value::Length(length) => match field {
|
||||
"em" => length.em.into_value(),
|
||||
"em" => length.em.get().into_value(),
|
||||
"abs" => length.abs.into_value(),
|
||||
_ => return missing(),
|
||||
},
|
||||
@ -26,44 +27,27 @@ pub(crate) fn field(value: &Value, field: &str) -> StrResult<Value> {
|
||||
_ => return missing(),
|
||||
},
|
||||
Value::Dyn(dynamic) => {
|
||||
if let Some(stroke) = dynamic.downcast::<PartialStroke>() {
|
||||
if let Some(stroke) = dynamic.downcast::<Stroke>() {
|
||||
match field {
|
||||
"paint" => stroke
|
||||
.paint
|
||||
.clone()
|
||||
.unwrap_or_else(|| Stroke::default().paint)
|
||||
.into_value(),
|
||||
"thickness" => stroke
|
||||
.thickness
|
||||
.unwrap_or_else(|| Stroke::default().thickness.into())
|
||||
.into_value(),
|
||||
"cap" => stroke
|
||||
.line_cap
|
||||
.unwrap_or_else(|| Stroke::default().line_cap)
|
||||
.into_value(),
|
||||
"join" => stroke
|
||||
.line_join
|
||||
.unwrap_or_else(|| Stroke::default().line_join)
|
||||
.into_value(),
|
||||
"dash" => stroke.dash_pattern.clone().unwrap_or(None).into_value(),
|
||||
"miter-limit" => stroke
|
||||
.miter_limit
|
||||
.unwrap_or_else(|| Stroke::default().miter_limit)
|
||||
.0
|
||||
.into_value(),
|
||||
"paint" => stroke.paint.clone().into_value(),
|
||||
"thickness" => stroke.thickness.into_value(),
|
||||
"cap" => stroke.line_cap.into_value(),
|
||||
"join" => stroke.line_join.into_value(),
|
||||
"dash" => stroke.dash_pattern.clone().into_value(),
|
||||
"miter-limit" => stroke.miter_limit.map(|limit| limit.0).into_value(),
|
||||
_ => return missing(),
|
||||
}
|
||||
} else if let Some(align2d) = dynamic.downcast::<Axes<GenAlign>>() {
|
||||
} else if let Some(align) = dynamic.downcast::<Align>() {
|
||||
match field {
|
||||
"x" => align2d.x.into_value(),
|
||||
"y" => align2d.y.into_value(),
|
||||
"x" => align.x().into_value(),
|
||||
"y" => align.y().into_value(),
|
||||
_ => return missing(),
|
||||
}
|
||||
} else {
|
||||
return not_supported();
|
||||
return nope();
|
||||
}
|
||||
}
|
||||
_ => return not_supported(),
|
||||
_ => return nope(),
|
||||
};
|
||||
|
||||
Ok(result)
|
||||
@ -71,23 +55,27 @@ pub(crate) fn field(value: &Value, field: &str) -> StrResult<Value> {
|
||||
|
||||
/// The error message for a type not supporting field access.
|
||||
#[cold]
|
||||
fn no_fields(type_name: &str) -> EcoString {
|
||||
eco_format!("cannot access fields on type {type_name}")
|
||||
fn no_fields(ty: Type) -> EcoString {
|
||||
eco_format!("cannot access fields on type {ty}")
|
||||
}
|
||||
|
||||
/// The missing field error message.
|
||||
#[cold]
|
||||
fn missing_field(type_name: &str, field: &str) -> EcoString {
|
||||
eco_format!("{type_name} does not contain field \"{field}\"")
|
||||
fn missing_field(ty: Type, field: &str) -> EcoString {
|
||||
eco_format!("{ty} does not contain field \"{field}\"")
|
||||
}
|
||||
|
||||
/// List the available fields for a type.
|
||||
pub fn fields_on(type_name: &str) -> &[&'static str] {
|
||||
match type_name {
|
||||
"length" => &["em", "abs"],
|
||||
"relative length" => &["ratio", "length"],
|
||||
"stroke" => &["paint", "thickness", "cap", "join", "dash", "miter-limit"],
|
||||
"2d alignment" => &["x", "y"],
|
||||
_ => &[],
|
||||
pub fn fields_on(ty: Type) -> &'static [&'static str] {
|
||||
if ty == Type::of::<Length>() {
|
||||
&["em", "abs"]
|
||||
} else if ty == Type::of::<Rel>() {
|
||||
&["ratio", "length"]
|
||||
} else if ty == Type::of::<Stroke>() {
|
||||
&["paint", "thickness", "cap", "join", "dash", "miter-limit"]
|
||||
} else if ty == Type::of::<Align>() {
|
||||
&["x", "y"]
|
||||
} else {
|
||||
&[]
|
||||
}
|
||||
}
|
||||
|
60
crates/typst/src/eval/float.rs
Normal file
60
crates/typst/src/eval/float.rs
Normal file
@ -0,0 +1,60 @@
|
||||
use ecow::eco_format;
|
||||
|
||||
use super::{cast, func, scope, ty, Str};
|
||||
use crate::geom::Ratio;
|
||||
|
||||
/// A floating-point number.
|
||||
///
|
||||
/// A limited-precision representation of a real number. Typst uses 64 bits to
|
||||
/// store floats. Wherever a float is expected, you can also pass an
|
||||
/// [integer]($int).
|
||||
///
|
||||
/// You can convert a value to a float with this type's constructor.
|
||||
///
|
||||
/// # Example
|
||||
/// ```example
|
||||
/// #3.14 \
|
||||
/// #1e4 \
|
||||
/// #(10 / 4)
|
||||
/// ```
|
||||
#[ty(scope, name = "float")]
|
||||
type f64;
|
||||
|
||||
#[scope]
|
||||
impl f64 {
|
||||
/// Converts a value to a float.
|
||||
///
|
||||
/// - Booleans are converted to `0.0` or `1.0`.
|
||||
/// - Integers are converted to the closest 64-bit float.
|
||||
/// - Ratios are divided by 100%.
|
||||
/// - Strings are parsed in base 10 to the closest 64-bit float.
|
||||
/// Exponential notation is supported.
|
||||
///
|
||||
/// ```example
|
||||
/// #float(false) \
|
||||
/// #float(true) \
|
||||
/// #float(4) \
|
||||
/// #float(40%) \
|
||||
/// #float("2.7") \
|
||||
/// #float("1e5")
|
||||
/// ```
|
||||
#[func(constructor)]
|
||||
pub fn construct(
|
||||
/// The value that should be converted to a float.
|
||||
value: ToFloat,
|
||||
) -> f64 {
|
||||
value.0
|
||||
}
|
||||
}
|
||||
|
||||
/// A value that can be cast to a float.
|
||||
pub struct ToFloat(f64);
|
||||
|
||||
cast! {
|
||||
ToFloat,
|
||||
v: bool => Self(v as i64 as f64),
|
||||
v: i64 => Self(v as f64),
|
||||
v: Ratio => Self(v.get()),
|
||||
v: Str => Self(v.parse().map_err(|_| eco_format!("invalid float: {}", v))?),
|
||||
v: f64 => Self(v),
|
||||
}
|
@ -1,22 +1,123 @@
|
||||
use std::fmt::{self, Debug, Formatter};
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::sync::Arc;
|
||||
|
||||
use comemo::{Prehashed, Tracked, TrackedMut};
|
||||
use ecow::eco_format;
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
use super::{
|
||||
cast, Args, CastInfo, Eval, FlowEvent, IntoValue, Route, Scope, Scopes, Tracer,
|
||||
Value, Vm,
|
||||
cast, scope, ty, Args, CastInfo, Eval, FlowEvent, IntoValue, Route, Scope, Scopes,
|
||||
Tracer, Type, Value, Vm,
|
||||
};
|
||||
use crate::diag::{bail, SourceResult, StrResult};
|
||||
use crate::model::{DelayedErrors, ElemFunc, Introspector, Locator, Vt};
|
||||
use crate::model::{
|
||||
Content, DelayedErrors, Element, Introspector, Locator, Selector, Vt,
|
||||
};
|
||||
use crate::syntax::ast::{self, AstNode};
|
||||
use crate::syntax::{FileId, Span, SyntaxNode};
|
||||
use crate::util::Static;
|
||||
use crate::World;
|
||||
|
||||
/// An evaluatable function.
|
||||
#[doc(inline)]
|
||||
pub use typst_macros::func;
|
||||
|
||||
/// A mapping from argument values to a return value.
|
||||
///
|
||||
/// You can call a function by writing a comma-separated list of function
|
||||
/// _arguments_ enclosed in parentheses directly after the function name.
|
||||
/// Additionally, you can pass any number of trailing content blocks arguments
|
||||
/// to a function _after_ the normal argument list. If the normal argument list
|
||||
/// would become empty, it can be omitted. Typst supports positional and named
|
||||
/// arguments. The former are identified by position and type, while the later
|
||||
/// are written as `name: value`.
|
||||
///
|
||||
/// Within math mode, function calls have special behaviour. See the
|
||||
/// [math documentation]($category/math) for more details.
|
||||
///
|
||||
/// # Example
|
||||
/// ```example
|
||||
/// // Call a function.
|
||||
/// #list([A], [B])
|
||||
///
|
||||
/// // Named arguments and trailing
|
||||
/// // content blocks.
|
||||
/// #enum(start: 2)[A][B]
|
||||
///
|
||||
/// // Version without parentheses.
|
||||
/// #list[A][B]
|
||||
/// ```
|
||||
///
|
||||
/// Functions are a fundamental building block of Typst. Typst provides
|
||||
/// functions for a variety of typesetting tasks. Moreover, the markup you write
|
||||
/// is backed by functions and all styling happens through functions. This
|
||||
/// reference lists all available functions and how you can use them. Please
|
||||
/// also refer to the documentation about [set]($styling/#set-rules) and
|
||||
/// [show]($styling/#show-rules) rules to learn about additional ways you can
|
||||
/// work with functions in Typst.
|
||||
///
|
||||
/// # Element functions
|
||||
/// Some functions are associated with _elements_ like [headings]($heading) or
|
||||
/// [tables]($table). When called, these create an element of their respective
|
||||
/// kind. In contrast to normal functions, they can further be used in [set
|
||||
/// rules]($styling/#set-rules), [show rules]($styling/#show-rules), and
|
||||
/// [selectors]($selector).
|
||||
///
|
||||
/// # Function scopes
|
||||
/// Functions can hold related definitions in their own scope, similar to a
|
||||
/// [module]($scripting/#modules). Examples of this are
|
||||
/// [`assert.eq`]($assert.eq) or [`list.item`]($list.item). However, this
|
||||
/// feature is currently only available for built-in functions.
|
||||
///
|
||||
/// # Defining functions
|
||||
/// You can define your own function with a [let binding]($scripting/#bindings)
|
||||
/// that has a parameter list after the binding's name. The parameter list can
|
||||
/// contain positional parameters, named parameters with default values and
|
||||
/// [argument sinks]($arguments). The right-hand side of the binding can be a
|
||||
/// block or any other expression. It defines the function's return value and
|
||||
/// can depend on the parameters.
|
||||
///
|
||||
/// ```example
|
||||
/// #let alert(body, fill: red) = {
|
||||
/// set text(white)
|
||||
/// set align(center)
|
||||
/// rect(
|
||||
/// fill: fill,
|
||||
/// inset: 8pt,
|
||||
/// radius: 4pt,
|
||||
/// [*Warning:\ #body*],
|
||||
/// )
|
||||
/// }
|
||||
///
|
||||
/// #alert[
|
||||
/// Danger is imminent!
|
||||
/// ]
|
||||
///
|
||||
/// #alert(fill: blue)[
|
||||
/// KEEP OFF TRACKS
|
||||
/// ]
|
||||
/// ```
|
||||
///
|
||||
/// # Unnamed functions { #unnamed }
|
||||
/// You can also created an unnamed function without creating a binding by
|
||||
/// specifying a parameter list followed by `=>` and the function body. If your
|
||||
/// function has just one parameter, the parentheses around the parameter list
|
||||
/// are optional. Unnamed functions are mainly useful for show rules, but also
|
||||
/// for settable properties that take functions like the page function's
|
||||
/// [`footer`]($page.footer) property.
|
||||
///
|
||||
/// ```example
|
||||
/// #show "once?": it => [#it #it]
|
||||
/// once?
|
||||
/// ```
|
||||
///
|
||||
/// # Notable fact
|
||||
/// In Typst, all functions are _pure._ This means that for the same
|
||||
/// arguments, they always return the same result. They cannot "remember" things to
|
||||
/// produce another value when they are called a second time.
|
||||
///
|
||||
/// The only exception are built-in methods like
|
||||
/// [`array.push(value)`]($array.push). These can modify the values they are
|
||||
/// called on.
|
||||
#[ty(scope, name = "function")]
|
||||
#[derive(Clone, Hash)]
|
||||
#[allow(clippy::derived_hash_with_manual_eq)]
|
||||
pub struct Func {
|
||||
@ -30,9 +131,9 @@ pub struct Func {
|
||||
#[derive(Clone, PartialEq, Hash)]
|
||||
enum Repr {
|
||||
/// A native Rust function.
|
||||
Native(&'static NativeFunc),
|
||||
Native(Static<NativeFuncData>),
|
||||
/// A function for an element.
|
||||
Elem(ElemFunc),
|
||||
Element(Element),
|
||||
/// A user-defined closure.
|
||||
Closure(Arc<Prehashed<Closure>>),
|
||||
/// A nested function with pre-applied arguments.
|
||||
@ -40,37 +141,106 @@ enum Repr {
|
||||
}
|
||||
|
||||
impl Func {
|
||||
/// The name of the function.
|
||||
/// The function's name (e.g. `min`).
|
||||
///
|
||||
/// Returns `None` if this is an anonymous closure.
|
||||
pub fn name(&self) -> Option<&str> {
|
||||
match &self.repr {
|
||||
Repr::Native(native) => Some(native.info.name),
|
||||
Repr::Elem(func) => Some(func.info().name),
|
||||
Repr::Native(native) => Some(native.name),
|
||||
Repr::Element(elem) => Some(elem.name()),
|
||||
Repr::Closure(closure) => closure.name(),
|
||||
Repr::With(arc) => arc.0.name(),
|
||||
Repr::With(with) => with.0.name(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract details the function.
|
||||
pub fn info(&self) -> Option<&FuncInfo> {
|
||||
/// The function's title case name, for use in documentation (e.g. `Minimum`).
|
||||
///
|
||||
/// Returns `None` if this is a closure.
|
||||
pub fn title(&self) -> Option<&'static str> {
|
||||
match &self.repr {
|
||||
Repr::Native(native) => Some(&native.info),
|
||||
Repr::Elem(func) => Some(func.info()),
|
||||
Repr::Native(native) => Some(native.title),
|
||||
Repr::Element(elem) => Some(elem.title()),
|
||||
Repr::Closure(_) => None,
|
||||
Repr::With(arc) => arc.0.info(),
|
||||
Repr::With(with) => with.0.title(),
|
||||
}
|
||||
}
|
||||
|
||||
/// The function's span.
|
||||
pub fn span(&self) -> Span {
|
||||
self.span
|
||||
/// Documentation for the function (as Markdown).
|
||||
pub fn docs(&self) -> Option<&'static str> {
|
||||
match &self.repr {
|
||||
Repr::Native(native) => Some(native.docs),
|
||||
Repr::Element(elem) => Some(elem.docs()),
|
||||
Repr::Closure(_) => None,
|
||||
Repr::With(with) => with.0.docs(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Attach a span to this function if it doesn't already have one.
|
||||
pub fn spanned(mut self, span: Span) -> Self {
|
||||
if self.span.is_detached() {
|
||||
self.span = span;
|
||||
/// Get details about this function's parameters if available.
|
||||
pub fn params(&self) -> Option<&'static [ParamInfo]> {
|
||||
match &self.repr {
|
||||
Repr::Native(native) => Some(&native.0.params),
|
||||
Repr::Element(elem) => Some(elem.params()),
|
||||
Repr::Closure(_) => None,
|
||||
Repr::With(with) => with.0.params(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the parameter info for a parameter with the given name if it exist.
|
||||
pub fn param(&self, name: &str) -> Option<&'static ParamInfo> {
|
||||
self.params()?.iter().find(|param| param.name == name)
|
||||
}
|
||||
|
||||
/// Get details about the function's return type.
|
||||
pub fn returns(&self) -> Option<&'static CastInfo> {
|
||||
static CONTENT: Lazy<CastInfo> =
|
||||
Lazy::new(|| CastInfo::Type(Type::of::<Content>()));
|
||||
match &self.repr {
|
||||
Repr::Native(native) => Some(&native.0.returns),
|
||||
Repr::Element(_) => Some(&CONTENT),
|
||||
Repr::Closure(_) => None,
|
||||
Repr::With(with) => with.0.returns(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Search keywords for the function.
|
||||
pub fn keywords(&self) -> &'static [&'static str] {
|
||||
match &self.repr {
|
||||
Repr::Native(native) => native.keywords,
|
||||
Repr::Element(elem) => elem.keywords(),
|
||||
Repr::Closure(_) => &[],
|
||||
Repr::With(with) => with.0.keywords(),
|
||||
}
|
||||
}
|
||||
|
||||
/// The function's associated scope of sub-definition.
|
||||
pub fn scope(&self) -> Option<&'static Scope> {
|
||||
match &self.repr {
|
||||
Repr::Native(native) => Some(&native.0.scope),
|
||||
Repr::Element(elem) => Some(elem.scope()),
|
||||
Repr::Closure(_) => None,
|
||||
Repr::With(with) => with.0.scope(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a field from this function's scope, if possible.
|
||||
pub fn field(&self, field: &str) -> StrResult<&'static Value> {
|
||||
let scope =
|
||||
self.scope().ok_or("cannot access fields on user-defined functions")?;
|
||||
match scope.get(field) {
|
||||
Some(field) => Ok(field),
|
||||
None => match self.name() {
|
||||
Some(name) => bail!("function `{name}` does not contain field `{field}`"),
|
||||
None => bail!("function does not contain field `{field}`"),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract the element function, if it is one.
|
||||
pub fn element(&self) -> Option<Element> {
|
||||
match self.repr {
|
||||
Repr::Element(func) => Some(func),
|
||||
_ => None,
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Call the function with the given arguments.
|
||||
@ -83,11 +253,11 @@ impl Func {
|
||||
|
||||
match &self.repr {
|
||||
Repr::Native(native) => {
|
||||
let value = (native.func)(vm, &mut args)?;
|
||||
let value = (native.function)(vm, &mut args)?;
|
||||
args.finish()?;
|
||||
Ok(value)
|
||||
}
|
||||
Repr::Elem(func) => {
|
||||
Repr::Element(func) => {
|
||||
let value = func.construct(vm, &mut args)?;
|
||||
args.finish()?;
|
||||
Ok(Value::Content(value))
|
||||
@ -109,9 +279,9 @@ impl Func {
|
||||
args,
|
||||
)
|
||||
}
|
||||
Repr::With(arc) => {
|
||||
args.items = arc.1.items.iter().cloned().chain(args.items).collect();
|
||||
arc.0.call_vm(vm, args)
|
||||
Repr::With(with) => {
|
||||
args.items = with.1.items.iter().cloned().chain(args.items).collect();
|
||||
with.0.call_vm(vm, args)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -138,42 +308,56 @@ impl Func {
|
||||
self.call_vm(&mut vm, args)
|
||||
}
|
||||
|
||||
/// Apply the given arguments to the function.
|
||||
pub fn with(self, args: Args) -> Self {
|
||||
/// The function's span.
|
||||
pub fn span(&self) -> Span {
|
||||
self.span
|
||||
}
|
||||
|
||||
/// Attach a span to this function if it doesn't already have one.
|
||||
pub fn spanned(mut self, span: Span) -> Self {
|
||||
if self.span.is_detached() {
|
||||
self.span = span;
|
||||
}
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[scope]
|
||||
impl Func {
|
||||
/// Returns a new function that has the given arguments pre-applied.
|
||||
#[func]
|
||||
pub fn with(
|
||||
self,
|
||||
/// The real arguments (the other argument is just for the docs).
|
||||
/// The docs argument cannot be called `args`.
|
||||
args: Args,
|
||||
/// The arguments to apply to the function.
|
||||
#[external]
|
||||
arguments: Args,
|
||||
) -> Func {
|
||||
let span = self.span;
|
||||
Self { repr: Repr::With(Arc::new((self, args))), span }
|
||||
}
|
||||
|
||||
/// Extract the element function, if it is one.
|
||||
pub fn element(&self) -> Option<ElemFunc> {
|
||||
match self.repr {
|
||||
Repr::Elem(func) => Some(func),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a field from this function's scope, if possible.
|
||||
pub fn get(&self, field: &str) -> StrResult<&Value> {
|
||||
match &self.repr {
|
||||
Repr::Native(func) => func.info.scope.get(field).ok_or_else(|| {
|
||||
eco_format!(
|
||||
"function `{}` does not contain field `{}`",
|
||||
func.info.name,
|
||||
field
|
||||
)
|
||||
}),
|
||||
Repr::Elem(func) => func.info().scope.get(field).ok_or_else(|| {
|
||||
eco_format!(
|
||||
"function `{}` does not contain field `{}`",
|
||||
func.name(),
|
||||
field
|
||||
)
|
||||
}),
|
||||
Repr::Closure(_) => {
|
||||
Err(eco_format!("cannot access fields on user-defined functions"))
|
||||
}
|
||||
Repr::With(arc) => arc.0.get(field),
|
||||
}
|
||||
/// Returns a selector that filters for elements belonging to this function
|
||||
/// whose fields have the values of the given arguments.
|
||||
#[func]
|
||||
pub fn where_(
|
||||
self,
|
||||
/// The real arguments (the other argument is just for the docs).
|
||||
/// The docs argument cannot be called `args`.
|
||||
args: Args,
|
||||
/// The fields to filter for.
|
||||
#[external]
|
||||
fields: Args,
|
||||
) -> StrResult<Selector> {
|
||||
let mut args = args;
|
||||
let fields = args.to_named();
|
||||
args.items.retain(|arg| arg.name.is_none());
|
||||
Ok(self
|
||||
.element()
|
||||
.ok_or("`where()` can only be called on element functions")?
|
||||
.where_(fields))
|
||||
}
|
||||
}
|
||||
|
||||
@ -198,82 +382,56 @@ impl From<Repr> for Func {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ElemFunc> for Func {
|
||||
fn from(func: ElemFunc) -> Self {
|
||||
Repr::Elem(func).into()
|
||||
impl From<Element> for Func {
|
||||
fn from(func: Element) -> Self {
|
||||
Repr::Element(func).into()
|
||||
}
|
||||
}
|
||||
|
||||
/// A Typst function defined by a native Rust function.
|
||||
pub struct NativeFunc {
|
||||
/// The function's implementation.
|
||||
pub func: fn(&mut Vm, &mut Args) -> SourceResult<Value>,
|
||||
/// Details about the function.
|
||||
pub info: Lazy<FuncInfo>,
|
||||
}
|
||||
|
||||
impl PartialEq for NativeFunc {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.func as usize == other.func as usize
|
||||
/// A Typst function that is defined by a native Rust type that shadows a
|
||||
/// native Rust function.
|
||||
pub trait NativeFunc {
|
||||
/// Get the function for the native Rust type.
|
||||
fn func() -> Func {
|
||||
Func::from(Self::data())
|
||||
}
|
||||
|
||||
/// Get the function data for the native Rust type.
|
||||
fn data() -> &'static NativeFuncData;
|
||||
}
|
||||
|
||||
impl Eq for NativeFunc {}
|
||||
|
||||
impl Hash for NativeFunc {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
(self.func as usize).hash(state);
|
||||
}
|
||||
/// Defines a native function.
|
||||
pub struct NativeFuncData {
|
||||
pub function: fn(&mut Vm, &mut Args) -> SourceResult<Value>,
|
||||
pub name: &'static str,
|
||||
pub title: &'static str,
|
||||
pub docs: &'static str,
|
||||
pub keywords: &'static [&'static str],
|
||||
pub scope: Lazy<Scope>,
|
||||
pub params: Lazy<Vec<ParamInfo>>,
|
||||
pub returns: Lazy<CastInfo>,
|
||||
}
|
||||
|
||||
impl From<&'static NativeFunc> for Func {
|
||||
fn from(native: &'static NativeFunc) -> Self {
|
||||
Repr::Native(native).into()
|
||||
impl From<&'static NativeFuncData> for Func {
|
||||
fn from(data: &'static NativeFuncData) -> Self {
|
||||
Repr::Native(Static(data)).into()
|
||||
}
|
||||
}
|
||||
|
||||
cast! {
|
||||
&'static NativeFunc,
|
||||
self => Value::Func(self.into()),
|
||||
&'static NativeFuncData,
|
||||
self => Func::from(self).into_value(),
|
||||
}
|
||||
|
||||
/// Details about a function.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FuncInfo {
|
||||
/// The function's name.
|
||||
pub name: &'static str,
|
||||
/// The display name of the function.
|
||||
pub display: &'static str,
|
||||
/// 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 values for the return value.
|
||||
pub returns: CastInfo,
|
||||
/// The function's own scope of fields and sub-functions.
|
||||
pub scope: Scope,
|
||||
}
|
||||
|
||||
impl FuncInfo {
|
||||
/// Get the parameter info for a parameter with the given name
|
||||
pub fn param(&self, name: &str) -> Option<&ParamInfo> {
|
||||
self.params.iter().find(|param| param.name == name)
|
||||
}
|
||||
}
|
||||
|
||||
/// Describes a named parameter.
|
||||
/// Describes a function parameter.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ParamInfo {
|
||||
/// The parameter's name.
|
||||
pub name: &'static str,
|
||||
/// Documentation for the parameter.
|
||||
pub docs: &'static str,
|
||||
/// Valid values for the parameter.
|
||||
pub cast: CastInfo,
|
||||
/// Describe what values this parameter accepts.
|
||||
pub input: CastInfo,
|
||||
/// Creates an instance of the parameter's default value.
|
||||
pub default: Option<fn() -> Value>,
|
||||
/// Is the parameter positional?
|
||||
|
@ -1,12 +1,72 @@
|
||||
use std::num::{NonZeroI64, NonZeroIsize, NonZeroU64, NonZeroUsize};
|
||||
|
||||
use super::{cast, Value};
|
||||
use ecow::eco_format;
|
||||
|
||||
use super::{cast, func, scope, ty, Str, Value};
|
||||
|
||||
/// A whole number.
|
||||
///
|
||||
/// The number can be negative, zero, or positive. As Typst uses 64 bits to
|
||||
/// store integers, integers cannot be smaller than `{-9223372036854775808}` or
|
||||
/// larger than `{9223372036854775807}`.
|
||||
///
|
||||
/// The number can also be specified as hexadecimal, octal, or binary by
|
||||
/// starting it with a zero followed by either `x`, `o`, or `b`.
|
||||
///
|
||||
/// You can convert a value to an integer with this type's constructor.
|
||||
///
|
||||
/// # Example
|
||||
/// ```example
|
||||
/// #(1 + 2) \
|
||||
/// #(2 - 5) \
|
||||
/// #(3 + 4 < 8)
|
||||
///
|
||||
/// #0xff \
|
||||
/// #0o10 \
|
||||
/// #0b1001
|
||||
/// ```
|
||||
#[ty(scope, name = "int", title = "Integer")]
|
||||
type i64;
|
||||
|
||||
#[scope]
|
||||
impl i64 {
|
||||
/// Converts a value to an integer.
|
||||
///
|
||||
/// - Booleans are converted to `0` or `1`.
|
||||
/// - Floats are floored to the next 64-bit integer.
|
||||
/// - Strings are parsed in base 10.
|
||||
///
|
||||
/// ```example
|
||||
/// #int(false) \
|
||||
/// #int(true) \
|
||||
/// #int(2.7) \
|
||||
/// #(int("27") + int("4"))
|
||||
/// ```
|
||||
#[func(constructor)]
|
||||
pub fn construct(
|
||||
/// The value that should be converted to an integer.
|
||||
value: ToInt,
|
||||
) -> i64 {
|
||||
value.0
|
||||
}
|
||||
}
|
||||
|
||||
/// A value that can be cast to an integer.
|
||||
pub struct ToInt(i64);
|
||||
|
||||
cast! {
|
||||
ToInt,
|
||||
v: bool => Self(v as i64),
|
||||
v: f64 => Self(v as i64),
|
||||
v: Str => Self(v.parse().map_err(|_| eco_format!("invalid integer: {}", v))?),
|
||||
v: i64 => Self(v),
|
||||
}
|
||||
|
||||
macro_rules! signed_int {
|
||||
($($ty:ty)*) => {
|
||||
$(cast! {
|
||||
$ty,
|
||||
self => Value::Int(self as i64),
|
||||
self => Value::Int(self as _),
|
||||
v: i64 => v.try_into().map_err(|_| "number too large")?,
|
||||
})*
|
||||
}
|
||||
@ -16,7 +76,7 @@ macro_rules! unsigned_int {
|
||||
($($ty:ty)*) => {
|
||||
$(cast! {
|
||||
$ty,
|
||||
self => Value::Int(self as i64),
|
||||
self => Value::Int(self as _),
|
||||
v: i64 => v.try_into().map_err(|_| {
|
||||
if v < 0 {
|
||||
"number must be at least zero"
|
||||
@ -28,54 +88,55 @@ macro_rules! unsigned_int {
|
||||
}
|
||||
}
|
||||
|
||||
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"
|
||||
})?,
|
||||
})*
|
||||
}
|
||||
signed_int! { i8 i16 i32 isize }
|
||||
unsigned_int! { u8 u16 u32 u64 usize }
|
||||
|
||||
cast! {
|
||||
NonZeroI64,
|
||||
self => Value::Int(self.get() as _),
|
||||
v: i64 => v.try_into()
|
||||
.map_err(|_| 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"
|
||||
})?,
|
||||
})*
|
||||
}
|
||||
cast! {
|
||||
NonZeroIsize,
|
||||
self => Value::Int(self.get() as _),
|
||||
v: i64 => v
|
||||
.try_into()
|
||||
.and_then(|v: isize| v.try_into())
|
||||
.map_err(|_| if v == 0 {
|
||||
"number must not be zero"
|
||||
} else {
|
||||
"number too large"
|
||||
})?,
|
||||
}
|
||||
|
||||
signed_int! {
|
||||
i8 i16 i32 isize
|
||||
cast! {
|
||||
NonZeroU64,
|
||||
self => Value::Int(self.get() as _),
|
||||
v: i64 => v
|
||||
.try_into()
|
||||
.and_then(|v: u64| v.try_into())
|
||||
.map_err(|_| if v <= 0 {
|
||||
"number must be positive"
|
||||
} else {
|
||||
"number too large"
|
||||
})?,
|
||||
}
|
||||
|
||||
unsigned_int! {
|
||||
u8 u16 u32 u64 usize
|
||||
}
|
||||
|
||||
signed_nonzero! {
|
||||
NonZeroI64 NonZeroIsize
|
||||
}
|
||||
|
||||
unsigned_nonzero! {
|
||||
NonZeroU64 NonZeroUsize
|
||||
cast! {
|
||||
NonZeroUsize,
|
||||
self => Value::Int(self.get() as _),
|
||||
v: i64 => v
|
||||
.try_into()
|
||||
.and_then(|v: usize| v.try_into())
|
||||
.map_err(|_| if v <= 0 {
|
||||
"number must be positive"
|
||||
} else {
|
||||
"number too large"
|
||||
})?,
|
||||
}
|
||||
|
@ -6,12 +6,11 @@ use comemo::Tracked;
|
||||
use ecow::EcoString;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use super::{Args, Dynamic, Module, NativeFunc, Value, Vm};
|
||||
use super::Module;
|
||||
use crate::diag::SourceResult;
|
||||
use crate::doc::Document;
|
||||
use crate::geom::{Abs, Dir};
|
||||
use crate::model::{Content, ElemFunc, Introspector, Label, StyleChain, Styles, Vt};
|
||||
use crate::syntax::Span;
|
||||
use crate::model::{Content, Element, Introspector, Label, StyleChain, Styles, Vt};
|
||||
use crate::util::hash128;
|
||||
|
||||
/// Definition of Typst's standard library.
|
||||
@ -43,8 +42,8 @@ pub struct LangItems {
|
||||
pub linebreak: fn() -> Content,
|
||||
/// Plain text without markup.
|
||||
pub text: fn(text: EcoString) -> Content,
|
||||
/// The text function.
|
||||
pub text_func: ElemFunc,
|
||||
/// The text element.
|
||||
pub text_elem: Element,
|
||||
/// Get the string if this is a text element.
|
||||
pub text_str: fn(&Content) -> Option<EcoString>,
|
||||
/// A smart quote: `'` or `"`.
|
||||
@ -69,20 +68,14 @@ pub struct LangItems {
|
||||
fn(introspector: Tracked<Introspector>) -> Vec<(EcoString, Option<EcoString>)>,
|
||||
/// A section heading: `= Introduction`.
|
||||
pub heading: fn(level: NonZeroUsize, body: Content) -> Content,
|
||||
/// The heading function.
|
||||
pub heading_func: ElemFunc,
|
||||
/// The heading element.
|
||||
pub heading_elem: Element,
|
||||
/// An item in a bullet list: `- ...`.
|
||||
pub list_item: fn(body: Content) -> Content,
|
||||
/// An item in an enumeration (numbered list): `+ ...` or `1. ...`.
|
||||
pub enum_item: fn(number: Option<usize>, body: Content) -> Content,
|
||||
/// An item in a term list: `/ Term: Details`.
|
||||
pub term_item: fn(term: Content, description: Content) -> Content,
|
||||
/// The constructor for the 'rgba' color kind.
|
||||
pub rgb_func: &'static NativeFunc,
|
||||
/// The constructor for the 'cmyk' color kind.
|
||||
pub cmyk_func: &'static NativeFunc,
|
||||
/// The constructor for the 'luma' color kind.
|
||||
pub luma_func: &'static NativeFunc,
|
||||
/// A mathematical equation: `$x$`, `$ x^2 $`.
|
||||
pub equation: fn(body: Content, block: bool) -> Content,
|
||||
/// An alignment point in math: `&`.
|
||||
@ -110,14 +103,6 @@ pub struct LangItems {
|
||||
pub math_frac: fn(num: Content, denom: Content) -> Content,
|
||||
/// A root in math: `√x`, `∛x` or `∜x`.
|
||||
pub math_root: fn(index: Option<Content>, radicand: Content) -> Content,
|
||||
/// Dispatch a method on a library value.
|
||||
pub library_method: fn(
|
||||
vm: &mut Vm,
|
||||
dynamic: &Dynamic,
|
||||
method: &str,
|
||||
args: Args,
|
||||
span: Span,
|
||||
) -> SourceResult<Value>,
|
||||
}
|
||||
|
||||
impl Debug for LangItems {
|
||||
@ -134,7 +119,7 @@ impl Hash for LangItems {
|
||||
self.space.hash(state);
|
||||
self.linebreak.hash(state);
|
||||
self.text.hash(state);
|
||||
self.text_func.hash(state);
|
||||
self.text_elem.hash(state);
|
||||
(self.text_str as usize).hash(state);
|
||||
self.smart_quote.hash(state);
|
||||
self.parbreak.hash(state);
|
||||
@ -146,13 +131,10 @@ impl Hash for LangItems {
|
||||
self.reference.hash(state);
|
||||
(self.bibliography_keys as usize).hash(state);
|
||||
self.heading.hash(state);
|
||||
self.heading_func.hash(state);
|
||||
self.heading_elem.hash(state);
|
||||
self.list_item.hash(state);
|
||||
self.enum_item.hash(state);
|
||||
self.term_item.hash(state);
|
||||
self.rgb_func.hash(state);
|
||||
self.cmyk_func.hash(state);
|
||||
self.luma_func.hash(state);
|
||||
self.equation.hash(state);
|
||||
self.math_align_point.hash(state);
|
||||
self.math_delimited.hash(state);
|
||||
@ -160,7 +142,6 @@ impl Hash for LangItems {
|
||||
self.math_accent.hash(state);
|
||||
self.math_frac.hash(state);
|
||||
self.math_root.hash(state);
|
||||
(self.library_method as usize).hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,323 +1,36 @@
|
||||
//! Methods on values.
|
||||
//! Handles special built-in methods on values.
|
||||
|
||||
use ecow::{eco_format, EcoString};
|
||||
|
||||
use super::{Args, Bytes, IntoValue, Plugin, Str, Value, Vm};
|
||||
use crate::diag::{At, Hint, SourceResult};
|
||||
use crate::eval::bail;
|
||||
use crate::geom::{Align, Axes, Color, Dir, Em, GenAlign};
|
||||
use crate::model::{Location, Selector};
|
||||
use super::{Args, Array, Dict, Str, Type, Value};
|
||||
use crate::diag::{At, SourceResult};
|
||||
use crate::syntax::Span;
|
||||
|
||||
/// Call a method on a value.
|
||||
pub fn call(
|
||||
vm: &mut Vm,
|
||||
value: Value,
|
||||
method: &str,
|
||||
mut args: Args,
|
||||
span: Span,
|
||||
) -> SourceResult<Value> {
|
||||
let name = value.type_name();
|
||||
let missing = || Err(missing_method(name, method)).at(span);
|
||||
/// Whether a specific method is mutating.
|
||||
pub fn is_mutating(method: &str) -> bool {
|
||||
matches!(method, "push" | "pop" | "insert" | "remove")
|
||||
}
|
||||
|
||||
let output = match value {
|
||||
Value::Color(color) => match method {
|
||||
"lighten" => color.lighten(args.expect("amount")?).into_value(),
|
||||
"darken" => color.darken(args.expect("amount")?).into_value(),
|
||||
"negate" => color.negate().into_value(),
|
||||
"kind" => match color {
|
||||
Color::Luma(_) => vm.items.luma_func.into_value(),
|
||||
Color::Rgba(_) => vm.items.rgb_func.into_value(),
|
||||
Color::Cmyk(_) => vm.items.cmyk_func.into_value(),
|
||||
},
|
||||
"hex" => color.to_rgba().to_hex().into_value(),
|
||||
"rgba" => color.to_rgba().to_array().into_value(),
|
||||
"cmyk" => match color {
|
||||
Color::Luma(luma) => luma.to_cmyk().to_array().into_value(),
|
||||
Color::Rgba(_) => {
|
||||
bail!(span, "cannot obtain cmyk values from rgba color")
|
||||
}
|
||||
Color::Cmyk(cmyk) => cmyk.to_array().into_value(),
|
||||
},
|
||||
"luma" => match color {
|
||||
Color::Luma(luma) => luma.0.into_value(),
|
||||
Color::Rgba(_) => {
|
||||
bail!(span, "cannot obtain the luma value of rgba color")
|
||||
}
|
||||
Color::Cmyk(_) => {
|
||||
bail!(span, "cannot obtain the luma value of cmyk color")
|
||||
}
|
||||
},
|
||||
_ => return missing(),
|
||||
},
|
||||
/// Whether a specific method is an accessor.
|
||||
pub fn is_accessor(method: &str) -> bool {
|
||||
matches!(method, "first" | "last" | "at")
|
||||
}
|
||||
|
||||
Value::Str(string) => match method {
|
||||
"len" => string.len().into_value(),
|
||||
"first" => string.first().at(span)?.into_value(),
|
||||
"last" => string.last().at(span)?.into_value(),
|
||||
"at" => string
|
||||
.at(args.expect("index")?, args.named("default")?)
|
||||
.at(span)?
|
||||
.into_value(),
|
||||
"slice" => {
|
||||
let start = args.expect("start")?;
|
||||
let mut end = args.eat()?;
|
||||
if end.is_none() {
|
||||
end = args.named("count")?.map(|c: i64| start + c);
|
||||
}
|
||||
string.slice(start, end).at(span)?.into_value()
|
||||
}
|
||||
"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")?;
|
||||
string.replace(vm, pattern, with, count)?.into_value()
|
||||
}
|
||||
"rev" => string.rev().into_value(),
|
||||
"trim" => {
|
||||
let pattern = args.eat()?;
|
||||
let at = args.named("at")?;
|
||||
let repeat = args.named("repeat")?.unwrap_or(true);
|
||||
string.trim(pattern, at, repeat).into_value()
|
||||
}
|
||||
"split" => string.split(args.eat()?).into_value(),
|
||||
_ => return missing(),
|
||||
},
|
||||
|
||||
Value::Bytes(bytes) => match method {
|
||||
"len" => bytes.len().into_value(),
|
||||
"at" => bytes.at(args.expect("index")?, args.named("default")?).at(span)?,
|
||||
"slice" => {
|
||||
let start = args.expect("start")?;
|
||||
let mut end = args.eat()?;
|
||||
if end.is_none() {
|
||||
end = args.named("count")?.map(|c: i64| start + c);
|
||||
}
|
||||
bytes.slice(start, end).at(span)?.into_value()
|
||||
}
|
||||
_ => return missing(),
|
||||
},
|
||||
|
||||
Value::Datetime(datetime) => match method {
|
||||
"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(),
|
||||
"ordinal" => datetime.ordinal().into_value(),
|
||||
_ => return missing(),
|
||||
},
|
||||
|
||||
Value::Duration(duration) => match method {
|
||||
"seconds" => duration.seconds().into_value(),
|
||||
"minutes" => duration.minutes().into_value(),
|
||||
"hours" => duration.hours().into_value(),
|
||||
"days" => duration.days().into_value(),
|
||||
"weeks" => duration.weeks().into_value(),
|
||||
_ => return missing(),
|
||||
},
|
||||
|
||||
Value::Content(content) => match method {
|
||||
"func" => content.func().into_value(),
|
||||
"has" => content.has(&args.expect::<EcoString>("field")?).into_value(),
|
||||
"at" => content
|
||||
.at(&args.expect::<Str>("field")?, args.named("default")?)
|
||||
.at(span)?,
|
||||
"fields" => content.dict().into_value(),
|
||||
"location" => content
|
||||
.location()
|
||||
.ok_or("this method can only be called on content returned by query(..)")
|
||||
.at(span)?
|
||||
.into_value(),
|
||||
_ => return missing(),
|
||||
},
|
||||
|
||||
Value::Array(array) => match method {
|
||||
"len" => array.len().into_value(),
|
||||
"first" => array.first().at(span)?.clone(),
|
||||
"last" => array.last().at(span)?.clone(),
|
||||
"at" => array.at(args.expect("index")?, args.named("default")?).at(span)?,
|
||||
"slice" => {
|
||||
let start = args.expect("start")?;
|
||||
let mut end = args.eat()?;
|
||||
if end.is_none() {
|
||||
end = args.named("count")?.map(|c: i64| start + c);
|
||||
}
|
||||
array.slice(start, end).at(span)?.into_value()
|
||||
}
|
||||
"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" => 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)?
|
||||
}
|
||||
"intersperse" => array.intersperse(args.expect("separator")?).into_value(),
|
||||
"sorted" => array.sorted(vm, span, args.named("key")?)?.into_value(),
|
||||
"zip" => array.zip(&mut args)?.into_value(),
|
||||
"enumerate" => array
|
||||
.enumerate(args.named("start")?.unwrap_or(0))
|
||||
.at(span)?
|
||||
.into_value(),
|
||||
"dedup" => array.dedup(vm, args.named("key")?)?.into_value(),
|
||||
_ => return missing(),
|
||||
},
|
||||
|
||||
Value::Dict(dict) => match method {
|
||||
"len" => dict.len().into_value(),
|
||||
"at" => dict
|
||||
.at(&args.expect::<Str>("key")?, args.named("default")?)
|
||||
.at(span)?,
|
||||
"keys" => dict.keys().into_value(),
|
||||
"values" => dict.values().into_value(),
|
||||
"pairs" => dict.pairs().into_value(),
|
||||
_ => {
|
||||
return if matches!(dict.at(method, None), Ok(Value::Func(_))) {
|
||||
Err(missing_method(name, method))
|
||||
.hint(eco_format!(
|
||||
"to call the function stored in the dictionary, surround the field access with parentheses"
|
||||
))
|
||||
.at(span)
|
||||
} else {
|
||||
missing()
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
Value::Func(func) => match method {
|
||||
"with" => func.with(args.take()).into_value(),
|
||||
"where" => {
|
||||
let fields = args.to_named();
|
||||
args.items.retain(|arg| arg.name.is_none());
|
||||
func.element()
|
||||
.ok_or("`where()` can only be called on element functions")
|
||||
.at(span)?
|
||||
.where_(fields)
|
||||
.into_value()
|
||||
}
|
||||
_ => return missing(),
|
||||
},
|
||||
|
||||
Value::Length(length) => match method {
|
||||
unit @ ("pt" | "cm" | "mm" | "inches") => {
|
||||
if length.em != Em::zero() {
|
||||
return Err(eco_format!("cannot convert a length with non-zero em units ({length:?}) to {unit}"))
|
||||
.hint(eco_format!("use 'length.abs.{unit}()' instead to ignore its em component"))
|
||||
.at(span);
|
||||
}
|
||||
match unit {
|
||||
"pt" => length.abs.to_pt().into_value(),
|
||||
"cm" => length.abs.to_cm().into_value(),
|
||||
"mm" => length.abs.to_mm().into_value(),
|
||||
"inches" => length.abs.to_inches().into_value(),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
_ => return missing(),
|
||||
},
|
||||
|
||||
Value::Angle(angle) => match method {
|
||||
"deg" => angle.to_deg().into_value(),
|
||||
"rad" => angle.to_rad().into_value(),
|
||||
_ => return missing(),
|
||||
},
|
||||
|
||||
Value::Args(args) => match method {
|
||||
"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_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_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_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_value()
|
||||
}
|
||||
_ => return missing(),
|
||||
}
|
||||
} else if let Some(direction) = dynamic.downcast::<Dir>() {
|
||||
match method {
|
||||
"axis" => direction.axis().description().into_value(),
|
||||
"start" => {
|
||||
GenAlign::from(Align::from(direction.start())).into_value()
|
||||
}
|
||||
"end" => GenAlign::from(Align::from(direction.end())).into_value(),
|
||||
"inv" => direction.inv().into_value(),
|
||||
_ => return missing(),
|
||||
}
|
||||
} else if let Some(align) = dynamic.downcast::<GenAlign>() {
|
||||
match method {
|
||||
"axis" => align.axis().description().into_value(),
|
||||
"inv" => align.inv().into_value(),
|
||||
_ => return missing(),
|
||||
}
|
||||
} else if let Some(align2d) = dynamic.downcast::<Axes<GenAlign>>() {
|
||||
match method {
|
||||
"inv" => align2d.map(GenAlign::inv).into_value(),
|
||||
_ => return missing(),
|
||||
}
|
||||
} else if let Some(plugin) = dynamic.downcast::<Plugin>() {
|
||||
if plugin.iter().any(|func_name| func_name == method) {
|
||||
let bytes = args.all::<Bytes>()?;
|
||||
args.take().finish()?;
|
||||
plugin.call(method, bytes).at(span)?.into_value()
|
||||
} else {
|
||||
return missing();
|
||||
}
|
||||
} else {
|
||||
return (vm.items.library_method)(vm, &dynamic, method, args, span);
|
||||
}
|
||||
}
|
||||
|
||||
_ => return missing(),
|
||||
};
|
||||
|
||||
args.finish()?;
|
||||
Ok(output)
|
||||
/// List the available methods for a type and whether they take arguments.
|
||||
pub fn mutable_methods_on(ty: Type) -> &'static [(&'static str, bool)] {
|
||||
if ty == Type::of::<Array>() {
|
||||
&[
|
||||
("first", false),
|
||||
("last", false),
|
||||
("at", true),
|
||||
("pop", false),
|
||||
("push", true),
|
||||
("insert", true),
|
||||
("remove", true),
|
||||
]
|
||||
} else if ty == Type::of::<Dict>() {
|
||||
&[("at", true), ("insert", true), ("remove", true)]
|
||||
} else {
|
||||
&[]
|
||||
}
|
||||
}
|
||||
|
||||
/// Call a mutating method on a value.
|
||||
@ -327,8 +40,8 @@ pub fn call_mut(
|
||||
mut args: Args,
|
||||
span: Span,
|
||||
) -> SourceResult<Value> {
|
||||
let name = value.type_name();
|
||||
let missing = || Err(missing_method(name, method)).at(span);
|
||||
let ty = value.ty();
|
||||
let missing = || Err(missing_method(ty, method)).at(span);
|
||||
let mut output = Value::None;
|
||||
|
||||
match value {
|
||||
@ -344,9 +57,7 @@ pub fn call_mut(
|
||||
|
||||
Value::Dict(dict) => match method {
|
||||
"insert" => dict.insert(args.expect::<Str>("key")?, args.expect("value")?),
|
||||
"remove" => {
|
||||
output = dict.remove(&args.expect::<EcoString>("key")?).at(span)?
|
||||
}
|
||||
"remove" => output = dict.remove(args.expect::<Str>("key")?).at(span)?,
|
||||
_ => return missing(),
|
||||
},
|
||||
|
||||
@ -364,8 +75,8 @@ pub fn call_access<'a>(
|
||||
mut args: Args,
|
||||
span: Span,
|
||||
) -> SourceResult<&'a mut Value> {
|
||||
let name = value.type_name();
|
||||
let missing = || Err(missing_method(name, method)).at(span);
|
||||
let ty = value.ty();
|
||||
let missing = || Err(missing_method(ty, method)).at(span);
|
||||
|
||||
let slot = match value {
|
||||
Value::Array(array) => match method {
|
||||
@ -385,134 +96,8 @@ pub fn call_access<'a>(
|
||||
Ok(slot)
|
||||
}
|
||||
|
||||
/// Whether a specific method is mutating.
|
||||
pub fn is_mutating(method: &str) -> bool {
|
||||
matches!(method, "push" | "pop" | "insert" | "remove")
|
||||
}
|
||||
|
||||
/// Whether a specific method is an accessor.
|
||||
pub fn is_accessor(method: &str) -> bool {
|
||||
matches!(method, "first" | "last" | "at")
|
||||
}
|
||||
|
||||
/// The missing method error message.
|
||||
#[cold]
|
||||
fn missing_method(type_name: &str, method: &str) -> String {
|
||||
format!("type {type_name} has no method `{method}`")
|
||||
}
|
||||
|
||||
/// List the available methods for a type and whether they take arguments.
|
||||
pub fn methods_on(type_name: &str) -> &[(&'static str, bool)] {
|
||||
match type_name {
|
||||
"color" => &[
|
||||
("lighten", true),
|
||||
("darken", true),
|
||||
("negate", false),
|
||||
("kind", false),
|
||||
("hex", false),
|
||||
("rgba", false),
|
||||
("cmyk", false),
|
||||
("luma", false),
|
||||
],
|
||||
"string" => &[
|
||||
("len", false),
|
||||
("at", true),
|
||||
("clusters", false),
|
||||
("codepoints", false),
|
||||
("contains", true),
|
||||
("ends-with", true),
|
||||
("find", true),
|
||||
("first", false),
|
||||
("last", false),
|
||||
("match", true),
|
||||
("matches", true),
|
||||
("position", true),
|
||||
("replace", true),
|
||||
("slice", true),
|
||||
("split", true),
|
||||
("starts-with", true),
|
||||
("trim", true),
|
||||
],
|
||||
"bytes" => &[("len", false), ("at", true), ("slice", true)],
|
||||
"datetime" => &[
|
||||
("display", true),
|
||||
("year", false),
|
||||
("month", false),
|
||||
("weekday", false),
|
||||
("day", false),
|
||||
("hour", false),
|
||||
("minute", false),
|
||||
("second", false),
|
||||
("ordinal", false),
|
||||
],
|
||||
"duration" => &[
|
||||
("seconds", false),
|
||||
("minutes", false),
|
||||
("hours", false),
|
||||
("days", false),
|
||||
("weeks", false),
|
||||
],
|
||||
"content" => &[
|
||||
("func", false),
|
||||
("has", true),
|
||||
("at", true),
|
||||
("fields", false),
|
||||
("location", false),
|
||||
],
|
||||
"array" => &[
|
||||
("all", true),
|
||||
("any", true),
|
||||
("at", true),
|
||||
("contains", true),
|
||||
("filter", true),
|
||||
("find", true),
|
||||
("first", false),
|
||||
("flatten", false),
|
||||
("fold", true),
|
||||
("insert", true),
|
||||
("split", true),
|
||||
("join", true),
|
||||
("last", false),
|
||||
("len", false),
|
||||
("map", true),
|
||||
("pop", false),
|
||||
("position", true),
|
||||
("push", true),
|
||||
("remove", true),
|
||||
("rev", false),
|
||||
("slice", true),
|
||||
("sorted", false),
|
||||
("enumerate", false),
|
||||
("zip", true),
|
||||
],
|
||||
"dictionary" => &[
|
||||
("at", true),
|
||||
("insert", true),
|
||||
("keys", false),
|
||||
("len", false),
|
||||
("pairs", false),
|
||||
("remove", true),
|
||||
("values", false),
|
||||
],
|
||||
"function" => &[("where", true), ("with", true)],
|
||||
"length" => &[("pt", false), ("cm", false), ("mm", false), ("inches", false)],
|
||||
"angle" => &[("deg", false), ("rad", false)],
|
||||
"arguments" => &[("named", false), ("pos", false)],
|
||||
"location" => &[("page", false), ("position", false), ("page-numbering", false)],
|
||||
"selector" => &[("or", true), ("and", true), ("before", true), ("after", true)],
|
||||
"direction" => {
|
||||
&[("axis", false), ("start", false), ("end", false), ("inv", false)]
|
||||
}
|
||||
"alignment" => &[("axis", false), ("inv", false)],
|
||||
"2d alignment" => &[("inv", false)],
|
||||
"counter" => &[
|
||||
("display", true),
|
||||
("at", true),
|
||||
("final", true),
|
||||
("step", true),
|
||||
("update", true),
|
||||
],
|
||||
"state" => &[("display", true), ("at", true), ("final", true), ("update", true)],
|
||||
_ => &[],
|
||||
}
|
||||
fn missing_method(ty: Type, method: &str) -> String {
|
||||
format!("type {ty} has no method `{method}`")
|
||||
}
|
||||
|
@ -14,10 +14,12 @@ mod str;
|
||||
mod value;
|
||||
mod args;
|
||||
mod auto;
|
||||
mod bool;
|
||||
mod bytes;
|
||||
mod datetime;
|
||||
mod duration;
|
||||
mod fields;
|
||||
mod float;
|
||||
mod func;
|
||||
mod int;
|
||||
mod methods;
|
||||
@ -28,6 +30,7 @@ mod plugin;
|
||||
mod scope;
|
||||
mod symbol;
|
||||
mod tracer;
|
||||
mod ty;
|
||||
|
||||
#[doc(hidden)]
|
||||
pub use {
|
||||
@ -37,9 +40,6 @@ pub use {
|
||||
once_cell::sync::Lazy,
|
||||
};
|
||||
|
||||
#[doc(inline)]
|
||||
pub use typst_macros::{func, symbols};
|
||||
|
||||
pub use self::args::{Arg, Args};
|
||||
pub use self::array::{array, Array};
|
||||
pub use self::auto::AutoValue;
|
||||
@ -50,18 +50,20 @@ pub use self::cast::{
|
||||
pub use self::datetime::Datetime;
|
||||
pub use self::dict::{dict, Dict};
|
||||
pub use self::duration::Duration;
|
||||
pub use self::fields::fields_on;
|
||||
pub use self::func::{Func, FuncInfo, NativeFunc, ParamInfo};
|
||||
pub use self::func::{func, Func, NativeFunc, NativeFuncData, 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::plugin::Plugin;
|
||||
pub use self::scope::{Scope, Scopes};
|
||||
pub use self::scope::{NativeScope, Scope, Scopes};
|
||||
pub use self::str::{format_str, Regex, Str};
|
||||
pub use self::symbol::Symbol;
|
||||
pub use self::symbol::{symbols, Symbol};
|
||||
pub use self::tracer::Tracer;
|
||||
pub use self::value::{Dynamic, Type, Value};
|
||||
pub use self::ty::{scope, ty, NativeType, NativeTypeData, Type};
|
||||
pub use self::value::{Dynamic, Value};
|
||||
|
||||
pub(crate) use self::fields::fields_on;
|
||||
pub(crate) use self::methods::mutable_methods_on;
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::mem;
|
||||
@ -150,7 +152,7 @@ pub fn eval(
|
||||
.unwrap_or_default()
|
||||
.to_string_lossy();
|
||||
|
||||
Ok(Module::new(name).with_scope(vm.scopes.top).with_content(output))
|
||||
Ok(Module::new(name, vm.scopes.top).with_content(output))
|
||||
}
|
||||
|
||||
/// Evaluate a string as code and return the resulting value.
|
||||
@ -575,7 +577,7 @@ impl Eval for ast::Escape<'_> {
|
||||
|
||||
#[tracing::instrument(name = "Escape::eval", skip_all)]
|
||||
fn eval(self, _: &mut Vm) -> SourceResult<Self::Output> {
|
||||
Ok(Value::Symbol(Symbol::new(self.get())))
|
||||
Ok(Value::Symbol(Symbol::single(self.get())))
|
||||
}
|
||||
}
|
||||
|
||||
@ -584,7 +586,7 @@ impl Eval for ast::Shorthand<'_> {
|
||||
|
||||
#[tracing::instrument(name = "Shorthand::eval", skip_all)]
|
||||
fn eval(self, _: &mut Vm) -> SourceResult<Self::Output> {
|
||||
Ok(Value::Symbol(Symbol::new(self.get())))
|
||||
Ok(Value::Symbol(Symbol::single(self.get())))
|
||||
}
|
||||
}
|
||||
|
||||
@ -992,7 +994,7 @@ impl Eval for ast::Array<'_> {
|
||||
ast::ArrayItem::Spread(expr) => match expr.eval(vm)? {
|
||||
Value::None => {}
|
||||
Value::Array(array) => vec.extend(array.into_iter()),
|
||||
v => bail!(expr.span(), "cannot spread {} into array", v.type_name()),
|
||||
v => bail!(expr.span(), "cannot spread {} into array", v.ty()),
|
||||
},
|
||||
}
|
||||
}
|
||||
@ -1019,11 +1021,7 @@ impl Eval for ast::Dict<'_> {
|
||||
ast::DictItem::Spread(expr) => match expr.eval(vm)? {
|
||||
Value::None => {}
|
||||
Value::Dict(dict) => map.extend(dict.into_iter()),
|
||||
v => bail!(
|
||||
expr.span(),
|
||||
"cannot spread {} into dictionary",
|
||||
v.type_name()
|
||||
),
|
||||
v => bail!(expr.span(), "cannot spread {} into dictionary", v.ty()),
|
||||
},
|
||||
}
|
||||
}
|
||||
@ -1085,8 +1083,8 @@ fn apply_binary_expr(
|
||||
let lhs = binary.lhs().eval(vm)?;
|
||||
|
||||
// Short-circuit boolean operations.
|
||||
if (binary.op() == ast::BinOp::And && lhs == Value::Bool(false))
|
||||
|| (binary.op() == ast::BinOp::Or && lhs == Value::Bool(true))
|
||||
if (binary.op() == ast::BinOp::And && lhs == false.into_value())
|
||||
|| (binary.op() == ast::BinOp::Or && lhs == true.into_value())
|
||||
{
|
||||
return Ok(lhs);
|
||||
}
|
||||
@ -1146,49 +1144,83 @@ impl Eval for ast::FuncCall<'_> {
|
||||
let callee_span = callee.span();
|
||||
let args = self.args();
|
||||
|
||||
// Try to evaluate as a method call. This is possible if the callee is a
|
||||
// field access and does not evaluate to a module.
|
||||
// Try to evaluate as a call to an associated function or field.
|
||||
let (callee, mut args) = if let ast::Expr::FieldAccess(access) = callee {
|
||||
let target = access.target();
|
||||
let target_span = target.span();
|
||||
let field = access.field();
|
||||
let point = || Tracepoint::Call(Some(field.get().clone()));
|
||||
if methods::is_mutating(&field) {
|
||||
let args = args.eval(vm)?;
|
||||
let field_span = field.span();
|
||||
|
||||
let target = if methods::is_mutating(&field) {
|
||||
let mut args = args.eval(vm)?;
|
||||
let target = target.access(vm)?;
|
||||
|
||||
// Prioritize a function's own methods (with, where) over its
|
||||
// fields. This is fine as we define each field of a function,
|
||||
// if it has any.
|
||||
// ('methods_on' will be empty for Symbol and Module - their
|
||||
// method calls always refer to their fields.)
|
||||
if !matches!(target, Value::Symbol(_) | Value::Module(_) | Value::Func(_))
|
||||
|| methods_on(target.type_name())
|
||||
.iter()
|
||||
.any(|&(m, _)| m == field.as_str())
|
||||
{
|
||||
// Only arrays and dictionaries have mutable methods.
|
||||
if matches!(target, Value::Array(_) | Value::Dict(_)) {
|
||||
args.span = span;
|
||||
let point = || Tracepoint::Call(Some(field.get().clone()));
|
||||
return methods::call_mut(target, &field, args, span).trace(
|
||||
vm.world(),
|
||||
point,
|
||||
span,
|
||||
);
|
||||
}
|
||||
(target.field(&field).at(field.span())?, args)
|
||||
} else {
|
||||
let target = target.eval(vm)?;
|
||||
let args = args.eval(vm)?;
|
||||
|
||||
if !matches!(target, Value::Symbol(_) | Value::Module(_) | Value::Func(_))
|
||||
|| methods_on(target.type_name())
|
||||
.iter()
|
||||
.any(|&(m, _)| m == field.as_str())
|
||||
{
|
||||
return methods::call(vm, target, &field, args, span).trace(
|
||||
vm.world(),
|
||||
point,
|
||||
span,
|
||||
);
|
||||
target.clone()
|
||||
} else {
|
||||
access.target().eval(vm)?
|
||||
};
|
||||
|
||||
let mut args = args.eval(vm)?;
|
||||
|
||||
// Handle plugins.
|
||||
if let Value::Plugin(plugin) = &target {
|
||||
let bytes = args.all::<Bytes>()?;
|
||||
args.finish()?;
|
||||
return Ok(plugin.call(&field, bytes).at(span)?.into_value());
|
||||
}
|
||||
|
||||
// Prioritize associated functions on the value's type (i.e.,
|
||||
// methods) over its fields. A function call on a field is only
|
||||
// allowed for functions, types, modules (because they are scopes),
|
||||
// and symbols (because they have modifiers).
|
||||
//
|
||||
// For dictionaries, it is not allowed because it would be ambigious
|
||||
// (prioritizing associated functions would make an addition of a
|
||||
// new associated function a breaking change and prioritizing fields
|
||||
// would break associated functions for certain dictionaries).
|
||||
if let Some(callee) = target.ty().scope().get(&field) {
|
||||
let this = Arg {
|
||||
span: target_span,
|
||||
name: None,
|
||||
value: Spanned::new(target, target_span),
|
||||
};
|
||||
args.span = span;
|
||||
args.items.insert(0, this);
|
||||
(callee.clone(), args)
|
||||
} else if matches!(
|
||||
target,
|
||||
Value::Symbol(_) | Value::Func(_) | Value::Type(_) | Value::Module(_)
|
||||
) {
|
||||
(target.field(&field).at(field_span)?, args)
|
||||
} else {
|
||||
let mut error = error!(
|
||||
field_span,
|
||||
"type {} has no method `{}`",
|
||||
target.ty(),
|
||||
field.as_str()
|
||||
);
|
||||
|
||||
if let Value::Dict(dict) = target {
|
||||
if matches!(dict.get(&field), Ok(Value::Func(_))) {
|
||||
error.hint(
|
||||
"to call the function stored in the dictionary, \
|
||||
surround the field access with parentheses",
|
||||
);
|
||||
}
|
||||
}
|
||||
(target.field(&field).at(field.span())?, args)
|
||||
|
||||
bail!(error);
|
||||
}
|
||||
} else {
|
||||
(callee.eval(vm)?, args.eval(vm)?)
|
||||
@ -1284,7 +1316,7 @@ impl Eval for ast::Args<'_> {
|
||||
}));
|
||||
}
|
||||
Value::Args(args) => items.extend(args.items),
|
||||
v => bail!(expr.span(), "cannot spread {}", v.type_name()),
|
||||
v => bail!(expr.span(), "cannot spread {}", v.ty()),
|
||||
},
|
||||
}
|
||||
}
|
||||
@ -1364,7 +1396,7 @@ where
|
||||
ast::Pattern::Destructuring(destruct) => match value {
|
||||
Value::Array(value) => destructure_array(vm, pattern, value, f, destruct)?,
|
||||
Value::Dict(value) => destructure_dict(vm, value, f, destruct)?,
|
||||
_ => bail!(pattern.span(), "cannot destructure {}", value.type_name()),
|
||||
_ => bail!(pattern.span(), "cannot destructure {}", value.ty()),
|
||||
},
|
||||
}
|
||||
Ok(())
|
||||
@ -1436,21 +1468,15 @@ where
|
||||
for p in destruct.bindings() {
|
||||
match p {
|
||||
ast::DestructuringKind::Normal(ast::Expr::Ident(ident)) => {
|
||||
let v = dict
|
||||
.at(&ident, None)
|
||||
.map_err(|_| "destructuring key not found in dictionary")
|
||||
.at(ident.span())?;
|
||||
f(vm, ast::Expr::Ident(ident), v)?;
|
||||
let v = dict.get(&ident).at(ident.span())?;
|
||||
f(vm, ast::Expr::Ident(ident), v.clone())?;
|
||||
used.insert(ident.as_str());
|
||||
}
|
||||
ast::DestructuringKind::Sink(spread) => sink = spread.expr(),
|
||||
ast::DestructuringKind::Named(named) => {
|
||||
let name = named.name();
|
||||
let v = dict
|
||||
.at(&name, None)
|
||||
.map_err(|_| "destructuring key not found in dictionary")
|
||||
.at(name.span())?;
|
||||
f(vm, named.expr(), v)?;
|
||||
let v = dict.get(&name).at(name.span())?;
|
||||
f(vm, named.expr(), v.clone())?;
|
||||
used.insert(name.as_str());
|
||||
}
|
||||
ast::DestructuringKind::Placeholder(_) => {}
|
||||
@ -1690,10 +1716,10 @@ impl Eval for ast::ForLoop<'_> {
|
||||
iter!(for pattern in array);
|
||||
}
|
||||
(ast::Pattern::Normal(_), _) => {
|
||||
bail!(self.iter().span(), "cannot loop over {}", iter.type_name());
|
||||
bail!(self.iter().span(), "cannot loop over {}", iter.ty());
|
||||
}
|
||||
(_, _) => {
|
||||
bail!(pattern.span(), "cannot destructure values of {}", iter.type_name())
|
||||
bail!(pattern.span(), "cannot destructure values of {}", iter.ty())
|
||||
}
|
||||
}
|
||||
|
||||
@ -1789,16 +1815,25 @@ impl Eval for ast::ModuleImport<'_> {
|
||||
}
|
||||
}
|
||||
if let Value::Func(func) = source {
|
||||
if func.info().is_none() {
|
||||
let Some(scope) = func.scope() else {
|
||||
bail!(span, "cannot import from user-defined functions");
|
||||
}
|
||||
};
|
||||
apply_imports(
|
||||
self.imports(),
|
||||
vm,
|
||||
func,
|
||||
new_name,
|
||||
|func| func.info().unwrap().name.into(),
|
||||
|func| &func.info().unwrap().scope,
|
||||
|func| func.name().unwrap_or_default().into(),
|
||||
|_| scope,
|
||||
)?;
|
||||
} else if let Value::Type(ty) = source {
|
||||
apply_imports(
|
||||
self.imports(),
|
||||
vm,
|
||||
ty,
|
||||
new_name,
|
||||
|ty| ty.short_name().into(),
|
||||
|ty| ty.scope(),
|
||||
)?;
|
||||
} else {
|
||||
let module = import(vm, source, span, true)?;
|
||||
@ -1833,16 +1868,16 @@ fn import(
|
||||
vm: &mut Vm,
|
||||
source: Value,
|
||||
span: Span,
|
||||
accept_functions: bool,
|
||||
allow_scopes: bool,
|
||||
) -> SourceResult<Module> {
|
||||
let path = match source {
|
||||
Value::Str(path) => path,
|
||||
Value::Module(module) => return Ok(module),
|
||||
v => {
|
||||
if accept_functions {
|
||||
bail!(span, "expected path, module or function, found {}", v.type_name())
|
||||
if allow_scopes {
|
||||
bail!(span, "expected path, module, function, or type, found {}", v.ty())
|
||||
} else {
|
||||
bail!(span, "expected path or module, found {}", v.type_name())
|
||||
bail!(span, "expected path or module, found {}", v.ty())
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -2038,20 +2073,22 @@ fn access_dict<'a>(
|
||||
match access.target().access(vm)? {
|
||||
Value::Dict(dict) => Ok(dict),
|
||||
value => {
|
||||
let type_name = value.type_name();
|
||||
let ty = value.ty();
|
||||
let span = access.target().span();
|
||||
if matches!(
|
||||
value, // those types have their own field getters
|
||||
Value::Symbol(_) | Value::Content(_) | Value::Module(_) | Value::Func(_)
|
||||
) {
|
||||
bail!(span, "cannot mutate fields on {type_name}");
|
||||
} else if fields::fields_on(type_name).is_empty() {
|
||||
bail!(span, "{type_name} does not have accessible fields");
|
||||
bail!(span, "cannot mutate fields on {ty}");
|
||||
} else if fields::fields_on(ty).is_empty() {
|
||||
bail!(span, "{ty} does not have accessible fields");
|
||||
} else {
|
||||
// type supports static fields, which don't yet have
|
||||
// setters
|
||||
Err(eco_format!("fields on {type_name} are not yet mutable"))
|
||||
.hint(eco_format!("try creating a new {type_name} with the updated field value instead"))
|
||||
Err(eco_format!("fields on {ty} are not yet mutable"))
|
||||
.hint(eco_format!(
|
||||
"try creating a new {ty} with the updated field value instead"
|
||||
))
|
||||
.at(span)
|
||||
}
|
||||
}
|
||||
|
@ -3,12 +3,27 @@ use std::sync::Arc;
|
||||
|
||||
use ecow::{eco_format, EcoString};
|
||||
|
||||
use super::{Content, Scope, Value};
|
||||
use super::{ty, Content, Scope, Value};
|
||||
use crate::diag::StrResult;
|
||||
|
||||
/// An evaluated module, ready for importing or typesetting.
|
||||
/// An evaluated module, either built-in or resulting from a file.
|
||||
///
|
||||
/// Values of this type are cheap to clone and hash.
|
||||
/// You can access definitions from the module using [field access
|
||||
/// notation]($scripting/#fields) and interact with it using the [import and
|
||||
/// include syntaxes]($scripting/#modules).
|
||||
///
|
||||
/// # Example
|
||||
/// ```example
|
||||
/// <<< #import "utils.typ"
|
||||
/// <<< #utils.add(2, 5)
|
||||
///
|
||||
/// <<< #import utils: sub
|
||||
/// <<< #sub(1, 4)
|
||||
/// >>> #7
|
||||
/// >>>
|
||||
/// >>> #(-3)
|
||||
/// ```
|
||||
#[ty]
|
||||
#[derive(Clone, Hash)]
|
||||
#[allow(clippy::derived_hash_with_manual_eq)]
|
||||
pub struct Module {
|
||||
@ -29,10 +44,10 @@ struct Repr {
|
||||
|
||||
impl Module {
|
||||
/// Create a new module.
|
||||
pub fn new(name: impl Into<EcoString>) -> Self {
|
||||
pub fn new(name: impl Into<EcoString>, scope: Scope) -> Self {
|
||||
Self {
|
||||
name: name.into(),
|
||||
inner: Arc::new(Repr { scope: Scope::new(), content: Content::empty() }),
|
||||
inner: Arc::new(Repr { scope, content: Content::empty() }),
|
||||
}
|
||||
}
|
||||
|
||||
@ -70,7 +85,7 @@ impl Module {
|
||||
}
|
||||
|
||||
/// Try to access a definition in the module.
|
||||
pub fn get(&self, name: &str) -> StrResult<&Value> {
|
||||
pub fn field(&self, name: &str) -> StrResult<&Value> {
|
||||
self.scope().get(name).ok_or_else(|| {
|
||||
eco_format!("module `{}` does not contain `{name}`", self.name())
|
||||
})
|
||||
|
@ -1,15 +1,33 @@
|
||||
use std::fmt::{self, Debug, Formatter};
|
||||
|
||||
use super::{cast, CastInfo, FromValue, IntoValue, Reflect, Value};
|
||||
use serde::{Serialize, Serializer};
|
||||
|
||||
use super::{cast, ty, CastInfo, FromValue, IntoValue, Reflect, Type, Value};
|
||||
use crate::diag::StrResult;
|
||||
|
||||
/// A value that indicates the absence of any other value.
|
||||
///
|
||||
/// The none type has exactly one value: `{none}`.
|
||||
///
|
||||
/// When inserted into the document, it is not visible. This is also the value
|
||||
/// that is produced by empty code blocks. It can be
|
||||
/// [joined]($scripting/#blocks) with any value, yielding the other value.
|
||||
///
|
||||
/// # Example
|
||||
/// ```example
|
||||
/// Not visible: #none
|
||||
/// ```
|
||||
#[ty(name = "none")]
|
||||
#[derive(Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
|
||||
pub struct NoneValue;
|
||||
|
||||
impl Reflect for NoneValue {
|
||||
fn describe() -> CastInfo {
|
||||
CastInfo::Type("none")
|
||||
fn input() -> CastInfo {
|
||||
CastInfo::Type(Type::of::<Self>())
|
||||
}
|
||||
|
||||
fn output() -> CastInfo {
|
||||
CastInfo::Type(Type::of::<Self>())
|
||||
}
|
||||
|
||||
fn castable(value: &Value) -> bool {
|
||||
@ -38,6 +56,15 @@ impl Debug for NoneValue {
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for NoneValue {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_none()
|
||||
}
|
||||
}
|
||||
|
||||
cast! {
|
||||
(),
|
||||
self => Value::None,
|
||||
@ -45,8 +72,12 @@ cast! {
|
||||
}
|
||||
|
||||
impl<T: Reflect> Reflect for Option<T> {
|
||||
fn describe() -> CastInfo {
|
||||
T::describe() + NoneValue::describe()
|
||||
fn input() -> CastInfo {
|
||||
T::input() + NoneValue::input()
|
||||
}
|
||||
|
||||
fn output() -> CastInfo {
|
||||
T::output() + NoneValue::output()
|
||||
}
|
||||
|
||||
fn castable(value: &Value) -> bool {
|
||||
|
@ -5,15 +5,15 @@ use std::fmt::Debug;
|
||||
|
||||
use ecow::eco_format;
|
||||
|
||||
use super::{format_str, Regex, Value};
|
||||
use super::{format_str, IntoValue, Regex, Value};
|
||||
use crate::diag::{bail, StrResult};
|
||||
use crate::geom::{Axes, Axis, GenAlign, Length, Numeric, PartialStroke, Rel, Smart};
|
||||
use crate::geom::{Align, Length, Numeric, Rel, Smart, Stroke};
|
||||
use Value::*;
|
||||
|
||||
/// Bail with a type mismatch error.
|
||||
macro_rules! mismatch {
|
||||
($fmt:expr, $($value:expr),* $(,)?) => {
|
||||
return Err(eco_format!($fmt, $($value.type_name()),*))
|
||||
return Err(eco_format!($fmt, $($value.ty()),*))
|
||||
};
|
||||
}
|
||||
|
||||
@ -55,7 +55,7 @@ pub fn pos(value: Value) -> StrResult<Value> {
|
||||
/// Compute the negation of a value.
|
||||
pub fn neg(value: Value) -> StrResult<Value> {
|
||||
Ok(match value {
|
||||
Int(v) => Int(v.checked_neg().ok_or("value is too large")?),
|
||||
Int(v) => Int(v.checked_neg().ok_or_else(too_large)?),
|
||||
Float(v) => Float(-v),
|
||||
Length(v) => Length(-v),
|
||||
Angle(v) => Angle(-v),
|
||||
@ -73,7 +73,7 @@ pub fn add(lhs: Value, rhs: Value) -> StrResult<Value> {
|
||||
(a, None) => a,
|
||||
(None, b) => b,
|
||||
|
||||
(Int(a), Int(b)) => Int(a.checked_add(b).ok_or("value is too large")?),
|
||||
(Int(a), Int(b)) => Int(a.checked_add(b).ok_or_else(too_large)?),
|
||||
(Int(a), Float(b)) => Float(a as f64 + b),
|
||||
(Float(a), Int(b)) => Float(a + b as f64),
|
||||
(Float(a), Float(b)) => Float(a + b),
|
||||
@ -108,32 +108,22 @@ pub fn add(lhs: Value, rhs: Value) -> StrResult<Value> {
|
||||
(Array(a), Array(b)) => Array(a + b),
|
||||
(Dict(a), Dict(b)) => Dict(a + b),
|
||||
|
||||
(Color(color), Length(thickness)) | (Length(thickness), Color(color)) => {
|
||||
Value::dynamic(PartialStroke {
|
||||
paint: Smart::Custom(color.into()),
|
||||
thickness: Smart::Custom(thickness),
|
||||
..PartialStroke::default()
|
||||
})
|
||||
(Color(color), Length(thickness)) | (Length(thickness), Color(color)) => Stroke {
|
||||
paint: Smart::Custom(color.into()),
|
||||
thickness: Smart::Custom(thickness),
|
||||
..Stroke::default()
|
||||
}
|
||||
.into_value(),
|
||||
|
||||
(Duration(a), Duration(b)) => Duration(a + b),
|
||||
(Datetime(a), Duration(b)) => Datetime(a + b),
|
||||
(Duration(a), Datetime(b)) => Datetime(b + a),
|
||||
|
||||
(Dyn(a), Dyn(b)) => {
|
||||
// 1D alignments can be summed into 2D alignments.
|
||||
if let (Some(&a), Some(&b)) =
|
||||
(a.downcast::<GenAlign>(), b.downcast::<GenAlign>())
|
||||
{
|
||||
if a.axis() == b.axis() {
|
||||
return Err(eco_format!("cannot add two {:?} alignments", a.axis()));
|
||||
}
|
||||
|
||||
return Ok(Value::dynamic(match a.axis() {
|
||||
Axis::X => Axes { x: a, y: b },
|
||||
Axis::Y => Axes { x: b, y: a },
|
||||
}));
|
||||
};
|
||||
// Alignments can be summed.
|
||||
if let (Some(&a), Some(&b)) = (a.downcast::<Align>(), b.downcast::<Align>()) {
|
||||
return Ok((a + b)?.into_value());
|
||||
}
|
||||
|
||||
mismatch!("cannot add {} and {}", a, b);
|
||||
}
|
||||
@ -145,7 +135,7 @@ pub fn add(lhs: Value, rhs: Value) -> StrResult<Value> {
|
||||
/// Compute the difference of two values.
|
||||
pub fn sub(lhs: Value, rhs: Value) -> StrResult<Value> {
|
||||
Ok(match (lhs, rhs) {
|
||||
(Int(a), Int(b)) => Int(a.checked_sub(b).ok_or("value is too large")?),
|
||||
(Int(a), Int(b)) => Int(a.checked_sub(b).ok_or_else(too_large)?),
|
||||
(Int(a), Float(b)) => Float(a as f64 - b),
|
||||
(Float(a), Int(b)) => Float(a - b as f64),
|
||||
(Float(a), Float(b)) => Float(a - b),
|
||||
@ -177,7 +167,7 @@ pub fn sub(lhs: Value, rhs: Value) -> StrResult<Value> {
|
||||
/// Compute the product of two values.
|
||||
pub fn mul(lhs: Value, rhs: Value) -> StrResult<Value> {
|
||||
Ok(match (lhs, rhs) {
|
||||
(Int(a), Int(b)) => Int(a.checked_mul(b).ok_or("value is too large")?),
|
||||
(Int(a), Int(b)) => Int(a.checked_mul(b).ok_or_else(too_large)?),
|
||||
(Int(a), Float(b)) => Float(a as f64 * b),
|
||||
(Float(a), Int(b)) => Float(a * b as f64),
|
||||
(Float(a), Float(b)) => Float(a * b),
|
||||
@ -216,10 +206,10 @@ pub fn mul(lhs: Value, rhs: Value) -> StrResult<Value> {
|
||||
(Float(a), Fraction(b)) => Fraction(a * b),
|
||||
(Ratio(a), Fraction(b)) => Fraction(a.get() * b),
|
||||
|
||||
(Str(a), Int(b)) => Str(a.repeat(b)?),
|
||||
(Int(a), Str(b)) => Str(b.repeat(a)?),
|
||||
(Array(a), Int(b)) => Array(a.repeat(b)?),
|
||||
(Int(a), Array(b)) => Array(b.repeat(a)?),
|
||||
(Str(a), Int(b)) => Str(a.repeat(Value::Int(b).cast()?)?),
|
||||
(Int(a), Str(b)) => Str(b.repeat(Value::Int(a).cast()?)?),
|
||||
(Array(a), Int(b)) => Array(a.repeat(Value::Int(b).cast()?)?),
|
||||
(Int(a), Array(b)) => Array(b.repeat(Value::Int(a).cast()?)?),
|
||||
(Content(a), b @ Int(_)) => Content(a.repeat(b.cast()?)),
|
||||
(a @ Int(_), Content(b)) => Content(b.repeat(a.cast()?)),
|
||||
|
||||
@ -375,7 +365,9 @@ pub fn equal(lhs: &Value, rhs: &Value) -> bool {
|
||||
(Dict(a), Dict(b)) => a == b,
|
||||
(Func(a), Func(b)) => a == b,
|
||||
(Args(a), Args(b)) => a == b,
|
||||
(Type(a), Type(b)) => a == b,
|
||||
(Module(a), Module(b)) => a == b,
|
||||
(Plugin(a), Plugin(b)) => a == b,
|
||||
(Datetime(a), Datetime(b)) => a == b,
|
||||
(Duration(a), Duration(b)) => a == b,
|
||||
(Dyn(a), Dyn(b)) => a == b,
|
||||
@ -456,7 +448,12 @@ pub fn contains(lhs: &Value, rhs: &Value) -> Option<bool> {
|
||||
(Str(a), Str(b)) => Some(b.as_str().contains(a.as_str())),
|
||||
(Dyn(a), Str(b)) => a.downcast::<Regex>().map(|regex| regex.is_match(b)),
|
||||
(Str(a), Dict(b)) => Some(b.contains(a)),
|
||||
(a, Array(b)) => Some(b.contains(a)),
|
||||
(a, Array(b)) => Some(b.contains(a.clone())),
|
||||
_ => Option::None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cold]
|
||||
fn too_large() -> &'static str {
|
||||
"value is too large"
|
||||
}
|
||||
|
@ -5,14 +5,105 @@ use ecow::{eco_format, EcoString};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use wasmi::{AsContext, AsContextMut, Caller, Engine, Linker, Module};
|
||||
|
||||
use super::{cast, Bytes};
|
||||
use crate::diag::{bail, StrResult};
|
||||
use super::{func, scope, ty, Bytes, Vm};
|
||||
use crate::diag::{bail, At, SourceResult, StrResult};
|
||||
use crate::syntax::Spanned;
|
||||
use crate::World;
|
||||
|
||||
/// A plugin loaded from WebAssembly code.
|
||||
/// A WebAssembly plugin.
|
||||
///
|
||||
/// It can run external code conforming to its protocol.
|
||||
/// This is **advanced functionality** and not to be confused with
|
||||
/// [Typst packages]($scripting/#packages).
|
||||
///
|
||||
/// This type is cheap to clone and hash.
|
||||
/// Typst is capable of interfacing with plugins compiled to WebAssembly. Plugin
|
||||
/// functions may accept multiple [byte buffers]($bytes) as arguments and return
|
||||
/// a single byte buffer. They should typically be wrapped in idiomatic Typst
|
||||
/// functions that perform the necessary conversions between native Typst types
|
||||
/// and bytes.
|
||||
///
|
||||
/// Plugins run in isolation from your system, which means that printing,
|
||||
/// reading files, or anything like that will not be supported for security
|
||||
/// reasons. To run as a plugin, a program needs to be compiled to a 32-bit
|
||||
/// shared WebAssembly library. Many compilers will use the
|
||||
/// [WASI ABI](https://wasi.dev/) by default or as their only option (e.g.
|
||||
/// emscripten), which allows printing, reading files, etc. This ABI will not
|
||||
/// directly work with Typst. You will either need to compile to a different
|
||||
/// target or [stub all functions](https://github.com/astrale-sharp/wasm-minimal-protocol/blob/master/wasi-stub).
|
||||
///
|
||||
/// # Example
|
||||
/// ```example
|
||||
/// #let myplugin = plugin("hello.wasm")
|
||||
/// #let concat(a, b) = str(
|
||||
/// myplugin.concatenate(
|
||||
/// bytes(a),
|
||||
/// bytes(b),
|
||||
/// )
|
||||
/// )
|
||||
///
|
||||
/// #concat("hello", "world")
|
||||
/// ```
|
||||
///
|
||||
/// # Protocol
|
||||
/// To be used as a plugin, a WebAssembly module must conform to the following
|
||||
/// protocol:
|
||||
///
|
||||
/// ## Exports
|
||||
/// A plugin module can export functions to make them callable from Typst. To
|
||||
/// conform to the protocol, an exported function should:
|
||||
///
|
||||
/// - Take `n` 32-bit integer arguments `a_1`, `a_2`, ..., `a_n` (interpreted as
|
||||
/// lengths, so `usize/size_t` may be preferable), and return one 32-bit
|
||||
/// integer.
|
||||
///
|
||||
/// - The function should first allocate a buffer `buf` of length
|
||||
/// `a_1 + a_2 + ... + a_n`, and then call
|
||||
/// `wasm_minimal_protocol_write_args_to_buffer(buf.ptr)`.
|
||||
///
|
||||
/// - The `a_1` first bytes of the buffer now constitute the first argument, the
|
||||
/// `a_2` next bytes the second argument, and so on.
|
||||
///
|
||||
/// - The function can now do its job with the arguments and produce an output
|
||||
/// buffer. Before returning, it should call
|
||||
/// `wasm_minimal_protocol_send_result_to_host` to send its result back to the
|
||||
/// host.
|
||||
///
|
||||
/// - To signal success, the function should return `0`.
|
||||
///
|
||||
/// - To signal an error, the function should return `1`. The written buffer is
|
||||
/// then interpreted as an UTF-8 encoded error message.
|
||||
///
|
||||
/// ## Imports
|
||||
/// Plugin modules need to import two functions that are provided by the runtime.
|
||||
/// (Types and functions are described using WAT syntax.)
|
||||
///
|
||||
/// - `(import "typst_env" "wasm_minimal_protocol_write_args_to_buffer" (func (param i32)))`
|
||||
///
|
||||
/// Writes the arguments for the current function into a plugin-allocated
|
||||
/// buffer. When a plugin function is called, it
|
||||
/// [receives the lengths](#exports) of its input buffers as arguments. It
|
||||
/// should then allocate a buffer whose capacity is at least the sum of these
|
||||
/// lengths. It should then call this function with a `ptr` to the buffer to
|
||||
/// fill it with the arguments, one after another.
|
||||
///
|
||||
/// - `(import "typst_env" "wasm_minimal_protocol_send_result_to_host" (func (param i32 i32)))`
|
||||
///
|
||||
/// Sends the output of the current function to the host (Typst). The first
|
||||
/// parameter shall be a pointer to a buffer (`ptr`), while the second is the
|
||||
/// length of that buffer (`len`). The memory pointed at by `ptr` can be freed
|
||||
/// immediately after this function returns. If the message should be
|
||||
/// interpreted as an error message, it should be encoded as UTF-8.
|
||||
///
|
||||
/// # Resources
|
||||
/// For more resources, check out the
|
||||
/// [wasm-minimal-protocol repository](https://github.com/astrale-sharp/wasm-minimal-protocol).
|
||||
/// It contains:
|
||||
///
|
||||
/// - A list of example plugin implementations and a test runner for these
|
||||
/// examples
|
||||
/// - Wrappers to help you write your plugin in Rust (Zig wrapper in
|
||||
/// development)
|
||||
/// - A stubber for WASI
|
||||
#[ty(scope)]
|
||||
#[derive(Clone)]
|
||||
pub struct Plugin(Arc<Repr>);
|
||||
|
||||
@ -36,6 +127,23 @@ struct StoreData {
|
||||
output: Vec<u8>,
|
||||
}
|
||||
|
||||
#[scope]
|
||||
impl Plugin {
|
||||
/// Creates a new plugin from a WebAssembly file.
|
||||
#[func(constructor)]
|
||||
pub fn construct(
|
||||
/// The virtual machine.
|
||||
vm: &mut Vm,
|
||||
/// Path to a WebAssembly file.
|
||||
path: Spanned<EcoString>,
|
||||
) -> SourceResult<Plugin> {
|
||||
let Spanned { v: path, span } = path;
|
||||
let id = vm.resolve_path(&path).at(span)?;
|
||||
let data = vm.world().file(id).at(span)?;
|
||||
Plugin::new(data).at(span)
|
||||
}
|
||||
}
|
||||
|
||||
impl Plugin {
|
||||
/// Create a new plugin from raw WebAssembly bytes.
|
||||
#[comemo::memoize]
|
||||
@ -179,10 +287,6 @@ impl Hash for Plugin {
|
||||
}
|
||||
}
|
||||
|
||||
cast! {
|
||||
type Plugin: "plugin",
|
||||
}
|
||||
|
||||
/// Write the arguments to the plugin function into the plugin's memory.
|
||||
fn wasm_minimal_protocol_write_args_to_buffer(mut caller: Caller<StoreData>, ptr: u32) {
|
||||
let memory = caller.get_export("memory").unwrap().into_memory().unwrap();
|
||||
|
@ -1,11 +1,14 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::fmt::{self, Debug, Formatter};
|
||||
use std::hash::Hash;
|
||||
use std::hash::{Hash, Hasher};
|
||||
|
||||
use ecow::{eco_format, EcoString};
|
||||
use indexmap::IndexMap;
|
||||
|
||||
use super::{IntoValue, Library, Value};
|
||||
use super::{
|
||||
Func, IntoValue, Library, Module, NativeFunc, NativeFuncData, NativeType, Type, Value,
|
||||
};
|
||||
use crate::diag::{bail, StrResult};
|
||||
use crate::model::{Element, NativeElement};
|
||||
|
||||
/// A stack of scopes.
|
||||
#[derive(Debug, Default, Clone)]
|
||||
@ -83,18 +86,27 @@ fn unknown_variable(var: &str) -> EcoString {
|
||||
}
|
||||
|
||||
/// A map from binding names to values.
|
||||
#[derive(Default, Clone, Hash)]
|
||||
pub struct Scope(BTreeMap<EcoString, Slot>, bool);
|
||||
#[derive(Default, Clone)]
|
||||
pub struct Scope {
|
||||
map: IndexMap<EcoString, Slot>,
|
||||
deduplicate: bool,
|
||||
category: Option<&'static str>,
|
||||
}
|
||||
|
||||
impl Scope {
|
||||
/// Create a new empty scope.
|
||||
pub fn new() -> Self {
|
||||
Self(BTreeMap::new(), false)
|
||||
Default::default()
|
||||
}
|
||||
|
||||
/// Create a new scope with duplication prevention.
|
||||
pub fn deduplicating() -> Self {
|
||||
Self(BTreeMap::new(), true)
|
||||
Self { deduplicate: true, ..Default::default() }
|
||||
}
|
||||
|
||||
/// Enter a new category.
|
||||
pub fn category(&mut self, name: &'static str) {
|
||||
self.category = Some(name);
|
||||
}
|
||||
|
||||
/// Bind a value to a name.
|
||||
@ -103,32 +115,68 @@ impl Scope {
|
||||
let name = name.into();
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
if self.1 && self.0.contains_key(&name) {
|
||||
if self.deduplicate && self.map.contains_key(&name) {
|
||||
panic!("duplicate definition: {name}");
|
||||
}
|
||||
|
||||
self.0.insert(name, Slot::new(value.into_value(), Kind::Normal));
|
||||
self.map
|
||||
.insert(name, Slot::new(value.into_value(), Kind::Normal, self.category));
|
||||
}
|
||||
|
||||
/// Define a native function through a Rust type that shadows the function.
|
||||
pub fn define_func<T: NativeFunc>(&mut self) {
|
||||
let data = T::data();
|
||||
self.define(data.name, Func::from(data));
|
||||
}
|
||||
|
||||
/// Define a native function with raw function data.
|
||||
pub fn define_func_with_data(&mut self, data: &'static NativeFuncData) {
|
||||
self.define(data.name, Func::from(data));
|
||||
}
|
||||
|
||||
/// Define a native type.
|
||||
pub fn define_type<T: NativeType>(&mut self) {
|
||||
let data = T::data();
|
||||
self.define(data.name, Type::from(data));
|
||||
}
|
||||
|
||||
/// Define a native element.
|
||||
pub fn define_elem<T: NativeElement>(&mut self) {
|
||||
let data = T::data();
|
||||
self.define(data.name, Element::from(data));
|
||||
}
|
||||
|
||||
/// Define a module.
|
||||
pub fn define_module(&mut self, module: Module) {
|
||||
self.define(module.name().clone(), module);
|
||||
}
|
||||
|
||||
/// Define a captured, immutable binding.
|
||||
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));
|
||||
self.map.insert(
|
||||
var.into(),
|
||||
Slot::new(value.into_value(), Kind::Captured, self.category),
|
||||
);
|
||||
}
|
||||
|
||||
/// Try to access a variable immutably.
|
||||
pub fn get(&self, var: &str) -> Option<&Value> {
|
||||
self.0.get(var).map(Slot::read)
|
||||
self.map.get(var).map(Slot::read)
|
||||
}
|
||||
|
||||
/// Try to access a variable mutably.
|
||||
pub fn get_mut(&mut self, var: &str) -> Option<StrResult<&mut Value>> {
|
||||
self.0.get_mut(var).map(Slot::write)
|
||||
self.map.get_mut(var).map(Slot::write)
|
||||
}
|
||||
|
||||
/// Get the category of a definition.
|
||||
pub fn get_category(&self, var: &str) -> Option<&'static str> {
|
||||
self.map.get(var)?.category
|
||||
}
|
||||
|
||||
/// Iterate over all definitions.
|
||||
pub fn iter(&self) -> impl Iterator<Item = (&EcoString, &Value)> {
|
||||
self.0.iter().map(|(k, v)| (k, v.read()))
|
||||
self.map.iter().map(|(k, v)| (k, v.read()))
|
||||
}
|
||||
}
|
||||
|
||||
@ -136,11 +184,22 @@ impl Debug for Scope {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
f.write_str("Scope ")?;
|
||||
f.debug_map()
|
||||
.entries(self.0.iter().map(|(k, v)| (k, v.read())))
|
||||
.entries(self.map.iter().map(|(k, v)| (k, v.read())))
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl Hash for Scope {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
state.write_usize(self.map.len());
|
||||
for item in &self.map {
|
||||
item.hash(state);
|
||||
}
|
||||
self.deduplicate.hash(state);
|
||||
self.category.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
/// A slot where a value is stored.
|
||||
#[derive(Clone, Hash)]
|
||||
struct Slot {
|
||||
@ -148,6 +207,8 @@ struct Slot {
|
||||
value: Value,
|
||||
/// The kind of slot, determines how the value can be accessed.
|
||||
kind: Kind,
|
||||
/// The category of the slot.
|
||||
category: Option<&'static str>,
|
||||
}
|
||||
|
||||
/// The different kinds of slots.
|
||||
@ -161,8 +222,8 @@ enum Kind {
|
||||
|
||||
impl Slot {
|
||||
/// Create a new slot.
|
||||
fn new(value: Value, kind: Kind) -> Self {
|
||||
Self { value, kind }
|
||||
fn new(value: Value, kind: Kind, category: Option<&'static str>) -> Self {
|
||||
Self { value, kind, category }
|
||||
}
|
||||
|
||||
/// Read the value.
|
||||
@ -183,3 +244,12 @@ impl Slot {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Defines the associated scope of a Rust type.
|
||||
pub trait NativeScope {
|
||||
/// The constructor function for the type, if any.
|
||||
fn constructor() -> Option<&'static NativeFuncData>;
|
||||
|
||||
/// Get the associated scope for the type.
|
||||
fn scope() -> Scope;
|
||||
}
|
||||
|
@ -7,9 +7,14 @@ use ecow::EcoString;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
|
||||
use super::{cast, dict, Args, Array, Dict, Func, IntoValue, Value, Vm};
|
||||
use super::{
|
||||
cast, dict, func, scope, ty, Args, Array, Bytes, Dict, Func, IntoValue, Type, Value,
|
||||
Vm,
|
||||
};
|
||||
use crate::diag::{bail, At, SourceResult, StrResult};
|
||||
use crate::geom::GenAlign;
|
||||
use crate::geom::Align;
|
||||
use crate::model::Label;
|
||||
use crate::syntax::{Span, Spanned};
|
||||
|
||||
/// Create a new [`Str`] from a format string.
|
||||
#[macro_export]
|
||||
@ -22,10 +27,47 @@ macro_rules! __format_str {
|
||||
|
||||
#[doc(inline)]
|
||||
pub use crate::__format_str as format_str;
|
||||
|
||||
#[doc(hidden)]
|
||||
pub use ecow::eco_format;
|
||||
|
||||
/// An immutable reference counted string.
|
||||
/// A sequence of Unicode codepoints.
|
||||
///
|
||||
/// You can iterate over the grapheme clusters of the string using a [for
|
||||
/// loop]($scripting/#loops). Grapheme clusters are basically characters but
|
||||
/// keep together things that belong together, e.g. multiple codepoints that
|
||||
/// together form a flag emoji. Strings can be added with the `+` operator,
|
||||
/// [joined together]($scripting/#blocks) and multiplied with integers.
|
||||
///
|
||||
/// Typst provides utility methods for string manipulation. Many of these
|
||||
/// methods (e.g., `split`, `trim` and `replace`) operate on _patterns:_ A
|
||||
/// pattern can be either a string or a [regular expression]($regex). This makes
|
||||
/// the methods quite versatile.
|
||||
///
|
||||
/// All lengths and indices are expressed in terms of UTF-8 bytes. Indices are
|
||||
/// zero-based and negative indices wrap around to the end of the string.
|
||||
///
|
||||
/// You can convert a value to a string with this type's constructor.
|
||||
///
|
||||
/// # Example
|
||||
/// ```example
|
||||
/// #"hello world!" \
|
||||
/// #"\"hello\n world\"!" \
|
||||
/// #"1 2 3".split() \
|
||||
/// #"1,2;3".split(regex("[,;]")) \
|
||||
/// #(regex("\d+") in "ten euros") \
|
||||
/// #(regex("\d+") in "10 euros")
|
||||
/// ```
|
||||
///
|
||||
/// # Escape sequences { #escapes }
|
||||
/// Just like in markup, you can escape a few symbols in strings:
|
||||
/// - `[\\]` for a backslash
|
||||
/// - `[\"]` for a quote
|
||||
/// - `[\n]` for a newline
|
||||
/// - `[\r]` for a carriage return
|
||||
/// - `[\t]` for a tab
|
||||
/// - `[\u{1f600}]` for a hexadecimal Unicode escape sequence
|
||||
#[ty(scope, title = "String")]
|
||||
#[derive(Default, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct Str(EcoString);
|
||||
@ -38,12 +80,15 @@ impl Str {
|
||||
|
||||
/// Return `true` if the length is 0.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.0.len() == 0
|
||||
self.0.is_empty()
|
||||
}
|
||||
|
||||
/// The length of the string in bytes.
|
||||
pub fn len(&self) -> usize {
|
||||
self.0.len()
|
||||
/// Repeat the string a number of times.
|
||||
pub fn repeat(&self, n: usize) -> StrResult<Self> {
|
||||
if self.0.len().checked_mul(n).is_none() {
|
||||
return Err(eco_format!("cannot repeat this string {n} times"));
|
||||
}
|
||||
Ok(Self(self.0.repeat(n)))
|
||||
}
|
||||
|
||||
/// A string slice containing the entire string.
|
||||
@ -51,8 +96,87 @@ impl Str {
|
||||
self
|
||||
}
|
||||
|
||||
/// Extract the first grapheme cluster.
|
||||
pub fn first(&self) -> StrResult<Self> {
|
||||
/// Resolve an index or throw an out of bounds error.
|
||||
fn locate(&self, index: i64) -> StrResult<usize> {
|
||||
self.locate_opt(index)?
|
||||
.ok_or_else(|| out_of_bounds(index, self.len()))
|
||||
}
|
||||
|
||||
/// Resolve an index, if it is within bounds and on a valid char boundary.
|
||||
///
|
||||
/// `index == len` is considered in bounds.
|
||||
fn locate_opt(&self, index: i64) -> StrResult<Option<usize>> {
|
||||
let wrapped =
|
||||
if index >= 0 { Some(index) } else { (self.len() as i64).checked_add(index) };
|
||||
|
||||
let resolved = wrapped
|
||||
.and_then(|v| usize::try_from(v).ok())
|
||||
.filter(|&v| v <= self.0.len());
|
||||
|
||||
if resolved.map_or(false, |i| !self.0.is_char_boundary(i)) {
|
||||
return Err(not_a_char_boundary(index));
|
||||
}
|
||||
|
||||
Ok(resolved)
|
||||
}
|
||||
}
|
||||
|
||||
#[scope]
|
||||
impl Str {
|
||||
/// Converts a value to a string.
|
||||
///
|
||||
/// - Integers are formatted in base 10. This can be overridden with the
|
||||
/// optional `base` parameter.
|
||||
/// - Floats are formatted in base 10 and never in exponential notation.
|
||||
/// - From labels the name is extracted.
|
||||
/// - Bytes are decoded as UTF-8.
|
||||
///
|
||||
/// If you wish to convert from and to Unicode code points, see the
|
||||
/// [`to-unicode`]($str.to-unicode) and [`from-unicode`]($str.from-unicode)
|
||||
/// functions.
|
||||
///
|
||||
/// ```example
|
||||
/// #str(10) \
|
||||
/// #str(4000, base: 16) \
|
||||
/// #str(2.7) \
|
||||
/// #str(1e8) \
|
||||
/// #str(<intro>)
|
||||
/// ```
|
||||
#[func(constructor)]
|
||||
pub fn construct(
|
||||
/// The value that should be converted to a string.
|
||||
value: ToStr,
|
||||
/// The base (radix) to display integers in, between 2 and 36.
|
||||
#[named]
|
||||
#[default(Spanned::new(10, Span::detached()))]
|
||||
base: Spanned<i64>,
|
||||
) -> SourceResult<Str> {
|
||||
Ok(match value {
|
||||
ToStr::Str(s) => {
|
||||
if base.v != 10 {
|
||||
bail!(base.span, "base is only supported for integers");
|
||||
}
|
||||
s
|
||||
}
|
||||
ToStr::Int(n) => {
|
||||
if base.v < 2 || base.v > 36 {
|
||||
bail!(base.span, "base must be between 2 and 36");
|
||||
}
|
||||
format_int_with_base(n, base.v).into()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// The length of the string in UTF-8 encoded bytes.
|
||||
#[func(title = "Length")]
|
||||
pub fn len(&self) -> usize {
|
||||
self.0.len()
|
||||
}
|
||||
|
||||
/// Extracts the first grapheme cluster of the string.
|
||||
/// Fails with an error if the string is empty.
|
||||
#[func]
|
||||
pub fn first(&self) -> StrResult<Str> {
|
||||
self.0
|
||||
.graphemes(true)
|
||||
.next()
|
||||
@ -60,8 +184,10 @@ impl Str {
|
||||
.ok_or_else(string_is_empty)
|
||||
}
|
||||
|
||||
/// Extract the last grapheme cluster.
|
||||
pub fn last(&self) -> StrResult<Self> {
|
||||
/// Extracts the last grapheme cluster of the string.
|
||||
/// Fails with an error if the string is empty.
|
||||
#[func]
|
||||
pub fn last(&self) -> StrResult<Str> {
|
||||
self.0
|
||||
.graphemes(true)
|
||||
.next_back()
|
||||
@ -69,8 +195,18 @@ impl Str {
|
||||
.ok_or_else(string_is_empty)
|
||||
}
|
||||
|
||||
/// Extract the grapheme cluster at the given index.
|
||||
pub fn at(&self, index: i64, default: Option<Value>) -> StrResult<Value> {
|
||||
/// Extracts the first grapheme cluster after the specified index. Returns
|
||||
/// the default value if the index is out of bounds or fails with an error
|
||||
/// if no default value was specified.
|
||||
#[func]
|
||||
pub fn at(
|
||||
&self,
|
||||
/// The byte index. If negative, indexes from the back.
|
||||
index: i64,
|
||||
/// A default value to return if the index is out of bounds.
|
||||
#[named]
|
||||
default: Option<Value>,
|
||||
) -> StrResult<Value> {
|
||||
let len = self.len();
|
||||
self.locate_opt(index)?
|
||||
.and_then(|i| self.0[i..].graphemes(true).next().map(|s| s.into_value()))
|
||||
@ -78,41 +214,110 @@ impl Str {
|
||||
.ok_or_else(|| no_default_and_out_of_bounds(index, len))
|
||||
}
|
||||
|
||||
/// Extract a contiguous substring.
|
||||
pub fn slice(&self, start: i64, end: Option<i64>) -> StrResult<Self> {
|
||||
/// Extracts a substring of the string.
|
||||
/// Fails with an error if the start or end index is out of bounds.
|
||||
#[func]
|
||||
pub fn slice(
|
||||
&self,
|
||||
/// The start byte index (inclusive). If negative, indexes from the
|
||||
/// back.
|
||||
start: i64,
|
||||
/// The end byte index (exclusive). If omitted, the whole slice until
|
||||
/// the end of the string is extracted. If negative, indexes from the
|
||||
/// back.
|
||||
#[default]
|
||||
end: Option<i64>,
|
||||
/// The number of bytes to extract. This is equivalent to passing
|
||||
/// `start + count` as the `end` position. Mutually exclusive with `end`.
|
||||
#[named]
|
||||
count: Option<i64>,
|
||||
) -> StrResult<Str> {
|
||||
let end = end.or(count.map(|c| start + c)).unwrap_or(self.len() as i64);
|
||||
let start = self.locate(start)?;
|
||||
let end = self.locate(end.unwrap_or(self.len() as i64))?.max(start);
|
||||
let end = self.locate(end)?.max(start);
|
||||
Ok(self.0[start..end].into())
|
||||
}
|
||||
|
||||
/// The grapheme clusters the string consists of.
|
||||
/// Returns the grapheme clusters of the string as an array of substrings.
|
||||
#[func]
|
||||
pub fn clusters(&self) -> Array {
|
||||
self.as_str().graphemes(true).map(|s| Value::Str(s.into())).collect()
|
||||
}
|
||||
|
||||
/// The codepoints the string consists of.
|
||||
/// Returns the Unicode codepoints of the string as an array of substrings.
|
||||
#[func]
|
||||
pub fn codepoints(&self) -> Array {
|
||||
self.chars().map(|c| Value::Str(c.into())).collect()
|
||||
}
|
||||
|
||||
/// Whether the given pattern exists in this string.
|
||||
pub fn contains(&self, pattern: StrPattern) -> bool {
|
||||
/// Converts a character into its corresponding code point.
|
||||
///
|
||||
/// ```example
|
||||
/// #"a".to-unicode() \
|
||||
/// #("a\u{0300}"
|
||||
/// .codepoints()
|
||||
/// .map(str.to-unicode))
|
||||
/// ```
|
||||
#[func]
|
||||
pub fn to_unicode(
|
||||
/// The character that should be converted.
|
||||
character: char,
|
||||
) -> u32 {
|
||||
character as u32
|
||||
}
|
||||
|
||||
/// Converts a unicode code point into its corresponding string.
|
||||
///
|
||||
/// ```example
|
||||
/// #str.from-unicode(97)
|
||||
/// ```
|
||||
#[func]
|
||||
pub fn from_unicode(
|
||||
/// The code point that should be converted.
|
||||
value: u32,
|
||||
) -> StrResult<Str> {
|
||||
let c: char = value
|
||||
.try_into()
|
||||
.map_err(|_| eco_format!("{value:#x} is not a valid codepoint"))?;
|
||||
Ok(c.into())
|
||||
}
|
||||
|
||||
/// Whether the string contains the specified pattern.
|
||||
///
|
||||
/// This method also has dedicated syntax: You can write `{"bc" in "abcd"}`
|
||||
/// instead of `{"abcd".contains("bc")}`.
|
||||
#[func]
|
||||
pub fn contains(
|
||||
&self,
|
||||
/// The pattern to search for.
|
||||
pattern: StrPattern,
|
||||
) -> bool {
|
||||
match pattern {
|
||||
StrPattern::Str(pat) => self.0.contains(pat.as_str()),
|
||||
StrPattern::Regex(re) => re.is_match(self),
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether this string begins with the given pattern.
|
||||
pub fn starts_with(&self, pattern: StrPattern) -> bool {
|
||||
/// Whether the string starts with the specified pattern.
|
||||
#[func]
|
||||
pub fn starts_with(
|
||||
&self,
|
||||
/// The pattern the string might start with.
|
||||
pattern: StrPattern,
|
||||
) -> bool {
|
||||
match pattern {
|
||||
StrPattern::Str(pat) => self.0.starts_with(pat.as_str()),
|
||||
StrPattern::Regex(re) => re.find(self).map_or(false, |m| m.start() == 0),
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether this string ends with the given pattern.
|
||||
pub fn ends_with(&self, pattern: StrPattern) -> bool {
|
||||
/// Whether the string ends with the specified pattern.
|
||||
#[func]
|
||||
pub fn ends_with(
|
||||
&self,
|
||||
/// The pattern the string might end with.
|
||||
pattern: StrPattern,
|
||||
) -> bool {
|
||||
match pattern {
|
||||
StrPattern::Str(pat) => self.0.ends_with(pat.as_str()),
|
||||
StrPattern::Regex(re) => {
|
||||
@ -132,25 +337,52 @@ impl Str {
|
||||
}
|
||||
}
|
||||
|
||||
/// The text of the pattern's first match in this string.
|
||||
pub fn find(&self, pattern: StrPattern) -> Option<Self> {
|
||||
/// Searches for the specified pattern in the string and returns the first
|
||||
/// match as a string or `{none}` if there is no match.
|
||||
#[func]
|
||||
pub fn find(
|
||||
&self,
|
||||
/// The pattern to search for.
|
||||
pattern: StrPattern,
|
||||
) -> Option<Str> {
|
||||
match pattern {
|
||||
StrPattern::Str(pat) => self.0.contains(pat.as_str()).then_some(pat),
|
||||
StrPattern::Regex(re) => re.find(self).map(|m| m.as_str().into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// The position of the pattern's first match in this string.
|
||||
pub fn position(&self, pattern: StrPattern) -> Option<i64> {
|
||||
/// Searches for the specified pattern in the string and returns the index
|
||||
/// of the first match as an integer or `{none}` if there is no match.
|
||||
#[func]
|
||||
pub fn position(
|
||||
&self,
|
||||
/// The pattern to search for.
|
||||
pattern: StrPattern,
|
||||
) -> Option<usize> {
|
||||
match pattern {
|
||||
StrPattern::Str(pat) => self.0.find(pat.as_str()).map(|i| i as i64),
|
||||
StrPattern::Regex(re) => re.find(self).map(|m| m.start() as i64),
|
||||
StrPattern::Str(pat) => self.0.find(pat.as_str()),
|
||||
StrPattern::Regex(re) => re.find(self).map(|m| m.start()),
|
||||
}
|
||||
}
|
||||
|
||||
/// The start and, text and capture groups (if any) of the first match of
|
||||
/// the pattern in this string.
|
||||
pub fn match_(&self, pattern: StrPattern) -> Option<Dict> {
|
||||
/// Searches for the specified pattern in the string and returns a
|
||||
/// dictionary with details about the first match or `{none}` if there is no
|
||||
/// match.
|
||||
///
|
||||
/// The returned dictionary has the following keys:
|
||||
/// - `start`: The start offset of the match
|
||||
/// - `end`: The end offset of the match
|
||||
/// - `text`: The text that matched.
|
||||
/// - `captures`: An array containing a string for each matched capturing
|
||||
/// group. The first item of the array contains the first matched
|
||||
/// capturing, not the whole match! This is empty unless the `pattern` was
|
||||
/// a regex with capturing groups.
|
||||
#[func]
|
||||
pub fn match_(
|
||||
&self,
|
||||
/// The pattern to search for.
|
||||
pattern: StrPattern,
|
||||
) -> Option<Dict> {
|
||||
match pattern {
|
||||
StrPattern::Str(pat) => {
|
||||
self.0.match_indices(pat.as_str()).next().map(match_to_dict)
|
||||
@ -159,9 +391,15 @@ impl Str {
|
||||
}
|
||||
}
|
||||
|
||||
/// The start, end, text and capture groups (if any) of all matches of the
|
||||
/// pattern in this string.
|
||||
pub fn matches(&self, pattern: StrPattern) -> Array {
|
||||
/// Searches for the specified pattern in the string and returns an array of
|
||||
/// dictionaries with details about all matches. For details about the
|
||||
/// returned dictionaries, see above.
|
||||
#[func]
|
||||
pub fn matches(
|
||||
&self,
|
||||
/// The pattern to search for.
|
||||
pattern: StrPattern,
|
||||
) -> Array {
|
||||
match pattern {
|
||||
StrPattern::Str(pat) => self
|
||||
.0
|
||||
@ -177,30 +415,89 @@ impl Str {
|
||||
}
|
||||
}
|
||||
|
||||
/// Split this string at whitespace or a specific pattern.
|
||||
pub fn split(&self, pattern: Option<StrPattern>) -> Array {
|
||||
let s = self.as_str();
|
||||
match pattern {
|
||||
None => s.split_whitespace().map(|v| Value::Str(v.into())).collect(),
|
||||
Some(StrPattern::Str(pat)) => {
|
||||
s.split(pat.as_str()).map(|v| Value::Str(v.into())).collect()
|
||||
/// Replace at most `count` occurrences of the given pattern with a
|
||||
/// replacement string or function (beginning from the start). If no count
|
||||
/// is given, all occurrences are replaced.
|
||||
#[func]
|
||||
pub fn replace(
|
||||
&self,
|
||||
/// The virtual machine.
|
||||
vm: &mut Vm,
|
||||
/// The pattern to search for.
|
||||
pattern: StrPattern,
|
||||
/// The string to replace the matches with or a function that gets a
|
||||
/// dictionary for each match and can return individual replacement
|
||||
/// strings.
|
||||
replacement: Replacement,
|
||||
/// If given, only the first `count` matches of the pattern are placed.
|
||||
#[named]
|
||||
count: Option<usize>,
|
||||
) -> SourceResult<Str> {
|
||||
// Heuristic: Assume the new string is about the same length as
|
||||
// the current string.
|
||||
let mut output = EcoString::with_capacity(self.as_str().len());
|
||||
|
||||
// Replace one match of a pattern with the replacement.
|
||||
let mut last_match = 0;
|
||||
let mut handle_match = |range: Range<usize>, dict: Dict| -> SourceResult<()> {
|
||||
// Push everything until the match.
|
||||
output.push_str(&self[last_match..range.start]);
|
||||
last_match = range.end;
|
||||
|
||||
// Determine and push the replacement.
|
||||
match &replacement {
|
||||
Replacement::Str(s) => output.push_str(s),
|
||||
Replacement::Func(func) => {
|
||||
let args = Args::new(func.span(), [dict.into_value()]);
|
||||
let piece = func.call_vm(vm, args)?.cast::<Str>().at(func.span())?;
|
||||
output.push_str(&piece);
|
||||
}
|
||||
}
|
||||
Some(StrPattern::Regex(re)) => {
|
||||
re.split(s).map(|v| Value::Str(v.into())).collect()
|
||||
|
||||
Ok(())
|
||||
};
|
||||
|
||||
// Iterate over the matches of the `pattern`.
|
||||
let count = count.unwrap_or(usize::MAX);
|
||||
match &pattern {
|
||||
StrPattern::Str(pat) => {
|
||||
for m in self.match_indices(pat.as_str()).take(count) {
|
||||
let (start, text) = m;
|
||||
handle_match(start..start + text.len(), match_to_dict(m))?;
|
||||
}
|
||||
}
|
||||
StrPattern::Regex(re) => {
|
||||
for caps in re.captures_iter(self).take(count) {
|
||||
// Extract the entire match over all capture groups.
|
||||
let m = caps.get(0).unwrap();
|
||||
handle_match(m.start()..m.end(), captures_to_dict(caps))?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Push the remainder.
|
||||
output.push_str(&self[last_match..]);
|
||||
Ok(output.into())
|
||||
}
|
||||
|
||||
/// Trim either whitespace or the given pattern at both or just one side of
|
||||
/// the string. If `repeat` is true, the pattern is trimmed repeatedly
|
||||
/// instead of just once. Repeat must only be given in combination with a
|
||||
/// pattern.
|
||||
/// Removes matches of a pattern from one or both sides of the string, once or
|
||||
/// repeatedly and returns the resulting string.
|
||||
#[func]
|
||||
pub fn trim(
|
||||
&self,
|
||||
/// The pattern to search for.
|
||||
#[default]
|
||||
pattern: Option<StrPattern>,
|
||||
/// Can be `start` or `end` to only trim the start or end of the string.
|
||||
/// If omitted, both sides are trimmed.
|
||||
#[named]
|
||||
at: Option<StrSide>,
|
||||
/// Whether to repeatedly removes matches of the pattern or just once.
|
||||
/// Defaults to `{true}`.
|
||||
#[named]
|
||||
#[default(true)]
|
||||
repeat: bool,
|
||||
) -> Self {
|
||||
) -> Str {
|
||||
let mut start = matches!(at, Some(StrSide::Start) | None);
|
||||
let end = matches!(at, Some(StrSide::End) | None);
|
||||
|
||||
@ -268,101 +565,93 @@ impl Str {
|
||||
trimmed.into()
|
||||
}
|
||||
|
||||
/// Replace at most `count` occurrences of the given pattern with a
|
||||
/// replacement string or function (beginning from the start). If no count
|
||||
/// is given, all occurrences are replaced.
|
||||
pub fn replace(
|
||||
/// Splits a string at matches of a specified pattern and returns an array
|
||||
/// of the resulting parts.
|
||||
#[func]
|
||||
pub fn split(
|
||||
&self,
|
||||
vm: &mut Vm,
|
||||
pattern: StrPattern,
|
||||
with: Replacement,
|
||||
count: Option<usize>,
|
||||
) -> SourceResult<Self> {
|
||||
// Heuristic: Assume the new string is about the same length as
|
||||
// the current string.
|
||||
let mut output = EcoString::with_capacity(self.as_str().len());
|
||||
|
||||
// Replace one match of a pattern with the replacement.
|
||||
let mut last_match = 0;
|
||||
let mut handle_match = |range: Range<usize>, dict: Dict| -> SourceResult<()> {
|
||||
// Push everything until the match.
|
||||
output.push_str(&self[last_match..range.start]);
|
||||
last_match = range.end;
|
||||
|
||||
// Determine and push the replacement.
|
||||
match &with {
|
||||
Replacement::Str(s) => output.push_str(s),
|
||||
Replacement::Func(func) => {
|
||||
let args = Args::new(func.span(), [dict]);
|
||||
let piece = func.call_vm(vm, args)?.cast::<Str>().at(func.span())?;
|
||||
output.push_str(&piece);
|
||||
}
|
||||
/// The pattern to split at. Defaults to whitespace.
|
||||
#[default]
|
||||
pattern: Option<StrPattern>,
|
||||
) -> Array {
|
||||
let s = self.as_str();
|
||||
match pattern {
|
||||
None => s.split_whitespace().map(|v| Value::Str(v.into())).collect(),
|
||||
Some(StrPattern::Str(pat)) => {
|
||||
s.split(pat.as_str()).map(|v| Value::Str(v.into())).collect()
|
||||
}
|
||||
|
||||
Ok(())
|
||||
};
|
||||
|
||||
// Iterate over the matches of the `pattern`.
|
||||
let count = count.unwrap_or(usize::MAX);
|
||||
match &pattern {
|
||||
StrPattern::Str(pat) => {
|
||||
for m in self.match_indices(pat.as_str()).take(count) {
|
||||
let (start, text) = m;
|
||||
handle_match(start..start + text.len(), match_to_dict(m))?;
|
||||
}
|
||||
}
|
||||
StrPattern::Regex(re) => {
|
||||
for caps in re.captures_iter(self).take(count) {
|
||||
// Extract the entire match over all capture groups.
|
||||
let m = caps.get(0).unwrap();
|
||||
handle_match(m.start()..m.end(), captures_to_dict(caps))?;
|
||||
}
|
||||
Some(StrPattern::Regex(re)) => {
|
||||
re.split(s).map(|v| Value::Str(v.into())).collect()
|
||||
}
|
||||
}
|
||||
|
||||
// Push the remainder.
|
||||
output.push_str(&self[last_match..]);
|
||||
Ok(output.into())
|
||||
}
|
||||
|
||||
/// Repeat the string a number of times.
|
||||
pub fn repeat(&self, n: i64) -> StrResult<Self> {
|
||||
let n = usize::try_from(n)
|
||||
.ok()
|
||||
.and_then(|n| self.0.len().checked_mul(n).map(|_| n))
|
||||
.ok_or_else(|| format!("cannot repeat this string {} times", n))?;
|
||||
|
||||
Ok(Self(self.0.repeat(n)))
|
||||
}
|
||||
|
||||
/// Reverse the string.
|
||||
pub fn rev(&self) -> Self {
|
||||
#[func(title = "Reverse")]
|
||||
pub fn rev(&self) -> Str {
|
||||
self.as_str().graphemes(true).rev().collect::<String>().into()
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolve an index or throw an out of bounds error.
|
||||
fn locate(&self, index: i64) -> StrResult<usize> {
|
||||
self.locate_opt(index)?
|
||||
.ok_or_else(|| out_of_bounds(index, self.len()))
|
||||
/// A value that can be cast to a string.
|
||||
pub enum ToStr {
|
||||
/// A string value ready to be used as-is.
|
||||
Str(Str),
|
||||
/// An integer about to be formatted in a given base.
|
||||
Int(i64),
|
||||
}
|
||||
|
||||
cast! {
|
||||
ToStr,
|
||||
v: i64 => Self::Int(v),
|
||||
v: f64 => Self::Str(format_str!("{}", v)),
|
||||
v: Bytes => Self::Str(
|
||||
std::str::from_utf8(&v)
|
||||
.map_err(|_| "bytes are not valid utf-8")?
|
||||
.into()
|
||||
),
|
||||
v: Label => Self::Str(v.0.into()),
|
||||
v: Type => Self::Str(v.long_name().into()),
|
||||
v: Str => Self::Str(v),
|
||||
}
|
||||
|
||||
/// Format an integer in a base.
|
||||
fn format_int_with_base(mut n: i64, base: i64) -> EcoString {
|
||||
if n == 0 {
|
||||
return "0".into();
|
||||
}
|
||||
|
||||
/// Resolve an index, if it is within bounds and on a valid char boundary.
|
||||
///
|
||||
/// `index == len` is considered in bounds.
|
||||
fn locate_opt(&self, index: i64) -> StrResult<Option<usize>> {
|
||||
let wrapped =
|
||||
if index >= 0 { Some(index) } else { (self.len() as i64).checked_add(index) };
|
||||
|
||||
let resolved = wrapped
|
||||
.and_then(|v| usize::try_from(v).ok())
|
||||
.filter(|&v| v <= self.0.len());
|
||||
|
||||
if resolved.map_or(false, |i| !self.0.is_char_boundary(i)) {
|
||||
return Err(not_a_char_boundary(index));
|
||||
}
|
||||
|
||||
Ok(resolved)
|
||||
// In Rust, `format!("{:x}", -14i64)` is not `-e` but `fffffffffffffff2`.
|
||||
// So we can only use the built-in for decimal, not bin/oct/hex.
|
||||
if base == 10 {
|
||||
return eco_format!("{n}");
|
||||
}
|
||||
|
||||
// The largest output is `to_base(i64::MIN, 2)`, which is 65 chars long.
|
||||
const SIZE: usize = 65;
|
||||
let mut digits = [b'\0'; SIZE];
|
||||
let mut i = SIZE;
|
||||
|
||||
// It's tempting to take the absolute value, but this will fail for i64::MIN.
|
||||
// Instead, we turn n negative, as -i64::MAX is perfectly representable.
|
||||
let negative = n < 0;
|
||||
if n > 0 {
|
||||
n = -n;
|
||||
}
|
||||
|
||||
while n != 0 {
|
||||
let digit = char::from_digit(-(n % base) as u32, base as u32);
|
||||
i -= 1;
|
||||
digits[i] = digit.unwrap_or('?') as u8;
|
||||
n /= base;
|
||||
}
|
||||
|
||||
if negative {
|
||||
i -= 1;
|
||||
digits[i] = b'-';
|
||||
}
|
||||
|
||||
std::str::from_utf8(&digits[i..]).unwrap_or_default().into()
|
||||
}
|
||||
|
||||
/// The out of bounds access error message.
|
||||
@ -547,6 +836,25 @@ cast! {
|
||||
}
|
||||
|
||||
/// A regular expression.
|
||||
///
|
||||
/// Can be used as a [show rule selector]($styling/#show-rules) and with
|
||||
/// [string methods]($str) like `find`, `split`, and `replace`.
|
||||
///
|
||||
/// [See here](https://docs.rs/regex/latest/regex/#syntax) for a specification
|
||||
/// of the supported syntax.
|
||||
///
|
||||
/// # Example
|
||||
/// ```example
|
||||
/// // Works with show rules.
|
||||
/// #show regex("\d+"): set text(red)
|
||||
///
|
||||
/// The numbers 1 to 10.
|
||||
///
|
||||
/// // Works with string methods.
|
||||
/// #("a,b;c"
|
||||
/// .split(regex("[,;]")))
|
||||
/// ```
|
||||
#[ty(scope)]
|
||||
#[derive(Clone)]
|
||||
pub struct Regex(regex::Regex);
|
||||
|
||||
@ -557,6 +865,27 @@ impl Regex {
|
||||
}
|
||||
}
|
||||
|
||||
#[scope]
|
||||
impl Regex {
|
||||
/// Create a regular expression from a string.
|
||||
#[func(constructor)]
|
||||
pub fn construct(
|
||||
/// The regular expression as a string.
|
||||
///
|
||||
/// Most regex escape sequences just work because they are not valid Typst
|
||||
/// escape sequences. To produce regex escape sequences that are also valid in
|
||||
/// Typst (e.g. `[\\]`), you need to escape twice. Thus, to match a verbatim
|
||||
/// backslash, you would need to write `{regex("\\\\")}`.
|
||||
///
|
||||
/// If you need many escape sequences, you can also create a raw element
|
||||
/// and extract its text to use it for your regular expressions:
|
||||
/// ```{regex(`\d+\.\d+\.\d+`.text)}```.
|
||||
regex: Spanned<Str>,
|
||||
) -> SourceResult<Regex> {
|
||||
Self::new(®ex.v).at(regex.span)
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for Regex {
|
||||
type Target = regex::Regex;
|
||||
|
||||
@ -584,7 +913,7 @@ impl Hash for Regex {
|
||||
}
|
||||
|
||||
cast! {
|
||||
type Regex: "regular expression",
|
||||
type Regex,
|
||||
}
|
||||
|
||||
/// A pattern which can be searched for in a string.
|
||||
@ -598,8 +927,12 @@ pub enum StrPattern {
|
||||
|
||||
cast! {
|
||||
StrPattern,
|
||||
text: Str => Self::Str(text),
|
||||
regex: Regex => Self::Regex(regex),
|
||||
self => match self {
|
||||
Self::Str(v) => v.into_value(),
|
||||
Self::Regex(v) => v.into_value(),
|
||||
},
|
||||
v: Str => Self::Str(v),
|
||||
v: Regex => Self::Regex(v),
|
||||
}
|
||||
|
||||
/// A side of a string.
|
||||
@ -614,9 +947,9 @@ pub enum StrSide {
|
||||
|
||||
cast! {
|
||||
StrSide,
|
||||
align: GenAlign => match align {
|
||||
GenAlign::Start => Self::Start,
|
||||
GenAlign::End => Self::End,
|
||||
v: Align => match v {
|
||||
Align::START => Self::Start,
|
||||
Align::END => Self::End,
|
||||
_ => bail!("expected either `start` or `end`"),
|
||||
},
|
||||
}
|
||||
@ -632,6 +965,36 @@ pub enum Replacement {
|
||||
|
||||
cast! {
|
||||
Replacement,
|
||||
text: Str => Self::Str(text),
|
||||
func: Func => Self::Func(func)
|
||||
self => match self {
|
||||
Self::Str(v) => v.into_value(),
|
||||
Self::Func(v) => v.into_value(),
|
||||
},
|
||||
v: Str => Self::Str(v),
|
||||
v: Func => Self::Func(v)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_to_base() {
|
||||
assert_eq!(&format_int_with_base(0, 10), "0");
|
||||
assert_eq!(&format_int_with_base(0, 16), "0");
|
||||
assert_eq!(&format_int_with_base(0, 36), "0");
|
||||
assert_eq!(
|
||||
&format_int_with_base(i64::MAX, 2),
|
||||
"111111111111111111111111111111111111111111111111111111111111111"
|
||||
);
|
||||
assert_eq!(
|
||||
&format_int_with_base(i64::MIN, 2),
|
||||
"-1000000000000000000000000000000000000000000000000000000000000000"
|
||||
);
|
||||
assert_eq!(&format_int_with_base(i64::MAX, 10), "9223372036854775807");
|
||||
assert_eq!(&format_int_with_base(i64::MIN, 10), "-9223372036854775808");
|
||||
assert_eq!(&format_int_with_base(i64::MAX, 16), "7fffffffffffffff");
|
||||
assert_eq!(&format_int_with_base(i64::MIN, 16), "-8000000000000000");
|
||||
assert_eq!(&format_int_with_base(i64::MAX, 36), "1y2p0ij32e8e7");
|
||||
assert_eq!(&format_int_with_base(i64::MIN, 36), "-1y2p0ij32e8e8");
|
||||
}
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user