summaryrefslogtreecommitdiff
path: root/crates/typst-library/src
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2023-11-23 16:25:49 +0100
committerLaurenz <laurmaedje@gmail.com>2023-11-24 12:30:02 +0100
commit7eebafa7837ec173a7b2064ae60fd45b5413d17c (patch)
treeb63b302b6d7747bcbb28571713745b9ca1aa83a4 /crates/typst-library/src
parent76e173b78b511b506b928c27ac360f75fa455747 (diff)
Merge `typst` and `typst-library`
Diffstat (limited to 'crates/typst-library/src')
-rw-r--r--crates/typst-library/src/compute/calc.rs949
-rw-r--r--crates/typst-library/src/compute/data.rs609
-rw-r--r--crates/typst-library/src/compute/foundations.rs236
-rw-r--r--crates/typst-library/src/compute/mod.rs20
-rw-r--r--crates/typst-library/src/compute/sys.rs24
-rw-r--r--crates/typst-library/src/layout/align.rs46
-rw-r--r--crates/typst-library/src/layout/columns.rs169
-rw-r--r--crates/typst-library/src/layout/container.rs502
-rw-r--r--crates/typst-library/src/layout/enum.rs319
-rw-r--r--crates/typst-library/src/layout/flow.rs714
-rw-r--r--crates/typst-library/src/layout/fragment.rs87
-rw-r--r--crates/typst-library/src/layout/grid.rs734
-rw-r--r--crates/typst-library/src/layout/hide.rs29
-rw-r--r--crates/typst-library/src/layout/list.rs238
-rw-r--r--crates/typst-library/src/layout/measure.rs53
-rw-r--r--crates/typst-library/src/layout/mod.rs786
-rw-r--r--crates/typst-library/src/layout/pad.rs121
-rw-r--r--crates/typst-library/src/layout/page.rs949
-rw-r--r--crates/typst-library/src/layout/par.rs1520
-rw-r--r--crates/typst-library/src/layout/place.rs124
-rw-r--r--crates/typst-library/src/layout/regions.rs144
-rw-r--r--crates/typst-library/src/layout/repeat.rs75
-rw-r--r--crates/typst-library/src/layout/spacing.rs262
-rw-r--r--crates/typst-library/src/layout/stack.rs345
-rw-r--r--crates/typst-library/src/layout/table.rs361
-rw-r--r--crates/typst-library/src/layout/terms.rs162
-rw-r--r--crates/typst-library/src/layout/transform.rs187
-rw-r--r--crates/typst-library/src/lib.rs170
-rw-r--r--crates/typst-library/src/math/accent.rs137
-rw-r--r--crates/typst-library/src/math/align.rs60
-rw-r--r--crates/typst-library/src/math/attach.rs440
-rw-r--r--crates/typst-library/src/math/cancel.rs230
-rw-r--r--crates/typst-library/src/math/class.rs38
-rw-r--r--crates/typst-library/src/math/ctx.rs335
-rw-r--r--crates/typst-library/src/math/frac.rs157
-rw-r--r--crates/typst-library/src/math/fragment.rs513
-rw-r--r--crates/typst-library/src/math/lr.rs195
-rw-r--r--crates/typst-library/src/math/matrix.rs655
-rw-r--r--crates/typst-library/src/math/mod.rs500
-rw-r--r--crates/typst-library/src/math/op.rs115
-rw-r--r--crates/typst-library/src/math/root.rs137
-rw-r--r--crates/typst-library/src/math/row.rs261
-rw-r--r--crates/typst-library/src/math/spacing.rs63
-rw-r--r--crates/typst-library/src/math/stretch.rs199
-rw-r--r--crates/typst-library/src/math/style.rs574
-rw-r--r--crates/typst-library/src/math/underover.rs315
-rw-r--r--crates/typst-library/src/meta/bibliography.rs1038
-rw-r--r--crates/typst-library/src/meta/cite.rs156
-rw-r--r--crates/typst-library/src/meta/context.rs169
-rw-r--r--crates/typst-library/src/meta/counter.rs722
-rw-r--r--crates/typst-library/src/meta/document.rs124
-rw-r--r--crates/typst-library/src/meta/figure.rs588
-rw-r--r--crates/typst-library/src/meta/footnote.rs305
-rw-r--r--crates/typst-library/src/meta/heading.rs269
-rw-r--r--crates/typst-library/src/meta/link.rs133
-rw-r--r--crates/typst-library/src/meta/metadata.rs40
-rw-r--r--crates/typst-library/src/meta/mod.rs76
-rw-r--r--crates/typst-library/src/meta/numbering.rs567
-rw-r--r--crates/typst-library/src/meta/outline.rs525
-rw-r--r--crates/typst-library/src/meta/query.rs157
-rw-r--r--crates/typst-library/src/meta/reference.rs283
-rw-r--r--crates/typst-library/src/meta/state.rs413
-rw-r--r--crates/typst-library/src/prelude.rs42
-rw-r--r--crates/typst-library/src/shared/behave.rs114
-rw-r--r--crates/typst-library/src/shared/ext.rs92
-rw-r--r--crates/typst-library/src/shared/mod.rs7
-rw-r--r--crates/typst-library/src/symbols/emoji.rs1356
-rw-r--r--crates/typst-library/src/symbols/mod.rs17
-rw-r--r--crates/typst-library/src/symbols/sym.rs873
-rw-r--r--crates/typst-library/src/text/deco.rs566
-rw-r--r--crates/typst-library/src/text/linebreak.rs262
-rw-r--r--crates/typst-library/src/text/misc.rs315
-rw-r--r--crates/typst-library/src/text/mod.rs992
-rw-r--r--crates/typst-library/src/text/quote.rs208
-rw-r--r--crates/typst-library/src/text/quotes.rs369
-rw-r--r--crates/typst-library/src/text/raw.rs835
-rw-r--r--crates/typst-library/src/text/shaping.rs1123
-rw-r--r--crates/typst-library/src/text/shift.rs223
-rw-r--r--crates/typst-library/src/visualize/image.rs271
-rw-r--r--crates/typst-library/src/visualize/line.rs86
-rw-r--r--crates/typst-library/src/visualize/mod.rs31
-rw-r--r--crates/typst-library/src/visualize/path.rs208
-rw-r--r--crates/typst-library/src/visualize/polygon.rs164
-rw-r--r--crates/typst-library/src/visualize/shape.rs547
84 files changed, 0 insertions, 29125 deletions
diff --git a/crates/typst-library/src/compute/calc.rs b/crates/typst-library/src/compute/calc.rs
deleted file mode 100644
index 6276905c..00000000
--- a/crates/typst-library/src/compute/calc.rs
+++ /dev/null
@@ -1,949 +0,0 @@
-//! Calculations and processing of numeric values.
-
-use std::cmp;
-use std::cmp::Ordering;
-use std::ops::{Div, Rem};
-
-use typst::eval::{Module, Scope};
-
-use crate::prelude::*;
-
-/// 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.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::<div_euclid>();
- scope.define_func::<rem_euclid>();
- 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", scope)
-}
-
-/// Calculates the absolute value of a numeric value.
-///
-/// ```example
-/// #calc.abs(-5) \
-/// #calc.abs(5pt - 2cm) \
-/// #calc.abs(2fr)
-/// ```
-#[func(title = "Absolute")]
-pub fn abs(
- /// The value whose absolute value to calculate.
- value: ToAbs,
-) -> Value {
- value.0
-}
-
-/// A value of which the absolute value can be taken.
-pub struct ToAbs(Value);
-
-cast! {
- ToAbs,
- v: i64 => Self(v.abs().into_value()),
- v: f64 => Self(v.abs().into_value()),
- v: Length => Self(Value::Length(v.try_abs()
- .ok_or("cannot take absolute value of this length")?)),
- v: Angle => Self(Value::Angle(v.abs())),
- v: Ratio => Self(Value::Ratio(v.abs())),
- v: Fr => Self(Value::Fraction(v.abs())),
-}
-
-/// Raises a value to some exponent.
-///
-/// ```example
-/// #calc.pow(2, 3)
-/// ```
-#[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>,
-) -> SourceResult<Num> {
- match exponent.v {
- _ if exponent.v.float() == 0.0 && base.float() == 0.0 => {
- bail!(span, "zero to the power of zero is undefined")
- }
- Num::Int(i) if i32::try_from(i).is_err() => {
- bail!(exponent.span, "exponent is too large")
- }
- Num::Float(f) if !f.is_normal() && f != 0.0 => {
- bail!(exponent.span, "exponent may not be infinite, subnormal, or NaN")
- }
- _ => {}
- };
-
- let result = match (base, exponent.v) {
- (Num::Int(a), Num::Int(b)) if b >= 0 => a
- .checked_pow(b as u32)
- .map(Num::Int)
- .ok_or("the result is too large")
- .at(span)?,
- (a, b) => Num::Float(if a.float() == std::f64::consts::E {
- b.float().exp()
- } else if a.float() == 2.0 {
- b.float().exp2()
- } else if let Num::Int(b) = b {
- a.float().powi(b as i32)
- } else {
- a.float().powf(b.float())
- }),
- };
-
- if result.float().is_nan() {
- bail!(span, "the result is not a real number")
- }
-
- Ok(result)
-}
-
-/// Raises a value to some exponent of e.
-///
-/// ```example
-/// #calc.exp(1)
-/// ```
-#[func(title = "Exponential")]
-pub fn exp(
- /// 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() => {
- bail!(exponent.span, "exponent is too large")
- }
- Num::Float(f) if !f.is_normal() && f != 0.0 => {
- bail!(exponent.span, "exponent may not be infinite, subnormal, or NaN")
- }
- _ => {}
- };
-
- let result = exponent.v.float().exp();
- if result.is_nan() {
- bail!(span, "the result is not a real number")
- }
-
- Ok(result)
-}
-
-/// Calculates the square root of a number.
-///
-/// ```example
-/// #calc.sqrt(16) \
-/// #calc.sqrt(2.5)
-/// ```
-#[func(title = "Square Root")]
-pub fn sqrt(
- /// The number whose square root to calculate. Must be non-negative.
- value: Spanned<Num>,
-) -> SourceResult<f64> {
- if value.v.float() < 0.0 {
- bail!(value.span, "cannot take square root of negative number");
- }
- Ok(value.v.float().sqrt())
-}
-
-/// Calculates the sine of an angle.
-///
-/// When called with an integer or a float, they will be interpreted as
-/// radians.
-///
-/// ```example
-/// #assert(calc.sin(90deg) == calc.sin(-270deg))
-/// #calc.sin(1.5) \
-/// #calc.sin(90deg)
-/// ```
-#[func(title = "Sine")]
-pub fn sin(
- /// The angle whose sine to calculate.
- angle: AngleLike,
-) -> f64 {
- match angle {
- AngleLike::Angle(a) => a.sin(),
- AngleLike::Int(n) => (n as f64).sin(),
- AngleLike::Float(n) => n.sin(),
- }
-}
-
-/// Calculates the cosine of an angle.
-///
-/// When called with an integer or a float, they will be interpreted as
-/// radians.
-///
-/// ```example
-/// #calc.cos(90deg) \
-/// #calc.cos(1.5) \
-/// #calc.cos(90deg)
-/// ```
-#[func(title = "Cosine")]
-pub fn cos(
- /// The angle whose cosine to calculate.
- angle: AngleLike,
-) -> f64 {
- match angle {
- AngleLike::Angle(a) => a.cos(),
- AngleLike::Int(n) => (n as f64).cos(),
- AngleLike::Float(n) => n.cos(),
- }
-}
-
-/// Calculates the tangent of an angle.
-///
-/// When called with an integer or a float, they will be interpreted as
-/// radians.
-///
-/// ```example
-/// #calc.tan(1.5) \
-/// #calc.tan(90deg)
-/// ```
-#[func(title = "Tangent")]
-pub fn tan(
- /// The angle whose tangent to calculate.
- angle: AngleLike,
-) -> f64 {
- match angle {
- AngleLike::Angle(a) => a.tan(),
- AngleLike::Int(n) => (n as f64).tan(),
- AngleLike::Float(n) => n.tan(),
- }
-}
-
-/// Calculates the arcsine of a number.
-///
-/// ```example
-/// #calc.asin(0) \
-/// #calc.asin(1)
-/// ```
-#[func(title = "Arcsine")]
-pub fn asin(
- /// The number whose arcsine to calculate. Must be between -1 and 1.
- value: Spanned<Num>,
-) -> SourceResult<Angle> {
- let val = value.v.float();
- if val < -1.0 || val > 1.0 {
- bail!(value.span, "value must be between -1 and 1");
- }
- Ok(Angle::rad(val.asin()))
-}
-
-/// Calculates the arccosine of a number.
-///
-/// ```example
-/// #calc.acos(0) \
-/// #calc.acos(1)
-/// ```
-#[func(title = "Arccosine")]
-pub fn acos(
- /// The number whose arcsine to calculate. Must be between -1 and 1.
- value: Spanned<Num>,
-) -> SourceResult<Angle> {
- let val = value.v.float();
- if val < -1.0 || val > 1.0 {
- bail!(value.span, "value must be between -1 and 1");
- }
- Ok(Angle::rad(val.acos()))
-}
-
-/// Calculates the arctangent of a number.
-///
-/// ```example
-/// #calc.atan(0) \
-/// #calc.atan(1)
-/// ```
-#[func(title = "Arctangent")]
-pub fn atan(
- /// The number whose arctangent to calculate.
- value: Num,
-) -> Angle {
- Angle::rad(value.float().atan())
-}
-
-/// Calculates the four-quadrant arctangent of a coordinate.
-///
-/// The arguments are `(x, y)`, not `(y, x)`.
-///
-/// ```example
-/// #calc.atan2(1, 1) \
-/// #calc.atan2(-2, -3)
-/// ```
-#[func(title = "Four-quadrant Arctangent")]
-pub fn atan2(
- /// The X coordinate.
- x: Num,
- /// The Y coordinate.
- y: Num,
-) -> Angle {
- Angle::rad(f64::atan2(y.float(), x.float()))
-}
-
-/// Calculates the hyperbolic sine of a hyperbolic angle.
-///
-/// ```example
-/// #calc.sinh(0) \
-/// #calc.sinh(1.5)
-/// ```
-#[func(title = "Hyperbolic Sine")]
-pub fn sinh(
- /// The hyperbolic angle whose hyperbolic sine to calculate.
- value: f64,
-) -> f64 {
- value.sinh()
-}
-
-/// Calculates the hyperbolic cosine of a hyperbolic angle.
-///
-/// ```example
-/// #calc.cosh(0) \
-/// #calc.cosh(1.5)
-/// ```
-#[func(title = "Hyperbolic Cosine")]
-pub fn cosh(
- /// The hyperbolic angle whose hyperbolic cosine to calculate.
- value: f64,
-) -> f64 {
- value.cosh()
-}
-
-/// Calculates the hyperbolic tangent of an hyperbolic angle.
-///
-/// ```example
-/// #calc.tanh(0) \
-/// #calc.tanh(1.5)
-/// ```
-#[func(title = "Hyperbolic Tangent")]
-pub fn tanh(
- /// The hyperbolic angle whose hyperbolic tangent to calculate.
- value: f64,
-) -> f64 {
- value.tanh()
-}
-
-/// Calculates the logarithm of a number.
-///
-/// If the base is not specified, the logarithm is calculated in base 10.
-///
-/// ```example
-/// #calc.log(100)
-/// ```
-#[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>,
-) -> SourceResult<f64> {
- let number = value.v.float();
- if number <= 0.0 {
- bail!(value.span, "value must be strictly positive")
- }
-
- if !base.v.is_normal() {
- bail!(base.span, "base may not be zero, NaN, infinite, or subnormal")
- }
-
- let result = if base.v == std::f64::consts::E {
- number.ln()
- } else if base.v == 2.0 {
- number.log2()
- } else if base.v == 10.0 {
- number.log10()
- } else {
- number.log(base.v)
- };
-
- if result.is_infinite() || result.is_nan() {
- bail!(span, "the result is not a real number")
- }
-
- Ok(result)
-}
-
-/// Calculates the natural logarithm of a number.
-///
-/// ```example
-/// #calc.ln(calc.e)
-/// ```
-#[func(title = "Natural Logarithm")]
-pub fn ln(
- /// 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 {
- bail!(value.span, "value must be strictly positive")
- }
-
- let result = number.ln();
- if result.is_infinite() {
- bail!(span, "result close to -inf")
- }
-
- Ok(result)
-}
-
-/// Calculates the factorial of a number.
-///
-/// ```example
-/// #calc.fact(5)
-/// ```
-#[func(title = "Factorial")]
-pub fn fact(
- /// The number whose factorial to calculate. Must be non-negative.
- number: u64,
-) -> StrResult<i64> {
- Ok(fact_impl(1, number).ok_or("the result is too large")?)
-}
-
-/// Calculates a permutation.
-///
-/// Returns the `k`-permutation of `n`, or the number of ways to choose `k`
-/// items from a set of `n` with regard to order.
-///
-/// ```example
-/// $ "perm"(n, k) &= n!/((n - k)!) \
-/// "perm"(5, 3) &= #calc.perm(5, 3) $
-/// ```
-#[func(title = "Permutation")]
-pub fn perm(
- /// The base number. Must be non-negative.
- base: u64,
- /// The number of permutations. Must be non-negative.
- numbers: u64,
-) -> StrResult<i64> {
- // By convention.
- if base < numbers {
- return Ok(0);
- }
-
- Ok(fact_impl(base - numbers + 1, base).ok_or("the result is too large")?)
-}
-
-/// Calculates the product of a range of numbers. Used to calculate
-/// permutations. Returns None if the result is larger than `i64::MAX`
-fn fact_impl(start: u64, end: u64) -> Option<i64> {
- // By convention
- if end + 1 < start {
- return Some(0);
- }
-
- let real_start: u64 = cmp::max(1, start);
- let mut count: u64 = 1;
- for i in real_start..=end {
- count = count.checked_mul(i)?;
- }
-
- count.try_into().ok()
-}
-
-/// Calculates a binomial coefficient.
-///
-/// Returns the `k`-combination of `n`, or the number of ways to choose `k`
-/// items from a set of `n` without regard to order.
-///
-/// ```example
-/// #calc.binom(10, 5)
-/// ```
-#[func(title = "Binomial")]
-pub fn binom(
- /// The upper coefficient. Must be non-negative.
- n: u64,
- /// The lower coefficient. Must be non-negative.
- k: u64,
-) -> StrResult<i64> {
- Ok(binom_impl(n, k).ok_or("the result is too large")?)
-}
-
-/// Calculates a binomial coefficient, with `n` the upper coefficient and `k`
-/// the lower coefficient. Returns `None` if the result is larger than
-/// `i64::MAX`
-fn binom_impl(n: u64, k: u64) -> Option<i64> {
- if k > n {
- return Some(0);
- }
-
- // By symmetry
- let real_k = cmp::min(n - k, k);
- if real_k == 0 {
- return Some(1);
- }
-
- let mut result: u64 = 1;
- for i in 0..real_k {
- result = result.checked_mul(n - i)?.checked_div(i + 1)?;
- }
-
- result.try_into().ok()
-}
-
-/// Calculates the greatest common divisor of two integers.
-///
-/// ```example
-/// #calc.gcd(7, 42)
-/// ```
-#[func(title = "Greatest Common Divisor")]
-pub fn gcd(
- /// The first integer.
- a: i64,
- /// The second integer.
- b: i64,
-) -> i64 {
- let (mut a, mut b) = (a, b);
- while b != 0 {
- let temp = b;
- b = a % b;
- a = temp;
- }
-
- a.abs()
-}
-
-/// Calculates the least common multiple of two integers.
-///
-/// ```example
-/// #calc.lcm(96, 13)
-/// ```
-#[func(title = "Least Common Multiple")]
-pub fn lcm(
- /// The first integer.
- a: i64,
- /// The second integer.
- b: i64,
-) -> StrResult<i64> {
- if a == b {
- return Ok(a.abs());
- }
-
- Ok(a.checked_div(gcd(a, b))
- .and_then(|gcd| gcd.checked_mul(b))
- .map(|v| v.abs())
- .ok_or("the return value is too large")?)
-}
-
-/// Rounds a number down to the nearest integer.
-///
-/// If the number is already an integer, it is returned unchanged.
-///
-/// ```example
-/// #assert(calc.floor(3.14) == 3)
-/// #assert(calc.floor(3) == 3)
-/// #calc.floor(500.1)
-/// ```
-#[func]
-pub fn floor(
- /// The number to round down.
- value: Num,
-) -> i64 {
- match value {
- Num::Int(n) => n,
- Num::Float(n) => n.floor() as i64,
- }
-}
-
-/// Rounds a number up to the nearest integer.
-///
-/// If the number is already an integer, it is returned unchanged.
-///
-/// ```example
-/// #assert(calc.ceil(3.14) == 4)
-/// #assert(calc.ceil(3) == 3)
-/// #calc.ceil(500.1)
-/// ```
-#[func]
-pub fn ceil(
- /// The number to round up.
- value: Num,
-) -> i64 {
- match value {
- Num::Int(n) => n,
- Num::Float(n) => n.ceil() as i64,
- }
-}
-
-/// Returns the integer part of a number.
-///
-/// If the number is already an integer, it is returned unchanged.
-///
-/// ```example
-/// #assert(calc.trunc(3) == 3)
-/// #assert(calc.trunc(-3.7) == -3)
-/// #calc.trunc(15.9)
-/// ```
-#[func(title = "Truncate")]
-pub fn trunc(
- /// The number to truncate.
- value: Num,
-) -> i64 {
- match value {
- Num::Int(n) => n,
- Num::Float(n) => n.trunc() as i64,
- }
-}
-
-/// Returns the fractional part of a number.
-///
-/// If the number is an integer, returns `0`.
-///
-/// ```example
-/// #assert(calc.fract(3) == 0)
-/// #calc.fract(-3.1)
-/// ```
-#[func(title = "Fractional")]
-pub fn fract(
- /// The number to truncate.
- value: Num,
-) -> Num {
- match value {
- Num::Int(_) => Num::Int(0),
- Num::Float(n) => Num::Float(n.fract()),
- }
-}
-
-/// Rounds a number to the nearest integer.
-///
-/// Optionally, a number of decimal places can be specified.
-///
-/// ```example
-/// #assert(calc.round(3.14) == 3)
-/// #assert(calc.round(3.5) == 4)
-/// #calc.round(3.1415, digits: 2)
-/// ```
-#[func]
-pub fn round(
- /// The number to round.
- value: Num,
- /// The number of decimal places.
- #[named]
- #[default(0)]
- digits: i64,
-) -> Num {
- match value {
- Num::Int(n) if digits == 0 => Num::Int(n),
- _ => {
- let n = value.float();
- let factor = 10.0_f64.powi(digits as i32);
- Num::Float((n * factor).round() / factor)
- }
- }
-}
-
-/// Clamps a number between a minimum and maximum value.
-///
-/// ```example
-/// #assert(calc.clamp(5, 0, 10) == 5)
-/// #assert(calc.clamp(5, 6, 10) == 6)
-/// #calc.clamp(5, 0, 4)
-/// ```
-#[func]
-pub fn clamp(
- /// The number to clamp.
- value: Num,
- /// The inclusive minimum value.
- min: Num,
- /// The inclusive maximum value.
- max: Spanned<Num>,
-) -> SourceResult<Num> {
- if max.v.float() < min.float() {
- bail!(max.span, "max must be greater than or equal to min")
- }
- Ok(value.apply3(min, max.v, i64::clamp, f64::clamp))
-}
-
-/// Determines the minimum of a sequence of values.
-///
-/// ```example
-/// #calc.min(1, -3, -5, 20, 3, 6) \
-/// #calc.min("typst", "in", "beta")
-/// ```
-#[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>>,
-) -> SourceResult<Value> {
- minmax(span, values, Ordering::Less)
-}
-
-/// Determines the maximum of a sequence of values.
-///
-/// ```example
-/// #calc.max(1, -3, -5, 20, 3, 6) \
-/// #calc.max("typst", "in", "beta")
-/// ```
-#[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>>,
-) -> SourceResult<Value> {
- minmax(span, values, Ordering::Greater)
-}
-
-/// Find the minimum or maximum of a sequence of values.
-fn minmax(
- span: Span,
- values: Vec<Spanned<Value>>,
- goal: Ordering,
-) -> SourceResult<Value> {
- let mut iter = values.into_iter();
- let Some(Spanned { v: mut extremum, .. }) = iter.next() else {
- bail!(span, "expected at least one value");
- };
-
- for Spanned { v, span } in iter {
- let ordering = typst::eval::ops::compare(&v, &extremum).at(span)?;
- if ordering == goal {
- extremum = v;
- }
- }
-
- Ok(extremum)
-}
-
-/// Determines whether an integer is even.
-///
-/// ```example
-/// #calc.even(4) \
-/// #calc.even(5) \
-/// #range(10).filter(calc.even)
-/// ```
-#[func]
-pub fn even(
- /// The number to check for evenness.
- value: i64,
-) -> bool {
- value % 2 == 0
-}
-
-/// Determines whether an integer is odd.
-///
-/// ```example
-/// #calc.odd(4) \
-/// #calc.odd(5) \
-/// #range(10).filter(calc.odd)
-/// ```
-#[func]
-pub fn odd(
- /// The number to check for oddness.
- value: i64,
-) -> bool {
- value % 2 != 0
-}
-
-/// Calculates the remainder of two numbers.
-///
-/// The value `calc.rem(x, y)` always has the same sign as `x`, and is smaller
-/// in magnitude than `y`.
-///
-/// ```example
-/// #calc.rem(7, 3) \
-/// #calc.rem(7, -3) \
-/// #calc.rem(-7, 3) \
-/// #calc.rem(-7, -3) \
-/// #calc.rem(1.75, 0.5)
-/// ```
-#[func(title = "Remainder")]
-pub fn rem(
- /// The dividend of the remainder.
- dividend: Num,
- /// The divisor of the remainder.
- divisor: Spanned<Num>,
-) -> SourceResult<Num> {
- if divisor.v.float() == 0.0 {
- bail!(divisor.span, "divisor must not be zero");
- }
- Ok(dividend.apply2(divisor.v, Rem::rem, Rem::rem))
-}
-
-/// Performs euclidean division of two numbers.
-///
-/// The result of this computation is that of a division rounded to the integer
-/// `{n}` such that the dividend is greater than or equal to `{n}` times the divisor.
-///
-/// ```example
-/// #calc.div-euclid(7, 3) \
-/// #calc.div-euclid(7, -3) \
-/// #calc.div-euclid(-7, 3) \
-/// #calc.div-euclid(-7, -3) \
-/// #calc.div-euclid(1.75, 0.5)
-/// ```
-#[func(title = "Euclidean Division")]
-pub fn div_euclid(
- /// The dividend of the division.
- dividend: Num,
- /// The divisor of the division.
- divisor: Spanned<Num>,
-) -> SourceResult<Num> {
- if divisor.v.float() == 0.0 {
- bail!(divisor.span, "divisor must not be zero");
- }
- Ok(dividend.apply2(divisor.v, i64::div_euclid, f64::div_euclid))
-}
-
-/// This calculates the least nonnegative remainder of a division.
-///
-/// Warning: Due to a floating point round-off error, the remainder may equal the absolute
-/// value of the divisor if the dividend is much smaller in magnitude than the divisor
-/// and the dividend is negative. This only applies for floating point inputs.
-///
-/// ```example
-/// #calc.rem-euclid(7, 3) \
-/// #calc.rem-euclid(7, -3) \
-/// #calc.rem-euclid(-7, 3) \
-/// #calc.rem-euclid(-7, -3) \
-/// #calc.rem(1.75, 0.5)
-/// ```
-#[func(title = "Euclidean Remainder")]
-pub fn rem_euclid(
- /// The dividend of the remainder.
- dividend: Num,
- /// The divisor of the remainder.
- divisor: Spanned<Num>,
-) -> SourceResult<Num> {
- if divisor.v.float() == 0.0 {
- bail!(divisor.span, "divisor must not be zero");
- }
- Ok(dividend.apply2(divisor.v, i64::rem_euclid, f64::rem_euclid))
-}
-
-/// Calculates the quotient (floored division) of two numbers.
-///
-/// ```example
-/// $ "quo"(a, b) &= floor(a/b) \
-/// "quo"(14, 5) &= #calc.quo(14, 5) \
-/// "quo"(3.46, 0.5) &= #calc.quo(3.46, 0.5) $
-/// ```
-#[func(title = "Quotient")]
-pub fn quo(
- /// The dividend of the quotient.
- dividend: Num,
- /// The divisor of the quotient.
- divisor: Spanned<Num>,
-) -> SourceResult<i64> {
- if divisor.v.float() == 0.0 {
- bail!(divisor.span, "divisor must not be zero");
- }
-
- Ok(floor(dividend.apply2(divisor.v, Div::div, Div::div)))
-}
-
-/// A value which can be passed to functions that work with integers and floats.
-#[derive(Debug, Copy, Clone)]
-pub enum Num {
- Int(i64),
- Float(f64),
-}
-
-impl Num {
- fn apply2(
- self,
- other: Self,
- int: impl FnOnce(i64, i64) -> i64,
- float: impl FnOnce(f64, f64) -> f64,
- ) -> Num {
- match (self, other) {
- (Self::Int(a), Self::Int(b)) => Num::Int(int(a, b)),
- (a, b) => Num::Float(float(a.float(), b.float())),
- }
- }
-
- fn apply3(
- self,
- other: Self,
- third: Self,
- int: impl FnOnce(i64, i64, i64) -> i64,
- float: impl FnOnce(f64, f64, f64) -> f64,
- ) -> Num {
- match (self, other, third) {
- (Self::Int(a), Self::Int(b), Self::Int(c)) => Num::Int(int(a, b, c)),
- (a, b, c) => Num::Float(float(a.float(), b.float(), c.float())),
- }
- }
-
- fn float(self) -> f64 {
- match self {
- Self::Int(v) => v as f64,
- Self::Float(v) => v,
- }
- }
-}
-
-cast! {
- Num,
- self => match self {
- Self::Int(v) => v.into_value(),
- Self::Float(v) => v.into_value(),
- },
- v: i64 => Self::Int(v),
- v: f64 => Self::Float(v),
-}
-
-/// A value that can be passed to a trigonometric function.
-pub enum AngleLike {
- Int(i64),
- Float(f64),
- Angle(Angle),
-}
-
-cast! {
- AngleLike,
- v: i64 => Self::Int(v),
- v: f64 => Self::Float(v),
- v: Angle => Self::Angle(v),
-}
diff --git a/crates/typst-library/src/compute/data.rs b/crates/typst-library/src/compute/data.rs
deleted file mode 100644
index e4767ebf..00000000
--- a/crates/typst-library/src/compute/data.rs
+++ /dev/null
@@ -1,609 +0,0 @@
-use typst::diag::{format_xml_like_error, FileError};
-use typst::eval::Bytes;
-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]($str).
-///
-/// If you specify `{encoding: none}`, this returns raw [bytes]($bytes) instead.
-///
-/// # Example
-/// ```example
-/// An example for a HTML file: \
-/// #let text = read("data.html")
-/// #raw(text, lang: "html")
-///
-/// Raw bytes:
-/// #read("tiger.jpg", encoding: none)
-/// ```
-#[func]
-pub fn read(
- /// The virtual machine.
- vm: &mut Vm,
- /// Path to a file.
- path: Spanned<EcoString>,
- /// The encoding to read the file with.
- ///
- /// If set to `{none}`, this function returns raw bytes.
- #[named]
- #[default(Some(Encoding::Utf8))]
- encoding: Option<Encoding>,
-) -> SourceResult<Readable> {
- let Spanned { v: path, span } = path;
- let id = vm.resolve_path(&path).at(span)?;
- let data = vm.world().file(id).at(span)?;
- Ok(match encoding {
- None => Readable::Bytes(data),
- Some(Encoding::Utf8) => Readable::Str(
- std::str::from_utf8(&data)
- .map_err(|_| "file is not valid utf-8")
- .at(span)?
- .into(),
- ),
- })
-}
-
-/// An encoding of a file.
-#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)]
-pub enum Encoding {
- /// The Unicode UTF-8 encoding.
- Utf8,
-}
-
-/// A value that can be read from a file.
-#[derive(Debug, Clone, PartialEq, Hash)]
-pub enum Readable {
- /// A decoded string.
- Str(Str),
- /// Raw bytes.
- Bytes(Bytes),
-}
-
-impl Readable {
- fn as_slice(&self) -> &[u8] {
- match self {
- Readable::Bytes(v) => v,
- Readable::Str(v) => v.as_bytes(),
- }
- }
-}
-
-cast! {
- Readable,
- self => match self {
- Self::Str(v) => v.into_value(),
- Self::Bytes(v) => v.into_value(),
- },
- v: Str => Self::Str(v),
- v: Bytes => Self::Bytes(v),
-}
-
-impl From<Readable> for Bytes {
- fn from(value: Readable) -> Self {
- match value {
- Readable::Bytes(v) => v,
- Readable::Str(v) => v.as_bytes().into(),
- }
- }
-}
-
-/// Reads structured data from a CSV file.
-///
-/// The CSV file will be read and parsed into a 2-dimensional array of strings:
-/// Each row in the CSV file will be represented as an array of strings, and all
-/// rows will be collected into a single array. Header rows will not be
-/// stripped.
-///
-/// # Example
-/// ```example
-/// #let results = csv("data.csv")
-///
-/// #table(
-/// columns: 2,
-/// [*Condition*], [*Result*],
-/// ..results.flatten(),
-/// )
-/// ```
-#[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.
- /// Must be a single ASCII character.
- #[named]
- #[default]
- delimiter: Delimiter,
-) -> SourceResult<Array> {
- let Spanned { v: path, span } = path;
- let id = vm.resolve_path(&path).at(span)?;
- let data = vm.world().file(id).at(span)?;
- self::csv::decode(Spanned::new(Readable::Bytes(data), span), delimiter)
-}
-
-#[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)
- }
-}
-
-/// The delimiter to use when parsing CSV files.
-pub struct Delimiter(char);
-
-impl Default for Delimiter {
- fn default() -> Self {
- Self(',')
- }
-}
-
-cast! {
- Delimiter,
- self => self.0.into_value(),
- v: EcoString => {
- let mut chars = v.chars();
- let first = chars.next().ok_or("delimiter must not be empty")?;
- if chars.next().is_some() {
- bail!("delimiter must be a single character");
- }
-
- if !first.is_ascii() {
- bail!("delimiter must be an ASCII character");
- }
-
- Self(first)
- },
-}
-
-/// Format the user-facing CSV error message.
-fn format_csv_error(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, .. } => {
- eco_format!(
- "failed to parse CSV (found {len} instead of \
- {expected_len} fields in line {line})"
- )
- }
- _ => eco_format!("failed to parse CSV ({err})"),
- }
-}
-
-/// Reads structured data from a JSON file.
-///
-/// The file must contain a valid JSON object or array. JSON objects will be
-/// converted into Typst dictionaries, and JSON arrays will be converted into
-/// Typst arrays. Strings and booleans will be converted into the Typst
-/// equivalents, `null` will be converted into `{none}`, and numbers will be
-/// converted to floats or integers depending on whether they are whole numbers.
-///
-/// The function returns a dictionary or an array, depending on the JSON file.
-///
-/// The JSON files in the example contain objects with the keys `temperature`,
-/// `unit`, and `weather`.
-///
-/// # Example
-/// ```example
-/// #let forecast(day) = block[
-/// #box(square(
-/// width: 2cm,
-/// inset: 8pt,
-/// fill: if day.weather == "sunny" {
-/// yellow
-/// } else {
-/// aqua
-/// },
-/// align(
-/// bottom + right,
-/// strong(day.weather),
-/// ),
-/// ))
-/// #h(6pt)
-/// #set text(22pt, baseline: -8pt)
-/// #day.temperature °#day.unit
-/// ]
-///
-/// #forecast(json("monday.json"))
-/// #forecast(json("tuesday.json"))
-/// ```
-#[func(scope, title = "JSON")]
-pub fn json(
- /// 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))
-}
-
-#[scope]
-impl json {
- /// Reads structured data from a JSON string/bytes.
- #[func(title = "Decode JSON")]
- pub fn decode(
- /// JSON data.
- data: Spanned<Readable>,
- ) -> SourceResult<Value> {
- let Spanned { v: data, span } = data;
- serde_json::from_slice(data.as_slice())
- .map_err(|err| eco_format!("failed to parse JSON ({err})"))
- .at(span)
- }
-
- /// Encodes structured data into a JSON string.
- #[func(title = "Encode JSON")]
- pub fn encode(
- /// Value to be encoded.
- value: Spanned<Value>,
- /// Whether to pretty print the JSON with newlines and indentation.
- #[named]
- #[default(true)]
- pretty: bool,
- ) -> SourceResult<Str> {
- let Spanned { v: value, span } = value;
- if pretty {
- serde_json::to_string_pretty(&value)
- } else {
- serde_json::to_string(&value)
- }
- .map(|v| v.into())
- .map_err(|err| eco_format!("failed to encode value as JSON ({err})"))
- .at(span)
- }
-}
-
-/// Reads structured data from a TOML file.
-///
-/// The file must contain a valid TOML table. TOML tables will be converted into
-/// Typst dictionaries, and TOML arrays will be converted into Typst arrays.
-/// Strings, booleans and datetimes will be converted into the Typst equivalents
-/// and numbers will be converted to floats or integers depending on whether
-/// they are whole numbers.
-///
-/// The TOML file in the example consists of a table with the keys `title`,
-/// `version`, and `authors`.
-///
-/// # Example
-/// ```example
-/// #let details = toml("details.toml")
-///
-/// Title: #details.title \
-/// Version: #details.version \
-/// Authors: #(details.authors
-/// .join(", ", last: " and "))
-/// ```
-#[func(scope, title = "TOML")]
-pub fn toml(
- /// 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))
-}
-
-#[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.
- #[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 {
- 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();
- eco_format!(
- "failed to parse TOML ({} at line {line} column {column})",
- error.message(),
- )
- } else {
- eco_format!("failed to parse TOML ({})", error.message())
- }
-}
-
-/// Reads structured data from a YAML file.
-///
-/// The file must contain a valid YAML object or array. YAML mappings will be
-/// converted into Typst dictionaries, and YAML sequences will be converted into
-/// Typst arrays. Strings and booleans will be converted into the Typst
-/// equivalents, null-values (`null`, `~` or empty ``) will be converted into
-/// `{none}`, and numbers will be converted to floats or integers depending on
-/// whether they are whole numbers. Custom YAML tags are ignored, though the
-/// loaded value will still be present.
-///
-/// The YAML files in the example contain objects with authors as keys,
-/// each with a sequence of their own submapping with the keys
-/// "title" and "published"
-///
-/// # Example
-/// ```example
-/// #let bookshelf(contents) = {
-/// for (author, works) in contents {
-/// author
-/// for work in works [
-/// - #work.title (#work.published)
-/// ]
-/// }
-/// }
-///
-/// #bookshelf(
-/// yaml("scifi-authors.yaml")
-/// )
-/// ```
-#[func(scope, title = "YAML")]
-pub fn yaml(
- /// 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))
-}
-
-#[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.
- #[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.
-///
-/// The file must contain a valid cbor serialization. Mappings will be
-/// converted into Typst dictionaries, and sequences will be converted into
-/// Typst arrays. Strings and booleans will be converted into the Typst
-/// equivalents, null-values (`null`, `~` or empty ``) will be converted into
-/// `{none}`, and numbers will be converted to floats or integers depending on
-/// whether they are whole numbers.
-#[func(scope, title = "CBOR")]
-pub fn cbor(
- /// 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))
-}
-
-#[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.
- #[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.
-///
-/// The XML file is parsed into an array of dictionaries and strings. XML nodes
-/// can be elements or strings. Elements are represented as dictionaries with
-/// the the following keys:
-///
-/// - `tag`: The name of the element as a string.
-/// - `attrs`: A dictionary of the element's attributes as strings.
-/// - `children`: An array of the element's child nodes.
-///
-/// The XML file in the example contains a root `news` tag with multiple
-/// `article` tags. Each article has a `title`, `author`, and `content` tag. The
-/// `content` tag contains one or more paragraphs, which are represented as `p`
-/// tags.
-///
-/// # Example
-/// ```example
-/// #let find-child(elem, tag) = {
-/// elem.children
-/// .find(e => "tag" in e and e.tag == tag)
-/// }
-///
-/// #let article(elem) = {
-/// let title = find-child(elem, "title")
-/// let author = find-child(elem, "author")
-/// let pars = find-child(elem, "content")
-///
-/// heading(title.children.first())
-/// text(10pt, weight: "medium")[
-/// Published by
-/// #author.children.first()
-/// ]
-///
-/// for p in pars.children {
-/// if (type(p) == "dictionary") {
-/// parbreak()
-/// p.children.first()
-/// }
-/// }
-/// }
-///
-/// #let data = xml("example.xml")
-/// #for elem in data.first().children {
-/// if (type(elem) == "dictionary") {
-/// article(elem)
-/// }
-/// }
-/// ```
-#[func(scope, title = "XML")]
-pub fn xml(
- /// 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))
-}
-
-#[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.
-fn convert_xml(node: roxmltree::Node) -> Value {
- if node.is_text() {
- return node.text().unwrap_or_default().into_value();
- }
-
- let children: Array = node.children().map(convert_xml).collect();
- if node.is_root() {
- return Value::Array(children);
- }
-
- let tag: Str = node.tag_name().name().into();
- let attrs: Dict = node
- .attributes()
- .map(|attr| (attr.name().into(), attr.value().into_value()))
- .collect();
-
- Value::Dict(dict! {
- "tag" => tag,
- "attrs" => attrs,
- "children" => children,
- })
-}
-
-/// Format the user-facing XML error message.
-fn format_xml_error(error: roxmltree::Error) -> EcoString {
- format_xml_like_error("XML", error)
-}
diff --git a/crates/typst-library/src/compute/foundations.rs b/crates/typst-library/src/compute/foundations.rs
deleted file mode 100644
index fef00269..00000000
--- a/crates/typst-library/src/compute/foundations.rs
+++ /dev/null
@@ -1,236 +0,0 @@
-use typst::eval::{
- Datetime, Duration, EvalMode, Module, Never, NoneValue, Plugin, Regex, Repr, Version,
-};
-
-use crate::prelude::*;
-
-/// 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::<Version>();
- 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.
-///
-/// When inserted into content, most values are displayed as this representation
-/// in monospace with syntax-highlighting. The exceptions are `{none}`,
-/// integers, floats, strings, content, and functions.
-///
-/// **Note:** This function is for debugging purposes. Its output should not be
-/// considered stable and may change at any time!
-///
-/// # Example
-/// ```example
-/// #none vs #repr(none) \
-/// #"hello" vs #repr("hello") \
-/// #(1, 2) vs #repr((1, 2)) \
-/// #[*Hi*] vs #repr([*Hi*])
-/// ```
-#[func(title = "Representation")]
-pub fn repr(
- /// The value whose string representation to produce.
- value: Value,
-) -> Str {
- value.repr().into()
-}
-
-/// Fails with an error.
-///
-/// Arguments are displayed to the user (not rendered in the document) as
-/// strings, converting with `repr` if necessary.
-///
-/// # Example
-/// The code below produces the error `panicked with: "this is wrong"`.
-/// ```typ
-/// #panic("this is wrong")
-/// ```
-#[func(keywords = ["error"])]
-pub fn panic(
- /// The values to panic with and display to the user.
- #[variadic]
- values: Vec<Value>,
-) -> StrResult<Never> {
- let mut msg = EcoString::from("panicked");
- if !values.is_empty() {
- msg.push_str(" with: ");
- for (i, value) in values.iter().enumerate() {
- if i > 0 {
- msg.push_str(", ");
- }
- msg.push_str(&value.repr());
- }
- }
- Err(msg)
-}
-
-/// Ensures that a condition is fulfilled.
-///
-/// Fails with an error if the condition is not fulfilled. Does not
-/// produce any output in the document.
-///
-/// If you wish to test equality between two values, see
-/// [`assert.eq`]($assert.eq) and [`assert.ne`]($assert.ne).
-///
-/// # Example
-/// ```typ
-/// #assert(1 < 2, message: "math broke")
-/// ```
-#[func(scope)]
-pub fn assert(
- /// The condition that must be true for the assertion to pass.
- condition: bool,
- /// The error message when the assertion fails.
- #[named]
- message: Option<EcoString>,
-) -> StrResult<NoneValue> {
- if !condition {
- if let Some(message) = message {
- bail!("assertion failed: {message}");
- } else {
- bail!("assertion failed");
- }
- }
- Ok(NoneValue)
-}
-
-#[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 {} was not equal to {}",
- left.repr(),
- right.repr()
- );
- }
- }
- 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.
- ///
- /// ```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 {} was equal to {}",
- left.repr(),
- right.repr()
- );
- }
- }
- Ok(NoneValue)
- }
-}
-
-/// Evaluates a string as Typst code.
-///
-/// This function should only be used as a last resort.
-///
-/// # Example
-/// ```example
-/// #eval("1 + 1") \
-/// #eval("(1, 2, 3, 4)").len() \
-/// #eval("*Markup!*", mode: "markup") \
-/// ```
-#[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.
- source: Spanned<String>,
- /// The syntactical mode in which the string is parsed.
- ///
- /// ```example
- /// #eval("= Heading", mode: "markup")
- /// #eval("1_2^3", mode: "math")
- /// ```
- #[named]
- #[default(EvalMode::Code)]
- mode: EvalMode,
- /// A scope of definitions that are made available.
- ///
- /// ```example
- /// #eval("x + 1", scope: (x: 2)) \
- /// #eval(
- /// "abc/xyz",
- /// mode: "math",
- /// scope: (
- /// abc: $a + b + c$,
- /// xyz: $x + y + z$,
- /// ),
- /// )
- /// ```
- #[named]
- #[default]
- scope: Dict,
-) -> SourceResult<Value> {
- let Spanned { v: text, span } = source;
- let dict = scope;
- let mut scope = Scope::new();
- for (key, value) in dict {
- scope.define(key, value);
- }
- typst::eval::eval_string(vm.world(), &text, span, mode, scope)
-}
diff --git a/crates/typst-library/src/compute/mod.rs b/crates/typst-library/src/compute/mod.rs
deleted file mode 100644
index f1af24c5..00000000
--- a/crates/typst-library/src/compute/mod.rs
+++ /dev/null
@@ -1,20 +0,0 @@
-//! Computational functions.
-
-pub mod calc;
-pub mod sys;
-
-mod data;
-mod foundations;
-
-pub use self::data::*;
-pub use self::foundations::*;
-
-use crate::prelude::*;
-
-/// Hook up all compute definitions.
-pub(super) fn define(global: &mut Scope) {
- self::foundations::define(global);
- self::data::define(global);
- self::calc::define(global);
- self::sys::define(global);
-}
diff --git a/crates/typst-library/src/compute/sys.rs b/crates/typst-library/src/compute/sys.rs
deleted file mode 100644
index 6404e625..00000000
--- a/crates/typst-library/src/compute/sys.rs
+++ /dev/null
@@ -1,24 +0,0 @@
-//! System-related things.
-
-use typst::eval::{Module, Scope, Version};
-
-/// Hook up all calculation definitions.
-pub(super) fn define(global: &mut Scope) {
- global.category("sys");
- global.define_module(module());
-}
-
-/// A module with system-related things.
-fn module() -> Module {
- let mut scope = Scope::deduplicating();
- scope.category("sys");
- scope.define(
- "version",
- Version::from_iter([
- env!("CARGO_PKG_VERSION_MAJOR").parse::<u32>().unwrap(),
- env!("CARGO_PKG_VERSION_MINOR").parse::<u32>().unwrap(),
- env!("CARGO_PKG_VERSION_PATCH").parse::<u32>().unwrap(),
- ]),
- );
- Module::new("sys", scope)
-}
diff --git a/crates/typst-library/src/layout/align.rs b/crates/typst-library/src/layout/align.rs
deleted file mode 100644
index 9c18266d..00000000
--- a/crates/typst-library/src/layout/align.rs
+++ /dev/null
@@ -1,46 +0,0 @@
-use crate::prelude::*;
-
-/// Aligns content horizontally and vertically.
-///
-/// # Example
-/// ```example
-/// #set align(center)
-///
-/// Centered text, a sight to see \
-/// In perfect balance, visually \
-/// Not left nor right, it stands alone \
-/// A work of art, a visual throne
-/// ```
-#[elem(Show)]
-pub struct AlignElem {
- /// The [alignment]($alignment) along both axes.
- ///
- /// ```example
- /// #set page(height: 6cm)
- /// #set text(lang: "ar")
- ///
- /// مثال
- /// #align(
- /// end + horizon,
- /// rect(inset: 12pt)[ركن]
- /// )
- /// ```
- #[positional]
- #[fold]
- #[default]
- pub alignment: Align,
-
- /// The content to align.
- #[required]
- pub body: Content,
-}
-
-impl Show for AlignElem {
- #[tracing::instrument(name = "AlignElem::show", skip_all)]
- fn show(&self, _: &mut Vt, styles: StyleChain) -> SourceResult<Content> {
- Ok(self
- .body()
- .clone()
- .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
deleted file mode 100644
index bf111506..00000000
--- a/crates/typst-library/src/layout/columns.rs
+++ /dev/null
@@ -1,169 +0,0 @@
-use crate::prelude::*;
-use crate::text::TextElem;
-
-/// Separates a region into multiple equally sized columns.
-///
-/// The `column` function allows to separate the interior of any container into
-/// multiple columns. It will not equalize the height of the columns, instead,
-/// the columns will take up the height of their container or the remaining
-/// height on the page. The columns function can break across pages if
-/// necessary.
-///
-/// If you need to insert columns across your whole document, you can use the
-/// [`{page}` function's `columns` parameter]($page.columns) instead.
-///
-/// # Example
-/// ```example
-/// = Towards Advanced Deep Learning
-///
-/// #box(height: 68pt,
-/// columns(2, gutter: 11pt)[
-/// #set par(justify: true)
-/// This research was funded by the
-/// National Academy of Sciences.
-/// NAoS provided support for field
-/// tests and interviews with a
-/// grant of up to USD 40.000 for a
-/// period of 6 months.
-/// ]
-/// )
-///
-/// In recent years, deep learning has
-/// increasingly been used to solve a
-/// variety of problems.
-/// ```
-#[elem(Layout)]
-pub struct ColumnsElem {
- /// The number of columns.
- #[positional]
- #[default(NonZeroUsize::new(2).unwrap())]
- pub count: NonZeroUsize,
-
- /// The size of the gutter space between each column.
- #[resolve]
- #[default(Ratio::new(0.04).into())]
- pub gutter: Rel<Length>,
-
- /// The content that should be layouted into the columns.
- #[required]
- pub body: Content,
-}
-
-impl Layout for ColumnsElem {
- #[tracing::instrument(name = "ColumnsElem::layout", skip_all)]
- fn layout(
- &self,
- vt: &mut Vt,
- styles: StyleChain,
- regions: Regions,
- ) -> SourceResult<Fragment> {
- let body = self.body();
-
- // Separating the infinite space into infinite columns does not make
- // much sense.
- if !regions.size.x.is_finite() {
- return body.layout(vt, styles, regions);
- }
-
- // Determine the width of the gutter and each column.
- let columns = self.count(styles).get();
- let gutter = self.gutter(styles).relative_to(regions.base().x);
- let width = (regions.size.x - gutter * (columns - 1) as f64) / columns as f64;
-
- let backlog: Vec<_> = std::iter::once(&regions.size.y)
- .chain(regions.backlog)
- .flat_map(|&height| std::iter::repeat(height).take(columns))
- .skip(1)
- .collect();
-
- // Create the pod regions.
- let pod = Regions {
- size: Size::new(width, regions.size.y),
- full: regions.full,
- backlog: &backlog,
- last: regions.last,
- expand: Axes::new(true, regions.expand.y),
- root: regions.root,
- };
-
- // Layout the children.
- let mut frames = body.layout(vt, styles, pod)?.into_iter();
- let mut finished = vec![];
-
- let dir = TextElem::dir_in(styles);
- let total_regions = (frames.len() as f32 / columns as f32).ceil() as usize;
-
- // Stitch together the columns for each region.
- for region in regions.iter().take(total_regions) {
- // The height should be the parent height if we should expand.
- // Otherwise its the maximum column height for the frame. In that
- // case, the frame is first created with zero height and then
- // resized.
- let height = if regions.expand.y { region.y } else { Abs::zero() };
- let mut output = Frame::hard(Size::new(regions.size.x, height));
- let mut cursor = Abs::zero();
-
- for _ in 0..columns {
- let Some(frame) = frames.next() else { break };
- if !regions.expand.y {
- output.size_mut().y.set_max(frame.height());
- }
-
- let width = frame.width();
- let x = if dir == Dir::LTR {
- cursor
- } else {
- regions.size.x - cursor - width
- };
-
- output.push_frame(Point::with_x(x), frame);
- cursor += width + gutter;
- }
-
- finished.push(output);
- }
-
- Ok(Fragment::frames(finished))
- }
-}
-
-/// Forces a column break.
-///
-/// 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
-/// #set page(columns: 2)
-/// Preliminary findings from our
-/// ongoing research project have
-/// revealed a hitherto unknown
-/// phenomenon of extraordinary
-/// significance.
-///
-/// #colbreak()
-/// Through rigorous experimentation
-/// and analysis, we have discovered
-/// a hitherto uncharacterized process
-/// that defies our current
-/// understanding of the fundamental
-/// laws of nature.
-/// ```
-#[elem(title = "Column Break", Behave)]
-pub struct ColbreakElem {
- /// If `{true}`, the column break is skipped if the current column is
- /// already empty.
- #[default(false)]
- pub weak: bool,
-}
-
-impl Behave for ColbreakElem {
- fn behaviour(&self) -> Behaviour {
- if self.weak(StyleChain::default()) {
- Behaviour::Weak(1)
- } else {
- Behaviour::Destructive
- }
- }
-}
diff --git a/crates/typst-library/src/layout/container.rs b/crates/typst-library/src/layout/container.rs
deleted file mode 100644
index 9268f8df..00000000
--- a/crates/typst-library/src/layout/container.rs
+++ /dev/null
@@ -1,502 +0,0 @@
-use typst::eval::AutoValue;
-
-use crate::layout::{Spacing, VElem};
-use crate::prelude::*;
-
-/// An inline-level container that sizes content.
-///
-/// All elements except inline math, text, and boxes are block-level and cannot
-/// occur inside of a paragraph. The box function can be used to integrate such
-/// elements into a paragraph. Boxes take the size of their contents by default
-/// but can also be sized explicitly.
-///
-/// # Example
-/// ```example
-/// Refer to the docs
-/// #box(
-/// height: 9pt,
-/// image("docs.svg")
-/// )
-/// for more information.
-/// ```
-#[elem(Layout)]
-pub struct BoxElem {
- /// The width of the box.
- ///
- /// 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,
- /// and more might be added in the future.
- ///
- /// ```example
- /// Line in #box(width: 1fr, line(length: 100%)) between.
- /// ```
- pub width: Sizing,
-
- /// The height of the box.
- pub height: Smart<Rel<Length>>,
-
- /// An amount to shift the box's baseline by.
- ///
- /// ```example
- /// Image: #box(baseline: 40%, image("tiger.jpg", width: 2cm)).
- /// ```
- #[resolve]
- pub baseline: Rel<Length>,
-
- /// The box's background color. See the
- /// [rectangle's documentation]($rect.fill) for more details.
- pub fill: Option<Paint>,
-
- /// The box's border color. See the
- /// [rectangle's documentation]($rect.stroke) for more details.
- #[resolve]
- #[fold]
- pub stroke: Sides<Option<Option<Stroke>>>,
-
- /// 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.
- ///
- /// _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>>>,
-
- /// How much to expand the box's size without affecting the layout.
- ///
- /// 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]($raw.block).
- ///
- /// ```example
- /// An inline
- /// #box(
- /// fill: luma(235),
- /// inset: (x: 3pt, y: 0pt),
- /// outset: (y: 3pt),
- /// radius: 2pt,
- /// )[rectangle].
- /// ```
- #[resolve]
- #[fold]
- pub outset: Sides<Option<Rel<Length>>>,
-
- /// Whether to clip the content inside the box.
- #[default(false)]
- pub clip: bool,
-
- /// The contents of the box.
- #[positional]
- pub body: Option<Content>,
-}
-
-impl Layout for BoxElem {
- #[tracing::instrument(name = "BoxElem::layout", skip_all)]
- fn layout(
- &self,
- vt: &mut Vt,
- styles: StyleChain,
- regions: Regions,
- ) -> SourceResult<Fragment> {
- let width = match self.width(styles) {
- Sizing::Auto => Smart::Auto,
- Sizing::Rel(rel) => Smart::Custom(rel),
- Sizing::Fr(_) => Smart::Custom(Ratio::one().into()),
- };
-
- // Resolve the sizing to a concrete size.
- let sizing = Axes::new(width, self.height(styles));
- let expand = sizing.as_ref().map(Smart::is_custom);
- let size = sizing
- .resolve(styles)
- .zip_map(regions.base(), |s, b| s.map(|v| v.relative_to(b)))
- .unwrap_or(regions.base());
-
- // Apply inset.
- let mut body = self.body(styles).unwrap_or_default();
- let inset = self.inset(styles);
- if inset.iter().any(|v| !v.is_zero()) {
- body = body.padded(inset.map(|side| side.map(Length::from)));
- }
-
- // Select the appropriate base and expansion for the child depending
- // on whether it is automatically or relatively sized.
- let pod = Regions::one(size, expand);
- let mut frame = body.layout(vt, styles, pod)?.into_frame();
-
- // Enforce correct size.
- *frame.size_mut() = expand.select(size, frame.size());
-
- // Apply baseline shift.
- let shift = self.baseline(styles).relative_to(frame.height());
- if !shift.is_zero() {
- frame.set_baseline(frame.baseline() - shift);
- }
-
- // Prepare fill and stroke.
- let fill = self.fill(styles);
- let stroke = self.stroke(styles).map(|s| s.map(Stroke::unwrap_or_default));
-
- // Clip the contents
- if self.clip(styles) {
- let outset = self.outset(styles).relative_to(frame.size());
- let size = frame.size() + outset.sum_by_axis();
- let radius = self.radius(styles);
- frame.clip(clip_rect(size, radius, &stroke));
- }
-
- // Add fill and/or stroke.
- if fill.is_some() || stroke.iter().any(Option::is_some) {
- let outset = self.outset(styles);
- let radius = self.radius(styles);
- frame.fill_and_stroke(fill, stroke, outset, radius, self.span());
- }
-
- // Apply metadata.
- frame.meta(styles, false);
- frame.set_kind(FrameKind::Hard);
-
- Ok(Fragment::frame(frame))
- }
-}
-
-/// A block-level container.
-///
-/// Such a container can be used to separate content, size it, and give it a
-/// background or border.
-///
-/// # Examples
-/// With a block, you can give a background to content while still allowing it
-/// to break across multiple pages.
-/// ```example
-/// #set page(height: 100pt)
-/// #block(
-/// fill: luma(230),
-/// inset: 8pt,
-/// radius: 4pt,
-/// lorem(30),
-/// )
-/// ```
-///
-/// Blocks are also useful to force elements that would otherwise be inline to
-/// become block-level, especially when writing show rules.
-/// ```example
-/// #show heading: it => it.body
-/// = Blockless
-/// More text.
-///
-/// #show heading: it => block(it.body)
-/// = Blocky
-/// More text.
-/// ```
-#[elem(Layout)]
-pub struct BlockElem {
- /// The block's width.
- ///
- /// ```example
- /// #set align(center)
- /// #block(
- /// width: 60%,
- /// inset: 8pt,
- /// fill: silver,
- /// lorem(10),
- /// )
- /// ```
- pub width: Smart<Rel<Length>>,
-
- /// The block's height. When the height is larger than the remaining space
- /// on a page and [`breakable`]($block.breakable) is `{true}`, the
- /// block will continue on the next page with the remaining height.
- ///
- /// ```example
- /// #set page(height: 80pt)
- /// #set align(center)
- /// #block(
- /// width: 80%,
- /// height: 150%,
- /// fill: aqua,
- /// )
- /// ```
- pub height: Smart<Rel<Length>>,
-
- /// Whether the block can be broken and continue on the next page.
- ///
- /// ```example
- /// #set page(height: 80pt)
- /// The following block will
- /// jump to its own page.
- /// #block(
- /// breakable: false,
- /// lorem(15),
- /// )
- /// ```
- #[default(true)]
- pub breakable: bool,
-
- /// The block's background color. See the
- /// [rectangle's documentation]($rect.fill) for more details.
- pub fill: Option<Paint>,
-
- /// The block's border color. See the
- /// [rectangle's documentation]($rect.stroke) for more details.
- #[resolve]
- #[fold]
- pub stroke: Sides<Option<Option<Stroke>>>,
-
- /// 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
- /// [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 [box's documentation]($box.outset) for more details.
- #[resolve]
- #[fold]
- pub outset: Sides<Option<Rel<Length>>>,
-
- /// The spacing around this block. This is shorthand to set `above` and
- /// `below` to the same value.
- ///
- /// ```example
- /// #set align(center)
- /// #show math.equation: set block(above: 8pt, below: 16pt)
- ///
- /// This sum of $x$ and $y$:
- /// $ x + y = z $
- /// A second paragraph.
- /// ```
- #[external]
- #[default(Em::new(1.2).into())]
- pub spacing: Spacing,
-
- /// The spacing between this block and its predecessor. Takes precedence
- /// over `spacing`. Can be used in combination with a show rule to adjust
- /// the spacing around arbitrary block-level elements.
- #[external]
- #[default(Em::new(1.2).into())]
- pub above: Spacing,
- #[internal]
- #[parse(
- let spacing = args.named("spacing")?;
- args.named("above")?
- .map(VElem::block_around)
- .or_else(|| spacing.map(VElem::block_spacing))
- )]
- #[default(VElem::block_spacing(Em::new(1.2).into()))]
- pub above: VElem,
-
- /// The spacing between this block and its successor. Takes precedence
- /// over `spacing`.
- #[external]
- #[default(Em::new(1.2).into())]
- pub below: Spacing,
- #[internal]
- #[parse(
- args.named("below")?
- .map(VElem::block_around)
- .or_else(|| spacing.map(VElem::block_spacing))
- )]
- #[default(VElem::block_spacing(Em::new(1.2).into()))]
- pub below: VElem,
-
- /// Whether to clip the content inside the block.
- #[default(false)]
- pub clip: bool,
-
- /// The contents of the block.
- #[positional]
- pub body: Option<Content>,
-
- /// Whether this block must stick to the following one.
- ///
- /// Use this to prevent page breaks between e.g. a heading and its body.
- #[internal]
- #[default(false)]
- pub sticky: bool,
-}
-
-impl Layout for BlockElem {
- #[tracing::instrument(name = "BlockElem::layout", skip_all)]
- fn layout(
- &self,
- vt: &mut Vt,
- styles: StyleChain,
- regions: Regions,
- ) -> SourceResult<Fragment> {
- // Apply inset.
- let mut body = self.body(styles).unwrap_or_default();
- let inset = self.inset(styles);
- if inset.iter().any(|v| !v.is_zero()) {
- body = body.clone().padded(inset.map(|side| side.map(Length::from)));
- }
-
- // Resolve the sizing to a concrete size.
- let sizing = Axes::new(self.width(styles), self.height(styles));
- let mut expand = sizing.as_ref().map(Smart::is_custom);
- let mut size = sizing
- .resolve(styles)
- .zip_map(regions.base(), |s, b| s.map(|v| v.relative_to(b)))
- .unwrap_or(regions.base());
-
- // Layout the child.
- let mut frames = if self.breakable(styles) {
- // Measure to ensure frames for all regions have the same width.
- if sizing.x == Smart::Auto {
- let pod = Regions::one(size, Axes::splat(false));
- let frame = body.measure(vt, styles, pod)?.into_frame();
- size.x = frame.width();
- expand.x = true;
- }
-
- let mut pod = regions;
- pod.size.x = size.x;
- pod.expand = expand;
-
- if expand.y {
- pod.full = size.y;
- }
-
- // Generate backlog for fixed height.
- let mut heights = vec![];
- if sizing.y.is_custom() {
- let mut remaining = size.y;
- for region in regions.iter() {
- let limited = region.y.min(remaining);
- heights.push(limited);
- remaining -= limited;
- if Abs::zero().fits(remaining) {
- break;
- }
- }
-
- if let Some(last) = heights.last_mut() {
- *last += remaining;
- }
-
- pod.size.y = heights[0];
- pod.backlog = &heights[1..];
- pod.last = None;
- }
-
- let mut frames = body.layout(vt, styles, pod)?.into_frames();
- for (frame, &height) in frames.iter_mut().zip(&heights) {
- *frame.size_mut() =
- expand.select(Size::new(size.x, height), frame.size());
- }
- frames
- } else {
- let pod = Regions::one(size, expand);
- let mut frames = body.layout(vt, styles, pod)?.into_frames();
- *frames[0].size_mut() = expand.select(size, frames[0].size());
- frames
- };
-
- // Prepare fill and stroke.
- let fill = self.fill(styles);
- let stroke = self.stroke(styles).map(|s| s.map(Stroke::unwrap_or_default));
-
- // Clip the contents
- if self.clip(styles) {
- for frame in frames.iter_mut() {
- let outset = self.outset(styles).relative_to(frame.size());
- let size = frame.size() + outset.sum_by_axis();
- let radius = self.radius(styles);
- frame.clip(clip_rect(size, radius, &stroke));
- }
- }
-
- // Add fill and/or stroke.
- if fill.is_some() || stroke.iter().any(Option::is_some) {
- let mut skip = false;
- if let [first, rest @ ..] = frames.as_slice() {
- skip = first.is_empty() && rest.iter().any(|frame| !frame.is_empty());
- }
-
- let outset = self.outset(styles);
- let radius = self.radius(styles);
- for frame in frames.iter_mut().skip(skip as usize) {
- frame.fill_and_stroke(
- fill.clone(),
- stroke.clone(),
- outset,
- radius,
- self.span(),
- );
- }
- }
-
- // Apply metadata.
- for frame in &mut frames {
- frame.set_kind(FrameKind::Hard);
- frame.meta(styles, false);
- }
-
- Ok(Fragment::frames(frames))
- }
-}
-
-/// Defines how to size a grid cell along an axis.
-#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
-pub enum Sizing {
- /// A track that fits its cell's contents.
- Auto,
- /// A track size specified in absolute terms and relative to the parent's
- /// size.
- Rel(Rel<Length>),
- /// A track size specified as a fraction of the remaining free space in the
- /// parent.
- Fr(Fr),
-}
-
-impl Sizing {
- /// Whether this is fractional sizing.
- pub fn is_fractional(self) -> bool {
- matches!(self, Self::Fr(_))
- }
-}
-
-impl Default for Sizing {
- fn default() -> Self {
- Self::Auto
- }
-}
-
-impl<T: Into<Spacing>> From<T> for Sizing {
- fn from(spacing: T) -> Self {
- match spacing.into() {
- Spacing::Rel(rel) => Self::Rel(rel),
- Spacing::Fr(fr) => Self::Fr(fr),
- }
- }
-}
-
-cast! {
- Sizing,
- self => match self {
- Self::Auto => Value::Auto,
- Self::Rel(rel) => rel.into_value(),
- Self::Fr(fr) => fr.into_value(),
- },
- _: AutoValue => Self::Auto,
- v: Rel<Length> => Self::Rel(v),
- v: Fr => Self::Fr(v),
-}
diff --git a/crates/typst-library/src/layout/enum.rs b/crates/typst-library/src/layout/enum.rs
deleted file mode 100644
index 0c98c18a..00000000
--- a/crates/typst-library/src/layout/enum.rs
+++ /dev/null
@@ -1,319 +0,0 @@
-use std::str::FromStr;
-
-use crate::layout::{BlockElem, GridLayouter, ParElem, Sizing, Spacing};
-use crate::meta::{Numbering, NumberingPattern};
-use crate::prelude::*;
-use crate::text::TextElem;
-
-/// A numbered list.
-///
-/// Displays a sequence of items vertically and numbers them consecutively.
-///
-/// # Example
-/// ```example
-/// Automatically numbered:
-/// + Preparations
-/// + Analysis
-/// + Conclusions
-///
-/// Manually numbered:
-/// 2. What is the first step?
-/// 5. I am confused.
-/// + Moving on ...
-///
-/// Multiple lines:
-/// + This enum item has multiple
-/// lines because the next line
-/// is indented.
-///
-/// Function call.
-/// #enum[First][Second]
-/// ```
-///
-/// You can easily switch all your enumerations to a different numbering style
-/// with a set rule.
-/// ```example
-/// #set enum(numbering: "a)")
-///
-/// + Starting off ...
-/// + Don't forget step two
-/// ```
-///
-/// You can also use [`enum.item`]($enum.item) to programmatically customize the
-/// number of each item in the enumeration:
-///
-/// ```example
-/// #enum(
-/// enum.item(1)[First step],
-/// enum.item(5)[Fifth step],
-/// enum.item(10)[Tenth step]
-/// )
-/// ```
-///
-/// # Syntax
-/// This functions also has dedicated syntax:
-///
-/// - Starting a line with a plus sign creates an automatically numbered
-/// enumeration item.
-/// - Starting a line with a number followed by a dot creates an explicitly
-/// numbered enumeration item.
-///
-/// 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.
-#[elem(scope, title = "Numbered List", Layout)]
-pub struct EnumElem {
- /// If this is `{false}`, the items are spaced apart with
- /// [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
- /// whether items are separated with a blank line. If items directly follow
- /// each other, this is set to `{true}`; if items are separated by a blank
- /// line, this is set to `{false}`.
- ///
- /// ```example
- /// + If an enum has a lot of text, and
- /// maybe other inline content, it
- /// should not be tight anymore.
- ///
- /// + To make an enum wide, simply
- /// insert a blank line between the
- /// items.
- /// ```
- #[default(true)]
- pub tight: bool,
-
- /// How to number the enumeration. Accepts a
- /// [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
- /// if `full` is `{false}` and multiple arguments if `full` is `{true}`.
- ///
- /// ```example
- /// #set enum(numbering: "1.a)")
- /// + Different
- /// + Numbering
- /// + Nested
- /// + Items
- /// + Style
- ///
- /// #set enum(numbering: n => super[#n])
- /// + Superscript
- /// + Numbering!
- /// ```
- #[default(Numbering::Pattern(NumberingPattern::from_str("1.").unwrap()))]
- #[borrowed]
- pub numbering: Numbering,
-
- /// Which number to start the enumeration with.
- ///
- /// ```example
- /// #enum(
- /// start: 3,
- /// [Skipping],
- /// [Ahead],
- /// )
- /// ```
- #[default(1)]
- pub start: usize,
-
- /// Whether to display the full numbering, including the numbers of
- /// all parent enumerations.
- ///
- ///
- /// ```example
- /// #set enum(numbering: "1.a)", full: true)
- /// + Cook
- /// + Heat water
- /// + Add integredients
- /// + Eat
- /// ```
- #[default(false)]
- pub full: bool,
-
- /// The indentation of each item.
- #[resolve]
- pub indent: Length,
-
- /// The space between the numbering and the body of each item.
- #[resolve]
- #[default(Em::new(0.5).into())]
- pub body_indent: Length,
-
- /// The spacing between the items of a wide (non-tight) enumeration.
- ///
- /// If set to `{auto}`, uses the spacing [below blocks]($block.below).
- pub spacing: Smart<Spacing>,
-
- /// The horizontal alignment that enum numbers should have.
- ///
- /// By default, this is set to `{end}`, which aligns enum numbers
- /// towards end of the current text direction (in left-to-right script,
- /// for example, this is the same as `{right}`). The choice of `{end}`
- /// for horizontal alignment of enum numbers is usually preferred over
- /// `{start}`, as numbers then grow away from the text instead of towards
- /// it, avoiding certain visual issues. This option lets you override this
- /// behavior, however.
- ///
- /// ````example
- /// #set enum(number-align: start)
- ///
- /// Here are some powers of two:
- /// 1. One
- /// 2. Two
- /// 4. Four
- /// 8. Eight
- /// 16. Sixteen
- /// 32. Thirty two
- /// ````
- #[default(HAlign::End)]
- pub number_align: HAlign,
-
- /// The numbered list's items.
- ///
- /// When using the enum syntax, adjacent items are automatically collected
- /// into enumerations, even through constructs like for loops.
- ///
- /// ```example
- /// #for phase in (
- /// "Launch",
- /// "Orbit",
- /// "Descent",
- /// ) [+ #phase]
- /// ```
- #[variadic]
- pub children: Vec<EnumItem>,
-
- /// The numbers of parent items.
- #[internal]
- #[fold]
- parents: Parent,
-}
-
-#[scope]
-impl EnumElem {
- #[elem]
- type EnumItem;
-}
-
-impl Layout for EnumElem {
- #[tracing::instrument(name = "EnumElem::layout", skip_all)]
- fn layout(
- &self,
- vt: &mut Vt,
- styles: StyleChain,
- regions: Regions,
- ) -> SourceResult<Fragment> {
- let numbering = self.numbering(styles);
- let indent = self.indent(styles);
- let body_indent = self.body_indent(styles);
- let gutter = if self.tight(styles) {
- ParElem::leading_in(styles).into()
- } else {
- self.spacing(styles)
- .unwrap_or_else(|| *BlockElem::below_in(styles).amount())
- };
-
- let mut cells = vec![];
- let mut number = self.start(styles);
- let mut parents = self.parents(styles);
- 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 = self.number_align(styles) + VAlign::Top;
-
- for item in self.children() {
- number = item.number(styles).unwrap_or(number);
-
- let resolved = if full {
- parents.push(number);
- let content = numbering.apply_vt(vt, &parents)?.display();
- parents.pop();
- content
- } else {
- match numbering {
- Numbering::Pattern(pattern) => {
- TextElem::packed(pattern.apply_kth(parents.len(), number))
- }
- other => other.apply_vt(vt, &[number])?.display(),
- }
- };
-
- // Disable overhang as a workaround to end-aligned dots glitching
- // and decreasing spacing between numbers and items.
- let resolved =
- resolved.aligned(number_align).styled(TextElem::set_overhang(false));
-
- cells.push(Content::empty());
- cells.push(resolved);
- cells.push(Content::empty());
- cells.push(item.body().clone().styled(Self::set_parents(Parent(number))));
- number = number.saturating_add(1);
- }
-
- let layouter = GridLayouter::new(
- Axes::with_x(&[
- Sizing::Rel(indent.into()),
- Sizing::Auto,
- Sizing::Rel(body_indent.into()),
- Sizing::Auto,
- ]),
- Axes::with_y(&[gutter.into()]),
- &cells,
- regions,
- styles,
- self.span(),
- );
-
- Ok(layouter.layout(vt)?.fragment)
- }
-}
-
-/// An enumeration item.
-#[elem(name = "item", title = "Numbered List Item")]
-pub struct EnumItem {
- /// The item's number.
- #[positional]
- pub number: Option<usize>,
-
- /// The item's body.
- #[required]
- pub body: Content,
-}
-
-cast! {
- EnumItem,
- array: Array => {
- let mut iter = array.into_iter();
- let (number, body) = match (iter.next(), iter.next(), iter.next()) {
- (Some(a), Some(b), None) => (a.cast()?, b.cast()?),
- _ => bail!("array must contain exactly two entries"),
- };
- Self::new(body).with_number(number)
- },
- v: Content => v.to::<Self>().cloned().unwrap_or_else(|| Self::new(v.clone())),
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Hash)]
-struct Parent(usize);
-
-cast! {
- Parent,
- self => self.0.into_value(),
- v: usize => Self(v),
-}
-
-impl Fold for Parent {
- type Output = Vec<usize>;
-
- fn fold(self, mut outer: Self::Output) -> Self::Output {
- outer.push(self.0);
- outer
- }
-}
diff --git a/crates/typst-library/src/layout/flow.rs b/crates/typst-library/src/layout/flow.rs
deleted file mode 100644
index 17a39254..00000000
--- a/crates/typst-library/src/layout/flow.rs
+++ /dev/null
@@ -1,714 +0,0 @@
-use std::mem;
-
-use comemo::Prehashed;
-
-use crate::layout::{
- AlignElem, BlockElem, ColbreakElem, ColumnsElem, ParElem, PlaceElem, Spacing, VElem,
-};
-use crate::meta::{FootnoteElem, FootnoteEntry};
-use crate::prelude::*;
-use crate::visualize::{
- CircleElem, EllipseElem, ImageElem, LineElem, PathElem, PolygonElem, RectElem,
- SquareElem,
-};
-
-/// Arranges spacing, paragraphs and block-level elements into a flow.
-///
-/// This element is responsible for layouting both the top-level content flow
-/// and the contents of boxes.
-#[elem(Layout)]
-pub struct FlowElem {
- /// The children that will be arranges into a flow.
- #[variadic]
- pub children: Vec<Prehashed<Content>>,
-}
-
-impl Layout for FlowElem {
- #[tracing::instrument(name = "FlowElem::layout", skip_all)]
- fn layout(
- &self,
- vt: &mut Vt,
- styles: StyleChain,
- regions: Regions,
- ) -> SourceResult<Fragment> {
- if !regions.size.x.is_finite() && regions.expand.x {
- bail!(error!(self.span(), "cannot expand into infinite width"));
- }
- if !regions.size.y.is_finite() && regions.expand.y {
- bail!(error!(self.span(), "cannot expand into infinite height"));
- }
- let mut layouter = FlowLayouter::new(regions, styles);
-
- for mut child in self.children().iter().map(|c| &**c) {
- let outer = styles;
- let mut styles = styles;
- if let Some((elem, map)) = child.to_styled() {
- child = elem;
- styles = outer.chain(map);
- }
-
- if let Some(elem) = child.to::<VElem>() {
- layouter.layout_spacing(vt, elem, styles)?;
- } else if let Some(elem) = child.to::<ParElem>() {
- layouter.layout_par(vt, elem, styles)?;
- } else if child.is::<LineElem>()
- || child.is::<RectElem>()
- || child.is::<SquareElem>()
- || child.is::<EllipseElem>()
- || child.is::<CircleElem>()
- || child.is::<ImageElem>()
- || child.is::<PolygonElem>()
- || child.is::<PathElem>()
- {
- let layoutable = child.with::<dyn Layout>().unwrap();
- layouter.layout_single(vt, layoutable, styles)?;
- } else if child.is::<MetaElem>() {
- let mut frame = Frame::soft(Size::zero());
- frame.meta(styles, true);
- layouter.items.push(FlowItem::Frame {
- frame,
- align: Axes::splat(FixedAlign::Start),
- sticky: true,
- movable: false,
- });
- } else if let Some(placed) = child.to::<PlaceElem>() {
- layouter.layout_placed(vt, placed, styles)?;
- } else if child.can::<dyn Layout>() {
- layouter.layout_multiple(vt, child, styles)?;
- } else if child.is::<ColbreakElem>() {
- if !layouter.regions.backlog.is_empty() || layouter.regions.last.is_some()
- {
- layouter.finish_region(vt)?;
- }
- } else {
- bail!(child.span(), "unexpected flow child");
- }
- }
-
- layouter.finish(vt)
- }
-}
-
-/// Performs flow layout.
-struct FlowLayouter<'a> {
- /// Whether this is the root flow.
- root: bool,
- /// The regions to layout children into.
- regions: Regions<'a>,
- /// The shared styles.
- styles: StyleChain<'a>,
- /// Whether the flow should expand to fill the region.
- expand: Axes<bool>,
- /// The initial size of `regions.size` that was available before we started
- /// subtracting.
- initial: Size,
- /// Whether the last block was a paragraph.
- last_was_par: bool,
- /// Spacing and layouted blocks for the current region.
- items: Vec<FlowItem>,
- /// A queue of floats.
- pending_floats: Vec<FlowItem>,
- /// Whether we have any footnotes in the current region.
- has_footnotes: bool,
- /// Footnote configuration.
- footnote_config: FootnoteConfig,
- /// Finished frames for previous regions.
- finished: Vec<Frame>,
-}
-
-/// Cached footnote configuration.
-struct FootnoteConfig {
- separator: Content,
- clearance: Abs,
- gap: Abs,
-}
-
-/// A prepared item in a flow layout.
-#[derive(Debug)]
-enum FlowItem {
- /// Spacing between other items and whether it is weak.
- Absolute(Abs, bool),
- /// Fractional spacing between other items.
- Fractional(Fr),
- /// 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, align: Axes<FixedAlign>, sticky: bool, movable: bool },
- /// An absolutely placed frame.
- Placed {
- frame: Frame,
- x_align: FixedAlign,
- y_align: Smart<Option<FixedAlign>>,
- delta: Axes<Rel<Abs>>,
- float: bool,
- clearance: Abs,
- },
- /// A footnote frame (can also be the separator).
- Footnote(Frame),
-}
-
-impl FlowItem {
- /// The inherent height of the item.
- fn height(&self) -> Abs {
- match self {
- Self::Absolute(v, _) => *v,
- Self::Fractional(_) | Self::Placed { .. } => Abs::zero(),
- Self::Frame { frame, .. } | Self::Footnote(frame) => frame.height(),
- }
- }
-}
-
-impl<'a> FlowLayouter<'a> {
- /// Create a new flow layouter.
- fn new(mut regions: Regions<'a>, styles: StyleChain<'a>) -> Self {
- let expand = regions.expand;
-
- // Disable vertical expansion & root for children.
- regions.expand.y = false;
- let root = mem::replace(&mut regions.root, false);
-
- Self {
- root,
- regions,
- styles,
- expand,
- initial: regions.size,
- last_was_par: false,
- items: vec![],
- pending_floats: vec![],
- has_footnotes: false,
- footnote_config: FootnoteConfig {
- separator: FootnoteEntry::separator_in(styles),
- clearance: FootnoteEntry::clearance_in(styles),
- gap: FootnoteEntry::gap_in(styles),
- },
- finished: vec![],
- }
- }
-
- /// Layout vertical spacing.
- #[tracing::instrument(name = "FlowLayouter::layout_spacing", skip_all)]
- fn layout_spacing(
- &mut self,
- vt: &mut Vt,
- v: &VElem,
- styles: StyleChain,
- ) -> SourceResult<()> {
- self.layout_item(
- vt,
- match v.amount() {
- Spacing::Rel(rel) => FlowItem::Absolute(
- rel.resolve(styles).relative_to(self.initial.y),
- v.weakness(styles) > 0,
- ),
- Spacing::Fr(fr) => FlowItem::Fractional(*fr),
- },
- )
- }
-
- /// Layout a paragraph.
- #[tracing::instrument(name = "FlowLayouter::layout_par", skip_all)]
- fn layout_par(
- &mut self,
- vt: &mut Vt,
- par: &ParElem,
- styles: StyleChain,
- ) -> SourceResult<()> {
- let align = AlignElem::alignment_in(styles).resolve(styles);
- let leading = ParElem::leading_in(styles);
- let consecutive = self.last_was_par;
- let lines = par
- .layout(vt, styles, consecutive, self.regions.base(), self.regions.expand.x)?
- .into_frames();
-
- let mut sticky = self.items.len();
- for (i, item) in self.items.iter().enumerate().rev() {
- match *item {
- FlowItem::Absolute(_, _) => {}
- FlowItem::Frame { sticky: true, .. } => sticky = i,
- _ => break,
- }
- }
-
- if let Some(first) = lines.first() {
- if !self.regions.size.y.fits(first.height()) && !self.regions.in_last() {
- let carry: Vec<_> = self.items.drain(sticky..).collect();
- self.finish_region(vt)?;
- for item in carry {
- self.layout_item(vt, item)?;
- }
- }
- }
-
- for (i, frame) in lines.into_iter().enumerate() {
- if i > 0 {
- self.layout_item(vt, FlowItem::Absolute(leading, true))?;
- }
-
- self.layout_item(
- vt,
- FlowItem::Frame { frame, align, sticky: false, movable: true },
- )?;
- }
-
- self.last_was_par = true;
- Ok(())
- }
-
- /// Layout into a single region.
- #[tracing::instrument(name = "FlowLayouter::layout_single", skip_all)]
- fn layout_single(
- &mut self,
- vt: &mut Vt,
- content: &dyn Layout,
- styles: StyleChain,
- ) -> SourceResult<()> {
- 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, align, sticky, movable: true })?;
- self.last_was_par = false;
- Ok(())
- }
-
- /// Layout a placed element.
- fn layout_placed(
- &mut self,
- vt: &mut Vt,
- placed: &PlaceElem,
- styles: StyleChain,
- ) -> SourceResult<()> {
- let float = placed.float(styles);
- 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(FixedAlign::Center, |align| {
- align.x().unwrap_or_default().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)
- }
-
- /// Layout into multiple regions.
- fn layout_multiple(
- &mut self,
- vt: &mut Vt,
- block: &Content,
- styles: StyleChain,
- ) -> SourceResult<()> {
- // Temporarily delegerate rootness to the columns.
- let is_root = self.root;
- if is_root && block.is::<ColumnsElem>() {
- self.root = false;
- self.regions.root = true;
- }
-
- let mut notes = Vec::new();
-
- if self.regions.is_full() {
- // Skip directly if region is already full.
- self.finish_region(vt)?;
- }
-
- // How to align the block.
- 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))
- } else {
- AlignElem::alignment_in(styles)
- }
- .resolve(styles);
-
- // Layout the block itself.
- let sticky = BlockElem::sticky_in(styles);
- let fragment = block.layout(vt, styles, self.regions)?;
-
- for (i, frame) in fragment.into_iter().enumerate() {
- // Find footnotes in the frame.
- if self.root {
- find_footnotes(&mut notes, &frame);
- }
-
- if i > 0 {
- self.finish_region(vt)?;
- }
-
- let item = FlowItem::Frame { frame, align, sticky, movable: false };
- self.layout_item(vt, item)?;
- }
-
- self.try_handle_footnotes(vt, notes)?;
-
- self.root = is_root;
- self.regions.root = false;
- self.last_was_par = false;
-
- Ok(())
- }
-
- /// Layout a finished frame.
- #[tracing::instrument(name = "FlowLayouter::layout_item", skip_all)]
- fn layout_item(&mut self, vt: &mut Vt, mut item: FlowItem) -> SourceResult<()> {
- match item {
- FlowItem::Absolute(v, weak) => {
- if weak
- && !self
- .items
- .iter()
- .any(|item| matches!(item, FlowItem::Frame { .. }))
- {
- return Ok(());
- }
- self.regions.size.y -= v
- }
- FlowItem::Fractional(_) => {}
- FlowItem::Frame { ref frame, movable, .. } => {
- let height = frame.height();
- if !self.regions.size.y.fits(height) && !self.regions.in_last() {
- self.finish_region(vt)?;
- }
-
- self.regions.size.y -= height;
- if self.root && movable {
- let mut notes = Vec::new();
- find_footnotes(&mut notes, frame);
- self.items.push(item);
- if !self.handle_footnotes(vt, &mut notes, true, false)? {
- let item = self.items.pop();
- self.finish_region(vt)?;
- self.items.extend(item);
- self.regions.size.y -= height;
- self.handle_footnotes(vt, &mut notes, true, true)?;
- }
- return Ok(());
- }
- }
- FlowItem::Placed { float: false, .. } => {}
- FlowItem::Placed {
- ref mut frame,
- ref mut y_align,
- float: true,
- clearance,
- ..
- } => {
- // If the float doesn't fit, queue it for the next region.
- if !self.regions.size.y.fits(frame.height() + clearance)
- && !self.regions.in_last()
- {
- self.pending_floats.push(item);
- return Ok(());
- }
-
- // Select the closer placement, top or bottom.
- if y_align.is_auto() {
- let ratio = (self.regions.size.y
- - (frame.height() + clearance) / 2.0)
- / self.regions.full;
- let better_align =
- 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(FixedAlign::End)) {
- frame.translate(Point::with_y(clearance));
- }
-
- self.regions.size.y -= frame.height();
-
- // Find footnotes in the frame.
- if self.root {
- let mut notes = vec![];
- find_footnotes(&mut notes, frame);
- self.try_handle_footnotes(vt, notes)?;
- }
- }
- FlowItem::Footnote(_) => {}
- }
-
- self.items.push(item);
- Ok(())
- }
-
- /// Finish the frame for one region.
- fn finish_region(&mut self, vt: &mut Vt) -> SourceResult<()> {
- // Trim weak spacing.
- while self
- .items
- .last()
- .map_or(false, |item| matches!(item, FlowItem::Absolute(_, true)))
- {
- self.items.pop();
- }
-
- // Determine the used size.
- let mut fr = Fr::zero();
- let mut used = Size::zero();
- let mut footnote_height = Abs::zero();
- let mut float_top_height = Abs::zero();
- let mut float_bottom_height = Abs::zero();
- let mut first_footnote = true;
- for item in &self.items {
- match item {
- FlowItem::Absolute(v, _) => used.y += *v,
- FlowItem::Fractional(v) => fr += *v,
- FlowItem::Frame { frame, .. } => {
- used.y += frame.height();
- used.x.set_max(frame.width());
- }
- FlowItem::Placed { float: false, .. } => {}
- FlowItem::Placed { frame, float: true, y_align, .. } => match y_align {
- Smart::Custom(Some(FixedAlign::Start)) => {
- float_top_height += frame.height()
- }
- Smart::Custom(Some(FixedAlign::End)) => {
- float_bottom_height += frame.height()
- }
- _ => {}
- },
- FlowItem::Footnote(frame) => {
- footnote_height += frame.height();
- if !first_footnote {
- footnote_height += self.footnote_config.gap;
- }
- first_footnote = false;
- used.x.set_max(frame.width());
- }
- }
- }
- used.y += footnote_height + float_top_height + float_bottom_height;
-
- // Determine the size of the flow in this region depending on whether
- // the region expands. Also account for fractional spacing and
- // footnotes.
- let mut size = self.expand.select(self.initial, used).min(self.initial);
- if (fr.get() > 0.0 || self.has_footnotes) && self.initial.y.is_finite() {
- size.y = self.initial.y;
- }
-
- let mut output = Frame::soft(size);
- 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();
- let mut footnote_offset = Abs::zero();
-
- // Place all frames.
- for item in self.items.drain(..) {
- match item {
- FlowItem::Absolute(v, _) => {
- offset += v;
- }
- FlowItem::Fractional(v) => {
- let remaining = self.initial.y - used.y;
- offset += v.share(fr, remaining);
- }
- 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();
- output.push_frame(pos, frame);
- }
- FlowItem::Placed { frame, x_align, y_align, delta, float, .. } => {
- let x = x_align.position(size.x - frame.width());
- let y = if float {
- match y_align {
- Smart::Custom(Some(FixedAlign::Start)) => {
- let y = float_top_offset;
- float_top_offset += frame.height();
- y
- }
- Smart::Custom(Some(FixedAlign::End)) => {
- let y = size.y - footnote_height - float_bottom_height
- + float_bottom_offset;
- float_bottom_offset += frame.height();
- y
- }
- _ => unreachable!("float must be y aligned"),
- }
- } else {
- match y_align {
- Smart::Custom(Some(align)) => {
- align.position(size.y - frame.height())
- }
- _ => offset + ruler.position(size.y - used.y),
- }
- };
-
- let pos = Point::new(x, y)
- + delta.zip_map(size, Rel::relative_to).to_point();
-
- output.push_frame(pos, frame);
- }
- FlowItem::Footnote(frame) => {
- let y = size.y - footnote_height + footnote_offset;
- footnote_offset += frame.height() + self.footnote_config.gap;
- output.push_frame(Point::with_y(y), frame);
- }
- }
- }
-
- // Advance to the next region.
- self.finished.push(output);
- self.regions.next();
- self.initial = self.regions.size;
- self.has_footnotes = false;
-
- // Try to place floats.
- for item in mem::take(&mut self.pending_floats) {
- self.layout_item(vt, item)?;
- }
-
- Ok(())
- }
-
- /// Finish layouting and return the resulting fragment.
- fn finish(mut self, vt: &mut Vt) -> SourceResult<Fragment> {
- if self.expand.y {
- while !self.regions.backlog.is_empty() {
- self.finish_region(vt)?;
- }
- }
-
- self.finish_region(vt)?;
- while !self.items.is_empty() {
- self.finish_region(vt)?;
- }
-
- Ok(Fragment::frames(self.finished))
- }
-}
-
-impl FlowLayouter<'_> {
- fn try_handle_footnotes(
- &mut self,
- vt: &mut Vt,
- mut notes: Vec<FootnoteElem>,
- ) -> SourceResult<()> {
- if self.root && !self.handle_footnotes(vt, &mut notes, false, false)? {
- self.finish_region(vt)?;
- self.handle_footnotes(vt, &mut notes, false, true)?;
- }
- Ok(())
- }
-
- /// Processes all footnotes in the frame.
- #[tracing::instrument(skip_all)]
- fn handle_footnotes(
- &mut self,
- vt: &mut Vt,
- notes: &mut Vec<FootnoteElem>,
- movable: bool,
- force: bool,
- ) -> SourceResult<bool> {
- let items_len = self.items.len();
- let notes_len = notes.len();
-
- // Process footnotes one at a time.
- let mut k = 0;
- while k < notes.len() {
- if notes[k].is_ref() {
- k += 1;
- continue;
- }
-
- if !self.has_footnotes {
- self.layout_footnote_separator(vt)?;
- }
-
- self.regions.size.y -= self.footnote_config.gap;
- let checkpoint = vt.locator.clone();
- let frames = FootnoteEntry::new(notes[k].clone())
- .pack()
- .layout(vt, self.styles, self.regions.with_root(false))?
- .into_frames();
-
- // If the entries didn't fit, abort (to keep footnote and entry
- // together).
- if !force
- && (k == 0 || movable)
- && frames.first().map_or(false, Frame::is_empty)
- {
- // Remove existing footnotes attempts because we need to
- // move the item to the next page.
- notes.truncate(notes_len);
-
- // Undo region modifications.
- for item in self.items.drain(items_len..) {
- self.regions.size.y -= item.height();
- }
-
- // Undo Vt modifications.
- *vt.locator = checkpoint;
-
- return Ok(false);
- }
-
- let prev = notes.len();
- for (i, frame) in frames.into_iter().enumerate() {
- find_footnotes(notes, &frame);
- if i > 0 {
- self.finish_region(vt)?;
- self.layout_footnote_separator(vt)?;
- self.regions.size.y -= self.footnote_config.gap;
- }
- self.regions.size.y -= frame.height();
- self.items.push(FlowItem::Footnote(frame));
- }
-
- k += 1;
-
- // Process the nested notes before dealing with further top-level
- // notes.
- let nested = notes.len() - prev;
- if nested > 0 {
- notes[k..].rotate_right(nested);
- }
- }
-
- Ok(true)
- }
-
- /// Layout and save the footnote separator, typically a line.
- #[tracing::instrument(skip_all)]
- fn layout_footnote_separator(&mut self, vt: &mut Vt) -> SourceResult<()> {
- let expand = Axes::new(self.regions.expand.x, false);
- let pod = Regions::one(self.regions.base(), expand);
- let separator = &self.footnote_config.separator;
-
- let mut frame = separator.layout(vt, self.styles, pod)?.into_frame();
- frame.size_mut().y += self.footnote_config.clearance;
- frame.translate(Point::with_y(self.footnote_config.clearance));
-
- self.has_footnotes = true;
- self.regions.size.y -= frame.height();
- self.items.push(FlowItem::Footnote(frame));
-
- Ok(())
- }
-}
-
-/// Finds all footnotes in the frame.
-#[tracing::instrument(skip_all)]
-fn find_footnotes(notes: &mut Vec<FootnoteElem>, frame: &Frame) {
- for (_, item) in frame.items() {
- match item {
- FrameItem::Group(group) => find_footnotes(notes, &group.frame),
- FrameItem::Meta(Meta::Elem(content), _)
- if !notes.iter().any(|note| note.location() == content.location()) =>
- {
- let Some(footnote) = content.to::<FootnoteElem>() else { continue };
- notes.push(footnote.clone());
- }
- _ => {}
- }
- }
-}
diff --git a/crates/typst-library/src/layout/fragment.rs b/crates/typst-library/src/layout/fragment.rs
deleted file mode 100644
index 3550df2a..00000000
--- a/crates/typst-library/src/layout/fragment.rs
+++ /dev/null
@@ -1,87 +0,0 @@
-use crate::prelude::*;
-
-/// A partial layout result.
-#[derive(Clone)]
-pub struct Fragment(Vec<Frame>);
-
-impl Fragment {
- /// Create a fragment from a single frame.
- pub fn frame(frame: Frame) -> Self {
- Self(vec![frame])
- }
-
- /// Create a fragment from multiple frames.
- pub fn frames(frames: Vec<Frame>) -> Self {
- Self(frames)
- }
-
- /// Return `true` if the length is 0.
- pub fn is_empty(&self) -> bool {
- self.0.is_empty()
- }
-
- /// The number of frames in the fragment.
- pub fn len(&self) -> usize {
- self.0.len()
- }
-
- /// Extract the first and only frame.
- ///
- /// Panics if there are multiple frames.
- #[track_caller]
- pub fn into_frame(self) -> Frame {
- assert_eq!(self.0.len(), 1, "expected exactly one frame");
- self.0.into_iter().next().unwrap()
- }
-
- /// Extract the frames.
- pub fn into_frames(self) -> Vec<Frame> {
- self.0
- }
-
- /// Iterate over the contained frames.
- pub fn iter(&self) -> std::slice::Iter<Frame> {
- self.0.iter()
- }
-
- /// Iterate over the contained frames.
- pub fn iter_mut(&mut self) -> std::slice::IterMut<Frame> {
- self.0.iter_mut()
- }
-}
-
-impl Debug for Fragment {
- fn fmt(&self, f: &mut Formatter) -> fmt::Result {
- match self.0.as_slice() {
- [frame] => frame.fmt(f),
- frames => frames.fmt(f),
- }
- }
-}
-
-impl IntoIterator for Fragment {
- type Item = Frame;
- type IntoIter = std::vec::IntoIter<Frame>;
-
- fn into_iter(self) -> Self::IntoIter {
- self.0.into_iter()
- }
-}
-
-impl<'a> IntoIterator for &'a Fragment {
- type Item = &'a Frame;
- type IntoIter = std::slice::Iter<'a, Frame>;
-
- fn into_iter(self) -> Self::IntoIter {
- self.0.iter()
- }
-}
-
-impl<'a> IntoIterator for &'a mut Fragment {
- type Item = &'a mut Frame;
- type IntoIter = std::slice::IterMut<'a, Frame>;
-
- fn into_iter(self) -> Self::IntoIter {
- self.0.iter_mut()
- }
-}
diff --git a/crates/typst-library/src/layout/grid.rs b/crates/typst-library/src/layout/grid.rs
deleted file mode 100644
index 1cde6564..00000000
--- a/crates/typst-library/src/layout/grid.rs
+++ /dev/null
@@ -1,734 +0,0 @@
-use smallvec::{smallvec, SmallVec};
-
-use crate::layout::Sizing;
-use crate::prelude::*;
-use crate::text::TextElem;
-
-/// Arranges content in a grid.
-///
-/// The grid element allows you to arrange content in a grid. You can define the
-/// number of rows and columns, as well as the size of the gutters between them.
-/// There are multiple sizing modes for columns and rows that can be used to
-/// create complex layouts.
-///
-/// The sizing of the grid is determined by the track sizes specified in the
-/// arguments. Because each of the sizing parameters accepts the same values, we
-/// will explain them just once, here. Each sizing argument accepts an array of
-/// individual track sizes. A track size is either:
-///
-/// - `{auto}`: The track will be sized to fit its contents. It will be at most
-/// as large as the remaining space. If there is more than one `{auto}` track
-/// which, and together they claim more than the available space, the `{auto}`
-/// tracks will fairly distribute the available space among themselves.
-///
-/// - A fixed or relative length (e.g. `{10pt}` or `{20% - 1cm}`): The track
-/// will be exactly of this size.
-///
-/// - A fractional length (e.g. `{1fr}`): Once all other tracks have been sized,
-/// the remaining space will be divided among the fractional tracks according
-/// to their fractions. For example, if there are two fractional tracks, each
-/// with a fraction of `{1fr}`, they will each take up half of the remaining
-/// space.
-///
-/// To specify a single track, the array can be omitted in favor of a single
-/// value. To specify multiple `{auto}` tracks, enter the number of tracks
-/// instead of an array. For example, `columns:` `{3}` is equivalent to
-/// `columns:` `{(auto, auto, auto)}`.
-///
-/// # Examples
-/// The example below demonstrates the different track sizing options.
-///
-/// ```example
-/// // We use `rect` to emphasize the
-/// // area of cells.
-/// #set rect(
-/// inset: 8pt,
-/// fill: rgb("e4e5ea"),
-/// width: 100%,
-/// )
-///
-/// #grid(
-/// columns: (60pt, 1fr, 2fr),
-/// rows: (auto, 60pt),
-/// gutter: 3pt,
-/// rect[Fixed width, auto height],
-/// rect[1/3 of the remains],
-/// rect[2/3 of the remains],
-/// rect(height: 100%)[Fixed height],
-/// image("tiger.jpg", height: 100%),
-/// image("tiger.jpg", height: 100%),
-/// )
-/// ```
-///
-/// You can also [spread]($arguments/#spreading) an array of strings or content
-/// into a grid to populate its cells.
-///
-/// ```example
-/// #grid(
-/// columns: 5,
-/// gutter: 5pt,
-/// ..range(25).map(str)
-/// )
-/// ```
-#[elem(Layout)]
-pub struct GridElem {
- /// The column sizes.
- ///
- /// Either specify a track size array or provide an integer to create a grid
- /// with that many `{auto}`-sized columns. Note that opposed to rows and
- /// gutters, providing a single track size will only ever create a single
- /// column.
- #[borrowed]
- pub columns: TrackSizings,
-
- /// The row sizes.
- ///
- /// If there are more cells than fit the defined rows, the last row is
- /// repeated until there are no more cells.
- #[borrowed]
- pub rows: TrackSizings,
-
- /// The gaps between rows & columns.
- ///
- /// If there are more gutters than defined sizes, the last gutter is repeated.
- #[external]
- pub gutter: TrackSizings,
-
- /// The gaps between columns. Takes precedence over `gutter`.
- #[parse(
- let gutter = args.named("gutter")?;
- args.named("column-gutter")?.or_else(|| gutter.clone())
- )]
- #[borrowed]
- pub column_gutter: TrackSizings,
-
- /// The gaps between rows. Takes precedence over `gutter`.
- #[parse(args.named("row-gutter")?.or_else(|| gutter.clone()))]
- #[borrowed]
- pub row_gutter: TrackSizings,
-
- /// The contents of the grid cells.
- ///
- /// The cells are populated in row-major order.
- #[variadic]
- pub children: Vec<Content>,
-}
-
-impl Layout for GridElem {
- #[tracing::instrument(name = "GridElem::layout", skip_all)]
- fn layout(
- &self,
- vt: &mut Vt,
- styles: StyleChain,
- regions: Regions,
- ) -> SourceResult<Fragment> {
- let columns = self.columns(styles);
- let rows = self.rows(styles);
- let column_gutter = self.column_gutter(styles);
- let row_gutter = self.row_gutter(styles);
-
- // Prepare grid layout by unifying content and gutter tracks.
- let layouter = GridLayouter::new(
- Axes::new(&columns.0, &rows.0),
- Axes::new(&column_gutter.0, &row_gutter.0),
- &self.children,
- regions,
- styles,
- self.span(),
- );
-
- // Measure the columns and layout the grid row-by-row.
- Ok(layouter.layout(vt)?.fragment)
- }
-}
-
-/// Track sizing definitions.
-#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
-pub struct TrackSizings(pub SmallVec<[Sizing; 4]>);
-
-cast! {
- TrackSizings,
- self => self.0.into_value(),
- sizing: Sizing => Self(smallvec![sizing]),
- count: NonZeroUsize => Self(smallvec![Sizing::Auto; count.get()]),
- values: Array => Self(values.into_iter().map(Value::cast).collect::<StrResult<_>>()?),
-}
-
-/// Performs grid layout.
-pub struct GridLayouter<'a> {
- /// The grid cells.
- cells: &'a [Content],
- /// Whether this is an RTL grid.
- is_rtl: bool,
- /// Whether this grid has gutters.
- has_gutter: bool,
- /// The column tracks including gutter tracks.
- cols: Vec<Sizing>,
- /// The row tracks including gutter tracks.
- rows: Vec<Sizing>,
- /// The regions to layout children into.
- regions: Regions<'a>,
- /// The inherited styles.
- styles: StyleChain<'a>,
- /// Resolved column sizes.
- rcols: Vec<Abs>,
- /// The sum of `rcols`.
- width: Abs,
- /// Resolve row sizes, by region.
- rrows: Vec<Vec<RowPiece>>,
- /// Rows in the current region.
- lrows: Vec<Row>,
- /// The initial size of the current region before we started subtracting.
- initial: Size,
- /// Frames for finished regions.
- finished: Vec<Frame>,
- /// The span of the grid element.
- span: Span,
-}
-
-/// The resulting sizes of columns and rows in a grid.
-#[derive(Debug)]
-pub struct GridLayout {
- /// The fragment.
- pub fragment: Fragment,
- /// The column widths.
- pub cols: Vec<Abs>,
- /// The heights of the resulting rows segments, by region.
- pub rows: Vec<Vec<RowPiece>>,
-}
-
-/// Details about a resulting row piece.
-#[derive(Debug)]
-pub struct RowPiece {
- /// The height of the segment.
- pub height: Abs,
- /// The index of the row.
- pub y: usize,
-}
-
-/// Produced by initial row layout, auto and relative rows are already finished,
-/// fractional rows not yet.
-enum Row {
- /// Finished row frame of auto or relative row with y index.
- Frame(Frame, usize),
- /// Fractional row with y index.
- Fr(Fr, usize),
-}
-
-impl<'a> GridLayouter<'a> {
- /// Create a new grid layouter.
- ///
- /// This prepares grid layout by unifying content and gutter tracks.
- pub fn new(
- tracks: Axes<&[Sizing]>,
- gutter: Axes<&[Sizing]>,
- cells: &'a [Content],
- regions: Regions<'a>,
- styles: StyleChain<'a>,
- span: Span,
- ) -> Self {
- let mut cols = vec![];
- let mut rows = vec![];
-
- // Number of content columns: Always at least one.
- let c = tracks.x.len().max(1);
-
- // Number of content rows: At least as many as given, but also at least
- // as many as needed to place each item.
- let r = {
- let len = cells.len();
- let given = tracks.y.len();
- let needed = len / c + (len % c).clamp(0, 1);
- given.max(needed)
- };
-
- let has_gutter = gutter.any(|tracks| !tracks.is_empty());
- let auto = Sizing::Auto;
- let zero = Sizing::Rel(Rel::zero());
- let get_or = |tracks: &[_], idx, default| {
- tracks.get(idx).or(tracks.last()).copied().unwrap_or(default)
- };
-
- // Collect content and gutter columns.
- for x in 0..c {
- cols.push(get_or(tracks.x, x, auto));
- if has_gutter {
- cols.push(get_or(gutter.x, x, zero));
- }
- }
-
- // Collect content and gutter rows.
- for y in 0..r {
- rows.push(get_or(tracks.y, y, auto));
- if has_gutter {
- rows.push(get_or(gutter.y, y, zero));
- }
- }
-
- // Remove superfluous gutter tracks.
- if has_gutter {
- cols.pop();
- rows.pop();
- }
-
- // Reverse for RTL.
- let is_rtl = TextElem::dir_in(styles) == Dir::RTL;
- if is_rtl {
- cols.reverse();
- }
-
- // We use these regions for auto row measurement. Since at that moment,
- // columns are already sized, we can enable horizontal expansion.
- let mut regions = regions;
- regions.expand = Axes::new(true, false);
-
- Self {
- cells,
- is_rtl,
- has_gutter,
- rows,
- regions,
- styles,
- rcols: vec![Abs::zero(); cols.len()],
- cols,
- width: Abs::zero(),
- rrows: vec![],
- lrows: vec![],
- initial: regions.size,
- finished: vec![],
- span,
- }
- }
-
- /// Determines the columns sizes and then layouts the grid row-by-row.
- pub fn layout(mut self, vt: &mut Vt) -> SourceResult<GridLayout> {
- self.measure_columns(vt)?;
-
- for y in 0..self.rows.len() {
- // Skip to next region if current one is full, but only for content
- // rows, not for gutter rows.
- if self.regions.is_full() && (!self.has_gutter || y % 2 == 0) {
- self.finish_region(vt)?;
- }
-
- match self.rows[y] {
- Sizing::Auto => self.layout_auto_row(vt, y)?,
- Sizing::Rel(v) => self.layout_relative_row(vt, v, y)?,
- Sizing::Fr(v) => self.lrows.push(Row::Fr(v, y)),
- }
- }
-
- self.finish_region(vt)?;
-
- Ok(GridLayout {
- fragment: Fragment::frames(self.finished),
- cols: self.rcols,
- rows: self.rrows,
- })
- }
-
- /// Determine all column sizes.
- #[tracing::instrument(name = "GridLayouter::measure_columns", skip_all)]
- fn measure_columns(&mut self, vt: &mut Vt) -> SourceResult<()> {
- // Sum of sizes of resolved relative tracks.
- let mut rel = Abs::zero();
-
- // Sum of fractions of all fractional tracks.
- let mut fr = Fr::zero();
-
- // Resolve the size of all relative columns and compute the sum of all
- // fractional tracks.
- for (&col, rcol) in self.cols.iter().zip(&mut self.rcols) {
- match col {
- Sizing::Auto => {}
- Sizing::Rel(v) => {
- let resolved =
- v.resolve(self.styles).relative_to(self.regions.base().x);
- *rcol = resolved;
- rel += resolved;
- }
- Sizing::Fr(v) => fr += v,
- }
- }
-
- // Size that is not used by fixed-size columns.
- let available = self.regions.size.x - rel;
- if available >= Abs::zero() {
- // Determine size of auto columns.
- let (auto, count) = self.measure_auto_columns(vt, available)?;
-
- // If there is remaining space, distribute it to fractional columns,
- // otherwise shrink auto columns.
- let remaining = available - auto;
- if remaining >= Abs::zero() {
- self.grow_fractional_columns(remaining, fr);
- } else {
- self.shrink_auto_columns(available, count);
- }
- }
-
- // Sum up the resolved column sizes once here.
- self.width = self.rcols.iter().sum();
-
- Ok(())
- }
-
- /// Measure the size that is available to auto columns.
- fn measure_auto_columns(
- &mut self,
- vt: &mut Vt,
- available: Abs,
- ) -> SourceResult<(Abs, usize)> {
- let mut auto = Abs::zero();
- let mut count = 0;
-
- // Determine size of auto columns by laying out all cells in those
- // columns, measuring them and finding the largest one.
- for (x, &col) in self.cols.iter().enumerate() {
- if col != Sizing::Auto {
- continue;
- }
-
- let mut resolved = Abs::zero();
- for y in 0..self.rows.len() {
- if let Some(cell) = self.cell(x, y) {
- // For relative rows, we can already resolve the correct
- // base and for auto and fr we could only guess anyway.
- let height = match self.rows[y] {
- Sizing::Rel(v) => {
- v.resolve(self.styles).relative_to(self.regions.base().y)
- }
- _ => self.regions.base().y,
- };
-
- let size = Size::new(available, height);
- let pod = Regions::one(size, Axes::splat(false));
- let frame = cell.measure(vt, self.styles, pod)?.into_frame();
- resolved.set_max(frame.width());
- }
- }
-
- self.rcols[x] = resolved;
- auto += resolved;
- count += 1;
- }
-
- Ok((auto, count))
- }
-
- /// Distribute remaining space to fractional columns.
- fn grow_fractional_columns(&mut self, remaining: Abs, fr: Fr) {
- if fr.is_zero() {
- return;
- }
-
- for (&col, rcol) in self.cols.iter().zip(&mut self.rcols) {
- if let Sizing::Fr(v) = col {
- *rcol = v.share(fr, remaining);
- }
- }
- }
-
- /// Redistribute space to auto columns so that each gets a fair share.
- fn shrink_auto_columns(&mut self, available: Abs, count: usize) {
- let mut last;
- let mut fair = -Abs::inf();
- let mut redistribute = available;
- let mut overlarge = count;
- let mut changed = true;
-
- // Iteratively remove columns that don't need to be shrunk.
- while changed && overlarge > 0 {
- changed = false;
- last = fair;
- fair = redistribute / (overlarge as f64);
-
- for (&col, &rcol) in self.cols.iter().zip(&self.rcols) {
- // Remove an auto column if it is not overlarge (rcol <= fair),
- // but also hasn't already been removed (rcol > last).
- if col == Sizing::Auto && rcol <= fair && rcol > last {
- redistribute -= rcol;
- overlarge -= 1;
- changed = true;
- }
- }
- }
-
- // Redistribute space fairly among overlarge columns.
- for (&col, rcol) in self.cols.iter().zip(&mut self.rcols) {
- if col == Sizing::Auto && *rcol > fair {
- *rcol = fair;
- }
- }
- }
-
- /// Layout a row with automatic height. Such a row may break across multiple
- /// regions.
- fn layout_auto_row(&mut self, vt: &mut Vt, y: usize) -> SourceResult<()> {
- // Determine the size for each region of the row. If the first region
- // ends up empty for some column, skip the region and remeasure.
- let mut resolved = match self.measure_auto_row(vt, y, true)? {
- Some(resolved) => resolved,
- None => {
- self.finish_region(vt)?;
- self.measure_auto_row(vt, y, false)?.unwrap()
- }
- };
-
- // Nothing to layout.
- if resolved.is_empty() {
- return Ok(());
- }
-
- // Layout into a single region.
- if let &[first] = resolved.as_slice() {
- let frame = self.layout_single_row(vt, first, y)?;
- self.push_row(frame, y);
- return Ok(());
- }
-
- // Expand all but the last region.
- // Skip the first region if the space is eaten up by an fr row.
- let len = resolved.len();
- for (region, target) in self
- .regions
- .iter()
- .zip(&mut resolved[..len - 1])
- .skip(self.lrows.iter().any(|row| matches!(row, Row::Fr(..))) as usize)
- {
- target.set_max(region.y);
- }
-
- // Layout into multiple regions.
- let fragment = self.layout_multi_row(vt, &resolved, y)?;
- let len = fragment.len();
- for (i, frame) in fragment.into_iter().enumerate() {
- self.push_row(frame, y);
- if i + 1 < len {
- self.finish_region(vt)?;
- }
- }
-
- Ok(())
- }
-
- /// Measure the regions sizes of an auto row. The option is always `Some(_)`
- /// if `can_skip` is false.
- fn measure_auto_row(
- &mut self,
- vt: &mut Vt,
- y: usize,
- can_skip: bool,
- ) -> SourceResult<Option<Vec<Abs>>> {
- let mut resolved: Vec<Abs> = vec![];
-
- for (x, &rcol) in self.rcols.iter().enumerate() {
- if let Some(cell) = self.cell(x, y) {
- let mut pod = self.regions;
- pod.size.x = rcol;
-
- let frames = cell.measure(vt, self.styles, pod)?.into_frames();
-
- // Skip the first region if one cell in it is empty. Then,
- // remeasure.
- if let [first, rest @ ..] = frames.as_slice() {
- if can_skip
- && first.is_empty()
- && rest.iter().any(|frame| !frame.is_empty())
- {
- return Ok(None);
- }
- }
-
- let mut sizes = frames.iter().map(|frame| frame.height());
- for (target, size) in resolved.iter_mut().zip(&mut sizes) {
- target.set_max(size);
- }
-
- // New heights are maximal by virtue of being new. Note that
- // this extend only uses the rest of the sizes iterator.
- resolved.extend(sizes);
- }
- }
-
- Ok(Some(resolved))
- }
-
- /// Layout a row with relative height. Such a row cannot break across
- /// multiple regions, but it may force a region break.
- fn layout_relative_row(
- &mut self,
- vt: &mut Vt,
- v: Rel<Length>,
- y: usize,
- ) -> SourceResult<()> {
- let resolved = v.resolve(self.styles).relative_to(self.regions.base().y);
- let frame = self.layout_single_row(vt, resolved, y)?;
-
- // Skip to fitting region.
- let height = frame.height();
- while !self.regions.size.y.fits(height) && !self.regions.in_last() {
- self.finish_region(vt)?;
-
- // Don't skip multiple regions for gutter and don't push a row.
- if self.has_gutter && y % 2 == 1 {
- return Ok(());
- }
- }
-
- self.push_row(frame, y);
-
- Ok(())
- }
-
- /// Layout a row with fixed height and return its frame.
- fn layout_single_row(
- &mut self,
- vt: &mut Vt,
- height: Abs,
- y: usize,
- ) -> SourceResult<Frame> {
- if !height.is_finite() {
- bail!(error!(self.span, "cannot create grid with infinite height"));
- }
-
- let mut output = Frame::soft(Size::new(self.width, height));
- let mut pos = Point::zero();
-
- for (x, &rcol) in self.rcols.iter().enumerate() {
- if let Some(cell) = self.cell(x, y) {
- let size = Size::new(rcol, height);
- let mut pod = Regions::one(size, Axes::splat(true));
- if self.rows[y] == Sizing::Auto {
- pod.full = self.regions.full;
- }
- let frame = cell.layout(vt, self.styles, pod)?.into_frame();
- output.push_frame(pos, frame);
- }
-
- pos.x += rcol;
- }
-
- Ok(output)
- }
-
- /// Layout a row spanning multiple regions.
- fn layout_multi_row(
- &mut self,
- vt: &mut Vt,
- heights: &[Abs],
- y: usize,
- ) -> SourceResult<Fragment> {
- // Prepare frames.
- let mut outputs: Vec<_> = heights
- .iter()
- .map(|&h| Frame::soft(Size::new(self.width, h)))
- .collect();
-
- // Prepare regions.
- let size = Size::new(self.width, heights[0]);
- let mut pod = Regions::one(size, Axes::splat(true));
- pod.full = self.regions.full;
- pod.backlog = &heights[1..];
-
- // Layout the row.
- let mut pos = Point::zero();
- for (x, &rcol) in self.rcols.iter().enumerate() {
- if let Some(cell) = self.cell(x, y) {
- pod.size.x = rcol;
-
- // Push the layouted frames into the individual output frames.
- let fragment = cell.layout(vt, self.styles, pod)?;
- for (output, frame) in outputs.iter_mut().zip(fragment) {
- output.push_frame(pos, frame);
- }
- }
-
- pos.x += rcol;
- }
-
- Ok(Fragment::frames(outputs))
- }
-
- /// Push a row frame into the current region.
- fn push_row(&mut self, frame: Frame, y: usize) {
- self.regions.size.y -= frame.height();
- self.lrows.push(Row::Frame(frame, y));
- }
-
- /// Finish rows for one region.
- fn finish_region(&mut self, vt: &mut Vt) -> SourceResult<()> {
- // Determine the height of existing rows in the region.
- let mut used = Abs::zero();
- let mut fr = Fr::zero();
- for row in &self.lrows {
- match row {
- Row::Frame(frame, _) => used += frame.height(),
- Row::Fr(v, _) => fr += *v,
- }
- }
-
- // Determine the size of the grid in this region, expanding fully if
- // there are fr rows.
- let mut size = Size::new(self.width, used).min(self.initial);
- if fr.get() > 0.0 && self.initial.y.is_finite() {
- size.y = self.initial.y;
- }
-
- // The frame for the region.
- let mut output = Frame::soft(size);
- let mut pos = Point::zero();
- let mut rrows = vec![];
-
- // Place finished rows and layout fractional rows.
- for row in std::mem::take(&mut self.lrows) {
- let (frame, y) = match row {
- Row::Frame(frame, y) => (frame, y),
- Row::Fr(v, y) => {
- let remaining = self.regions.full - used;
- let height = v.share(fr, remaining);
- (self.layout_single_row(vt, height, y)?, y)
- }
- };
-
- let height = frame.height();
- output.push_frame(pos, frame);
- rrows.push(RowPiece { height, y });
- pos.y += height;
- }
-
- self.finished.push(output);
- self.rrows.push(rrows);
- self.regions.next();
- self.initial = self.regions.size;
-
- Ok(())
- }
-
- /// Get the content of the cell in column `x` and row `y`.
- ///
- /// Returns `None` if it's a gutter cell.
- #[track_caller]
- fn cell(&self, mut x: usize, y: usize) -> Option<&'a Content> {
- assert!(x < self.cols.len());
- assert!(y < self.rows.len());
-
- // Columns are reorder, but the cell slice is not.
- if self.is_rtl {
- x = self.cols.len() - 1 - x;
- }
-
- if self.has_gutter {
- // Even columns and rows are children, odd ones are gutter.
- if x % 2 == 0 && y % 2 == 0 {
- let c = 1 + self.cols.len() / 2;
- self.cells.get((y / 2) * c + x / 2)
- } else {
- None
- }
- } else {
- let c = self.cols.len();
- self.cells.get(y * c + x)
- }
- }
-}
diff --git a/crates/typst-library/src/layout/hide.rs b/crates/typst-library/src/layout/hide.rs
deleted file mode 100644
index af3d0631..00000000
--- a/crates/typst-library/src/layout/hide.rs
+++ /dev/null
@@ -1,29 +0,0 @@
-use smallvec::smallvec;
-
-use crate::prelude::*;
-
-/// Hides content without affecting layout.
-///
-/// The `hide` function allows you to hide content while the layout still 'sees'
-/// it. This is useful to create whitespace that is exactly as large as some
-/// content. It may also be useful to redact content because its arguments are
-/// not included in the output.
-///
-/// # Example
-/// ```example
-/// Hello Jane \
-/// #hide[Hello] Joe
-/// ```
-#[elem(Show)]
-pub struct HideElem {
- /// The content to hide.
- #[required]
- pub body: Content,
-}
-
-impl Show for HideElem {
- #[tracing::instrument(name = "HideElem::show", skip(self))]
- fn show(&self, _: &mut Vt, _: StyleChain) -> SourceResult<Content> {
- Ok(self.body().clone().styled(MetaElem::set_data(smallvec![Meta::Hide])))
- }
-}
diff --git a/crates/typst-library/src/layout/list.rs b/crates/typst-library/src/layout/list.rs
deleted file mode 100644
index 7c089760..00000000
--- a/crates/typst-library/src/layout/list.rs
+++ /dev/null
@@ -1,238 +0,0 @@
-use crate::layout::{BlockElem, GridLayouter, ParElem, Sizing, Spacing};
-use crate::prelude::*;
-use crate::text::TextElem;
-
-/// A bullet list.
-///
-/// Displays a sequence of items vertically, with each item introduced by a
-/// marker.
-///
-/// # Example
-/// ```example
-/// Normal list.
-/// - Text
-/// - Math
-/// - Layout
-/// - ...
-///
-/// Multiple lines.
-/// - This list item spans multiple
-/// lines because it is indented.
-///
-/// Function call.
-/// #list(
-/// [Foundations],
-/// [Calculate],
-/// [Construct],
-/// [Data Loading],
-/// )
-/// ```
-///
-/// # 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.
-#[elem(scope, title = "Bullet List", Layout)]
-pub struct ListElem {
- /// 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
- /// each other, this is set to `{true}`; if items are separated by a blank
- /// line, this is set to `{false}`.
- ///
- /// ```example
- /// - If a list has a lot of text, and
- /// maybe other inline content, it
- /// should not be tight anymore.
- ///
- /// - To make a list wide, simply insert
- /// a blank line between the items.
- /// ```
- #[default(true)]
- pub tight: bool,
-
- /// The marker which introduces each item.
- ///
- /// Instead of plain content, you can also pass an array with multiple
- /// markers that should be used for nested lists. If the list nesting depth
- /// exceeds the number of markers, the last one is repeated. For total
- /// control, you may pass a function that maps the list's nesting depth
- /// (starting from `{0}`) to a desired marker.
- ///
- /// ```example
- /// #set list(marker: [--])
- /// - A more classic list
- /// - With en-dashes
- ///
- /// #set list(marker: ([•], [--]))
- /// - Top-level
- /// - Nested
- /// - Items
- /// - Items
- /// ```
- #[borrowed]
- #[default(ListMarker::Content(vec![TextElem::packed('•')]))]
- pub marker: ListMarker,
-
- /// The indent of each item.
- #[resolve]
- pub indent: Length,
-
- /// The spacing between the marker and the body of each item.
- #[resolve]
- #[default(Em::new(0.5).into())]
- pub body_indent: Length,
-
- /// The spacing between the items of a wide (non-tight) list.
- ///
- /// If set to `{auto}`, uses the spacing [below blocks]($block.below).
- pub spacing: Smart<Spacing>,
-
- /// The bullet list's children.
- ///
- /// When using the list syntax, adjacent items are automatically collected
- /// into lists, even through constructs like for loops.
- ///
- /// ```example
- /// #for letter in "ABC" [
- /// - Letter #letter
- /// ]
- /// ```
- #[variadic]
- pub children: Vec<ListItem>,
-
- /// The nesting depth.
- #[internal]
- #[fold]
- depth: Depth,
-}
-
-#[scope]
-impl ListElem {
- #[elem]
- type ListItem;
-}
-
-impl Layout for ListElem {
- #[tracing::instrument(name = "ListElem::layout", skip_all)]
- fn layout(
- &self,
- vt: &mut Vt,
- styles: StyleChain,
- regions: Regions,
- ) -> SourceResult<Fragment> {
- let indent = self.indent(styles);
- let body_indent = self.body_indent(styles);
- let gutter = if self.tight(styles) {
- ParElem::leading_in(styles).into()
- } else {
- self.spacing(styles)
- .unwrap_or_else(|| *BlockElem::below_in(styles).amount())
- };
-
- let depth = self.depth(styles);
- let marker = self
- .marker(styles)
- .resolve(vt, depth)?
- // avoid '#set align' interference with the list
- .aligned(HAlign::Start + VAlign::Top);
-
- let mut cells = vec![];
- for item in self.children() {
- cells.push(Content::empty());
- cells.push(marker.clone());
- cells.push(Content::empty());
- cells.push(item.body().clone().styled(Self::set_depth(Depth)));
- }
-
- let layouter = GridLayouter::new(
- Axes::with_x(&[
- Sizing::Rel(indent.into()),
- Sizing::Auto,
- Sizing::Rel(body_indent.into()),
- Sizing::Auto,
- ]),
- Axes::with_y(&[gutter.into()]),
- &cells,
- regions,
- styles,
- self.span(),
- );
-
- Ok(layouter.layout(vt)?.fragment)
- }
-}
-
-/// A bullet list item.
-#[elem(name = "item", title = "Bullet List Item")]
-pub struct ListItem {
- /// The item's body.
- #[required]
- pub body: Content,
-}
-
-cast! {
- ListItem,
- v: Content => v.to::<Self>().cloned().unwrap_or_else(|| Self::new(v.clone())),
-}
-
-/// A list's marker.
-#[derive(Debug, Clone, PartialEq, Hash)]
-pub enum ListMarker {
- Content(Vec<Content>),
- Func(Func),
-}
-
-impl ListMarker {
- /// Resolve the marker for the given depth.
- fn resolve(&self, vt: &mut Vt, depth: usize) -> SourceResult<Content> {
- Ok(match self {
- Self::Content(list) => {
- list.get(depth).or(list.last()).cloned().unwrap_or_default()
- }
- Self::Func(func) => func.call_vt(vt, [depth])?.display(),
- })
- }
-}
-
-cast! {
- ListMarker,
- self => match self {
- Self::Content(vec) => if vec.len() == 1 {
- vec.into_iter().next().unwrap().into_value()
- } else {
- vec.into_value()
- },
- Self::Func(func) => func.into_value(),
- },
- v: Content => Self::Content(vec![v]),
- array: Array => {
- if array.is_empty() {
- bail!("array must contain at least one marker");
- }
- Self::Content(array.into_iter().map(Value::display).collect())
- },
- v: Func => Self::Func(v),
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Hash)]
-struct Depth;
-
-cast! {
- Depth,
- self => Value::None,
- _: Value => Self,
-}
-
-impl Fold for Depth {
- type Output = usize;
-
- fn fold(self, outer: Self::Output) -> Self::Output {
- outer + 1
- }
-}
diff --git a/crates/typst-library/src/layout/measure.rs b/crates/typst-library/src/layout/measure.rs
deleted file mode 100644
index d41b7f95..00000000
--- a/crates/typst-library/src/layout/measure.rs
+++ /dev/null
@@ -1,53 +0,0 @@
-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 combine `measure`
-/// and [`layout`]($layout).
-///
-/// # 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.
-///
-/// ```example
-/// #let content = [Hello!]
-/// #content
-/// #set text(14pt)
-/// #content
-/// ```
-///
-/// To do a meaningful measurement, you therefore first need to retrieve the
-/// active styles with the [`style`]($style) function. You can then pass them to
-/// the `measure` function.
-///
-/// ```example
-/// #let thing(body) = style(styles => {
-/// let size = measure(body, styles)
-/// [Width of "#body" is #size.width]
-/// })
-///
-/// #thing[Hey] \
-/// #thing[Welcome]
-/// ```
-///
-/// The measure function returns a dictionary with the entries `width` and
-/// `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,
-) -> SourceResult<Dict> {
- let pod = Regions::one(Axes::splat(Abs::inf()), Axes::splat(false));
- let styles = StyleChain::new(&styles);
- let frame = content.measure(&mut vm.vt, styles, pod)?.into_frame();
- let Size { x, y } = frame.size();
- Ok(dict! { "width" => x, "height" => y })
-}
diff --git a/crates/typst-library/src/layout/mod.rs b/crates/typst-library/src/layout/mod.rs
deleted file mode 100644
index bcbe3b2b..00000000
--- a/crates/typst-library/src/layout/mod.rs
+++ /dev/null
@@ -1,786 +0,0 @@
-//! Composable layouts.
-
-mod align;
-mod columns;
-mod container;
-#[path = "enum.rs"]
-mod enum_;
-mod flow;
-mod fragment;
-mod grid;
-mod hide;
-mod list;
-#[path = "measure.rs"]
-mod measure_;
-mod pad;
-mod page;
-mod par;
-mod place;
-mod regions;
-mod repeat;
-mod spacing;
-mod stack;
-mod table;
-mod terms;
-mod transform;
-
-pub use self::align::*;
-pub use self::columns::*;
-pub use self::container::*;
-pub use self::enum_::*;
-pub use self::flow::*;
-pub use self::fragment::*;
-pub use self::grid::*;
-pub use self::hide::*;
-pub use self::list::*;
-pub use self::measure_::*;
-pub use self::pad::*;
-pub use self::page::*;
-pub use self::par::*;
-pub use self::place::*;
-pub use self::regions::*;
-pub use self::repeat::*;
-pub use self::spacing::*;
-pub use self::stack::*;
-pub use self::table::*;
-pub use self::terms::*;
-pub use self::transform::*;
-
-use std::borrow::Cow;
-use std::mem;
-
-use typed_arena::Arena;
-use typst::diag::SourceResult;
-use typst::eval::Tracer;
-use typst::model::{applicable, realize, DelayedErrors, StyleVecBuilder};
-
-use crate::math::{EquationElem, LayoutMath};
-use crate::meta::{CiteElem, CiteGroup, DocumentElem};
-use crate::prelude::*;
-use crate::shared::BehavedBuilder;
-use crate::text::{LinebreakElem, SmartquoteElem, SpaceElem, TextElem};
-use crate::visualize::{
- CircleElem, EllipseElem, ImageElem, LineElem, PathElem, PolygonElem, RectElem,
- SquareElem,
-};
-
-/// Hook up all layout definitions.
-pub(super) fn define(global: &mut Scope) {
- 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.
-pub trait LayoutRoot {
- /// Layout into one frame per page.
- fn layout_root(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Document>;
-}
-
-impl LayoutRoot for Content {
- #[tracing::instrument(name = "Content::layout_root", skip_all)]
- fn layout_root(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Document> {
- #[comemo::memoize]
- fn cached(
- content: &Content,
- world: Tracked<dyn World + '_>,
- introspector: Tracked<Introspector>,
- locator: Tracked<Locator>,
- delayed: TrackedMut<DelayedErrors>,
- tracer: TrackedMut<Tracer>,
- styles: StyleChain,
- ) -> SourceResult<Document> {
- let mut locator = Locator::chained(locator);
- let mut vt = Vt {
- world,
- introspector,
- locator: &mut locator,
- delayed,
- tracer,
- };
- let scratch = Scratch::default();
- let (realized, styles) = realize_root(&mut vt, &scratch, content, styles)?;
- realized
- .with::<dyn LayoutRoot>()
- .unwrap()
- .layout_root(&mut vt, styles)
- }
-
- tracing::info!("Starting layout");
- cached(
- self,
- vt.world,
- vt.introspector,
- vt.locator.track(),
- TrackedMut::reborrow_mut(&mut vt.delayed),
- TrackedMut::reborrow_mut(&mut vt.tracer),
- styles,
- )
- }
-}
-
-/// Layout into regions.
-pub trait Layout {
- /// Layout into one frame per region.
- fn layout(
- &self,
- vt: &mut Vt,
- styles: StyleChain,
- regions: Regions,
- ) -> SourceResult<Fragment>;
-
- /// Layout without side effects.
- ///
- /// This element must be layouted again in the same order for the results to
- /// be valid.
- #[tracing::instrument(name = "Layout::measure", skip_all)]
- fn measure(
- &self,
- vt: &mut Vt,
- styles: StyleChain,
- regions: Regions,
- ) -> SourceResult<Fragment> {
- let mut locator = Locator::chained(vt.locator.track());
- let mut vt = Vt {
- world: vt.world,
- introspector: vt.introspector,
- locator: &mut locator,
- tracer: TrackedMut::reborrow_mut(&mut vt.tracer),
- delayed: TrackedMut::reborrow_mut(&mut vt.delayed),
- };
- self.layout(&mut vt, styles, regions)
- }
-}
-
-impl Layout for Content {
- #[tracing::instrument(name = "Content::layout", skip_all)]
- fn layout(
- &self,
- vt: &mut Vt,
- styles: StyleChain,
- regions: Regions,
- ) -> SourceResult<Fragment> {
- #[allow(clippy::too_many_arguments)]
- #[comemo::memoize]
- fn cached(
- content: &Content,
- world: Tracked<dyn World + '_>,
- introspector: Tracked<Introspector>,
- locator: Tracked<Locator>,
- delayed: TrackedMut<DelayedErrors>,
- tracer: TrackedMut<Tracer>,
- styles: StyleChain,
- regions: Regions,
- ) -> SourceResult<Fragment> {
- let mut locator = Locator::chained(locator);
- let mut vt = Vt {
- world,
- introspector,
- locator: &mut locator,
- delayed,
- tracer,
- };
- let scratch = Scratch::default();
- let (realized, styles) = realize_block(&mut vt, &scratch, content, styles)?;
- realized
- .with::<dyn Layout>()
- .unwrap()
- .layout(&mut vt, styles, regions)
- }
-
- tracing::info!("Layouting `Content`");
-
- let fragment = cached(
- self,
- vt.world,
- vt.introspector,
- vt.locator.track(),
- TrackedMut::reborrow_mut(&mut vt.delayed),
- TrackedMut::reborrow_mut(&mut vt.tracer),
- styles,
- regions,
- )?;
-
- vt.locator.visit_frames(&fragment);
- Ok(fragment)
- }
-}
-
-/// Realize into an element that is capable of root-level layout.
-#[tracing::instrument(skip_all)]
-fn realize_root<'a>(
- vt: &mut Vt,
- scratch: &'a Scratch<'a>,
- content: &'a Content,
- styles: StyleChain<'a>,
-) -> SourceResult<(Cow<'a, Content>, StyleChain<'a>)> {
- if content.can::<dyn LayoutRoot>() && !applicable(content, styles) {
- return Ok((Cow::Borrowed(content), styles));
- }
-
- let mut builder = Builder::new(vt, scratch, true);
- builder.accept(content, styles)?;
- builder.interrupt_page(Some(styles), true)?;
- let (pages, shared) = builder.doc.unwrap().pages.finish();
- Ok((Cow::Owned(DocumentElem::new(pages.to_vec()).pack()), shared))
-}
-
-/// Realize into an element that is capable of block-level layout.
-#[tracing::instrument(skip_all)]
-fn realize_block<'a>(
- vt: &mut Vt,
- scratch: &'a Scratch<'a>,
- content: &'a Content,
- styles: StyleChain<'a>,
-) -> SourceResult<(Cow<'a, Content>, StyleChain<'a>)> {
- // These elements implement `Layout` but still require a flow for
- // proper layout.
- if content.can::<dyn Layout>()
- && !content.is::<BoxElem>()
- && !content.is::<LineElem>()
- && !content.is::<RectElem>()
- && !content.is::<SquareElem>()
- && !content.is::<EllipseElem>()
- && !content.is::<CircleElem>()
- && !content.is::<ImageElem>()
- && !content.is::<PolygonElem>()
- && !content.is::<PathElem>()
- && !content.is::<PlaceElem>()
- && !applicable(content, styles)
- {
- return Ok((Cow::Borrowed(content), styles));
- }
-
- let mut builder = Builder::new(vt, scratch, false);
- builder.accept(content, styles)?;
- builder.interrupt_par()?;
- let (children, shared) = builder.flow.0.finish();
- Ok((Cow::Owned(FlowElem::new(children.to_vec()).pack()), shared))
-}
-
-/// Builds a document or a flow element from content.
-struct Builder<'a, 'v, 't> {
- /// The virtual typesetter.
- vt: &'v mut Vt<'t>,
- /// Scratch arenas for building.
- scratch: &'a Scratch<'a>,
- /// The current document building state.
- doc: Option<DocBuilder<'a>>,
- /// The current flow building state.
- flow: FlowBuilder<'a>,
- /// The current paragraph building state.
- par: ParBuilder<'a>,
- /// The current list building state.
- list: ListBuilder<'a>,
- /// The current citation grouping state.
- cites: CiteGroupBuilder<'a>,
-}
-
-/// Temporary storage arenas for building.
-#[derive(Default)]
-struct Scratch<'a> {
- /// An arena where intermediate style chains are stored.
- styles: Arena<StyleChain<'a>>,
- /// An arena where intermediate content resulting from show rules is stored.
- content: Arena<Content>,
-}
-
-impl<'a, 'v, 't> Builder<'a, 'v, 't> {
- fn new(vt: &'v mut Vt<'t>, scratch: &'a Scratch<'a>, top: bool) -> Self {
- Self {
- vt,
- scratch,
- doc: top.then(DocBuilder::default),
- flow: FlowBuilder::default(),
- par: ParBuilder::default(),
- list: ListBuilder::default(),
- cites: CiteGroupBuilder::default(),
- }
- }
-
- fn accept(
- &mut self,
- mut content: &'a Content,
- styles: StyleChain<'a>,
- ) -> SourceResult<()> {
- if content.can::<dyn LayoutMath>() && !content.is::<EquationElem>() {
- content =
- self.scratch.content.alloc(EquationElem::new(content.clone()).pack());
- }
-
- if let Some(realized) = realize(self.vt, content, styles)? {
- let stored = self.scratch.content.alloc(realized);
- return self.accept(stored, styles);
- }
-
- if let Some((elem, local)) = content.to_styled() {
- return self.styled(elem, local, styles);
- }
-
- if let Some(children) = content.to_sequence() {
- for elem in children {
- self.accept(elem, styles)?;
- }
- return Ok(());
- }
-
- if self.cites.accept(content, styles) {
- return Ok(());
- }
-
- self.interrupt_cites()?;
-
- if self.list.accept(content, styles) {
- return Ok(());
- }
-
- self.interrupt_list()?;
-
- if self.list.accept(content, styles) {
- return Ok(());
- }
-
- if self.par.accept(content, styles) {
- return Ok(());
- }
-
- self.interrupt_par()?;
-
- if self.flow.accept(content, styles) {
- return Ok(());
- }
-
- let keep = content
- .to::<PagebreakElem>()
- .map_or(false, |pagebreak| !pagebreak.weak(styles));
-
- self.interrupt_page(keep.then_some(styles), false)?;
-
- if let Some(doc) = &mut self.doc {
- if doc.accept(content, styles) {
- return Ok(());
- }
- }
-
- if content.is::<PagebreakElem>() {
- bail!(content.span(), "pagebreaks are not allowed inside of containers");
- } else {
- bail!(content.span(), "{} is not allowed here", content.func().name());
- }
- }
-
- fn styled(
- &mut self,
- elem: &'a Content,
- map: &'a Styles,
- styles: StyleChain<'a>,
- ) -> SourceResult<()> {
- let stored = self.scratch.styles.alloc(styles);
- let styles = stored.chain(map);
- self.interrupt_style(map, None)?;
- self.accept(elem, styles)?;
- self.interrupt_style(map, Some(styles))?;
- Ok(())
- }
-
- fn interrupt_style(
- &mut self,
- local: &Styles,
- outer: Option<StyleChain<'a>>,
- ) -> SourceResult<()> {
- if let Some(Some(span)) = local.interruption::<DocumentElem>() {
- if self.doc.is_none() {
- bail!(span, "document set rules are not allowed inside of containers");
- }
- if outer.is_none()
- && (!self.flow.0.is_empty()
- || !self.par.0.is_empty()
- || !self.list.items.is_empty())
- {
- bail!(span, "document set rules must appear before any content");
- }
- } else if let Some(Some(span)) = local.interruption::<PageElem>() {
- if self.doc.is_none() {
- bail!(span, "page configuration is not allowed inside of containers");
- }
- self.interrupt_page(outer, false)?;
- } else if local.interruption::<ParElem>().is_some()
- || local.interruption::<AlignElem>().is_some()
- {
- self.interrupt_par()?;
- } else if local.interruption::<ListElem>().is_some()
- || local.interruption::<EnumElem>().is_some()
- || local.interruption::<TermsElem>().is_some()
- {
- self.interrupt_list()?;
- }
- Ok(())
- }
-
- fn interrupt_cites(&mut self) -> SourceResult<()> {
- if !self.cites.items.is_empty() {
- let staged = mem::take(&mut self.cites.staged);
- let (group, styles) = mem::take(&mut self.cites).finish();
- let stored = self.scratch.content.alloc(group);
- self.accept(stored, styles)?;
- for (content, styles) in staged {
- self.accept(content, styles)?;
- }
- }
- Ok(())
- }
-
- fn interrupt_list(&mut self) -> SourceResult<()> {
- self.interrupt_cites()?;
- if !self.list.items.is_empty() {
- let staged = mem::take(&mut self.list.staged);
- let (list, styles) = mem::take(&mut self.list).finish();
- let stored = self.scratch.content.alloc(list);
- self.accept(stored, styles)?;
- for (content, styles) in staged {
- self.accept(content, styles)?;
- }
- }
- Ok(())
- }
-
- fn interrupt_par(&mut self) -> SourceResult<()> {
- self.interrupt_list()?;
- if !self.par.0.is_empty() {
- let (par, styles) = mem::take(&mut self.par).finish();
- let stored = self.scratch.content.alloc(par);
- self.accept(stored, styles)?;
- }
-
- Ok(())
- }
-
- fn interrupt_page(
- &mut self,
- styles: Option<StyleChain<'a>>,
- last: bool,
- ) -> SourceResult<()> {
- self.interrupt_par()?;
- let Some(doc) = &mut self.doc else { return Ok(()) };
- if (doc.keep_next && styles.is_some()) || self.flow.0.has_strong_elements(last) {
- let (flow, shared) = mem::take(&mut self.flow).0.finish();
- let styles = if shared == StyleChain::default() {
- styles.unwrap_or_default()
- } else {
- shared
- };
- let page = PageElem::new(FlowElem::new(flow.to_vec()).pack());
- let stored = self.scratch.content.alloc(page.pack());
- self.accept(stored, styles)?;
- }
- Ok(())
- }
-}
-
-/// Accepts pagebreaks and pages.
-struct DocBuilder<'a> {
- /// The page runs built so far.
- pages: StyleVecBuilder<'a, Cow<'a, Content>>,
- /// Whether to keep a following page even if it is empty.
- keep_next: bool,
- /// Whether the next page should be cleared to an even or odd number.
- clear_next: Option<Parity>,
-}
-
-impl<'a> DocBuilder<'a> {
- fn accept(&mut self, content: &'a Content, styles: StyleChain<'a>) -> bool {
- if let Some(pagebreak) = content.to::<PagebreakElem>() {
- self.keep_next = !pagebreak.weak(styles);
- self.clear_next = pagebreak.to(styles);
- return true;
- }
-
- if let Some(page) = content.to::<PageElem>() {
- let elem = if let Some(clear_to) = self.clear_next.take() {
- let mut page = page.clone();
- page.push_clear_to(Some(clear_to));
- Cow::Owned(page.pack())
- } else {
- Cow::Borrowed(content)
- };
-
- self.pages.push(elem, styles);
- self.keep_next = false;
- return true;
- }
-
- false
- }
-}
-
-impl Default for DocBuilder<'_> {
- fn default() -> Self {
- Self {
- pages: StyleVecBuilder::new(),
- keep_next: true,
- clear_next: None,
- }
- }
-}
-
-/// Accepts flow content.
-#[derive(Default)]
-struct FlowBuilder<'a>(BehavedBuilder<'a>, bool);
-
-impl<'a> FlowBuilder<'a> {
- fn accept(&mut self, content: &'a Content, styles: StyleChain<'a>) -> bool {
- if content.is::<ParbreakElem>() {
- self.1 = true;
- return true;
- }
-
- let last_was_parbreak = self.1;
- self.1 = false;
-
- if content.is::<VElem>()
- || content.is::<ColbreakElem>()
- || content.is::<MetaElem>()
- || content.is::<PlaceElem>()
- {
- self.0.push(Cow::Borrowed(content), styles);
- return true;
- }
-
- if content.can::<dyn Layout>() || content.is::<ParElem>() {
- let is_tight_list = if let Some(elem) = content.to::<ListElem>() {
- elem.tight(styles)
- } else if let Some(elem) = content.to::<EnumElem>() {
- elem.tight(styles)
- } else if let Some(elem) = content.to::<TermsElem>() {
- elem.tight(styles)
- } else {
- false
- };
-
- if !last_was_parbreak && is_tight_list {
- let leading = ParElem::leading_in(styles);
- let spacing = VElem::list_attach(leading.into());
- self.0.push(Cow::Owned(spacing.pack()), styles);
- }
-
- let (above, below) = if let Some(block) = content.to::<BlockElem>() {
- (block.above(styles), block.below(styles))
- } else {
- (BlockElem::above_in(styles), BlockElem::below_in(styles))
- };
-
- self.0.push(Cow::Owned(above.pack()), styles);
- self.0.push(Cow::Borrowed(content), styles);
- self.0.push(Cow::Owned(below.pack()), styles);
- return true;
- }
-
- false
- }
-}
-
-/// Accepts paragraph content.
-#[derive(Default)]
-struct ParBuilder<'a>(BehavedBuilder<'a>);
-
-impl<'a> ParBuilder<'a> {
- fn accept(&mut self, content: &'a Content, styles: StyleChain<'a>) -> bool {
- if content.is::<MetaElem>() {
- if self.0.has_strong_elements(false) {
- self.0.push(Cow::Borrowed(content), styles);
- return true;
- }
- } else if content.is::<SpaceElem>()
- || content.is::<TextElem>()
- || content.is::<HElem>()
- || content.is::<LinebreakElem>()
- || content.is::<SmartquoteElem>()
- || content.to::<EquationElem>().map_or(false, |elem| !elem.block(styles))
- || content.is::<BoxElem>()
- {
- self.0.push(Cow::Borrowed(content), styles);
- return true;
- }
-
- false
- }
-
- fn finish(self) -> (Content, StyleChain<'a>) {
- let (children, shared) = self.0.finish();
-
- // Find the first span that isn't detached.
- let span = children
- .iter()
- .map(|(cnt, _)| cnt.span())
- .find(|span| !span.is_detached())
- .unwrap_or_else(Span::detached);
-
- (ParElem::new(children.to_vec()).spanned(span).pack(), shared)
- }
-}
-
-/// Accepts list / enum items, spaces, paragraph breaks.
-struct ListBuilder<'a> {
- /// The list items collected so far.
- items: StyleVecBuilder<'a, Cow<'a, Content>>,
- /// Whether the list contains no paragraph breaks.
- tight: bool,
- /// Trailing content for which it is unclear whether it is part of the list.
- staged: Vec<(&'a Content, StyleChain<'a>)>,
-}
-
-impl<'a> ListBuilder<'a> {
- fn accept(&mut self, content: &'a Content, styles: StyleChain<'a>) -> bool {
- if !self.items.is_empty()
- && (content.is::<SpaceElem>() || content.is::<ParbreakElem>())
- {
- self.staged.push((content, styles));
- return true;
- }
-
- if (content.is::<ListItem>()
- || content.is::<EnumItem>()
- || content.is::<TermItem>())
- && self
- .items
- .elems()
- .next()
- .map_or(true, |first| first.func() == content.func())
- {
- self.items.push(Cow::Borrowed(content), styles);
- self.tight &= self.staged.drain(..).all(|(t, _)| !t.is::<ParbreakElem>());
- return true;
- }
-
- false
- }
-
- fn finish(self) -> (Content, StyleChain<'a>) {
- let (items, shared) = self.items.finish();
- let item = items.items().next().unwrap();
- let output = if item.is::<ListItem>() {
- ListElem::new(
- items
- .iter()
- .map(|(item, local)| {
- let item = item.to::<ListItem>().unwrap();
- item.clone()
- .with_body(item.body().clone().styled_with_map(local.clone()))
- })
- .collect::<Vec<_>>(),
- )
- .with_tight(self.tight)
- .pack()
- } else if item.is::<EnumItem>() {
- EnumElem::new(
- items
- .iter()
- .map(|(item, local)| {
- let item = item.to::<EnumItem>().unwrap();
- item.clone()
- .with_body(item.body().clone().styled_with_map(local.clone()))
- })
- .collect::<Vec<_>>(),
- )
- .with_tight(self.tight)
- .pack()
- } else if item.is::<TermItem>() {
- TermsElem::new(
- items
- .iter()
- .map(|(item, local)| {
- let item = item.to::<TermItem>().unwrap();
- item.clone()
- .with_term(item.term().clone().styled_with_map(local.clone()))
- .with_description(
- item.description().clone().styled_with_map(local.clone()),
- )
- })
- .collect::<Vec<_>>(),
- )
- .with_tight(self.tight)
- .pack()
- } else {
- unreachable!()
- };
- (output, shared)
- }
-}
-
-impl Default for ListBuilder<'_> {
- fn default() -> Self {
- Self {
- items: StyleVecBuilder::default(),
- tight: true,
- staged: vec![],
- }
- }
-}
-
-/// Accepts citations.
-#[derive(Default)]
-struct CiteGroupBuilder<'a> {
- /// The styles.
- styles: StyleChain<'a>,
- /// The citations.
- items: Vec<CiteElem>,
- /// Trailing content for which it is unclear whether it is part of the list.
- staged: Vec<(&'a Content, StyleChain<'a>)>,
-}
-
-impl<'a> CiteGroupBuilder<'a> {
- fn accept(&mut self, content: &'a Content, styles: StyleChain<'a>) -> bool {
- if !self.items.is_empty()
- && (content.is::<SpaceElem>() || content.is::<MetaElem>())
- {
- self.staged.push((content, styles));
- return true;
- }
-
- if let Some(citation) = content.to::<CiteElem>() {
- if self.items.is_empty() {
- self.styles = styles;
- }
- self.staged.retain(|(elem, _)| !elem.is::<SpaceElem>());
- self.items.push(citation.clone());
- return true;
- }
-
- false
- }
-
- fn finish(self) -> (Content, StyleChain<'a>) {
- let span = self.items.first().map(|cite| cite.span()).unwrap_or(Span::detached());
- (CiteGroup::new(self.items).pack().spanned(span), self.styles)
- }
-}
diff --git a/crates/typst-library/src/layout/pad.rs b/crates/typst-library/src/layout/pad.rs
deleted file mode 100644
index d1b0cb1f..00000000
--- a/crates/typst-library/src/layout/pad.rs
+++ /dev/null
@@ -1,121 +0,0 @@
-use crate::prelude::*;
-
-/// Adds spacing around content.
-///
-/// The spacing can be specified for each side individually, or for all sides at
-/// once by specifying a positional argument.
-///
-/// # Example
-/// ```example
-/// #set align(center)
-///
-/// #pad(x: 16pt, image("typing.jpg"))
-/// _Typing speeds can be
-/// measured in words per minute._
-/// ```
-#[elem(title = "Padding", Layout)]
-pub struct PadElem {
- /// The padding at the left side.
- #[parse(
- let all = args.named("rest")?.or(args.find()?);
- let x = args.named("x")?.or(all);
- let y = args.named("y")?.or(all);
- args.named("left")?.or(x)
- )]
- pub left: Rel<Length>,
-
- /// The padding at the top side.
- #[parse(args.named("top")?.or(y))]
- pub top: Rel<Length>,
-
- /// The padding at the right side.
- #[parse(args.named("right")?.or(x))]
- pub right: Rel<Length>,
-
- /// The padding at the bottom side.
- #[parse(args.named("bottom")?.or(y))]
- pub bottom: Rel<Length>,
-
- /// The horizontal padding. Both `left` and `right` take precedence over
- /// this.
- #[external]
- pub x: Rel<Length>,
-
- /// The vertical padding. Both `top` and `bottom` take precedence over this.
- #[external]
- pub y: Rel<Length>,
-
- /// The padding for all sides. All other parameters take precedence over
- /// this.
- #[external]
- pub rest: Rel<Length>,
-
- /// The content to pad at the sides.
- #[required]
- pub body: Content,
-}
-
-impl Layout for PadElem {
- #[tracing::instrument(name = "PadElem::layout", skip_all)]
- fn layout(
- &self,
- vt: &mut Vt,
- styles: StyleChain,
- regions: Regions,
- ) -> SourceResult<Fragment> {
- let sides = Sides::new(
- self.left(styles),
- self.top(styles),
- self.right(styles),
- self.bottom(styles),
- );
-
- // Layout child into padded regions.
- let mut backlog = vec![];
- let padding = sides.resolve(styles);
- let pod = regions.map(&mut backlog, |size| shrink(size, padding));
- let mut fragment = self.body().layout(vt, styles, pod)?;
-
- for frame in &mut fragment {
- // Apply the padding inversely such that the grown size padded
- // yields the frame's size.
- let padded = grow(frame.size(), padding);
- let padding = padding.relative_to(padded);
- let offset = Point::new(padding.left, padding.top);
-
- // Grow the frame and translate everything in the frame inwards.
- frame.set_size(padded);
- frame.translate(offset);
- }
-
- Ok(fragment)
- }
-}
-
-/// Shrink a size by padding relative to the size itself.
-fn shrink(size: Size, padding: Sides<Rel<Abs>>) -> Size {
- size - padding.relative_to(size).sum_by_axis()
-}
-
-/// Grow a size by padding relative to the grown size.
-/// This is the inverse operation to `shrink()`.
-///
-/// For the horizontal axis the derivation looks as follows.
-/// (Vertical axis is analogous.)
-///
-/// Let w be the grown target width,
-/// s be the given width,
-/// l be the left padding,
-/// r be the right padding,
-/// p = l + r.
-///
-/// We want that: w - l.resolve(w) - r.resolve(w) = s
-///
-/// Thus: w - l.resolve(w) - r.resolve(w) = s
-/// <=> w - p.resolve(w) = s
-/// <=> w - p.rel * w - p.abs = s
-/// <=> (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_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
deleted file mode 100644
index 53646c7d..00000000
--- a/crates/typst-library/src/layout/page.rs
+++ /dev/null
@@ -1,949 +0,0 @@
-use std::borrow::Cow;
-use std::ptr;
-use std::str::FromStr;
-
-use typst::eval::AutoValue;
-
-use crate::layout::{AlignElem, ColumnsElem};
-use crate::meta::{Counter, CounterKey, ManualPageCounter, Numbering};
-use crate::prelude::*;
-use crate::text::TextElem;
-
-/// Layouts its child onto one or multiple pages.
-///
-/// Although this function is primarily used in set rules to affect page
-/// properties, it can also be used to explicitly render its argument onto
-/// a set of pages of its own.
-///
-/// Pages can be set to use `{auto}` as their width or height. In this case, the
-/// pages will grow to fit their content on the respective axis.
-///
-/// 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
-/// >>> #set page(margin: auto)
-/// #set page("us-letter")
-///
-/// There you go, US friends!
-/// ```
-#[elem]
-pub struct PageElem {
- /// A standard paper size to set width and height.
- #[external]
- #[default(Paper::A4)]
- pub paper: Paper,
-
- /// The width of the page.
- ///
- /// ```example
- /// #set page(
- /// width: 3cm,
- /// margin: (x: 0cm),
- /// )
- ///
- /// #for i in range(3) {
- /// box(square(width: 1cm))
- /// }
- /// ```
- #[resolve]
- #[parse(
- let paper = args.named_or_find::<Paper>("paper")?;
- args.named("width")?
- .or_else(|| paper.map(|paper| Smart::Custom(paper.width().into())))
- )]
- #[default(Smart::Custom(Paper::A4.width().into()))]
- pub width: Smart<Length>,
-
- /// The height of the page.
- ///
- /// If this is set to `{auto}`, page breaks can only be triggered manually
- /// 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")?
- .or_else(|| paper.map(|paper| Smart::Custom(paper.height().into())))
- )]
- #[default(Smart::Custom(Paper::A4.height().into()))]
- pub height: Smart<Length>,
-
- /// Whether the page is flipped into landscape orientation.
- ///
- /// ```example
- /// #set page(
- /// "us-business-card",
- /// flipped: true,
- /// fill: rgb("f2e5dd"),
- /// )
- ///
- /// #set align(bottom + end)
- /// #text(14pt)[*Sam H. Richards*] \
- /// _Procurement Manager_
- ///
- /// #set text(10pt)
- /// 17 Main Street \
- /// New York, NY 10001 \
- /// +1 555 555 5555
- /// ```
- #[default(false)]
- pub flipped: bool,
-
- /// The page's margins.
- ///
- /// - `{auto}`: The margins are set automatically to 2.5/21 times the smaller
- /// dimension of the page. This results in 2.5cm margins for an A4 page.
- /// - A single length: The same margin on all sides.
- /// - A dictionary: With a dictionary, the margins can be set individually.
- /// The dictionary can contain the following keys in order of precedence:
- /// - `top`: The top margin.
- /// - `right`: The right margin.
- /// - `bottom`: The bottom margin.
- /// - `left`: The left margin.
- /// - `inside`: The margin at the inner side of the page (where the
- /// [binding]($page.binding) is).
- /// - `outside`: The margin at the outer side of the page (opposite to the
- /// [binding]($page.binding)).
- /// - `x`: The horizontal margins.
- /// - `y`: The vertical margins.
- /// - `rest`: The margins on all sides except those for which the
- /// dictionary explicitly sets a size.
- ///
- /// The values for `left` and `right` are mutually exclusive with
- /// the values for `inside` and `outside`.
- ///
- /// ```example
- /// #set page(
- /// width: 3cm,
- /// height: 4cm,
- /// margin: (x: 8pt, y: 4pt),
- /// )
- ///
- /// #rect(
- /// width: 100%,
- /// height: 100%,
- /// fill: aqua,
- /// )
- /// ```
- #[fold]
- pub margin: Margin,
-
- /// On which side the pages will be bound.
- ///
- /// - `{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.
- ///
- /// This affects the meaning of the `inside` and `outside` options for
- /// margins.
- pub binding: Smart<Binding>,
-
- /// 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]($columns).
- ///
- /// ```example:single
- /// #set page(columns: 2, height: 4.8cm)
- /// Climate change is one of the most
- /// pressing issues of our time, with
- /// the potential to devastate
- /// communities, ecosystems, and
- /// economies around the world. It's
- /// clear that we need to take urgent
- /// action to reduce our carbon
- /// emissions and mitigate the impacts
- /// of a rapidly changing climate.
- /// ```
- #[default(NonZeroUsize::ONE)]
- pub columns: NonZeroUsize,
-
- /// The page's background color.
- ///
- /// This instructs the printer to color the complete page with the given
- /// color. If you are considering larger production runs, it may be more
- /// environmentally friendly and cost-effective to source pre-dyed pages and
- /// not set this property.
- ///
- /// ```example
- /// #set page(fill: rgb("444352"))
- /// #set text(fill: rgb("fdfdfd"))
- /// *Dark mode enabled.*
- /// ```
- #[borrowed]
- pub fill: Option<Paint>,
-
- /// How to [number]($numbering) the pages.
- ///
- /// If an explicit `footer` (or `header` for top-aligned numbering) is
- /// given, the numbering is ignored.
- ///
- /// ```example
- /// #set page(
- /// height: 100pt,
- /// margin: (top: 16pt, bottom: 24pt),
- /// numbering: "1 / 1",
- /// )
- ///
- /// #lorem(48)
- /// ```
- #[borrowed]
- pub numbering: Option<Numbering>,
-
- /// The alignment of the page numbering.
- ///
- /// If the vertical component is `top`, the numbering is placed into the
- /// header and if it is `bottom`, it is placed in the footer. Horizon
- /// alignment is forbidden. If an explicit matching `header` or `footer` is
- /// given, the numbering is ignored.
- ///
- /// ```example
- /// #set page(
- /// margin: (top: 16pt, bottom: 24pt),
- /// numbering: "1",
- /// number-align: right,
- /// )
- ///
- /// #lorem(30)
- /// ```
- #[default(HAlign::Center + VAlign::Bottom)]
- #[parse({
- 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");
- }
- }
- option.map(|spanned| spanned.v)
- })]
- pub number_align: Align,
-
- /// The page's header. Fills the top margin of each page.
- ///
- /// ```example
- /// #set par(justify: true)
- /// #set page(
- /// margin: (top: 32pt, bottom: 20pt),
- /// header: [
- /// #set text(8pt)
- /// #smallcaps[Typst Academcy]
- /// #h(1fr) _Exercise Sheet 3_
- /// ],
- /// )
- ///
- /// #lorem(19)
- /// ```
- #[borrowed]
- pub header: Option<Content>,
-
- /// The amount the header is raised into the top margin.
- #[resolve]
- #[default(Ratio::new(0.3).into())]
- pub header_ascent: Rel<Length>,
-
- /// The page's footer. Fills the bottom margin of each page.
- ///
- /// 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]($counter).
- ///
- /// ```example
- /// #set par(justify: true)
- /// #set page(
- /// height: 100pt,
- /// margin: 20pt,
- /// footer: [
- /// #set align(right)
- /// #set text(8pt)
- /// #counter(page).display(
- /// "1 of I",
- /// both: true,
- /// )
- /// ]
- /// )
- ///
- /// #lorem(48)
- /// ```
- #[borrowed]
- pub footer: Option<Content>,
-
- /// The amount the footer is lowered into the bottom margin.
- #[resolve]
- #[default(Ratio::new(0.3).into())]
- pub footer_descent: Rel<Length>,
-
- /// Content in the page's background.
- ///
- /// This content will be placed behind the page's body. It can be
- /// used to place a background image or a watermark.
- ///
- /// ```example
- /// #set page(background: rotate(24deg,
- /// text(18pt, fill: rgb("FFCBC4"))[
- /// *CONFIDENTIAL*
- /// ]
- /// ))
- ///
- /// = Typst's secret plans
- /// In the year 2023, we plan to take
- /// over the world (of typesetting).
- /// ```
- #[borrowed]
- pub background: Option<Content>,
-
- /// Content in the page's foreground.
- ///
- /// This content will overlay the page's body.
- ///
- /// ```example
- /// #set page(foreground: text(24pt)[🥸])
- ///
- /// Reviewer 2 has marked our paper
- /// "Weak Reject" because they did
- /// not understand our approach...
- /// ```
- #[borrowed]
- pub foreground: Option<Content>,
-
- /// The contents of the page(s).
- ///
- /// Multiple pages will be created if the content does not fit on a single
- /// page. A new page with the page properties prior to the function invocation
- /// will be created after the body has been typeset.
- #[required]
- pub body: Content,
-
- /// Whether the page should be aligned to an even or odd page.
- #[internal]
- pub clear_to: Option<Parity>,
-}
-
-impl PageElem {
- /// A document can consist of multiple `PageElem`s, one per run of pages
- /// with equal properties (not one per actual output page!). The `number` is
- /// the physical page number of the first page of this run. It is mutated
- /// while we post-process the pages in this function. This function returns
- /// a fragment consisting of multiple frames, one per output page of this
- /// page run.
- #[tracing::instrument(skip_all)]
- pub fn layout(
- &self,
- vt: &mut Vt,
- styles: StyleChain,
- page_counter: &mut ManualPageCounter,
- extend_to: Option<Parity>,
- ) -> SourceResult<Fragment> {
- tracing::info!("Page layout");
-
- // When one of the lengths is infinite the page fits its content along
- // that axis.
- let width = self.width(styles).unwrap_or(Abs::inf());
- let height = self.height(styles).unwrap_or(Abs::inf());
- let mut size = Size::new(width, height);
- if self.flipped(styles) {
- std::mem::swap(&mut size.x, &mut size.y);
- }
-
- let mut min = width.min(height);
- if !min.is_finite() {
- min = Paper::A4.width();
- }
-
- // Determine the margins.
- let default = Rel::<Length>::from((2.5 / 21.0) * min);
- let margin = self.margin(styles);
- let two_sided = margin.two_sided.unwrap_or(false);
- let margin = margin
- .sides
- .map(|side| side.and_then(Smart::as_custom).unwrap_or(default))
- .resolve(styles)
- .relative_to(size);
-
- // Determine the binding.
- let binding =
- self.binding(styles)
- .unwrap_or_else(|| match TextElem::dir_in(styles) {
- Dir::LTR => Binding::Left,
- _ => Binding::Right,
- });
-
- // Realize columns.
- let mut child = self.body().clone();
- let columns = self.columns(styles);
- if columns.get() > 1 {
- child = ColumnsElem::new(child).with_count(columns).pack();
- }
-
- let area = size - margin.sum_by_axis();
- let mut regions = Regions::repeat(area, area.map(Abs::is_finite));
- regions.root = true;
-
- // Layout the child.
- let mut frames = child.layout(vt, styles, regions)?.into_frames();
-
- // Align the child to the pagebreak's parity.
- // Check for page count after adding the pending frames
- if extend_to
- .is_some_and(|p| !p.matches(page_counter.physical().get() + frames.len()))
- {
- // Insert empty page after the current pages.
- let size = area.map(Abs::is_finite).select(area, Size::zero());
- frames.push(Frame::hard(size));
- }
-
- let fill = self.fill(styles);
- let foreground = Cow::Borrowed(self.foreground(styles));
- let background = Cow::Borrowed(self.background(styles));
- let header_ascent = self.header_ascent(styles);
- let footer_descent = self.footer_descent(styles);
- let numbering = self.numbering(styles);
- let numbering_meta = Meta::PageNumbering(numbering.clone().into_value());
- let number_align = self.number_align(styles);
- let mut header = Cow::Borrowed(self.header(styles));
- let mut footer = Cow::Borrowed(self.footer(styles));
-
- // Construct the numbering (for header or footer).
- let numbering_marginal = Cow::Owned(numbering.as_ref().map(|numbering| {
- let both = match numbering {
- Numbering::Pattern(pattern) => pattern.pieces() >= 2,
- Numbering::Func(_) => true,
- };
-
- let mut counter =
- Counter::new(CounterKey::Page).display(Some(numbering.clone()), both);
-
- // 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(x.into());
- }
-
- counter
- }));
-
- if matches!(number_align.y(), Some(VAlign::Top)) {
- header = if header.is_some() { header } else { numbering_marginal };
- } else {
- footer = if footer.is_some() { footer } else { numbering_marginal };
- }
-
- // Post-process pages.
- for frame in frames.iter_mut() {
- tracing::info!("Layouting page #{}", page_counter.physical());
-
- // The padded width of the page's content without margins.
- let pw = frame.width();
-
- // If two sided, left becomes inside and right becomes outside.
- // Thus, for left-bound pages, we want to swap on even pages and
- // for right-bound pages, we want to swap on odd pages.
- let mut margin = margin;
- if two_sided && binding.swap(page_counter.physical()) {
- std::mem::swap(&mut margin.left, &mut margin.right);
- }
-
- // Realize margins.
- frame.set_size(frame.size() + margin.sum_by_axis());
- frame.translate(Point::new(margin.left, margin.top));
- frame.push_positionless_meta(numbering_meta.clone());
-
- // The page size with margins.
- let size = frame.size();
-
- // Realize overlays.
- for (name, marginal) in [
- ("header", &header),
- ("footer", &footer),
- ("background", &background),
- ("foreground", &foreground),
- ] {
- tracing::info!("Layouting {name}");
-
- let Some(content) = &**marginal else { continue };
-
- let (pos, area, align);
- if ptr::eq(marginal, &header) {
- let ascent = header_ascent.relative_to(margin.top);
- pos = Point::with_x(margin.left);
- area = Size::new(pw, margin.top - ascent);
- 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;
- } else {
- pos = Point::zero();
- area = size;
- align = HAlign::Center + VAlign::Horizon;
- };
-
- let pod = Regions::one(area, Axes::splat(true));
- let sub = content
- .clone()
- .styled(AlignElem::set_alignment(align))
- .layout(vt, styles, pod)?
- .into_frame();
-
- if ptr::eq(marginal, &header) || ptr::eq(marginal, &background) {
- frame.prepend_frame(pos, sub);
- } else {
- frame.push_frame(pos, sub);
- }
- }
-
- if let Some(fill) = fill {
- frame.fill(fill.clone());
- }
-
- page_counter.visit(vt, frame)?;
-
- // Add a PDF page label if there is a numbering.
- if let Some(num) = numbering {
- if let Some(page_label) = num.apply_pdf(page_counter.logical()) {
- frame.push_positionless_meta(Meta::PdfPageLabel(page_label));
- }
- }
-
- page_counter.step();
- }
-
- Ok(Fragment::frames(frames))
- }
-}
-
-/// Specification of the page's margins.
-#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash)]
-pub struct Margin {
- /// The margins for each side.
- pub sides: Sides<Option<Smart<Rel<Length>>>>,
- /// Whether to swap `left` and `right` to make them `inside` and `outside`
- /// (when to swap depends on the binding).
- pub two_sided: Option<bool>,
-}
-
-impl Margin {
- /// Create an instance with four equal components.
- pub fn splat(value: Option<Smart<Rel<Length>>>) -> Self {
- Self { sides: Sides::splat(value), two_sided: None }
- }
-}
-
-impl Fold for Margin {
- type Output = Margin;
-
- fn fold(self, outer: Self::Output) -> Self::Output {
- let sides =
- self.sides
- .zip(outer.sides)
- .map(|(inner, outer)| match (inner, outer) {
- (Some(value), Some(outer)) => Some(value.fold(outer)),
- _ => inner.or(outer),
- });
- let two_sided = self.two_sided.or(outer.two_sided);
- Margin { sides, two_sided }
- }
-}
-
-cast! {
- Margin,
- self => {
- let mut dict = Dict::new();
- let mut handle = |key: &str, component: Value| {
- let value = component.into_value();
- if value != Value::None {
- dict.insert(key.into(), value);
- }
- };
-
- handle("top", self.sides.top.into_value());
- handle("bottom", self.sides.bottom.into_value());
- if self.two_sided.unwrap_or(false) {
- handle("inside", self.sides.left.into_value());
- handle("outside", self.sides.right.into_value());
- } else {
- handle("left", self.sides.left.into_value());
- handle("right", self.sides.right.into_value());
- }
-
- Value::Dict(dict)
- },
- _: AutoValue => Self::splat(Some(Smart::Auto)),
- v: Rel<Length> => Self::splat(Some(Smart::Custom(v))),
- mut dict: Dict => {
- let mut take = |key| dict.take(key).ok().map(Value::cast).transpose();
-
- let rest = take("rest")?;
- let x = take("x")?.or(rest);
- let y = take("y")?.or(rest);
- let top = take("top")?.or(y);
- let bottom = take("bottom")?.or(y);
- let outside = take("outside")?;
- let inside = take("inside")?;
- let left = take("left")?;
- let right = take("right")?;
-
- let implicitly_two_sided = outside.is_some() || inside.is_some();
- let implicitly_not_two_sided = left.is_some() || right.is_some();
- if implicitly_two_sided && implicitly_not_two_sided {
- bail!("`inside` and `outside` are mutually exclusive with `left` and `right`");
- }
-
- // - If 'implicitly_two_sided' is false here, then
- // 'implicitly_not_two_sided' will be guaranteed to be true
- // due to the previous two 'if' conditions.
- // - If both are false, this means that this margin change does not
- // affect lateral margins, and thus shouldn't make a difference on
- // the 'two_sided' attribute of this margin.
- let two_sided = (implicitly_two_sided || implicitly_not_two_sided)
- .then_some(implicitly_two_sided);
-
- dict.finish(&[
- "left", "top", "right", "bottom", "outside", "inside", "x", "y", "rest",
- ])?;
-
- Margin {
- sides: Sides {
- left: inside.or(left).or(x),
- top,
- right: outside.or(right).or(x),
- bottom,
- },
- two_sided,
- }
- }
-}
-
-/// Specification of the page's binding.
-#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
-pub enum Binding {
- /// Bound on the left, as customary in LTR languages.
- Left,
- /// Bound on the right, as customary in RTL languages.
- Right,
-}
-
-impl Binding {
- /// Whether to swap left and right margin for the page with this number.
- fn swap(self, number: NonZeroUsize) -> bool {
- match self {
- // Left-bound must swap on even pages
- // (because it is correct on the first page).
- Self::Left => number.get() % 2 == 0,
- // Right-bound must swap on odd pages
- // (because it is wrong on the first page).
- Self::Right => number.get() % 2 == 1,
- }
- }
-}
-
-cast! {
- Binding,
- self => match self {
- Self::Left => Align::LEFT.into_value(),
- Self::Right => Align::RIGHT.into_value(),
- },
- v: Align => match v {
- Align::LEFT => Self::Left,
- Align::RIGHT => Self::Right,
- _ => bail!("must be `left` or `right`"),
- },
-}
-
-/// A header, footer, foreground or background definition.
-#[derive(Debug, Clone, Hash)]
-pub enum Marginal {
- /// Bare content.
- Content(Content),
- /// A closure mapping from a page number to content.
- Func(Func),
-}
-
-impl Marginal {
- /// Resolve the marginal based on the page number.
- pub fn resolve(&self, vt: &mut Vt, page: usize) -> SourceResult<Cow<'_, Content>> {
- Ok(match self {
- Self::Content(content) => Cow::Borrowed(content),
- Self::Func(func) => Cow::Owned(func.call_vt(vt, [page])?.display()),
- })
- }
-}
-
-cast! {
- Marginal,
- self => match self {
- Self::Content(v) => v.into_value(),
- Self::Func(v) => v.into_value(),
- },
- v: Content => Self::Content(v),
- v: Func => Self::Func(v),
-}
-
-/// A manual page break.
-///
-/// Must not be used inside any containers.
-///
-/// # Example
-/// ```example
-/// The next page contains
-/// more details on compound theory.
-/// #pagebreak()
-///
-/// == Compound Theory
-/// In 1984, the first ...
-/// ```
-#[elem(title = "Page Break")]
-pub struct PagebreakElem {
- /// If `{true}`, the page break is skipped if the current page is already
- /// empty.
- #[default(false)]
- pub weak: bool,
-
- /// If given, ensures that the next page will be an even/odd page, with an
- /// empty page in between if necessary.
- ///
- /// ```example
- /// #set page(height: 30pt)
- ///
- /// First.
- /// #pagebreak(to: "odd")
- /// Third.
- /// ```
- pub to: Option<Parity>,
-}
-
-/// Whether something should be even or odd.
-#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)]
-pub enum Parity {
- /// Next page will be an even page.
- Even,
- /// Next page will be an odd page.
- Odd,
-}
-
-impl Parity {
- /// Whether the given number matches the parity.
- fn matches(self, number: usize) -> bool {
- match self {
- Self::Even => number % 2 == 0,
- Self::Odd => number % 2 == 1,
- }
- }
-}
-
-/// Specification of a paper.
-#[derive(Debug, Copy, Clone, Hash)]
-pub struct Paper {
- /// The name of the paper.
- name: &'static str,
- /// The width of the paper in millimeters.
- width: Scalar,
- /// The height of the paper in millimeters.
- height: Scalar,
-}
-
-impl Paper {
- /// The width of the paper.
- pub fn width(self) -> Abs {
- Abs::mm(self.width.get())
- }
-
- /// The height of the paper.
- pub fn height(self) -> Abs {
- Abs::mm(self.height.get())
- }
-}
-
-/// Defines paper constants and a paper parsing implementation.
-macro_rules! papers {
- ($(($var:ident: $width:expr, $height: expr, $name:literal))*) => {
- /// Predefined papers.
- ///
- /// Each paper is parsable from its name in kebab-case.
- impl Paper {
- $(pub const $var: Self = Self {
- name: $name,
- width: Scalar::new($width),
- height: Scalar::new($height),
- };)*
- }
-
- impl FromStr for Paper {
- type Err = &'static str;
-
- fn from_str(name: &str) -> Result<Self, Self::Err> {
- match name.to_lowercase().as_str() {
- $($name => Ok(Self::$var),)*
- _ => Err("unknown paper size"),
- }
- }
- }
-
- cast! {
- Paper,
- self => self.name.into_value(),
- $(
- /// Produces a paper of the respective size.
- $name => Self::$var,
- )*
- }
- };
-}
-
-// All paper sizes in mm.
-//
-// Resources:
-// - https://papersizes.io/
-// - https://en.wikipedia.org/wiki/Paper_size
-// - https://www.theedkins.co.uk/jo/units/oldunits/print.htm
-// - https://vintagepaper.co/blogs/news/traditional-paper-sizes
-papers! {
- // ---------------------------------------------------------------------- //
- // ISO 216 A Series
- (A0: 841.0, 1189.0, "a0")
- (A1: 594.0, 841.0, "a1")
- (A2: 420.0, 594.0, "a2")
- (A3: 297.0, 420.0, "a3")
- (A4: 210.0, 297.0, "a4")
- (A5: 148.0, 210.0, "a5")
- (A6: 105.0, 148.0, "a6")
- (A7: 74.0, 105.0, "a7")
- (A8: 52.0, 74.0, "a8")
- (A9: 37.0, 52.0, "a9")
- (A10: 26.0, 37.0, "a10")
- (A11: 18.0, 26.0, "a11")
-
- // ISO 216 B Series
- (ISO_B1: 707.0, 1000.0, "iso-b1")
- (ISO_B2: 500.0, 707.0, "iso-b2")
- (ISO_B3: 353.0, 500.0, "iso-b3")
- (ISO_B4: 250.0, 353.0, "iso-b4")
- (ISO_B5: 176.0, 250.0, "iso-b5")
- (ISO_B6: 125.0, 176.0, "iso-b6")
- (ISO_B7: 88.0, 125.0, "iso-b7")
- (ISO_B8: 62.0, 88.0, "iso-b8")
-
- // ISO 216 C Series
- (ISO_C3: 324.0, 458.0, "iso-c3")
- (ISO_C4: 229.0, 324.0, "iso-c4")
- (ISO_C5: 162.0, 229.0, "iso-c5")
- (ISO_C6: 114.0, 162.0, "iso-c6")
- (ISO_C7: 81.0, 114.0, "iso-c7")
- (ISO_C8: 57.0, 81.0, "iso-c8")
-
- // DIN D Series (extension to ISO)
- (DIN_D3: 272.0, 385.0, "din-d3")
- (DIN_D4: 192.0, 272.0, "din-d4")
- (DIN_D5: 136.0, 192.0, "din-d5")
- (DIN_D6: 96.0, 136.0, "din-d6")
- (DIN_D7: 68.0, 96.0, "din-d7")
- (DIN_D8: 48.0, 68.0, "din-d8")
-
- // SIS (used in academia)
- (SIS_G5: 169.0, 239.0, "sis-g5")
- (SIS_E5: 115.0, 220.0, "sis-e5")
-
- // ANSI Extensions
- (ANSI_A: 216.0, 279.0, "ansi-a")
- (ANSI_B: 279.0, 432.0, "ansi-b")
- (ANSI_C: 432.0, 559.0, "ansi-c")
- (ANSI_D: 559.0, 864.0, "ansi-d")
- (ANSI_E: 864.0, 1118.0, "ansi-e")
-
- // ANSI Architectural Paper
- (ARCH_A: 229.0, 305.0, "arch-a")
- (ARCH_B: 305.0, 457.0, "arch-b")
- (ARCH_C: 457.0, 610.0, "arch-c")
- (ARCH_D: 610.0, 914.0, "arch-d")
- (ARCH_E1: 762.0, 1067.0, "arch-e1")
- (ARCH_E: 914.0, 1219.0, "arch-e")
-
- // JIS B Series
- (JIS_B0: 1030.0, 1456.0, "jis-b0")
- (JIS_B1: 728.0, 1030.0, "jis-b1")
- (JIS_B2: 515.0, 728.0, "jis-b2")
- (JIS_B3: 364.0, 515.0, "jis-b3")
- (JIS_B4: 257.0, 364.0, "jis-b4")
- (JIS_B5: 182.0, 257.0, "jis-b5")
- (JIS_B6: 128.0, 182.0, "jis-b6")
- (JIS_B7: 91.0, 128.0, "jis-b7")
- (JIS_B8: 64.0, 91.0, "jis-b8")
- (JIS_B9: 45.0, 64.0, "jis-b9")
- (JIS_B10: 32.0, 45.0, "jis-b10")
- (JIS_B11: 22.0, 32.0, "jis-b11")
-
- // SAC D Series
- (SAC_D0: 764.0, 1064.0, "sac-d0")
- (SAC_D1: 532.0, 760.0, "sac-d1")
- (SAC_D2: 380.0, 528.0, "sac-d2")
- (SAC_D3: 264.0, 376.0, "sac-d3")
- (SAC_D4: 188.0, 260.0, "sac-d4")
- (SAC_D5: 130.0, 184.0, "sac-d5")
- (SAC_D6: 92.0, 126.0, "sac-d6")
-
- // ISO 7810 ID
- (ISO_ID_1: 85.6, 53.98, "iso-id-1")
- (ISO_ID_2: 74.0, 105.0, "iso-id-2")
- (ISO_ID_3: 88.0, 125.0, "iso-id-3")
-
- // ---------------------------------------------------------------------- //
- // Asia
- (ASIA_F4: 210.0, 330.0, "asia-f4")
-
- // Japan
- (JP_SHIROKU_BAN_4: 264.0, 379.0, "jp-shiroku-ban-4")
- (JP_SHIROKU_BAN_5: 189.0, 262.0, "jp-shiroku-ban-5")
- (JP_SHIROKU_BAN_6: 127.0, 188.0, "jp-shiroku-ban-6")
- (JP_KIKU_4: 227.0, 306.0, "jp-kiku-4")
- (JP_KIKU_5: 151.0, 227.0, "jp-kiku-5")
- (JP_BUSINESS_CARD: 91.0, 55.0, "jp-business-card")
-
- // China
- (CN_BUSINESS_CARD: 90.0, 54.0, "cn-business-card")
-
- // Europe
- (EU_BUSINESS_CARD: 85.0, 55.0, "eu-business-card")
-
- // French Traditional (AFNOR)
- (FR_TELLIERE: 340.0, 440.0, "fr-tellière")
- (FR_COURONNE_ECRITURE: 360.0, 460.0, "fr-couronne-écriture")
- (FR_COURONNE_EDITION: 370.0, 470.0, "fr-couronne-édition")
- (FR_RAISIN: 500.0, 650.0, "fr-raisin")
- (FR_CARRE: 450.0, 560.0, "fr-carré")
- (FR_JESUS: 560.0, 760.0, "fr-jésus")
-
- // United Kingdom Imperial
- (UK_BRIEF: 406.4, 342.9, "uk-brief")
- (UK_DRAFT: 254.0, 406.4, "uk-draft")
- (UK_FOOLSCAP: 203.2, 330.2, "uk-foolscap")
- (UK_QUARTO: 203.2, 254.0, "uk-quarto")
- (UK_CROWN: 508.0, 381.0, "uk-crown")
- (UK_BOOK_A: 111.0, 178.0, "uk-book-a")
- (UK_BOOK_B: 129.0, 198.0, "uk-book-b")
-
- // Unites States
- (US_LETTER: 215.9, 279.4, "us-letter")
- (US_LEGAL: 215.9, 355.6, "us-legal")
- (US_TABLOID: 279.4, 431.8, "us-tabloid")
- (US_EXECUTIVE: 84.15, 266.7, "us-executive")
- (US_FOOLSCAP_FOLIO: 215.9, 342.9, "us-foolscap-folio")
- (US_STATEMENT: 139.7, 215.9, "us-statement")
- (US_LEDGER: 431.8, 279.4, "us-ledger")
- (US_OFICIO: 215.9, 340.36, "us-oficio")
- (US_GOV_LETTER: 203.2, 266.7, "us-gov-letter")
- (US_GOV_LEGAL: 215.9, 330.2, "us-gov-legal")
- (US_BUSINESS_CARD: 88.9, 50.8, "us-business-card")
- (US_DIGEST: 139.7, 215.9, "us-digest")
- (US_TRADE: 152.4, 228.6, "us-trade")
-
- // ---------------------------------------------------------------------- //
- // Other
- (NEWSPAPER_COMPACT: 280.0, 430.0, "newspaper-compact")
- (NEWSPAPER_BERLINER: 315.0, 470.0, "newspaper-berliner")
- (NEWSPAPER_BROADSHEET: 381.0, 578.0, "newspaper-broadsheet")
- (PRESENTATION_16_9: 297.0, 167.0625, "presentation-16-9")
- (PRESENTATION_4_3: 280.0, 210.0, "presentation-4-3")
-}
diff --git a/crates/typst-library/src/layout/par.rs b/crates/typst-library/src/layout/par.rs
deleted file mode 100644
index 2d7c9080..00000000
--- a/crates/typst-library/src/layout/par.rs
+++ /dev/null
@@ -1,1520 +0,0 @@
-use comemo::Prehashed;
-use typst::eval::Tracer;
-use typst::model::DelayedErrors;
-use unicode_bidi::{BidiInfo, Level as BidiLevel};
-use unicode_script::{Script, UnicodeScript};
-
-use crate::layout::{AlignElem, BoxElem, HElem, Sizing, Spacing};
-use crate::math::EquationElem;
-use crate::prelude::*;
-use crate::text::{
- breakpoints, char_is_cjk_script, is_gb_style, shape, Breakpoint, LinebreakElem,
- Quoter, Quotes, ShapedGlyph, ShapedText, SmartquoteElem, SpaceElem, TextElem,
- BEGIN_PUNCT_PAT, END_PUNCT_PAT,
-};
-
-/// Arranges text, spacing and inline-level elements into a paragraph.
-///
-/// Although this function is primarily used in set rules to affect paragraph
-/// properties, it can also be used to explicitly render its argument onto a
-/// paragraph of its own.
-///
-/// # Example
-/// ```example
-/// #show par: set block(spacing: 0.65em)
-/// #set par(
-/// first-line-indent: 1em,
-/// justify: true,
-/// )
-///
-/// We proceed by contradiction.
-/// Suppose that there exists a set
-/// of positive integers $a$, $b$, and
-/// $c$ that satisfies the equation
-/// $a^n + b^n = c^n$ for some
-/// integer value of $n > 2$.
-///
-/// Without loss of generality,
-/// let $a$ be the smallest of the
-/// three integers. Then, we ...
-/// ```
-#[elem(title = "Paragraph", Construct)]
-pub struct ParElem {
- /// The spacing between lines.
- #[resolve]
- #[ghost]
- #[default(Em::new(0.65).into())]
- pub leading: Length,
-
- /// Whether to justify text in its line.
- ///
- /// 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]($align) still has an effect on the
- /// placement of the last line except if it ends with a
- /// [justified line break]($linebreak.justify).
- #[ghost]
- #[default(false)]
- pub justify: bool,
-
- /// How to determine line breaks.
- ///
- /// When this property is set to `{auto}`, its default value, optimized line
- /// breaks will be used for justified paragraphs. Enabling optimized line
- /// breaks for ragged paragraphs may also be worthwhile to improve the
- /// appearance of the text.
- ///
- /// ```example
- /// #set page(width: 207pt)
- /// #set par(linebreaks: "simple")
- /// Some texts feature many longer
- /// words. Those are often exceedingly
- /// challenging to break in a visually
- /// pleasing way.
- ///
- /// #set par(linebreaks: "optimized")
- /// Some texts feature many longer
- /// words. Those are often exceedingly
- /// challenging to break in a visually
- /// pleasing way.
- /// ```
- #[ghost]
- pub linebreaks: Smart<Linebreaks>,
-
- /// The indent the first line of a paragraph should have.
- ///
- /// Only the first line of a consecutive paragraph will be indented (not
- /// the first one in a block or on the page).
- ///
- /// By typographic convention, paragraph breaks are indicated either by some
- /// space between paragraphs or by indented first lines. Consider reducing
- /// the [paragraph spacing]($block.spacing) to the [`leading`] when
- /// using this property (e.g. using
- /// `[#show par: set block(spacing: 0.65em)]`).
- #[ghost]
- pub first_line_indent: Length,
-
- /// The indent all but the first line of a paragraph should have.
- #[ghost]
- #[resolve]
- pub hanging_indent: Length,
-
- /// The contents of the paragraph.
- #[external]
- #[required]
- pub body: Content,
-
- /// The paragraph's children.
- #[internal]
- #[variadic]
- pub children: Vec<Prehashed<Content>>,
-}
-
-impl Construct for ParElem {
- fn construct(vm: &mut Vm, args: &mut Args) -> SourceResult<Content> {
- // The paragraph constructor is special: It doesn't create a paragraph
- // element. Instead, it just ensures that the passed content lives in a
- // separate paragraph and styles it.
- let styles = Self::set(vm, args)?;
- let body = args.expect::<Content>("body")?;
- Ok(Content::sequence([
- ParbreakElem::new().pack(),
- body.styled_with_map(styles),
- ParbreakElem::new().pack(),
- ]))
- }
-}
-
-impl ParElem {
- /// Layout the paragraph into a collection of lines.
- #[tracing::instrument(name = "ParElement::layout", skip_all)]
- pub fn layout(
- &self,
- vt: &mut Vt,
- styles: StyleChain,
- consecutive: bool,
- region: Size,
- expand: bool,
- ) -> SourceResult<Fragment> {
- #[comemo::memoize]
- #[allow(clippy::too_many_arguments)]
- fn cached(
- par: &ParElem,
- world: Tracked<dyn World + '_>,
- introspector: Tracked<Introspector>,
- locator: Tracked<Locator>,
- delayed: TrackedMut<DelayedErrors>,
- tracer: TrackedMut<Tracer>,
- styles: StyleChain,
- consecutive: bool,
- region: Size,
- expand: bool,
- ) -> SourceResult<Fragment> {
- let mut locator = Locator::chained(locator);
- let mut vt = Vt {
- world,
- introspector,
- locator: &mut locator,
- delayed,
- tracer,
- };
- let children = par.children();
-
- // Collect all text into one string for BiDi analysis.
- let (text, segments, spans) = collect(children, &styles, consecutive)?;
-
- // Perform BiDi analysis and then prepare paragraph layout by building a
- // representation on which we can do line breaking without layouting
- // each and every line from scratch.
- let p = prepare(&mut vt, children, &text, segments, spans, styles, region)?;
-
- // Break the paragraph into lines.
- let lines = linebreak(&vt, &p, region.x - p.hang);
-
- // Stack the lines into one frame per region.
- finalize(&mut vt, &p, &lines, region, expand)
- }
-
- let fragment = cached(
- self,
- vt.world,
- vt.introspector,
- vt.locator.track(),
- TrackedMut::reborrow_mut(&mut vt.delayed),
- TrackedMut::reborrow_mut(&mut vt.tracer),
- styles,
- consecutive,
- region,
- expand,
- )?;
-
- vt.locator.visit_frames(&fragment);
- Ok(fragment)
- }
-}
-
-/// How to determine line breaks in a paragraph.
-#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)]
-pub enum Linebreaks {
- /// Determine the line breaks in a simple first-fit style.
- Simple,
- /// Optimize the line breaks for the whole paragraph.
- ///
- /// Typst will try to produce more evenly filled lines of text by
- /// considering the whole paragraph when calculating line breaks.
- Optimized,
-}
-
-/// A paragraph break.
-///
-/// This starts a new paragraph. Especially useful when used within code like
-/// [for loops]($scripting/#loops). Multiple consecutive
-/// paragraph breaks collapse into a single one.
-///
-/// # Example
-/// ```example
-/// #for i in range(3) {
-/// [Blind text #i: ]
-/// lorem(5)
-/// parbreak()
-/// }
-/// ```
-///
-/// # Syntax
-/// Instead of calling this function, you can insert a blank line into your
-/// markup to create a paragraph break.
-#[elem(title = "Paragraph Break", Unlabellable)]
-pub struct ParbreakElem {}
-
-impl Unlabellable for ParbreakElem {}
-
-/// Range of a substring of text.
-type Range = std::ops::Range<usize>;
-
-// The characters by which spacing, inline content and pins are replaced in the
-// paragraph's full text.
-const SPACING_REPLACE: char = ' '; // Space
-const OBJ_REPLACE: char = '\u{FFFC}'; // Object Replacement Character
-
-/// A paragraph representation in which children are already layouted and text
-/// is already preshaped.
-///
-/// In many cases, we can directly reuse these results when constructing a line.
-/// Only when a line break falls onto a text index that is not safe-to-break per
-/// rustybuzz, we have to reshape that portion.
-pub(crate) struct Preparation<'a> {
- /// Bidirectional text embedding levels for the paragraph.
- pub bidi: BidiInfo<'a>,
- /// Text runs, spacing and layouted elements.
- pub items: Vec<Item<'a>>,
- /// The span mapper.
- pub spans: SpanMapper,
- /// Whether to hyphenate if it's the same for all children.
- pub hyphenate: Option<bool>,
- /// The text language if it's the same for all children.
- pub lang: Option<Lang>,
- /// The paragraph's resolved horizontal alignment.
- pub align: FixedAlign,
- /// Whether to justify the paragraph.
- pub justify: bool,
- /// The paragraph's hanging indent.
- pub hang: Abs,
- /// Whether to add spacing between CJK and Latin characters.
- pub cjk_latin_spacing: bool,
- /// Whether font fallback is enabled for this paragraph.
- pub fallback: bool,
- /// The leading of the paragraph.
- pub leading: Abs,
- /// How to determine line breaks.
- pub linebreaks: Smart<Linebreaks>,
- /// The text size.
- pub size: Abs,
-}
-
-impl<'a> Preparation<'a> {
- /// Find the item that contains the given `text_offset`.
- pub fn find(&self, text_offset: usize) -> Option<&Item<'a>> {
- let mut cursor = 0;
- for item in &self.items {
- let end = cursor + item.len();
- if (cursor..end).contains(&text_offset) {
- return Some(item);
- }
- cursor = end;
- }
- None
- }
-
- /// Return the items that intersect the given `text_range`.
- ///
- /// Returns the expanded range around the items and the items.
- pub fn slice(&self, text_range: Range) -> (Range, &[Item<'a>]) {
- let mut cursor = 0;
- let mut start = 0;
- let mut end = 0;
- let mut expanded = text_range.clone();
-
- for (i, item) in self.items.iter().enumerate() {
- if cursor <= text_range.start {
- start = i;
- expanded.start = cursor;
- }
-
- let len = item.len();
- if cursor < text_range.end || cursor + len <= text_range.end {
- end = i + 1;
- expanded.end = cursor + len;
- } else {
- break;
- }
-
- cursor += len;
- }
-
- (expanded, &self.items[start..end])
- }
-}
-
-/// A segment of one or multiple collapsed children.
-#[derive(Debug, Copy, Clone)]
-enum Segment<'a> {
- /// One or multiple collapsed text or text-equivalent children. Stores how
- /// long the segment is (in bytes of the full text string).
- Text(usize),
- /// Horizontal spacing between other segments.
- Spacing(Spacing),
- /// A mathematical equation.
- Equation(&'a EquationElem),
- /// A box with arbitrary content.
- Box(&'a BoxElem, bool),
- /// Metadata.
- Meta,
-}
-
-impl Segment<'_> {
- /// The text length of the item.
- fn len(&self) -> usize {
- match *self {
- Self::Text(len) => len,
- Self::Spacing(_) => SPACING_REPLACE.len_utf8(),
- Self::Box(_, true) => SPACING_REPLACE.len_utf8(),
- Self::Equation(_) | Self::Box(_, _) => OBJ_REPLACE.len_utf8(),
- Self::Meta => 0,
- }
- }
-}
-
-/// A prepared item in a paragraph layout.
-#[derive(Debug)]
-pub(crate) enum Item<'a> {
- /// A shaped text run with consistent style and direction.
- Text(ShapedText<'a>),
- /// Absolute spacing between other items.
- Absolute(Abs),
- /// Fractional spacing between other items.
- Fractional(Fr, Option<(&'a BoxElem, StyleChain<'a>)>),
- /// Layouted inline-level content.
- Frame(Frame),
- /// Metadata.
- Meta(Frame),
-}
-
-impl<'a> Item<'a> {
- /// If this a text item, return it.
- pub fn text(&self) -> Option<&ShapedText<'a>> {
- match self {
- Self::Text(shaped) => Some(shaped),
- _ => None,
- }
- }
-
- pub fn text_mut(&mut self) -> Option<&mut ShapedText<'a>> {
- match self {
- Self::Text(shaped) => Some(shaped),
- _ => None,
- }
- }
-
- /// The text length of the item.
- pub fn len(&self) -> usize {
- match self {
- Self::Text(shaped) => shaped.text.len(),
- Self::Absolute(_) | Self::Fractional(_, _) => SPACING_REPLACE.len_utf8(),
- Self::Frame(_) => OBJ_REPLACE.len_utf8(),
- Self::Meta(_) => 0,
- }
- }
-
- /// The natural layouted width of the item.
- pub fn width(&self) -> Abs {
- match self {
- Self::Text(shaped) => shaped.width,
- Self::Absolute(v) => *v,
- Self::Frame(frame) => frame.width(),
- Self::Fractional(_, _) | Self::Meta(_) => Abs::zero(),
- }
- }
-}
-
-/// Maps byte offsets back to spans.
-#[derive(Default)]
-pub struct SpanMapper(Vec<(usize, Span)>);
-
-impl SpanMapper {
- /// Create a new span mapper.
- pub fn new() -> Self {
- Self::default()
- }
-
- /// Push a span for a segment with the given length.
- pub fn push(&mut self, len: usize, span: Span) {
- self.0.push((len, span));
- }
-
- /// Determine the span at the given byte offset.
- ///
- /// May return a detached span.
- pub fn span_at(&self, offset: usize) -> (Span, u16) {
- let mut cursor = 0;
- for &(len, span) in &self.0 {
- if (cursor..=cursor + len).contains(&offset) {
- return (span, u16::try_from(offset - cursor).unwrap_or(0));
- }
- cursor += len;
- }
- (Span::detached(), 0)
- }
-}
-
-/// A layouted line, consisting of a sequence of layouted paragraph items that
-/// are mostly borrowed from the preparation phase. This type enables you to
-/// measure the size of a line in a range before committing to building the
-/// line's frame.
-///
-/// At most two paragraph items must be created individually for this line: The
-/// first and last one since they may be broken apart by the start or end of the
-/// line, respectively. But even those can partially reuse previous results when
-/// the break index is safe-to-break per rustybuzz.
-struct Line<'a> {
- /// Bidi information about the paragraph.
- bidi: &'a BidiInfo<'a>,
- /// The trimmed range the line spans in the paragraph.
- trimmed: Range,
- /// The untrimmed end where the line ends.
- end: usize,
- /// A reshaped text item if the line sliced up a text item at the start.
- first: Option<Item<'a>>,
- /// Inner items which don't need to be reprocessed.
- inner: &'a [Item<'a>],
- /// A reshaped text item if the line sliced up a text item at the end. If
- /// there is only one text item, this takes precedence over `first`.
- last: Option<Item<'a>>,
- /// The width of the line.
- width: Abs,
- /// Whether the line should be justified.
- justify: bool,
- /// Whether the line ends with a hyphen or dash, either naturally or through
- /// hyphenation.
- dash: bool,
-}
-
-impl<'a> Line<'a> {
- /// Iterate over the line's items.
- fn items(&self) -> impl Iterator<Item = &Item<'a>> {
- self.first.iter().chain(self.inner).chain(&self.last)
- }
-
- /// Return items that intersect the given `text_range`.
- fn slice(&self, text_range: Range) -> impl Iterator<Item = &Item<'a>> {
- let mut cursor = self.trimmed.start;
- let mut start = 0;
- let mut end = 0;
-
- for (i, item) in self.items().enumerate() {
- if cursor <= text_range.start {
- start = i;
- }
-
- let len = item.len();
- if cursor < text_range.end || cursor + len <= text_range.end {
- end = i + 1;
- } else {
- break;
- }
-
- cursor += len;
- }
-
- self.items().skip(start).take(end - start)
- }
-
- /// How many glyphs are in the text where we can insert additional
- /// space when encountering underfull lines.
- fn justifiables(&self) -> usize {
- let mut count = 0;
- for shaped in self.items().filter_map(Item::text) {
- count += shaped.justifiables();
- }
- // CJK character at line end should not be adjusted.
- if self
- .items()
- .last()
- .and_then(Item::text)
- .map(|s| s.cjk_justifiable_at_last())
- .unwrap_or(false)
- {
- count -= 1;
- }
-
- count
- }
-
- /// How much can the line stretch
- fn stretchability(&self) -> Abs {
- self.items().filter_map(Item::text).map(|s| s.stretchability()).sum()
- }
-
- /// How much can the line shrink
- fn shrinkability(&self) -> Abs {
- self.items().filter_map(Item::text).map(|s| s.shrinkability()).sum()
- }
-
- /// The sum of fractions in the line.
- fn fr(&self) -> Fr {
- self.items()
- .filter_map(|item| match item {
- Item::Fractional(fr, _) => Some(*fr),
- _ => None,
- })
- .sum()
- }
-}
-
-/// Collect all text of the paragraph into one string. This also performs
-/// string-level preprocessing like case transformations.
-#[allow(clippy::type_complexity)]
-fn collect<'a>(
- children: &'a [Prehashed<Content>],
- styles: &'a StyleChain<'a>,
- consecutive: bool,
-) -> SourceResult<(String, Vec<(Segment<'a>, StyleChain<'a>)>, SpanMapper)> {
- let mut full = String::new();
- let mut quoter = Quoter::new();
- let mut segments = Vec::with_capacity(2 + children.len());
- let mut spans = SpanMapper::new();
- let mut iter = children.iter().map(|c| &**c).peekable();
-
- let first_line_indent = ParElem::first_line_indent_in(*styles);
- if !first_line_indent.is_zero()
- && consecutive
- && AlignElem::alignment_in(*styles).resolve(*styles).x
- == TextElem::dir_in(*styles).start().into()
- {
- full.push(SPACING_REPLACE);
- segments.push((Segment::Spacing(first_line_indent.into()), *styles));
- }
-
- let hang = ParElem::hanging_indent_in(*styles);
- if !hang.is_zero() {
- full.push(SPACING_REPLACE);
- segments.push((Segment::Spacing((-hang).into()), *styles));
- }
-
- while let Some(mut child) = iter.next() {
- let outer = styles;
- let mut styles = *styles;
- if let Some((elem, local)) = child.to_styled() {
- child = elem;
- styles = outer.chain(local);
- }
-
- let segment = if child.is::<SpaceElem>() {
- full.push(' ');
- Segment::Text(1)
- } else if let Some(elem) = child.to::<TextElem>() {
- let prev = full.len();
- if let Some(case) = TextElem::case_in(styles) {
- full.push_str(&case.apply(elem.text()));
- } else {
- full.push_str(elem.text());
- }
- Segment::Text(full.len() - prev)
- } else if let Some(elem) = child.to::<HElem>() {
- if elem.amount().is_zero() {
- continue;
- }
-
- full.push(SPACING_REPLACE);
- Segment::Spacing(*elem.amount())
- } else if let Some(elem) = child.to::<LinebreakElem>() {
- 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>() {
- let prev = full.len();
- if SmartquoteElem::enabled_in(styles) {
- let quotes = SmartquoteElem::quotes_in(styles);
- let lang = TextElem::lang_in(styles);
- let region = TextElem::region_in(styles);
- let quotes = Quotes::new(
- quotes,
- lang,
- region,
- SmartquoteElem::alternative_in(styles),
- );
- let peeked = iter.peek().and_then(|child| {
- let child = if let Some((child, _)) = child.to_styled() {
- child
- } else {
- child
- };
- if let Some(elem) = child.to::<TextElem>() {
- elem.text().chars().next()
- } else if child.is::<SmartquoteElem>() {
- Some('"')
- } else if child.is::<SpaceElem>()
- || child.is::<HElem>()
- || child.is::<LinebreakElem>()
- {
- Some(SPACING_REPLACE)
- } else {
- Some(OBJ_REPLACE)
- }
- });
-
- full.push_str(quoter.quote(&quotes, elem.double(styles), peeked));
- } else {
- full.push(if elem.double(styles) { '"' } else { '\'' });
- }
- Segment::Text(full.len() - prev)
- } else if let Some(elem) = child.to::<EquationElem>() {
- full.push(OBJ_REPLACE);
- Segment::Equation(elem)
- } else if let Some(elem) = child.to::<BoxElem>() {
- let frac = elem.width(styles).is_fractional();
- full.push(if frac { SPACING_REPLACE } else { OBJ_REPLACE });
- Segment::Box(elem, frac)
- } else if child.is::<MetaElem>() {
- Segment::Meta
- } else {
- bail!(child.span(), "unexpected paragraph child");
- };
-
- if let Some(last) = full.chars().last() {
- quoter.last(last, child.is::<SmartquoteElem>());
- }
-
- spans.push(segment.len(), child.span());
-
- if let (Some((Segment::Text(last_len), last_styles)), Segment::Text(len)) =
- (segments.last_mut(), segment)
- {
- if *last_styles == styles {
- *last_len += len;
- continue;
- }
- }
-
- segments.push((segment, styles));
- }
-
- Ok((full, segments, spans))
-}
-
-/// Prepare paragraph layout by shaping the whole paragraph and layouting all
-/// contained inline-level content.
-fn prepare<'a>(
- vt: &mut Vt,
- children: &'a [Prehashed<Content>],
- text: &'a str,
- segments: Vec<(Segment<'a>, StyleChain<'a>)>,
- spans: SpanMapper,
- styles: StyleChain<'a>,
- region: Size,
-) -> SourceResult<Preparation<'a>> {
- let dir = TextElem::dir_in(styles);
- let bidi = BidiInfo::new(
- text,
- match dir {
- Dir::LTR => Some(BidiLevel::ltr()),
- Dir::RTL => Some(BidiLevel::rtl()),
- _ => None,
- },
- );
-
- let mut cursor = 0;
- let mut items = Vec::with_capacity(segments.len());
-
- // Shape / layout the children and collect them into items.
- for (segment, styles) in segments {
- let end = cursor + segment.len();
- match segment {
- Segment::Text(_) => {
- shape_range(&mut items, vt, &bidi, cursor..end, &spans, styles);
- }
- Segment::Spacing(spacing) => match spacing {
- Spacing::Rel(v) => {
- let resolved = v.resolve(styles).relative_to(region.x);
- items.push(Item::Absolute(resolved));
- }
- Spacing::Fr(v) => {
- items.push(Item::Fractional(v, None));
- }
- },
- Segment::Equation(equation) => {
- let pod = Regions::one(region, Axes::splat(false));
- let mut frame = equation.layout(vt, styles, pod)?.into_frame();
- frame.translate(Point::with_y(TextElem::baseline_in(styles)));
- items.push(Item::Frame(frame));
- }
- Segment::Box(elem, _) => {
- if let Sizing::Fr(v) = elem.width(styles) {
- items.push(Item::Fractional(v, Some((elem, styles))));
- } else {
- let pod = Regions::one(region, Axes::splat(false));
- let mut frame = elem.layout(vt, styles, pod)?.into_frame();
- frame.translate(Point::with_y(TextElem::baseline_in(styles)));
- items.push(Item::Frame(frame));
- }
- }
- Segment::Meta => {
- let mut frame = Frame::soft(Size::zero());
- frame.meta(styles, true);
- items.push(Item::Meta(frame));
- }
- }
-
- cursor = end;
- }
-
- let cjk_latin_spacing = TextElem::cjk_latin_spacing_in(styles).is_auto();
- if cjk_latin_spacing {
- add_cjk_latin_spacing(&mut items);
- }
-
- Ok(Preparation {
- bidi,
- items,
- spans,
- hyphenate: shared_get(styles, children, TextElem::hyphenate_in),
- lang: shared_get(styles, children, TextElem::lang_in),
- align: AlignElem::alignment_in(styles).resolve(styles).x,
- justify: ParElem::justify_in(styles),
- hang: ParElem::hanging_indent_in(styles),
- cjk_latin_spacing,
- fallback: TextElem::fallback_in(styles),
- leading: ParElem::leading_in(styles),
- linebreaks: ParElem::linebreaks_in(styles),
- size: TextElem::size_in(styles),
- })
-}
-
-/// Add some spacing between Han characters and western characters.
-/// See Requirements for Chinese Text Layout, Section 3.2.2 Mixed Text Composition in Horizontal
-/// Written Mode
-fn add_cjk_latin_spacing(items: &mut [Item]) {
- let mut items = items.iter_mut().filter(|x| !matches!(x, Item::Meta(_))).peekable();
- let mut prev: Option<&ShapedGlyph> = None;
- while let Some(item) = items.next() {
- let Some(text) = item.text_mut() else {
- prev = None;
- continue;
- };
-
- // Since we only call this function in [`prepare`], we can assume
- // that the Cow is owned, and `to_mut` can be called without overhead.
- debug_assert!(matches!(text.glyphs, std::borrow::Cow::Owned(_)));
- let mut glyphs = text.glyphs.to_mut().iter_mut().peekable();
-
- while let Some(glyph) = glyphs.next() {
- let next = glyphs.peek().map(|n| n as _).or_else(|| {
- items
- .peek()
- .and_then(|i| i.text())
- .and_then(|shaped| shaped.glyphs.first())
- });
-
- // Case 1: CJK followed by a Latin character
- if glyph.is_cjk_script() && next.map_or(false, |g| g.is_letter_or_number()) {
- // The spacing is default to 1/4 em, and can be shrunk to 1/8 em.
- glyph.x_advance += Em::new(0.25);
- glyph.adjustability.shrinkability.1 += Em::new(0.125);
- text.width += Em::new(0.25).at(text.size);
- }
-
- // Case 2: Latin followed by a CJK character
- if glyph.is_cjk_script() && prev.map_or(false, |g| g.is_letter_or_number()) {
- glyph.x_advance += Em::new(0.25);
- glyph.x_offset += Em::new(0.25);
- glyph.adjustability.shrinkability.0 += Em::new(0.125);
- text.width += Em::new(0.25).at(text.size);
- }
-
- prev = Some(glyph);
- }
- }
-}
-
-/// Group a range of text by BiDi level and script, shape the runs and generate
-/// items for them.
-fn shape_range<'a>(
- items: &mut Vec<Item<'a>>,
- vt: &Vt,
- bidi: &BidiInfo<'a>,
- range: Range,
- spans: &SpanMapper,
- styles: StyleChain<'a>,
-) {
- let script = TextElem::script_in(styles);
- let lang = TextElem::lang_in(styles);
- let region = TextElem::region_in(styles);
- let mut process = |range: Range, level: BidiLevel| {
- let dir = if level.is_ltr() { Dir::LTR } else { Dir::RTL };
- let shaped =
- shape(vt, range.start, &bidi.text[range], spans, styles, dir, lang, region);
- items.push(Item::Text(shaped));
- };
-
- let mut prev_level = BidiLevel::ltr();
- let mut prev_script = Script::Unknown;
- let mut cursor = range.start;
-
- // Group by embedding level and script. If the text's script is explicitly
- // set (rather than inferred from the glyphs), we keep the script at an
- // unchanging `Script::Unknown` so that only level changes cause breaks.
- for i in range.clone() {
- if !bidi.text.is_char_boundary(i) {
- continue;
- }
-
- let level = bidi.levels[i];
- let curr_script = match script {
- Smart::Auto => {
- bidi.text[i..].chars().next().map_or(Script::Unknown, |c| c.script())
- }
- Smart::Custom(_) => Script::Unknown,
- };
-
- if level != prev_level || !is_compatible(curr_script, prev_script) {
- if cursor < i {
- process(cursor..i, prev_level);
- }
- cursor = i;
- prev_level = level;
- prev_script = curr_script;
- } else if is_generic_script(prev_script) {
- prev_script = curr_script;
- }
- }
-
- process(cursor..range.end, prev_level);
-}
-
-/// Whether this is not a specific script.
-fn is_generic_script(script: Script) -> bool {
- matches!(script, Script::Unknown | Script::Common | Script::Inherited)
-}
-
-/// Whether these script can be part of the same shape run.
-fn is_compatible(a: Script, b: Script) -> bool {
- is_generic_script(a) || is_generic_script(b) || a == b
-}
-
-/// Get a style property, but only if it is the same for all children of the
-/// paragraph.
-fn shared_get<T: PartialEq>(
- styles: StyleChain<'_>,
- children: &[Prehashed<Content>],
- getter: fn(StyleChain) -> T,
-) -> Option<T> {
- let value = getter(styles);
- children
- .iter()
- .filter_map(|child| child.to_styled())
- .all(|(_, local)| getter(styles.chain(local)) == value)
- .then_some(value)
-}
-
-/// Find suitable linebreaks.
-fn linebreak<'a>(vt: &Vt, p: &'a Preparation<'a>, width: Abs) -> Vec<Line<'a>> {
- let linebreaks = p.linebreaks.unwrap_or_else(|| {
- if p.justify {
- Linebreaks::Optimized
- } else {
- Linebreaks::Simple
- }
- });
-
- match linebreaks {
- Linebreaks::Simple => linebreak_simple(vt, p, width),
- Linebreaks::Optimized => linebreak_optimized(vt, p, width),
- }
-}
-
-/// Perform line breaking in simple first-fit style. This means that we build
-/// lines greedily, always taking the longest possible line. This may lead to
-/// very unbalanced line, but is fast and simple.
-fn linebreak_simple<'a>(vt: &Vt, p: &'a Preparation<'a>, width: Abs) -> Vec<Line<'a>> {
- let mut lines = Vec::with_capacity(16);
- let mut start = 0;
- let mut last = None;
-
- breakpoints(p, |end, breakpoint| {
- // Compute the line and its size.
- let mut attempt = line(vt, p, start..end, breakpoint);
-
- // If the line doesn't fit anymore, we push the last fitting attempt
- // into the stack and rebuild the line from the attempt's end. The
- // resulting line cannot be broken up further.
- if !width.fits(attempt.width) {
- if let Some((last_attempt, last_end)) = last.take() {
- lines.push(last_attempt);
- start = last_end;
- attempt = line(vt, p, start..end, breakpoint);
- }
- }
-
- // Finish the current line if there is a mandatory line break (i.e.
- // due to "\n") or if the line doesn't fit horizontally already
- // since then no shorter line will be possible.
- if breakpoint == Breakpoint::Mandatory || !width.fits(attempt.width) {
- lines.push(attempt);
- start = end;
- last = None;
- } else {
- last = Some((attempt, end));
- }
- });
-
- if let Some((line, _)) = last {
- lines.push(line);
- }
-
- lines
-}
-
-/// Perform line breaking in optimized Knuth-Plass style. Here, we use more
-/// context to determine the line breaks than in the simple first-fit style. For
-/// example, we might choose to cut a line short even though there is still a
-/// bit of space to improve the fit of one of the following lines. The
-/// Knuth-Plass algorithm is based on the idea of "cost". A line which has a
-/// very tight or very loose fit has a higher cost than one that is just right.
-/// Ending a line with a hyphen incurs extra cost and endings two successive
-/// lines with hyphens even more.
-///
-/// To find the layout with the minimal total cost the algorithm uses dynamic
-/// programming: For each possible breakpoint it determines the optimal
-/// paragraph layout _up to that point_. It walks over all possible start points
-/// for a line ending at that point and finds the one for which the cost of the
-/// line plus the cost of the optimal paragraph up to the start point (already
-/// computed and stored in dynamic programming table) is minimal. The final
-/// result is simply the layout determined for the last breakpoint at the end of
-/// text.
-fn linebreak_optimized<'a>(vt: &Vt, p: &'a Preparation<'a>, width: Abs) -> Vec<Line<'a>> {
- /// The cost of a line or paragraph layout.
- type Cost = f64;
-
- /// An entry in the dynamic programming table.
- struct Entry<'a> {
- pred: usize,
- total: Cost,
- line: Line<'a>,
- }
-
- // Cost parameters.
- const HYPH_COST: Cost = 0.5;
- const RUNT_COST: Cost = 0.5;
- const CONSECUTIVE_DASH_COST: Cost = 0.3;
- const MAX_COST: Cost = 1_000_000.0;
- const MIN_RATIO: f64 = -1.0;
-
- // Dynamic programming table.
- let mut active = 0;
- let mut table = vec![Entry {
- pred: 0,
- total: 0.0,
- line: line(vt, p, 0..0, Breakpoint::Mandatory),
- }];
-
- let em = p.size;
- let mut lines = Vec::with_capacity(16);
- breakpoints(p, |end, breakpoint| {
- let k = table.len();
- let eof = end == p.bidi.text.len();
- let mut best: Option<Entry> = None;
-
- // Find the optimal predecessor.
- for (i, pred) in table.iter().enumerate().skip(active) {
- // Layout the line.
- let start = pred.line.end;
-
- let attempt = line(vt, p, start..end, breakpoint);
-
- // Determine how much the line's spaces would need to be stretched
- // to make it the desired width.
- let delta = width - attempt.width;
- // Determine how much stretch are permitted.
- let adjust = if delta >= Abs::zero() {
- attempt.stretchability()
- } else {
- attempt.shrinkability()
- };
- // Ideally, the ratio should between -1.0 and 1.0, but sometimes a value above 1.0
- // is possible, in which case the line is underfull.
- let mut ratio = delta / adjust;
- if ratio.is_nan() {
- // The line is not stretchable, but it just fits.
- // This often happens with monospace fonts and CJK texts.
- ratio = 0.0;
- }
- if ratio > 1.0 {
- // We should stretch the line above its stretchability. Now
- // calculate the extra amount. Also, don't divide by zero.
- let extra_stretch =
- (delta - adjust) / attempt.justifiables().max(1) as f64;
- // Normalize the amount by half Em size.
- ratio = 1.0 + extra_stretch / (em / 2.0);
- }
-
- // Determine the cost of the line.
- let min_ratio = if p.justify { MIN_RATIO } else { 0.0 };
- let mut cost = if ratio < min_ratio {
- // The line is overfull. This is the case if
- // - justification is on, but we'd need to shrink too much
- // - justification is off and the line just doesn't fit
- //
- // If this is the earliest breakpoint in the active set
- // (active == i), remove it from the active set. If there is an
- // earlier one (active < i), then the logically shorter line was
- // in fact longer (can happen with negative spacing) and we
- // can't trim the active set just yet.
- if active == i {
- active += 1;
- }
- MAX_COST
- } else if breakpoint == Breakpoint::Mandatory || eof {
- // This is a mandatory break and the line is not overfull, so
- // all breakpoints before this one become inactive since no line
- // can span above the mandatory break.
- active = k;
- // If ratio > 0, we need to stretch the line only when justify is needed.
- // If ratio < 0, we always need to shrink the line.
- if (ratio > 0.0 && attempt.justify) || ratio < 0.0 {
- ratio.powi(3).abs()
- } else {
- 0.0
- }
- } else {
- // Normal line with cost of |ratio^3|.
- ratio.powi(3).abs()
- };
-
- // Penalize runts.
- if k == i + 1 && eof {
- cost += RUNT_COST;
- }
-
- // Penalize hyphens.
- if breakpoint == Breakpoint::Hyphen {
- cost += HYPH_COST;
- }
-
- // In Knuth paper, cost = (1 + 100|r|^3 + p)^2 + a,
- // where r is the ratio, p=50 is the penalty, and a=3000 is consecutive the penalty.
- // We divide the whole formula by 10, resulting (0.01 + |r|^3 + p)^2 + a,
- // where p=0.5 and a=0.3
- cost = (0.01 + cost).powi(2);
-
- // Penalize two consecutive dashes (not necessarily hyphens) extra.
- if attempt.dash && pred.line.dash {
- cost += CONSECUTIVE_DASH_COST;
- }
-
- // The total cost of this line and its chain of predecessors.
- let total = pred.total + cost;
-
- // If this attempt is better than what we had before, take it!
- if best.as_ref().map_or(true, |best| best.total >= total) {
- best = Some(Entry { pred: i, total, line: attempt });
- }
- }
-
- table.push(best.unwrap());
- });
-
- // Retrace the best path.
- let mut idx = table.len() - 1;
- while idx != 0 {
- table.truncate(idx + 1);
- let entry = table.pop().unwrap();
- lines.push(entry.line);
- idx = entry.pred;
- }
-
- lines.reverse();
- lines
-}
-
-/// Create a line which spans the given range.
-fn line<'a>(
- vt: &Vt,
- p: &'a Preparation,
- mut range: Range,
- breakpoint: Breakpoint,
-) -> Line<'a> {
- let end = range.end;
- let mut justify =
- p.justify && end < p.bidi.text.len() && breakpoint != Breakpoint::Mandatory;
-
- if range.is_empty() {
- return Line {
- bidi: &p.bidi,
- end,
- trimmed: range,
- first: None,
- inner: &[],
- last: None,
- width: Abs::zero(),
- justify,
- dash: false,
- };
- }
-
- // Slice out the relevant items.
- let (expanded, mut inner) = p.slice(range.clone());
- let mut width = Abs::zero();
-
- // Reshape the last item if it's split in half or hyphenated.
- let mut last = None;
- let mut dash = false;
- if let Some((Item::Text(shaped), before)) = inner.split_last() {
- // Compute the range we want to shape, trimming whitespace at the
- // end of the line.
- let base = expanded.end - shaped.text.len();
- let start = range.start.max(base);
- let text = &p.bidi.text[start..range.end];
- // U+200B ZERO WIDTH SPACE is used to provide a line break opportunity,
- // we want to trim it too.
- let trimmed = text.trim_end().trim_end_matches('\u{200B}');
- range.end = start + trimmed.len();
-
- // Deal with hyphens, dashes and justification.
- let shy = trimmed.ends_with('\u{ad}');
- let hyphen = breakpoint == Breakpoint::Hyphen;
- dash = hyphen || shy || trimmed.ends_with(['-', '–', '—']);
- justify |= text.ends_with('\u{2028}');
-
- // Deal with CJK punctuation at line ends.
- let gb_style = is_gb_style(shaped.lang, shaped.region);
- let maybe_adjust_last_glyph = trimmed.ends_with(END_PUNCT_PAT)
- || (p.cjk_latin_spacing && trimmed.ends_with(char_is_cjk_script));
-
- // Usually, we don't want to shape an empty string because:
- // - We don't want the height of trimmed whitespace in a different
- // font to be considered for the line height.
- // - Even if it's in the same font, its unnecessary.
- //
- // There is one exception though. When the whole line is empty, we
- // need the shaped empty string to make the line the appropriate
- // height. That is the case exactly if the string is empty and there
- // are no other items in the line.
- if hyphen || start + shaped.text.len() > range.end || maybe_adjust_last_glyph {
- if hyphen || start < range.end || before.is_empty() {
- let mut reshaped = shaped.reshape(vt, &p.spans, start..range.end);
- if hyphen || shy {
- reshaped.push_hyphen(vt, p.fallback);
- }
-
- if let Some(last_glyph) = reshaped.glyphs.last() {
- if last_glyph.is_cjk_left_aligned_punctuation(gb_style) {
- // If the last glyph is a CJK punctuation, we want to shrink it.
- // See Requirements for Chinese Text Layout, Section 3.1.6.3
- // Compression of punctuation marks at line start or line end
- let shrink_amount = last_glyph.shrinkability().1;
- let punct = reshaped.glyphs.to_mut().last_mut().unwrap();
- punct.shrink_right(shrink_amount);
- reshaped.width -= shrink_amount.at(reshaped.size);
- } else if p.cjk_latin_spacing
- && last_glyph.is_cjk_script()
- && (last_glyph.x_advance - last_glyph.x_offset) > Em::one()
- {
- // If the last glyph is a CJK character adjusted by [`add_cjk_latin_spacing`],
- // restore the original width.
- let shrink_amount =
- last_glyph.x_advance - last_glyph.x_offset - Em::one();
- let glyph = reshaped.glyphs.to_mut().last_mut().unwrap();
- glyph.x_advance -= shrink_amount;
- glyph.adjustability.shrinkability.1 = Em::zero();
- reshaped.width -= shrink_amount.at(reshaped.size);
- }
- }
-
- width += reshaped.width;
- last = Some(Item::Text(reshaped));
- }
-
- inner = before;
- }
- }
-
- // Deal with CJK characters at line starts.
- let text = &p.bidi.text[range.start..end];
- let maybe_adjust_first_glyph = text.starts_with(BEGIN_PUNCT_PAT)
- || (p.cjk_latin_spacing && text.starts_with(char_is_cjk_script));
-
- // Reshape the start item if it's split in half.
- let mut first = None;
- if let Some((Item::Text(shaped), after)) = inner.split_first() {
- // Compute the range we want to shape.
- let base = expanded.start;
- let end = range.end.min(base + shaped.text.len());
-
- // Reshape if necessary.
- if range.start + shaped.text.len() > end || maybe_adjust_first_glyph {
- // If the range is empty, we don't want to push an empty text item.
- if range.start < end {
- let reshaped = shaped.reshape(vt, &p.spans, range.start..end);
- width += reshaped.width;
- first = Some(Item::Text(reshaped));
- }
-
- inner = after;
- }
- }
-
- if maybe_adjust_first_glyph {
- let reshaped = first.as_mut().or(last.as_mut()).and_then(Item::text_mut);
- if let Some(reshaped) = reshaped {
- if let Some(first_glyph) = reshaped.glyphs.first() {
- if first_glyph.is_cjk_right_aligned_punctuation() {
- // If the first glyph is a CJK punctuation, we want to shrink it.
- let shrink_amount = first_glyph.shrinkability().0;
- let glyph = reshaped.glyphs.to_mut().first_mut().unwrap();
- glyph.shrink_left(shrink_amount);
- let amount_abs = shrink_amount.at(reshaped.size);
- reshaped.width -= amount_abs;
- width -= amount_abs;
- } else if p.cjk_latin_spacing
- && first_glyph.is_cjk_script()
- && first_glyph.x_offset > Em::zero()
- {
- // If the first glyph is a CJK character adjusted by [`add_cjk_latin_spacing`],
- // restore the original width.
- let shrink_amount = first_glyph.x_offset;
- let glyph = reshaped.glyphs.to_mut().first_mut().unwrap();
- glyph.x_advance -= shrink_amount;
- glyph.x_offset = Em::zero();
- glyph.adjustability.shrinkability.0 = Em::zero();
- let amount_abs = shrink_amount.at(reshaped.size);
- reshaped.width -= amount_abs;
- width -= amount_abs;
- }
- }
- }
- }
-
- // Measure the inner items.
- for item in inner {
- width += item.width();
- }
-
- Line {
- bidi: &p.bidi,
- trimmed: range,
- end,
- first,
- inner,
- last,
- width,
- justify,
- dash,
- }
-}
-
-/// Combine layouted lines into one frame per region.
-fn finalize(
- vt: &mut Vt,
- p: &Preparation,
- lines: &[Line],
- region: Size,
- expand: bool,
-) -> SourceResult<Fragment> {
- // Determine the paragraph's width: Full width of the region if we
- // should expand or there's fractional spacing, fit-to-width otherwise.
- let width = if !region.x.is_finite()
- || (!expand && lines.iter().all(|line| line.fr().is_zero()))
- {
- region
- .x
- .min(p.hang + lines.iter().map(|line| line.width).max().unwrap_or_default())
- } else {
- region.x
- };
-
- // Stack the lines into one frame per region.
- let mut frames: Vec<Frame> = lines
- .iter()
- .map(|line| commit(vt, p, line, width, region.y))
- .collect::<SourceResult<_>>()?;
-
- // Prevent orphans.
- if frames.len() >= 2 && !frames[1].is_empty() {
- let second = frames.remove(1);
- let first = &mut frames[0];
- merge(first, second, p.leading);
- }
-
- // Prevent widows.
- let len = frames.len();
- if len >= 2 && !frames[len - 2].is_empty() {
- let second = frames.pop().unwrap();
- let first = frames.last_mut().unwrap();
- merge(first, second, p.leading);
- }
-
- Ok(Fragment::frames(frames))
-}
-
-/// Merge two line frames
-fn merge(first: &mut Frame, second: Frame, leading: Abs) {
- let offset = first.height() + leading;
- let total = offset + second.height();
- first.push_frame(Point::with_y(offset), second);
- first.size_mut().y = total;
-}
-
-/// Commit to a line and build its frame.
-fn commit(
- vt: &mut Vt,
- p: &Preparation,
- line: &Line,
- width: Abs,
- full: Abs,
-) -> SourceResult<Frame> {
- let mut remaining = width - line.width - p.hang;
- let mut offset = Abs::zero();
-
- // Reorder the line from logical to visual order.
- let (reordered, starts_rtl) = reorder(line);
- if !starts_rtl {
- offset += p.hang;
- }
-
- // Handle hanging punctuation to the left.
- if let Some(Item::Text(text)) = reordered.first() {
- if let Some(glyph) = text.glyphs.first() {
- if !text.dir.is_positive()
- && TextElem::overhang_in(text.styles)
- && (reordered.len() > 1 || text.glyphs.len() > 1)
- {
- let amount = overhang(glyph.c) * glyph.x_advance.at(text.size);
- offset -= amount;
- remaining += amount;
- }
- }
- }
-
- // Handle hanging punctuation to the right.
- if let Some(Item::Text(text)) = reordered.last() {
- if let Some(glyph) = text.glyphs.last() {
- if text.dir.is_positive()
- && TextElem::overhang_in(text.styles)
- && (reordered.len() > 1 || text.glyphs.len() > 1)
- {
- let amount = overhang(glyph.c) * glyph.x_advance.at(text.size);
- remaining += amount;
- }
- }
- }
-
- // Determine how much additional space is needed.
- // The justicication_ratio is for the first step justification,
- // extra_justification is for the last step.
- // For more info on multi-step justification, see Procedures for Inter-
- // Character Space Expansion in W3C document Chinese Layout Requirements.
- let fr = line.fr();
- let mut justification_ratio = 0.0;
- let mut extra_justification = Abs::zero();
-
- let shrink = line.shrinkability();
- let stretch = line.stretchability();
- if remaining < Abs::zero() && shrink > Abs::zero() {
- // Attempt to reduce the length of the line, using shrinkability.
- justification_ratio = (remaining / shrink).max(-1.0);
- remaining = (remaining + shrink).min(Abs::zero());
- } else if line.justify && fr.is_zero() {
- // Attempt to increase the length of the line, using stretchability.
- if stretch > Abs::zero() {
- justification_ratio = (remaining / stretch).min(1.0);
- remaining = (remaining - stretch).max(Abs::zero());
- }
-
- let justifiables = line.justifiables();
- if justifiables > 0 && remaining > Abs::zero() {
- // Underfull line, distribute the extra space.
- extra_justification = remaining / justifiables as f64;
- remaining = Abs::zero();
- }
- }
-
- let mut top = Abs::zero();
- let mut bottom = Abs::zero();
-
- // Build the frames and determine the height and baseline.
- let mut frames = vec![];
- for item in reordered {
- let mut push = |offset: &mut Abs, frame: Frame| {
- let width = frame.width();
- top.set_max(frame.baseline());
- bottom.set_max(frame.size().y - frame.baseline());
- frames.push((*offset, frame));
- *offset += width;
- };
-
- match item {
- Item::Absolute(v) => {
- offset += *v;
- }
- Item::Fractional(v, elem) => {
- let amount = v.share(fr, remaining);
- if let Some((elem, styles)) = elem {
- let region = Size::new(amount, full);
- let pod = Regions::one(region, Axes::new(true, false));
- let mut frame = elem.layout(vt, *styles, pod)?.into_frame();
- frame.translate(Point::with_y(TextElem::baseline_in(*styles)));
- push(&mut offset, frame);
- } else {
- offset += amount;
- }
- }
- Item::Text(shaped) => {
- let frame = shaped.build(vt, justification_ratio, extra_justification);
- push(&mut offset, frame);
- }
- Item::Frame(frame) | Item::Meta(frame) => {
- push(&mut offset, frame.clone());
- }
- }
- }
-
- // Remaining space is distributed now.
- if !fr.is_zero() {
- remaining = Abs::zero();
- }
-
- let size = Size::new(width, top + bottom);
- let mut output = Frame::soft(size);
- output.set_baseline(top);
-
- // Construct the line's frame.
- for (offset, frame) in frames {
- let x = offset + p.align.position(remaining);
- let y = top - frame.baseline();
- output.push_frame(Point::new(x, y), frame);
- }
-
- Ok(output)
-}
-
-/// Return a line's items in visual order.
-fn reorder<'a>(line: &'a Line<'a>) -> (Vec<&Item<'a>>, bool) {
- let mut reordered = vec![];
-
- // The bidi crate doesn't like empty lines.
- if line.trimmed.is_empty() {
- return (line.slice(line.trimmed.clone()).collect(), false);
- }
-
- // Find the paragraph that contains the line.
- let para = line
- .bidi
- .paragraphs
- .iter()
- .find(|para| para.range.contains(&line.trimmed.start))
- .unwrap();
-
- // Compute the reordered ranges in visual order (left to right).
- let (levels, runs) = line.bidi.visual_runs(para, line.trimmed.clone());
- let starts_rtl = levels.first().map_or(false, |level| level.is_rtl());
-
- // Collect the reordered items.
- for run in runs {
- // Skip reset L1 runs because handling them would require reshaping
- // again in some cases.
- if line.bidi.levels[run.start] != levels[run.start] {
- continue;
- }
-
- let prev = reordered.len();
- reordered.extend(line.slice(run.clone()));
-
- if levels[run.start].is_rtl() {
- reordered[prev..].reverse();
- }
- }
-
- (reordered, starts_rtl)
-}
-
-/// How much a character should hang into the end margin.
-///
-/// For more discussion, see:
-/// https://recoveringphysicist.com/21/
-fn overhang(c: char) -> f64 {
- match c {
- // Dashes.
- '–' | '—' => 0.2,
- '-' => 0.55,
-
- // Punctuation.
- '.' | ',' => 0.8,
- ':' | ';' => 0.3,
-
- // Arabic
- '\u{60C}' | '\u{6D4}' => 0.4,
-
- _ => 0.0,
- }
-}
diff --git a/crates/typst-library/src/layout/place.rs b/crates/typst-library/src/layout/place.rs
deleted file mode 100644
index c8e83383..00000000
--- a/crates/typst-library/src/layout/place.rs
+++ /dev/null
@@ -1,124 +0,0 @@
-use crate::prelude::*;
-
-/// Places content at an absolute position.
-///
-/// Placed content will not affect the position of other content. Place is
-/// always relative to its parent container and will be in the foreground of all
-/// other content in the container. Page margins will be respected.
-///
-///
-/// # Example
-/// ```example
-/// #set page(height: 60pt)
-/// Hello, world!
-///
-/// #place(
-/// top + right,
-/// square(
-/// width: 20pt,
-/// stroke: 2pt + blue
-/// ),
-/// )
-/// ```
-#[elem(Layout, Behave)]
-pub struct PlaceElem {
- /// Relative to which position in the parent container to place the content.
- ///
- /// Cannot be `{auto}` if `float` is `{false}` and must be either
- /// `{auto}`, `{top}`, or `{bottom}` if `float` is `{true}`.
- ///
- /// When an axis of the page is `{auto}` sized, all alignments relative to
- /// that axis will be ignored, instead, the item will be placed in the
- /// origin of the axis.
- #[positional]
- #[default(Smart::Custom(Align::START))]
- pub alignment: Smart<Align>,
-
- /// Whether the placed element has floating layout.
- ///
- /// Floating elements are positioned at the top or bottom of the page,
- /// displacing in-flow content.
- ///
- /// ```example
- /// #set page(height: 150pt)
- /// #let note(where, body) = place(
- /// center + where,
- /// float: true,
- /// clearance: 6pt,
- /// rect(body),
- /// )
- ///
- /// #lorem(10)
- /// #note(bottom)[Bottom 1]
- /// #note(bottom)[Bottom 2]
- /// #lorem(40)
- /// #note(top)[Top]
- /// #lorem(10)
- /// ```
- pub float: bool,
-
- /// The amount of clearance the placed element has in a floating layout.
- #[default(Em::new(1.5).into())]
- #[resolve]
- pub clearance: Length,
-
- /// The horizontal displacement of the placed content.
- ///
- /// ```example
- /// #set page(height: 100pt)
- /// #for i in range(16) {
- /// let amount = i * 4pt
- /// place(center, dx: amount - 32pt, dy: amount)[A]
- /// }
- /// ```
- pub dx: Rel<Length>,
-
- /// The vertical displacement of the placed content.
- pub dy: Rel<Length>,
-
- /// The content to place.
- #[required]
- pub body: Content,
-}
-
-impl Layout for PlaceElem {
- #[tracing::instrument(name = "PlaceElem::layout", skip_all)]
- fn layout(
- &self,
- vt: &mut Vt,
- styles: StyleChain,
- regions: Regions,
- ) -> SourceResult<Fragment> {
- // The pod is the base area of the region because for absolute
- // placement we don't really care about the already used area.
- let base = regions.base();
- let float = self.float(styles);
- let alignment = self.alignment(styles);
-
- if float
- && alignment
- .map_or(false, |align| matches!(align.y(), None | 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")
- .hint("you can enable floating placement with `place(float: true, ..)`")
- .at(self.span());
- }
-
- let child = self
- .body()
- .clone()
- .aligned(alignment.unwrap_or_else(|| Align::CENTER));
-
- let pod = Regions::one(base, Axes::splat(false));
- let frame = child.layout(vt, styles, pod)?.into_frame();
- Ok(Fragment::frame(frame))
- }
-}
-
-impl Behave for PlaceElem {
- fn behaviour(&self) -> Behaviour {
- Behaviour::Ignorant
- }
-}
diff --git a/crates/typst-library/src/layout/regions.rs b/crates/typst-library/src/layout/regions.rs
deleted file mode 100644
index 6dd549b0..00000000
--- a/crates/typst-library/src/layout/regions.rs
+++ /dev/null
@@ -1,144 +0,0 @@
-use std::fmt::{self, Debug, Formatter};
-
-use typst::geom::{Abs, Axes, Size};
-
-/// A sequence of regions to layout into.
-#[derive(Copy, Clone, Hash)]
-pub struct Regions<'a> {
- /// The remaining size of the first region.
- pub size: Size,
- /// The full height of the region for relative sizing.
- pub full: Abs,
- /// The height of followup regions. The width is the same for all regions.
- pub backlog: &'a [Abs],
- /// The height of the final region that is repeated once the backlog is
- /// drained. The width is the same for all regions.
- pub last: Option<Abs>,
- /// Whether elements should expand to fill the regions instead of shrinking
- /// to fit the content.
- pub expand: Axes<bool>,
- /// Whether these are the root regions or direct descendants.
- ///
- /// True for the padded page regions and columns directly in the page,
- /// false otherwise.
- pub root: bool,
-}
-
-impl Regions<'_> {
- /// Create a new region sequence with exactly one region.
- pub fn one(size: Size, expand: Axes<bool>) -> Self {
- Self {
- size,
- full: size.y,
- backlog: &[],
- last: None,
- expand,
- root: false,
- }
- }
-
- /// Create a new sequence of same-size regions that repeats indefinitely.
- pub fn repeat(size: Size, expand: Axes<bool>) -> Self {
- Self {
- size,
- full: size.y,
- backlog: &[],
- last: Some(size.y),
- expand,
- root: false,
- }
- }
-
- /// The base size, which doesn't take into account that the regions is
- /// already partially used up.
- ///
- /// This is also used for relative sizing.
- pub fn base(&self) -> Size {
- Size::new(self.size.x, self.full)
- }
-
- /// Create new regions where all sizes are mapped with `f`.
- ///
- /// Note that since all regions must have the same width, the width returned
- /// by `f` is ignored for the backlog and the final region.
- pub fn map<'v, F>(&self, backlog: &'v mut Vec<Abs>, mut f: F) -> Regions<'v>
- where
- F: FnMut(Size) -> Size,
- {
- let x = self.size.x;
- backlog.clear();
- backlog.extend(self.backlog.iter().map(|&y| f(Size::new(x, y)).y));
- Regions {
- size: f(self.size),
- full: f(Size::new(x, self.full)).y,
- backlog,
- last: self.last.map(|y| f(Size::new(x, y)).y),
- expand: self.expand,
- root: false,
- }
- }
-
- /// Whether the first region is full and a region break is called for.
- pub fn is_full(&self) -> bool {
- Abs::zero().fits(self.size.y) && !self.in_last()
- }
-
- /// Whether the first region is the last usable region.
- ///
- /// If this is true, calling `next()` will have no effect.
- pub fn in_last(&self) -> bool {
- self.backlog.is_empty() && self.last.map_or(true, |height| self.size.y == height)
- }
-
- /// The same regions, but with different `root` configuration.
- pub fn with_root(self, root: bool) -> Self {
- Self { root, ..self }
- }
-
- /// Advance to the next region if there is any.
- pub fn next(&mut self) {
- if let Some(height) = self
- .backlog
- .split_first()
- .map(|(first, tail)| {
- self.backlog = tail;
- *first
- })
- .or(self.last)
- {
- self.size.y = height;
- self.full = height;
- }
- }
-
- /// An iterator that returns the sizes of the first and all following
- /// regions, equivalently to what would be produced by calling
- /// [`next()`](Self::next) repeatedly until all regions are exhausted.
- /// This iterator may be infinite.
- pub fn iter(&self) -> impl Iterator<Item = Size> + '_ {
- let first = std::iter::once(self.size);
- let backlog = self.backlog.iter();
- let last = self.last.iter().cycle();
- first.chain(backlog.chain(last).map(|&h| Size::new(self.size.x, h)))
- }
-}
-
-impl Debug for Regions<'_> {
- fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
- f.write_str("Regions ")?;
- let mut list = f.debug_list();
- let mut prev = self.size.y;
- list.entry(&self.size);
- for &height in self.backlog {
- list.entry(&Size::new(self.size.x, height));
- prev = height;
- }
- if let Some(last) = self.last {
- if last != prev {
- list.entry(&Size::new(self.size.x, last));
- }
- list.entry(&(..));
- }
- list.finish()
- }
-}
diff --git a/crates/typst-library/src/layout/repeat.rs b/crates/typst-library/src/layout/repeat.rs
deleted file mode 100644
index ce31164e..00000000
--- a/crates/typst-library/src/layout/repeat.rs
+++ /dev/null
@@ -1,75 +0,0 @@
-use crate::layout::AlignElem;
-use crate::prelude::*;
-
-/// Repeats content to the available space.
-///
-/// This can be useful when implementing a custom index, reference, or outline.
-///
-/// Space may be inserted between the instances of the body parameter, so be
-/// sure to include negative space if you need the instances to overlap.
-///
-/// Errors if there no bounds on the available space, as it would create
-/// infinite content.
-///
-/// # Example
-/// ```example
-/// Sign on the dotted line:
-/// #box(width: 1fr, repeat[.])
-///
-/// #set text(10pt)
-/// #v(8pt, weak: true)
-/// #align(right)[
-/// Berlin, the 22nd of December, 2022
-/// ]
-/// ```
-#[elem(Layout)]
-pub struct RepeatElem {
- /// The content to repeat.
- #[required]
- pub body: Content,
-}
-
-impl Layout for RepeatElem {
- #[tracing::instrument(name = "RepeatElem::layout", skip_all)]
- fn layout(
- &self,
- vt: &mut Vt,
- styles: StyleChain,
- regions: Regions,
- ) -> 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).resolve(styles);
-
- let fill = regions.size.x;
- let width = piece.width();
- let count = (fill / width).floor();
- let remaining = fill % width;
- let apart = remaining / (count - 1.0);
-
- let size = Size::new(regions.size.x, piece.height());
-
- if !size.is_finite() {
- bail!(self.span(), "repeat with no size restrictions");
- }
-
- let mut frame = Frame::soft(size);
- if piece.has_baseline() {
- frame.set_baseline(piece.baseline());
- }
-
- let mut offset = Abs::zero();
- if count == 1.0 {
- offset += align.x.position(remaining);
- }
-
- if width > Abs::zero() {
- for _ in 0..(count as usize).min(1000) {
- frame.push_frame(Point::with_x(offset), piece.clone());
- offset += piece.width() + apart;
- }
- }
-
- Ok(Fragment::frame(frame))
- }
-}
diff --git a/crates/typst-library/src/layout/spacing.rs b/crates/typst-library/src/layout/spacing.rs
deleted file mode 100644
index 88b6e2cd..00000000
--- a/crates/typst-library/src/layout/spacing.rs
+++ /dev/null
@@ -1,262 +0,0 @@
-use std::borrow::Cow;
-
-use crate::prelude::*;
-
-/// Inserts horizontal spacing into a paragraph.
-///
-/// The spacing can be absolute, relative, or fractional. In the last case, the
-/// remaining space on the line is distributed among all fractional spacings
-/// according to their relative fractions.
-///
-/// # Example
-/// ```example
-/// First #h(1cm) Second \
-/// First #h(30%) Second \
-/// First #h(2fr) Second #h(1fr) Third
-/// ```
-///
-/// # Mathematical Spacing { #math-spacing }
-/// In [mathematical formulas]($category/math), you can additionally use these
-/// constants to add spacing between elements: `thin`, `med`, `thick`, `quad`, `wide`.
-#[elem(title = "Spacing (H)", Behave)]
-pub struct HElem {
- /// How much spacing to insert.
- #[required]
- pub amount: Spacing,
-
- /// If `{true}`, the spacing collapses at the start or end of a paragraph.
- /// Moreover, from multiple adjacent weak spacings all but the largest one
- /// collapse.
- ///
- /// Weak spacing in markup also causes all adjacent markup spaces to be
- /// removed, regardless of the amount of spacing inserted. To force a space
- /// next to weak spacing, you can explicitly write `[#" "]` (for a normal
- /// space) or `[~]` (for a non-breaking space). The latter can be useful to
- /// create a construct that always attaches to the preceding word with one
- /// non-breaking space, independently of whether a markup space existed in
- /// front or not.
- ///
- /// ```example
- /// #h(1cm, weak: true)
- /// We identified a group of _weak_
- /// specimens that fail to manifest
- /// in most cases. However, when
- /// #h(8pt, weak: true) supported
- /// #h(8pt, weak: true) on both sides,
- /// they do show up.
- ///
- /// Further #h(0pt, weak: true) more,
- /// even the smallest of them swallow
- /// adjacent markup spaces.
- /// ```
- #[default(false)]
- pub weak: bool,
-}
-
-impl HElem {
- /// Zero-width horizontal weak spacing that eats surrounding spaces.
- pub fn hole() -> Self {
- Self::new(Abs::zero().into()).with_weak(true)
- }
-}
-
-impl Behave for HElem {
- fn behaviour(&self) -> Behaviour {
- if self.amount().is_fractional() {
- Behaviour::Destructive
- } else if self.weak(StyleChain::default()) {
- Behaviour::Weak(1)
- } else {
- Behaviour::Invisible
- }
- }
-
- fn larger(
- &self,
- prev: &(Cow<Content>, Behaviour, StyleChain),
- styles: StyleChain,
- ) -> bool {
- let Some(other) = prev.0.to::<Self>() else { return false };
- match (self.amount(), other.amount()) {
- (Spacing::Fr(this), Spacing::Fr(other)) => this > other,
- (Spacing::Rel(this), Spacing::Rel(other)) => {
- this.resolve(styles) > other.resolve(prev.2)
- }
- _ => false,
- }
- }
-}
-
-/// Inserts vertical spacing into a flow of blocks.
-///
-/// The spacing can be absolute, relative, or fractional. In the last case,
-/// the remaining space on the page is distributed among all fractional spacings
-/// according to their relative fractions.
-///
-/// # Example
-/// ```example
-/// #grid(
-/// rows: 3cm,
-/// columns: 6,
-/// gutter: 1fr,
-/// [A #parbreak() B],
-/// [A #v(0pt) B],
-/// [A #v(10pt) B],
-/// [A #v(0pt, weak: true) B],
-/// [A #v(40%, weak: true) B],
-/// [A #v(1fr) B],
-/// )
-/// ```
-#[elem(title = "Spacing (V)", Behave)]
-pub struct VElem {
- /// How much spacing to insert.
- #[required]
- pub amount: Spacing,
-
- /// If `{true}`, the spacing collapses at the start or end of a flow.
- /// Moreover, from multiple adjacent weak spacings all but the largest one
- /// collapse. Weak spacings will always collapse adjacent paragraph spacing,
- /// even if the paragraph spacing is larger.
- ///
- /// ```example
- /// The following theorem is
- /// foundational to the field:
- /// #v(4pt, weak: true)
- /// $ x^2 + y^2 = r^2 $
- /// #v(4pt, weak: true)
- /// The proof is simple:
- /// ```
- #[external]
- pub weak: bool,
-
- /// The element's weakness level, see also [`Behaviour`].
- #[internal]
- #[parse(args.named("weak")?.map(|v: bool| v as usize))]
- pub weakness: usize,
-}
-
-impl VElem {
- /// Normal strong spacing.
- pub fn strong(amount: Spacing) -> Self {
- Self::new(amount).with_weakness(0)
- }
-
- /// User-created weak spacing.
- pub fn weak(amount: Spacing) -> Self {
- Self::new(amount).with_weakness(1)
- }
-
- /// Weak spacing with list attach weakness.
- pub fn list_attach(amount: Spacing) -> Self {
- Self::new(amount).with_weakness(2)
- }
-
- /// Weak spacing with BlockElem::ABOVE/BELOW weakness.
- pub fn block_around(amount: Spacing) -> Self {
- Self::new(amount).with_weakness(3)
- }
-
- /// Weak spacing with BlockElem::SPACING weakness.
- pub fn block_spacing(amount: Spacing) -> Self {
- Self::new(amount).with_weakness(4)
- }
-}
-
-impl Behave for VElem {
- fn behaviour(&self) -> Behaviour {
- if self.amount().is_fractional() {
- Behaviour::Destructive
- } else if self.weakness(StyleChain::default()) > 0 {
- Behaviour::Weak(self.weakness(StyleChain::default()))
- } else {
- Behaviour::Invisible
- }
- }
-
- fn larger(
- &self,
- prev: &(Cow<Content>, Behaviour, StyleChain),
- styles: StyleChain,
- ) -> bool {
- let Some(other) = prev.0.to::<Self>() else { return false };
- match (self.amount(), other.amount()) {
- (Spacing::Fr(this), Spacing::Fr(other)) => this > other,
- (Spacing::Rel(this), Spacing::Rel(other)) => {
- this.resolve(styles) > other.resolve(prev.2)
- }
- _ => false,
- }
- }
-}
-
-cast! {
- VElem,
- v: Content => v.to::<Self>().cloned().ok_or("expected `v` element")?,
-}
-
-/// Kinds of spacing.
-#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
-pub enum Spacing {
- /// Spacing specified in absolute terms and relative to the parent's size.
- Rel(Rel<Length>),
- /// Spacing specified as a fraction of the remaining free space in the
- /// parent.
- Fr(Fr),
-}
-
-impl Spacing {
- /// Whether this is fractional spacing.
- pub fn is_fractional(self) -> bool {
- matches!(self, Self::Fr(_))
- }
-
- /// Whether the spacing is actually no spacing.
- pub fn is_zero(&self) -> bool {
- match self {
- Self::Rel(rel) => rel.is_zero(),
- Self::Fr(fr) => fr.is_zero(),
- }
- }
-}
-
-impl From<Abs> for Spacing {
- fn from(abs: Abs) -> Self {
- Self::Rel(abs.into())
- }
-}
-
-impl From<Em> for Spacing {
- fn from(em: Em) -> Self {
- Self::Rel(Rel::new(Ratio::zero(), em.into()))
- }
-}
-
-impl From<Length> for Spacing {
- fn from(length: Length) -> Self {
- Self::Rel(length.into())
- }
-}
-
-impl From<Fr> for Spacing {
- fn from(fr: Fr) -> Self {
- Self::Fr(fr)
- }
-}
-
-cast! {
- Spacing,
- self => match self {
- Self::Rel(rel) => {
- if rel.rel.is_zero() {
- rel.abs.into_value()
- } else if rel.abs.is_zero() {
- rel.rel.into_value()
- } else {
- rel.into_value()
- }
- }
- Self::Fr(fr) => fr.into_value(),
- },
- v: Rel<Length> => Self::Rel(v),
- v: Fr => Self::Fr(v),
-}
diff --git a/crates/typst-library/src/layout/stack.rs b/crates/typst-library/src/layout/stack.rs
deleted file mode 100644
index c12d2048..00000000
--- a/crates/typst-library/src/layout/stack.rs
+++ /dev/null
@@ -1,345 +0,0 @@
-use crate::layout::{AlignElem, Spacing};
-use crate::prelude::*;
-
-/// Arranges content and spacing horizontally or vertically.
-///
-/// The stack places a list of items along an axis, with optional spacing
-/// between each item.
-///
-/// # Example
-/// ```example
-/// #stack(
-/// dir: ttb,
-/// rect(width: 40pt),
-/// rect(width: 120pt),
-/// rect(width: 90pt),
-/// )
-/// ```
-#[elem(Layout)]
-pub struct StackElem {
- /// The direction along which the items are stacked. Possible values are:
- ///
- /// - `{ltr}`: Left to right.
- /// - `{rtl}`: Right to left.
- /// - `{ttb}`: Top to bottom.
- /// - `{btt}`: Bottom to top.
- ///
- /// 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
- /// direction's inverse direction.
- ///
- /// For example, `{ttb.start()}` is `top`, `{ttb.end()}` is `bottom`,
- /// `{ttb.axis()}` is `{"vertical"}` and `{ttb.inv()}` is equal to `btt`.
- #[default(Dir::TTB)]
- pub dir: Dir,
-
- /// Spacing to insert between items where no explicit spacing was provided.
- pub spacing: Option<Spacing>,
-
- /// The children to stack along the axis.
- #[variadic]
- pub children: Vec<StackChild>,
-}
-
-impl Layout for StackElem {
- #[tracing::instrument(name = "StackElem::layout", skip_all)]
- fn layout(
- &self,
- vt: &mut Vt,
- styles: StyleChain,
- regions: Regions,
- ) -> SourceResult<Fragment> {
- let mut layouter = StackLayouter::new(self.dir(styles), regions, styles);
-
- // Spacing to insert before the next block.
- let spacing = self.spacing(styles);
- let mut deferred = None;
-
- for child in self.children() {
- match child {
- StackChild::Spacing(kind) => {
- layouter.layout_spacing(*kind);
- deferred = None;
- }
- StackChild::Block(block) => {
- if let Some(kind) = deferred {
- layouter.layout_spacing(kind);
- }
-
- layouter.layout_block(vt, block, styles)?;
- deferred = spacing;
- }
- }
- }
-
- Ok(layouter.finish())
- }
-}
-
-/// A child of a stack element.
-#[derive(Clone, PartialEq, Hash)]
-pub enum StackChild {
- /// Spacing between other children.
- Spacing(Spacing),
- /// Arbitrary block-level content.
- Block(Content),
-}
-
-impl Debug for StackChild {
- fn fmt(&self, f: &mut Formatter) -> fmt::Result {
- match self {
- Self::Spacing(kind) => kind.fmt(f),
- Self::Block(block) => block.fmt(f),
- }
- }
-}
-
-cast! {
- StackChild,
- self => match self {
- Self::Spacing(spacing) => spacing.into_value(),
- Self::Block(content) => content.into_value(),
- },
- v: Spacing => Self::Spacing(v),
- v: Content => Self::Block(v),
-}
-
-/// Performs stack layout.
-struct StackLayouter<'a> {
- /// The stacking direction.
- dir: Dir,
- /// The axis of the stacking direction.
- axis: Axis,
- /// The regions to layout children into.
- regions: Regions<'a>,
- /// The inherited styles.
- styles: StyleChain<'a>,
- /// Whether the stack itself should expand to fill the region.
- expand: Axes<bool>,
- /// The initial size of the current region before we started subtracting.
- initial: Size,
- /// The generic size used by the frames for the current region.
- used: Gen<Abs>,
- /// The sum of fractions in the current region.
- fr: Fr,
- /// Already layouted items whose exact positions are not yet known due to
- /// fractional spacing.
- items: Vec<StackItem>,
- /// Finished frames for previous regions.
- finished: Vec<Frame>,
-}
-
-/// A prepared item in a stack layout.
-enum StackItem {
- /// Absolute spacing between other items.
- Absolute(Abs),
- /// Fractional spacing between other items.
- Fractional(Fr),
- /// A frame for a layouted block.
- Frame(Frame, Axes<FixedAlign>),
-}
-
-impl<'a> StackLayouter<'a> {
- /// Create a new stack layouter.
- fn new(dir: Dir, mut regions: Regions<'a>, styles: StyleChain<'a>) -> Self {
- let axis = dir.axis();
- let expand = regions.expand;
-
- // Disable expansion along the block axis for children.
- regions.expand.set(axis, false);
-
- Self {
- dir,
- axis,
- regions,
- styles,
- expand,
- initial: regions.size,
- used: Gen::zero(),
- fr: Fr::zero(),
- items: vec![],
- finished: vec![],
- }
- }
-
- /// Add spacing along the spacing direction.
- #[tracing::instrument(name = "StackLayouter::layout_spacing", skip_all)]
- fn layout_spacing(&mut self, spacing: Spacing) {
- match spacing {
- Spacing::Rel(v) => {
- // Resolve the spacing and limit it to the remaining space.
- let resolved = v
- .resolve(self.styles)
- .relative_to(self.regions.base().get(self.axis));
- let remaining = self.regions.size.get_mut(self.axis);
- let limited = resolved.min(*remaining);
- if self.dir.axis() == Axis::Y {
- *remaining -= limited;
- }
- self.used.main += limited;
- self.items.push(StackItem::Absolute(resolved));
- }
- Spacing::Fr(v) => {
- self.fr += v;
- self.items.push(StackItem::Fractional(v));
- }
- }
- }
-
- /// Layout an arbitrary block.
- #[tracing::instrument(name = "StackLayouter::layout_block", skip_all)]
- fn layout_block(
- &mut self,
- vt: &mut Vt,
- block: &Content,
- styles: StyleChain,
- ) -> SourceResult<()> {
- if self.regions.is_full() {
- self.finish_region();
- }
-
- // Block-axis alignment of the `AlignElement` is respected by stacks.
- 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))
- } else {
- AlignElem::alignment_in(styles)
- }
- .resolve(styles);
-
- let fragment = block.layout(vt, styles, self.regions)?;
- let len = fragment.len();
- for (i, frame) in fragment.into_iter().enumerate() {
- // Grow our size, shrink the region and save the frame for later.
- let size = frame.size();
- if self.dir.axis() == Axis::Y {
- self.regions.size.y -= size.y;
- }
-
- let gen = match self.axis {
- Axis::X => Gen::new(size.y, size.x),
- Axis::Y => Gen::new(size.x, size.y),
- };
-
- self.used.main += gen.main;
- self.used.cross.set_max(gen.cross);
-
- self.items.push(StackItem::Frame(frame, align));
-
- if i + 1 < len {
- self.finish_region();
- }
- }
-
- Ok(())
- }
-
- /// Advance to the next region.
- fn finish_region(&mut self) {
- // Determine the size of the stack in this region depending on whether
- // the region expands.
- let mut size = self
- .expand
- .select(self.initial, self.used.into_axes(self.axis))
- .min(self.initial);
-
- // Expand fully if there are fr spacings.
- let full = self.initial.get(self.axis);
- let remaining = full - self.used.main;
- if self.fr.get() > 0.0 && full.is_finite() {
- self.used.main = full;
- size.set(self.axis, full);
- }
-
- let mut output = Frame::hard(size);
- let mut cursor = Abs::zero();
- 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, align) => {
- if self.dir.is_positive() {
- ruler = ruler.max(align.get(self.axis));
- } else {
- ruler = ruler.min(align.get(self.axis));
- }
-
- // Align along the main axis.
- let parent = size.get(self.axis);
- let child = frame.size().get(self.axis);
- let main = ruler.position(parent - self.used.main)
- + if self.dir.is_positive() {
- cursor
- } else {
- self.used.main - child - cursor
- };
-
- // Align along the cross axis.
- let other = self.axis.other();
- let cross = align
- .get(other)
- .position(size.get(other) - frame.size().get(other));
-
- let pos = Gen::new(cross, main).to_point(self.axis);
- cursor += child;
- output.push_frame(pos, frame);
- }
- }
- }
-
- // Advance to the next region.
- self.regions.next();
- self.initial = self.regions.size;
- self.used = Gen::zero();
- self.fr = Fr::zero();
- self.finished.push(output);
- }
-
- /// Finish layouting and return the resulting frames.
- fn finish(mut self) -> Fragment {
- self.finish_region();
- Fragment::frames(self.finished)
- }
-}
-
-/// A container with a main and cross component.
-#[derive(Default, Copy, Clone, Eq, PartialEq, Hash)]
-struct Gen<T> {
- /// The main component.
- pub cross: T,
- /// The cross component.
- pub main: T,
-}
-
-impl<T> Gen<T> {
- /// Create a new instance from the two components.
- const fn new(cross: T, main: T) -> Self {
- Self { cross, main }
- }
-
- /// Convert to the specific representation, given the current main axis.
- fn into_axes(self, main: Axis) -> Axes<T> {
- match main {
- Axis::X => Axes::new(self.main, self.cross),
- Axis::Y => Axes::new(self.cross, self.main),
- }
- }
-}
-
-impl Gen<Abs> {
- /// The zero value.
- fn zero() -> Self {
- Self { cross: Abs::zero(), main: Abs::zero() }
- }
-
- /// Convert to a point.
- fn to_point(self, main: Axis) -> Point {
- self.into_axes(main).to_point()
- }
-}
diff --git a/crates/typst-library/src/layout/table.rs b/crates/typst-library/src/layout/table.rs
deleted file mode 100644
index 0eff7a44..00000000
--- a/crates/typst-library/src/layout/table.rs
+++ /dev/null
@@ -1,361 +0,0 @@
-use typst::eval::{CastInfo, Reflect};
-
-use crate::layout::{AlignElem, GridLayouter, TrackSizings};
-use crate::meta::Figurable;
-use crate::prelude::*;
-
-/// A table of items.
-///
-/// 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]($grid) for more information on how to size the
-/// table tracks.
-///
-/// To give a table a caption and make it [referenceable]($ref), put it into a
-/// [figure]($figure).
-///
-/// # Example
-/// ```example
-/// #table(
-/// columns: (1fr, auto, auto),
-/// inset: 10pt,
-/// align: horizon,
-/// [], [*Area*], [*Parameters*],
-/// image("cylinder.svg"),
-/// $ pi h (D^2 - d^2) / 4 $,
-/// [
-/// $h$: height \
-/// $D$: outer radius \
-/// $d$: inner radius
-/// ],
-/// image("tetrahedron.svg"),
-/// $ sqrt(2) / 12 a^3 $,
-/// [$a$: edge length]
-/// )
-/// ```
-#[elem(Layout, LocalName, Figurable)]
-pub struct TableElem {
- /// The column sizes. See the [grid documentation]($grid) for more
- /// information on track sizing.
- #[borrowed]
- pub columns: TrackSizings,
-
- /// The row sizes. See the [grid documentation]($grid) for more information
- /// on track sizing.
- #[borrowed]
- pub rows: TrackSizings,
-
- /// 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]($grid) for more information on gutters.
- #[borrowed]
- #[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]($grid) for more information on gutters.
- #[parse(args.named("row-gutter")?.or_else(|| gutter.clone()))]
- #[borrowed]
- pub row_gutter: TrackSizings,
-
- /// How to fill the cells.
- ///
- /// This can be a color or a function that returns a color. The function is
- /// passed the cells' column and row index, starting at zero. This can be
- /// used to implement striped tables.
- ///
- /// ```example
- /// #table(
- /// fill: (col, _) => if calc.odd(col) { luma(240) } else { white },
- /// align: (col, row) =>
- /// if row == 0 { center }
- /// else if col == 0 { left }
- /// else { right },
- /// columns: 4,
- /// [], [*Q1*], [*Q2*], [*Q3*],
- /// [Revenue:], [1000 €], [2000 €], [3000 €],
- /// [Expenses:], [500 €], [1000 €], [1500 €],
- /// [Profit:], [500 €], [1000 €], [1500 €],
- /// )
- /// ```
- #[borrowed]
- pub fill: Celled<Option<Paint>>,
-
- /// How to align the cells' content.
- ///
- /// This can either be a single alignment, an array of alignments
- /// (corresponding to each column) or a function that returns an alignment.
- /// The function is passed the cells' column and row index, starting at zero.
- /// If set to `{auto}`, the outer alignment is used.
- ///
- /// ```example
- /// #table(
- /// columns: 3,
- /// align: (x, y) => (left, center, right).at(x),
- /// [Hello], [Hello], [Hello],
- /// [A], [B], [C],
- /// )
- /// ```
- #[borrowed]
- pub align: Celled<Smart<Align>>,
-
- /// How to [stroke]($stroke) the cells.
- ///
- /// 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/).
- #[resolve]
- #[fold]
- #[default(Some(Stroke::default()))]
- pub stroke: Option<Stroke>,
-
- /// How much to pad the cells' content.
- ///
- /// ```example
- /// #table(
- /// inset: 10pt,
- /// [Hello],
- /// [World],
- /// )
- ///
- /// #table(
- /// columns: 2,
- /// inset: (
- /// x: 20pt,
- /// y: 10pt,
- /// ),
- /// [Hello],
- /// [World],
- /// )
- /// ```
- #[fold]
- #[default(Sides::splat(Abs::pt(5.0).into()))]
- pub inset: Sides<Option<Rel<Length>>>,
-
- /// The contents of the table cells.
- #[variadic]
- pub children: Vec<Content>,
-}
-
-impl Layout for TableElem {
- #[tracing::instrument(name = "TableElem::layout", skip_all)]
- fn layout(
- &self,
- vt: &mut Vt,
- styles: StyleChain,
- regions: Regions,
- ) -> SourceResult<Fragment> {
- let inset = self.inset(styles);
- let align = self.align(styles);
- let columns = self.columns(styles);
- let rows = self.rows(styles);
- let column_gutter = self.column_gutter(styles);
- let row_gutter = self.row_gutter(styles);
-
- let tracks = Axes::new(columns.0.as_slice(), rows.0.as_slice());
- let gutter = Axes::new(column_gutter.0.as_slice(), row_gutter.0.as_slice());
- let cols = tracks.x.len().max(1);
- let cells: Vec<_> = self
- .children()
- .iter()
- .enumerate()
- .map(|(i, child)| {
- let mut child = child.clone().padded(inset);
-
- let x = i % cols;
- let y = i / cols;
- if let Smart::Custom(alignment) = align.resolve(vt, x, y)? {
- child = child.styled(AlignElem::set_alignment(alignment));
- }
-
- Ok(child)
- })
- .collect::<SourceResult<_>>()?;
-
- let fill = self.fill(styles);
- let stroke = self.stroke(styles).map(Stroke::unwrap_or_default);
-
- // Prepare grid layout by unifying content and gutter tracks.
- let layouter =
- GridLayouter::new(tracks, gutter, &cells, regions, styles, self.span());
-
- // Measure the columns and layout the grid row-by-row.
- let mut layout = layouter.layout(vt)?;
-
- // Add lines and backgrounds.
- for (frame, rows) in layout.fragment.iter_mut().zip(&layout.rows) {
- if layout.cols.is_empty() || rows.is_empty() {
- continue;
- }
-
- // Render table lines.
- if let Some(stroke) = &stroke {
- let thickness = stroke.thickness;
- let half = thickness / 2.0;
-
- // Render horizontal lines.
- for offset in points(rows.iter().map(|piece| piece.height)) {
- let target = Point::with_x(frame.width() + thickness);
- let hline = Geometry::Line(target).stroked(stroke.clone());
- frame.prepend(
- Point::new(-half, offset),
- FrameItem::Shape(hline, self.span()),
- );
- }
-
- // Render vertical lines.
- for offset in points(layout.cols.iter().copied()) {
- let target = Point::with_y(frame.height() + thickness);
- let vline = Geometry::Line(target).stroked(stroke.clone());
- frame.prepend(
- Point::new(offset, -half),
- FrameItem::Shape(vline, self.span()),
- );
- }
- }
-
- // Render cell backgrounds.
- let mut dx = Abs::zero();
- for (x, &col) in layout.cols.iter().enumerate() {
- let mut dy = Abs::zero();
- for row in rows {
- if let Some(fill) = fill.resolve(vt, x, row.y)? {
- let pos = Point::new(dx, dy);
- let size = Size::new(col, row.height);
- let rect = Geometry::Rect(size).filled(fill);
- frame.prepend(pos, FrameItem::Shape(rect, self.span()));
- }
- dy += row.height;
- }
- dx += col;
- }
- }
-
- Ok(layout.fragment)
- }
-}
-
-/// Turn an iterator of extents into an iterator of offsets before, in between,
-/// and after the extents, e.g. [10mm, 5mm] -> [0mm, 10mm, 15mm].
-fn points(extents: impl IntoIterator<Item = Abs>) -> impl Iterator<Item = Abs> {
- let mut offset = Abs::zero();
- std::iter::once(Abs::zero()).chain(extents).map(move |extent| {
- offset += extent;
- offset
- })
-}
-
-/// A value that can be configured per cell.
-#[derive(Debug, Clone, PartialEq, Hash)]
-pub enum Celled<T> {
- /// A bare value, the same for all cells.
- Value(T),
- /// A closure mapping from cell coordinates to a value.
- Func(Func),
- /// An array of alignment values corresponding to each column.
- Array(Vec<T>),
-}
-
-impl<T: Default + Clone + FromValue> Celled<T> {
- /// Resolve the value based on the cell position.
- pub fn resolve(&self, vt: &mut Vt, x: usize, y: usize) -> SourceResult<T> {
- Ok(match self {
- Self::Value(value) => value.clone(),
- Self::Func(func) => func.call_vt(vt, [x, y])?.cast().at(func.span())?,
- Self::Array(array) => x
- .checked_rem(array.len())
- .and_then(|i| array.get(i))
- .cloned()
- .unwrap_or_default(),
- })
- }
-}
-
-impl<T: Default> Default for Celled<T> {
- fn default() -> Self {
- Self::Value(T::default())
- }
-}
-
-impl<T: Reflect> Reflect for Celled<T> {
- fn input() -> CastInfo {
- T::input() + Array::input() + Func::input()
- }
-
- fn output() -> CastInfo {
- T::output() + Array::output() + Func::output()
- }
-
- fn castable(value: &Value) -> bool {
- Array::castable(value) || Func::castable(value) || T::castable(value)
- }
-}
-
-impl<T: IntoValue> IntoValue for Celled<T> {
- fn into_value(self) -> Value {
- match self {
- Self::Value(value) => value.into_value(),
- Self::Func(func) => func.into_value(),
- Self::Array(arr) => arr.into_value(),
- }
- }
-}
-
-impl<T: FromValue> FromValue for Celled<T> {
- fn from_value(value: Value) -> StrResult<Self> {
- match value {
- Value::Func(v) => Ok(Self::Func(v)),
- Value::Array(array) => Ok(Self::Array(
- array.into_iter().map(T::from_value).collect::<StrResult<_>>()?,
- )),
- v if T::castable(&v) => Ok(Self::Value(T::from_value(v)?)),
- v => Err(Self::error(&v)),
- }
- }
-}
-
-impl LocalName for TableElem {
- fn local_name(lang: Lang, _: Option<Region>) -> &'static str {
- match lang {
- Lang::ALBANIAN => "Tabel",
- Lang::ARABIC => "جدول",
- Lang::BOKMÅL => "Tabell",
- Lang::CHINESE => "表",
- Lang::CZECH => "Tabulka",
- Lang::DANISH => "Tabel",
- Lang::DUTCH => "Tabel",
- Lang::FILIPINO => "Talaan",
- Lang::FINNISH => "Taulukko",
- Lang::FRENCH => "Tableau",
- Lang::GERMAN => "Tabelle",
- Lang::GREEK => "Πίνακας",
- Lang::HUNGARIAN => "Táblázat",
- Lang::ITALIAN => "Tabella",
- Lang::NYNORSK => "Tabell",
- Lang::POLISH => "Tabela",
- Lang::PORTUGUESE => "Tabela",
- Lang::ROMANIAN => "Tabelul",
- Lang::RUSSIAN => "Таблица",
- Lang::SLOVENIAN => "Tabela",
- Lang::SPANISH => "Tabla",
- Lang::SWEDISH => "Tabell",
- Lang::TURKISH => "Tablo",
- Lang::UKRAINIAN => "Таблиця",
- Lang::VIETNAMESE => "Bảng",
- Lang::JAPANESE => "表",
- Lang::ENGLISH | _ => "Table",
- }
- }
-}
-
-impl Figurable for TableElem {}
diff --git a/crates/typst-library/src/layout/terms.rs b/crates/typst-library/src/layout/terms.rs
deleted file mode 100644
index d373768d..00000000
--- a/crates/typst-library/src/layout/terms.rs
+++ /dev/null
@@ -1,162 +0,0 @@
-use crate::layout::{BlockElem, HElem, ParElem, Spacing, VElem};
-use crate::prelude::*;
-
-/// A list of terms and their descriptions.
-///
-/// Displays a sequence of terms and their descriptions vertically. When the
-/// descriptions span over multiple lines, they use hanging indent to
-/// communicate the visual hierarchy.
-///
-/// # Example
-/// ```example
-/// / Ligature: A merged glyph.
-/// / Kerning: A spacing adjustment
-/// between two adjacent letters.
-/// ```
-///
-/// # 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.
-#[elem(scope, title = "Term List", Layout)]
-pub struct TermsElem {
- /// 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
- /// each other, this is set to `{true}`; if items are separated by a blank
- /// line, this is set to `{false}`.
- ///
- /// ```example
- /// / Fact: If a term list has a lot
- /// of text, and maybe other inline
- /// content, it should not be tight
- /// anymore.
- ///
- /// / Tip: To make it wide, simply
- /// insert a blank line between the
- /// items.
- /// ```
- #[default(true)]
- pub tight: bool,
-
- /// The separator between the item and the description.
- ///
- /// If you want to just separate them with a certain amount of space, use
- /// `{h(2cm, weak: true)}` as the separator and replace `{2cm}` with your
- /// desired amount of space.
- ///
- /// ```example
- /// #set terms(separator: [: ])
- ///
- /// / Colon: A nice separator symbol.
- /// ```
- #[default(HElem::new(Em::new(0.6).into()).with_weak(true).pack())]
- #[borrowed]
- pub separator: Content,
-
- /// The indentation of each item.
- pub indent: Length,
-
- /// The hanging indent of the description.
- ///
- /// This is in addition to the whole item's `indent`.
- ///
- /// ```example
- /// #set terms(hanging-indent: 0pt)
- /// / Term: This term list does not
- /// make use of hanging indents.
- /// ```
- #[default(Em::new(2.0).into())]
- pub hanging_indent: Length,
-
- /// The spacing between the items of a wide (non-tight) term list.
- ///
- /// If set to `{auto}`, uses the spacing [below blocks]($block.below).
- pub spacing: Smart<Spacing>,
-
- /// The term list's children.
- ///
- /// When using the term list syntax, adjacent items are automatically
- /// collected into term lists, even through constructs like for loops.
- ///
- /// ```example
- /// #for (year, product) in (
- /// "1978": "TeX",
- /// "1984": "LaTeX",
- /// "2019": "Typst",
- /// ) [/ #product: Born in #year.]
- /// ```
- #[variadic]
- pub children: Vec<TermItem>,
-}
-
-#[scope]
-impl TermsElem {
- #[elem]
- type TermItem;
-}
-
-impl Layout for TermsElem {
- #[tracing::instrument(name = "TermsElem::layout", skip_all)]
- fn layout(
- &self,
- vt: &mut Vt,
- styles: StyleChain,
- regions: Regions,
- ) -> SourceResult<Fragment> {
- let separator = self.separator(styles);
- let indent = self.indent(styles);
- let hanging_indent = self.hanging_indent(styles);
- let gutter = if self.tight(styles) {
- ParElem::leading_in(styles).into()
- } else {
- self.spacing(styles)
- .unwrap_or_else(|| *BlockElem::below_in(styles).amount())
- };
-
- let mut seq = vec![];
- for (i, child) in self.children().iter().enumerate() {
- if i > 0 {
- seq.push(VElem::new(gutter).with_weakness(1).pack());
- }
- if !indent.is_zero() {
- seq.push(HElem::new(indent.into()).pack());
- }
- seq.push(child.term().clone().strong());
- seq.push((*separator).clone());
- seq.push(child.description().clone());
- }
-
- Content::sequence(seq)
- .styled(ParElem::set_hanging_indent(hanging_indent + indent))
- .layout(vt, styles, regions)
- }
-}
-
-/// A term list item.
-#[elem(name = "item", title = "Term List Item")]
-pub struct TermItem {
- /// The term described by the list item.
- #[required]
- pub term: Content,
-
- /// The description of the term.
- #[required]
- pub description: Content,
-}
-
-cast! {
- TermItem,
- array: Array => {
- let mut iter = array.into_iter();
- let (term, description) = match (iter.next(), iter.next(), iter.next()) {
- (Some(a), Some(b), None) => (a.cast()?, b.cast()?),
- _ => bail!("array must contain exactly two entries"),
- };
- Self::new(term, description)
- },
- v: Content => v.to::<Self>().cloned().ok_or("expected term item or array")?,
-}
diff --git a/crates/typst-library/src/layout/transform.rs b/crates/typst-library/src/layout/transform.rs
deleted file mode 100644
index 012a146d..00000000
--- a/crates/typst-library/src/layout/transform.rs
+++ /dev/null
@@ -1,187 +0,0 @@
-use typst::geom::Transform;
-
-use crate::prelude::*;
-
-/// Moves content without affecting layout.
-///
-/// The `move` function allows you to move content while the layout still 'sees'
-/// it at the original positions. Containers will still be sized as if the
-/// content was not moved.
-///
-/// # Example
-/// ```example
-/// #rect(inset: 0pt, move(
-/// dx: 6pt, dy: 6pt,
-/// rect(
-/// inset: 8pt,
-/// fill: white,
-/// stroke: black,
-/// [Abra cadabra]
-/// )
-/// ))
-/// ```
-#[elem(Layout)]
-pub struct MoveElem {
- /// The horizontal displacement of the content.
- pub dx: Rel<Length>,
-
- /// The vertical displacement of the content.
- pub dy: Rel<Length>,
-
- /// The content to move.
- #[required]
- pub body: Content,
-}
-
-impl Layout for MoveElem {
- #[tracing::instrument(name = "MoveElem::layout", skip_all)]
- fn layout(
- &self,
- vt: &mut Vt,
- styles: StyleChain,
- regions: Regions,
- ) -> SourceResult<Fragment> {
- 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_map(regions.base(), Rel::relative_to);
- frame.translate(delta.to_point());
- Ok(Fragment::frame(frame))
- }
-}
-
-/// Rotates content without affecting layout.
-///
-/// Rotates an element by a given angle. The layout will act as if the element
-/// was not rotated.
-///
-/// # Example
-/// ```example
-/// #stack(
-/// dir: ltr,
-/// spacing: 1fr,
-/// ..range(16)
-/// .map(i => rotate(24deg * i)[X]),
-/// )
-/// ```
-#[elem(Layout)]
-pub struct RotateElem {
- /// The amount of rotation.
- ///
- /// ```example
- /// #rotate(-1.571rad)[Space!]
- /// ```
- ///
- #[positional]
- pub angle: Angle,
-
- /// The origin of the rotation.
- ///
- /// If, for instance, you wanted the bottom left corner of the rotated
- /// element to stay aligned with the baseline, you would set it to `bottom +
- /// left` instead.
- ///
- /// ```example
- /// #set text(spacing: 8pt)
- /// #let square = square.with(width: 8pt)
- ///
- /// #box(square())
- /// #box(rotate(30deg, origin: center, square()))
- /// #box(rotate(30deg, origin: top + left, square()))
- /// #box(rotate(30deg, origin: bottom + right, square()))
- /// ```
- #[fold]
- #[default(HAlign::Center + VAlign::Horizon)]
- pub origin: Align,
-
- /// The content to rotate.
- #[required]
- pub body: Content,
-}
-
-impl Layout for RotateElem {
- #[tracing::instrument(name = "RotateElem::layout", skip_all)]
- fn layout(
- &self,
- vt: &mut Vt,
- styles: StyleChain,
- regions: Regions,
- ) -> 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)
- .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));
- frame.transform(ts);
- Ok(Fragment::frame(frame))
- }
-}
-
-/// Scales content without affecting layout.
-///
-/// Lets you mirror content by specifying a negative scale on a single axis.
-///
-/// # Example
-/// ```example
-/// #set align(center)
-/// #scale(x: -100%)[This is mirrored.]
-/// ```
-#[elem(Layout)]
-pub struct ScaleElem {
- /// The horizontal scaling factor.
- ///
- /// The body will be mirrored horizontally if the parameter is negative.
- #[parse(
- let all = args.find()?;
- args.named("x")?.or(all)
- )]
- #[default(Ratio::one())]
- pub x: Ratio,
-
- /// The vertical scaling factor.
- ///
- /// The body will be mirrored vertically if the parameter is negative.
- #[parse(args.named("y")?.or(all))]
- #[default(Ratio::one())]
- pub y: Ratio,
-
- /// The origin of the transformation.
- ///
- /// ```example
- /// A#box(scale(75%)[A])A \
- /// B#box(scale(75%, origin: bottom + left)[B])B
- /// ```
- #[fold]
- #[default(HAlign::Center + VAlign::Horizon)]
- pub origin: Align,
-
- /// The content to scale.
- #[required]
- pub body: Content,
-}
-
-impl Layout for ScaleElem {
- #[tracing::instrument(name = "ScaleElem::layout", skip_all)]
- fn layout(
- &self,
- vt: &mut Vt,
- styles: StyleChain,
- regions: Regions,
- ) -> 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)
- .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));
- frame.transform(transform);
- Ok(Fragment::frame(frame))
- }
-}
diff --git a/crates/typst-library/src/lib.rs b/crates/typst-library/src/lib.rs
deleted file mode 100644
index 212debb0..00000000
--- a/crates/typst-library/src/lib.rs
+++ /dev/null
@@ -1,170 +0,0 @@
-//! Typst's standard library.
-
-#![allow(clippy::wildcard_in_or_patterns)]
-#![allow(clippy::manual_range_contains)]
-#![allow(clippy::comparison_chain)]
-
-pub mod compute;
-pub mod layout;
-pub mod math;
-pub mod meta;
-pub mod prelude;
-pub mod shared;
-pub mod symbols;
-pub mod text;
-pub mod visualize;
-
-use typst::eval::{Array, LangItems, Library, Module, Scope, Smart};
-use typst::geom::{Align, Color, Dir};
-use typst::model::{NativeElement, Styles};
-
-use self::layout::LayoutRoot;
-
-/// Construct the standard library.
-pub fn build() -> Library {
- let math = math::module();
- let global = global(math.clone());
- Library { global, math, styles: styles(), items: items() }
-}
-
-/// Construct the module with global definitions.
-#[tracing::instrument(skip_all)]
-fn global(math: Module) -> Module {
- let mut global = Scope::deduplicating();
- text::define(&mut global);
- global.define_module(math);
- layout::define(&mut global);
- visualize::define(&mut global);
- meta::define(&mut global);
- symbols::define(&mut global);
- compute::define(&mut global);
- prelude(&mut global);
- Module::new("global", global)
-}
-
-/// Defines scoped values that are globally available, too.
-fn prelude(global: &mut Scope) {
- global.reset_category();
- 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("oklab", Color::oklab_data());
- global.define("oklch", Color::oklch_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.
-fn styles() -> Styles {
- Styles::new()
-}
-
-/// Construct the standard lang item mapping.
-fn items() -> LangItems {
- LangItems {
- layout: |world, content, styles| content.layout_root(world, styles),
- em: text::TextElem::size_in,
- dir: text::TextElem::dir_in,
- space: || text::SpaceElem::new().pack(),
- linebreak: || text::LinebreakElem::new().pack(),
- text: |text| text::TextElem::new(text).pack(),
- text_elem: text::TextElem::elem(),
- text_str: |content| Some(content.to::<text::TextElem>()?.text()),
- 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(),
- raw: |text, lang, block| {
- let mut elem = text::RawElem::new(text).with_block(block);
- if let Some(lang) = lang {
- elem.push_lang(Some(lang));
- }
- elem.pack()
- },
- raw_languages: text::RawElem::languages,
- link: |url| meta::LinkElem::from_url(url).pack(),
- reference: |target, supplement| {
- let mut elem = meta::RefElem::new(target);
- if let Some(supplement) = supplement {
- elem.push_supplement(Smart::Custom(Some(meta::Supplement::Content(
- supplement,
- ))));
- }
- elem.pack()
- },
- bibliography_keys: meta::BibliographyElem::keys,
- heading: |level, title| meta::HeadingElem::new(title).with_level(level).pack(),
- heading_elem: meta::HeadingElem::elem(),
- list_item: |body| layout::ListItem::new(body).pack(),
- enum_item: |number, body| {
- let mut elem = layout::EnumItem::new(body);
- if let Some(number) = number {
- elem.push_number(Some(number));
- }
- elem.pack()
- },
- term_item: |term, description| layout::TermItem::new(term, description).pack(),
- 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(),
- math_attach: |base, t, b, tl, bl, tr, br| {
- let mut elem = math::AttachElem::new(base);
- if let Some(t) = t {
- elem.push_t(Some(t));
- }
- if let Some(b) = b {
- elem.push_b(Some(b));
- }
- if let Some(tl) = tl {
- elem.push_tl(Some(tl));
- }
- if let Some(bl) = bl {
- elem.push_bl(Some(bl));
- }
- if let Some(tr) = tr {
- elem.push_tr(Some(tr));
- }
- if let Some(br) = br {
- elem.push_br(Some(br));
- }
- elem.pack()
- },
- math_primes: |count| math::PrimesElem::new(count).pack(),
- math_accent: |base, accent| {
- math::AccentElem::new(base, math::Accent::new(accent)).pack()
- },
- math_frac: |num, denom| math::FracElem::new(num, denom).pack(),
- math_root: |index, radicand| {
- math::RootElem::new(radicand).with_index(index).pack()
- },
- }
-}
diff --git a/crates/typst-library/src/math/accent.rs b/crates/typst-library/src/math/accent.rs
deleted file mode 100644
index 1b2d4793..00000000
--- a/crates/typst-library/src/math/accent.rs
+++ /dev/null
@@ -1,137 +0,0 @@
-use crate::math::*;
-
-/// How much the accent can be shorter than the base.
-const ACCENT_SHORT_FALL: Em = Em::new(0.5);
-
-/// Attaches an accent to a base.
-///
-/// # Example
-/// ```example
-/// $grave(a) = accent(a, `)$ \
-/// $arrow(a) = accent(a, arrow)$ \
-/// $tilde(a) = accent(a, \u{0303})$
-/// ```
-#[elem(LayoutMath)]
-pub struct AccentElem {
- /// The base to which the accent is applied.
- /// May consist of multiple letters.
- ///
- /// ```example
- /// $arrow(A B C)$
- /// ```
- #[required]
- pub base: Content,
-
- /// The accent to apply to the base.
- ///
- /// Supported accents include:
- ///
- /// | Accent | Name | Codepoint |
- /// | ------------- | --------------- | --------- |
- /// | Grave | `grave` | <code>&DiacriticalGrave;</code> |
- /// | Acute | `acute` | `´` |
- /// | Circumflex | `hat` | `^` |
- /// | Tilde | `tilde` | `~` |
- /// | Macron | `macron` | `¯` |
- /// | Breve | `breve` | `˘` |
- /// | Dot | `dot` | `.` |
- /// | Double dot | `dot.double` | `¨` |
- /// | Triple dot | `dot.triple` | <code>&tdot;</code> |
- /// | Quadruple dot | `dot.quad` | <code>&DotDot;</code> |
- /// | Diaeresis | `diaer` | `¨` |
- /// | Circle | `circle` | `∘` |
- /// | Double acute | `acute.double` | `˝` |
- /// | Caron | `caron` | `ˇ` |
- /// | Right arrow | `arrow`, `->` | `→` |
- /// | Left arrow | `arrow.l`, `<-` | `←` |
- #[required]
- pub accent: Accent,
-}
-
-impl LayoutMath for AccentElem {
- #[tracing::instrument(skip(ctx))]
- fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
- ctx.style(ctx.style.with_cramped(true));
- let base = ctx.layout_fragment(self.base())?;
- ctx.unstyle();
-
- // Preserve class to preserve automatic spacing.
- let base_class = base.class().unwrap_or(MathClass::Normal);
- let base_attach = match &base {
- MathFragment::Glyph(base) => {
- attachment(ctx, base.id, base.italics_correction)
- }
- _ => (base.width() + base.italics_correction()) / 2.0,
- };
-
- // Forcing the accent to be at least as large as the base makes it too
- // wide in many case.
- let Accent(c) = self.accent();
- let glyph = GlyphFragment::new(ctx, *c, self.span());
- let short_fall = ACCENT_SHORT_FALL.scaled(ctx);
- let variant = glyph.stretch_horizontal(ctx, base.width(), short_fall);
- let accent = variant.frame;
- let accent_attach = match variant.id {
- Some(id) => attachment(ctx, id, variant.italics_correction),
- None => accent.width() / 2.0,
- };
-
- // Descent is negative because the accent's ink bottom is above the
- // baseline. Therefore, the default gap is the accent's negated descent
- // minus the accent base height. Only if the base is very small, we need
- // a larger gap so that the accent doesn't move too low.
- let accent_base_height = scaled!(ctx, accent_base_height);
- let gap = -accent.descent() - base.height().min(accent_base_height);
- let size = Size::new(base.width(), accent.height() + gap + base.height());
- let accent_pos = Point::with_x(base_attach - accent_attach);
- let base_pos = Point::with_y(accent.height() + gap);
- let base_ascent = base.ascent();
- let baseline = base_pos.y + base.ascent();
-
- let mut frame = Frame::soft(size);
- frame.set_baseline(baseline);
- frame.push_frame(accent_pos, accent);
- frame.push_frame(base_pos, base.into_frame());
- ctx.push(
- FrameFragment::new(ctx, frame)
- .with_class(base_class)
- .with_base_ascent(base_ascent),
- );
-
- Ok(())
- }
-}
-
-/// The horizontal attachment position for the given glyph.
-fn attachment(ctx: &MathContext, id: GlyphId, italics_correction: Abs) -> Abs {
- ctx.table
- .glyph_info
- .and_then(|info| info.top_accent_attachments)
- .and_then(|attachments| attachments.get(id))
- .map(|record| record.value.scaled(ctx))
- .unwrap_or_else(|| {
- let advance = ctx.ttf.glyph_hor_advance(id).unwrap_or_default();
- (advance.scaled(ctx) + italics_correction) / 2.0
- })
-}
-
-/// An accent character.
-#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash)]
-pub struct Accent(char);
-
-impl Accent {
- /// Normalize a character into an accent.
- pub fn new(c: char) -> Self {
- Self(Symbol::combining_accent(c).unwrap_or(c))
- }
-}
-
-cast! {
- Accent,
- self => self.0.into_value(),
- v: char => Self::new(v),
- v: Content => match v.to::<TextElem>() {
- Some(elem) => Value::Str(elem.text().clone().into()).cast()?,
- None => bail!("expected text"),
- },
-}
diff --git a/crates/typst-library/src/math/align.rs b/crates/typst-library/src/math/align.rs
deleted file mode 100644
index 4192e97b..00000000
--- a/crates/typst-library/src/math/align.rs
+++ /dev/null
@@ -1,60 +0,0 @@
-use crate::math::*;
-
-/// A math alignment point: `&`, `&&`.
-#[elem(title = "Alignment Point", LayoutMath)]
-pub struct AlignPointElem {}
-
-impl LayoutMath for AlignPointElem {
- #[tracing::instrument(skip(ctx))]
- fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
- ctx.push(MathFragment::Align);
- Ok(())
- }
-}
-
-pub(super) struct AlignmentResult {
- pub points: Vec<Abs>,
- pub width: Abs,
-}
-
-/// Determine the position of the alignment points.
-pub(super) fn alignments(rows: &[MathRow]) -> AlignmentResult {
- let mut widths = Vec::<Abs>::new();
-
- let mut pending_width = Abs::zero();
- for row in rows {
- let mut width = Abs::zero();
- let mut alignment_index = 0;
-
- for fragment in row.iter() {
- if matches!(fragment, MathFragment::Align) {
- if alignment_index < widths.len() {
- widths[alignment_index].set_max(width);
- } else {
- widths.push(width.max(pending_width));
- }
- width = Abs::zero();
- alignment_index += 1;
- } else {
- width += fragment.width();
- }
- }
- if widths.is_empty() {
- pending_width.set_max(width);
- } else if alignment_index < widths.len() {
- widths[alignment_index].set_max(width);
- } else {
- widths.push(width.max(pending_width));
- }
- }
-
- let mut points = widths;
- for i in 1..points.len() {
- let prev = points[i - 1];
- points[i] += prev;
- }
- AlignmentResult {
- width: points.last().copied().unwrap_or(pending_width),
- points,
- }
-}
diff --git a/crates/typst-library/src/math/attach.rs b/crates/typst-library/src/math/attach.rs
deleted file mode 100644
index 3e6b69f2..00000000
--- a/crates/typst-library/src/math/attach.rs
+++ /dev/null
@@ -1,440 +0,0 @@
-use super::*;
-
-/// A base with optional attachments.
-///
-/// ```example
-/// $ attach(
-/// Pi, t: alpha, b: beta,
-/// tl: 1, tr: 2+3, bl: 4+5, br: 6,
-/// ) $
-/// ```
-#[elem(LayoutMath)]
-pub struct AttachElem {
- /// The base to which things are attached.
- #[required]
- pub base: Content,
-
- /// The top attachment, smartly positioned at top-right or above the base.
- ///
- /// You can wrap the base in `{limits()}` or `{scripts()}` to override the
- /// smart positioning.
- pub t: Option<Content>,
-
- /// The bottom attachment, smartly positioned at the bottom-right or below
- /// the base.
- ///
- /// You can wrap the base in `{limits()}` or `{scripts()}` to override the
- /// smart positioning.
- pub b: Option<Content>,
-
- /// The top-left attachment (before the base).
- pub tl: Option<Content>,
-
- /// The bottom-left attachment (before base).
- pub bl: Option<Content>,
-
- /// The top-right attachment (after the base).
- pub tr: Option<Content>,
-
- /// The bottom-right attachment (after the base).
- pub br: Option<Content>,
-}
-
-impl LayoutMath for AttachElem {
- #[tracing::instrument(skip(ctx))]
- fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
- type GetAttachment = fn(&AttachElem, styles: StyleChain) -> Option<Content>;
- let layout_attachment = |ctx: &mut MathContext, getter: GetAttachment| {
- getter(self, ctx.styles())
- .map(|elem| ctx.layout_fragment(&elem))
- .transpose()
- };
-
- let base = ctx.layout_fragment(self.base())?;
-
- ctx.style(ctx.style.for_superscript());
- let tl = layout_attachment(ctx, Self::tl)?;
- let tr = layout_attachment(ctx, Self::tr)?;
- let t = layout_attachment(ctx, Self::t)?;
- ctx.unstyle();
-
- ctx.style(ctx.style.for_subscript());
- let bl = layout_attachment(ctx, Self::bl)?;
- let br = layout_attachment(ctx, Self::br)?;
- let b = layout_attachment(ctx, Self::b)?;
- ctx.unstyle();
-
- let limits = base.limits().active(ctx);
- let (t, tr) = if limits || tr.is_some() { (t, tr) } else { (None, t) };
- let (b, br) = if limits || br.is_some() { (b, br) } else { (None, b) };
- layout_attachments(ctx, base, [tl, t, tr, bl, b, br])
- }
-}
-
-/// Grouped primes.
-///
-/// ```example
-/// $ a'''_b = a^'''_b $
-/// ```
-///
-/// # 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.
-#[elem(LayoutMath)]
-pub struct PrimesElem {
- /// The number of grouped primes.
- #[required]
- pub count: usize,
-}
-
-impl LayoutMath for PrimesElem {
- #[tracing::instrument(skip(ctx))]
- fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
- match *self.count() {
- count @ 1..=4 => {
- let f = ctx.layout_fragment(&TextElem::packed(match count {
- 1 => '′',
- 2 => '″',
- 3 => '‴',
- 4 => '⁗',
- _ => unreachable!(),
- }))?;
- ctx.push(f);
- }
- count => {
- // Custom amount of primes
- let prime = ctx.layout_fragment(&TextElem::packed('′'))?.into_frame();
- let width = prime.width() * (count + 1) as f64 / 2.0;
- let mut frame = Frame::soft(Size::new(width, prime.height()));
- frame.set_baseline(prime.ascent());
-
- for i in 0..count {
- frame.push_frame(
- Point::new(prime.width() * (i as f64 / 2.0), Abs::zero()),
- prime.clone(),
- )
- }
- ctx.push(FrameFragment::new(ctx, frame));
- }
- }
- Ok(())
- }
-}
-
-/// Forces a base to display attachments as scripts.
-///
-/// ```example
-/// $ scripts(sum)_1^2 != sum_1^2 $
-/// ```
-#[elem(LayoutMath)]
-pub struct ScriptsElem {
- /// The base to attach the scripts to.
- #[required]
- pub body: Content,
-}
-
-impl LayoutMath for ScriptsElem {
- #[tracing::instrument(skip(ctx))]
- fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
- let mut fragment = ctx.layout_fragment(self.body())?;
- fragment.set_limits(Limits::Never);
- ctx.push(fragment);
- Ok(())
- }
-}
-
-/// Forces a base to display attachments as limits.
-///
-/// ```example
-/// $ limits(A)_1^2 != A_1^2 $
-/// ```
-#[elem(LayoutMath)]
-pub struct LimitsElem {
- /// The base to attach the limits to.
- #[required]
- pub body: Content,
-
- /// Whether to also force limits in inline equations.
- ///
- /// When applying limits globally (e.g., through a show rule), it is
- /// typically a good idea to disable this.
- #[default(true)]
- pub inline: bool,
-}
-
-impl LayoutMath for LimitsElem {
- #[tracing::instrument(skip(ctx))]
- fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
- let mut fragment = ctx.layout_fragment(self.body())?;
- fragment.set_limits(if self.inline(ctx.styles()) {
- Limits::Always
- } else {
- Limits::Display
- });
- ctx.push(fragment);
- Ok(())
- }
-}
-
-/// Describes in which situation a frame should use limits for attachments.
-#[derive(Debug, Copy, Clone)]
-pub enum Limits {
- /// Always scripts.
- Never,
- /// Display limits only in `display` math.
- Display,
- /// Always limits.
- Always,
-}
-
-impl Limits {
- /// The default limit configuration if the given character is the base.
- pub fn for_char(c: char) -> Self {
- match unicode_math_class::class(c) {
- Some(MathClass::Large) => {
- if is_integral_char(c) {
- Limits::Never
- } else {
- Limits::Display
- }
- }
- Some(MathClass::Relation) => Limits::Always,
- _ => Limits::Never,
- }
- }
-
- /// Whether limits should be displayed in this context
- pub fn active(&self, ctx: &MathContext) -> bool {
- match self {
- Self::Always => true,
- Self::Display => ctx.style.size == MathSize::Display,
- Self::Never => false,
- }
- }
-}
-
-macro_rules! measure {
- ($e: ident, $attr: ident) => {
- $e.as_ref().map(|e| e.$attr()).unwrap_or_default()
- };
-}
-
-/// Layout the attachments.
-fn layout_attachments(
- ctx: &mut MathContext,
- base: MathFragment,
- [tl, t, tr, bl, b, br]: [Option<MathFragment>; 6],
-) -> SourceResult<()> {
- let (shift_up, shift_down) =
- compute_shifts_up_and_down(ctx, &base, [&tl, &tr, &bl, &br]);
-
- let sup_delta = Abs::zero();
- let sub_delta = -base.italics_correction();
- let (base_width, base_ascent, base_descent) =
- (base.width(), base.ascent(), base.descent());
- let base_class = base.class().unwrap_or(MathClass::Normal);
-
- let ascent = base_ascent
- .max(shift_up + measure!(tr, ascent))
- .max(shift_up + measure!(tl, ascent))
- .max(shift_up + measure!(t, height));
-
- let descent = base_descent
- .max(shift_down + measure!(br, descent))
- .max(shift_down + measure!(bl, descent))
- .max(shift_down + measure!(b, height));
-
- let pre_sup_width = measure!(tl, width);
- let pre_sub_width = measure!(bl, width);
- let pre_width_dif = pre_sup_width - pre_sub_width; // Could be negative.
- let pre_width_max = pre_sup_width.max(pre_sub_width);
- let post_max_width =
- (sup_delta + measure!(tr, width)).max(sub_delta + measure!(br, width));
-
- let (center_frame, base_offset) = attach_top_and_bottom(ctx, base, t, b);
- let base_pos =
- Point::new(sup_delta + pre_width_max, ascent - base_ascent - base_offset);
- if [&tl, &bl, &tr, &br].iter().all(|&e| e.is_none()) {
- ctx.push(FrameFragment::new(ctx, center_frame).with_class(base_class));
- return Ok(());
- }
-
- let mut frame = Frame::soft(Size::new(
- pre_width_max + base_width + post_max_width + scaled!(ctx, space_after_script),
- ascent + descent,
- ));
- frame.set_baseline(ascent);
- frame.push_frame(base_pos, center_frame);
-
- if let Some(tl) = tl {
- let pos =
- Point::new(-pre_width_dif.min(Abs::zero()), ascent - shift_up - tl.ascent());
- frame.push_frame(pos, tl.into_frame());
- }
-
- if let Some(bl) = bl {
- let pos =
- Point::new(pre_width_dif.max(Abs::zero()), ascent + shift_down - bl.ascent());
- frame.push_frame(pos, bl.into_frame());
- }
-
- if let Some(tr) = tr {
- let pos = Point::new(
- sup_delta + pre_width_max + base_width,
- ascent - shift_up - tr.ascent(),
- );
- frame.push_frame(pos, tr.into_frame());
- }
-
- if let Some(br) = br {
- let pos = Point::new(
- sub_delta + pre_width_max + base_width,
- ascent + shift_down - br.ascent(),
- );
- frame.push_frame(pos, br.into_frame());
- }
-
- ctx.push(FrameFragment::new(ctx, frame).with_class(base_class));
-
- Ok(())
-}
-
-fn attach_top_and_bottom(
- ctx: &mut MathContext,
- base: MathFragment,
- t: Option<MathFragment>,
- b: Option<MathFragment>,
-) -> (Frame, Abs) {
- let upper_gap_min = scaled!(ctx, upper_limit_gap_min);
- let upper_rise_min = scaled!(ctx, upper_limit_baseline_rise_min);
- let lower_gap_min = scaled!(ctx, lower_limit_gap_min);
- let lower_drop_min = scaled!(ctx, lower_limit_baseline_drop_min);
-
- let mut base_offset = Abs::zero();
- let mut width = base.width();
- let mut height = base.height();
-
- if let Some(t) = &t {
- let top_gap = upper_gap_min.max(upper_rise_min - t.descent());
- width.set_max(t.width());
- height += t.height() + top_gap;
- base_offset = top_gap + t.height();
- }
-
- if let Some(b) = &b {
- let bottom_gap = lower_gap_min.max(lower_drop_min - b.ascent());
- width.set_max(b.width());
- height += b.height() + bottom_gap;
- }
-
- let base_pos = Point::new((width - base.width()) / 2.0, base_offset);
- let delta = base.italics_correction() / 2.0;
-
- let mut frame = Frame::soft(Size::new(width, height));
- frame.set_baseline(base_pos.y + base.ascent());
- frame.push_frame(base_pos, base.into_frame());
-
- if let Some(t) = t {
- let top_pos = Point::with_x((width - t.width()) / 2.0 + delta);
- frame.push_frame(top_pos, t.into_frame());
- }
-
- if let Some(b) = b {
- let bottom_pos =
- Point::new((width - b.width()) / 2.0 - delta, height - b.height());
- frame.push_frame(bottom_pos, b.into_frame());
- }
-
- (frame, base_offset)
-}
-
-fn compute_shifts_up_and_down(
- ctx: &MathContext,
- base: &MathFragment,
- [tl, tr, bl, br]: [&Option<MathFragment>; 4],
-) -> (Abs, Abs) {
- let sup_shift_up = if ctx.style.cramped {
- scaled!(ctx, superscript_shift_up_cramped)
- } else {
- scaled!(ctx, superscript_shift_up)
- };
-
- let sup_bottom_min = scaled!(ctx, superscript_bottom_min);
- let sup_bottom_max_with_sub = scaled!(ctx, superscript_bottom_max_with_subscript);
- let sup_drop_max = scaled!(ctx, superscript_baseline_drop_max);
- let gap_min = scaled!(ctx, sub_superscript_gap_min);
- let sub_shift_down = scaled!(ctx, subscript_shift_down);
- let sub_top_max = scaled!(ctx, subscript_top_max);
- let sub_drop_min = scaled!(ctx, subscript_baseline_drop_min);
-
- let mut shift_up = Abs::zero();
- let mut shift_down = Abs::zero();
- let is_char_box = is_character_box(base);
-
- if tl.is_some() || tr.is_some() {
- let ascent = match &base {
- MathFragment::Frame(frame) => frame.base_ascent,
- _ => base.ascent(),
- };
- shift_up = shift_up
- .max(sup_shift_up)
- .max(if is_char_box { Abs::zero() } else { ascent - sup_drop_max })
- .max(sup_bottom_min + measure!(tl, descent))
- .max(sup_bottom_min + measure!(tr, descent));
- }
-
- if bl.is_some() || br.is_some() {
- shift_down = shift_down
- .max(sub_shift_down)
- .max(if is_char_box { Abs::zero() } else { base.descent() + sub_drop_min })
- .max(measure!(bl, ascent) - sub_top_max)
- .max(measure!(br, ascent) - sub_top_max);
- }
-
- for (sup, sub) in [(tl, bl), (tr, br)] {
- if let (Some(sup), Some(sub)) = (&sup, &sub) {
- let sup_bottom = shift_up - sup.descent();
- let sub_top = sub.ascent() - shift_down;
- let gap = sup_bottom - sub_top;
- if gap >= gap_min {
- continue;
- }
-
- let increase = gap_min - gap;
- let sup_only =
- (sup_bottom_max_with_sub - sup_bottom).clamp(Abs::zero(), increase);
- let rest = (increase - sup_only) / 2.0;
- shift_up += sup_only + rest;
- shift_down += rest;
- }
- }
-
- (shift_up, shift_down)
-}
-
-/// Determines if the character is one of a variety of integral signs
-fn is_integral_char(c: char) -> bool {
- ('∫'..='∳').contains(&c) || ('⨋'..='⨜').contains(&c)
-}
-
-/// Whether the fragment consists of a single character or atomic piece of text.
-fn is_character_box(fragment: &MathFragment) -> bool {
- match fragment {
- MathFragment::Glyph(_) | MathFragment::Variant(_) => {
- fragment.class() != Some(MathClass::Large)
- }
- MathFragment::Frame(fragment) => is_atomic_text_frame(&fragment.frame),
- _ => false,
- }
-}
-
-/// Handles e.g. "sin", "log", "exp", "CustomOperator".
-fn is_atomic_text_frame(frame: &Frame) -> bool {
- // Meta information isn't visible or renderable, so we exclude it.
- let mut iter = frame
- .items()
- .map(|(_, item)| item)
- .filter(|item| !matches!(item, FrameItem::Meta(_, _)));
- matches!(iter.next(), Some(FrameItem::Text(_))) && iter.next().is_none()
-}
diff --git a/crates/typst-library/src/math/cancel.rs b/crates/typst-library/src/math/cancel.rs
deleted file mode 100644
index 455750f7..00000000
--- a/crates/typst-library/src/math/cancel.rs
+++ /dev/null
@@ -1,230 +0,0 @@
-use super::*;
-
-/// Displays a diagonal line over a part of an equation.
-///
-/// This is commonly used to show the elimination of a term.
-///
-/// # Example
-/// ```example
-/// >>> #set page(width: 140pt)
-/// Here, we can simplify:
-/// $ (a dot b dot cancel(x)) /
-/// cancel(x) $
-/// ```
-#[elem(LayoutMath)]
-pub struct CancelElem {
- /// The content over which the line should be placed.
- #[required]
- pub body: Content,
-
- /// The length of the line, relative to the length of the diagonal spanning
- /// the whole element being "cancelled". A value of `{100%}` would then have
- /// the line span precisely the element's diagonal.
- ///
- /// ```example
- /// >>> #set page(width: 140pt)
- /// $ a + cancel(x, length: #200%)
- /// - cancel(x, length: #200%) $
- /// ```
- #[default(Rel::new(Ratio::one(), Abs::pt(3.0).into()))]
- pub length: Rel<Length>,
-
- /// Whether the cancel line should be inverted (flipped along the y-axis).
- /// For the default angle setting, inverted means the cancel line
- /// points to the top left instead of top right.
- ///
- /// ```example
- /// >>> #set page(width: 140pt)
- /// $ (a cancel((b + c), inverted: #true)) /
- /// cancel(b + c, inverted: #true) $
- /// ```
- #[default(false)]
- pub inverted: bool,
-
- /// Whether two opposing cancel lines should be drawn, forming a cross over
- /// the element. Overrides `inverted`.
- ///
- /// ```example
- /// >>> #set page(width: 140pt)
- /// $ cancel(Pi, cross: #true) $
- /// ```
- #[default(false)]
- pub cross: bool,
-
- /// How much to rotate the cancel line.
- ///
- /// - If `{auto}`, the line assumes the default angle; that is, along the
- /// diagonal line of the content box.
- /// - If given an angle, the line is rotated by that angle clockwise w.r.t
- /// the y-axis.
- /// - If given a function `angle => angle`, the line is rotated by the angle
- /// returned by that function. The function receives the default angle as
- /// its input.
- ///
- /// ```example
- /// >>> #set page(width: 140pt)
- /// $ cancel(Pi)
- /// cancel(Pi, angle: #0deg)
- /// cancel(Pi, angle: #45deg)
- /// cancel(Pi, angle: #90deg)
- /// cancel(1/(1+x), angle: #(a => a + 45deg))
- /// cancel(1/(1+x), angle: #(a => a + 90deg)) $
- /// ```
- pub angle: Smart<CancelAngle>,
-
- /// How to [stroke]($stroke) the cancel line.
- ///
- /// ```example
- /// >>> #set page(width: 140pt)
- /// $ cancel(
- /// sum x,
- /// stroke: #(
- /// paint: red,
- /// thickness: 1.5pt,
- /// dash: "dashed",
- /// ),
- /// ) $
- /// ```
- #[resolve]
- #[fold]
- #[default(Stroke {
- // Default stroke has 0.5pt for better visuals.
- thickness: Smart::Custom(Abs::pt(0.5)),
- ..Default::default()
- })]
- pub stroke: Stroke,
-}
-
-impl LayoutMath for CancelElem {
- fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
- let body = ctx.layout_fragment(self.body())?;
- // Use the same math class as the body, in order to preserve automatic spacing around it.
- let body_class = body.class().unwrap_or(MathClass::Special);
- let mut body = body.into_frame();
-
- let styles = ctx.styles();
- let body_size = body.size();
- let span = self.span();
- let length = self.length(styles).resolve(styles);
-
- let stroke = self.stroke(styles).unwrap_or(FixedStroke {
- paint: TextElem::fill_in(styles).as_decoration(),
- ..Default::default()
- });
-
- let invert = self.inverted(styles);
- let cross = self.cross(styles);
- let angle = self.angle(styles);
-
- let invert_first_line = !cross && invert;
- let first_line = draw_cancel_line(
- ctx,
- length,
- stroke.clone(),
- invert_first_line,
- &angle,
- body_size,
- span,
- )?;
-
- // The origin of our line is the very middle of the element.
- let center = body_size.to_point() / 2.0;
- body.push_frame(center, first_line);
-
- if cross {
- // Draw the second line.
- let second_line =
- draw_cancel_line(ctx, length, stroke, true, &angle, body_size, span)?;
-
- body.push_frame(center, second_line);
- }
-
- ctx.push(FrameFragment::new(ctx, body).with_class(body_class));
-
- Ok(())
- }
-}
-
-/// Defines the cancel line.
-#[derive(Debug, Clone, PartialEq, Hash)]
-pub enum CancelAngle {
- Angle(Angle),
- Func(Func),
-}
-
-cast! {
- CancelAngle,
- self => match self {
- Self::Angle(v) => v.into_value(),
- Self::Func(v) => v.into_value()
- },
- v: Angle => CancelAngle::Angle(v),
- v: Func => CancelAngle::Func(v),
-}
-
-/// Draws a cancel line.
-fn draw_cancel_line(
- ctx: &mut MathContext,
- length_scale: Rel<Abs>,
- stroke: FixedStroke,
- invert: bool,
- angle: &Smart<CancelAngle>,
- body_size: Size,
- span: Span,
-) -> SourceResult<Frame> {
- let default = default_angle(body_size);
- let mut angle = match angle {
- // Non specified angle defaults to the diagonal
- Smart::Auto => default,
- Smart::Custom(angle) => match angle {
- // This specifies the absolute angle w.r.t y-axis clockwise.
- CancelAngle::Angle(v) => *v,
- // This specifies a function that takes the default angle as input.
- CancelAngle::Func(func) => {
- func.call_vt(ctx.vt, [default])?.cast().at(span)?
- }
- },
- };
-
- // invert means flipping along the y-axis
- if invert {
- angle *= -1.0;
- }
-
- // same as above, the default length is the diagonal of the body box.
- let default_length = body_size.to_point().hypot();
- let length = length_scale.relative_to(default_length);
-
- // Draw a vertical line of length and rotate it by angle
- let start = Point::new(Abs::zero(), length / 2.0);
- let delta = Point::new(Abs::zero(), -length);
-
- let mut frame = Frame::soft(body_size);
- frame.push(start, FrameItem::Shape(Geometry::Line(delta).stroked(stroke), span));
-
- // Having the middle of the line at the origin is convenient here.
- frame.transform(Transform::rotate(angle));
- Ok(frame)
-}
-
-/// The default line angle for a body of the given size.
-fn default_angle(body: Size) -> Angle {
- // The default cancel line is the diagonal.
- // We infer the default angle from
- // the diagonal w.r.t to the body box.
- //
- // The returned angle is in the range of [0, Pi/2]
- //
- // Note that the angle is computed w.r.t to the y-axis
- //
- // B
- // /|
- // diagonal / | height
- // / |
- // / |
- // O ----
- // width
- let (width, height) = (body.x, body.y);
- let default_angle = (width / height).atan(); // arctangent (in the range [0, Pi/2])
- Angle::rad(default_angle)
-}
diff --git a/crates/typst-library/src/math/class.rs b/crates/typst-library/src/math/class.rs
deleted file mode 100644
index d2c5192d..00000000
--- a/crates/typst-library/src/math/class.rs
+++ /dev/null
@@ -1,38 +0,0 @@
-use super::*;
-
-/// Forced use of a certain math class.
-///
-/// 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
-/// #let loves = math.class(
-/// "relation",
-/// sym.suit.heart,
-/// )
-///
-/// $x loves y and y loves 5$
-/// ```
-#[elem(LayoutMath)]
-pub struct ClassElem {
- /// The class to apply to the content.
- #[required]
- pub class: MathClass,
-
- /// The content to which the class is applied.
- #[required]
- pub body: Content,
-}
-
-impl LayoutMath for ClassElem {
- fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
- ctx.style(ctx.style.with_class(*self.class()));
- let mut fragment = ctx.layout_fragment(self.body())?;
- ctx.unstyle();
-
- fragment.set_class(*self.class());
- ctx.push(fragment);
- Ok(())
- }
-}
diff --git a/crates/typst-library/src/math/ctx.rs b/crates/typst-library/src/math/ctx.rs
deleted file mode 100644
index 789bd332..00000000
--- a/crates/typst-library/src/math/ctx.rs
+++ /dev/null
@@ -1,335 +0,0 @@
-use comemo::Prehashed;
-use ttf_parser::gsub::SubstitutionSubtable;
-use ttf_parser::math::MathValue;
-use typst::font::{FontStyle, FontWeight};
-use typst::model::realize;
-use typst::syntax::is_newline;
-use unicode_segmentation::UnicodeSegmentation;
-
-use super::*;
-use crate::text::{tags, BottomEdge, BottomEdgeMetric, TopEdge, TopEdgeMetric};
-
-macro_rules! scaled {
- ($ctx:expr, text: $text:ident, display: $display:ident $(,)?) => {
- match $ctx.style.size {
- MathSize::Display => scaled!($ctx, $display),
- _ => scaled!($ctx, $text),
- }
- };
- ($ctx:expr, $name:ident) => {
- $ctx.constants.$name().scaled($ctx)
- };
-}
-
-macro_rules! percent {
- ($ctx:expr, $name:ident) => {
- $ctx.constants.$name() as f64 / 100.0
- };
-}
-
-/// The context for math layout.
-pub struct MathContext<'a, 'b, 'v> {
- pub vt: &'v mut Vt<'b>,
- pub regions: Regions<'static>,
- pub font: &'a Font,
- pub ttf: &'a ttf_parser::Face<'a>,
- pub table: ttf_parser::math::Table<'a>,
- pub constants: ttf_parser::math::Constants<'a>,
- pub ssty_table: Option<ttf_parser::gsub::AlternateSubstitution<'a>>,
- pub glyphwise_tables: Option<Vec<GlyphwiseSubsts<'a>>>,
- pub space_width: Em,
- pub fragments: Vec<MathFragment>,
- pub local: Styles,
- pub style: MathStyle,
- pub size: Abs,
- outer: StyleChain<'a>,
- style_stack: Vec<(MathStyle, Abs)>,
-}
-
-impl<'a, 'b, 'v> MathContext<'a, 'b, 'v> {
- pub fn new(
- vt: &'v mut Vt<'b>,
- styles: StyleChain<'a>,
- regions: Regions,
- font: &'a Font,
- block: bool,
- ) -> Self {
- let math_table = font.ttf().tables().math.unwrap();
- let gsub_table = font.ttf().tables().gsub;
- let constants = math_table.constants.unwrap();
-
- let ssty_table = gsub_table
- .and_then(|gsub| {
- gsub.features
- .find(ttf_parser::Tag::from_bytes(b"ssty"))
- .and_then(|feature| feature.lookup_indices.get(0))
- .and_then(|index| gsub.lookups.get(index))
- })
- .and_then(|ssty| ssty.subtables.get::<SubstitutionSubtable>(0))
- .and_then(|ssty| match ssty {
- SubstitutionSubtable::Alternate(alt_glyphs) => Some(alt_glyphs),
- _ => None,
- });
-
- let features = tags(styles);
- let glyphwise_tables = gsub_table.map(|gsub| {
- features
- .into_iter()
- .filter_map(|feature| GlyphwiseSubsts::new(gsub, feature))
- .collect()
- });
-
- let size = TextElem::size_in(styles);
- let ttf = font.ttf();
- let space_width = ttf
- .glyph_index(' ')
- .and_then(|id| ttf.glyph_hor_advance(id))
- .map(|advance| font.to_em(advance))
- .unwrap_or(THICK);
-
- let variant = variant(styles);
- Self {
- vt,
- regions: Regions::one(regions.base(), Axes::splat(false)),
- font,
- ttf: font.ttf(),
- table: math_table,
- constants,
- ssty_table,
- glyphwise_tables,
- space_width,
- fragments: vec![],
- local: Styles::new(),
- style: MathStyle {
- variant: MathVariant::Serif,
- size: if block { MathSize::Display } else { MathSize::Text },
- class: Smart::Auto,
- cramped: false,
- bold: variant.weight >= FontWeight::BOLD,
- italic: match variant.style {
- FontStyle::Normal => Smart::Auto,
- FontStyle::Italic | FontStyle::Oblique => Smart::Custom(true),
- },
- },
- size,
- outer: styles,
- style_stack: vec![],
- }
- }
-
- pub fn push(&mut self, fragment: impl Into<MathFragment>) {
- self.fragments.push(fragment.into());
- }
-
- pub fn extend(&mut self, fragments: Vec<MathFragment>) {
- self.fragments.extend(fragments);
- }
-
- pub fn layout_fragment(
- &mut self,
- elem: &dyn LayoutMath,
- ) -> SourceResult<MathFragment> {
- let row = self.layout_fragments(elem)?;
- Ok(MathRow::new(row).into_fragment(self))
- }
-
- pub fn layout_fragments(
- &mut self,
- elem: &dyn LayoutMath,
- ) -> SourceResult<Vec<MathFragment>> {
- let prev = std::mem::take(&mut self.fragments);
- elem.layout_math(self)?;
- Ok(std::mem::replace(&mut self.fragments, prev))
- }
-
- pub fn layout_row(&mut self, elem: &dyn LayoutMath) -> SourceResult<MathRow> {
- let fragments = self.layout_fragments(elem)?;
- Ok(MathRow::new(fragments))
- }
-
- pub fn layout_frame(&mut self, elem: &dyn LayoutMath) -> SourceResult<Frame> {
- Ok(self.layout_fragment(elem)?.into_frame())
- }
-
- pub fn layout_box(&mut self, boxed: &BoxElem) -> SourceResult<Frame> {
- Ok(boxed
- .layout(self.vt, self.outer.chain(&self.local), self.regions)?
- .into_frame())
- }
-
- pub fn layout_content(&mut self, content: &Content) -> SourceResult<Frame> {
- Ok(content
- .layout(self.vt, self.outer.chain(&self.local), self.regions)?
- .into_frame())
- }
-
- pub fn layout_text(&mut self, elem: &TextElem) -> SourceResult<MathFragment> {
- let text = elem.text();
- let span = elem.span();
- let mut chars = text.chars();
- let fragment = if let Some(mut glyph) = chars
- .next()
- .filter(|_| chars.next().is_none())
- .map(|c| self.style.styled_char(c))
- .and_then(|c| GlyphFragment::try_new(self, c, span))
- {
- // A single letter that is available in the math font.
- match self.style.size {
- MathSize::Script => {
- glyph.make_scriptsize(self);
- }
- MathSize::ScriptScript => {
- glyph.make_scriptscriptsize(self);
- }
- _ => (),
- }
-
- let class = self.style.class.as_custom().or(glyph.class);
- if class == Some(MathClass::Large) {
- let mut variant = if self.style.size == MathSize::Display {
- let height = scaled!(self, display_operator_min_height);
- glyph.stretch_vertical(self, height, Abs::zero())
- } else {
- glyph.into_variant()
- };
- // TeXbook p 155. Large operators are always vertically centered on the axis.
- variant.center_on_axis(self);
- variant.into()
- } else {
- glyph.into()
- }
- } else if text.chars().all(|c| c.is_ascii_digit() || c == '.') {
- // Numbers aren't that difficult.
- let mut fragments = vec![];
- for c in text.chars() {
- let c = self.style.styled_char(c);
- fragments.push(GlyphFragment::new(self, c, span).into());
- }
- let frame = MathRow::new(fragments).into_frame(self);
- FrameFragment::new(self, frame).into()
- } else {
- // Anything else is handled by Typst's standard text layout.
- let mut style = self.style;
- if self.style.italic == Smart::Auto {
- style = style.with_italic(false);
- }
- let text: EcoString = text.chars().map(|c| style.styled_char(c)).collect();
- if text.contains(is_newline) {
- let mut fragments = vec![];
- for (i, piece) in text.split(is_newline).enumerate() {
- if i != 0 {
- fragments.push(MathFragment::Linebreak);
- }
- if !piece.is_empty() {
- fragments.push(self.layout_complex_text(piece, span)?.into());
- }
- }
- let mut frame = MathRow::new(fragments).into_frame(self);
- let axis = scaled!(self, axis_height);
- frame.set_baseline(frame.height() / 2.0 + axis);
- FrameFragment::new(self, frame).into()
- } else {
- self.layout_complex_text(&text, span)?.into()
- }
- };
- Ok(fragment)
- }
-
- pub fn layout_complex_text(
- &mut self,
- text: &str,
- span: Span,
- ) -> SourceResult<FrameFragment> {
- let spaced = text.graphemes(true).nth(1).is_some();
- let elem = TextElem::packed(text)
- .styled(TextElem::set_top_edge(TopEdge::Metric(TopEdgeMetric::Bounds)))
- .styled(TextElem::set_bottom_edge(BottomEdge::Metric(
- BottomEdgeMetric::Bounds,
- )))
- .spanned(span);
-
- // There isn't a natural width for a paragraph in a math environment;
- // because it will be placed somewhere probably not at the left margin
- // it will overflow. So emulate an `hbox` instead and allow the paragraph
- // to extend as far as needed.
- let span = elem.span();
- let frame = ParElem::new(vec![Prehashed::new(elem)])
- .spanned(span)
- .layout(
- self.vt,
- self.outer.chain(&self.local),
- false,
- Size::splat(Abs::inf()),
- false,
- )?
- .into_frame();
-
- Ok(FrameFragment::new(self, frame)
- .with_class(MathClass::Alphabetic)
- .with_spaced(spaced))
- }
-
- pub fn styles(&self) -> StyleChain {
- self.outer.chain(&self.local)
- }
-
- pub fn realize(&mut self, content: &Content) -> SourceResult<Option<Content>> {
- realize(self.vt, content, self.outer.chain(&self.local))
- }
-
- pub fn style(&mut self, style: MathStyle) {
- self.style_stack.push((self.style, self.size));
- let base_size = TextElem::size_in(self.styles()) / self.style.size.factor(self);
- self.size = base_size * style.size.factor(self);
- self.local.set(TextElem::set_size(TextSize(self.size.into())));
- self.local
- .set(TextElem::set_style(if style.italic == Smart::Custom(true) {
- FontStyle::Italic
- } else {
- FontStyle::Normal
- }));
- self.local.set(TextElem::set_weight(if style.bold {
- FontWeight::BOLD
- } else {
- // The normal weight is what we started with.
- // It's 400 for CM Regular, 450 for CM Book.
- self.font.info().variant.weight
- }));
- self.style = style;
- }
-
- pub fn unstyle(&mut self) {
- (self.style, self.size) = self.style_stack.pop().unwrap();
- self.local.unset();
- self.local.unset();
- self.local.unset();
- }
-}
-
-pub(super) trait Scaled {
- fn scaled(self, ctx: &MathContext) -> Abs;
-}
-
-impl Scaled for i16 {
- fn scaled(self, ctx: &MathContext) -> Abs {
- ctx.font.to_em(self).scaled(ctx)
- }
-}
-
-impl Scaled for u16 {
- fn scaled(self, ctx: &MathContext) -> Abs {
- ctx.font.to_em(self).scaled(ctx)
- }
-}
-
-impl Scaled for Em {
- fn scaled(self, ctx: &MathContext) -> Abs {
- self.at(ctx.size)
- }
-}
-
-impl Scaled for MathValue<'_> {
- fn scaled(self, ctx: &MathContext) -> Abs {
- self.value.scaled(ctx)
- }
-}
diff --git a/crates/typst-library/src/math/frac.rs b/crates/typst-library/src/math/frac.rs
deleted file mode 100644
index c3014178..00000000
--- a/crates/typst-library/src/math/frac.rs
+++ /dev/null
@@ -1,157 +0,0 @@
-use super::*;
-
-const FRAC_AROUND: Em = Em::new(0.1);
-
-/// A mathematical fraction.
-///
-/// # Example
-/// ```example
-/// $ 1/2 < (x+1)/2 $
-/// $ ((x+1)) / 2 = frac(a, b) $
-/// ```
-///
-/// # 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.
-#[elem(title = "Fraction", LayoutMath)]
-pub struct FracElem {
- /// The fraction's numerator.
- #[required]
- pub num: Content,
-
- /// The fraction's denominator.
- #[required]
- pub denom: Content,
-}
-
-impl LayoutMath for FracElem {
- #[tracing::instrument(skip(ctx))]
- fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
- layout(ctx, self.num(), std::slice::from_ref(self.denom()), false, self.span())
- }
-}
-
-/// A binomial expression.
-///
-/// # Example
-/// ```example
-/// $ binom(n, k) $
-/// $ binom(n, k_1, k_2, k_3, ..., k_m) $
-/// ```
-#[elem(title = "Binomial", LayoutMath)]
-pub struct BinomElem {
- /// The binomial's upper index.
- #[required]
- pub upper: Content,
-
- /// The binomial's lower index.
- #[required]
- #[variadic]
- #[parse(
- let values = args.all::<Spanned<Value>>()?;
- if values.is_empty() {
- // Prevents one element binomials
- bail!(args.span, "missing argument: lower");
- }
- values.into_iter().map(|spanned| spanned.v.display()).collect()
- )]
- pub lower: Vec<Content>,
-}
-
-impl LayoutMath for BinomElem {
- fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
- layout(ctx, self.upper(), self.lower(), true, self.span())
- }
-}
-
-/// Layout a fraction or binomial.
-fn layout(
- ctx: &mut MathContext,
- num: &Content,
- denom: &[Content],
- binom: bool,
- span: Span,
-) -> SourceResult<()> {
- let short_fall = DELIM_SHORT_FALL.scaled(ctx);
- let axis = scaled!(ctx, axis_height);
- let thickness = scaled!(ctx, fraction_rule_thickness);
- let shift_up = scaled!(
- ctx,
- text: fraction_numerator_shift_up,
- display: fraction_numerator_display_style_shift_up,
- );
- let shift_down = scaled!(
- ctx,
- text: fraction_denominator_shift_down,
- display: fraction_denominator_display_style_shift_down,
- );
- let num_min = scaled!(
- ctx,
- text: fraction_numerator_gap_min,
- display: fraction_num_display_style_gap_min,
- );
- let denom_min = scaled!(
- ctx,
- text: fraction_denominator_gap_min,
- display: fraction_denom_display_style_gap_min,
- );
-
- ctx.style(ctx.style.for_numerator());
- let num = ctx.layout_frame(num)?;
- ctx.unstyle();
-
- ctx.style(ctx.style.for_denominator());
- let denom = ctx.layout_frame(&Content::sequence(
- // Add a comma between each element.
- denom.iter().flat_map(|a| [TextElem::packed(','), a.clone()]).skip(1),
- ))?;
- ctx.unstyle();
-
- let around = FRAC_AROUND.scaled(ctx);
- let num_gap = (shift_up - axis - num.descent()).max(num_min + thickness / 2.0);
- let denom_gap = (shift_down + axis - denom.ascent()).max(denom_min + thickness / 2.0);
-
- let line_width = num.width().max(denom.width());
- let width = line_width + 2.0 * around;
- let height = num.height() + num_gap + thickness + denom_gap + denom.height();
- let size = Size::new(width, height);
- let num_pos = Point::with_x((width - num.width()) / 2.0);
- let line_pos =
- Point::new((width - line_width) / 2.0, num.height() + num_gap + thickness / 2.0);
- let denom_pos = Point::new((width - denom.width()) / 2.0, height - denom.height());
- let baseline = line_pos.y + axis;
-
- let mut frame = Frame::soft(size);
- frame.set_baseline(baseline);
- frame.push_frame(num_pos, num);
- frame.push_frame(denom_pos, denom);
-
- if binom {
- let mut left =
- GlyphFragment::new(ctx, '(', span).stretch_vertical(ctx, height, short_fall);
- left.center_on_axis(ctx);
- ctx.push(left);
- ctx.push(FrameFragment::new(ctx, frame));
- let mut right =
- GlyphFragment::new(ctx, ')', span).stretch_vertical(ctx, height, short_fall);
- right.center_on_axis(ctx);
- ctx.push(right);
- } else {
- frame.push(
- line_pos,
- FrameItem::Shape(
- Geometry::Line(Point::with_x(line_width)).stroked(FixedStroke {
- paint: TextElem::fill_in(ctx.styles()).as_decoration(),
- thickness,
- ..FixedStroke::default()
- }),
- span,
- ),
- );
- ctx.push(FrameFragment::new(ctx, frame));
- }
-
- Ok(())
-}
diff --git a/crates/typst-library/src/math/fragment.rs b/crates/typst-library/src/math/fragment.rs
deleted file mode 100644
index 76ee2512..00000000
--- a/crates/typst-library/src/math/fragment.rs
+++ /dev/null
@@ -1,513 +0,0 @@
-use rustybuzz::Feature;
-use ttf_parser::gsub::{
- AlternateSet, AlternateSubstitution, SingleSubstitution, SubstitutionSubtable,
-};
-use ttf_parser::opentype_layout::LayoutTable;
-
-use super::*;
-
-#[derive(Debug, Clone)]
-pub enum MathFragment {
- Glyph(GlyphFragment),
- Variant(VariantFragment),
- Frame(FrameFragment),
- Spacing(Abs),
- Space(Abs),
- Linebreak,
- Align,
-}
-
-impl MathFragment {
- pub fn size(&self) -> Size {
- Size::new(self.width(), self.height())
- }
-
- pub fn width(&self) -> Abs {
- match self {
- Self::Glyph(glyph) => glyph.width,
- Self::Variant(variant) => variant.frame.width(),
- Self::Frame(fragment) => fragment.frame.width(),
- Self::Spacing(amount) => *amount,
- Self::Space(amount) => *amount,
- _ => Abs::zero(),
- }
- }
-
- pub fn height(&self) -> Abs {
- match self {
- Self::Glyph(glyph) => glyph.height(),
- Self::Variant(variant) => variant.frame.height(),
- Self::Frame(fragment) => fragment.frame.height(),
- _ => Abs::zero(),
- }
- }
-
- pub fn ascent(&self) -> Abs {
- match self {
- Self::Glyph(glyph) => glyph.ascent,
- Self::Variant(variant) => variant.frame.ascent(),
- Self::Frame(fragment) => fragment.frame.baseline(),
- _ => Abs::zero(),
- }
- }
-
- pub fn descent(&self) -> Abs {
- match self {
- Self::Glyph(glyph) => glyph.descent,
- Self::Variant(variant) => variant.frame.descent(),
- Self::Frame(fragment) => fragment.frame.descent(),
- _ => Abs::zero(),
- }
- }
-
- pub fn class(&self) -> Option<MathClass> {
- self.style().and_then(|style| style.class.as_custom()).or(match self {
- Self::Glyph(glyph) => glyph.class,
- Self::Variant(variant) => variant.class,
- Self::Frame(fragment) => Some(fragment.class),
- _ => None,
- })
- }
-
- pub fn style(&self) -> Option<MathStyle> {
- match self {
- Self::Glyph(glyph) => Some(glyph.style),
- Self::Variant(variant) => Some(variant.style),
- Self::Frame(fragment) => Some(fragment.style),
- _ => None,
- }
- }
-
- pub fn font_size(&self) -> Option<Abs> {
- match self {
- Self::Glyph(glyph) => Some(glyph.font_size),
- Self::Variant(variant) => Some(variant.font_size),
- Self::Frame(fragment) => Some(fragment.font_size),
- _ => None,
- }
- }
-
- pub fn set_class(&mut self, class: MathClass) {
- macro_rules! set_style_class {
- ($fragment:ident) => {
- if $fragment.style.class.is_custom() {
- $fragment.style.class = Smart::Custom(class);
- }
- };
- }
-
- match self {
- Self::Glyph(glyph) => {
- glyph.class = Some(class);
- set_style_class!(glyph);
- }
- Self::Variant(variant) => {
- variant.class = Some(class);
- set_style_class!(variant);
- }
- Self::Frame(fragment) => {
- fragment.class = class;
- set_style_class!(fragment);
- }
- _ => {}
- }
- }
-
- pub fn set_limits(&mut self, limits: Limits) {
- match self {
- Self::Glyph(glyph) => glyph.limits = limits,
- Self::Variant(variant) => variant.limits = limits,
- Self::Frame(fragment) => fragment.limits = limits,
- _ => {}
- }
- }
-
- pub fn is_spaced(&self) -> bool {
- match self {
- MathFragment::Frame(frame) => {
- match self.style().and_then(|style| style.class.as_custom()) {
- Some(MathClass::Fence) => true,
- Some(_) => false,
- None => frame.spaced,
- }
- }
- _ => self.class() == Some(MathClass::Fence),
- }
- }
-
- pub fn italics_correction(&self) -> Abs {
- match self {
- Self::Glyph(glyph) => glyph.italics_correction,
- Self::Variant(variant) => variant.italics_correction,
- _ => Abs::zero(),
- }
- }
-
- pub fn into_frame(self) -> Frame {
- match self {
- Self::Glyph(glyph) => glyph.into_frame(),
- Self::Variant(variant) => variant.frame,
- Self::Frame(fragment) => fragment.frame,
- _ => Frame::soft(self.size()),
- }
- }
-
- pub fn limits(&self) -> Limits {
- match self {
- MathFragment::Glyph(glyph) => glyph.limits,
- MathFragment::Variant(variant) => variant.limits,
- MathFragment::Frame(fragment) => fragment.limits,
- _ => Limits::Never,
- }
- }
-}
-
-impl From<GlyphFragment> for MathFragment {
- fn from(glyph: GlyphFragment) -> Self {
- Self::Glyph(glyph)
- }
-}
-
-impl From<VariantFragment> for MathFragment {
- fn from(variant: VariantFragment) -> Self {
- Self::Variant(variant)
- }
-}
-
-impl From<FrameFragment> for MathFragment {
- fn from(fragment: FrameFragment) -> Self {
- Self::Frame(fragment)
- }
-}
-
-#[derive(Clone)]
-pub struct GlyphFragment {
- pub id: GlyphId,
- pub c: char,
- pub font: Font,
- pub lang: Lang,
- pub fill: Paint,
- pub shift: Abs,
- pub width: Abs,
- pub ascent: Abs,
- pub descent: Abs,
- pub italics_correction: Abs,
- pub style: MathStyle,
- pub font_size: Abs,
- pub class: Option<MathClass>,
- pub span: Span,
- pub meta: SmallVec<[Meta; 1]>,
- pub limits: Limits,
-}
-
-impl GlyphFragment {
- pub fn new(ctx: &MathContext, c: char, span: Span) -> Self {
- let id = ctx.ttf.glyph_index(c).unwrap_or_default();
- let id = Self::adjust_glyph_index(ctx, id);
- Self::with_id(ctx, c, id, span)
- }
-
- pub fn try_new(ctx: &MathContext, c: char, span: Span) -> Option<Self> {
- let c = ctx.style.styled_char(c);
- let id = ctx.ttf.glyph_index(c)?;
- let id = Self::adjust_glyph_index(ctx, id);
- Some(Self::with_id(ctx, c, id, span))
- }
-
- pub fn with_id(ctx: &MathContext, c: char, id: GlyphId, span: Span) -> Self {
- let class = match c {
- ':' => Some(MathClass::Relation),
- '.' | '/' | '⋯' | '⋱' | '⋰' | '⋮' => Some(MathClass::Normal),
- _ => unicode_math_class::class(c),
- };
- let mut fragment = Self {
- id,
- c,
- font: ctx.font.clone(),
- lang: TextElem::lang_in(ctx.styles()),
- fill: TextElem::fill_in(ctx.styles()).as_decoration(),
- shift: TextElem::baseline_in(ctx.styles()),
- style: ctx.style,
- font_size: ctx.size,
- width: Abs::zero(),
- ascent: Abs::zero(),
- descent: Abs::zero(),
- limits: Limits::for_char(c),
- italics_correction: Abs::zero(),
- class,
- span,
- meta: MetaElem::data_in(ctx.styles()),
- };
- fragment.set_id(ctx, id);
- fragment
- }
-
- /// Apply GSUB substitutions.
- fn adjust_glyph_index(ctx: &MathContext, id: GlyphId) -> GlyphId {
- if let Some(glyphwise_tables) = &ctx.glyphwise_tables {
- glyphwise_tables.iter().fold(id, |id, table| table.apply(id))
- } else {
- id
- }
- }
-
- /// Sets element id and boxes in appropriate way without changing other
- /// styles. This is used to replace the glyph with a stretch variant.
- pub fn set_id(&mut self, ctx: &MathContext, id: GlyphId) {
- let advance = ctx.ttf.glyph_hor_advance(id).unwrap_or_default();
- let italics = italics_correction(ctx, id).unwrap_or_default();
- let bbox = ctx.ttf.glyph_bounding_box(id).unwrap_or(Rect {
- x_min: 0,
- y_min: 0,
- x_max: 0,
- y_max: 0,
- });
-
- let mut width = advance.scaled(ctx);
- if !is_extended_shape(ctx, id) {
- width += italics;
- }
-
- self.id = id;
- self.width = width;
- self.ascent = bbox.y_max.scaled(ctx);
- self.descent = -bbox.y_min.scaled(ctx);
- self.italics_correction = italics;
- }
-
- pub fn height(&self) -> Abs {
- self.ascent + self.descent
- }
-
- pub fn into_variant(self) -> VariantFragment {
- VariantFragment {
- c: self.c,
- id: Some(self.id),
- style: self.style,
- font_size: self.font_size,
- italics_correction: self.italics_correction,
- class: self.class,
- span: self.span,
- limits: self.limits,
- frame: self.into_frame(),
- }
- }
-
- pub fn into_frame(self) -> Frame {
- let item = TextItem {
- font: self.font.clone(),
- size: self.font_size,
- fill: self.fill,
- lang: self.lang,
- text: self.c.into(),
- glyphs: vec![Glyph {
- id: self.id.0,
- x_advance: Em::from_length(self.width, self.font_size),
- x_offset: Em::zero(),
- range: 0..self.c.len_utf8() as u16,
- span: (self.span, 0),
- }],
- };
- let size = Size::new(self.width, self.ascent + self.descent);
- let mut frame = Frame::soft(size);
- frame.set_baseline(self.ascent);
- frame.push(Point::with_y(self.ascent + self.shift), FrameItem::Text(item));
- frame.meta_iter(self.meta);
- frame
- }
-
- pub fn make_scriptsize(&mut self, ctx: &MathContext) {
- let alt_id =
- script_alternatives(ctx, self.id).and_then(|alts| alts.alternates.get(0));
-
- if let Some(alt_id) = alt_id {
- self.set_id(ctx, alt_id);
- }
- }
-
- pub fn make_scriptscriptsize(&mut self, ctx: &MathContext) {
- let alts = script_alternatives(ctx, self.id);
- let alt_id = alts
- .and_then(|alts| alts.alternates.get(1).or_else(|| alts.alternates.get(0)));
-
- if let Some(alt_id) = alt_id {
- self.set_id(ctx, alt_id);
- }
- }
-}
-
-impl Debug for GlyphFragment {
- fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
- write!(f, "GlyphFragment({:?})", self.c)
- }
-}
-
-#[derive(Clone)]
-pub struct VariantFragment {
- pub c: char,
- pub id: Option<GlyphId>,
- pub italics_correction: Abs,
- pub frame: Frame,
- pub style: MathStyle,
- pub font_size: Abs,
- pub class: Option<MathClass>,
- pub span: Span,
- pub limits: Limits,
-}
-
-impl VariantFragment {
- /// Vertically adjust the fragment's frame so that it is centered
- /// on the axis.
- pub fn center_on_axis(&mut self, ctx: &MathContext) {
- let h = self.frame.height();
- self.frame.set_baseline(h / 2.0 + scaled!(ctx, axis_height));
- }
-}
-
-impl Debug for VariantFragment {
- fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
- write!(f, "VariantFragment({:?})", self.c)
- }
-}
-
-#[derive(Debug, Clone)]
-pub struct FrameFragment {
- pub frame: Frame,
- pub style: MathStyle,
- pub font_size: Abs,
- pub class: MathClass,
- pub limits: Limits,
- pub spaced: bool,
- pub base_ascent: Abs,
-}
-
-impl FrameFragment {
- pub fn new(ctx: &MathContext, mut frame: Frame) -> Self {
- let base_ascent = frame.ascent();
- frame.meta(ctx.styles(), false);
- Self {
- frame,
- font_size: ctx.size,
- style: ctx.style,
- class: MathClass::Normal,
- limits: Limits::Never,
- spaced: false,
- base_ascent,
- }
- }
-
- pub fn with_class(self, class: MathClass) -> Self {
- Self { class, ..self }
- }
-
- pub fn with_limits(self, limits: Limits) -> Self {
- Self { limits, ..self }
- }
-
- pub fn with_spaced(self, spaced: bool) -> Self {
- Self { spaced, ..self }
- }
-
- pub fn with_base_ascent(self, base_ascent: Abs) -> Self {
- Self { base_ascent, ..self }
- }
-}
-
-/// Look up the italics correction for a glyph.
-fn italics_correction(ctx: &MathContext, id: GlyphId) -> Option<Abs> {
- Some(ctx.table.glyph_info?.italic_corrections?.get(id)?.scaled(ctx))
-}
-
-/// Look up the script/scriptscript alternates for a glyph
-fn script_alternatives<'a>(
- ctx: &MathContext<'a, '_, '_>,
- id: GlyphId,
-) -> Option<AlternateSet<'a>> {
- ctx.ssty_table.and_then(|ssty| {
- ssty.coverage.get(id).and_then(|index| ssty.alternate_sets.get(index))
- })
-}
-
-/// Look up the italics correction for a glyph.
-fn is_extended_shape(ctx: &MathContext, id: GlyphId) -> bool {
- ctx.table
- .glyph_info
- .and_then(|info| info.extended_shapes)
- .and_then(|info| info.get(id))
- .is_some()
-}
-
-/// Look up a kerning value at a specific corner and height.
-///
-/// This can be integrated once we've found a font that actually provides this
-/// data.
-#[allow(unused)]
-fn kern_at_height(
- ctx: &MathContext,
- id: GlyphId,
- corner: Corner,
- height: Abs,
-) -> Option<Abs> {
- let kerns = ctx.table.glyph_info?.kern_infos?.get(id)?;
- let kern = match corner {
- Corner::TopLeft => kerns.top_left,
- Corner::TopRight => kerns.top_right,
- Corner::BottomRight => kerns.bottom_right,
- Corner::BottomLeft => kerns.bottom_left,
- }?;
-
- let mut i = 0;
- while i < kern.count() && height > kern.height(i)?.scaled(ctx) {
- i += 1;
- }
-
- Some(kern.kern(i)?.scaled(ctx))
-}
-
-/// An OpenType substitution table that is applicable to glyph-wise substitutions.
-pub enum GlyphwiseSubsts<'a> {
- Single(SingleSubstitution<'a>),
- Alternate(AlternateSubstitution<'a>, u32),
-}
-
-impl<'a> GlyphwiseSubsts<'a> {
- pub fn new(gsub: LayoutTable<'a>, feature: Feature) -> Option<Self> {
- let table = gsub
- .features
- .find(ttf_parser::Tag(feature.tag.0))
- .and_then(|feature| feature.lookup_indices.get(0))
- .and_then(|index| gsub.lookups.get(index))?;
- let table = table.subtables.get::<SubstitutionSubtable>(0)?;
- match table {
- SubstitutionSubtable::Single(single_glyphs) => {
- Some(Self::Single(single_glyphs))
- }
- SubstitutionSubtable::Alternate(alt_glyphs) => {
- Some(Self::Alternate(alt_glyphs, feature.value))
- }
- _ => None,
- }
- }
-
- pub fn try_apply(&self, glyph_id: GlyphId) -> Option<GlyphId> {
- match self {
- Self::Single(single) => match single {
- SingleSubstitution::Format1 { coverage, delta } => coverage
- .get(glyph_id)
- .map(|_| GlyphId(glyph_id.0.wrapping_add(*delta as u16))),
- SingleSubstitution::Format2 { coverage, substitutes } => {
- coverage.get(glyph_id).and_then(|idx| substitutes.get(idx))
- }
- },
- Self::Alternate(alternate, value) => alternate
- .coverage
- .get(glyph_id)
- .and_then(|idx| alternate.alternate_sets.get(idx))
- .and_then(|set| set.alternates.get(*value as u16)),
- }
- }
-
- pub fn apply(&self, glyph_id: GlyphId) -> GlyphId {
- self.try_apply(glyph_id).unwrap_or(glyph_id)
- }
-}
diff --git a/crates/typst-library/src/math/lr.rs b/crates/typst-library/src/math/lr.rs
deleted file mode 100644
index 39143620..00000000
--- a/crates/typst-library/src/math/lr.rs
+++ /dev/null
@@ -1,195 +0,0 @@
-use super::*;
-
-/// How much less high scaled delimiters can be than what they wrap.
-pub(super) const DELIM_SHORT_FALL: Em = Em::new(0.1);
-
-/// Scales delimiters.
-///
-/// While matched delimiters scale by default, this can be used to scale
-/// unmatched delimiters and to control the delimiter scaling more precisely.
-#[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>>,
-
- /// The delimited content, including the delimiters.
- #[required]
- #[parse(
- let mut body = Content::empty();
- for (i, arg) in args.all::<Content>()?.into_iter().enumerate() {
- if i > 0 {
- body += TextElem::packed(',');
- }
- body += arg;
- }
- body
- )]
- pub body: Content,
-}
-
-impl LayoutMath for LrElem {
- #[tracing::instrument(skip(ctx))]
- fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
- let mut body = self.body();
- if let Some(elem) = body.to::<LrElem>() {
- if elem.size(ctx.styles()).is_auto() {
- body = elem.body();
- }
- }
-
- let mut fragments = ctx.layout_fragments(body)?;
- let axis = scaled!(ctx, axis_height);
- let max_extent = fragments
- .iter()
- .map(|fragment| (fragment.ascent() - axis).max(fragment.descent() + axis))
- .max()
- .unwrap_or_default();
-
- let height = self
- .size(ctx.styles())
- .unwrap_or(Rel::one())
- .resolve(ctx.styles())
- .relative_to(2.0 * max_extent);
-
- match fragments.as_mut_slice() {
- [one] => scale(ctx, one, height, None),
- [first, .., last] => {
- scale(ctx, first, height, Some(MathClass::Opening));
- scale(ctx, last, height, Some(MathClass::Closing));
- }
- _ => {}
- }
-
- ctx.extend(fragments);
-
- Ok(())
- }
-}
-
-/// Scale a math fragment to a height.
-fn scale(
- ctx: &mut MathContext,
- fragment: &mut MathFragment,
- height: Abs,
- apply: Option<MathClass>,
-) {
- if matches!(
- fragment.class(),
- Some(MathClass::Opening | MathClass::Closing | MathClass::Fence)
- ) {
- let glyph = match fragment {
- MathFragment::Glyph(glyph) => glyph.clone(),
- MathFragment::Variant(variant) => {
- GlyphFragment::new(ctx, variant.c, variant.span)
- }
- _ => return,
- };
-
- let short_fall = DELIM_SHORT_FALL.scaled(ctx);
- let mut stretched = glyph.stretch_vertical(ctx, height, short_fall);
- stretched.center_on_axis(ctx);
-
- *fragment = MathFragment::Variant(stretched);
- if let Some(class) = apply {
- fragment.set_class(class);
- }
- }
-}
-
-/// Floors an expression.
-///
-/// ```example
-/// $ floor(x/2) $
-/// ```
-#[func]
-pub fn floor(
- /// The size of the brackets, relative to the height of the wrapped content.
- #[named]
- size: Option<Smart<Rel<Length>>>,
- /// The expression to floor.
- body: Content,
-) -> Content {
- delimited(body, '⌊', '⌋', size)
-}
-
-/// Ceils an expression.
-///
-/// ```example
-/// $ ceil(x/2) $
-/// ```
-#[func]
-pub fn ceil(
- /// The size of the brackets, relative to the height of the wrapped content.
- #[named]
- size: Option<Smart<Rel<Length>>>,
- /// The expression to ceil.
- body: Content,
-) -> Content {
- delimited(body, '⌈', '⌉', size)
-}
-
-/// Rounds an expression.
-///
-/// ```example
-/// $ round(x/2) $
-/// ```
-#[func]
-pub fn round(
- /// The size of the brackets, relative to the height of the wrapped content.
- #[named]
- size: Option<Smart<Rel<Length>>>,
- /// The expression to round.
- body: Content,
-) -> Content {
- delimited(body, '⌊', '⌉', size)
-}
-
-/// Takes the absolute value of an expression.
-///
-/// ```example
-/// $ abs(x/2) $
-/// ```
-#[func]
-pub fn abs(
- /// The size of the brackets, relative to the height of the wrapped content.
- #[named]
- size: Option<Smart<Rel<Length>>>,
- /// The expression to take the absolute value of.
- body: Content,
-) -> Content {
- delimited(body, '|', '|', size)
-}
-
-/// Takes the norm of an expression.
-///
-/// ```example
-/// $ norm(x/2) $
-/// ```
-#[func]
-pub fn norm(
- /// The size of the brackets, relative to the height of the wrapped content.
- #[named]
- size: Option<Smart<Rel<Length>>>,
- /// The expression to take the norm of.
- body: Content,
-) -> Content {
- delimited(body, '‖', '‖', size)
-}
-
-fn delimited(
- body: Content,
- left: char,
- right: char,
- size: Option<Smart<Rel<Length>>>,
-) -> Content {
- let mut elem = LrElem::new(Content::sequence([
- TextElem::packed(left),
- body,
- TextElem::packed(right),
- ]));
- // Push size only if size is provided
- if let Some(size) = size {
- elem.push_size(size);
- }
- elem.pack()
-}
diff --git a/crates/typst-library/src/math/matrix.rs b/crates/typst-library/src/math/matrix.rs
deleted file mode 100644
index b5d21ed6..00000000
--- a/crates/typst-library/src/math/matrix.rs
+++ /dev/null
@@ -1,655 +0,0 @@
-use super::*;
-
-const DEFAULT_ROW_GAP: Em = Em::new(0.5);
-const DEFAULT_COL_GAP: Em = Em::new(0.5);
-const VERTICAL_PADDING: Ratio = Ratio::new(0.1);
-
-const DEFAULT_STROKE_THICKNESS: Em = Em::new(0.05);
-
-/// A column vector.
-///
-/// Content in the vector's elements can be aligned with the `&` symbol.
-///
-/// # Example
-/// ```example
-/// $ vec(a, b, c) dot vec(1, 2, 3)
-/// = a + 2b + 3c $
-/// ```
-#[elem(title = "Vector", LayoutMath)]
-pub struct VecElem {
- /// The delimiter to use.
- ///
- /// ```example
- /// #set math.vec(delim: "[")
- /// $ vec(1, 2) $
- /// ```
- #[default(Some(Delimiter::Paren))]
- pub delim: Option<Delimiter>,
-
- /// The gap between elements.
- ///
- /// ```example
- /// #set math.vec(gap: 1em)
- /// $ vec(1, 2) $
- /// ```
- #[resolve]
- #[default(DEFAULT_ROW_GAP.into())]
- pub gap: Rel<Length>,
-
- /// The elements of the vector.
- #[variadic]
- pub children: Vec<Content>,
-}
-
-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(),
- FixedAlign::Center,
- self.gap(ctx.styles()),
- )?;
- layout_delimiters(
- ctx,
- frame,
- delim.map(Delimiter::open),
- delim.map(Delimiter::close),
- self.span(),
- )
- }
-}
-
-/// A matrix.
-///
-/// The elements of a row should be separated by commas, while the rows
-/// themselves should be separated by semicolons. The semicolon syntax merges
-/// preceding arguments separated by commas into an array. You can also use this
-/// special syntax of math function calls to define custom functions that take
-/// 2D data.
-///
-/// Content in cells that are in the same row can be aligned with the `&` symbol.
-///
-/// # Example
-/// ```example
-/// $ mat(
-/// 1, 2, ..., 10;
-/// 2, 2, ..., 10;
-/// dots.v, dots.v, dots.down, dots.v;
-/// 10, 10, ..., 10;
-/// ) $
-/// ```
-#[elem(title = "Matrix", LayoutMath)]
-pub struct MatElem {
- /// The delimiter to use.
- ///
- /// ```example
- /// #set math.mat(delim: "[")
- /// $ mat(1, 2; 3, 4) $
- /// ```
- #[default(Some(Delimiter::Paren))]
- pub delim: Option<Delimiter>,
-
- /// Draws augmentation lines in a matrix.
- ///
- /// - `{none}`: No lines are drawn.
- /// - A single number: A vertical augmentation line is drawn
- /// after the specified column number. Negative numbers start from the end.
- /// - A dictionary: With a dictionary, multiple augmentation lines can be
- /// drawn both horizontally and vertically. Additionally, the style of the
- /// lines can be set. The dictionary can contain the following keys:
- /// - `hline`: The offsets at which horizontal lines should be drawn.
- /// For example, an offset of `2` would result in a horizontal line
- /// being drawn after the second row of the matrix. Accepts either an
- /// integer for a single line, or an array of integers
- /// for multiple lines. Like for a single number, negative numbers start from the end.
- /// - `vline`: The offsets at which vertical lines should be drawn.
- /// For example, an offset of `2` would result in a vertical line being
- /// drawn after the second column of the matrix. Accepts either an
- /// integer for a single line, or an array of integers
- /// for multiple lines. Like for a single number, negative numbers start from the end.
- /// - `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) $
- /// // Equivalent to:
- /// $ mat(1, 0, 1; 0, 1, 2; augment: #(-1)) $
- /// ```
- ///
- /// ```example
- /// $ mat(0, 0, 0; 1, 1, 1; augment: #(hline: 1, stroke: 2pt + green)) $
- /// ```
- #[resolve]
- #[fold]
- pub augment: Option<Augment>,
-
- /// The gap between rows and columns.
- ///
- /// ```example
- /// #set math.mat(gap: 1em)
- /// $ mat(1, 2; 3, 4) $
- /// ```
- #[external]
- pub gap: Rel<Length>,
-
- /// The gap between rows. Takes precedence over `gap`.
- ///
- /// ```example
- /// #set math.mat(row-gap: 1em)
- /// $ mat(1, 2; 3, 4) $
- /// ```
- #[resolve]
- #[parse(
- let gap = args.named("gap")?;
- args.named("row-gap")?.or(gap)
- )]
- #[default(DEFAULT_ROW_GAP.into())]
- pub row_gap: Rel<Length>,
-
- /// The gap between columns. Takes precedence over `gap`.
- ///
- /// ```example
- /// #set math.mat(column-gap: 1em)
- /// $ mat(1, 2; 3, 4) $
- /// ```
- #[resolve]
- #[parse(args.named("column-gap")?.or(gap))]
- #[default(DEFAULT_COL_GAP.into())]
- pub column_gap: Rel<Length>,
-
- /// An array of arrays with the rows of the matrix.
- ///
- /// ```example
- /// #let data = ((1, 2, 3), (4, 5, 6))
- /// #let matrix = math.mat(..data)
- /// $ v := matrix $
- /// ```
- #[variadic]
- #[parse(
- let mut rows = vec![];
- let mut width = 0;
-
- let values = args.all::<Spanned<Value>>()?;
- if values.iter().any(|spanned| matches!(spanned.v, Value::Array(_))) {
- for Spanned { v, span } in values {
- let array = v.cast::<Array>().at(span)?;
- let row: Vec<_> = array.into_iter().map(Value::display).collect();
- width = width.max(row.len());
- rows.push(row);
- }
- } else {
- rows = vec![values.into_iter().map(|spanned| spanned.v.display()).collect()];
- }
-
- for row in &mut rows {
- if row.len() < width {
- row.resize(width, Content::empty());
- }
- }
-
- rows
- )]
- pub rows: Vec<Vec<Content>>,
-}
-
-impl LayoutMath for MatElem {
- #[tracing::instrument(skip(ctx))]
- fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
- // validate inputs
-
- let augment = self.augment(ctx.styles());
- let rows = self.rows();
-
- if let Some(aug) = &augment {
- for &offset in &aug.hline.0 {
- if offset == 0 || offset.unsigned_abs() >= rows.len() {
- bail!(
- self.span(),
- "cannot draw a horizontal line after row {} of a matrix with {} rows",
- if offset < 0 { rows.len() as isize + offset } else { offset },
- rows.len()
- );
- }
- }
-
- let ncols = self.rows().first().map_or(0, |row| row.len());
-
- for &offset in &aug.vline.0 {
- if offset == 0 || offset.unsigned_abs() >= ncols {
- bail!(
- self.span(),
- "cannot draw a vertical line after column {} of a matrix with {} columns",
- if offset < 0 { ncols as isize + offset } else { offset },
- ncols
- );
- }
- }
- }
-
- let delim = self.delim(ctx.styles());
- let frame = layout_mat_body(
- ctx,
- rows,
- augment,
- Axes::new(self.column_gap(ctx.styles()), self.row_gap(ctx.styles())),
- self.span(),
- )?;
-
- layout_delimiters(
- ctx,
- frame,
- delim.map(Delimiter::open),
- delim.map(Delimiter::close),
- self.span(),
- )
- }
-}
-
-/// A case distinction.
-///
-/// Content across different branches can be aligned with the `&` symbol.
-///
-/// # Example
-/// ```example
-/// $ f(x, y) := cases(
-/// 1 "if" (x dot y)/2 <= 0,
-/// 2 "if" x "is even",
-/// 3 "if" x in NN,
-/// 4 "else",
-/// ) $
-/// ```
-#[elem(LayoutMath)]
-pub struct CasesElem {
- /// The delimiter to use.
- ///
- /// ```example
- /// #set math.cases(delim: "[")
- /// $ x = cases(1, 2) $
- /// ```
- #[default(Delimiter::Brace)]
- pub delim: Delimiter,
-
- /// Whether the direction of cases should be reversed.
- ///
- /// ```example
- /// #set math.cases(reverse: true)
- /// $ cases(1, 2) = x $
- /// ```
- #[default(false)]
- pub reverse: bool,
-
- /// The gap between branches.
- ///
- /// ```example
- /// #set math.cases(gap: 1em)
- /// $ x = cases(1, 2) $
- /// ```
- #[resolve]
- #[default(DEFAULT_ROW_GAP.into())]
- pub gap: Rel<Length>,
-
- /// The branches of the case distinction.
- #[variadic]
- pub children: Vec<Content>,
-}
-
-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(),
- FixedAlign::Start,
- self.gap(ctx.styles()),
- )?;
-
- let (open, close) = if self.reverse(ctx.styles()) {
- (None, Some(delim.close()))
- } else {
- (Some(delim.open()), None)
- };
-
- layout_delimiters(ctx, frame, open, close, self.span())
- }
-}
-
-/// A vector / matrix delimiter.
-#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)]
-pub enum Delimiter {
- /// Delimit with parentheses.
- #[string("(")]
- Paren,
- /// Delimit with brackets.
- #[string("[")]
- Bracket,
- /// Delimit with curly braces.
- #[string("{")]
- Brace,
- /// Delimit with vertical bars.
- #[string("|")]
- Bar,
- /// Delimit with double vertical bars.
- #[string("||")]
- DoubleBar,
-}
-
-impl Delimiter {
- /// The delimiter's opening character.
- fn open(self) -> char {
- match self {
- Self::Paren => '(',
- Self::Bracket => '[',
- Self::Brace => '{',
- Self::Bar => '|',
- Self::DoubleBar => '‖',
- }
- }
-
- /// The delimiter's closing character.
- fn close(self) -> char {
- match self {
- Self::Paren => ')',
- Self::Bracket => ']',
- Self::Brace => '}',
- Self::Bar => '|',
- Self::DoubleBar => '‖',
- }
- }
-}
-
-/// Layout the inner contents of a vector.
-fn layout_vec_body(
- ctx: &mut MathContext,
- column: &[Content],
- align: FixedAlign,
- row_gap: Rel<Abs>,
-) -> SourceResult<Frame> {
- let gap = row_gap.relative_to(ctx.regions.base().y);
- ctx.style(ctx.style.for_denominator());
- let mut flat = vec![];
- for child in column {
- flat.push(ctx.layout_row(child)?);
- }
- ctx.unstyle();
- Ok(stack(ctx, flat, align, gap, 0))
-}
-
-/// Layout the inner contents of a matrix.
-fn layout_mat_body(
- ctx: &mut MathContext,
- rows: &[Vec<Content>],
- augment: Option<Augment<Abs>>,
- gap: Axes<Rel<Abs>>,
- span: Span,
-) -> SourceResult<Frame> {
- let gap = gap.zip_map(ctx.regions.base(), Rel::relative_to);
- let half_gap = gap * 0.5;
-
- // We provide a default stroke thickness that scales
- // with font size to ensure that augmentation lines
- // 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 = FixedStroke {
- thickness: default_stroke_thickness,
- paint: TextElem::fill_in(ctx.styles()).as_decoration(),
- line_cap: LineCap::Square,
- ..Default::default()
- };
-
- let (hline, vline, stroke) = match augment {
- Some(v) => {
- // need to get stroke here for ownership
- let stroke = v.stroke_or(default_stroke);
-
- (v.hline, v.vline, stroke)
- }
- _ => (Offsets::default(), Offsets::default(), default_stroke),
- };
-
- let ncols = rows.first().map_or(0, |row| row.len());
- let nrows = rows.len();
- if ncols == 0 || nrows == 0 {
- return Ok(Frame::soft(Size::zero()));
- }
-
- // Before the full matrix body can be laid out, the
- // individual cells must first be independently laid out
- // so we can ensure alignment across rows and columns.
-
- // This variable stores the maximum ascent and descent for each row.
- let mut heights = vec![(Abs::zero(), Abs::zero()); nrows];
-
- // We want to transpose our data layout to columns
- // before final layout. For efficiency, the columns
- // variable is set up here and newly generated
- // individual cells are then added to it.
- let mut cols = vec![vec![]; ncols];
-
- ctx.style(ctx.style.for_denominator());
- for (row, (ascent, descent)) in rows.iter().zip(&mut heights) {
- for (cell, col) in row.iter().zip(&mut cols) {
- let cell = ctx.layout_row(cell)?;
-
- ascent.set_max(cell.ascent());
- descent.set_max(cell.descent());
-
- col.push(cell);
- }
- }
- ctx.unstyle();
-
- // For each row, combine maximum ascent and descent into a row height.
- // Sum the row heights, then add the total height of the gaps between rows.
- let total_height =
- heights.iter().map(|&(a, b)| a + b).sum::<Abs>() + gap.y * (nrows - 1) as f64;
-
- // Width starts at zero because it can't be calculated until later
- let mut frame = Frame::soft(Size::new(Abs::zero(), total_height));
-
- let mut x = Abs::zero();
-
- for (index, col) in cols.into_iter().enumerate() {
- let AlignmentResult { points, width: rcol } = alignments(&col);
-
- let mut y = Abs::zero();
-
- for (cell, &(ascent, descent)) in col.into_iter().zip(&heights) {
- 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(),
- );
-
- frame.push_frame(pos, cell);
-
- y += ascent + descent + gap.y;
- }
-
- // Advance to the end of the column
- x += rcol;
-
- // If a vertical line should be inserted after this column
- if vline.0.contains(&(index as isize + 1))
- || vline.0.contains(&(1 - ((ncols - index) as isize)))
- {
- frame.push(
- Point::with_x(x + half_gap.x),
- line_item(total_height, true, stroke.clone(), span),
- );
- }
-
- // Advance to the start of the next column
- x += gap.x;
- }
-
- // Once all the columns are laid out, the total width can be calculated
- let total_width = x - gap.x;
-
- // This allows the horizontal lines to be laid out
- for line in hline.0 {
- let real_line =
- if line < 0 { nrows - line.unsigned_abs() } else { line as usize };
- let offset = (heights[0..real_line].iter().map(|&(a, b)| a + b).sum::<Abs>()
- + gap.y * (real_line - 1) as f64)
- + half_gap.y;
-
- frame.push(
- Point::with_y(offset),
- line_item(total_width, false, stroke.clone(), span),
- );
- }
-
- frame.size_mut().x = total_width;
-
- Ok(frame)
-}
-
-fn line_item(length: Abs, vertical: bool, stroke: FixedStroke, span: Span) -> FrameItem {
- let line_geom = if vertical {
- Geometry::Line(Point::with_y(length))
- } else {
- Geometry::Line(Point::with_x(length))
- };
-
- FrameItem::Shape(
- Shape {
- geometry: line_geom,
- fill: None,
- stroke: Some(stroke),
- },
- span,
- )
-}
-
-/// Layout the outer wrapper around the body of a vector or matrix.
-fn layout_delimiters(
- ctx: &mut MathContext,
- mut frame: Frame,
- left: Option<char>,
- right: Option<char>,
- span: Span,
-) -> SourceResult<()> {
- let axis = scaled!(ctx, axis_height);
- let short_fall = DELIM_SHORT_FALL.scaled(ctx);
- let height = frame.height();
- let target = height + VERTICAL_PADDING.of(height);
- frame.set_baseline(height / 2.0 + axis);
-
- if let Some(left) = left {
- let mut left =
- GlyphFragment::new(ctx, left, span).stretch_vertical(ctx, target, short_fall);
- left.center_on_axis(ctx);
- ctx.push(left);
- }
-
- ctx.push(FrameFragment::new(ctx, frame));
-
- if let Some(right) = right {
- let mut right = GlyphFragment::new(ctx, right, span)
- .stretch_vertical(ctx, target, short_fall);
- right.center_on_axis(ctx);
- ctx.push(right);
- }
-
- Ok(())
-}
-
-/// Parameters specifying how augmentation lines
-/// should be drawn on a matrix.
-#[derive(Debug, Default, Clone, PartialEq, Hash)]
-pub struct Augment<T: Numeric = Length> {
- pub hline: Offsets,
- pub vline: Offsets,
- pub stroke: Smart<Stroke<T>>,
-}
-
-impl Augment<Abs> {
- fn stroke_or(&self, fallback: FixedStroke) -> FixedStroke {
- match &self.stroke {
- Smart::Custom(v) => v.clone().unwrap_or(fallback),
- Smart::Auto => fallback,
- }
- }
-}
-
-impl Resolve for Augment {
- type Output = Augment<Abs>;
-
- fn resolve(self, styles: StyleChain) -> Self::Output {
- Augment {
- hline: self.hline,
- vline: self.vline,
- stroke: self.stroke.resolve(styles),
- }
- }
-}
-
-impl Fold for Augment<Abs> {
- type Output = Augment<Abs>;
-
- fn fold(mut self, outer: Self::Output) -> Self::Output {
- // Special case for handling `auto` strokes in subsequent `Augment`.
- if self.stroke.is_auto() && outer.stroke.is_custom() {
- self.stroke = outer.stroke;
- } else {
- self.stroke = self.stroke.fold(outer.stroke);
- }
-
- self
- }
-}
-
-cast! {
- Augment,
- self => {
- // if the stroke is auto and there is only one vertical line,
- if self.stroke.is_auto() && self.hline.0.is_empty() && self.vline.0.len() == 1 {
- return self.vline.0[0].into_value();
- }
-
- let d = dict! {
- "hline" => self.hline.into_value(),
- "vline" => self.vline.into_value(),
- "stroke" => self.stroke.into_value()
- };
-
- d.into_value()
- },
- v: isize => Augment {
- hline: Offsets::default(),
- vline: Offsets(smallvec![v]),
- stroke: Smart::Auto,
- },
- mut dict: Dict => {
- // need the transpose for the defaults to work
- let hline = dict.take("hline").ok().map(Offsets::from_value)
- .transpose().unwrap_or_default().unwrap_or_default();
- let vline = dict.take("vline").ok().map(Offsets::from_value)
- .transpose().unwrap_or_default().unwrap_or_default();
-
- let stroke = dict.take("stroke").ok().map(Stroke::from_value)
- .transpose()?.map(Smart::Custom).unwrap_or(Smart::Auto);
-
- Augment { hline, vline, stroke }
- },
-}
-
-cast! {
- Augment<Abs>,
- self => self.into_value(),
-}
-
-/// The offsets at which augmentation lines should be drawn on a matrix.
-#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
-pub struct Offsets(SmallVec<[isize; 1]>);
-
-cast! {
- Offsets,
- self => self.0.into_value(),
- v: isize => Self(smallvec![v]),
- v: Array => Self(v.into_iter().map(Value::cast).collect::<StrResult<_>>()?),
-}
diff --git a/crates/typst-library/src/math/mod.rs b/crates/typst-library/src/math/mod.rs
deleted file mode 100644
index 7ced638b..00000000
--- a/crates/typst-library/src/math/mod.rs
+++ /dev/null
@@ -1,500 +0,0 @@
-//! Mathematical formulas.
-
-#[macro_use]
-mod ctx;
-mod accent;
-mod align;
-mod attach;
-mod cancel;
-mod class;
-mod frac;
-mod fragment;
-mod lr;
-mod matrix;
-mod op;
-mod root;
-mod row;
-mod spacing;
-mod stretch;
-mod style;
-mod underover;
-
-pub use self::accent::*;
-pub use self::align::*;
-pub use self::attach::*;
-pub use self::cancel::*;
-pub use self::class::*;
-pub use self::frac::*;
-pub use self::lr::*;
-pub use self::matrix::*;
-pub use self::op::*;
-pub use self::root::*;
-pub use self::style::*;
-pub use self::underover::*;
-
-use std::borrow::Cow;
-
-use ttf_parser::{GlyphId, Rect};
-use typst::eval::{Module, Scope};
-use typst::font::{Font, FontWeight};
-use typst::model::Guard;
-use typst::util::option_eq;
-use unicode_math_class::MathClass;
-
-use self::ctx::*;
-use self::fragment::*;
-use self::row::*;
-use self::spacing::*;
-use crate::layout::{AlignElem, BoxElem, HElem, ParElem, Spacing};
-use crate::meta::{
- Count, Counter, CounterUpdate, LocalNameIn, Numbering, Outlinable, Refable,
- Supplement,
-};
-use crate::prelude::*;
-use crate::shared::BehavedBuilder;
-use crate::text::{
- families, variant, FontFamily, FontList, LinebreakElem, SpaceElem, TextElem, TextSize,
-};
-
-/// Create a module with all math definitions.
-pub fn module() -> Module {
- let mut math = Scope::deduplicating();
- 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);
- spacing::define(&mut math);
- for (name, symbol) in crate::symbols::SYM {
- math.define(*name, symbol.clone());
- }
-
- Module::new("math", math)
-}
-
-/// A mathematical equation.
-///
-/// Can be displayed inline with text or as a separate block.
-///
-/// # Example
-/// ```example
-/// #set text(font: "New Computer Modern")
-///
-/// Let $a$, $b$, and $c$ be the side
-/// lengths of right-angled triangle.
-/// Then, we know that:
-/// $ a^2 + b^2 = c^2 $
-///
-/// Prove by induction:
-/// $ sum_(k=1)^n k = (n(n+1)) / 2 $
-/// ```
-///
-/// # 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).
-#[elem(
- Locatable, Synthesize, Show, Finalize, Layout, LayoutMath, Count, LocalName, Refable,
- Outlinable
-)]
-pub struct EquationElem {
- /// Whether the equation is displayed as a separate block.
- #[default(false)]
- pub block: bool,
-
- /// How to [number]($numbering) block-level equations.
- ///
- /// ```example
- /// #set math.equation(numbering: "(1)")
- ///
- /// We define:
- /// $ phi.alt := (1 + sqrt(5)) / 2 $ <ratio>
- ///
- /// With @ratio, we get:
- /// $ F_n = floor(1 / sqrt(5) phi.alt^n) $
- /// ```
- pub numbering: Option<Numbering>,
-
- /// A supplement for the equation.
- ///
- /// For references to equations, this is added before the referenced number.
- ///
- /// If a function is specified, it is passed the referenced equation and
- /// should return content.
- ///
- /// ```example
- /// #set math.equation(numbering: "(1)", supplement: [Eq.])
- ///
- /// We define:
- /// $ phi.alt := (1 + sqrt(5)) / 2 $ <ratio>
- ///
- /// With @ratio, we get:
- /// $ F_n = floor(1 / sqrt(5) phi.alt^n) $
- /// ```
- pub supplement: Smart<Option<Supplement>>,
-
- /// The contents of the equation.
- #[required]
- pub body: Content,
-}
-
-impl Synthesize for EquationElem {
- fn synthesize(&mut self, vt: &mut Vt, styles: StyleChain) -> SourceResult<()> {
- // Resolve the supplement.
- let supplement = match self.supplement(styles) {
- Smart::Auto => TextElem::packed(Self::local_name_in(styles)),
- Smart::Custom(None) => Content::empty(),
- Smart::Custom(Some(supplement)) => supplement.resolve(vt, [self.clone()])?,
- };
-
- self.push_block(self.block(styles));
- self.push_numbering(self.numbering(styles));
- self.push_supplement(Smart::Custom(Some(Supplement::Content(supplement))));
-
- Ok(())
- }
-}
-
-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::elem()));
- if self.block(styles) {
- realized = AlignElem::new(realized).pack();
- }
- Ok(realized)
- }
-}
-
-impl Finalize for EquationElem {
- fn finalize(&self, realized: Content, style: StyleChain) -> Content {
- let mut realized = realized;
- if self.block(style) {
- realized = realized.styled(AlignElem::set_alignment(Align::CENTER));
- }
- realized
- .styled(TextElem::set_weight(FontWeight::from_number(450)))
- .styled(TextElem::set_font(FontList(vec![FontFamily::new(
- "New Computer Modern Math",
- )])))
- }
-}
-
-impl Layout for EquationElem {
- #[tracing::instrument(name = "EquationElem::layout", skip_all)]
- fn layout(
- &self,
- vt: &mut Vt,
- styles: StyleChain,
- regions: Regions,
- ) -> SourceResult<Fragment> {
- const NUMBER_GUTTER: Em = Em::new(0.5);
-
- let block = self.block(styles);
-
- // Find a math font.
- let variant = variant(styles);
- let world = vt.world;
- let Some(font) = families(styles).find_map(|family| {
- let id = world.book().select(family, variant)?;
- let font = world.font(id)?;
- let _ = font.ttf().tables().math?.constants?;
- Some(font)
- }) else {
- bail!(self.span(), "current font does not support math");
- };
-
- let mut ctx = MathContext::new(vt, styles, regions, &font, block);
- let mut frame = ctx.layout_frame(self)?;
-
- if block {
- if let Some(numbering) = self.numbering(styles) {
- let pod = Regions::one(regions.base(), Axes::splat(false));
- let counter = Counter::of(Self::elem())
- .display(Some(numbering), false)
- .layout(vt, styles, pod)?
- .into_frame();
-
- let full_counter_width = counter.width() + NUMBER_GUTTER.resolve(styles);
- let width = if regions.size.x.is_finite() {
- regions.size.x
- } else {
- frame.width() + 2.0 * full_counter_width
- };
-
- let height = frame.height().max(counter.height());
- let align = AlignElem::alignment_in(styles).resolve(styles).x;
- frame.resize(Size::new(width, height), Axes::splat(align));
-
- let dir = TextElem::dir_in(styles);
- let offset = match (align, dir) {
- (FixedAlign::Start, Dir::RTL) => full_counter_width,
- (FixedAlign::End, Dir::LTR) => -full_counter_width,
- _ => Abs::zero(),
- };
- frame.translate(Point::with_x(offset));
-
- let x = if dir.is_positive() {
- frame.width() - counter.width()
- } else {
- Abs::zero()
- };
- let y = (frame.height() - counter.height()) / 2.0;
-
- frame.push_frame(Point::new(x, y), counter)
- }
- } else {
- let font_size = TextElem::size_in(styles);
- let slack = ParElem::leading_in(styles) * 0.7;
- let top_edge = TextElem::top_edge_in(styles).resolve(font_size, &font, None);
- let bottom_edge =
- -TextElem::bottom_edge_in(styles).resolve(font_size, &font, None);
-
- let ascent = top_edge.max(frame.ascent() - slack);
- let descent = bottom_edge.max(frame.descent() - slack);
- frame.translate(Point::with_y(ascent - frame.baseline()));
- frame.size_mut().y = ascent + descent;
- }
-
- // Apply metadata.
- frame.meta(styles, false);
-
- Ok(Fragment::frame(frame))
- }
-}
-
-impl Count for EquationElem {
- fn update(&self) -> Option<CounterUpdate> {
- (self.block(StyleChain::default())
- && self.numbering(StyleChain::default()).is_some())
- .then(|| CounterUpdate::Step(NonZeroUsize::ONE))
- }
-}
-
-impl LocalName for EquationElem {
- fn local_name(lang: Lang, region: Option<Region>) -> &'static str {
- match lang {
- Lang::ALBANIAN => "Ekuacion",
- Lang::ARABIC => "معادلة",
- Lang::BOKMÅL => "Ligning",
- Lang::CHINESE if option_eq(region, "TW") => "方程式",
- Lang::CHINESE => "公式",
- Lang::CZECH => "Rovnice",
- Lang::DANISH => "Ligning",
- Lang::DUTCH => "Vergelijking",
- Lang::FILIPINO => "Ekwasyon",
- Lang::FINNISH => "Yhtälö",
- Lang::FRENCH => "Équation",
- Lang::GERMAN => "Gleichung",
- Lang::GREEK => "Εξίσωση",
- Lang::HUNGARIAN => "Egyenlet",
- Lang::ITALIAN => "Equazione",
- Lang::NYNORSK => "Likning",
- Lang::POLISH => "Równanie",
- Lang::PORTUGUESE => "Equação",
- Lang::ROMANIAN => "Ecuația",
- Lang::RUSSIAN => "Уравнение",
- Lang::SLOVENIAN => "Enačba",
- Lang::SPANISH => "Ecuación",
- Lang::SWEDISH => "Ekvation",
- Lang::TURKISH => "Denklem",
- Lang::UKRAINIAN => "Рівняння",
- Lang::VIETNAMESE => "Phương trình",
- Lang::JAPANESE => "式",
- Lang::ENGLISH | _ => "Equation",
- }
- }
-}
-
-impl Refable for EquationElem {
- fn supplement(&self) -> Content {
- // After synthesis, this should always be custom content.
- match self.supplement(StyleChain::default()) {
- Smart::Custom(Some(Supplement::Content(content))) => content,
- _ => Content::empty(),
- }
- }
-
- fn counter(&self) -> Counter {
- Counter::of(Self::elem())
- }
-
- fn numbering(&self) -> Option<Numbering> {
- self.numbering(StyleChain::default())
- }
-}
-
-impl Outlinable for EquationElem {
- fn outline(&self, vt: &mut Vt) -> SourceResult<Option<Content>> {
- if !self.block(StyleChain::default()) {
- return Ok(None);
- }
- let Some(numbering) = self.numbering(StyleChain::default()) else {
- return Ok(None);
- };
-
- // After synthesis, this should always be custom content.
- let mut supplement = match self.supplement(StyleChain::default()) {
- Smart::Custom(Some(Supplement::Content(content))) => content,
- _ => Content::empty(),
- };
-
- if !supplement.is_empty() {
- supplement += TextElem::packed("\u{a0}");
- }
-
- let numbers = self
- .counter()
- .at(vt, self.location().unwrap())?
- .display(vt, &numbering)?;
-
- Ok(Some(supplement + numbers))
- }
-}
-
-pub trait LayoutMath {
- fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()>;
-}
-
-impl LayoutMath for EquationElem {
- #[tracing::instrument(skip(ctx))]
- fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
- self.body().layout_math(ctx)
- }
-}
-
-impl LayoutMath for Content {
- #[tracing::instrument(skip(ctx))]
- fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
- // Directly layout the body of nested equations instead of handling it
- // like a normal equation so that things like this work:
- // ```
- // #let my = $pi$
- // $ my r^2 $
- // ```
- if let Some(elem) = self.to::<EquationElem>() {
- return elem.layout_math(ctx);
- }
-
- if let Some(realized) = ctx.realize(self)? {
- return realized.layout_math(ctx);
- }
-
- if self.is_sequence() {
- let mut bb = BehavedBuilder::new();
- self.sequence_recursive_for_each(&mut |child: &Content| {
- bb.push(Cow::Owned(child.clone()), StyleChain::default())
- });
-
- for (child, _) in bb.finish().0.iter() {
- child.layout_math(ctx)?;
- }
- return Ok(());
- }
-
- if let Some((elem, styles)) = self.to_styled() {
- if TextElem::font_in(ctx.styles().chain(styles))
- != TextElem::font_in(ctx.styles())
- {
- let frame = ctx.layout_content(self)?;
- ctx.push(FrameFragment::new(ctx, frame).with_spaced(true));
- return Ok(());
- }
-
- let prev_map = std::mem::replace(&mut ctx.local, styles.clone());
- let prev_size = ctx.size;
- ctx.local.apply(prev_map.clone());
- ctx.size = TextElem::size_in(ctx.styles());
- elem.layout_math(ctx)?;
- ctx.size = prev_size;
- ctx.local = prev_map;
- return Ok(());
- }
-
- if self.is::<SpaceElem>() {
- ctx.push(MathFragment::Space(ctx.space_width.scaled(ctx)));
- return Ok(());
- }
-
- if self.is::<LinebreakElem>() {
- ctx.push(MathFragment::Linebreak);
- return Ok(());
- }
-
- if let Some(elem) = self.to::<HElem>() {
- if let Spacing::Rel(rel) = elem.amount() {
- if rel.rel.is_zero() {
- ctx.push(MathFragment::Spacing(rel.abs.resolve(ctx.styles())));
- }
- }
- return Ok(());
- }
-
- if let Some(elem) = self.to::<TextElem>() {
- let fragment = ctx.layout_text(elem)?;
- ctx.push(fragment);
- return Ok(());
- }
-
- if let Some(boxed) = self.to::<BoxElem>() {
- let frame = ctx.layout_box(boxed)?;
- ctx.push(FrameFragment::new(ctx, frame).with_spaced(true));
- return Ok(());
- }
-
- if let Some(elem) = self.with::<dyn LayoutMath>() {
- return elem.layout_math(ctx);
- }
-
- let mut frame = ctx.layout_content(self)?;
- if !frame.has_baseline() {
- let axis = scaled!(ctx, axis_height);
- frame.set_baseline(frame.height() / 2.0 + axis);
- }
- ctx.push(FrameFragment::new(ctx, frame).with_spaced(true));
-
- Ok(())
- }
-}
diff --git a/crates/typst-library/src/math/op.rs b/crates/typst-library/src/math/op.rs
deleted file mode 100644
index 9e35d207..00000000
--- a/crates/typst-library/src/math/op.rs
+++ /dev/null
@@ -1,115 +0,0 @@
-use super::*;
-
-/// A text operator in an equation.
-///
-/// # Example
-/// ```example
-/// $ tan x = (sin x)/(cos x) $
-/// $ op("custom",
-/// limits: #true)_(n->oo) n $
-/// ```
-///
-/// # Predefined Operators { #predefined }
-/// Typst predefines the operators `arccos`, `arcsin`, `arctan`, `arg`, `cos`,
-/// `cosh`, `cot`, `coth`, `csc`, `csch`, `ctg`, `deg`, `det`, `dim`, `exp`,
-/// `gcd`, `hom`, `id`, `im`, `inf`, `ker`, `lg`, `lim`, `liminf`, `limsup`,
-/// `ln`, `log`, `max`, `min`, `mod`, `Pr`, `sec`, `sech`, `sin`, `sinc`,
-/// `sinh`, `sup`, `tan`, `tanh`, `tg` and `tr`.
-#[elem(title = "Text Operator", LayoutMath)]
-pub struct OpElem {
- /// The operator's text.
- #[required]
- pub text: Content,
-
- /// Whether the operator should show attachments as limits in display mode.
- #[default(false)]
- pub limits: bool,
-}
-
-impl LayoutMath for OpElem {
- #[tracing::instrument(skip(ctx))]
- fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
- let fragment = ctx.layout_fragment(self.text())?;
- ctx.push(
- FrameFragment::new(ctx, fragment.into_frame())
- .with_class(MathClass::Large)
- .with_limits(if self.limits(ctx.styles()) {
- Limits::Display
- } else {
- Limits::Never
- }),
- );
- Ok(())
- }
-}
-
-macro_rules! ops {
- ($($name:ident $(: $value:literal)? $(($tts:tt))?),* $(,)?) => {
- pub(super) fn define(math: &mut Scope) {
- $({
- let operator = EcoString::from(ops!(@name $name $(: $value)?));
- math.define(
- stringify!($name),
- OpElem::new(TextElem::new(operator).into())
- .with_limits(ops!(@limit $($tts)*))
- .pack()
- );
- })*
-
- let dif = |d| {
- HElem::new(THIN.into()).with_weak(true).pack()
- + MathStyleElem::new(TextElem::packed(d)).with_italic(Some(false)).pack()
- };
- math.define("dif", dif('d'));
- math.define("Dif", dif('D'));
- }
- };
- (@name $name:ident) => { stringify!($name) };
- (@name $name:ident: $value:literal) => { $value };
- (@limit limits) => { true };
- (@limit) => { false };
-}
-
-ops! {
- arccos,
- arcsin,
- arctan,
- arg,
- cos,
- cosh,
- cot,
- coth,
- csc,
- csch,
- ctg,
- deg,
- det (limits),
- dim,
- exp,
- gcd (limits),
- hom,
- id,
- im,
- inf (limits),
- ker,
- lg,
- lim (limits),
- liminf: "lim inf" (limits),
- limsup: "lim sup" (limits),
- ln,
- log,
- max (limits),
- min (limits),
- mod,
- Pr (limits),
- sec,
- sech,
- sin,
- sinc,
- sinh,
- sup (limits),
- tan,
- tanh,
- tg,
- tr,
-}
diff --git a/crates/typst-library/src/math/root.rs b/crates/typst-library/src/math/root.rs
deleted file mode 100644
index ba918ea9..00000000
--- a/crates/typst-library/src/math/root.rs
+++ /dev/null
@@ -1,137 +0,0 @@
-use super::*;
-
-/// A square root.
-///
-/// ```example
-/// $ sqrt(3 - 2 sqrt(2)) = sqrt(2) - 1 $
-/// ```
-#[func(title = "Square Root")]
-pub fn sqrt(
- /// The expression to take the square root of.
- radicand: Content,
-) -> Content {
- RootElem::new(radicand).pack()
-}
-
-/// A general root.
-///
-/// ```example
-/// $ root(3, x) $
-/// ```
-#[elem(LayoutMath)]
-pub struct RootElem {
- /// Which root of the radicand to take.
- #[positional]
- pub index: Option<Content>,
-
- /// The expression to take the root of.
- #[required]
- pub radicand: Content,
-}
-
-impl LayoutMath for RootElem {
- #[tracing::instrument(skip(ctx))]
- fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
- layout(ctx, self.index(ctx.styles()).as_ref(), self.radicand(), self.span())
- }
-}
-
-/// Layout a root.
-///
-/// TeXbook page 443, page 360
-/// See also: https://www.w3.org/TR/mathml-core/#radicals-msqrt-mroot
-fn layout(
- ctx: &mut MathContext,
- index: Option<&Content>,
- radicand: &Content,
- span: Span,
-) -> SourceResult<()> {
- let gap = scaled!(
- ctx,
- text: radical_vertical_gap,
- display: radical_display_style_vertical_gap,
- );
- let thickness = scaled!(ctx, radical_rule_thickness);
- let extra_ascender = scaled!(ctx, radical_extra_ascender);
- let kern_before = scaled!(ctx, radical_kern_before_degree);
- let kern_after = scaled!(ctx, radical_kern_after_degree);
- let raise_factor = percent!(ctx, radical_degree_bottom_raise_percent);
-
- // Layout radicand.
- ctx.style(ctx.style.with_cramped(true));
- let radicand = ctx.layout_frame(radicand)?;
- ctx.unstyle();
-
- // Layout root symbol.
- let target = radicand.height() + thickness + gap;
- let sqrt = GlyphFragment::new(ctx, '√', span)
- .stretch_vertical(ctx, target, Abs::zero())
- .frame;
-
- // Layout the index.
- ctx.style(ctx.style.with_size(MathSize::ScriptScript));
- let index = index.map(|elem| ctx.layout_frame(elem)).transpose()?;
- ctx.unstyle();
-
- // TeXbook, page 443, item 11
- // Keep original gap, and then distribute any remaining free space
- // equally above and below.
- let gap = gap.max((sqrt.height() - thickness - radicand.height() + gap) / 2.0);
-
- let sqrt_ascent = radicand.ascent() + gap + thickness;
- let descent = sqrt.height() - sqrt_ascent;
- let inner_ascent = sqrt_ascent + extra_ascender;
-
- let mut sqrt_offset = Abs::zero();
- let mut shift_up = Abs::zero();
- let mut ascent = inner_ascent;
-
- if let Some(index) = &index {
- sqrt_offset = kern_before + index.width() + kern_after;
- // The formula below for how much raise the index by comes from
- // the TeXbook, page 360, in the definition of `\root`.
- // However, the `+ index.descent()` part is different from TeX.
- // Without it, descenders can collide with the surd, a rarity
- // in practice, but possible. MS Word also adjusts index positions
- // for descenders.
- shift_up = raise_factor * (inner_ascent - descent) + index.descent();
- ascent.set_max(shift_up + index.ascent());
- }
-
- let radicand_x = sqrt_offset + sqrt.width();
- let radicand_y = ascent - radicand.ascent();
- let width = radicand_x + radicand.width();
- let size = Size::new(width, ascent + descent);
-
- // The extra "- thickness" comes from the fact that the sqrt is placed
- // in `push_frame` with respect to its top, not its baseline.
- let sqrt_pos = Point::new(sqrt_offset, radicand_y - gap - thickness);
- let line_pos = Point::new(radicand_x, radicand_y - gap - (thickness / 2.0));
- let radicand_pos = Point::new(radicand_x, radicand_y);
-
- let mut frame = Frame::soft(size);
- frame.set_baseline(ascent);
-
- if let Some(index) = index {
- let index_pos = Point::new(kern_before, ascent - index.ascent() - shift_up);
- frame.push_frame(index_pos, index);
- }
-
- frame.push_frame(sqrt_pos, sqrt);
- frame.push(
- line_pos,
- FrameItem::Shape(
- Geometry::Line(Point::with_x(radicand.width())).stroked(FixedStroke {
- paint: TextElem::fill_in(ctx.styles()).as_decoration(),
- thickness,
- ..FixedStroke::default()
- }),
- span,
- ),
- );
-
- frame.push_frame(radicand_pos, radicand);
- ctx.push(FrameFragment::new(ctx, frame));
-
- Ok(())
-}
diff --git a/crates/typst-library/src/math/row.rs b/crates/typst-library/src/math/row.rs
deleted file mode 100644
index 70813598..00000000
--- a/crates/typst-library/src/math/row.rs
+++ /dev/null
@@ -1,261 +0,0 @@
-use std::iter::once;
-
-use crate::layout::AlignElem;
-
-use super::*;
-
-pub const TIGHT_LEADING: Em = Em::new(0.25);
-
-#[derive(Debug, Default, Clone)]
-pub struct MathRow(Vec<MathFragment>);
-
-impl MathRow {
- pub fn new(fragments: Vec<MathFragment>) -> Self {
- let iter = fragments.into_iter().peekable();
- let mut last: Option<usize> = None;
- let mut space: Option<MathFragment> = None;
- let mut resolved: Vec<MathFragment> = vec![];
-
- for mut fragment in iter {
- match fragment {
- // Keep space only if supported by spaced fragments.
- MathFragment::Space(_) => {
- if last.is_some() {
- space = Some(fragment);
- }
- continue;
- }
-
- // Explicit spacing disables automatic spacing.
- MathFragment::Spacing(_) => {
- last = None;
- space = None;
- resolved.push(fragment);
- continue;
- }
-
- // Alignment points are resolved later.
- MathFragment::Align => {
- resolved.push(fragment);
- continue;
- }
-
- // New line, new things.
- MathFragment::Linebreak => {
- resolved.push(fragment);
- space = None;
- last = None;
- continue;
- }
-
- _ => {}
- }
-
- // Convert variable operators into binary operators if something
- // precedes them and they are not preceded by a operator or comparator.
- if fragment.class() == Some(MathClass::Vary)
- && matches!(
- last.and_then(|i| resolved[i].class()),
- Some(
- MathClass::Normal
- | MathClass::Alphabetic
- | MathClass::Closing
- | MathClass::Fence
- )
- )
- {
- fragment.set_class(MathClass::Binary);
- }
-
- // Insert spacing between the last and this item.
- if let Some(i) = last {
- if let Some(s) = spacing(&resolved[i], space.take(), &fragment) {
- resolved.insert(i + 1, s);
- }
- }
-
- last = Some(resolved.len());
- resolved.push(fragment);
- }
-
- Self(resolved)
- }
-
- pub fn iter(&self) -> std::slice::Iter<'_, MathFragment> {
- self.0.iter()
- }
-
- /// Extract the sublines of the row.
- ///
- /// It is very unintuitive, but in current state of things, a `MathRow` can
- /// contain several actual rows. That function deconstructs it to "single"
- /// rows. Hopefully this is only a temporary hack.
- pub fn rows(&self) -> Vec<Self> {
- self.0
- .split(|frag| matches!(frag, MathFragment::Linebreak))
- .map(|slice| Self(slice.to_vec()))
- .collect()
- }
-
- pub fn ascent(&self) -> Abs {
- self.iter().map(MathFragment::ascent).max().unwrap_or_default()
- }
-
- pub fn descent(&self) -> Abs {
- self.iter().map(MathFragment::descent).max().unwrap_or_default()
- }
-
- pub fn class(&self) -> MathClass {
- // Predict the class of the output of 'into_fragment'
- if self.0.len() == 1 {
- self.0
- .first()
- .and_then(|fragment| fragment.class())
- .unwrap_or(MathClass::Special)
- } else {
- // FrameFragment::new() (inside 'into_fragment' in this branch) defaults
- // to MathClass::Normal for its class.
- MathClass::Normal
- }
- }
-
- pub fn into_frame(self, ctx: &MathContext) -> Frame {
- let styles = ctx.styles();
- let align = AlignElem::alignment_in(styles).resolve(styles).x;
- self.into_aligned_frame(ctx, &[], align)
- }
-
- pub fn into_fragment(self, ctx: &MathContext) -> MathFragment {
- if self.0.len() == 1 {
- self.0.into_iter().next().unwrap()
- } else {
- FrameFragment::new(ctx, self.into_frame(ctx)).into()
- }
- }
-
- pub fn into_aligned_frame(
- self,
- ctx: &MathContext,
- points: &[Abs],
- align: FixedAlign,
- ) -> Frame {
- if !self.iter().any(|frag| matches!(frag, MathFragment::Linebreak)) {
- return self.into_line_frame(points, align);
- }
-
- let leading = if ctx.style.size >= MathSize::Text {
- ParElem::leading_in(ctx.styles())
- } else {
- TIGHT_LEADING.scaled(ctx)
- };
-
- let mut rows: Vec<_> = self.rows();
-
- if matches!(rows.last(), Some(row) if row.0.is_empty()) {
- rows.pop();
- }
-
- let AlignmentResult { points, width } = alignments(&rows);
- let mut frame = Frame::soft(Size::zero());
-
- for (i, row) in rows.into_iter().enumerate() {
- let sub = row.into_line_frame(&points, align);
- let size = frame.size_mut();
- if i > 0 {
- size.y += leading;
- }
-
- let mut pos = Point::with_y(size.y);
- if points.is_empty() {
- pos.x = align.position(width - sub.width());
- }
- size.y += sub.height();
- size.x.set_max(sub.width());
- frame.push_frame(pos, sub);
- }
-
- frame
- }
-
- fn into_line_frame(self, points: &[Abs], align: FixedAlign) -> Frame {
- let ascent = self.ascent();
- let mut frame = Frame::soft(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 != FixedAlign::Start {
- let mut width = Abs::zero();
- for fragment in self.iter() {
- if matches!(fragment, MathFragment::Align) {
- widths.push(width);
- width = Abs::zero();
- } else {
- width += fragment.width();
- }
- }
- widths.push(width);
- }
- let widths = widths;
-
- let mut prev_points = once(Abs::zero()).chain(points.iter().copied());
- let mut point_widths = points.iter().copied().zip(widths);
- let mut alternator = LeftRightAlternator::Right;
- move || match align {
- FixedAlign::Start => prev_points.next(),
- FixedAlign::End => {
- point_widths.next().map(|(point, width)| point - width)
- }
- _ => point_widths
- .next()
- .zip(prev_points.next())
- .zip(alternator.next())
- .map(|(((point, width), prev_point), alternator)| match alternator {
- LeftRightAlternator::Left => prev_point,
- LeftRightAlternator::Right => point - width,
- }),
- }
- };
- let mut x = next_x().unwrap_or_default();
-
- for fragment in self.0.into_iter() {
- if matches!(fragment, MathFragment::Align) {
- x = next_x().unwrap_or(x);
- continue;
- }
-
- let y = ascent - fragment.ascent();
- let pos = Point::new(x, y);
- x += fragment.width();
- frame.push_frame(pos, fragment.into_frame());
- }
-
- frame.size_mut().x = x;
- frame
- }
-}
-
-impl<T: Into<MathFragment>> From<T> for MathRow {
- fn from(fragment: T) -> Self {
- Self(vec![fragment.into()])
- }
-}
-
-#[derive(Debug, Copy, Clone, Eq, PartialEq)]
-enum LeftRightAlternator {
- Left,
- Right,
-}
-
-impl Iterator for LeftRightAlternator {
- type Item = LeftRightAlternator;
-
- fn next(&mut self) -> Option<Self::Item> {
- let r = Some(*self);
- match self {
- Self::Left => *self = Self::Right,
- Self::Right => *self = Self::Left,
- }
- r
- }
-}
diff --git a/crates/typst-library/src/math/spacing.rs b/crates/typst-library/src/math/spacing.rs
deleted file mode 100644
index 3dfce024..00000000
--- a/crates/typst-library/src/math/spacing.rs
+++ /dev/null
@@ -1,63 +0,0 @@
-use super::*;
-
-pub(super) const THIN: Em = Em::new(1.0 / 6.0);
-pub(super) const MEDIUM: Em = Em::new(2.0 / 9.0);
-pub(super) const THICK: Em = Em::new(5.0 / 18.0);
-pub(super) const QUAD: Em = Em::new(1.0);
-pub(super) const WIDE: Em = Em::new(2.0);
-
-/// Hook up all spacings.
-pub(super) fn define(math: &mut Scope) {
- math.define("thin", HElem::new(THIN.into()).pack());
- math.define("med", HElem::new(MEDIUM.into()).pack());
- math.define("thick", HElem::new(THICK.into()).pack());
- math.define("quad", HElem::new(QUAD.into()).pack());
- math.define("wide", HElem::new(WIDE.into()).pack());
-}
-
-/// Create the spacing between two fragments in a given style.
-pub(super) fn spacing(
- l: &MathFragment,
- space: Option<MathFragment>,
- r: &MathFragment,
-) -> Option<MathFragment> {
- use MathClass::*;
-
- let class = |f: &MathFragment| f.class().unwrap_or(Special);
- let resolve = |v: Em, f: &MathFragment| {
- Some(MathFragment::Spacing(f.font_size().map_or(Abs::zero(), |size| v.at(size))))
- };
- let script =
- |f: &MathFragment| f.style().map_or(false, |s| s.size <= MathSize::Script);
-
- match (class(l), class(r)) {
- // No spacing before punctuation; thin spacing after punctuation, unless
- // in script size.
- (_, Punctuation) => None,
- (Punctuation, _) if !script(l) => resolve(THIN, l),
-
- // No spacing after opening delimiters and before closing delimiters.
- (Opening, _) | (_, Closing) => None,
-
- // Thick spacing around relations, unless followed by a another relation
- // or in script size.
- (Relation, Relation) => None,
- (Relation, _) if !script(l) => resolve(THICK, l),
- (_, Relation) if !script(r) => resolve(THICK, r),
-
- // Medium spacing around binary operators, unless in script size.
- (Binary, _) if !script(l) => resolve(MEDIUM, l),
- (_, Binary) if !script(r) => resolve(MEDIUM, r),
-
- // Thin spacing around large operators, unless to the left of
- // an opening delimiter. TeXBook, p170
- (Large, Opening | Fence) => None,
- (Large, _) => resolve(THIN, l),
- (_, Large) => resolve(THIN, r),
-
- // Spacing around spaced frames.
- _ if (l.is_spaced() || r.is_spaced()) => space,
-
- _ => None,
- }
-}
diff --git a/crates/typst-library/src/math/stretch.rs b/crates/typst-library/src/math/stretch.rs
deleted file mode 100644
index e9bf6890..00000000
--- a/crates/typst-library/src/math/stretch.rs
+++ /dev/null
@@ -1,199 +0,0 @@
-use ttf_parser::math::{GlyphAssembly, GlyphConstruction, GlyphPart};
-use ttf_parser::LazyArray16;
-
-use super::*;
-
-/// Maximum number of times extenders can be repeated.
-const MAX_REPEATS: usize = 1024;
-
-impl GlyphFragment {
- /// Try to stretch a glyph to a desired height.
- pub fn stretch_vertical(
- self,
- ctx: &MathContext,
- height: Abs,
- short_fall: Abs,
- ) -> VariantFragment {
- stretch_glyph(ctx, self, height, short_fall, false)
- }
-
- /// Try to stretch a glyph to a desired width.
- pub fn stretch_horizontal(
- self,
- ctx: &MathContext,
- width: Abs,
- short_fall: Abs,
- ) -> VariantFragment {
- stretch_glyph(ctx, self, width, short_fall, true)
- }
-}
-
-/// Try to stretch a glyph to a desired width or height.
-///
-/// The resulting frame may not have the exact desired width.
-fn stretch_glyph(
- ctx: &MathContext,
- mut base: GlyphFragment,
- target: Abs,
- short_fall: Abs,
- horizontal: bool,
-) -> VariantFragment {
- let short_target = target - short_fall;
- let mut min_overlap = Abs::zero();
- let construction = ctx
- .table
- .variants
- .and_then(|variants| {
- min_overlap = variants.min_connector_overlap.scaled(ctx);
- if horizontal {
- variants.horizontal_constructions
- } else {
- variants.vertical_constructions
- }
- .get(base.id)
- })
- .unwrap_or(GlyphConstruction { assembly: None, variants: LazyArray16::new(&[]) });
-
- // If the base glyph is good enough, use it.
- let advance = if horizontal { base.width } else { base.height() };
- if short_target <= advance {
- return base.into_variant();
- }
-
- // Search for a pre-made variant with a good advance.
- let mut best_id = base.id;
- let mut best_advance = base.width;
- for variant in construction.variants {
- best_id = variant.variant_glyph;
- best_advance = base.font.to_em(variant.advance_measurement).at(base.font_size);
- if short_target <= best_advance {
- break;
- }
- }
-
- // This is either good or the best we've got.
- if short_target <= best_advance || construction.assembly.is_none() {
- base.set_id(ctx, best_id);
- return base.into_variant();
- }
-
- // Assemble from parts.
- let assembly = construction.assembly.unwrap();
- assemble(ctx, base, assembly, min_overlap, target, horizontal)
-}
-
-/// Assemble a glyph from parts.
-fn assemble(
- ctx: &MathContext,
- base: GlyphFragment,
- assembly: GlyphAssembly,
- min_overlap: Abs,
- target: Abs,
- horizontal: bool,
-) -> VariantFragment {
- // Determine the number of times the extenders need to be repeated as well
- // as a ratio specifying how much to spread the parts apart
- // (0 = maximal overlap, 1 = minimal overlap).
- let mut full;
- let mut ratio;
- let mut repeat = 0;
- loop {
- full = Abs::zero();
- ratio = 0.0;
-
- let mut parts = parts(assembly, repeat).peekable();
- let mut growable = Abs::zero();
-
- while let Some(part) = parts.next() {
- let mut advance = part.full_advance.scaled(ctx);
- if let Some(next) = parts.peek() {
- let max_overlap = part
- .end_connector_length
- .min(next.start_connector_length)
- .scaled(ctx);
-
- advance -= max_overlap;
- growable += max_overlap - min_overlap;
- }
-
- full += advance;
- }
-
- if full < target {
- let delta = target - full;
- ratio = (delta / growable).min(1.0);
- full += ratio * growable;
- }
-
- if target <= full || repeat >= MAX_REPEATS {
- break;
- }
-
- repeat += 1;
- }
-
- let mut selected = vec![];
- let mut parts = parts(assembly, repeat).peekable();
- while let Some(part) = parts.next() {
- let mut advance = part.full_advance.scaled(ctx);
- if let Some(next) = parts.peek() {
- let max_overlap =
- part.end_connector_length.min(next.start_connector_length).scaled(ctx);
- advance -= max_overlap;
- advance += ratio * (max_overlap - min_overlap);
- }
-
- let mut fragment = base.clone();
- fragment.set_id(ctx, part.glyph_id);
- selected.push((fragment, advance));
- }
-
- let size;
- let baseline;
- if horizontal {
- let height = base.ascent + base.descent;
- size = Size::new(full, height);
- baseline = base.ascent;
- } else {
- let axis = scaled!(ctx, axis_height);
- let width = selected.iter().map(|(f, _)| f.width).max().unwrap_or_default();
- size = Size::new(width, full);
- baseline = full / 2.0 + axis;
- }
-
- let mut frame = Frame::soft(size);
- let mut offset = Abs::zero();
- frame.set_baseline(baseline);
- frame.meta_iter(base.meta);
-
- for (fragment, advance) in selected {
- let pos = if horizontal {
- Point::new(offset, frame.baseline() - fragment.ascent)
- } else {
- Point::with_y(full - offset - fragment.height())
- };
- frame.push_frame(pos, fragment.into_frame());
- offset += advance;
- }
-
- VariantFragment {
- c: base.c,
- id: None,
- frame,
- style: base.style,
- font_size: base.font_size,
- italics_correction: Abs::zero(),
- class: base.class,
- span: base.span,
- limits: base.limits,
- }
-}
-
-/// Return an iterator over the assembly's parts with extenders repeated the
-/// specified number of times.
-fn parts(assembly: GlyphAssembly, repeat: usize) -> impl Iterator<Item = GlyphPart> + '_ {
- assembly.parts.into_iter().flat_map(move |part| {
- let count = if part.part_flags.extender() { repeat } else { 1 };
- std::iter::repeat(part).take(count)
- })
-}
diff --git a/crates/typst-library/src/math/style.rs b/crates/typst-library/src/math/style.rs
deleted file mode 100644
index 774fadac..00000000
--- a/crates/typst-library/src/math/style.rs
+++ /dev/null
@@ -1,574 +0,0 @@
-use super::*;
-
-/// Bold font style in math.
-///
-/// ```example
-/// $ bold(A) := B^+ $
-/// ```
-#[func]
-pub fn bold(
- /// The content to style.
- body: Content,
-) -> Content {
- MathStyleElem::new(body).with_bold(Some(true)).pack()
-}
-
-/// Upright (non-italic) font style in math.
-///
-/// ```example
-/// $ upright(A) != A $
-/// ```
-#[func]
-pub fn upright(
- /// The content to style.
- body: Content,
-) -> Content {
- MathStyleElem::new(body).with_italic(Some(false)).pack()
-}
-
-/// Italic font style in math.
-///
-/// For roman letters and greek lowercase letters, this is already the default.
-#[func]
-pub fn italic(
- /// The content to style.
- body: Content,
-) -> Content {
- MathStyleElem::new(body).with_italic(Some(true)).pack()
-}
-/// Serif (roman) font style in math.
-///
-/// This is already the default.
-#[func]
-pub fn serif(
- /// The content to style.
- body: Content,
-) -> Content {
- MathStyleElem::new(body).with_variant(Some(MathVariant::Serif)).pack()
-}
-
-/// Sans-serif font style in math.
-///
-/// ```example
-/// $ sans(A B C) $
-/// ```
-#[func(title = "Sans Serif")]
-pub fn sans(
- /// The content to style.
- body: Content,
-) -> Content {
- MathStyleElem::new(body).with_variant(Some(MathVariant::Sans)).pack()
-}
-
-/// Calligraphic font style in math.
-///
-/// ```example
-/// Let $cal(P)$ be the set of ...
-/// ```
-#[func(title = "Calligraphic")]
-pub fn cal(
- /// The content to style.
- body: Content,
-) -> Content {
- MathStyleElem::new(body).with_variant(Some(MathVariant::Cal)).pack()
-}
-
-/// Fraktur font style in math.
-///
-/// ```example
-/// $ frak(P) $
-/// ```
-#[func(title = "Fraktur")]
-pub fn frak(
- /// The content to style.
- body: Content,
-) -> Content {
- MathStyleElem::new(body).with_variant(Some(MathVariant::Frak)).pack()
-}
-
-/// Monospace font style in math.
-///
-/// ```example
-/// $ mono(x + y = z) $
-/// ```
-#[func(title = "Monospace")]
-pub fn mono(
- /// The content to style.
- body: Content,
-) -> Content {
- MathStyleElem::new(body).with_variant(Some(MathVariant::Mono)).pack()
-}
-
-/// Blackboard bold (double-struck) font style in math.
-///
-/// For uppercase latin letters, blackboard bold is additionally available
-/// through [symbols]($category/symbols/sym) of the form `NN` and `RR`.
-///
-/// ```example
-/// $ bb(b) $
-/// $ bb(N) = NN $
-/// $ f: NN -> RR $
-/// ```
-#[func(title = "Blackboard Bold")]
-pub fn bb(
- /// The content to style.
- body: Content,
-) -> Content {
- MathStyleElem::new(body).with_variant(Some(MathVariant::Bb)).pack()
-}
-
-/// Forced display style in math.
-///
-/// This is the normal size for block equations.
-///
-/// ```example
-/// $sum_i x_i/2 = display(sum_i x_i/2)$
-/// ```
-#[func(title = "Display Size")]
-pub fn display(
- /// The content to size.
- body: Content,
- /// Whether to impose a height restriction for exponents, like regular sub-
- /// and superscripts do.
- #[named]
- #[default(false)]
- cramped: bool,
-) -> Content {
- MathStyleElem::new(body)
- .with_size(Some(MathSize::Display))
- .with_cramped(Some(cramped))
- .pack()
-}
-
-/// Forced inline (text) style in math.
-///
-/// This is the normal size for inline equations.
-///
-/// ```example
-/// $ sum_i x_i/2
-/// = inline(sum_i x_i/2) $
-/// ```
-#[func(title = "Inline Size")]
-pub fn inline(
- /// The content to size.
- body: Content,
- /// Whether to impose a height restriction for exponents, like regular sub-
- /// and superscripts do.
- #[named]
- #[default(false)]
- cramped: bool,
-) -> Content {
- MathStyleElem::new(body)
- .with_size(Some(MathSize::Text))
- .with_cramped(Some(cramped))
- .pack()
-}
-
-/// Forced script style in math.
-///
-/// This is the smaller size used in powers or sub- or superscripts.
-///
-/// ```example
-/// $sum_i x_i/2 = script(sum_i x_i/2)$
-/// ```
-#[func(title = "Script Size")]
-pub fn script(
- /// The content to size.
- body: Content,
- /// Whether to impose a height restriction for exponents, like regular sub-
- /// and superscripts do.
- #[named]
- #[default(true)]
- cramped: bool,
-) -> Content {
- MathStyleElem::new(body)
- .with_size(Some(MathSize::Script))
- .with_cramped(Some(cramped))
- .pack()
-}
-
-/// Forced second script style in math.
-///
-/// This is the smallest size, used in second-level sub- and superscripts
-/// (script of the script).
-///
-/// ```example
-/// $sum_i x_i/2 = sscript(sum_i x_i/2)$
-/// ```
-#[func(title = "Script-Script Size")]
-pub fn sscript(
- /// The content to size.
- body: Content,
- /// Whether to impose a height restriction for exponents, like regular sub-
- /// and superscripts do.
- #[named]
- #[default(true)]
- cramped: bool,
-) -> Content {
- MathStyleElem::new(body)
- .with_size(Some(MathSize::ScriptScript))
- .with_cramped(Some(cramped))
- .pack()
-}
-
-/// A font variant in math.
-#[elem(LayoutMath)]
-pub struct MathStyleElem {
- /// The content to style.
- #[required]
- pub body: Content,
-
- /// The variant to select.
- pub variant: Option<MathVariant>,
-
- /// Whether to use bold glyphs.
- pub bold: Option<bool>,
-
- /// Whether to use italic glyphs.
- pub italic: Option<bool>,
-
- /// Whether to use forced size
- pub size: Option<MathSize>,
-
- /// Whether to limit height of exponents
- pub cramped: Option<bool>,
-}
-
-impl LayoutMath for MathStyleElem {
- #[tracing::instrument(skip(ctx))]
- fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
- let mut style = ctx.style;
- if let Some(variant) = self.variant(StyleChain::default()) {
- style = style.with_variant(variant);
- }
- if let Some(bold) = self.bold(StyleChain::default()) {
- style = style.with_bold(bold);
- }
- if let Some(italic) = self.italic(StyleChain::default()) {
- style = style.with_italic(italic);
- }
- if let Some(size) = self.size(StyleChain::default()) {
- style = style.with_size(size);
- }
- if let Some(cramped) = self.cramped(StyleChain::default()) {
- style = style.with_cramped(cramped);
- }
- ctx.style(style);
- self.body().layout_math(ctx)?;
- ctx.unstyle();
- Ok(())
- }
-}
-
-/// Text properties in math.
-#[derive(Debug, Copy, Clone, Eq, PartialEq)]
-pub struct MathStyle {
- /// The style variant to select.
- pub variant: MathVariant,
- /// The size of the glyphs.
- pub size: MathSize,
- /// The class of the element.
- pub class: Smart<MathClass>,
- /// Affects the height of exponents.
- pub cramped: bool,
- /// Whether to use bold glyphs.
- pub bold: bool,
- /// Whether to use italic glyphs.
- pub italic: Smart<bool>,
-}
-
-impl MathStyle {
- /// This style, with the given `variant`.
- pub fn with_variant(self, variant: MathVariant) -> Self {
- Self { variant, ..self }
- }
-
- /// This style, with the given `size`.
- pub fn with_size(self, size: MathSize) -> Self {
- Self { size, ..self }
- }
-
- // This style, with the given `class`.
- pub fn with_class(self, class: MathClass) -> Self {
- Self { class: Smart::Custom(class), ..self }
- }
-
- /// This style, with `cramped` set to the given value.
- pub fn with_cramped(self, cramped: bool) -> Self {
- Self { cramped, ..self }
- }
-
- /// This style, with `bold` set to the given value.
- pub fn with_bold(self, bold: bool) -> Self {
- Self { bold, ..self }
- }
-
- /// This style, with `italic` set to the given value.
- pub fn with_italic(self, italic: bool) -> Self {
- Self { italic: Smart::Custom(italic), ..self }
- }
-
- /// The style for subscripts in the current style.
- pub fn for_subscript(self) -> Self {
- self.for_superscript().with_cramped(true)
- }
-
- /// The style for superscripts in the current style.
- pub fn for_superscript(self) -> Self {
- self.with_size(match self.size {
- MathSize::Display | MathSize::Text => MathSize::Script,
- MathSize::Script | MathSize::ScriptScript => MathSize::ScriptScript,
- })
- }
-
- /// The style for numerators in the current style.
- pub fn for_numerator(self) -> Self {
- self.with_size(match self.size {
- MathSize::Display => MathSize::Text,
- MathSize::Text => MathSize::Script,
- MathSize::Script | MathSize::ScriptScript => MathSize::ScriptScript,
- })
- }
-
- /// The style for denominators in the current style.
- pub fn for_denominator(self) -> Self {
- self.for_numerator().with_cramped(true)
- }
-
- /// Apply the style to a character.
- pub fn styled_char(self, c: char) -> char {
- styled_char(self, c)
- }
-}
-
-/// The size of elements in an equation.
-///
-/// See the TeXbook p. 141.
-#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Cast, Hash)]
-pub enum MathSize {
- /// Second-level sub- and superscripts.
- ScriptScript,
- /// Sub- and superscripts.
- Script,
- /// Math in text.
- Text,
- /// Math on its own line.
- Display,
-}
-
-impl MathSize {
- pub(super) fn factor(self, ctx: &MathContext) -> f64 {
- match self {
- Self::Display | Self::Text => 1.0,
- Self::Script => percent!(ctx, script_percent_scale_down),
- Self::ScriptScript => percent!(ctx, script_script_percent_scale_down),
- }
- }
-}
-
-/// A mathematical style variant, as defined by Unicode.
-#[derive(Debug, Copy, Clone, Eq, PartialEq, Cast, Hash)]
-pub enum MathVariant {
- Serif,
- Sans,
- Cal,
- Frak,
- Mono,
- Bb,
-}
-
-impl Default for MathVariant {
- fn default() -> Self {
- Self::Serif
- }
-}
-
-/// Select the correct styled math letter.
-///
-/// https://www.w3.org/TR/mathml-core/#new-text-transform-mappings
-/// https://en.wikipedia.org/wiki/Mathematical_Alphanumeric_Symbols
-pub(super) fn styled_char(style: MathStyle, c: char) -> char {
- use MathVariant::*;
-
- let MathStyle { variant, bold, .. } = style;
- let italic = style.italic.unwrap_or(matches!(
- c,
- 'a'..='z' | 'ı' | 'ȷ' | 'A'..='Z' | 'α'..='ω' |
- '∂' | 'ϵ' | 'ϑ' | 'ϰ' | 'ϕ' | 'ϱ' | 'ϖ'
- ));
-
- if let Some(c) = basic_exception(c) {
- return c;
- }
-
- if let Some(c) = latin_exception(c, variant, bold, italic) {
- return c;
- }
-
- if let Some(c) = greek_exception(c, variant, bold, italic) {
- return c;
- }
-
- let base = match c {
- 'A'..='Z' => 'A',
- 'a'..='z' => 'a',
- 'Α'..='Ω' => 'Α',
- 'α'..='ω' => 'α',
- '0'..='9' => '0',
- _ => return c,
- };
-
- let tuple = (variant, bold, italic);
- let start = match c {
- // Latin upper.
- 'A'..='Z' => match tuple {
- (Serif, false, false) => 0x0041,
- (Serif, true, false) => 0x1D400,
- (Serif, false, true) => 0x1D434,
- (Serif, true, true) => 0x1D468,
- (Sans, false, false) => 0x1D5A0,
- (Sans, true, false) => 0x1D5D4,
- (Sans, false, true) => 0x1D608,
- (Sans, true, true) => 0x1D63C,
- (Cal, false, _) => 0x1D49C,
- (Cal, true, _) => 0x1D4D0,
- (Frak, false, _) => 0x1D504,
- (Frak, true, _) => 0x1D56C,
- (Mono, _, _) => 0x1D670,
- (Bb, _, _) => 0x1D538,
- },
-
- // Latin lower.
- 'a'..='z' => match tuple {
- (Serif, false, false) => 0x0061,
- (Serif, true, false) => 0x1D41A,
- (Serif, false, true) => 0x1D44E,
- (Serif, true, true) => 0x1D482,
- (Sans, false, false) => 0x1D5BA,
- (Sans, true, false) => 0x1D5EE,
- (Sans, false, true) => 0x1D622,
- (Sans, true, true) => 0x1D656,
- (Cal, false, _) => 0x1D4B6,
- (Cal, true, _) => 0x1D4EA,
- (Frak, false, _) => 0x1D51E,
- (Frak, true, _) => 0x1D586,
- (Mono, _, _) => 0x1D68A,
- (Bb, _, _) => 0x1D552,
- },
-
- // Greek upper.
- 'Α'..='Ω' => match tuple {
- (Serif, false, false) => 0x0391,
- (Serif, true, false) => 0x1D6A8,
- (Serif, false, true) => 0x1D6E2,
- (Serif, true, true) => 0x1D71C,
- (Sans, _, false) => 0x1D756,
- (Sans, _, true) => 0x1D790,
- (Cal | Frak | Mono | Bb, _, _) => return c,
- },
-
- // Greek lower.
- 'α'..='ω' => match tuple {
- (Serif, false, false) => 0x03B1,
- (Serif, true, false) => 0x1D6C2,
- (Serif, false, true) => 0x1D6FC,
- (Serif, true, true) => 0x1D736,
- (Sans, _, false) => 0x1D770,
- (Sans, _, true) => 0x1D7AA,
- (Cal | Frak | Mono | Bb, _, _) => return c,
- },
-
- // Numbers.
- '0'..='9' => match tuple {
- (Serif, false, _) => 0x0030,
- (Serif, true, _) => 0x1D7CE,
- (Bb, _, _) => 0x1D7D8,
- (Sans, false, _) => 0x1D7E2,
- (Sans, true, _) => 0x1D7EC,
- (Mono, _, _) => 0x1D7F6,
- (Cal | Frak, _, _) => return c,
- },
-
- _ => unreachable!(),
- };
-
- std::char::from_u32(start + (c as u32 - base as u32)).unwrap()
-}
-
-fn basic_exception(c: char) -> Option<char> {
- Some(match c {
- '〈' => '⟨',
- '〉' => '⟩',
- '《' => '⟪',
- '》' => '⟫',
- _ => return None,
- })
-}
-
-fn latin_exception(
- c: char,
- variant: MathVariant,
- bold: bool,
- italic: bool,
-) -> Option<char> {
- use MathVariant::*;
- Some(match (c, variant, bold, italic) {
- ('B', Cal, false, _) => 'ℬ',
- ('E', Cal, false, _) => 'ℰ',
- ('F', Cal, false, _) => 'ℱ',
- ('H', Cal, false, _) => 'ℋ',
- ('I', Cal, false, _) => 'ℐ',
- ('L', Cal, false, _) => 'ℒ',
- ('M', Cal, false, _) => 'ℳ',
- ('R', Cal, false, _) => 'ℛ',
- ('C', Frak, false, _) => 'ℭ',
- ('H', Frak, false, _) => 'ℌ',
- ('I', Frak, false, _) => 'ℑ',
- ('R', Frak, false, _) => 'ℜ',
- ('Z', Frak, false, _) => 'ℨ',
- ('C', Bb, ..) => 'ℂ',
- ('H', Bb, ..) => 'ℍ',
- ('N', Bb, ..) => 'ℕ',
- ('P', Bb, ..) => 'ℙ',
- ('Q', Bb, ..) => 'ℚ',
- ('R', Bb, ..) => 'ℝ',
- ('Z', Bb, ..) => 'ℤ',
- ('h', Serif, false, true) => 'ℎ',
- ('e', Cal, false, _) => 'ℯ',
- ('g', Cal, false, _) => 'ℊ',
- ('o', Cal, false, _) => 'ℴ',
- ('ı', Serif, .., true) => '𝚤',
- ('ȷ', Serif, .., true) => '𝚥',
- _ => return None,
- })
-}
-
-fn greek_exception(
- c: char,
- variant: MathVariant,
- bold: bool,
- italic: bool,
-) -> Option<char> {
- use MathVariant::*;
- let list = match c {
- 'ϴ' => ['𝚹', '𝛳', '𝜭', '𝝧', '𝞡'],
- '∇' => ['𝛁', '𝛻', '𝜵', '𝝯', '𝞩'],
- '∂' => ['𝛛', '𝜕', '𝝏', '𝞉', '𝟃'],
- 'ϵ' => ['𝛜', '𝜖', '𝝐', '𝞊', '𝟄'],
- 'ϑ' => ['𝛝', '𝜗', '𝝑', '𝞋', '𝟅'],
- 'ϰ' => ['𝛞', '𝜘', '𝝒', '𝞌', '𝟆'],
- 'ϕ' => ['𝛟', '𝜙', '𝝓', '𝞍', '𝟇'],
- 'ϱ' => ['𝛠', '𝜚', '𝝔', '𝞎', '𝟈'],
- 'ϖ' => ['𝛡', '𝜛', '𝝕', '𝞏', '𝟉'],
- _ => return None,
- };
-
- Some(match (variant, bold, italic) {
- (Serif, true, false) => list[0],
- (Serif, false, true) => list[1],
- (Serif, true, true) => list[2],
- (Sans, _, false) => list[3],
- (Sans, _, true) => list[4],
- _ => return None,
- })
-}
diff --git a/crates/typst-library/src/math/underover.rs b/crates/typst-library/src/math/underover.rs
deleted file mode 100644
index 6fc76830..00000000
--- a/crates/typst-library/src/math/underover.rs
+++ /dev/null
@@ -1,315 +0,0 @@
-use super::*;
-
-const BRACE_GAP: Em = Em::new(0.25);
-const BRACKET_GAP: Em = Em::new(0.25);
-
-/// A marker to distinguish under- vs. overlines.
-enum LineKind {
- Over,
- Under,
-}
-
-/// A horizontal line under content.
-///
-/// ```example
-/// $ underline(1 + 2 + ... + 5) $
-/// ```
-#[elem(LayoutMath)]
-pub struct UnderlineElem {
- /// The content above the line.
- #[required]
- pub body: Content,
-}
-
-impl LayoutMath for UnderlineElem {
- #[tracing::instrument(skip(ctx))]
- fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
- layout_underoverline(ctx, self.body(), self.span(), LineKind::Under)
- }
-}
-
-/// A horizontal line over content.
-///
-/// ```example
-/// $ overline(1 + 2 + ... + 5) $
-/// ```
-#[elem(LayoutMath)]
-pub struct OverlineElem {
- /// The content below the line.
- #[required]
- pub body: Content,
-}
-
-impl LayoutMath for OverlineElem {
- #[tracing::instrument(skip(ctx))]
- fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
- layout_underoverline(ctx, self.body(), self.span(), LineKind::Over)
- }
-}
-
-/// layout under- or overlined content
-fn layout_underoverline(
- ctx: &mut MathContext,
- body: &Content,
- span: Span,
- line: LineKind,
-) -> SourceResult<()> {
- let (extra_height, content, line_pos, content_pos, baseline, bar_height);
- match line {
- LineKind::Under => {
- let sep = scaled!(ctx, underbar_extra_descender);
- bar_height = scaled!(ctx, underbar_rule_thickness);
- let gap = scaled!(ctx, underbar_vertical_gap);
- extra_height = sep + bar_height + gap;
-
- content = ctx.layout_fragment(body)?;
-
- line_pos = Point::with_y(content.height() + gap + bar_height / 2.0);
- content_pos = Point::zero();
- baseline = content.ascent()
- }
- LineKind::Over => {
- let sep = scaled!(ctx, overbar_extra_ascender);
- bar_height = scaled!(ctx, overbar_rule_thickness);
- let gap = scaled!(ctx, overbar_vertical_gap);
- extra_height = sep + bar_height + gap;
-
- ctx.style(ctx.style.with_cramped(true));
- content = ctx.layout_fragment(body)?;
- ctx.unstyle();
-
- line_pos = Point::with_y(sep + bar_height / 2.0);
- content_pos = Point::with_y(extra_height);
- baseline = content.ascent() + extra_height;
- }
- }
-
- let width = content.width();
- let height = content.height() + extra_height;
- let size = Size::new(width, height);
-
- let content_class = content.class().unwrap_or(MathClass::Normal);
- let mut frame = Frame::soft(size);
- frame.set_baseline(baseline);
- frame.push_frame(content_pos, content.into_frame());
- frame.push(
- line_pos,
- FrameItem::Shape(
- Geometry::Line(Point::with_x(width)).stroked(FixedStroke {
- paint: TextElem::fill_in(ctx.styles()).as_decoration(),
- thickness: bar_height,
- ..FixedStroke::default()
- }),
- span,
- ),
- );
-
- ctx.push(FrameFragment::new(ctx, frame).with_class(content_class));
-
- Ok(())
-}
-
-/// A horizontal brace under content, with an optional annotation below.
-///
-/// ```example
-/// $ underbrace(1 + 2 + ... + 5, "numbers") $
-/// ```
-#[elem(LayoutMath)]
-pub struct UnderbraceElem {
- /// The content above the brace.
- #[required]
- pub body: Content,
-
- /// The optional content below the brace.
- #[positional]
- pub annotation: Option<Content>,
-}
-
-impl LayoutMath for UnderbraceElem {
- #[tracing::instrument(skip(ctx))]
- fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
- layout_underoverspreader(
- ctx,
- self.body(),
- &self.annotation(ctx.styles()),
- '⏟',
- BRACE_GAP,
- false,
- self.span(),
- )
- }
-}
-
-/// A horizontal brace over content, with an optional annotation above.
-///
-/// ```example
-/// $ overbrace(1 + 2 + ... + 5, "numbers") $
-/// ```
-#[elem(LayoutMath)]
-pub struct OverbraceElem {
- /// The content below the brace.
- #[required]
- pub body: Content,
-
- /// The optional content above the brace.
- #[positional]
- pub annotation: Option<Content>,
-}
-
-impl LayoutMath for OverbraceElem {
- #[tracing::instrument(skip(ctx))]
- fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
- layout_underoverspreader(
- ctx,
- self.body(),
- &self.annotation(ctx.styles()),
- '⏞',
- BRACE_GAP,
- true,
- self.span(),
- )
- }
-}
-
-/// A horizontal bracket under content, with an optional annotation below.
-///
-/// ```example
-/// $ underbracket(1 + 2 + ... + 5, "numbers") $
-/// ```
-#[elem(LayoutMath)]
-pub struct UnderbracketElem {
- /// The content above the bracket.
- #[required]
- pub body: Content,
-
- /// The optional content below the bracket.
- #[positional]
- pub annotation: Option<Content>,
-}
-
-impl LayoutMath for UnderbracketElem {
- #[tracing::instrument(skip(ctx))]
- fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
- layout_underoverspreader(
- ctx,
- self.body(),
- &self.annotation(ctx.styles()),
- '⎵',
- BRACKET_GAP,
- false,
- self.span(),
- )
- }
-}
-
-/// A horizontal bracket over content, with an optional annotation above.
-///
-/// ```example
-/// $ overbracket(1 + 2 + ... + 5, "numbers") $
-/// ```
-#[elem(LayoutMath)]
-pub struct OverbracketElem {
- /// The content below the bracket.
- #[required]
- pub body: Content,
-
- /// The optional content above the bracket.
- #[positional]
- pub annotation: Option<Content>,
-}
-
-impl LayoutMath for OverbracketElem {
- #[tracing::instrument(skip(ctx))]
- fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> {
- layout_underoverspreader(
- ctx,
- self.body(),
- &self.annotation(ctx.styles()),
- '⎴',
- BRACKET_GAP,
- true,
- self.span(),
- )
- }
-}
-
-/// Layout an over- or underbrace-like object.
-fn layout_underoverspreader(
- ctx: &mut MathContext,
- body: &Content,
- annotation: &Option<Content>,
- c: char,
- gap: Em,
- reverse: bool,
- span: Span,
-) -> SourceResult<()> {
- let gap = gap.scaled(ctx);
- let body = ctx.layout_row(body)?;
- let body_class = body.class();
- let body = body.into_fragment(ctx);
- let glyph = GlyphFragment::new(ctx, c, span);
- let stretched = glyph.stretch_horizontal(ctx, body.width(), Abs::zero());
-
- let mut rows = vec![MathRow::new(vec![body]), stretched.into()];
- ctx.style(if reverse {
- ctx.style.for_subscript()
- } else {
- ctx.style.for_superscript()
- });
- rows.extend(
- annotation
- .as_ref()
- .map(|annotation| ctx.layout_row(annotation))
- .transpose()?,
- );
- ctx.unstyle();
-
- let mut baseline = 0;
- if reverse {
- rows.reverse();
- baseline = rows.len() - 1;
- }
-
- let frame = stack(ctx, rows, FixedAlign::Center, gap, baseline);
- ctx.push(FrameFragment::new(ctx, frame).with_class(body_class));
-
- Ok(())
-}
-
-/// Stack rows on top of each other.
-///
-/// Add a `gap` between each row and uses the baseline of the `baseline`th
-/// row for the whole frame.
-pub(super) fn stack(
- ctx: &MathContext,
- rows: Vec<MathRow>,
- align: FixedAlign,
- gap: Abs,
- baseline: usize,
-) -> Frame {
- let rows: Vec<_> = rows.into_iter().flat_map(|r| r.rows()).collect();
- let AlignmentResult { points, width } = alignments(&rows);
- let rows: Vec<_> = rows
- .into_iter()
- .map(|row| row.into_aligned_frame(ctx, &points, align))
- .collect();
-
- let mut y = Abs::zero();
- let mut frame = Frame::soft(Size::new(
- width,
- rows.iter().map(|row| row.height()).sum::<Abs>()
- + rows.len().saturating_sub(1) as f64 * gap,
- ));
-
- for (i, row) in rows.into_iter().enumerate() {
- let x = align.position(width - row.width());
- let pos = Point::new(x, y);
- if i == baseline {
- frame.set_baseline(y + row.baseline());
- }
- y += row.height() + gap;
- frame.push_frame(pos, row);
- }
-
- frame
-}
diff --git a/crates/typst-library/src/meta/bibliography.rs b/crates/typst-library/src/meta/bibliography.rs
deleted file mode 100644
index b84c330a..00000000
--- a/crates/typst-library/src/meta/bibliography.rs
+++ /dev/null
@@ -1,1038 +0,0 @@
-use std::collections::HashMap;
-use std::ffi::OsStr;
-use std::fmt::{self, Debug, Formatter};
-use std::hash::{Hash, Hasher};
-use std::path::Path;
-use std::sync::Arc;
-
-use comemo::Prehashed;
-use ecow::EcoVec;
-use hayagriva::citationberg;
-use hayagriva::io::BibLaTeXError;
-use hayagriva::{
- BibliographyDriver, BibliographyRequest, CitationItem, CitationRequest,
- SpecificLocator,
-};
-use indexmap::IndexMap;
-use once_cell::sync::Lazy;
-use smallvec::SmallVec;
-use typed_arena::Arena;
-use typst::diag::FileError;
-use typst::eval::{eval_string, Bytes, CastInfo, EvalMode, Reflect};
-use typst::font::FontStyle;
-use typst::util::{option_eq, PicoStr};
-
-use crate::layout::{
- BlockElem, GridElem, HElem, PadElem, ParElem, Sizing, TrackSizings, VElem,
-};
-use crate::meta::{
- CitationForm, CiteGroup, FootnoteElem, HeadingElem, LinkElem, LocalName, LocalNameIn,
-};
-use crate::prelude::*;
-use crate::text::{Delta, SubElem, SuperElem, TextElem};
-
-/// A bibliography / reference listing.
-///
-/// You can create a new bibliography by calling this function with a path
-/// to a bibliography file in either one of two formats:
-///
-/// - A Hayagriva `.yml` file. Hayagriva is a new bibliography file format
-/// designed for use with Typst. Visit its
-/// [documentation](https://github.com/typst/hayagriva/blob/main/docs/file-format.md)
-/// for more details.
-/// - A BibLaTeX `.bib` file.
-///
-/// 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]($cite) function (`[#cite(<key>)]`). The bibliography will only
-/// show entries for works that were referenced in the document.
-///
-/// # Styles
-/// Typst offers a wide selection of built-in
-/// [citation and bibliography styles]($bibliography.style). Beyond those, you
-/// can add and use custom [CSL](https://citationstyles.org/) (Citation Style
-/// Language) files. Wondering which style to use? Here are some good defaults
-/// based on what discipline you're working in:
-///
-/// | Fields | Typical Styles |
-/// |-----------------|--------------------------------------------------------|
-/// | Engineering, IT | `{"ieee"}` |
-/// | Psychology, Life Sciences | `{"apa"}` |
-/// | Social sciences | `{"chicago-author-date"}` |
-/// | Humanities | `{"mla"}`, `{"chicago-notes"}`, `{"harvard-cite-them-right"}` |
-/// | Economics | `{"harvard-cite-them-right"}` |
-/// | Physics | `{"american-physics-society"}` |
-///
-/// # Example
-/// ```example
-/// This was already noted by
-/// pirates long ago. @arrgh
-///
-/// Multiple sources say ...
-/// @arrgh @netwok.
-///
-/// #bibliography("works.bib")
-/// ```
-#[elem(Locatable, Synthesize, Show, Finalize, LocalName)]
-pub struct BibliographyElem {
- /// Path(s) to Hayagriva `.yml` and/or BibLaTeX `.bib` files.
- #[required]
- #[parse(
- let (paths, bibliography) = Bibliography::parse(vm, args)?;
- paths
- )]
- pub path: BibPaths,
-
- /// The title of the bibliography.
- ///
- /// - 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.
- ///
- /// The bibliography's heading will not be numbered by default, but you can
- /// force it to be with a show-set rule:
- /// `{show bibliography: set heading(numbering: "1.")}`
- #[default(Some(Smart::Auto))]
- pub title: Option<Smart<Content>>,
-
- /// Whether to include all works from the given bibliography files, even
- /// those that weren't cited in the document.
- ///
- /// To selectively add individual cited works without showing them, you can
- /// also use the `cite` function with [`form`]($cite.form) set to `{none}`.
- #[default(false)]
- pub full: bool,
-
- /// The bibliography style.
- ///
- /// Should be either one of the built-in styles (see below) or a path to
- /// a [CSL file](https://citationstyles.org/). Some of the styles listed
- /// below appear twice, once with their full name and once with a short
- /// alias.
- #[parse(CslStyle::parse(vm, args)?)]
- #[default(CslStyle::from_name("ieee").unwrap())]
- pub style: CslStyle,
-
- /// The loaded bibliography.
- #[internal]
- #[required]
- #[parse(bibliography)]
- pub bibliography: Bibliography,
-
- /// The language setting where the bibliography is.
- #[internal]
- #[synthesized]
- pub lang: Lang,
-
- /// The region setting where the bibliography is.
- #[internal]
- #[synthesized]
- pub region: Option<Region>,
-}
-
-/// A list of bibliography file paths.
-#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
-pub struct BibPaths(Vec<EcoString>);
-
-cast! {
- BibPaths,
- self => self.0.into_value(),
- v: EcoString => Self(vec![v]),
- v: Array => Self(v.into_iter().map(Value::cast).collect::<StrResult<_>>()?),
-}
-
-impl BibliographyElem {
- /// Find the document's bibliography.
- pub fn find(introspector: Tracked<Introspector>) -> StrResult<Self> {
- let query = introspector.query(&Self::elem().select());
- let mut iter = query.iter();
- let Some(elem) = iter.next() else {
- bail!("the document does not contain a bibliography");
- };
-
- if iter.next().is_some() {
- bail!("multiple bibliographies are not yet supported");
- }
-
- Ok(elem.to::<Self>().cloned().unwrap())
- }
-
- /// Whether the bibliography contains the given key.
- pub fn has(vt: &Vt, key: impl Into<PicoStr>) -> bool {
- let key = key.into();
- vt.introspector
- .query(&Self::elem().select())
- .iter()
- .any(|elem| elem.to::<Self>().unwrap().bibliography().has(key))
- }
-
- /// Find all bibliography keys.
- pub fn keys(
- introspector: Tracked<Introspector>,
- ) -> Vec<(EcoString, Option<EcoString>)> {
- let mut vec = vec![];
- for elem in introspector.query(&Self::elem().select()).iter() {
- let this = elem.to::<Self>().unwrap();
- for entry in this.bibliography().entries() {
- let key = entry.key().into();
- let detail = entry.title().map(|title| title.value.to_str().into());
- vec.push((key, detail))
- }
- }
- vec
- }
-}
-
-impl Synthesize for BibliographyElem {
- fn synthesize(&mut self, _vt: &mut Vt, styles: StyleChain) -> SourceResult<()> {
- self.push_full(self.full(styles));
- self.push_style(self.style(styles));
- self.push_lang(TextElem::lang_in(styles));
- self.push_region(TextElem::region_in(styles));
- Ok(())
- }
-}
-
-impl Show for BibliographyElem {
- #[tracing::instrument(name = "BibliographyElem::show", skip_all)]
- fn show(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Content> {
- const COLUMN_GUTTER: Em = Em::new(0.65);
- const INDENT: Em = Em::new(1.5);
-
- let mut seq = vec![];
- if let Some(title) = self.title(styles) {
- let title = title.unwrap_or_else(|| {
- TextElem::packed(Self::local_name_in(styles)).spanned(self.span())
- });
-
- seq.push(HeadingElem::new(title).with_level(NonZeroUsize::ONE).pack());
- }
-
- Ok(vt.delayed(|vt| {
- let span = self.span();
- let works = Works::generate(vt.world, vt.introspector).at(span)?;
- let references = works
- .references
- .as_ref()
- .ok_or("CSL style is not suitable for bibliographies")
- .at(span)?;
-
- let row_gutter = *BlockElem::below_in(styles).amount();
- if references.iter().any(|(prefix, _)| prefix.is_some()) {
- let mut cells = vec![];
- for (prefix, reference) in references {
- cells.push(prefix.clone().unwrap_or_default());
- cells.push(reference.clone());
- }
-
- seq.push(VElem::new(row_gutter).with_weakness(3).pack());
- seq.push(
- GridElem::new(cells)
- .with_columns(TrackSizings(smallvec![Sizing::Auto; 2]))
- .with_column_gutter(TrackSizings(smallvec![COLUMN_GUTTER.into()]))
- .with_row_gutter(TrackSizings(smallvec![(row_gutter).into()]))
- .pack(),
- );
- } else {
- for (_, reference) in references {
- seq.push(VElem::new(row_gutter).with_weakness(3).pack());
- seq.push(reference.clone());
- }
- }
-
- let mut content = Content::sequence(seq);
- if works.hanging_indent {
- content = content.styled(ParElem::set_hanging_indent(INDENT.into()));
- }
-
- Ok(content)
- }))
- }
-}
-
-impl Finalize for BibliographyElem {
- fn finalize(&self, realized: Content, _: StyleChain) -> Content {
- const INDENT: Em = Em::new(1.0);
- realized
- .styled(HeadingElem::set_numbering(None))
- .styled(PadElem::set_left(INDENT.into()))
- }
-}
-
-impl LocalName for BibliographyElem {
- fn local_name(lang: Lang, region: Option<Region>) -> &'static str {
- match lang {
- Lang::ALBANIAN => "Bibliografi",
- Lang::ARABIC => "المراجع",
- Lang::BOKMÅL => "Bibliografi",
- Lang::CHINESE if option_eq(region, "TW") => "書目",
- Lang::CHINESE => "参考文献",
- Lang::CZECH => "Bibliografie",
- Lang::DANISH => "Bibliografi",
- Lang::DUTCH => "Bibliografie",
- Lang::FILIPINO => "Bibliograpiya",
- Lang::FINNISH => "Viitteet",
- Lang::FRENCH => "Bibliographie",
- Lang::GERMAN => "Bibliographie",
- Lang::GREEK => "Βιβλιογραφία",
- Lang::HUNGARIAN => "Irodalomjegyzék",
- Lang::ITALIAN => "Bibliografia",
- Lang::NYNORSK => "Bibliografi",
- Lang::POLISH => "Bibliografia",
- Lang::PORTUGUESE => "Bibliografia",
- Lang::ROMANIAN => "Bibliografie",
- Lang::RUSSIAN => "Библиография",
- Lang::SLOVENIAN => "Literatura",
- Lang::SPANISH => "Bibliografía",
- Lang::SWEDISH => "Bibliografi",
- Lang::TURKISH => "Kaynakça",
- Lang::UKRAINIAN => "Бібліографія",
- Lang::VIETNAMESE => "Tài liệu tham khảo",
- Lang::JAPANESE => "参考文献",
- Lang::ENGLISH | _ => "Bibliography",
- }
- }
-}
-
-/// A loaded bibliography.
-#[ty]
-#[derive(Clone, PartialEq)]
-pub struct Bibliography {
- map: Arc<IndexMap<PicoStr, hayagriva::Entry>>,
- hash: u128,
-}
-
-impl Bibliography {
- /// Parse the bibliography argument.
- fn parse(vm: &mut Vm, args: &mut Args) -> SourceResult<(BibPaths, Bibliography)> {
- let Spanned { v: paths, span } =
- args.expect::<Spanned<BibPaths>>("path to bibliography file")?;
-
- // Load bibliography files.
- let data = paths
- .0
- .iter()
- .map(|path| {
- let id = vm.resolve_path(path).at(span)?;
- vm.world().file(id).at(span)
- })
- .collect::<SourceResult<Vec<Bytes>>>()?;
-
- // Parse.
- let bibliography = Self::load(&paths, &data).at(span)?;
-
- Ok((paths, bibliography))
- }
-
- /// Load bibliography entries from paths.
- #[comemo::memoize]
- fn load(paths: &BibPaths, data: &[Bytes]) -> StrResult<Bibliography> {
- let mut map = IndexMap::new();
- let mut duplicates = Vec::<EcoString>::new();
-
- // We might have multiple bib/yaml files
- for (path, bytes) in paths.0.iter().zip(data) {
- let src = std::str::from_utf8(bytes).map_err(FileError::from)?;
-
- let ext = Path::new(path.as_str())
- .extension()
- .and_then(OsStr::to_str)
- .unwrap_or_default();
-
- let library = match ext.to_lowercase().as_str() {
- "yml" | "yaml" => hayagriva::io::from_yaml_str(src)
- .map_err(|err| eco_format!("failed to parse YAML ({err})"))?,
- "bib" => hayagriva::io::from_biblatex_str(src)
- .map_err(|errors| format_biblatex_error(path, src, errors))?,
- _ => bail!("unknown bibliography format (must be .yml/.yaml or .bib)"),
- };
-
- for entry in library {
- match map.entry(entry.key().into()) {
- indexmap::map::Entry::Vacant(vacant) => {
- vacant.insert(entry);
- }
- indexmap::map::Entry::Occupied(_) => {
- duplicates.push(entry.key().into());
- }
- }
- }
- }
-
- if !duplicates.is_empty() {
- bail!("duplicate bibliography keys: {}", duplicates.join(", "));
- }
-
- Ok(Bibliography {
- map: Arc::new(map),
- hash: typst::util::hash128(data),
- })
- }
-
- fn has(&self, key: impl Into<PicoStr>) -> bool {
- self.map.contains_key(&key.into())
- }
-
- fn entries(&self) -> impl Iterator<Item = &hayagriva::Entry> {
- self.map.values()
- }
-}
-
-impl Debug for Bibliography {
- fn fmt(&self, f: &mut Formatter) -> fmt::Result {
- f.debug_set().entries(self.map.keys()).finish()
- }
-}
-
-impl Hash for Bibliography {
- fn hash<H: Hasher>(&self, state: &mut H) {
- self.hash.hash(state);
- }
-}
-
-impl Repr for Bibliography {
- fn repr(&self) -> EcoString {
- "..".into()
- }
-}
-
-cast! {
- type Bibliography,
-}
-
-/// Format a BibLaTeX loading error.
-fn format_biblatex_error(path: &str, src: &str, errors: Vec<BibLaTeXError>) -> EcoString {
- let Some(error) = errors.first() else {
- return eco_format!("failed to parse BibLaTeX file ({path})");
- };
-
- let (span, msg) = match error {
- BibLaTeXError::Parse(error) => (&error.span, error.kind.to_string()),
- BibLaTeXError::Type(error) => (&error.span, error.kind.to_string()),
- };
- let line = src.get(..span.start).unwrap_or_default().lines().count();
- eco_format!("failed to parse BibLaTeX file ({path}:{line}: {msg})")
-}
-
-/// A loaded CSL style.
-#[ty]
-#[derive(Debug, Clone, PartialEq, Hash)]
-pub struct CslStyle {
- name: Option<EcoString>,
- style: Arc<Prehashed<citationberg::IndependentStyle>>,
-}
-
-impl CslStyle {
- /// Parse the style argument.
- pub fn parse(vm: &mut Vm, args: &mut Args) -> SourceResult<Option<CslStyle>> {
- let Some(Spanned { v: string, span }) =
- args.named::<Spanned<EcoString>>("style")?
- else {
- return Ok(None);
- };
-
- Ok(Some(Self::parse_impl(vm, &string).at(span)?))
- }
-
- /// Parse the style argument with `Smart`.
- pub fn parse_smart(
- vm: &mut Vm,
- args: &mut Args,
- ) -> SourceResult<Option<Smart<CslStyle>>> {
- let Some(Spanned { v: smart, span }) =
- args.named::<Spanned<Smart<EcoString>>>("style")?
- else {
- return Ok(None);
- };
-
- Ok(Some(match smart {
- Smart::Auto => Smart::Auto,
- Smart::Custom(string) => {
- Smart::Custom(Self::parse_impl(vm, &string).at(span)?)
- }
- }))
- }
-
- /// Parse internally.
- fn parse_impl(vm: &mut Vm, string: &str) -> StrResult<CslStyle> {
- let ext = Path::new(string)
- .extension()
- .and_then(OsStr::to_str)
- .unwrap_or_default()
- .to_lowercase();
-
- if ext == "csl" {
- let id = vm.resolve_path(string)?;
- let data = vm.world().file(id)?;
- CslStyle::from_data(&data)
- } else {
- CslStyle::from_name(string)
- }
- }
-
- /// Load a built-in CSL style.
- #[comemo::memoize]
- pub fn from_name(name: &str) -> StrResult<Self> {
- match hayagriva::archive::style_by_name(name) {
- Some(citationberg::Style::Independent(style)) => Ok(Self {
- name: Some(name.into()),
- style: Arc::new(Prehashed::new(style)),
- }),
- _ => bail!("unknown style: `{name}`"),
- }
- }
-
- /// Load a CSL style from file contents.
- #[comemo::memoize]
- pub fn from_data(data: &Bytes) -> StrResult<Self> {
- let text = std::str::from_utf8(data.as_slice()).map_err(FileError::from)?;
- citationberg::IndependentStyle::from_xml(text)
- .map(|style| Self { name: None, style: Arc::new(Prehashed::new(style)) })
- .map_err(|err| eco_format!("failed to load CSL style ({err})"))
- }
-
- /// Get the underlying independent style.
- pub fn get(&self) -> &citationberg::IndependentStyle {
- self.style.as_ref()
- }
-}
-
-// This Reflect impl is technically a bit wrong because it doesn't say what
-// FromValue and IntoValue really do. Instead, it says what the `style` argument
-// on `bibliography` and `cite` expect (through manual parsing).
-impl Reflect for CslStyle {
- #[comemo::memoize]
- fn input() -> CastInfo {
- let ty = std::iter::once(CastInfo::Type(Type::of::<Str>()));
- let options = hayagriva::archive::styles()
- .map(|style| CastInfo::Value(style.name.into_value(), style.full_name));
- CastInfo::Union(ty.chain(options).collect())
- }
-
- fn output() -> CastInfo {
- EcoString::output()
- }
-
- fn castable(value: &Value) -> bool {
- if let Value::Dyn(dynamic) = &value {
- if dynamic.is::<Self>() {
- return true;
- }
- }
-
- false
- }
-}
-
-impl FromValue for CslStyle {
- fn from_value(value: Value) -> StrResult<Self> {
- if let Value::Dyn(dynamic) = &value {
- if let Some(concrete) = dynamic.downcast::<Self>() {
- return Ok(concrete.clone());
- }
- }
-
- Err(<Self as Reflect>::error(&value))
- }
-}
-
-impl IntoValue for CslStyle {
- fn into_value(self) -> Value {
- Value::dynamic(self)
- }
-}
-
-impl Repr for CslStyle {
- fn repr(&self) -> EcoString {
- self.name
- .as_ref()
- .map(|name| name.repr())
- .unwrap_or_else(|| "..".into())
- }
-}
-
-/// Fully formatted citations and references, generated once (through
-/// memoization) for the whole document. This setup is necessary because
-/// citation formatting is inherently stateful and we need access to all
-/// citations to do it.
-pub(super) struct Works {
- /// Maps from the location of a citation group to its rendered content.
- pub citations: HashMap<Location, SourceResult<Content>>,
- /// Lists all references in the bibliography, with optional prefix, or
- /// `None` if the citation style can't be used for bibliographies.
- pub references: Option<Vec<(Option<Content>, Content)>>,
- /// Whether the bibliography should have hanging indent.
- pub hanging_indent: bool,
-}
-
-impl Works {
- /// Generate all citations and the whole bibliography.
- #[comemo::memoize]
- pub fn generate(
- world: Tracked<dyn World + '_>,
- introspector: Tracked<Introspector>,
- ) -> StrResult<Arc<Self>> {
- let mut generator = Generator::new(world, introspector)?;
- let rendered = generator.drive();
- let works = generator.display(&rendered)?;
- Ok(Arc::new(works))
- }
-}
-
-/// Context for generating the bibliography.
-struct Generator<'a> {
- /// The world that is used to evaluate mathematical material in citations.
- world: Tracked<'a, dyn World + 'a>,
- /// The document's bibliography.
- bibliography: BibliographyElem,
- /// The document's citation groups.
- groups: EcoVec<Prehashed<Content>>,
- /// Details about each group that are accumulated while driving hayagriva's
- /// bibliography driver and needed when processing hayagriva's output.
- infos: Vec<GroupInfo>,
- /// Citations with unresolved keys.
- failures: HashMap<Location, SourceResult<Content>>,
-}
-
-/// Details about a group of merged citations. All citations are put into groups
-/// of adjacent ones (e.g., `@foo @bar` will merge into a group of length two).
-/// Even single citations will be put into groups of length ones.
-struct GroupInfo {
- /// The group's location.
- location: Location,
- /// The group's span.
- span: Span,
- /// Whether the group should be displayed in a footnote.
- footnote: bool,
- /// Details about the groups citations.
- subinfos: SmallVec<[CiteInfo; 1]>,
-}
-
-/// Details about a citation item in a request.
-struct CiteInfo {
- /// The citation's key.
- key: Label,
- /// The citation's supplement.
- supplement: Option<Content>,
- /// Whether this citation was hidden.
- hidden: bool,
-}
-
-impl<'a> Generator<'a> {
- /// Create a new generator.
- fn new(
- world: Tracked<'a, dyn World + 'a>,
- introspector: Tracked<Introspector>,
- ) -> StrResult<Self> {
- let bibliography = BibliographyElem::find(introspector)?;
- let groups = introspector.query(&CiteGroup::elem().select());
- let infos = Vec::with_capacity(groups.len());
- Ok(Self {
- world,
- bibliography,
- groups,
- infos,
- failures: HashMap::new(),
- })
- }
-
- /// Drives hayagriva's citation driver.
- fn drive(&mut self) -> hayagriva::Rendered {
- static LOCALES: Lazy<Vec<citationberg::Locale>> =
- Lazy::new(hayagriva::archive::locales);
-
- let database = self.bibliography.bibliography();
- let bibliography_style = self.bibliography.style(StyleChain::default());
- let styles = Arena::new();
-
- // Process all citation groups.
- let mut driver = BibliographyDriver::new();
- for elem in &self.groups {
- let group = elem.to::<CiteGroup>().unwrap();
- let location = group.location().unwrap();
- let children = group.children();
-
- // Groups should never be empty.
- let Some(first) = children.first() else { continue };
-
- let mut subinfos = SmallVec::with_capacity(children.len());
- let mut items = Vec::with_capacity(children.len());
- let mut errors = EcoVec::new();
- let mut normal = true;
-
- // Create infos and items for each child in the group.
- for child in children {
- let key = *child.key();
- let Some(entry) = database.map.get(&key.into_inner()) else {
- errors.push(error!(
- child.span(),
- "key `{}` does not exist in the bibliography",
- key.as_str()
- ));
- continue;
- };
-
- let supplement = child.supplement(StyleChain::default());
- let locator = supplement.as_ref().map(|_| {
- SpecificLocator(
- citationberg::taxonomy::Locator::Custom,
- hayagriva::LocatorPayload::Transparent,
- )
- });
-
- let mut hidden = false;
- let special_form = match child.form(StyleChain::default()) {
- None => {
- hidden = true;
- None
- }
- Some(CitationForm::Normal) => None,
- Some(CitationForm::Prose) => Some(hayagriva::CitePurpose::Prose),
- Some(CitationForm::Full) => Some(hayagriva::CitePurpose::Full),
- Some(CitationForm::Author) => Some(hayagriva::CitePurpose::Author),
- Some(CitationForm::Year) => Some(hayagriva::CitePurpose::Year),
- };
-
- normal &= special_form.is_none();
- subinfos.push(CiteInfo { key, supplement, hidden });
- items.push(CitationItem::new(entry, locator, None, hidden, special_form));
- }
-
- if !errors.is_empty() {
- self.failures.insert(location, Err(errors));
- continue;
- }
-
- let style = match first.style(StyleChain::default()) {
- Smart::Auto => &bibliography_style.style,
- Smart::Custom(style) => styles.alloc(style.style),
- };
-
- self.infos.push(GroupInfo {
- location,
- subinfos,
- span: first.span(),
- footnote: normal
- && style.settings.class == citationberg::StyleClass::Note,
- });
-
- driver.citation(CitationRequest::new(
- items,
- style,
- Some(locale(*first.lang(), *first.region())),
- &LOCALES,
- None,
- ));
- }
-
- let locale = locale(*self.bibliography.lang(), *self.bibliography.region());
-
- // Add hidden items for everything if we should print the whole
- // bibliography.
- if self.bibliography.full(StyleChain::default()) {
- for entry in database.map.values() {
- driver.citation(CitationRequest::new(
- vec![CitationItem::new(entry, None, None, true, None)],
- bibliography_style.get(),
- Some(locale.clone()),
- &LOCALES,
- None,
- ));
- }
- }
-
- driver.finish(BibliographyRequest {
- style: bibliography_style.get(),
- locale: Some(locale),
- locale_files: &LOCALES,
- })
- }
-
- /// Displays hayagriva's output as content for the citations and references.
- fn display(&mut self, rendered: &hayagriva::Rendered) -> StrResult<Works> {
- let citations = self.display_citations(rendered);
- let references = self.display_references(rendered);
- let hanging_indent =
- rendered.bibliography.as_ref().map_or(false, |b| b.hanging_indent);
- Ok(Works { citations, references, hanging_indent })
- }
-
- /// Display the citation groups.
- fn display_citations(
- &mut self,
- rendered: &hayagriva::Rendered,
- ) -> HashMap<Location, SourceResult<Content>> {
- // Determine for each citation key where in the bibliography it is,
- // so that we can link there.
- let mut links = HashMap::new();
- if let Some(bibliography) = &rendered.bibliography {
- let location = self.bibliography.location().unwrap();
- for (k, item) in bibliography.items.iter().enumerate() {
- links.insert(item.key.as_str(), location.variant(k + 1));
- }
- }
-
- let mut output = std::mem::take(&mut self.failures);
- for (info, citation) in self.infos.iter().zip(&rendered.citations) {
- let supplement = |i: usize| info.subinfos.get(i)?.supplement.clone();
- let link = |i: usize| links.get(info.subinfos.get(i)?.key.as_str()).copied();
-
- let renderer = ElemRenderer {
- world: self.world,
- span: info.span,
- supplement: &supplement,
- link: &link,
- };
-
- let content = if info.subinfos.iter().all(|sub| sub.hidden) {
- Content::empty()
- } else {
- let mut content =
- renderer.display_elem_children(&citation.citation, &mut None);
-
- if info.footnote {
- content = FootnoteElem::with_content(content).pack();
- }
-
- content
- };
-
- output.insert(info.location, Ok(content));
- }
-
- output
- }
-
- /// Display the bibliography references.
- fn display_references(
- &self,
- rendered: &hayagriva::Rendered,
- ) -> Option<Vec<(Option<Content>, Content)>> {
- let rendered = rendered.bibliography.as_ref()?;
-
- // Determine for each citation key where it first occured, so that we
- // can link there.
- let mut first_occurances = HashMap::new();
- for info in &self.infos {
- for subinfo in &info.subinfos {
- let key = subinfo.key.as_str();
- first_occurances.entry(key).or_insert(info.location);
- }
- }
-
- // The location of the bibliography.
- let location = self.bibliography.location().unwrap();
-
- let mut output = vec![];
- for (k, item) in rendered.items.iter().enumerate() {
- let renderer = ElemRenderer {
- world: self.world,
- span: self.bibliography.span(),
- supplement: &|_| None,
- link: &|_| None,
- };
-
- // Each reference is assigned a manually created well-known location
- // that is derived from the bibliography's location. This way,
- // citations can link to them.
- let backlink = location.variant(k + 1);
-
- // Render the first field.
- let mut prefix = item.first_field.as_ref().map(|elem| {
- let mut content = renderer.display_elem_child(elem, &mut None);
- if let Some(location) = first_occurances.get(item.key.as_str()) {
- let dest = Destination::Location(*location);
- content = content.linked(dest);
- }
- content.backlinked(backlink)
- });
-
- // Render the main reference content.
- let reference = renderer
- .display_elem_children(&item.content, &mut prefix)
- .backlinked(backlink);
-
- output.push((prefix, reference));
- }
-
- Some(output)
- }
-}
-
-/// Renders hayagriva elements into content.
-struct ElemRenderer<'a> {
- /// The world that is used to evaluate mathematical material.
- world: Tracked<'a, dyn World + 'a>,
- /// The span that is attached to all of the resulting content.
- span: Span,
- /// Resolves the supplement of i-th citation in the request.
- supplement: &'a dyn Fn(usize) -> Option<Content>,
- /// Resolves where the i-th citation in the request should link to.
- link: &'a dyn Fn(usize) -> Option<Location>,
-}
-
-impl ElemRenderer<'_> {
- /// Display rendered hayagriva elements.
- ///
- /// The `prefix` can be a separate content storage where `left-margin`
- /// elements will be accumulated into.
- fn display_elem_children(
- &self,
- elems: &hayagriva::ElemChildren,
- prefix: &mut Option<Content>,
- ) -> Content {
- Content::sequence(
- elems.0.iter().map(|elem| self.display_elem_child(elem, prefix)),
- )
- }
-
- /// Display a rendered hayagriva element.
- fn display_elem_child(
- &self,
- elem: &hayagriva::ElemChild,
- prefix: &mut Option<Content>,
- ) -> Content {
- match elem {
- hayagriva::ElemChild::Text(formatted) => self.display_formatted(formatted),
- hayagriva::ElemChild::Elem(elem) => self.display_elem(elem, prefix),
- hayagriva::ElemChild::Markup(markup) => self.display_math(markup),
- hayagriva::ElemChild::Link { text, url } => self.display_link(text, url),
- hayagriva::ElemChild::Transparent { cite_idx, format } => {
- self.display_transparent(*cite_idx, format)
- }
- }
- }
-
- /// Display a block-level element.
- fn display_elem(
- &self,
- elem: &hayagriva::Elem,
- prefix: &mut Option<Content>,
- ) -> Content {
- use citationberg::Display;
-
- let block_level = matches!(elem.display, Some(Display::Block | Display::Indent));
-
- let mut suf_prefix = None;
- let mut content = self.display_elem_children(
- &elem.children,
- if block_level { &mut suf_prefix } else { prefix },
- );
-
- if let Some(prefix) = suf_prefix {
- const COLUMN_GUTTER: Em = Em::new(0.65);
- content = GridElem::new(vec![prefix, content])
- .with_columns(TrackSizings(smallvec![Sizing::Auto; 2]))
- .with_column_gutter(TrackSizings(smallvec![COLUMN_GUTTER.into()]))
- .pack();
- }
-
- match elem.display {
- Some(Display::Block) => {
- content = BlockElem::new().with_body(Some(content)).pack();
- }
- Some(Display::Indent) => {
- content = PadElem::new(content).pack();
- }
- Some(Display::LeftMargin) => {
- *prefix.get_or_insert_with(Default::default) += content;
- return Content::empty();
- }
- _ => {}
- }
-
- if let Some(hayagriva::ElemMeta::Entry(i)) = elem.meta {
- if let Some(location) = (self.link)(i) {
- let dest = Destination::Location(location);
- content = content.linked(dest);
- }
- }
-
- content
- }
-
- /// Display math.
- fn display_math(&self, math: &str) -> Content {
- eval_string(self.world, math, self.span, EvalMode::Math, Scope::new())
- .map(Value::display)
- .unwrap_or_else(|_| TextElem::packed(math).spanned(self.span))
- }
-
- /// Display a link.
- fn display_link(&self, text: &hayagriva::Formatted, url: &str) -> Content {
- let dest = Destination::Url(url.into());
- LinkElem::new(dest.into(), self.display_formatted(text)).pack()
- }
-
- /// Display transparent pass-through content.
- fn display_transparent(&self, i: usize, format: &hayagriva::Formatting) -> Content {
- let content = (self.supplement)(i).unwrap_or_default();
- apply_formatting(content, format)
- }
-
- /// Display formatted hayagriva text as content.
- fn display_formatted(&self, formatted: &hayagriva::Formatted) -> Content {
- let content = TextElem::packed(formatted.text.as_str()).spanned(self.span);
- apply_formatting(content, &formatted.formatting)
- }
-}
-
-/// Applies formatting to content.
-fn apply_formatting(mut content: Content, format: &hayagriva::Formatting) -> Content {
- match format.font_style {
- citationberg::FontStyle::Normal => {}
- citationberg::FontStyle::Italic => {
- content = content.styled(TextElem::set_style(FontStyle::Italic));
- }
- }
-
- match format.font_variant {
- citationberg::FontVariant::Normal => {}
- citationberg::FontVariant::SmallCaps => {
- content = content.styled(TextElem::set_smallcaps(true));
- }
- }
-
- match format.font_weight {
- citationberg::FontWeight::Normal => {}
- citationberg::FontWeight::Bold => {
- content = content.styled(TextElem::set_delta(Delta(300)));
- }
- citationberg::FontWeight::Light => {
- content = content.styled(TextElem::set_delta(Delta(-100)));
- }
- }
-
- match format.text_decoration {
- citationberg::TextDecoration::None => {}
- citationberg::TextDecoration::Underline => {
- content = content.underlined();
- }
- }
-
- match format.vertical_align {
- citationberg::VerticalAlign::None => {}
- citationberg::VerticalAlign::Baseline => {}
- citationberg::VerticalAlign::Sup => {
- // Add zero-width weak spacing to make the superscript "sticky".
- content = HElem::hole().pack() + SuperElem::new(content).pack();
- }
- citationberg::VerticalAlign::Sub => {
- content = HElem::hole().pack() + SubElem::new(content).pack();
- }
- }
-
- content
-}
-
-/// Create a locale code from language and optionally region.
-fn locale(lang: Lang, region: Option<Region>) -> citationberg::LocaleCode {
- let mut value = String::with_capacity(5);
- value.push_str(lang.as_str());
- if let Some(region) = region {
- value.push('-');
- value.push_str(region.as_str())
- }
- citationberg::LocaleCode(value)
-}
diff --git a/crates/typst-library/src/meta/cite.rs b/crates/typst-library/src/meta/cite.rs
deleted file mode 100644
index c0bd71ae..00000000
--- a/crates/typst-library/src/meta/cite.rs
+++ /dev/null
@@ -1,156 +0,0 @@
-use crate::meta::bibliography::Works;
-use crate::meta::CslStyle;
-use crate::prelude::*;
-use crate::text::TextElem;
-
-/// Cite a work from the bibliography.
-///
-/// Before you starting citing, you need to add a [bibliography]($bibliography)
-/// somewhere in your document.
-///
-/// # Example
-/// ```example
-/// This was already noted by
-/// pirates long ago. @arrgh
-///
-/// Multiple sources say ...
-/// @arrgh @netwok.
-///
-/// You can also call `cite`
-/// explicitly. #cite(<arrgh>)
-///
-/// #bibliography("works.bib")
-/// ```
-///
-/// # Syntax
-/// 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(Synthesize)]
-pub struct CiteElem {
- /// The citation key that identifies the entry in the bibliography that
- /// shall be cited, as a label.
- ///
- /// ```example
- /// // All the same
- /// @netwok \
- /// #cite(<netwok>) \
- /// #cite(label("netwok"))
- /// >>> #set text(0pt)
- /// >>> #bibliography("works.bib", style: "apa")
- /// ```
- #[required]
- pub key: Label,
-
- /// A supplement for the citation such as page or chapter number.
- ///
- /// In reference syntax, the supplement can be added in square brackets:
- ///
- /// ```example
- /// This has been proven. @distress[p.~7]
- ///
- /// #bibliography("works.bib")
- /// ```
- pub supplement: Option<Content>,
-
- /// The kind of citation to produce. Different forms are useful in different
- /// scenarios: A normal citation is useful as a source at the end of a
- /// sentence, while a "prose" citation is more suitable for inclusion in the
- /// flow of text.
- ///
- /// If set to `{none}`, the cited work is included in the bibliography, but
- /// nothing will be displayed.
- ///
- /// ```example
- /// #cite(<netwok>, form: "prose")
- /// show the outsized effects of
- /// pirate life on the human psyche.
- /// >>> #set text(0pt)
- /// >>> #bibliography("works.bib", style: "apa")
- /// ```
- #[default(Some(CitationForm::Normal))]
- pub form: Option<CitationForm>,
-
- /// The citation style.
- ///
- /// Should be either `{auto}`, one of the built-in styles (see below) or a
- /// path to a [CSL file](https://citationstyles.org/). Some of the styles
- /// listed below appear twice, once with their full name and once with a
- /// short alias.
- ///
- /// When set to `{auto}`, automatically use the
- /// [bibliography's style]($bibliography.style) for the citations.
- #[parse(CslStyle::parse_smart(vm, args)?)]
- pub style: Smart<CslStyle>,
-
- /// The text language setting where the citation is.
- #[internal]
- #[synthesized]
- pub lang: Lang,
-
- /// The text region setting where the citation is.
- #[internal]
- #[synthesized]
- pub region: Option<Region>,
-}
-
-impl Synthesize for CiteElem {
- fn synthesize(&mut self, _vt: &mut Vt, styles: StyleChain) -> SourceResult<()> {
- self.push_supplement(self.supplement(styles));
- self.push_form(self.form(styles));
- self.push_style(self.style(styles));
- self.push_lang(TextElem::lang_in(styles));
- self.push_region(TextElem::region_in(styles));
- Ok(())
- }
-}
-
-cast! {
- CiteElem,
- v: Content => v.to::<Self>().cloned().ok_or("expected citation")?,
-}
-
-/// The form of the citation.
-#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash, Cast)]
-pub enum CitationForm {
- /// Display in the standard way for the active style.
- #[default]
- Normal,
- /// Produces a citation that is suitable for inclusion in a sentence.
- Prose,
- /// Mimics a bibliography entry, with full information about the cited work.
- Full,
- /// Shows only the cited work's author(s).
- Author,
- /// Shows only the cited work's year.
- Year,
-}
-
-/// A group of citations.
-///
-/// This is automatically created from adjacent citations during show rule
-/// application.
-#[elem(Locatable, Show)]
-pub struct CiteGroup {
- /// The citations.
- #[required]
- pub children: Vec<CiteElem>,
-}
-
-impl Show for CiteGroup {
- #[tracing::instrument(name = "CiteGroup::show", skip(self, vt))]
- fn show(&self, vt: &mut Vt, _: StyleChain) -> SourceResult<Content> {
- Ok(vt.delayed(|vt| {
- let location = self.location().unwrap();
- let span = self.span();
- Works::generate(vt.world, vt.introspector)
- .at(span)?
- .citations
- .get(&location)
- .cloned()
- .unwrap_or_else(|| {
- bail!(span, "failed to format citation (this is a bug)")
- })
- }))
- }
-}
diff --git a/crates/typst-library/src/meta/context.rs b/crates/typst-library/src/meta/context.rs
deleted file mode 100644
index 59b35577..00000000
--- a/crates/typst-library/src/meta/context.rs
+++ /dev/null
@@ -1,169 +0,0 @@
-use crate::prelude::*;
-
-/// Provides access to the location of content.
-///
-/// This is useful in combination with [queries]($query), [counters]($counter),
-/// [state]($state), and [links]($link). See their documentation for more
-/// details.
-///
-/// ```example
-/// #locate(loc => [
-/// My location: \
-/// #loc.position()!
-/// ])
-/// ```
-#[func]
-pub fn locate(
- /// 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
- /// content that depends on its own location in the document.
- func: Func,
-) -> Content {
- LocateElem::new(func).pack()
-}
-
-/// Executes a `locate` call.
-#[elem(Locatable, Show)]
-struct LocateElem {
- /// The function to call with the location.
- #[required]
- func: Func,
-}
-
-impl Show for LocateElem {
- #[tracing::instrument(name = "LocateElem::show", skip(self, vt))]
- fn show(&self, vt: &mut Vt, _: StyleChain) -> SourceResult<Content> {
- Ok(vt.delayed(|vt| {
- let location = self.location().unwrap();
- Ok(self.func().call_vt(vt, [location])?.display())
- }))
- }
-}
-
-/// Provides access to active styles.
-///
-/// The styles are currently opaque and only useful in combination with the
-/// [`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 => {
-/// let size = measure(body, styles)
-/// [Width of "#body" is #size.width]
-/// })
-///
-/// #thing[Hey] \
-/// #thing[Welcome]
-/// ```
-#[func]
-pub fn style(
- /// A function to call with the styles. Its return value is displayed
- /// in the document.
- ///
- /// This function is called once for each time the content returned by
- /// `style` appears in the document. That makes it possible to generate
- /// content that depends on the style context it appears in.
- func: Func,
-) -> Content {
- StyleElem::new(func).pack()
-}
-
-/// Executes a style access.
-#[elem(Show)]
-struct StyleElem {
- /// The function to call with the styles.
- #[required]
- func: Func,
-}
-
-impl Show for StyleElem {
- #[tracing::instrument(name = "StyleElem::show", skip_all)]
- fn show(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Content> {
- Ok(self.func().call_vt(vt, [styles.to_map()])?.display())
- }
-}
-
-/// Provides access to the current outer container's (or page's, if none) size
-/// (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`]($length).
-///
-/// ```example
-/// #let text = lorem(30)
-/// #layout(size => style(styles => [
-/// #let (height,) = measure(
-/// block(width: size.width, text),
-/// styles,
-/// )
-/// This text is #height high with
-/// the current page width: \
-/// #text
-/// ]))
-/// ```
-///
-/// If the `layout` call is placed inside of a box width a width of `{800pt}`
-/// 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]($measure).
-///
-/// 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.
-///
-/// ```example
-/// #layout(size => {
-/// let half = 50% * size.width
-/// [Half a page is #half wide.]
-/// })
-/// ```
-///
-/// Note that this function will provide an infinite width or height if one of
-/// the page width or height is `auto`, respectively.
-#[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]($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
- /// content that depends on the size of the container it is inside of.
- func: Func,
-) -> Content {
- LayoutElem::new(func).pack()
-}
-
-/// Executes a `layout` call.
-#[elem(Layout)]
-struct LayoutElem {
- /// The function to call with the outer container's (or page's) size.
- #[required]
- func: Func,
-}
-
-impl Layout for LayoutElem {
- #[tracing::instrument(name = "LayoutElem::layout", skip_all)]
- fn layout(
- &self,
- vt: &mut Vt,
- styles: StyleChain,
- regions: Regions,
- ) -> SourceResult<Fragment> {
- // Gets the current region's base size, which will be the size of the
- // outer container, or of the page if there is no such container.
- let Size { x, y } = regions.base();
- let result = self
- .func()
- .call_vt(vt, [dict! { "width" => x, "height" => y }])?
- .display();
- result.layout(vt, styles, regions)
- }
-}
diff --git a/crates/typst-library/src/meta/counter.rs b/crates/typst-library/src/meta/counter.rs
deleted file mode 100644
index 0e733efb..00000000
--- a/crates/typst-library/src/meta/counter.rs
+++ /dev/null
@@ -1,722 +0,0 @@
-use std::str::FromStr;
-
-use ecow::{eco_vec, EcoVec};
-use smallvec::{smallvec, SmallVec};
-use typst::eval::{Repr, Tracer};
-use typst::model::DelayedErrors;
-
-use crate::layout::PageElem;
-use crate::math::EquationElem;
-use crate::meta::{FigureElem, HeadingElem, Numbering, NumberingPattern};
-use crate::prelude::*;
-
-/// Counts through pages, elements, and more.
-///
-/// With the counter function, you can access and modify counters for pages,
-/// headings, figures, and more. Moreover, you can define custom counters for
-/// other things you want to count.
-///
-/// # 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]($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]($numbering).
-///
-/// ```example
-/// #set heading(numbering: "1.")
-///
-/// = Introduction
-/// Some text here.
-///
-/// = Background
-/// The current value is:
-/// #counter(heading).display()
-///
-/// Or in roman numerals:
-/// #counter(heading).display("I")
-/// ```
-///
-/// # 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
-/// counters can have multiple levels (in the case of headings for sections,
-/// subsections, and so on), the `step` method optionally takes a `level`
-/// argument. If given, the counter steps at the given depth.
-///
-/// - The `update` method allows you to arbitrarily modify the counter. In its
-/// basic form, you give it an integer (or multiple for multiple levels). For
-/// more flexibility, you can instead also give it a function that gets the
-/// current value and returns a new value.
-///
-/// The heading counter is stepped before the heading is displayed, so
-/// `Analysis` gets the number seven even though the counter is at six after the
-/// second update.
-///
-/// ```example
-/// #set heading(numbering: "1.")
-///
-/// = Introduction
-/// #counter(heading).step()
-///
-/// = Background
-/// #counter(heading).update(3)
-/// #counter(heading).update(n => n * 2)
-///
-/// = Analysis
-/// Let's skip 7.1.
-/// #counter(heading).step(level: 2)
-///
-/// == Analysis
-/// Still at #counter(heading).display().
-/// ```
-///
-/// To define your own counter, call the `counter` function with a string as a
-/// key. This key identifies the counter globally.
-///
-/// ```example
-/// #let mine = counter("mycounter")
-/// #mine.display() \
-/// #mine.step()
-/// #mine.display() \
-/// #mine.update(c => c * 3)
-/// #mine.display() \
-/// ```
-///
-/// # 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
-/// counter for, let's say, theorems, your theorem's definition would thus first
-/// include the counter step and only then display the counter and the theorem's
-/// contents.
-///
-/// ```example
-/// #let c = counter("theorem")
-/// #let theorem(it) = block[
-/// #c.step()
-/// *Theorem #c.display():* #it
-/// ]
-///
-/// #theorem[$1 = 1$]
-/// #theorem[$2 < 3$]
-/// ```
-///
-/// The rationale behind this is best explained on the example of the heading
-/// counter: An update to the heading counter depends on the heading's level.
-/// By stepping directly before the heading, we can correctly step from `1` to
-/// `1.1` when encountering a level 2 heading. If we were to step after the
-/// heading, we wouldn't know what to step to.
-///
-/// Because counters should always be stepped before the elements they count,
-/// they always start at zero. This way, they are at one for the first display
-/// (which happens after the first step).
-///
-/// # 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
-/// numbers for your main content and reset the page counter to one.
-///
-/// ```example
-/// >>> #set page(
-/// >>> height: 100pt,
-/// >>> margin: (bottom: 24pt, rest: 16pt),
-/// >>> )
-/// #set page(numbering: "(i)")
-///
-/// = Preface
-/// The preface is numbered with
-/// roman numerals.
-///
-/// #set page(numbering: "1 / 1")
-/// #counter(page).update(1)
-///
-/// = Main text
-/// Here, the counter is reset to one.
-/// We also display both the current
-/// page and total number of pages in
-/// Arabic numbers.
-/// ```
-///
-/// # 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.
-///
-/// ```example
-/// #let mine = counter("mycounter")
-///
-/// = Values
-/// #locate(loc => {
-/// let start-val = mine.at(loc)
-/// let elements = query(<intro>, loc)
-/// let intro-val = mine.at(
-/// elements.first().location()
-/// )
-/// let final-val = mine.final(loc)
-/// [Starts as: #start-val \
-/// Value at intro is: #intro-val \
-/// Final value is: #final-val \ ]
-/// })
-///
-/// #mine.update(n => n + 3)
-///
-/// = Introduction <intro>
-/// #lorem(10)
-///
-/// #mine.step()
-/// #mine.step()
-/// ```
-///
-/// Let's dissect what happens in the example above:
-///
-/// - 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`]($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]($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,)}`.
-///
-/// - Last but not least, we call the `final` method on the counter. It tells us
-/// what the counter's value will be at the end of the document. We also need
-/// to give it a location to prove that we are inside of a `locate` call, but
-/// 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` 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(Debug, Clone, PartialEq, Hash)]
-pub struct Counter(CounterKey);
-
-impl Counter {
- /// 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: Element) -> Self {
- Self::construct(CounterKey::Selector(Selector::Elem(func, None)))
- }
-
- /// 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(&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();
- if self.is_page() {
- let at_delta =
- vt.introspector.page(location).get().saturating_sub(at_page.get());
- at_state.step(NonZeroUsize::ONE, at_delta);
- let final_delta =
- vt.introspector.pages().get().saturating_sub(final_page.get());
- final_state.step(NonZeroUsize::ONE, final_delta);
- }
- Ok(CounterState(smallvec![at_state.first(), final_state.first()]))
- }
-
- /// Produce 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.
- fn sequence(
- &self,
- vt: &mut Vt,
- ) -> SourceResult<EcoVec<(CounterState, NonZeroUsize)>> {
- self.sequence_impl(
- vt.world,
- vt.introspector,
- vt.locator.track(),
- TrackedMut::reborrow_mut(&mut vt.delayed),
- TrackedMut::reborrow_mut(&mut vt.tracer),
- )
- }
-
- /// Memoized implementation of `sequence`.
- #[comemo::memoize]
- fn sequence_impl(
- &self,
- world: Tracked<dyn World + '_>,
- introspector: Tracked<Introspector>,
- locator: Tracked<Locator>,
- delayed: TrackedMut<DelayedErrors>,
- tracer: TrackedMut<Tracer>,
- ) -> SourceResult<EcoVec<(CounterState, NonZeroUsize)>> {
- let mut locator = Locator::chained(locator);
- let mut vt = Vt {
- world,
- introspector,
- locator: &mut locator,
- delayed,
- tracer,
- };
-
- let mut state = CounterState::init(&self.0);
- let mut page = NonZeroUsize::ONE;
- let mut stops = eco_vec![(state.clone(), page)];
-
- for elem in introspector.query(&self.selector()) {
- if self.is_page() {
- let prev = page;
- page = introspector.page(elem.location().unwrap());
-
- let delta = page.get() - prev.get();
- if delta > 0 {
- state.step(NonZeroUsize::ONE, delta);
- }
- }
-
- if let Some(update) = match elem.to::<UpdateElem>() {
- Some(elem) => Some(elem.update().clone()),
- None => match elem.with::<dyn Count>() {
- Some(countable) => countable.update().clone(),
- None => Some(CounterUpdate::Step(NonZeroUsize::ONE)),
- },
- } {
- state.update(&mut vt, update)?;
- }
-
- stops.push((state.clone(), page));
- }
-
- Ok(stops)
- }
-
- /// The selector relevant for this counter's updates.
- fn selector(&self) -> Selector {
- let mut selector = select_where!(UpdateElem, Key => self.0.clone());
-
- if let CounterKey::Selector(key) = &self.0 {
- selector = Selector::Or(eco_vec![selector, key.clone()]);
- }
-
- selector
- }
-
- /// Whether this is the page counter.
- fn is_page(&self) -> bool {
- self.0 == CounterKey::Page
- }
-}
-
-#[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 Repr for Counter {
- fn repr(&self) -> EcoString {
- eco_format!("counter({})", self.0.repr())
- }
-}
-
-cast! {
- type Counter,
-}
-
-/// Identifies a counter.
-#[derive(Debug, Clone, PartialEq, Hash)]
-pub enum CounterKey {
- /// The page counter.
- Page,
- /// Counts elements matching the given selectors. Only works for locatable
- /// elements or labels.
- Selector(Selector),
- /// Counts through manual counters with the same key.
- Str(Str),
-}
-
-cast! {
- CounterKey,
- self => match self {
- 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: Element => {
- if v == PageElem::elem() {
- Self::Page
- } else {
- Self::Selector(LocatableSelector::from_value(v.into_value())?.0)
- }
- },
- v: LocatableSelector => Self::Selector(v.0),
-}
-
-impl Repr for CounterKey {
- fn repr(&self) -> EcoString {
- match self {
- Self::Page => "page".into(),
- Self::Selector(selector) => selector.repr(),
- Self::Str(str) => str.repr(),
- }
- }
-}
-
-/// An update to perform on a counter.
-#[ty]
-#[derive(Debug, Clone, PartialEq, Hash)]
-pub enum CounterUpdate {
- /// Set the counter to the specified state.
- Set(CounterState),
- /// Increase the number for the given level by one.
- Step(NonZeroUsize),
- /// Apply the given function to the counter's state.
- Func(Func),
-}
-
-impl Repr for CounterUpdate {
- fn repr(&self) -> EcoString {
- "..".into()
- }
-}
-
-cast! {
- type CounterUpdate,
- v: CounterState => Self::Set(v),
- v: Func => Self::Func(v),
-}
-
-/// Elements that have special counting behaviour.
-pub trait Count {
- /// Get the counter update for this element.
- fn update(&self) -> Option<CounterUpdate>;
-}
-
-/// Counts through elements with different levels.
-#[derive(Debug, Clone, PartialEq, Hash)]
-pub struct CounterState(pub SmallVec<[usize; 3]>);
-
-impl CounterState {
- /// Get the initial counter state for the key.
- pub fn init(key: &CounterKey) -> Self {
- Self(match key {
- // special case, because pages always start at one.
- CounterKey::Page => smallvec![1],
- _ => smallvec![0],
- })
- }
-
- /// Advance the counter and return the numbers for the given heading.
- pub fn update(&mut self, vt: &mut Vt, update: CounterUpdate) -> SourceResult<()> {
- match update {
- CounterUpdate::Set(state) => *self = state,
- CounterUpdate::Step(level) => self.step(level, 1),
- CounterUpdate::Func(func) => {
- *self = func.call_vt(vt, self.0.iter().copied())?.cast().at(func.span())?
- }
- }
- Ok(())
- }
-
- /// Advance the number of the given level by the specified amount.
- pub fn step(&mut self, level: NonZeroUsize, by: usize) {
- let level = level.get();
-
- if self.0.len() >= level {
- self.0[level - 1] = self.0[level - 1].saturating_add(by);
- self.0.truncate(level);
- }
-
- while self.0.len() < level {
- self.0.push(1);
- }
- }
-
- /// Get the first number of the state.
- pub fn first(&self) -> usize {
- self.0.first().copied().unwrap_or(1)
- }
-
- /// Display the counter state with a numbering.
- pub fn display(&self, vt: &mut Vt, numbering: &Numbering) -> SourceResult<Content> {
- Ok(numbering.apply_vt(vt, &self.0)?.display())
- }
-}
-
-cast! {
- CounterState,
- self => Value::Array(self.0.into_iter().map(IntoValue::into_value).collect()),
- num: usize => Self(smallvec![num]),
- array: Array => Self(array
- .into_iter()
- .map(Value::cast)
- .collect::<StrResult<_>>()?),
-}
-
-/// Executes a display of a state.
-#[elem(Locatable, Show)]
-struct DisplayElem {
- /// The counter.
- #[required]
- counter: Counter,
-
- /// The numbering to display the counter with.
- #[required]
- numbering: Option<Numbering>,
-
- /// Whether to display both the current and final value.
- #[required]
- both: bool,
-}
-
-impl Show for DisplayElem {
- #[tracing::instrument(name = "DisplayElem::show", skip_all)]
- fn show(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Content> {
- Ok(vt.delayed(|vt| {
- let location = self.location().unwrap();
- let counter = self.counter();
- let numbering = self
- .numbering()
- .clone()
- .or_else(|| {
- let CounterKey::Selector(Selector::Elem(func, _)) = counter.0 else {
- return None;
- };
-
- if func == HeadingElem::elem() {
- HeadingElem::numbering_in(styles).clone()
- } else if func == FigureElem::elem() {
- FigureElem::numbering_in(styles)
- } else if func == EquationElem::elem() {
- EquationElem::numbering_in(styles)
- } else {
- None
- }
- })
- .unwrap_or_else(|| NumberingPattern::from_str("1.1").unwrap().into());
-
- let state = if *self.both() {
- counter.both(vt, location)?
- } else {
- counter.at(vt, location)?
- };
-
- state.display(vt, &numbering)
- }))
- }
-}
-
-/// Executes an update of a counter.
-#[elem(Locatable, Show)]
-struct UpdateElem {
- /// The key that identifies the counter.
- #[required]
- key: CounterKey,
-
- /// The update to perform on the counter.
- #[required]
- update: CounterUpdate,
-}
-
-impl Show for UpdateElem {
- #[tracing::instrument(name = "UpdateElem::show", skip(self))]
- fn show(&self, _: &mut Vt, _: StyleChain) -> SourceResult<Content> {
- Ok(Content::empty())
- }
-}
-
-/// An specialized handler of the page counter that tracks both the physical
-/// and the logical page counter.
-#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
-pub struct ManualPageCounter {
- physical: NonZeroUsize,
- logical: usize,
-}
-
-impl ManualPageCounter {
- /// Create a new fast page counter, starting at 1.
- pub fn new() -> Self {
- Self { physical: NonZeroUsize::ONE, logical: 1 }
- }
-
- /// Get the current physical page counter state.
- pub fn physical(&self) -> NonZeroUsize {
- self.physical
- }
-
- /// Get the current logical page counter state.
- pub fn logical(&self) -> usize {
- self.logical
- }
-
- /// Advance past a page.
- pub fn visit(&mut self, vt: &mut Vt, page: &Frame) -> SourceResult<()> {
- for (_, item) in page.items() {
- match item {
- FrameItem::Group(group) => self.visit(vt, &group.frame)?,
- FrameItem::Meta(Meta::Elem(elem), _) => {
- let Some(elem) = elem.to::<UpdateElem>() else { continue };
- if *elem.key() == CounterKey::Page {
- let mut state = CounterState(smallvec![self.logical]);
- state.update(vt, elem.update().clone())?;
- self.logical = state.first();
- }
- }
- _ => {}
- }
- }
-
- Ok(())
- }
-
- /// Step past a page _boundary._
- pub fn step(&mut self) {
- self.physical = self.physical.saturating_add(1);
- self.logical += 1;
- }
-}
-
-impl Default for ManualPageCounter {
- fn default() -> Self {
- Self::new()
- }
-}
diff --git a/crates/typst-library/src/meta/document.rs b/crates/typst-library/src/meta/document.rs
deleted file mode 100644
index 581be60c..00000000
--- a/crates/typst-library/src/meta/document.rs
+++ /dev/null
@@ -1,124 +0,0 @@
-use comemo::Prehashed;
-use typst::eval::Datetime;
-
-use crate::layout::{LayoutRoot, PageElem};
-use crate::meta::ManualPageCounter;
-use crate::prelude::*;
-
-/// The root element of a document and its metadata.
-///
-/// All documents are automatically wrapped in a `document` element. You cannot
-/// create a document element yourself. This function is only used with
-/// [set rules]($styling/#set-rules) to specify document metadata. Such a set
-/// rule must appear before any of the document's contents.
-///
-/// ```example
-/// #set document(title: [Hello])
-///
-/// This has no visible output, but
-/// embeds metadata into the PDF!
-/// ```
-///
-/// Note that metadata set with this function is not rendered within the
-/// document. Instead, it is embedded in the compiled PDF file.
-#[elem(Construct, LayoutRoot)]
-pub struct DocumentElem {
- /// The document's title. This is often rendered as the title of the
- /// PDF viewer window.
- ///
- /// While this can be arbitrary content, PDF viewers only support plain text
- /// titles, so the conversion might be lossy.
- pub title: Option<Content>,
-
- /// The document's authors.
- pub author: Author,
-
- /// The document's keywords.
- pub keywords: Keywords,
-
- /// The document's creation date.
- ///
- /// If this is `{auto}` (default), Typst uses the current date and time.
- /// Setting it to `{none}` prevents Typst from embedding any creation date
- /// into the PDF metadata.
- ///
- /// The year component must be at least zero in order to be embedded into a
- /// PDF.
- pub date: Smart<Option<Datetime>>,
-
- /// The page runs.
- #[variadic]
- pub children: Vec<Prehashed<Content>>,
-}
-
-impl Construct for DocumentElem {
- fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> {
- bail!(args.span, "can only be used in set rules")
- }
-}
-
-impl LayoutRoot for DocumentElem {
- /// Layout the document into a sequence of frames, one per page.
- #[tracing::instrument(name = "DocumentElem::layout_root", skip_all)]
- fn layout_root(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Document> {
- tracing::info!("Document layout");
-
- let mut pages = vec![];
- let mut page_counter = ManualPageCounter::new();
-
- let children = self.children();
- let mut iter = children.iter().map(|c| &**c).peekable();
-
- while let Some(mut child) = iter.next() {
- let outer = styles;
- let mut styles = styles;
- if let Some((elem, local)) = child.to_styled() {
- styles = outer.chain(local);
- child = elem;
- }
-
- if let Some(page) = child.to::<PageElem>() {
- let extend_to = iter.peek().and_then(|&next| {
- next.to_styled()
- .map_or(next, |(elem, _)| elem)
- .to::<PageElem>()?
- .clear_to(styles)
- });
- let fragment = page.layout(vt, styles, &mut page_counter, extend_to)?;
- pages.extend(fragment);
- } else {
- bail!(child.span(), "unexpected document child");
- }
- }
-
- Ok(Document {
- pages,
- title: self.title(styles).map(|content| content.plain_text()),
- author: self.author(styles).0,
- keywords: self.keywords(styles).0,
- date: self.date(styles),
- })
- }
-}
-
-/// A list of authors.
-#[derive(Debug, Default, Clone, PartialEq, Hash)]
-pub struct Author(Vec<EcoString>);
-
-cast! {
- Author,
- self => self.0.into_value(),
- v: EcoString => Self(vec![v]),
- v: Array => Self(v.into_iter().map(Value::cast).collect::<StrResult<_>>()?),
-}
-
-/// A list of keywords.
-#[derive(Debug, Default, Clone, PartialEq, Hash)]
-pub struct Keywords(Vec<EcoString>);
-
-cast! {
- Keywords,
- self => self.0.into_value(),
- v: EcoString => Self(vec![v]),
- v: Array => Self(v.into_iter().map(Value::cast).collect::<StrResult<_>>()?),
-}
diff --git a/crates/typst-library/src/meta/figure.rs b/crates/typst-library/src/meta/figure.rs
deleted file mode 100644
index e96b076c..00000000
--- a/crates/typst-library/src/meta/figure.rs
+++ /dev/null
@@ -1,588 +0,0 @@
-use std::borrow::Cow;
-use std::str::FromStr;
-
-use crate::layout::{BlockElem, PlaceElem, VElem};
-use crate::meta::{
- Count, Counter, CounterKey, CounterUpdate, Numbering, NumberingPattern, Outlinable,
- Refable, Supplement,
-};
-use crate::prelude::*;
-use crate::text::TextElem;
-use crate::visualize::ImageElem;
-
-/// A figure with an optional caption.
-///
-/// Automatically detects its kind to select the correct counting track. For
-/// example, figures containing images will be numbered separately from figures
-/// containing tables.
-///
-/// # Examples
-/// The example below shows a basic figure with an image:
-/// ```example
-/// @glacier shows a glacier. Glaciers
-/// are complex systems.
-///
-/// #figure(
-/// image("glacier.jpg", width: 80%),
-/// caption: [A curious figure.],
-/// ) <glacier>
-/// ```
-///
-/// 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(
-/// table(
-/// columns: 4,
-/// [t], [1], [2], [3],
-/// [y], [0.3s], [0.4s], [0.8s],
-/// ),
-/// caption: [Timing results],
-/// )
-/// ```
-///
-/// This behaviour can be overridden by explicitly specifying the figure's
-/// `kind`. All figures of the same kind share a common counter.
-///
-/// # Figure behaviour
-/// By default, figures are placed within the flow of content. To make them
-/// float to the top or bottom of the page, you can use the
-/// [`placement`]($figure.placement) argument.
-///
-/// 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 itself
-/// breakable across pages as well with this show rule:
-/// ```typ
-/// #show figure: set block(breakable: true)
-/// ```
-///
-/// See the [block]($block.breakable) documentation for more information about
-/// breakable and non-breakable blocks.
-///
-/// # Caption customization
-/// You can modify the appearance of the figure's caption with its associated
-/// [`caption`]($figure.caption) function. In the example below, we emphasize
-/// all captions:
-///
-/// ```example
-/// #show figure.caption: emph
-///
-/// #figure(
-/// rect[Hello],
-/// caption: [I am emphasized!],
-/// )
-/// ```
-///
-/// By using a [`where`]($function.where) selector, we can scope such rules to
-/// specific kinds of figures. For example, to position the caption above
-/// tables, but keep it below for all other kinds of figures, we could write the
-/// following show-set rule:
-///
-/// ```example
-/// #show figure.where(
-/// kind: table
-/// ): set figure.caption(position: top)
-///
-/// #figure(
-/// table(columns: 2)[A][B][C][D],
-/// caption: [I'm up here],
-/// )
-/// ```
-#[elem(scope, Locatable, Synthesize, Count, Show, Finalize, Refable, Outlinable)]
-pub struct FigureElem {
- /// The content of the figure. Often, an [image]($image).
- #[required]
- pub body: Content,
-
- /// The figure's placement on the page.
- ///
- /// - `{none}`: The figure stays in-flow exactly where it was specified
- /// like other content.
- /// - `{auto}`: The figure picks `{top}` or `{bottom}` depending on which
- /// is closer.
- /// - `{top}`: The figure floats to the top of the page.
- /// - `{bottom}`: The figure floats to the bottom of the page.
- ///
- /// The gap between the main flow content and the floating figure is
- /// controlled by the [`clearance`]($place.clearance) argument on the
- /// `place` function.
- ///
- /// ```example
- /// #set page(height: 200pt)
- ///
- /// = Introduction
- /// #figure(
- /// placement: bottom,
- /// caption: [A glacier],
- /// image("glacier.jpg", width: 60%),
- /// )
- /// #lorem(60)
- /// ```
- pub placement: Option<Smart<VAlign>>,
-
- /// The figure's caption.
- pub caption: Option<FigureCaption>,
-
- /// The kind of figure this is.
- ///
- /// All figures of the same kind share a common counter.
- ///
- /// If set to `{auto}`, the figure will try to automatically determine its
- /// kind based on the type of its body. Automatically detected kinds are
- /// [tables]($table) and [code]($raw). In other cases, the inferred kind is
- /// that of an [image]($image).
- ///
- /// 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]($image), a [table]($table) or [code]($raw),
- /// - you want to force the figure to use a specific counter regardless of
- /// its content.
- ///
- /// You can set the kind to be an element function or a string. If you set
- /// it to an element function other than [`{table}`]($table), [`{raw}`](raw)
- /// or [`{image}`](image), you will need to manually specify the figure's
- /// supplement.
- ///
- /// ```example
- /// #figure(
- /// circle(radius: 10pt),
- /// caption: [A curious atom.],
- /// kind: "atom",
- /// supplement: [Atom],
- /// )
- /// ```
- #[default(Smart::Auto)]
- pub kind: Smart<FigureKind>,
-
- /// 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]($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
- /// specified `kind` (typically, the figure's body) and should return
- /// content.
- ///
- /// ```example
- /// #figure(
- /// [The contents of my figure!],
- /// caption: [My custom figure],
- /// supplement: [Bar],
- /// kind: "foo",
- /// )
- /// ```
- #[borrowed]
- pub supplement: Smart<Option<Supplement>>,
-
- /// How to number the figure. Accepts a
- /// [numbering pattern or function]($numbering).
- #[default(Some(NumberingPattern::from_str("1").unwrap().into()))]
- pub numbering: Option<Numbering>,
-
- /// The vertical gap between the body and caption.
- #[default(Em::new(0.65).into())]
- pub gap: Length,
-
- /// 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)[@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
- /// number or reset the counter.
- #[synthesized]
- pub counter: Option<Counter>,
-}
-
-#[scope]
-impl FigureElem {
- #[elem]
- type FigureCaption;
-}
-
-impl Synthesize for FigureElem {
- fn synthesize(&mut self, vt: &mut Vt, styles: StyleChain) -> SourceResult<()> {
- let numbering = self.numbering(styles);
-
- // Determine the figure's kind.
- let kind = self.kind(styles).unwrap_or_else(|| {
- self.body()
- .query_first(Selector::can::<dyn Figurable>())
- .map(|elem| FigureKind::Elem(elem.func()))
- .unwrap_or_else(|| FigureKind::Elem(ImageElem::elem()))
- });
-
- // Resolve the supplement.
- let supplement = match self.supplement(styles).as_ref() {
- Smart::Auto => {
- // Default to the local name for the kind, if available.
- let name = match &kind {
- FigureKind::Elem(func) => func
- .local_name(
- TextElem::lang_in(styles),
- TextElem::region_in(styles),
- )
- .map(TextElem::packed),
- FigureKind::Name(_) => None,
- };
-
- if numbering.is_some() && name.is_none() {
- bail!(self.span(), "please specify the figure's supplement")
- }
-
- Some(name.unwrap_or_default())
- }
- Smart::Custom(None) => None,
- Smart::Custom(Some(supplement)) => {
- // Resolve the supplement with the first descendant of the kind or
- // just the body, if none was found.
- let descendant = match kind {
- FigureKind::Elem(func) => self
- .body()
- .query_first(Selector::Elem(func, None))
- .map(Cow::Owned),
- FigureKind::Name(_) => None,
- };
-
- let target = descendant.unwrap_or_else(|| Cow::Borrowed(self.body()));
- Some(supplement.resolve(vt, [target])?)
- }
- };
-
- // Construct the figure's counter.
- let counter =
- Counter::new(CounterKey::Selector(select_where!(Self, Kind => kind.clone())));
-
- // Fill the figure's caption.
- let mut caption = self.caption(styles);
- if let Some(caption) = &mut caption {
- caption.push_kind(kind.clone());
- caption.push_supplement(supplement.clone());
- caption.push_numbering(numbering.clone());
- caption.push_counter(Some(counter.clone()));
- caption.push_figure_location(self.location());
- }
-
- self.push_placement(self.placement(styles));
- self.push_caption(caption);
- self.push_kind(Smart::Custom(kind));
- self.push_supplement(Smart::Custom(supplement.map(Supplement::Content)));
- self.push_numbering(numbering);
- self.push_outlined(self.outlined(styles));
- self.push_counter(Some(counter));
-
- Ok(())
- }
-}
-
-impl Show for FigureElem {
- #[tracing::instrument(name = "FigureElem::show", skip_all)]
- fn show(&self, _: &mut Vt, styles: StyleChain) -> SourceResult<Content> {
- let mut realized = self.body().clone();
-
- // Build the caption, if any.
- if let Some(caption) = self.caption(styles) {
- let v = VElem::weak(self.gap(styles).into()).pack();
- realized = if caption.position(styles) == VAlign::Bottom {
- realized + v + caption.pack()
- } else {
- caption.pack() + v + realized
- };
- }
-
- // Wrap the contents in a block.
- realized = BlockElem::new()
- .with_body(Some(realized))
- .pack()
- .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(|align| HAlign::Center + align))
- .pack();
- }
-
- Ok(realized)
- }
-}
-
-impl Finalize for FigureElem {
- fn finalize(&self, realized: Content, _: StyleChain) -> Content {
- // Allow breakable figures with `show figure: set block(breakable: true)`.
- realized.styled(BlockElem::set_breakable(false))
- }
-}
-
-impl Count for FigureElem {
- fn update(&self) -> Option<CounterUpdate> {
- // If the figure is numbered, step the counter by one.
- // This steps the `counter(figure)` which is global to all numbered figures.
- self.numbering(StyleChain::default())
- .is_some()
- .then(|| CounterUpdate::Step(NonZeroUsize::ONE))
- }
-}
-
-impl Refable for FigureElem {
- fn supplement(&self) -> Content {
- // After synthesis, this should always be custom content.
- let default = StyleChain::default();
- match self.supplement(default).as_ref() {
- Smart::Custom(Some(Supplement::Content(content))) => content.clone(),
- _ => Content::empty(),
- }
- }
-
- fn counter(&self) -> Counter {
- self.counter().clone().unwrap_or_else(|| Counter::of(Self::elem()))
- }
-
- fn numbering(&self) -> Option<Numbering> {
- self.numbering(StyleChain::default())
- }
-}
-
-impl Outlinable for FigureElem {
- fn outline(&self, vt: &mut Vt) -> SourceResult<Option<Content>> {
- if !self.outlined(StyleChain::default()) {
- return Ok(None);
- }
-
- let Some(caption) = self.caption(StyleChain::default()) else {
- return Ok(None);
- };
-
- let mut realized = caption.body().clone();
- if let (
- Smart::Custom(Some(Supplement::Content(mut supplement))),
- Some(counter),
- Some(numbering),
- ) = (
- self.supplement(StyleChain::default()).clone(),
- self.counter(),
- self.numbering(StyleChain::default()),
- ) {
- let location = self.location().unwrap();
- let numbers = counter.at(vt, location)?.display(vt, &numbering)?;
-
- if !supplement.is_empty() {
- supplement += TextElem::packed('\u{a0}');
- }
-
- let separator = caption.get_separator(StyleChain::default());
-
- realized = supplement + numbers + separator + caption.body();
- }
-
- Ok(Some(realized))
- }
-}
-
-/// The caption of a figure. This element can be used in set and show rules to
-/// customize the appearance of captions for all figures or figures of a
-/// specific kind.
-///
-/// In addition to its `pos` and `body`, the `caption` also provides the
-/// figure's `kind`, `supplement`, `counter`, `numbering`, and `location` as
-/// fields. These parts can be used in [`where`]($function.where) selectors and
-/// show rules to build a completely custom caption.
-///
-/// ```example
-/// #show figure.caption: emph
-///
-/// #figure(
-/// rect[Hello],
-/// caption: [A rectangle],
-/// )
-/// ```
-#[elem(name = "caption", Synthesize, Show)]
-pub struct FigureCaption {
- /// The caption's position in the figure. Either `{top}` or `{bottom}`.
- ///
- /// ```example
- /// #show figure.where(
- /// kind: table
- /// ): set figure.caption(position: top)
- ///
- /// #figure(
- /// table(columns: 2)[A][B],
- /// caption: [I'm up here],
- /// )
- ///
- /// #figure(
- /// rect[Hi],
- /// caption: [I'm down here],
- /// )
- ///
- /// #figure(
- /// table(columns: 2)[A][B],
- /// caption: figure.caption(
- /// position: bottom,
- /// [I'm down here too!]
- /// )
- /// )
- /// ```
- #[default(VAlign::Bottom)]
- #[parse({
- let option: Option<Spanned<VAlign>> = args.named("position")?;
- if let Some(Spanned { v: align, span }) = option {
- if align == VAlign::Horizon {
- bail!(span, "expected `top` or `bottom`");
- }
- }
- option.map(|spanned| spanned.v)
- })]
- pub position: VAlign,
-
- /// The separator which will appear between the number and body.
- ///
- /// ```example
- /// #set figure.caption(separator: [ --- ])
- ///
- /// #figure(
- /// rect[Hello],
- /// caption: [A rectangle],
- /// )
- /// ```
- ///
- /// If set to `{auto}`, the separator will be adapted to the current
- /// [language]($text.lang) and [region]($text.region).
- pub separator: Smart<Content>,
-
- /// The caption's body.
- ///
- /// Can be used alongside `kind`, `supplement`, `counter`, `numbering`, and
- /// `location` to completely customize the caption.
- ///
- /// ```example
- /// #show figure.caption: it => [
- /// #underline(it.body) |
- /// #it.supplement #it.counter.display(it.numbering)
- /// ]
- ///
- /// #figure(
- /// rect[Hello],
- /// caption: [A rectangle],
- /// )
- /// ```
- #[required]
- pub body: Content,
-
- /// The figure's supplement.
- #[synthesized]
- pub kind: FigureKind,
-
- /// The figure's supplement.
- #[synthesized]
- #[default(None)]
- pub supplement: Option<Content>,
-
- /// How to number the figure.
- #[synthesized]
- #[default(None)]
- pub numbering: Option<Numbering>,
-
- /// The counter for the figure.
- #[synthesized]
- #[default(None)]
- pub counter: Option<Counter>,
-
- /// The figure's location.
- #[internal]
- #[synthesized]
- #[default(None)]
- pub figure_location: Option<Location>,
-}
-
-impl FigureCaption {
- /// Gets the default separator in the given language and (optionally)
- /// region.
- fn local_separator(lang: Lang, _: Option<Region>) -> &'static str {
- match lang {
- Lang::CHINESE => "\u{2003}",
- Lang::FRENCH => ".\u{a0}– ",
- Lang::RUSSIAN => ". ",
- Lang::ENGLISH | _ => ": ",
- }
- }
-
- fn get_separator(&self, styles: StyleChain) -> Content {
- self.separator(styles).unwrap_or_else(|| {
- TextElem::packed(Self::local_separator(
- TextElem::lang_in(styles),
- TextElem::region_in(styles),
- ))
- })
- }
-}
-
-impl Synthesize for FigureCaption {
- fn synthesize(&mut self, _: &mut Vt, styles: StyleChain) -> SourceResult<()> {
- self.push_position(self.position(styles));
- self.push_separator(Smart::Custom(self.get_separator(styles)));
- Ok(())
- }
-}
-
-impl Show for FigureCaption {
- #[tracing::instrument(name = "FigureCaption::show", skip_all)]
- fn show(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Content> {
- let mut realized = self.body().clone();
-
- if let (Some(mut supplement), Some(numbering), Some(counter), Some(location)) = (
- self.supplement().clone(),
- self.numbering(),
- self.counter(),
- self.figure_location(),
- ) {
- let numbers = counter.at(vt, *location)?.display(vt, numbering)?;
- if !supplement.is_empty() {
- supplement += TextElem::packed('\u{a0}');
- }
- realized = supplement + numbers + self.get_separator(styles) + realized;
- }
-
- Ok(realized)
- }
-}
-
-cast! {
- FigureCaption,
- v: Content => v.to::<Self>().cloned().unwrap_or_else(|| Self::new(v.clone())),
-}
-
-/// The `kind` parameter of a [`FigureElem`].
-#[derive(Debug, Clone, PartialEq, Hash)]
-pub enum FigureKind {
- /// The kind is an element function.
- Elem(Element),
- /// The kind is a name.
- Name(EcoString),
-}
-
-cast! {
- FigureKind,
- self => match self {
- Self::Elem(v) => v.into_value(),
- Self::Name(v) => v.into_value(),
- },
- v: Element => Self::Elem(v),
- v: EcoString => Self::Name(v),
-}
-
-/// An element that can be auto-detected in a figure.
-///
-/// This trait is used to determine the type of a figure.
-pub trait Figurable {}
diff --git a/crates/typst-library/src/meta/footnote.rs b/crates/typst-library/src/meta/footnote.rs
deleted file mode 100644
index 189dbcb8..00000000
--- a/crates/typst-library/src/meta/footnote.rs
+++ /dev/null
@@ -1,305 +0,0 @@
-use comemo::Prehashed;
-use std::str::FromStr;
-
-use crate::layout::{HElem, ParElem};
-use crate::meta::{Count, Counter, CounterUpdate, Numbering, NumberingPattern};
-use crate::prelude::*;
-use crate::text::{SuperElem, TextElem, TextSize};
-use crate::visualize::LineElem;
-
-/// A footnote.
-///
-/// Includes additional remarks and references on the same page with footnotes.
-/// A footnote will insert a superscript number that links to the note at the
-/// bottom of the page. Notes are numbered sequentially throughout your document
-/// and can break across multiple pages.
-///
-/// To customize the appearance of the entry in the footnote listing, see
-/// [`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
-/// Check the docs for more details.
-/// #footnote[https://typst.app/docs]
-/// ```
-///
-/// 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]($h).
-///
-/// By giving a label to a footnote, you can have multiple references to it.
-///
-/// ```example
-/// You can edit Typst documents online.
-/// #footnote[https://typst.app/app] <fn>
-/// Checkout Typst's website. @fn
-/// And the online app. #footnote(<fn>)
-/// ```
-///
-/// _Note:_ Set and show rules in the scope where `footnote` is called may not
-/// apply to the footnote's content. See [here][issue] for more information.
-///
-/// [issue]: https://github.com/typst/typst/issues/1467#issuecomment-1588799440
-#[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]($counter) in the page [header]($page.header). In the future,
- /// there might be a simpler way to achieve this.
- ///
- /// ```example
- /// #set footnote(numbering: "*")
- ///
- /// Footnotes:
- /// #footnote[Star],
- /// #footnote[Dagger]
- /// ```
- #[borrowed]
- #[default(Numbering::Pattern(NumberingPattern::from_str("1").unwrap()))]
- pub numbering: Numbering,
-
- /// The content to put into the footnote. Can also be the label of another
- /// footnote this one should point to.
- #[required]
- 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 {
- Self::new(FootnoteBody::Content(content))
- }
-
- /// Creates a new footnote referencing the footnote with the specified label.
- pub fn with_label(label: Label) -> Self {
- Self::new(FootnoteBody::Reference(label))
- }
-
- /// Tests if this footnote is a reference to another footnote.
- pub fn is_ref(&self) -> bool {
- matches!(self.body(), FootnoteBody::Reference(_))
- }
-
- /// Returns the content of the body of this footnote if it is not a ref.
- pub fn body_content(&self) -> Option<&Content> {
- match self.body() {
- FootnoteBody::Content(content) => Some(content),
- _ => None,
- }
- }
-
- /// Returns the location of the definition of this footnote.
- pub fn declaration_location(&self, vt: &Vt) -> StrResult<Location> {
- match self.body() {
- FootnoteBody::Reference(label) => {
- let element: Prehashed<Content> = vt.introspector.query_label(*label)?;
- let footnote = element
- .to::<FootnoteElem>()
- .ok_or("referenced element should be a footnote")?;
- footnote.declaration_location(vt)
- }
- _ => Ok(self.location().unwrap()),
- }
- }
-}
-
-impl Synthesize for FootnoteElem {
- fn synthesize(&mut self, _vt: &mut Vt, styles: StyleChain) -> SourceResult<()> {
- self.push_numbering(self.numbering(styles).clone());
- Ok(())
- }
-}
-
-impl Show for FootnoteElem {
- #[tracing::instrument(name = "FootnoteElem::show", skip_all)]
- fn show(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Content> {
- Ok(vt.delayed(|vt| {
- let loc = self.declaration_location(vt).at(self.span())?;
- let numbering = self.numbering(styles);
- let counter = Counter::of(Self::elem());
- let num = counter.at(vt, loc)?.display(vt, numbering)?;
- let sup = SuperElem::new(num).pack();
- let loc = loc.variant(1);
- // Add zero-width weak spacing to make the footnote "sticky".
- Ok(HElem::hole().pack() + sup.linked(Destination::Location(loc)))
- }))
- }
-}
-
-impl Count for FootnoteElem {
- fn update(&self) -> Option<CounterUpdate> {
- (!self.is_ref()).then(|| CounterUpdate::Step(NonZeroUsize::ONE))
- }
-}
-
-/// The body of a footnote can be either some content or a label referencing
-/// another footnote.
-#[derive(Debug, Clone, PartialEq, Hash)]
-pub enum FootnoteBody {
- Content(Content),
- Reference(Label),
-}
-
-cast! {
- FootnoteBody,
- self => match self {
- Self::Content(v) => v.into_value(),
- Self::Reference(v) => v.into_value(),
- },
- v: Content => Self::Content(v),
- v: Label => Self::Reference(v),
-}
-
-/// An entry in a footnote list.
-///
-/// This function is not intended to be called directly. Instead, it is used
-/// in set and show rules to customize footnote listings.
-///
-/// _Note:_ Set and show rules for `footnote.entry` must be defined at the
-/// beginning of the document in order to work correctly.
-/// See [here](https://github.com/typst/typst/issues/1348#issuecomment-1566316463)
-/// for more information.
-///
-/// ```example
-/// #show footnote.entry: set text(red)
-///
-/// My footnote listing
-/// #footnote[It's down here]
-/// has red text!
-/// ```
-#[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.
- ///
- /// ```example
- /// #show footnote.entry: it => {
- /// let loc = it.note.location()
- /// numbering(
- /// "1: ",
- /// ..counter(footnote).at(loc),
- /// )
- /// it.note.body
- /// }
- ///
- /// Customized #footnote[Hello]
- /// listing #footnote[World! 🌏]
- /// ```
- #[required]
- pub note: FootnoteElem,
-
- /// The separator between the document body and the footnote listing.
- ///
- /// ```example
- /// #set footnote.entry(
- /// separator: repeat[.]
- /// )
- ///
- /// Testing a different separator.
- /// #footnote[
- /// Unconventional, but maybe
- /// not that bad?
- /// ]
- /// ```
- #[default(
- LineElem::new()
- .with_length(Ratio::new(0.3).into())
- .with_stroke(Stroke {
- thickness: Smart::Custom(Abs::pt(0.5).into()),
- ..Default::default()
- })
- .pack()
- )]
- pub separator: Content,
-
- /// The amount of clearance between the document body and the separator.
- ///
- /// ```example
- /// #set footnote.entry(clearance: 3em)
- ///
- /// Footnotes also need ...
- /// #footnote[
- /// ... some space to breathe.
- /// ]
- /// ```
- #[default(Em::new(1.0).into())]
- #[resolve]
- pub clearance: Length,
-
- /// The gap between footnote entries.
- ///
- /// ```example
- /// #set footnote.entry(gap: 0.8em)
- ///
- /// Footnotes:
- /// #footnote[Spaced],
- /// #footnote[Apart]
- /// ```
- #[default(Em::new(0.5).into())]
- #[resolve]
- pub gap: Length,
-
- /// The indent of each footnote entry.
- ///
- /// ```example
- /// #set footnote.entry(indent: 0em)
- ///
- /// Footnotes:
- /// #footnote[No],
- /// #footnote[Indent]
- /// ```
- #[default(Em::new(1.0).into())]
- pub indent: Length,
-}
-
-impl Show for FootnoteEntry {
- fn show(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Content> {
- let note = self.note();
- let number_gap = Em::new(0.05);
- let default = StyleChain::default();
- let numbering = note.numbering(default);
- let counter = Counter::of(FootnoteElem::elem());
- let Some(loc) = note.location() else {
- bail!(error!(self.span(), "footnote entry must have a location").with_hint(
- "try using a query or a show rule to customize the footnote instead"
- ))
- };
-
- let num = counter.at(vt, loc)?.display(vt, numbering)?;
- let sup = SuperElem::new(num)
- .pack()
- .linked(Destination::Location(loc))
- .backlinked(loc.variant(1));
- Ok(Content::sequence([
- HElem::new(self.indent(styles).into()).pack(),
- sup,
- HElem::new(number_gap.into()).with_weak(true).pack(),
- note.body_content().unwrap().clone(),
- ]))
- }
-}
-
-impl Finalize for FootnoteEntry {
- fn finalize(&self, realized: Content, _: StyleChain) -> Content {
- let text_size = Em::new(0.85);
- let leading = Em::new(0.5);
- realized
- .styled(ParElem::set_leading(leading.into()))
- .styled(TextElem::set_size(TextSize(text_size.into())))
- }
-}
-
-cast! {
- FootnoteElem,
- v: Content => v.to::<Self>().cloned().unwrap_or_else(|| Self::with_content(v.clone())),
-}
diff --git a/crates/typst-library/src/meta/heading.rs b/crates/typst-library/src/meta/heading.rs
deleted file mode 100644
index 0f814dc9..00000000
--- a/crates/typst-library/src/meta/heading.rs
+++ /dev/null
@@ -1,269 +0,0 @@
-use typst::font::FontWeight;
-use typst::util::option_eq;
-
-use super::{Counter, CounterUpdate, LocalName, Numbering, Outlinable, Refable};
-use crate::layout::{BlockElem, HElem, VElem};
-use crate::meta::{Count, LocalNameIn, Supplement};
-use crate::prelude::*;
-use crate::text::{SpaceElem, TextElem, TextSize};
-
-/// A section heading.
-///
-/// With headings, you can structure your document into sections. Each heading
-/// has a _level,_ which starts at one and is unbounded upwards. This level
-/// indicates the logical role of the following content (section, subsection,
-/// etc.) A top-level heading indicates a top-level section of the document
-/// (not the document's title).
-///
-/// 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]($numbering).
-///
-/// Independently from the numbering, Typst can also automatically generate an
-/// [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
-/// #set heading(numbering: "1.a)")
-///
-/// = Introduction
-/// In recent years, ...
-///
-/// == Preliminaries
-/// To start, ...
-/// ```
-///
-/// # 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.
-#[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]($numbering).
- ///
- /// ```example
- /// #set heading(numbering: "1.a.")
- ///
- /// = A section
- /// == A subsection
- /// === A sub-subsection
- /// ```
- #[borrowed]
- pub numbering: Option<Numbering>,
-
- /// A supplement for the heading.
- ///
- /// For references to headings, this is added before the referenced number.
- ///
- /// If a function is specified, it is passed the referenced heading and
- /// should return content.
- ///
- /// ```example
- /// #set heading(numbering: "1.", supplement: [Chapter])
- ///
- /// = Introduction <intro>
- /// In @intro, we see how to turn
- /// Sections into Chapters. And
- /// in @intro[Part], it is done
- /// manually.
- /// ```
- pub supplement: Smart<Option<Supplement>>,
-
- /// 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.
- ///
- /// ```example
- /// #outline()
- ///
- /// #heading[Normal]
- /// This is a normal heading.
- ///
- /// #heading(outlined: false)[Hidden]
- /// This heading does not appear
- /// in the outline.
- /// ```
- #[default(true)]
- pub outlined: bool,
-
- /// Whether the heading should appear as a bookmark in the exported PDF's
- /// outline. Doesn't affect other export formats, such as PNG.
- ///
- /// 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]($outline). Setting this property to either `{true}` (bookmark)
- /// or `{false}` (don't bookmark) bypasses that behavior.
- ///
- /// ```example
- /// #heading[Normal heading]
- /// This heading will be shown in
- /// the PDF's bookmark outline.
- ///
- /// #heading(bookmarked: false)[Not bookmarked]
- /// This heading won't be
- /// bookmarked in the resulting
- /// PDF.
- /// ```
- #[default(Smart::Auto)]
- pub bookmarked: Smart<bool>,
-
- /// The heading's title.
- #[required]
- pub body: Content,
-}
-
-impl Synthesize for HeadingElem {
- fn synthesize(&mut self, vt: &mut Vt, styles: StyleChain) -> SourceResult<()> {
- // Resolve the supplement.
- let supplement = match self.supplement(styles) {
- Smart::Auto => TextElem::packed(Self::local_name_in(styles)),
- Smart::Custom(None) => Content::empty(),
- Smart::Custom(Some(supplement)) => supplement.resolve(vt, [self.clone()])?,
- };
-
- self.push_level(self.level(styles));
- self.push_numbering(self.numbering(styles).clone());
- self.push_supplement(Smart::Custom(Some(Supplement::Content(supplement))));
- self.push_outlined(self.outlined(styles));
- self.push_bookmarked(self.bookmarked(styles));
-
- Ok(())
- }
-}
-
-impl Show for HeadingElem {
- #[tracing::instrument(name = "HeadingElem::show", skip_all)]
- fn show(&self, _: &mut Vt, styles: StyleChain) -> SourceResult<Content> {
- let mut realized = self.body().clone();
- if let Some(numbering) = self.numbering(styles).as_ref() {
- realized = Counter::of(Self::elem())
- .display(Some(numbering.clone()), false)
- .spanned(self.span())
- + HElem::new(Em::new(0.3).into()).with_weak(true).pack()
- + realized;
- }
- Ok(BlockElem::new().with_body(Some(realized)).pack())
- }
-}
-
-impl Finalize for HeadingElem {
- fn finalize(&self, realized: Content, styles: StyleChain) -> Content {
- let level = self.level(styles).get();
- let scale = match level {
- 1 => 1.4,
- 2 => 1.2,
- _ => 1.0,
- };
-
- let size = Em::new(scale);
- let above = Em::new(if level == 1 { 1.8 } else { 1.44 }) / scale;
- let below = Em::new(0.75) / scale;
-
- let mut styles = Styles::new();
- styles.set(TextElem::set_size(TextSize(size.into())));
- styles.set(TextElem::set_weight(FontWeight::BOLD));
- styles.set(BlockElem::set_above(VElem::block_around(above.into())));
- styles.set(BlockElem::set_below(VElem::block_around(below.into())));
- styles.set(BlockElem::set_sticky(true));
- realized.styled_with_map(styles)
- }
-}
-
-impl Count for HeadingElem {
- fn update(&self) -> Option<CounterUpdate> {
- self.numbering(StyleChain::default())
- .is_some()
- .then(|| CounterUpdate::Step(self.level(StyleChain::default())))
- }
-}
-
-cast! {
- HeadingElem,
- v: Content => v.to::<Self>().ok_or("expected heading")?.clone(),
-}
-
-impl Refable for HeadingElem {
- fn supplement(&self) -> Content {
- // After synthesis, this should always be custom content.
- match self.supplement(StyleChain::default()) {
- Smart::Custom(Some(Supplement::Content(content))) => content,
- _ => Content::empty(),
- }
- }
-
- fn counter(&self) -> Counter {
- Counter::of(Self::elem())
- }
-
- fn numbering(&self) -> Option<Numbering> {
- self.numbering(StyleChain::default()).clone()
- }
-}
-
-impl Outlinable for HeadingElem {
- fn outline(&self, vt: &mut Vt) -> SourceResult<Option<Content>> {
- if !self.outlined(StyleChain::default()) {
- return Ok(None);
- }
-
- let mut content = self.body().clone();
- let default = StyleChain::default();
- if let Some(numbering) = self.numbering(default).as_ref() {
- let numbers = Counter::of(Self::elem())
- .at(vt, self.location().unwrap())?
- .display(vt, numbering)?;
- content = numbers + SpaceElem::new().pack() + content;
- };
-
- Ok(Some(content))
- }
-
- fn level(&self) -> NonZeroUsize {
- self.level(StyleChain::default())
- }
-}
-
-impl LocalName for HeadingElem {
- fn local_name(lang: Lang, region: Option<Region>) -> &'static str {
- match lang {
- Lang::ALBANIAN => "Kapitull",
- Lang::ARABIC => "الفصل",
- Lang::BOKMÅL => "Kapittel",
- Lang::CHINESE if option_eq(region, "TW") => "小節",
- Lang::CHINESE => "小节",
- Lang::CZECH => "Kapitola",
- Lang::DANISH => "Afsnit",
- Lang::DUTCH => "Hoofdstuk",
- Lang::FILIPINO => "Seksyon",
- Lang::FINNISH => "Osio",
- Lang::FRENCH => "Chapitre",
- Lang::GERMAN => "Abschnitt",
- Lang::GREEK => "Κεφάλαιο",
- Lang::HUNGARIAN => "Fejezet",
- Lang::ITALIAN => "Sezione",
- Lang::NYNORSK => "Kapittel",
- Lang::POLISH => "Sekcja",
- Lang::PORTUGUESE if option_eq(region, "PT") => "Secção",
- Lang::PORTUGUESE => "Seção",
- Lang::ROMANIAN => "Secțiunea",
- Lang::RUSSIAN => "Раздел",
- Lang::SLOVENIAN => "Poglavje",
- Lang::SPANISH => "Sección",
- Lang::SWEDISH => "Kapitel",
- Lang::TURKISH => "Bölüm",
- Lang::UKRAINIAN => "Розділ",
- Lang::VIETNAMESE => "Phần", // TODO: This may be wrong.
- Lang::JAPANESE => "節",
- Lang::ENGLISH | _ => "Section",
- }
- }
-}
diff --git a/crates/typst-library/src/meta/link.rs b/crates/typst-library/src/meta/link.rs
deleted file mode 100644
index 74857f1b..00000000
--- a/crates/typst-library/src/meta/link.rs
+++ /dev/null
@@ -1,133 +0,0 @@
-use crate::prelude::*;
-use crate::text::{Hyphenate, TextElem};
-
-/// Links to a URL or a location in the document.
-///
-/// 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
-/// #show link: underline
-///
-/// https://example.com \
-///
-/// #link("https://example.com") \
-/// #link("https://example.com")[
-/// See example.com
-/// ]
-/// ```
-///
-/// # Syntax
-/// This function also has dedicated syntax: Text that starts with `http://` or
-/// `https://` is automatically turned into a link.
-#[elem(Show)]
-pub struct LinkElem {
- /// The destination the link points to.
- ///
- /// - To link to web pages, `dest` should be a valid URL string. If the URL
- /// is in the `mailto:` or `tel:` scheme and the `body` parameter is
- /// omitted, the email address or phone number will be the link's body,
- /// without the scheme.
- ///
- /// - To link to another part of the document, `dest` can take one of three
- /// forms:
- /// - 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]($locate) resulting from a [`locate`]($locate) call or
- /// [`query`]($query).
- ///
- /// - 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>
- /// #link("mailto:hello@typst.app") \
- /// #link(<intro>)[Go to intro] \
- /// #link((page: 1, x: 0pt, y: 0pt))[
- /// Go to top
- /// ]
- /// ```
- #[required]
- #[parse(
- let dest = args.expect::<LinkTarget>("destination")?;
- dest.clone()
- )]
- pub dest: LinkTarget,
-
- /// The content that should become a link.
- ///
- /// If `dest` is an URL string, the parameter can be omitted. In this case,
- /// the URL will be shown as the link.
- #[required]
- #[parse(match &dest {
- LinkTarget::Dest(Destination::Url(url)) => match args.eat()? {
- Some(body) => body,
- None => body_from_url(url),
- },
- _ => args.expect("body")?,
- })]
- pub body: Content,
-}
-
-impl LinkElem {
- /// Create a link element from a URL with its bare text.
- pub fn from_url(url: EcoString) -> Self {
- let body = body_from_url(&url);
- Self::new(LinkTarget::Dest(Destination::Url(url)), body)
- }
-}
-
-impl Show for LinkElem {
- #[tracing::instrument(name = "LinkElem::show", skip(self, vt))]
- fn show(&self, vt: &mut Vt, _: StyleChain) -> SourceResult<Content> {
- let body = self.body().clone();
- let linked = match self.dest() {
- LinkTarget::Dest(dest) => body.linked(dest.clone()),
- LinkTarget::Label(label) => vt
- .delayed(|vt| {
- let elem = vt.introspector.query_label(*label).at(self.span())?;
- let dest = Destination::Location(elem.location().unwrap());
- Ok(Some(body.clone().linked(dest)))
- })
- .unwrap_or(body),
- };
-
- Ok(linked.styled(TextElem::set_hyphenate(Hyphenate(Smart::Custom(false)))))
- }
-}
-
-fn body_from_url(url: &EcoString) -> Content {
- let mut text = url.as_str();
- for prefix in ["mailto:", "tel:"] {
- text = text.trim_start_matches(prefix);
- }
- let shorter = text.len() < url.len();
- TextElem::packed(if shorter { text.into() } else { url.clone() })
-}
-
-/// A target where a link can go.
-#[derive(Debug, Clone, PartialEq, Hash)]
-pub enum LinkTarget {
- Dest(Destination),
- Label(Label),
-}
-
-cast! {
- LinkTarget,
- self => match self {
- Self::Dest(v) => v.into_value(),
- Self::Label(v) => v.into_value(),
- },
- v: Destination => Self::Dest(v),
- v: Label => Self::Label(v),
-}
-
-impl From<Destination> for LinkTarget {
- fn from(dest: Destination) -> Self {
- Self::Dest(dest)
- }
-}
diff --git a/crates/typst-library/src/meta/metadata.rs b/crates/typst-library/src/meta/metadata.rs
deleted file mode 100644
index b4ae64cb..00000000
--- a/crates/typst-library/src/meta/metadata.rs
+++ /dev/null
@@ -1,40 +0,0 @@
-use crate::prelude::*;
-
-/// Exposes a value to the query system without producing visible content.
-///
-/// 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.
-///
-/// ```example
-/// // Put metadata somewhere.
-/// #metadata("This is a note") <note>
-///
-/// // And find it from anywhere else.
-/// #locate(loc => {
-/// query(<note>, loc).first().value
-/// })
-/// ```
-#[elem(Behave, Show, Locatable)]
-pub struct MetadataElem {
- /// The value to embed into the document.
- #[required]
- pub value: Value,
-}
-
-impl Show for MetadataElem {
- fn show(&self, _vt: &mut Vt, _styles: StyleChain) -> SourceResult<Content> {
- Ok(Content::empty())
- }
-}
-
-impl Behave for MetadataElem {
- fn behaviour(&self) -> Behaviour {
- Behaviour::Invisible
- }
-}
diff --git a/crates/typst-library/src/meta/mod.rs b/crates/typst-library/src/meta/mod.rs
deleted file mode 100644
index ffe861ab..00000000
--- a/crates/typst-library/src/meta/mod.rs
+++ /dev/null
@@ -1,76 +0,0 @@
-//! Interaction between document parts.
-
-mod bibliography;
-mod cite;
-mod context;
-mod counter;
-mod document;
-mod figure;
-mod footnote;
-mod heading;
-mod link;
-mod metadata;
-#[path = "numbering.rs"]
-mod numbering_;
-mod outline;
-#[path = "query.rs"]
-mod query_;
-mod reference;
-mod state;
-
-pub use self::bibliography::*;
-pub use self::cite::*;
-pub use self::context::*;
-pub use self::counter::*;
-pub use self::document::*;
-pub use self::figure::*;
-pub use self::footnote::*;
-pub use self::heading::*;
-pub use self::link::*;
-pub use self::metadata::*;
-pub use self::numbering_::*;
-pub use self::outline::*;
-pub use self::query_::*;
-pub use self::reference::*;
-pub use self::state::*;
-
-use crate::prelude::*;
-use crate::text::TextElem;
-
-/// Hook up all meta definitions.
-pub(super) fn define(global: &mut Scope) {
- 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>();
-}
-
-/// An element that has a local name.
-pub trait LocalNameIn: LocalName {
- /// Gets the local name from the style chain.
- fn local_name_in(styles: StyleChain) -> &'static str
- where
- Self: Sized,
- {
- Self::local_name(TextElem::lang_in(styles), TextElem::region_in(styles))
- }
-}
-
-impl<T: LocalName> LocalNameIn for T {}
diff --git a/crates/typst-library/src/meta/numbering.rs b/crates/typst-library/src/meta/numbering.rs
deleted file mode 100644
index 72906ff6..00000000
--- a/crates/typst-library/src/meta/numbering.rs
+++ /dev/null
@@ -1,567 +0,0 @@
-use std::str::FromStr;
-
-use chinese_number::{ChineseCase, ChineseCountMethod, ChineseVariant, NumberToChinese};
-use ecow::EcoVec;
-use typst::doc::{PdfPageLabel, PdfPageLabelStyle};
-
-use crate::prelude::*;
-use crate::text::Case;
-
-/// Applies a numbering to a sequence of numbers.
-///
-/// A numbering defines how a sequence of numbers should be displayed as
-/// content. It is defined either through a pattern string or an arbitrary
-/// function.
-///
-/// A numbering pattern consists of counting symbols, for which the actual
-/// number is substituted, their prefixes, and one suffix. The prefixes and the
-/// suffix are repeated as-is.
-///
-/// # Example
-/// ```example
-/// #numbering("1.1)", 1, 2, 3) \
-/// #numbering("1.a.i", 1, 2) \
-/// #numbering("I – 1", 12, 2) \
-/// #numbering(
-/// (..nums) => nums
-/// .pos()
-/// .map(str)
-/// .join(".") + ")",
-/// 1, 2, 3,
-/// )
-/// ```
-#[func]
-pub fn numbering(
- /// The virtual machine.
- vm: &mut Vm,
- /// Defines how the numbering works.
- ///
- /// **Counting symbols** are `1`, `a`, `A`, `i`, `I`, `い`, `イ`, `א`, `가`,
- /// `ㄱ`, and `*`. They are replaced by the number in the sequence, in the
- /// given case.
- ///
- /// The `*` character means that symbols should be used to count, in the
- /// order of `*`, `†`, `‡`, `§`, `¶`, and `‖`. If there are more than six
- /// items, the number is represented using multiple symbols.
- ///
- /// **Suffixes** are all characters after the last counting symbol. They are
- /// repeated as-is at the end of any rendered number.
- ///
- /// **Prefixes** are all characters that are neither counting symbols nor
- /// suffixes. They are repeated as-is at in front of their rendered
- /// equivalent of their counting symbol.
- ///
- /// This parameter can also be an arbitrary function that gets each number
- /// as an individual argument. When given a function, the `numbering`
- /// function just forwards the arguments to that function. While this is not
- /// particularly useful in itself, it means that you can just give arbitrary
- /// numberings to the `numbering` function without caring whether they are
- /// defined as a pattern or function.
- numbering: Numbering,
- /// The numbers to apply the numbering to. Must be positive.
- ///
- /// If `numbering` is a pattern and more numbers than counting symbols are
- /// given, the last counting symbol with its prefix is repeated.
- #[variadic]
- numbers: Vec<usize>,
-) -> SourceResult<Value> {
- numbering.apply_vm(vm, &numbers)
-}
-
-/// How to number a sequence of things.
-#[derive(Debug, Clone, PartialEq, Hash)]
-pub enum Numbering {
- /// A pattern with prefix, numbering, lower / upper case and suffix.
- Pattern(NumberingPattern),
- /// A closure mapping from an item's number to content.
- Func(Func),
-}
-
-impl Numbering {
- /// Apply the pattern to the given numbers.
- pub fn apply_vm(&self, vm: &mut Vm, numbers: &[usize]) -> SourceResult<Value> {
- Ok(match self {
- Self::Pattern(pattern) => Value::Str(pattern.apply(numbers).into()),
- Self::Func(func) => {
- let args = Args::new(func.span(), numbers.iter().copied());
- func.call_vm(vm, args)?
- }
- })
- }
-
- /// Apply the pattern to the given numbers.
- pub fn apply_vt(&self, vt: &mut Vt, numbers: &[usize]) -> SourceResult<Value> {
- Ok(match self {
- Self::Pattern(pattern) => Value::Str(pattern.apply(numbers).into()),
- Self::Func(func) => func.call_vt(vt, numbers.iter().copied())?,
- })
- }
-
- /// Create a new `PdfNumbering` from a `Numbering` applied to a page
- /// number.
- pub fn apply_pdf(&self, number: usize) -> Option<PdfPageLabel> {
- let Numbering::Pattern(pat) = self else {
- return None;
- };
-
- let Some((prefix, kind, case)) = pat.pieces.first() else {
- return None;
- };
-
- // If there is a suffix, we cannot use the common style optimisation,
- // since PDF does not provide a suffix field.
- let mut style = None;
- if pat.suffix.is_empty() {
- use NumberingKind as Kind;
- use PdfPageLabelStyle as Style;
- match (kind, case) {
- (Kind::Arabic, _) => style = Some(Style::Arabic),
- (Kind::Roman, Case::Lower) => style = Some(Style::LowerRoman),
- (Kind::Roman, Case::Upper) => style = Some(Style::UpperRoman),
- (Kind::Letter, Case::Lower) if number <= 26 => {
- style = Some(Style::LowerAlpha)
- }
- (Kind::Letter, Case::Upper) if number <= 26 => {
- style = Some(Style::UpperAlpha)
- }
- _ => {}
- }
- }
-
- // Prefix and offset depend on the style: If it is supported by the PDF
- // spec, we use the given prefix and an offset. Otherwise, everything
- // goes into prefix.
- let prefix = if style.is_none() {
- Some(pat.apply(&[number]))
- } else {
- (!prefix.is_empty()).then(|| prefix.clone())
- };
-
- let offset = style.and(NonZeroUsize::new(number));
- Some(PdfPageLabel { prefix, style, offset })
- }
-
- /// Trim the prefix suffix if this is a pattern.
- pub fn trimmed(mut self) -> Self {
- if let Self::Pattern(pattern) = &mut self {
- pattern.trimmed = true;
- }
- self
- }
-}
-
-impl From<NumberingPattern> for Numbering {
- fn from(pattern: NumberingPattern) -> Self {
- Self::Pattern(pattern)
- }
-}
-
-cast! {
- Numbering,
- self => match self {
- Self::Pattern(pattern) => pattern.into_value(),
- Self::Func(func) => func.into_value(),
- },
- v: NumberingPattern => Self::Pattern(v),
- v: Func => Self::Func(v),
-}
-
-/// How to turn a number into text.
-///
-/// A pattern consists of a prefix, followed by one of `1`, `a`, `A`, `i`,
-/// `I`, `い`, `イ`, `א`, `가`, `ㄱ`, or `*`, and then a suffix.
-///
-/// Examples of valid patterns:
-/// - `1)`
-/// - `a.`
-/// - `(I)`
-#[derive(Debug, Clone, Eq, PartialEq, Hash)]
-pub struct NumberingPattern {
- pub pieces: EcoVec<(EcoString, NumberingKind, Case)>,
- pub suffix: EcoString,
- trimmed: bool,
-}
-
-impl NumberingPattern {
- /// Apply the pattern to the given number.
- pub fn apply(&self, numbers: &[usize]) -> EcoString {
- let mut fmt = EcoString::new();
- let mut numbers = numbers.iter();
-
- for (i, ((prefix, kind, case), &n)) in
- self.pieces.iter().zip(&mut numbers).enumerate()
- {
- if i > 0 || !self.trimmed {
- fmt.push_str(prefix);
- }
- fmt.push_str(&kind.apply(n, *case));
- }
-
- for ((prefix, kind, case), &n) in
- self.pieces.last().into_iter().cycle().zip(numbers)
- {
- if prefix.is_empty() {
- fmt.push_str(&self.suffix);
- } else {
- fmt.push_str(prefix);
- }
- fmt.push_str(&kind.apply(n, *case));
- }
-
- if !self.trimmed {
- fmt.push_str(&self.suffix);
- }
-
- fmt
- }
-
- /// Apply only the k-th segment of the pattern to a number.
- pub fn apply_kth(&self, k: usize, number: usize) -> EcoString {
- let mut fmt = EcoString::new();
- if let Some((prefix, _, _)) = self.pieces.first() {
- fmt.push_str(prefix);
- }
- if let Some((_, kind, case)) = self
- .pieces
- .iter()
- .chain(self.pieces.last().into_iter().cycle())
- .nth(k)
- {
- fmt.push_str(&kind.apply(number, *case));
- }
- fmt.push_str(&self.suffix);
- fmt
- }
-
- /// How many counting symbols this pattern has.
- pub fn pieces(&self) -> usize {
- self.pieces.len()
- }
-}
-
-impl FromStr for NumberingPattern {
- type Err = &'static str;
-
- fn from_str(pattern: &str) -> Result<Self, Self::Err> {
- let mut pieces = EcoVec::new();
- let mut handled = 0;
-
- for (i, c) in pattern.char_indices() {
- let Some(kind) = NumberingKind::from_char(c.to_ascii_lowercase()) else {
- continue;
- };
-
- let prefix = pattern[handled..i].into();
- let case =
- if c.is_uppercase() || c == '壹' { Case::Upper } else { Case::Lower };
- pieces.push((prefix, kind, case));
- handled = c.len_utf8() + i;
- }
-
- let suffix = pattern[handled..].into();
- if pieces.is_empty() {
- return Err("invalid numbering pattern");
- }
-
- Ok(Self { pieces, suffix, trimmed: false })
- }
-}
-
-cast! {
- NumberingPattern,
- self => {
- let mut pat = EcoString::new();
- for (prefix, kind, case) in &self.pieces {
- pat.push_str(prefix);
- let mut c = kind.to_char();
- if *case == Case::Upper {
- c = c.to_ascii_uppercase();
- }
- pat.push(c);
- }
- pat.push_str(&self.suffix);
- pat.into_value()
- },
- v: Str => v.parse()?,
-}
-
-/// Different kinds of numberings.
-#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
-pub enum NumberingKind {
- Arabic,
- Letter,
- Roman,
- Symbol,
- Hebrew,
- SimplifiedChinese,
- // TODO: Pick the numbering pattern based on languages choice.
- // As the `1st` numbering character of Chinese (Simplified) and
- // Chinese (Traditional) is same, we are unable to determine
- // if the context is Simplified or Traditional by only this
- // character.
- #[allow(unused)]
- TraditionalChinese,
- HiraganaIroha,
- KatakanaIroha,
- KoreanJamo,
- KoreanSyllable,
-}
-
-impl NumberingKind {
- /// Create a numbering kind from a lowercase character.
- pub fn from_char(c: char) -> Option<Self> {
- Some(match c {
- '1' => NumberingKind::Arabic,
- 'a' => NumberingKind::Letter,
- 'i' => NumberingKind::Roman,
- '*' => NumberingKind::Symbol,
- 'א' => NumberingKind::Hebrew,
- '一' | '壹' => NumberingKind::SimplifiedChinese,
- 'い' => NumberingKind::HiraganaIroha,
- 'イ' => NumberingKind::KatakanaIroha,
- 'ㄱ' => NumberingKind::KoreanJamo,
- '가' => NumberingKind::KoreanSyllable,
- _ => return None,
- })
- }
-
- /// The lowercase character for this numbering kind.
- pub fn to_char(self) -> char {
- match self {
- Self::Arabic => '1',
- Self::Letter => 'a',
- Self::Roman => 'i',
- Self::Symbol => '*',
- Self::Hebrew => 'א',
- Self::SimplifiedChinese => '一',
- Self::TraditionalChinese => '一',
- Self::HiraganaIroha => 'い',
- Self::KatakanaIroha => 'イ',
- Self::KoreanJamo => 'ㄱ',
- Self::KoreanSyllable => '가',
- }
- }
-
- /// Apply the numbering to the given number.
- pub fn apply(self, mut n: usize, case: Case) -> EcoString {
- match self {
- Self::Arabic => {
- eco_format!("{n}")
- }
- Self::Letter => zeroless::<26>(
- |x| match case {
- Case::Lower => char::from(b'a' + x as u8),
- Case::Upper => char::from(b'A' + x as u8),
- },
- n,
- ),
- Self::HiraganaIroha => zeroless::<47>(
- |x| {
- [
- 'い', 'ろ', 'は', 'に', 'ほ', 'へ', 'と', 'ち', 'り', 'ぬ', 'る',
- 'を', 'わ', 'か', 'よ', 'た', 'れ', 'そ', 'つ', 'ね', 'な', 'ら',
- 'む', 'う', 'ゐ', 'の', 'お', 'く', 'や', 'ま', 'け', 'ふ', 'こ',
- 'え', 'て', 'あ', 'さ', 'き', 'ゆ', 'め', 'み', 'し', 'ゑ', 'ひ',
- 'も', 'せ', 'す',
- ][x]
- },
- n,
- ),
- Self::KatakanaIroha => zeroless::<47>(
- |x| {
- [
- 'イ', 'ロ', 'ハ', 'ニ', 'ホ', 'ヘ', 'ト', 'チ', 'リ', 'ヌ', 'ル',
- 'ヲ', 'ワ', 'カ', 'ヨ', 'タ', 'レ', 'ソ', 'ツ', 'ネ', 'ナ', 'ラ',
- 'ム', 'ウ', 'ヰ', 'ノ', 'オ', 'ク', 'ヤ', 'マ', 'ケ', 'フ', 'コ',
- 'エ', 'テ', 'ア', 'サ', 'キ', 'ユ', 'メ', 'ミ', 'シ', 'ヱ', 'ヒ',
- 'モ', 'セ', 'ス',
- ][x]
- },
- n,
- ),
- Self::Roman => {
- if n == 0 {
- return 'N'.into();
- }
-
- // Adapted from Yann Villessuzanne's roman.rs under the
- // Unlicense, at https://github.com/linfir/roman.rs/
- let mut fmt = EcoString::new();
- for &(name, value) in &[
- ("M̅", 1000000),
- ("D̅", 500000),
- ("C̅", 100000),
- ("L̅", 50000),
- ("X̅", 10000),
- ("V̅", 5000),
- ("I̅V̅", 4000),
- ("M", 1000),
- ("CM", 900),
- ("D", 500),
- ("CD", 400),
- ("C", 100),
- ("XC", 90),
- ("L", 50),
- ("XL", 40),
- ("X", 10),
- ("IX", 9),
- ("V", 5),
- ("IV", 4),
- ("I", 1),
- ] {
- while n >= value {
- n -= value;
- for c in name.chars() {
- match case {
- Case::Lower => fmt.extend(c.to_lowercase()),
- Case::Upper => fmt.push(c),
- }
- }
- }
- }
-
- fmt
- }
- Self::Symbol => {
- if n == 0 {
- return '-'.into();
- }
-
- const SYMBOLS: &[char] = &['*', '†', '‡', '§', '¶', '‖'];
- let symbol = SYMBOLS[(n - 1) % SYMBOLS.len()];
- let amount = ((n - 1) / SYMBOLS.len()) + 1;
- std::iter::repeat(symbol).take(amount).collect()
- }
- Self::Hebrew => {
- if n == 0 {
- return '-'.into();
- }
-
- let mut fmt = EcoString::new();
- 'outer: for &(name, value) in &[
- ('ת', 400),
- ('ש', 300),
- ('ר', 200),
- ('ק', 100),
- ('צ', 90),
- ('פ', 80),
- ('ע', 70),
- ('ס', 60),
- ('נ', 50),
- ('מ', 40),
- ('ל', 30),
- ('כ', 20),
- ('י', 10),
- ('ט', 9),
- ('ח', 8),
- ('ז', 7),
- ('ו', 6),
- ('ה', 5),
- ('ד', 4),
- ('ג', 3),
- ('ב', 2),
- ('א', 1),
- ] {
- while n >= value {
- match n {
- 15 => fmt.push_str("ט״ו"),
- 16 => fmt.push_str("ט״ז"),
- _ => {
- let append_geresh = n == value && fmt.is_empty();
- if n == value && !fmt.is_empty() {
- fmt.push('״');
- }
- fmt.push(name);
- if append_geresh {
- fmt.push('׳');
- }
-
- n -= value;
- continue;
- }
- }
- break 'outer;
- }
- }
- fmt
- }
- l @ (Self::SimplifiedChinese | Self::TraditionalChinese) => {
- let chinese_case = match case {
- Case::Lower => ChineseCase::Lower,
- Case::Upper => ChineseCase::Upper,
- };
-
- match (n as u8).to_chinese(
- match l {
- Self::SimplifiedChinese => ChineseVariant::Simple,
- Self::TraditionalChinese => ChineseVariant::Traditional,
- _ => unreachable!(),
- },
- chinese_case,
- ChineseCountMethod::TenThousand,
- ) {
- Ok(num_str) => EcoString::from(num_str),
- Err(_) => '-'.into(),
- }
- }
- Self::KoreanJamo => zeroless::<14>(
- |x| {
- [
- 'ㄱ', 'ㄴ', 'ㄷ', 'ㄹ', 'ㅁ', 'ㅂ', 'ㅅ', 'ㅇ', 'ㅈ', 'ㅊ', 'ㅋ',
- 'ㅌ', 'ㅍ', 'ㅎ',
- ][x]
- },
- n,
- ),
- Self::KoreanSyllable => zeroless::<14>(
- |x| {
- [
- '가', '나', '다', '라', '마', '바', '사', '아', '자', '차', '카',
- '타', '파', '하',
- ][x]
- },
- n,
- ),
- }
- }
-}
-
-/// Stringify a number using a base-N counting system with no zero digit.
-///
-/// This is best explained by example. Suppose our digits are 'A', 'B', and 'C'.
-/// we would get the following:
-///
-/// ```text
-/// 1 => "A"
-/// 2 => "B"
-/// 3 => "C"
-/// 4 => "AA"
-/// 5 => "AB"
-/// 6 => "AC"
-/// 7 => "BA"
-/// 8 => "BB"
-/// 9 => "BC"
-/// 10 => "CA"
-/// 11 => "CB"
-/// 12 => "CC"
-/// 13 => "AAA"
-/// etc.
-/// ```
-///
-/// You might be familiar with this scheme from the way spreadsheet software
-/// tends to label its columns.
-fn zeroless<const N_DIGITS: usize>(
- mk_digit: impl Fn(usize) -> char,
- mut n: usize,
-) -> EcoString {
- if n == 0 {
- return '-'.into();
- }
- let mut cs = vec![];
- while n > 0 {
- n -= 1;
- cs.push(mk_digit(n % N_DIGITS));
- n /= N_DIGITS;
- }
- cs.into_iter().rev().collect()
-}
diff --git a/crates/typst-library/src/meta/outline.rs b/crates/typst-library/src/meta/outline.rs
deleted file mode 100644
index ebd6c4b0..00000000
--- a/crates/typst-library/src/meta/outline.rs
+++ /dev/null
@@ -1,525 +0,0 @@
-use std::str::FromStr;
-
-use typst::util::option_eq;
-
-use crate::layout::{BoxElem, HElem, HideElem, ParbreakElem, RepeatElem, Spacing};
-use crate::meta::{
- Counter, CounterKey, HeadingElem, LocalName, LocalNameIn, Numbering,
- NumberingPattern, Refable,
-};
-use crate::prelude::*;
-use crate::text::{LinebreakElem, SpaceElem, TextElem};
-
-/// A table of contents, figures, or other elements.
-///
-/// This function generates a list of all occurrences of an element in the
-/// document, up to a given depth. The element's numbering and page number will
-/// be displayed in the outline alongside its title or caption. By default this
-/// generates a table of contents.
-///
-/// # Example
-/// ```example
-/// #outline()
-///
-/// = Introduction
-/// #lorem(5)
-///
-/// = Prior work
-/// #lorem(10)
-/// ```
-///
-/// # 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]($function.where).
-///
-/// ```example
-/// #outline(
-/// title: [List of Figures],
-/// target: figure.where(kind: image),
-/// )
-///
-/// #figure(
-/// image("tiger.jpg"),
-/// caption: [A nice figure!],
-/// )
-/// ```
-///
-/// # 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`]($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]($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.
- ///
- /// The outline's heading will not be numbered by default, but you can
- /// force it to be with a show-set rule:
- /// `{show outline: set heading(numbering: "1.")}`
- /// ```
- #[default(Some(Smart::Auto))]
- pub title: Option<Smart<Content>>,
-
- /// The type of element to include in the outline.
- ///
- /// To list figures containing a specific kind of element, like a table, you
- /// can write `{figure.where(kind: table)}`.
- ///
- /// ```example
- /// #outline(
- /// title: [List of Tables],
- /// target: figure.where(kind: table),
- /// )
- ///
- /// #figure(
- /// table(
- /// columns: 4,
- /// [t], [1], [2], [3],
- /// [y], [0.3], [0.7], [0.5],
- /// ),
- /// caption: [Experiment results],
- /// )
- /// ```
- #[default(LocatableSelector(select_where!(HeadingElem, Outlined => true)))]
- #[borrowed]
- pub target: LocatableSelector,
-
- /// The maximum level up to which elements are included in the outline. When
- /// this argument is `{none}`, all elements are included.
- ///
- /// ```example
- /// #set heading(numbering: "1.")
- /// #outline(depth: 2)
- ///
- /// = Yes
- /// Top-level section.
- ///
- /// == Still
- /// Subsection.
- ///
- /// === Nope
- /// Not included.
- /// ```
- pub depth: Option<NonZeroUsize>,
-
- /// How to indent the outline's entries.
- ///
- /// - `{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]($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]($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
- /// will be removed in a future release.
- ///
- /// ```example
- /// #set heading(numbering: "1.a.")
- ///
- /// #outline(
- /// title: [Contents (Automatic)],
- /// indent: auto,
- /// )
- ///
- /// #outline(
- /// title: [Contents (Length)],
- /// indent: 2em,
- /// )
- ///
- /// #outline(
- /// title: [Contents (Function)],
- /// indent: n => [→ ] * n,
- /// )
- ///
- /// = About ACME Corp.
- /// == History
- /// === Origins
- /// #lorem(10)
- ///
- /// == Products
- /// #lorem(10)
- /// ```
- #[default(None)]
- #[borrowed]
- pub indent: Option<Smart<OutlineIndent>>,
-
- /// Content to fill the space between the title and the page number. Can be
- /// set to `none` to disable filling.
- ///
- /// ```example
- /// #outline(fill: line(length: 100%))
- ///
- /// = A New Beginning
- /// ```
- #[default(Some(RepeatElem::new(TextElem::packed(".")).pack()))]
- 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> {
- let mut seq = vec![ParbreakElem::new().pack()];
- // Build the outline title.
- if let Some(title) = self.title(styles) {
- let title = title.unwrap_or_else(|| {
- TextElem::packed(Self::local_name_in(styles)).spanned(self.span())
- });
-
- seq.push(HeadingElem::new(title).with_level(NonZeroUsize::ONE).pack());
- }
-
- let indent = self.indent(styles);
- let depth = self.depth(styles).unwrap_or(NonZeroUsize::new(usize::MAX).unwrap());
-
- let mut ancestors: Vec<&Content> = vec![];
- let elems = vt.introspector.query(&self.target(styles).0);
-
- for elem in &elems {
- let Some(entry) = OutlineEntry::from_outlinable(
- vt,
- self.span(),
- elem.clone().into_inner(),
- self.fill(styles),
- )?
- else {
- continue;
- };
-
- let level = entry.level();
- if depth < *level {
- continue;
- }
-
- // Deals with the ancestors of the current element.
- // This is only applicable for elements with a hierarchy/level.
- while ancestors
- .last()
- .and_then(|ancestor| ancestor.with::<dyn Outlinable>())
- .map_or(false, |last| last.level() >= *level)
- {
- ancestors.pop();
- }
-
- OutlineIndent::apply(indent, vt, &ancestors, &mut seq, self.span())?;
-
- // Add the overridable outline entry, followed by a line break.
- seq.push(entry.pack());
- seq.push(LinebreakElem::new().pack());
-
- ancestors.push(elem);
- }
-
- seq.push(ParbreakElem::new().pack());
-
- Ok(Content::sequence(seq))
- }
-}
-
-impl Finalize for OutlineElem {
- fn finalize(&self, realized: Content, _: StyleChain) -> Content {
- realized
- .styled(HeadingElem::set_outlined(false))
- .styled(HeadingElem::set_numbering(None))
- }
-}
-
-impl LocalName for OutlineElem {
- fn local_name(lang: Lang, region: Option<Region>) -> &'static str {
- match lang {
- Lang::ALBANIAN => "Përmbajtja",
- Lang::ARABIC => "المحتويات",
- Lang::BOKMÅL => "Innhold",
- Lang::CHINESE if option_eq(region, "TW") => "目錄",
- Lang::CHINESE => "目录",
- Lang::CZECH => "Obsah",
- Lang::DANISH => "Indhold",
- Lang::DUTCH => "Inhoudsopgave",
- Lang::FILIPINO => "Talaan ng mga Nilalaman",
- Lang::FINNISH => "Sisällys",
- Lang::FRENCH => "Table des matières",
- Lang::GERMAN => "Inhaltsverzeichnis",
- Lang::GREEK => "Περιεχόμενα",
- Lang::HUNGARIAN => "Tartalomjegyzék",
- Lang::ITALIAN => "Indice",
- Lang::NYNORSK => "Innhald",
- Lang::POLISH => "Spis treści",
- Lang::PORTUGUESE if option_eq(region, "PT") => "Índice",
- Lang::PORTUGUESE => "Sumário",
- Lang::ROMANIAN => "Cuprins",
- Lang::RUSSIAN => "Содержание",
- Lang::SLOVENIAN => "Kazalo",
- Lang::SPANISH => "Índice",
- Lang::SWEDISH => "Innehåll",
- Lang::TURKISH => "İçindekiler",
- Lang::UKRAINIAN => "Зміст",
- Lang::VIETNAMESE => "Mục lục",
- Lang::JAPANESE => "目次",
- Lang::ENGLISH | _ => "Contents",
- }
- }
-}
-
-/// Marks an element as being able to be outlined. This is used to implement the
-/// `#outline()` element.
-pub trait Outlinable: Refable {
- /// Produce an outline item for this element.
- fn outline(&self, vt: &mut Vt) -> SourceResult<Option<Content>>;
-
- /// Returns the nesting level of this element.
- fn level(&self) -> NonZeroUsize {
- NonZeroUsize::ONE
- }
-}
-
-#[derive(Debug, Clone, PartialEq, Hash)]
-pub enum OutlineIndent {
- Bool(bool),
- Rel(Rel<Length>),
- Func(Func),
-}
-
-impl OutlineIndent {
- fn apply(
- indent: &Option<Smart<Self>>,
- vt: &mut Vt,
- ancestors: &Vec<&Content>,
- seq: &mut Vec<Content>,
- span: Span,
- ) -> SourceResult<()> {
- match indent {
- // 'none' | 'false' => no indenting
- None | Some(Smart::Custom(OutlineIndent::Bool(false))) => {}
-
- // 'auto' | 'true' => use numbering alignment for indenting
- Some(Smart::Auto | Smart::Custom(OutlineIndent::Bool(true))) => {
- // Add hidden ancestors numberings to realize the indent.
- let mut hidden = Content::empty();
- for ancestor in ancestors {
- let ancestor_outlinable = ancestor.with::<dyn Outlinable>().unwrap();
-
- if let Some(numbering) = ancestor_outlinable.numbering() {
- let numbers = ancestor_outlinable
- .counter()
- .at(vt, ancestor.location().unwrap())?
- .display(vt, &numbering)?;
-
- hidden += numbers + SpaceElem::new().pack();
- };
- }
-
- if !ancestors.is_empty() {
- seq.push(HideElem::new(hidden).pack());
- seq.push(SpaceElem::new().pack());
- }
- }
-
- // Length => indent with some fixed spacing per level
- Some(Smart::Custom(OutlineIndent::Rel(length))) => {
- seq.push(
- HElem::new(Spacing::Rel(*length)).pack().repeat(ancestors.len()),
- );
- }
-
- // Function => call function with the current depth and take
- // the returned content
- Some(Smart::Custom(OutlineIndent::Func(func))) => {
- let depth = ancestors.len();
- let LengthOrContent(content) =
- func.call_vt(vt, [depth])?.cast().at(span)?;
- if !content.is_empty() {
- seq.push(content);
- }
- }
- };
-
- Ok(())
- }
-}
-
-cast! {
- OutlineIndent,
- self => match self {
- Self::Bool(v) => v.into_value(),
- Self::Rel(v) => v.into_value(),
- Self::Func(v) => v.into_value()
- },
- v: bool => OutlineIndent::Bool(v),
- v: Rel<Length> => OutlineIndent::Rel(v),
- v: Func => OutlineIndent::Func(v),
-}
-
-struct LengthOrContent(Content);
-
-cast! {
- LengthOrContent,
- v: Rel<Length> => Self(HElem::new(Spacing::Rel(v)).pack()),
- v: Content => Self(v),
-}
-
-/// Represents each entry line in an outline, including the reference to the
-/// 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. 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.")
-///
-/// #show outline.entry.where(
-/// level: 1
-/// ): it => {
-/// v(12pt, weak: true)
-/// strong(it)
-/// }
-///
-/// #outline(indent: auto)
-///
-/// = Introduction
-/// = Background
-/// == History
-/// == State of the Art
-/// = Analysis
-/// == Setup
-/// ```
-#[elem(name = "entry", title = "Outline Entry", Show)]
-pub struct OutlineEntry {
- /// The nesting level of this outline entry. Starts at `{1}` for top-level
- /// entries.
- #[required]
- pub level: NonZeroUsize,
-
- /// The element this entry refers to. Its location will be available
- /// through the [`location`]($content.location) method on content
- /// and can be [linked]($link) to.
- #[required]
- pub element: Content,
-
- /// The content which is displayed in place of the referred element at its
- /// entry in the outline. For a heading, this would be its number followed
- /// by the heading's title, for example.
- #[required]
- pub body: Content,
-
- /// The content used to fill the space between the element's outline and
- /// its page number, as defined by the outline element this entry is
- /// 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`]($box) with
- /// fractional width. For example, `{box(width: 1fr, repeat[-])}` would show
- /// precisely as many `-` characters as necessary to fill a particular gap.
- #[required]
- pub fill: Option<Content>,
-
- /// The page number of the element this entry links to, formatted with the
- /// numbering set for the referenced page.
- #[required]
- pub page: Content,
-}
-
-impl OutlineEntry {
- /// Generates an OutlineEntry from the given element, if possible (errors if
- /// the element does not implement `Outlinable`). If the element should not
- /// be outlined (e.g. heading with 'outlined: false'), does not generate an
- /// entry instance (returns `Ok(None)`).
- fn from_outlinable(
- vt: &mut Vt,
- span: Span,
- elem: Content,
- fill: Option<Content>,
- ) -> SourceResult<Option<Self>> {
- let Some(outlinable) = elem.with::<dyn Outlinable>() else {
- bail!(span, "cannot outline {}", elem.func().name());
- };
-
- let Some(body) = outlinable.outline(vt)? else {
- return Ok(None);
- };
-
- let location = elem.location().unwrap();
- let page_numbering = vt
- .introspector
- .page_numbering(location)
- .cast::<Option<Numbering>>()
- .unwrap()
- .unwrap_or_else(|| {
- Numbering::Pattern(NumberingPattern::from_str("1").unwrap())
- });
-
- let page = Counter::new(CounterKey::Page)
- .at(vt, location)?
- .display(vt, &page_numbering)?;
-
- Ok(Some(Self::new(outlinable.level(), elem, body, fill, page)))
- }
-}
-
-impl Show for OutlineEntry {
- fn show(&self, _vt: &mut Vt, _: StyleChain) -> SourceResult<Content> {
- let mut seq = vec![];
- let elem = self.element();
-
- // In case a user constructs an outline entry with an arbitrary element.
- let Some(location) = elem.location() else {
- if elem.can::<dyn Locatable>() && elem.can::<dyn Outlinable>() {
- bail!(error!(self.span(), "{} must have a location", elem.func().name())
- .with_hint(
- "try using a query or a show rule to customize the outline.entry instead",
- ))
- } else {
- bail!(self.span(), "cannot outline {}", elem.func().name())
- }
- };
-
- // The body text remains overridable.
- seq.push(self.body().clone().linked(Destination::Location(location)));
-
- // Add filler symbols between the section name and page number.
- if let Some(filler) = self.fill() {
- seq.push(SpaceElem::new().pack());
- seq.push(
- BoxElem::new()
- .with_body(Some(filler.clone()))
- .with_width(Fr::one().into())
- .pack(),
- );
- seq.push(SpaceElem::new().pack());
- } else {
- seq.push(HElem::new(Fr::one().into()).pack());
- }
-
- // Add the page number.
- let page = self.page().clone().linked(Destination::Location(location));
- seq.push(page);
-
- Ok(Content::sequence(seq))
- }
-}
diff --git a/crates/typst-library/src/meta/query.rs b/crates/typst-library/src/meta/query.rs
deleted file mode 100644
index d6c600d7..00000000
--- a/crates/typst-library/src/meta/query.rs
+++ /dev/null
@@ -1,157 +0,0 @@
-use crate::prelude::*;
-
-/// Finds elements in the document.
-///
-/// 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`]($locate)
-/// function.
-///
-/// # 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
-/// first section heading.
-///
-/// To realize this layout, we call `locate` and then query for all headings
-/// after the current location. The function we pass to locate is called twice
-/// in this case: Once per page.
-///
-/// - On the first page the query for all headings before the current location
-/// yields an empty array: There are no previous headings. We check for this
-/// case and and just display "Typst Academy".
-///
-/// - For the second page, we retrieve the last element from the query's result.
-/// This is the latest heading before the current position and as such, it is
-/// the heading of the section we are currently in. We access its content
-/// through the `body` field and display it alongside "Typst Academy".
-///
-/// ```example
-/// >>> #set page(
-/// >>> width: 240pt,
-/// >>> height: 180pt,
-/// >>> margin: (top: 35pt, rest: 15pt),
-/// >>> header-ascent: 12pt,
-/// >>> )
-/// #set page(header: locate(loc => {
-/// let elems = query(
-/// selector(heading).before(loc),
-/// loc,
-/// )
-/// let academy = smallcaps[
-/// Typst Academy
-/// ]
-/// if elems == () {
-/// align(right, academy)
-/// } else {
-/// let body = elems.last().body
-/// academy + h(1fr) + emph(body)
-/// }
-/// }))
-///
-/// = Introduction
-/// #lorem(23)
-///
-/// = Background
-/// #lorem(30)
-///
-/// = Analysis
-/// #lorem(15)
-/// ```
-///
-/// # 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
-/// affect itself—leading to a result that never stabilizes.
-///
-/// In the example below, we query for all headings in the document. We then
-/// generate as many headings. In the beginning, there's just one heading,
-/// titled `Real`. Thus, `count` is `1` and one `Fake` heading is generated.
-/// Typst sees that the query's result has changed and processes it again. This
-/// time, `count` is `2` and two `Fake` headings are generated. This goes on and
-/// 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]($counter) and [state]($state).
-///
-/// ```example
-/// = Real
-/// #locate(loc => {
-/// let elems = query(heading, loc)
-/// let count = elems.len()
-/// count * [= Fake]
-/// })
-/// ```
-///
-/// # 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]($metadata):
-///
-/// ```typ
-/// #metadata("This is a note") <note>
-/// ```
-///
-/// You can execute a query on it as follows using Typst's CLI:
-/// ```sh
-/// $ typst query example.typ "<note>"
-/// [
-/// {
-/// "func": "metadata",
-/// "value": "This is a note",
-/// "label": "<note>"
-/// }
-/// ]
-/// ```
-///
-/// Frequently, you're interested in only one specific field of the resulting
-/// elements. In the case of the `metadata` element, the `value` field is the
-/// interesting one. You can extract just this field with the `--field`
-/// argument.
-///
-/// ```sh
-/// $ typst query example.typ "<note>" --field value
-/// ["This is a note"]
-/// ```
-///
-/// If you are interested in just a single element, you can use the `--one`
-/// flag to extract just it.
-///
-/// ```sh
-/// $ typst query example.typ "<note>" --field value --one
-/// "This is a note"
-/// ```
-#[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`]($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`]($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,
-) -> Array {
- let _ = location;
- let vec = vm.vt.introspector.query(&target.0);
- vec.into_iter()
- .map(|elem| Value::Content(elem.into_inner()))
- .collect()
-}
diff --git a/crates/typst-library/src/meta/reference.rs b/crates/typst-library/src/meta/reference.rs
deleted file mode 100644
index 3a463c80..00000000
--- a/crates/typst-library/src/meta/reference.rs
+++ /dev/null
@@ -1,283 +0,0 @@
-use super::{BibliographyElem, CiteElem, Counter, Figurable, Numbering};
-use crate::math::EquationElem;
-use crate::meta::FootnoteElem;
-use crate::prelude::*;
-use crate::text::TextElem;
-
-/// A reference to a label or bibliography.
-///
-/// 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]($cite) from a
-/// bibliography.
-///
-/// 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`]($link) function instead.
-///
-/// # Example
-/// ```example
-/// #set heading(numbering: "1.")
-/// #set math.equation(numbering: "(1)")
-///
-/// = Introduction <intro>
-/// Recent developments in
-/// typesetting software have
-/// rekindled hope in previously
-/// frustrated researchers. @distress
-/// As shown in @results, we ...
-///
-/// = Results <results>
-/// We discuss our approach in
-/// comparison with others.
-///
-/// == Performance <perf>
-/// @slow demonstrates what slow
-/// software looks like.
-/// $ O(n) = 2^n $ <slow>
-///
-/// #bibliography("works.bib")
-/// ```
-///
-/// # 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]`).
-///
-/// To customize the supplement, add content in square brackets after the
-/// reference: `[@intro[Chapter]]`.
-///
-/// # 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
-/// always need to handle that case in your code.
-///
-/// ```example
-/// #set heading(numbering: "1.")
-/// #set math.equation(numbering: "(1)")
-///
-/// #show ref: it => {
-/// let eq = math.equation
-/// let el = it.element
-/// if el != none and el.func() == eq {
-/// // Override equation references.
-/// numbering(
-/// el.numbering,
-/// ..counter(eq).at(el.location())
-/// )
-/// } else {
-/// // Other references as usual.
-/// it
-/// }
-/// }
-///
-/// = Beginnings <beginning>
-/// In @beginning we prove @pythagoras.
-/// $ a^2 + b^2 = c^2 $ <pythagoras>
-/// ```
-#[elem(title = "Reference", Synthesize, Locatable, Show)]
-pub struct RefElem {
- /// The target label that should be referenced.
- ///
- /// Can be a label that is defined in the document or an entry from the
- /// [`bibliography`]($bibliography).
- #[required]
- pub target: Label,
-
- /// A supplement for the reference.
- ///
- /// For references to headings or figures, this is added before the
- /// referenced number. For citations, this can be used to add a page number.
- ///
- /// If a function is specified, it is passed the referenced element and
- /// should return content.
- ///
- /// ```example
- /// #set heading(numbering: "1.")
- /// #set ref(supplement: it => {
- /// if it.func() == heading {
- /// "Chapter"
- /// } else {
- /// "Thing"
- /// }
- /// })
- ///
- /// = Introduction <intro>
- /// In @intro, we see how to turn
- /// Sections into Chapters. And
- /// in @intro[Part], it is done
- /// manually.
- /// ```
- #[borrowed]
- pub supplement: Smart<Option<Supplement>>,
-
- /// A synthesized citation.
- #[synthesized]
- pub citation: Option<CiteElem>,
-
- /// The referenced element.
- #[synthesized]
- pub element: Option<Content>,
-}
-
-impl Synthesize for RefElem {
- fn synthesize(&mut self, vt: &mut Vt, styles: StyleChain) -> SourceResult<()> {
- let citation = self.to_citation(vt, styles)?;
- self.push_citation(Some(citation));
- self.push_element(None);
-
- let target = *self.target();
- if !BibliographyElem::has(vt, target) {
- if let Ok(elem) = vt.introspector.query_label(target) {
- self.push_element(Some(elem.into_inner()));
- return Ok(());
- }
- }
-
- Ok(())
- }
-}
-
-impl Show for RefElem {
- #[tracing::instrument(name = "RefElem::show", skip_all)]
- fn show(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Content> {
- Ok(vt.delayed(|vt| {
- let target = *self.target();
- let elem = vt.introspector.query_label(target);
- let span = self.span();
-
- if BibliographyElem::has(vt, target) {
- if elem.is_ok() {
- bail!(span, "label occurs in the document and its bibliography");
- }
-
- return Ok(self.to_citation(vt, styles)?.spanned(span).pack());
- }
-
- let elem = elem.at(span)?;
-
- if elem.func() == FootnoteElem::elem() {
- return Ok(FootnoteElem::with_label(target).spanned(span).pack());
- }
-
- let refable = elem
- .with::<dyn Refable>()
- .ok_or_else(|| {
- if elem.can::<dyn Figurable>() {
- eco_format!(
- "cannot reference {} directly, try putting it into a figure",
- elem.func().name()
- )
- } else {
- eco_format!("cannot reference {}", elem.func().name())
- }
- })
- .at(span)?;
-
- let numbering = refable
- .numbering()
- .ok_or_else(|| {
- eco_format!(
- "cannot reference {} without numbering",
- elem.func().name()
- )
- })
- .hint(eco_format!(
- "you can enable {} numbering with `#set {}(numbering: \"1.\")`",
- elem.func().name(),
- if elem.func() == EquationElem::elem() {
- "math.equation"
- } else {
- elem.func().name()
- }
- ))
- .at(span)?;
-
- let numbers = refable
- .counter()
- .at(vt, elem.location().unwrap())?
- .display(vt, &numbering.trimmed())?;
-
- let supplement = match self.supplement(styles).as_ref() {
- Smart::Auto => refable.supplement(),
- Smart::Custom(None) => Content::empty(),
- Smart::Custom(Some(supplement)) => {
- supplement.resolve(vt, [(*elem).clone()])?
- }
- };
-
- let mut content = numbers;
- if !supplement.is_empty() {
- content = supplement + TextElem::packed("\u{a0}") + content;
- }
-
- Ok(content.linked(Destination::Location(elem.location().unwrap())))
- }))
- }
-}
-
-impl RefElem {
- /// Turn the reference into a citation.
- pub fn to_citation(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<CiteElem> {
- let mut elem = CiteElem::new(*self.target());
- elem.set_location(self.location().unwrap());
- elem.synthesize(vt, styles)?;
- elem.push_supplement(match self.supplement(styles).clone() {
- Smart::Custom(Some(Supplement::Content(content))) => Some(content),
- _ => None,
- });
-
- Ok(elem)
- }
-}
-
-/// Additional content for a reference.
-#[derive(Debug, Clone, PartialEq, Hash)]
-pub enum Supplement {
- Content(Content),
- Func(Func),
-}
-
-impl Supplement {
- /// Tries to resolve the supplement into its content.
- pub fn resolve<T: IntoValue>(
- &self,
- vt: &mut Vt,
- args: impl IntoIterator<Item = T>,
- ) -> SourceResult<Content> {
- Ok(match self {
- Supplement::Content(content) => content.clone(),
- Supplement::Func(func) => func.call_vt(vt, args)?.display(),
- })
- }
-}
-
-cast! {
- Supplement,
- self => match self {
- Self::Content(v) => v.into_value(),
- Self::Func(v) => v.into_value(),
- },
- v: Content => Self::Content(v),
- v: Func => Self::Func(v),
-}
-
-/// Marks an element as being able to be referenced. This is used to implement
-/// the `@ref` element.
-pub trait Refable {
- /// The supplement, if not overridden by the reference.
- fn supplement(&self) -> Content;
-
- /// Returns the counter of this element.
- fn counter(&self) -> Counter;
-
- /// Returns the numbering of this element.
- fn numbering(&self) -> Option<Numbering>;
-}
diff --git a/crates/typst-library/src/meta/state.rs b/crates/typst-library/src/meta/state.rs
deleted file mode 100644
index 4f04628f..00000000
--- a/crates/typst-library/src/meta/state.rs
+++ /dev/null
@@ -1,413 +0,0 @@
-use std::fmt::Debug;
-
-use ecow::{eco_vec, EcoVec};
-use typst::eval::{Repr, Tracer};
-use typst::model::DelayedErrors;
-
-use crate::prelude::*;
-
-/// Manages stateful parts of your document.
-///
-/// Let's say you have some computations in your document and want to remember
-/// the result of your last computation to use it in the next one. You might try
-/// something similar to the code below and expect it to output 10, 13, 26, and
-/// 21. However this **does not work** in Typst. If you test this code, you will
-/// see that Typst complains with the following error message: _Variables from
-/// outside the function are read-only and cannot be modified._
-///
-/// ```typ
-/// #let x = 0
-/// #let compute(expr) = {
-/// x = eval(
-/// expr.replace("x", str(x))
-/// )
-/// [New value is #x. ]
-/// }
-///
-/// #compute("10") \
-/// #compute("x + 3") \
-/// #compute("x * 2") \
-/// #compute("x - 5")
-/// ```
-///
-/// # 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
-/// order in which the results will be laid out in the document. In our simple
-/// example, that's the case, but in general it might not be.
-///
-/// Let's look at a slightly different, but similar kind of state: The heading
-/// numbering. We want to increase the heading counter at each heading. Easy
-/// enough, right? Just add one. Well, it's not that simple. Consider the
-/// following example:
-///
-/// ```example
-/// #set heading(numbering: "1.")
-/// #let template(body) = [
-/// = Outline
-/// ...
-/// #body
-/// ]
-///
-/// #show: template
-///
-/// = Introduction
-/// ...
-/// ```
-///
-/// Here, Typst first processes the body of the document after the show rule,
-/// sees the `Introduction` heading, then passes the resulting content to the
-/// `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 }
-/// 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
-/// important ones are `display` and `update`:
-///
-/// - The `display` method shows the current value of the state. You can
-/// optionally give it a function that receives the value and formats it in
-/// some way.
-///
-/// - The `update` method modifies the state. You can give it any value. If
-/// given a non-function value, it sets the state to that value. If given a
-/// function, that function receives the previous state and has to return the
-/// new state.
-///
-/// Our initial example would now look like this:
-///
-/// ```example
-/// #let s = state("x", 0)
-/// #let compute(expr) = [
-/// #s.update(x =>
-/// eval(expr.replace("x", str(x)))
-/// )
-/// New value is #s.display().
-/// ]
-///
-/// #compute("10") \
-/// #compute("x + 3") \
-/// #compute("x * 2") \
-/// #compute("x - 5")
-/// ```
-///
-/// State managed by Typst is always updated in layout order, not in evaluation
-/// order. The `update` method returns content and its effect occurs at the
-/// position where the returned content is inserted into the document.
-///
-/// As a result, we can now also store some of the computations in
-/// variables, but they still show the correct results:
-///
-/// ```example
-/// >>> #let s = state("x", 0)
-/// >>> #let compute(expr) = [
-/// >>> #s.update(x =>
-/// >>> eval(expr.replace("x", str(x)))
-/// >>> )
-/// >>> New value is #s.display().
-/// >>> ]
-/// <<< ...
-///
-/// #let more = [
-/// #compute("x * 2") \
-/// #compute("x - 5")
-/// ]
-///
-/// #compute("10") \
-/// #compute("x + 3") \
-/// #more
-/// ```
-///
-/// 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]($counter) is very similar to its state system.
-///
-/// # Time Travel
-/// By using Typst's state management system you also get time travel
-/// 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
-/// >>> #let s = state("x", 0)
-/// >>> #let compute(expr) = [
-/// >>> #s.update(x => {
-/// >>> eval(expr.replace("x", str(x)))
-/// >>> })
-/// >>> New value is #s.display().
-/// >>> ]
-/// <<< ...
-///
-/// Value at `<here>` is
-/// #locate(loc => s.at(
-/// query(<here>, loc)
-/// .first()
-/// .location()
-/// ))
-///
-/// #compute("10") \
-/// #compute("x + 3") \
-/// *Here.* <here> \
-/// #compute("x * 2") \
-/// #compute("x - 5")
-/// ```
-///
-/// # 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.
-///
-/// For instance, if you generate state updates depending on the final value of
-/// a state, the results might never converge. The example below illustrates
-/// this. We initialize our state with `1` and then update it to its own final
-/// value plus 1. So it should be `2`, but then its final value is `2`, so it
-/// should be `3`, and so on. This example display `4` because Typst simply
-/// gives up after a few attempts.
-///
-/// ```example
-/// #let s = state("x", 1)
-/// #locate(loc => {
-/// s.update(s.final(loc) + 1)
-/// })
-/// #s.display()
-/// ```
-///
-/// In general, you should _typically_ not generate state updates from within
-/// `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.
-#[ty(scope)]
-#[derive(Debug, Clone, PartialEq, Hash)]
-pub struct State {
- /// The key that identifies the state.
- key: Str,
- /// The initial value of the state.
- init: Value,
-}
-
-impl State {
- /// 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.
- ///
- /// This has to happen just once for all states, cutting down the number
- /// of state updates from quadratic to linear.
- fn sequence(&self, vt: &mut Vt) -> SourceResult<EcoVec<Value>> {
- self.sequence_impl(
- vt.world,
- vt.introspector,
- vt.locator.track(),
- TrackedMut::reborrow_mut(&mut vt.delayed),
- TrackedMut::reborrow_mut(&mut vt.tracer),
- )
- }
-
- /// Memoized implementation of `sequence`.
- #[comemo::memoize]
- fn sequence_impl(
- &self,
- world: Tracked<dyn World + '_>,
- introspector: Tracked<Introspector>,
- locator: Tracked<Locator>,
- delayed: TrackedMut<DelayedErrors>,
- tracer: TrackedMut<Tracer>,
- ) -> SourceResult<EcoVec<Value>> {
- let mut locator = Locator::chained(locator);
- let mut vt = Vt {
- world,
- introspector,
- locator: &mut locator,
- delayed,
- tracer,
- };
- let mut state = self.init.clone();
- let mut stops = eco_vec![state.clone()];
-
- for elem in introspector.query(&self.selector()) {
- let elem = elem.to::<UpdateElem>().unwrap();
- match elem.update() {
- StateUpdate::Set(value) => state = value.clone(),
- StateUpdate::Func(func) => state = func.call_vt(&mut vt, [state])?,
- }
- stops.push(state.clone());
- }
-
- Ok(stops)
- }
-
- /// The selector for this state's updates.
- fn selector(&self) -> Selector {
- select_where!(UpdateElem, 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())
- }
-}
-
-impl Repr for State {
- fn repr(&self) -> EcoString {
- eco_format!("state({}, {})", self.key.repr(), self.init.repr())
- }
-}
-
-cast! {
- type State,
-}
-
-/// An update to perform on a state.
-#[ty]
-#[derive(Debug, Clone, PartialEq, Hash)]
-pub enum StateUpdate {
- /// Set the state to the specified value.
- Set(Value),
- /// Apply the given function to the state.
- Func(Func),
-}
-
-impl Repr for StateUpdate {
- fn repr(&self) -> EcoString {
- "..".into()
- }
-}
-
-cast! {
- type StateUpdate,
- v: Func => Self::Func(v),
- v: Value => Self::Set(v),
-}
-
-/// Executes a display of a state.
-#[elem(Locatable, Show)]
-struct DisplayElem {
- /// The state.
- #[required]
- state: State,
-
- /// The function to display the state with.
- #[required]
- func: Option<Func>,
-}
-
-impl Show for DisplayElem {
- #[tracing::instrument(name = "DisplayElem::show", skip(self, vt))]
- fn show(&self, vt: &mut Vt, _: StyleChain) -> SourceResult<Content> {
- Ok(vt.delayed(|vt| {
- let location = self.location().unwrap();
- let value = self.state().at(vt, location)?;
- Ok(match self.func() {
- Some(func) => func.call_vt(vt, [value])?.display(),
- None => value.display(),
- })
- }))
- }
-}
-
-/// Executes a display of a state.
-#[elem(Locatable, Show)]
-struct UpdateElem {
- /// The key that identifies the state.
- #[required]
- key: Str,
-
- /// The update to perform on the state.
- #[required]
- update: StateUpdate,
-}
-
-impl Show for UpdateElem {
- #[tracing::instrument(name = "UpdateElem::show")]
- fn show(&self, _: &mut Vt, _: StyleChain) -> SourceResult<Content> {
- Ok(Content::empty())
- }
-}
diff --git a/crates/typst-library/src/prelude.rs b/crates/typst-library/src/prelude.rs
deleted file mode 100644
index 00700540..00000000
--- a/crates/typst-library/src/prelude.rs
+++ /dev/null
@@ -1,42 +0,0 @@
-//! Helpful imports for creating library functionality.
-
-#[doc(no_inline)]
-pub use std::fmt::{self, Debug, Formatter};
-#[doc(no_inline)]
-pub use std::num::NonZeroUsize;
-
-#[doc(no_inline)]
-pub use comemo::{Track, Tracked, TrackedMut};
-#[doc(no_inline)]
-pub use ecow::{eco_format, EcoString};
-#[doc(no_inline)]
-pub use smallvec::{smallvec, SmallVec};
-#[doc(no_inline)]
-pub use typst::diag::{bail, error, At, Hint, SourceResult, StrResult};
-#[doc(no_inline)]
-pub use typst::doc::*;
-#[doc(no_inline)]
-pub use typst::eval::{
- array, cast, dict, format_str, func, scope, ty, Args, Array, Bytes, Cast, Dict,
- FromValue, Func, IntoValue, Repr, Scope, Smart, Str, Symbol, Type, Value, Vm,
-};
-#[doc(no_inline)]
-pub use typst::geom::*;
-#[doc(no_inline)]
-pub use typst::model::{
- elem, select_where, Behave, Behaviour, Construct, Content, Element, ElementFields,
- Finalize, Fold, Introspector, Label, LocalName, Locatable, LocatableSelector,
- Location, Locator, MetaElem, NativeElement, PlainText, Resolve, Selector, Set, Show,
- StyleChain, StyleVec, Styles, Synthesize, Unlabellable, Vt,
-};
-#[doc(no_inline)]
-pub use typst::syntax::{FileId, Span, Spanned};
-#[doc(no_inline)]
-pub use typst::util::NonZeroExt;
-#[doc(no_inline)]
-pub use typst::World;
-
-#[doc(no_inline)]
-pub use crate::layout::{Fragment, Layout, Regions};
-#[doc(no_inline)]
-pub use crate::shared::{ContentExt, StylesExt};
diff --git a/crates/typst-library/src/shared/behave.rs b/crates/typst-library/src/shared/behave.rs
deleted file mode 100644
index e152fbb5..00000000
--- a/crates/typst-library/src/shared/behave.rs
+++ /dev/null
@@ -1,114 +0,0 @@
-//! Element interaction.
-
-use std::borrow::Cow;
-
-use typst::model::{Behave, Behaviour, Content, StyleChain, StyleVec, StyleVecBuilder};
-
-/// A wrapper around a [`StyleVecBuilder`] that allows elements to interact.
-#[derive(Debug)]
-pub struct BehavedBuilder<'a> {
- /// The internal builder.
- builder: StyleVecBuilder<'a, Cow<'a, Content>>,
- /// Staged weak and ignorant elements that we can't yet commit to the
- /// builder. The option is `Some(_)` for weak elements and `None` for
- /// ignorant elements.
- staged: Vec<(Cow<'a, Content>, Behaviour, StyleChain<'a>)>,
- /// What the last non-ignorant item was.
- last: Behaviour,
-}
-
-impl<'a> BehavedBuilder<'a> {
- /// Create a new style-vec builder.
- pub fn new() -> Self {
- Self {
- builder: StyleVecBuilder::new(),
- staged: vec![],
- last: Behaviour::Destructive,
- }
- }
-
- /// Whether the builder is totally empty.
- pub fn is_empty(&self) -> bool {
- self.builder.is_empty() && self.staged.is_empty()
- }
-
- /// Whether the builder is empty except for some weak elements that will
- /// probably collapse.
- pub fn has_strong_elements(&self, last: bool) -> bool {
- !self.builder.is_empty()
- || self.staged.iter().any(|(_, behaviour, _)| {
- !matches!(behaviour, Behaviour::Weak(_) | Behaviour::Invisible)
- || (last && *behaviour == Behaviour::Invisible)
- })
- }
-
- /// Push an item into the sequence.
- pub fn push(&mut self, elem: Cow<'a, Content>, styles: StyleChain<'a>) {
- let interaction = elem
- .with::<dyn Behave>()
- .map_or(Behaviour::Supportive, Behave::behaviour);
-
- match interaction {
- Behaviour::Weak(level) => {
- if matches!(self.last, Behaviour::Weak(_)) {
- let item = elem.with::<dyn Behave>().unwrap();
- let i = self.staged.iter().position(|prev| {
- let Behaviour::Weak(prev_level) = prev.1 else { return false };
- level < prev_level
- || (level == prev_level && item.larger(prev, styles))
- });
- let Some(i) = i else { return };
- self.staged.remove(i);
- }
-
- if self.last != Behaviour::Destructive {
- self.staged.push((elem, interaction, styles));
- self.last = interaction;
- }
- }
- Behaviour::Supportive => {
- self.flush(true);
- self.builder.push(elem, styles);
- self.last = interaction;
- }
- Behaviour::Destructive => {
- self.flush(false);
- self.builder.push(elem, styles);
- self.last = interaction;
- }
- Behaviour::Ignorant | Behaviour::Invisible => {
- self.staged.push((elem, interaction, styles));
- }
- }
- }
-
- /// Iterate over the contained elements.
- pub fn elems(&self) -> impl DoubleEndedIterator<Item = &Cow<'a, Content>> {
- self.builder.elems().chain(self.staged.iter().map(|(item, ..)| item))
- }
-
- /// Return the finish style vec and the common prefix chain.
- pub fn finish(mut self) -> (StyleVec<Cow<'a, Content>>, StyleChain<'a>) {
- self.flush(false);
- self.builder.finish()
- }
-
- /// Push the staged elements, filtering out weak elements if `supportive` is
- /// false.
- fn flush(&mut self, supportive: bool) {
- for (item, interaction, styles) in self.staged.drain(..) {
- if supportive
- || interaction == Behaviour::Ignorant
- || interaction == Behaviour::Invisible
- {
- self.builder.push(item, styles);
- }
- }
- }
-}
-
-impl<'a> Default for BehavedBuilder<'a> {
- fn default() -> Self {
- Self::new()
- }
-}
diff --git a/crates/typst-library/src/shared/ext.rs b/crates/typst-library/src/shared/ext.rs
deleted file mode 100644
index 60614820..00000000
--- a/crates/typst-library/src/shared/ext.rs
+++ /dev/null
@@ -1,92 +0,0 @@
-//! Extension traits.
-
-use crate::layout::{AlignElem, MoveElem, PadElem};
-use crate::prelude::*;
-use crate::text::{EmphElem, FontFamily, FontList, StrongElem, TextElem, UnderlineElem};
-
-/// Additional methods on content.
-pub trait ContentExt {
- /// Make this content strong.
- fn strong(self) -> Self;
-
- /// Make this content emphasized.
- fn emph(self) -> Self;
-
- /// Underline this content.
- fn underlined(self) -> Self;
-
- /// Link the content somewhere.
- fn linked(self, dest: Destination) -> Self;
-
- /// Make the content linkable by `.linked(Destination::Location(loc))`.
- ///
- /// Should be used in combination with [`Location::variant`].
- fn backlinked(self, loc: Location) -> Self;
-
- /// Set alignments for this content.
- fn aligned(self, align: Align) -> Self;
-
- /// Pad this content at the sides.
- fn padded(self, padding: Sides<Rel<Length>>) -> Self;
-
- /// Transform this content's contents without affecting layout.
- fn moved(self, delta: Axes<Rel<Length>>) -> Self;
-}
-
-impl ContentExt for Content {
- fn strong(self) -> Self {
- StrongElem::new(self).pack()
- }
-
- fn emph(self) -> Self {
- EmphElem::new(self).pack()
- }
-
- fn underlined(self) -> Self {
- UnderlineElem::new(self).pack()
- }
-
- fn linked(self, dest: Destination) -> Self {
- self.styled(MetaElem::set_data(smallvec![Meta::Link(dest)]))
- }
-
- fn backlinked(self, loc: Location) -> Self {
- let mut backlink = Content::empty();
- backlink.set_location(loc);
- self.styled(MetaElem::set_data(smallvec![Meta::Elem(backlink)]))
- }
-
- fn aligned(self, align: Align) -> Self {
- self.styled(AlignElem::set_alignment(align))
- }
-
- fn padded(self, padding: Sides<Rel<Length>>) -> Self {
- PadElem::new(self)
- .with_left(padding.left)
- .with_top(padding.top)
- .with_right(padding.right)
- .with_bottom(padding.bottom)
- .pack()
- }
-
- fn moved(self, delta: Axes<Rel<Length>>) -> Self {
- MoveElem::new(self).with_dx(delta.x).with_dy(delta.y).pack()
- }
-}
-
-/// Additional methods for style lists.
-pub trait StylesExt {
- /// Set a font family composed of a preferred family and existing families
- /// from a style chain.
- fn set_family(&mut self, preferred: FontFamily, existing: StyleChain);
-}
-
-impl StylesExt for Styles {
- fn set_family(&mut self, preferred: FontFamily, existing: StyleChain) {
- self.set(TextElem::set_font(FontList(
- std::iter::once(preferred)
- .chain(TextElem::font_in(existing).into_iter().cloned())
- .collect(),
- )));
- }
-}
diff --git a/crates/typst-library/src/shared/mod.rs b/crates/typst-library/src/shared/mod.rs
deleted file mode 100644
index f54241cf..00000000
--- a/crates/typst-library/src/shared/mod.rs
+++ /dev/null
@@ -1,7 +0,0 @@
-//! Shared definitions for the standard library.
-
-mod behave;
-mod ext;
-
-pub use behave::*;
-pub use ext::*;
diff --git a/crates/typst-library/src/symbols/emoji.rs b/crates/typst-library/src/symbols/emoji.rs
deleted file mode 100644
index b35dfcaa..00000000
--- a/crates/typst-library/src/symbols/emoji.rs
+++ /dev/null
@@ -1,1356 +0,0 @@
-use typst::eval::{symbols, Module, Scope, Symbol};
-
-/// A module with all emoji.
-pub fn emoji() -> Module {
- let mut scope = Scope::new();
- for (name, symbol) in EMOJI {
- scope.define(*name, symbol.clone());
- }
- Module::new("emoji", scope)
-}
-
-/// A list of named emoji.
-const EMOJI: &[(&str, Symbol)] = symbols! {
- abacus: '🧮',
- abc: '🔤',
- abcd: '🔡',
- ABCD: '🔠',
- accordion: '🪗',
- aesculapius: '⚕',
- airplane: [
- '✈',
- landing: '🛬',
- small: '🛩',
- takeoff: '🛫',
- ],
- alembic: '⚗',
- alien: ['👽', monster: '👾'],
- ambulance: '🚑',
- amphora: '🏺',
- anchor: '⚓',
- anger: '💢',
- ant: '🐜',
- apple: [green: '🍏', red: '🍎'],
- arm: [mech: '🦾', muscle: '💪', selfie: '🤳'],
- arrow: [
- r.filled: '➡',
- r.hook: '↪',
- r.soon: '🔜',
- l.filled: '⬅',
- l.hook: '↩',
- l.back: '🔙',
- l.end: '🔚',
- t.filled: '⬆',
- t.curve: '⤴',
- t.top: '🔝',
- b.filled: '⬇',
- b.curve: '⤵',
- l.r: '↔',
- l.r.on: '🔛',
- t.b: '↕',
- bl: '↙',
- br: '↘',
- tl: '↖',
- tr: '↗',
- ],
- arrows: [cycle: '🔄'],
- ast: ['*', box: '✳'],
- atm: '🏧',
- atom: '⚛',
- aubergine: '🍆',
- avocado: '🥑',
- axe: '🪓',
- baby: ['👶', angel: '👼', box: '🚼'],
- babybottle: '🍼',
- backpack: '🎒',
- bacon: '🥓',
- badger: '🦡',
- badminton: '🏸',
- bagel: '🥯',
- baggageclaim: '🛄',
- baguette: '🥖',
- balloon: '🎈',
- ballot: [check: '☑'],
- ballotbox: '🗳',
- banana: '🍌',
- banjo: '🪕',
- bank: '🏦',
- barberpole: '💈',
- baseball: '⚾',
- basecap: '🧢',
- basket: '🧺',
- basketball: ['⛹', ball: '🏀'],
- bat: '🦇',
- bathtub: ['🛀', foam: '🛁'],
- battery: ['🔋', low: '🪫'],
- beach: [palm: '🏝', umbrella: '🏖'],
- beads: '📿',
- beans: '🫘',
- bear: '🐻',
- beaver: '🦫',
- bed: ['🛏', person: '🛌'],
- bee: '🐝',
- beer: ['🍺', clink: '🍻'],
- beetle: ['🪲', lady: '🐞'],
- bell: ['🔔', ding: '🛎', not: '🔕'],
- bento: '🍱',
- bicyclist: ['🚴', mountain: '🚵'],
- bike: ['🚲', not: '🚳'],
- bikini: '👙',
- billiards: '🎱',
- bin: '🗑',
- biohazard: '☣',
- bird: '🐦',
- bison: '🦬',
- blood: '🩸',
- blouse: '👚',
- blowfish: '🐡',
- blueberries: '🫐',
- boar: '🐗',
- boat: [
- sail: '⛵',
- row: '🚣',
- motor: '🛥',
- speed: '🚤',
- canoe: '🛶',
- ],
- bolt: '🔩',
- bomb: '💣',
- bone: '🦴',
- book: [
- red: '📕',
- blue: '📘',
- green: '📗',
- orange: '📙',
- spiral: '📒',
- open: '📖',
- ],
- bookmark: '🔖',
- books: '📚',
- boomerang: '🪃',
- bordercontrol: '🛂',
- bouquet: '💐',
- bow: '🏹',
- bowl: [spoon: '🥣', steam: '🍜'],
- bowling: '🎳',
- boxing: '🥊',
- boy: '👦',
- brain: '🧠',
- bread: '🍞',
- brick: '🧱',
- bride: '👰',
- bridge: [fog: '🌁', night: '🌉'],
- briefcase: '💼',
- briefs: '🩲',
- brightness: [high: '🔆', low: '🔅'],
- broccoli: '🥦',
- broom: '🧹',
- brush: '🖌',
- bubble: [
- speech.r: '💬',
- speech.l: '🗨',
- thought: '💭',
- anger.r: '🗯',
- ],
- bubbles: '🫧',
- bubbletea: '🧋',
- bucket: '🪣',
- buffalo: [water: '🐃'],
- bug: '🐛',
- builder: '👷',
- burger: '🍔',
- burrito: '🌯',
- bus: [
- '🚌',
- front: '🚍',
- small: '🚐',
- stop: '🚏',
- trolley: '🚎',
- ],
- butter: '🧈',
- butterfly: '🦋',
- button: ['🔲', alt: '🔳', radio: '🔘'],
- cabinet: [file: '🗄'],
- cablecar: ['🚠', small: '🚡'],
- cactus: '🌵',
- cake: [
- '🎂',
- fish: '🍥',
- moon: '🥮',
- slice: '🍰',
- ],
- calendar: ['📅', spiral: '🗓', tearoff: '📆'],
- camel: ['🐫', dromedar: '🐪'],
- camera: [
- '📷',
- flash: '📸',
- movie: '🎥',
- movie.box: '🎦',
- video: '📹',
- ],
- camping: '🏕',
- can: '🥫',
- candle: '🕯',
- candy: '🍬',
- cane: '🦯',
- car: [
- '🚗',
- front: '🚘',
- pickup: '🛻',
- police: '🚓',
- police.front: '🚔',
- racing: '🏎',
- rickshaw: '🛺',
- suv: '🚙',
- ],
- card: [credit: '💳', id: '🪪'],
- cardindex: '📇',
- carrot: '🥕',
- cart: '🛒',
- cassette: '📼',
- castle: [eu: '🏰', jp: '🏯'],
- cat: [
- '🐈',
- face: '🐱',
- face.angry: '😾',
- face.cry: '😿',
- face.heart: '😻',
- face.joy: '😹',
- face.kiss: '😽',
- face.laugh: '😸',
- face.shock: '🙀',
- face.smile: '😺',
- face.smirk: '😼',
- ],
- chain: '🔗',
- chains: '⛓',
- chair: '🪑',
- champagne: '🍾',
- chart: [
- bar: '📊',
- up: '📈',
- down: '📉',
- yen.up: '💹',
- ],
- checkmark: [heavy: '✔', box: '✅'],
- cheese: '🧀',
- cherries: '🍒',
- chess: '♟',
- chestnut: '🌰',
- chicken: [
- '🐔',
- baby: '🐥',
- baby.egg: '🐣',
- baby.head: '🐤',
- leg: '🍗',
- male: '🐓',
- ],
- child: '🧒',
- chipmunk: '🐿',
- chocolate: '🍫',
- chopsticks: '🥢',
- church: ['⛪', love: '💒'],
- cigarette: ['🚬', not: '🚭'],
- circle: [
- black: '⚫',
- blue: '🔵',
- brown: '🟤',
- green: '🟢',
- orange: '🟠',
- purple: '🟣',
- white: '⚪',
- red: '🔴',
- yellow: '🟡',
- stroked: '⭕',
- ],
- circus: '🎪',
- city: [
- '🏙',
- dusk: '🌆',
- night: '🌃',
- sunset: '🌇',
- ],
- clamp: '🗜',
- clapperboard: '🎬',
- climbing: '🧗',
- clip: '📎',
- clipboard: '📋',
- clips: '🖇',
- clock: [
- one: '🕐',
- one.thirty: '🕜',
- two: '🕑',
- two.thirty: '🕝',
- three: '🕒',
- three.thirty: '🕞',
- four: '🕓',
- four.thirty: '🕟',
- five: '🕔',
- five.thirty: '🕠',
- six: '🕕',
- six.thirty: '🕡',
- seven: '🕖',
- seven.thirty: '🕢',
- eight: '🕗',
- eight.thirty: '🕣',
- nine: '🕘',
- nine.thirty: '🕤',
- ten: '🕙',
- ten.thirty: '🕥',
- eleven: '🕚',
- eleven.thirty: '🕦',
- twelve: '🕛',
- twelve.thirty: '🕧',
- alarm: '⏰',
- old: '🕰',
- timer: '⏲',
- ],
- cloud: [
- '☁',
- dust: '💨',
- rain: '🌧',
- snow: '🌨',
- storm: '⛈',
- sun: '⛅',
- sun.hidden: '🌥',
- sun.rain: '🌦',
- thunder: '🌩',
- ],
- coat: ['🧥', lab: '🥼'],
- cockroach: '🪳',
- cocktail: [martini: '🍸', tropical: '🍹'],
- coconut: '🥥',
- coffee: '☕',
- coffin: '⚰',
- coin: '🪙',
- comet: '☄',
- compass: '🧭',
- computer: '🖥',
- computermouse: '🖱',
- confetti: '🎊',
- construction: '🚧',
- controller: '🎮',
- cookie: ['🍪', fortune: '🥠'],
- cooking: '🍳',
- cool: '🆒',
- copyright: '©',
- coral: '🪸',
- corn: '🌽',
- couch: '🛋',
- couple: '💑',
- cow: ['🐄', face: '🐮'],
- crab: '🦀',
- crane: '🏗',
- crayon: '🖍',
- cricket: '🦗',
- cricketbat: '🏏',
- crocodile: '🐊',
- croissant: '🥐',
- crossmark: ['❌', box: '❎'],
- crown: '👑',
- crutch: '🩼',
- crystal: '🔮',
- cucumber: '🥒',
- cup: [straw: '🥤'],
- cupcake: '🧁',
- curling: '🥌',
- curry: '🍛',
- custard: '🍮',
- customs: '🛃',
- cutlery: '🍴',
- cyclone: '🌀',
- dancing: [man: '🕺', woman: '💃', women.bunny: '👯'],
- darts: '🎯',
- dash: [wave.double: '〰'],
- deer: '🦌',
- desert: '🏜',
- detective: '🕵',
- diamond: [
- blue: '🔷',
- blue.small: '🔹',
- orange: '🔶',
- orange.small: '🔸',
- dot: '💠',
- ],
- die: '🎲',
- dino: [pod: '🦕', rex: '🦖'],
- disc: [cd: '💿', dvd: '📀', mini: '💽'],
- discoball: '🪩',
- diving: '🤿',
- dodo: '🦤',
- dog: [
- '🐕',
- face: '🐶',
- guide: '🦮',
- poodle: '🐩',
- ],
- dollar: '💲',
- dolphin: '🐬',
- donut: '🍩',
- door: '🚪',
- dove: [peace: '🕊'],
- dragon: ['🐉', face: '🐲'],
- dress: ['👗', kimono: '👘', sari: '🥻'],
- drop: '💧',
- drops: '💦',
- drum: ['🥁', big: '🪘'],
- duck: '🦆',
- dumpling: '🥟',
- eagle: '🦅',
- ear: ['👂', aid: '🦻'],
- egg: '🥚',
- eighteen: [not: '🔞'],
- elephant: '🐘',
- elevator: '🛗',
- elf: '🧝',
- email: '📧',
- excl: [
- '❗',
- white: '❕',
- double: '‼',
- quest: '⁉',
- ],
- explosion: '💥',
- extinguisher: '🧯',
- eye: '👁',
- eyes: '👀',
- face: [
- grin: '😀',
- angry: '😠',
- angry.red: '😡',
- anguish: '😧',
- astonish: '😲',
- bandage: '🤕',
- beam: '😁',
- blank: '😶',
- clown: '🤡',
- cold: '🥶',
- concern: '😦',
- cool: '😎',
- cover: '🤭',
- cowboy: '🤠',
- cry: '😭',
- devil.smile: '😈',
- devil.frown: '👿',
- diagonal: '🫤',
- disguise: '🥸',
- dizzy: '😵',
- dotted: '🫥',
- down: '😞',
- down.sweat: '😓',
- drool: '🤤',
- explode: '🤯',
- eyeroll: '🙄',
- friendly: '☺',
- fear: '😨',
- fear.sweat: '😰',
- fever: '🤒',
- flush: '😳',
- frown: '☹',
- frown.slight: '🙁',
- frust: '😣',
- goofy: '🤪',
- halo: '😇',
- happy: '😊',
- heart: '😍',
- hearts: '🥰',
- heat: '🥵',
- hug: '🤗',
- inv: '🙃',
- joy: '😂',
- kiss: '😗',
- kiss.smile: '😙',
- kiss.heart: '😘',
- kiss.blush: '😚',
- lick: '😋',
- lie: '🤥',
- mask: '😷',
- meh: '😒',
- melt: '🫠',
- money: '🤑',
- monocle: '🧐',
- nausea: '🤢',
- nerd: '🤓',
- neutral: '😐',
- open: '😃',
- party: '🥳',
- peek: '🫣',
- plead: '🥺',
- relief: '😌',
- rofl: '🤣',
- sad: '😔',
- salute: '🫡',
- shock: '😱',
- shush: '🤫',
- skeptic: '🤨',
- sleep: '😴',
- sleepy: '😪',
- smile: '😄',
- smile.slight: '🙂',
- smile.sweat: '😅',
- smile.tear: '🥲',
- smirk: '😏',
- sneeze: '🤧',
- speak.not: '🫢',
- squint: '😆',
- stars: '🤩',
- straight: '😑',
- suffer: '😖',
- surprise: '😯',
- symbols: '🤬',
- tear: '😢',
- tear.relief: '😥',
- tear.withheld: '🥹',
- teeth: '😬',
- think: '🤔',
- tired: '😫',
- tongue: '😛',
- tongue.squint: '😝',
- tongue.wink: '😜',
- triumph: '😤',
- unhappy: '😕',
- vomit: '🤮',
- weary: '😩',
- wink: '😉',
- woozy: '🥴',
- worry: '😟',
- wow: '😮',
- yawn: '🥱',
- zip: '🤐',
- ],
- factory: '🏭',
- fairy: '🧚',
- faith: [
- christ: '✝',
- dharma: '☸',
- islam: '☪',
- judaism: '✡',
- menorah: '🕎',
- om: '🕉',
- orthodox: '☦',
- peace: '☮',
- star.dot: '🔯',
- worship: '🛐',
- yinyang: '☯',
- ],
- falafel: '🧆',
- family: '👪',
- fax: '📠',
- feather: '🪶',
- feeding: [breast: '🤱'],
- fencing: '🤺',
- ferriswheel: '🎡',
- filebox: '🗃',
- filedividers: '🗂',
- film: '🎞',
- finger: [
- r: '👉',
- l: '👈',
- t: '👆',
- t.alt: '☝',
- b: '👇',
- front: '🫵',
- m: '🖕',
- ],
- fingers: [cross: '🤞', pinch: '🤌', snap: '🫰'],
- fire: '🔥',
- firecracker: '🧨',
- fireengine: '🚒',
- fireworks: '🎆',
- fish: ['🐟', tropical: '🐠'],
- fishing: '🎣',
- fist: [
- front: '👊',
- r: '🤜',
- l: '🤛',
- raised: '✊',
- ],
- flag: [
- black: '🏴',
- white: '🏳',
- goal: '🏁',
- golf: '⛳',
- red: '🚩',
- ],
- flags: [jp.crossed: '🎌'],
- flamingo: '🦩',
- flashlight: '🔦',
- flatbread: '🫓',
- fleur: '⚜',
- floppy: '💾',
- flower: [
- hibiscus: '🌺',
- lotus: '🪷',
- pink: '🌸',
- rose: '🌹',
- sun: '🌻',
- tulip: '🌷',
- white: '💮',
- wilted: '🥀',
- yellow: '🌼',
- ],
- fly: '🪰',
- fog: '🌫',
- folder: ['📁', open: '📂'],
- fondue: '🫕',
- foot: '🦶',
- football: ['⚽', am: '🏈'],
- forex: '💱',
- fountain: '⛲',
- fox: '🦊',
- free: '🆓',
- fries: '🍟',
- frisbee: '🥏',
- frog: [face: '🐸'],
- fuelpump: '⛽',
- garlic: '🧄',
- gear: '⚙',
- gem: '💎',
- genie: '🧞',
- ghost: '👻',
- giraffe: '🦒',
- girl: '👧',
- glass: [
- clink: '🥂',
- milk: '🥛',
- pour: '🫗',
- tumbler: '🥃',
- ],
- glasses: ['👓', sun: '🕶'],
- globe: [
- am: '🌎',
- as.au: '🌏',
- eu.af: '🌍',
- meridian: '🌐',
- ],
- gloves: '🧤',
- goal: '🥅',
- goat: '🐐',
- goggles: '🥽',
- golfing: '🏌',
- gorilla: '🦍',
- grapes: '🍇',
- guard: [man: '💂'],
- guitar: '🎸',
- gymnastics: '🤸',
- haircut: '💇',
- hammer: ['🔨', pick: '⚒', wrench: '🛠'],
- hamsa: '🪬',
- hamster: [face: '🐹'],
- hand: [
- raised: '✋',
- raised.alt: '🤚',
- r: '🫱',
- l: '🫲',
- t: '🫴',
- b: '🫳',
- ok: '👌',
- call: '🤙',
- love: '🤟',
- part: '🖖',
- peace: '✌',
- pinch: '🤏',
- rock: '🤘',
- splay: '🖐',
- wave: '👋',
- write: '✍',
- ],
- handbag: '👜',
- handball: '🤾',
- handholding: [man.man: '👬', woman.man: '👫', woman.woman: '👭'],
- hands: [
- folded: '🙏',
- palms: '🤲',
- clap: '👏',
- heart: '🫶',
- open: '👐',
- raised: '🙌',
- shake: '🤝',
- ],
- hash: '#',
- hat: [ribbon: '👒', top: '🎩'],
- headphone: '🎧',
- heart: [
- '❤',
- arrow: '💘',
- beat: '💓',
- black: '🖤',
- blue: '💙',
- box: '💟',
- broken: '💔',
- brown: '🤎',
- double: '💕',
- excl: '❣',
- green: '💚',
- grow: '💗',
- orange: '🧡',
- purple: '💜',
- real: '🫀',
- revolve: '💞',
- ribbon: '💝',
- spark: '💖',
- white: '🤍',
- yellow: '💛',
- ],
- hedgehog: '🦔',
- helicopter: '🚁',
- helix: '🧬',
- helmet: [cross: '⛑', military: '🪖'],
- hippo: '🦛',
- hockey: '🏑',
- hole: '🕳',
- honey: '🍯',
- hongbao: '🧧',
- hook: '🪝',
- horn: [postal: '📯'],
- horse: [
- '🐎',
- carousel: '🎠',
- face: '🐴',
- race: '🏇',
- ],
- hospital: '🏥',
- hotdog: '🌭',
- hotel: ['🏨', love: '🏩'],
- hotspring: '♨',
- hourglass: ['⌛', flow: '⏳'],
- house: [
- '🏠',
- derelict: '🏚',
- garden: '🏡',
- multiple: '🏘',
- ],
- hundred: '💯',
- hut: '🛖',
- ice: '🧊',
- icecream: ['🍨', shaved: '🍧', soft: '🍦'],
- icehockey: '🏒',
- id: '🆔',
- info: 'ℹ',
- izakaya: '🏮',
- jar: '🫙',
- jeans: '👖',
- jigsaw: '🧩',
- joystick: '🕹',
- juggling: '🤹',
- juice: '🧃',
- kaaba: '🕋',
- kadomatsu: '🎍',
- kangaroo: '🦘',
- gachi: '🈷',
- go: '🈴',
- hi: '㊙',
- ka: '🉑',
- kachi: '🈹',
- kara: '🈳',
- kon: '🈲',
- man: '🈵',
- muryo: '🈚',
- shin: '🈸',
- shuku: '㊗',
- toku: '🉐',
- yo: '🈺',
- yubi: '🈯',
- yuryo: '🈶',
- koko: '🈁',
- sa: '🈂',
- kebab: '🥙',
- key: ['🔑', old: '🗝'],
- keyboard: '⌨',
- kiss: '💏',
- kissmark: '💋',
- kite: '🪁',
- kiwi: '🥝',
- knife: ['🔪', dagger: '🗡'],
- knot: '🪢',
- koala: '🐨',
- koinobori: '🎏',
- label: '🏷',
- lacrosse: '🥍',
- ladder: '🪜',
- lamp: [diya: '🪔'],
- laptop: '💻',
- a: '🅰',
- ab: '🆎',
- b: '🅱',
- cl: '🆑',
- o: '🅾',
- leaf: [
- clover.three: '☘',
- clover.four: '🍀',
- fall: '🍂',
- herb: '🌿',
- maple: '🍁',
- wind: '🍃',
- ],
- leftluggage: '🛅',
- leg: ['🦵', mech: '🦿'],
- lemon: '🍋',
- leopard: '🐆',
- letter: [love: '💌'],
- liberty: '🗽',
- lightbulb: '💡',
- lightning: '⚡',
- lion: '🦁',
- lipstick: '💄',
- litter: ['🚮', not: '🚯'],
- lizard: '🦎',
- llama: '🦙',
- lobster: '🦞',
- lock: [
- '🔒',
- key: '🔐',
- open: '🔓',
- pen: '🔏',
- ],
- lollipop: '🍭',
- lotion: '🧴',
- luggage: '🧳',
- lungs: '🫁',
- mage: '🧙',
- magnet: '🧲',
- magnify: [r: '🔎', l: '🔍'],
- mahjong: [dragon.red: '🀄'],
- mail: ['✉', arrow: '📩'],
- mailbox: [
- closed.empty: '📪',
- closed.full: '📫',
- open.empty: '📭',
- open.full: '📬',
- ],
- mammoth: '🦣',
- man: [
- '👨',
- box: '🚹',
- crown: '🤴',
- guapimao: '👲',
- levitate: '🕴',
- old: '👴',
- pregnant: '🫃',
- turban: '👳',
- tuxedo: '🤵',
- ],
- mango: '🥭',
- map: [world: '🗺', jp: '🗾'],
- martialarts: '🥋',
- masks: '🎭',
- mate: '🧉',
- matryoshka: '🪆',
- meat: ['🥩', bone: '🍖'],
- medal: [
- first: '🥇',
- second: '🥈',
- third: '🥉',
- sports: '🏅',
- military: '🎖',
- ],
- megaphone: ['📢', simple: '📣'],
- melon: '🍈',
- merperson: '🧜',
- metro: 'Ⓜ',
- microbe: '🦠',
- microphone: ['🎤', studio: '🎙'],
- microscope: '🔬',
- milkyway: '🌌',
- mirror: '🪞',
- mixer: '🎛',
- money: [
- bag: '💰',
- dollar: '💵',
- euro: '💶',
- pound: '💷',
- yen: '💴',
- wings: '💸',
- ],
- monkey: [
- '🐒',
- face: '🐵',
- hear.not: '🙉',
- see.not: '🙈',
- speak.not: '🙊',
- ],
- moon: [
- crescent: '🌙',
- full: '🌕',
- full.face: '🌝',
- new: '🌑',
- new.face: '🌚',
- wane.one: '🌖',
- wane.two: '🌗',
- wane.three.face: '🌜',
- wane.three: '🌘',
- wax.one: '🌒',
- wax.two: '🌓',
- wax.two.face: '🌛',
- wax.three: '🌔',
- ],
- mortarboard: '🎓',
- mosque: '🕌',
- mosquito: '🦟',
- motorcycle: '🏍',
- motorway: '🛣',
- mountain: [
- '⛰',
- fuji: '🗻',
- snow: '🏔',
- sunrise: '🌄',
- ],
- mouse: ['🐁', face: '🐭'],
- mousetrap: '🪤',
- mouth: ['👄', bite: '🫦'],
- moyai: '🗿',
- museum: '🏛',
- mushroom: '🍄',
- musicalscore: '🎼',
- nails: [polish: '💅'],
- namebadge: '📛',
- nazar: '🧿',
- necktie: '👔',
- needle: '🪡',
- nest: [empty: '🪹', eggs: '🪺'],
- new: '🆕',
- newspaper: ['📰', rolled: '🗞'],
- ng: '🆖',
- ningyo: '🎎',
- ninja: '🥷',
- noentry: '⛔',
- nose: '👃',
- notebook: ['📓', deco: '📔'],
- notepad: '🗒',
- notes: ['🎵', triple: '🎶'],
- numbers: '🔢',
- octopus: '🐙',
- office: '🏢',
- oil: '🛢',
- ok: '🆗',
- olive: '🫒',
- oni: '👹',
- onion: '🧅',
- orangutan: '🦧',
- otter: '🦦',
- owl: '🦉',
- ox: '🐂',
- oyster: '🦪',
- package: '📦',
- paella: '🥘',
- page: ['📄', curl: '📃', pencil: '📝'],
- pager: '📟',
- pages: [tabs: '📑'],
- painting: '🖼',
- palette: '🎨',
- pancakes: '🥞',
- panda: '🐼',
- parachute: '🪂',
- park: '🏞',
- parking: '🅿',
- parrot: '🦜',
- partalteration: '〽',
- party: '🎉',
- peach: '🍑',
- peacock: '🦚',
- peanuts: '🥜',
- pear: '🍐',
- pedestrian: ['🚶', not: '🚷'],
- pen: [ball: '🖊', fountain: '🖋'],
- pencil: '✏',
- penguin: '🐧',
- pepper: ['🫑', hot: '🌶'],
- person: [
- '🧑',
- angry: '🙎',
- beard: '🧔',
- blonde: '👱',
- bow: '🙇',
- crown: '🫅',
- deaf: '🧏',
- facepalm: '🤦',
- frown: '🙍',
- hijab: '🧕',
- kneel: '🧎',
- lotus: '🧘',
- massage: '💆',
- no: '🙅',
- ok: '🙆',
- old: '🧓',
- pregnant: '🫄',
- raise: '🙋',
- sassy: '💁',
- shrug: '🤷',
- stand: '🧍',
- steam: '🧖',
- ],
- petri: '🧫',
- phone: [
- '📱',
- arrow: '📲',
- classic: '☎',
- not: '📵',
- off: '📴',
- receiver: '📞',
- signal: '📶',
- vibrate: '📳',
- ],
- piano: '🎹',
- pick: '⛏',
- pie: '🥧',
- pig: ['🐖', face: '🐷', nose: '🐽'],
- pill: '💊',
- pin: ['📌', round: '📍'],
- pinata: '🪅',
- pineapple: '🍍',
- pingpong: '🏓',
- pistol: '🔫',
- pizza: '🍕',
- placard: '🪧',
- planet: '🪐',
- plant: '🪴',
- plaster: '🩹',
- plate: [cutlery: '🍽'],
- playback: [
- down: '⏬',
- eject: '⏏',
- forward: '⏩',
- pause: '⏸',
- record: '⏺',
- repeat: '🔁',
- repeat.once: '🔂',
- repeat.v: '🔃',
- restart: '⏮',
- rewind: '⏪',
- shuffle: '🔀',
- skip: '⏭',
- stop: '⏹',
- toggle: '⏯',
- up: '⏫',
- ],
- playingcard: [flower: '🎴', joker: '🃏'],
- plunger: '🪠',
- policeofficer: '👮',
- poo: '💩',
- popcorn: '🍿',
- post: [eu: '🏤', jp: '🏣'],
- postbox: '📮',
- potato: ['🥔', sweet: '🍠'],
- pouch: '👝',
- powerplug: '🔌',
- present: '🎁',
- pretzel: '🥨',
- printer: '🖨',
- prints: [foot: '👣', paw: '🐾'],
- prohibited: '🚫',
- projector: '📽',
- pumpkin: [lantern: '🎃'],
- purse: '👛',
- quest: ['❓', white: '❔'],
- rabbit: ['🐇', face: '🐰'],
- raccoon: '🦝',
- radio: '📻',
- radioactive: '☢',
- railway: '🛤',
- rainbow: '🌈',
- ram: '🐏',
- rat: '🐀',
- razor: '🪒',
- receipt: '🧾',
- recycling: '♻',
- reg: '®',
- restroom: '🚻',
- rhino: '🦏',
- ribbon: ['🎀', remind: '🎗'],
- rice: [
- '🍚',
- cracker: '🍘',
- ear: '🌾',
- onigiri: '🍙',
- ],
- ring: '💍',
- ringbuoy: '🛟',
- robot: '🤖',
- rock: '🪨',
- rocket: '🚀',
- rollercoaster: '🎢',
- rosette: '🏵',
- rugby: '🏉',
- ruler: ['📏', triangle: '📐'],
- running: '🏃',
- safetypin: '🧷',
- safetyvest: '🦺',
- sake: '🍶',
- salad: '🥗',
- salt: '🧂',
- sandwich: '🥪',
- santa: [man: '🎅', woman: '🤶'],
- satdish: '📡',
- satellite: '🛰',
- saw: '🪚',
- saxophone: '🎷',
- scales: '⚖',
- scarf: '🧣',
- school: '🏫',
- scissors: '✂',
- scooter: ['🛴', motor: '🛵'],
- scorpion: '🦂',
- screwdriver: '🪛',
- scroll: '📜',
- seal: '🦭',
- seat: '💺',
- seedling: '🌱',
- shark: '🦈',
- sheep: '🐑',
- shell: [spiral: '🐚'],
- shield: '🛡',
- ship: ['🚢', cruise: '🛳', ferry: '⛴'],
- shirt: [sports: '🎽', t: '👕'],
- shoe: [
- '👞',
- ballet: '🩰',
- flat: '🥿',
- heel: '👠',
- hike: '🥾',
- ice: '⛸',
- roller: '🛼',
- sandal.heel: '👡',
- ski: '🎿',
- sneaker: '👟',
- tall: '👢',
- thong: '🩴',
- ],
- shopping: '🛍',
- shorts: '🩳',
- shoshinsha: '🔰',
- shower: '🚿',
- shrimp: ['🦐', fried: '🍤'],
- shrine: '⛩',
- sign: [crossing: '🚸', stop: '🛑'],
- silhouette: [
- '👤',
- double: '👥',
- hug: '🫂',
- speak: '🗣',
- ],
- siren: '🚨',
- skateboard: '🛹',
- skewer: [dango: '🍡', oden: '🍢'],
- skiing: '⛷',
- skull: ['💀', bones: '☠'],
- skunk: '🦨',
- sled: '🛷',
- slide: '🛝',
- slider: '🎚',
- sloth: '🦥',
- slots: '🎰',
- snail: '🐌',
- snake: '🐍',
- snowboarding: '🏂',
- snowflake: '❄',
- snowman: ['⛄', snow: '☃'],
- soap: '🧼',
- socks: '🧦',
- softball: '🥎',
- sos: '🆘',
- soup: '🍲',
- spaghetti: '🍝',
- sparkle: [box: '❇'],
- sparkler: '🎇',
- sparkles: '✨',
- speaker: [
- '🔈',
- not: '🔇',
- wave: '🔉',
- waves: '🔊',
- ],
- spider: '🕷',
- spiderweb: '🕸',
- spinach: '🥬',
- sponge: '🧽',
- spoon: '🥄',
- square: [
- black: '⬛',
- black.tiny: '▪',
- black.small: '◾',
- black.medium: '◼',
- white: '⬜',
- white.tiny: '▫',
- white.small: '◽',
- white.medium: '◻',
- blue: '🟦',
- brown: '🟫',
- green: '🟩',
- orange: '🟧',
- purple: '🟪',
- red: '🟥',
- yellow: '🟨',
- ],
- squid: '🦑',
- stadium: '🏟',
- star: [
- '⭐',
- arc: '💫',
- box: '✴',
- glow: '🌟',
- shoot: '🌠',
- ],
- stethoscope: '🩺',
- store: [big: '🏬', small: '🏪'],
- strawberry: '🍓',
- suit: [
- club: '♣',
- diamond: '♦',
- heart: '♥',
- spade: '♠',
- ],
- sun: ['☀', cloud: '🌤', face: '🌞'],
- sunrise: '🌅',
- superhero: '🦸',
- supervillain: '🦹',
- surfing: '🏄',
- sushi: '🍣',
- swan: '🦢',
- swimming: '🏊',
- swimsuit: '🩱',
- swords: '⚔',
- symbols: '🔣',
- synagogue: '🕍',
- syringe: '💉',
- taco: '🌮',
- takeout: '🥡',
- tamale: '🫔',
- tanabata: '🎋',
- tangerine: '🍊',
- tap: ['🚰', not: '🚱'],
- taxi: ['🚕', front: '🚖'],
- teacup: '🍵',
- teapot: '🫖',
- teddy: '🧸',
- telescope: '🔭',
- temple: '🛕',
- ten: '🔟',
- tengu: '👺',
- tennis: '🎾',
- tent: '⛺',
- testtube: '🧪',
- thermometer: '🌡',
- thread: '🧵',
- thumb: [up: '👍', down: '👎'],
- ticket: [event: '🎟', travel: '🎫'],
- tiger: ['🐅', face: '🐯'],
- tm: '™',
- toilet: '🚽',
- toiletpaper: '🧻',
- tomato: '🍅',
- tombstone: '🪦',
- tongue: '👅',
- toolbox: '🧰',
- tooth: '🦷',
- toothbrush: '🪥',
- tornado: '🌪',
- tower: [tokyo: '🗼'],
- trackball: '🖲',
- tractor: '🚜',
- trafficlight: [v: '🚦', h: '🚥'],
- train: [
- '🚆',
- car: '🚃',
- light: '🚈',
- metro: '🚇',
- mono: '🚝',
- mountain: '🚞',
- speed: '🚄',
- speed.bullet: '🚅',
- steam: '🚂',
- stop: '🚉',
- suspend: '🚟',
- tram: '🚊',
- tram.car: '🚋',
- ],
- transgender: '⚧',
- tray: [inbox: '📥', mail: '📨', outbox: '📤'],
- tree: [
- deciduous: '🌳',
- evergreen: '🌲',
- palm: '🌴',
- xmas: '🎄',
- ],
- triangle: [
- r: '▶',
- l: '◀',
- t: '🔼',
- b: '🔽',
- t.red: '🔺',
- b.red: '🔻',
- ],
- trident: '🔱',
- troll: '🧌',
- trophy: '🏆',
- truck: ['🚚', trailer: '🚛'],
- trumpet: '🎺',
- tsukimi: '🎑',
- turkey: '🦃',
- turtle: '🐢',
- tv: '📺',
- ufo: '🛸',
- umbrella: [
- open: '☂',
- closed: '🌂',
- rain: '☔',
- sun: '⛱',
- ],
- unicorn: '🦄',
- unknown: '🦳',
- up: '🆙',
- urn: '⚱',
- vampire: '🧛',
- violin: '🎻',
- volcano: '🌋',
- volleyball: '🏐',
- vs: '🆚',
- waffle: '🧇',
- wand: '🪄',
- warning: '⚠',
- watch: ['⌚', stop: '⏱'],
- watermelon: '🍉',
- waterpolo: '🤽',
- wave: '🌊',
- wc: '🚾',
- weightlifting: '🏋',
- whale: ['🐋', spout: '🐳'],
- wheel: '🛞',
- wheelchair: ['🦽', box: '♿', motor: '🦼'],
- wind: '🌬',
- windchime: '🎐',
- window: '🪟',
- wine: '🍷',
- wolf: '🐺',
- woman: [
- '👩',
- box: '🚺',
- crown: '👸',
- old: '👵',
- pregnant: '🤰',
- ],
- wood: '🪵',
- worm: '🪱',
- wrench: '🔧',
- wrestling: '🤼',
- xray: '🩻',
- yarn: '🧶',
- yoyo: '🪀',
- zebra: '🦓',
- zodiac: [
- aquarius: '♒',
- aries: '♈',
- cancer: '♋',
- capri: '♑',
- gemini: '♊',
- leo: '♌',
- libra: '♎',
- ophi: '⛎',
- pisces: '♓',
- sagit: '♐',
- scorpio: '♏',
- taurus: '♉',
- virgo: '♍',
- ],
- zombie: '🧟',
- zzz: '💤',
-};
diff --git a/crates/typst-library/src/symbols/mod.rs b/crates/typst-library/src/symbols/mod.rs
deleted file mode 100644
index 0d288c3b..00000000
--- a/crates/typst-library/src/symbols/mod.rs
+++ /dev/null
@@ -1,17 +0,0 @@
-//! Modifiable symbols.
-
-mod emoji;
-mod sym;
-
-pub use emoji::*;
-pub use sym::*;
-
-use crate::prelude::*;
-
-/// Hook up all symbol definitions.
-pub(super) fn define(global: &mut Scope) {
- 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
deleted file mode 100644
index 79696a38..00000000
--- a/crates/typst-library/src/symbols/sym.rs
+++ /dev/null
@@ -1,873 +0,0 @@
-use typst::eval::{symbols, Module, Scope, Symbol};
-
-/// A module with all general symbols.
-pub fn sym() -> Module {
- let mut scope = Scope::new();
- for (name, symbol) in SYM {
- scope.define(*name, symbol.clone());
- }
- Module::new("sym", scope)
-}
-
-/// The list of general symbols.
-pub(crate) const SYM: &[(&str, Symbol)] = symbols! {
- // Control.
- wj: '\u{2060}',
- zwj: '\u{200D}',
- zwnj: '\u{200C}',
- zws: '\u{200B}',
-
- // Spaces.
- space: [
- ' ',
- nobreak: '\u{A0}',
- en: '\u{2002}',
- quad: '\u{2003}',
- third: '\u{2004}',
- quarter: '\u{2005}',
- sixth: '\u{2006}',
- med: '\u{205F}',
- fig: '\u{2007}',
- punct: '\u{2008}',
- thin: '\u{2009}',
- hair: '\u{200A}',
- ],
-
- // Delimiters.
- paren: [l: '(', r: ')', t: '⏜', b: '⏝'],
- brace: [l: '{', r: '}', t: '⏞', b: '⏟'],
- bracket: [l: '[', l.double: '⟦', r: ']', r.double: '⟧', t: '⎴', b: '⎵'],
- turtle: [l: '〔', r: '〕', t: '⏠', b: '⏡'],
- bar: [v: '|', v.double: '‖', v.triple: '⦀', v.broken: '¦', v.circle: '⦶', h: '―'],
- fence: [l: '⧘', l.double: '⧚', r: '⧙', r.double: '⧛', dotted: '⦙'],
- angle: [
- '∠',
- l: '⟨',
- r: '⟩',
- l.double: '《',
- r.double: '》',
- acute: '⦟',
- arc: '∡',
- arc.rev: '⦛',
- rev: '⦣',
- right: '∟',
- right.rev: '⯾',
- right.arc: '⊾',
- right.dot: '⦝',
- right.sq: '⦜',
- spatial: '⟀',
- spheric: '∢',
- spheric.rev: '⦠',
- spheric.top: '⦡',
- ],
-
- // Punctuation.
- amp: ['&', inv: '⅋'],
- ast: [
- op: '∗',
- basic: '*',
- low: '⁎',
- double: '⁑',
- triple: '⁂',
- small: '﹡',
- circle: '⊛',
- square: '⧆',
- ],
- at: '@',
- backslash: ['\\', circle: '⦸', not: '⧷'],
- co: '℅',
- colon: [':', eq: '≔', double.eq: '⩴'],
- comma: ',',
- dagger: ['†', double: '‡'],
- dash: [
- en: '–',
- em: '—',
- fig: '‒',
- wave: '〜',
- colon: '∹',
- circle: '⊝',
- wave.double: '〰',
- ],
- dot: [
- op: '⋅',
- basic: '.',
- c: '·',
- circle: '⊙',
- circle.big: '⨀',
- square: '⊡',
- double: '¨',
- triple: '\u{20db}',
- quad: '\u{20dc}',
- ],
- excl: ['!', double: '‼', inv: '¡', quest: '⁉'],
- quest: ['?', double: '⁇', excl: '⁈', inv: '¿'],
- interrobang: '‽',
- hash: '#',
- hyph: ['‐', minus: '\u{2D}', nobreak: '\u{2011}', point: '‧', soft: '\u{ad}'],
- percent: '%',
- copyright: ['©', sound: '℗'],
- permille: '‰',
- pilcrow: ['¶', rev: '⁋'],
- section: '§',
- semi: [';', rev: '⁏'],
- slash: ['/', double: '⫽', triple: '⫻', big: '\u{29f8}'],
- dots: [h.c: '⋯', h: '…', v: '⋮', down: '⋱', up: '⋰'],
- tilde: [
- op: '∼',
- basic: '~',
- eq: '≃',
- eq.not: '≄',
- eq.rev: '⋍',
- equiv: '≅',
- equiv.not: '≇',
- nequiv: '≆',
- not: '≁',
- rev: '∽',
- rev.equiv: '≌',
- triple: '≋',
- ],
-
- // Accents, quotes, and primes.
- acute: ['´', double: '˝'],
- breve: '˘',
- caret: '‸',
- caron: 'ˇ',
- hat: '^',
- diaer: '¨',
- grave: '`',
- macron: '¯',
- quote: [
- double: '"',
- single: '\'',
- l.double: '“',
- l.single: '‘',
- r.double: '”',
- r.single: '’',
- angle.l.double: '«',
- angle.l.single: '‹',
- angle.r.double: '»',
- angle.r.single: '›',
- high.double: '‟',
- high.single: '‛',
- low.double: '„',
- low.single: '‚',
- ],
- prime: [
- '′',
- rev: '‵',
- double: '″',
- double.rev: '‶',
- triple: '‴',
- triple.rev: '‷',
- quad: '⁗',
- ],
-
- // https://en.wikipedia.org/wiki/List_of_mathematical_symbols_by_subject
- // Arithmetic.
- plus: [
- '+',
- circle: '⊕',
- circle.arrow: '⟴',
- circle.big: '⨁',
- dot: '∔',
- minus: '±',
- small: '﹢',
- square: '⊞',
- triangle: '⨹',
- ],
- minus: [
- '−',
- circle: '⊖',
- dot: '∸',
- plus: '∓',
- square: '⊟',
- tilde: '≂',
- triangle: '⨺',
- ],
- div: ['÷', circle: '⨸'],
- times: [
- '×',
- big: '⨉',
- circle: '⊗',
- circle.big: '⨂',
- div: '⋇',
- three.l: '⋋',
- three.r: '⋌',
- l: '⋉',
- r: '⋊',
- square: '⊠',
- triangle: '⨻',
- ],
- ratio: '∶',
-
- // Relations.
- eq: [
- '=',
- star: '≛',
- circle: '⊜',
- colon: '≕',
- def: '≝',
- delta: '≜',
- equi: '≚',
- est: '≙',
- gt: '⋝',
- lt: '⋜',
- m: '≞',
- not: '≠',
- prec: '⋞',
- quest: '≟',
- small: '﹦',
- succ: '⋟',
- triple: '≡',
- quad: '≣',
- ],
- gt: [
- '>',
- circle: '⧁',
- curly: '≻',
- curly.approx: '⪸',
- curly.double: '⪼',
- curly.eq: '≽',
- curly.eq.not: '⋡',
- curly.equiv: '⪴',
- curly.napprox: '⪺',
- curly.nequiv: '⪶',
- curly.not: '⊁',
- curly.ntilde: '⋩',
- curly.tilde: '≿',
- dot: '⋗',
- double: '≫',
- eq: '≥',
- eq.slant: '⩾',
- eq.lt: '⋛',
- eq.not: '≱',
- equiv: '≧',
- lt: '≷',
- lt.not: '≹',
- nequiv: '≩',
- not: '≯',
- ntilde: '⋧',
- small: '﹥',
- tilde: '≳',
- tilde.not: '≵',
- tri: '⊳',
- tri.eq: '⊵',
- tri.eq.not: '⋭',
- tri.not: '⋫',
- triple: '⋙',
- triple.nested: '⫸',
- ],
- lt: [
- '<',
- circle: '⧀',
- curly: '≺',
- curly.approx: '⪷',
- curly.double: '⪻',
- curly.eq: '≼',
- curly.eq.not: '⋠',
- curly.equiv: '⪳',
- curly.napprox: '⪹',
- curly.nequiv: '⪵',
- curly.not: '⊀',
- curly.ntilde: '⋨',
- curly.tilde: '≾',
- dot: '⋖',
- double: '≪',
- eq: '≤',
- eq.slant: '⩽' ,
- eq.gt: '⋚',
- eq.not: '≰',
- equiv: '≦',
- gt: '≶',
- gt.not: '≸',
- nequiv: '≨',
- not: '≮',
- ntilde: '⋦',
- small: '﹤',
- tilde: '≲',
- tilde.not: '≴',
- tri: '⊲',
- tri.eq: '⊴',
- tri.eq.not: '⋬',
- tri.not: '⋪',
- triple: '⋘',
- triple.nested: '⫷',
- ],
- approx: ['≈', eq: '≊', not: '≉'],
- prec: [
- '≺',
- approx: '⪷',
- double: '⪻',
- eq: '≼',
- eq.not: '⋠',
- equiv: '⪳',
- napprox: '⪹',
- nequiv: '⪵',
- not: '⊀',
- ntilde: '⋨',
- tilde: '≾',
- ],
- succ: [
- '≻',
- approx: '⪸',
- double: '⪼',
- eq: '≽',
- eq.not: '⋡',
- equiv: '⪴',
- napprox: '⪺',
- nequiv: '⪶',
- not: '⊁',
- ntilde: '⋩',
- tilde: '≿',
- ],
- equiv: ['≡', not: '≢'],
- prop: '∝',
-
- // Set theory.
- emptyset: ['∅', rev: '⦰'],
- nothing: ['∅', rev: '⦰'],
- without: '∖',
- complement: '∁',
- in: [
- '∈',
- not: '∉',
- rev: '∋',
- rev.not: '∌',
- rev.small: '∍',
- small: '∊',
- ],
- subset: [
- '⊂',
- dot: '⪽',
- double: '⋐',
- eq: '⊆',
- eq.not: '⊈',
- eq.sq: '⊑',
- eq.sq.not: '⋢',
- neq: '⊊',
- not: '⊄',
- sq: '⊏',
- sq.neq: '⋤',
- ],
- supset: [
- '⊃',
- dot: '⪾',
- double: '⋑',
- eq: '⊇',
- eq.not: '⊉',
- eq.sq: '⊒',
- eq.sq.not: '⋣',
- neq: '⊋',
- not: '⊅',
- sq: '⊐',
- sq.neq: '⋥',
- ],
- union: [
- '∪',
- arrow: '⊌',
- big: '⋃',
- dot: '⊍',
- dot.big: '⨃',
- double: '⋓',
- minus: '⩁',
- or: '⩅',
- plus: '⊎',
- plus.big: '⨄',
- sq: '⊔',
- sq.big: '⨆',
- sq.double: '⩏',
- ],
- sect: [
- '∩',
- and: '⩄',
- big: '⋂',
- dot: '⩀',
- double: '⋒',
- sq: '⊓',
- sq.big: '⨅',
- sq.double: '⩎',
- ],
-
- // Calculus.
- infinity: '∞',
- oo: '∞',
- diff: '∂',
- nabla: '∇',
- sum: ['∑', integral: '⨋'],
- product: ['∏', co: '∐'],
- integral: [
- '∫',
- arrow.hook: '⨗',
- ccw: '⨑',
- cont: '∮',
- cont.ccw: '∳',
- cont.cw: '∲',
- cw: '∱',
- dash: '⨍',
- dash.double: '⨎',
- double: '∬',
- quad: '⨌',
- sect: '⨙',
- slash: '⨏',
- square: '⨖',
- surf: '∯',
- times: '⨘',
- triple: '∭',
- union: '⨚',
- vol: '∰',
- ],
- laplace: '∆',
-
- // Logic.
- forall: '∀',
- exists: ['∃', not: '∄'],
- top: '⊤',
- bot: '⊥',
- not: '¬',
- and: ['∧', big: '⋀', curly: '⋏', dot: '⟑', double: '⩓'],
- or: ['∨', big: '⋁', curly: '⋎', dot: '⟇', double: '⩔'],
- xor: ['⊕', big: '⨁'],
- models: '⊧',
- therefore: '∴',
- because: '∵',
- qed: '∎',
-
- // Function and category theory.
- compose: '∘',
- convolve: '∗',
- multimap: '⊸',
-
- // Number theory.
- divides: ['∣', not: '∤'],
-
- // Algebra.
- wreath: '≀',
-
- // Geometry.
- parallel: ['∥', circle: '⦷', not: '∦'],
- perp: ['⟂', circle: '⦹'],
-
- // Miscellaneous Technical.
- diameter: '⌀',
- join: ['⨝', r: '⟖', l: '⟕', l.r: '⟗'],
- degree: ['°', c: '℃', f: '℉'],
- smash: '⨳',
-
- // Currency.
- bitcoin: '₿',
- dollar: '$',
- euro: '€',
- franc: '₣',
- lira: '₺',
- peso: '₱',
- pound: '£',
- ruble: '₽',
- rupee: '₹',
- won: '₩',
- yen: '¥',
-
- // Miscellaneous.
- ballot: ['☐', x: '☒'],
- checkmark: ['✓', light: '🗸'],
- floral: ['❦', l: '☙', r: '❧'],
- notes: [up: '🎜', down: '🎝'],
- refmark: '※',
- servicemark: '℠',
- maltese: '✠',
- suit: [club: '♣', diamond: '♦', heart: '♥', spade: '♠'],
-
- // Shapes.
- bullet: '•',
- circle: [
- stroked: '○',
- stroked.tiny: '∘',
- stroked.small: '⚬',
- stroked.big: '◯',
- filled: '●',
- filled.tiny: '⦁',
- filled.small: '∙',
- filled.big: '⬤',
- dotted: '◌',
- nested: '⊚',
- ],
- ellipse: [
- stroked.h: '⬭',
- stroked.v: '⬯',
- filled.h: '⬬',
- filled.v: '⬮',
- ],
- triangle: [
- stroked.r: '▷',
- stroked.l: '◁',
- stroked.t: '△',
- stroked.b: '▽',
- stroked.bl: '◺',
- stroked.br: '◿',
- stroked.tl: '◸',
- stroked.tr: '◹',
- stroked.small.r: '▹',
- stroked.small.b: '▿',
- stroked.small.l: '◃',
- stroked.small.t: '▵',
- stroked.rounded: '🛆',
- stroked.nested: '⟁',
- stroked.dot: '◬',
- filled.r: '▶',
- filled.l: '◀',
- filled.t: '▲',
- filled.b: '▼',
- filled.bl: '◣',
- filled.br: '◢',
- filled.tl: '◤',
- filled.tr: '◥',
- filled.small.r: '▸',
- filled.small.b: '▾',
- filled.small.l: '◂',
- filled.small.t: '▴',
- ],
- square: [
- stroked: '□',
- stroked.tiny: '▫',
- stroked.small: '◽',
- stroked.medium: '◻',
- stroked.big: '⬜',
- stroked.dotted: '⬚',
- stroked.rounded: '▢',
- filled: '■',
- filled.tiny: '▪',
- filled.small: '◾',
- filled.medium: '◼',
- filled.big: '⬛',
- ],
- rect: [
- stroked.h: '▭',
- stroked.v: '▯',
- filled.h: '▬',
- filled.v: '▮',
- ],
- penta: [stroked: '⬠', filled: '⬟'],
- hexa: [stroked: '⬡', filled: '⬢'],
- diamond: [
- stroked: '◇',
- stroked.small: '⋄',
- stroked.medium: '⬦',
- stroked.dot: '⟐',
- filled: '◆',
- filled.medium: '⬥',
- filled.small: '⬩',
- ],
- lozenge: [
- stroked: '◊',
- stroked.small: '⬫',
- stroked.medium: '⬨',
- filled: '⧫',
- filled.small: '⬪',
- filled.medium: '⬧',
- ],
- star: [op: '⋆', stroked: '★', filled: '★'],
-
- // Arrows, harpoons, and tacks.
- arrow: [
- r: '→',
- r.long.bar: '⟼',
- r.bar: '↦',
- r.curve: '⤷',
- r.dashed: '⇢',
- r.dotted: '⤑',
- r.double: '⇒',
- r.double.bar: '⤇',
- r.double.long: '⟹',
- r.double.long.bar: '⟾',
- r.double.not: '⇏',
- r.filled: '➡',
- r.hook: '↪',
- r.long: '⟶',
- r.long.squiggly: '⟿',
- r.loop: '↬',
- r.not: '↛',
- r.quad: '⭆',
- r.squiggly: '⇝',
- r.stop: '⇥',
- r.stroked: '⇨',
- r.tail: '↣',
- r.triple: '⇛',
- r.twohead.bar: '⤅',
- r.twohead: '↠',
- r.wave: '↝',
- l: '←',
- l.bar: '↤',
- l.curve: '⤶',
- l.dashed: '⇠',
- l.dotted: '⬸',
- l.double: '⇐',
- l.double.bar: '⤆',
- l.double.long: '⟸',
- l.double.long.bar: '⟽',
- l.double.not: '⇍',
- l.filled: '⬅',
- l.hook: '↩',
- l.long: '⟵',
- l.long.bar: '⟻',
- l.long.squiggly: '⬳',
- l.loop: '↫',
- l.not: '↚',
- l.quad: '⭅',
- l.squiggly: '⇜',
- l.stop: '⇤',
- l.stroked: '⇦',
- l.tail: '↢',
- l.triple: '⇚',
- l.twohead.bar: '⬶',
- l.twohead: '↞',
- l.wave: '↜',
- t: '↑',
- t.bar: '↥',
- t.curve: '⤴',
- t.dashed: '⇡',
- t.double: '⇑',
- t.filled: '⬆',
- t.quad: '⟰',
- t.stop: '⤒',
- t.stroked: '⇧',
- t.triple: '⤊',
- t.twohead: '↟',
- b: '↓',
- b.bar: '↧',
- b.curve: '⤵',
- b.dashed: '⇣',
- b.double: '⇓',
- b.filled: '⬇',
- b.quad: '⟱',
- b.stop: '⤓',
- b.stroked: '⇩',
- b.triple: '⤋',
- b.twohead: '↡',
- l.r: '↔',
- l.r.double: '⇔',
- l.r.double.long: '⟺',
- l.r.double.not: '⇎',
- l.r.filled: '⬌',
- l.r.long: '⟷',
- l.r.not: '↮',
- l.r.stroked: '⬄',
- l.r.wave: '↭',
- t.b: '↕',
- t.b.double: '⇕',
- t.b.filled: '⬍',
- t.b.stroked: '⇳',
- tr: '↗',
- tr.double: '⇗',
- tr.filled: '⬈',
- tr.hook: '⤤',
- tr.stroked: '⬀',
- br: '↘',
- br.double: '⇘',
- br.filled: '⬊',
- br.hook: '⤥',
- br.stroked: '⬂',
- tl: '↖',
- tl.double: '⇖',
- tl.filled: '⬉',
- tl.hook: '⤣',
- tl.stroked: '⬁',
- bl: '↙',
- bl.double: '⇙',
- bl.filled: '⬋',
- bl.hook: '⤦',
- bl.stroked: '⬃',
- tl.br: '⤡',
- tr.bl: '⤢',
- ccw: '↺',
- ccw.half: '↶',
- cw: '↻',
- cw.half: '↷',
- zigzag: '↯',
- ],
- arrows: [
- rr: '⇉',
- ll: '⇇',
- tt: '⇈',
- bb: '⇊',
- lr: '⇆',
- lr.stop: '↹',
- rl: '⇄',
- tb: '⇅',
- bt: '⇵',
- rrr: '⇶',
- lll: '⬱',
- ],
- arrowhead: [
- t: '⌃',
- b: '⌄',
- ],
- harpoon: [
- rt: '⇀',
- rt.bar: '⥛',
- rt.stop: '⥓',
- rb: '⇁',
- rb.bar: '⥟',
- rb.stop: '⥗',
- lt: '↼',
- lt.bar: '⥚',
- lt.stop: '⥒',
- lb: '↽',
- lb.bar: '⥞',
- lb.stop: '⥖',
- tl: '↿',
- tl.bar: '⥠',
- tl.stop: '⥘',
- tr: '↾',
- tr.bar: '⥜',
- tr.stop: '⥔',
- bl: '⇃',
- bl.bar: '⥡',
- bl.stop: '⥙',
- br: '⇂',
- br.bar: '⥝',
- br.stop: '⥕',
- lt.rt: '⥎',
- lb.rb: '⥐',
- lb.rt: '⥋',
- lt.rb: '⥊',
- tl.bl: '⥑',
- tr.br: '⥏',
- tl.br: '⥍',
- tr.bl: '⥌',
- ],
- harpoons: [
- rtrb: '⥤',
- blbr: '⥥',
- bltr: '⥯',
- lbrb: '⥧',
- ltlb: '⥢',
- ltrb: '⇋',
- ltrt: '⥦',
- rblb: '⥩',
- rtlb: '⇌',
- rtlt: '⥨',
- tlbr: '⥮',
- tltr: '⥣',
- ],
- tack: [
- r: '⊢',
- r.not: '⊬',
- r.long: '⟝',
- r.short: '⊦',
- r.double: '⊨',
- r.double.not: '⊭',
- l: '⊣',
- l.long: '⟞',
- l.short: '⫞',
- l.double: '⫤',
- t: '⊥',
- t.big: '⟘',
- t.double: '⫫',
- t.short: '⫠',
- b: '⊤',
- b.big: '⟙',
- b.double: '⫪',
- b.short: '⫟',
- l.r: '⟛',
- ],
-
- // Lowercase Greek.
- alpha: 'α',
- beta: ['β', alt: 'ϐ'],
- chi: 'χ',
- delta: 'δ',
- epsilon: ['ε', alt: 'ϵ'],
- eta: 'η',
- gamma: 'γ',
- iota: 'ι',
- kai: 'ϗ',
- kappa: ['κ', alt: 'ϰ'],
- lambda: 'λ',
- mu: 'μ',
- nu: 'ν',
- ohm: ['Ω', inv: '℧'],
- omega: 'ω',
- omicron: 'ο',
- phi: ['φ', alt: 'ϕ'],
- pi: ['π', alt: 'ϖ'],
- psi: 'ψ',
- rho: ['ρ', alt: 'ϱ'],
- sigma: ['σ', alt: 'ς'],
- tau: 'τ',
- theta: ['θ', alt: 'ϑ'],
- upsilon: 'υ',
- xi: 'ξ',
- zeta: 'ζ',
-
- // Uppercase Greek.
- Alpha: 'Α',
- Beta: 'Β',
- Chi: 'Χ',
- Delta: 'Δ',
- Epsilon: 'Ε',
- Eta: 'Η',
- Gamma: 'Γ',
- Iota: 'Ι',
- Kai: 'Ϗ',
- Kappa: 'Κ',
- Lambda: 'Λ',
- Mu: 'Μ',
- Nu: 'Ν',
- Omega: 'Ω',
- Omicron: 'Ο',
- Phi: 'Φ',
- Pi: 'Π',
- Psi: 'Ψ',
- Rho: 'Ρ',
- Sigma: 'Σ',
- Tau: 'Τ',
- Theta: 'Θ',
- Upsilon: 'Υ',
- Xi: 'Ξ',
- Zeta: 'Ζ',
-
- // Hebrew.
- aleph: 'א',
- alef: 'א',
- beth: 'ב',
- bet: 'ב',
- gimmel: 'ג',
- gimel: 'ג',
- shin: 'ש',
-
- // Double-struck.
- AA: '𝔸',
- BB: '𝔹',
- CC: 'ℂ',
- DD: '𝔻',
- EE: '𝔼',
- FF: '𝔽',
- GG: '𝔾',
- HH: 'ℍ',
- II: '𝕀',
- JJ: '𝕁',
- KK: '𝕂',
- LL: '𝕃',
- MM: '𝕄',
- NN: 'ℕ',
- OO: '𝕆',
- PP: 'ℙ',
- QQ: 'ℚ',
- RR: 'ℝ',
- SS: '𝕊',
- TT: '𝕋',
- UU: '𝕌',
- VV: '𝕍',
- WW: '𝕎',
- XX: '𝕏',
- YY: '𝕐',
- ZZ: 'ℤ',
-
- // Miscellaneous letter-likes.
- ell: 'ℓ',
- planck: ['ℎ', reduce: 'ℏ'],
- angstrom: 'Å',
- kelvin: 'K',
- Re: 'ℜ',
- Im: 'ℑ',
- dotless: [i: '𝚤', j: '𝚥'],
-};
diff --git a/crates/typst-library/src/text/deco.rs b/crates/typst-library/src/text/deco.rs
deleted file mode 100644
index aecff401..00000000
--- a/crates/typst-library/src/text/deco.rs
+++ /dev/null
@@ -1,566 +0,0 @@
-use kurbo::{BezPath, Line, ParamCurve};
-use ttf_parser::{GlyphId, OutlineBuilder};
-
-use crate::prelude::*;
-use crate::text::{BottomEdge, BottomEdgeMetric, TextElem, TopEdge, TopEdgeMetric};
-
-/// Underlines text.
-///
-/// # Example
-/// ```example
-/// This is #underline[important].
-/// ```
-#[elem(Show)]
-pub struct UnderlineElem {
- /// How to [stroke]($stroke) the line.
- ///
- /// If set to `{auto}`, takes on the text's color and a thickness defined in
- /// the current font.
- ///
- /// ```example
- /// Take #underline(
- /// stroke: 1.5pt + red,
- /// offset: 2pt,
- /// [care],
- /// )
- /// ```
- #[resolve]
- #[fold]
- pub stroke: Smart<Stroke>,
-
- /// The position of the line relative to the baseline, read from the font
- /// tables if `{auto}`.
- ///
- /// ```example
- /// #underline(offset: 5pt)[
- /// The Tale Of A Faraway Line I
- /// ]
- /// ```
- #[resolve]
- pub offset: Smart<Length>,
-
- /// The amount by which to extend the line beyond (or within if negative)
- /// the content.
- ///
- /// ```example
- /// #align(center,
- /// underline(extent: 2pt)[Chapter 1]
- /// )
- /// ```
- #[resolve]
- pub extent: Length,
-
- /// Whether the line skips sections in which it would collide with the
- /// glyphs.
- ///
- /// ```example
- /// This #underline(evade: true)[is great].
- /// This #underline(evade: false)[is less great].
- /// ```
- #[default(true)]
- pub evade: bool,
-
- /// Whether the line is placed behind the content it underlines.
- ///
- /// ```example
- /// #set underline(stroke: (thickness: 1em, paint: maroon, cap: "round"))
- /// #underline(background: true)[This is stylized.] \
- /// #underline(background: false)[This is partially hidden.]
- /// ```
- #[default(false)]
- pub background: bool,
-
- /// The content to underline.
- #[required]
- pub body: Content,
-}
-
-impl Show for UnderlineElem {
- #[tracing::instrument(name = "UnderlineElem::show", skip_all)]
- fn show(&self, _: &mut Vt, styles: StyleChain) -> SourceResult<Content> {
- Ok(self.body().clone().styled(TextElem::set_deco(Decoration {
- line: DecoLine::Underline {
- stroke: self.stroke(styles).unwrap_or_default(),
- offset: self.offset(styles),
- evade: self.evade(styles),
- background: self.background(styles),
- },
- extent: self.extent(styles),
- })))
- }
-}
-
-/// Adds a line over text.
-///
-/// # Example
-/// ```example
-/// #overline[A line over text.]
-/// ```
-#[elem(Show)]
-pub struct OverlineElem {
- /// How to [stroke]($stroke) the line.
- ///
- /// If set to `{auto}`, takes on the text's color and a thickness defined in
- /// the current font.
- ///
- /// ```example
- /// #set text(fill: olive)
- /// #overline(
- /// stroke: green.darken(20%),
- /// offset: -12pt,
- /// [The Forest Theme],
- /// )
- /// ```
- #[resolve]
- #[fold]
- pub stroke: Smart<Stroke>,
-
- /// The position of the line relative to the baseline. Read from the font
- /// tables if `{auto}`.
- ///
- /// ```example
- /// #overline(offset: -1.2em)[
- /// The Tale Of A Faraway Line II
- /// ]
- /// ```
- #[resolve]
- pub offset: Smart<Length>,
-
- /// The amount by which to extend the line beyond (or within if negative)
- /// the content.
- ///
- /// ```example
- /// #set overline(extent: 4pt)
- /// #set underline(extent: 4pt)
- /// #overline(underline[Typography Today])
- /// ```
- #[resolve]
- pub extent: Length,
-
- /// Whether the line skips sections in which it would collide with the
- /// glyphs.
- ///
- /// ```example
- /// #overline(
- /// evade: false,
- /// offset: -7.5pt,
- /// stroke: 1pt,
- /// extent: 3pt,
- /// [Temple],
- /// )
- /// ```
- #[default(true)]
- pub evade: bool,
-
- /// Whether the line is placed behind the content it overlines.
- ///
- /// ```example
- /// #set overline(stroke: (thickness: 1em, paint: maroon, cap: "round"))
- /// #overline(background: true)[This is stylized.] \
- /// #overline(background: false)[This is partially hidden.]
- /// ```
- #[default(false)]
- pub background: bool,
-
- /// The content to add a line over.
- #[required]
- pub body: Content,
-}
-
-impl Show for OverlineElem {
- #[tracing::instrument(name = "OverlineElem::show", skip_all)]
- fn show(&self, _: &mut Vt, styles: StyleChain) -> SourceResult<Content> {
- Ok(self.body().clone().styled(TextElem::set_deco(Decoration {
- line: DecoLine::Overline {
- stroke: self.stroke(styles).unwrap_or_default(),
- offset: self.offset(styles),
- evade: self.evade(styles),
- background: self.background(styles),
- },
- extent: self.extent(styles),
- })))
- }
-}
-
-/// Strikes through text.
-///
-/// # Example
-/// ```example
-/// This is #strike[not] relevant.
-/// ```
-#[elem(title = "Strikethrough", Show)]
-pub struct StrikeElem {
- /// How to [stroke]($stroke) the line.
- ///
- /// 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.
- ///
- /// ```example
- /// This is #strike(stroke: 1.5pt + red)[very stricken through]. \
- /// This is #strike(stroke: 10pt)[redacted].
- /// ```
- #[resolve]
- #[fold]
- pub stroke: Smart<Stroke>,
-
- /// The position of the line relative to the baseline. Read from the font
- /// tables if `{auto}`.
- ///
- /// This is useful if you are unhappy with the offset your font provides.
- ///
- /// ```example
- /// #set text(font: "Inria Serif")
- /// This is #strike(offset: auto)[low-ish]. \
- /// This is #strike(offset: -3.5pt)[on-top].
- /// ```
- #[resolve]
- pub offset: Smart<Length>,
-
- /// The amount by which to extend the line beyond (or within if negative)
- /// the content.
- ///
- /// ```example
- /// This #strike(extent: -2pt)[skips] parts of the word.
- /// This #strike(extent: 2pt)[extends] beyond the word.
- /// ```
- #[resolve]
- pub extent: Length,
-
- /// Whether the line is placed behind the content.
- ///
- /// ```example
- /// #set strike(stroke: red)
- /// #strike(background: true)[This is behind.] \
- /// #strike(background: false)[This is in front.]
- /// ```
- #[default(false)]
- pub background: bool,
-
- /// The content to strike through.
- #[required]
- pub body: Content,
-}
-
-impl Show for StrikeElem {
- #[tracing::instrument(name = "StrikeElem::show", skip_all)]
- fn show(&self, _: &mut Vt, styles: StyleChain) -> SourceResult<Content> {
- Ok(self.body().clone().styled(TextElem::set_deco(Decoration {
- // Note that we do not support evade option for strikethrough.
- line: DecoLine::Strikethrough {
- stroke: self.stroke(styles).unwrap_or_default(),
- offset: self.offset(styles),
- background: self.background(styles),
- },
- extent: self.extent(styles),
- })))
- }
-}
-
-/// Highlights text with a background color.
-///
-/// # Example
-/// ```example
-/// This is #highlight[important].
-/// ```
-#[elem(Show)]
-pub struct HighlightElem {
- /// The color to highlight the text with.
- /// (Default: 0xffff5f)
- ///
- /// ```example
- /// This is #highlight(fill: blue)[with blue].
- /// ```
- #[default(Color::from_u8(0xFF, 0xFF, 0x5F, 0xFF).into())]
- pub fill: Paint,
-
- /// The top end of the background rectangle.
- ///
- /// ```example
- /// #set highlight(top-edge: "ascender")
- /// #highlight[a] #highlight[aib]
- ///
- /// #set highlight(top-edge: "x-height")
- /// #highlight[a] #highlight[aib]
- /// ```
- #[default(TopEdge::Metric(TopEdgeMetric::Ascender))]
- pub top_edge: TopEdge,
-
- /// The bottom end of the background rectangle.
- ///
- /// ```example
- /// #set highlight(bottom-edge: "descender")
- /// #highlight[a] #highlight[ap]
- ///
- /// #set highlight(bottom-edge: "baseline")
- /// #highlight[a] #highlight[ap]
- /// ```
- #[default(BottomEdge::Metric(BottomEdgeMetric::Descender))]
- pub bottom_edge: BottomEdge,
-
- /// The amount by which to extend the background to the sides beyond
- /// (or within if negative) the content.
- ///
- /// ```example
- /// A long #highlight(extent: 4pt)[background].
- /// ```
- #[resolve]
- pub extent: Length,
-
- /// The content that should be highlighted.
- #[required]
- pub body: Content,
-}
-
-impl Show for HighlightElem {
- #[tracing::instrument(name = "HighlightElem::show", skip_all)]
- fn show(&self, _: &mut Vt, styles: StyleChain) -> SourceResult<Content> {
- Ok(self.body().clone().styled(TextElem::set_deco(Decoration {
- line: DecoLine::Highlight {
- fill: self.fill(styles),
- top_edge: self.top_edge(styles),
- bottom_edge: self.bottom_edge(styles),
- },
- extent: self.extent(styles),
- })))
- }
-}
-
-/// 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,
- extent: Abs,
-}
-
-impl Fold for Decoration {
- type Output = Vec<Self>;
-
- fn fold(self, mut outer: Self::Output) -> Self::Output {
- outer.insert(0, self);
- outer
- }
-}
-
-impl Repr for Decoration {
- fn repr(&self) -> EcoString {
- eco_format!("{self:?}")
- }
-}
-
-cast! {
- type Decoration,
-}
-
-/// A kind of decorative line.
-#[derive(Debug, Clone, Eq, PartialEq, Hash)]
-enum DecoLine {
- Underline { stroke: Stroke<Abs>, offset: Smart<Abs>, evade: bool, background: bool },
- Strikethrough { stroke: Stroke<Abs>, offset: Smart<Abs>, background: bool },
- Overline { stroke: Stroke<Abs>, offset: Smart<Abs>, evade: bool, background: bool },
- Highlight { fill: Paint, top_edge: TopEdge, bottom_edge: BottomEdge },
-}
-
-/// Add line decorations to a single run of shaped text.
-pub(super) fn decorate(
- frame: &mut Frame,
- deco: &Decoration,
- text: &TextItem,
- width: Abs,
- shift: Abs,
- pos: Point,
-) {
- let font_metrics = text.font.metrics();
-
- if let DecoLine::Highlight { fill, top_edge, bottom_edge } = &deco.line {
- let (top, bottom) = determine_edges(text, *top_edge, *bottom_edge);
- let rect = Geometry::Rect(Size::new(width + 2.0 * deco.extent, top - bottom))
- .filled(fill.clone());
- let origin = Point::new(pos.x - deco.extent, pos.y - top - shift);
- frame.prepend(origin, FrameItem::Shape(rect, Span::detached()));
- return;
- }
-
- let (stroke, metrics, offset, evade, background) = match &deco.line {
- DecoLine::Strikethrough { stroke, offset, background } => {
- (stroke, font_metrics.strikethrough, offset, false, *background)
- }
- DecoLine::Overline { stroke, offset, evade, background } => {
- (stroke, font_metrics.overline, offset, *evade, *background)
- }
- DecoLine::Underline { stroke, offset, evade, background } => {
- (stroke, font_metrics.underline, offset, *evade, *background)
- }
- _ => return,
- };
-
- let offset = offset.unwrap_or(-metrics.position.at(text.size)) - shift;
- let stroke = stroke.clone().unwrap_or(FixedStroke {
- paint: text.fill.as_decoration(),
- thickness: metrics.thickness.at(text.size),
- ..FixedStroke::default()
- });
-
- let gap_padding = 0.08 * text.size;
- let min_width = 0.162 * text.size;
-
- let start = pos.x - deco.extent;
- let end = pos.x + width + deco.extent;
-
- let mut push_segment = |from: Abs, to: Abs, prepend: bool| {
- let origin = Point::new(from, pos.y + offset);
- let target = Point::new(to - from, Abs::zero());
-
- if target.x >= min_width || !evade {
- let shape = Geometry::Line(target).stroked(stroke.clone());
-
- if prepend {
- frame.prepend(origin, FrameItem::Shape(shape, Span::detached()));
- } else {
- frame.push(origin, FrameItem::Shape(shape, Span::detached()));
- }
- }
- };
-
- if !evade {
- push_segment(start, end, background);
- return;
- }
-
- let line = Line::new(
- kurbo::Point::new(pos.x.to_raw(), offset.to_raw()),
- kurbo::Point::new((pos.x + width).to_raw(), offset.to_raw()),
- );
-
- let mut x = pos.x;
- let mut intersections = vec![];
-
- for glyph in text.glyphs.iter() {
- let dx = glyph.x_offset.at(text.size) + x;
- let mut builder =
- BezPathBuilder::new(font_metrics.units_per_em, text.size, dx.to_raw());
-
- let bbox = text.font.ttf().outline_glyph(GlyphId(glyph.id), &mut builder);
- let path = builder.finish();
-
- x += glyph.x_advance.at(text.size);
-
- // Only do the costly segments intersection test if the line
- // intersects the bounding box.
- let intersect = bbox.map_or(false, |bbox| {
- let y_min = -text.font.to_em(bbox.y_max).at(text.size);
- let y_max = -text.font.to_em(bbox.y_min).at(text.size);
- offset >= y_min && offset <= y_max
- });
-
- if intersect {
- // Find all intersections of segments with the line.
- intersections.extend(
- path.segments()
- .flat_map(|seg| seg.intersect_line(line))
- .map(|is| Abs::raw(line.eval(is.line_t).x)),
- );
- }
- }
-
- // Add start and end points, taking padding into account.
- intersections.push(start - gap_padding);
- intersections.push(end + gap_padding);
- // When emitting the decorative line segments, we move from left to
- // right. The intersections are not necessarily in this order, yet.
- intersections.sort();
-
- for edge in intersections.windows(2) {
- let l = edge[0];
- let r = edge[1];
-
- // If we are too close, don't draw the segment
- if r - l < gap_padding {
- continue;
- } else {
- push_segment(l + gap_padding, r - gap_padding, background);
- }
- }
-}
-
-// Return the top/bottom edge of the text given the metric of the font.
-fn determine_edges(
- text: &TextItem,
- top_edge: TopEdge,
- bottom_edge: BottomEdge,
-) -> (Abs, Abs) {
- let mut bbox = None;
- if top_edge.is_bounds() || bottom_edge.is_bounds() {
- let ttf = text.font.ttf();
- bbox = text
- .glyphs
- .iter()
- .filter_map(|g| ttf.glyph_bounding_box(ttf_parser::GlyphId(g.id)))
- .reduce(|a, b| ttf_parser::Rect {
- y_max: a.y_max.max(b.y_max),
- y_min: a.y_min.min(b.y_min),
- ..a
- });
- }
-
- let top = top_edge.resolve(text.size, &text.font, bbox);
- let bottom = bottom_edge.resolve(text.size, &text.font, bbox);
- (top, bottom)
-}
-
-/// Builds a kurbo [`BezPath`] for a glyph.
-struct BezPathBuilder {
- path: BezPath,
- units_per_em: f64,
- font_size: Abs,
- x_offset: f64,
-}
-
-impl BezPathBuilder {
- fn new(units_per_em: f64, font_size: Abs, x_offset: f64) -> Self {
- Self {
- path: BezPath::new(),
- units_per_em,
- font_size,
- x_offset,
- }
- }
-
- fn finish(self) -> BezPath {
- self.path
- }
-
- fn p(&self, x: f32, y: f32) -> kurbo::Point {
- kurbo::Point::new(self.s(x) + self.x_offset, -self.s(y))
- }
-
- fn s(&self, v: f32) -> f64 {
- Em::from_units(v, self.units_per_em).at(self.font_size).to_raw()
- }
-}
-
-impl OutlineBuilder for BezPathBuilder {
- fn move_to(&mut self, x: f32, y: f32) {
- self.path.move_to(self.p(x, y));
- }
-
- fn line_to(&mut self, x: f32, y: f32) {
- self.path.line_to(self.p(x, y));
- }
-
- fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) {
- self.path.quad_to(self.p(x1, y1), self.p(x, y));
- }
-
- fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) {
- self.path.curve_to(self.p(x1, y1), self.p(x2, y2), self.p(x, y));
- }
-
- fn close(&mut self) {
- self.path.close_path();
- }
-}
diff --git a/crates/typst-library/src/text/linebreak.rs b/crates/typst-library/src/text/linebreak.rs
deleted file mode 100644
index 8a69e0a8..00000000
--- a/crates/typst-library/src/text/linebreak.rs
+++ /dev/null
@@ -1,262 +0,0 @@
-use icu_properties::maps::CodePointMapData;
-use icu_properties::LineBreak;
-use icu_provider::AsDeserializingBufferProvider;
-use icu_provider_adapters::fork::ForkByKeyProvider;
-use icu_provider_blob::BlobDataProvider;
-use icu_segmenter::LineSegmenter;
-use once_cell::sync::Lazy;
-use typst::doc::Lang;
-use typst::syntax::link_prefix;
-
-use crate::layout::Preparation;
-use crate::text::TextElem;
-
-/// Generated by the following command:
-///
-/// ```sh
-/// icu4x-datagen --locales full \
-/// --format blob \
-/// --keys-for-bin target/debug/typst \
-/// --out crates/typst-library/assets/icudata.postcard \
-/// --overwrite
-/// ```
-///
-/// Install icu_datagen with `cargo install icu_datagen`.
-static ICU_DATA: &[u8] = include_bytes!("../../assets/icudata.postcard");
-
-/// Generated by the following command:
-///
-/// ```sh
-/// icu4x-datagen --locales zh ja \
-/// --format blob \
-/// --keys segmenter/line@1 \
-/// --out crates/typst-library/assets/cj_linebreak_data.postcard \
-/// --overwrite
-/// ```
-///
-/// The used icu_datagen should be patched by
-/// https://github.com/peng1999/icu4x/commit/b9beb6cbf633d61fc3d7983e5baf7f4449fbfae5
-static CJ_LINEBREAK_DATA: &[u8] =
- include_bytes!("../../assets/cj_linebreak_data.postcard");
-
-/// The general line break segmenter.
-static SEGMENTER: Lazy<LineSegmenter> = Lazy::new(|| {
- let provider = BlobDataProvider::try_new_from_static_blob(ICU_DATA).unwrap();
- LineSegmenter::try_new_lstm_with_buffer_provider(&provider).unwrap()
-});
-
-/// The line break segmenter for Chinese/Japanese text.
-static CJ_SEGMENTER: Lazy<LineSegmenter> = Lazy::new(|| {
- let provider = BlobDataProvider::try_new_from_static_blob(ICU_DATA).unwrap();
- let cj_blob = BlobDataProvider::try_new_from_static_blob(CJ_LINEBREAK_DATA).unwrap();
- let cj_provider = ForkByKeyProvider::new(cj_blob, provider);
- LineSegmenter::try_new_lstm_with_buffer_provider(&cj_provider).unwrap()
-});
-
-/// The Unicode line break properties for each code point.
-static LINEBREAK_DATA: Lazy<CodePointMapData<LineBreak>> = Lazy::new(|| {
- let provider = BlobDataProvider::try_new_from_static_blob(ICU_DATA).unwrap();
- let deser_provider = provider.as_deserializing();
- icu_properties::maps::load_line_break(&deser_provider).unwrap()
-});
-
-/// A line break opportunity.
-#[derive(Debug, Copy, Clone, Eq, PartialEq)]
-pub(crate) enum Breakpoint {
- /// Just a normal opportunity (e.g. after a space).
- Normal,
- /// A mandatory breakpoint (after '\n' or at the end of the text).
- Mandatory,
- /// An opportunity for hyphenating.
- Hyphen,
-}
-
-/// Calls `f` for all possible points in the text where lines can broken.
-///
-/// Yields for each breakpoint the text index, whether the break is mandatory
-/// (after `\n`) and whether a hyphen is required (when breaking inside of a
-/// word).
-///
-/// This is an internal instead of an external iterator because it makes the
-/// code much simpler and the consumers of this function don't need the
-/// composability and flexibility of external iteration anyway.
-pub(crate) fn breakpoints<'a>(
- p: &'a Preparation<'a>,
- mut f: impl FnMut(usize, Breakpoint),
-) {
- let text = p.bidi.text;
- let hyphenate = p.hyphenate != Some(false);
- let lb = LINEBREAK_DATA.as_borrowed();
- let segmenter = match p.lang {
- Some(Lang::CHINESE | Lang::JAPANESE) => &CJ_SEGMENTER,
- _ => &SEGMENTER,
- };
-
- let mut last = 0;
- let mut iter = segmenter.segment_str(text).peekable();
-
- loop {
- // Special case for links. UAX #14 doesn't handle them well.
- let (head, tail) = text.split_at(last);
- if head.ends_with("://") || tail.starts_with("www.") {
- let (link, _) = link_prefix(tail);
- let end = last + link.len();
- linebreak_link(link, |i| f(last + i, Breakpoint::Normal));
- while iter.peek().map_or(false, |&p| p < end) {
- iter.next();
- }
- }
-
- // Get the UAX #14 linebreak opportunities.
- let Some(point) = iter.next() else { break };
-
- // Skip breakpoint if there is no char before it. icu4x generates one
- // at offset 0, but we don't want it.
- let Some(c) = text[..point].chars().next_back() else { continue };
-
- // Find out whether the last break was mandatory by checking against
- // rules LB4 and LB5, special-casing the end of text according to LB3.
- // See also: https://docs.rs/icu_segmenter/latest/icu_segmenter/struct.LineSegmenter.html
- let breakpoint = if point == text.len() {
- Breakpoint::Mandatory
- } else {
- match lb.get(c) {
- // Fix for: https://github.com/unicode-org/icu4x/issues/4146
- LineBreak::Glue | LineBreak::WordJoiner | LineBreak::ZWJ => continue,
- LineBreak::MandatoryBreak
- | LineBreak::CarriageReturn
- | LineBreak::LineFeed
- | LineBreak::NextLine => Breakpoint::Mandatory,
- _ => Breakpoint::Normal,
- }
- };
-
- // Hyphenate between the last and current breakpoint.
- 'hyphenate: {
- if !hyphenate {
- break 'hyphenate;
- }
-
- // Extract a hyphenatable "word".
- let word = &text[last..point].trim_end_matches(|c: char| !c.is_alphabetic());
- if word.is_empty() {
- break 'hyphenate;
- }
-
- let end = last + word.len();
- let mut offset = last;
-
- // Determine the language to hyphenate this word in.
- let Some(lang) = lang_at(p, last) else { break 'hyphenate };
-
- for syllable in hypher::hyphenate(word, lang) {
- // Don't hyphenate after the final syllable.
- offset += syllable.len();
- if offset == end {
- continue;
- }
-
- // Filter out hyphenation opportunities where hyphenation was
- // actually disabled.
- if !hyphenate_at(p, offset) {
- continue;
- }
-
- // Filter out forbidden hyphenation opportunities.
- if matches!(
- syllable.chars().next_back().map(|c| lb.get(c)),
- Some(LineBreak::Glue | LineBreak::WordJoiner | LineBreak::ZWJ)
- ) {
- continue;
- }
-
- // Call `f` for the word-internal hyphenation opportunity.
- f(offset, Breakpoint::Hyphen);
- }
- }
-
- // Call `f` for the UAX #14 break opportunity.
- f(point, breakpoint);
-
- last = point;
- }
-}
-
-/// Produce linebreak opportunities for a link.
-fn linebreak_link(link: &str, mut f: impl FnMut(usize)) {
- #[derive(PartialEq)]
- enum Class {
- Alphabetic,
- Digit,
- Open,
- Other,
- }
-
- impl Class {
- fn of(c: char) -> Self {
- if c.is_alphabetic() {
- Class::Alphabetic
- } else if c.is_numeric() {
- Class::Digit
- } else if matches!(c, '(' | '[') {
- Class::Open
- } else {
- Class::Other
- }
- }
- }
-
- let mut offset = 0;
- let mut prev = Class::Other;
-
- for (end, c) in link.char_indices() {
- let class = Class::of(c);
-
- // Emit opportunities when going from
- // - other -> other
- // - alphabetic -> numeric
- // - numeric -> alphabetic
- // Never before after opening delimiters.
- if end > 0
- && prev != Class::Open
- && if class == Class::Other { prev == Class::Other } else { class != prev }
- {
- let piece = &link[offset..end];
- if piece.len() < 16 {
- // For bearably long segments, emit them as one.
- offset = end;
- f(offset);
- } else {
- // If it gets very long (e.g. a hash in the URL), just allow a
- // break at every char.
- for c in piece.chars() {
- offset += c.len_utf8();
- f(offset);
- }
- }
- }
-
- prev = class;
- }
-}
-
-/// Whether hyphenation is enabled at the given offset.
-fn hyphenate_at(p: &Preparation, offset: usize) -> bool {
- p.hyphenate
- .or_else(|| {
- let shaped = p.find(offset)?.text()?;
- Some(TextElem::hyphenate_in(shaped.styles))
- })
- .unwrap_or(false)
-}
-
-/// The text language at the given offset.
-fn lang_at(p: &Preparation, offset: usize) -> Option<hypher::Lang> {
- let lang = p.lang.or_else(|| {
- let shaped = p.find(offset)?.text()?;
- Some(TextElem::lang_in(shaped.styles))
- })?;
-
- let bytes = lang.as_str().as_bytes().try_into().ok()?;
- hypher::Lang::from_iso(bytes)
-}
diff --git a/crates/typst-library/src/text/misc.rs b/crates/typst-library/src/text/misc.rs
deleted file mode 100644
index 1bf28a34..00000000
--- a/crates/typst-library/src/text/misc.rs
+++ /dev/null
@@ -1,315 +0,0 @@
-use crate::prelude::*;
-use crate::text::TextElem;
-
-/// A text space.
-#[elem(Behave, Unlabellable, PlainText, Repr)]
-pub struct SpaceElem {}
-
-impl Repr for SpaceElem {
- fn repr(&self) -> EcoString {
- EcoString::inline("[ ]")
- }
-}
-
-impl Behave for SpaceElem {
- fn behaviour(&self) -> Behaviour {
- Behaviour::Weak(2)
- }
-}
-
-impl Unlabellable for SpaceElem {}
-
-impl PlainText for SpaceElem {
- fn plain_text(&self, text: &mut EcoString) {
- text.push(' ');
- }
-}
-
-/// Inserts a line break.
-///
-/// Advances the paragraph to the next line. A single trailing line break at the
-/// end of a paragraph is ignored, but more than one creates additional empty
-/// lines.
-///
-/// # Example
-/// ```example
-/// *Date:* 26.12.2022 \
-/// *Topic:* Infrastructure Test \
-/// *Severity:* High \
-/// ```
-///
-/// # 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.
-#[elem(title = "Line Break", Behave)]
-pub struct LinebreakElem {
- /// Whether to justify the line before the break.
- ///
- /// This is useful if you found a better line break opportunity in your
- /// justified text than Typst did.
- ///
- /// ```example
- /// #set par(justify: true)
- /// #let jb = linebreak(justify: true)
- ///
- /// I have manually tuned the #jb
- /// line breaks in this paragraph #jb
- /// for an _interesting_ result. #jb
- /// ```
- #[default(false)]
- pub justify: bool,
-}
-
-impl Behave for LinebreakElem {
- fn behaviour(&self) -> Behaviour {
- Behaviour::Destructive
- }
-}
-
-/// Strongly emphasizes content by increasing the font weight.
-///
-/// Increases the current font weight by a given `delta`.
-///
-/// # Example
-/// ```example
-/// This is *strong.* \
-/// This is #strong[too.] \
-///
-/// #show strong: set text(red)
-/// And this is *evermore.*
-/// ```
-///
-/// # 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.
-#[elem(title = "Strong Emphasis", Show)]
-pub struct StrongElem {
- /// The delta to apply on the font weight.
- ///
- /// ```example
- /// #set strong(delta: 0)
- /// No *effect!*
- /// ```
- #[default(300)]
- pub delta: i64,
-
- /// The content to strongly emphasize.
- #[required]
- pub body: Content,
-}
-
-impl Show for StrongElem {
- #[tracing::instrument(name = "StrongElem::show", skip_all)]
- fn show(&self, _: &mut Vt, styles: StyleChain) -> SourceResult<Content> {
- Ok(self
- .body()
- .clone()
- .styled(TextElem::set_delta(Delta(self.delta(styles)))))
- }
-}
-
-/// A delta that is summed up when folded.
-#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
-pub struct Delta(pub i64);
-
-cast! {
- Delta,
- self => self.0.into_value(),
- v: i64 => Self(v),
-}
-
-impl Fold for Delta {
- type Output = i64;
-
- fn fold(self, outer: Self::Output) -> Self::Output {
- outer + self.0
- }
-}
-
-/// Emphasizes content by setting it in italics.
-///
-/// - 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
-/// This is _emphasized._ \
-/// This is #emph[too.]
-///
-/// #show emph: it => {
-/// text(blue, it.body)
-/// }
-///
-/// This is _emphasized_ differently.
-/// ```
-///
-/// # 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.
-#[elem(title = "Emphasis", Show)]
-pub struct EmphElem {
- /// The content to emphasize.
- #[required]
- pub body: Content,
-}
-
-impl Show for EmphElem {
- #[tracing::instrument(name = "EmphElem::show", skip(self))]
- fn show(&self, _: &mut Vt, _: StyleChain) -> SourceResult<Content> {
- Ok(self.body().clone().styled(TextElem::set_emph(Toggle)))
- }
-}
-
-/// A toggle that turns on and off alternatingly if folded.
-#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
-pub struct Toggle;
-
-cast! {
- Toggle,
- self => Value::None,
- _: Value => Self,
-}
-
-impl Fold for Toggle {
- type Output = bool;
-
- fn fold(self, outer: Self::Output) -> Self::Output {
- !outer
- }
-}
-
-/// Converts text or content to lowercase.
-///
-/// # Example
-/// ```example
-/// #lower("ABC") \
-/// #lower[*My Text*] \
-/// #lower[already low]
-/// ```
-#[func(title = "Lowercase")]
-pub fn lower(
- /// The text to convert to lowercase.
- text: Caseable,
-) -> Caseable {
- case(text, Case::Lower)
-}
-
-/// Converts text or content to uppercase.
-///
-/// # Example
-/// ```example
-/// #upper("abc") \
-/// #upper[*my text*] \
-/// #upper[ALREADY HIGH]
-/// ```
-#[func(title = "Uppercase")]
-pub fn upper(
- /// The text to convert to uppercase.
- text: Caseable,
-) -> Caseable {
- case(text, Case::Upper)
-}
-
-/// Change the case of text.
-fn case(text: Caseable, case: Case) -> Caseable {
- match text {
- Caseable::Str(v) => Caseable::Str(case.apply(&v).into()),
- Caseable::Content(v) => {
- Caseable::Content(v.styled(TextElem::set_case(Some(case))))
- }
- }
-}
-
-/// A value whose case can be changed.
-pub enum Caseable {
- Str(Str),
- Content(Content),
-}
-
-cast! {
- Caseable,
- self => match self {
- Self::Str(v) => v.into_value(),
- Self::Content(v) => v.into_value(),
- },
- v: Str => Self::Str(v),
- v: Content => Self::Content(v),
-}
-
-/// A case transformation on text.
-#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)]
-pub enum Case {
- /// Everything is lowercased.
- Lower,
- /// Everything is uppercased.
- Upper,
-}
-
-impl Case {
- /// Apply the case to a string.
- pub fn apply(self, text: &str) -> String {
- match self {
- Self::Lower => text.to_lowercase(),
- Self::Upper => text.to_uppercase(),
- }
- }
-}
-
-/// Displays text in small capitals.
-///
-/// _Note:_ This enables the OpenType `smcp` feature for the font. Not all fonts
-/// support this feature. Sometimes smallcaps are part of a dedicated font and
-/// sometimes they are not available at all. In the future, this function will
-/// support selecting a dedicated smallcaps font as well as synthesizing
-/// smallcaps from normal letters, but this is not yet implemented.
-///
-/// # Example
-/// ```example
-/// #set par(justify: true)
-/// #set heading(numbering: "I.")
-///
-/// #show heading: it => {
-/// set block(below: 10pt)
-/// set text(weight: "regular")
-/// align(center, smallcaps(it))
-/// }
-///
-/// = Introduction
-/// #lorem(40)
-/// ```
-#[func(title = "Small Capitals")]
-pub fn smallcaps(
- /// The text to display to small capitals.
- body: Content,
-) -> Content {
- body.styled(TextElem::set_smallcaps(true))
-}
-
-/// Creates blind text.
-///
-/// This function yields a Latin-like _Lorem Ipsum_ blind text with the given
-/// number of words. The sequence of words generated by the function is always
-/// 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
-/// = Blind Text
-/// #lorem(30)
-///
-/// = More Blind Text
-/// #lorem(15)
-/// ```
-#[func(keywords = ["Blind Text"])]
-pub fn lorem(
- /// The length of the blind text in words.
- words: usize,
-) -> Str {
- lipsum::lipsum(words).replace("--", "–").into()
-}
diff --git a/crates/typst-library/src/text/mod.rs b/crates/typst-library/src/text/mod.rs
deleted file mode 100644
index 202ab2c5..00000000
--- a/crates/typst-library/src/text/mod.rs
+++ /dev/null
@@ -1,992 +0,0 @@
-//! Text handling.
-
-mod deco;
-mod linebreak;
-mod misc;
-mod quote;
-mod quotes;
-mod raw;
-mod shaping;
-mod shift;
-
-pub use self::deco::*;
-pub(crate) use self::linebreak::*;
-pub use self::misc::*;
-pub use self::quote::*;
-pub use self::quotes::*;
-pub use self::raw::*;
-pub use self::shaping::*;
-pub use self::shift::*;
-
-use rustybuzz::Tag;
-use ttf_parser::Rect;
-use typst::diag::{bail, error, SourceResult};
-use typst::eval::Never;
-use typst::font::{Font, FontStretch, FontStyle, FontWeight, VerticalFontMetric};
-
-use crate::layout::ParElem;
-use crate::prelude::*;
-
-/// Hook up all text definitions.
-pub(super) fn define(global: &mut Scope) {
- 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::<QuoteElem>();
- 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.
-///
-/// This function is used frequently, both with set rules and directly. While
-/// 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
-/// #set text(18pt)
-/// With a set rule.
-///
-/// #emph(text(blue)[
-/// With a function call.
-/// ])
-/// ```
-#[elem(Construct, PlainText, Repr)]
-pub struct TextElem {
- /// A font family name or priority list of font family names.
- ///
- /// When processing text, Typst tries all specified font families in order
- /// until it finds a font that has the necessary glyphs. In the example
- /// below, the font `Inria Serif` is preferred, but since it does not
- /// contain Arabic glyphs, the arabic text uses `Noto Sans Arabic` instead.
- ///
- /// The collection of available fonts differs by platform:
- ///
- /// - In the web app, you can see the list of available fonts by clicking on
- /// the "Ag" button. You can provide additional fonts by uploading `.ttf`
- /// or `.otf` files into your project. They will be discovered
- /// automatically.
- ///
- /// - Locally, Typst uses your installed system fonts. In addition, you can
- /// use the `--font-path` argument or `TYPST_FONT_PATHS` environment
- /// variable to add directories that should be scanned for fonts.
- ///
- /// ```example
- /// #set text(font: "PT Sans")
- /// This is sans-serif.
- ///
- /// #set text(font: (
- /// "Inria Serif",
- /// "Noto Sans Arabic",
- /// ))
- ///
- /// This is Latin. \
- /// هذا عربي.
- /// ```
- #[default(FontList(vec![FontFamily::new("Linux Libertine")]))]
- #[borrowed]
- #[ghost]
- pub font: FontList,
-
- /// Whether to allow last resort font fallback when the primary font list
- /// contains no match. This lets Typst search through all available fonts
- /// for the most similar one that has the necessary glyphs.
- ///
- /// _Note:_ Currently, there are no warnings when fallback is disabled and
- /// no glyphs are found. Instead, your text shows up in the form of "tofus":
- /// Small boxes that indicate the lack of an appropriate glyph. In the
- /// future, you will be able to instruct Typst to issue warnings so you know
- /// something is up.
- ///
- /// ```example
- /// #set text(font: "Inria Serif")
- /// هذا عربي
- ///
- /// #set text(fallback: false)
- /// هذا عربي
- /// ```
- #[default(true)]
- #[ghost]
- pub fallback: bool,
-
- /// The desired font style.
- ///
- /// When an italic style is requested and only an oblique one is available,
- /// it is used. Similarly, the other way around, an italic style can stand
- /// in for an oblique one. When neither an italic nor an oblique style is
- /// available, Typst selects the normal style. Since most fonts are only
- /// available either in an italic or oblique style, the difference between
- /// italic and oblique style is rarely observable.
- ///
- /// If you want to emphasize your text, you should do so using the
- /// [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]
- /// #text(font: "DejaVu Sans", style: "oblique")[Oblique]
- /// ```
- #[ghost]
- pub style: FontStyle,
-
- /// The desired thickness of the font's glyphs. Accepts an integer between
- /// `{100}` and `{900}` or one of the predefined weight names. When the
- /// desired weight is not available, Typst selects the font from the family
- /// that is closest in weight.
- ///
- /// If you want to strongly emphasize your text, you should do so using 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.
- ///
- /// ```example
- /// #set text(font: "IBM Plex Sans")
- ///
- /// #text(weight: "light")[Light] \
- /// #text(weight: "regular")[Regular] \
- /// #text(weight: "medium")[Medium] \
- /// #text(weight: 500)[Medium] \
- /// #text(weight: "bold")[Bold]
- /// ```
- #[ghost]
- pub weight: FontWeight,
-
- /// The desired width of the glyphs. Accepts a ratio between `{50%}` and
- /// `{200%}`. When the desired width is not available, Typst selects the
- /// font from the family that is closest in stretch. This will only stretch
- /// 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`]($text.tracking)
- /// property instead.
- ///
- /// ```example
- /// #text(stretch: 75%)[Condensed] \
- /// #text(stretch: 100%)[Normal]
- /// ```
- #[ghost]
- pub stretch: FontStretch,
-
- /// The size of the glyphs. This value forms the basis of the `em` unit:
- /// `{1em}` is equivalent to the font size.
- ///
- /// You can also give the font size itself in `em` units. Then, it is
- /// relative to the previous font size.
- ///
- /// ```example
- /// #set text(size: 20pt)
- /// very #text(1.5em)[big] text
- /// ```
- #[parse(args.named_or_find("size")?)]
- #[fold]
- #[default(Abs::pt(11.0))]
- #[ghost]
- pub size: TextSize,
-
- /// The glyph fill paint.
- ///
- /// ```example
- /// #set text(fill: red)
- /// This text is red.
- /// ```
- #[parse({
- let paint: Option<Spanned<Paint>> = args.named_or_find("fill")?;
- if let Some(paint) = &paint {
- if let Paint::Gradient(gradient) = &paint.v {
- if gradient.relative() == Smart::Custom(Relative::Self_) {
- bail!(
- error!(
- paint.span,
- "gradients on text must be relative to the parent"
- )
- .with_hint("make sure to set `relative: auto` on your text fill")
- );
- }
- }
- }
- paint.map(|paint| paint.v)
- })]
- #[default(Color::BLACK.into())]
- #[ghost]
- pub fill: Paint,
-
- /// The amount of space that should be added between characters.
- ///
- /// ```example
- /// #set text(tracking: 1.5pt)
- /// Distant text.
- /// ```
- #[resolve]
- #[ghost]
- pub tracking: Length,
-
- /// The amount of space between words.
- ///
- /// Can be given as an absolute length, but also relative to the width of
- /// the space character in the font.
- ///
- /// If you want to adjust the amount of space between characters rather than
- /// words, use the [`tracking`]($text.tracking) property instead.
- ///
- /// ```example
- /// #set text(spacing: 200%)
- /// Text with distant words.
- /// ```
- #[resolve]
- #[default(Rel::one())]
- #[ghost]
- pub spacing: Rel<Length>,
-
- /// Whether to automatically insert spacing between CJK and Latin characters.
- ///
- /// ```example
- /// #set text(cjk-latin-spacing: auto)
- /// 第4章介绍了基本的API。
- ///
- /// #set text(cjk-latin-spacing: none)
- /// 第4章介绍了基本的API。
- /// ```
- #[ghost]
- pub cjk_latin_spacing: Smart<Option<Never>>,
-
- /// An amount to shift the text baseline by.
- ///
- /// ```example
- /// A #text(baseline: 3pt)[lowered]
- /// word.
- /// ```
- #[resolve]
- #[ghost]
- pub baseline: Length,
-
- /// Whether certain glyphs can hang over into the margin in justified text.
- /// This can make justification visually more pleasing.
- ///
- /// ```example
- /// #set par(justify: true)
- /// This justified text has a hyphen in
- /// the paragraph's first line. Hanging
- /// the hyphen slightly into the margin
- /// results in a clearer paragraph edge.
- ///
- /// #set text(overhang: false)
- /// This justified text has a hyphen in
- /// the paragraph's first line. Hanging
- /// the hyphen slightly into the margin
- /// results in a clearer paragraph edge.
- /// ```
- #[default(true)]
- #[ghost]
- pub overhang: bool,
-
- /// The top end of the conceptual frame around the text used for layout and
- /// positioning. This affects the size of containers that hold text.
- ///
- /// ```example
- /// #set rect(inset: 0pt)
- /// #set text(size: 20pt)
- ///
- /// #set text(top-edge: "ascender")
- /// #rect(fill: aqua)[Typst]
- ///
- /// #set text(top-edge: "cap-height")
- /// #rect(fill: aqua)[Typst]
- /// ```
- #[default(TopEdge::Metric(TopEdgeMetric::CapHeight))]
- #[ghost]
- pub top_edge: TopEdge,
-
- /// The bottom end of the conceptual frame around the text used for layout
- /// and positioning. This affects the size of containers that hold text.
- ///
- /// ```example
- /// #set rect(inset: 0pt)
- /// #set text(size: 20pt)
- ///
- /// #set text(bottom-edge: "baseline")
- /// #rect(fill: aqua)[Typst]
- ///
- /// #set text(bottom-edge: "descender")
- /// #rect(fill: aqua)[Typst]
- /// ```
- #[default(BottomEdge::Metric(BottomEdgeMetric::Baseline))]
- #[ghost]
- pub bottom_edge: BottomEdge,
-
- /// An [ISO 639-1/2/3 language code.](https://en.wikipedia.org/wiki/ISO_639)
- ///
- /// Setting the correct language affects various parts of Typst:
- ///
- /// - The text processing pipeline can make more informed choices.
- /// - Hyphenation will use the correct patterns for the language.
- /// - [Smart quotes]($smartquote) turns into the correct quotes for the
- /// language.
- /// - And all other things which are language-aware.
- ///
- /// ```example
- /// #set text(lang: "de")
- /// #outline()
- ///
- /// = Einleitung
- /// In diesem Dokument, ...
- /// ```
- #[default(Lang::ENGLISH)]
- #[ghost]
- pub lang: Lang,
-
- /// An [ISO 3166-1 alpha-2 region code.](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2)
- ///
- /// This lets the text processing pipeline make more informed choices.
- #[ghost]
- pub region: Option<Region>,
-
- /// The OpenType writing script.
- ///
- /// The combination of `{lang}` and `{script}` determine how font features,
- /// such as glyph substitution, are implemented. Frequently the value is a
- /// modified (all-lowercase) ISO 15924 script identifier, and the `math`
- /// writing script is used for features appropriate for mathematical
- /// symbols.
- ///
- /// When set to `{auto}`, the default and recommended setting, an
- /// appropriate script is chosen for each block of characters sharing a
- /// common Unicode script property.
- ///
- /// ```example
- /// #set text(
- /// font: "Linux Libertine",
- /// size: 20pt,
- /// )
- ///
- /// #let scedilla = [Ş]
- /// #scedilla // S with a cedilla
- ///
- /// #set text(lang: "ro", script: "latn")
- /// #scedilla // S with a subscript comma
- ///
- /// #set text(lang: "ro", script: "grek")
- /// #scedilla // S with a cedilla
- /// ```
- #[ghost]
- pub script: Smart<WritingScript>,
-
- /// The dominant direction for text and inline objects. Possible values are:
- ///
- /// - `{auto}`: Automatically infer the direction from the `lang` property.
- /// - `{ltr}`: Layout text from left to right.
- /// - `{rtl}`: Layout text from right to left.
- ///
- /// When writing in right-to-left scripts like Arabic or Hebrew, you should
- /// 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
- /// [contact form](https://typst.app/contact) or our
- /// [Discord server]($community/#discord)!
- ///
- /// ```example
- /// #set text(dir: rtl)
- /// هذا عربي.
- /// ```
- #[resolve]
- #[ghost]
- pub dir: TextDir,
-
- /// 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]($text.lang) ensures that the correct
- /// hyphenation patterns are used.
- ///
- /// ```example
- /// #set page(width: 200pt)
- ///
- /// #set par(justify: true)
- /// This text illustrates how
- /// enabling hyphenation can
- /// improve justification.
- ///
- /// #set text(hyphenate: false)
- /// This text illustrates how
- /// enabling hyphenation can
- /// improve justification.
- /// ```
- #[resolve]
- #[ghost]
- pub hyphenate: Hyphenate,
-
- /// Whether to apply kerning.
- ///
- /// When enabled, specific letter pairings move closer together or further
- /// apart for a more visually pleasing result. The example below
- /// demonstrates how decreasing the gap between the "T" and "o" results in a
- /// more natural look. Setting this to `{false}` disables kerning by turning
- /// off the OpenType `kern` font feature.
- ///
- /// ```example
- /// #set text(size: 25pt)
- /// Totally
- ///
- /// #set text(kerning: false)
- /// Totally
- /// ```
- #[default(true)]
- #[ghost]
- pub kerning: bool,
-
- /// Whether to apply stylistic alternates.
- ///
- /// Sometimes fonts contain alternative glyphs for the same codepoint.
- /// Setting this to `{true}` switches to these by enabling the OpenType
- /// `salt` font feature.
- ///
- /// ```example
- /// #set text(
- /// font: "IBM Plex Sans",
- /// size: 20pt,
- /// )
- ///
- /// 0, a, g, ß
- ///
- /// #set text(alternates: true)
- /// 0, a, g, ß
- /// ```
- #[default(false)]
- #[ghost]
- pub alternates: bool,
-
- /// Which stylistic set to apply. Font designers can categorize alternative
- /// glyphs forms into stylistic sets. As this value is highly font-specific,
- /// you need to consult your font to know which sets are available. When set
- /// to an integer between `{1}` and `{20}`, enables the corresponding
- /// OpenType font feature from `ss01`, ..., `ss20`.
- #[ghost]
- pub stylistic_set: Option<StylisticSet>,
-
- /// Whether standard ligatures are active.
- ///
- /// Certain letter combinations like "fi" are often displayed as a single
- /// merged glyph called a _ligature._ Setting this to `{false}` disables
- /// these ligatures by turning off the OpenType `liga` and `clig` font
- /// features.
- ///
- /// ```example
- /// #set text(size: 20pt)
- /// A fine ligature.
- ///
- /// #set text(ligatures: false)
- /// A fine ligature.
- /// ```
- #[default(true)]
- #[ghost]
- pub ligatures: bool,
-
- /// Whether ligatures that should be used sparingly are active. Setting this
- /// to `{true}` enables the OpenType `dlig` font feature.
- #[default(false)]
- #[ghost]
- pub discretionary_ligatures: bool,
-
- /// Whether historical ligatures are active. Setting this to `{true}`
- /// enables the OpenType `hlig` font feature.
- #[default(false)]
- #[ghost]
- pub historical_ligatures: bool,
-
- /// Which kind of numbers / figures to select. When set to `{auto}`, the
- /// default numbers for the font are used.
- ///
- /// ```example
- /// #set text(font: "Noto Sans", 20pt)
- /// #set text(number-type: "lining")
- /// Number 9.
- ///
- /// #set text(number-type: "old-style")
- /// Number 9.
- /// ```
- #[ghost]
- pub number_type: Smart<NumberType>,
-
- /// The width of numbers / figures. When set to `{auto}`, the default
- /// numbers for the font are used.
- ///
- /// ```example
- /// #set text(font: "Noto Sans", 20pt)
- /// #set text(number-width: "proportional")
- /// A 12 B 34. \
- /// A 56 B 78.
- ///
- /// #set text(number-width: "tabular")
- /// A 12 B 34. \
- /// A 56 B 78.
- /// ```
- #[ghost]
- pub number_width: Smart<NumberWidth>,
-
- /// Whether to have a slash through the zero glyph. Setting this to `{true}`
- /// enables the OpenType `zero` font feature.
- ///
- /// ```example
- /// 0, #text(slashed-zero: true)[0]
- /// ```
- #[default(false)]
- #[ghost]
- pub slashed_zero: bool,
-
- /// Whether to turn numbers into fractions. Setting this to `{true}`
- /// enables the OpenType `frac` font feature.
- ///
- /// It is not advisable to enable this property globally as it will mess
- /// with all appearances of numbers after a slash (e.g., in URLs). Instead,
- /// enable it locally when you want a fraction.
- ///
- /// ```example
- /// 1/2 \
- /// #text(fractions: true)[1/2]
- /// ```
- #[default(false)]
- #[ghost]
- pub fractions: bool,
-
- /// Raw OpenType features to apply.
- ///
- /// - If given an array of strings, sets the features identified by the
- /// strings to `{1}`.
- /// - If given a dictionary mapping to numbers, sets the features
- /// identified by the keys to the values.
- ///
- /// ```example
- /// // Enable the `frac` feature manually.
- /// #set text(features: ("frac",))
- /// 1/2
- /// ```
- #[fold]
- #[ghost]
- pub features: FontFeatures,
-
- /// Content in which all text is styled according to the other arguments.
- #[external]
- #[required]
- pub body: Content,
-
- /// The text.
- #[required]
- #[variant(0)]
- pub text: EcoString,
-
- /// A delta to apply on the font weight.
- #[internal]
- #[fold]
- #[ghost]
- pub delta: Delta,
-
- /// Whether the font style should be inverted.
- #[internal]
- #[fold]
- #[default(false)]
- #[ghost]
- pub emph: Toggle,
-
- /// Decorative lines.
- #[internal]
- #[fold]
- #[ghost]
- pub deco: Decoration,
-
- /// A case transformation that should be applied to the text.
- #[internal]
- #[ghost]
- pub case: Option<Case>,
-
- /// Whether small capital glyphs should be used. ("smcp")
- #[internal]
- #[default(false)]
- #[ghost]
- pub smallcaps: bool,
-}
-
-impl TextElem {
- /// Create a new packed text element.
- pub fn packed(text: impl Into<EcoString>) -> Content {
- Self::new(text.into()).pack()
- }
-}
-
-impl Repr for TextElem {
- fn repr(&self) -> EcoString {
- eco_format!("[{}]", self.text)
- }
-}
-
-impl Construct for TextElem {
- fn construct(vm: &mut Vm, args: &mut Args) -> SourceResult<Content> {
- // The text constructor is special: It doesn't create a text element.
- // Instead, it leaves the passed argument structurally unchanged, but
- // styles all text in it.
- let styles = Self::set(vm, args)?;
- let body = args.expect::<Content>("body")?;
- Ok(body.styled_with_map(styles))
- }
-}
-
-impl PlainText for TextElem {
- fn plain_text(&self, text: &mut EcoString) {
- text.push_str(self.text());
- }
-}
-
-/// A lowercased font family like "arial".
-#[derive(Clone, Eq, PartialEq, Hash)]
-pub struct FontFamily(EcoString);
-
-impl FontFamily {
- /// Create a named font family variant.
- pub fn new(string: &str) -> Self {
- Self(string.to_lowercase().into())
- }
-
- /// The lowercased family name.
- pub fn as_str(&self) -> &str {
- &self.0
- }
-}
-
-impl Debug for FontFamily {
- fn fmt(&self, f: &mut Formatter) -> fmt::Result {
- self.0.fmt(f)
- }
-}
-
-cast! {
- FontFamily,
- self => self.0.into_value(),
- string: EcoString => Self::new(&string),
-}
-
-/// Font family fallback list.
-#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
-pub struct FontList(pub Vec<FontFamily>);
-
-impl<'a> IntoIterator for &'a FontList {
- type IntoIter = std::slice::Iter<'a, FontFamily>;
- type Item = &'a FontFamily;
-
- fn into_iter(self) -> Self::IntoIter {
- self.0.iter()
- }
-}
-
-cast! {
- FontList,
- self => if self.0.len() == 1 {
- self.0.into_iter().next().unwrap().0.into_value()
- } else {
- self.0.into_value()
- },
- family: FontFamily => Self(vec![family]),
- values: Array => Self(values.into_iter().map(|v| v.cast()).collect::<StrResult<_>>()?),
-}
-
-/// The size of text.
-#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
-pub struct TextSize(pub Length);
-
-impl Fold for TextSize {
- type Output = Abs;
-
- fn fold(self, outer: Self::Output) -> Self::Output {
- self.0.em.at(outer) + self.0.abs
- }
-}
-
-cast! {
- TextSize,
- self => self.0.into_value(),
- v: Length => Self(v),
-}
-
-/// Specifies the top edge of text.
-#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
-pub enum TopEdge {
- /// An edge specified via font metrics or bounding box.
- Metric(TopEdgeMetric),
- /// An edge specified as a length.
- Length(Length),
-}
-
-impl TopEdge {
- /// Determine if the edge is specified from bounding box info.
- pub fn is_bounds(&self) -> bool {
- matches!(self, Self::Metric(TopEdgeMetric::Bounds))
- }
-
- /// Resolve the value of the text edge given a font's metrics.
- pub fn resolve(self, font_size: Abs, font: &Font, bbox: Option<Rect>) -> Abs {
- match self {
- TopEdge::Metric(metric) => {
- if let Ok(metric) = metric.try_into() {
- font.metrics().vertical(metric).at(font_size)
- } else {
- bbox.map(|bbox| (font.to_em(bbox.y_max)).at(font_size))
- .unwrap_or_default()
- }
- }
- TopEdge::Length(length) => length.at(font_size),
- }
- }
-}
-
-cast! {
- TopEdge,
- self => match self {
- Self::Metric(metric) => metric.into_value(),
- Self::Length(length) => length.into_value(),
- },
- v: TopEdgeMetric => Self::Metric(v),
- v: Length => Self::Length(v),
-}
-
-/// Metrics that describe the top edge of text.
-#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)]
-pub enum TopEdgeMetric {
- /// The font's ascender, which typically exceeds the height of all glyphs.
- Ascender,
- /// The approximate height of uppercase letters.
- CapHeight,
- /// The approximate height of non-ascending lowercase letters.
- XHeight,
- /// The baseline on which the letters rest.
- Baseline,
- /// The top edge of the glyph's bounding box.
- Bounds,
-}
-
-impl TryInto<VerticalFontMetric> for TopEdgeMetric {
- type Error = ();
-
- fn try_into(self) -> Result<VerticalFontMetric, Self::Error> {
- match self {
- Self::Ascender => Ok(VerticalFontMetric::Ascender),
- Self::CapHeight => Ok(VerticalFontMetric::CapHeight),
- Self::XHeight => Ok(VerticalFontMetric::XHeight),
- Self::Baseline => Ok(VerticalFontMetric::Baseline),
- _ => Err(()),
- }
- }
-}
-
-/// Specifies the top edge of text.
-#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
-pub enum BottomEdge {
- /// An edge specified via font metrics or bounding box.
- Metric(BottomEdgeMetric),
- /// An edge specified as a length.
- Length(Length),
-}
-
-impl BottomEdge {
- /// Determine if the edge is specified from bounding box info.
- pub fn is_bounds(&self) -> bool {
- matches!(self, Self::Metric(BottomEdgeMetric::Bounds))
- }
-
- /// Resolve the value of the text edge given a font's metrics.
- pub fn resolve(self, font_size: Abs, font: &Font, bbox: Option<Rect>) -> Abs {
- match self {
- BottomEdge::Metric(metric) => {
- if let Ok(metric) = metric.try_into() {
- font.metrics().vertical(metric).at(font_size)
- } else {
- bbox.map(|bbox| (font.to_em(bbox.y_min)).at(font_size))
- .unwrap_or_default()
- }
- }
- BottomEdge::Length(length) => length.at(font_size),
- }
- }
-}
-
-cast! {
- BottomEdge,
- self => match self {
- Self::Metric(metric) => metric.into_value(),
- Self::Length(length) => length.into_value(),
- },
- v: BottomEdgeMetric => Self::Metric(v),
- v: Length => Self::Length(v),
-}
-
-/// Metrics that describe the bottom edge of text.
-#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)]
-pub enum BottomEdgeMetric {
- /// The baseline on which the letters rest.
- Baseline,
- /// The font's descender, which typically exceeds the depth of all glyphs.
- Descender,
- /// The bottom edge of the glyph's bounding box.
- Bounds,
-}
-
-impl TryInto<VerticalFontMetric> for BottomEdgeMetric {
- type Error = ();
-
- fn try_into(self) -> Result<VerticalFontMetric, Self::Error> {
- match self {
- Self::Baseline => Ok(VerticalFontMetric::Baseline),
- Self::Descender => Ok(VerticalFontMetric::Descender),
- _ => Err(()),
- }
- }
-}
-
-/// The direction of text and inline objects in their line.
-#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash)]
-pub struct TextDir(pub Smart<Dir>);
-
-cast! {
- TextDir,
- self => self.0.into_value(),
- v: Smart<Dir> => {
- if v.map_or(false, |dir| dir.axis() == Axis::Y) {
- bail!("text direction must be horizontal");
- }
- Self(v)
- },
-}
-
-impl Resolve for TextDir {
- type Output = Dir;
-
- fn resolve(self, styles: StyleChain) -> Self::Output {
- match self.0 {
- Smart::Auto => TextElem::lang_in(styles).dir(),
- Smart::Custom(dir) => dir,
- }
- }
-}
-
-/// Whether to hyphenate text.
-#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash)]
-pub struct Hyphenate(pub Smart<bool>);
-
-cast! {
- Hyphenate,
- self => self.0.into_value(),
- v: Smart<bool> => Self(v),
-}
-
-impl Resolve for Hyphenate {
- type Output = bool;
-
- fn resolve(self, styles: StyleChain) -> Self::Output {
- match self.0 {
- Smart::Auto => ParElem::justify_in(styles),
- Smart::Custom(v) => v,
- }
- }
-}
-
-/// A stylistic set in a font.
-#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
-pub struct StylisticSet(u8);
-
-impl StylisticSet {
- /// Create a new set, clamping to 1-20.
- pub fn new(index: u8) -> Self {
- Self(index.clamp(1, 20))
- }
-
- /// Get the value, guaranteed to be 1-20.
- pub fn get(self) -> u8 {
- self.0
- }
-}
-
-cast! {
- StylisticSet,
- self => self.0.into_value(),
- v: i64 => match v {
- 1 ..= 20 => Self::new(v as u8),
- _ => bail!("stylistic set must be between 1 and 20"),
- },
-}
-
-/// Which kind of numbers / figures to select.
-#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)]
-pub enum NumberType {
- /// Numbers that fit well with capital text (the OpenType `lnum`
- /// font feature).
- Lining,
- /// Numbers that fit well into a flow of upper- and lowercase text (the
- /// OpenType `onum` font feature).
- OldStyle,
-}
-
-/// The width of numbers / figures.
-#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)]
-pub enum NumberWidth {
- /// Numbers with glyph-specific widths (the OpenType `pnum` font feature).
- Proportional,
- /// Numbers of equal width (the OpenType `tnum` font feature).
- Tabular,
-}
-
-/// OpenType font features settings.
-#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
-pub struct FontFeatures(pub Vec<(Tag, u32)>);
-
-cast! {
- FontFeatures,
- self => self.0
- .into_iter()
- .map(|(tag, num)| {
- let bytes = tag.to_bytes();
- let key = std::str::from_utf8(&bytes).unwrap_or_default();
- (key.into(), num.into_value())
- })
- .collect::<Dict>()
- .into_value(),
- values: Array => Self(values
- .into_iter()
- .map(|v| {
- let tag = v.cast::<EcoString>()?;
- Ok((Tag::from_bytes_lossy(tag.as_bytes()), 1))
- })
- .collect::<StrResult<_>>()?),
- values: Dict => Self(values
- .into_iter()
- .map(|(k, v)| {
- let num = v.cast::<u32>()?;
- let tag = Tag::from_bytes_lossy(k.as_bytes());
- Ok((tag, num))
- })
- .collect::<StrResult<_>>()?),
-}
-
-impl Fold for FontFeatures {
- type Output = Self;
-
- fn fold(mut self, outer: Self::Output) -> Self::Output {
- self.0.extend(outer.0);
- self
- }
-}
diff --git a/crates/typst-library/src/text/quote.rs b/crates/typst-library/src/text/quote.rs
deleted file mode 100644
index 8d7bd15b..00000000
--- a/crates/typst-library/src/text/quote.rs
+++ /dev/null
@@ -1,208 +0,0 @@
-use crate::layout::{BlockElem, HElem, PadElem, Spacing, VElem};
-use crate::meta::{CitationForm, CiteElem};
-use crate::prelude::*;
-use crate::text::{SmartquoteElem, SpaceElem, TextElem};
-
-/// Displays a quote alongside an optional attribution.
-///
-/// # Example
-/// ```example
-/// Plato is often misquoted as the author of #quote[I know that I know
-/// nothing], however, this is a derivation form his original quote:
-///
-/// #set quote(block: true)
-///
-/// #quote(attribution: [Plato])[
-/// ... ἔοικα γοῦν τούτου γε σμικρῷ τινι αὐτῷ τούτῳ σοφώτερος εἶναι, ὅτι
-/// ἃ μὴ οἶδα οὐδὲ οἴομαι εἰδέναι.
-/// ]
-/// #quote(attribution: [from the Henry Cary literal translation of 1897])[
-/// ... I seem, then, in just this little thing to be wiser than this man at
-/// any rate, that what I do not know I do not think I know either.
-/// ]
-/// ```
-///
-/// By default block quotes are padded left and right by `{1em}`, alignment and
-/// padding can be controlled with show rules:
-/// ```example
-/// #set quote(block: true)
-/// #show quote: set align(center)
-/// #show quote: set pad(x: 5em)
-///
-/// #quote[
-/// You cannot pass... I am a servant of the Secret Fire, wielder of the
-/// flame of Anor. You cannot pass. The dark fire will not avail you,
-/// flame of Udûn. Go back to the Shadow! You cannot pass.
-/// ]
-/// ```
-#[elem(Finalize, Show, Synthesize)]
-pub struct QuoteElem {
- /// Whether this is a block quote.
- ///
- /// ```example
- /// An inline citation would look like
- /// this: #quote(
- /// attribution: [René Descartes]
- /// )[
- /// cogito, ergo sum
- /// ], and a block equation like this:
- /// #quote(
- /// block: true,
- /// attribution: [JFK]
- /// )[
- /// Ich bin ein Berliner.
- /// ]
- /// ```
- block: bool,
-
- /// Whether double quotes should be added around this quote.
- ///
- /// The double quotes used are inferred from the `quotes` property on
- /// [smartquote]($smartquote), which is affected by the `lang` property on
- /// [text]($text).
- ///
- /// - `{true}`: Wrap this quote in double quotes.
- /// - `{false}`: Do not wrap this quote in double quotes.
- /// - `{auto}`: Infer whether to wrap this quote in double quotes based on
- /// the `block` property. If `block` is `{false}`, double quotes are
- /// automatically added.
- ///
- /// ```example
- /// #set text(lang: "de")
- ///
- /// Ein deutsch-sprechender Author
- /// zitiert unter umständen JFK:
- /// #quote[Ich bin ein Berliner.]
- ///
- /// #set text(lang: "en")
- ///
- /// And an english speaking one may
- /// translate the quote:
- /// #quote[I am a Berliner.]
- /// ```
- quotes: Smart<bool>,
-
- /// The attribution of this quote, usually the author or source. Can be a
- /// label pointing to a bibliography entry or any content. By default only
- /// displayed for block quotes, but can be changed using a `{show}` rule.
- ///
- /// ```example
- /// #quote(attribution: [René Descartes])[
- /// cogito, ergo sum
- /// ]
- ///
- /// #show quote.where(block: false): it => {
- /// ["] + h(0pt, weak: true) + it.body + h(0pt, weak: true) + ["]
- /// if it.attribution != none [ (#it.attribution)]
- /// }
- ///
- /// #quote(
- /// attribution: link("https://typst.app/home")[typst.com]
- /// )[
- /// Compose papers faster
- /// ]
- ///
- /// #set quote(block: true)
- ///
- /// #quote(attribution: <tolkien54>)[
- /// You cannot pass... I am a servant
- /// of the Secret Fire, wielder of the
- /// flame of Anor. You cannot pass. The
- /// dark fire will not avail you, flame
- /// of Udûn. Go back to the Shadow! You
- /// cannot pass.
- /// ]
- ///
- /// #bibliography("works.bib", style: "apa")
- /// ```
- #[borrowed]
- attribution: Option<Attribution>,
-
- /// The quote.
- #[required]
- body: Content,
-}
-
-#[derive(Debug, Clone, PartialEq, Hash)]
-pub enum Attribution {
- Content(Content),
- Label(Label),
-}
-
-cast! {
- Attribution,
- self => match self {
- Self::Content(content) => content.into_value(),
- Self::Label(label) => label.into_value(),
- },
- content: Content => Self::Content(content),
- label: Label => Self::Label(label),
-}
-
-impl Synthesize for QuoteElem {
- fn synthesize(&mut self, _: &mut Vt, styles: StyleChain) -> SourceResult<()> {
- self.push_block(self.block(styles));
- self.push_quotes(self.quotes(styles));
- Ok(())
- }
-}
-
-impl Show for QuoteElem {
- fn show(&self, _: &mut Vt, styles: StyleChain) -> SourceResult<Content> {
- let mut realized = self.body().clone();
- let block = self.block(styles);
-
- if self.quotes(styles) == Smart::Custom(true) || !block {
- // Add zero-width weak spacing to make the quotes "sticky".
- let hole = HElem::hole().pack();
- let quote = SmartquoteElem::new().with_double(true).pack();
- realized =
- Content::sequence([quote.clone(), hole.clone(), realized, hole, quote]);
- }
-
- if block {
- realized = BlockElem::new().with_body(Some(realized)).pack();
-
- if let Some(attribution) = self.attribution(styles).as_ref() {
- let mut seq = vec![TextElem::packed('—'), SpaceElem::new().pack()];
-
- match attribution {
- Attribution::Content(content) => {
- seq.push(content.clone());
- }
- Attribution::Label(label) => {
- seq.push(
- CiteElem::new(*label)
- .with_form(Some(CitationForm::Prose))
- .pack(),
- );
- }
- }
-
- // Use v(0.9em, weak: true) bring the attribution closer to the
- // quote.
- let weak_v = VElem::weak(Spacing::Rel(Em::new(0.9).into())).pack();
- realized += weak_v + Content::sequence(seq).aligned(Align::END);
- }
-
- realized = PadElem::new(realized).pack();
- } else if let Some(Attribution::Label(label)) = self.attribution(styles) {
- realized += SpaceElem::new().pack() + CiteElem::new(*label).pack();
- }
-
- Ok(realized)
- }
-}
-
-impl Finalize for QuoteElem {
- fn finalize(&self, realized: Content, _: StyleChain) -> Content {
- let x = Em::new(1.0).into();
- let above = Em::new(2.4).into();
- let below = Em::new(1.8).into();
- realized
- .styled(PadElem::set_left(x))
- .styled(PadElem::set_right(x))
- .styled(BlockElem::set_above(VElem::block_around(above)))
- .styled(BlockElem::set_below(VElem::block_around(below)))
- }
-}
diff --git a/crates/typst-library/src/text/quotes.rs b/crates/typst-library/src/text/quotes.rs
deleted file mode 100644
index 7a19b9dc..00000000
--- a/crates/typst-library/src/text/quotes.rs
+++ /dev/null
@@ -1,369 +0,0 @@
-use typst::syntax::is_newline;
-use unicode_segmentation::UnicodeSegmentation;
-
-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]($text.lang).
-///
-/// # Example
-/// ```example
-/// "This is in quotes."
-///
-/// #set text(lang: "de")
-/// "Das ist in Anführungszeichen."
-///
-/// #set text(lang: "fr")
-/// "C'est entre guillemets."
-/// ```
-///
-/// # Syntax
-/// This function also has dedicated syntax: The normal quote characters
-/// (`'` and `"`). Typst automatically makes your quotes smart.
-#[elem]
-pub struct SmartquoteElem {
- /// Whether this should be a double quote.
- #[default(true)]
- pub double: bool,
-
- /// Whether smart quotes are enabled.
- ///
- /// To disable smartness for a single quote, you can also escape it with a
- /// backslash.
- ///
- /// ```example
- /// #set smartquote(enabled: false)
- ///
- /// These are "dumb" quotes.
- /// ```
- #[default(true)]
- pub enabled: bool,
-
- /// Whether to use alternative quotes.
- ///
- /// Does nothing for languages that don't have alternative quotes, or if
- /// explicit quotes were set.
- ///
- /// ```example
- /// #set text(lang: "de")
- /// #set smartquote(alternative: true)
- ///
- /// "Das ist in anderen Anführungszeichen."
- /// ```
- #[default(false)]
- pub alternative: bool,
-
- /// The quotes to use.
- ///
- /// - When set to `{auto}`, the appropriate single quotes for the
- /// [text language]($text.lang) will be used. This is the default.
- /// - Custom quotes can be passed as a string, array, or dictionary of either
- /// - [string]($str): a string consisting of two characters containing the
- /// opening and closing double quotes (characters here refer to Unicode
- /// grapheme clusters)
- /// - [array]($array): an array containing the opening and closing double
- /// quotes
- /// - [dictionary]($dictionary): an array containing the double and single
- /// quotes, each specified as either `{auto}`, string, or array
- ///
- /// ```example
- /// #set text(lang: "de")
- /// 'Das sind normale Anführungszeichen.'
- ///
- /// #set smartquote(quotes: "()")
- /// "Das sind eigene Anführungszeichen."
- ///
- /// #set smartquote(quotes: (single: ("[[", "]]"), double: auto))
- /// 'Das sind eigene Anführungszeichen.'
- /// ```
- #[borrowed]
- pub quotes: Smart<QuoteDict>,
-}
-
-/// State machine for smart quote substitution.
-#[derive(Debug, Clone)]
-pub struct Quoter {
- /// How many quotes have been opened.
- quote_depth: usize,
- /// Whether an opening quote might follow.
- expect_opening: bool,
- /// Whether the last character was numeric.
- last_num: bool,
- /// The previous type of quote character, if it was an opening quote.
- prev_quote_type: Option<bool>,
-}
-
-impl Quoter {
- /// Start quoting.
- pub fn new() -> Self {
- Self {
- quote_depth: 0,
- expect_opening: true,
- last_num: false,
- prev_quote_type: None,
- }
- }
-
- /// Process the last seen character.
- pub fn last(&mut self, c: char, is_quote: bool) {
- self.expect_opening = is_ignorable(c) || is_opening_bracket(c);
- self.last_num = c.is_numeric();
- if !is_quote {
- self.prev_quote_type = None;
- }
- }
-
- /// Process and substitute a quote.
- pub fn quote<'a>(
- &mut self,
- quotes: &Quotes<'a>,
- double: bool,
- peeked: Option<char>,
- ) -> &'a str {
- let peeked = peeked.unwrap_or(' ');
- let mut expect_opening = self.expect_opening;
- if let Some(prev_double) = self.prev_quote_type.take() {
- if double != prev_double {
- expect_opening = true;
- }
- }
-
- if expect_opening {
- self.quote_depth += 1;
- self.prev_quote_type = Some(double);
- quotes.open(double)
- } else if self.quote_depth > 0
- && (peeked.is_ascii_punctuation() || is_ignorable(peeked))
- {
- self.quote_depth -= 1;
- quotes.close(double)
- } else if self.last_num {
- quotes.prime(double)
- } else {
- quotes.fallback(double)
- }
- }
-}
-
-impl Default for Quoter {
- fn default() -> Self {
- Self::new()
- }
-}
-
-fn is_ignorable(c: char) -> bool {
- c.is_whitespace() || is_newline(c)
-}
-
-fn is_opening_bracket(c: char) -> bool {
- matches!(c, '(' | '{' | '[')
-}
-
-/// Decides which quotes to substitute smart quotes with.
-pub struct Quotes<'s> {
- /// The opening single quote.
- pub single_open: &'s str,
- /// The closing single quote.
- pub single_close: &'s str,
- /// The opening double quote.
- pub double_open: &'s str,
- /// The closing double quote.
- pub double_close: &'s str,
-}
-
-impl<'s> Quotes<'s> {
- /// Create a new `Quotes` struct with the given quotes, optionally falling
- /// back to the defaults for a language and region.
- ///
- /// The language should be specified as an all-lowercase ISO 639-1 code, the
- /// region as an all-uppercase ISO 3166-alpha2 code.
- ///
- /// Currently, the supported languages are: English, Czech, Danish, German,
- /// Swiss / Liechtensteinian German, Estonian, Icelandic, Lithuanian,
- /// Latvian, Slovak, Slovenian, Spanish, Bosnian, Finnish, Swedish, French,
- /// Hungarian, Polish, Romanian, Japanese, Traditional Chinese, Russian, and
- /// Norwegian.
- ///
- /// For unknown languages, the English quotes are used as fallback.
- pub fn new(
- quotes: &'s Smart<QuoteDict>,
- lang: Lang,
- region: Option<Region>,
- alternative: bool,
- ) -> Self {
- let region = region.as_ref().map(Region::as_str);
-
- let default = ("‘", "’", "“", "”");
- let low_high = ("‚", "‘", "„", "“");
-
- let (single_open, single_close, double_open, double_close) = match lang.as_str() {
- "de" if matches!(region, Some("CH" | "LI")) => match alternative {
- false => ("‹", "›", "«", "»"),
- true => low_high,
- },
- "cs" | "da" | "de" | "sk" | "sl" if alternative => ("›", "‹", "»", "«"),
- "cs" | "da" | "de" | "et" | "is" | "lt" | "lv" | "sk" | "sl" => low_high,
- "fr" | "ru" if alternative => default,
- "fr" => ("‹\u{00A0}", "\u{00A0}›", "«\u{00A0}", "\u{00A0}»"),
- "fi" | "sv" if alternative => ("’", "’", "»", "»"),
- "bs" | "fi" | "sv" => ("’", "’", "”", "”"),
- "es" if matches!(region, Some("ES") | None) => ("“", "”", "«", "»"),
- "hu" | "pl" | "ro" => ("’", "’", "„", "”"),
- "no" | "nb" | "nn" if alternative => low_high,
- "ru" | "no" | "nb" | "nn" | "ua" => ("’", "’", "«", "»"),
- "gr" => ("‘", "’", "«", "»"),
- _ if lang.dir() == Dir::RTL => ("’", "‘", "”", "“"),
- _ => default,
- };
-
- fn inner_or_default<'s>(
- quotes: Smart<&'s QuoteDict>,
- f: impl FnOnce(&'s QuoteDict) -> Smart<&'s QuoteSet>,
- default: [&'s str; 2],
- ) -> [&'s str; 2] {
- match quotes.and_then(f) {
- Smart::Auto => default,
- Smart::Custom(QuoteSet { open, close }) => {
- [open, close].map(|s| s.as_str())
- }
- }
- }
-
- let quotes = quotes.as_ref();
- let [single_open, single_close] =
- inner_or_default(quotes, |q| q.single.as_ref(), [single_open, single_close]);
- let [double_open, double_close] =
- inner_or_default(quotes, |q| q.double.as_ref(), [double_open, double_close]);
-
- Self {
- single_open,
- single_close,
- double_open,
- double_close,
- }
- }
-
- /// The opening quote.
- fn open(&self, double: bool) -> &'s str {
- if double {
- self.double_open
- } else {
- self.single_open
- }
- }
-
- /// The closing quote.
- fn close(&self, double: bool) -> &'s str {
- if double {
- self.double_close
- } else {
- self.single_close
- }
- }
-
- /// Which character should be used as a prime.
- fn prime(&self, double: bool) -> &'static str {
- if double {
- "″"
- } else {
- "′"
- }
- }
-
- /// Which character should be used as a fallback quote.
- fn fallback(&self, double: bool) -> &'static str {
- if double {
- "\""
- } else {
- "’"
- }
- }
-}
-
-/// An opening and closing quote.
-#[derive(Debug, Clone, Eq, PartialEq, Hash)]
-pub struct QuoteSet {
- open: EcoString,
- close: EcoString,
-}
-
-cast! {
- QuoteSet,
- self => array![self.open, self.close].into_value(),
- value: Array => {
- let [open, close] = array_to_set(value)?;
- Self { open, close }
- },
- value: Str => {
- let [open, close] = str_to_set(value.as_str())?;
- Self { open, close }
- },
-}
-
-fn str_to_set(value: &str) -> StrResult<[EcoString; 2]> {
- let mut iter = value.graphemes(true);
- match (iter.next(), iter.next(), iter.next()) {
- (Some(open), Some(close), None) => Ok([open.into(), close.into()]),
- _ => {
- let count = value.graphemes(true).count();
- bail!(
- "expected 2 characters, found {count} character{}",
- if count > 1 { "s" } else { "" }
- );
- }
- }
-}
-
-fn array_to_set(value: Array) -> StrResult<[EcoString; 2]> {
- let value = value.as_slice();
- if value.len() != 2 {
- bail!(
- "expected 2 quotes, found {} quote{}",
- value.len(),
- if value.len() > 1 { "s" } else { "" }
- );
- }
-
- let open: EcoString = value[0].clone().cast()?;
- let close: EcoString = value[1].clone().cast()?;
-
- Ok([open, close])
-}
-
-/// A dict of single and double quotes.
-#[derive(Debug, Clone, Eq, PartialEq, Hash)]
-pub struct QuoteDict {
- double: Smart<QuoteSet>,
- single: Smart<QuoteSet>,
-}
-
-cast! {
- QuoteDict,
- self => dict! { "double" => self.double, "single" => self.single }.into_value(),
- mut value: Dict => {
- let keys = ["double", "single"];
-
- let double = value
- .take("double")
- .ok()
- .map(FromValue::from_value)
- .transpose()?
- .unwrap_or(Smart::Auto);
- let single = value
- .take("single")
- .ok()
- .map(FromValue::from_value)
- .transpose()?
- .unwrap_or(Smart::Auto);
-
- value.finish(&keys)?;
-
- Self { single, double }
- },
- value: QuoteSet => Self {
- double: Smart::Custom(value),
- single: Smart::Auto,
- },
-}
diff --git a/crates/typst-library/src/text/raw.rs b/crates/typst-library/src/text/raw.rs
deleted file mode 100644
index c5e152cf..00000000
--- a/crates/typst-library/src/text/raw.rs
+++ /dev/null
@@ -1,835 +0,0 @@
-use std::hash::Hash;
-use std::ops::Range;
-use std::sync::Arc;
-
-use ecow::EcoVec;
-use once_cell::sync::Lazy;
-use once_cell::unsync::Lazy as UnsyncLazy;
-use syntect::highlighting as synt;
-use syntect::parsing::{SyntaxDefinition, SyntaxSet, SyntaxSetBuilder};
-use typst::diag::FileError;
-use typst::eval::Bytes;
-use typst::syntax::{self, split_newlines, LinkedNode};
-use typst::util::option_eq;
-use unicode_segmentation::UnicodeSegmentation;
-
-use crate::layout::BlockElem;
-use crate::meta::Figurable;
-use crate::prelude::*;
-use crate::text::{
- FontFamily, FontList, Hyphenate, LinebreakElem, SmartquoteElem, TextElem, TextSize,
-};
-
-// Shorthand for highlighter closures.
-type StyleFn<'a> = &'a mut dyn FnMut(&LinkedNode, Range<usize>, synt::Style) -> Content;
-type LineFn<'a> = &'a mut dyn FnMut(i64, Range<usize>, &mut Vec<Content>);
-
-/// Raw text with optional syntax highlighting.
-///
-/// Displays the text verbatim and in a monospace font. This is typically used
-/// to embed computer code into your document.
-///
-/// # Example
-/// ````example
-/// Adding `rbx` to `rcx` gives
-/// the desired result.
-///
-/// What is ```rust fn main()``` in Rust
-/// would be ```c int main()``` in C.
-///
-/// ```rust
-/// fn main() {
-/// println!("Hello World!");
-/// }
-/// ```
-///
-/// This has ``` `backticks` ``` in it
-/// (but the spaces are trimmed). And
-/// ``` here``` the leading space is
-/// also trimmed.
-/// ````
-///
-/// # 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
-/// language tag for syntax highlighting directly after the opening backticks.
-/// Within raw blocks, everything (except for the language tag, if applicable)
-/// is rendered as is, in particular, there are no escape sequences.
-///
-/// The language tag is an identifier that directly follows the opening
-/// backticks only if there are three or more backticks. If your text starts
-/// with something that looks like an identifier, but no syntax highlighting is
-/// 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).
-#[elem(
- scope,
- title = "Raw Text / Code",
- Synthesize,
- Show,
- Finalize,
- LocalName,
- Figurable,
- PlainText
-)]
-pub struct RawElem {
- /// The raw text.
- ///
- /// You can also use raw blocks creatively to create custom syntaxes for
- /// your automations.
- ///
- /// ````example
- /// // Parse numbers in raw blocks with the
- /// // `mydsl` tag and sum them up.
- /// #show raw.where(lang: "mydsl"): it => {
- /// let sum = 0
- /// for part in it.text.split("+") {
- /// sum += int(part.trim())
- /// }
- /// sum
- /// }
- ///
- /// ```mydsl
- /// 1 + 2 + 3 + 4 + 5
- /// ```
- /// ````
- #[required]
- pub text: EcoString,
-
- /// Whether the raw text is displayed as a separate block.
- ///
- /// In markup mode, using one-backtick notation makes this `{false}`.
- /// Using three-backtick notation makes it `{true}` if the enclosed content
- /// contains at least one line break.
- ///
- /// ````example
- /// // Display inline code in a small box
- /// // that retains the correct baseline.
- /// #show raw.where(block: false): box.with(
- /// fill: luma(240),
- /// inset: (x: 3pt, y: 0pt),
- /// outset: (y: 3pt),
- /// radius: 2pt,
- /// )
- ///
- /// // Display block code in a larger block
- /// // with more padding.
- /// #show raw.where(block: true): block.with(
- /// fill: luma(240),
- /// inset: 10pt,
- /// radius: 4pt,
- /// )
- ///
- /// With `rg`, you can search through your files quickly.
- /// This example searches the current directory recursively
- /// for the text `Hello World`:
- ///
- /// ```bash
- /// rg "Hello World"
- /// ```
- /// ````
- #[default(false)]
- pub block: bool,
-
- /// The language to syntax-highlight in.
- ///
- /// Apart from typical language tags known from Markdown, this supports the
- /// `{"typ"}` and `{"typc"}` tags for Typst markup and Typst code,
- /// respectively.
- ///
- /// ````example
- /// ```typ
- /// This is *Typst!*
- /// ```
- ///
- /// This is ```typ also *Typst*```, but inline!
- /// ````
- #[borrowed]
- pub lang: Option<EcoString>,
-
- /// The horizontal alignment that each line in a raw block should have.
- /// This option is ignored if this is not a raw block (if specified
- /// `block: false` or single backticks were used in markup mode).
- ///
- /// By default, this is set to `{start}`, meaning that raw text is
- /// aligned towards the start of the text direction inside the block
- /// by default, regardless of the current context's alignment (allowing
- /// you to center the raw block itself without centering the text inside
- /// it, for example).
- ///
- /// ````example
- /// #set raw(align: center)
- ///
- /// ```typc
- /// let f(x) = x
- /// code = "centered"
- /// ```
- /// ````
- #[default(HAlign::Start)]
- pub align: HAlign,
-
- /// One or multiple additional syntax definitions to load. The syntax
- /// definitions should be in the
- /// [`sublime-syntax` file format](https://www.sublimetext.com/docs/syntax.html).
- ///
- /// ````example
- /// #set raw(syntaxes: "SExpressions.sublime-syntax")
- ///
- /// ```sexp
- /// (defun factorial (x)
- /// (if (zerop x)
- /// ; with a comment
- /// 1
- /// (* x (factorial (- x 1)))))
- /// ```
- /// ````
- #[parse(
- let (syntaxes, syntaxes_data) = parse_syntaxes(vm, args)?;
- syntaxes
- )]
- #[fold]
- pub syntaxes: SyntaxPaths,
-
- /// The raw file buffers of syntax definition files.
- #[internal]
- #[parse(syntaxes_data)]
- #[fold]
- pub syntaxes_data: Vec<Bytes>,
-
- /// The theme to use for syntax highlighting. Theme files should be in the
- /// in the [`tmTheme` file format](https://www.sublimetext.com/docs/color_schemes_tmtheme.html).
- ///
- /// 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`]($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")
- /// #show raw: it => block(
- /// fill: rgb("#1d2433"),
- /// inset: 8pt,
- /// radius: 5pt,
- /// text(fill: rgb("#a2aabc"), it)
- /// )
- ///
- /// ```typ
- /// = Chapter 1
- /// #let hi = "Hello World"
- /// ```
- /// ````
- #[parse(
- let (theme_path, theme_data) = parse_theme(vm, args)?;
- theme_path.map(Some)
- )]
- #[borrowed]
- pub theme: Option<EcoString>,
-
- /// The raw file buffer of syntax theme file.
- #[internal]
- #[parse(theme_data.map(Some))]
- #[borrowed]
- pub theme_data: Option<Bytes>,
-
- /// The size for a tab stop in spaces. A tab is replaced with enough spaces to
- /// align with the next multiple of the size.
- ///
- /// ````example
- /// #set raw(tab-size: 8)
- /// ```tsv
- /// Year Month Day
- /// 2000 2 3
- /// 2001 2 1
- /// 2002 3 10
- /// ```
- /// ````
- #[default(2)]
- pub tab_size: usize,
-
- /// The stylized lines of raw text.
- ///
- /// Made accessible for the [`raw.line` element]($raw.line).
- /// Allows more styling control in `show` rules.
- #[synthesized]
- pub lines: Vec<RawLine>,
-}
-
-#[scope]
-impl RawElem {
- #[elem]
- type RawLine;
-}
-
-impl RawElem {
- /// The supported language names and tags.
- pub fn languages() -> Vec<(&'static str, Vec<&'static str>)> {
- SYNTAXES
- .syntaxes()
- .iter()
- .map(|syntax| {
- (
- syntax.name.as_str(),
- syntax.file_extensions.iter().map(|s| s.as_str()).collect(),
- )
- })
- .chain([("Typst", vec!["typ"]), ("Typst (code)", vec!["typc"])])
- .collect()
- }
-}
-
-impl Synthesize for RawElem {
- fn synthesize(&mut self, _vt: &mut Vt, styles: StyleChain) -> SourceResult<()> {
- self.push_lang(self.lang(styles).clone());
-
- let mut text = self.text().clone();
- if text.contains('\t') {
- let tab_size = RawElem::tab_size_in(styles);
- text = align_tabs(&text, tab_size);
- }
-
- let lines = split_newlines(&text);
- let count = lines.len() as i64;
-
- let lang = self
- .lang(styles)
- .as_ref()
- .as_ref()
- .map(|s| s.to_lowercase())
- .or(Some("txt".into()));
-
- let extra_syntaxes = UnsyncLazy::new(|| {
- load_syntaxes(&self.syntaxes(styles), &self.syntaxes_data(styles)).unwrap()
- });
-
- let theme = self.theme(styles).as_ref().as_ref().map(|theme_path| {
- load_theme(theme_path, self.theme_data(styles).as_ref().as_ref().unwrap())
- .unwrap()
- });
-
- let theme = theme.as_deref().unwrap_or(&THEME);
- let foreground = theme.settings.foreground.unwrap_or(synt::Color::BLACK);
-
- let mut seq = vec![];
- if matches!(lang.as_deref(), Some("typ" | "typst" | "typc")) {
- let root = match lang.as_deref() {
- Some("typc") => syntax::parse_code(&text),
- _ => syntax::parse(&text),
- };
-
- ThemedHighlighter::new(
- &text,
- LinkedNode::new(&root),
- synt::Highlighter::new(theme),
- &mut |_, range, style| styled(&text[range], foreground, style),
- &mut |i, range, line| {
- seq.push(
- RawLine::new(
- i + 1,
- count,
- EcoString::from(&text[range]),
- Content::sequence(line.drain(..)),
- )
- .spanned(self.span()),
- );
- },
- )
- .highlight();
- } else if let Some((syntax_set, syntax)) = lang.and_then(|token| {
- SYNTAXES
- .find_syntax_by_token(&token)
- .map(|syntax| (&*SYNTAXES, syntax))
- .or_else(|| {
- extra_syntaxes
- .find_syntax_by_token(&token)
- .map(|syntax| (&**extra_syntaxes, syntax))
- })
- }) {
- let mut highlighter = syntect::easy::HighlightLines::new(syntax, theme);
- for (i, line) in lines.into_iter().enumerate() {
- let mut line_content = vec![];
- for (style, piece) in
- highlighter.highlight_line(line, syntax_set).into_iter().flatten()
- {
- line_content.push(styled(piece, foreground, style));
- }
-
- seq.push(
- RawLine::new(
- i as i64 + 1,
- count,
- EcoString::from(line),
- Content::sequence(line_content),
- )
- .spanned(self.span()),
- );
- }
- } else {
- seq.extend(lines.into_iter().enumerate().map(|(i, line)| {
- RawLine::new(
- i as i64 + 1,
- count,
- EcoString::from(line),
- TextElem::packed(line),
- )
- .spanned(self.span())
- }));
- };
-
- self.push_lines(seq);
-
- Ok(())
- }
-}
-
-impl Show for RawElem {
- #[tracing::instrument(name = "RawElem::show", skip_all)]
- fn show(&self, _: &mut Vt, styles: StyleChain) -> SourceResult<Content> {
- let mut lines = EcoVec::with_capacity((2 * self.lines().len()).saturating_sub(1));
- for (i, line) in self.lines().iter().enumerate() {
- if i != 0 {
- lines.push(LinebreakElem::new().pack());
- }
-
- lines.push(line.clone().pack());
- }
-
- let mut realized = Content::sequence(lines);
- if self.block(styles) {
- // Align the text before inserting it into the block.
- realized = realized.aligned(self.align(styles).into());
- realized = BlockElem::new().with_body(Some(realized)).pack();
- }
-
- Ok(realized)
- }
-}
-
-impl Finalize for RawElem {
- fn finalize(&self, realized: Content, _: StyleChain) -> Content {
- let mut styles = Styles::new();
- styles.set(TextElem::set_overhang(false));
- styles.set(TextElem::set_hyphenate(Hyphenate(Smart::Custom(false))));
- 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));
- realized.styled_with_map(styles)
- }
-}
-
-impl LocalName for RawElem {
- fn local_name(lang: Lang, region: Option<Region>) -> &'static str {
- match lang {
- Lang::ALBANIAN => "List",
- Lang::ARABIC => "قائمة",
- Lang::BOKMÅL => "Utskrift",
- Lang::CHINESE if option_eq(region, "TW") => "程式",
- Lang::CHINESE => "代码",
- Lang::CZECH => "Seznam",
- Lang::DANISH => "Liste",
- Lang::DUTCH => "Listing",
- Lang::FILIPINO => "Listahan",
- Lang::FINNISH => "Esimerkki",
- Lang::FRENCH => "Liste",
- Lang::GERMAN => "Listing",
- Lang::GREEK => "Παράθεση",
- Lang::ITALIAN => "Codice",
- Lang::NYNORSK => "Utskrift",
- Lang::POLISH => "Program",
- Lang::ROMANIAN => "Listă", // TODO: I dunno
- Lang::RUSSIAN => "Листинг",
- Lang::SLOVENIAN => "Program",
- Lang::SPANISH => "Listado",
- Lang::SWEDISH => "Listing",
- Lang::TURKISH => "Liste",
- Lang::UKRAINIAN => "Лістинг",
- Lang::VIETNAMESE => "Chương trình", // TODO: This may be wrong.
- Lang::JAPANESE => "リスト",
- Lang::ENGLISH | _ => "Listing",
- }
- }
-}
-
-impl Figurable for RawElem {}
-
-impl PlainText for RawElem {
- fn plain_text(&self, text: &mut EcoString) {
- text.push_str(self.text());
- }
-}
-
-/// A highlighted line of raw text.
-///
-/// This is a helper element that is synthesized by [`raw`]($raw) elements.
-///
-/// It allows you to access various properties of the line, such as the line
-/// number, the raw non-highlighted text, the highlighted text, and whether it
-/// is the first or last line of the raw block.
-#[elem(name = "line", title = "Raw Text / Code Line", Show, PlainText)]
-pub struct RawLine {
- /// The line number of the raw line inside of the raw block, starts at 1.
- #[required]
- pub number: i64,
-
- /// The total number of lines in the raw block.
- #[required]
- pub count: i64,
-
- /// The line of raw text.
- #[required]
- pub text: EcoString,
-
- /// The highlighted raw text.
- #[required]
- pub body: Content,
-}
-
-impl Show for RawLine {
- fn show(&self, _vt: &mut Vt, _styles: StyleChain) -> SourceResult<Content> {
- Ok(self.body().clone())
- }
-}
-
-impl PlainText for RawLine {
- fn plain_text(&self, text: &mut EcoString) {
- text.push_str(self.text());
- }
-}
-
-/// Wrapper struct for the state required to highlight typst code.
-struct ThemedHighlighter<'a> {
- /// The code being highlighted.
- code: &'a str,
- /// The current node being highlighted.
- node: LinkedNode<'a>,
- /// The highlighter.
- highlighter: synt::Highlighter<'a>,
- /// The current scopes.
- scopes: Vec<syntect::parsing::Scope>,
- /// The current highlighted line.
- current_line: Vec<Content>,
- /// The range of the current line.
- range: Range<usize>,
- /// The current line number.
- line: i64,
- /// The function to style a piece of text.
- style_fn: StyleFn<'a>,
- /// The function to append a line.
- line_fn: LineFn<'a>,
-}
-
-impl<'a> ThemedHighlighter<'a> {
- pub fn new(
- code: &'a str,
- top: LinkedNode<'a>,
- highlighter: synt::Highlighter<'a>,
- style_fn: StyleFn<'a>,
- line_fn: LineFn<'a>,
- ) -> Self {
- Self {
- code,
- node: top,
- highlighter,
- range: 0..0,
- scopes: Vec::new(),
- current_line: Vec::new(),
- line: 0,
- style_fn,
- line_fn,
- }
- }
-
- pub fn highlight(&mut self) {
- self.highlight_inner();
-
- if !self.current_line.is_empty() {
- (self.line_fn)(
- self.line,
- self.range.start..self.code.len(),
- &mut self.current_line,
- );
-
- self.current_line.clear();
- }
- }
-
- fn highlight_inner(&mut self) {
- if self.node.children().len() == 0 {
- let style = self.highlighter.style_for_stack(&self.scopes);
- let segment = &self.code[self.node.range()];
-
- let mut len = 0;
- for (i, line) in split_newlines(segment).into_iter().enumerate() {
- if i != 0 {
- (self.line_fn)(
- self.line,
- self.range.start..self.range.end + len - 1,
- &mut self.current_line,
- );
- self.range.start = self.range.end + len;
- self.line += 1;
- }
-
- let offset = self.node.range().start + len;
- let token_range = offset..(offset + line.len());
- self.current_line
- .push((self.style_fn)(&self.node, token_range, style));
-
- len += line.len() + 1;
- }
-
- self.range.end += segment.len();
- }
-
- for child in self.node.children() {
- let mut scopes = self.scopes.clone();
- if let Some(tag) = typst::syntax::highlight(&child) {
- scopes.push(syntect::parsing::Scope::new(tag.tm_scope()).unwrap())
- }
-
- std::mem::swap(&mut scopes, &mut self.scopes);
- self.node = child;
- self.highlight_inner();
- std::mem::swap(&mut scopes, &mut self.scopes);
- }
- }
-}
-
-/// Style a piece of text with a syntect style.
-fn styled(piece: &str, foreground: synt::Color, style: synt::Style) -> Content {
- let mut body = TextElem::packed(piece);
-
- if style.foreground != foreground {
- body = body.styled(TextElem::set_fill(to_typst(style.foreground).into()));
- }
-
- if style.font_style.contains(synt::FontStyle::BOLD) {
- body = body.strong();
- }
-
- if style.font_style.contains(synt::FontStyle::ITALIC) {
- body = body.emph();
- }
-
- if style.font_style.contains(synt::FontStyle::UNDERLINE) {
- body = body.underlined();
- }
-
- body
-}
-
-fn to_typst(synt::Color { r, g, b, a }: synt::Color) -> Color {
- Color::from_u8(r, g, b, a)
-}
-
-fn to_syn(color: Color) -> synt::Color {
- let [r, g, b, a] = color.to_vec4_u8();
- synt::Color { r, g, b, a }
-}
-
-/// A list of bibliography file paths.
-#[derive(Debug, Default, Clone, PartialEq, Hash)]
-pub struct SyntaxPaths(Vec<EcoString>);
-
-cast! {
- SyntaxPaths,
- self => self.0.into_value(),
- v: EcoString => Self(vec![v]),
- v: Array => Self(v.into_iter().map(Value::cast).collect::<StrResult<_>>()?),
-}
-
-impl Fold for SyntaxPaths {
- type Output = Self;
-
- fn fold(mut self, outer: Self::Output) -> Self::Output {
- self.0.extend(outer.0);
- self
- }
-}
-
-/// Load a syntax set from a list of syntax file paths.
-#[comemo::memoize]
-fn load_syntaxes(paths: &SyntaxPaths, bytes: &[Bytes]) -> StrResult<Arc<SyntaxSet>> {
- let mut out = SyntaxSetBuilder::new();
-
- // We might have multiple sublime-syntax/yaml files
- for (path, bytes) in paths.0.iter().zip(bytes.iter()) {
- let src = std::str::from_utf8(bytes).map_err(FileError::from)?;
- out.add(SyntaxDefinition::load_from_str(src, false, None).map_err(|err| {
- eco_format!("failed to parse syntax file `{path}` ({err})")
- })?);
- }
-
- Ok(Arc::new(out.build()))
-}
-
-/// Function to parse the syntaxes argument.
-/// Much nicer than having it be part of the `element` macro.
-fn parse_syntaxes(
- vm: &mut Vm,
- args: &mut Args,
-) -> SourceResult<(Option<SyntaxPaths>, Option<Vec<Bytes>>)> {
- let Some(Spanned { v: paths, span }) =
- args.named::<Spanned<SyntaxPaths>>("syntaxes")?
- else {
- return Ok((None, None));
- };
-
- // Load syntax files.
- let data = paths
- .0
- .iter()
- .map(|path| {
- let id = vm.resolve_path(path).at(span)?;
- vm.world().file(id).at(span)
- })
- .collect::<SourceResult<Vec<Bytes>>>()?;
-
- // Check that parsing works.
- let _ = load_syntaxes(&paths, &data).at(span)?;
-
- Ok((Some(paths), Some(data)))
-}
-
-#[comemo::memoize]
-fn load_theme(path: &str, bytes: &Bytes) -> StrResult<Arc<synt::Theme>> {
- let mut cursor = std::io::Cursor::new(bytes.as_slice());
-
- synt::ThemeSet::load_from_reader(&mut cursor)
- .map(Arc::new)
- .map_err(|err| eco_format!("failed to parse theme file `{path}` ({err})"))
-}
-
-/// Function to parse the theme argument.
-/// Much nicer than having it be part of the `element` macro.
-fn parse_theme(
- vm: &mut Vm,
- args: &mut Args,
-) -> SourceResult<(Option<EcoString>, Option<Bytes>)> {
- let Some(Spanned { v: path, span }) = args.named::<Spanned<EcoString>>("theme")?
- else {
- return Ok((None, None));
- };
-
- // Load theme file.
- let id = vm.resolve_path(&path).at(span)?;
- let data = vm.world().file(id).at(span)?;
-
- // Check that parsing works.
- let _ = load_theme(&path, &data).at(span)?;
-
- Ok((Some(path), Some(data)))
-}
-
-/// The syntect syntax definitions.
-///
-/// Code for syntax set generation is below. The `syntaxes` directory is from
-/// <https://github.com/sharkdp/bat/tree/master/assets/syntaxes>
-///
-/// ```ignore
-/// fn main() {
-/// let mut builder = syntect::parsing::SyntaxSet::load_defaults_nonewlines().into_builder();
-/// builder.add_from_folder("syntaxes/02_Extra", false).unwrap();
-/// syntect::dumps::dump_to_file(&builder.build(), "syntect.bin").unwrap();
-/// }
-/// ```
-///
-/// The following syntaxes are disabled due to compatibility issues:
-/// ```text
-/// syntaxes/02_Extra/Assembly (ARM).sublime-syntax
-/// syntaxes/02_Extra/Elixir/Regular Expressions (Elixir).sublime-syntax
-/// syntaxes/02_Extra/JavaScript (Babel).sublime-syntax
-/// syntaxes/02_Extra/LiveScript.sublime-syntax
-/// syntaxes/02_Extra/PowerShell.sublime-syntax
-/// syntaxes/02_Extra/SCSS_Sass/Syntaxes/Sass.sublime-syntax
-/// syntaxes/02_Extra/SLS/SLS.sublime-syntax
-/// syntaxes/02_Extra/VimHelp.sublime-syntax
-/// syntaxes/02_Extra/cmd-help/syntaxes/cmd-help.sublime-syntax
-/// ```
-pub static SYNTAXES: Lazy<syntect::parsing::SyntaxSet> =
- Lazy::new(|| syntect::dumps::from_binary(include_bytes!("../../assets/syntect.bin")));
-
-/// The default theme used for syntax highlighting.
-pub static THEME: Lazy<synt::Theme> = Lazy::new(|| synt::Theme {
- name: Some("Typst Light".into()),
- author: Some("The Typst Project Developers".into()),
- settings: synt::ThemeSettings::default(),
- scopes: vec![
- item("comment", Some("#8a8a8a"), None),
- item("constant.character.escape", Some("#1d6c76"), None),
- item("markup.bold", None, Some(synt::FontStyle::BOLD)),
- item("markup.italic", None, Some(synt::FontStyle::ITALIC)),
- item("markup.underline", None, Some(synt::FontStyle::UNDERLINE)),
- item("markup.raw", Some("#818181"), None),
- item("string.other.math.typst", None, None),
- item("punctuation.definition.math", Some("#298e0d"), None),
- item("keyword.operator.math", Some("#1d6c76"), None),
- item("markup.heading, entity.name.section", None, Some(synt::FontStyle::BOLD)),
- item(
- "markup.heading.typst",
- None,
- Some(synt::FontStyle::BOLD | synt::FontStyle::UNDERLINE),
- ),
- item("punctuation.definition.list", Some("#8b41b1"), None),
- item("markup.list.term", None, Some(synt::FontStyle::BOLD)),
- item("entity.name.label, markup.other.reference", Some("#1d6c76"), None),
- item("keyword, constant.language, variable.language", Some("#d73a49"), None),
- item("storage.type, storage.modifier", Some("#d73a49"), None),
- item("constant", Some("#b60157"), None),
- item("string", Some("#298e0d"), None),
- item("entity.name, variable.function, support", Some("#4b69c6"), None),
- item("support.macro", Some("#16718d"), None),
- item("meta.annotation", Some("#301414"), None),
- item("entity.other, meta.interpolation", Some("#8b41b1"), None),
- ],
-});
-
-/// Create a syntect theme item.
-fn item(
- scope: &str,
- color: Option<&str>,
- font_style: Option<synt::FontStyle>,
-) -> synt::ThemeItem {
- synt::ThemeItem {
- scope: scope.parse().unwrap(),
- style: synt::StyleModifier {
- foreground: color.map(|s| to_syn(s.parse::<Color>().unwrap())),
- background: None,
- font_style,
- },
- }
-}
-
-/// Replace tabs with spaces to align with multiples of `tab_size`.
-fn align_tabs(text: &str, tab_size: usize) -> EcoString {
- let replacement = " ".repeat(tab_size);
- let divisor = tab_size.max(1);
- let amount = text.chars().filter(|&c| c == '\t').count();
-
- let mut res = EcoString::with_capacity(text.len() - amount + amount * tab_size);
- let mut column = 0;
-
- for grapheme in text.graphemes(true) {
- match grapheme {
- "\t" => {
- let required = tab_size - column % divisor;
- res.push_str(&replacement[..required]);
- column += required;
- }
- "\n" => {
- res.push_str(grapheme);
- column = 0;
- }
- _ => {
- res.push_str(grapheme);
- column += 1;
- }
- }
- }
-
- res
-}
diff --git a/crates/typst-library/src/text/shaping.rs b/crates/typst-library/src/text/shaping.rs
deleted file mode 100644
index 2d820c64..00000000
--- a/crates/typst-library/src/text/shaping.rs
+++ /dev/null
@@ -1,1123 +0,0 @@
-use std::borrow::Cow;
-use std::ops::Range;
-use std::str::FromStr;
-
-use az::SaturatingAs;
-use rustybuzz::{Feature, Tag, UnicodeBuffer};
-use typst::font::{Font, FontStyle, FontVariant};
-use typst::util::SliceExt;
-use unicode_script::{Script, UnicodeScript};
-
-use crate::layout::SpanMapper;
-use crate::prelude::*;
-use crate::text::{decorate, NumberType, NumberWidth, TextElem};
-
-/// The result of shaping text.
-///
-/// This type contains owned or borrowed shaped text runs, which can be
-/// measured, used to reshape substrings more quickly and converted into a
-/// frame.
-pub struct ShapedText<'a> {
- /// The start of the text in the full paragraph.
- pub base: usize,
- /// The text that was shaped.
- pub text: &'a str,
- /// The text direction.
- pub dir: Dir,
- /// The text language.
- pub lang: Lang,
- /// The text region.
- pub region: Option<Region>,
- /// The text's style properties.
- pub styles: StyleChain<'a>,
- /// The font variant.
- pub variant: FontVariant,
- /// The font size.
- pub size: Abs,
- /// The width of the text's bounding box.
- pub width: Abs,
- /// The shaped glyphs.
- pub glyphs: Cow<'a, [ShapedGlyph]>,
-}
-
-/// A single glyph resulting from shaping.
-#[derive(Debug, Clone)]
-pub struct ShapedGlyph {
- /// The font the glyph is contained in.
- pub font: Font,
- /// The glyph's index in the font.
- pub glyph_id: u16,
- /// The advance width of the glyph.
- pub x_advance: Em,
- /// The horizontal offset of the glyph.
- pub x_offset: Em,
- /// The vertical offset of the glyph.
- pub y_offset: Em,
- /// The adjustability of the glyph.
- pub adjustability: Adjustability,
- /// The byte range of this glyph's cluster in the full paragraph. A cluster
- /// is a sequence of one or multiple glyphs that cannot be separated and
- /// must always be treated as a union.
- ///
- /// The range values of the glyphs in a [`ShapedText`] should not
- /// overlap with each other, and they should be monotonically
- /// increasing (for left-to-right or top-to-bottom text) or
- /// monotonically decreasing (for right-to-left or bottom-to-top
- /// text).
- pub range: Range<usize>,
- /// Whether splitting the shaping result before this glyph would yield the
- /// same results as shaping the parts to both sides of `text_index`
- /// separately.
- pub safe_to_break: bool,
- /// The first char in this glyph's cluster.
- pub c: char,
- /// The source code location of the glyph and its byte offset within it.
- pub span: (Span, u16),
- /// Whether this glyph is justifiable for CJK scripts.
- pub is_justifiable: bool,
- /// The script of the glyph.
- pub script: Script,
-}
-
-#[derive(Debug, Clone, Default)]
-pub struct Adjustability {
- /// The left and right strechability
- pub stretchability: (Em, Em),
- /// The left and right shrinkability
- pub shrinkability: (Em, Em),
-}
-
-impl ShapedGlyph {
- /// Whether the glyph is a space.
- pub fn is_space(&self) -> bool {
- is_space(self.c)
- }
-
- /// Whether the glyph is justifiable.
- pub fn is_justifiable(&self) -> bool {
- // GB style is not relevant here.
- self.is_justifiable
- }
-
- /// Whether the glyph is part of a CJK script.
- pub fn is_cjk_script(&self) -> bool {
- is_cjk_script(self.c, self.script)
- }
-
- pub fn is_cjk_punctuation(&self) -> bool {
- self.is_cjk_left_aligned_punctuation(true)
- || self.is_cjk_right_aligned_punctuation()
- || self.is_cjk_center_aligned_punctuation(true)
- }
-
- /// See <https://www.w3.org/TR/clreq/#punctuation_width_adjustment>
- pub fn is_cjk_left_aligned_punctuation(&self, gb_style: bool) -> bool {
- is_cjk_left_aligned_punctuation(
- self.c,
- self.x_advance,
- self.stretchability(),
- gb_style,
- )
- }
-
- /// See <https://www.w3.org/TR/clreq/#punctuation_width_adjustment>
- pub fn is_cjk_right_aligned_punctuation(&self) -> bool {
- is_cjk_right_aligned_punctuation(self.c, self.x_advance, self.stretchability())
- }
-
- /// See <https://www.w3.org/TR/clreq/#punctuation_width_adjustment>
- pub fn is_cjk_center_aligned_punctuation(&self, gb_style: bool) -> bool {
- is_cjk_center_aligned_punctuation(self.c, gb_style)
- }
-
- /// Whether the glyph is a western letter or number.
- pub fn is_letter_or_number(&self) -> bool {
- matches!(self.c.script(), Script::Latin | Script::Greek | Script::Cyrillic)
- || matches!(self.c, '#' | '$' | '%' | '&')
- || self.c.is_ascii_digit()
- }
-
- pub fn base_adjustability(&self, gb_style: bool) -> Adjustability {
- let width = self.x_advance;
- if self.is_space() {
- Adjustability {
- // The number for spaces is from Knuth-Plass' paper
- stretchability: (Em::zero(), width / 2.0),
- shrinkability: (Em::zero(), width / 3.0),
- }
- } else if self.is_cjk_left_aligned_punctuation(gb_style) {
- Adjustability {
- stretchability: (Em::zero(), Em::zero()),
- shrinkability: (Em::zero(), width / 2.0),
- }
- } else if self.is_cjk_right_aligned_punctuation() {
- Adjustability {
- stretchability: (Em::zero(), Em::zero()),
- shrinkability: (width / 2.0, Em::zero()),
- }
- } else if self.is_cjk_center_aligned_punctuation(gb_style) {
- Adjustability {
- stretchability: (Em::zero(), Em::zero()),
- shrinkability: (width / 4.0, width / 4.0),
- }
- } else {
- Adjustability::default()
- }
- }
-
- /// The stretchability of the character.
- pub fn stretchability(&self) -> (Em, Em) {
- self.adjustability.stretchability
- }
-
- /// The shrinkability of the character.
- pub fn shrinkability(&self) -> (Em, Em) {
- self.adjustability.shrinkability
- }
-
- /// Shrink the width of glyph on the left side.
- pub fn shrink_left(&mut self, amount: Em) {
- self.x_offset -= amount;
- self.x_advance -= amount;
- self.adjustability.shrinkability.0 -= amount;
- self.adjustability.stretchability.0 += amount;
- }
-
- /// Shrink the width of glyph on the right side.
- pub fn shrink_right(&mut self, amount: Em) {
- self.x_advance -= amount;
- self.adjustability.shrinkability.1 -= amount;
- self.adjustability.stretchability.1 += amount;
- }
-}
-
-/// A side you can go toward.
-enum Side {
- /// To the left-hand side.
- Left,
- /// To the right-hand side.
- Right,
-}
-
-impl<'a> ShapedText<'a> {
- /// Build the shaped text's frame.
- ///
- /// The `justification` defines how much extra advance width each
- /// [justifiable glyph](ShapedGlyph::is_justifiable) will get.
- pub fn build(
- &self,
- vt: &Vt,
- justification_ratio: f64,
- extra_justification: Abs,
- ) -> Frame {
- let (top, bottom) = self.measure(vt);
- let size = Size::new(self.width, top + bottom);
-
- let mut offset = Abs::zero();
- let mut frame = Frame::soft(size);
- frame.set_baseline(top);
-
- let shift = TextElem::baseline_in(self.styles);
- let lang = TextElem::lang_in(self.styles);
- let decos = TextElem::deco_in(self.styles);
- let fill = TextElem::fill_in(self.styles);
-
- for ((font, y_offset), group) in
- self.glyphs.as_ref().group_by_key(|g| (g.font.clone(), g.y_offset))
- {
- let mut range = group[0].range.clone();
- for glyph in group {
- range.start = range.start.min(glyph.range.start);
- range.end = range.end.max(glyph.range.end);
- }
-
- let pos = Point::new(offset, top + shift - y_offset.at(self.size));
- let glyphs = group
- .iter()
- .map(|glyph| {
- let adjustability_left = if justification_ratio < 0.0 {
- glyph.shrinkability().0
- } else {
- glyph.stretchability().0
- };
- let adjustability_right = if justification_ratio < 0.0 {
- glyph.shrinkability().1
- } else {
- glyph.stretchability().1
- };
-
- let justification_left = adjustability_left * justification_ratio;
- let mut justification_right =
- adjustability_right * justification_ratio;
- if glyph.is_justifiable() {
- justification_right +=
- Em::from_length(extra_justification, self.size)
- }
-
- frame.size_mut().x += justification_left.at(self.size)
- + justification_right.at(self.size);
-
- Glyph {
- id: glyph.glyph_id,
- x_advance: glyph.x_advance
- + justification_left
- + justification_right,
- x_offset: glyph.x_offset + justification_left,
- range: (glyph.range.start - range.start).saturating_as()
- ..(glyph.range.end - range.start).saturating_as(),
- span: glyph.span,
- }
- })
- .collect();
-
- let item = TextItem {
- font,
- size: self.size,
- lang,
- fill: fill.clone(),
- text: self.text[range.start - self.base..range.end - self.base].into(),
- glyphs,
- };
-
- let width = item.width();
- if decos.is_empty() {
- frame.push(pos, FrameItem::Text(item));
- } else {
- // Apply line decorations.
- frame.push(pos, FrameItem::Text(item.clone()));
- for deco in &decos {
- decorate(&mut frame, deco, &item, width, shift, pos);
- }
- }
-
- offset += width;
- }
-
- // Apply metadata.
- frame.meta(self.styles, false);
-
- frame
- }
-
- /// Measure the top and bottom extent of this text.
- fn measure(&self, vt: &Vt) -> (Abs, Abs) {
- let mut top = Abs::zero();
- let mut bottom = Abs::zero();
-
- let top_edge = TextElem::top_edge_in(self.styles);
- let bottom_edge = TextElem::bottom_edge_in(self.styles);
-
- // Expand top and bottom by reading the font's vertical metrics.
- let mut expand = |font: &Font, bbox: Option<ttf_parser::Rect>| {
- top.set_max(top_edge.resolve(self.size, font, bbox));
- bottom.set_max(-bottom_edge.resolve(self.size, font, bbox));
- };
-
- if self.glyphs.is_empty() {
- // When there are no glyphs, we just use the vertical metrics of the
- // first available font.
- let world = vt.world;
- for family in families(self.styles) {
- if let Some(font) = world
- .book()
- .select(family, self.variant)
- .and_then(|id| world.font(id))
- {
- expand(&font, None);
- break;
- }
- }
- } else {
- for g in self.glyphs.iter() {
- let bbox = if top_edge.is_bounds() || bottom_edge.is_bounds() {
- g.font.ttf().glyph_bounding_box(ttf_parser::GlyphId(g.glyph_id))
- } else {
- None
- };
- expand(&g.font, bbox);
- }
- }
-
- (top, bottom)
- }
-
- /// How many glyphs are in the text where we can insert additional
- /// space when encountering underfull lines.
- pub fn justifiables(&self) -> usize {
- self.glyphs.iter().filter(|g| g.is_justifiable()).count()
- }
-
- /// Whether the last glyph is a CJK character which should not be justified
- /// on line end.
- pub fn cjk_justifiable_at_last(&self) -> bool {
- self.glyphs
- .last()
- .map(|g| g.is_cjk_script() || g.is_cjk_punctuation())
- .unwrap_or(false)
- }
-
- /// The stretchability of the text.
- pub fn stretchability(&self) -> Abs {
- self.glyphs
- .iter()
- .map(|g| g.stretchability().0 + g.stretchability().1)
- .sum::<Em>()
- .at(self.size)
- }
-
- /// The shrinkability of the text
- pub fn shrinkability(&self) -> Abs {
- self.glyphs
- .iter()
- .map(|g| g.shrinkability().0 + g.shrinkability().1)
- .sum::<Em>()
- .at(self.size)
- }
-
- /// Reshape a range of the shaped text, reusing information from this
- /// shaping process if possible.
- ///
- /// The text `range` is relative to the whole paragraph.
- pub fn reshape(
- &'a self,
- vt: &Vt,
- spans: &SpanMapper,
- text_range: Range<usize>,
- ) -> ShapedText<'a> {
- let text = &self.text[text_range.start - self.base..text_range.end - self.base];
- if let Some(glyphs) = self.slice_safe_to_break(text_range.clone()) {
- #[cfg(debug_assertions)]
- assert_all_glyphs_in_range(glyphs, text, text_range.clone());
- Self {
- base: text_range.start,
- text,
- dir: self.dir,
- lang: self.lang,
- region: self.region,
- styles: self.styles,
- size: self.size,
- variant: self.variant,
- width: glyphs.iter().map(|g| g.x_advance).sum::<Em>().at(self.size),
- glyphs: Cow::Borrowed(glyphs),
- }
- } else {
- shape(
- vt,
- text_range.start,
- text,
- spans,
- self.styles,
- self.dir,
- self.lang,
- self.region,
- )
- }
- }
-
- /// Push a hyphen to end of the text.
- pub fn push_hyphen(&mut self, vt: &Vt, fallback: bool) {
- let world = vt.world;
- let book = world.book();
- let fallback_func = if fallback {
- Some(|| book.select_fallback(None, self.variant, "-"))
- } else {
- None
- };
- let mut chain = families(self.styles)
- .map(|family| book.select(family, self.variant))
- .chain(fallback_func.iter().map(|f| f()))
- .flatten();
-
- chain.find_map(|id| {
- let font = world.font(id)?;
- let ttf = font.ttf();
- let glyph_id = ttf.glyph_index('-')?;
- let x_advance = font.to_em(ttf.glyph_hor_advance(glyph_id)?);
- let range = self
- .glyphs
- .last()
- .map(|g| g.range.end..g.range.end)
- // In the unlikely chance that we hyphenate after an empty line,
- // ensure that the glyph range still falls after self.base so
- // that subtracting either of the endpoints by self.base doesn't
- // underflow. See <https://github.com/typst/typst/issues/2283>.
- .unwrap_or_else(|| self.base..self.base);
- self.width += x_advance.at(self.size);
- self.glyphs.to_mut().push(ShapedGlyph {
- font,
- glyph_id: glyph_id.0,
- x_advance,
- x_offset: Em::zero(),
- y_offset: Em::zero(),
- adjustability: Adjustability::default(),
- range,
- safe_to_break: true,
- c: '-',
- span: (Span::detached(), 0),
- is_justifiable: false,
- script: Script::Common,
- });
- Some(())
- });
- }
-
- /// Find the subslice of glyphs that represent the given text range if both
- /// sides are safe to break.
- fn slice_safe_to_break(&self, text_range: Range<usize>) -> Option<&[ShapedGlyph]> {
- let Range { mut start, mut end } = text_range;
- if !self.dir.is_positive() {
- std::mem::swap(&mut start, &mut end);
- }
-
- let left = self.find_safe_to_break(start, Side::Left)?;
- let right = self.find_safe_to_break(end, Side::Right)?;
- Some(&self.glyphs[left..right])
- }
-
- /// Find the glyph offset matching the text index that is most towards the
- /// given side and safe-to-break.
- fn find_safe_to_break(&self, text_index: usize, towards: Side) -> Option<usize> {
- let ltr = self.dir.is_positive();
-
- // Handle edge cases.
- let len = self.glyphs.len();
- if text_index == self.base {
- return Some(if ltr { 0 } else { len });
- } else if text_index == self.base + self.text.len() {
- return Some(if ltr { len } else { 0 });
- }
-
- // Find any glyph with the text index.
- let found = self.glyphs.binary_search_by(|g: &ShapedGlyph| {
- let ordering = g.range.start.cmp(&text_index);
- if ltr {
- ordering
- } else {
- ordering.reverse()
- }
- });
- let mut idx = match found {
- Ok(idx) => idx,
- Err(idx) => {
- // Handle the special case where we break before a '\n'
- //
- // For example: (assume `a` is a CJK character with three bytes)
- // text: " a \n b "
- // index: 0 1 2 3 4 5
- // text_index: ^
- // glyphs: 0 . 1
- //
- // We will get found = Err(1), because '\n' does not have a glyph.
- // But it's safe to break here. Thus the following condition:
- // - glyphs[0].end == text_index == 3
- // - text[3] == '\n'
- return (idx > 0
- && self.glyphs[idx - 1].range.end == text_index
- && self.text[text_index - self.base..].starts_with('\n'))
- .then_some(idx);
- }
- };
-
- let next = match towards {
- Side::Left => usize::checked_sub,
- Side::Right => usize::checked_add,
- };
-
- // Search for the outermost glyph with the text index.
- while let Some(next) = next(idx, 1) {
- if self.glyphs.get(next).map_or(true, |g| g.range.start != text_index) {
- break;
- }
- idx = next;
- }
-
- // RTL needs offset one because the left side of the range should be
- // exclusive and the right side inclusive, contrary to the normal
- // behaviour of ranges.
- self.glyphs[idx].safe_to_break.then_some(idx + usize::from(!ltr))
- }
-}
-
-impl Debug for ShapedText<'_> {
- fn fmt(&self, f: &mut Formatter) -> fmt::Result {
- self.text.fmt(f)
- }
-}
-
-/// Holds shaping results and metadata common to all shaped segments.
-struct ShapingContext<'a, 'v> {
- vt: &'a Vt<'v>,
- spans: &'a SpanMapper,
- glyphs: Vec<ShapedGlyph>,
- used: Vec<Font>,
- styles: StyleChain<'a>,
- size: Abs,
- variant: FontVariant,
- tags: Vec<rustybuzz::Feature>,
- fallback: bool,
- dir: Dir,
-}
-
-/// Shape text into [`ShapedText`].
-#[allow(clippy::too_many_arguments)]
-pub fn shape<'a>(
- vt: &Vt,
- base: usize,
- text: &'a str,
- spans: &SpanMapper,
- styles: StyleChain<'a>,
- dir: Dir,
- lang: Lang,
- region: Option<Region>,
-) -> ShapedText<'a> {
- let size = TextElem::size_in(styles);
- let mut ctx = ShapingContext {
- vt,
- spans,
- size,
- glyphs: vec![],
- used: vec![],
- styles,
- variant: variant(styles),
- tags: tags(styles),
- fallback: TextElem::fallback_in(styles),
- dir,
- };
-
- if !text.is_empty() {
- shape_segment(&mut ctx, base, text, families(styles));
- }
-
- track_and_space(&mut ctx);
- calculate_adjustability(&mut ctx, lang, region);
-
- #[cfg(debug_assertions)]
- assert_all_glyphs_in_range(&ctx.glyphs, text, base..(base + text.len()));
- #[cfg(debug_assertions)]
- assert_glyph_ranges_in_order(&ctx.glyphs, dir);
-
- ShapedText {
- base,
- text,
- dir,
- lang,
- region,
- styles,
- variant: ctx.variant,
- size,
- width: ctx.glyphs.iter().map(|g| g.x_advance).sum::<Em>().at(size),
- glyphs: Cow::Owned(ctx.glyphs),
- }
-}
-
-/// Shape text with font fallback using the `families` iterator.
-fn shape_segment<'a>(
- ctx: &mut ShapingContext,
- base: usize,
- text: &str,
- mut families: impl Iterator<Item = &'a str> + Clone,
-) {
- // Fonts dont have newlines and tabs.
- if text.chars().all(|c| c == '\n' || c == '\t') {
- return;
- }
-
- // Find the next available family.
- let world = ctx.vt.world;
- let book = world.book();
- let mut selection = families.find_map(|family| {
- book.select(family, ctx.variant)
- .and_then(|id| world.font(id))
- .filter(|font| !ctx.used.contains(font))
- });
-
- // Do font fallback if the families are exhausted and fallback is enabled.
- if selection.is_none() && ctx.fallback {
- let first = ctx.used.first().map(Font::info);
- selection = book
- .select_fallback(first, ctx.variant, text)
- .and_then(|id| world.font(id))
- .filter(|font| !ctx.used.contains(font));
- }
-
- // Extract the font id or shape notdef glyphs if we couldn't find any font.
- let Some(font) = selection else {
- if let Some(font) = ctx.used.first().cloned() {
- shape_tofus(ctx, base, text, font);
- }
- return;
- };
-
- ctx.used.push(font.clone());
-
- // Fill the buffer with our text.
- let mut buffer = UnicodeBuffer::new();
- buffer.push_str(text);
- buffer.set_language(language(ctx.styles));
- if let Some(script) = TextElem::script_in(ctx.styles).as_custom().and_then(|script| {
- rustybuzz::Script::from_iso15924_tag(Tag::from_bytes(script.as_bytes()))
- }) {
- buffer.set_script(script)
- }
- buffer.set_direction(match ctx.dir {
- Dir::LTR => rustybuzz::Direction::LeftToRight,
- Dir::RTL => rustybuzz::Direction::RightToLeft,
- _ => unimplemented!("vertical text layout"),
- });
-
- // Shape!
- let buffer = rustybuzz::shape(font.rusty(), &ctx.tags, buffer);
- let infos = buffer.glyph_infos();
- let pos = buffer.glyph_positions();
- let ltr = ctx.dir.is_positive();
-
- // Collect the shaped glyphs, doing fallback and shaping parts again with
- // the next font if necessary.
- let mut i = 0;
- while i < infos.len() {
- let info = &infos[i];
- let cluster = info.cluster as usize;
-
- // Add the glyph to the shaped output.
- if info.glyph_id != 0 {
- // Determine the text range of the glyph.
- let start = base + cluster;
- let end = base
- + if ltr { i.checked_add(1) } else { i.checked_sub(1) }
- .and_then(|last| infos.get(last))
- .map_or(text.len(), |info| info.cluster as usize);
-
- let c = text[cluster..].chars().next().unwrap();
- let script = c.script();
- let x_advance = font.to_em(pos[i].x_advance);
- ctx.glyphs.push(ShapedGlyph {
- font: font.clone(),
- glyph_id: info.glyph_id as u16,
- // TODO: Don't ignore y_advance.
- x_advance,
- x_offset: font.to_em(pos[i].x_offset),
- y_offset: font.to_em(pos[i].y_offset),
- adjustability: Adjustability::default(),
- range: start..end,
- safe_to_break: !info.unsafe_to_break(),
- c,
- span: ctx.spans.span_at(start),
- is_justifiable: is_justifiable(
- c,
- script,
- x_advance,
- Adjustability::default().stretchability,
- ),
- script,
- });
- } else {
- // First, search for the end of the tofu sequence.
- let k = i;
- while infos.get(i + 1).map_or(false, |info| info.glyph_id == 0) {
- i += 1;
- }
-
- // Then, determine the start and end text index for the tofu
- // sequence.
- //
- // Examples:
- // Everything is shown in visual order. Tofus are written as "_".
- // We want to find out that the tofus span the text `2..6`.
- // Note that the clusters are longer than 1 char.
- //
- // Left-to-right:
- // Text: h a l i h a l l o
- // Glyphs: A _ _ C E
- // Clusters: 0 2 4 6 8
- // k=1 i=2
- //
- // Right-to-left:
- // Text: O L L A H I L A H
- // Glyphs: E C _ _ A
- // Clusters: 8 6 4 2 0
- // k=2 i=3
- let start = infos[if ltr { k } else { i }].cluster as usize;
- let end = if ltr { i.checked_add(1) } else { k.checked_sub(1) }
- .and_then(|last| infos.get(last))
- .map_or(text.len(), |info| info.cluster as usize);
-
- // Trim half-baked cluster.
- let remove = base + start..base + end;
- while ctx.glyphs.last().map_or(false, |g| remove.contains(&g.range.start)) {
- ctx.glyphs.pop();
- }
-
- // Recursively shape the tofu sequence with the next family.
- shape_segment(ctx, base + start, &text[start..end], families.clone());
- }
-
- i += 1;
- }
-
- ctx.used.pop();
-}
-
-/// Shape the text with tofus from the given font.
-fn shape_tofus(ctx: &mut ShapingContext, base: usize, text: &str, font: Font) {
- let x_advance = font.advance(0).unwrap_or_default();
- let add_glyph = |(cluster, c): (usize, char)| {
- let start = base + cluster;
- let end = start + c.len_utf8();
- let script = c.script();
- ctx.glyphs.push(ShapedGlyph {
- font: font.clone(),
- glyph_id: 0,
- x_advance,
- x_offset: Em::zero(),
- y_offset: Em::zero(),
- adjustability: Adjustability::default(),
- range: start..end,
- safe_to_break: true,
- c,
- span: ctx.spans.span_at(start),
- is_justifiable: is_justifiable(
- c,
- script,
- x_advance,
- Adjustability::default().stretchability,
- ),
- script,
- });
- };
- if ctx.dir.is_positive() {
- text.char_indices().for_each(add_glyph);
- } else {
- text.char_indices().rev().for_each(add_glyph);
- }
-}
-
-/// Apply tracking and spacing to the shaped glyphs.
-fn track_and_space(ctx: &mut ShapingContext) {
- let tracking = Em::from_length(TextElem::tracking_in(ctx.styles), ctx.size);
- let spacing =
- TextElem::spacing_in(ctx.styles).map(|abs| Em::from_length(abs, ctx.size));
-
- let mut glyphs = ctx.glyphs.iter_mut().peekable();
- while let Some(glyph) = glyphs.next() {
- // Make non-breaking space same width as normal space.
- if glyph.c == '\u{00A0}' {
- glyph.x_advance -= nbsp_delta(&glyph.font).unwrap_or_default();
- }
-
- if glyph.is_space() {
- glyph.x_advance = spacing.relative_to(glyph.x_advance);
- }
-
- if glyphs
- .peek()
- .map_or(false, |next| glyph.range.start != next.range.start)
- {
- glyph.x_advance += tracking;
- }
- }
-}
-
-pub fn is_gb_style(lang: Lang, region: Option<Region>) -> bool {
- // Most CJK variants, including zh-CN, ja-JP, zh-SG, zh-MY use GB-style punctuation,
- // while zh-HK and zh-TW use alternative style. We default to use GB-style.
- !(lang == Lang::CHINESE
- && matches!(region.as_ref().map(Region::as_str), Some("TW" | "HK")))
-}
-
-/// Calculate stretchability and shrinkability of each glyph,
-/// and CJK punctuation adjustments according to Chinese Layout Requirements.
-fn calculate_adjustability(ctx: &mut ShapingContext, lang: Lang, region: Option<Region>) {
- let gb_style = is_gb_style(lang, region);
-
- for glyph in &mut ctx.glyphs {
- glyph.adjustability = glyph.base_adjustability(gb_style);
- }
-
- let mut glyphs = ctx.glyphs.iter_mut().peekable();
- while let Some(glyph) = glyphs.next() {
- // Only GB style needs further adjustment.
- if glyph.is_cjk_punctuation() && !gb_style {
- continue;
- }
-
- // Now we apply consecutive punctuation adjustment, specified in Chinese Layout
- // Requirements, section 3.1.6.1 Punctuation Adjustment Space, and Japanese Layout
- // Requirements, section 3.1 Line Composition Rules for Punctuation Marks
- let Some(next) = glyphs.peek_mut() else { continue };
- let width = glyph.x_advance;
- let delta = width / 2.0;
- if glyph.is_cjk_punctuation()
- && next.is_cjk_punctuation()
- && (glyph.shrinkability().1 + next.shrinkability().0) >= delta
- {
- let left_delta = glyph.shrinkability().1.min(delta);
- glyph.shrink_right(left_delta);
- next.shrink_left(delta - left_delta);
- }
- }
-}
-
-/// Difference between non-breaking and normal space.
-fn nbsp_delta(font: &Font) -> Option<Em> {
- let space = font.ttf().glyph_index(' ')?.0;
- let nbsp = font.ttf().glyph_index('\u{00A0}')?.0;
- Some(font.advance(nbsp)? - font.advance(space)?)
-}
-
-/// Resolve the font variant.
-pub fn variant(styles: StyleChain) -> FontVariant {
- let mut variant = FontVariant::new(
- TextElem::style_in(styles),
- TextElem::weight_in(styles),
- TextElem::stretch_in(styles),
- );
-
- let delta = TextElem::delta_in(styles);
- variant.weight = variant
- .weight
- .thicken(delta.clamp(i16::MIN as i64, i16::MAX as i64) as i16);
-
- if TextElem::emph_in(styles) {
- variant.style = match variant.style {
- FontStyle::Normal => FontStyle::Italic,
- FontStyle::Italic => FontStyle::Normal,
- FontStyle::Oblique => FontStyle::Normal,
- }
- }
-
- variant
-}
-
-/// Resolve a prioritized iterator over the font families.
-pub fn families(styles: StyleChain) -> impl Iterator<Item = &str> + Clone {
- const FALLBACKS: &[&str] = &[
- "linux libertine",
- "twitter color emoji",
- "noto color emoji",
- "apple color emoji",
- "segoe ui emoji",
- ];
-
- let tail = if TextElem::fallback_in(styles) { FALLBACKS } else { &[] };
- TextElem::font_in(styles)
- .into_iter()
- .map(|family| family.as_str())
- .chain(tail.iter().copied())
-}
-
-/// Collect the tags of the OpenType features to apply.
-pub fn tags(styles: StyleChain) -> Vec<Feature> {
- let mut tags = vec![];
- let mut feat = |tag, value| {
- tags.push(Feature::new(Tag::from_bytes(tag), value, ..));
- };
-
- // Features that are on by default in Harfbuzz are only added if disabled.
- if !TextElem::kerning_in(styles) {
- feat(b"kern", 0);
- }
-
- // Features that are off by default in Harfbuzz are only added if enabled.
- if TextElem::smallcaps_in(styles) {
- feat(b"smcp", 1);
- }
-
- if TextElem::alternates_in(styles) {
- feat(b"salt", 1);
- }
-
- let storage;
- if let Some(set) = TextElem::stylistic_set_in(styles) {
- storage = [b's', b's', b'0' + set.get() / 10, b'0' + set.get() % 10];
- feat(&storage, 1);
- }
-
- if !TextElem::ligatures_in(styles) {
- feat(b"liga", 0);
- feat(b"clig", 0);
- }
-
- if TextElem::discretionary_ligatures_in(styles) {
- feat(b"dlig", 1);
- }
-
- if TextElem::historical_ligatures_in(styles) {
- feat(b"hilg", 1);
- }
-
- match TextElem::number_type_in(styles) {
- Smart::Auto => {}
- Smart::Custom(NumberType::Lining) => feat(b"lnum", 1),
- Smart::Custom(NumberType::OldStyle) => feat(b"onum", 1),
- }
-
- match TextElem::number_width_in(styles) {
- Smart::Auto => {}
- Smart::Custom(NumberWidth::Proportional) => feat(b"pnum", 1),
- Smart::Custom(NumberWidth::Tabular) => feat(b"tnum", 1),
- }
-
- if TextElem::slashed_zero_in(styles) {
- feat(b"zero", 1);
- }
-
- if TextElem::fractions_in(styles) {
- feat(b"frac", 1);
- }
-
- for (tag, value) in TextElem::features_in(styles).0 {
- tags.push(Feature::new(tag, value, ..))
- }
-
- tags
-}
-
-/// Process the language and and region of a style chain into a
-/// rustybuzz-compatible BCP 47 language.
-fn language(styles: StyleChain) -> rustybuzz::Language {
- let mut bcp: EcoString = TextElem::lang_in(styles).as_str().into();
- if let Some(region) = TextElem::region_in(styles) {
- bcp.push('-');
- bcp.push_str(region.as_str());
- }
- rustybuzz::Language::from_str(&bcp).unwrap()
-}
-
-/// Returns true if all glyphs in `glyphs` have ranges within the range `range`.
-#[cfg(debug_assertions)]
-fn assert_all_glyphs_in_range(glyphs: &[ShapedGlyph], text: &str, range: Range<usize>) {
- if glyphs
- .iter()
- .any(|g| g.range.start < range.start || g.range.end > range.end)
- {
- panic!("one or more glyphs in {text:?} fell out of range");
- }
-}
-
-/// Asserts that the ranges of `glyphs` is in the proper order according to `dir`.
-///
-/// This asserts instead of returning a bool in order to provide a more informative message when the invariant is violated.
-#[cfg(debug_assertions)]
-fn assert_glyph_ranges_in_order(glyphs: &[ShapedGlyph], dir: Dir) {
- if glyphs.is_empty() {
- return;
- }
-
- // Iterator::is_sorted and friends are unstable as of Rust 1.70.0
- for i in 0..(glyphs.len() - 1) {
- let a = &glyphs[i];
- let b = &glyphs[i + 1];
- let ord = a.range.start.cmp(&b.range.start);
- let ord = if dir.is_positive() { ord } else { ord.reverse() };
- if ord == std::cmp::Ordering::Greater {
- panic!(
- "glyph ranges should be monotonically {}, \
- but found glyphs out of order:\n\n\
- first: {a:#?}\nsecond: {b:#?}",
- if dir.is_positive() { "increasing" } else { "decreasing" },
- );
- }
- }
-}
-
-// The CJK punctuation that can appear at the beginning or end of a line.
-pub(crate) const BEGIN_PUNCT_PAT: &[char] =
- &['“', '‘', '《', '〈', '(', '『', '「', '【', '〖', '〔', '[', '{'];
-pub(crate) const END_PUNCT_PAT: &[char] = &[
- '”', '’', ',', '.', '。', '、', ':', ';', '》', '〉', ')', '』', '」', '】',
- '〗', '〕', ']', '}', '?', '!',
-];
-
-/// Whether the glyph is a space.
-#[inline]
-fn is_space(c: char) -> bool {
- matches!(c, ' ' | '\u{00A0}' | ' ')
-}
-
-/// Whether the glyph is part of a CJK script.
-#[inline]
-pub fn char_is_cjk_script(c: char) -> bool {
- is_cjk_script(c, c.script())
-}
-
-/// Whether the glyph is part of a CJK script.
-#[inline]
-fn is_cjk_script(c: char, script: Script) -> bool {
- use Script::*;
- // U+30FC: Katakana-Hiragana Prolonged Sound Mark
- matches!(script, Hiragana | Katakana | Han) || c == '\u{30FC}'
-}
-
-/// See <https://www.w3.org/TR/clreq/#punctuation_width_adjustment>
-#[inline]
-fn is_cjk_left_aligned_punctuation(
- c: char,
- x_advance: Em,
- stretchability: (Em, Em),
- gb_style: bool,
-) -> bool {
- // CJK quotation marks shares codepoints with latin quotation marks.
- // But only the CJK ones have full width.
- if matches!(c, '”' | '’') && x_advance + stretchability.1 == Em::one() {
- return true;
- }
-
- if gb_style && matches!(c, ',' | '。' | '.' | '、' | ':' | ';' | '!' | '?')
- {
- // In GB style, exclamations and question marks are also left aligned and can be adjusted.
- // Note that they are not adjustable in other styles.
- return true;
- }
-
- // See appendix A.3 https://www.w3.org/TR/clreq/#tables_of_chinese_punctuation_marks
- matches!(c, '》' | ')' | '』' | '」' | '】' | '〗' | '〕' | '〉' | ']' | '}')
-}
-
-/// See <https://www.w3.org/TR/clreq/#punctuation_width_adjustment>
-#[inline]
-fn is_cjk_right_aligned_punctuation(
- c: char,
- x_advance: Em,
- stretchability: (Em, Em),
-) -> bool {
- // CJK quotation marks shares codepoints with latin quotation marks.
- // But only the CJK ones have full width.
- if matches!(c, '“' | '‘') && x_advance + stretchability.0 == Em::one() {
- return true;
- }
- // See appendix A.3 https://www.w3.org/TR/clreq/#tables_of_chinese_punctuation_marks
- matches!(c, '《' | '(' | '『' | '「' | '【' | '〖' | '〔' | '〈' | '[' | '{')
-}
-
-/// See <https://www.w3.org/TR/clreq/#punctuation_width_adjustment>
-#[inline]
-fn is_cjk_center_aligned_punctuation(c: char, gb_style: bool) -> bool {
- if !gb_style && matches!(c, ',' | '。' | '.' | '、' | ':' | ';') {
- return true;
- }
-
- // U+30FB: Katakana Middle Dot
- matches!(c, '\u{30FB}')
-}
-
-/// Whether the glyph is justifiable.
-///
-/// Quotations in latin script and CJK are unfortunately the same codepoint
-/// (U+2018, U+2019, U+201C, U+201D), but quotations in Chinese must be
-/// fullwidth. This heuristics can therefore fail for monospace latin fonts.
-/// However, since monospace fonts are usually not justified this edge case
-/// should be rare enough.
-#[inline]
-fn is_justifiable(
- c: char,
- script: Script,
- x_advance: Em,
- stretchability: (Em, Em),
-) -> bool {
- // GB style is not relevant here.
- is_space(c)
- || is_cjk_script(c, script)
- || is_cjk_left_aligned_punctuation(c, x_advance, stretchability, true)
- || is_cjk_right_aligned_punctuation(c, x_advance, stretchability)
- || is_cjk_center_aligned_punctuation(c, true)
-}
diff --git a/crates/typst-library/src/text/shift.rs b/crates/typst-library/src/text/shift.rs
deleted file mode 100644
index a1862098..00000000
--- a/crates/typst-library/src/text/shift.rs
+++ /dev/null
@@ -1,223 +0,0 @@
-use crate::prelude::*;
-use crate::text::{variant, SpaceElem, TextElem, TextSize};
-
-/// Renders text in subscript.
-///
-/// The text is rendered smaller and its baseline is lowered.
-///
-/// # Example
-/// ```example
-/// Revenue#sub[yearly]
-/// ```
-#[elem(title = "Subscript", Show)]
-pub struct SubElem {
- /// Whether to prefer the dedicated subscript characters of the font.
- ///
- /// If this is enabled, Typst first tries to transform the text to subscript
- /// codepoints. If that fails, it falls back to rendering lowered and shrunk
- /// normal letters.
- ///
- /// ```example
- /// N#sub(typographic: true)[1]
- /// N#sub(typographic: false)[1]
- /// ```
- #[default(true)]
- pub typographic: bool,
-
- /// The baseline shift for synthetic subscripts. Does not apply if
- /// `typographic` is true and the font has subscript codepoints for the
- /// given `body`.
- #[default(Em::new(0.2).into())]
- pub baseline: Length,
-
- /// The font size for synthetic subscripts. Does not apply if
- /// `typographic` is true and the font has subscript codepoints for the
- /// given `body`.
- #[default(TextSize(Em::new(0.6).into()))]
- pub size: TextSize,
-
- /// The text to display in subscript.
- #[required]
- pub body: Content,
-}
-
-impl Show for SubElem {
- #[tracing::instrument(name = "SubElem::show", skip_all)]
- fn show(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Content> {
- let body = self.body().clone();
- let mut transformed = None;
- if self.typographic(styles) {
- if let Some(text) = search_text(&body, true) {
- if is_shapable(vt, &text, styles) {
- transformed = Some(TextElem::packed(text));
- }
- }
- };
-
- Ok(transformed.unwrap_or_else(|| {
- body.styled(TextElem::set_baseline(self.baseline(styles)))
- .styled(TextElem::set_size(self.size(styles)))
- }))
- }
-}
-
-/// Renders text in superscript.
-///
-/// The text is rendered smaller and its baseline is raised.
-///
-/// # Example
-/// ```example
-/// 1#super[st] try!
-/// ```
-#[elem(title = "Superscript", Show)]
-pub struct SuperElem {
- /// Whether to prefer the dedicated superscript characters of the font.
- ///
- /// If this is enabled, Typst first tries to transform the text to
- /// superscript codepoints. If that fails, it falls back to rendering
- /// raised and shrunk normal letters.
- ///
- /// ```example
- /// N#super(typographic: true)[1]
- /// N#super(typographic: false)[1]
- /// ```
- #[default(true)]
- pub typographic: bool,
-
- /// The baseline shift for synthetic superscripts. Does not apply if
- /// `typographic` is true and the font has superscript codepoints for the
- /// given `body`.
- #[default(Em::new(-0.5).into())]
- pub baseline: Length,
-
- /// The font size for synthetic superscripts. Does not apply if
- /// `typographic` is true and the font has superscript codepoints for the
- /// given `body`.
- #[default(TextSize(Em::new(0.6).into()))]
- pub size: TextSize,
-
- /// The text to display in superscript.
- #[required]
- pub body: Content,
-}
-
-impl Show for SuperElem {
- #[tracing::instrument(name = "SuperElem::show", skip_all)]
- fn show(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Content> {
- let body = self.body().clone();
- let mut transformed = None;
- if self.typographic(styles) {
- if let Some(text) = search_text(&body, false) {
- if is_shapable(vt, &text, styles) {
- transformed = Some(TextElem::packed(text));
- }
- }
- };
-
- Ok(transformed.unwrap_or_else(|| {
- body.styled(TextElem::set_baseline(self.baseline(styles)))
- .styled(TextElem::set_size(self.size(styles)))
- }))
- }
-}
-
-/// Find and transform the text contained in `content` to the given script kind
-/// if and only if it only consists of `Text`, `Space`, and `Empty` leafs.
-fn search_text(content: &Content, sub: bool) -> Option<EcoString> {
- if content.is::<SpaceElem>() {
- Some(' '.into())
- } else if let Some(elem) = content.to::<TextElem>() {
- convert_script(elem.text(), sub)
- } else if let Some(children) = content.to_sequence() {
- let mut full = EcoString::new();
- for item in children {
- match search_text(item, sub) {
- Some(text) => full.push_str(&text),
- None => return None,
- }
- }
- Some(full)
- } else {
- None
- }
-}
-
-/// Checks whether the first retrievable family contains all code points of the
-/// given string.
-fn is_shapable(vt: &Vt, text: &str, styles: StyleChain) -> bool {
- let world = vt.world;
- for family in TextElem::font_in(styles) {
- if let Some(font) = world
- .book()
- .select(family.as_str(), variant(styles))
- .and_then(|id| world.font(id))
- {
- return text.chars().all(|c| font.ttf().glyph_index(c).is_some());
- }
- }
-
- false
-}
-
-/// Convert a string to sub- or superscript codepoints if all characters
-/// can be mapped to such a codepoint.
-fn convert_script(text: &str, sub: bool) -> Option<EcoString> {
- let mut result = EcoString::with_capacity(text.len());
- let converter = if sub { to_subscript_codepoint } else { to_superscript_codepoint };
-
- for c in text.chars() {
- match converter(c) {
- Some(c) => result.push(c),
- None => return None,
- }
- }
-
- Some(result)
-}
-
-/// Convert a character to its corresponding Unicode superscript.
-fn to_superscript_codepoint(c: char) -> Option<char> {
- char::from_u32(match c {
- '0' => 0x2070,
- '1' => 0x00B9,
- '2' => 0x00B2,
- '3' => 0x00B3,
- '4'..='9' => 0x2070 + (c as u32 + 4 - '4' as u32),
- '+' => 0x207A,
- '-' => 0x207B,
- '=' => 0x207C,
- '(' => 0x207D,
- ')' => 0x207E,
- 'n' => 0x207F,
- 'i' => 0x2071,
- ' ' => 0x0020,
- _ => return None,
- })
-}
-
-/// Convert a character to its corresponding Unicode subscript.
-fn to_subscript_codepoint(c: char) -> Option<char> {
- char::from_u32(match c {
- '0' => 0x2080,
- '1'..='9' => 0x2080 + (c as u32 - '0' as u32),
- '+' => 0x208A,
- '-' => 0x208B,
- '=' => 0x208C,
- '(' => 0x208D,
- ')' => 0x208E,
- 'a' => 0x2090,
- 'e' => 0x2091,
- 'o' => 0x2092,
- 'x' => 0x2093,
- 'h' => 0x2095,
- 'k' => 0x2096,
- 'l' => 0x2097,
- 'm' => 0x2098,
- 'n' => 0x2099,
- 'p' => 0x209A,
- 's' => 0x209B,
- 't' => 0x209C,
- ' ' => 0x0020,
- _ => return None,
- })
-}
diff --git a/crates/typst-library/src/visualize/image.rs b/crates/typst-library/src/visualize/image.rs
deleted file mode 100644
index 0996ae7c..00000000
--- a/crates/typst-library/src/visualize/image.rs
+++ /dev/null
@@ -1,271 +0,0 @@
-use std::ffi::OsStr;
-
-use typst::image::{Image, ImageFormat, RasterFormat, VectorFormat};
-use typst::util::option_eq;
-
-use crate::compute::Readable;
-use crate::meta::Figurable;
-use crate::prelude::*;
-use crate::text::families;
-
-/// A raster or vector graphic.
-///
-/// Supported formats are PNG, JPEG, GIF and SVG.
-///
-/// _Note:_ Work on SVG export is ongoing and there might be visual inaccuracies
-/// 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
-/// #figure(
-/// image("molecular.jpg", width: 80%),
-/// caption: [
-/// A step in the molecular testing
-/// pipeline of our lab.
-/// ],
-/// )
-/// ```
-///
-/// [gh-svg]: https://github.com/typst/typst/issues?q=is%3Aopen+is%3Aissue+label%3Asvg
-#[elem(scope, Layout, LocalName, Figurable)]
-pub struct ImageElem {
- /// Path to an image file.
- #[required]
- #[parse(
- let Spanned { v: path, span } =
- args.expect::<Spanned<EcoString>>("path to image file")?;
- let id = vm.resolve_path(&path).at(span)?;
- let data = vm.world().file(id).at(span)?;
- path
- )]
- #[borrowed]
- pub path: EcoString,
-
- /// The raw file data.
- #[internal]
- #[required]
- #[parse(Readable::Bytes(data))]
- pub data: Readable,
-
- /// The image's format. Detected automatically by default.
- pub format: Smart<ImageFormat>,
-
- /// The width of the image.
- pub width: Smart<Rel<Length>>,
-
- /// The height of the image.
- pub height: Smart<Rel<Length>>,
-
- /// A text describing the image.
- pub alt: Option<EcoString>,
-
- /// How the image should adjust itself to a given area.
- #[default(ImageFit::Cover)]
- pub fit: ImageFit,
-}
-
-#[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())
- }
-}
-
-impl Layout for ImageElem {
- #[tracing::instrument(name = "ImageElem::layout", skip_all)]
- fn layout(
- &self,
- vt: &mut Vt,
- styles: StyleChain,
- regions: Regions,
- ) -> SourceResult<Fragment> {
- // Take the format that was explicitly defined, or parse the extension,
- // or try to detect the format.
- let data = self.data();
- let format = match self.format(styles) {
- Smart::Custom(v) => v,
- Smart::Auto => {
- let ext = std::path::Path::new(self.path().as_str())
- .extension()
- .and_then(OsStr::to_str)
- .unwrap_or_default()
- .to_lowercase();
-
- match ext.as_str() {
- "png" => ImageFormat::Raster(RasterFormat::Png),
- "jpg" | "jpeg" => ImageFormat::Raster(RasterFormat::Jpg),
- "gif" => ImageFormat::Raster(RasterFormat::Gif),
- "svg" | "svgz" => ImageFormat::Vector(VectorFormat::Svg),
- _ => match &data {
- Readable::Str(_) => ImageFormat::Vector(VectorFormat::Svg),
- Readable::Bytes(bytes) => match RasterFormat::detect(bytes) {
- Some(f) => ImageFormat::Raster(f),
- None => bail!(self.span(), "unknown image format"),
- },
- },
- }
- }
- };
-
- let image = Image::with_fonts(
- data.clone().into(),
- format,
- self.alt(styles),
- vt.world,
- &families(styles).map(|s| s.into()).collect::<Vec<_>>(),
- )
- .at(self.span())?;
-
- let sizing = Axes::new(self.width(styles), self.height(styles));
- let region = sizing
- .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;
- let region_ratio = region.x / region.y;
-
- // Find out whether the image is wider or taller than the target size.
- let pxw = image.width() as f64;
- let pxh = image.height() as f64;
- let px_ratio = pxw / pxh;
- let wide = px_ratio > region_ratio;
-
- // The space into which the image will be placed according to its fit.
- let target = if expand.x && expand.y {
- region
- } else if expand.x || (!expand.y && wide && region.x.is_finite()) {
- Size::new(region.x, region.y.min(region.x.safe_div(px_ratio)))
- } else if region.y.is_finite() {
- Size::new(region.x.min(region.y * px_ratio), region.y)
- } else {
- Size::new(Abs::pt(pxw), Abs::pt(pxh))
- };
-
- // Compute the actual size of the fitted image.
- let fit = self.fit(styles);
- let fitted = match fit {
- ImageFit::Cover | ImageFit::Contain => {
- if wide == (fit == ImageFit::Contain) {
- Size::new(target.x, target.x / px_ratio)
- } else {
- Size::new(target.y * px_ratio, target.y)
- }
- }
- ImageFit::Stretch => target,
- };
-
- // First, place the image in a frame of exactly its size and then resize
- // the frame to the target size, center aligning the image in the
- // process.
- let mut frame = Frame::soft(fitted);
- frame.push(Point::zero(), FrameItem::Image(image, fitted, self.span()));
- 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) {
- frame.clip(Path::rect(frame.size()));
- }
-
- // Apply metadata.
- frame.meta(styles, false);
-
- Ok(Fragment::frame(frame))
- }
-}
-
-impl LocalName for ImageElem {
- fn local_name(lang: Lang, region: Option<Region>) -> &'static str {
- match lang {
- Lang::ALBANIAN => "Figurë",
- Lang::ARABIC => "شكل",
- Lang::BOKMÅL => "Figur",
- Lang::CHINESE if option_eq(region, "TW") => "圖",
- Lang::CHINESE => "图",
- Lang::CZECH => "Obrázek",
- Lang::DANISH => "Figur",
- Lang::DUTCH => "Figuur",
- Lang::FILIPINO => "Pigura",
- Lang::FINNISH => "Kuva",
- Lang::FRENCH => "Fig.",
- Lang::GERMAN => "Abbildung",
- Lang::GREEK => "Σχήμα",
- Lang::HUNGARIAN => "Ábra",
- Lang::ITALIAN => "Figura",
- Lang::NYNORSK => "Figur",
- Lang::POLISH => "Rysunek",
- Lang::PORTUGUESE => "Figura",
- Lang::ROMANIAN => "Figura",
- Lang::RUSSIAN => "Рис.",
- Lang::SLOVENIAN => "Slika",
- Lang::SPANISH => "Figura",
- Lang::SWEDISH => "Figur",
- Lang::TURKISH => "Şekil",
- Lang::UKRAINIAN => "Рисунок",
- Lang::VIETNAMESE => "Hình",
- Lang::JAPANESE => "図",
- Lang::ENGLISH | _ => "Figure",
- }
- }
-}
-
-impl Figurable for ImageElem {}
-
-/// How an image should adjust itself to a given area.
-#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)]
-pub enum ImageFit {
- /// The image should completely cover the area. This is the default.
- Cover,
- /// The image should be fully contained in the area.
- Contain,
- /// The image should be stretched so that it exactly fills the area, even if
- /// this means that the image will be distorted.
- Stretch,
-}
diff --git a/crates/typst-library/src/visualize/line.rs b/crates/typst-library/src/visualize/line.rs
deleted file mode 100644
index da497cf4..00000000
--- a/crates/typst-library/src/visualize/line.rs
+++ /dev/null
@@ -1,86 +0,0 @@
-use crate::prelude::*;
-
-/// A line from one point to another.
-///
-/// # Example
-/// ```example
-/// #set page(height: 100pt)
-///
-/// #line(length: 100%)
-/// #line(end: (50%, 50%))
-/// #line(
-/// length: 4cm,
-/// stroke: 2pt + maroon,
-/// )
-/// ```
-#[elem(Layout)]
-pub struct LineElem {
- /// The start point of the line.
- ///
- /// Must be an array of exactly two relative lengths.
- #[resolve]
- pub start: Axes<Rel<Length>>,
-
- /// The offset from `start` where the line ends.
- #[resolve]
- pub end: Option<Axes<Rel<Length>>>,
-
- /// The line's length. This is only respected if `end` is `none`.
- #[resolve]
- #[default(Abs::pt(30.0).into())]
- pub length: Rel<Length>,
-
- /// The angle at which the line points away from the origin. This is only
- /// respected if `end` is `none`.
- pub angle: Angle,
-
- /// How to [stroke]($stroke) the line.
- ///
- /// ```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")),
- /// line(stroke: (paint: blue, thickness: 1pt, dash: ("dot", 2pt, 4pt, 2pt))),
- /// )
- /// ```
- #[resolve]
- #[fold]
- pub stroke: Stroke,
-}
-
-impl Layout for LineElem {
- #[tracing::instrument(name = "LineElem::layout", skip_all)]
- fn layout(
- &self,
- _: &mut Vt,
- styles: StyleChain,
- regions: Regions,
- ) -> SourceResult<Fragment> {
- 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(|| {
- let length = self.length(styles);
- let angle = self.angle(styles);
- let x = angle.cos() * length;
- let y = angle.sin() * length;
- resolve(Axes::new(x, y))
- });
-
- let stroke = self.stroke(styles).unwrap_or_default();
- let size = start.max(start + delta).max(Size::zero());
- let target = regions.expand.select(regions.size, size);
-
- if !target.is_finite() {
- bail!(error!(self.span(), "cannot create line with infinite length"));
- }
- let mut frame = Frame::soft(target);
- let shape = Geometry::Line(delta.to_point()).stroked(stroke);
- frame.push(start.to_point(), FrameItem::Shape(shape, self.span()));
- Ok(Fragment::frame(frame))
- }
-}
diff --git a/crates/typst-library/src/visualize/mod.rs b/crates/typst-library/src/visualize/mod.rs
deleted file mode 100644
index e39d50bc..00000000
--- a/crates/typst-library/src/visualize/mod.rs
+++ /dev/null
@@ -1,31 +0,0 @@
-//! Drawing and visualization.
-
-mod image;
-mod line;
-mod path;
-mod polygon;
-mod shape;
-
-pub use self::image::*;
-pub use self::line::*;
-pub use self::path::*;
-pub use self::polygon::*;
-pub use self::shape::*;
-
-use crate::prelude::*;
-
-/// Hook up all visualize definitions.
-pub(super) fn define(global: &mut Scope) {
- global.category("visualize");
- global.define_type::<Color>();
- global.define_type::<Gradient>();
- 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
deleted file mode 100644
index 43b11526..00000000
--- a/crates/typst-library/src/visualize/path.rs
+++ /dev/null
@@ -1,208 +0,0 @@
-use kurbo::{CubicBez, ParamCurveExtrema};
-use typst::eval::Reflect;
-
-use crate::prelude::*;
-
-use PathVertex::{AllControlPoints, MirroredControlPoint, Vertex};
-
-/// A path through a list of points, connected by Bezier curves.
-///
-/// # Example
-/// ```example
-/// #path(
-/// fill: blue.lighten(80%),
-/// stroke: blue,
-/// closed: true,
-/// (0pt, 50pt),
-/// (100%, 50pt),
-/// ((50%, 0pt), (40pt, 0pt)),
-/// )
-/// ```
-#[elem(Layout)]
-pub struct PathElem {
- /// 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).
- pub fill: Option<Paint>,
-
- /// How to [stroke]($stroke) the path. This can be:
- ///
- /// 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<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
- /// with a straight line, simply add one last point that's the same as the
- /// start point.
- #[default(false)]
- pub closed: bool,
-
- /// The vertices of the path.
- ///
- /// Each vertex can be defined in 3 ways:
- ///
- /// - 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
- /// control point is the one that affects the curve coming _into_ this
- /// vertex (even for the first point). The mirrored control point affects
- /// 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).
- #[variadic]
- pub vertices: Vec<PathVertex>,
-}
-
-impl Layout for PathElem {
- #[tracing::instrument(name = "PathElem::layout", skip_all)]
- fn layout(
- &self,
- _: &mut Vt,
- styles: StyleChain,
- regions: Regions,
- ) -> SourceResult<Fragment> {
- let resolve = |axes: Axes<Rel<Length>>| {
- axes.resolve(styles)
- .zip_map(regions.base(), Rel::relative_to)
- .to_point()
- };
-
- let vertices = self.vertices();
- let points: Vec<Point> = vertices.iter().map(|c| resolve(c.vertex())).collect();
-
- let mut size = Size::zero();
- if points.is_empty() {
- return Ok(Fragment::frame(Frame::soft(size)));
- }
-
- // Only create a path if there are more than zero points.
- // Construct a closed path given all points.
- let mut path = Path::new();
- path.move_to(points[0]);
-
- let mut add_cubic =
- |from_point: Point, to_point: Point, from: PathVertex, to: PathVertex| {
- let from_control_point = resolve(from.control_point_from()) + from_point;
- let to_control_point = resolve(to.control_point_to()) + to_point;
- path.cubic_to(from_control_point, to_control_point, to_point);
-
- let p0 = kurbo::Point::new(from_point.x.to_raw(), from_point.y.to_raw());
- let p1 = kurbo::Point::new(
- from_control_point.x.to_raw(),
- from_control_point.y.to_raw(),
- );
- let p2 = kurbo::Point::new(
- to_control_point.x.to_raw(),
- to_control_point.y.to_raw(),
- );
- let p3 = kurbo::Point::new(to_point.x.to_raw(), to_point.y.to_raw());
- let extrema = CubicBez::new(p0, p1, p2, p3).bounding_box();
- size.x.set_max(Abs::raw(extrema.x1));
- size.y.set_max(Abs::raw(extrema.y1));
- };
-
- for (vertex_window, point_window) in vertices.windows(2).zip(points.windows(2)) {
- let from = vertex_window[0];
- let to = vertex_window[1];
- let from_point = point_window[0];
- let to_point = point_window[1];
-
- add_cubic(from_point, to_point, from, to);
- }
-
- if self.closed(styles) {
- let from = *vertices.last().unwrap(); // We checked that we have at least one element.
- let to = vertices[0];
- let from_point = *points.last().unwrap();
- let to_point = points[0];
-
- add_cubic(from_point, to_point, from, to);
- path.close_path();
- }
-
- // Prepare fill and stroke.
- let fill = self.fill(styles);
- let stroke = match self.stroke(styles) {
- Smart::Auto if fill.is_none() => Some(FixedStroke::default()),
- Smart::Auto => None,
- Smart::Custom(stroke) => stroke.map(Stroke::unwrap_or_default),
- };
-
- let mut frame = Frame::soft(size);
- let shape = Shape { geometry: Geometry::Path(path), stroke, fill };
- frame.push(Point::zero(), FrameItem::Shape(shape, self.span()));
-
- Ok(Fragment::frame(frame))
- }
-}
-
-#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
-pub enum PathVertex {
- Vertex(Axes<Rel<Length>>),
- MirroredControlPoint(Axes<Rel<Length>>, Axes<Rel<Length>>),
- AllControlPoints(Axes<Rel<Length>>, Axes<Rel<Length>>, Axes<Rel<Length>>),
-}
-
-impl PathVertex {
- pub fn vertex(&self) -> Axes<Rel<Length>> {
- match self {
- Vertex(x) => *x,
- MirroredControlPoint(x, _) => *x,
- AllControlPoints(x, _, _) => *x,
- }
- }
-
- pub fn control_point_from(&self) -> Axes<Rel<Length>> {
- match self {
- Vertex(_) => Axes::new(Rel::zero(), Rel::zero()),
- MirroredControlPoint(_, a) => a.map(|x| -x),
- AllControlPoints(_, _, b) => *b,
- }
- }
-
- pub fn control_point_to(&self) -> Axes<Rel<Length>> {
- match self {
- Vertex(_) => Axes::new(Rel::zero(), Rel::zero()),
- MirroredControlPoint(_, a) => *a,
- AllControlPoints(_, a, _) => *a,
- }
- }
-}
-
-cast! {
- PathVertex,
- self => match self {
- Vertex(x) => x.into_value(),
- MirroredControlPoint(x, c) => array![x, c].into_value(),
- AllControlPoints(x, c1, c2) => array![x, c1, c2].into_value(),
- },
- array: Array => {
- let mut iter = array.into_iter();
- match (iter.next(), iter.next(), iter.next(), iter.next()) {
- (Some(a), None, None, None) => {
- Vertex(a.cast()?)
- },
- (Some(a), Some(b), None, None) => {
- if Axes::<Rel<Length>>::castable(&a) {
- MirroredControlPoint(a.cast()?, b.cast()?)
- } else {
- Vertex(Axes::new(a.cast()?, b.cast()?))
- }
- },
- (Some(a), Some(b), Some(c), None) => {
- AllControlPoints(a.cast()?, b.cast()?, c.cast()?)
- },
- _ => bail!("path vertex must have 1, 2, or 3 points"),
- }
- },
-}
diff --git a/crates/typst-library/src/visualize/polygon.rs b/crates/typst-library/src/visualize/polygon.rs
deleted file mode 100644
index b1ed9eaa..00000000
--- a/crates/typst-library/src/visualize/polygon.rs
+++ /dev/null
@@ -1,164 +0,0 @@
-use std::f64::consts::PI;
-
-use crate::prelude::*;
-
-/// A closed polygon.
-///
-/// The polygon is defined by its corner points and is closed automatically.
-///
-/// # Example
-/// ```example
-/// #polygon(
-/// fill: blue.lighten(80%),
-/// stroke: blue,
-/// (20%, 0pt),
-/// (60%, 0pt),
-/// (80%, 2cm),
-/// (0%, 2cm),
-/// )
-/// ```
-#[elem(scope, Layout)]
-pub struct PolygonElem {
- /// 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]($stroke) the polygon. This can be:
- ///
- /// 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<Stroke>>,
-
- /// The vertices of the polygon. Each point is specified as an array of two
- /// [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(
- &self,
- _: &mut Vt,
- styles: StyleChain,
- regions: Regions,
- ) -> SourceResult<Fragment> {
- let points: Vec<Point> = self
- .vertices()
- .iter()
- .map(|c| {
- c.resolve(styles).zip_map(regions.base(), Rel::relative_to).to_point()
- })
- .collect();
-
- let size = points.iter().fold(Point::zero(), |max, c| c.max(max)).to_size();
- if !size.is_finite() {
- bail!(error!(self.span(), "cannot create polygon with infinite size"));
- }
- let mut frame = Frame::hard(size);
-
- // Only create a path if there are more than zero points.
- if points.is_empty() {
- return Ok(Fragment::frame(frame));
- }
-
- // Prepare fill and stroke.
- let fill = self.fill(styles);
- let stroke = match self.stroke(styles) {
- Smart::Auto if fill.is_none() => Some(FixedStroke::default()),
- Smart::Auto => None,
- Smart::Custom(stroke) => stroke.map(Stroke::unwrap_or_default),
- };
-
- // Construct a closed path given all points.
- let mut path = Path::new();
- path.move_to(points[0]);
- for &point in &points[1..] {
- path.line_to(point);
- }
- path.close_path();
-
- let shape = Shape { geometry: Geometry::Path(path), stroke, fill };
- frame.push(Point::zero(), FrameItem::Shape(shape, self.span()));
-
- Ok(Fragment::frame(frame))
- }
-}
diff --git a/crates/typst-library/src/visualize/shape.rs b/crates/typst-library/src/visualize/shape.rs
deleted file mode 100644
index 779b07ff..00000000
--- a/crates/typst-library/src/visualize/shape.rs
+++ /dev/null
@@ -1,547 +0,0 @@
-use std::f64::consts::SQRT_2;
-
-use crate::prelude::*;
-
-/// A rectangle with optional content.
-///
-/// # Example
-/// ```example
-/// // Without content.
-/// #rect(width: 35%, height: 30pt)
-///
-/// // With content.
-/// #rect[
-/// Automatically sized \
-/// to fit the content.
-/// ]
-/// ```
-#[elem(title = "Rectangle", Layout)]
-pub struct RectElem {
- /// The rectangle's width, relative to its parent container.
- pub width: Smart<Rel<Length>>,
-
- /// The rectangle's height, relative to its parent container.
- pub height: Smart<Rel<Length>>,
-
- /// How to fill the rectangle.
- ///
- /// When setting a fill, the default stroke disappears. To create a
- /// rectangle with both fill and stroke, you have to configure both.
- ///
- /// ```example
- /// #rect(fill: blue)
- /// ```
- pub fill: Option<Paint>,
-
- /// How to stroke the rectangle. This can be:
- ///
- /// - `{none}` to disable stroking
- /// - `{auto}` for a stroke of `{1pt + black}` if and if only if no fill is
- /// given.
- /// - 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.
- /// - `right`: The right stroke.
- /// - `bottom`: The bottom stroke.
- /// - `left`: The left stroke.
- /// - `x`: The horizontal stroke.
- /// - `y`: The vertical stroke.
- /// - `rest`: The stroke on all sides except those for which the
- /// dictionary explicitly sets a size.
- ///
- /// ```example
- /// #stack(
- /// dir: ltr,
- /// spacing: 1fr,
- /// rect(stroke: red),
- /// rect(stroke: 2pt),
- /// rect(stroke: 2pt + red),
- /// )
- /// ```
- #[resolve]
- #[fold]
- 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:
- ///
- /// - A relative length for a uniform corner radius.
- /// - A dictionary: With a dictionary, the stroke for each side can be set
- /// individually. The dictionary can contain the following keys in order
- /// of precedence:
- /// - `top-left`: The top-left corner radius.
- /// - `top-right`: The top-right corner radius.
- /// - `bottom-right`: The bottom-right corner radius.
- /// - `bottom-left`: The bottom-left corner radius.
- /// - `left`: The top-left and bottom-left corner radii.
- /// - `top`: The top-left and top-right corner radii.
- /// - `right`: The top-right and bottom-right corner radii.
- /// - `bottom`: The bottom-left and bottom-right corner radii.
- /// - `rest`: The radii for all corners except those for which the
- /// dictionary explicitly sets a size.
- ///
- /// ```example
- /// #set rect(stroke: 4pt)
- /// #rect(
- /// radius: (
- /// left: 5pt,
- /// top-right: 20pt,
- /// bottom-right: 10pt,
- /// ),
- /// stroke: (
- /// left: red,
- /// top: yellow,
- /// right: green,
- /// bottom: blue,
- /// ),
- /// )
- /// ```
- #[resolve]
- #[fold]
- pub radius: Corners<Option<Rel<Length>>>,
-
- /// How much to pad the rectangle's content.
- /// 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]($box.outset) for more details.
- #[resolve]
- #[fold]
- pub outset: Sides<Option<Rel<Length>>>,
-
- /// The content to place into the rectangle.
- ///
- /// When this is omitted, the rectangle takes on a default size of at most
- /// `{45pt}` by `{30pt}`.
- #[positional]
- pub body: Option<Content>,
-}
-
-impl Layout for RectElem {
- #[tracing::instrument(name = "RectElem::layout", skip_all)]
- fn layout(
- &self,
- vt: &mut Vt,
- styles: StyleChain,
- regions: Regions,
- ) -> SourceResult<Fragment> {
- layout(
- vt,
- styles,
- regions,
- ShapeKind::Rect,
- &self.body(styles),
- Axes::new(self.width(styles), self.height(styles)),
- self.fill(styles),
- self.stroke(styles),
- self.inset(styles),
- self.outset(styles),
- self.radius(styles),
- self.span(),
- )
- }
-}
-
-/// A square with optional content.
-///
-/// # Example
-/// ```example
-/// // Without content.
-/// #square(size: 40pt)
-///
-/// // With content.
-/// #square[
-/// Automatically \
-/// sized to fit.
-/// ]
-/// ```
-#[elem(Layout)]
-pub struct SquareElem {
- /// The square's side length. This is mutually exclusive with `width` and
- /// `height`.
- #[external]
- pub size: Smart<Length>,
-
- /// The square's width. This is mutually exclusive with `size` and `height`.
- ///
- /// In contrast to `size`, this can be relative to the parent container's
- /// width.
- #[parse(
- let size = args.named::<Smart<Length>>("size")?.map(|s| s.map(Rel::from));
- match size {
- None => args.named("width")?,
- size => size,
- }
- )]
- pub width: Smart<Rel<Length>>,
-
- /// The square's height. This is mutually exclusive with `size` and `width`.
- ///
- /// In contrast to `size`, this can be relative to the parent container's
- /// height.
- #[parse(match size {
- None => args.named("height")?,
- size => size,
- })]
- pub height: Smart<Rel<Length>>,
-
- /// 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]($rect.stroke) for more details.
- #[resolve]
- #[fold]
- pub stroke: Smart<Sides<Option<Option<Stroke>>>>,
-
- /// 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
- /// [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 [box's documentation]($box.outset) for more details.
- #[resolve]
- #[fold]
- pub outset: Sides<Option<Rel<Length>>>,
-
- /// The content to place into the square. The square expands to fit this
- /// content, keeping the 1-1 aspect ratio.
- ///
- /// When this is omitted, the square takes on a default size of at most
- /// `{30pt}`.
- #[positional]
- pub body: Option<Content>,
-}
-
-impl Layout for SquareElem {
- #[tracing::instrument(name = "SquareElem::layout", skip_all)]
- fn layout(
- &self,
- vt: &mut Vt,
- styles: StyleChain,
- regions: Regions,
- ) -> SourceResult<Fragment> {
- layout(
- vt,
- styles,
- regions,
- ShapeKind::Square,
- &self.body(styles),
- Axes::new(self.width(styles), self.height(styles)),
- self.fill(styles),
- self.stroke(styles),
- self.inset(styles),
- self.outset(styles),
- self.radius(styles),
- self.span(),
- )
- }
-}
-
-/// An ellipse with optional content.
-///
-/// # Example
-/// ```example
-/// // Without content.
-/// #ellipse(width: 35%, height: 30pt)
-///
-/// // With content.
-/// #ellipse[
-/// #set align(center)
-/// Automatically sized \
-/// to fit the content.
-/// ]
-/// ```
-#[elem(Layout)]
-pub struct EllipseElem {
- /// The ellipse's width, relative to its parent container.
- pub width: Smart<Rel<Length>>,
-
- /// The ellipse's height, relative to its parent container.
- pub height: Smart<Rel<Length>>,
-
- /// 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]($rect.stroke) for more details.
- #[resolve]
- #[fold]
- pub stroke: Smart<Option<Stroke>>,
-
- /// 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 [box's documentation]($box.outset) for more details.
- #[resolve]
- #[fold]
- pub outset: Sides<Option<Rel<Length>>>,
-
- /// The content to place into the ellipse.
- ///
- /// When this is omitted, the ellipse takes on a default size of at most
- /// `{45pt}` by `{30pt}`.
- #[positional]
- pub body: Option<Content>,
-}
-
-impl Layout for EllipseElem {
- #[tracing::instrument(name = "EllipseElem::layout", skip_all)]
- fn layout(
- &self,
- vt: &mut Vt,
- styles: StyleChain,
- regions: Regions,
- ) -> SourceResult<Fragment> {
- layout(
- vt,
- styles,
- regions,
- ShapeKind::Ellipse,
- &self.body(styles),
- Axes::new(self.width(styles), self.height(styles)),
- self.fill(styles),
- self.stroke(styles).map(Sides::splat),
- self.inset(styles),
- self.outset(styles),
- Corners::splat(Rel::zero()),
- self.span(),
- )
- }
-}
-
-/// A circle with optional content.
-///
-/// # Example
-/// ```example
-/// // Without content.
-/// #circle(radius: 25pt)
-///
-/// // With content.
-/// #circle[
-/// #set align(center + horizon)
-/// Automatically \
-/// sized to fit.
-/// ]
-/// ```
-#[elem(Layout)]
-pub struct CircleElem {
- /// The circle's radius. This is mutually exclusive with `width` and
- /// `height`.
- #[external]
- pub radius: Length,
-
- /// The circle's width. This is mutually exclusive with `radius` and
- /// `height`.
- ///
- /// In contrast to `radius`, this can be relative to the parent container's
- /// width.
- #[parse(
- let size = args
- .named::<Smart<Length>>("radius")?
- .map(|s| s.map(|r| 2.0 * Rel::from(r)));
- match size {
- None => args.named("width")?,
- size => size,
- }
- )]
- pub width: Smart<Rel<Length>>,
-
- /// The circle's height. This is mutually exclusive with `radius` and
- /// `width`.
- ///
- /// In contrast to `radius`, this can be relative to the parent container's
- /// height.
- #[parse(match size {
- None => args.named("height")?,
- size => size,
- })]
- pub height: Smart<Rel<Length>>,
-
- /// 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]($rect.stroke) for more details.
- #[resolve]
- #[fold]
- #[default(Smart::Auto)]
- pub stroke: Smart<Option<Stroke>>,
-
- /// 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 [box's documentation]($box.outset) for more details.
- #[resolve]
- #[fold]
- pub outset: Sides<Option<Rel<Length>>>,
-
- /// The content to place into the circle. The circle expands to fit this
- /// content, keeping the 1-1 aspect ratio.
- #[positional]
- pub body: Option<Content>,
-}
-
-impl Layout for CircleElem {
- #[tracing::instrument(name = "CircleElem::layout", skip_all)]
- fn layout(
- &self,
- vt: &mut Vt,
- styles: StyleChain,
- regions: Regions,
- ) -> SourceResult<Fragment> {
- layout(
- vt,
- styles,
- regions,
- ShapeKind::Circle,
- &self.body(styles),
- Axes::new(self.width(styles), self.height(styles)),
- self.fill(styles),
- self.stroke(styles).map(Sides::splat),
- self.inset(styles),
- self.outset(styles),
- Corners::splat(Rel::zero()),
- self.span(),
- )
- }
-}
-
-/// Layout a shape.
-#[tracing::instrument(name = "shape::layout", skip_all)]
-#[allow(clippy::too_many_arguments)]
-fn layout(
- vt: &mut Vt,
- styles: StyleChain,
- regions: Regions,
- kind: ShapeKind,
- body: &Option<Content>,
- sizing: Axes<Smart<Rel<Length>>>,
- fill: Option<Paint>,
- 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_map(regions.base(), |s, r| s.map(|v| v.resolve(styles).relative_to(r)));
-
- let mut frame;
- if let Some(child) = body {
- let region = resolved.unwrap_or(regions.base());
- if kind.is_round() {
- inset = inset.map(|side| side + Ratio::new(0.5 - SQRT_2 / 4.0));
- }
-
- // Pad the child.
- let child = child.clone().padded(inset.map(|side| side.map(Length::from)));
- let expand = sizing.as_ref().map(Smart::is_custom);
- let pod = Regions::one(region, expand);
- frame = child.layout(vt, styles, pod)?.into_frame();
-
- // Enforce correct size.
- *frame.size_mut() = expand.select(region, frame.size());
-
- // Relayout with full expansion into square region to make sure
- // the result is really a square or circle.
- if kind.is_quadratic() {
- frame.set_size(Size::splat(frame.size().max_by_side()));
- let length = frame.size().max_by_side().min(region.min_by_side());
- let pod = Regions::one(Size::splat(length), Axes::splat(true));
- frame = child.layout(vt, styles, pod)?.into_frame();
- }
-
- // Enforce correct size again.
- *frame.size_mut() = expand.select(region, frame.size());
- if kind.is_quadratic() {
- frame.set_size(Size::splat(frame.size().max_by_side()));
- }
- } else {
- // The default size that a shape takes on if it has no child and
- // enough space.
- let default = Size::new(Abs::pt(45.0), Abs::pt(30.0));
- let mut size = resolved.unwrap_or(default.min(regions.base()));
- if kind.is_quadratic() {
- size = Size::splat(size.min_by_side());
- }
- frame = Frame::soft(size);
- }
-
- // Prepare stroke.
- let stroke = match stroke {
- Smart::Auto if fill.is_none() => Sides::splat(Some(FixedStroke::default())),
- Smart::Auto => Sides::splat(None),
- Smart::Custom(strokes) => strokes.map(|s| s.map(Stroke::unwrap_or_default)),
- };
-
- // Add fill and/or stroke.
- if fill.is_some() || stroke.iter().any(Option::is_some) {
- if kind.is_round() {
- let outset = outset.relative_to(frame.size());
- let size = frame.size() + outset.sum_by_axis();
- let pos = Point::new(-outset.left, -outset.top);
- let shape = ellipse(size, fill, stroke.left);
- frame.prepend(pos, FrameItem::Shape(shape, span));
- } else {
- frame.fill_and_stroke(fill, stroke, outset, radius, span);
- }
- }
-
- // Apply metadata.
- frame.meta(styles, false);
-
- Ok(Fragment::frame(frame))
-}
-
-/// A category of shape.
-#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
-pub enum ShapeKind {
- /// A rectangle with equal side lengths.
- Square,
- /// A quadrilateral with four right angles.
- Rect,
- /// An ellipse with coinciding foci.
- Circle,
- /// A curve around two focal points.
- Ellipse,
-}
-
-impl ShapeKind {
- /// Whether this shape kind is curvy.
- fn is_round(self) -> bool {
- matches!(self, Self::Circle | Self::Ellipse)
- }
-
- /// Whether this shape kind has equal side length.
- fn is_quadratic(self) -> bool {
- matches!(self, Self::Square | Self::Circle)
- }
-}