summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock4
-rw-r--r--crates/typst-library/Cargo.toml2
-rw-r--r--crates/typst-library/src/compute/calc.rs295
-rw-r--r--crates/typst-library/src/compute/construct.rs1015
-rw-r--r--crates/typst-library/src/compute/data.rs474
-rw-r--r--crates/typst-library/src/compute/foundations.rs208
-rw-r--r--crates/typst-library/src/compute/mod.rs35
-rw-r--r--crates/typst-library/src/layout/align.rs48
-rw-r--r--crates/typst-library/src/layout/columns.rs18
-rw-r--r--crates/typst-library/src/layout/container.rs68
-rw-r--r--crates/typst-library/src/layout/enum.rs49
-rw-r--r--crates/typst-library/src/layout/flow.rs55
-rw-r--r--crates/typst-library/src/layout/grid.rs7
-rw-r--r--crates/typst-library/src/layout/hide.rs7
-rw-r--r--crates/typst-library/src/layout/list.rs36
-rw-r--r--crates/typst-library/src/layout/measure.rs25
-rw-r--r--crates/typst-library/src/layout/mod.rs79
-rw-r--r--crates/typst-library/src/layout/pad.rs10
-rw-r--r--crates/typst-library/src/layout/page.rs68
-rw-r--r--crates/typst-library/src/layout/par.rs54
-rw-r--r--crates/typst-library/src/layout/place.rs26
-rw-r--r--crates/typst-library/src/layout/repeat.rs11
-rw-r--r--crates/typst-library/src/layout/spacing.rs16
-rw-r--r--crates/typst-library/src/layout/stack.rs25
-rw-r--r--crates/typst-library/src/layout/table.rs54
-rw-r--r--crates/typst-library/src/layout/terms.rs34
-rw-r--r--crates/typst-library/src/layout/transform.rs45
-rw-r--r--crates/typst-library/src/lib.rs72
-rw-r--r--crates/typst-library/src/math/accent.rs7
-rw-r--r--crates/typst-library/src/math/align.rs5
-rw-r--r--crates/typst-library/src/math/attach.rs35
-rw-r--r--crates/typst-library/src/math/cancel.rs26
-rw-r--r--crates/typst-library/src/math/class.rs7
-rw-r--r--crates/typst-library/src/math/frac.rs20
-rw-r--r--crates/typst-library/src/math/lr.rs (renamed from crates/typst-library/src/math/delimited.rs)32
-rw-r--r--crates/typst-library/src/math/matrix.rs47
-rw-r--r--crates/typst-library/src/math/mod.rs130
-rw-r--r--crates/typst-library/src/math/op.rs9
-rw-r--r--crates/typst-library/src/math/root.rs16
-rw-r--r--crates/typst-library/src/math/row.rs71
-rw-r--r--crates/typst-library/src/math/style.rs73
-rw-r--r--crates/typst-library/src/math/underover.rs44
-rw-r--r--crates/typst-library/src/meta/bibliography.rs37
-rw-r--r--crates/typst-library/src/meta/context.rs83
-rw-r--r--crates/typst-library/src/meta/counter.rs373
-rw-r--r--crates/typst-library/src/meta/document.rs5
-rw-r--r--crates/typst-library/src/meta/figure.rs82
-rw-r--r--crates/typst-library/src/meta/footnote.rs46
-rw-r--r--crates/typst-library/src/meta/heading.rs37
-rw-r--r--crates/typst-library/src/meta/link.rs26
-rw-r--r--crates/typst-library/src/meta/metadata.rs15
-rw-r--r--crates/typst-library/src/meta/mod.rs49
-rw-r--r--crates/typst-library/src/meta/numbering.rs9
-rw-r--r--crates/typst-library/src/meta/outline.rs73
-rw-r--r--crates/typst-library/src/meta/query.rs49
-rw-r--r--crates/typst-library/src/meta/reference.rs30
-rw-r--r--crates/typst-library/src/meta/state.rs236
-rw-r--r--crates/typst-library/src/prelude.rs8
-rw-r--r--crates/typst-library/src/shared/ext.rs6
-rw-r--r--crates/typst-library/src/symbols/emoji.rs2
-rw-r--r--crates/typst-library/src/symbols/mod.rs6
-rw-r--r--crates/typst-library/src/symbols/sym.rs2
-rw-r--r--crates/typst-library/src/text/deco.rs66
-rw-r--r--crates/typst-library/src/text/misc.rs68
-rw-r--r--crates/typst-library/src/text/mod.rs68
-rw-r--r--crates/typst-library/src/text/quotes.rs13
-rw-r--r--crates/typst-library/src/text/raw.rs35
-rw-r--r--crates/typst-library/src/text/shift.rs14
-rw-r--r--crates/typst-library/src/visualize/image.rs119
-rw-r--r--crates/typst-library/src/visualize/line.rs52
-rw-r--r--crates/typst-library/src/visualize/mod.rs37
-rw-r--r--crates/typst-library/src/visualize/path.rs39
-rw-r--r--crates/typst-library/src/visualize/polygon.rs176
-rw-r--r--crates/typst-library/src/visualize/shape.rs106
-rw-r--r--crates/typst-syntax/Cargo.toml2
-rw-r--r--crates/typst/Cargo.toml2
-rw-r--r--crates/typst/src/diag.rs12
-rw-r--r--crates/typst/src/doc.rs29
-rw-r--r--crates/typst/src/eval/args.rs62
-rw-r--r--crates/typst/src/eval/array.rs713
-rw-r--r--crates/typst/src/eval/auto.rs18
-rw-r--r--crates/typst/src/eval/bool.rs15
-rw-r--r--crates/typst/src/eval/bytes.rs141
-rw-r--r--crates/typst/src/eval/cast.rs126
-rw-r--r--crates/typst/src/eval/datetime.rs531
-rw-r--r--crates/typst/src/eval/dict.rs172
-rw-r--r--crates/typst/src/eval/duration.rs84
-rw-r--r--crates/typst/src/eval/fields.rs84
-rw-r--r--crates/typst/src/eval/float.rs60
-rw-r--r--crates/typst/src/eval/func.rs392
-rw-r--r--crates/typst/src/eval/int.rs153
-rw-r--r--crates/typst/src/eval/library.rs35
-rw-r--r--crates/typst/src/eval/methods.rs485
-rw-r--r--crates/typst/src/eval/mod.rs193
-rw-r--r--crates/typst/src/eval/module.rs27
-rw-r--r--crates/typst/src/eval/none.rs41
-rw-r--r--crates/typst/src/eval/ops.rs59
-rw-r--r--crates/typst/src/eval/plugin.rs122
-rw-r--r--crates/typst/src/eval/scope.rs104
-rw-r--r--crates/typst/src/eval/str.rs635
-rw-r--r--crates/typst/src/eval/symbol.rs102
-rw-r--r--crates/typst/src/eval/ty.rs165
-rw-r--r--crates/typst/src/eval/value.rs256
-rw-r--r--crates/typst/src/export/pdf/extg.rs (renamed from crates/typst/src/export/pdf/external_graphics_state.rs)3
-rw-r--r--crates/typst/src/export/pdf/mod.rs6
-rw-r--r--crates/typst/src/export/pdf/outline.rs2
-rw-r--r--crates/typst/src/export/pdf/page.rs14
-rw-r--r--crates/typst/src/export/render.rs6
-rw-r--r--crates/typst/src/export/svg.rs6
-rw-r--r--crates/typst/src/geom/align.rs449
-rw-r--r--crates/typst/src/geom/angle.rs54
-rw-r--r--crates/typst/src/geom/axes.rs23
-rw-r--r--crates/typst/src/geom/color.rs276
-rw-r--r--crates/typst/src/geom/corners.rs8
-rw-r--r--crates/typst/src/geom/dir.rs84
-rw-r--r--crates/typst/src/geom/ellipse.rs2
-rw-r--r--crates/typst/src/geom/fr.rs30
-rw-r--r--crates/typst/src/geom/length.rs98
-rw-r--r--crates/typst/src/geom/mod.rs8
-rw-r--r--crates/typst/src/geom/ratio.rs28
-rw-r--r--crates/typst/src/geom/rel.rs21
-rw-r--r--crates/typst/src/geom/rounded.rs6
-rw-r--r--crates/typst/src/geom/shape.rs4
-rw-r--r--crates/typst/src/geom/sides.rs20
-rw-r--r--crates/typst/src/geom/smart.rs12
-rw-r--r--crates/typst/src/geom/stroke.rs187
-rw-r--r--crates/typst/src/ide/complete.rs169
-rw-r--r--crates/typst/src/ide/tooltip.rs11
-rw-r--r--crates/typst/src/image.rs2
-rw-r--r--crates/typst/src/lib.rs2
-rw-r--r--crates/typst/src/model/content.rs245
-rw-r--r--crates/typst/src/model/element.rs186
-rw-r--r--crates/typst/src/model/introspect.rs51
-rw-r--r--crates/typst/src/model/label.rs32
-rw-r--r--crates/typst/src/model/mod.rs4
-rw-r--r--crates/typst/src/model/realize.rs6
-rw-r--r--crates/typst/src/model/selector.rs226
-rw-r--r--crates/typst/src/model/styles.rs45
-rw-r--r--crates/typst/src/util/fmt.rs78
-rw-r--r--crates/typst/src/util/mod.rs119
140 files changed, 6753 insertions, 5919 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 95e0511e..feb932b0 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -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",
]
diff --git a/crates/typst-library/Cargo.toml b/crates/typst-library/Cargo.toml
index 68ba7308..040a4405 100644
--- a/crates/typst-library/Cargo.toml
+++ b/crates/typst-library/Cargo.toml
@@ -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"] }
diff --git a/crates/typst-library/src/compute/calc.rs b/crates/typst-library/src/compute/calc.rs
index 83ecac5d..9043e1f1 100644
--- a/crates/typst-library/src/compute/calc.rs
+++ b/crates/typst-library/src/compute/calc.rs
@@ -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,
diff --git a/crates/typst-library/src/compute/construct.rs b/crates/typst-library/src/compute/construct.rs
deleted file mode 100644
index 6ea8bd82..00000000
--- a/crates/typst-library/src/compute/construct.rs
+++ /dev/null
@@ -1,1015 +0,0 @@
-use std::num::NonZeroI64;
-use std::str::FromStr;
-
-use time::{Month, PrimitiveDateTime};
-
-use typst::eval::{Bytes, Datetime, Duration, Module, Plugin, Reflect, Regex};
-
-use crate::prelude::*;
-
-/// Converts a value to an integer.
-///
-/// - Booleans are converted to `0` or `1`.
-/// - Floats are floored to the next 64-bit integer.
-/// - Strings are parsed in base 10.
-///
-/// ## Example { #example }
-/// ```example
-/// #int(false) \
-/// #int(true) \
-/// #int(2.7) \
-/// #{ int("27") + int("4") }
-/// ```
-///
-/// Display: Integer
-/// Category: construct
-#[func]
-pub fn int(
- /// The value that should be converted to an integer.
- value: ToInt,
-) -> i64 {
- value.0
-}
-
-/// A value that can be cast to an integer.
-pub struct ToInt(i64);
-
-cast! {
- ToInt,
- v: bool => Self(v as i64),
- v: f64 => Self(v as i64),
- v: EcoString => Self(v.parse().map_err(|_| eco_format!("invalid integer: {}", v))?),
- v: i64 => Self(v),
-}
-
-/// Converts a value to a float.
-///
-/// - Booleans are converted to `0.0` or `1.0`.
-/// - Integers are converted to the closest 64-bit float.
-/// - Ratios are divided by 100%.
-/// - Strings are parsed in base 10 to the closest 64-bit float.
-/// Exponential notation is supported.
-///
-/// ## Example { #example }
-/// ```example
-/// #float(false) \
-/// #float(true) \
-/// #float(4) \
-/// #float(40%) \
-/// #float("2.7") \
-/// #float("1e5")
-/// ```
-///
-/// Display: Float
-/// Category: construct
-#[func]
-pub fn float(
- /// The value that should be converted to a float.
- value: ToFloat,
-) -> f64 {
- value.0
-}
-
-/// A value that can be cast to a float.
-pub struct ToFloat(f64);
-
-cast! {
- ToFloat,
- v: bool => Self(v as i64 as f64),
- v: i64 => Self(v as f64),
- v: Ratio => Self(v.get()),
- v: EcoString => Self(v.parse().map_err(|_| eco_format!("invalid float: {}", v))?),
- v: f64 => Self(v),
-}
-
-/// Creates a grayscale color.
-///
-/// ## Example { #example }
-/// ```example
-/// #for x in range(250, step: 50) {
-/// box(square(fill: luma(x)))
-/// }
-/// ```
-///
-/// Display: Luma
-/// Category: construct
-#[func]
-pub fn luma(
- /// The gray component.
- gray: Component,
-) -> Color {
- LumaColor::new(gray.0).into()
-}
-
-/// Creates an RGB(A) color.
-///
-/// The color is specified in the sRGB color space.
-///
-/// ## Example { #example }
-/// ```example
-/// #square(fill: rgb("#b1f2eb"))
-/// #square(fill: rgb(87, 127, 230))
-/// #square(fill: rgb(25%, 13%, 65%))
-/// ```
-///
-/// Display: RGB
-/// Category: construct
-#[func]
-pub fn rgb(
- /// The color in hexadecimal notation.
- ///
- /// Accepts three, four, six or eight hexadecimal digits and optionally
- /// a leading hashtag.
- ///
- /// If this string is given, the individual components should not be given.
- ///
- /// ```example
- /// #text(16pt, rgb("#239dad"))[
- /// *Typst*
- /// ]
- /// ```
- #[external]
- hex: EcoString,
- /// The red component.
- #[external]
- red: Component,
- /// The green component.
- #[external]
- green: Component,
- /// The blue component.
- #[external]
- blue: Component,
- /// The alpha component.
- #[external]
- alpha: Component,
- /// The arguments.
- args: Args,
-) -> SourceResult<Color> {
- let mut args = args;
- Ok(if let Some(string) = args.find::<Spanned<EcoString>>()? {
- match RgbaColor::from_str(&string.v) {
- Ok(color) => color.into(),
- Err(msg) => bail!(string.span, "{msg}"),
- }
- } else {
- let Component(r) = args.expect("red component")?;
- let Component(g) = args.expect("green component")?;
- let Component(b) = args.expect("blue component")?;
- let Component(a) = args.eat()?.unwrap_or(Component(255));
- RgbaColor::new(r, g, b, a).into()
- })
-}
-
-/// An integer or ratio component.
-pub struct Component(u8);
-
-cast! {
- Component,
- v: i64 => match v {
- 0 ..= 255 => Self(v as u8),
- _ => bail!("number must be between 0 and 255"),
- },
- v: Ratio => if (0.0 ..= 1.0).contains(&v.get()) {
- Self((v.get() * 255.0).round() as u8)
- } else {
- bail!("ratio must be between 0% and 100%");
- },
-}
-
-/// Creates a new datetime.
-///
-/// You can specify the [datetime]($type/datetime) using a year, month, day,
-/// hour, minute, and second. You can also get the current date with
-/// [`datetime.today`]($func/datetime.today).
-///
-/// ## Example
-/// ```example
-/// #let date = datetime(
-/// year: 2012,
-/// month: 8,
-/// day: 3,
-/// )
-///
-/// #date.display() \
-/// #date.display(
-/// "[day].[month].[year]"
-/// )
-/// ```
-///
-/// ## Format
-/// _Note_: Depending on which components of the datetime you specify, Typst
-/// will store it in one of the following three ways:
-/// * If you specify year, month and day, Typst will store just a date.
-/// * If you specify hour, minute and second, Typst will store just a time.
-/// * If you specify all of year, month, day, hour, minute and second, Typst
-/// will store a full datetime.
-///
-/// Depending on how it is stored, the [`display`]($type/datetime.display)
-/// method will choose a different formatting by default.
-///
-/// Display: Datetime
-/// Category: construct
-#[func]
-#[scope(
- scope.define("today", datetime_today_func());
- scope
-)]
-pub fn datetime(
- /// The year of the datetime.
- #[named]
- year: Option<YearComponent>,
- /// The month of the datetime.
- #[named]
- month: Option<MonthComponent>,
- /// The day of the datetime.
- #[named]
- day: Option<DayComponent>,
- /// The hour of the datetime.
- #[named]
- hour: Option<HourComponent>,
- /// The minute of the datetime.
- #[named]
- minute: Option<MinuteComponent>,
- /// The second of the datetime.
- #[named]
- second: Option<SecondComponent>,
-) -> StrResult<Datetime> {
- let time = match (hour, minute, second) {
- (Some(hour), Some(minute), Some(second)) => {
- match time::Time::from_hms(hour.0, minute.0, second.0) {
- Ok(time) => Some(time),
- Err(_) => bail!("time is invalid"),
- }
- }
- (None, None, None) => None,
- _ => bail!("time is incomplete"),
- };
-
- let date = match (year, month, day) {
- (Some(year), Some(month), Some(day)) => {
- match time::Date::from_calendar_date(year.0, month.0, day.0) {
- Ok(date) => Some(date),
- Err(_) => bail!("date is invalid"),
- }
- }
- (None, None, None) => None,
- _ => bail!("date is incomplete"),
- };
-
- Ok(match (date, time) {
- (Some(date), Some(time)) => {
- Datetime::Datetime(PrimitiveDateTime::new(date, time))
- }
- (Some(date), None) => Datetime::Date(date),
- (None, Some(time)) => Datetime::Time(time),
- (None, None) => {
- bail!("at least one of date or time must be fully specified")
- }
- })
-}
-
-pub struct YearComponent(i32);
-pub struct MonthComponent(Month);
-pub struct DayComponent(u8);
-pub struct HourComponent(u8);
-pub struct MinuteComponent(u8);
-pub struct SecondComponent(u8);
-
-cast! {
- YearComponent,
- v: i32 => Self(v),
-}
-
-cast! {
- MonthComponent,
- v: u8 => Self(Month::try_from(v).map_err(|_| "month is invalid")?)
-}
-
-cast! {
- DayComponent,
- v: u8 => Self(v),
-}
-
-cast! {
- HourComponent,
- v: u8 => Self(v),
-}
-
-cast! {
- MinuteComponent,
- v: u8 => Self(v),
-}
-
-cast! {
- SecondComponent,
- v: u8 => Self(v),
-}
-
-/// Returns the current date.
-///
-/// Refer to the documentation of the [`display`]($type/datetime.display) method
-/// for details on how to affect the formatting of the date.
-///
-/// ## Example
-/// ```example
-/// Today's date is
-/// #datetime.today().display().
-/// ```
-///
-/// Display: Today
-/// Category: construct
-#[func]
-pub fn datetime_today(
- /// An offset to apply to the current UTC date. If set to `{auto}`, the
- /// offset will be the local offset.
- #[named]
- #[default]
- offset: Smart<i64>,
- /// The virtual machine.
- vt: &mut Vt,
-) -> StrResult<Datetime> {
- Ok(vt
- .world
- .today(offset.as_custom())
- .ok_or("unable to get the current date")?)
-}
-
-/// Creates a new duration.
-///
-/// You can specify the [duration]($type/duration) using weeks, days, hours,
-/// minutes and seconds. You can also get a duration by subtracting two
-/// [datetimes]($type/datetime).
-///
-/// ## Example
-/// ```example
-/// #duration(
-/// days: 3,
-/// hours: 12,
-/// ).hours()
-/// ```
-///
-/// Display: Duration
-/// Category: construct
-#[func]
-pub fn duration(
- /// 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),
- )
-}
-
-/// Creates a CMYK color.
-///
-/// This is useful if you want to target a specific printer. The conversion
-/// to RGB for display preview might differ from how your printer reproduces
-/// the color.
-///
-/// ## Example { #example }
-/// ```example
-/// #square(
-/// fill: cmyk(27%, 0%, 3%, 5%)
-/// )
-/// ```
-///
-/// Display: CMYK
-/// Category: construct
-#[func]
-pub fn cmyk(
- /// The cyan component.
- cyan: RatioComponent,
- /// The magenta component.
- magenta: RatioComponent,
- /// The yellow component.
- yellow: RatioComponent,
- /// The key component.
- key: RatioComponent,
-) -> Color {
- CmykColor::new(cyan.0, magenta.0, yellow.0, key.0).into()
-}
-
-/// A component that must be a ratio.
-pub struct RatioComponent(u8);
-
-cast! {
- RatioComponent,
- v: Ratio => if (0.0 ..= 1.0).contains(&v.get()) {
- Self((v.get() * 255.0).round() as u8)
- } else {
- bail!("ratio must be between 0% and 100%");
- },
-}
-
-/// A module with functions operating on colors.
-pub fn color_module() -> Module {
- let mut scope = Scope::new();
- scope.define("mix", mix_func());
- Module::new("color").with_scope(scope)
-}
-
-/// Create a color by mixing two or more colors.
-///
-/// ## Example { #example }
-/// ```example
-/// #set block(height: 20pt, width: 100%)
-/// #block(fill: color.mix(red, blue))
-/// #block(fill: color.mix(red, blue, space: "srgb"))
-/// #block(fill: color.mix((red, 70%), (blue, 30%)))
-/// #block(fill: color.mix(red, blue, white))
-/// ```
-///
-/// _Note:_ This function must be specified as `color.mix`, not just `mix`.
-/// Currently, `color` is a module, but it is designed to be forward compatible
-/// with a future `color` type.
-///
-/// Display: Mix
-/// Category: construct
-#[func]
-pub fn mix(
- /// The colors, optionally with weights, specified as a pair (array of
- /// length two) of color and weight (float or ratio).
- ///
- /// The weights do not need to add to `{100%}`, they are relative to the
- /// sum of all weights.
- #[variadic]
- colors: Vec<WeightedColor>,
- /// The color space to mix in. By default, this happens in a perceptual
- /// color space (Oklab).
- #[named]
- #[default(ColorSpace::Oklab)]
- space: ColorSpace,
-) -> StrResult<Color> {
- Color::mix(colors, space)
-}
-
-/// Creates a custom symbol with modifiers.
-///
-/// ## Example { #example }
-/// ```example
-/// #let envelope = symbol(
-/// "🖂",
-/// ("stamped", "🖃"),
-/// ("stamped.pen", "🖆"),
-/// ("lightning", "🖄"),
-/// ("fly", "🖅"),
-/// )
-///
-/// #envelope
-/// #envelope.stamped
-/// #envelope.stamped.pen
-/// #envelope.lightning
-/// #envelope.fly
-/// ```
-///
-/// Display: Symbol
-/// Category: construct
-#[func]
-pub fn symbol(
- /// The variants of the symbol.
- ///
- /// Can be a just a string consisting of a single character for the
- /// modifierless variant or an array with two strings specifying the modifiers
- /// and the symbol. Individual modifiers should be separated by dots. When
- /// displaying a symbol, Typst selects the first from the variants that have
- /// all attached modifiers and the minimum number of other modifiers.
- #[variadic]
- variants: Vec<Spanned<Variant>>,
- /// The callsite span.
- span: Span,
-) -> SourceResult<Symbol> {
- let mut list = Vec::new();
- if variants.is_empty() {
- bail!(span, "expected at least one variant");
- }
- for Spanned { v, span } in variants {
- if list.iter().any(|(prev, _)| &v.0 == prev) {
- bail!(span, "duplicate variant");
- }
- list.push((v.0, v.1));
- }
- Ok(Symbol::runtime(list.into_boxed_slice()))
-}
-
-/// A value that can be cast to a symbol.
-pub struct Variant(EcoString, char);
-
-cast! {
- Variant,
- c: char => Self(EcoString::new(), c),
- array: Array => {
- let mut iter = array.into_iter();
- match (iter.next(), iter.next(), iter.next()) {
- (Some(a), Some(b), None) => Self(a.cast()?, b.cast()?),
- _ => bail!("point array must contain exactly two entries"),
- }
- },
-}
-
-/// Converts a value to a string.
-///
-/// - Integers are formatted in base 10. This can be overridden with the
-/// optional `base` parameter.
-/// - Floats are formatted in base 10 and never in exponential notation.
-/// - From labels the name is extracted.
-/// - Bytes are decoded as UTF-8.
-///
-/// If you wish to convert from and to Unicode code points, see
-/// [`str.to-unicode`]($func/str.to-unicode) and
-/// [`str.from-unicode`]($func/str.from-unicode).
-///
-/// ## Example { #example }
-/// ```example
-/// #str(10) \
-/// #str(4000, base: 16) \
-/// #str(2.7) \
-/// #str(1e8) \
-/// #str(<intro>)
-/// ```
-///
-/// Display: String
-/// Category: construct
-#[func]
-#[scope(
- scope.define("to-unicode", str_to_unicode_func());
- scope.define("from-unicode", str_from_unicode_func());
- scope
-)]
-pub fn str(
- /// The value that should be converted to a string.
- value: ToStr,
- /// The base (radix) to display integers in, between 2 and 36.
- #[named]
- #[default(Spanned::new(10, Span::detached()))]
- base: Spanned<i64>,
-) -> SourceResult<Str> {
- Ok(match value {
- ToStr::Str(s) => {
- if base.v != 10 {
- bail!(base.span, "base is only supported for integers");
- }
- s
- }
- ToStr::Int(n) => {
- if base.v < 2 || base.v > 36 {
- bail!(base.span, "base must be between 2 and 36");
- }
- int_to_base(n, base.v).into()
- }
- })
-}
-
-/// A value that can be cast to a string.
-pub enum ToStr {
- /// A string value ready to be used as-is.
- Str(Str),
- /// An integer about to be formatted in a given base.
- Int(i64),
-}
-
-cast! {
- ToStr,
- v: i64 => Self::Int(v),
- v: f64 => Self::Str(format_str!("{}", v)),
- v: Label => Self::Str(v.0.into()),
- v: Bytes => Self::Str(
- std::str::from_utf8(&v)
- .map_err(|_| "bytes are not valid utf-8")?
- .into()
- ),
- v: Str => Self::Str(v),
-}
-
-/// Format an integer in a base.
-fn int_to_base(mut n: i64, base: i64) -> EcoString {
- if n == 0 {
- return "0".into();
- }
-
- // In Rust, `format!("{:x}", -14i64)` is not `-e` but `fffffffffffffff2`.
- // So we can only use the built-in for decimal, not bin/oct/hex.
- if base == 10 {
- return eco_format!("{n}");
- }
-
- // The largest output is `to_base(i64::MIN, 2)`, which is 65 chars long.
- const SIZE: usize = 65;
- let mut digits = [b'\0'; SIZE];
- let mut i = SIZE;
-
- // It's tempting to take the absolute value, but this will fail for i64::MIN.
- // Instead, we turn n negative, as -i64::MAX is perfectly representable.
- let negative = n < 0;
- if n > 0 {
- n = -n;
- }
-
- while n != 0 {
- let digit = char::from_digit(-(n % base) as u32, base as u32);
- i -= 1;
- digits[i] = digit.unwrap_or('?') as u8;
- n /= base;
- }
-
- if negative {
- i -= 1;
- digits[i] = b'-';
- }
-
- std::str::from_utf8(&digits[i..]).unwrap_or_default().into()
-}
-
-/// Converts a character into its corresponding code point.
-///
-/// ## Example
-/// ```example
-/// #str.to-unicode("a") \
-/// #"a\u{0300}".codepoints().map(str.to-unicode)
-/// ```
-///
-/// Display: String To Unicode
-/// Category: construct
-#[func]
-pub fn str_to_unicode(
- /// The character that should be converted.
- value: char,
-) -> u32 {
- value.into()
-}
-
-/// Converts a Unicode code point into its corresponding string.
-///
-/// ```example
-/// #str.from-unicode(97)
-/// ```
-///
-/// Display: String From Unicode
-/// Category: construct
-#[func]
-pub fn str_from_unicode(
- /// The code point that should be converted.
- value: CodePoint,
-) -> Str {
- format_str!("{}", value.0)
-}
-
-/// The numeric representation of a single unicode code point.
-pub struct CodePoint(char);
-
-cast! {
- CodePoint,
- v: i64 => {
- Self(v.try_into().ok().and_then(|v: u32| v.try_into().ok()).ok_or_else(
- || eco_format!("{:#x} is not a valid codepoint", v),
- )?)
- },
-}
-
-/// Creates a regular expression from a string.
-///
-/// The result can be used as a
-/// [show rule selector]($styling/#show-rules) and with
-/// [string methods]($type/string) like `find`, `split`, and `replace`.
-///
-/// See [the specification of the supported syntax](https://docs.rs/regex/latest/regex/#syntax).
-///
-/// ## Example { #example }
-/// ```example
-/// // Works with show rules.
-/// #show regex("\d+"): set text(red)
-///
-/// The numbers 1 to 10.
-///
-/// // Works with string methods.
-/// #("a,b;c"
-/// .split(regex("[,;]")))
-/// ```
-///
-/// Display: Regex
-/// Category: construct
-#[func]
-pub fn regex(
- /// The regular expression as a string.
- ///
- /// Most regex escape sequences just work because they are not valid Typst
- /// escape sequences. To produce regex escape sequences that are also valid in
- /// Typst (e.g. `[\\]`), you need to escape twice. Thus, to match a verbatim
- /// backslash, you would need to write `{regex("\\\\")}`.
- ///
- /// If you need many escape sequences, you can also create a raw element
- /// and extract its text to use it for your regular expressions:
- /// ```{regex(`\d+\.\d+\.\d+`.text)}```.
- regex: Spanned<EcoString>,
-) -> SourceResult<Regex> {
- Regex::new(&regex.v).at(regex.span)
-}
-
-/// 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))
-/// ```
-///
-/// Display: Bytes
-/// Category: construct
-#[func]
-pub fn bytes(
- /// The value that should be converted to bytes.
- value: ToBytes,
-) -> Bytes {
- value.0
-}
-
-/// 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(|v| match v {
- 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),
-}
-
-/// Creates a label from a string.
-///
-/// Inserting a label into content attaches it to the closest previous element
-/// that is not a space. Then, the element can be [referenced]($func/ref) and
-/// styled through the label.
-///
-/// ## Example { #example }
-/// ```example
-/// #show <a>: set text(blue)
-/// #show label("b"): set text(red)
-///
-/// = Heading <a>
-/// *Strong* #label("b")
-/// ```
-///
-/// ## Syntax { #syntax }
-/// This function also has dedicated syntax: You can create a label by enclosing
-/// its name in angle brackets. This works both in markup and code.
-///
-/// Display: Label
-/// Category: construct
-#[func]
-pub fn label(
- /// The name of the label.
- name: EcoString,
-) -> Label {
- Label(name)
-}
-
-/// 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))
-/// ```
-///
-/// Display: Array
-/// Category: construct
-#[func]
-pub fn array(
- /// The value that should be converted to an array.
- value: ToArray,
-) -> Array {
- value.0
-}
-
-/// A value that can be cast to bytes.
-pub struct ToArray(Array);
-
-cast! {
- ToArray,
- v: Bytes => Self(v.iter().map(|&b| Value::Int(b as i64)).collect()),
- v: Array => Self(v),
-}
-
-/// Creates an array consisting of consecutive integers.
-///
-/// If you pass just one positional parameter, it is interpreted as the `end` of
-/// the range. If you pass two, they describe the `start` and `end` of the
-/// range.
-///
-/// ## Example { #example }
-/// ```example
-/// #range(5) \
-/// #range(2, 5) \
-/// #range(20, step: 4) \
-/// #range(21, step: 4) \
-/// #range(5, 2, step: -1)
-/// ```
-///
-/// Display: Range
-/// Category: construct
-#[func]
-pub fn range(
- /// The start of the range (inclusive).
- #[external]
- #[default]
- start: i64,
- /// The end of the range (exclusive).
- #[external]
- end: i64,
- /// The distance between the generated numbers.
- #[named]
- #[default(NonZeroI64::new(1).unwrap())]
- step: NonZeroI64,
- /// The arguments.
- args: Args,
-) -> SourceResult<Array> {
- let mut args = args;
- let first = args.expect::<i64>("end")?;
- let (start, end) = match args.eat::<i64>()? {
- Some(second) => (first, second),
- None => (0, first),
- };
-
- let step = step.get();
-
- let mut x = start;
- let mut array = Array::new();
-
- while x.cmp(&end) == 0.cmp(&step) {
- array.push(Value::Int(x));
- x += step;
- }
-
- Ok(array)
-}
-
-/// Loads a WebAssembly plugin.
-///
-/// This is **advanced functionality** and not to be confused with
-/// [Typst packages]($scripting/#packages).
-///
-/// Typst is capable of interfacing with plugins compiled to WebAssembly. Plugin
-/// functions may accept multiple [byte buffers]($type/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 }
-/// ```example
-/// #let myplugin = plugin("hello.wasm")
-/// #let concat(a, b) = str(
-/// myplugin.concatenate(
-/// bytes(a),
-/// bytes(b),
-/// )
-/// )
-///
-/// #concat("hello", "world")
-/// ```
-///
-/// ## Protocol { #protocol }
-/// To be used as a plugin, a WebAssembly module must conform to the following
-/// protocol:
-///
-/// ### Exports { #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 { #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](#exported-functions) 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 { #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
-///
-/// Display: Plugin
-/// Category: construct
-#[func]
-pub fn plugin(
- /// Path to a WebAssembly file.
- path: Spanned<EcoString>,
- /// The virtual machine.
- vm: &mut Vm,
-) -> 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)
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn test_to_base() {
- assert_eq!(&int_to_base(0, 10), "0");
- assert_eq!(&int_to_base(0, 16), "0");
- assert_eq!(&int_to_base(0, 36), "0");
- assert_eq!(
- &int_to_base(i64::MAX, 2),
- "111111111111111111111111111111111111111111111111111111111111111"
- );
- assert_eq!(
- &int_to_base(i64::MIN, 2),
- "-1000000000000000000000000000000000000000000000000000000000000000"
- );
- assert_eq!(&int_to_base(i64::MAX, 10), "9223372036854775807");
- assert_eq!(&int_to_base(i64::MIN, 10), "-9223372036854775808");
- assert_eq!(&int_to_base(i64::MAX, 16), "7fffffffffffffff");
- assert_eq!(&int_to_base(i64::MIN, 16), "-8000000000000000");
- assert_eq!(&int_to_base(i64::MAX, 36), "1y2p0ij32e8e7");
- assert_eq!(&int_to_base(i64::MIN, 36), "-1y2p0ij32e8e8");
- }
-}
diff --git a/crates/typst-library/src/compute/data.rs b/crates/typst-library/src/compute/data.rs
index 222b14d3..dadf0bed 100644
--- a/crates/typst-library/src/compute/data.rs
+++ b/crates/typst-library/src/compute/data.rs
@@ -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();
-
- 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))
- }
+#[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))
+ }
- 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)
-}
+#[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.
-///
-/// 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)
+ /// 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.
diff --git a/crates/typst-library/src/compute/foundations.rs b/crates/typst-library/src/compute/foundations.rs
index 3d07a3af..dad05717 100644
--- a/crates/typst-library/src/compute/foundations.rs
+++ b/crates/typst-library/src/compute/foundations.rs
@@ -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;
diff --git a/crates/typst-library/src/compute/mod.rs b/crates/typst-library/src/compute/mod.rs
index ca95f7b7..9e897653 100644
--- a/crates/typst-library/src/compute/mod.rs
+++ b/crates/typst-library/src/compute/mod.rs
@@ -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);
}
diff --git a/crates/typst-library/src/layout/align.rs b/crates/typst-library/src/layout/align.rs
index 5f7e8bc0..f080f677 100644
--- a/crates/typst-library/src/layout/align.rs
+++ b/crates/typst-library/src/layout/align.rs
@@ -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))))
}
}
diff --git a/crates/typst-library/src/layout/columns.rs b/crates/typst-library/src/layout/columns.rs
index 6645ba9e..961bedc5 100644
--- a/crates/typst-library/src/layout/columns.rs
+++ b/crates/typst-library/src/layout/columns.rs
@@ -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.
diff --git a/crates/typst-library/src/layout/container.rs b/crates/typst-library/src/layout/container.rs
index c79669d0..e966398f 100644
--- a/crates/typst-library/src/layout/container.rs
+++ b/crates/typst-library/src/layout/container.rs
@@ -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) {
diff --git a/crates/typst-library/src/layout/enum.rs b/crates/typst-library/src/layout/enum.rs
index 8f23b6dc..c505f189 100644
--- a/crates/typst-library/src/layout/enum.rs
+++ b/crates/typst-library/src/layout/enum.rs
@@ -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]
diff --git a/crates/typst-library/src/layout/flow.rs b/crates/typst-library/src/layout/flow.rs
index 4ce78c94..fe6e9398 100644
--- a/crates/typst-library/src/layout/flow.rs
+++ b/crates/typst-library/src/layout/flow.rs
@@ -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);
}
diff --git a/crates/typst-library/src/layout/grid.rs b/crates/typst-library/src/layout/grid.rs
index 4f5175e9..06962524 100644
--- a/crates/typst-library/src/layout/grid.rs
+++ b/crates/typst-library/src/layout/grid.rs
@@ -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.
///
diff --git a/crates/typst-library/src/layout/hide.rs b/crates/typst-library/src/layout/hide.rs
index c6e83e0c..7f17a7d7 100644
--- a/crates/typst-library/src/layout/hide.rs
+++ b/crates/typst-library/src/layout/hide.rs
@@ -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]
diff --git a/crates/typst-library/src/layout/list.rs b/crates/typst-library/src/layout/list.rs
index 8bb8744b..a9dad85b 100644
--- a/crates/typst-library/src/layout/list.rs
+++ b/crates/typst-library/src/layout/list.rs
@@ -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]
diff --git a/crates/typst-library/src/layout/measure.rs b/crates/typst-library/src/layout/measure.rs
index eb8e509e..d41b7f95 100644
--- a/crates/typst-library/src/layout/measure.rs
+++ b/crates/typst-library/src/layout/measure.rs
@@ -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);
diff --git a/crates/typst-library/src/layout/mod.rs b/crates/typst-library/src/layout/mod.rs
index 3334d5aa..ace5cd6e 100644
--- a/crates/typst-library/src/layout/mod.rs
+++ b/crates/typst-library/src/layout/mod.rs
@@ -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>()
{
diff --git a/crates/typst-library/src/layout/pad.rs b/crates/typst-library/src/layout/pad.rs
index a3d5646b..d1b0cb1f 100644
--- a/crates/typst-library/src/layout/pad.rs
+++ b/crates/typst-library/src/layout/pad.rs
@@ -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()))
}
diff --git a/crates/typst-library/src/layout/page.rs b/crates/typst-library/src/layout/page.rs
index 4ef90753..d182a417 100644
--- a/crates/typst-library/src/layout/page.rs
+++ b/crates/typst-library/src/layout/page.rs
@@ -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.
diff --git a/crates/typst-library/src/layout/par.rs b/crates/typst-library/src/layout/par.rs
index 39689477..e28e661c 100644
--- a/crates/typst-library/src/layout/par.rs
+++ b/crates/typst-library/src/layout/par.rs
@@ -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),
})
diff --git a/crates/typst-library/src/layout/place.rs b/crates/typst-library/src/layout/place.rs
index 95c042ff..39a38b16 100644
--- a/crates/typst-library/src/layout/place.rs
+++ b/crates/typst-library/src/layout/place.rs
@@ -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();
diff --git a/crates/typst-library/src/layout/repeat.rs b/crates/typst-library/src/layout/repeat.rs
index 646eb991..41dede51 100644
--- a/crates/typst-library/src/layout/repeat.rs
+++ b/crates/typst-library/src/layout/repeat.rs
@@ -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() {
diff --git a/crates/typst-library/src/layout/spacing.rs b/crates/typst-library/src/layout/spacing.rs
index 69a5d952..868b3d50 100644
--- a/crates/typst-library/src/layout/spacing.rs
+++ b/crates/typst-library/src/layout/spacing.rs
@@ -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]
diff --git a/crates/typst-library/src/layout/stack.rs b/crates/typst-library/src/layout/stack.rs
index 52a2f289..d3fcba8d 100644
--- a/crates/typst-library/src/layout/stack.rs
+++ b/crates/typst-library/src/layout/stack.rs
@@ -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));
diff --git a/crates/typst-library/src/layout/table.rs b/crates/typst-library/src/layout/table.rs
index a7bc8a0e..4bbe79a6 100644
--- a/crates/typst-library/src/layout/table.rs
+++ b/crates/typst-library/src/layout/table.rs
@@ -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 {
diff --git a/crates/typst-library/src/layout/terms.rs b/crates/typst-library/src/layout/terms.rs
index d693f100..07f17bb0 100644
--- a/crates/typst-library/src/layout/terms.rs
+++ b/crates/typst-library/src/layout/terms.rs
@@ -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]
diff --git a/crates/typst-library/src/layout/transform.rs b/crates/typst-library/src/layout/transform.rs
index a57a5edc..012a146d 100644
--- a/crates/typst-library/src/layout/transform.rs
+++ b/crates/typst-library/src/layout/transform.rs
@@ -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));
diff --git a/crates/typst-library/src/lib.rs b/crates/typst-library/src/lib.rs
index e9bb72ce..bdb97f84 100644
--- a/crates/typst-library/src/lib.rs
+++ b/crates/typst-library/src/lib.rs
@@ -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)
- }
- },
}
}
diff --git a/crates/typst-library/src/math/accent.rs b/crates/typst-library/src/math/accent.rs
index d1bee198..c92f9585 100644
--- a/crates/typst-library/src/math/accent.rs
+++ b/crates/typst-library/src/math/accent.rs
@@ -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.
diff --git a/crates/typst-library/src/math/align.rs b/crates/typst-library/src/math/align.rs
index aee89a89..bf81597c 100644
--- a/crates/typst-library/src/math/align.rs
+++ b/crates/typst-library/src/math/align.rs
@@ -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 {
diff --git a/crates/typst-library/src/math/attach.rs b/crates/typst-library/src/math/attach.rs
index d74beafe..c33b58e4 100644
--- a/crates/typst-library/src/math/attach.rs
+++ b/crates/typst-library/src/math/attach.rs
@@ -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]
diff --git a/crates/typst-library/src/math/cancel.rs b/crates/typst-library/src/math/cancel.rs
index f576a727..d27031b9 100644
--- a/crates/typst-library/src/math/cancel.rs
+++ b/crates/typst-library/src/math/cancel.rs
@@ -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(
diff --git a/crates/typst-library/src/math/class.rs b/crates/typst-library/src/math/class.rs
index 69635c62..fc8a6c79 100644
--- a/crates/typst-library/src/math/class.rs
+++ b/crates/typst-library/src/math/class.rs
@@ -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]
diff --git a/crates/typst-library/src/math/frac.rs b/crates/typst-library/src/math/frac.rs
index cf1d38e9..6a296203 100644
--- a/crates/typst-library/src/math/frac.rs
+++ b/crates/typst-library/src/math/frac.rs
@@ -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,
),
diff --git a/crates/typst-library/src/math/delimited.rs b/crates/typst-library/src/math/lr.rs
index 25ecf623..0d3c855e 100644
--- a/crates/typst-library/src/math/delimited.rs
+++ b/crates/typst-library/src/math/lr.rs
@@ -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.
diff --git a/crates/typst-library/src/math/matrix.rs b/crates/typst-library/src/math/matrix.rs
index c913592d..abb0da35 100644
--- a/crates/typst-library/src/math/matrix.rs
+++ b/crates/typst-library/src/math/matrix.rs
@@ -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 }
diff --git a/crates/typst-library/src/math/mod.rs b/crates/typst-library/src/math/mod.rs
index 5d32af64..578064ba 100644
--- a/crates/typst-library/src/math/mod.rs
+++ b/crates/typst-library/src/math/mod.rs
@@ -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());
-
- // 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());
+ 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>();
+
+ // 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> {
diff --git a/crates/typst-library/src/math/op.rs b/crates/typst-library/src/math/op.rs
index 4016d24f..eed16465 100644
--- a/crates/typst-library/src/math/op.rs
+++ b/crates/typst-library/src/math/op.rs
@@ -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]
diff --git a/crates/typst-library/src/math/root.rs b/crates/typst-library/src/math/root.rs
index b889f1cb..03d0a212 100644
--- a/crates/typst-library/src/math/root.rs
+++ b/crates/typst-library/src/math/root.rs
@@ -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,
),
diff --git a/crates/typst-library/src/math/row.rs b/crates/typst-library/src/math/row.rs
index 687f82b8..cf3a8af2 100644
--- a/crates/typst-library/src/math/row.rs
+++ b/crates/typst-library/src/math/row.rs
@@ -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)
- };
+ if !self.iter().any(|frag| matches!(frag, MathFragment::Linebreak)) {
+ return self.into_line_frame(points, align);
+ }
- let mut rows: Vec<_> = self.rows();
+ let leading = if ctx.style.size >= MathSize::Text {
+ ParElem::leading_in(ctx.styles())
+ } else {
+ TIGHT_LEADING.scaled(ctx)
+ };
- if matches!(rows.last(), Some(row) if row.0.is_empty()) {
- rows.pop();
- }
+ let mut rows: Vec<_> = self.rows();
- let AlignmentResult { points, width } = alignments(&rows);
- let mut frame = Frame::new(Size::zero());
+ if matches!(rows.last(), Some(row) if row.0.is_empty()) {
+ rows.pop();
+ }
- 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 AlignmentResult { points, width } = alignments(&rows);
+ let mut frame = Frame::new(Size::zero());
- 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);
+ 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;
}
- frame
- } else {
- self.into_line_frame(points, align)
+
+ 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())
diff --git a/crates/typst-library/src/math/style.rs b/crates/typst-library/src/math/style.rs
index 35a4cfdb..4d80a235 100644
--- a/crates/typst-library/src/math/style.rs
+++ b/crates/typst-library/src/math/style.rs
@@ -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]
diff --git a/crates/typst-library/src/math/underover.rs b/crates/typst-library/src/math/underover.rs
index 796c9ebc..3e8dba1a 100644
--- a/crates/typst-library/src/math/underover.rs
+++ b/crates/typst-library/src/math/underover.rs
@@ -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 {
diff --git a/crates/typst-library/src/meta/bibliography.rs b/crates/typst-library/src/meta/bibliography.rs
index d871db23..4f1cc32c 100644
--- a/crates/typst-library/src/meta/bibliography.rs
+++ b/crates/typst-library/src/meta/bibliography.rs
@@ -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>() {
diff --git a/crates/typst-library/src/meta/context.rs b/crates/typst-library/src/meta/context.rs
index a42c6980..3a82a925 100644
--- a/crates/typst-library/src/meta/context.rs
+++ b/crates/typst-library/src/meta/context.rs
@@ -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]
diff --git a/crates/typst-library/src/meta/counter.rs b/crates/typst-library/src/meta/counter.rs
index 6c437469..a2a63e81 100644
--- a/crates/typst-library/src/meta/counter.rs
+++ b/crates/typst-library/src/meta/counter.rs
@@ -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)))
- }
-
- /// 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)
+ pub fn of(func: Element) -> Self {
+ Self::construct(CounterKey::Selector(Selector::Elem(func, None)))
}
- /// 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]
diff --git a/crates/typst-library/src/meta/document.rs b/crates/typst-library/src/meta/document.rs
index db036e0a..66f8aeb5 100644
--- a/crates/typst-library/src/meta/document.rs
+++ b/crates/typst-library/src/meta/document.rs
@@ -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.
diff --git a/crates/typst-library/src/meta/figure.rs b/crates/typst-library/src/meta/figure.rs
index d63ae3a8..6e95dce7 100644
--- a/crates/typst-library/src/meta/figure.rs
+++ b/crates/typst-library/src/meta/figure.rs
@@ -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),
}
diff --git a/crates/typst-library/src/meta/footnote.rs b/crates/typst-library/src/meta/footnote.rs
index 848c0b7c..ed7242bb 100644
--- a/crates/typst-library/src/meta/footnote.rs
+++ b/crates/typst-library/src/meta/footnote.rs
@@ -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.
-///
-/// [issue]: https://github.com/typst/typst/issues/1348#issuecomment-1566316463
+/// See [here](https://github.com/typst/typst/issues/1348#issuecomment-1566316463)
+/// for more information.
///
-/// ## 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)
diff --git a/crates/typst-library/src/meta/heading.rs b/crates/typst-library/src/meta/heading.rs
index 84da7c03..edd2d0f0 100644
--- a/crates/typst-library/src/meta/heading.rs
+++ b/crates/typst-library/src/meta/heading.rs
@@ -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;
diff --git a/crates/typst-library/src/meta/link.rs b/crates/typst-library/src/meta/link.rs
index 2a53b84f..7b68b186 100644
--- a/crates/typst-library/src/meta/link.rs
+++ b/crates/typst-library/src/meta/link.rs
@@ -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>
diff --git a/crates/typst-library/src/meta/metadata.rs b/crates/typst-library/src/meta/metadata.rs
index 3e08a9f7..b4ae64cb 100644
--- a/crates/typst-library/src/meta/metadata.rs
+++ b/crates/typst-library/src/meta/metadata.rs
@@ -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]
diff --git a/crates/typst-library/src/meta/mod.rs b/crates/typst-library/src/meta/mod.rs
index bb6ac3d3..659cb5a3 100644
--- a/crates/typst-library/src/meta/mod.rs
+++ b/crates/typst-library/src/meta/mod.rs
@@ -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.
diff --git a/crates/typst-library/src/meta/numbering.rs b/crates/typst-library/src/meta/numbering.rs
index 8698f7b9..40308af0 100644
--- a/crates/typst-library/src/meta/numbering.rs
+++ b/crates/typst-library/src/meta/numbering.rs
@@ -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)
}
diff --git a/crates/typst-library/src/meta/outline.rs b/crates/typst-library/src/meta/outline.rs
index 6fd4ebd0..8583d96f 100644
--- a/crates/typst-library/src/meta/outline.rs
+++ b/crates/typst-library/src/meta/outline.rs
@@ -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]
diff --git a/crates/typst-library/src/meta/query.rs b/crates/typst-library/src/meta/query.rs
index eb520896..d6c600d7 100644
--- a/crates/typst-library/src/meta/query.rs
+++ b/crates/typst-library/src/meta/query.rs
@@ -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
-}
diff --git a/crates/typst-library/src/meta/reference.rs b/crates/typst-library/src/meta/reference.rs
index 015157a8..7f05bfcc 100644
--- a/crates/typst-library/src/meta/reference.rs
+++ b/crates/typst-library/src/meta/reference.rs
@@ -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()
diff --git a/crates/typst-library/src/meta/state.rs b/crates/typst-library/src/meta/state.rs
index ee2a6e32..f7b5eb77 100644
--- a/crates/typst-library/src/meta/state.rs
+++ b/crates/typst-library/src/meta/state.rs
@@ -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]
diff --git a/crates/typst-library/src/prelude.rs b/crates/typst-library/src/prelude.rs
index b4acbea0..aca5a064 100644
--- a/crates/typst-library/src/prelude.rs
+++ b/crates/typst-library/src/prelude.rs
@@ -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,
};
diff --git a/crates/typst-library/src/shared/ext.rs b/crates/typst-library/src/shared/ext.rs
index d7c80a30..e6431423 100644
--- a/crates/typst-library/src/shared/ext.rs
+++ b/crates/typst-library/src/shared/ext.rs
@@ -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 {
diff --git a/crates/typst-library/src/symbols/emoji.rs b/crates/typst-library/src/symbols/emoji.rs
index 44bc3e14..b35dfcaa 100644
--- a/crates/typst-library/src/symbols/emoji.rs
+++ b/crates/typst-library/src/symbols/emoji.rs
@@ -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.
diff --git a/crates/typst-library/src/symbols/mod.rs b/crates/typst-library/src/symbols/mod.rs
index 5036aa11..0d288c3b 100644
--- a/crates/typst-library/src/symbols/mod.rs
+++ b/crates/typst-library/src/symbols/mod.rs
@@ -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());
}
diff --git a/crates/typst-library/src/symbols/sym.rs b/crates/typst-library/src/symbols/sym.rs
index a9ec2485..bfc22ea4 100644
--- a/crates/typst-library/src/symbols/sym.rs
+++ b/crates/typst-library/src/symbols/sym.rs
@@ -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.
diff --git a/crates/typst-library/src/text/deco.rs b/crates/typst-library/src/text/deco.rs
index c97ef325..e34bf363 100644
--- a/crates/typst-library/src/text/deco.rs
+++ b/crates/typst-library/src/text/deco.rs
@@ -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;
diff --git a/crates/typst-library/src/text/misc.rs b/crates/typst-library/src/text/misc.rs
index 811b027e..73657345 100644
--- a/crates/typst-library/src/text/misc.rs
+++ b/crates/typst-library/src/text/misc.rs
@@ -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,
diff --git a/crates/typst-library/src/text/mod.rs b/crates/typst-library/src/text/mod.rs
index 4f3c1591..8bce5e8a 100644
--- a/crates/typst-library/src/text/mod.rs
+++ b/crates/typst-library/src/text/mod.rs
@@ -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
diff --git a/crates/typst-library/src/text/quotes.rs b/crates/typst-library/src/text/quotes.rs
index cf4a03d5..a47f7ed5 100644
--- a/crates/typst-library/src/text/quotes.rs
+++ b/crates/typst-library/src/text/quotes.rs
@@ -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,
diff --git a/crates/typst-library/src/text/raw.rs b/crates/typst-library/src/text/raw.rs
index a5699afd..1f46f94d 100644
--- a/crates/typst-library/src/text/raw.rs
+++ b/crates/typst-library/src/text/raw.rs
@@ -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)
}
}
diff --git a/crates/typst-library/src/text/shift.rs b/crates/typst-library/src/text/shift.rs
index 65e309e1..6cb4d895 100644
--- a/crates/typst-library/src/text/shift.rs
+++ b/crates/typst-library/src/text/shift.rs
@@ -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.
///
diff --git a/crates/typst-library/src/visualize/image.rs b/crates/typst-library/src/visualize/image.rs
index a06509dd..e6269198 100644
--- a/crates/typst-library/src/visualize/image.rs
+++ b/crates/typst-library/src/visualize/image.rs
@@ -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);
- }
- 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);
+#[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())
}
- 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) {
diff --git a/crates/typst-library/src/visualize/line.rs b/crates/typst-library/src/visualize/line.rs
index a476ffa7..9960a2d3 100644
--- a/crates/typst-library/src/visualize/line.rs
+++ b/crates/typst-library/src/visualize/line.rs
@@ -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(|| {
diff --git a/crates/typst-library/src/visualize/mod.rs b/crates/typst-library/src/visualize/mod.rs
index ea873f44..a013853f 100644
--- a/crates/typst-library/src/visualize/mod.rs
+++ b/crates/typst-library/src/visualize/mod.rs
@@ -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>();
}
diff --git a/crates/typst-library/src/visualize/path.rs b/crates/typst-library/src/visualize/path.rs
index d78abce1..c252e95f 100644
--- a/crates/typst-library/src/visualize/path.rs
+++ b/crates/typst-library/src/visualize/path.rs
@@ -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.
+ ///
+ /// 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).
+ /// 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);
diff --git a/crates/typst-library/src/visualize/polygon.rs b/crates/typst-library/src/visualize/polygon.rs
index b244b2e9..9f573467 100644
--- a/crates/typst-library/src/visualize/polygon.rs
+++ b/crates/typst-library/src/visualize/polygon.rs
@@ -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()
-}
diff --git a/crates/typst-library/src/visualize/shape.rs b/crates/typst-library/src/visualize/shape.rs
index 6129b70b..64d1ece6 100644
--- a/crates/typst-library/src/visualize/shape.rs
+++ b/crates/typst-library/src/visualize/shape.rs
@@ -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.
diff --git a/crates/typst-syntax/Cargo.toml b/crates/typst-syntax/Cargo.toml
index ec25a336..fe56e5dd 100644
--- a/crates/typst-syntax/Cargo.toml
+++ b/crates/typst-syntax/Cargo.toml
@@ -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"
diff --git a/crates/typst/Cargo.toml b/crates/typst/Cargo.toml
index 2634d6d0..e3c4c6c1 100644
--- a/crates/typst/Cargo.toml
+++ b/crates/typst/Cargo.toml
@@ -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"
diff --git a/crates/typst/src/diag.rs b/crates/typst/src/diag.rs
index a6379d65..846ab78f 100644
--- a/crates/typst/src/diag.rs
+++ b/crates/typst/src/diag.rs
@@ -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>>>;
diff --git a/crates/typst/src/doc.rs b/crates/typst/src/doc.rs
index 6f005097..20e03b6b 100644
--- a/crates/typst/src/doc.rs
+++ b/crates/typst/src/doc.rs
@@ -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 {
diff --git a/crates/typst/src/eval/args.rs b/crates/typst/src/eval/args.rs
index 81dd5845..ac403eef 100644
--- a/crates/typst/src/eval/args.rs
+++ b/crates/typst/src/eval/args.rs
@@ -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()
diff --git a/crates/typst/src/eval/array.rs b/crates/typst/src/eval/array.rs
index 41def66c..34bfc80e 100644
--- a/crates/typst/src/eval/array.rs
+++ b/crates/typst/src/eval/array.rs
@@ -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,78 +329,259 @@ 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> {
+ /// 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())
+ })
+ .collect()
+ }
+
+ /// 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(other)
+ .map(|(first, second)| array![first.clone(), second].into_value())
+ .collect());
+ }
+
+ // If there is more than one array, we use the manual method.
+ let mut out = Self::with_capacity(self.len());
+ let mut iterators = args
+ .all::<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());
+ row.push(this.clone());
+
+ for iterator in &mut iterators {
+ let Some(item) = iterator.next() else {
+ return Ok(out);
+ };
+
+ row.push(item);
+ }
+
+ out.push(row.into_value());
+ }
+
+ Ok(out)
+ }
+
+ /// 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(func.span(), [acc, item.clone()]);
- acc = func.call_vm(vm, args)?;
+ let args = Args::new(folder.span(), [acc, item.clone()]);
+ acc = folder.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> {
+ /// 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()
- .map(|x| x.clone())
- .or_else(|_| {
- default.ok_or_else(|| {
- eco_format!("cannot calculate sum of empty array with no default")
- })
- })
- .at(span)?;
+ .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()).at(span)?;
+ acc = add(acc, i.clone())?;
}
Ok(acc)
}
- /// Calculates the product of the array's items
- pub fn product(&self, default: Option<Value>, span: Span) -> SourceResult<Value> {
+ /// 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()
- .map(|x| x.clone())
- .or_else(|_| {
- default.ok_or_else(|| {
- eco_format!("cannot calculate product of empty array with no default")
- })
- })
- .at(span)?;
+ .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()).at(span)?;
+ acc = mul(acc, i.clone())?;
}
Ok(acc)
}
- /// Whether any item matches.
- pub fn any(&self, vm: &mut Vm, func: Func) -> SourceResult<bool> {
+ /// 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(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())? {
return Ok(true);
}
}
@@ -233,11 +589,18 @@ impl Array {
Ok(false)
}
- /// Whether all items match.
- pub fn all(&self, vm: &mut Vm, func: Func) -> SourceResult<bool> {
+ /// 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(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())? {
return Ok(false);
}
}
@@ -245,8 +608,9 @@ impl Array {
Ok(true)
}
- /// Return a new array with all items from this and nested arrays.
- pub fn flatten(&self) -> Self {
+ /// 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 {
@@ -258,32 +622,47 @@ impl Array {
flat.into()
}
- /// Returns a new array with reversed order.
- pub fn rev(&self) -> Self {
+ /// 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 all values in the array.
- pub fn split(&self, at: Value) -> Array {
+ /// 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()
}
- /// 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> {
+ /// 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 sep = sep.unwrap_or(Value::None);
+ 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, sep.clone())?;
+ result = ops::join(result, separator.clone())?;
}
}
@@ -295,9 +674,10 @@ impl Array {
/// Returns an array with a copy of the separator value placed between
/// adjacent elements.
+ #[func]
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.
+ // 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,
@@ -317,57 +697,23 @@ impl Array {
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> {
- // Fast path for just two arrays.
- if args.remaining() <= 1 {
- return Ok(self
- .iter()
- .zip(args.expect::<Array>("others")?)
- .map(|(first, second)| array![first.clone(), second].into_value())
- .collect());
- }
-
- // If there is more than one array, we use the manual method.
- let mut out = Self::with_capacity(self.len());
- let mut iterators = args
- .all::<Array>()?
- .into_iter()
- .map(|i| i.into_iter())
- .collect::<Vec<_>>();
-
- for this in self.iter() {
- let mut row = Self::with_capacity(1 + iterators.len());
- row.push(this.clone());
-
- for iterator in &mut iterators {
- let Some(item) = iterator.next() else {
- return Ok(out);
- };
-
- row.push(item);
- }
-
- out.push(row.into_value());
- }
-
- Ok(out)
- }
-
- /// Return a sorted version of this array, optionally by a given key function.
+ /// 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()
- }
-
- /// 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) };
+/// A value that can be cast to bytes.
+pub struct ToArray(Array);
- 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 {
diff --git a/crates/typst/src/eval/auto.rs b/crates/typst/src/eval/auto.rs
index e73b3f33..a9d8fc9e 100644
--- a/crates/typst/src/eval/auto.rs
+++ b/crates/typst/src/eval/auto.rs
@@ -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 {
diff --git a/crates/typst/src/eval/bool.rs b/crates/typst/src/eval/bool.rs
new file mode 100644
index 00000000..dcc73e66
--- /dev/null
+++ b/crates/typst/src/eval/bool.rs
@@ -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;
diff --git a/crates/typst/src/eval/bytes.rs b/crates/typst/src/eval/bytes.rs
index 2e0c098f..12f9bcf1 100644
--- a/crates/typst/src/eval/bytes.rs
+++ b/crates/typst/src/eval/bytes.rs
@@ -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 {
diff --git a/crates/typst/src/eval/cast.rs b/crates/typst/src/eval/cast.rs
index 85cd02d4..14413a61 100644
--- a/crates/typst/src/eval/cast.rs
+++ b/crates/typst/src/eval/cast.rs
@@ -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![])
}
diff --git a/crates/typst/src/eval/datetime.rs b/crates/typst/src/eval/datetime.rs
index ff2d7634..6b340a8e 100644
--- a/crates/typst/src/eval/datetime.rs
+++ b/crates/typst/src/eval/datetime.rs
@@ -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,6 +124,82 @@ pub enum Datetime {
}
impl Datetime {
+ /// Create a datetime from year, month, and day.
+ pub fn from_ymd(year: i32, month: u8, day: u8) -> Option<Self> {
+ Some(Datetime::Date(
+ time::Date::from_calendar_date(year, time::Month::try_from(month).ok()?, day)
+ .ok()?,
+ ))
+ }
+
+ /// Create a datetime from hour, minute, and second.
+ pub fn from_hms(hour: u8, minute: u8, second: u8) -> Option<Self> {
+ Some(Datetime::Time(time::Time::from_hms(hour, minute, second).ok()?))
+ }
+
+ /// Create a datetime from day and time.
+ pub fn from_ymd_hms(
+ year: i32,
+ month: u8,
+ day: u8,
+ hour: u8,
+ minute: u8,
+ second: u8,
+ ) -> Option<Self> {
+ let date =
+ time::Date::from_calendar_date(year, time::Month::try_from(month).ok()?, day)
+ .ok()?;
+ let time = time::Time::from_hms(hour, minute, second).ok()?;
+ Some(Datetime::Datetime(PrimitiveDateTime::new(date, time)))
+ }
+
+ /// 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
+ }
+ }
+
/// Which kind of variant this datetime stores.
pub fn kind(&self) -> &'static str {
match self {
@@ -33,163 +208,274 @@ impl Datetime {
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]",
- });
+#[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"),
+ };
- let format = format_description::parse(pattern)
- .map_err(format_time_invalid_format_description_error)?;
+ 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"),
+ };
- 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);
+ 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]"))
+ }
+ },
- formatted_result.map_err(format_time_format_error)
+ 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)
}
- /// Return the year of the datetime, if existing.
+ /// The year if it was specified or `{none}`, otherwise.
+ #[func]
pub fn year(&self) -> Option<i32> {
match self {
- Datetime::Date(date) => Some(date.year()),
- Datetime::Time(_) => None,
- Datetime::Datetime(datetime) => Some(datetime.year()),
+ Self::Date(date) => Some(date.year()),
+ Self::Time(_) => None,
+ Self::Datetime(datetime) => Some(datetime.year()),
}
}
- /// Return the month of the datetime, if existing.
+ /// The month if it was specified or `{none}`, otherwise.
+ #[func]
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()),
+ Self::Date(date) => Some(date.month().into()),
+ Self::Time(_) => None,
+ Self::Datetime(datetime) => Some(datetime.month().into()),
}
}
- /// Return the weekday of the datetime, if existing.
+ /// The weekday if it was specified or `{none}`, otherwise.
+ #[func]
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()),
+ Self::Date(date) => Some(date.weekday().number_from_monday()),
+ Self::Time(_) => None,
+ Self::Datetime(datetime) => Some(datetime.weekday().number_from_monday()),
}
}
- /// Return the day of the datetime, if existing.
+ /// The day if it was specified or `{none}`, otherwise.
+ #[func]
pub fn day(&self) -> Option<u8> {
match self {
- Datetime::Date(date) => Some(date.day()),
- Datetime::Time(_) => None,
- Datetime::Datetime(datetime) => Some(datetime.day()),
+ Self::Date(date) => Some(date.day()),
+ Self::Time(_) => None,
+ Self::Datetime(datetime) => Some(datetime.day()),
}
}
- /// Return the hour of the datetime, if existing.
+ /// The hour if it was specified or `{none}`, otherwise.
+ #[func]
pub fn hour(&self) -> Option<u8> {
match self {
- Datetime::Date(_) => None,
- Datetime::Time(time) => Some(time.hour()),
- Datetime::Datetime(datetime) => Some(datetime.hour()),
+ Self::Date(_) => None,
+ Self::Time(time) => Some(time.hour()),
+ Self::Datetime(datetime) => Some(datetime.hour()),
}
}
- /// Return the minute of the datetime, if existing.
+ /// The minute if it was specified or `{none}`, otherwise.
+ #[func]
pub fn minute(&self) -> Option<u8> {
match self {
- Datetime::Date(_) => None,
- Datetime::Time(time) => Some(time.minute()),
- Datetime::Datetime(datetime) => Some(datetime.minute()),
+ Self::Date(_) => None,
+ Self::Time(time) => Some(time.minute()),
+ Self::Datetime(datetime) => Some(datetime.minute()),
}
}
- /// Return the second of the datetime, if existing.
+ /// The second if it was specified or `{none}`, otherwise.
+ #[func]
pub fn second(&self) -> Option<u8> {
match self {
Datetime::Date(_) => None,
- Datetime::Time(time) => Some(time.second()),
- Datetime::Datetime(datetime) => Some(datetime.second()),
+ Self::Time(time) => Some(time.second()),
+ Self::Datetime(datetime) => Some(datetime.second()),
}
}
- /// Return the ordinal (day of the year), if existing.
+ /// The ordinal (day of the year), if it exists.
+ #[func]
pub fn ordinal(&self) -> Option<u16> {
match self {
- Datetime::Datetime(datetime) => Some(datetime.ordinal()),
- Datetime::Date(date) => Some(date.ordinal()),
- Datetime::Time(_) => None,
+ Self::Datetime(datetime) => Some(datetime.ordinal()),
+ Self::Date(date) => Some(date.ordinal()),
+ Self::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(
- time::Date::from_calendar_date(year, time::Month::try_from(month).ok()?, day)
- .ok()?,
- ))
- }
-
- /// Create a datetime from hour, minute, and second.
- pub fn from_hms(hour: u8, minute: u8, second: u8) -> Option<Self> {
- Some(Datetime::Time(time::Time::from_hms(hour, minute, second).ok()?))
- }
+impl Debug for Datetime {
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ let year = self.year().map(|y| eco_format!("year: {y}"));
+ let month = self.month().map(|m| eco_format!("month: {m}"));
+ let day = self.day().map(|d| eco_format!("day: {d}"));
+ let hour = self.hour().map(|h| eco_format!("hour: {h}"));
+ let minute = self.minute().map(|m| eco_format!("minute: {m}"));
+ let second = self.second().map(|s| eco_format!("second: {s}"));
+ let filtered = [year, month, day, hour, minute, second]
+ .into_iter()
+ .flatten()
+ .collect::<EcoVec<_>>();
- /// Create a datetime from day and time.
- pub fn from_ymd_hms(
- year: i32,
- month: u8,
- day: u8,
- hour: u8,
- minute: u8,
- second: u8,
- ) -> Option<Self> {
- let date =
- time::Date::from_calendar_date(year, time::Month::try_from(month).ok()?, day)
- .ok()?;
- let time = time::Time::from_hms(hour, minute, second).ok()?;
- Some(Datetime::Datetime(PrimitiveDateTime::new(date, time)))
+ write!(f, "datetime{}", &pretty_array_like(&filtered, false))
}
}
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),
+ (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 = Datetime;
+ type Output = Self;
fn add(self, rhs: Duration) -> Self::Output {
let rhs: time::Duration = rhs.into();
match self {
- Datetime::Datetime(datetime) => Self::Datetime(datetime + rhs),
- Datetime::Date(date) => Self::Date(date + rhs),
- Datetime::Time(time) => Self::Time(time + rhs),
+ 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 = Datetime;
+ type Output = Self;
fn sub(self, rhs: Duration) -> Self::Output {
let rhs: time::Duration = rhs.into();
match self {
- Datetime::Datetime(datetime) => Self::Datetime(datetime - rhs),
- Datetime::Date(date) => Self::Date(date - rhs),
- Datetime::Time(time) => Self::Time(time - rhs),
+ Self::Datetime(datetime) => Self::Datetime(datetime - rhs),
+ Self::Date(date) => Self::Date(date - rhs),
+ Self::Time(time) => Self::Time(time - rhs),
}
}
}
@@ -199,28 +485,61 @@ impl Sub for Datetime {
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()),
+ (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()),
}
}
}
-impl Debug for Datetime {
- fn fmt(&self, f: &mut Formatter) -> fmt::Result {
- let year = self.year().map(|y| eco_format!("year: {y}"));
- let month = self.month().map(|m| eco_format!("month: {m}"));
- let day = self.day().map(|d| eco_format!("day: {d}"));
- let hour = self.hour().map(|h| eco_format!("hour: {h}"));
- let minute = self.minute().map(|m| eco_format!("minute: {m}"));
- let second = self.second().map(|s| eco_format!("second: {s}"));
- let filtered = [year, month, day, hour, minute, second]
- .into_iter()
- .flatten()
- .collect::<EcoVec<_>>();
+pub struct YearComponent(i32);
+pub struct MonthComponent(Month);
+pub struct DayComponent(u8);
+pub struct HourComponent(u8);
+pub struct MinuteComponent(u8);
+pub struct SecondComponent(u8);
- write!(f, "datetime{}", &pretty_array_like(&filtered, false))
+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)
}
}
diff --git a/crates/typst/src/eval/dict.rs b/crates/typst/src/eval/dict.rs
index a1a3948a..5f17a691 100644
--- a/crates/typst/src/eval/dict.rs
+++ b/crates/typst/src/eval/dict.rs
@@ -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() {
diff --git a/crates/typst/src/eval/duration.rs b/crates/typst/src/eval/duration.rs
index 500ce209..1bbf8492 100644
--- a/crates/typst/src/eval/duration.rs
+++ b/crates/typst/src/eval/duration.rs
@@ -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
diff --git a/crates/typst/src/eval/fields.rs b/crates/typst/src/eval/fields.rs
index 8c00873b..094cfa38 100644
--- a/crates/typst/src/eval/fields.rs
+++ b/crates/typst/src/eval/fields.rs
@@ -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 {
+ &[]
}
}
diff --git a/crates/typst/src/eval/float.rs b/crates/typst/src/eval/float.rs
new file mode 100644
index 00000000..094ff1db
--- /dev/null
+++ b/crates/typst/src/eval/float.rs
@@ -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),
+}
diff --git a/crates/typst/src/eval/func.rs b/crates/typst/src/eval/func.rs
index effafa7b..45a1efbd 100644
--- a/crates/typst/src/eval/func.rs
+++ b/crates/typst/src/eval/func.rs
@@ -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 {
- let span = self.span;
- Self { repr: Repr::With(Arc::new((self, args))), span }
+ /// The function's span.
+ pub fn span(&self) -> Span {
+ self.span
}
- /// Extract the element function, if it is one.
- pub fn element(&self) -> Option<ElemFunc> {
- match self.repr {
- Repr::Elem(func) => Some(func),
- _ => None,
+ /// 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
}
+}
- /// 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),
- }
+#[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 }
+ }
+
+ /// 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())
}
-}
-
-impl Eq for NativeFunc {}
-impl Hash for NativeFunc {
- fn hash<H: Hasher>(&self, state: &mut H) {
- (self.func as usize).hash(state);
- }
+ /// Get the function data for the native Rust type.
+ fn data() -> &'static NativeFuncData;
}
-impl From<&'static NativeFunc> for Func {
- fn from(native: &'static NativeFunc) -> Self {
- Repr::Native(native).into()
- }
-}
-
-cast! {
- &'static NativeFunc,
- self => Value::Func(self.into()),
-}
-
-/// Details about a function.
-#[derive(Debug, Clone)]
-pub struct FuncInfo {
- /// The function's name.
+/// Defines a native function.
+pub struct NativeFuncData {
+ pub function: fn(&mut Vm, &mut Args) -> SourceResult<Value>,
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 title: &'static str,
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,
+ pub keywords: &'static [&'static str],
+ pub scope: Lazy<Scope>,
+ pub params: Lazy<Vec<ParamInfo>>,
+ pub returns: Lazy<CastInfo>,
}
-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)
+impl From<&'static NativeFuncData> for Func {
+ fn from(data: &'static NativeFuncData) -> Self {
+ Repr::Native(Static(data)).into()
}
}
-/// Describes a named parameter.
+cast! {
+ &'static NativeFuncData,
+ self => Func::from(self).into_value(),
+}
+
+/// 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?
diff --git a/crates/typst/src/eval/int.rs b/crates/typst/src/eval/int.rs
index 4e081617..55c709c0 100644
--- a/crates/typst/src/eval/int.rs
+++ b/crates/typst/src/eval/int.rs
@@ -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"
- })?,
- })*
- }
-}
-
-macro_rules! unsigned_nonzero {
- ($($ty:ty)*) => {
- $(cast! {
- $ty,
- self => Value::Int(self.get() as i64),
- v: i64 => v
- .try_into()
- .ok()
- .and_then($ty::new)
- .ok_or_else(|| if v <= 0 {
- "number must be positive"
- } else {
- "number too large"
- })?,
- })*
- }
-}
+signed_int! { i8 i16 i32 isize }
+unsigned_int! { u8 u16 u32 u64 usize }
-signed_int! {
- i8 i16 i32 isize
+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"
+ })?,
}
-unsigned_int! {
- u8 u16 u32 u64 usize
+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_nonzero! {
- NonZeroI64 NonZeroIsize
+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_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"
+ })?,
}
diff --git a/crates/typst/src/eval/library.rs b/crates/typst/src/eval/library.rs
index 78ae7a59..5d65e193 100644
--- a/crates/typst/src/eval/library.rs
+++ b/crates/typst/src/eval/library.rs
@@ -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);
}
}
diff --git a/crates/typst/src/eval/methods.rs b/crates/typst/src/eval/methods.rs
index 85f87cc7..b8d71c76 100644
--- a/crates/typst/src/eval/methods.rs
+++ b/crates/typst/src/eval/methods.rs
@@ -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);
-
- 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(),
- },
-
- 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);
- }
- }
+/// Whether a specific method is mutating.
+pub fn is_mutating(method: &str) -> bool {
+ matches!(method, "push" | "pop" | "insert" | "remove")
+}
- _ => return missing(),
- };
+/// Whether a specific method is an accessor.
+pub fn is_accessor(method: &str) -> bool {
+ matches!(method, "first" | "last" | "at")
+}
- 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}`")
}
diff --git a/crates/typst/src/eval/mod.rs b/crates/typst/src/eval/mod.rs
index a6fd5e17..cbd00bc1 100644
--- a/crates/typst/src/eval/mod.rs
+++ b/crates/typst/src/eval/mod.rs
@@ -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)
+
+ target.clone()
} 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,
- );
+ 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)
}
}
diff --git a/crates/typst/src/eval/module.rs b/crates/typst/src/eval/module.rs
index 0bc6bf38..9f02948c 100644
--- a/crates/typst/src/eval/module.rs
+++ b/crates/typst/src/eval/module.rs
@@ -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())
})
diff --git a/crates/typst/src/eval/none.rs b/crates/typst/src/eval/none.rs
index ab7644a7..5262301f 100644
--- a/crates/typst/src/eval/none.rs
+++ b/crates/typst/src/eval/none.rs
@@ -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 {
diff --git a/crates/typst/src/eval/ops.rs b/crates/typst/src/eval/ops.rs
index 2dc3c8f9..e44c3a41 100644
--- a/crates/typst/src/eval/ops.rs
+++ b/crates/typst/src/eval/ops.rs
@@ -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"
+}
diff --git a/crates/typst/src/eval/plugin.rs b/crates/typst/src/eval/plugin.rs
index 7dbc8b3e..29ca79c0 100644
--- a/crates/typst/src/eval/plugin.rs
+++ b/crates/typst/src/eval/plugin.rs
@@ -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();
diff --git a/crates/typst/src/eval/scope.rs b/crates/typst/src/eval/scope.rs
index b8d5b75a..35ff202a 100644
--- a/crates/typst/src/eval/scope.rs
+++ b/crates/typst/src/eval/scope.rs
@@ -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;
+}
diff --git a/crates/typst/src/eval/str.rs b/crates/typst/src/eval/str.rs
index 636b9640..6684389b 100644
--- a/crates/typst/src/eval/str.rs
+++ b/crates/typst/src/eval/str.rs
@@ -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);
- }
- }
-
- 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))?;
- }
+ /// 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()
}
- 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())
+ /// Reverse the string.
+ #[func(title = "Reverse")]
+ pub fn rev(&self) -> Str {
+ self.as_str().graphemes(true).rev().collect::<String>().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))?;
+/// 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),
+}
- Ok(Self(self.0.repeat(n)))
- }
+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),
+}
- /// Reverse the string.
- pub fn rev(&self) -> Self {
- self.as_str().graphemes(true).rev().collect::<String>().into()
+/// 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 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()))
+ // 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}");
}
- /// 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) };
+ // 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;
- let resolved = wrapped
- .and_then(|v| usize::try_from(v).ok())
- .filter(|&v| v <= self.0.len());
+ // 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;
+ }
- if resolved.map_or(false, |i| !self.0.is_char_boundary(i)) {
- return Err(not_a_char_boundary(index));
- }
+ 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;
+ }
- Ok(resolved)
+ 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(&regex.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");
+ }
}
diff --git a/crates/typst/src/eval/symbol.rs b/crates/typst/src/eval/symbol.rs
index 58cfd534..ec5da51c 100644
--- a/crates/typst/src/eval/symbol.rs
+++ b/crates/typst/src/eval/symbol.rs
@@ -6,9 +6,43 @@ use std::sync::Arc;
use ecow::EcoString;
use serde::{Serialize, Serializer};
-use crate::diag::{bail, StrResult};
+use super::{cast, func, scope, ty, Array};
+use crate::diag::{bail, SourceResult, StrResult};
+use crate::syntax::{Span, Spanned};
-/// A symbol, possibly with variants.
+#[doc(inline)]
+pub use typst_macros::symbols;
+
+/// A Unicode symbol.
+///
+/// Typst defines common symbols so that they can easily be written with
+/// standard keyboards. The symbols are defined in modules, from which they can
+/// be accessed using [field access notation]($scripting/#fields):
+///
+/// - General symbols are defined in the [`sym` module]($category/symbols/sym)
+/// - Emoji are defined in the [`emoji` module]($category/symbols/emoji)
+///
+/// Moreover, you can define custom symbols with this type's constructor
+/// function.
+///
+/// ```example
+/// #sym.arrow.r \
+/// #sym.gt.eq.not \
+/// $gt.eq.not$ \
+/// #emoji.face.halo
+/// ```
+///
+/// Many symbols have different variants, which can be selected by appending the
+/// modifiers with dot notation. The order of the modifiers is not relevant.
+/// Visit the documentation pages of the symbol modules and click on a symbol to
+/// see its available variants.
+///
+/// ```example
+/// $arrow.l$ \
+/// $arrow.r$ \
+/// $arrow.t.quad$
+/// ```
+#[ty(scope)]
#[derive(Clone, Eq, PartialEq, Hash)]
pub struct Symbol(Repr);
@@ -29,7 +63,7 @@ enum List {
impl Symbol {
/// Create a new symbol from a single character.
- pub const fn new(c: char) -> Self {
+ pub const fn single(c: char) -> Self {
Self(Repr::Single(c))
}
@@ -124,6 +158,53 @@ impl Symbol {
}
}
+#[scope]
+impl Symbol {
+ /// Create a custom symbol with modifiers.
+ ///
+ /// ```example
+ /// #let envelope = symbol(
+ /// "🖂",
+ /// ("stamped", "🖃"),
+ /// ("stamped.pen", "🖆"),
+ /// ("lightning", "🖄"),
+ /// ("fly", "🖅"),
+ /// )
+ ///
+ /// #envelope
+ /// #envelope.stamped
+ /// #envelope.stamped.pen
+ /// #envelope.lightning
+ /// #envelope.fly
+ /// ```
+ #[func(constructor)]
+ pub fn construct(
+ /// The callsite span.
+ span: Span,
+ /// The variants of the symbol.
+ ///
+ /// Can be a just a string consisting of a single character for the
+ /// modifierless variant or an array with two strings specifying the modifiers
+ /// and the symbol. Individual modifiers should be separated by dots. When
+ /// displaying a symbol, Typst selects the first from the variants that have
+ /// all attached modifiers and the minimum number of other modifiers.
+ #[variadic]
+ variants: Vec<Spanned<Variant>>,
+ ) -> SourceResult<Symbol> {
+ let mut list = Vec::new();
+ if variants.is_empty() {
+ bail!(span, "expected at least one variant");
+ }
+ for Spanned { v, span } in variants {
+ if list.iter().any(|(prev, _)| &v.0 == prev) {
+ bail!(span, "duplicate variant");
+ }
+ list.push((v.0, v.1));
+ }
+ Ok(Symbol::runtime(list.into_boxed_slice()))
+ }
+}
+
impl Debug for Symbol {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.write_char(self.get())
@@ -155,6 +236,21 @@ impl List {
}
}
+/// A value that can be cast to a symbol.
+pub struct Variant(EcoString, char);
+
+cast! {
+ Variant,
+ c: char => Self(EcoString::new(), c),
+ array: Array => {
+ let mut iter = array.into_iter();
+ match (iter.next(), iter.next(), iter.next()) {
+ (Some(a), Some(b), None) => Self(a.cast()?, b.cast()?),
+ _ => Err("point array must contain exactly two entries")?,
+ }
+ },
+}
+
/// Iterator over variants.
enum Variants<'a> {
Single(std::option::IntoIter<char>),
diff --git a/crates/typst/src/eval/ty.rs b/crates/typst/src/eval/ty.rs
new file mode 100644
index 00000000..f7832672
--- /dev/null
+++ b/crates/typst/src/eval/ty.rs
@@ -0,0 +1,165 @@
+use std::cmp::Ordering;
+use std::fmt::{self, Debug, Display, Formatter};
+
+use ecow::eco_format;
+use once_cell::sync::Lazy;
+
+use super::{cast, func, Func, NativeFuncData, Scope, Value};
+use crate::diag::StrResult;
+use crate::util::Static;
+
+#[doc(inline)]
+pub use typst_macros::{scope, ty};
+
+/// Describes a kind of value.
+///
+/// To style your document, you need to work with values of different kinds:
+/// Lengths specifying the size of your elements, colors for your text and
+/// shapes, and more. Typst categorizes these into clearly defined _types_ and
+/// tells you where it expects which type of value.
+///
+/// Apart from very basic types for numeric values and [typical]($int)
+/// [types]($float) [known]($str) [from]($array) [programming]($dictionary)
+/// languages, Typst provides a special type for [_content._]($content) A value
+/// of this type can hold anything that you can enter into your document: Text,
+/// elements like headings and shapes, and style information.
+#[ty(scope)]
+#[derive(Copy, Clone, Eq, PartialEq, Hash)]
+pub struct Type(Static<NativeTypeData>);
+
+impl Type {
+ /// Get the type for `T`.
+ pub fn of<T: NativeType>() -> Self {
+ T::ty()
+ }
+
+ /// The type's short name, how it is used in code (e.g. `str`).
+ pub fn short_name(&self) -> &'static str {
+ self.0.name
+ }
+
+ /// The type's long name, for use in diagnostics (e.g. `string`).
+ pub fn long_name(&self) -> &'static str {
+ self.0.long_name
+ }
+
+ /// The type's title case name, for use in documentation (e.g. `String`).
+ pub fn title(&self) -> &'static str {
+ self.0.title
+ }
+
+ /// Documentation for the type (as Markdown).
+ pub fn docs(&self) -> &'static str {
+ self.0.docs
+ }
+
+ /// Search keywords for the type.
+ pub fn keywords(&self) -> &'static [&'static str] {
+ self.0.keywords
+ }
+
+ /// This type's constructor function.
+ pub fn constructor(&self) -> StrResult<Func> {
+ self.0
+ .constructor
+ .as_ref()
+ .map(|lazy| Func::from(*lazy))
+ .ok_or_else(|| eco_format!("type {self} does not have a constructor"))
+ }
+
+ /// The type's associated scope of sub-definition.
+ pub fn scope(&self) -> &'static Scope {
+ &(self.0).0.scope
+ }
+
+ /// Get a field from this type's scope, if possible.
+ pub fn field(&self, field: &str) -> StrResult<&'static Value> {
+ self.scope()
+ .get(field)
+ .ok_or_else(|| eco_format!("type {self} does not contain field `{}`", field))
+ }
+}
+
+#[scope]
+impl Type {
+ /// Determines a value's type.
+ ///
+ /// ```example
+ /// #type(12) \
+ /// #type(14.7) \
+ /// #type("hello") \
+ /// #type(<glacier>) \
+ /// #type([Hi]) \
+ /// #type(x => x + 1) \
+ /// #type(type)
+ /// ```
+ #[func(constructor)]
+ pub fn construct(
+ /// The value whose type's to determine.
+ value: Value,
+ ) -> Type {
+ value.ty()
+ }
+}
+
+impl Debug for Type {
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ f.pad(self.long_name())
+ }
+}
+
+impl Display for Type {
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ f.pad(self.long_name())
+ }
+}
+
+impl Ord for Type {
+ fn cmp(&self, other: &Self) -> Ordering {
+ self.long_name().cmp(other.long_name())
+ }
+}
+
+impl PartialOrd for Type {
+ fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
+ Some(self.cmp(other))
+ }
+}
+
+/// A Typst type that is defined by a native Rust type.
+pub trait NativeType {
+ /// The type's name.
+ ///
+ /// In contrast to `data()`, this is usable in const contexts.
+ const NAME: &'static str;
+
+ /// Get the type for the native Rust type.
+ fn ty() -> Type {
+ Type::from(Self::data())
+ }
+
+ // Get the type data for the native Rust type.
+ fn data() -> &'static NativeTypeData;
+}
+
+/// Defines a native type.
+pub struct NativeTypeData {
+ pub name: &'static str,
+ pub long_name: &'static str,
+ pub title: &'static str,
+ pub docs: &'static str,
+ pub keywords: &'static [&'static str],
+ pub constructor: Lazy<Option<&'static NativeFuncData>>,
+ pub scope: Lazy<Scope>,
+}
+
+impl From<&'static NativeTypeData> for Type {
+ fn from(data: &'static NativeTypeData) -> Self {
+ Self(Static(data))
+ }
+}
+
+cast! {
+ &'static NativeTypeData,
+ self => Type::from(self).into_value(),
+}
diff --git a/crates/typst/src/eval/value.rs b/crates/typst/src/eval/value.rs
index 4e870ab4..539cb516 100644
--- a/crates/typst/src/eval/value.rs
+++ b/crates/typst/src/eval/value.rs
@@ -9,12 +9,12 @@ use serde::de::value::{MapAccessDeserializer, SeqAccessDeserializer};
use serde::de::{Error, MapAccess, SeqAccess, Visitor};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use siphasher::sip128::{Hasher128, SipHasher13};
-use time::macros::format_description;
use typst::eval::Duration;
use super::{
- cast, fields, format_str, ops, Args, Array, Bytes, CastInfo, Content, Dict,
- FromValue, Func, IntoValue, Module, Reflect, Str, Symbol,
+ fields, format_str, ops, Args, Array, AutoValue, Bytes, CastInfo, Content, Dict,
+ FromValue, Func, IntoValue, Module, NativeType, NoneValue, Plugin, Reflect, Scope,
+ Str, Symbol, Type,
};
use crate::diag::StrResult;
use crate::eval::Datetime;
@@ -72,8 +72,12 @@ pub enum Value {
Func(Func),
/// Captured arguments to a function.
Args(Args),
+ /// A type.
+ Type(Type),
/// A module.
Module(Module),
+ /// A WebAssembly plugin.
+ Plugin(Plugin),
/// A dynamic value.
Dyn(Dynamic),
}
@@ -82,7 +86,7 @@ impl Value {
/// Create a new dynamic value.
pub fn dynamic<T>(any: T) -> Self
where
- T: Type + Debug + PartialEq + Hash + Sync + Send + 'static,
+ T: Debug + NativeType + PartialEq + Hash + Sync + Send + 'static,
{
Self::Dyn(Dynamic::new(any))
}
@@ -103,34 +107,36 @@ impl Value {
}
}
- /// The name of the stored value's type.
- pub fn type_name(&self) -> &'static str {
+ /// The type of this value.
+ pub fn ty(&self) -> Type {
match self {
- Self::None => "none",
- Self::Auto => "auto",
- Self::Bool(_) => bool::TYPE_NAME,
- Self::Int(_) => i64::TYPE_NAME,
- Self::Float(_) => f64::TYPE_NAME,
- Self::Length(_) => Length::TYPE_NAME,
- Self::Angle(_) => Angle::TYPE_NAME,
- Self::Ratio(_) => Ratio::TYPE_NAME,
- Self::Relative(_) => Rel::<Length>::TYPE_NAME,
- Self::Fraction(_) => Fr::TYPE_NAME,
- Self::Color(_) => Color::TYPE_NAME,
- Self::Symbol(_) => Symbol::TYPE_NAME,
- Self::Str(_) => Str::TYPE_NAME,
- Self::Bytes(_) => Bytes::TYPE_NAME,
- Self::Label(_) => Label::TYPE_NAME,
- Self::Datetime(_) => Datetime::TYPE_NAME,
- Self::Duration(_) => Duration::TYPE_NAME,
- Self::Content(_) => Content::TYPE_NAME,
- Self::Styles(_) => Styles::TYPE_NAME,
- Self::Array(_) => Array::TYPE_NAME,
- Self::Dict(_) => Dict::TYPE_NAME,
- Self::Func(_) => Func::TYPE_NAME,
- Self::Args(_) => Args::TYPE_NAME,
- Self::Module(_) => Module::TYPE_NAME,
- Self::Dyn(v) => v.type_name(),
+ Self::None => Type::of::<NoneValue>(),
+ Self::Auto => Type::of::<AutoValue>(),
+ Self::Bool(_) => Type::of::<bool>(),
+ Self::Int(_) => Type::of::<i64>(),
+ Self::Float(_) => Type::of::<f64>(),
+ Self::Length(_) => Type::of::<Length>(),
+ Self::Angle(_) => Type::of::<Angle>(),
+ Self::Ratio(_) => Type::of::<Ratio>(),
+ Self::Relative(_) => Type::of::<Rel<Length>>(),
+ Self::Fraction(_) => Type::of::<Fr>(),
+ Self::Color(_) => Type::of::<Color>(),
+ Self::Symbol(_) => Type::of::<Symbol>(),
+ Self::Str(_) => Type::of::<Str>(),
+ Self::Bytes(_) => Type::of::<Bytes>(),
+ Self::Label(_) => Type::of::<Label>(),
+ Self::Datetime(_) => Type::of::<Datetime>(),
+ Self::Duration(_) => Type::of::<Duration>(),
+ Self::Content(_) => Type::of::<Content>(),
+ Self::Styles(_) => Type::of::<Styles>(),
+ Self::Array(_) => Type::of::<Array>(),
+ Self::Dict(_) => Type::of::<Dict>(),
+ Self::Func(_) => Type::of::<Func>(),
+ Self::Args(_) => Type::of::<Args>(),
+ Self::Type(_) => Type::of::<Type>(),
+ Self::Module(_) => Type::of::<Module>(),
+ Self::Plugin(_) => Type::of::<Module>(),
+ Self::Dyn(v) => v.ty(),
}
}
@@ -143,48 +149,59 @@ impl Value {
pub fn field(&self, field: &str) -> StrResult<Value> {
match self {
Self::Symbol(symbol) => symbol.clone().modified(field).map(Self::Symbol),
- Self::Dict(dict) => dict.at(field, None),
- Self::Content(content) => content.at(field, None),
- Self::Module(module) => module.get(field).cloned(),
- Self::Func(func) => func.get(field).cloned(),
+ Self::Dict(dict) => dict.get(field).cloned(),
+ Self::Content(content) => content.get(field),
+ Self::Type(ty) => ty.field(field).cloned(),
+ Self::Func(func) => func.field(field).cloned(),
+ Self::Module(module) => module.field(field).cloned(),
_ => fields::field(self, field),
}
}
- /// Return the debug representation of the value.
- pub fn repr(&self) -> Str {
- format_str!("{:?}", self)
+ /// The associated scope, if this is a function, type, or module.
+ pub fn scope(&self) -> Option<&Scope> {
+ match self {
+ Self::Func(func) => func.scope(),
+ Self::Type(ty) => Some(ty.scope()),
+ Self::Module(module) => Some(module.scope()),
+ _ => None,
+ }
}
- /// Attach a span to the value, if possible.
- pub fn spanned(self, span: Span) -> Self {
+ /// Try to extract documentation for the value.
+ pub fn docs(&self) -> Option<&'static str> {
match self {
- Value::Content(v) => Value::Content(v.spanned(span)),
- Value::Func(v) => Value::Func(v.spanned(span)),
- v => v,
+ Self::Func(func) => func.docs(),
+ Self::Type(ty) => Some(ty.docs()),
+ _ => None,
}
}
+ /// Return the debug representation of the value.
+ pub fn repr(&self) -> Str {
+ format_str!("{self:?}")
+ }
+
/// Return the display representation of the value.
pub fn display(self) -> Content {
match self {
Self::None => Content::empty(),
- Self::Int(v) => item!(text)(eco_format!("{}", v)),
- Self::Float(v) => item!(text)(eco_format!("{}", v)),
+ Self::Int(v) => item!(text)(eco_format!("{v}")),
+ Self::Float(v) => item!(text)(eco_format!("{v}")),
Self::Str(v) => item!(text)(v.into()),
Self::Symbol(v) => item!(text)(v.get().into()),
Self::Content(v) => v,
- Self::Func(_) => Content::empty(),
Self::Module(module) => module.content(),
_ => item!(raw)(self.repr().into(), Some("typc".into()), false),
}
}
- /// Try to extract documentation for the value.
- pub fn docs(&self) -> Option<&'static str> {
+ /// Attach a span to the value, if possible.
+ pub fn spanned(self, span: Span) -> Self {
match self {
- Self::Func(func) => func.info().map(|info| info.docs),
- _ => None,
+ Value::Content(v) => Value::Content(v.spanned(span)),
+ Value::Func(v) => Value::Func(v.spanned(span)),
+ v => v,
}
}
}
@@ -192,8 +209,8 @@ impl Value {
impl Debug for Value {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match self {
- Self::None => f.pad("none"),
- Self::Auto => f.pad("auto"),
+ Self::None => Debug::fmt(&NoneValue, f),
+ Self::Auto => Debug::fmt(&AutoValue, f),
Self::Bool(v) => Debug::fmt(v, f),
Self::Int(v) => Debug::fmt(v, f),
Self::Float(v) => Debug::fmt(v, f),
@@ -215,7 +232,9 @@ impl Debug for Value {
Self::Dict(v) => Debug::fmt(v, f),
Self::Func(v) => Debug::fmt(v, f),
Self::Args(v) => Debug::fmt(v, f),
+ Self::Type(v) => Debug::fmt(v, f),
Self::Module(v) => Debug::fmt(v, f),
+ Self::Plugin(v) => Debug::fmt(v, f),
Self::Dyn(v) => Debug::fmt(v, f),
}
}
@@ -260,7 +279,9 @@ impl Hash for Value {
Self::Dict(v) => v.hash(state),
Self::Func(v) => v.hash(state),
Self::Args(v) => v.hash(state),
+ Self::Type(v) => v.hash(state),
Self::Module(v) => v.hash(state),
+ Self::Plugin(v) => v.hash(state),
Self::Dyn(v) => v.hash(state),
}
}
@@ -272,10 +293,10 @@ impl Serialize for Value {
S: Serializer,
{
match self {
- Self::None => serializer.serialize_none(),
- Self::Bool(v) => serializer.serialize_bool(*v),
- Self::Int(v) => serializer.serialize_i64(*v),
- Self::Float(v) => serializer.serialize_f64(*v),
+ Self::None => NoneValue.serialize(serializer),
+ Self::Bool(v) => v.serialize(serializer),
+ Self::Int(v) => v.serialize(serializer),
+ Self::Float(v) => v.serialize(serializer),
Self::Str(v) => v.serialize(serializer),
Self::Bytes(v) => v.serialize(serializer),
Self::Symbol(v) => v.serialize(serializer),
@@ -289,59 +310,16 @@ impl Serialize for Value {
}
}
-fn parse_toml_date(dict: &Dict) -> Option<Datetime> {
- if dict.len() != 1 || !dict.contains("$__toml_private_datetime") {
- return None;
- }
-
- let Ok(s) = String::from_value(dict.at("$__toml_private_datetime", None).unwrap())
- else {
- return None;
- };
-
- if let Ok(d) = time::PrimitiveDateTime::parse(
- &s,
- &format_description!("[year]-[month]-[day]T[hour]:[minute]:[second]Z"),
- ) {
- Some(
- Datetime::from_ymd_hms(
- d.year(),
- d.month() as u8,
- d.day(),
- d.hour(),
- d.minute(),
- d.second(),
- )
- .unwrap(),
- )
- } else if let Ok(d) = time::PrimitiveDateTime::parse(
- &s,
- &format_description!("[year]-[month]-[day]T[hour]:[minute]:[second]"),
- ) {
- Some(
- Datetime::from_ymd_hms(
- d.year(),
- d.month() as u8,
- d.day(),
- d.hour(),
- d.minute(),
- d.second(),
- )
- .unwrap(),
- )
- } else if let Ok(d) =
- time::Date::parse(&s, &format_description!("[year]-[month]-[day]"))
- {
- Some(Datetime::from_ymd(d.year(), d.month() as u8, d.day()).unwrap())
- } else if let Ok(d) =
- time::Time::parse(&s, &format_description!("[hour]:[minute]:[second]"))
+impl<'de> Deserialize<'de> for Value {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
{
- Some(Datetime::from_hms(d.hour(), d.minute(), d.second()).unwrap())
- } else {
- None
+ deserializer.deserialize_any(ValueVisitor)
}
}
+/// Visitor for value deserialization.
struct ValueVisitor;
impl<'de> Visitor<'de> for ValueVisitor {
@@ -444,23 +422,14 @@ impl<'de> Visitor<'de> for ValueVisitor {
fn visit_map<A: MapAccess<'de>>(self, map: A) -> Result<Self::Value, A::Error> {
let dict = Dict::deserialize(MapAccessDeserializer::new(map))?;
- Ok(match parse_toml_date(&dict) {
+ Ok(match Datetime::from_toml_dict(&dict) {
None => dict.into_value(),
Some(datetime) => datetime.into_value(),
})
}
}
-impl<'de> Deserialize<'de> for Value {
- fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
- where
- D: Deserializer<'de>,
- {
- deserializer.deserialize_any(ValueVisitor)
- }
-}
-
-/// A dynamic value.
+/// A value that is not part of the built-in enum.
#[derive(Clone, Hash)]
#[allow(clippy::derived_hash_with_manual_eq)]
pub struct Dynamic(Arc<dyn Bounds>);
@@ -469,24 +438,24 @@ impl Dynamic {
/// Create a new instance from any value that satisfies the required bounds.
pub fn new<T>(any: T) -> Self
where
- T: Type + Debug + PartialEq + Hash + Sync + Send + 'static,
+ T: Debug + NativeType + PartialEq + Hash + Sync + Send + 'static,
{
Self(Arc::new(any))
}
/// Whether the wrapped type is `T`.
- pub fn is<T: Type + 'static>(&self) -> bool {
+ pub fn is<T: 'static>(&self) -> bool {
(*self.0).as_any().is::<T>()
}
/// Try to downcast to a reference to a specific type.
- pub fn downcast<T: Type + 'static>(&self) -> Option<&T> {
+ pub fn downcast<T: 'static>(&self) -> Option<&T> {
(*self.0).as_any().downcast_ref()
}
/// The name of the stored value's type.
- pub fn type_name(&self) -> &'static str {
- self.0.dyn_type_name()
+ pub fn ty(&self) -> Type {
+ self.0.dyn_ty()
}
}
@@ -502,21 +471,16 @@ impl PartialEq for Dynamic {
}
}
-cast! {
- Dynamic,
- self => Value::Dyn(self),
-}
-
trait Bounds: Debug + Sync + Send + 'static {
fn as_any(&self) -> &dyn Any;
fn dyn_eq(&self, other: &Dynamic) -> bool;
- fn dyn_type_name(&self) -> &'static str;
+ fn dyn_ty(&self) -> Type;
fn hash128(&self) -> u128;
}
impl<T> Bounds for T
where
- T: Type + Debug + PartialEq + Hash + Sync + Send + 'static,
+ T: Debug + NativeType + PartialEq + Hash + Sync + Send + 'static,
{
fn as_any(&self) -> &dyn Any {
self
@@ -527,8 +491,8 @@ where
self == other
}
- fn dyn_type_name(&self) -> &'static str {
- T::TYPE_NAME
+ fn dyn_ty(&self) -> Type {
+ Type::of::<T>()
}
#[tracing::instrument(skip_all)]
@@ -548,25 +512,19 @@ impl Hash for dyn Bounds {
}
}
-/// The type of a value.
-pub trait Type {
- /// The name of the type.
- const TYPE_NAME: &'static str;
-}
-
-/// Implement traits for primitives.
+/// Implements traits for primitives (Value enum variants).
macro_rules! primitive {
(
$ty:ty: $name:literal, $variant:ident
$(, $other:ident$(($binding:ident))? => $out:expr)*
) => {
- impl Type for $ty {
- const TYPE_NAME: &'static str = $name;
- }
-
impl Reflect for $ty {
- fn describe() -> CastInfo {
- CastInfo::Type(Self::TYPE_NAME)
+ fn input() -> CastInfo {
+ CastInfo::Type(Type::of::<Self>())
+ }
+
+ fn output() -> CastInfo {
+ CastInfo::Type(Type::of::<Self>())
}
fn castable(value: &Value) -> bool {
@@ -588,8 +546,8 @@ macro_rules! primitive {
$(Value::$other$(($binding))? => Ok($out),)*
v => Err(eco_format!(
"expected {}, found {}",
- Self::TYPE_NAME,
- v.type_name(),
+ Type::of::<Self>(),
+ v.ty(),
)),
}
}
@@ -632,9 +590,15 @@ primitive! { Content: "content",
primitive! { Styles: "styles", Styles }
primitive! { Array: "array", Array }
primitive! { Dict: "dictionary", Dict }
-primitive! { Func: "function", Func }
+primitive! {
+ Func: "function",
+ Func,
+ Type(ty) => ty.constructor()?.clone()
+}
primitive! { Args: "arguments", Args }
+primitive! { Type: "type", Type }
primitive! { Module: "module", Module }
+primitive! { Plugin: "plugin", Plugin }
#[cfg(test)]
mod tests {
diff --git a/crates/typst/src/export/pdf/external_graphics_state.rs b/crates/typst/src/export/pdf/extg.rs
index 164de1b6..f62bec6a 100644
--- a/crates/typst/src/export/pdf/external_graphics_state.rs
+++ b/crates/typst/src/export/pdf/extg.rs
@@ -1,6 +1,7 @@
-use crate::export::pdf::{PdfContext, RefExt};
use pdf_writer::Finish;
+use crate::export::pdf::{PdfContext, RefExt};
+
/// A PDF external graphics state.
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
pub struct ExternalGraphicsState {
diff --git a/crates/typst/src/export/pdf/mod.rs b/crates/typst/src/export/pdf/mod.rs
index b650d5b9..3e3f2650 100644
--- a/crates/typst/src/export/pdf/mod.rs
+++ b/crates/typst/src/export/pdf/mod.rs
@@ -1,6 +1,6 @@
//! Exporting into PDF documents.
-mod external_graphics_state;
+mod extg;
mod font;
mod image;
mod outline;
@@ -22,7 +22,7 @@ use crate::geom::{Abs, Dir, Em};
use crate::image::Image;
use crate::model::Introspector;
-use external_graphics_state::ExternalGraphicsState;
+use extg::ExternalGraphicsState;
/// Export a document into a PDF file.
///
@@ -33,7 +33,7 @@ pub fn pdf(document: &Document) -> Vec<u8> {
page::construct_pages(&mut ctx, &document.pages);
font::write_fonts(&mut ctx);
image::write_images(&mut ctx);
- external_graphics_state::write_external_graphics_states(&mut ctx);
+ extg::write_external_graphics_states(&mut ctx);
page::write_page_tree(&mut ctx);
write_catalog(&mut ctx);
ctx.writer.finish()
diff --git a/crates/typst/src/export/pdf/outline.rs b/crates/typst/src/export/pdf/outline.rs
index 89d01135..855d9f4f 100644
--- a/crates/typst/src/export/pdf/outline.rs
+++ b/crates/typst/src/export/pdf/outline.rs
@@ -17,7 +17,7 @@ pub fn write_outline(ctx: &mut PdfContext) -> Option<Ref> {
// Therefore, its next descendant must be added at its level, which is
// enforced in the manner shown below.
let mut last_skipped_level = None;
- for heading in ctx.introspector.query(&item!(heading_func).select()) {
+ for heading in ctx.introspector.query(&item!(heading_elem).select()) {
let leaf = HeadingNode::leaf((*heading).clone());
if leaf.bookmarked {
diff --git a/crates/typst/src/export/pdf/page.rs b/crates/typst/src/export/pdf/page.rs
index 0c0d2957..c3d389ea 100644
--- a/crates/typst/src/export/pdf/page.rs
+++ b/crates/typst/src/export/pdf/page.rs
@@ -5,13 +5,13 @@ use pdf_writer::types::{
use pdf_writer::writers::ColorSpace;
use pdf_writer::{Content, Filter, Finish, Name, Rect, Ref, Str};
-use super::external_graphics_state::ExternalGraphicsState;
+use super::extg::ExternalGraphicsState;
use super::{deflate, AbsExt, EmExt, PdfContext, RefExt, D65_GRAY, SRGB};
use crate::doc::{Destination, Frame, FrameItem, GroupItem, Meta, TextItem};
use crate::font::Font;
use crate::geom::{
- self, Abs, Color, Em, Geometry, LineCap, LineJoin, Numeric, Paint, Point, Ratio,
- Shape, Size, Stroke, Transform,
+ self, Abs, Color, Em, FixedStroke, Geometry, LineCap, LineJoin, Numeric, Paint,
+ Point, Ratio, Shape, Size, Transform,
};
use crate::image::Image;
@@ -209,7 +209,7 @@ struct State {
fill: Option<Paint>,
fill_space: Option<Name<'static>>,
external_graphics_state: Option<ExternalGraphicsState>,
- stroke: Option<Stroke>,
+ stroke: Option<FixedStroke>,
stroke_space: Option<Name<'static>>,
}
@@ -237,7 +237,7 @@ impl PageContext<'_, '_> {
}
}
- fn set_opacities(&mut self, stroke: Option<&Stroke>, fill: Option<&Paint>) {
+ fn set_opacities(&mut self, stroke: Option<&FixedStroke>, fill: Option<&Paint>) {
let stroke_opacity = stroke
.map(|stroke| {
let Paint::Solid(color) = stroke.paint;
@@ -319,9 +319,9 @@ impl PageContext<'_, '_> {
self.state.fill_space = None;
}
- fn set_stroke(&mut self, stroke: &Stroke) {
+ fn set_stroke(&mut self, stroke: &FixedStroke) {
if self.state.stroke.as_ref() != Some(stroke) {
- let Stroke {
+ let FixedStroke {
paint,
thickness,
line_cap,
diff --git a/crates/typst/src/export/render.rs b/crates/typst/src/export/render.rs
index 145e64a3..10a3e813 100644
--- a/crates/typst/src/export/render.rs
+++ b/crates/typst/src/export/render.rs
@@ -14,8 +14,8 @@ use usvg::{NodeExt, TreeParsing};
use crate::doc::{Frame, FrameItem, GroupItem, Meta, TextItem};
use crate::font::Font;
use crate::geom::{
- self, Abs, Color, Geometry, LineCap, LineJoin, Paint, PathItem, Shape, Size, Stroke,
- Transform,
+ self, Abs, Color, FixedStroke, Geometry, LineCap, LineJoin, Paint, PathItem, Shape,
+ Size, Transform,
};
use crate::image::{DecodedImage, Image};
@@ -466,7 +466,7 @@ fn render_shape(
canvas.fill_path(&path, &paint, rule, ts, mask);
}
- if let Some(Stroke {
+ if let Some(FixedStroke {
paint,
thickness,
line_cap,
diff --git a/crates/typst/src/export/svg.rs b/crates/typst/src/export/svg.rs
index 24c4b1a3..7d67274c 100644
--- a/crates/typst/src/export/svg.rs
+++ b/crates/typst/src/export/svg.rs
@@ -10,8 +10,8 @@ use xmlwriter::XmlWriter;
use crate::doc::{Frame, FrameItem, GroupItem, TextItem};
use crate::font::Font;
use crate::geom::{
- Abs, Axes, Geometry, LineCap, LineJoin, Paint, PathItem, Ratio, Shape, Size, Stroke,
- Transform,
+ Abs, Axes, FixedStroke, Geometry, LineCap, LineJoin, Paint, PathItem, Ratio, Shape,
+ Size, Transform,
};
use crate::image::{Image, ImageFormat, RasterFormat, VectorFormat};
use crate::util::hash128;
@@ -303,7 +303,7 @@ impl SVGRenderer {
}
/// Write a stroke attribute.
- fn write_stroke(&mut self, stroke: &Stroke) {
+ fn write_stroke(&mut self, stroke: &FixedStroke) {
let Paint::Solid(color) = stroke.paint;
self.xml.write_attribute("stroke", &color.to_rgba().to_hex());
self.xml.write_attribute("stroke-width", &stroke.thickness.to_pt());
diff --git a/crates/typst/src/geom/align.rs b/crates/typst/src/geom/align.rs
index 2007db96..bfe377fb 100644
--- a/crates/typst/src/geom/align.rs
+++ b/crates/typst/src/geom/align.rs
@@ -1,248 +1,379 @@
use super::*;
-/// Where to align something along an axis.
-#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
+/// Where to [align]($align) something along an axis.
+///
+/// Possible values are:
+/// - `start`: Aligns at the [start]($direction.start) of the [text
+/// direction]($text.dir).
+/// - `end`: Aligns at the [end]($direction.end) of the [text
+/// direction]($text.dir).
+/// - `left`: Align at the left.
+/// - `center`: Aligns in the middle, horizontally.
+/// - `right`: Aligns at the right.
+/// - `top`: Aligns at the top.
+/// - `horizon`: Aligns in the middle, vertically.
+/// - `bottom`: Align at the bottom.
+///
+/// These values are available globally and also in the alignment type's scope,
+/// so you can write either of the following two:
+///
+/// ```example
+/// #align(center)[Hi]
+/// #align(alignment.center)[Hi]
+/// ```
+///
+/// # 2D alignments
+/// To align along both axes at the same time, add the two alignments using the
+/// `+` operator. For example, `top + right` aligns the content to the top right
+/// corner.
+///
+/// ```example
+/// #set page(height: 3cm)
+/// #align(center + bottom)[Hi]
+/// ```
+///
+/// # Fields
+/// The `x` and `y` fields hold the alignment's horizontal and vertical
+/// components, respectively (as yet another `alignment`). They may be `{none}`.
+///
+/// ```example
+/// #(top + right).x \
+/// #left.x \
+/// #left.y (none)
+/// ```
+#[ty(scope, name = "alignment")]
+#[derive(Copy, Clone, Eq, PartialEq, Hash)]
pub enum Align {
- /// Align at the left side.
- Left,
- /// Align in the horizontal middle.
- Center,
- /// Align at the right side.
- Right,
- /// Align at the top side.
- Top,
- /// Align in the vertical middle.
- Horizon,
- /// Align at the bottom side.
- Bottom,
+ H(HAlign),
+ V(VAlign),
+ Both(HAlign, VAlign),
}
impl Align {
- /// Top-left alignment.
- pub const LEFT_TOP: Axes<Self> = Axes { x: Align::Left, y: Align::Top };
+ /// The horizontal component.
+ pub const fn x(self) -> Option<HAlign> {
+ match self {
+ Self::H(x) | Self::Both(x, _) => Some(x),
+ Self::V(_) => None,
+ }
+ }
+
+ /// The vertical component.
+ pub const fn y(self) -> Option<VAlign> {
+ match self {
+ Self::V(y) | Self::Both(_, y) => Some(y),
+ Self::H(_) => None,
+ }
+ }
- /// Center-horizon alignment.
- pub const CENTER_HORIZON: Axes<Self> = Axes { x: Align::Center, y: Align::Horizon };
+ /// Normalize the alignment to a LTR-TTB space.
+ pub fn fix(self, text_dir: Dir) -> Axes<FixedAlign> {
+ Axes::new(
+ self.x().unwrap_or_default().fix(text_dir),
+ self.y().unwrap_or_default().fix(),
+ )
+ }
+}
+
+#[scope]
+impl Align {
+ pub const START: Self = Align::H(HAlign::Start);
+ pub const LEFT: Self = Align::H(HAlign::Left);
+ pub const CENTER: Self = Align::H(HAlign::Center);
+ pub const RIGHT: Self = Align::H(HAlign::Right);
+ pub const END: Self = Align::H(HAlign::End);
+ pub const TOP: Self = Align::V(VAlign::Top);
+ pub const HORIZON: Self = Align::V(VAlign::Horizon);
+ pub const BOTTOM: Self = Align::V(VAlign::Bottom);
/// The axis this alignment belongs to.
- pub const fn axis(self) -> Axis {
+ /// - `{"horizontal"}` for `start`, `left`, `center`, `right`, and `end`
+ /// - `{"vertical"}` for `top`, `horizon`, and `bottom`
+ /// - `{none}` for 2-dimensional alignments
+ ///
+ /// ```example
+ /// #left.axis() \
+ /// #bottom.axis()
+ /// ```
+ #[func]
+ pub const fn axis(self) -> Option<Axis> {
match self {
- Self::Left | Self::Center | Self::Right => Axis::X,
- Self::Top | Self::Horizon | Self::Bottom => Axis::Y,
+ Self::H(_) => Some(Axis::X),
+ Self::V(_) => Some(Axis::Y),
+ Self::Both(..) => None,
}
}
/// The inverse alignment.
- pub const fn inv(self) -> Self {
+ ///
+ /// ```example
+ /// #top.inv() \
+ /// #left.inv() \
+ /// #center.inv() \
+ /// #(left + bottom).inv()
+ /// ```
+ #[func(title = "Inverse")]
+ pub const fn inv(self) -> Align {
match self {
- Self::Left => Self::Right,
- Self::Center => Self::Center,
- Self::Right => Self::Left,
- Self::Top => Self::Bottom,
- Self::Horizon => Self::Horizon,
- Self::Bottom => Self::Top,
+ Self::H(h) => Self::H(h.inv()),
+ Self::V(v) => Self::V(v.inv()),
+ Self::Both(h, v) => Self::Both(h.inv(), v.inv()),
}
}
+}
- /// Returns the position of this alignment in a container with the given
- /// extent.
- pub fn position(self, extent: Abs) -> Abs {
+impl Default for Align {
+ fn default() -> Self {
+ HAlign::default() + VAlign::default()
+ }
+}
+
+impl Add for Align {
+ type Output = StrResult<Self>;
+
+ fn add(self, rhs: Self) -> Self::Output {
+ match (self, rhs) {
+ (Self::H(x), Self::V(y)) | (Self::V(y), Self::H(x)) => Ok(x + y),
+ (Self::H(_), Self::H(_)) => bail!("cannot add two horizontal alignments"),
+ (Self::V(_), Self::V(_)) => bail!("cannot add two vertical alignments"),
+ (Self::H(_), Self::Both(..)) | (Self::Both(..), Self::H(_)) => {
+ bail!("cannot add a horizontal and a 2D alignment")
+ }
+ (Self::V(_), Self::Both(..)) | (Self::Both(..), Self::V(_)) => {
+ bail!("cannot add a vertical and a 2D alignment")
+ }
+ (Self::Both(..), Self::Both(..)) => {
+ bail!("cannot add two 2D alignments")
+ }
+ }
+ }
+}
+
+impl Debug for Align {
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match self {
- Self::Left | Self::Top => Abs::zero(),
- Self::Center | Self::Horizon => extent / 2.0,
- Self::Right | Self::Bottom => extent,
+ Self::H(x) => x.fmt(f),
+ Self::V(y) => y.fmt(f),
+ Self::Both(x, y) => write!(f, "{x:?} + {y:?}"),
+ }
+ }
+}
+
+impl Fold for Align {
+ type Output = Self;
+
+ fn fold(self, outer: Self::Output) -> Self::Output {
+ match (self, outer) {
+ (Self::H(x), Self::V(y) | Self::Both(_, y)) => Self::Both(x, y),
+ (Self::V(y), Self::H(x) | Self::Both(x, _)) => Self::Both(x, y),
+ _ => self,
}
}
}
+impl Resolve for Align {
+ type Output = Axes<FixedAlign>;
+
+ fn resolve(self, styles: StyleChain) -> Self::Output {
+ self.fix(item!(dir)(styles))
+ }
+}
+
impl From<Side> for Align {
fn from(side: Side) -> Self {
match side {
- Side::Left => Self::Left,
- Side::Top => Self::Top,
- Side::Right => Self::Right,
- Side::Bottom => Self::Bottom,
+ Side::Left => Self::LEFT,
+ Side::Top => Self::TOP,
+ Side::Right => Self::RIGHT,
+ Side::Bottom => Self::BOTTOM,
}
}
}
-impl Debug for Align {
- fn fmt(&self, f: &mut Formatter) -> fmt::Result {
- f.pad(match self {
- Self::Left => "left",
- Self::Center => "center",
- Self::Right => "right",
- Self::Top => "top",
- Self::Horizon => "horizon",
- Self::Bottom => "bottom",
- })
- }
+cast! {
+ type Align,
}
-/// The generic alignment representation.
-#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
-pub enum GenAlign {
- /// Align at the start side of the text direction.
+/// Where to align something horizontally.
+#[derive(Default, Copy, Clone, Eq, PartialEq, Hash)]
+pub enum HAlign {
+ #[default]
Start,
- /// Align at the end side of the text direction.
+ Left,
+ Center,
+ Right,
End,
- /// Align at a specific alignment.
- Specific(Align),
}
-impl GenAlign {
- /// The axis this alignment belongs to.
- pub const fn axis(self) -> Axis {
- match self {
- Self::Start | Self::End => Axis::X,
- Self::Specific(align) => align.axis(),
- }
- }
-
- /// The inverse alignment.
+impl HAlign {
+ /// The inverse horizontal alignment.
pub const fn inv(self) -> Self {
match self {
Self::Start => Self::End,
+ Self::Left => Self::Right,
+ Self::Center => Self::Center,
+ Self::Right => Self::Left,
Self::End => Self::Start,
- Self::Specific(align) => Self::Specific(align.inv()),
}
}
-}
-impl From<Align> for GenAlign {
- fn from(align: Align) -> Self {
- Self::Specific(align)
+ /// Resolve the axis alignment based on the horizontal direction.
+ pub const fn fix(self, dir: Dir) -> FixedAlign {
+ match (self, dir.is_positive()) {
+ (Self::Start, true) | (Self::End, false) => FixedAlign::Start,
+ (Self::Left, _) => FixedAlign::Start,
+ (Self::Center, _) => FixedAlign::Center,
+ (Self::Right, _) => FixedAlign::End,
+ (Self::End, true) | (Self::Start, false) => FixedAlign::End,
+ }
}
}
-impl From<HorizontalAlign> for GenAlign {
- fn from(align: HorizontalAlign) -> Self {
- align.0
+impl Debug for HAlign {
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ f.pad(match self {
+ Self::Start => "start",
+ Self::Left => "left",
+ Self::Center => "center",
+ Self::Right => "right",
+ Self::End => "end",
+ })
}
}
-impl From<VerticalAlign> for GenAlign {
- fn from(align: VerticalAlign) -> Self {
- align.0
+impl Add<VAlign> for HAlign {
+ type Output = Align;
+
+ fn add(self, rhs: VAlign) -> Self::Output {
+ Align::Both(self, rhs)
}
}
-impl Debug for GenAlign {
- fn fmt(&self, f: &mut Formatter) -> fmt::Result {
- match self {
- Self::Start => f.pad("start"),
- Self::End => f.pad("end"),
- Self::Specific(align) => align.fmt(f),
- }
+impl From<HAlign> for Align {
+ fn from(align: HAlign) -> Self {
+ Self::H(align)
}
}
-cast! {
- type GenAlign: "alignment",
-}
+impl Resolve for HAlign {
+ type Output = FixedAlign;
-cast! {
- type Axes<GenAlign>: "2d alignment",
+ fn resolve(self, styles: StyleChain) -> Self::Output {
+ self.fix(item!(dir)(styles))
+ }
}
cast! {
- Axes<Align>,
- self => self.map(GenAlign::from).into_value(),
+ HAlign,
+ self => Align::H(self).into_value(),
+ align: Align => match align {
+ Align::H(v) => v,
+ v => bail!("expected `start`, `left`, `center`, `right`, or `end`, found {v:?}"),
+ }
}
-cast! {
- Axes<Option<GenAlign>>,
- self => match (self.x, self.y) {
- (Some(x), Some(y)) => Axes::new(x, y).into_value(),
- (Some(x), None) => x.into_value(),
- (None, Some(y)) => y.into_value(),
- (None, None) => Value::None,
- },
- align: GenAlign => {
- let mut aligns = Axes::default();
- aligns.set(align.axis(), Some(align));
- aligns
- },
- aligns: Axes<GenAlign> => aligns.map(Some),
+/// Where to align something vertically.
+#[derive(Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
+pub enum VAlign {
+ #[default]
+ Top,
+ Horizon,
+ Bottom,
}
-impl From<Axes<GenAlign>> for Axes<Option<GenAlign>> {
- fn from(axes: Axes<GenAlign>) -> Self {
- axes.map(Some)
+impl VAlign {
+ /// The inverse vertical alignment.
+ pub const fn inv(self) -> Self {
+ match self {
+ Self::Top => Self::Bottom,
+ Self::Horizon => Self::Horizon,
+ Self::Bottom => Self::Top,
+ }
}
-}
-impl From<Axes<Align>> for Axes<Option<GenAlign>> {
- fn from(axes: Axes<Align>) -> Self {
- axes.map(GenAlign::Specific).into()
+ /// Turns into a fixed alignment.
+ pub const fn fix(self) -> FixedAlign {
+ match self {
+ Self::Top => FixedAlign::Start,
+ Self::Horizon => FixedAlign::Center,
+ Self::Bottom => FixedAlign::End,
+ }
}
}
-impl From<Align> for Axes<Option<GenAlign>> {
- fn from(align: Align) -> Self {
- let mut axes = Axes::splat(None);
- axes.set(align.axis(), Some(align.into()));
- axes
+impl Debug for VAlign {
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ f.pad(match self {
+ Self::Top => "top",
+ Self::Horizon => "horizon",
+ Self::Bottom => "bottom",
+ })
}
}
-impl Resolve for GenAlign {
+impl Add<HAlign> for VAlign {
type Output = Align;
- fn resolve(self, styles: StyleChain) -> Self::Output {
- let dir = item!(dir)(styles);
- match self {
- Self::Start => dir.start().into(),
- Self::End => dir.end().into(),
- Self::Specific(align) => align,
- }
+ fn add(self, rhs: HAlign) -> Self::Output {
+ Align::Both(rhs, self)
}
}
-impl Fold for GenAlign {
- type Output = Self;
-
- fn fold(self, _: Self::Output) -> Self::Output {
- self
+impl From<VAlign> for Align {
+ fn from(align: VAlign) -> Self {
+ Self::V(align)
}
}
-impl Fold for Align {
- type Output = Self;
-
- fn fold(self, _: Self::Output) -> Self::Output {
- self
+cast! {
+ VAlign,
+ self => Align::V(self).into_value(),
+ align: Align => match align {
+ Align::V(v) => v,
+ v => bail!("expected `top`, `horizon`, or `bottom`, found {v:?}"),
}
}
-/// Utility struct to restrict a passed alignment value to the horizontal axis
-/// on cast.
-#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
-pub struct HorizontalAlign(pub GenAlign);
+/// A fixed alignment in the global coordinate space.
+///
+/// For horizontal alignment, start is globally left and for vertical alignment
+/// it is globally top.
+#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
+pub enum FixedAlign {
+ Start,
+ Center,
+ End,
+}
-cast! {
- HorizontalAlign,
- self => self.0.into_value(),
- align: GenAlign => {
- if align.axis() != Axis::X {
- bail!("alignment must be horizontal");
+impl FixedAlign {
+ /// Returns the position of this alignment in a container with the given
+ /// extent.
+ pub fn position(self, extent: Abs) -> Abs {
+ match self {
+ Self::Start => Abs::zero(),
+ Self::Center => extent / 2.0,
+ Self::End => extent,
}
- Self(align)
- },
+ }
}
-/// Utility struct to restrict a passed alignment value to the vertical axis on
-/// cast.
-#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
-pub struct VerticalAlign(pub GenAlign);
+impl Debug for FixedAlign {
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ f.pad(match self {
+ Self::Start => "start",
+ Self::Center => "center",
+ Self::End => "end",
+ })
+ }
+}
-cast! {
- VerticalAlign,
- self => self.0.into_value(),
- align: GenAlign => {
- if align.axis() != Axis::Y {
- bail!("alignment must be vertical");
+impl From<Side> for FixedAlign {
+ fn from(side: Side) -> Self {
+ match side {
+ Side::Left => Self::Start,
+ Side::Top => Self::Start,
+ Side::Right => Self::End,
+ Side::Bottom => Self::End,
}
- Self(align)
- },
+ }
}
diff --git a/crates/typst/src/geom/angle.rs b/crates/typst/src/geom/angle.rs
index c03810d9..242c80c9 100644
--- a/crates/typst/src/geom/angle.rs
+++ b/crates/typst/src/geom/angle.rs
@@ -1,6 +1,17 @@
use super::*;
-/// An angle.
+/// An angle describing a rotation.
+///
+/// Typst supports the following angular units:
+///
+/// - Degrees: `{180deg}`
+/// - Radians: `{3.14rad}`
+///
+/// # Example
+/// ```example
+/// #rotate(10deg)[Hello there!]
+/// ```
+#[ty(scope)]
#[derive(Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct Angle(Scalar);
@@ -40,16 +51,6 @@ impl Angle {
self.to_raw() / unit.raw_scale()
}
- /// Convert this to a number of radians.
- pub fn to_rad(self) -> f64 {
- self.to_unit(AngleUnit::Rad)
- }
-
- /// Convert this to a number of degrees.
- pub fn to_deg(self) -> f64 {
- self.to_unit(AngleUnit::Deg)
- }
-
/// The absolute value of the this angle.
pub fn abs(self) -> Self {
Self::raw(self.to_raw().abs())
@@ -71,6 +72,21 @@ impl Angle {
}
}
+#[scope]
+impl Angle {
+ /// Converts this angle to radians.
+ #[func(name = "rad", title = "Radians")]
+ pub fn to_rad(self) -> f64 {
+ self.to_unit(AngleUnit::Rad)
+ }
+
+ /// Converts this angle to degrees.
+ #[func(name = "deg", title = "Degrees")]
+ pub fn to_deg(self) -> f64 {
+ self.to_unit(AngleUnit::Deg)
+ }
+}
+
impl Numeric for Angle {
fn zero() -> Self {
Self::zero()
@@ -121,14 +137,6 @@ impl Mul<Angle> for f64 {
}
}
-impl Div<f64> for Angle {
- type Output = Self;
-
- fn div(self, other: f64) -> Self {
- Self(self.0 / other)
- }
-}
-
impl Div for Angle {
type Output = f64;
@@ -137,6 +145,14 @@ impl Div for Angle {
}
}
+impl Div<f64> for Angle {
+ type Output = Self;
+
+ fn div(self, other: f64) -> Self {
+ Self(self.0 / other)
+ }
+}
+
assign_impl!(Angle += Angle);
assign_impl!(Angle -= Angle);
assign_impl!(Angle *= f64);
diff --git a/crates/typst/src/geom/axes.rs b/crates/typst/src/geom/axes.rs
index 1c084888..9384b799 100644
--- a/crates/typst/src/geom/axes.rs
+++ b/crates/typst/src/geom/axes.rs
@@ -57,6 +57,14 @@ impl<T> Axes<T> {
Axes { x: (self.x, other.x), y: (self.y, other.y) }
}
+ /// Apply a function to this and another-instance componentwise.
+ pub fn zip_map<F, V, U>(self, other: Axes<V>, mut f: F) -> Axes<U>
+ where
+ F: FnMut(T, V) -> U,
+ {
+ Axes { x: f(self.x, other.x), y: f(self.y, other.y) }
+ }
+
/// Whether a condition is true for at least one of fields.
pub fn any<F>(self, mut f: F) -> bool
where
@@ -139,11 +147,7 @@ where
T: Debug + 'static,
{
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
- if let Axes { x: Some(x), y: Some(y) } =
- self.as_ref().map(|v| (v as &dyn Any).downcast_ref::<GenAlign>())
- {
- write!(f, "{:?} + {:?}", x, y)
- } else if (&self.x as &dyn Any).is::<Abs>() {
+ if (&self.x as &dyn Any).is::<Abs>() {
write!(f, "Size({:?}, {:?})", self.x, self.y)
} else {
write!(f, "Axes({:?}, {:?})", self.x, self.y)
@@ -194,6 +198,13 @@ impl Debug for Axis {
}
}
+cast! {
+ Axis,
+ self => self.description().into_value(),
+ "horizontal" => Self::X,
+ "vertical" => Self::X,
+}
+
impl<T> Axes<Option<T>> {
/// Unwrap the individual fields.
pub fn unwrap_or(self, other: Axes<T>) -> Axes<T> {
@@ -302,7 +313,7 @@ impl<T: Fold> Fold for Axes<Option<T>> {
type Output = Axes<T::Output>;
fn fold(self, outer: Self::Output) -> Self::Output {
- self.zip(outer).map(|(inner, outer)| match inner {
+ self.zip_map(outer, |inner, outer| match inner {
Some(value) => value.fold(outer),
None => outer,
})
diff --git a/crates/typst/src/geom/color.rs b/crates/typst/src/geom/color.rs
index ab5aa39e..05565fef 100644
--- a/crates/typst/src/geom/color.rs
+++ b/crates/typst/src/geom/color.rs
@@ -2,10 +2,32 @@ use ecow::{eco_format, EcoString};
use std::str::FromStr;
use super::*;
-use crate::diag::bail;
-use crate::eval::{cast, Array, Cast};
-
-/// A color in a dynamic format.
+use crate::diag::{bail, At, SourceResult};
+use crate::eval::{cast, Args, Array, Cast, Func, Str};
+use crate::syntax::Spanned;
+
+/// A color in a specific color space.
+///
+/// Typst supports:
+/// - sRGB through the [`rgb` function]($rgb)
+/// - Device CMYK through [`cmyk` function]($cmyk)
+/// - D65 Gray through the [`luma` function]($luma)
+///
+/// Typst provides the following built-in colors:
+///
+/// `black`, `gray`, `silver`, `white`, `navy`, `blue`, `aqua`, `teal`,
+/// `eastern`, `purple`, `fuchsia`, `maroon`, `red`, `orange`, `yellow`,
+/// `olive`, `green`, and `lime`.
+///
+/// # Example
+/// The predefined colors and the color constructors are available globally and
+/// also in the color type's scope, so you can write either of the following
+/// two:
+/// ```example
+/// #rect(fill: aqua)
+/// #rect(fill: color.aqua)
+/// ```
+#[ty(scope)]
#[derive(Copy, Clone, Eq, PartialEq, Hash)]
pub enum Color {
/// An 8-bit luma color.
@@ -17,6 +39,18 @@ pub enum Color {
}
impl Color {
+ /// Convert this color to RGBA.
+ pub fn to_rgba(self) -> RgbaColor {
+ match self {
+ Self::Luma(luma) => luma.to_rgba(),
+ Self::Rgba(rgba) => rgba,
+ Self::Cmyk(cmyk) => cmyk.to_rgba(),
+ }
+ }
+}
+
+#[scope]
+impl Color {
pub const BLACK: Self = Self::Rgba(RgbaColor::new(0x00, 0x00, 0x00, 0xFF));
pub const GRAY: Self = Self::Rgba(RgbaColor::new(0xAA, 0xAA, 0xAA, 0xFF));
pub const SILVER: Self = Self::Rgba(RgbaColor::new(0xDD, 0xDD, 0xDD, 0xFF));
@@ -36,17 +70,172 @@ impl Color {
pub const GREEN: Self = Self::Rgba(RgbaColor::new(0x2E, 0xCC, 0x40, 0xFF));
pub const LIME: Self = Self::Rgba(RgbaColor::new(0x01, 0xFF, 0x70, 0xFF));
- /// Convert this color to RGBA.
- pub fn to_rgba(self) -> RgbaColor {
+ /// Create a grayscale color.
+ ///
+ /// ```example
+ /// #for x in range(250, step: 50) {
+ /// box(square(fill: luma(x)))
+ /// }
+ /// ```
+ #[func]
+ pub fn luma(
+ /// The gray component.
+ gray: Component,
+ ) -> Color {
+ LumaColor::new(gray.0).into()
+ }
+
+ /// Create an RGB(A) color.
+ ///
+ /// The color is specified in the sRGB color space.
+ ///
+ /// _Note:_ While you can specify transparent colors and Typst's preview will
+ /// render them correctly, the PDF export does not handle them properly at the
+ /// moment. This will be fixed in the future.
+ ///
+ /// ```example
+ /// #square(fill: rgb("#b1f2eb"))
+ /// #square(fill: rgb(87, 127, 230))
+ /// #square(fill: rgb(25%, 13%, 65%))
+ /// ```
+ #[func(title = "RGB")]
+ pub fn rgb(
+ /// 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 color in hexadecimal notation.
+ ///
+ /// Accepts three, four, six or eight hexadecimal digits and optionally
+ /// a leading hashtag.
+ ///
+ /// If this string is given, the individual components should not be given.
+ ///
+ /// ```example
+ /// #text(16pt, rgb("#239dad"))[
+ /// *Typst*
+ /// ]
+ /// ```
+ #[external]
+ hex: Str,
+ /// The red component.
+ #[external]
+ red: Component,
+ /// The green component.
+ #[external]
+ green: Component,
+ /// The blue component.
+ #[external]
+ blue: Component,
+ /// The alpha component.
+ #[external]
+ alpha: Component,
+ ) -> SourceResult<Color> {
+ let mut args = args;
+ Ok(if let Some(string) = args.find::<Spanned<Str>>()? {
+ RgbaColor::from_str(&string.v).at(string.span)?.into()
+ } else {
+ let Component(r) = args.expect("red component")?;
+ let Component(g) = args.expect("green component")?;
+ let Component(b) = args.expect("blue component")?;
+ let Component(a) = args.eat()?.unwrap_or(Component(255));
+ RgbaColor::new(r, g, b, a).into()
+ })
+ }
+
+ /// Create a CMYK color.
+ ///
+ /// This is useful if you want to target a specific printer. The conversion
+ /// to RGB for display preview might differ from how your printer reproduces
+ /// the color.
+ ///
+ /// ```example
+ /// #square(
+ /// fill: cmyk(27%, 0%, 3%, 5%)
+ /// )
+ /// ```
+ #[func(title = "CMYK")]
+ pub fn cmyk(
+ /// The cyan component.
+ cyan: RatioComponent,
+ /// The magenta component.
+ magenta: RatioComponent,
+ /// The yellow component.
+ yellow: RatioComponent,
+ /// The key component.
+ key: RatioComponent,
+ ) -> Color {
+ CmykColor::new(cyan.0, magenta.0, yellow.0, key.0).into()
+ }
+
+ /// Returns the constructor function for this color's kind
+ /// ([`rgb`]($color.rgb), [`cmyk`]($color.cmyk) or [`luma`]($color.luma)).
+ ///
+ /// ```example
+ /// #let color = cmyk(1%, 2%, 3%, 4%)
+ /// #(color.kind() == cmyk)
+ /// ```
+ #[func]
+ pub fn kind(self) -> Func {
match self {
- Self::Luma(luma) => luma.to_rgba(),
- Self::Rgba(rgba) => rgba,
- Self::Cmyk(cmyk) => cmyk.to_rgba(),
+ Self::Luma(_) => Self::luma_data().into(),
+ Self::Rgba(_) => Self::rgb_data().into(),
+ Self::Cmyk(_) => Self::cmyk_data().into(),
}
}
- /// Lighten this color by the given factor.
- pub fn lighten(self, factor: Ratio) -> Self {
+ /// Returns the color's RGB(A) hex representation (such as `#ffaa32` or
+ /// `#020304fe`). The alpha component (last two digits in `#020304fe`) is
+ /// omitted if it is equal to `ff` (255 / 100%).
+ #[func]
+ pub fn to_hex(self) -> EcoString {
+ self.to_rgba().to_hex()
+ }
+
+ /// Converts this color to sRGB and returns its components (R, G, B, A) as
+ /// an array of [integers]($int).
+ #[func(name = "to-rgba")]
+ pub fn to_rgba_array(self) -> Array {
+ self.to_rgba().to_array()
+ }
+
+ /// Converts this color to Digital CMYK and returns its components
+ /// (C, M, Y, K) as an array of [ratios]($ratio). Note that this function
+ /// will throw an error when applied to an [rgb]($rgb) color, since its
+ /// conversion to CMYK is not available.
+ #[func]
+ pub fn to_cmyk(self) -> StrResult<Array> {
+ match self {
+ Self::Luma(luma) => Ok(luma.to_cmyk().to_array()),
+ Self::Rgba(_) => {
+ bail!("cannot obtain cmyk values from rgba color")
+ }
+ Self::Cmyk(cmyk) => Ok(cmyk.to_array()),
+ }
+ }
+
+ /// If this color was created with [luma]($luma), returns the
+ /// [integer]($int) value used on construction. Otherwise (for [rgb]($rgb)
+ /// and [cmyk]($cmyk) colors), throws an error.
+ #[func]
+ pub fn to_luma(self) -> StrResult<u8> {
+ match self {
+ Self::Luma(luma) => Ok(luma.0),
+ Self::Rgba(_) => {
+ bail!("cannot obtain the luma value of rgba color")
+ }
+ Self::Cmyk(_) => {
+ bail!("cannot obtain the luma value of cmyk color")
+ }
+ }
+ }
+
+ /// Lightens a color by a given factor.
+ #[func]
+ pub fn lighten(
+ self,
+ /// The factor to lighten the color by.
+ factor: Ratio,
+ ) -> Color {
match self {
Self::Luma(luma) => Self::Luma(luma.lighten(factor)),
Self::Rgba(rgba) => Self::Rgba(rgba.lighten(factor)),
@@ -54,8 +243,13 @@ impl Color {
}
}
- /// Darken this color by the given factor.
- pub fn darken(self, factor: Ratio) -> Self {
+ /// Darkens a color by a given factor.
+ #[func]
+ pub fn darken(
+ self,
+ /// The factor to darken the color by.
+ factor: Ratio,
+ ) -> Color {
match self {
Self::Luma(luma) => Self::Luma(luma.darken(factor)),
Self::Rgba(rgba) => Self::Rgba(rgba.darken(factor)),
@@ -63,8 +257,9 @@ impl Color {
}
}
- /// Negate this color.
- pub fn negate(self) -> Self {
+ /// Produces the negative of the color.
+ #[func]
+ pub fn negate(self) -> Color {
match self {
Self::Luma(luma) => Self::Luma(luma.negate()),
Self::Rgba(rgba) => Self::Rgba(rgba.negate()),
@@ -72,9 +267,28 @@ impl Color {
}
}
- /// Mixes multiple colors through weight.
+ /// Create a color by mixing two or more colors.
+ ///
+ /// ```example
+ /// #set block(height: 20pt, width: 100%)
+ /// #block(fill: red.mix(blue))
+ /// #block(fill: red.mix(blue, space: "srgb"))
+ /// #block(fill: color.mix(red, blue, white))
+ /// #block(fill: color.mix((red, 70%), (blue, 30%)))
+ /// ```
+ #[func]
pub fn mix(
- colors: impl IntoIterator<Item = WeightedColor>,
+ /// The colors, optionally with weights, specified as a pair (array of
+ /// length two) of color and weight (float or ratio).
+ ///
+ /// The weights do not need to add to `{100%}`, they are relative to the
+ /// sum of all weights.
+ #[variadic]
+ colors: Vec<WeightedColor>,
+ /// The color space to mix in. By default, this happens in a perceptual
+ /// color space (Oklab).
+ #[named]
+ #[default(ColorSpace::Oklab)]
space: ColorSpace,
) -> StrResult<Color> {
let mut total = 0.0;
@@ -465,6 +679,34 @@ cast! {
self => Value::Color(self.into()),
}
+/// An integer or ratio component.
+pub struct Component(u8);
+
+cast! {
+ Component,
+ v: i64 => match v {
+ 0 ..= 255 => Self(v as u8),
+ _ => bail!("number must be between 0 and 255"),
+ },
+ v: Ratio => if (0.0 ..= 1.0).contains(&v.get()) {
+ Self((v.get() * 255.0).round() as u8)
+ } else {
+ bail!("ratio must be between 0% and 100%");
+ },
+}
+
+/// A component that must be a ratio.
+pub struct RatioComponent(u8);
+
+cast! {
+ RatioComponent,
+ v: Ratio => if (0.0 ..= 1.0).contains(&v.get()) {
+ Self((v.get() * 255.0).round() as u8)
+ } else {
+ bail!("ratio must be between 0% and 100%");
+ },
+}
+
/// Convert to the closest u8.
fn round_u8(value: f64) -> u8 {
value.round() as u8
diff --git a/crates/typst/src/geom/corners.rs b/crates/typst/src/geom/corners.rs
index 5ee1e063..d21dc22e 100644
--- a/crates/typst/src/geom/corners.rs
+++ b/crates/typst/src/geom/corners.rs
@@ -111,8 +111,12 @@ pub enum Corner {
}
impl<T: Reflect> Reflect for Corners<Option<T>> {
- fn describe() -> CastInfo {
- T::describe() + Dict::describe()
+ fn input() -> CastInfo {
+ T::input() + Dict::input()
+ }
+
+ fn output() -> CastInfo {
+ T::output() + Dict::output()
}
fn castable(value: &Value) -> bool {
diff --git a/crates/typst/src/geom/dir.rs b/crates/typst/src/geom/dir.rs
index 48915471..897d7769 100644
--- a/crates/typst/src/geom/dir.rs
+++ b/crates/typst/src/geom/dir.rs
@@ -1,6 +1,21 @@
use super::*;
/// The four directions into which content can be laid out.
+///
+/// Possible values are:
+/// - `{ltr}`: Left to right.
+/// - `{rtl}`: Right to left.
+/// - `{ttb}`: Top to bottom.
+/// - `{btt}`: Bottom to top.
+///
+/// These values are available globally and
+/// also in the direction type's scope, so you can write either of the following
+/// two:
+/// ```example
+/// #stack(dir: rtl)[A][B][C]
+/// #stack(dir: direction.rtl)[A][B][C]
+/// ```
+#[ty(scope, name = "direction")]
#[derive(Copy, Clone, Eq, PartialEq, Hash)]
pub enum Dir {
/// Left to right.
@@ -14,7 +29,32 @@ pub enum Dir {
}
impl Dir {
- /// The specific axis this direction belongs to.
+ /// Whether this direction points into the positive coordinate direction.
+ ///
+ /// The positive directions are left-to-right and top-to-bottom.
+ pub const fn is_positive(self) -> bool {
+ match self {
+ Self::LTR | Self::TTB => true,
+ Self::RTL | Self::BTT => false,
+ }
+ }
+}
+
+#[scope]
+impl Dir {
+ pub const LTR: Self = Self::LTR;
+ pub const RTL: Self = Self::RTL;
+ pub const TTB: Self = Self::TTB;
+ pub const BTT: Self = Self::BTT;
+
+ /// The axis this direction belongs to, either `{"horizontal"}` or
+ /// `{"vertical"}`.
+ ///
+ /// ```example
+ /// #ltr.axis() \
+ /// #ttb.axis()
+ /// ```
+ #[func]
pub const fn axis(self) -> Axis {
match self {
Self::LTR | Self::RTL => Axis::X,
@@ -22,7 +62,15 @@ impl Dir {
}
}
- /// The side this direction starts at.
+ /// The start point of this direction, as an alignment.
+ ///
+ /// ```example
+ /// #ltr.start() \
+ /// #rtl.start() \
+ /// #ttb.start() \
+ /// #btt.start()
+ /// ```
+ #[func]
pub const fn start(self) -> Side {
match self {
Self::LTR => Side::Left,
@@ -32,7 +80,15 @@ impl Dir {
}
}
- /// The side this direction ends at.
+ /// The end point of this direction, as an alignment.
+ ///
+ /// ```example
+ /// #ltr.end() \
+ /// #rtl.end() \
+ /// #ttb.end() \
+ /// #btt.end()
+ /// ```
+ #[func]
pub const fn end(self) -> Side {
match self {
Self::LTR => Side::Right,
@@ -43,7 +99,15 @@ impl Dir {
}
/// The inverse direction.
- pub const fn inv(self) -> Self {
+ ///
+ /// ```example
+ /// #ltr.inv() \
+ /// #rtl.inv() \
+ /// #ttb.inv() \
+ /// #btt.inv()
+ /// ```
+ #[func(title = "Inverse")]
+ pub const fn inv(self) -> Dir {
match self {
Self::LTR => Self::RTL,
Self::RTL => Self::LTR,
@@ -51,16 +115,6 @@ impl Dir {
Self::BTT => Self::TTB,
}
}
-
- /// Whether this direction points into the positive coordinate direction.
- ///
- /// The positive directions are left-to-right and top-to-bottom.
- pub const fn is_positive(self) -> bool {
- match self {
- Self::LTR | Self::TTB => true,
- Self::RTL | Self::BTT => false,
- }
- }
}
impl Debug for Dir {
@@ -75,5 +129,5 @@ impl Debug for Dir {
}
cast! {
- type Dir: "direction",
+ type Dir,
}
diff --git a/crates/typst/src/geom/ellipse.rs b/crates/typst/src/geom/ellipse.rs
index ac20ffd3..36046d95 100644
--- a/crates/typst/src/geom/ellipse.rs
+++ b/crates/typst/src/geom/ellipse.rs
@@ -1,7 +1,7 @@
use super::*;
/// Produce a shape that approximates an axis-aligned ellipse.
-pub fn ellipse(size: Size, fill: Option<Paint>, stroke: Option<Stroke>) -> Shape {
+pub fn ellipse(size: Size, fill: Option<Paint>, stroke: Option<FixedStroke>) -> Shape {
// https://stackoverflow.com/a/2007782
let z = Abs::zero();
let rx = size.x / 2.0;
diff --git a/crates/typst/src/geom/fr.rs b/crates/typst/src/geom/fr.rs
index c602634d..f7cec5d7 100644
--- a/crates/typst/src/geom/fr.rs
+++ b/crates/typst/src/geom/fr.rs
@@ -1,6 +1,18 @@
use super::*;
-/// A fraction of remaining space.
+/// Defines how the the remaining space in a layout is distributed.
+///
+/// Each fractionally sized element gets space based on the ratio of its
+/// fraction to the sum of all fractions.
+///
+/// For more details, also see the [h]($h) and [v]($v) functions and the
+/// [grid function]($grid).
+///
+/// # Example
+/// ```example
+/// Left #h(1fr) Left-ish #h(2fr) Right
+/// ```
+#[ty(name = "fraction")]
#[derive(Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct Fr(Scalar);
@@ -91,14 +103,6 @@ impl Mul<Fr> for f64 {
}
}
-impl Div<f64> for Fr {
- type Output = Self;
-
- fn div(self, other: f64) -> Self {
- Self(self.0 / other)
- }
-}
-
impl Div for Fr {
type Output = f64;
@@ -107,6 +111,14 @@ impl Div for Fr {
}
}
+impl Div<f64> for Fr {
+ type Output = Self;
+
+ fn div(self, other: f64) -> Self {
+ Self(self.0 / other)
+ }
+}
+
assign_impl!(Fr += Fr);
assign_impl!(Fr -= Fr);
assign_impl!(Fr *= f64);
diff --git a/crates/typst/src/geom/length.rs b/crates/typst/src/geom/length.rs
index ccd5362c..453dbe59 100644
--- a/crates/typst/src/geom/length.rs
+++ b/crates/typst/src/geom/length.rs
@@ -1,9 +1,38 @@
+use ecow::eco_format;
+
use super::*;
+use crate::diag::{At, Hint, SourceResult};
+use crate::syntax::Span;
/// A size or distance, possibly expressed with contextual units.
///
-/// Currently supports absolute and font-relative units, but support could quite
-/// easily be extended to other units.
+/// Typst supports the following length units:
+///
+/// - Points: `{72pt}`
+/// - Millimeters: `{254mm}`
+/// - Centimeters: `{2.54cm}`
+/// - Inches: `{1in}`
+/// - Relative to font size: `{2.5em}`
+///
+/// You can multiply lengths with and divide them by integers and floats.
+///
+/// # Example
+/// ```example
+/// #rect(width: 20pt)
+/// #rect(width: 2em)
+/// #rect(width: 1in)
+///
+/// #(3em + 5pt).em \
+/// #(20pt).em \
+/// #(40em + 2pt).abs \
+/// #(5em).abs
+/// ```
+///
+/// # Fields
+/// - `abs`: A length with just the absolute component of the current length
+/// (that is, excluding the `em` component).
+/// - `em`: The amount of `em` units in this length, as a [float]($float).
+#[ty(scope)]
#[derive(Default, Copy, Clone, Eq, PartialEq, Hash)]
pub struct Length {
/// The absolute part.
@@ -39,6 +68,63 @@ impl Length {
pub fn at(self, font_size: Abs) -> Abs {
self.abs + self.em.at(font_size)
}
+
+ /// Fails with an error if the length has a non-zero font-relative part.
+ fn ensure_that_em_is_zero(&self, span: Span, unit: &str) -> SourceResult<()> {
+ if self.em == Em::zero() {
+ return Ok(());
+ }
+ Err(eco_format!(
+ "cannot convert a length with non-zero em units (`{self:?}`) to {unit}"
+ ))
+ .hint(eco_format!("use `length.abs.{unit}()` instead to ignore its em component"))
+ .at(span)
+ }
+}
+
+#[scope]
+impl Length {
+ /// Converts this length to points.
+ ///
+ /// Fails with an error if this length has non-zero `em` units (such as
+ /// `5em + 2pt` instead of just `2pt`). Use the `abs` field (such as in
+ /// `(5em + 2pt).abs.pt()`) to ignore the `em` component of the length (thus
+ /// converting only its absolute component).
+ #[func(name = "pt", title = "Points")]
+ pub fn to_pt(&self, span: Span) -> SourceResult<f64> {
+ self.ensure_that_em_is_zero(span, "pt")?;
+ Ok(self.abs.to_pt())
+ }
+
+ /// Converts this length to millimeters.
+ ///
+ /// Fails with an error if this length has non-zero `em` units. See the
+ /// [`pt`]($length.pt) method for more details.
+ #[func(name = "mm", title = "Millimeters")]
+ pub fn to_mm(&self, span: Span) -> SourceResult<f64> {
+ self.ensure_that_em_is_zero(span, "mm")?;
+ Ok(self.abs.to_mm())
+ }
+
+ /// Converts this length to centimeters.
+ ///
+ /// Fails with an error if this length has non-zero `em` units. See the
+ /// [`pt`]($length.pt) method for more details.
+ #[func(name = "cm", title = "Centimeters")]
+ pub fn to_cm(&self, span: Span) -> SourceResult<f64> {
+ self.ensure_that_em_is_zero(span, "cm")?;
+ Ok(self.abs.to_cm())
+ }
+
+ /// Converts this length to inches.
+ ///
+ /// Fails with an error if this length has non-zero `em` units. See the
+ /// [`pt`]($length.pt) method for more details.
+ #[func(name = "inches")]
+ pub fn to_inches(&self, span: Span) -> SourceResult<f64> {
+ self.ensure_that_em_is_zero(span, "inches")?;
+ Ok(self.abs.to_inches())
+ }
}
impl Debug for Length {
@@ -111,6 +197,14 @@ impl Mul<f64> for Length {
}
}
+impl Mul<Length> for f64 {
+ type Output = Length;
+
+ fn mul(self, rhs: Length) -> Self::Output {
+ rhs * self
+ }
+}
+
impl Div<f64> for Length {
type Output = Self;
diff --git a/crates/typst/src/geom/mod.rs b/crates/typst/src/geom/mod.rs
index 922d25d7..b6ccfb3a 100644
--- a/crates/typst/src/geom/mod.rs
+++ b/crates/typst/src/geom/mod.rs
@@ -28,7 +28,7 @@ mod stroke;
mod transform;
pub use self::abs::{Abs, AbsUnit};
-pub use self::align::{Align, GenAlign, HorizontalAlign, VerticalAlign};
+pub use self::align::{Align, FixedAlign, HAlign, VAlign};
pub use self::angle::{Angle, AngleUnit};
pub use self::axes::{Axes, Axis};
pub use self::color::{
@@ -51,9 +51,7 @@ pub use self::shape::{Geometry, Shape};
pub use self::sides::{Side, Sides};
pub use self::size::Size;
pub use self::smart::Smart;
-pub use self::stroke::{
- DashLength, DashPattern, LineCap, LineJoin, PartialStroke, Stroke,
-};
+pub use self::stroke::{DashLength, DashPattern, FixedStroke, LineCap, LineJoin, Stroke};
pub use self::transform::Transform;
use std::cmp::Ordering;
@@ -64,7 +62,7 @@ use std::iter::Sum;
use std::ops::*;
use crate::diag::{bail, StrResult};
-use crate::eval::{array, cast, Array, Dict, Value};
+use crate::eval::{array, cast, func, scope, ty, Array, Dict, Value};
use crate::model::{Fold, Resolve, StyleChain};
/// Generic access to a structure's components.
diff --git a/crates/typst/src/geom/ratio.rs b/crates/typst/src/geom/ratio.rs
index fe87dd6c..76a09006 100644
--- a/crates/typst/src/geom/ratio.rs
+++ b/crates/typst/src/geom/ratio.rs
@@ -2,8 +2,16 @@ use super::*;
/// A ratio of a whole.
///
-/// _Note_: `50%` is represented as `0.5` here, but stored as `50.0` in the
-/// corresponding [literal](crate::syntax::ast::Numeric).
+/// Written as a number, followed by a percent sign.
+///
+/// # Example
+/// ```example
+/// #set align(center)
+/// #scale(x: 150%)[
+/// Scaled apart.
+/// ]
+/// ```
+#[ty]
#[derive(Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct Ratio(Scalar);
@@ -102,6 +110,14 @@ impl Mul<Ratio> for f64 {
}
}
+impl Div for Ratio {
+ type Output = f64;
+
+ fn div(self, other: Self) -> f64 {
+ self.get() / other.get()
+ }
+}
+
impl Div<f64> for Ratio {
type Output = Self;
@@ -118,14 +134,6 @@ impl Div<Ratio> for f64 {
}
}
-impl Div for Ratio {
- type Output = f64;
-
- fn div(self, other: Self) -> f64 {
- self.get() / other.get()
- }
-}
-
assign_impl!(Ratio += Ratio);
assign_impl!(Ratio -= Ratio);
assign_impl!(Ratio *= Ratio);
diff --git a/crates/typst/src/geom/rel.rs b/crates/typst/src/geom/rel.rs
index 88972222..59a1348d 100644
--- a/crates/typst/src/geom/rel.rs
+++ b/crates/typst/src/geom/rel.rs
@@ -1,8 +1,25 @@
use super::*;
-/// A value that is composed of a relative and an absolute part.
+/// A length in relation to some known length.
+///
+/// This type is a combination of a [length]($length) with a [ratio]($ratio). It
+/// results from addition and subtraction of a length and a ratio. Wherever a
+/// relative length is expected, you can also use a bare length or ratio.
+///
+/// # Example
+/// ```example
+/// #rect(width: 100% - 50pt)
+///
+/// #(100% - 50pt).length \
+/// #(100% - 50pt).ratio
+/// ```
+///
+/// A relative length has the following fields:
+/// - `length`: Its length component.
+/// - `ratio`: Its ratio component.
+#[ty(name = "relative", title = "Relative Length")]
#[derive(Default, Copy, Clone, Eq, PartialEq, Hash)]
-pub struct Rel<T: Numeric> {
+pub struct Rel<T: Numeric = Length> {
/// The relative part.
pub rel: Ratio,
/// The absolute part.
diff --git a/crates/typst/src/geom/rounded.rs b/crates/typst/src/geom/rounded.rs
index f1a7ea08..abaf46de 100644
--- a/crates/typst/src/geom/rounded.rs
+++ b/crates/typst/src/geom/rounded.rs
@@ -5,7 +5,7 @@ pub fn rounded_rect(
size: Size,
radius: Corners<Abs>,
fill: Option<Paint>,
- stroke: Sides<Option<Stroke>>,
+ stroke: Sides<Option<FixedStroke>>,
) -> Vec<Shape> {
let mut res = vec![];
if fill.is_some() || (stroke.iter().any(Option::is_some) && stroke.is_uniform()) {
@@ -43,8 +43,8 @@ fn fill_geometry(size: Size, radius: Corners<Abs>) -> Geometry {
fn stroke_segments(
size: Size,
radius: Corners<Abs>,
- stroke: Sides<Option<Stroke>>,
-) -> Vec<(Path, Option<Stroke>)> {
+ stroke: Sides<Option<FixedStroke>>,
+) -> Vec<(Path, Option<FixedStroke>)> {
let mut res = vec![];
let mut connection = Connection::default();
diff --git a/crates/typst/src/geom/shape.rs b/crates/typst/src/geom/shape.rs
index 5658c21f..0681e3b2 100644
--- a/crates/typst/src/geom/shape.rs
+++ b/crates/typst/src/geom/shape.rs
@@ -8,7 +8,7 @@ pub struct Shape {
/// The shape's background fill.
pub fill: Option<Paint>,
/// The shape's border stroke.
- pub stroke: Option<Stroke>,
+ pub stroke: Option<FixedStroke>,
}
/// A shape's geometry.
@@ -29,7 +29,7 @@ impl Geometry {
}
/// Stroke the geometry without a fill.
- pub fn stroked(self, stroke: Stroke) -> Shape {
+ pub fn stroked(self, stroke: FixedStroke) -> Shape {
Shape { geometry: self, fill: None, stroke: Some(stroke) }
}
}
diff --git a/crates/typst/src/geom/sides.rs b/crates/typst/src/geom/sides.rs
index d4b72a9d..c1c2dadf 100644
--- a/crates/typst/src/geom/sides.rs
+++ b/crates/typst/src/geom/sides.rs
@@ -180,9 +180,25 @@ impl Side {
}
}
+cast! {
+ Side,
+ self => Align::from(self).into_value(),
+ align: Align => match align {
+ Align::LEFT => Self::Left,
+ Align::RIGHT => Self::Right,
+ Align::TOP => Self::Top,
+ Align::BOTTOM => Self::Bottom,
+ _ => bail!("cannot convert this alignment to a side"),
+ },
+}
+
impl<T: Reflect> Reflect for Sides<Option<T>> {
- fn describe() -> CastInfo {
- T::describe() + Dict::describe()
+ fn input() -> CastInfo {
+ T::input() + Dict::input()
+ }
+
+ fn output() -> CastInfo {
+ T::output() + Dict::output()
}
fn castable(value: &Value) -> bool {
diff --git a/crates/typst/src/geom/smart.rs b/crates/typst/src/geom/smart.rs
index a6271c20..2c6e241e 100644
--- a/crates/typst/src/geom/smart.rs
+++ b/crates/typst/src/geom/smart.rs
@@ -97,12 +97,16 @@ impl<T> Default for Smart<T> {
}
impl<T: Reflect> Reflect for Smart<T> {
- fn castable(value: &Value) -> bool {
- AutoValue::castable(value) || T::castable(value)
+ fn input() -> CastInfo {
+ T::input() + AutoValue::input()
+ }
+
+ fn output() -> CastInfo {
+ T::output() + AutoValue::output()
}
- fn describe() -> CastInfo {
- T::describe() + AutoValue::describe()
+ fn castable(value: &Value) -> bool {
+ AutoValue::castable(value) || T::castable(value)
}
}
diff --git a/crates/typst/src/geom/stroke.rs b/crates/typst/src/geom/stroke.rs
index b0387fb7..820f4f44 100644
--- a/crates/typst/src/geom/stroke.rs
+++ b/crates/typst/src/geom/stroke.rs
@@ -2,43 +2,73 @@ use crate::eval::{dict, Cast, FromValue, NoneValue};
use super::*;
-/// A stroke of a geometric shape.
-#[derive(Debug, Clone, Eq, PartialEq, Hash)]
-pub struct Stroke {
- /// The stroke's paint.
- pub paint: Paint,
- /// The stroke's thickness.
- pub thickness: Abs,
- /// The stroke's line cap.
- pub line_cap: LineCap,
- /// The stroke's line join.
- pub line_join: LineJoin,
- /// The stroke's line dash pattern.
- pub dash_pattern: Option<DashPattern<Abs, Abs>>,
- /// The miter limit. Defaults to 4.0, same as `tiny-skia`.
- pub miter_limit: Scalar,
-}
-
-impl Default for Stroke {
- fn default() -> Self {
- Self {
- paint: Paint::Solid(Color::BLACK),
- thickness: Abs::pt(1.0),
- line_cap: LineCap::Butt,
- line_join: LineJoin::Miter,
- dash_pattern: None,
- miter_limit: Scalar(4.0),
- }
- }
-}
-
-/// A partial stroke representation.
+/// Defines how to draw a line.
+///
+/// A stroke has a _paint_ (typically a solid color), a _thickness,_ a line
+/// _cap,_ a line _join,_ a _miter-limit,_ and a _dash_ pattern. All of these
+/// values are optional and have sensible defaults.
+///
+/// # Example
+/// ```example
+/// #set line(length: 100%)
+/// #stack(
+/// spacing: 1em,
+/// line(stroke: 2pt + red),
+/// line(stroke: (paint: blue, thickness: 4pt, cap: "round")),
+/// line(stroke: (paint: blue, thickness: 1pt, dash: "dashed")),
+/// )
+/// ```
+///
+/// # Simple strokes
+/// You can create a simple solid stroke from a color, a thickness, or a
+/// combination of the two. Specifically, wherever a stroke is expected you can
+/// pass any of the following values:
+///
+/// - 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}`.
///
-/// In this representation, both fields are optional so that you can pass either
-/// just a paint (`red`), just a thickness (`0.1em`) or both (`2pt + red`) where
-/// this is expected.
+/// # Complex strokes
+/// For full control, you can also pass a [dictionary]($dictionary) to any
+/// function that expects a stroke. This dictionary has the following keys:
+///
+/// - `paint`: The [color]($color) to use for the stroke.
+///
+/// - `thickness`: The stroke's thickness as a [length]($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]($polygon) or [paths]($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]($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]($dictionary) with the keys `array` (same as the array
+/// above), and `phase` (of type [length]($length)), which defines where in
+/// the pattern to start drawing.
+///
+/// # Fields
+/// 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.
+#[ty]
#[derive(Default, Clone, Eq, PartialEq, Hash)]
-pub struct PartialStroke<T = Length> {
+pub struct Stroke<T: Numeric = Length> {
/// The stroke's paint.
pub paint: Smart<Paint>,
/// The stroke's thickness.
@@ -53,13 +83,13 @@ pub struct PartialStroke<T = Length> {
pub miter_limit: Smart<Scalar>,
}
-impl<T> PartialStroke<T> {
+impl<T: Numeric> Stroke<T> {
/// Map the contained lengths with `f`.
- pub fn map<F, U>(self, f: F) -> PartialStroke<U>
+ pub fn map<F, U: Numeric>(self, f: F) -> Stroke<U>
where
F: Fn(T) -> U,
{
- PartialStroke {
+ Stroke {
paint: self.paint,
thickness: self.thickness.map(&f),
line_cap: self.line_cap,
@@ -82,9 +112,9 @@ impl<T> PartialStroke<T> {
}
}
-impl PartialStroke<Abs> {
+impl Stroke<Abs> {
/// Unpack the stroke, filling missing fields from the `default`.
- pub fn unwrap_or(self, default: Stroke) -> Stroke {
+ pub fn unwrap_or(self, default: FixedStroke) -> FixedStroke {
let thickness = self.thickness.unwrap_or(default.thickness);
let dash_pattern = self
.dash_pattern
@@ -100,7 +130,7 @@ impl PartialStroke<Abs> {
})
.unwrap_or(default.dash_pattern);
- Stroke {
+ FixedStroke {
paint: self.paint.unwrap_or(default.paint),
thickness,
line_cap: self.line_cap.unwrap_or(default.line_cap),
@@ -111,12 +141,12 @@ impl PartialStroke<Abs> {
}
/// Unpack the stroke, filling missing fields with the default values.
- pub fn unwrap_or_default(self) -> Stroke {
- self.unwrap_or(Stroke::default())
+ pub fn unwrap_or_default(self) -> FixedStroke {
+ self.unwrap_or(FixedStroke::default())
}
}
-impl<T: Debug> Debug for PartialStroke<T> {
+impl<T: Numeric + Debug> Debug for Stroke<T> {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
let Self {
paint,
@@ -176,11 +206,11 @@ impl<T: Debug> Debug for PartialStroke<T> {
}
}
-impl Resolve for PartialStroke {
- type Output = PartialStroke<Abs>;
+impl Resolve for Stroke {
+ type Output = Stroke<Abs>;
fn resolve(self, styles: StyleChain) -> Self::Output {
- PartialStroke {
+ Stroke {
paint: self.paint,
thickness: self.thickness.resolve(styles),
line_cap: self.line_cap,
@@ -191,7 +221,7 @@ impl Resolve for PartialStroke {
}
}
-impl Fold for PartialStroke<Abs> {
+impl Fold for Stroke<Abs> {
type Output = Self;
fn fold(self, outer: Self::Output) -> Self::Output {
@@ -207,7 +237,7 @@ impl Fold for PartialStroke<Abs> {
}
cast! {
- type PartialStroke: "stroke",
+ type Stroke,
thickness: Length => Self {
thickness: Smart::Custom(thickness),
..Default::default()
@@ -242,7 +272,7 @@ cast! {
}
cast! {
- PartialStroke<Abs>,
+ Stroke<Abs>,
self => self.map(Length::from).into_value(),
}
@@ -284,14 +314,14 @@ impl Debug for LineJoin {
/// A line dash pattern.
#[derive(Clone, Eq, PartialEq, Hash)]
-pub struct DashPattern<T = Length, DT = DashLength<T>> {
+pub struct DashPattern<T: Numeric = Length, DT = DashLength<T>> {
/// The dash array.
pub array: Vec<DT>,
/// The dash phase.
pub phase: T,
}
-impl<T: Debug, DT: Debug> Debug for DashPattern<T, DT> {
+impl<T: Numeric + Debug, DT: Debug> Debug for DashPattern<T, DT> {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "(array: (")?;
for (i, elem) in self.array.iter().enumerate() {
@@ -306,7 +336,7 @@ impl<T: Debug, DT: Debug> Debug for DashPattern<T, DT> {
}
}
-impl<T: Default> From<Vec<DashLength<T>>> for DashPattern<T> {
+impl<T: Numeric + Default> From<Vec<DashLength<T>>> for DashPattern<T> {
fn from(array: Vec<DashLength<T>>) -> Self {
Self { array, phase: T::default() }
}
@@ -353,20 +383,14 @@ cast! {
},
}
-/// The length of a dash in a line dash pattern
+/// The length of a dash in a line dash pattern.
#[derive(Clone, Eq, PartialEq, Hash)]
-pub enum DashLength<T = Length> {
+pub enum DashLength<T: Numeric = Length> {
LineWidth,
Length(T),
}
-impl From<Abs> for DashLength {
- fn from(l: Abs) -> Self {
- DashLength::Length(l.into())
- }
-}
-
-impl<T> DashLength<T> {
+impl<T: Numeric> DashLength<T> {
fn finish(self, line_width: T) -> T {
match self {
Self::LineWidth => line_width,
@@ -375,7 +399,7 @@ impl<T> DashLength<T> {
}
}
-impl<T: Debug> Debug for DashLength<T> {
+impl<T: Numeric + Debug> Debug for DashLength<T> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
Self::LineWidth => write!(f, "\"dot\""),
@@ -395,13 +419,48 @@ impl Resolve for DashLength {
}
}
+impl From<Abs> for DashLength {
+ fn from(l: Abs) -> Self {
+ DashLength::Length(l.into())
+ }
+}
+
cast! {
DashLength,
self => match self {
Self::LineWidth => "dot".into_value(),
Self::Length(v) => v.into_value(),
},
-
"dot" => Self::LineWidth,
v: Length => Self::Length(v),
}
+
+/// A fully specified stroke of a geometric shape.
+#[derive(Debug, Clone, Eq, PartialEq, Hash)]
+pub struct FixedStroke {
+ /// The stroke's paint.
+ pub paint: Paint,
+ /// The stroke's thickness.
+ pub thickness: Abs,
+ /// The stroke's line cap.
+ pub line_cap: LineCap,
+ /// The stroke's line join.
+ pub line_join: LineJoin,
+ /// The stroke's line dash pattern.
+ pub dash_pattern: Option<DashPattern<Abs, Abs>>,
+ /// The miter limit. Defaults to 4.0, same as `tiny-skia`.
+ pub miter_limit: Scalar,
+}
+
+impl Default for FixedStroke {
+ fn default() -> Self {
+ Self {
+ paint: Paint::Solid(Color::BLACK),
+ thickness: Abs::pt(1.0),
+ line_cap: LineCap::Butt,
+ line_join: LineJoin::Miter,
+ dash_pattern: None,
+ miter_limit: Scalar(4.0),
+ }
+ }
+}
diff --git a/crates/typst/src/ide/complete.rs b/crates/typst/src/ide/complete.rs
index 4f2bacfd..f100de8d 100644
--- a/crates/typst/src/ide/complete.rs
+++ b/crates/typst/src/ide/complete.rs
@@ -10,8 +10,9 @@ use super::analyze::analyze_labels;
use super::{analyze_expr, analyze_import, plain_docs_sentence, summarize_font_family};
use crate::doc::Frame;
use crate::eval::{
- fields_on, format_str, methods_on, CastInfo, Func, Library, Plugin, Scope, Value,
+ format_str, AutoValue, CastInfo, Func, Library, NoneValue, Scope, Type, Value,
};
+use crate::geom::Color;
use crate::syntax::{
ast, is_id_continue, is_id_start, is_ident, LinkedNode, Source, SyntaxKind,
};
@@ -70,6 +71,8 @@ pub enum CompletionKind {
Syntax,
/// A function.
Func,
+ /// A type.
+ Type,
/// A function parameter.
Param,
/// A constant.
@@ -352,7 +355,17 @@ fn complete_field_accesses(ctx: &mut CompletionContext) -> bool {
/// Add completions for all fields on a value.
fn field_access_completions(ctx: &mut CompletionContext, value: &Value) {
- for &(method, args) in methods_on(value.type_name()) {
+ for (name, value) in value.ty().scope().iter() {
+ ctx.value_completion(Some(name.clone()), value, true, None);
+ }
+
+ if let Some(scope) = value.scope() {
+ for (name, value) in scope.iter() {
+ ctx.value_completion(Some(name.clone()), value, true, None);
+ }
+ }
+
+ for &(method, args) in crate::eval::mutable_methods_on(value.ty()) {
ctx.completions.push(Completion {
kind: CompletionKind::Func,
label: method.into(),
@@ -365,7 +378,7 @@ fn field_access_completions(ctx: &mut CompletionContext, value: &Value) {
})
}
- for &field in fields_on(value.type_name()) {
+ for &field in crate::eval::fields_on(value.ty()) {
// Complete the field name along with its value. Notes:
// 1. No parentheses since function fields cannot currently be called
// with method syntax;
@@ -394,7 +407,7 @@ fn field_access_completions(ctx: &mut CompletionContext, value: &Value) {
}
Value::Content(content) => {
for (name, value) in content.fields() {
- ctx.value_completion(Some(name.clone()), &value, false, None);
+ ctx.value_completion(Some(name.into()), &value, false, None);
}
}
Value::Dict(dict) => {
@@ -402,29 +415,14 @@ fn field_access_completions(ctx: &mut CompletionContext, value: &Value) {
ctx.value_completion(Some(name.clone().into()), value, false, None);
}
}
- Value::Module(module) => {
- for (name, value) in module.scope().iter() {
- ctx.value_completion(Some(name.clone()), value, true, None);
- }
- }
- Value::Func(func) => {
- if let Some(info) = func.info() {
- // Consider all names from the function's scope.
- for (name, value) in info.scope.iter() {
- ctx.value_completion(Some(name.clone()), value, true, None);
- }
- }
- }
- Value::Dyn(val) => {
- if let Some(plugin) = val.downcast::<Plugin>() {
- for name in plugin.iter() {
- ctx.completions.push(Completion {
- kind: CompletionKind::Func,
- label: name.clone(),
- apply: None,
- detail: None,
- })
- }
+ Value::Plugin(plugin) => {
+ for name in plugin.iter() {
+ ctx.completions.push(Completion {
+ kind: CompletionKind::Func,
+ label: name.clone(),
+ apply: None,
+ detail: None,
+ })
}
}
_ => {}
@@ -557,9 +555,10 @@ fn set_rule_completions(ctx: &mut CompletionContext) {
ctx.scope_completions(true, |value| {
matches!(
value,
- Value::Func(func) if func.info().map_or(false, |info| {
- info.params.iter().any(|param| param.settable)
- }),
+ Value::Func(func) if func.params()
+ .unwrap_or_default()
+ .iter()
+ .any(|param| param.settable),
)
});
}
@@ -693,9 +692,9 @@ fn param_completions<'a>(
exclude: &[ast::Ident<'a>],
) {
let Some(func) = resolve_global_callee(ctx, callee) else { return };
- let Some(info) = func.info() else { return };
+ let Some(params) = func.params() else { return };
- for param in &info.params {
+ for param in params {
if exclude.iter().any(|ident| ident.as_str() == param.name) {
continue;
}
@@ -714,7 +713,7 @@ fn param_completions<'a>(
}
if param.positional {
- ctx.cast_completions(&param.cast);
+ ctx.cast_completions(&param.input);
}
}
@@ -730,13 +729,12 @@ fn named_param_value_completions<'a>(
name: &str,
) {
let Some(func) = resolve_global_callee(ctx, callee) else { return };
- let Some(info) = func.info() else { return };
- let Some(param) = info.param(name) else { return };
+ let Some(param) = func.param(name) else { return };
if !param.named {
return;
}
- ctx.cast_completions(&param.cast);
+ ctx.cast_completions(&param.input);
if name == "font" {
ctx.font_completions();
}
@@ -755,8 +753,8 @@ fn resolve_global_callee<'a>(
ast::Expr::Ident(ident) => ctx.global.get(&ident)?,
ast::Expr::FieldAccess(access) => match access.target() {
ast::Expr::Ident(target) => match ctx.global.get(&target)? {
- Value::Module(module) => module.get(&access.field()).ok()?,
- Value::Func(func) => func.get(&access.field()).ok()?,
+ Value::Module(module) => module.field(&access.field()).ok()?,
+ Value::Func(func) => func.field(&access.field()).ok()?,
_ => return None,
},
_ => return None,
@@ -808,7 +806,7 @@ fn complete_code(ctx: &mut CompletionContext) -> bool {
#[rustfmt::skip]
fn code_completions(ctx: &mut CompletionContext, hashtag: bool) {
ctx.scope_completions(true, |value| !hashtag || {
- matches!(value, Value::Symbol(_) | Value::Func(_) | Value::Module(_))
+ matches!(value, Value::Symbol(_) | Value::Func(_) | Value::Type(_) | Value::Module(_))
});
ctx.snippet_completion(
@@ -1105,7 +1103,7 @@ impl<'a> CompletionContext<'a> {
let detail = docs.map(Into::into).or_else(|| match value {
Value::Symbol(_) => None,
- Value::Func(func) => func.info().map(|info| plain_docs_sentence(info.docs)),
+ Value::Func(func) => func.docs().map(plain_docs_sentence),
v => {
let repr = v.repr();
(repr.as_str() != label).then(|| repr.into())
@@ -1114,7 +1112,16 @@ impl<'a> CompletionContext<'a> {
let mut apply = None;
if parens && matches!(value, Value::Func(_)) {
- apply = Some(eco_format!("{label}(${{}})"));
+ if let Value::Func(func) = value {
+ if func
+ .params()
+ .is_some_and(|params| params.iter().all(|param| param.name == "self"))
+ {
+ apply = Some(eco_format!("{label}()${{}}"));
+ } else {
+ apply = Some(eco_format!("{label}(${{}})"));
+ }
+ }
} else if at {
apply = Some(eco_format!("at(\"{label}\")"));
} else if label.starts_with('"') && self.after.starts_with('"') {
@@ -1126,6 +1133,7 @@ impl<'a> CompletionContext<'a> {
self.completions.push(Completion {
kind: match value {
Value::Func(_) => CompletionKind::Func,
+ Value::Type(_) => CompletionKind::Type,
Value::Symbol(s) => CompletionKind::Symbol(s.get()),
_ => CompletionKind::Constant,
},
@@ -1147,47 +1155,46 @@ impl<'a> CompletionContext<'a> {
CastInfo::Value(value, docs) => {
self.value_completion(None, value, true, Some(docs));
}
- CastInfo::Type("none") => self.snippet_completion("none", "none", "Nothing."),
- CastInfo::Type("auto") => {
- self.snippet_completion("auto", "auto", "A smart default.");
- }
- CastInfo::Type("boolean") => {
- self.snippet_completion("false", "false", "No / Disabled.");
- self.snippet_completion("true", "true", "Yes / Enabled.");
- }
- CastInfo::Type("color") => {
- self.snippet_completion(
- "luma()",
- "luma(${v})",
- "A custom grayscale color.",
- );
- self.snippet_completion(
- "rgb()",
- "rgb(${r}, ${g}, ${b}, ${a})",
- "A custom RGBA color.",
- );
- self.snippet_completion(
- "cmyk()",
- "cmyk(${c}, ${m}, ${y}, ${k})",
- "A custom CMYK color.",
- );
- self.scope_completions(false, |value| value.type_name() == "color");
- }
- CastInfo::Type("function") => {
- self.snippet_completion(
- "function",
- "(${params}) => ${output}",
- "A custom function.",
- );
- }
CastInfo::Type(ty) => {
- self.completions.push(Completion {
- kind: CompletionKind::Syntax,
- label: (*ty).into(),
- apply: Some(eco_format!("${{{ty}}}")),
- detail: Some(eco_format!("A value of type {ty}.")),
- });
- self.scope_completions(false, |value| value.type_name() == *ty);
+ if *ty == Type::of::<NoneValue>() {
+ self.snippet_completion("none", "none", "Nothing.")
+ } else if *ty == Type::of::<AutoValue>() {
+ self.snippet_completion("auto", "auto", "A smart default.");
+ } else if *ty == Type::of::<bool>() {
+ self.snippet_completion("false", "false", "No / Disabled.");
+ self.snippet_completion("true", "true", "Yes / Enabled.");
+ } else if *ty == Type::of::<Color>() {
+ self.snippet_completion(
+ "luma()",
+ "luma(${v})",
+ "A custom grayscale color.",
+ );
+ self.snippet_completion(
+ "rgb()",
+ "rgb(${r}, ${g}, ${b}, ${a})",
+ "A custom RGBA color.",
+ );
+ self.snippet_completion(
+ "cmyk()",
+ "cmyk(${c}, ${m}, ${y}, ${k})",
+ "A custom CMYK color.",
+ );
+ self.scope_completions(false, |value| value.ty() == *ty);
+ } else if *ty == Type::of::<Func>() {
+ self.snippet_completion(
+ "function",
+ "(${params}) => ${output}",
+ "A custom function.",
+ );
+ } else {
+ self.completions.push(Completion {
+ kind: CompletionKind::Syntax,
+ label: ty.long_name().into(),
+ apply: Some(eco_format!("${{{ty}}}")),
+ detail: Some(eco_format!("A value of type {ty}.")),
+ });
+ self.scope_completions(false, |value| value.ty() == *ty);
+ }
}
CastInfo::Union(union) => {
for info in union {
diff --git a/crates/typst/src/ide/tooltip.rs b/crates/typst/src/ide/tooltip.rs
index 4f2a345e..f310cad0 100644
--- a/crates/typst/src/ide/tooltip.rs
+++ b/crates/typst/src/ide/tooltip.rs
@@ -138,7 +138,7 @@ fn named_param_tooltip(
world: &(dyn World + 'static),
leaf: &LinkedNode,
) -> Option<Tooltip> {
- let (info, named) = if_chain! {
+ let (func, named) = if_chain! {
// Ensure that we are in a named pair in the arguments to a function
// call or set rule.
if let Some(parent) = leaf.parent();
@@ -155,8 +155,7 @@ fn named_param_tooltip(
// Find metadata about the function.
if let Some(Value::Func(func)) = world.library().global.scope().get(&callee);
- if let Some(info) = func.info();
- then { (info, named) }
+ then { (func, named) }
else { return None; }
};
@@ -164,7 +163,7 @@ fn named_param_tooltip(
if_chain! {
if leaf.index() == 0;
if let Some(ident) = leaf.cast::<ast::Ident>();
- if let Some(param) = info.param(&ident);
+ if let Some(param) = func.param(&ident);
then {
return Some(Tooltip::Text(plain_docs_sentence(param.docs)));
}
@@ -173,8 +172,8 @@ fn named_param_tooltip(
// Hovering over a string parameter value.
if_chain! {
if let Some(string) = leaf.cast::<ast::Str>();
- if let Some(param) = info.param(&named.name());
- if let Some(docs) = find_string_doc(&param.cast, &string.get());
+ if let Some(param) = func.param(&named.name());
+ if let Some(docs) = find_string_doc(&param.input, &string.get());
then {
return Some(Tooltip::Text(docs.into()));
}
diff --git a/crates/typst/src/image.rs b/crates/typst/src/image.rs
index 3c2cae88..0ede1d7e 100644
--- a/crates/typst/src/image.rs
+++ b/crates/typst/src/image.rs
@@ -234,7 +234,7 @@ impl From<ttf_parser::RasterImageFormat> for ImageFormat {
pub enum DecodedImage {
/// A decoded pixel raster with its ICC profile.
Raster(image::DynamicImage, Option<IccProfile>, RasterFormat),
- /// An decoded SVG tree.
+ /// A decoded SVG tree.
Svg(usvg::Tree),
}
diff --git a/crates/typst/src/lib.rs b/crates/typst/src/lib.rs
index 75ebe5be..608abbcc 100644
--- a/crates/typst/src/lib.rs
+++ b/crates/typst/src/lib.rs
@@ -104,7 +104,7 @@ pub fn compile(world: &dyn World, tracer: &mut Tracer) -> SourceResult<Document>
/// change and can thus even be cached across multiple compilations (for
/// long-running applications like `typst watch`). Source files on the other
/// hand can change and should thus be cleared after. Advanced clients like
-/// language servers can also retain the source files and [edited](Source::edit)
+/// language servers can also retain the source files and [edit](Source::edit)
/// them in-place to benefit from better incremental performance.
#[comemo::track]
pub trait World {
diff --git a/crates/typst/src/model/content.rs b/crates/typst/src/model/content.rs
index 6541ab6b..3a6c648c 100644
--- a/crates/typst/src/model/content.rs
+++ b/crates/typst/src/model/content.rs
@@ -8,20 +8,63 @@ use ecow::{eco_format, EcoString, EcoVec};
use serde::{Serialize, Serializer};
use super::{
- element, Behave, Behaviour, ElemFunc, Element, Guard, Label, Locatable, Location,
+ elem, Behave, Behaviour, Element, Guard, Label, Locatable, Location, NativeElement,
Recipe, Selector, Style, Styles, Synthesize,
};
use crate::diag::{SourceResult, StrResult};
use crate::doc::Meta;
-use crate::eval::{Dict, FromValue, IntoValue, Str, Value, Vm};
+use crate::eval::{func, scope, ty, Dict, FromValue, IntoValue, Str, Value, Vm};
use crate::syntax::Span;
use crate::util::pretty_array_like;
-/// Composable representation of styled content.
+/// A piece of document content.
+///
+/// This type is at the heart of Typst. All markup you write and most
+/// [functions]($function) you call produce content values. You can create a
+/// content value by enclosing markup in square brackets. This is also how you
+/// pass content to functions.
+///
+/// # Example
+/// ```example
+/// Type of *Hello!* is
+/// #type([*Hello!*])
+/// ```
+///
+/// Content can be added with the `+` operator,
+/// [joined together]($scripting/#blocks) and multiplied with integers. Wherever
+/// content is expected, you can also pass a [string]($str) or `{none}`.
+///
+/// # Representation
+/// Content consists of elements with fields. When constructing an element with
+/// its _element function,_ you provide these fields as arguments and when you
+/// have a content value, you can access its fields with [field access
+/// syntax]($scripting/#field-access).
+///
+/// Some fields are required: These must be provided when constructing an
+/// element and as a consequence, they are always available through field access
+/// on content of that type. Required fields are marked as such in the
+/// documentation.
+///
+/// Most fields are optional: Like required fields, they can be passed to the
+/// element function to configure them for a single element. However, these can
+/// also be configured with [set rules]($styling/#set-rules) to apply them to
+/// all elements within a scope. Optional fields are only available with field
+/// access syntax when they are were explicitly passed to the element function,
+/// not when they result from a set rule.
+///
+/// Each element has a default appearance. However, you can also completely
+/// customize its appearance with a [show rule]($styling/#show-rules). The show
+/// rule is passed the element. It can access the element's field and produce
+/// arbitrary content from it.
+///
+/// In the web app, you can hover over a content variable to see exactly which
+/// elements the content is composed of and what fields they have.
+/// Alternatively, you can inspect the output of the [`repr`]($repr) function.
+#[ty(scope)]
#[derive(Clone, Hash)]
#[allow(clippy::derived_hash_with_manual_eq)]
pub struct Content {
- func: ElemFunc,
+ elem: Element,
attrs: EcoVec<Attr>,
}
@@ -40,13 +83,13 @@ enum Attr {
impl Content {
/// Create an empty element.
- pub fn new(func: ElemFunc) -> Self {
- Self { func, attrs: EcoVec::new() }
+ pub fn new(elem: Element) -> Self {
+ Self { elem, attrs: EcoVec::new() }
}
/// Create empty content.
pub fn empty() -> Self {
- Self::new(SequenceElem::func())
+ Self::new(SequenceElem::elem())
}
/// Create a new sequence element from multiples elements.
@@ -63,23 +106,18 @@ impl Content {
content
}
- /// The element function of the contained content.
- pub fn func(&self) -> ElemFunc {
- self.func
- }
-
/// Whether the content is an empty sequence.
pub fn is_empty(&self) -> bool {
self.is::<SequenceElem>() && self.attrs.is_empty()
}
/// Whether the contained element is of type `T`.
- pub fn is<T: Element>(&self) -> bool {
- self.func == T::func()
+ pub fn is<T: NativeElement>(&self) -> bool {
+ self.elem == T::elem()
}
/// Cast to `T` if the contained element is of type `T`.
- pub fn to<T: Element>(&self) -> Option<&T> {
+ pub fn to<T: NativeElement>(&self) -> Option<&T> {
T::unpack(self)
}
@@ -119,13 +157,13 @@ impl Content {
where
C: ?Sized + 'static,
{
- (self.func.0.vtable)(TypeId::of::<C>()).is_some()
+ self.elem.can::<C>()
}
- /// Whether the contained element has the given capability.
- /// Where the capability is given by a `TypeId`.
+ /// Whether the contained element has the given capability where the
+ /// capability is given by a `TypeId`.
pub fn can_type_id(&self, type_id: TypeId) -> bool {
- (self.func.0.vtable)(type_id).is_some()
+ self.elem.can_type_id(type_id)
}
/// Cast to a trait object if the contained element has the given
@@ -134,7 +172,7 @@ impl Content {
where
C: ?Sized + 'static,
{
- let vtable = (self.func.0.vtable)(TypeId::of::<C>())?;
+ let vtable = self.elem.vtable()(TypeId::of::<C>())?;
let data = self as *const Self as *const ();
Some(unsafe { &*crate::util::fat::from_raw_parts(data, vtable) })
}
@@ -145,7 +183,7 @@ impl Content {
where
C: ?Sized + 'static,
{
- let vtable = (self.func.0.vtable)(TypeId::of::<C>())?;
+ let vtable = self.elem.vtable()(TypeId::of::<C>())?;
let data = self as *mut Self as *mut ();
Some(unsafe { &mut *crate::util::fat::from_raw_parts_mut(data, vtable) })
}
@@ -211,26 +249,6 @@ impl Content {
/// Iter over all fields on the content.
///
/// Does not include synthesized fields for sequence and styled elements.
- pub fn fields(&self) -> impl Iterator<Item = (&EcoString, Value)> {
- static CHILD: EcoString = EcoString::inline("child");
- static CHILDREN: EcoString = EcoString::inline("children");
-
- let option = if let Some(iter) = self.to_sequence() {
- Some((&CHILDREN, Value::Array(iter.cloned().map(Value::Content).collect())))
- } else if let Some((child, _)) = self.to_styled() {
- Some((&CHILD, Value::Content(child.clone())))
- } else {
- None
- };
-
- self.fields_ref()
- .map(|(name, value)| (name, value.clone()))
- .chain(option)
- }
-
- /// Iter over all fields on the content.
- ///
- /// Does not include synthesized fields for sequence and styled elements.
pub fn fields_ref(&self) -> impl Iterator<Item = (&EcoString, &Value)> {
let mut iter = self.attrs.iter();
std::iter::from_fn(move || {
@@ -240,6 +258,11 @@ impl Content {
})
}
+ /// Borrow the value of the given field.
+ pub fn get(&self, key: &str) -> StrResult<Value> {
+ self.field(key).ok_or_else(|| missing_field(key))
+ }
+
/// Try to access a field on the content as a specified type.
pub fn cast_field<T: FromValue>(&self, name: &str) -> Option<T> {
match self.field(name) {
@@ -254,25 +277,6 @@ impl Content {
self.field(name).unwrap().cast().unwrap()
}
- /// Whether the content has the specified field.
- pub fn has(&self, field: &str) -> bool {
- self.field(field).is_some()
- }
-
- /// Borrow the value of the given field.
- pub fn at(&self, field: &str, default: Option<Value>) -> StrResult<Value> {
- self.field(field)
- .or(default)
- .ok_or_else(|| missing_field_no_default(field))
- }
-
- /// Return the fields of the content as a dict.
- pub fn dict(&self) -> Dict {
- self.fields()
- .map(|(key, value)| (key.to_owned().into(), value))
- .collect()
- }
-
/// The content's label.
pub fn label(&self) -> Option<&Label> {
match self.field_ref("label")? {
@@ -310,7 +314,7 @@ impl Content {
prev.apply(styles);
self
} else {
- let mut content = Content::new(StyledElem::func());
+ let mut content = Content::new(StyledElem::elem());
content.attrs.push(Attr::Child(Prehashed::new(self)));
content.attrs.push(Attr::Styles(styles));
content
@@ -365,14 +369,6 @@ impl Content {
&& !self.is_prepared()
}
- /// This content's location in the document flow.
- pub fn location(&self) -> Option<Location> {
- self.attrs.iter().find_map(|modifier| match modifier {
- Attr::Location(location) => Some(*location),
- _ => None,
- })
- }
-
/// Attach a location to this content.
pub fn set_location(&mut self, location: Location) {
self.attrs.push(Attr::Location(location));
@@ -451,9 +447,90 @@ impl Content {
}
}
+#[scope]
+impl Content {
+ /// The content's element function. This function can be used to create the element
+ /// contained in this content. It can be used in set and show rules for the
+ /// element. Can be compared with global functions to check whether you have
+ /// a specific
+ /// kind of element.
+ #[func]
+ pub fn func(&self) -> Element {
+ self.elem
+ }
+
+ /// Whether the content has the specified field.
+ #[func]
+ pub fn has(
+ &self,
+ /// The field to look for.
+ field: Str,
+ ) -> bool {
+ self.field(&field).is_some()
+ }
+
+ /// Access the specified field on the content. Returns the default value if
+ /// the field does not exist or fails with an error if no default value was
+ /// specified.
+ #[func]
+ pub fn at(
+ &self,
+ /// The field to access.
+ field: Str,
+ /// A default value to return if the field does not exist.
+ #[named]
+ default: Option<Value>,
+ ) -> StrResult<Value> {
+ self.field(&field)
+ .or(default)
+ .ok_or_else(|| missing_field_no_default(&field))
+ }
+
+ /// Returns the fields of this content.
+ ///
+ /// ```example
+ /// #rect(
+ /// width: 10cm,
+ /// height: 10cm,
+ /// ).fields()
+ /// ```
+ #[func]
+ pub fn fields(&self) -> Dict {
+ static CHILD: EcoString = EcoString::inline("child");
+ static CHILDREN: EcoString = EcoString::inline("children");
+
+ let option = if let Some(iter) = self.to_sequence() {
+ Some((&CHILDREN, Value::Array(iter.cloned().map(Value::Content).collect())))
+ } else if let Some((child, _)) = self.to_styled() {
+ Some((&CHILD, Value::Content(child.clone())))
+ } else {
+ None
+ };
+
+ self.fields_ref()
+ .map(|(name, value)| (name, value.clone()))
+ .chain(option)
+ .map(|(key, value)| (key.to_owned().into(), value))
+ .collect()
+ }
+
+ /// The location of the content. This is only available on content returned
+ /// by [query]($query) or provided by a
+ /// [show rule]($reference/styling/#show-rules), for other content it will
+ /// be `{none}`. The resulting location can be used with
+ /// [counters]($counter), [state]($state) and [queries]($query).
+ #[func]
+ pub fn location(&self) -> Option<Location> {
+ self.attrs.iter().find_map(|modifier| match modifier {
+ Attr::Location(location) => Some(*location),
+ _ => None,
+ })
+ }
+}
+
impl Debug for Content {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
- let name = self.func.name();
+ let name = self.elem.name();
if let Some(text) = item!(text_str)(self) {
f.write_char('[')?;
f.write_str(&text)?;
@@ -465,6 +542,7 @@ impl Debug for Content {
let mut pieces: Vec<_> = self
.fields()
+ .into_iter()
.map(|(name, value)| eco_format!("{name}: {value:?}"))
.collect();
@@ -490,7 +568,7 @@ impl PartialEq for Content {
} else if let (Some(left), Some(right)) = (self.to_styled(), other.to_styled()) {
left == right
} else {
- self.func == other.func && self.fields_ref().eq(other.fields_ref())
+ self.elem == other.elem && self.fields_ref().eq(other.fields_ref())
}
}
}
@@ -536,8 +614,8 @@ impl Serialize for Content {
S: Serializer,
{
serializer.collect_map(
- iter::once((&"func".into(), self.func().name().into_value()))
- .chain(self.fields()),
+ iter::once((&"func".into(), &self.func().name().into_value()))
+ .chain(self.fields_ref()),
)
}
}
@@ -586,21 +664,16 @@ impl Attr {
}
}
-/// Display: Sequence
-/// Category: special
-#[element]
+/// Defines the `ElemFunc` for sequences.
+#[elem]
struct SequenceElem {}
-/// Display: Sequence
-/// Category: special
-#[element]
+/// Defines the `ElemFunc` for styled elements.
+#[elem]
struct StyledElem {}
/// Hosts metadata and ensures metadata is produced even for empty elements.
-///
-/// Display: Meta
-/// Category: special
-#[element(Behave)]
+#[elem(Behave)]
pub struct MetaElem {
/// Metadata that should be attached to all elements affected by this style
/// property.
@@ -620,6 +693,12 @@ pub trait PlainText {
fn plain_text(&self, text: &mut EcoString);
}
+/// The missing field access error message.
+#[cold]
+fn missing_field(field: &str) -> EcoString {
+ eco_format!("content does not contain field {:?}", Str::from(field))
+}
+
/// The missing field access error message when no default value was given.
#[cold]
fn missing_field_no_default(field: &str) -> EcoString {
diff --git a/crates/typst/src/model/element.rs b/crates/typst/src/model/element.rs
index 27010cd0..c9744cda 100644
--- a/crates/typst/src/model/element.rs
+++ b/crates/typst/src/model/element.rs
@@ -1,134 +1,174 @@
use std::any::TypeId;
+use std::cmp::Ordering;
use std::fmt::{self, Debug, Formatter};
-use std::hash::{Hash, Hasher};
use once_cell::sync::Lazy;
use super::{Content, Selector, Styles};
use crate::diag::SourceResult;
-use crate::eval::{cast, Args, Dict, Func, FuncInfo, Value, Vm};
+use crate::eval::{cast, Args, Dict, Func, ParamInfo, Scope, Value, Vm};
+use crate::util::Static;
/// A document element.
-pub trait Element: Construct + Set + Sized + 'static {
- /// Pack the element into type-erased content.
- fn pack(self) -> Content;
-
- /// Extract this element from type-erased content.
- fn unpack(content: &Content) -> Option<&Self>;
+#[derive(Copy, Clone, Eq, PartialEq, Hash)]
+pub struct Element(Static<NativeElementData>);
- /// The element's function.
- fn func() -> ElemFunc;
-}
-
-/// An element's constructor function.
-pub trait Construct {
- /// Construct an element from the arguments.
- ///
- /// This is passed only the arguments that remain after execution of the
- /// element's set rule.
- fn construct(vm: &mut Vm, args: &mut Args) -> SourceResult<Content>;
-}
-
-/// An element's set rule.
-pub trait Set {
- /// Parse relevant arguments into style properties for this element.
- fn set(vm: &mut Vm, args: &mut Args) -> SourceResult<Styles>;
-}
-
-/// An element's function.
-#[derive(Copy, Clone)]
-pub struct ElemFunc(pub(super) &'static NativeElemFunc);
+impl Element {
+ /// Get the element for `T`.
+ pub fn of<T: NativeElement>() -> Self {
+ T::elem()
+ }
-impl ElemFunc {
- /// The function's name.
+ /// The element's normal name (e.g. `enum`).
pub fn name(self) -> &'static str {
self.0.name
}
- /// Apply the given arguments to the function.
- pub fn with(self, args: Args) -> Func {
- Func::from(self).with(args)
+ /// The element's title case name, for use in documentation
+ /// (e.g. `Numbered List`).
+ pub fn title(&self) -> &'static str {
+ self.0.title
}
- /// Extract details about the function.
- pub fn info(&self) -> &'static FuncInfo {
- &self.0.info
+ /// Documentation for the element (as Markdown).
+ pub fn docs(&self) -> &'static str {
+ self.0.docs
}
- /// Construct an element.
+ /// Search keywords for the element.
+ pub fn keywords(&self) -> &'static [&'static str] {
+ self.0.keywords
+ }
+
+ /// Construct an instance of this element.
pub fn construct(self, vm: &mut Vm, args: &mut Args) -> SourceResult<Content> {
(self.0.construct)(vm, args)
}
- /// Whether the contained element has the given capability.
- pub fn can<C>(&self) -> bool
+ /// Execute the set rule for the element and return the resulting style map.
+ pub fn set(self, vm: &mut Vm, mut args: Args) -> SourceResult<Styles> {
+ let styles = (self.0.set)(vm, &mut args)?;
+ args.finish()?;
+ Ok(styles)
+ }
+
+ /// Whether the element has the given capability.
+ pub fn can<C>(self) -> bool
where
C: ?Sized + 'static,
{
- (self.0.vtable)(TypeId::of::<C>()).is_some()
+ self.can_type_id(TypeId::of::<C>())
}
- /// Create a selector for elements of this function.
+ /// Whether the element has the given capability where the capability is
+ /// given by a `TypeId`.
+ pub fn can_type_id(self, type_id: TypeId) -> bool {
+ (self.0.vtable)(type_id).is_some()
+ }
+
+ /// The VTable for capabilities dispatch.
+ pub fn vtable(self) -> fn(of: TypeId) -> Option<*const ()> {
+ self.0.vtable
+ }
+
+ /// Create a selector for this element.
pub fn select(self) -> Selector {
Selector::Elem(self, None)
}
- /// Create a selector for elements of this function, filtering for those
- /// whose [fields](super::Content::field) match the given arguments.
+ /// Create a selector for this element, filtering for those
+ /// that [fields](super::Content::field) match the given argument.
pub fn where_(self, fields: Dict) -> Selector {
Selector::Elem(self, Some(fields))
}
- /// Execute the set rule for the element and return the resulting style map.
- pub fn set(self, vm: &mut Vm, mut args: Args) -> SourceResult<Styles> {
- let styles = (self.0.set)(vm, &mut args)?;
- args.finish()?;
- Ok(styles)
+ /// The element's associated scope of sub-definition.
+ pub fn scope(&self) -> &'static Scope {
+ &(self.0).0.scope
+ }
+
+ /// Details about the element's fields.
+ pub fn params(&self) -> &'static [ParamInfo] {
+ &(self.0).0.params
}
}
-impl Debug for ElemFunc {
+impl Debug for Element {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.pad(self.name())
}
}
-impl Eq for ElemFunc {}
-
-impl PartialEq for ElemFunc {
- fn eq(&self, other: &Self) -> bool {
- std::ptr::eq(self.0, other.0)
+impl Ord for Element {
+ fn cmp(&self, other: &Self) -> Ordering {
+ self.name().cmp(other.name())
}
}
-impl Hash for ElemFunc {
- fn hash<H: Hasher>(&self, state: &mut H) {
- state.write_usize(self.0 as *const _ as usize);
+impl PartialOrd for Element {
+ fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
+ Some(self.cmp(other))
}
}
cast! {
- ElemFunc,
+ Element,
self => Value::Func(self.into()),
- v: Func => v.element().ok_or("expected element function")?,
+ v: Func => v.element().ok_or("expected element")?,
}
-impl From<&'static NativeElemFunc> for ElemFunc {
- fn from(native: &'static NativeElemFunc) -> Self {
- Self(native)
+/// A Typst element that is defined by a native Rust type.
+pub trait NativeElement: Construct + Set + Sized + 'static {
+ /// Get the element for the native Rust element.
+ fn elem() -> Element {
+ Element::from(Self::data())
}
+
+ /// Get the element data for the native Rust element.
+ fn data() -> &'static NativeElementData;
+
+ /// Pack the element into type-erased content.
+ fn pack(self) -> Content;
+
+ /// Extract this element from type-erased content.
+ fn unpack(content: &Content) -> Option<&Self>;
}
-/// An element function backed by a Rust type.
-pub struct NativeElemFunc {
- /// The element's name.
+/// An element's constructor function.
+pub trait Construct {
+ /// Construct an element from the arguments.
+ ///
+ /// This is passed only the arguments that remain after execution of the
+ /// element's set rule.
+ fn construct(vm: &mut Vm, args: &mut Args) -> SourceResult<Content>;
+}
+
+/// An element's set rule.
+pub trait Set {
+ /// Parse relevant arguments into style properties for this element.
+ fn set(vm: &mut Vm, args: &mut Args) -> SourceResult<Styles>;
+}
+
+/// Defines a native element.
+pub struct NativeElementData {
pub name: &'static str,
- /// The element's vtable for capability dispatch.
- pub vtable: fn(of: TypeId) -> Option<*const ()>,
- /// The element's constructor.
+ pub title: &'static str,
+ pub docs: &'static str,
+ pub keywords: &'static [&'static str],
pub construct: fn(&mut Vm, &mut Args) -> SourceResult<Content>,
- /// The element's set rule.
pub set: fn(&mut Vm, &mut Args) -> SourceResult<Styles>,
- /// Details about the function.
- pub info: Lazy<FuncInfo>,
+ pub vtable: fn(of: TypeId) -> Option<*const ()>,
+ pub scope: Lazy<Scope>,
+ pub params: Lazy<Vec<ParamInfo>>,
+}
+
+impl From<&'static NativeElementData> for Element {
+ fn from(data: &'static NativeElementData) -> Self {
+ Self(Static(data))
+ }
+}
+
+cast! {
+ &'static NativeElementData,
+ self => Element::from(self).into_value(),
}
diff --git a/crates/typst/src/model/introspect.rs b/crates/typst/src/model/introspect.rs
index 2b2693d9..90598679 100644
--- a/crates/typst/src/model/introspect.rs
+++ b/crates/typst/src/model/introspect.rs
@@ -11,14 +11,19 @@ use indexmap::IndexMap;
use super::{Content, Selector};
use crate::diag::{bail, StrResult};
use crate::doc::{Frame, FrameItem, Meta, Position};
-use crate::eval::{cast, Value};
+use crate::eval::{cast, func, scope, ty, Dict, Value, Vm};
use crate::geom::{Point, Transform};
use crate::model::Label;
use crate::util::NonZeroExt;
-/// Identifies the location of an element in the document.
+/// Identifies an element in the document.
///
-/// This struct is created by [`Locator::locate`].
+/// A location uniquely identifies an element in the document and lets you
+/// access its absolute position on the pages. You can retrieve the current
+/// location with the [`locate`]($locate) function and the location of a queried
+/// or shown element with the [`location()`]($content.location) method on
+/// content.
+#[ty(scope)]
#[derive(Copy, Clone, Eq, PartialEq, Hash)]
pub struct Location {
/// The hash of the element.
@@ -40,6 +45,44 @@ impl Location {
}
}
+#[scope]
+impl Location {
+ /// Return the page number for this location.
+ ///
+ /// Note that this does not return the value of the [page counter]($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.
+ #[func]
+ pub fn page(self, vm: &mut Vm) -> NonZeroUsize {
+ vm.vt.introspector.page(self)
+ }
+
+ /// Return 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.
+ #[func]
+ pub fn position(self, vm: &mut Vm) -> Dict {
+ vm.vt.introspector.position(self).into()
+ }
+
+ /// 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`.
+ #[func]
+ pub fn page_numbering(self, vm: &mut Vm) -> Value {
+ vm.vt.introspector.page_numbering(self)
+ }
+}
+
impl Debug for Location {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.pad("..")
@@ -47,7 +90,7 @@ impl Debug for Location {
}
cast! {
- type Location: "location",
+ type Location,
}
/// Provides locations for elements in the document.
diff --git a/crates/typst/src/model/label.rs b/crates/typst/src/model/label.rs
index ef8f3edd..79f73881 100644
--- a/crates/typst/src/model/label.rs
+++ b/crates/typst/src/model/label.rs
@@ -2,10 +2,42 @@ use std::fmt::{self, Debug, Formatter};
use ecow::EcoString;
+use crate::eval::{func, scope, ty};
+
/// A label for an element.
+///
+/// Inserting a label into content attaches it to the closest previous element
+/// that is not a space. Then, the element can be [referenced]($ref) and styled
+/// through the label.
+///
+/// # Example
+/// ```example
+/// #show <a>: set text(blue)
+/// #show label("b"): set text(red)
+///
+/// = Heading <a>
+/// *Strong* #label("b")
+/// ```
+///
+/// # Syntax
+/// This function also has dedicated syntax: You can create a label by enclosing
+/// its name in angle brackets. This works both in markup and code.
+#[ty(scope)]
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct Label(pub EcoString);
+#[scope]
+impl Label {
+ /// Creates a label from a string.
+ #[func(constructor)]
+ pub fn construct(
+ /// The name of the label.
+ name: EcoString,
+ ) -> Label {
+ Self(name)
+ }
+}
+
impl Debug for Label {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "<{}>", self.0)
diff --git a/crates/typst/src/model/mod.rs b/crates/typst/src/model/mod.rs
index 100006b4..2741f731 100644
--- a/crates/typst/src/model/mod.rs
+++ b/crates/typst/src/model/mod.rs
@@ -9,10 +9,10 @@ mod selector;
mod styles;
#[doc(inline)]
-pub use typst_macros::element;
+pub use typst_macros::elem;
pub use self::content::{Content, MetaElem, PlainText};
-pub use self::element::{Construct, ElemFunc, Element, NativeElemFunc, Set};
+pub use self::element::{Construct, Element, NativeElement, NativeElementData, Set};
pub use self::introspect::{Introspector, Location, Locator};
pub use self::label::{Label, Unlabellable};
pub use self::realize::{
diff --git a/crates/typst/src/model/realize.rs b/crates/typst/src/model/realize.rs
index 3e683d3e..745d7f43 100644
--- a/crates/typst/src/model/realize.rs
+++ b/crates/typst/src/model/realize.rs
@@ -1,4 +1,6 @@
-use super::{Content, ElemFunc, Element, MetaElem, Recipe, Selector, StyleChain, Vt};
+use super::{
+ Content, Element, MetaElem, NativeElement, Recipe, Selector, StyleChain, Vt,
+};
use crate::diag::SourceResult;
use crate::doc::Meta;
use crate::util::hash128;
@@ -226,5 +228,5 @@ pub enum Guard {
/// The nth recipe from the top of the chain.
Nth(usize),
/// The [base recipe](Show) for a kind of element.
- Base(ElemFunc),
+ Base(Element),
}
diff --git a/crates/typst/src/model/selector.rs b/crates/typst/src/model/selector.rs
index 5e4f257b..9264c9ea 100644
--- a/crates/typst/src/model/selector.rs
+++ b/crates/typst/src/model/selector.rs
@@ -4,22 +4,58 @@ use std::sync::Arc;
use ecow::{eco_format, EcoString, EcoVec};
-use super::{Content, ElemFunc, Label, Location};
+use super::{Content, Element, Label, Locatable, Location};
use crate::diag::{bail, StrResult};
use crate::eval::{
- cast, CastInfo, Dict, FromValue, Func, IntoValue, Reflect, Regex, Value,
+ cast, func, scope, ty, CastInfo, Dict, FromValue, Func, Reflect, Regex, Str, Symbol,
+ Type, Value,
};
-use crate::model::Locatable;
use crate::util::pretty_array_like;
-/// A selector in a show rule.
+/// A filter for selecting elements within the document.
+///
+/// You can construct a selector in the following ways:
+/// - you can use an element [function]($function)
+/// - you can filter for an element function with
+/// [specific fields]($function.where)
+/// - you can use a [string]($str) or [regular expression]($regex)
+/// - you can use a [`{<label>}`]($label)
+/// - you can use a [`location`]($location)
+/// - call the [`selector`]($selector) constructor to convert any of the above
+/// types into a selector value and use the methods below to refine it
+///
+/// Selectors are used to [apply styling rules]($styling/#show-rules) to
+/// elements. You can also use selectors to [query]($query) the document for
+/// certain types of elements.
+///
+/// Furthermore, you can pass a selector to several of Typst's built-in
+/// functions to configure their behaviour. One such example is the
+/// [outline]($outline) where it can be used to change which elements are listed
+/// within the outline.
+///
+/// Multiple selectors can be combined using the methods shown below. However,
+/// not all kinds of selectors are supported in all places, at the moment.
+///
+/// # Example
+/// ```example
+/// #locate(loc => query(
+/// heading.where(level: 1)
+/// .or(heading.where(level: 2)),
+/// loc,
+/// ))
+///
+/// = This will be found
+/// == So will this
+/// === But this will not.
+/// ```
+#[ty(scope)]
#[derive(Clone, PartialEq, Hash)]
pub enum Selector {
/// Matches a specific type of element.
///
/// If there is a dictionary, only elements with the fields from the
/// dictionary match.
- Elem(ElemFunc, Option<Dict>),
+ Elem(Element, Option<Dict>),
/// Matches the element at the specified location.
Location(Location),
/// Matches elements with a specific label.
@@ -63,36 +99,6 @@ impl Selector {
Self::Can(TypeId::of::<T>())
}
- /// Transforms this selector and an iterator of other selectors into a
- /// [`Selector::And`] selector.
- pub fn and(self, others: impl IntoIterator<Item = Self>) -> Self {
- Self::And(others.into_iter().chain(Some(self)).collect())
- }
-
- /// Transforms this selector and an iterator of other selectors into a
- /// [`Selector::Or`] selector.
- pub fn or(self, others: impl IntoIterator<Item = Self>) -> Self {
- Self::Or(others.into_iter().chain(Some(self)).collect())
- }
-
- /// Transforms this selector into a [`Selector::Before`] selector.
- pub fn before(self, location: impl Into<Self>, inclusive: bool) -> Self {
- Self::Before {
- selector: Arc::new(self),
- end: Arc::new(location.into()),
- inclusive,
- }
- }
-
- /// Transforms this selector into a [`Selector::After`] selector.
- pub fn after(self, location: impl Into<Self>, inclusive: bool) -> Self {
- Self::After {
- selector: Arc::new(self),
- start: Arc::new(location.into()),
- inclusive,
- }
- }
-
/// Whether the selector matches for the target.
pub fn matches(&self, target: &Content) -> bool {
match self {
@@ -105,7 +111,7 @@ impl Selector {
}
Self::Label(label) => target.label() == Some(label),
Self::Regex(regex) => {
- target.func() == item!(text_func)
+ target.func() == item!(text_elem)
&& item!(text_str)(target).map_or(false, |text| regex.is_match(&text))
}
Self::Can(cap) => target.can_type_id(*cap),
@@ -118,6 +124,85 @@ impl Selector {
}
}
+#[scope]
+impl Selector {
+ /// 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)}`.
+ #[func(constructor)]
+ pub fn construct(
+ /// 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
+ }
+
+ /// Selects all elements that match this or any of the other selectors.
+ #[func]
+ pub fn or(
+ self,
+ /// The other selectors to match on.
+ #[variadic]
+ others: Vec<LocatableSelector>,
+ ) -> Selector {
+ Self::Or(others.into_iter().map(|s| s.0).chain(Some(self)).collect())
+ }
+
+ /// Selects all elements that match this and all of the the other selectors.
+ #[func]
+ pub fn and(
+ self,
+ /// The other selectors to match on.
+ #[variadic]
+ others: Vec<LocatableSelector>,
+ ) -> Selector {
+ Self::And(others.into_iter().map(|s| s.0).chain(Some(self)).collect())
+ }
+
+ /// Returns a modified selector that will only match elements that occur
+ /// before the first match of `end`.
+ #[func]
+ pub fn before(
+ self,
+ /// The original selection will end at the first match of `end`.
+ end: LocatableSelector,
+ /// Whether `end` itself should match or not. This is only relevant if
+ /// both selectors match the same type of element. Defaults to `{true}`.
+ #[named]
+ #[default(true)]
+ inclusive: bool,
+ ) -> Selector {
+ Self::Before {
+ selector: Arc::new(self),
+ end: Arc::new(end.0),
+ inclusive,
+ }
+ }
+
+ /// Returns a modified selector that will only match elements that occur
+ /// after the first match of `start`.
+ #[func]
+ pub fn after(
+ self,
+ /// The original selection will start at the first match of `start`.
+ start: LocatableSelector,
+ /// Whether `start` itself should match or not. This is only relevant
+ /// if both selectors match the same type of element. Defaults to
+ /// `{true}`.
+ #[named]
+ #[default(true)]
+ inclusive: bool,
+ ) -> Selector {
+ Self::After {
+ selector: Arc::new(self),
+ start: Arc::new(start.0),
+ inclusive,
+ }
+ }
+}
+
impl From<Location> for Selector {
fn from(value: Location) -> Self {
Self::Location(value)
@@ -166,7 +251,7 @@ impl Debug for Selector {
}
cast! {
- type Selector: "selector",
+ type Selector,
func: Func => func
.element()
.ok_or("only element functions can be used as selectors")?
@@ -185,23 +270,26 @@ cast! {
pub struct LocatableSelector(pub Selector);
impl Reflect for LocatableSelector {
- fn describe() -> CastInfo {
+ fn input() -> CastInfo {
CastInfo::Union(vec![
- CastInfo::Type("function"),
- CastInfo::Type("label"),
- CastInfo::Type("selector"),
+ CastInfo::Type(Type::of::<Label>()),
+ CastInfo::Type(Type::of::<Func>()),
+ CastInfo::Type(Type::of::<Selector>()),
])
}
+ fn output() -> CastInfo {
+ CastInfo::Type(Type::of::<Selector>())
+ }
+
fn castable(value: &Value) -> bool {
- matches!(value.type_name(), "function" | "label" | "selector")
+ Label::castable(value) || Func::castable(value) || Selector::castable(value)
}
}
-impl IntoValue for LocatableSelector {
- fn into_value(self) -> Value {
- self.0.into_value()
- }
+cast! {
+ LocatableSelector,
+ self => self.0.into_value(),
}
impl FromValue for LocatableSelector {
@@ -242,6 +330,12 @@ impl FromValue for LocatableSelector {
}
}
+impl From<Location> for LocatableSelector {
+ fn from(loc: Location) -> Self {
+ Self(Selector::Location(loc))
+ }
+}
+
/// A selector that can be used with show rules.
///
/// Hopefully, this is made obsolete by a more powerful showing mechanism in the
@@ -250,34 +344,34 @@ impl FromValue for LocatableSelector {
pub struct ShowableSelector(pub Selector);
impl Reflect for ShowableSelector {
- fn describe() -> CastInfo {
+ fn input() -> CastInfo {
CastInfo::Union(vec![
- CastInfo::Type("function"),
- CastInfo::Type("label"),
- CastInfo::Type("string"),
- CastInfo::Type("regular expression"),
- CastInfo::Type("symbol"),
- CastInfo::Type("selector"),
+ CastInfo::Type(Type::of::<Symbol>()),
+ CastInfo::Type(Type::of::<Str>()),
+ CastInfo::Type(Type::of::<Label>()),
+ CastInfo::Type(Type::of::<Func>()),
+ CastInfo::Type(Type::of::<Regex>()),
+ CastInfo::Type(Type::of::<Selector>()),
])
}
+ fn output() -> CastInfo {
+ CastInfo::Type(Type::of::<Selector>())
+ }
+
fn castable(value: &Value) -> bool {
- matches!(
- value.type_name(),
- "symbol"
- | "string"
- | "label"
- | "function"
- | "regular expression"
- | "selector"
- )
+ Symbol::castable(value)
+ || Str::castable(value)
+ || Label::castable(value)
+ || Func::castable(value)
+ || Regex::castable(value)
+ || Selector::castable(value)
}
}
-impl IntoValue for ShowableSelector {
- fn into_value(self) -> Value {
- self.0.into_value()
- }
+cast! {
+ ShowableSelector,
+ self => self.0.into_value(),
}
impl FromValue for ShowableSelector {
diff --git a/crates/typst/src/model/styles.rs b/crates/typst/src/model/styles.rs
index 75172680..347eae4c 100644
--- a/crates/typst/src/model/styles.rs
+++ b/crates/typst/src/model/styles.rs
@@ -6,12 +6,13 @@ use std::ptr;
use comemo::Prehashed;
use ecow::{eco_vec, EcoString, EcoVec};
-use super::{Content, ElemFunc, Element, Selector, Vt};
+use super::{Content, Element, NativeElement, Selector, Vt};
use crate::diag::{SourceResult, Trace, Tracepoint};
-use crate::eval::{cast, Args, FromValue, Func, IntoValue, Value, Vm};
+use crate::eval::{cast, ty, Args, FromValue, Func, IntoValue, Value, Vm};
use crate::syntax::Span;
/// A list of style properties.
+#[ty]
#[derive(Default, PartialEq, Clone, Hash)]
pub struct Styles(EcoVec<Prehashed<Style>>);
@@ -70,11 +71,11 @@ impl Styles {
/// Returns `Some(_)` with an optional span if this list contains
/// styles for the given element.
- pub fn interruption<T: Element>(&self) -> Option<Option<Span>> {
- let func = T::func();
+ pub fn interruption<T: NativeElement>(&self) -> Option<Option<Span>> {
+ let elem = T::elem();
self.0.iter().find_map(|entry| match &**entry {
- Style::Property(property) => property.is_of(func).then_some(property.span),
- Style::Recipe(recipe) => recipe.is_of(func).then_some(Some(recipe.span)),
+ Style::Property(property) => property.is_of(elem).then_some(property.span),
+ Style::Recipe(recipe) => recipe.is_of(elem).then_some(Some(recipe.span)),
})
}
}
@@ -143,7 +144,7 @@ impl From<Recipe> for Style {
#[derive(Clone, PartialEq, Hash)]
pub struct Property {
/// The element the property belongs to.
- element: ElemFunc,
+ elem: Element,
/// The property's name.
name: EcoString,
/// The property's value.
@@ -154,13 +155,9 @@ pub struct Property {
impl Property {
/// Create a new property from a key-value pair.
- pub fn new(
- element: ElemFunc,
- name: impl Into<EcoString>,
- value: impl IntoValue,
- ) -> Self {
+ pub fn new(elem: Element, name: impl Into<EcoString>, value: impl IntoValue) -> Self {
Self {
- element,
+ elem,
name: name.into(),
value: value.into_value(),
span: None,
@@ -168,19 +165,19 @@ impl Property {
}
/// Whether this property is the given one.
- pub fn is(&self, element: ElemFunc, name: &str) -> bool {
- self.element == element && self.name == name
+ pub fn is(&self, elem: Element, name: &str) -> bool {
+ self.elem == elem && self.name == name
}
/// Whether this property belongs to the given element.
- pub fn is_of(&self, element: ElemFunc) -> bool {
- self.element == element
+ pub fn is_of(&self, elem: Element) -> bool {
+ self.elem == elem
}
}
impl Debug for Property {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
- write!(f, "set {}({}: {:?})", self.element.name(), self.name, self.value)?;
+ write!(f, "set {}({}: {:?})", self.elem.name(), self.name, self.value)?;
Ok(())
}
}
@@ -198,7 +195,7 @@ pub struct Recipe {
impl Recipe {
/// Whether this recipe is for the given type of element.
- pub fn is_of(&self, element: ElemFunc) -> bool {
+ pub fn is_of(&self, element: Element) -> bool {
match self.selector {
Some(Selector::Elem(own, _)) => own == element,
_ => false,
@@ -323,7 +320,7 @@ impl<'a> StyleChain<'a> {
/// Cast the first value for the given property in the chain.
pub fn get<T: FromValue>(
self,
- func: ElemFunc,
+ func: Element,
name: &'a str,
inherent: Option<Value>,
default: impl Fn() -> T,
@@ -336,7 +333,7 @@ impl<'a> StyleChain<'a> {
/// Cast the first value for the given property in the chain.
pub fn get_resolve<T: FromValue + Resolve>(
self,
- func: ElemFunc,
+ func: Element,
name: &'a str,
inherent: Option<Value>,
default: impl Fn() -> T,
@@ -347,7 +344,7 @@ impl<'a> StyleChain<'a> {
/// Cast the first value for the given property in the chain.
pub fn get_fold<T: FromValue + Fold>(
self,
- func: ElemFunc,
+ func: Element,
name: &'a str,
inherent: Option<Value>,
default: impl Fn() -> T::Output,
@@ -368,7 +365,7 @@ impl<'a> StyleChain<'a> {
/// Cast the first value for the given property in the chain.
pub fn get_resolve_fold<T>(
self,
- func: ElemFunc,
+ func: Element,
name: &'a str,
inherent: Option<Value>,
default: impl Fn() -> <T::Output as Fold>::Output,
@@ -402,7 +399,7 @@ impl<'a> StyleChain<'a> {
/// Iterate over all values for the given property in the chain.
pub fn properties<T: FromValue + 'a>(
self,
- func: ElemFunc,
+ func: Element,
name: &'a str,
inherent: Option<Value>,
) -> impl Iterator<Item = T> + '_ {
diff --git a/crates/typst/src/util/fmt.rs b/crates/typst/src/util/fmt.rs
new file mode 100644
index 00000000..a2686dd7
--- /dev/null
+++ b/crates/typst/src/util/fmt.rs
@@ -0,0 +1,78 @@
+/// Format pieces separated with commas and a final "and" or "or".
+pub fn separated_list(pieces: &[impl AsRef<str>], last: &str) -> String {
+ let mut buf = String::new();
+ for (i, part) in pieces.iter().enumerate() {
+ match i {
+ 0 => {}
+ 1 if pieces.len() == 2 => {
+ buf.push(' ');
+ buf.push_str(last);
+ buf.push(' ');
+ }
+ i if i + 1 == pieces.len() => {
+ buf.push_str(", ");
+ buf.push_str(last);
+ buf.push(' ');
+ }
+ _ => buf.push_str(", "),
+ }
+ buf.push_str(part.as_ref());
+ }
+ buf
+}
+
+/// Format a comma-separated list.
+///
+/// Tries to format horizontally, but falls back to vertical formatting if the
+/// pieces are too long.
+pub fn pretty_comma_list(pieces: &[impl AsRef<str>], trailing_comma: bool) -> String {
+ const MAX_WIDTH: usize = 50;
+
+ let mut buf = String::new();
+ let len = pieces.iter().map(|s| s.as_ref().len()).sum::<usize>()
+ + 2 * pieces.len().saturating_sub(1);
+
+ if len <= MAX_WIDTH {
+ for (i, piece) in pieces.iter().enumerate() {
+ if i > 0 {
+ buf.push_str(", ");
+ }
+ buf.push_str(piece.as_ref());
+ }
+ if trailing_comma {
+ buf.push(',');
+ }
+ } else {
+ for piece in pieces {
+ buf.push_str(piece.as_ref().trim());
+ buf.push_str(",\n");
+ }
+ }
+
+ buf
+}
+
+/// Format an array-like construct.
+///
+/// Tries to format horizontally, but falls back to vertical formatting if the
+/// pieces are too long.
+pub fn pretty_array_like(parts: &[impl AsRef<str>], trailing_comma: bool) -> String {
+ let list = pretty_comma_list(parts, trailing_comma);
+ let mut buf = String::new();
+ buf.push('(');
+ if list.contains('\n') {
+ buf.push('\n');
+ for (i, line) in list.lines().enumerate() {
+ if i > 0 {
+ buf.push('\n');
+ }
+ buf.push_str(" ");
+ buf.push_str(line);
+ }
+ buf.push('\n');
+ } else {
+ buf.push_str(&list);
+ }
+ buf.push(')');
+ buf
+}
diff --git a/crates/typst/src/util/mod.rs b/crates/typst/src/util/mod.rs
index 1ba85bbb..6c183a4f 100644
--- a/crates/typst/src/util/mod.rs
+++ b/crates/typst/src/util/mod.rs
@@ -1,10 +1,14 @@
//! Utilities.
pub mod fat;
+mod fmt;
-use std::fmt::{self, Debug, Formatter};
+pub use self::fmt::{pretty_array_like, pretty_comma_list, separated_list};
+
+use std::fmt::{Debug, Formatter};
use std::hash::Hash;
use std::num::NonZeroUsize;
+use std::ops::Deref;
use std::sync::Arc;
use siphasher::sip128::{Hasher128, SipHasher13};
@@ -12,15 +16,15 @@ use siphasher::sip128::{Hasher128, SipHasher13};
/// Turn a closure into a struct implementing [`Debug`].
pub fn debug<F>(f: F) -> impl Debug
where
- F: Fn(&mut Formatter) -> fmt::Result,
+ F: Fn(&mut Formatter) -> std::fmt::Result,
{
struct Wrapper<F>(F);
impl<F> Debug for Wrapper<F>
where
- F: Fn(&mut Formatter) -> fmt::Result,
+ F: Fn(&mut Formatter) -> std::fmt::Result,
{
- fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+ fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
self.0(f)
}
}
@@ -103,89 +107,44 @@ where
}
}
-/// Format pieces separated with commas and a final "and" or "or".
-pub fn separated_list(pieces: &[impl AsRef<str>], last: &str) -> String {
- let mut buf = String::new();
- for (i, part) in pieces.iter().enumerate() {
- match i {
- 0 => {}
- 1 if pieces.len() == 2 => {
- buf.push(' ');
- buf.push_str(last);
- buf.push(' ');
- }
- i if i + 1 == pieces.len() => {
- buf.push_str(", ");
- buf.push_str(last);
- buf.push(' ');
- }
- _ => buf.push_str(", "),
- }
- buf.push_str(part.as_ref());
- }
- buf
+/// Check if the [`Option`]-wrapped L is same to R.
+pub fn option_eq<L, R>(left: Option<L>, other: R) -> bool
+where
+ L: PartialEq<R>,
+{
+ left.map_or(false, |v| v == other)
}
-/// Format a comma-separated list.
-///
-/// Tries to format horizontally, but falls back to vertical formatting if the
-/// pieces are too long.
-pub fn pretty_comma_list(pieces: &[impl AsRef<str>], trailing_comma: bool) -> String {
- const MAX_WIDTH: usize = 50;
-
- let mut buf = String::new();
- let len = pieces.iter().map(|s| s.as_ref().len()).sum::<usize>()
- + 2 * pieces.len().saturating_sub(1);
-
- if len <= MAX_WIDTH {
- for (i, piece) in pieces.iter().enumerate() {
- if i > 0 {
- buf.push_str(", ");
- }
- buf.push_str(piece.as_ref());
- }
- if trailing_comma {
- buf.push(',');
- }
- } else {
- for piece in pieces {
- buf.push_str(piece.as_ref().trim());
- buf.push_str(",\n");
- }
+/// A container around a static reference that is cheap to clone and hash.
+#[derive(Debug)]
+pub struct Static<T: 'static>(pub &'static T);
+
+impl<T> Deref for Static<T> {
+ type Target = T;
+
+ fn deref(&self) -> &Self::Target {
+ self.0
}
+}
+
+impl<T> Copy for Static<T> {}
- buf
+impl<T> Clone for Static<T> {
+ fn clone(&self) -> Self {
+ *self
+ }
}
-/// Format an array-like construct.
-///
-/// Tries to format horizontally, but falls back to vertical formatting if the
-/// pieces are too long.
-pub fn pretty_array_like(parts: &[impl AsRef<str>], trailing_comma: bool) -> String {
- let list = pretty_comma_list(parts, trailing_comma);
- let mut buf = String::new();
- buf.push('(');
- if list.contains('\n') {
- buf.push('\n');
- for (i, line) in list.lines().enumerate() {
- if i > 0 {
- buf.push('\n');
- }
- buf.push_str(" ");
- buf.push_str(line);
- }
- buf.push('\n');
- } else {
- buf.push_str(&list);
+impl<T> Eq for Static<T> {}
+
+impl<T> PartialEq for Static<T> {
+ fn eq(&self, other: &Self) -> bool {
+ std::ptr::eq(self.0, other.0)
}
- buf.push(')');
- buf
}
-/// Check if the [`Option`]-wrapped L is same to R.
-pub fn option_eq<L, R>(left: Option<L>, other: R) -> bool
-where
- L: PartialEq<R>,
-{
- left.map(|v| v == other).unwrap_or(false)
+impl<T> Hash for Static<T> {
+ fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
+ state.write_usize(self.0 as *const _ as _);
+ }
}