From ebfdb1dafa430786db10dad2ef7d5467c1bdbed1 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Sun, 2 Jul 2023 19:59:52 +0200 Subject: Move everything into `crates/` directory --- library/src/compute/calc.rs | 1024 ----------------------- library/src/compute/construct.rs | 743 ----------------- library/src/compute/data.rs | 492 ----------- library/src/compute/foundations.rs | 215 ----- library/src/compute/mod.rs | 39 - library/src/layout/align.rs | 67 -- library/src/layout/columns.rs | 172 ---- library/src/layout/container.rs | 497 ------------ library/src/layout/enum.rs | 318 -------- library/src/layout/flow.rs | 583 -------------- library/src/layout/fragment.rs | 87 -- library/src/layout/grid.rs | 708 ---------------- library/src/layout/hide.rs | 30 - library/src/layout/list.rs | 239 ------ library/src/layout/measure.rs | 56 -- library/src/layout/mod.rs | 709 ---------------- library/src/layout/pad.rs | 125 --- library/src/layout/page.rs | 898 --------------------- library/src/layout/par.rs | 1566 ------------------------------------ library/src/layout/place.rs | 103 --- library/src/layout/regions.rs | 144 ---- library/src/layout/repeat.rs | 79 -- library/src/layout/spacing.rs | 240 ------ library/src/layout/stack.rs | 339 -------- library/src/layout/table.rs | 334 -------- library/src/layout/terms.rs | 166 ---- library/src/layout/transform.rs | 194 ----- library/src/lib.rs | 141 ---- library/src/math/accent.rs | 139 ---- library/src/math/align.rs | 63 -- library/src/math/attach.rs | 411 ---------- library/src/math/cancel.rs | 187 ----- library/src/math/ctx.rs | 268 ------ library/src/math/delimited.rs | 200 ----- library/src/math/frac.rs | 148 ---- library/src/math/fragment.rs | 414 ---------- library/src/math/matrix.rs | 313 ------- library/src/math/mod.rs | 480 ----------- library/src/math/op.rs | 113 --- library/src/math/root.rs | 156 ---- library/src/math/row.rs | 258 ------ library/src/math/spacing.rs | 60 -- library/src/math/stretch.rs | 199 ----- library/src/math/style.rs | 620 -------------- library/src/math/underover.rs | 339 -------- library/src/meta/bibliography.rs | 724 ----------------- library/src/meta/context.rs | 220 ----- library/src/meta/counter.rs | 683 ---------------- library/src/meta/document.rs | 86 -- library/src/meta/figure.rs | 351 -------- library/src/meta/footnote.rs | 299 ------- library/src/meta/heading.rs | 239 ------ library/src/meta/link.rs | 137 ---- library/src/meta/mod.rs | 64 -- library/src/meta/numbering.rs | 525 ------------ library/src/meta/outline.rs | 528 ------------ library/src/meta/query.rs | 145 ---- library/src/meta/reference.rs | 276 ------- library/src/meta/state.rs | 440 ---------- library/src/prelude.rs | 42 - library/src/shared/behave.rs | 109 --- library/src/shared/ext.rs | 92 --- library/src/shared/mod.rs | 7 - library/src/symbols/emoji.rs | 1356 ------------------------------- library/src/symbols/mod.rs | 15 - library/src/symbols/sym.rs | 842 ------------------- library/src/text/deco.rs | 420 ---------- library/src/text/misc.rs | 330 -------- library/src/text/mod.rs | 769 ------------------ library/src/text/quotes.rs | 209 ----- library/src/text/raw.rs | 398 --------- library/src/text/shaping.rs | 973 ---------------------- library/src/text/shift.rs | 229 ------ library/src/visualize/image.rs | 197 ----- library/src/visualize/line.rs | 118 --- library/src/visualize/mod.rs | 45 -- library/src/visualize/path.rs | 211 ----- library/src/visualize/polygon.rs | 93 --- library/src/visualize/shape.rs | 569 ------------- 79 files changed, 26417 deletions(-) delete mode 100644 library/src/compute/calc.rs delete mode 100644 library/src/compute/construct.rs delete mode 100644 library/src/compute/data.rs delete mode 100644 library/src/compute/foundations.rs delete mode 100644 library/src/compute/mod.rs delete mode 100644 library/src/layout/align.rs delete mode 100644 library/src/layout/columns.rs delete mode 100644 library/src/layout/container.rs delete mode 100644 library/src/layout/enum.rs delete mode 100644 library/src/layout/flow.rs delete mode 100644 library/src/layout/fragment.rs delete mode 100644 library/src/layout/grid.rs delete mode 100644 library/src/layout/hide.rs delete mode 100644 library/src/layout/list.rs delete mode 100644 library/src/layout/measure.rs delete mode 100644 library/src/layout/mod.rs delete mode 100644 library/src/layout/pad.rs delete mode 100644 library/src/layout/page.rs delete mode 100644 library/src/layout/par.rs delete mode 100644 library/src/layout/place.rs delete mode 100644 library/src/layout/regions.rs delete mode 100644 library/src/layout/repeat.rs delete mode 100644 library/src/layout/spacing.rs delete mode 100644 library/src/layout/stack.rs delete mode 100644 library/src/layout/table.rs delete mode 100644 library/src/layout/terms.rs delete mode 100644 library/src/layout/transform.rs delete mode 100644 library/src/lib.rs delete mode 100644 library/src/math/accent.rs delete mode 100644 library/src/math/align.rs delete mode 100644 library/src/math/attach.rs delete mode 100644 library/src/math/cancel.rs delete mode 100644 library/src/math/ctx.rs delete mode 100644 library/src/math/delimited.rs delete mode 100644 library/src/math/frac.rs delete mode 100644 library/src/math/fragment.rs delete mode 100644 library/src/math/matrix.rs delete mode 100644 library/src/math/mod.rs delete mode 100644 library/src/math/op.rs delete mode 100644 library/src/math/root.rs delete mode 100644 library/src/math/row.rs delete mode 100644 library/src/math/spacing.rs delete mode 100644 library/src/math/stretch.rs delete mode 100644 library/src/math/style.rs delete mode 100644 library/src/math/underover.rs delete mode 100644 library/src/meta/bibliography.rs delete mode 100644 library/src/meta/context.rs delete mode 100644 library/src/meta/counter.rs delete mode 100644 library/src/meta/document.rs delete mode 100644 library/src/meta/figure.rs delete mode 100644 library/src/meta/footnote.rs delete mode 100644 library/src/meta/heading.rs delete mode 100644 library/src/meta/link.rs delete mode 100644 library/src/meta/mod.rs delete mode 100644 library/src/meta/numbering.rs delete mode 100644 library/src/meta/outline.rs delete mode 100644 library/src/meta/query.rs delete mode 100644 library/src/meta/reference.rs delete mode 100644 library/src/meta/state.rs delete mode 100644 library/src/prelude.rs delete mode 100644 library/src/shared/behave.rs delete mode 100644 library/src/shared/ext.rs delete mode 100644 library/src/shared/mod.rs delete mode 100644 library/src/symbols/emoji.rs delete mode 100644 library/src/symbols/mod.rs delete mode 100644 library/src/symbols/sym.rs delete mode 100644 library/src/text/deco.rs delete mode 100644 library/src/text/misc.rs delete mode 100644 library/src/text/mod.rs delete mode 100644 library/src/text/quotes.rs delete mode 100644 library/src/text/raw.rs delete mode 100644 library/src/text/shaping.rs delete mode 100644 library/src/text/shift.rs delete mode 100644 library/src/visualize/image.rs delete mode 100644 library/src/visualize/line.rs delete mode 100644 library/src/visualize/mod.rs delete mode 100644 library/src/visualize/path.rs delete mode 100644 library/src/visualize/polygon.rs delete mode 100644 library/src/visualize/shape.rs (limited to 'library/src') diff --git a/library/src/compute/calc.rs b/library/src/compute/calc.rs deleted file mode 100644 index 81715007..00000000 --- a/library/src/compute/calc.rs +++ /dev/null @@ -1,1024 +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::*; - -/// A module with computational functions. -pub fn module() -> Module { - let mut scope = Scope::new(); - scope.define("abs", abs_func()); - scope.define("pow", pow_func()); - scope.define("exp", exp_func()); - scope.define("sqrt", sqrt_func()); - scope.define("sin", sin_func()); - scope.define("cos", cos_func()); - scope.define("tan", tan_func()); - scope.define("asin", asin_func()); - scope.define("acos", acos_func()); - scope.define("atan", atan_func()); - scope.define("atan2", atan2_func()); - scope.define("sinh", sinh_func()); - scope.define("cosh", cosh_func()); - scope.define("tanh", tanh_func()); - scope.define("log", log_func()); - scope.define("ln", ln_func()); - scope.define("fact", fact_func()); - scope.define("perm", perm_func()); - scope.define("binom", binom_func()); - scope.define("gcd", gcd_func()); - scope.define("lcm", lcm_func()); - scope.define("floor", floor_func()); - scope.define("ceil", ceil_func()); - scope.define("trunc", trunc_func()); - scope.define("fract", fract_func()); - scope.define("round", round_func()); - scope.define("clamp", clamp_func()); - scope.define("min", min_func()); - scope.define("max", max_func()); - scope.define("even", even_func()); - scope.define("odd", odd_func()); - scope.define("rem", rem_func()); - scope.define("quo", quo_func()); - scope.define("inf", f64::INFINITY); - scope.define("nan", f64::NAN); - scope.define("pi", std::f64::consts::PI); - scope.define("e", std::f64::consts::E); - Module::new("calc").with_scope(scope) -} - -/// Calculates the absolute value of a numeric value. -/// -/// ## Example { #example } -/// ```example -/// #calc.abs(-5) \ -/// #calc.abs(5pt - 2cm) \ -/// #calc.abs(2fr) -/// ``` -/// -/// Display: Absolute -/// Category: calculate -#[func] -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 { #example } -/// ```example -/// #calc.pow(2, 3) -/// ``` -/// -/// Display: Power -/// Category: calculate -#[func] -pub fn pow( - /// The base of the power. - base: Num, - /// The exponent of the power. - exponent: Spanned, - /// The callsite span. - span: Span, -) -> SourceResult { - 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 { #example } -/// ```example -/// #calc.exp(1) -/// ``` -/// -/// Display: Exponential -/// Category: calculate -#[func] -pub fn exp( - /// The exponent of the power. - exponent: Spanned, - /// The callsite span. - span: Span, -) -> SourceResult { - 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) -} - -/// Extracts the square root of a number. -/// -/// ## Example { #example } -/// ```example -/// #calc.sqrt(16) \ -/// #calc.sqrt(2.5) -/// ``` -/// -/// Display: Square Root -/// Category: calculate -#[func] -pub fn sqrt( - /// The number whose square root to calculate. Must be non-negative. - value: Spanned, -) -> SourceResult { - 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 { #example } -/// ```example -/// #assert(calc.sin(90deg) == calc.sin(-270deg)) -/// #calc.sin(1.5) \ -/// #calc.sin(90deg) -/// ``` -/// -/// Display: Sine -/// Category: calculate -#[func] -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 { #example } -/// ```example -/// #calc.cos(90deg) \ -/// #calc.cos(1.5) \ -/// #calc.cos(90deg) -/// ``` -/// -/// Display: Cosine -/// Category: calculate -#[func] -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 { #example } -/// ```example -/// #calc.tan(1.5) \ -/// #calc.tan(90deg) -/// ``` -/// -/// Display: Tangent -/// Category: calculate -#[func] -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 { #example } -/// ```example -/// #calc.asin(0) \ -/// #calc.asin(1) -/// ``` -/// -/// Display: Arcsine -/// Category: calculate -#[func] -pub fn asin( - /// The number whose arcsine to calculate. Must be between -1 and 1. - value: Spanned, -) -> SourceResult { - 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 { #example } -/// ```example -/// #calc.acos(0) \ -/// #calc.acos(1) -/// ``` -/// -/// Display: Arccosine -/// Category: calculate -#[func] -pub fn acos( - /// The number whose arcsine to calculate. Must be between -1 and 1. - value: Spanned, -) -> SourceResult { - 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 { #example } -/// ```example -/// #calc.atan(0) \ -/// #calc.atan(1) -/// ``` -/// -/// Display: Arctangent -/// Category: calculate -#[func] -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 { #example } -/// ```example -/// #calc.atan2(1, 1) \ -/// #calc.atan2(-2, -3) -/// ``` -/// -/// Display: Four-quadrant Arctangent -/// Category: calculate -#[func] -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 an angle. -/// -/// When called with an integer or a float, they will be interpreted as radians. -/// -/// ## Example { #example } -/// ```example -/// #calc.sinh(0) \ -/// #calc.sinh(45deg) -/// ``` -/// -/// Display: Hyperbolic sine -/// Category: calculate -#[func] -pub fn sinh( - /// The angle whose hyperbolic sine to calculate. - angle: AngleLike, -) -> f64 { - match angle { - AngleLike::Angle(a) => a.to_rad().sinh(), - AngleLike::Int(n) => (n as f64).sinh(), - AngleLike::Float(n) => n.sinh(), - } -} - -/// Calculates the hyperbolic cosine of an angle. -/// -/// When called with an integer or a float, they will be interpreted as radians. -/// -/// ## Example { #example } -/// ```example -/// #calc.cosh(0) \ -/// #calc.cosh(45deg) -/// ``` -/// -/// Display: Hyperbolic cosine -/// Category: calculate -#[func] -pub fn cosh( - /// The angle whose hyperbolic cosine to calculate. - angle: AngleLike, -) -> f64 { - match angle { - AngleLike::Angle(a) => a.to_rad().cosh(), - AngleLike::Int(n) => (n as f64).cosh(), - AngleLike::Float(n) => n.cosh(), - } -} - -/// Calculates the hyperbolic tangent of an angle. -/// -/// When called with an integer or a float, they will be interpreted as radians. -/// -/// ## Example { #example } -/// ```example -/// #calc.tanh(0) \ -/// #calc.tanh(45deg) -/// ``` -/// -/// Display: Hyperbolic tangent -/// Category: calculate -#[func] -pub fn tanh( - /// The angle whose hyperbolic tangent to calculate. - angle: AngleLike, -) -> f64 { - match angle { - AngleLike::Angle(a) => a.to_rad().tanh(), - AngleLike::Int(n) => (n as f64).tanh(), - AngleLike::Float(n) => n.tanh(), - } -} - -/// Calculates the logarithm of a number. -/// -/// If the base is not specified, the logarithm is calculated in base 10. -/// -/// ## Example { #example } -/// ```example -/// #calc.log(100) -/// ``` -/// -/// Display: Logarithm -/// Category: calculate -#[func] -pub fn log( - /// The number whose logarithm to calculate. Must be strictly positive. - value: Spanned, - /// The base of the logarithm. May not be zero. - #[named] - #[default(Spanned::new(10.0, Span::detached()))] - base: Spanned, - /// The callsite span. - span: Span, -) -> SourceResult { - 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 { #example } -/// ```example -/// #calc.ln(calc.e) -/// ``` -/// -/// Display: Natural Logarithm -/// Category: calculate -#[func] -pub fn ln( - /// The number whose logarithm to calculate. Must be strictly positive. - value: Spanned, - /// The callsite span. - span: Span, -) -> SourceResult { - 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 { #example } -/// ```example -/// #calc.fact(5) -/// ``` -/// -/// Display: Factorial -/// Category: calculate -#[func] -pub fn fact( - /// The number whose factorial to calculate. Must be non-negative. - number: u64, -) -> StrResult { - Ok(fact_impl(1, number).ok_or("the result is too large")?) -} - -/// Calculates a permutation. -/// -/// ## Example { #example } -/// ```example -/// #calc.perm(10, 5) -/// ``` -/// -/// Display: Permutation -/// Category: calculate -#[func] -pub fn perm( - /// The base number. Must be non-negative. - base: u64, - /// The number of permutations. Must be non-negative. - numbers: u64, -) -> StrResult { - // 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 { - // 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. -/// -/// ## Example { #example } -/// ```example -/// #calc.binom(10, 5) -/// ``` -/// -/// Display: Binomial -/// Category: calculate -#[func] -pub fn binom( - /// The upper coefficient. Must be non-negative. - n: u64, - /// The lower coefficient. Must be non-negative. - k: u64, -) -> StrResult { - 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 { - 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 { #example } -/// ```example -/// #calc.gcd(7, 42) -/// ``` -/// -/// Display: Greatest Common Divisor -/// Category: calculate -#[func] -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 { #example } -/// ```example -/// #calc.lcm(96, 13) -/// ``` -/// -/// Display: Least Common Multiple -/// Category: calculate -#[func] -pub fn lcm( - /// The first integer. - a: i64, - /// The second integer. - b: i64, -) -> StrResult { - 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 { #example } -/// ```example -/// #assert(calc.floor(3.14) == 3) -/// #assert(calc.floor(3) == 3) -/// #calc.floor(500.1) -/// ``` -/// -/// Display: Round down -/// Category: calculate -#[func] -pub fn floor( - /// The number to round down. - 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 { #example } -/// ```example -/// #assert(calc.ceil(3.14) == 4) -/// #assert(calc.ceil(3) == 3) -/// #calc.ceil(500.1) -/// ``` -/// -/// Display: Round up -/// Category: calculate -#[func] -pub fn ceil( - /// The number to round up. - 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 { #example } -/// ```example -/// #assert(calc.trunc(3) == 3) -/// #assert(calc.trunc(-3.7) == -3) -/// #assert(calc.trunc(15.9) == 15) -/// ``` -/// -/// Display: Truncate -/// Category: calculate -#[func] -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 { #example } -/// ```example -/// #assert(calc.fract(3) == 0) -/// #calc.fract(-3.1) -/// ``` -/// -/// Display: Fractional -/// Category: calculate -#[func] -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 { #example } -/// ```example -/// #assert(calc.round(3.14) == 3) -/// #assert(calc.round(3.5) == 4) -/// #calc.round(3.1415, digits: 2) -/// ``` -/// -/// Display: Round -/// Category: calculate -#[func] -pub fn round( - /// The number to round. - 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 { #example } -/// ```example -/// #assert(calc.clamp(5, 0, 10) == 5) -/// #assert(calc.clamp(5, 6, 10) == 6) -/// #calc.clamp(5, 0, 4) -/// ``` -/// -/// Display: Clamp -/// Category: calculate -#[func] -pub fn clamp( - /// The number to clamp. - value: Num, - /// The inclusive minimum value. - min: Num, - /// The inclusive maximum value. - max: Spanned, -) -> SourceResult { - 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 { #example } -/// ```example -/// #calc.min(1, -3, -5, 20, 3, 6) \ -/// #calc.min("typst", "in", "beta") -/// ``` -/// -/// Display: Minimum -/// Category: calculate -#[func] -pub fn min( - /// The sequence of values from which to extract the minimum. - /// Must not be empty. - #[variadic] - values: Vec>, - /// The callsite span. - span: Span, -) -> SourceResult { - minmax(span, values, Ordering::Less) -} - -/// Determines the maximum of a sequence of values. -/// -/// ## Example { #example } -/// ```example -/// #calc.max(1, -3, -5, 20, 3, 6) \ -/// #calc.max("typst", "in", "beta") -/// ``` -/// -/// Display: Maximum -/// Category: calculate -#[func] -pub fn max( - /// The sequence of values from which to extract the maximum. - /// Must not be empty. - #[variadic] - values: Vec>, - /// The callsite span. - span: Span, -) -> SourceResult { - minmax(span, values, Ordering::Greater) -} - -/// Find the minimum or maximum of a sequence of values. -fn minmax( - span: Span, - values: Vec>, - goal: Ordering, -) -> SourceResult { - 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 { #example } -/// ```example -/// #calc.even(4) \ -/// #calc.even(5) \ -/// #range(10).filter(calc.even) -/// ``` -/// -/// Display: Even -/// Category: calculate -#[func] -pub fn even( - /// The number to check for evenness. - value: i64, -) -> bool { - value % 2 == 0 -} - -/// Determines whether an integer is odd. -/// -/// ## Example { #example } -/// ```example -/// #calc.odd(4) \ -/// #calc.odd(5) \ -/// #range(10).filter(calc.odd) -/// ``` -/// -/// Display: Odd -/// Category: calculate -#[func] -pub fn odd( - /// The number to check for oddness. - value: i64, -) -> bool { - value % 2 != 0 -} - -/// Calculates the remainder of two numbers. -/// -/// ## Example { #example } -/// ```example -/// #calc.rem(20, 6) \ -/// #calc.rem(1.75, 0.5) -/// ``` -/// -/// Display: Remainder -/// Category: calculate -#[func] -pub fn rem( - /// The dividend of the remainder. - dividend: Num, - /// The divisor of the remainder. - divisor: Spanned, -) -> SourceResult { - if divisor.v.float() == 0.0 { - bail!(divisor.span, "divisor must not be zero"); - } - Ok(dividend.apply2(divisor.v, Rem::rem, Rem::rem)) -} - -/// Calculates the quotient of two numbers. -/// -/// ## Example { #example } -/// ```example -/// #calc.quo(14, 5) \ -/// #calc.quo(3.46, 0.5) -/// ``` -/// -/// Display: Quotient -/// Category: calculate -#[func] -pub fn quo( - /// The dividend of the quotient. - dividend: Num, - /// The divisor of the quotient. - divisor: Spanned, -) -> SourceResult { - 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 { - pub 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())), - } - } - - pub 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())), - } - } - - pub 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/library/src/compute/construct.rs b/library/src/compute/construct.rs deleted file mode 100644 index 956212ee..00000000 --- a/library/src/compute/construct.rs +++ /dev/null @@ -1,743 +0,0 @@ -use std::num::NonZeroI64; -use std::str::FromStr; - -use time::{Month, PrimitiveDateTime}; - -use typst::eval::{Datetime, Regex}; - -use crate::prelude::*; - -/// Converts a value to an integer. -/// -/// - Booleans are converted to `0` or `1`. -/// - Floats are floored to the next 64-bit integer. -/// - Strings are parsed in base 10. -/// -/// ## Example { #example } -/// ```example -/// #int(false) \ -/// #int(true) \ -/// #int(2.7) \ -/// #{ int("27") + int("4") } -/// ``` -/// -/// Display: Integer -/// Category: construct -#[func] -pub fn int( - /// The value that should be converted to an integer. - value: ToInt, -) -> i64 { - value.0 -} - -/// A value that can be cast to an integer. -pub struct ToInt(i64); - -cast! { - ToInt, - v: bool => Self(v as i64), - v: i64 => Self(v), - v: f64 => Self(v as i64), - v: EcoString => Self(v.parse().map_err(|_| eco_format!("invalid integer: {}", v))?), -} - -/// Converts a value to a float. -/// -/// - Booleans are converted to `0.0` or `1.0`. -/// - Integers are converted to the closest 64-bit float. -/// - Ratios are divided by 100%. -/// - Strings are parsed in base 10 to the closest 64-bit float. -/// Exponential notation is supported. -/// -/// ## Example { #example } -/// ```example -/// #float(false) \ -/// #float(true) \ -/// #float(4) \ -/// #float(40%) \ -/// #float("2.7") \ -/// #float("1e5") -/// ``` -/// -/// Display: Float -/// Category: construct -#[func] -pub fn float( - /// The value that should be converted to a float. - value: ToFloat, -) -> f64 { - value.0 -} - -/// A value that can be cast to a float. -pub struct ToFloat(f64); - -cast! { - ToFloat, - v: bool => Self(v as i64 as f64), - v: i64 => Self(v as f64), - v: f64 => Self(v), - v: Ratio => Self(v.get()), - v: EcoString => Self(v.parse().map_err(|_| eco_format!("invalid float: {}", v))?), -} - -/// Creates a grayscale color. -/// -/// ## Example { #example } -/// ```example -/// #for x in range(250, step: 50) { -/// box(square(fill: luma(x))) -/// } -/// ``` -/// -/// Display: Luma -/// Category: construct -#[func] -pub fn luma( - /// The gray component. - gray: Component, -) -> Color { - LumaColor::new(gray.0).into() -} - -/// Creates an RGB(A) color. -/// -/// The color is specified in the sRGB color space. -/// -/// _Note:_ While you can specify transparent colors and Typst's preview will -/// render them correctly, the PDF export does not handle them properly at the -/// moment. This will be fixed in the future. -/// -/// ## Example { #example } -/// ```example -/// #square(fill: rgb("#b1f2eb")) -/// #square(fill: rgb(87, 127, 230)) -/// #square(fill: rgb(25%, 13%, 65%)) -/// ``` -/// -/// Display: RGB -/// Category: construct -#[func] -pub fn rgb( - /// The color in hexadecimal notation. - /// - /// Accepts three, four, six or eight hexadecimal digits and optionally - /// a leading hashtag. - /// - /// If this string is given, the individual components should not be given. - /// - /// ```example - /// #text(16pt, rgb("#239dad"))[ - /// *Typst* - /// ] - /// ``` - #[external] - hex: EcoString, - /// The red component. - #[external] - red: Component, - /// The green component. - #[external] - green: Component, - /// The blue component. - #[external] - blue: Component, - /// The alpha component. - #[external] - alpha: Component, - /// The arguments. - args: Args, -) -> SourceResult { - let mut args = args; - Ok(if let Some(string) = args.find::>()? { - match RgbaColor::from_str(&string.v) { - Ok(color) => color.into(), - Err(msg) => bail!(string.span, "{msg}"), - } - } else { - let Component(r) = args.expect("red component")?; - let Component(g) = args.expect("green component")?; - let Component(b) = args.expect("blue component")?; - let Component(a) = args.eat()?.unwrap_or(Component(255)); - RgbaColor::new(r, g, b, a).into() - }) -} - -/// An integer or ratio component. -pub struct Component(u8); - -cast! { - Component, - v: i64 => match v { - 0 ..= 255 => Self(v as u8), - _ => bail!("number must be between 0 and 255"), - }, - v: Ratio => if (0.0 ..= 1.0).contains(&v.get()) { - Self((v.get() * 255.0).round() as u8) - } else { - bail!("ratio must be between 0% and 100%"); - }, -} - -/// Creates a new datetime. -/// -/// You can specify the [datetime]($type/datetime) using a year, month, day, -/// hour, minute, and second. You can also get the current date with -/// [`datetime.today`]($func/datetime.today). -/// -/// ## Example -/// ```example -/// #let date = datetime( -/// year: 2012, -/// month: 8, -/// day: 3, -/// ) -/// -/// #date.display() \ -/// #date.display( -/// "[day].[month].[year]" -/// ) -/// ``` -/// -/// ## Format -/// _Note_: Depending on which components of the datetime you specify, Typst -/// will store it in one of the following three ways: -/// * If you specify year, month and day, Typst will store just a date. -/// * If you specify hour, minute and second, Typst will store just a time. -/// * If you specify all of year, month, day, hour, minute and second, Typst -/// will store a full datetime. -/// -/// Depending on how it is stored, the [`display`]($type/datetime.display) -/// method will choose a different formatting by default. -/// -/// Display: Datetime -/// Category: construct -#[func] -#[scope( - scope.define("today", datetime_today_func()); - scope -)] -pub fn datetime( - /// The year of the datetime. - #[named] - year: Option, - /// The month of the datetime. - #[named] - month: Option, - /// The day of the datetime. - #[named] - day: Option, - /// The hour of the datetime. - #[named] - hour: Option, - /// The minute of the datetime. - #[named] - minute: Option, - /// The second of the datetime. - #[named] - second: Option, -) -> StrResult { - let time = match (hour, minute, second) { - (Some(hour), Some(minute), Some(second)) => { - match time::Time::from_hms(hour.0, minute.0, second.0) { - Ok(time) => Some(time), - Err(_) => bail!("time is invalid"), - } - } - (None, None, None) => None, - _ => bail!("time is incomplete"), - }; - - let date = match (year, month, day) { - (Some(year), Some(month), Some(day)) => { - match time::Date::from_calendar_date(year.0, month.0, day.0) { - Ok(date) => Some(date), - Err(_) => bail!("date is invalid"), - } - } - (None, None, None) => None, - _ => bail!("date is incomplete"), - }; - - Ok(match (date, time) { - (Some(date), Some(time)) => { - Datetime::Datetime(PrimitiveDateTime::new(date, time)) - } - (Some(date), None) => Datetime::Date(date), - (None, Some(time)) => Datetime::Time(time), - (None, None) => { - bail!("at least one of date or time must be fully specified") - } - }) -} - -pub struct YearComponent(i32); -pub struct MonthComponent(Month); -pub struct DayComponent(u8); -pub struct HourComponent(u8); -pub struct MinuteComponent(u8); -pub struct SecondComponent(u8); - -cast! { - YearComponent, - v: i32 => Self(v), -} - -cast! { - MonthComponent, - v: u8 => Self(Month::try_from(v).map_err(|_| "month is invalid")?) -} - -cast! { - DayComponent, - v: u8 => Self(v), -} - -cast! { - HourComponent, - v: u8 => Self(v), -} - -cast! { - MinuteComponent, - v: u8 => Self(v), -} - -cast! { - SecondComponent, - v: u8 => Self(v), -} - -/// Returns the current date. -/// -/// Refer to the documentation of the [`display`]($type/datetime.display) method -/// for details on how to affect the formatting of the date. -/// -/// ## Example -/// ```example -/// Today's date is -/// #datetime.today().display(). -/// ``` -/// -/// Display: Today -/// Category: construct -#[func] -pub fn datetime_today( - /// An offset to apply to the current UTC date. If set to `{auto}`, the - /// offset will be the local offset. - #[named] - #[default] - offset: Smart, - /// The virtual machine. - vt: &mut Vt, -) -> StrResult { - Ok(vt - .world - .today(offset.as_custom()) - .ok_or("unable to get the current date")?) -} - -/// Creates a CMYK color. -/// -/// This is useful if you want to target a specific printer. The conversion -/// to RGB for display preview might differ from how your printer reproduces -/// the color. -/// -/// ## Example { #example } -/// ```example -/// #square( -/// fill: cmyk(27%, 0%, 3%, 5%) -/// ) -/// ```` -/// -/// Display: CMYK -/// Category: construct -#[func] -pub fn cmyk( - /// The cyan component. - cyan: RatioComponent, - /// The magenta component. - magenta: RatioComponent, - /// The yellow component. - yellow: RatioComponent, - /// The key component. - key: RatioComponent, -) -> Color { - CmykColor::new(cyan.0, magenta.0, yellow.0, key.0).into() -} - -/// A component that must be a ratio. -pub struct RatioComponent(u8); - -cast! { - RatioComponent, - v: Ratio => if (0.0 ..= 1.0).contains(&v.get()) { - Self((v.get() * 255.0).round() as u8) - } else { - bail!("ratio must be between 0% and 100%"); - }, -} - -/// Creates a custom symbol with modifiers. -/// -/// ## Example { #example } -/// ```example -/// #let envelope = symbol( -/// "🖂", -/// ("stamped", "🖃"), -/// ("stamped.pen", "🖆"), -/// ("lightning", "🖄"), -/// ("fly", "🖅"), -/// ) -/// -/// #envelope -/// #envelope.stamped -/// #envelope.stamped.pen -/// #envelope.lightning -/// #envelope.fly -/// ``` -/// -/// Display: Symbol -/// Category: construct -#[func] -pub fn symbol( - /// The variants of the symbol. - /// - /// Can be a just a string consisting of a single character for the - /// modifierless variant or an array with two strings specifying the modifiers - /// and the symbol. Individual modifiers should be separated by dots. When - /// displaying a symbol, Typst selects the first from the variants that have - /// all attached modifiers and the minimum number of other modifiers. - #[variadic] - variants: Vec>, - /// The callsite span. - span: Span, -) -> SourceResult { - let mut list = Vec::new(); - if variants.is_empty() { - bail!(span, "expected at least one variant"); - } - for Spanned { v, span } in variants { - if list.iter().any(|(prev, _)| &v.0 == prev) { - bail!(span, "duplicate variant"); - } - list.push((v.0, v.1)); - } - Ok(Symbol::runtime(list.into_boxed_slice())) -} - -/// A value that can be cast to a symbol. -pub struct Variant(EcoString, char); - -cast! { - Variant, - c: char => Self(EcoString::new(), c), - array: Array => { - let mut iter = array.into_iter(); - match (iter.next(), iter.next(), iter.next()) { - (Some(a), Some(b), None) => Self(a.cast()?, b.cast()?), - _ => bail!("point array must contain exactly two entries"), - } - }, -} - -/// Converts a value to a string. -/// -/// - Integers are formatted in base 10. This can be overridden with the -/// optional `base` parameter. -/// - Floats are formatted in base 10 and never in exponential notation. -/// - From labels the name is extracted. -/// -/// If you wish to convert from and to Unicode code points, see -/// [`str.to-unicode`]($func/str.to-unicode) and -/// [`str.from-unicode`]($func/str.from-unicode). -/// -/// ## Example { #example } -/// ```example -/// #str(10) \ -/// #str(4000, base: 16) \ -/// #str(2.7) \ -/// #str(1e8) \ -/// #str() -/// ``` -/// -/// Display: String -/// Category: construct -#[func] -#[scope( - scope.define("to-unicode", str_to_unicode_func()); - scope.define("from-unicode", str_from_unicode_func()); - scope -)] -pub fn str( - /// The value that should be converted to a string. - value: ToStr, - /// The base (radix) to display integers in, between 2 and 36. - #[named] - #[default(Spanned::new(10, Span::detached()))] - base: Spanned, -) -> SourceResult { - Ok(match value { - ToStr::Str(s) => { - if base.v != 10 { - bail!(base.span, "base is only supported for integers"); - } - s - } - ToStr::Int(n) => { - if base.v < 2 || base.v > 36 { - bail!(base.span, "base must be between 2 and 36"); - } - int_to_base(n, base.v).into() - } - }) -} - -/// A value that can be cast to a string. -pub enum ToStr { - /// A string value ready to be used as-is. - Str(Str), - /// An integer about to be formatted in a given base. - Int(i64), -} - -cast! { - ToStr, - v: i64 => Self::Int(v), - v: f64 => Self::Str(format_str!("{}", v)), - v: Label => Self::Str(v.0.into()), - v: Str => Self::Str(v), -} - -/// Format an integer in a base. -fn int_to_base(mut n: i64, base: i64) -> EcoString { - if n == 0 { - return "0".into(); - } - - // In Rust, `format!("{:x}", -14i64)` is not `-e` but `fffffffffffffff2`. - // So we can only use the built-in for decimal, not bin/oct/hex. - if base == 10 { - return eco_format!("{n}"); - } - - // The largest output is `to_base(i64::MIN, 2)`, which is 65 chars long. - const SIZE: usize = 65; - let mut digits = [b'\0'; SIZE]; - let mut i = SIZE; - - // It's tempting to take the absolute value, but this will fail for i64::MIN. - // Instead, we turn n negative, as -i64::MAX is perfectly representable. - let negative = n < 0; - if n > 0 { - n = -n; - } - - while n != 0 { - let digit = char::from_digit(-(n % base) as u32, base as u32); - i -= 1; - digits[i] = digit.unwrap_or('?') as u8; - n /= base; - } - - if negative { - i -= 1; - digits[i] = b'-'; - } - - std::str::from_utf8(&digits[i..]).unwrap_or_default().into() -} - -/// Converts a character into its corresponding code point. -/// -/// ## Example -/// ```example -/// #str.to-unicode("a") \ -/// #"a\u{0300}".codepoints().map(str.to-unicode) -/// ``` -/// -/// Display: String To Unicode -/// Category: construct -#[func] -pub fn str_to_unicode( - /// The character that should be converted. - value: char, -) -> u32 { - value.into() -} - -/// Converts a Unicode code point into its corresponding string. -/// -/// ```example -/// #str.from-unicode(97) -/// ``` -/// -/// Display: String From Unicode -/// Category: construct -#[func] -pub fn str_from_unicode( - /// The code point that should be converted. - value: CodePoint, -) -> Str { - format_str!("{}", value.0) -} - -/// The numeric representation of a single unicode code point. -pub struct CodePoint(char); - -cast! { - CodePoint, - v: i64 => { - Self(v.try_into().ok().and_then(|v: u32| v.try_into().ok()).ok_or_else( - || eco_format!("{:#x} is not a valid codepoint", v), - )?) - }, -} - -/// Creates a label from a string. -/// -/// Inserting a label into content attaches it to the closest previous element -/// that is not a space. Then, the element can be [referenced]($func/ref) and -/// styled through the label. -/// -/// ## Example { #example } -/// ```example -/// #show : set text(blue) -/// #show label("b"): set text(red) -/// -/// = Heading -/// *Strong* #label("b") -/// ``` -/// -/// ## Syntax { #syntax } -/// This function also has dedicated syntax: You can create a label by enclosing -/// its name in angle brackets. This works both in markup and code. -/// -/// Display: Label -/// Category: construct -#[func] -pub fn label( - /// The name of the label. - name: EcoString, -) -> Label { - Label(name) -} - -/// Creates a regular expression from a string. -/// -/// The result can be used as a -/// [show rule selector]($styling/#show-rules) and with -/// [string methods]($type/string) like `find`, `split`, and `replace`. -/// -/// See [the specification of the supported syntax](https://docs.rs/regex/latest/regex/#syntax). -/// -/// ## Example { #example } -/// ```example -/// // Works with show rules. -/// #show regex("\d+"): set text(red) -/// -/// The numbers 1 to 10. -/// -/// // Works with string methods. -/// #("a,b;c" -/// .split(regex("[,;]"))) -/// ``` -/// -/// Display: Regex -/// Category: construct -#[func] -pub fn regex( - /// The regular expression as a string. - /// - /// Most regex escape sequences just work because they are not valid Typst - /// escape sequences. To produce regex escape sequences that are also valid in - /// Typst (e.g. `[\\]`), you need to escape twice. Thus, to match a verbatim - /// backslash, you would need to write `{regex("\\\\")}`. - /// - /// If you need many escape sequences, you can also create a raw element - /// and extract its text to use it for your regular expressions: - /// ```{regex(`\d+\.\d+\.\d+`.text)}```. - regex: Spanned, -) -> SourceResult { - Regex::new(®ex.v).at(regex.span) -} - -/// Creates an array consisting of consecutive integers. -/// -/// If you pass just one positional parameter, it is interpreted as the `end` of -/// the range. If you pass two, they describe the `start` and `end` of the -/// range. -/// -/// ## Example { #example } -/// ```example -/// #range(5) \ -/// #range(2, 5) \ -/// #range(20, step: 4) \ -/// #range(21, step: 4) \ -/// #range(5, 2, step: -1) -/// ``` -/// -/// Display: Range -/// Category: construct -#[func] -pub fn range( - /// The start of the range (inclusive). - #[external] - #[default] - start: i64, - /// The end of the range (exclusive). - #[external] - end: i64, - /// The distance between the generated numbers. - #[named] - #[default(NonZeroI64::new(1).unwrap())] - step: NonZeroI64, - /// The arguments. - args: Args, -) -> SourceResult { - let mut args = args; - let first = args.expect::("end")?; - let (start, end) = match args.eat::()? { - Some(second) => (first, second), - None => (0, first), - }; - - let step = step.get(); - - let mut x = start; - let mut array = Array::new(); - - while x.cmp(&end) == 0.cmp(&step) { - array.push(Value::Int(x)); - x += step; - } - - Ok(array) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_to_base() { - assert_eq!(&int_to_base(0, 10), "0"); - assert_eq!(&int_to_base(0, 16), "0"); - assert_eq!(&int_to_base(0, 36), "0"); - assert_eq!( - &int_to_base(i64::MAX, 2), - "111111111111111111111111111111111111111111111111111111111111111" - ); - assert_eq!( - &int_to_base(i64::MIN, 2), - "-1000000000000000000000000000000000000000000000000000000000000000" - ); - assert_eq!(&int_to_base(i64::MAX, 10), "9223372036854775807"); - assert_eq!(&int_to_base(i64::MIN, 10), "-9223372036854775808"); - assert_eq!(&int_to_base(i64::MAX, 16), "7fffffffffffffff"); - assert_eq!(&int_to_base(i64::MIN, 16), "-8000000000000000"); - assert_eq!(&int_to_base(i64::MAX, 36), "1y2p0ij32e8e7"); - assert_eq!(&int_to_base(i64::MIN, 36), "-1y2p0ij32e8e8"); - } -} diff --git a/library/src/compute/data.rs b/library/src/compute/data.rs deleted file mode 100644 index 6e3a298e..00000000 --- a/library/src/compute/data.rs +++ /dev/null @@ -1,492 +0,0 @@ -use typst::diag::{format_xml_like_error, FileError}; -use typst::eval::Datetime; - -use crate::prelude::*; - -/// Reads plain text from a file. -/// -/// The file will be read and returned as a string. -/// -/// ## Example { #example } -/// ```example -/// #let text = read("data.html") -/// -/// An example for a HTML file:\ -/// #raw(text, lang: "html") -/// ``` -/// -/// Display: Read -/// Category: data-loading -#[func] -pub fn read( - /// Path to a file. - path: Spanned, - /// The virtual machine. - vm: &mut Vm, -) -> SourceResult { - let Spanned { v: path, span } = path; - let id = vm.location().join(&path).at(span)?; - let data = vm.world().file(id).at(span)?; - let text = std::str::from_utf8(&data) - .map_err(|_| "file is not valid utf-8") - .at(span)?; - Ok(text.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 } -/// ```example -/// #let results = csv("data.csv") -/// -/// #table( -/// columns: 2, -/// [*Condition*], [*Result*], -/// ..results.flatten(), -/// ) -/// ``` -/// -/// Display: CSV -/// Category: data-loading -#[func] -pub fn csv( - /// Path to a CSV file. - path: Spanned, - /// The delimiter that separates columns in the CSV file. - /// Must be a single ASCII character. - #[named] - #[default] - delimiter: Delimiter, - /// The virtual machine. - vm: &mut Vm, -) -> SourceResult { - let Spanned { v: path, span } = path; - let id = vm.location().join(&path).at(span)?; - let data = vm.world().file(id).at(span)?; - - 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(error: csv::Error, line: usize) -> EcoString { - match error.kind() { - csv::ErrorKind::Utf8 { .. } => "file is not valid utf-8".into(), - csv::ErrorKind::UnequalLengths { expected_len, len, .. } => { - eco_format!( - "failed to parse csv file: found {len} instead of {expected_len} fields in line {line}" - ) - } - _ => "failed to parse csv file".into(), - } -} - -/// 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 } -/// ```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")) -/// ``` -/// -/// Display: JSON -/// Category: data-loading -#[func] -pub fn json( - /// Path to a JSON file. - path: Spanned, - /// The virtual machine. - vm: &mut Vm, -) -> SourceResult { - let Spanned { v: path, span } = path; - let id = vm.location().join(&path).at(span)?; - let data = vm.world().file(id).at(span)?; - let value: serde_json::Value = - serde_json::from_slice(&data).map_err(format_json_error).at(span)?; - Ok(convert_json(value)) -} - -/// Convert a JSON value to a Typst value. -fn convert_json(value: serde_json::Value) -> Value { - match value { - serde_json::Value::Null => Value::None, - serde_json::Value::Bool(v) => v.into_value(), - serde_json::Value::Number(v) => match v.as_i64() { - Some(int) => int.into_value(), - None => v.as_f64().unwrap_or(f64::NAN).into_value(), - }, - serde_json::Value::String(v) => v.into_value(), - serde_json::Value::Array(v) => { - v.into_iter().map(convert_json).collect::().into_value() - } - serde_json::Value::Object(v) => v - .into_iter() - .map(|(key, value)| (key.into(), convert_json(value))) - .collect::() - .into_value(), - } -} - -/// Format the user-facing JSON error message. -fn format_json_error(error: serde_json::Error) -> EcoString { - assert!(error.is_syntax() || error.is_eof()); - eco_format!("failed to parse json file: syntax error in line {}", error.line()) -} - -/// 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 and booleans will be converted into the Typst -/// equivalents and numbers will be converted to floats or integers depending on -/// whether they are whole numbers. For the time being, datetimes will be -/// converted to strings as Typst does not have a built-in datetime yet. -/// -/// The TOML file in the example consists of a table with the keys `title`, -/// `version`, and `authors`. -/// -/// ## Example { #example } -/// ```example -/// #let details = toml("details.toml") -/// -/// Title: #details.title \ -/// Version: #details.version \ -/// Authors: #(details.authors -/// .join(", ", last: " and ")) -/// ``` -/// -/// Display: TOML -/// Category: data-loading -#[func] -pub fn toml( - /// Path to a TOML file. - path: Spanned, - /// The virtual machine. - vm: &mut Vm, -) -> SourceResult { - let Spanned { v: path, span } = path; - let id = vm.location().join(&path).at(span)?; - let data = vm.world().file(id).at(span)?; - - let raw = std::str::from_utf8(&data) - .map_err(|_| "file is not valid utf-8") - .at(span)?; - - let value: toml::Value = toml::from_str(raw).map_err(format_toml_error).at(span)?; - Ok(convert_toml(value)) -} - -/// Convert a TOML value to a Typst value. -fn convert_toml(value: toml::Value) -> Value { - match value { - toml::Value::String(v) => v.into_value(), - toml::Value::Integer(v) => v.into_value(), - toml::Value::Float(v) => v.into_value(), - toml::Value::Boolean(v) => v.into_value(), - toml::Value::Array(v) => { - v.into_iter().map(convert_toml).collect::().into_value() - } - toml::Value::Table(v) => v - .into_iter() - .map(|(key, value)| (key.into(), convert_toml(value))) - .collect::() - .into_value(), - toml::Value::Datetime(v) => match (v.date, v.time) { - (None, None) => Value::None, - (Some(date), None) => { - Datetime::from_ymd(date.year as i32, date.month, date.day).into_value() - } - (None, Some(time)) => { - Datetime::from_hms(time.hour, time.minute, time.second).into_value() - } - (Some(date), Some(time)) => Datetime::from_ymd_hms( - date.year as i32, - date.month, - date.day, - time.hour, - time.minute, - time.second, - ) - .into_value(), - }, - } -} - -/// Format the user-facing TOML error message. -fn format_toml_error(error: toml::de::Error) -> EcoString { - if let Some(range) = error.span() { - eco_format!( - "failed to parse toml file: {}, index {}-{}", - error.message(), - range.start, - range.end - ) - } else { - eco_format!("failed to parse toml file: {}", 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. -/// -/// Note that mapping keys that are not a string cause the entry to be -/// discarded. -/// -/// Custom YAML tags are ignored, though the loaded value will still be -/// present. -/// -/// The function returns a dictionary or value or an array, depending on -/// the YAML file. -/// -/// 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 } -/// ```example -/// #let bookshelf(contents) = { -/// for (author, works) in contents { -/// author -/// for work in works [ -/// - #work.title (#work.published) -/// ] -/// } -/// } -/// -/// #bookshelf( -/// yaml("scifi-authors.yaml") -/// ) -/// ``` -/// -/// Display: YAML -/// Category: data-loading -#[func] -pub fn yaml( - /// Path to a YAML file. - path: Spanned, - /// The virtual machine. - vm: &mut Vm, -) -> SourceResult { - let Spanned { v: path, span } = path; - let id = vm.location().join(&path).at(span)?; - let data = vm.world().file(id).at(span)?; - let value: serde_yaml::Value = - serde_yaml::from_slice(&data).map_err(format_yaml_error).at(span)?; - Ok(convert_yaml(value)) -} - -/// Convert a YAML value to a Typst value. -fn convert_yaml(value: serde_yaml::Value) -> Value { - match value { - serde_yaml::Value::Null => Value::None, - serde_yaml::Value::Bool(v) => v.into_value(), - serde_yaml::Value::Number(v) => match v.as_i64() { - Some(int) => int.into_value(), - None => v.as_f64().unwrap_or(f64::NAN).into_value(), - }, - serde_yaml::Value::String(v) => v.into_value(), - serde_yaml::Value::Sequence(v) => { - v.into_iter().map(convert_yaml).collect::().into_value() - } - serde_yaml::Value::Mapping(v) => v - .into_iter() - .map(|(key, value)| (convert_yaml_key(key), convert_yaml(value))) - .filter_map(|(key, value)| key.map(|key| (key, value))) - .collect::() - .into_value(), - } -} - -/// Converts an arbitrary YAML mapping key into a Typst Dict Key. -/// Currently it only does so for strings, everything else -/// returns None -fn convert_yaml_key(key: serde_yaml::Value) -> Option { - match key { - serde_yaml::Value::String(v) => Some(Str::from(v)), - _ => None, - } -} - -/// Format the user-facing YAML error message. -fn format_yaml_error(error: serde_yaml::Error) -> EcoString { - eco_format!("failed to parse yaml file: {}", error.to_string().trim()) -} - -/// 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 } -/// ```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) -/// } -/// } -/// ``` -/// -/// Display: XML -/// Category: data-loading -#[func] -pub fn xml( - /// Path to an XML file. - path: Spanned, - /// The virtual machine. - vm: &mut Vm, -) -> SourceResult { - let Spanned { v: path, span } = path; - let id = vm.location().join(&path).at(span)?; - let data = vm.world().file(id).at(span)?; - let text = std::str::from_utf8(&data).map_err(FileError::from).at(span)?; - let document = roxmltree::Document::parse(text).map_err(format_xml_error).at(span)?; - 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 file", error) -} diff --git a/library/src/compute/foundations.rs b/library/src/compute/foundations.rs deleted file mode 100644 index f83d71a0..00000000 --- a/library/src/compute/foundations.rs +++ /dev/null @@ -1,215 +0,0 @@ -use crate::prelude::*; - -/// Determines the type of a value. -/// -/// Returns the name of the value's type. -/// -/// ## Example { #example } -/// ```example -/// #type(12) \ -/// #type(14.7) \ -/// #type("hello") \ -/// #type(none) \ -/// #type([Hi]) \ -/// #type(x => x + 1) -/// ``` -/// -/// Display: Type -/// Category: foundations -#[func] -pub fn type_( - /// The value whose type's to determine. - value: Value, -) -> Str { - value.type_name().into() -} - -/// 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 } -/// ```example -/// #none vs #repr(none) \ -/// #"hello" vs #repr("hello") \ -/// #(1, 2) vs #repr((1, 2)) \ -/// #[*Hi*] vs #repr([*Hi*]) -/// ``` -/// -/// Display: Representation -/// Category: foundations -#[func] -pub fn repr( - /// The value whose string representation to produce. - value: Value, -) -> Str { - value.repr() -} - -/// Fails with an error. -/// -/// ## Example { #example } -/// The code below produces the error `panicked with: "this is wrong"`. -/// ```typ -/// #panic("this is wrong") -/// ``` -/// -/// Display: Panic -/// Category: foundations -#[func] -pub fn panic( - /// The values to panic with. - #[variadic] - values: Vec, -) -> StrResult { - 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`]($func/assert.eq) and [`assert.ne`]($func/assert.ne). -/// -/// ## Example { #example } -/// ```typ -/// #assert(1 < 2, message: "math broke") -/// ``` -/// -/// Display: Assert -/// Category: foundations -#[func] -#[scope( - scope.define("eq", assert_eq_func()); - scope.define("ne", assert_ne_func()); - scope -)] -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, -) -> StrResult { - if !condition { - if let Some(message) = message { - bail!("assertion failed: {message}"); - } else { - bail!("assertion failed"); - } - } - Ok(NoneValue) -} - -/// Ensures that two values are equal. -/// -/// Fails with an error if the first value is not equal to the second. Does not -/// produce any output in the document. -/// -/// ## Example { #example } -/// ```typ -/// #assert.eq(10, 10) -/// ``` -/// -/// Display: Assert Equals -/// Category: foundations -#[func] -pub fn assert_eq( - /// The first value to compare. - left: Value, - - /// The second value to compare. - right: Value, - - /// An optional message to display on error instead of the representations - /// of the compared values. - #[named] - message: Option, -) -> StrResult { - if left != right { - if let Some(message) = message { - bail!("equality assertion failed: {message}"); - } else { - bail!("equality assertion failed: value {left:?} was not equal to {right:?}"); - } - } - Ok(NoneValue) -} - -/// Ensures that two values are not equal. -/// -/// Fails with an error if the first value is equal to the second. Does not -/// produce any output in the document. -/// -/// ## Example { #example } -/// ```typ -/// #assert.ne(3, 4) -/// ``` -/// -/// Display: Assert Not Equals -/// Category: foundations -#[func] -pub fn assert_ne( - /// The first value to compare. - left: Value, - - /// The second value to compare. - right: Value, - - /// An optional message to display on error instead of the representations - /// of the compared values. - #[named] - message: Option, -) -> StrResult { - if left == right { - if let Some(message) = message { - bail!("inequality assertion failed: {message}"); - } else { - bail!("inequality assertion failed: value {left:?} was equal to {right:?}"); - } - } - Ok(NoneValue) -} - -/// Evaluates a string as Typst code. -/// -/// This function should only be used as a last resort. -/// -/// ## Example { #example } -/// ```example -/// #eval("1 + 1") \ -/// #eval("(1, 2, 3, 4)").len() \ -/// #eval("[*Strong text*]") -/// ``` -/// -/// Display: Evaluate -/// Category: foundations -#[func] -pub fn eval( - /// A string of Typst code to evaluate. - /// - /// The code in the string cannot interact with the file system. - source: Spanned, - /// The virtual machine. - vm: &mut Vm, -) -> SourceResult { - let Spanned { v: text, span } = source; - typst::eval::eval_string(vm.world(), &text, span) -} diff --git a/library/src/compute/mod.rs b/library/src/compute/mod.rs deleted file mode 100644 index e9e4870c..00000000 --- a/library/src/compute/mod.rs +++ /dev/null @@ -1,39 +0,0 @@ -//! Computational functions. - -pub mod calc; -mod construct; -mod data; -mod foundations; - -pub use self::construct::*; -pub use self::data::*; -pub use self::foundations::*; - -use crate::prelude::*; - -/// Hook up all compute definitions. -pub(super) fn define(global: &mut Scope) { - global.define("type", type_func()); - global.define("repr", repr_func()); - global.define("panic", panic_func()); - global.define("assert", assert_func()); - global.define("eval", eval_func()); - global.define("int", int_func()); - global.define("float", float_func()); - global.define("luma", luma_func()); - global.define("rgb", rgb_func()); - global.define("cmyk", cmyk_func()); - global.define("datetime", datetime_func()); - global.define("symbol", symbol_func()); - global.define("str", str_func()); - global.define("label", label_func()); - global.define("regex", regex_func()); - global.define("range", range_func()); - global.define("read", read_func()); - global.define("csv", csv_func()); - global.define("json", json_func()); - global.define("toml", toml_func()); - global.define("yaml", yaml_func()); - global.define("xml", xml_func()); - global.define("calc", calc::module()); -} diff --git a/library/src/layout/align.rs b/library/src/layout/align.rs deleted file mode 100644 index bbfe9f7e..00000000 --- a/library/src/layout/align.rs +++ /dev/null @@ -1,67 +0,0 @@ -use crate::prelude::*; - -/// Aligns content horizontally and vertically. -/// -/// ## Example { #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 -/// ``` -/// -/// Display: Align -/// Category: layout -#[element(Show)] -pub struct AlignElem { - /// The alignment along both axes. - /// - /// Possible values for horizontal alignments are: - /// - `start` - /// - `end` - /// - `left` - /// - `center` - /// - `right` - /// - /// The `start` and `end` alignments are relative to the current [text - /// direction]($func/text.dir). - /// - /// Possible values for vertical alignments are: - /// - `top` - /// - `horizon` - /// - `bottom` - /// - /// To align along both axes at the same time, add the two alignments using - /// the `+` operator to get a `2d alignment`. For example, `top + right` - /// aligns the content to the top right corner. - /// - /// ```example - /// #set page(height: 6cm) - /// #set text(lang: "ar") - /// - /// مثال - /// #align( - /// end + horizon, - /// rect(inset: 12pt)[ركن] - /// ) - /// ``` - #[positional] - #[fold] - #[default(Axes::new(GenAlign::Start, GenAlign::Specific(Align::Top)))] - pub alignment: Axes>, - - /// 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 { - Ok(self - .body() - .styled(Self::set_alignment(self.alignment(styles).map(Some)))) - } -} diff --git a/library/src/layout/columns.rs b/library/src/layout/columns.rs deleted file mode 100644 index d2138491..00000000 --- a/library/src/layout/columns.rs +++ /dev/null @@ -1,172 +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. -/// -/// ## Example { #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. -/// ``` -/// -/// Display: Columns -/// Category: layout -#[element(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, - - /// 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 { - 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(®ions.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::new(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]($func/pagebreak) when used in a -/// single column layout or the last column on a page. Otherwise, content after -/// the column break will be placed in the next column. -/// -/// ## Example { #example } -/// ```example -/// #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. -/// ``` -/// -/// Display: Column Break -/// Category: layout -#[element(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/library/src/layout/container.rs b/library/src/layout/container.rs deleted file mode 100644 index c79669d0..00000000 --- a/library/src/layout/container.rs +++ /dev/null @@ -1,497 +0,0 @@ -use typst::eval::AutoValue; - -use super::VElem; -use crate::layout::Spacing; -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 } -/// ```example -/// Refer to the docs -/// #box( -/// height: 9pt, -/// image("docs.svg") -/// ) -/// for more information. -/// ``` -/// -/// Display: Box -/// Category: layout -#[element(Layout)] -pub struct BoxElem { - /// The width of the box. - /// - /// Boxes can have [fractional]($type/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>, - - /// An amount to shift the box's baseline by. - /// - /// ```example - /// Image: #box(baseline: 40%, image("tiger.jpg", width: 2cm)). - /// ``` - #[resolve] - pub baseline: Rel, - - /// The box's background color. See the - /// [rectangle's documentation]($func/rect.fill) for more details. - pub fill: Option, - - /// The box's border color. See the - /// [rectangle's documentation]($func/rect.stroke) for more details. - #[resolve] - #[fold] - pub stroke: Sides>>, - - /// How much to round the box's corners. See the [rectangle's - /// documentation]($func/rect.radius) for more details. - #[resolve] - #[fold] - pub radius: Corners>>, - - /// How much to pad the box's content. See the [rectangle's - /// documentation]($func/rect.inset) for more details. - #[resolve] - #[fold] - pub inset: Sides>>, - - /// 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]($func/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>>, - - /// Whether to clip the content inside the box. - #[default(false)] - pub clip: bool, - - /// The contents of the box. - #[positional] - pub body: Option, -} - -impl Layout for BoxElem { - #[tracing::instrument(name = "BoxElem::layout", skip_all)] - fn layout( - &self, - vt: &mut Vt, - styles: StyleChain, - regions: Regions, - ) -> SourceResult { - 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(regions.base()) - .map(|(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); - } - - // Clip the contents - if self.clip(styles) { - frame.clip(); - } - - // Prepare fill and stroke. - let fill = self.fill(styles); - let stroke = self.stroke(styles).map(|s| s.map(PartialStroke::unwrap_or_default)); - - // 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); - - 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 { #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. -/// ``` -/// -/// Display: Block -/// Category: layout -#[element(Layout)] -pub struct BlockElem { - /// The block's width. - /// - /// ```example - /// #set align(center) - /// #block( - /// width: 60%, - /// inset: 8pt, - /// fill: silver, - /// lorem(10), - /// ) - /// ``` - pub width: Smart>, - - /// The block's height. When the height is larger than the remaining space - /// on a page and [`breakable`]($func/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>, - - /// 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]($func/rect.fill) for more details. - pub fill: Option, - - /// The block's border color. See the - /// [rectangle's documentation]($func/rect.stroke) for more details. - #[resolve] - #[fold] - pub stroke: Sides>>, - - /// How much to round the block's corners. See the [rectangle's - /// documentation]($func/rect.radius) for more details. - #[resolve] - #[fold] - pub radius: Corners>>, - - /// How much to pad the block's content. See the [rectangle's - /// documentation]($func/rect.inset) for more details. - #[resolve] - #[fold] - pub inset: Sides>>, - - /// How much to expand the block's size without affecting the layout. See - /// the [rectangle's documentation]($func/rect.outset) for more details. - #[resolve] - #[fold] - pub outset: Sides>>, - - /// 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, - - /// 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 { - // 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(regions.base()) - .map(|(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 - }; - - // Clip the contents - if self.clip(styles) { - for frame in frames.iter_mut() { - frame.clip(); - } - } - - // Prepare fill and stroke. - let fill = self.fill(styles); - let stroke = self.stroke(styles).map(|s| s.map(PartialStroke::unwrap_or_default)); - - // 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.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), - /// 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> From 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 => Self::Rel(v), - v: Fr => Self::Fr(v), -} diff --git a/library/src/layout/enum.rs b/library/src/layout/enum.rs deleted file mode 100644 index d66477fc..00000000 --- a/library/src/layout/enum.rs +++ /dev/null @@ -1,318 +0,0 @@ -use std::str::FromStr; - -use crate::layout::{BlockElem, ParElem, Sizing, Spacing}; -use crate::meta::{Numbering, NumberingPattern}; -use crate::prelude::*; -use crate::text::TextElem; - -use super::GridLayouter; - -/// A numbered list. -/// -/// Displays a sequence of items vertically and numbers them consecutively. -/// -/// ## Example { #example } -/// ```example -/// Automatically numbered: -/// + Preparations -/// + Analysis -/// + Conclusions -/// -/// Manually numbered: -/// 2. What is the first step? -/// 5. I am confused. -/// + Moving on ... -/// -/// 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`]($func/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 { #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 plus sign or dot -/// becomes part of that item. -/// -/// Display: Numbered List -/// Category: layout -#[element(Layout)] -#[scope( - scope.define("item", EnumItem::func()); - scope -)] -pub struct EnumElem { - /// If this is `{false}`, the items are spaced apart with - /// [enum spacing]($func/enum.spacing). If it is `{true}`, they use normal - /// [leading]($func/par.leading) instead. This makes the enumeration more - /// 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]($func/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()))] - 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]($func/block.below). - pub spacing: Smart, - - /// 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(HorizontalAlign(GenAlign::End))] - pub number_align: HorizontalAlign, - - /// 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, - - /// The numbers of parent items. - #[internal] - #[fold] - parents: Parent, -} - -impl Layout for EnumElem { - #[tracing::instrument(name = "EnumElem::layout", skip_all)] - fn layout( - &self, - vt: &mut Vt, - styles: StyleChain, - regions: Regions, - ) -> SourceResult { - 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: Axes> = - Axes::new(self.number_align(styles).into(), Align::Top.into()).map(Some); - - 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().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, - ); - - Ok(layouter.layout(vt)?.fragment) - } -} - -/// An enumeration item. -/// -/// Display: Numbered List Item -/// Category: layout -#[element] -pub struct EnumItem { - /// The item's number. - #[positional] - pub number: Option, - - /// 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::().cloned().unwrap_or_else(|| Self::new(v.clone())), -} - -struct Parent(usize); - -cast! { - Parent, - self => self.0.into_value(), - v: usize => Self(v), -} - -impl Fold for Parent { - type Output = Vec; - - fn fold(self, mut outer: Self::Output) -> Self::Output { - outer.push(self.0); - outer - } -} diff --git a/library/src/layout/flow.rs b/library/src/layout/flow.rs deleted file mode 100644 index accd092a..00000000 --- a/library/src/layout/flow.rs +++ /dev/null @@ -1,583 +0,0 @@ -use std::mem; - -use super::{ - 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. -/// -/// Display: Flow -/// Category: layout -#[element(Layout)] -pub struct FlowElem { - /// The children that will be arranges into a flow. - #[variadic] - pub children: Vec, -} - -impl Layout for FlowElem { - #[tracing::instrument(name = "FlowElem::layout", skip_all)] - fn layout( - &self, - vt: &mut Vt, - styles: StyleChain, - regions: Regions, - ) -> SourceResult { - let mut layouter = FlowLayouter::new(regions, styles); - - for mut child in &self.children() { - 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::() { - layouter.layout_spacing(vt, elem, styles)?; - } else if let Some(elem) = child.to::() { - layouter.layout_par(vt, elem, styles)?; - } else if child.is::() - || child.is::() - || child.is::() - || child.is::() - || child.is::() - || child.is::() - || child.is::() - || child.is::() - { - let layoutable = child.with::().unwrap(); - layouter.layout_single(vt, layoutable, styles)?; - } else if child.is::() { - let mut frame = Frame::new(Size::zero()); - frame.meta(styles, true); - layouter.items.push(FlowItem::Frame { - frame, - aligns: Axes::new(Align::Top, Align::Left), - sticky: true, - movable: false, - }); - } else if child.can::() { - layouter.layout_multiple(vt, child, styles)?; - } else if child.is::() { - if !layouter.regions.backlog.is_empty() || layouter.regions.last.is_some() - { - layouter.finish_region()?; - } - } else { - bail!(child.span(), "unexpected flow child"); - } - } - - layouter.finish() - } -} - -/// 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, - /// 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, - /// Whether we have any footnotes in the current region. - has_footnotes: bool, - /// Footnote configuration. - footnote_config: FootnoteConfig, - /// Finished frames for previous regions. - finished: Vec, -} - -/// 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, aligns: Axes, sticky: bool, movable: bool }, - /// An absolutely placed frame. - Placed(Frame), - /// 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![], - 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 aligns = 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()?; - 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, aligns, 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 aligns = AlignElem::alignment_in(styles).resolve(styles); - let sticky = BlockElem::sticky_in(styles); - let pod = Regions::one(self.regions.base(), Axes::splat(false)); - let frame = content.layout(vt, styles, pod)?.into_frame(); - self.layout_item(vt, FlowItem::Frame { frame, aligns, sticky, movable: true })?; - self.last_was_par = false; - Ok(()) - } - - /// Layout into multiple regions. - fn layout_multiple( - &mut self, - vt: &mut Vt, - block: &Content, - styles: StyleChain, - ) -> SourceResult<()> { - // Placed elements that are out of flow produce placed items which - // aren't aligned later. - if let Some(placed) = block.to::() { - if placed.out_of_flow(styles) { - let frame = block.layout(vt, styles, self.regions)?.into_frame(); - self.layout_item(vt, FlowItem::Placed(frame))?; - return Ok(()); - } - } else if self.regions.is_full() { - // Skip directly if region is already full. - self.finish_region()?; - } - - // How to align the block. - let aligns = if let Some(align) = block.to::() { - align.alignment(styles) - } else if let Some((_, local)) = block.to_styled() { - AlignElem::alignment_in(styles.chain(local)) - } else { - AlignElem::alignment_in(styles) - } - .resolve(styles); - - // Temporarily delegerate rootness to the columns. - let is_root = self.root; - if is_root && block.is::() { - self.root = false; - self.regions.root = true; - } - - // Layout the block itself. - let sticky = BlockElem::sticky_in(styles); - let fragment = block.layout(vt, styles, self.regions)?; - let mut notes = Vec::new(); - - 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()?; - } - - self.layout_item( - vt, - FlowItem::Frame { frame, aligns, sticky, movable: false }, - )?; - } - - if self.root && !self.handle_footnotes(vt, &mut notes, false, false)? { - self.finish_region()?; - self.handle_footnotes(vt, &mut notes, false, true)?; - } - - 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, 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 size = frame.size(); - if !self.regions.size.y.fits(size.y) && !self.regions.in_last() { - self.finish_region()?; - } - - self.regions.size.y -= size.y; - 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()?; - self.items.extend(item); - self.regions.size.y -= size.y; - self.handle_footnotes(vt, &mut notes, true, true)?; - } - return Ok(()); - } - } - FlowItem::Placed(_) => {} - FlowItem::Footnote(_) => {} - } - - self.items.push(item); - Ok(()) - } - - /// Finish the frame for one region. - fn finish_region(&mut self) -> 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 first_footnote = true; - for item in &self.items { - match item { - FlowItem::Absolute(v, _) => used.y += *v, - FlowItem::Fractional(v) => fr += *v, - FlowItem::Frame { frame, .. } => { - let size = frame.size(); - used.y += size.y; - used.x.set_max(size.x); - } - FlowItem::Placed(_) => {} - FlowItem::Footnote(frame) => { - let size = frame.size(); - footnote_height += size.y; - if !first_footnote { - footnote_height += self.footnote_config.gap; - } - first_footnote = false; - used.x.set_max(size.x); - } - } - } - used.y += footnote_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::new(size); - let mut offset = Abs::zero(); - let mut ruler = Align::Top; - let mut footnote_offset = size.y - footnote_height; - - // 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, aligns, .. } => { - ruler = ruler.max(aligns.y); - let x = aligns.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::Footnote(frame) => { - let pos = Point::with_y(footnote_offset); - footnote_offset += frame.height() + self.footnote_config.gap; - output.push_frame(pos, frame); - } - FlowItem::Placed(frame) => { - output.push_frame(Point::zero(), frame); - } - } - } - - // Advance to the next region. - self.finished.push(output); - self.regions.next(); - self.initial = self.regions.size; - self.has_footnotes = false; - Ok(()) - } - - /// Finish layouting and return the resulting fragment. - fn finish(mut self) -> SourceResult { - if self.expand.y { - while !self.regions.backlog.is_empty() { - self.finish_region()?; - } - } - - self.finish_region()?; - Ok(Fragment::frames(self.finished)) - } -} - -impl FlowLayouter<'_> { - /// Processes all footnotes in the frame. - #[tracing::instrument(skip_all)] - fn handle_footnotes( - &mut self, - vt: &mut Vt, - notes: &mut Vec, - movable: bool, - force: bool, - ) -> SourceResult { - 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 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(); - } - - 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()?; - 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, 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.0.location() == content.location()) => - { - let Some(footnote) = content.to::() else { continue }; - notes.push(footnote.clone()); - } - _ => {} - } - } -} diff --git a/library/src/layout/fragment.rs b/library/src/layout/fragment.rs deleted file mode 100644 index 3550df2a..00000000 --- a/library/src/layout/fragment.rs +++ /dev/null @@ -1,87 +0,0 @@ -use crate::prelude::*; - -/// A partial layout result. -#[derive(Clone)] -pub struct Fragment(Vec); - -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) -> 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 { - self.0 - } - - /// Iterate over the contained frames. - pub fn iter(&self) -> std::slice::Iter { - self.0.iter() - } - - /// Iterate over the contained frames. - pub fn iter_mut(&mut self) -> std::slice::IterMut { - 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; - - 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/library/src/layout/grid.rs b/library/src/layout/grid.rs deleted file mode 100644 index 4f5175e9..00000000 --- a/library/src/layout/grid.rs +++ /dev/null @@ -1,708 +0,0 @@ -use crate::prelude::*; -use crate::text::TextElem; - -use super::Sizing; - -/// 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)}`. -/// -/// ## Example { #example } -/// ```example -/// #set text(10pt, style: "italic") -/// #let cell = rect.with( -/// inset: 8pt, -/// fill: rgb("e4e5ea"), -/// width: 100%, -/// radius: 6pt -/// ) -/// #grid( -/// columns: (60pt, 1fr, 60pt), -/// rows: (60pt, auto), -/// gutter: 3pt, -/// cell(height: 100%)[Easy to learn], -/// cell(height: 100%)[Great output], -/// cell(height: 100%)[Intuitive], -/// cell[Our best Typst yet], -/// cell[ -/// Responsive design in print -/// for everyone -/// ], -/// cell[One more thing...], -/// ) -/// ``` -/// -/// Display: Grid -/// Category: layout -#[element(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. - 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. - 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()) - )] - pub column_gutter: TrackSizings, - - /// The gaps between rows. Takes precedence over `gutter`. - #[parse(args.named("row-gutter")?.or_else(|| gutter.clone()))] - pub row_gutter: TrackSizings, - - /// The contents of the grid cells. - /// - /// The cells are populated in row-major order. - #[variadic] - pub children: Vec, -} - -impl Layout for GridElem { - #[tracing::instrument(name = "GridElem::layout", skip_all)] - fn layout( - &self, - vt: &mut Vt, - styles: StyleChain, - regions: Regions, - ) -> SourceResult { - // Prepare grid layout by unifying content and gutter tracks. - let cells = self.children(); - let layouter = GridLayouter::new( - Axes::new(&self.columns(styles).0, &self.rows(styles).0), - Axes::new(&self.column_gutter(styles).0, &self.row_gutter(styles).0), - &cells, - regions, - styles, - ); - - // 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 Vec); - -cast! { - TrackSizings, - self => self.0.into_value(), - sizing: Sizing => Self(vec![sizing]), - count: NonZeroUsize => Self(vec![Sizing::Auto; count.get()]), - values: Array => Self(values.into_iter().map(Value::cast).collect::>()?), -} - -/// 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, - /// The row tracks including gutter tracks. - rows: Vec, - /// The regions to layout children into. - regions: Regions<'a>, - /// The inherited styles. - styles: StyleChain<'a>, - /// Resolved column sizes. - rcols: Vec, - /// The sum of `rcols`. - width: Abs, - /// Resolve row sizes, by region. - rrows: Vec>, - /// Rows in the current region. - lrows: Vec, - /// The initial size of the current region before we started subtracting. - initial: Size, - /// Frames for finished regions. - finished: Vec, -} - -/// 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, - /// The heights of the resulting rows segments, by region. - pub rows: Vec>, -} - -/// 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>, - ) -> 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![], - } - } - - /// Determines the columns sizes and then layouts the grid row-by-row. - pub fn layout(mut self, vt: &mut Vt) -> SourceResult { - 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>> { - let mut resolved: Vec = 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, - 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 { - let mut output = Frame::new(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 { - // Prepare frames. - let mut outputs: Vec<_> = heights - .iter() - .map(|&h| Frame::new(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::new(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/library/src/layout/hide.rs b/library/src/layout/hide.rs deleted file mode 100644 index c6e83e0c..00000000 --- a/library/src/layout/hide.rs +++ /dev/null @@ -1,30 +0,0 @@ -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 } -/// ```example -/// Hello Jane \ -/// #hide[Hello] Joe -/// ``` -/// -/// Display: Hide -/// Category: layout -#[element(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 { - Ok(self.body().styled(MetaElem::set_data(vec![Meta::Hide]))) - } -} diff --git a/library/src/layout/list.rs b/library/src/layout/list.rs deleted file mode 100644 index e39ec3f5..00000000 --- a/library/src/layout/list.rs +++ /dev/null @@ -1,239 +0,0 @@ -use crate::layout::{BlockElem, ParElem, Sizing, Spacing}; -use crate::prelude::*; -use crate::text::TextElem; - -use super::GridLayouter; - -/// A bullet list. -/// -/// Displays a sequence of items vertically, with each item introduced by a -/// marker. -/// -/// ## Example { #example } -/// ```example -/// - *Content* -/// - Text -/// - Math -/// - Layout -/// - Visualize -/// - Meta -/// - Symbols -/// -/// - *Compute* -/// #list( -/// [Foundations], -/// [Calculate], -/// [Construct], -/// [Data Loading], -/// ) -/// ``` -/// -/// ## Syntax { #syntax } -/// This functions also has dedicated syntax: Start a line with a hyphen, -/// followed by a space to create a list item. A list item can contain multiple -/// paragraphs and other block-level content. All content that is indented -/// more than an item's hyphen becomes part of that item. -/// -/// Display: Bullet List -/// Category: layout -#[element(Layout)] -#[scope( - scope.define("item", ListItem::func()); - scope -)] -pub struct ListElem { - /// If this is `{false}`, the items are spaced apart with [list - /// spacing]($func/list.spacing). If it is `{true}`, they use normal - /// [leading]($func/par.leading) instead. This makes the list more compact, - /// which can look better if the items are short. - /// - /// 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 - /// ``` - #[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]($func/block.below). - pub spacing: Smart, - - /// 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, - - /// The nesting depth. - #[internal] - #[fold] - depth: Depth, -} - -impl Layout for ListElem { - #[tracing::instrument(name = "ListElem::layout", skip_all)] - fn layout( - &self, - vt: &mut Vt, - styles: StyleChain, - regions: Regions, - ) -> SourceResult { - 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(Align::LEFT_TOP.into()); - - 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().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, - ); - - Ok(layouter.layout(vt)?.fragment) - } -} - -/// A bullet list item. -/// -/// Display: Bullet List Item -/// Category: layout -#[element] -pub struct ListItem { - /// The item's body. - #[required] - pub body: Content, -} - -cast! { - ListItem, - v: Content => v.to::().cloned().unwrap_or_else(|| Self::new(v.clone())), -} - -/// A list's marker. -#[derive(Debug, Clone, Hash)] -pub enum ListMarker { - Content(Vec), - Func(Func), -} - -impl ListMarker { - /// Resolve the marker for the given depth. - fn resolve(&self, vt: &mut Vt, depth: usize) -> SourceResult { - 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), -} - -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/library/src/layout/measure.rs b/library/src/layout/measure.rs deleted file mode 100644 index eb8e509e..00000000 --- a/library/src/layout/measure.rs +++ /dev/null @@ -1,56 +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 combined -/// `measure` and [`layout`]($func/layout). -/// -/// # Example { #example } -/// The same content can have a different size depending on the styles that -/// are active when it is layouted. For example, in the example below -/// `[#content]` is of course bigger when we increase the font size. -/// -/// ```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`]($func/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`]($type/length). -/// -/// Display: Measure -/// Category: layout -#[func] -pub fn measure( - /// The content whose size to measure. - content: Content, - /// The styles with which to layout the content. - styles: Styles, - /// The virtual machine. - vm: &mut Vm, -) -> SourceResult { - 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/library/src/layout/mod.rs b/library/src/layout/mod.rs deleted file mode 100644 index 41490eb8..00000000 --- a/library/src/layout/mod.rs +++ /dev/null @@ -1,709 +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; -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::mem; - -use typed_arena::Arena; -use typst::diag::SourceResult; -use typst::eval::Tracer; -use typst::model::DelayedErrors; -use typst::model::{applicable, realize, StyleVecBuilder}; - -use crate::math::{EquationElem, LayoutMath}; -use crate::meta::DocumentElem; -use crate::prelude::*; -use crate::shared::BehavedBuilder; -use crate::text::{LinebreakElem, SmartQuoteElem, SpaceElem, TextElem}; -use crate::visualize::{ - CircleElem, EllipseElem, ImageElem, LineElem, PathElem, PolygonElem, RectElem, - SquareElem, -}; - -/// Hook up all layout definitions. -pub(super) fn define(global: &mut Scope) { - global.define("page", PageElem::func()); - global.define("pagebreak", PagebreakElem::func()); - global.define("v", VElem::func()); - global.define("par", ParElem::func()); - global.define("parbreak", ParbreakElem::func()); - global.define("h", HElem::func()); - global.define("box", BoxElem::func()); - global.define("block", BlockElem::func()); - global.define("list", ListElem::func()); - global.define("enum", EnumElem::func()); - global.define("terms", TermsElem::func()); - global.define("table", TableElem::func()); - global.define("stack", StackElem::func()); - global.define("grid", GridElem::func()); - global.define("columns", ColumnsElem::func()); - global.define("colbreak", ColbreakElem::func()); - global.define("place", PlaceElem::func()); - global.define("align", AlignElem::func()); - global.define("pad", PadElem::func()); - global.define("repeat", RepeatElem::func()); - global.define("move", MoveElem::func()); - global.define("scale", ScaleElem::func()); - global.define("rotate", RotateElem::func()); - global.define("hide", HideElem::func()); - global.define("measure", measure_func()); - global.define("ltr", Dir::LTR); - global.define("rtl", Dir::RTL); - global.define("ttb", Dir::TTB); - global.define("btt", Dir::BTT); - global.define("start", GenAlign::Start); - global.define("end", GenAlign::End); - global.define("left", GenAlign::Specific(Align::Left)); - global.define("center", GenAlign::Specific(Align::Center)); - global.define("right", GenAlign::Specific(Align::Right)); - global.define("top", GenAlign::Specific(Align::Top)); - global.define("horizon", GenAlign::Specific(Align::Horizon)); - global.define("bottom", GenAlign::Specific(Align::Bottom)); -} - -/// Root-level layout. -pub trait LayoutRoot { - /// Layout into one frame per page. - fn layout_root(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult; -} - -impl LayoutRoot for Content { - #[tracing::instrument(name = "Content::layout_root", skip_all)] - fn layout_root(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult { - #[comemo::memoize] - fn cached( - content: &Content, - world: Tracked, - introspector: Tracked, - locator: Tracked, - delayed: TrackedMut, - tracer: TrackedMut, - styles: StyleChain, - ) -> SourceResult { - 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::() - .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; - - /// 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 { - 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 { - #[allow(clippy::too_many_arguments)] - #[comemo::memoize] - fn cached( - content: &Content, - world: Tracked, - introspector: Tracked, - locator: Tracked, - delayed: TrackedMut, - tracer: TrackedMut, - styles: StyleChain, - regions: Regions, - ) -> SourceResult { - 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::() - .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<(Content, StyleChain<'a>)> { - if content.can::() && !applicable(content, styles) { - return Ok((content.clone(), styles)); - } - - let mut builder = Builder::new(vt, scratch, true); - builder.accept(content, styles)?; - builder.interrupt_page(Some(styles))?; - let (pages, shared) = builder.doc.unwrap().pages.finish(); - Ok((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<(Content, StyleChain<'a>)> { - if content.can::() - && !content.is::() - && !content.is::() - && !content.is::() - && !content.is::() - && !content.is::() - && !content.is::() - && !content.is::() - && !content.is::() - && !applicable(content, styles) - { - return Ok((content.clone(), styles)); - } - - let mut builder = Builder::new(vt, scratch, false); - builder.accept(content, styles)?; - builder.interrupt_par()?; - let (children, shared) = builder.flow.0.finish(); - Ok((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>, - /// The current flow building state. - flow: FlowBuilder<'a>, - /// The current paragraph building state. - par: ParBuilder<'a>, - /// The current list building state. - list: ListBuilder<'a>, -} - -/// Temporary storage arenas for building. -#[derive(Default)] -struct Scratch<'a> { - /// An arena where intermediate style chains are stored. - styles: Arena>, - /// An arena where intermediate content resulting from show rules is stored. - content: Arena, -} - -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(), - } - } - - fn accept( - &mut self, - mut content: &'a Content, - styles: StyleChain<'a>, - ) -> SourceResult<()> { - if content.can::() && !content.is::() { - 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.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::() - .map_or(false, |pagebreak| !pagebreak.weak(styles)); - - self.interrupt_page(keep.then_some(styles))?; - - if let Some(doc) = &mut self.doc { - if doc.accept(content, styles) { - return Ok(()); - } - } - - if content.is::() { - 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>, - ) -> SourceResult<()> { - if let Some(Some(span)) = local.interruption::() { - 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::() { - if self.doc.is_none() { - bail!(span, "page configuration is not allowed inside of containers"); - } - self.interrupt_page(outer)?; - } else if local.interruption::().is_some() - || local.interruption::().is_some() - { - self.interrupt_par()?; - } else if local.interruption::().is_some() - || local.interruption::().is_some() - || local.interruption::().is_some() - { - self.interrupt_list()?; - } - Ok(()) - } - - fn interrupt_list(&mut self) -> SourceResult<()> { - 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>) -> SourceResult<()> { - self.interrupt_par()?; - let Some(doc) = &mut self.doc else { return Ok(()) }; - if !self.flow.0.is_empty() || (doc.keep_next && styles.is_some()) { - 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, 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, -} - -impl<'a> DocBuilder<'a> { - fn accept(&mut self, content: &Content, styles: StyleChain<'a>) -> bool { - if let Some(pagebreak) = content.to::() { - self.keep_next = !pagebreak.weak(styles); - self.clear_next = pagebreak.to(styles); - return true; - } - - if let Some(page) = content.to::() { - let elem = if let Some(clear_to) = self.clear_next.take() { - let mut page = page.clone(); - page.push_clear_to(Some(clear_to)); - page.pack() - } else { - content.clone() - }; - - 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::() { - self.1 = true; - return true; - } - - let last_was_parbreak = self.1; - self.1 = false; - - if content.is::() - || content.is::() - || content.is::() - { - self.0.push(content.clone(), styles); - return true; - } - - if content.can::() || content.is::() { - let is_tight_list = if let Some(elem) = content.to::() { - elem.tight(styles) - } else if let Some(elem) = content.to::() { - elem.tight(styles) - } else if let Some(elem) = content.to::() { - 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(spacing.pack(), styles); - } - - let (above, below) = if let Some(block) = content.to::() { - (block.above(styles), block.below(styles)) - } else { - (BlockElem::above_in(styles), BlockElem::below_in(styles)) - }; - - self.0.push(above.pack(), styles); - self.0.push(content.clone(), styles); - self.0.push(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::() { - if !self.0.is_basically_empty() { - self.0.push(content.clone(), styles); - return true; - } - } else if content.is::() - || content.is::() - || content.is::() - || content.is::() - || content.is::() - || content.to::().map_or(false, |elem| !elem.block(styles)) - || content.is::() - { - self.0.push(content.clone(), styles); - return true; - } - - false - } - - fn finish(self) -> (Content, StyleChain<'a>) { - let (children, shared) = self.0.finish(); - (ParElem::new(children.to_vec()).pack(), shared) - } -} - -/// Accepts list / enum items, spaces, paragraph breaks. -struct ListBuilder<'a> { - /// The list items collected so far. - items: StyleVecBuilder<'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::() || content.is::()) - { - self.staged.push((content, styles)); - return true; - } - - if (content.is::() - || content.is::() - || content.is::()) - && self - .items - .elems() - .next() - .map_or(true, |first| first.func() == content.func()) - { - self.items.push(content.clone(), styles); - self.tight &= self.staged.drain(..).all(|(t, _)| !t.is::()); - 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::() { - ListElem::new( - items - .iter() - .map(|(item, local)| { - let item = item.to::().unwrap(); - item.clone().with_body(item.body().styled_with_map(local.clone())) - }) - .collect::>(), - ) - .with_tight(self.tight) - .pack() - } else if item.is::() { - EnumElem::new( - items - .iter() - .map(|(item, local)| { - let item = item.to::().unwrap(); - item.clone().with_body(item.body().styled_with_map(local.clone())) - }) - .collect::>(), - ) - .with_tight(self.tight) - .pack() - } else if item.is::() { - TermsElem::new( - items - .iter() - .map(|(item, local)| { - let item = item.to::().unwrap(); - item.clone() - .with_term(item.term().styled_with_map(local.clone())) - .with_description( - item.description().styled_with_map(local.clone()), - ) - }) - .collect::>(), - ) - .with_tight(self.tight) - .pack() - } else { - unreachable!() - }; - (output, shared) - } -} - -impl Default for ListBuilder<'_> { - fn default() -> Self { - Self { - items: StyleVecBuilder::default(), - tight: true, - staged: vec![], - } - } -} diff --git a/library/src/layout/pad.rs b/library/src/layout/pad.rs deleted file mode 100644 index a3d5646b..00000000 --- a/library/src/layout/pad.rs +++ /dev/null @@ -1,125 +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 } -/// ```example -/// #set align(center) -/// -/// #pad(x: 16pt, image("typing.jpg")) -/// _Typing speeds can be -/// measured in words per minute._ -/// ``` -/// -/// Display: Padding -/// Category: layout -#[element(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, - - /// The padding at the top side. - #[parse(args.named("top")?.or(y))] - pub top: Rel, - - /// The padding at the right side. - #[parse(args.named("right")?.or(x))] - pub right: Rel, - - /// The padding at the bottom side. - #[parse(args.named("bottom")?.or(y))] - pub bottom: Rel, - - /// The horizontal padding. Both `left` and `right` take precedence over - /// this. - #[external] - pub x: Rel, - - /// The vertical padding. Both `top` and `bottom` take precedence over this. - #[external] - pub y: Rel, - - /// The padding for all sides. All other parameters take precedence over - /// this. - #[external] - pub rest: Rel, - - /// 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 { - 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>) -> 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>) -> Size { - size.zip(padding.sum_by_axis()) - .map(|(s, p)| (s + p.abs).safe_div(1.0 - p.rel.get())) -} diff --git a/library/src/layout/page.rs b/library/src/layout/page.rs deleted file mode 100644 index 3b81f92c..00000000 --- a/library/src/layout/page.rs +++ /dev/null @@ -1,898 +0,0 @@ -use std::ptr; -use std::str::FromStr; - -use super::{AlignElem, ColumnsElem}; -use crate::meta::{Counter, CounterKey, 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. -/// -/// ## Example { #example } -/// ```example -/// >>> #set page(margin: auto) -/// #set page("us-letter") -/// -/// There you go, US friends! -/// ``` -/// -/// Display: Page -/// Category: layout -#[element] -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")?; - args.named("width")? - .or_else(|| paper.map(|paper| Smart::Custom(paper.width().into()))) - )] - #[default(Smart::Custom(Paper::A4.width().into()))] - pub width: Smart, - - /// The height of the page. - /// - /// If this is set to `{auto}`, page breaks can only be triggered manually - /// by inserting a [page break]($func/pagebreak). Most examples throughout - /// this documentation use `{auto}` for the height of the page to - /// dynamically grow and shrink to fit their content. - #[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, - - /// 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. - /// - /// - A single length: The same margin on all sides. - /// - `{auto}`: The margin is set to the default value for the page's size. - /// - 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]($func/page.binding) is). - /// - `outside`: The margin at the outer side of the page (opposite to the - /// [binding]($func/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]($func/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, - - /// How many columns the page has. - /// - /// ```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.* - /// ``` - pub fill: Option, - - /// How to [number]($func/numbering) the pages. - /// - /// If an explicit `footer` is given, the numbering is ignored. - /// - /// ```example - /// #set page( - /// height: 100pt, - /// margin: (top: 16pt, bottom: 24pt), - /// numbering: "1 / 1", - /// ) - /// - /// #lorem(48) - /// ``` - pub numbering: Option, - - /// The alignment of the page numbering. - /// - /// ```example - /// #set page( - /// margin: (top: 16pt, bottom: 24pt), - /// numbering: "1", - /// number-align: right, - /// ) - /// - /// #lorem(30) - /// ``` - #[default(Align::Center.into())] - pub number_align: Axes>, - - /// 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) - /// ``` - pub header: Option, - - /// The amount the header is raised into the top margin. - #[resolve] - #[default(Ratio::new(0.3).into())] - pub header_ascent: Rel, - - /// 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]($func/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) - /// ``` - pub footer: Option, - - /// The amount the footer is lowered into the bottom margin. - #[resolve] - #[default(Ratio::new(0.3).into())] - pub footer_descent: Rel, - - /// 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). - /// ``` - pub background: Option, - - /// 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... - /// ``` - pub foreground: Option, - - /// 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. - /// Not part of the public API for now. - #[internal] - pub clear_to: Option, -} - -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, - mut number: NonZeroUsize, - ) -> SourceResult { - 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::::from(0.1190 * 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(); - 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. - if self.clear_to(styles).is_some_and(|p| !p.matches(number.get())) { - let size = area.map(Abs::is_finite).select(area, Size::zero()); - frames.insert(0, Frame::new(size)); - } - - let fill = self.fill(styles); - let foreground = self.foreground(styles); - let background = self.background(styles); - let header = self.header(styles); - let header_ascent = self.header_ascent(styles); - let footer = self.footer(styles).or_else(|| { - self.numbering(styles).map(|numbering| { - let both = match &numbering { - Numbering::Pattern(pattern) => pattern.pieces() >= 2, - Numbering::Func(_) => true, - }; - Counter::new(CounterKey::Page) - .display(Some(numbering), both) - .aligned(self.number_align(styles)) - }) - }); - let footer_descent = self.footer_descent(styles); - - let numbering_meta = FrameItem::Meta( - Meta::PageNumbering(self.numbering(styles).into_value()), - Size::zero(), - ); - - // Post-process pages. - for frame in frames.iter_mut() { - tracing::info!("Layouting page #{number}"); - - // 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(number) { - 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(Point::zero(), 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.into(); - } else if ptr::eq(marginal, &footer) { - let descent = footer_descent.relative_to(margin.bottom); - pos = Point::new(margin.left, size.y - margin.bottom + descent); - area = Size::new(pw, margin.bottom - descent); - align = Align::Top.into(); - } else { - pos = Point::zero(); - area = size; - align = Align::CENTER_HORIZON.into(); - }; - - 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()); - } - - number = number.saturating_add(1); - } - - 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>>>, - /// Whether to swap `left` and `right` to make them `inside` and `outside` - /// (when to swap depends on the binding). - pub two_sided: Option, -} - -impl Margin { - /// Create an instance with four equal components. - pub fn splat(value: Option>>) -> 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 => 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 => GenAlign::Specific(Align::Left).into_value(), - Self::Right => GenAlign::Specific(Align::Right).into_value(), - }, - v: GenAlign => match v { - GenAlign::Specific(Align::Left) => Self::Left, - GenAlign::Specific(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 { - Ok(match self { - Self::Content(content) => content.clone(), - Self::Func(func) => 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 } -/// ```example -/// The next page contains -/// more details on compound theory. -/// #pagebreak() -/// -/// == Compound Theory -/// In 1984, the first ... -/// ``` -/// -/// Display: Page Break -/// Category: layout -#[element] -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, -} - -/// 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.0) - } - - /// The height of the paper. - pub fn height(self) -> Abs { - Abs::mm(self.height.0) - } -} - -/// 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($width), - height: Scalar($height), - };)* - } - - impl FromStr for Paper { - type Err = &'static str; - - fn from_str(name: &str) -> Result { - 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/library/src/layout/par.rs b/library/src/layout/par.rs deleted file mode 100644 index 6b914e80..00000000 --- a/library/src/layout/par.rs +++ /dev/null @@ -1,1566 +0,0 @@ -use icu_properties::{maps::CodePointMapData, LineBreak}; -use icu_provider::AsDeserializingBufferProvider; -use icu_provider_adapters::fork::ForkByKeyProvider; -use icu_provider_blob::BlobDataProvider; -use icu_segmenter::{LineBreakIteratorUtf8, LineSegmenter}; -use once_cell::sync::Lazy; -use typst::eval::Tracer; -use typst::model::DelayedErrors; -use unicode_bidi::{BidiInfo, Level as BidiLevel}; -use unicode_script::{Script, UnicodeScript}; - -use super::{BoxElem, HElem, Sizing, Spacing}; -use crate::layout::AlignElem; -use crate::math::EquationElem; -use crate::prelude::*; -use crate::text::{ - is_gb_style, shape, LinebreakElem, Quoter, Quotes, ShapedText, SmartQuoteElem, - SpaceElem, TextElem, -}; - -/// 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 } -/// ```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 ... -/// ``` -/// -/// Display: Paragraph -/// Category: layout -#[element(Construct)] -pub struct ParElem { - /// The spacing between lines. - #[resolve] - #[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 - /// property hyphenate]($func/text.hyphenate) is set to `{auto}` and the - /// current language is known. - /// - /// Note that the current [alignment]($func/align) still has an effect on - /// the placement of the last line except if it ends with a [justified line - /// break]($func/linebreak.justify). - #[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: 190pt) - /// #set par(linebreaks: "simple") - /// Some texts are frustratingly - /// challenging to break in a - /// visually pleasing way. This - /// very aesthetic example is one - /// of them. - /// - /// #set par(linebreaks: "optimized") - /// Some texts are frustratingly - /// challenging to break in a - /// visually pleasing way. This - /// very aesthetic example is one - /// of them. - /// ``` - #[default] - pub linebreaks: Smart, - - /// 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]($func/block.spacing) to the [`leading`] when - /// using this property (e.g. using - /// `[#show par: set block(spacing: 0.65em)]`). - pub first_line_indent: Length, - - /// The indent all but the first line of a paragraph should have. - #[resolve] - pub hanging_indent: Length, - - /// The contents of the paragraph. - #[external] - #[required] - pub body: Content, - - /// The paragraph's children. - #[internal] - #[variadic] - pub children: Vec, -} - -impl Construct for ParElem { - fn construct(_: &mut Vm, args: &mut Args) -> SourceResult { - // 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(args)?; - let body = args.expect::("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 { - #[comemo::memoize] - #[allow(clippy::too_many_arguments)] - fn cached( - par: &ParElem, - world: Tracked, - introspector: Tracked, - locator: Tracked, - delayed: TrackedMut, - tracer: TrackedMut, - styles: StyleChain, - consecutive: bool, - region: Size, - expand: bool, - ) -> SourceResult { - 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 } -/// ```example -/// #for i in range(3) { -/// [Blind text #i: ] -/// lorem(5) -/// parbreak() -/// } -/// ``` -/// -/// ## Syntax { #syntax } -/// Instead of calling this function, you can insert a blank line into your -/// markup to create a paragraph break. -/// -/// Display: Paragraph Break -/// Category: layout -#[element(Unlabellable)] -pub struct ParbreakElem {} - -impl Unlabellable for ParbreakElem {} - -/// Range of a substring of text. -type Range = std::ops::Range; - -// 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. -struct Preparation<'a> { - /// Bidirectional text embedding levels for the paragraph. - bidi: BidiInfo<'a>, - /// Text runs, spacing and layouted elements. - items: Vec>, - /// The span mapper. - spans: SpanMapper, - /// The styles shared by all children. - styles: StyleChain<'a>, - /// Whether to hyphenate if it's the same for all children. - hyphenate: Option, - /// The text language if it's the same for all children. - lang: Option, - /// The paragraph's resolved alignment. - align: Align, - /// Whether to justify the paragraph. - justify: bool, - /// The paragraph's hanging indent. - hang: Abs, -} - -impl<'a> Preparation<'a> { - /// Find the item that contains the given `text_offset`. - 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. - 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)] -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. - fn text(&self) -> Option<&ShapedText<'a>> { - match self { - Self::Text(shaped) => Some(shaped), - _ => None, - } - } - - fn text_mut(&mut self) -> Option<&mut ShapedText<'a>> { - match self { - Self::Text(shaped) => Some(shaped), - _ => None, - } - } - - /// The text length of the item. - 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. - 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>, - /// 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>, - /// 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> { - 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> { - 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 [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![]; - let mut spans = SpanMapper::new(); - let mut iter = children.iter().peekable(); - - let first_line_indent = ParElem::first_line_indent_in(*styles); - if !first_line_indent.is_zero() - && consecutive - && AlignElem::alignment_in(*styles).x.resolve(*styles) - == 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::() { - full.push(' '); - Segment::Text(1) - } else if let Some(elem) = child.to::() { - 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::() { - if elem.amount().is_zero() { - continue; - } - - full.push(SPACING_REPLACE); - Segment::Spacing(elem.amount()) - } else if let Some(elem) = child.to::() { - 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::() { - let prev = full.len(); - if SmartQuoteElem::enabled_in(styles) { - let lang = TextElem::lang_in(styles); - let region = TextElem::region_in(styles); - let quotes = Quotes::from_lang(lang, region); - 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::() { - elem.text().chars().next() - } else if child.is::() { - Some('"') - } else if child.is::() - || child.is::() - || child.is::() - { - Some(SPACING_REPLACE) - } else { - Some(OBJ_REPLACE) - } - }); - - full.push_str(quoter.quote("es, elem.double(styles), peeked)); - } else { - full.push(if elem.double(styles) { '"' } else { '\'' }); - } - Segment::Text(full.len() - prev) - } else if let Some(elem) = child.to::() { - full.push(OBJ_REPLACE); - Segment::Equation(elem) - } else if let Some(elem) = child.to::() { - let frac = elem.width(styles).is_fractional(); - full.push(if frac { SPACING_REPLACE } else { OBJ_REPLACE }); - Segment::Box(elem, frac) - } else if child.is::() { - Segment::Meta - } else { - bail!(child.span(), "unexpected paragraph child"); - }; - - if let Some(last) = full.chars().last() { - quoter.last(last); - } - - 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 [Content], - text: &'a str, - segments: Vec<(Segment<'a>, StyleChain<'a>)>, - spans: SpanMapper, - styles: StyleChain<'a>, - region: Size, -) -> SourceResult> { - let bidi = BidiInfo::new( - text, - match TextElem::dir_in(styles) { - Dir::LTR => Some(BidiLevel::ltr()), - Dir::RTL => Some(BidiLevel::rtl()), - _ => None, - }, - ); - - let mut cursor = 0; - let mut items = vec![]; - - // 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::new(Size::zero()); - frame.meta(styles, true); - items.push(Item::Meta(frame)); - } - } - - cursor = end; - } - - Ok(Preparation { - bidi, - items, - spans, - styles, - hyphenate: shared_get(styles, children, TextElem::hyphenate_in), - lang: shared_get(styles, children, TextElem::lang_in), - align: AlignElem::alignment_in(styles).x.resolve(styles), - justify: ParElem::justify_in(styles), - hang: ParElem::hanging_indent_in(styles), - }) -} - -/// 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>, - vt: &Vt, - bidi: &BidiInfo<'a>, - range: Range, - spans: &SpanMapper, - styles: StyleChain<'a>, -) { - 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. - for i in range.clone() { - if !bidi.text.is_char_boundary(i) { - continue; - } - - let level = bidi.levels[i]; - let script = - bidi.text[i..].chars().next().map_or(Script::Unknown, |c| c.script()); - - if level != prev_level || !is_compatible(script, prev_script) { - if cursor < i { - process(cursor..i, prev_level); - } - cursor = i; - prev_level = level; - prev_script = script; - } else if is_generic_script(prev_script) { - prev_script = 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( - styles: StyleChain<'_>, - children: &[Content], - getter: fn(StyleChain) -> T, -) -> Option { - 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> { - let linebreaks = ParElem::linebreaks_in(p.styles).unwrap_or_else(|| { - if ParElem::justify_in(p.styles) { - 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> { - let mut lines = vec![]; - let mut start = 0; - let mut last = None; - - for (end, mandatory, hyphen) in breakpoints(p) { - // Compute the line and its size. - let mut attempt = line(vt, p, start..end, mandatory, hyphen); - - // 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, mandatory, hyphen); - } - } - - // 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 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> { - /// 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 CONSECUTIVE_DASH_COST: Cost = 300.0; - 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, false, false), - }]; - - let em = TextElem::size_in(p.styles); - - for (end, mandatory, hyphen) in breakpoints(p) { - let k = table.len(); - let eof = end == p.bidi.text.len(); - let mut best: Option = None; - - // Find the optimal predecessor. - for (i, pred) in table.iter_mut().enumerate().skip(active) { - // Layout the line. - let start = pred.line.end; - let attempt = line(vt, p, start..end, mandatory, hyphen); - - // 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. - let extra_stretch = (delta - adjust) / attempt.justifiables() 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 - // Since any longer line will also be overfull, we can deactivate - // this breakpoint. - active = i + 1; - MAX_COST - } else if 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 hyphens. - if hyphen { - cost += HYPH_COST; - } - - // In Knuth paper, cost = (1 + 100|r|^3 + p)^2 + a, - // where r is the ratio, p=50 is penaty, and a=3000 is consecutive penaty. - // We divide the whole formula by 10, resulting (0.01 + |r|^3 + p)^2 + a, - // where p=0.5 and a=300 - 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 lines = vec![]; - 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 -} - -/// Generated by the following command: -/// -/// ```sh -/// icu4x-datagen --locales full --keys-for-bin target/debug/typst \ -/// --format blob --out library/assets/icudata.postcard --overwrite -/// ``` -/// -/// Install icu4x-datagen with `cargo install icu4x-datagen`. -static ICU_DATA: &[u8] = include_bytes!("../../assets/icudata.postcard"); - -/// Generated by the following command: -/// -/// ```sh -/// icu4x-datagen --locales zh ja --keys segmenter/line@1 --format blob \ -/// --out library/assets/cj_linebreak_data.postcard --overwrite -/// ``` -/// -/// The used icu4x-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 = Lazy::new(|| { - let provider = BlobDataProvider::try_new_from_static_blob(ICU_DATA).unwrap(); - LineSegmenter::try_new_lstm_with_buffer_provider(&provider).unwrap() -}); - -/// The Unicode line break properties for each code point. -static CJ_SEGMENTER: Lazy = 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 line break segmenter for Chinese/Jpanese text. -static LINEBREAK_DATA: Lazy> = 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() -}); - -/// Determine all possible points in the text where lines can broken. -/// -/// Returns 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). -fn breakpoints<'a>(p: &'a Preparation<'a>) -> Breakpoints<'a> { - let mut linebreaks = if matches!(p.lang, Some(Lang::CHINESE | Lang::JAPANESE)) { - CJ_SEGMENTER.segment_str(p.bidi.text) - } else { - SEGMENTER.segment_str(p.bidi.text) - }; - // The iterator always yields a breakpoint at index 0, we want to ignore it - linebreaks.next(); - Breakpoints { - p, - linebreaks, - syllables: None, - offset: 0, - suffix: 0, - end: 0, - mandatory: false, - } -} - -/// An iterator over the line break opportunities in a text. -struct Breakpoints<'a> { - /// The paragraph's items. - p: &'a Preparation<'a>, - /// The inner iterator over the unicode line break opportunities. - linebreaks: LineBreakIteratorUtf8<'a, 'a>, - /// Iterator over syllables of the current word. - syllables: Option>, - /// The current text offset. - offset: usize, - /// The trimmed end of the current word. - suffix: usize, - /// The untrimmed end of the current word. - end: usize, - /// Whether the break after the current word is mandatory. - mandatory: bool, -} - -impl Iterator for Breakpoints<'_> { - type Item = (usize, bool, bool); - - fn next(&mut self) -> Option { - // If we're currently in a hyphenated "word", process the next syllable. - if let Some(syllable) = self.syllables.as_mut().and_then(Iterator::next) { - self.offset += syllable.len(); - if self.offset == self.suffix { - self.offset = self.end; - } - - // Filter out hyphenation opportunities where hyphenation was - // actually disabled. - let hyphen = self.offset < self.end; - if hyphen && !self.hyphenate(self.offset) { - return self.next(); - } - - return Some((self.offset, self.mandatory && !hyphen, hyphen)); - } - - let lb = LINEBREAK_DATA.as_borrowed(); - - // Get the next "word". - self.end = self.linebreaks.next()?; - self.mandatory = - self.p.bidi.text[..self.end].chars().next_back().map_or(false, |c| { - matches!( - lb.get(c), - LineBreak::MandatoryBreak - | LineBreak::CarriageReturn - | LineBreak::LineFeed - | LineBreak::NextLine - ) || self.end == self.p.bidi.text.len() - }); - - // Hyphenate the next word. - if self.p.hyphenate != Some(false) { - if let Some(lang) = self.lang(self.offset) { - let word = &self.p.bidi.text[self.offset..self.end]; - let trimmed = word.trim_end_matches(|c: char| !c.is_alphabetic()); - if !trimmed.is_empty() { - self.suffix = self.offset + trimmed.len(); - self.syllables = Some(hypher::hyphenate(trimmed, lang)); - return self.next(); - } - } - } - - self.offset = self.end; - Some((self.end, self.mandatory, false)) - } -} - -impl Breakpoints<'_> { - /// Whether hyphenation is enabled at the given offset. - fn hyphenate(&self, offset: usize) -> bool { - self.p - .hyphenate - .or_else(|| { - let shaped = self.p.find(offset)?.text()?; - Some(TextElem::hyphenate_in(shaped.styles)) - }) - .unwrap_or(false) - } - - /// The text language at the given offset. - fn lang(&self, offset: usize) -> Option { - let lang = self.p.lang.or_else(|| { - let shaped = self.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) - } -} - -/// Create a line which spans the given range. -fn line<'a>( - vt: &Vt, - p: &'a Preparation, - mut range: Range, - mandatory: bool, - hyphen: bool, -) -> Line<'a> { - let end = range.end; - let mut justify = p.justify && end < p.bidi.text.len() && !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}'); - 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 end_cjk_punct = trimmed - .ends_with(['”', '’', ',', '。', '、', ':', ';', '》', ')', '』', '」']); - - // 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 || end_cjk_punct { - 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); - } - let punct = reshaped.glyphs.last(); - if let Some(punct) = punct { - if punct.is_cjk_left_aligned_punctuation(gb_style) { - let shrink_amount = punct.shrinkability().1; - let punct = reshaped.glyphs.to_mut().last_mut().unwrap(); - punct.shrink_right(shrink_amount); - reshaped.width -= shrink_amount.at(reshaped.size); - } - } - width += reshaped.width; - last = Some(Item::Text(reshaped)); - } - - inner = before; - } - } - - // Deal with CJK punctuation at line starts. - let text = &p.bidi.text[range.start..end]; - let start_cjk_punct = text.starts_with(['“', '‘', '《', '(', '『', '「']); - - // 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 || start_cjk_punct { - if range.start < end || start_cjk_punct { - let reshaped = shaped.reshape(vt, &p.spans, range.start..end); - width += reshaped.width; - first = Some(Item::Text(reshaped)); - } - - inner = after; - } - } - - if start_cjk_punct { - let reshaped = first.as_mut().or(last.as_mut()).and_then(Item::text_mut); - if let Some(reshaped) = reshaped { - if let Some(punct) = reshaped.glyphs.first() { - if punct.is_cjk_right_aligned_punctuation() { - let shrink_amount = punct.shrinkability().0; - let punct = reshaped.glyphs.to_mut().first_mut().unwrap(); - punct.shrink_left(shrink_amount); - 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 { - // 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())) - { - 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 = lines - .iter() - .map(|line| commit(vt, p, line, width, region.y)) - .collect::>()?; - - // Prevent orphans. - let leading = ParElem::leading_in(p.styles); - if frames.len() >= 2 && !frames[1].is_empty() { - let second = frames.remove(1); - let first = &mut frames[0]; - merge(first, second, 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, 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 { - 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::new(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/library/src/layout/place.rs b/library/src/layout/place.rs deleted file mode 100644 index 6602948c..00000000 --- a/library/src/layout/place.rs +++ /dev/null @@ -1,103 +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 } -/// ```example -/// #set page(height: 60pt) -/// Hello, world! -/// -/// #place( -/// top + right, -/// square( -/// width: 20pt, -/// stroke: 2pt + blue -/// ), -/// ) -/// ``` -/// -/// Display: Place -/// Category: layout -#[element(Layout, Behave)] -pub struct PlaceElem { - /// Relative to which position in the parent container to place the content. - /// - /// 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(Axes::with_x(Some(GenAlign::Start)))] - pub alignment: Axes>, - - /// 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, - - /// The vertical displacement of the placed content. - pub dy: Rel, - - /// 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 { - let out_of_flow = self.out_of_flow(styles); - - // The pod is the base area of the region because for absolute - // placement we don't really care about the already used area. - let pod = { - let finite = regions.base().map(Abs::is_finite); - let expand = finite & (regions.expand | out_of_flow); - Regions::one(regions.base(), expand) - }; - - let child = self - .body() - .moved(Axes::new(self.dx(styles), self.dy(styles))) - .aligned(self.alignment(styles)); - - let mut frame = child.layout(vt, styles, pod)?.into_frame(); - - // If expansion is off, zero all sizes so that we don't take up any - // space in our parent. Otherwise, respect the expand settings. - let target = regions.expand.select(regions.size, Size::zero()); - frame.resize(target, Align::LEFT_TOP); - - Ok(Fragment::frame(frame)) - } -} - -impl PlaceElem { - /// Whether this element wants to be placed relative to its its parent's - /// base origin. Instead of relative to the parent's current flow/cursor - /// position. - pub fn out_of_flow(&self, styles: StyleChain) -> bool { - self.alignment(styles).y.is_some() - } -} - -impl Behave for PlaceElem { - fn behaviour(&self) -> Behaviour { - Behaviour::Ignorant - } -} diff --git a/library/src/layout/regions.rs b/library/src/layout/regions.rs deleted file mode 100644 index 6dd549b0..00000000 --- a/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, - /// Whether elements should expand to fill the regions instead of shrinking - /// to fit the content. - pub expand: Axes, - /// 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) -> 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) -> 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, 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 + '_ { - 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/library/src/layout/repeat.rs b/library/src/layout/repeat.rs deleted file mode 100644 index 646eb991..00000000 --- a/library/src/layout/repeat.rs +++ /dev/null @@ -1,79 +0,0 @@ -use crate::prelude::*; - -use super::AlignElem; - -/// 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 } -/// ```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 -/// ] -/// ``` -/// -/// Display: Repeat -/// Category: layout -#[element(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 { - let pod = Regions::one(regions.size, Axes::new(false, false)); - let piece = self.body().layout(vt, styles, pod)?.into_frame(); - let align = AlignElem::alignment_in(styles).x.resolve(styles); - - let 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::new(size); - if piece.has_baseline() { - frame.set_baseline(piece.baseline()); - } - - let mut offset = Abs::zero(); - if count == 1.0 { - offset += align.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/library/src/layout/spacing.rs b/library/src/layout/spacing.rs deleted file mode 100644 index e7dc24fb..00000000 --- a/library/src/layout/spacing.rs +++ /dev/null @@ -1,240 +0,0 @@ -use std::cmp::Ordering; - -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 } -/// ```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`. -/// -/// Display: Spacing (H) -/// Category: layout -#[element(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. - /// - /// ```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. - /// ``` - #[default(false)] - pub weak: bool, -} - -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::Ignorant - } - } - - fn larger(&self, prev: &Content) -> bool { - let Some(prev) = prev.to::() else { return false }; - self.amount() > prev.amount() - } -} - -/// 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 } -/// ```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], -/// ) -/// ``` -/// -/// Display: Spacing (V) -/// Category: layout -#[element(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::Ignorant - } - } - - fn larger(&self, prev: &Content) -> bool { - let Some(prev) = prev.to::() else { return false }; - self.amount() > prev.amount() - } -} - -cast! { - VElem, - v: Content => v.to::().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), - /// 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 for Spacing { - fn from(abs: Abs) -> Self { - Self::Rel(abs.into()) - } -} - -impl From for Spacing { - fn from(em: Em) -> Self { - Self::Rel(Rel::new(Ratio::zero(), em.into())) - } -} - -impl From for Spacing { - fn from(length: Length) -> Self { - Self::Rel(length.into()) - } -} - -impl From for Spacing { - fn from(fr: Fr) -> Self { - Self::Fr(fr) - } -} - -impl PartialOrd for Spacing { - fn partial_cmp(&self, other: &Self) -> Option { - match (self, other) { - (Self::Rel(a), Self::Rel(b)) => a.partial_cmp(b), - (Self::Fr(a), Self::Fr(b)) => a.partial_cmp(b), - _ => None, - } - } -} - -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 => Self::Rel(v), - v: Fr => Self::Fr(v), -} diff --git a/library/src/layout/stack.rs b/library/src/layout/stack.rs deleted file mode 100644 index 97305ddf..00000000 --- a/library/src/layout/stack.rs +++ /dev/null @@ -1,339 +0,0 @@ -use super::{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 } -/// ```example -/// #stack( -/// dir: ttb, -/// rect(width: 40pt), -/// rect(width: 120pt), -/// rect(width: 90pt), -/// ) -/// ``` -/// -/// Display: Stack -/// Category: layout -#[element(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. - #[default(Dir::TTB)] - pub dir: Dir, - - /// Spacing to insert between items where no explicit spacing was provided. - pub spacing: Option, - - /// The children to stack along the axis. - #[variadic] - pub children: Vec, -} - -impl Layout for StackElem { - #[tracing::instrument(name = "StackElem::layout", skip_all)] - fn layout( - &self, - vt: &mut Vt, - styles: StyleChain, - regions: Regions, - ) -> SourceResult { - 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(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, - /// 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, - /// 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, - /// Finished frames for previous regions. - finished: Vec, -} - -/// 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), -} - -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 aligns = if let Some(align) = block.to::() { - 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, aligns)); - - 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::new(size); - let mut cursor = Abs::zero(); - let mut ruler: Align = self.dir.start().into(); - - // Place all frames. - for item in self.items.drain(..) { - match item { - StackItem::Absolute(v) => cursor += v, - StackItem::Fractional(v) => cursor += v.share(self.fr, remaining), - StackItem::Frame(frame, aligns) => { - if self.dir.is_positive() { - ruler = ruler.max(aligns.get(self.axis)); - } else { - ruler = ruler.min(aligns.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 = aligns - .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 { - /// The main component. - pub cross: T, - /// The cross component. - pub main: T, -} - -impl Gen { - /// 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 { - match main { - Axis::X => Axes::new(self.main, self.cross), - Axis::Y => Axes::new(self.cross, self.main), - } - } -} - -impl Gen { - /// 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/library/src/layout/table.rs b/library/src/layout/table.rs deleted file mode 100644 index c2faedba..00000000 --- a/library/src/layout/table.rs +++ /dev/null @@ -1,334 +0,0 @@ -use typst::eval::{CastInfo, Reflect}; - -use crate::layout::{AlignElem, GridLayouter, TrackSizings}; -use crate::meta::{Figurable, LocalName}; -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]($func/grid) for more information on how to size the -/// table tracks. -/// -/// To give a table a caption and make it [referenceable]($func/ref), put it -/// into a [figure]($func/figure). -/// -/// ## Example { #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] -/// ) -/// ``` -/// -/// Display: Table -/// Category: layout -#[element(Layout, LocalName, Figurable)] -pub struct TableElem { - /// The column sizes. See the [grid documentation]($func/grid) for more - /// information on track sizing. - pub columns: TrackSizings, - - /// The row sizes. See the [grid documentation]($func/grid) for more - /// information on track sizing. - pub rows: TrackSizings, - - /// The gaps between rows & columns. See the [grid - /// documentation]($func/grid) for more information on gutters. - #[external] - pub gutter: TrackSizings, - - /// The gaps between columns. Takes precedence over `gutter`. See the [grid - /// documentation]($func/grid) for more information on gutters. - #[parse( - let gutter = args.named("gutter")?; - args.named("column-gutter")?.or_else(|| gutter.clone()) - )] - pub column_gutter: TrackSizings, - - /// The gaps between rows. Takes precedence over `gutter`. See the [grid - /// documentation]($func/grid) for more information on gutters. - #[parse(args.named("row-gutter")?.or_else(|| gutter.clone()))] - 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 €], - /// ) - /// ``` - pub fill: Celled>, - - /// 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], - /// ) - /// ``` - pub align: Celled>>>, - - /// How to stroke the cells. - /// - /// See the [line's documentation]($func/line.stroke) for more details. - /// Strokes can be disabled by setting this to `{none}`. - /// - /// _Note:_ Richer stroke customization for individual cells is not yet - /// implemented, but will be in the future. In the meantime, you can use - /// the third-party [tablex library](https://github.com/PgBiel/typst-tablex/). - #[resolve] - #[fold] - #[default(Some(PartialStroke::default()))] - pub stroke: Option, - - /// How much to pad the cells' content. - #[default(Abs::pt(5.0).into())] - pub inset: Rel, - - /// The contents of the table cells. - #[variadic] - pub children: Vec, -} - -impl Layout for TableElem { - #[tracing::instrument(name = "TableElem::layout", skip_all)] - fn layout( - &self, - vt: &mut Vt, - styles: StyleChain, - regions: Regions, - ) -> SourceResult { - let inset = self.inset(styles); - let align = self.align(styles); - - let tracks = Axes::new(self.columns(styles).0, self.rows(styles).0); - let gutter = Axes::new(self.column_gutter(styles).0, self.row_gutter(styles).0); - let cols = tracks.x.len().max(1); - let cells: Vec<_> = self - .children() - .into_iter() - .enumerate() - .map(|(i, child)| { - let mut child = child.padded(Sides::splat(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::>()?; - - let fill = self.fill(styles); - let stroke = self.stroke(styles).map(PartialStroke::unwrap_or_default); - - // Prepare grid layout by unifying content and gutter tracks. - let layouter = GridLayouter::new( - tracks.as_deref(), - gutter.as_deref(), - &cells, - regions, - styles, - ); - - // 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) -> impl Iterator { - let mut offset = Abs::zero(); - std::iter::once(Abs::zero()) - .chain(extents.into_iter()) - .map(move |extent| { - offset += extent; - offset - }) -} - -/// A value that can be configured per cell. -#[derive(Debug, Clone, PartialEq, Hash)] -pub enum Celled { - /// 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), -} - -impl Celled { - /// Resolve the value based on the cell position. - pub fn resolve(&self, vt: &mut Vt, x: usize, y: usize) -> SourceResult { - 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 Default for Celled { - fn default() -> Self { - Self::Value(T::default()) - } -} - -impl Reflect for Celled { - fn describe() -> CastInfo { - T::describe() + Array::describe() + Func::describe() - } - - fn castable(value: &Value) -> bool { - Array::castable(value) || Func::castable(value) || T::castable(value) - } -} - -impl IntoValue for Celled { - 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 FromValue for Celled { - fn from_value(value: Value) -> StrResult { - match value { - Value::Func(v) => Ok(Self::Func(v)), - Value::Array(array) => Ok(Self::Array( - array.into_iter().map(T::from_value).collect::>()?, - )), - v if T::castable(&v) => Ok(Self::Value(T::from_value(v)?)), - v => Err(Self::error(&v)), - } - } -} - -impl LocalName for TableElem { - fn local_name(&self, lang: Lang, _: Option) -> &'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::FRENCH => "Tableau", - Lang::GERMAN => "Tabelle", - Lang::ITALIAN => "Tabella", - Lang::NYNORSK => "Tabell", - Lang::POLISH => "Tabela", - Lang::PORTUGUESE => "Tabela", - Lang::RUSSIAN => "Таблица", - Lang::SLOVENIAN => "Tabela", - Lang::SPANISH => "Tabla", - Lang::SWEDISH => "Tabell", - Lang::TURKISH => "Tablo", - Lang::UKRAINIAN => "Таблиця", - Lang::VIETNAMESE => "Bảng", - Lang::ENGLISH | _ => "Table", - } - } -} - -impl Figurable for TableElem {} diff --git a/library/src/layout/terms.rs b/library/src/layout/terms.rs deleted file mode 100644 index d693f100..00000000 --- a/library/src/layout/terms.rs +++ /dev/null @@ -1,166 +0,0 @@ -use super::{HElem, VElem}; -use crate::layout::{BlockElem, ParElem, Spacing}; -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 } -/// ```example -/// / Ligature: A merged glyph. -/// / Kerning: A spacing adjustment -/// between two adjacent letters. -/// ``` -/// -/// ## Syntax { #syntax } -/// This function also has dedicated syntax: Starting a line with a slash, -/// followed by a term, a colon and a description creates a term list item. -/// -/// Display: Term List -/// Category: layout -#[element(Layout)] -#[scope( - scope.define("item", TermItem::func()); - scope -)] -pub struct TermsElem { - /// If this is `{false}`, the items are spaced apart with [term list - /// spacing]($func/terms.spacing). If it is `{true}`, they use normal - /// [leading]($func/par.leading) instead. This makes the term list more - /// compact, which can look better if the items are short. - /// - /// 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())] - 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]($func/block.below). - pub spacing: Smart, - - /// 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, -} - -impl Layout for TermsElem { - #[tracing::instrument(name = "TermsElem::layout", skip_all)] - fn layout( - &self, - vt: &mut Vt, - styles: StyleChain, - regions: Regions, - ) -> SourceResult { - 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().into_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().strong()); - seq.push(separator.clone()); - seq.push(child.description()); - } - - Content::sequence(seq) - .styled(ParElem::set_hanging_indent(hanging_indent + indent)) - .layout(vt, styles, regions) - } -} - -/// A term list item. -/// -/// Display: Term List Item -/// Category: layout -#[element] -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::().cloned().ok_or("expected term item or array")?, -} diff --git a/library/src/layout/transform.rs b/library/src/layout/transform.rs deleted file mode 100644 index a57a5edc..00000000 --- a/library/src/layout/transform.rs +++ /dev/null @@ -1,194 +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 } -/// ```example -/// #rect(inset: 0pt, move( -/// dx: 6pt, dy: 6pt, -/// rect( -/// inset: 8pt, -/// fill: white, -/// stroke: black, -/// [Abra cadabra] -/// ) -/// )) -/// ``` -/// -/// Display: Move -/// Category: layout -#[element(Layout)] -pub struct MoveElem { - /// The horizontal displacement of the content. - pub dx: Rel, - - /// The vertical displacement of the content. - pub dy: Rel, - - /// 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 { - let pod = Regions::one(regions.base(), Axes::splat(false)); - let mut frame = self.body().layout(vt, styles, pod)?.into_frame(); - let delta = Axes::new(self.dx(styles), self.dy(styles)).resolve(styles); - let delta = delta.zip(regions.base()).map(|(d, s)| d.relative_to(s)); - 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 } -/// ```example -/// #stack( -/// dir: ltr, -/// spacing: 1fr, -/// ..range(16) -/// .map(i => rotate(24deg * i)[X]), -/// ) -/// ``` -/// -/// Display: Rotate -/// Category: layout -#[element(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())) - /// ``` - #[resolve] - #[fold] - #[default(Align::CENTER_HORIZON)] - pub origin: Axes>, - - /// 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 { - let pod = Regions::one(regions.base(), Axes::splat(false)); - let mut frame = self.body().layout(vt, styles, pod)?.into_frame(); - let Axes { x, y } = - self.origin(styles).zip(frame.size()).map(|(o, s)| o.position(s)); - let 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 } -/// ```example -/// #set align(center) -/// #scale(x: -100%)[This is mirrored.] -/// ``` -/// -/// Display: Scale -/// Category: layout -#[element(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 - /// ``` - #[resolve] - #[fold] - #[default(Align::CENTER_HORIZON)] - pub origin: Axes>, - - /// 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 { - let pod = Regions::one(regions.base(), Axes::splat(false)); - let mut frame = self.body().layout(vt, styles, pod)?.into_frame(); - let Axes { x, y } = - self.origin(styles).zip(frame.size()).map(|(o, s)| o.position(s)); - let 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/library/src/lib.rs b/library/src/lib.rs deleted file mode 100644 index 0bd88501..00000000 --- a/library/src/lib.rs +++ /dev/null @@ -1,141 +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::diag::At; -use typst::eval::{LangItems, Library, Module, Scope}; -use typst::geom::Smart; -use typst::model::{Element, 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(); - - // Categories. - text::define(&mut global); - layout::define(&mut global); - visualize::define(&mut global); - meta::define(&mut global); - compute::define(&mut global); - symbols::define(&mut global); - global.define("math", math); - - Module::new("global").with_scope(global) -} - -/// 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_func: text::TextElem::func(), - text_str: |content| Some(content.to::()?.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_func: meta::HeadingElem::func(), - 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_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() - }, - library_method: |vm, dynamic, method, args, span| { - if let Some(counter) = dynamic.downcast::().cloned() { - counter.call_method(vm, method, args, span) - } else if let Some(state) = dynamic.downcast::().cloned() { - state.call_method(vm, method, args, span) - } else { - Err(format!("type {} has no method `{method}`", dynamic.type_name())) - .at(span) - } - }, - } -} diff --git a/library/src/math/accent.rs b/library/src/math/accent.rs deleted file mode 100644 index d1bee198..00000000 --- a/library/src/math/accent.rs +++ /dev/null @@ -1,139 +0,0 @@ -use super::*; - -/// 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 } -/// ```example -/// $grave(a) = accent(a, `)$ \ -/// $arrow(a) = accent(a, arrow)$ \ -/// $tilde(a) = accent(a, \u{0303})$ -/// ``` -/// -/// Display: Accent -/// Category: math -#[element(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` | ` | - /// | Acute | `acute` | `´` | - /// | Circumflex | `hat` | `^` | - /// | Tilde | `tilde` | `~` | - /// | Macron | `macron` | `¯` | - /// | Breve | `breve` | `˘` | - /// | Dot | `dot` | `.` | - /// | Double dot | `dot.double` | `¨` | - /// | Triple dot | `dot.triple` | | - /// | Quadruple dot | `dot.quad` | | - /// | 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::new(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. -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::() { - Some(elem) => Value::Str(elem.text().into()).cast()?, - None => bail!("expected text"), - }, -} diff --git a/library/src/math/align.rs b/library/src/math/align.rs deleted file mode 100644 index aee89a89..00000000 --- a/library/src/math/align.rs +++ /dev/null @@ -1,63 +0,0 @@ -use super::*; - -/// A math alignment point: `&`, `&&`. -/// -/// Display: Alignment Point -/// Category: math -#[element(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, - pub width: Abs, -} - -/// Determine the position of the alignment points. -pub(super) fn alignments(rows: &[MathRow]) -> AlignmentResult { - let mut widths = Vec::::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/library/src/math/attach.rs b/library/src/math/attach.rs deleted file mode 100644 index fedeb908..00000000 --- a/library/src/math/attach.rs +++ /dev/null @@ -1,411 +0,0 @@ -use super::*; - -/// A base with optional attachments. -/// -/// ## Example { #example } -/// ```example -/// // With syntax. -/// $ sum_(i=0)^n a_i = 2^(1+i) $ -/// -/// // With function call. -/// $ attach( -/// Pi, t: alpha, b: beta, -/// tl: 1, tr: 2+3, bl: 4+5, br: 6, -/// ) $ -/// ``` -/// -/// ## Syntax { #syntax } -/// This function also has dedicated syntax for attachments after the base: Use -/// the underscore (`_`) to indicate a subscript i.e. bottom attachment and the -/// hat (`^`) to indicate a superscript i.e. top attachment. -/// -/// Display: Attachment -/// Category: math -#[element(LayoutMath)] -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, - - /// 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, - - /// The top-left attachment (before the base). - pub tl: Option, - - /// The bottom-left attachment (before base). - pub bl: Option, - - /// The top-right attachment (after the base). - pub tr: Option, - - /// The bottom-right attachment (after the base). - pub br: Option, -} - -impl LayoutMath for AttachElem { - #[tracing::instrument(skip(ctx))] - fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { - type GetAttachment = fn(&AttachElem, styles: StyleChain) -> Option; - 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]) - } -} - -/// Forces a base to display attachments as scripts. -/// -/// ## Example { #example } -/// ```example -/// $ scripts(sum)_1^2 != sum_1^2 $ -/// ``` -/// -/// Display: Scripts -/// Category: math -#[element(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 { #example } -/// ```example -/// $ limits(A)_1^2 != A_1^2 $ -/// ``` -/// -/// Display: Limits -/// Category: math -#[element(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 { - if Self::DEFAULT_TO_LIMITS.contains(&c) { - Limits::Display - } else { - 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, - } - } - - /// Unicode codepoints that should show attachments as limits in display - /// mode. - #[rustfmt::skip] - const DEFAULT_TO_LIMITS: &[char] = &[ - /* ∏ */ '\u{220F}', /* ∐ */ '\u{2210}', /* ∑ */ '\u{2211}', - /* ⋀ */ '\u{22C0}', /* ⋁ */ '\u{22C1}', - /* ⋂ */ '\u{22C2}', /* ⋃ */ '\u{22C3}', - /* ⨀ */ '\u{2A00}', /* ⨁ */ '\u{2A01}', /* ⨂ */ '\u{2A02}', - /* ⨃ */ '\u{2A03}', /* ⨄ */ '\u{2A04}', - /* ⨅ */ '\u{2A05}', /* ⨆ */ '\u{2A06}', - ]; -} - -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; 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::new(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, - b: Option, -) -> (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::new(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; 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) -} - -/// 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/library/src/math/cancel.rs b/library/src/math/cancel.rs deleted file mode 100644 index f576a727..00000000 --- a/library/src/math/cancel.rs +++ /dev/null @@ -1,187 +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 } -/// ```example -/// >>> #set page(width: 140pt) -/// Here, we can simplify: -/// $ (a dot b dot cancel(x)) / -/// cancel(x) $ -/// ``` -/// -/// Display: Cancel -/// Category: math -#[element(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, - - /// If the cancel line should be inverted (pointing 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, - - /// If 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 to rotate the cancel line. See the [line's - /// documentation]($func/line.angle) for more details. - /// - /// ```example - /// >>> #set page(width: 140pt) - /// $ cancel(Pi, rotation: #30deg) $ - /// ``` - #[default(Angle::zero())] - pub rotation: Angle, - - /// How to stroke the cancel line. See the - /// [line's documentation]($func/line.stroke) for more details. - /// - /// ```example - /// >>> #set page(width: 140pt) - /// $ cancel( - /// sum x, - /// stroke: #( - /// paint: red, - /// thickness: 1.5pt, - /// dash: "dashed", - /// ), - /// ) $ - /// ``` - #[resolve] - #[fold] - #[default(PartialStroke { - // Default stroke has 0.5pt for better visuals. - thickness: Smart::Custom(Abs::pt(0.5)), - ..Default::default() - })] - pub stroke: PartialStroke, -} - -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(Stroke { - paint: TextElem::fill_in(styles), - ..Default::default() - }); - - let invert = self.inverted(styles); - let cross = self.cross(styles); - let angle = self.rotation(styles); - - let invert_first_line = !cross && invert; - let first_line = draw_cancel_line( - 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(length, stroke, true, angle, body_size, span); - - body.push_frame(center, second_line); - } - - ctx.push(FrameFragment::new(ctx, body).with_class(body_class)); - - Ok(()) - } -} - -/// Draws a cancel line. -fn draw_cancel_line( - length: Rel, - stroke: Stroke, - invert: bool, - angle: Angle, - body_size: Size, - span: Span, -) -> Frame { - // B - // /| - // diagonal / | height - // / | - // / | - // O ---- - // width - let diagonal = body_size.to_point().hypot(); - let length = length.relative_to(diagonal); - let (width, height) = (body_size.x, body_size.y); - let mid = body_size / 2.0; - - // Scale the amount needed such that the cancel line has the given 'length' - // (reference length, or 100%, is the whole diagonal). - // Scales from the center. - let scale = length.to_raw() / diagonal.to_raw(); - - // invert horizontally if 'invert' was given - let scale_x = scale * if invert { -1.0 } else { 1.0 }; - let scale_y = scale; - let scales = Axes::new(scale_x, scale_y); - - // Draw a line from bottom left to top right of the given element, where the - // origin represents the very middle of that element, that is, a line from - // (-width / 2, height / 2) with length components (width, -height) (sign is - // inverted in the y-axis). After applying the scale, the line will have the - // correct length and orientation (inverted if needed). - let start = Axes::new(-mid.x, mid.y).zip(scales).map(|(l, s)| l * s); - let delta = Axes::new(width, -height).zip(scales).map(|(l, s)| l * s); - - let mut frame = Frame::new(body_size); - frame.push( - start.to_point(), - FrameItem::Shape(Geometry::Line(delta.to_point()).stroked(stroke), span), - ); - - // Having the middle of the line at the origin is convenient here. - frame.transform(Transform::rotate(angle)); - frame -} diff --git a/library/src/math/ctx.rs b/library/src/math/ctx.rs deleted file mode 100644 index a1dc6cf4..00000000 --- a/library/src/math/ctx.rs +++ /dev/null @@ -1,268 +0,0 @@ -use ttf_parser::math::MathValue; -use typst::font::{FontStyle, FontWeight}; -use typst::model::realize; -use unicode_segmentation::UnicodeSegmentation; - -use super::*; - -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>, - pub space_width: Em, - pub fragments: Vec, - 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 table = font.ttf().tables().math.unwrap(); - let constants = table.constants.unwrap(); - - let ssty_table = font - .ttf() - .tables() - .gsub - .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::(0) - }) - .and_then(|ssty| match ssty { - ttf_parser::gsub::SubstitutionSubtable::Alternate(alt_glyphs) => { - Some(alt_glyphs) - } - _ => None, - }); - - 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, - constants, - ssty_table, - space_width, - fragments: vec![], - local: Styles::new(), - style: MathStyle { - variant: MathVariant::Serif, - size: if block { MathSize::Display } else { MathSize::Text }, - 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) { - self.fragments.push(fragment.into()); - } - - pub fn extend(&mut self, fragments: Vec) { - self.fragments.extend(fragments); - } - - pub fn layout_fragment( - &mut self, - elem: &dyn LayoutMath, - ) -> SourceResult { - let row = self.layout_fragments(elem)?; - Ok(MathRow::new(row).into_fragment(self)) - } - - pub fn layout_fragments( - &mut self, - elem: &dyn LayoutMath, - ) -> SourceResult> { - 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 { - let fragments = self.layout_fragments(elem)?; - Ok(MathRow::new(fragments)) - } - - pub fn layout_frame(&mut self, elem: &dyn LayoutMath) -> SourceResult { - Ok(self.layout_fragment(elem)?.into_frame()) - } - - pub fn layout_content(&mut self, content: &Content) -> SourceResult { - Ok(content - .layout(self.vt, self.outer.chain(&self.local), self.regions)? - .into_frame()) - } - - pub fn layout_text(&mut self, elem: &TextElem) -> SourceResult { - 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::Display => { - if glyph.class == Some(MathClass::Large) { - let height = scaled!(self, display_operator_min_height); - glyph.stretch_vertical(self, height, Abs::zero()).into() - } else { - glyph.into() - } - } - MathSize::Script => { - glyph.make_scriptsize(self); - glyph.into() - } - MathSize::ScriptScript => { - glyph.make_scriptscriptsize(self); - glyph.into() - } - _ => glyph.into(), - } - } else if text.chars().all(|c| c.is_ascii_digit()) { - // 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 spaced = text.graphemes(true).nth(1).is_some(); - 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(); - let frame = self.layout_content(&TextElem::packed(text).spanned(span))?; - FrameFragment::new(self, frame) - .with_class(MathClass::Alphabetic) - .with_spaced(spaced) - .into() - }; - Ok(fragment) - } - - pub fn styles(&self) -> StyleChain { - self.outer.chain(&self.local) - } - - pub fn realize(&mut self, content: &Content) -> SourceResult> { - 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 { - FontWeight::REGULAR - })); - 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/library/src/math/delimited.rs b/library/src/math/delimited.rs deleted file mode 100644 index 99cd6c33..00000000 --- a/library/src/math/delimited.rs +++ /dev/null @@ -1,200 +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. -/// -/// ## Example { #example } -/// ```example -/// $ lr(]a, b/2]) $ -/// $ lr(]sum_(x=1)^n] x, size: #50%) $ -/// ``` -/// -/// Display: Left/Right -/// Category: math -#[element(LayoutMath)] -pub struct LrElem { - /// The size of the brackets, relative to the height of the wrapped content. - pub size: Smart>, - - /// The delimited content, including the delimiters. - #[required] - #[parse( - let mut body = Content::empty(); - for (i, arg) in args.all::()?.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::() { - 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, -) { - 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); - *fragment = - MathFragment::Variant(glyph.stretch_vertical(ctx, height, short_fall)); - - if let Some(class) = apply { - fragment.set_class(class); - } - } -} - -/// Floors an expression. -/// -/// ## Example { #example } -/// ```example -/// $ floor(x/2) $ -/// ``` -/// -/// Display: Floor -/// Category: math -#[func] -pub fn floor( - /// The expression to floor. - body: Content, -) -> Content { - delimited(body, '⌊', '⌋') -} - -/// Ceils an expression. -/// -/// ## Example { #example } -/// ```example -/// $ ceil(x/2) $ -/// ``` -/// -/// Display: Ceil -/// Category: math -#[func] -pub fn ceil( - /// The expression to ceil. - body: Content, -) -> Content { - delimited(body, '⌈', '⌉') -} - -/// Rounds an expression. -/// -/// ## Example { #example } -/// ```example -/// $ round(x/2) $ -/// ``` -/// -/// Display: Round -/// Category: math -#[func] -pub fn round( - /// The expression to round. - body: Content, -) -> Content { - delimited(body, '⌊', '⌉') -} - -/// Takes the absolute value of an expression. -/// -/// ## Example { #example } -/// ```example -/// $ abs(x/2) $ -/// ``` -/// -/// -/// Display: Abs -/// Category: math -#[func] -pub fn abs( - /// The expression to take the absolute value of. - body: Content, -) -> Content { - delimited(body, '|', '|') -} - -/// Takes the norm of an expression. -/// -/// ## Example { #example } -/// ```example -/// $ norm(x/2) $ -/// ``` -/// -/// Display: Norm -/// Category: math -#[func] -pub fn norm( - /// The expression to take the norm of. - body: Content, -) -> Content { - delimited(body, '‖', '‖') -} - -fn delimited(body: Content, left: char, right: char) -> Content { - LrElem::new(Content::sequence([ - TextElem::packed(left), - body, - TextElem::packed(right), - ])) - .pack() -} diff --git a/library/src/math/frac.rs b/library/src/math/frac.rs deleted file mode 100644 index 0e1f78cc..00000000 --- a/library/src/math/frac.rs +++ /dev/null @@ -1,148 +0,0 @@ -use super::*; - -const FRAC_AROUND: Em = Em::new(0.1); - -/// A mathematical fraction. -/// -/// ## Example { #example } -/// ```example -/// $ 1/2 < (x+1)/2 $ -/// $ ((x+1)) / 2 = frac(a, b) $ -/// ``` -/// -/// ## Syntax { #syntax } -/// This function also has dedicated syntax: Use a slash to turn neighbouring -/// expressions into a fraction. Multiple atoms can be grouped into a single -/// expression using round grouping parenthesis. Such parentheses are removed -/// from the output, but you can nest multiple to force them. -/// -/// Display: Fraction -/// Category: math -#[element(LayoutMath)] -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(), &self.denom(), false, self.span()) - } -} - -/// A binomial expression. -/// -/// ## Example { #example } -/// ```example -/// $ binom(n, k) $ -/// ``` -/// -/// Display: Binomial -/// Category: math -#[element(LayoutMath)] -pub struct BinomElem { - /// The binomial's upper index. - #[required] - pub upper: Content, - - /// The binomial's lower index. - #[required] - pub lower: 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(denom)?; - 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::new(size); - frame.set_baseline(baseline); - frame.push_frame(num_pos, num); - frame.push_frame(denom_pos, denom); - - if binom { - ctx.push( - GlyphFragment::new(ctx, '(', span).stretch_vertical(ctx, height, short_fall), - ); - ctx.push(FrameFragment::new(ctx, frame)); - ctx.push( - GlyphFragment::new(ctx, ')', span).stretch_vertical(ctx, height, short_fall), - ); - } else { - frame.push( - line_pos, - FrameItem::Shape( - Geometry::Line(Point::with_x(line_width)).stroked(Stroke { - paint: TextElem::fill_in(ctx.styles()), - thickness, - ..Stroke::default() - }), - span, - ), - ); - ctx.push(FrameFragment::new(ctx, frame)); - } - - Ok(()) -} diff --git a/library/src/math/fragment.rs b/library/src/math/fragment.rs deleted file mode 100644 index 139ce07b..00000000 --- a/library/src/math/fragment.rs +++ /dev/null @@ -1,414 +0,0 @@ -use super::*; -use ttf_parser::gsub::AlternateSet; - -#[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 { - match self { - Self::Glyph(glyph) => glyph.class, - Self::Variant(variant) => variant.class, - Self::Frame(fragment) => Some(fragment.class), - _ => None, - } - } - - pub fn style(&self) -> Option { - 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 { - 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) { - match self { - Self::Glyph(glyph) => glyph.class = Some(class), - Self::Variant(variant) => variant.class = Some(class), - Self::Frame(fragment) => fragment.class = class, - _ => {} - } - } - - 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) => 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::new(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 for MathFragment { - fn from(glyph: GlyphFragment) -> Self { - Self::Glyph(glyph) - } -} - -impl From for MathFragment { - fn from(variant: VariantFragment) -> Self { - Self::Variant(variant) - } -} - -impl From 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 width: Abs, - pub ascent: Abs, - pub descent: Abs, - pub italics_correction: Abs, - pub style: MathStyle, - pub font_size: Abs, - pub class: Option, - pub span: Span, - pub meta: Vec, - 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(); - Self::with_id(ctx, c, id, span) - } - - pub fn try_new(ctx: &MathContext, c: char, span: Span) -> Option { - let c = ctx.style.styled_char(c); - let id = ctx.ttf.glyph_index(c)?; - 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), - _ => 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()), - 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 - } - - /// 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::new(size); - frame.set_baseline(self.ascent); - frame.push(Point::with_y(self.ascent), 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, - pub italics_correction: Abs, - pub frame: Frame, - pub style: MathStyle, - pub font_size: Abs, - pub class: Option, - pub span: Span, - pub limits: Limits, -} - -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 { - 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> { - 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 { - 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)) -} diff --git a/library/src/math/matrix.rs b/library/src/math/matrix.rs deleted file mode 100644 index aaccc332..00000000 --- a/library/src/math/matrix.rs +++ /dev/null @@ -1,313 +0,0 @@ -use super::*; - -const ROW_GAP: Em = Em::new(0.5); -const COL_GAP: Em = Em::new(0.5); -const VERTICAL_PADDING: Ratio = Ratio::new(0.1); - -/// A column vector. -/// -/// Content in the vector's elements can be aligned with the `&` symbol. -/// -/// ## Example { #example } -/// ```example -/// $ vec(a, b, c) dot vec(1, 2, 3) -/// = a + 2b + 3c $ -/// ``` -/// -/// Display: Vector -/// Category: math -#[element(LayoutMath)] -pub struct VecElem { - /// The delimiter to use. - /// - /// ```example - /// #set math.vec(delim: "[") - /// $ vec(1, 2) $ - /// ``` - #[default(Some(Delimiter::Paren))] - pub delim: Option, - - /// The elements of the vector. - #[variadic] - pub children: Vec, -} - -impl LayoutMath for VecElem { - #[tracing::instrument(skip(ctx))] - fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { - let delim = self.delim(ctx.styles()); - let frame = layout_vec_body(ctx, &self.children(), Align::Center)?; - 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 } -/// ```example -/// $ mat( -/// 1, 2, ..., 10; -/// 2, 2, ..., 10; -/// dots.v, dots.v, dots.down, dots.v; -/// 10, 10, ..., 10; -/// ) $ -/// ``` -/// -/// Display: Matrix -/// Category: math -#[element(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, - - /// 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::>()?; - if values.iter().any(|spanned| matches!(spanned.v, Value::Array(_))) { - for Spanned { v, span } in values { - let array = v.cast::().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>, -} - -impl LayoutMath for MatElem { - #[tracing::instrument(skip(ctx))] - fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { - let delim = self.delim(ctx.styles()); - let frame = layout_mat_body(ctx, &self.rows())?; - 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 } -/// ```example -/// $ f(x, y) := cases( -/// 1 "if" (x dot y)/2 <= 0, -/// 2 "if" x "is even", -/// 3 "if" x in NN, -/// 4 "else", -/// ) $ -/// ``` -/// -/// Display: Cases -/// Category: math -#[element(LayoutMath)] -pub struct CasesElem { - /// The delimiter to use. - /// - /// ```example - /// #set math.cases(delim: "[") - /// $ x = cases(1, 2) $ - /// ``` - #[default(Delimiter::Brace)] - pub delim: Delimiter, - - /// The branches of the case distinction. - #[variadic] - pub children: Vec, -} - -impl LayoutMath for CasesElem { - #[tracing::instrument(skip(ctx))] - fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { - let delim = self.delim(ctx.styles()); - let frame = layout_vec_body(ctx, &self.children(), Align::Left)?; - layout_delimiters(ctx, frame, Some(delim.open()), None, 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: Align, -) -> SourceResult { - let gap = ROW_GAP.scaled(ctx); - 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]) -> SourceResult { - let row_gap = ROW_GAP.scaled(ctx); - let col_gap = COL_GAP.scaled(ctx); - - let ncols = rows.first().map_or(0, |row| row.len()); - let nrows = rows.len(); - if ncols == 0 || nrows == 0 { - return Ok(Frame::new(Size::zero())); - } - - let mut heights = vec![(Abs::zero(), Abs::zero()); nrows]; - - ctx.style(ctx.style.for_denominator()); - let mut cols = vec![vec![]; ncols]; - 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(); - - let mut frame = Frame::new(Size::new( - Abs::zero(), - heights.iter().map(|&(a, b)| a + b).sum::() + row_gap * (nrows - 1) as f64, - )); - let mut x = Abs::zero(); - for col in cols { - 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, Align::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 + row_gap; - } - x += rcol + col_gap; - } - frame.size_mut().x = x - col_gap; - - Ok(frame) -} - -/// Layout the outer wrapper around a vector's or matrices' body. -fn layout_delimiters( - ctx: &mut MathContext, - mut frame: Frame, - left: Option, - right: Option, - 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 { - ctx.push( - GlyphFragment::new(ctx, left, span).stretch_vertical(ctx, target, short_fall), - ); - } - - ctx.push(FrameFragment::new(ctx, frame)); - - if let Some(right) = right { - ctx.push( - GlyphFragment::new(ctx, right, span) - .stretch_vertical(ctx, target, short_fall), - ); - } - - Ok(()) -} diff --git a/library/src/math/mod.rs b/library/src/math/mod.rs deleted file mode 100644 index 0429265f..00000000 --- a/library/src/math/mod.rs +++ /dev/null @@ -1,480 +0,0 @@ -//! Mathematical formulas. - -#[macro_use] -mod ctx; -mod accent; -mod align; -mod attach; -mod cancel; -mod delimited; -mod frac; -mod fragment; -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::delimited::*; -pub use self::frac::*; -pub use self::matrix::*; -pub use self::op::*; -pub use self::root::*; -pub use self::style::*; -pub use self::underover::*; - -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::{HElem, ParElem, Spacing}; -use crate::meta::Supplement; -use crate::meta::{ - Count, Counter, CounterUpdate, LocalName, Numbering, Outlinable, Refable, -}; -use crate::prelude::*; -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.define("equation", EquationElem::func()); - math.define("text", TextElem::func()); - - // Grouping. - math.define("lr", LrElem::func()); - math.define("abs", abs_func()); - math.define("norm", norm_func()); - math.define("floor", floor_func()); - math.define("ceil", ceil_func()); - math.define("round", round_func()); - - // Attachments and accents. - math.define("attach", AttachElem::func()); - math.define("scripts", ScriptsElem::func()); - math.define("limits", LimitsElem::func()); - math.define("accent", AccentElem::func()); - math.define("underline", UnderlineElem::func()); - math.define("overline", OverlineElem::func()); - math.define("underbrace", UnderbraceElem::func()); - math.define("overbrace", OverbraceElem::func()); - math.define("underbracket", UnderbracketElem::func()); - math.define("overbracket", OverbracketElem::func()); - math.define("cancel", CancelElem::func()); - - // Fractions and matrix-likes. - math.define("frac", FracElem::func()); - math.define("binom", BinomElem::func()); - math.define("vec", VecElem::func()); - math.define("mat", MatElem::func()); - math.define("cases", CasesElem::func()); - - // Roots. - math.define("sqrt", sqrt_func()); - math.define("root", RootElem::func()); - - // Styles. - math.define("upright", upright_func()); - math.define("bold", bold_func()); - math.define("italic", italic_func()); - math.define("serif", serif_func()); - math.define("sans", sans_func()); - math.define("cal", cal_func()); - math.define("frak", frak_func()); - math.define("mono", mono_func()); - math.define("bb", bb_func()); - - math.define("display", display_func()); - math.define("inline", inline_func()); - math.define("script", script_func()); - math.define("sscript", sscript_func()); - - // Text operators. - math.define("op", OpElem::func()); - op::define(&mut math); - - // Spacings. - spacing::define(&mut math); - - // Symbols. - for (name, symbol) in crate::symbols::SYM { - math.define(*name, symbol.clone()); - } - - Module::new("math").with_scope(math) -} - -/// A mathematical equation. -/// -/// Can be displayed inline with text or as a separate block. -/// -/// ## Example { #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 { #syntax } -/// This function also has dedicated syntax: Write mathematical markup within -/// dollar signs to create an equation. Starting and ending the equation with at -/// least one space lifts it into a separate block that is centered -/// horizontally. For more details about math syntax, see the -/// [main math page]($category/math). -/// -/// Display: Equation -/// Category: math -#[element( - 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]($func/numbering) block-level equations. - /// - /// ```example - /// #set math.equation(numbering: "(1)") - /// - /// We define: - /// $ phi.alt := (1 + sqrt(5)) / 2 $ - /// - /// With @ratio, we get: - /// $ F_n = floor(1 / sqrt(5) phi.alt^n) $ - /// ``` - pub numbering: Option, - - /// 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 $ - /// - /// With @ratio, we get: - /// $ F_n = floor(1 / sqrt(5) phi.alt^n) $ - /// ``` - pub supplement: Smart>, - - /// 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 { - let mut realized = self.clone().pack().guarded(Guard::Base(Self::func())); - if self.block(styles) { - realized = realized.aligned(Axes::with_x(Some(Align::Center.into()))) - } - Ok(realized) - } -} - -impl Finalize for EquationElem { - fn finalize(&self, realized: Content, _: StyleChain) -> Content { - 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 { - 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.as_str(), 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::func()) - .display(Some(numbering), false) - .layout(vt, styles, pod)? - .into_frame(); - - let width = if regions.size.x.is_finite() { - regions.size.x - } else { - frame.width() - + 2.0 * (counter.width() + NUMBER_GUTTER.resolve(styles)) - }; - - let height = frame.height().max(counter.height()); - frame.resize(Size::new(width, height), Align::CENTER_HORIZON); - - let x = if TextElem::dir_in(styles).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 slack = ParElem::leading_in(styles) * 0.7; - let top_edge = TextElem::top_edge_in(styles).resolve(styles, font.metrics()); - let bottom_edge = - -TextElem::bottom_edge_in(styles).resolve(styles, font.metrics()); - - 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 { - (self.block(StyleChain::default()) - && self.numbering(StyleChain::default()).is_some()) - .then(|| CounterUpdate::Step(NonZeroUsize::ONE)) - } -} - -impl LocalName for EquationElem { - fn local_name(&self, lang: Lang, region: Option) -> &'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::FRENCH => "Équation", - Lang::GERMAN => "Gleichung", - Lang::ITALIAN => "Equazione", - Lang::NYNORSK => "Likning", - Lang::POLISH => "Równanie", - Lang::PORTUGUESE => "Equação", - 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::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::func()) - } - - fn numbering(&self) -> Option { - self.numbering(StyleChain::default()) - } -} - -impl Outlinable for EquationElem { - fn outline(&self, vt: &mut Vt) -> SourceResult> { - 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.0.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::() { - return elem.layout_math(ctx); - } - - if let Some(realized) = ctx.realize(self)? { - return realized.layout_math(ctx); - } - - if let Some(children) = self.to_sequence() { - for child in children { - 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::() { - ctx.push(MathFragment::Space(ctx.space_width.scaled(ctx))); - return Ok(()); - } - - if self.is::() { - ctx.push(MathFragment::Linebreak); - return Ok(()); - } - - if let Some(elem) = self.to::() { - 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::() { - let fragment = ctx.layout_text(elem)?; - ctx.push(fragment); - return Ok(()); - } - - if let Some(elem) = self.with::() { - 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/library/src/math/op.rs b/library/src/math/op.rs deleted file mode 100644 index 8ad74c49..00000000 --- a/library/src/math/op.rs +++ /dev/null @@ -1,113 +0,0 @@ -use typst::eval::Scope; - -use super::*; - -/// A text operator in an equation. -/// -/// ## Example { #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`, `ctg`, `coth`, `csc`, `deg`, `det`, `dim`, -/// `exp`, `gcd`, `hom`, `mod`, `inf`, `ker`, `lg`, `lim`, `ln`, `log`, -/// `max`, `min`, `Pr`, `sec`, `sin`, `sinc`, `sinh`, `sup`, `tan`, `tg`, -/// `tanh`, `liminf`, and `limsup`. -/// -/// Display: Text Operator -/// Category: math -#[element(LayoutMath)] -pub struct OpElem { - /// The operator's text. - #[required] - pub text: EcoString, - - /// 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_text(&TextElem::new(self.text()).spanned(self.span()))?; - 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) { - $(math.define( - stringify!($name), - OpElem::new(ops!(@name $name $(: $value)?).into()) - .with_limits(ops!(@limit $($tts)*)) - .pack() - );)* - - let dif = |d| { - HElem::new(THIN.into()).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, - ctg, - coth, - csc, - deg, - det (limits), - dim, - exp, - gcd (limits), - hom, - mod, - inf (limits), - ker, - lg, - lim (limits), - ln, - log, - max (limits), - min (limits), - Pr (limits), - sec, - sin, - sinc, - sinh, - sup (limits), - tan, - tg, - tanh, - liminf: "lim inf" (limits), - limsup: "lim sup" (limits), -} diff --git a/library/src/math/root.rs b/library/src/math/root.rs deleted file mode 100644 index d1c5f46a..00000000 --- a/library/src/math/root.rs +++ /dev/null @@ -1,156 +0,0 @@ -use super::*; - -/// A square root. -/// -/// ## Example { #example } -/// ```example -/// $ sqrt(x^2) = x = sqrt(x)^2 $ -/// ``` -/// -/// Display: Square Root -/// Category: math -#[func] -pub fn sqrt( - /// The expression to take the square root of. - radicand: Content, -) -> Content { - RootElem::new(radicand).pack() -} - -/// A general root. -/// -/// ## Example { #example } -/// ```example -/// $ root(3, x) $ -/// ``` -/// -/// Display: Root -/// Category: math -#[element(LayoutMath)] -pub struct RootElem { - /// Which root of the radicand to take. - #[positional] - pub index: Option, - - /// 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. -/// -/// https://www.w3.org/TR/mathml-core/#radicals-msqrt-mroot -fn layout( - ctx: &mut MathContext, - mut 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 = precomposed(ctx, index, target) - .map(|frame| { - index = None; - frame - }) - .unwrap_or_else(|| { - let glyph = GlyphFragment::new(ctx, '√', span); - glyph.stretch_vertical(ctx, target, Abs::zero()).frame - }); - - // Layout the index. - // Script-script style looks too small, we use Script style instead. - ctx.style(ctx.style.with_size(MathSize::Script)); - let index = index.map(|elem| ctx.layout_frame(elem)).transpose()?; - ctx.unstyle(); - - let gap = gap.max((sqrt.height() - radicand.height() - thickness) / 2.0); - let descent = radicand.descent() + gap; - let inner_ascent = extra_ascender + thickness + gap + radicand.ascent(); - - 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; - shift_up = raise_factor * sqrt.height() - descent + index.descent(); - ascent.set_max(shift_up + index.ascent()); - } - - let radicant_offset = sqrt_offset + sqrt.width(); - let width = radicant_offset + radicand.width(); - let size = Size::new(width, ascent + descent); - - let sqrt_pos = Point::new(sqrt_offset, ascent - inner_ascent); - let line_pos = Point::new(radicant_offset, ascent - inner_ascent + thickness / 2.0); - let radicand_pos = Point::new(radicant_offset, ascent - radicand.ascent()); - - let mut frame = Frame::new(size); - frame.set_baseline(ascent); - - if let Some(index) = index { - let index_pos = Point::new(kern_before, ascent - shift_up - index.ascent()); - 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(Stroke { - paint: TextElem::fill_in(ctx.styles()), - thickness, - ..Stroke::default() - }), - span, - ), - ); - - frame.push_frame(radicand_pos, radicand); - ctx.push(FrameFragment::new(ctx, frame)); - - Ok(()) -} - -/// Select a precomposed radical, if the font has it. -fn precomposed(ctx: &MathContext, index: Option<&Content>, target: Abs) -> Option { - let elem = index?.to::()?; - let c = match elem.text().as_str() { - "3" => '∛', - "4" => '∜', - _ => return None, - }; - - ctx.ttf.glyph_index(c)?; - let glyph = GlyphFragment::new(ctx, c, elem.span()); - let variant = glyph.stretch_vertical(ctx, target, Abs::zero()).frame; - if variant.height() < target { - return None; - } - - Some(variant) -} diff --git a/library/src/math/row.rs b/library/src/math/row.rs deleted file mode 100644 index 687f82b8..00000000 --- a/library/src/math/row.rs +++ /dev/null @@ -1,258 +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); - -impl MathRow { - pub fn new(fragments: Vec) -> Self { - let iter = fragments.into_iter().peekable(); - let mut last: Option = None; - let mut space: Option = None; - let mut resolved: Vec = 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.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).x.resolve(styles); - 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: Align, - ) -> Frame { - if self.iter().any(|frag| matches!(frag, MathFragment::Linebreak)) { - let leading = if ctx.style.size >= MathSize::Text { - ParElem::leading_in(ctx.styles()) - } else { - TIGHT_LEADING.scaled(ctx) - }; - - let mut rows: Vec<_> = self.rows(); - - if matches!(rows.last(), Some(row) if row.0.is_empty()) { - rows.pop(); - } - - let AlignmentResult { points, width } = alignments(&rows); - let mut frame = Frame::new(Size::zero()); - - for (i, row) in rows.into_iter().enumerate() { - let sub = row.into_line_frame(&points, align); - let size = frame.size_mut(); - if i > 0 { - size.y += leading; - } - - let mut pos = Point::with_y(size.y); - if points.is_empty() { - pos.x = align.position(width - sub.width()); - } - size.y += sub.height(); - size.x.set_max(sub.width()); - frame.push_frame(pos, sub); - } - frame - } else { - self.into_line_frame(points, align) - } - } - - fn into_line_frame(self, points: &[Abs], align: Align) -> Frame { - let ascent = self.ascent(); - let mut frame = Frame::new(Size::new(Abs::zero(), ascent + self.descent())); - frame.set_baseline(ascent); - - let mut next_x = { - let mut widths = Vec::new(); - if !points.is_empty() && align != Align::Left { - 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 { - Align::Left => prev_points.next(), - Align::Right => 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> From 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 { - let r = Some(*self); - match self { - Self::Left => *self = Self::Right, - Self::Right => *self = Self::Left, - } - r - } -} diff --git a/library/src/math/spacing.rs b/library/src/math/spacing.rs deleted file mode 100644 index 848aca78..00000000 --- a/library/src/math/spacing.rs +++ /dev/null @@ -1,60 +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); - -/// 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()); -} - -/// Create the spacing between two fragments in a given style. -pub(super) fn spacing( - l: &MathFragment, - space: Option, - r: &MathFragment, -) -> Option { - 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 next to a delimiter. - (Large, Opening | Fence) | (Closing | Fence, Large) => 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/library/src/math/stretch.rs b/library/src/math/stretch.rs deleted file mode 100644 index 910f7a81..00000000 --- a/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::new(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 + '_ { - 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/library/src/math/style.rs b/library/src/math/style.rs deleted file mode 100644 index 235770db..00000000 --- a/library/src/math/style.rs +++ /dev/null @@ -1,620 +0,0 @@ -use super::*; - -/// Bold font style in math. -/// -/// ## Example { #example } -/// ```example -/// $ bold(A) := B^+ $ -/// ``` -/// -/// Display: Bold -/// Category: math -#[func] -pub fn bold( - /// The content to style. - body: Content, -) -> Content { - MathStyleElem::new(body).with_bold(Some(true)).pack() -} - -/// Upright (non-italic) font style in math. -/// -/// ## Example { #example } -/// ```example -/// $ upright(A) != A $ -/// ``` -/// -/// Display: Upright -/// Category: math -#[func] -pub fn upright( - /// The content to style. - 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. -/// -/// Display: Italic -/// Category: math -#[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. -/// -/// Display: Serif -/// Category: math -#[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 { #example } -/// ```example -/// $ sans(A B C) $ -/// ``` -/// -/// Display: Sans-serif -/// Category: math -#[func] -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 { #example } -/// ```example -/// Let $cal(P)$ be the set of ... -/// ``` -/// -/// Display: Calligraphic -/// Category: math -#[func] -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 { #example } -/// ```example -/// $ frak(P) $ -/// ``` -/// -/// Display: Fraktur -/// Category: math -#[func] -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 { #example } -/// ```example -/// $ mono(x + y = z) $ -/// ``` -/// -/// Display: Monospace -/// Category: math -#[func] -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 { #example } -/// ```example -/// $ bb(b) $ -/// $ bb(N) = NN $ -/// $ f: NN -> RR $ -/// ``` -/// -/// Display: Blackboard Bold -/// Category: math -#[func] -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 { #example } -/// ```example -/// $sum_i x_i/2 = display(sum_i x_i/2)$ -/// ``` -/// -/// Display: Display Size -/// Category: math -#[func] -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 { #example } -/// ```example -/// $ sum_i x_i/2 -/// = inline(sum_i x_i/2) $ -/// ``` -/// -/// Display: Inline Size -/// Category: math -#[func] -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 { #example } -/// ```example -/// $sum_i x_i/2 = script(sum_i x_i/2)$ -/// ``` -/// -/// Display: Script Size -/// Category: math -#[func] -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 { #example } -/// ```example -/// $sum_i x_i/2 = sscript(sum_i x_i/2)$ -/// ``` -/// -/// Display: Script-Script Size -/// Category: math -#[func] -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. -/// -/// Display: Bold -/// Category: math -#[element(LayoutMath)] -pub struct MathStyleElem { - /// The content to style. - #[required] - pub body: Content, - - /// The variant to select. - pub variant: Option, - - /// Whether to use bold glyphs. - pub bold: Option, - - /// Whether to use italic glyphs. - pub italic: Option, - - /// Whether to use forced size - pub size: Option, - - /// Whether to limit height of exponents - pub cramped: Option, -} - -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, - /// Affects the height of exponents. - pub cramped: bool, - /// Whether to use bold glyphs. - pub bold: bool, - /// Whether to use italic glyphs. - pub italic: Smart, -} - -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 `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)] -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)] -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 { - Some(match c { - '〈' => '⟨', - '〉' => '⟩', - '《' => '⟪', - '》' => '⟫', - _ => return None, - }) -} - -fn latin_exception( - c: char, - variant: MathVariant, - bold: bool, - italic: bool, -) -> Option { - 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 { - 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/library/src/math/underover.rs b/library/src/math/underover.rs deleted file mode 100644 index 796c9ebc..00000000 --- a/library/src/math/underover.rs +++ /dev/null @@ -1,339 +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 { #example } -/// ```example -/// $ underline(1 + 2 + ... + 5) $ -/// ``` -/// -/// Display: Underline -/// Category: math -#[element(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 { #example } -/// ```example -/// $ overline(1 + 2 + ... + 5) $ -/// ``` -/// -/// Display: Overline -/// Category: math -#[element(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::new(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(Stroke { - paint: TextElem::fill_in(ctx.styles()), - thickness: bar_height, - ..Stroke::default() - }), - span, - ), - ); - - ctx.push(FrameFragment::new(ctx, frame).with_class(content_class)); - - Ok(()) -} - -/// A horizontal brace under content, with an optional annotation below. -/// -/// ## Example { #example } -/// ```example -/// $ underbrace(1 + 2 + ... + 5, "numbers") $ -/// ``` -/// -/// Display: Underbrace -/// Category: math -#[element(LayoutMath)] -pub struct UnderbraceElem { - /// The content above the brace. - #[required] - pub body: Content, - - /// The optional content below the brace. - #[positional] - pub annotation: Option, -} - -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 { #example } -/// ```example -/// $ overbrace(1 + 2 + ... + 5, "numbers") $ -/// ``` -/// -/// Display: Overbrace -/// Category: math -#[element(LayoutMath)] -pub struct OverbraceElem { - /// The content below the brace. - #[required] - pub body: Content, - - /// The optional content above the brace. - #[positional] - pub annotation: Option, -} - -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 { #example } -/// ```example -/// $ underbracket(1 + 2 + ... + 5, "numbers") $ -/// ``` -/// -/// Display: Underbracket -/// Category: math -#[element(LayoutMath)] -pub struct UnderbracketElem { - /// The content above the bracket. - #[required] - pub body: Content, - - /// The optional content below the bracket. - #[positional] - pub annotation: Option, -} - -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 { #example } -/// ```example -/// $ overbracket(1 + 2 + ... + 5, "numbers") $ -/// ``` -/// -/// Display: Overbracket -/// Category: math -#[element(LayoutMath)] -pub struct OverbracketElem { - /// The content below the bracket. - #[required] - pub body: Content, - - /// The optional content above the bracket. - #[positional] - pub annotation: Option, -} - -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, - 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, Align::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, - align: Align, - 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::new(Size::new( - width, - rows.iter().map(|row| row.height()).sum::() - + 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/library/src/meta/bibliography.rs b/library/src/meta/bibliography.rs deleted file mode 100644 index 0531997d..00000000 --- a/library/src/meta/bibliography.rs +++ /dev/null @@ -1,724 +0,0 @@ -use std::collections::HashMap; -use std::ffi::OsStr; -use std::path::Path; -use std::sync::Arc; - -use ecow::{eco_vec, EcoVec}; -use hayagriva::io::{BibLaTeXError, YamlBibliographyError}; -use hayagriva::style::{self, Brackets, Citation, Database, DisplayString, Formatting}; -use hayagriva::Entry; -use typst::diag::FileError; -use typst::util::{option_eq, Bytes}; - -use super::{LinkElem, LocalName, RefElem}; -use crate::layout::{BlockElem, GridElem, ParElem, Sizing, TrackSizings, VElem}; -use crate::meta::{FootnoteElem, HeadingElem}; -use crate::prelude::*; -use crate::text::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]($func/cite) function (`[#cite("key")]`). The bibliography will -/// only show entries for works that were referenced in the document. -/// -/// # Example -/// ```example -/// This was already noted by -/// pirates long ago. @arrgh -/// -/// Multiple sources say ... -/// #cite("arrgh", "netwok"). -/// -/// #bibliography("works.bib") -/// ``` -/// -/// Display: Bibliography -/// Category: meta -#[element(Locatable, Synthesize, Show, Finalize, LocalName)] -pub struct BibliographyElem { - /// Path to a Hayagriva `.yml` or BibLaTeX `.bib` file. - #[required] - #[parse( - let Spanned { v: paths, span } = - args.expect::>("path to bibliography file")?; - - // Load bibliography files. - let data = paths.0 - .iter() - .map(|path| { - let id = vm.location().join(path).at(span)?; - vm.world().file(id).at(span) - }) - .collect::>>()?; - - // Check that parsing works. - let _ = load(&paths, &data).at(span)?; - - paths - )] - pub path: BibPaths, - - /// The raw file buffers. - #[internal] - #[required] - #[parse(data)] - pub data: Vec, - - /// The title of the bibliography. - /// - /// - When set to `{auto}`, an appropriate title for the [text - /// language]($func/text.lang) will be used. This is the default. - /// - When set to `{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>, - - /// The bibliography style. - #[default(BibliographyStyle::Ieee)] - pub style: BibliographyStyle, -} - -/// A list of bibliography file paths. -#[derive(Debug, Default, Clone, Hash)] -pub struct BibPaths(Vec); - -cast! { - BibPaths, - self => self.0.into_value(), - v: EcoString => Self(vec![v]), - v: Array => Self(v.into_iter().map(Value::cast).collect::>()?), -} - -impl BibliographyElem { - /// Find the document's bibliography. - pub fn find(introspector: Tracked) -> StrResult { - let mut iter = introspector.query(&Self::func().select()).into_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 supported"); - } - - Ok(elem.to::().unwrap().clone()) - } - - /// Whether the bibliography contains the given key. - pub fn has(vt: &Vt, key: &str) -> bool { - vt.introspector - .query(&Self::func().select()) - .into_iter() - .flat_map(|elem| { - let elem = elem.to::().unwrap(); - load(&elem.path(), &elem.data()) - }) - .flatten() - .any(|entry| entry.key() == key) - } - - /// Find all bibliography keys. - pub fn keys( - introspector: Tracked, - ) -> Vec<(EcoString, Option)> { - Self::find(introspector) - .and_then(|elem| load(&elem.path(), &elem.data())) - .into_iter() - .flatten() - .map(|entry| { - let key = entry.key().into(); - let detail = - entry.title().map(|title| title.canonical.value.as_str().into()); - (key, detail) - }) - .collect() - } -} - -impl Synthesize for BibliographyElem { - fn synthesize(&mut self, _vt: &mut Vt, styles: StyleChain) -> SourceResult<()> { - self.push_style(self.style(styles)); - Ok(()) - } -} - -impl Show for BibliographyElem { - #[tracing::instrument(name = "BibliographyElem::show", skip_all)] - fn show(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult { - 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( - TextElem::lang_in(styles), - TextElem::region_in(styles), - )) - .spanned(self.span()) - }); - - seq.push(HeadingElem::new(title).with_level(NonZeroUsize::ONE).pack()); - } - - Ok(vt.delayed(|vt| { - let works = Works::new(vt).at(self.span())?; - - let row_gutter = BlockElem::below_in(styles).amount(); - if works.references.iter().any(|(prefix, _)| prefix.is_some()) { - let mut cells = vec![]; - for (prefix, reference) in &works.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(vec![Sizing::Auto; 2])) - .with_column_gutter(TrackSizings(vec![COLUMN_GUTTER.into()])) - .with_row_gutter(TrackSizings(vec![row_gutter.into()])) - .pack(), - ); - } else { - let mut entries = vec![]; - for (_, reference) in &works.references { - entries.push(VElem::new(row_gutter).with_weakness(3).pack()); - entries.push(reference.clone()); - } - - seq.push( - Content::sequence(entries) - .styled(ParElem::set_hanging_indent(INDENT.into())), - ); - } - - Ok(Content::sequence(seq)) - })) - } -} - -impl Finalize for BibliographyElem { - fn finalize(&self, realized: Content, _: StyleChain) -> Content { - realized.styled(HeadingElem::set_numbering(None)) - } -} - -impl LocalName for BibliographyElem { - fn local_name(&self, lang: Lang, region: Option) -> &'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::FRENCH => "Bibliographie", - Lang::GERMAN => "Bibliographie", - Lang::ITALIAN => "Bibliografia", - Lang::NYNORSK => "Bibliografi", - Lang::POLISH => "Bibliografia", - Lang::PORTUGUESE => "Bibliografia", - 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::ENGLISH | _ => "Bibliography", - } - } -} - -/// A bibliography style. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)] -pub enum BibliographyStyle { - /// Follows guidance of the American Psychological Association. Based on the - /// 7th edition of the APA Publication Manual. - Apa, - /// The Chicago Author Date style. Based on the 17th edition of the Chicago - /// Manual of Style, Chapter 15. - ChicagoAuthorDate, - /// The Chicago Notes style. Based on the 17th edition of the Chicago - /// Manual of Style, Chapter 14. - ChicagoNotes, - /// The style of the Institute of Electrical and Electronics Engineers. - /// Based on the 2018 IEEE Reference Guide. - Ieee, - /// Follows guidance of the Modern Language Association. Based on the 8th - /// edition of the MLA Handbook. - Mla, -} - -impl BibliographyStyle { - /// The default citation style for this bibliography style. - pub fn default_citation_style(self) -> CitationStyle { - match self { - Self::Apa => CitationStyle::ChicagoAuthorDate, - Self::ChicagoAuthorDate => CitationStyle::ChicagoAuthorDate, - Self::ChicagoNotes => CitationStyle::ChicagoNotes, - Self::Ieee => CitationStyle::Numerical, - Self::Mla => CitationStyle::ChicagoAuthorDate, - } - } -} - -/// Cite a work from the bibliography. -/// -/// Before you starting citing, you need to add a -/// [bibliography]($func/bibliography) somewhere in your document. -/// -/// # Example -/// ```example -/// This was already noted by -/// pirates long ago. @arrgh -/// -/// Multiple sources say ... -/// #cite("arrgh", "netwok"). -/// -/// #bibliography("works.bib") -/// ``` -/// -/// # Syntax -/// This function indirectly has dedicated syntax. [References]($func/ref) -/// can be used to cite works from the bibliography. The label then -/// corresponds to the citation key. -/// -/// Display: Citation -/// Category: meta -#[element(Locatable, Synthesize, Show)] -pub struct CiteElem { - /// The citation keys that identify the elements that shall be cited in - /// the bibliography. - /// - /// Reference syntax supports only a single key. - #[variadic] - pub keys: Vec, - - /// 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 over and - /// over again. @distress[p.~7] - /// - /// #bibliography("works.bib") - /// ``` - #[positional] - pub supplement: Option, - - /// Whether the citation should include brackets. - /// - /// ```example - /// #set cite(brackets: false) - /// - /// @netwok follow these methods - /// in their work ... - /// - /// #bibliography( - /// "works.bib", - /// style: "chicago-author-date", - /// ) - /// ``` - #[default(true)] - pub brackets: bool, - - /// The citation style. - /// - /// When set to `{auto}`, automatically picks the preferred citation style - /// for the bibliography's style. - /// - /// ```example - /// #set cite(style: "alphanumerical") - /// Alphanumerical references. - /// @netwok - /// - /// #bibliography("works.bib") - /// ``` - pub style: Smart, -} - -impl Synthesize for CiteElem { - fn synthesize(&mut self, _vt: &mut Vt, styles: StyleChain) -> SourceResult<()> { - self.push_supplement(self.supplement(styles)); - self.push_brackets(self.brackets(styles)); - self.push_style(self.style(styles)); - Ok(()) - } -} - -impl Show for CiteElem { - #[tracing::instrument(name = "CiteElem::show", skip(self, vt))] - fn show(&self, vt: &mut Vt, _: StyleChain) -> SourceResult { - Ok(vt.delayed(|vt| { - let works = Works::new(vt).at(self.span())?; - let location = self.0.location().unwrap(); - works - .citations - .get(&location) - .cloned() - .flatten() - .ok_or("bibliography does not contain this key") - .at(self.span()) - })) - } -} - -cast! { - CiteElem, - v: Content => v.to::().cloned().ok_or("expected citation")?, -} - -/// A citation style. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)] -pub enum CitationStyle { - /// IEEE-style numerical reference markers. - Numerical, - /// A simple alphanumerical style. For example, the output could be Rass97 - /// or MKG+21. - Alphanumerical, - /// Citations that just consist of the entry keys. - Keys, - /// The Chicago Author Date style. Based on the 17th edition of the Chicago - /// Manual of Style, Chapter 15. - ChicagoAuthorDate, - /// The Chicago Notes style. Based on the 17th edition of the Chicago - /// Manual of Style, Chapter 14. - ChicagoNotes, - /// A Chicago-like author-title format. Results could look like this: - /// Prokopov, “It Is Fast or It Is Wrong”. - ChicagoAuthorTitle, -} - -impl CitationStyle { - fn is_short(self) -> bool { - matches!(self, Self::Numerical | Self::Alphanumerical | Self::Keys) - } -} - -/// Fully formatted citations and references. -#[derive(Default)] -struct Works { - citations: HashMap>, - references: Vec<(Option, Content)>, -} - -impl Works { - /// Prepare all things need to cite a work or format a bibliography. - fn new(vt: &Vt) -> StrResult> { - let bibliography = BibliographyElem::find(vt.introspector)?; - let citations = vt - .introspector - .query(&Selector::Or(eco_vec![ - RefElem::func().select(), - CiteElem::func().select(), - ])) - .into_iter() - .map(|elem| match elem.to::() { - Some(reference) => reference.citation().unwrap(), - _ => elem.to::().unwrap().clone(), - }) - .collect(); - Ok(create(bibliography, citations)) - } -} - -/// Generate all citations and the whole bibliography. -#[comemo::memoize] -fn create(bibliography: BibliographyElem, citations: Vec) -> Arc { - let span = bibliography.span(); - let entries = load(&bibliography.path(), &bibliography.data()).unwrap(); - let style = bibliography.style(StyleChain::default()); - let bib_location = bibliography.0.location().unwrap(); - let ref_location = |target: &Entry| { - let i = entries - .iter() - .position(|entry| entry.key() == target.key()) - .unwrap_or_default(); - bib_location.variant(i) - }; - - let mut db = Database::new(); - let mut ids = HashMap::new(); - let mut preliminary = vec![]; - - for citation in citations { - let cite_id = citation.0.location().unwrap(); - let entries = citation - .keys() - .into_iter() - .map(|key| { - let entry = entries.iter().find(|entry| entry.key() == key)?; - ids.entry(entry.key()).or_insert(cite_id); - db.push(entry); - Some(entry) - }) - .collect::>>(); - preliminary.push((citation, entries)); - } - - let mut current = CitationStyle::Numerical; - let mut citation_style: Box = - Box::new(style::Numerical::new()); - - let citations = preliminary - .into_iter() - .map(|(citation, cited)| { - let location = citation.0.location().unwrap(); - let Some(cited) = cited else { return (location, None) }; - - let mut supplement = citation.supplement(StyleChain::default()); - let brackets = citation.brackets(StyleChain::default()); - let style = citation - .style(StyleChain::default()) - .unwrap_or(style.default_citation_style()); - - if style != current { - current = style; - citation_style = match style { - CitationStyle::Numerical => Box::new(style::Numerical::new()), - CitationStyle::Alphanumerical => { - Box::new(style::Alphanumerical::new()) - } - CitationStyle::ChicagoAuthorDate => { - Box::new(style::ChicagoAuthorDate::new()) - } - CitationStyle::ChicagoNotes => Box::new(style::ChicagoNotes::new()), - CitationStyle::ChicagoAuthorTitle => { - Box::new(style::AuthorTitle::new()) - } - CitationStyle::Keys => Box::new(style::Keys::new()), - }; - } - - let len = cited.len(); - let mut content = Content::empty(); - for (i, entry) in cited.into_iter().enumerate() { - let supplement = if i + 1 == len { supplement.take() } else { None }; - let mut display = db - .citation( - &mut *citation_style, - &[Citation { - entry, - supplement: supplement.is_some().then_some(SUPPLEMENT), - }], - ) - .display; - - if style.is_short() { - display.value = display.value.replace(' ', "\u{a0}"); - } - - if brackets && len == 1 { - display = display.with_default_brackets(&*citation_style); - } - - if i > 0 { - content += TextElem::packed(",\u{a0}"); - } - - // Format and link to the reference entry. - content += format_display_string(&display, supplement, citation.span()) - .linked(Destination::Location(ref_location(entry))); - } - - if brackets && len > 1 { - content = match citation_style.brackets() { - Brackets::None => content, - Brackets::Round => { - TextElem::packed('(') + content + TextElem::packed(')') - } - Brackets::Square => { - TextElem::packed('[') + content + TextElem::packed(']') - } - }; - } - - if style == CitationStyle::ChicagoNotes { - content = FootnoteElem::with_content(content).pack(); - } - - (location, Some(content)) - }) - .collect(); - - let bibliography_style: Box = match style { - BibliographyStyle::Apa => Box::new(style::Apa::new()), - BibliographyStyle::ChicagoAuthorDate => Box::new(style::ChicagoAuthorDate::new()), - BibliographyStyle::ChicagoNotes => Box::new(style::ChicagoNotes::new()), - BibliographyStyle::Ieee => Box::new(style::Ieee::new()), - BibliographyStyle::Mla => Box::new(style::Mla::new()), - }; - - let references = db - .bibliography(&*bibliography_style, None) - .into_iter() - .map(|reference| { - let backlink = ref_location(reference.entry); - let prefix = reference.prefix.map(|prefix| { - // Format and link to first citation. - let bracketed = prefix.with_default_brackets(&*citation_style); - format_display_string(&bracketed, None, span) - .linked(Destination::Location(ids[reference.entry.key()])) - .backlinked(backlink) - }); - - let mut reference = format_display_string(&reference.display, None, span); - if prefix.is_none() { - reference = reference.backlinked(backlink); - } - - (prefix, reference) - }) - .collect(); - - Arc::new(Works { citations, references }) -} - -/// Load bibliography entries from a path. -#[comemo::memoize] -fn load(paths: &BibPaths, data: &[Bytes]) -> StrResult> { - let mut result = EcoVec::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::InvalidUtf8)?; - let entries = parse_bib(path, src)?; - result.extend(entries); - } - - // Biblatex only checks for duplicate keys within files - // -> We have to do this between files again - let mut keys = result.iter().map(|r| r.key()).collect::>(); - keys.sort_unstable(); - // Waiting for `slice_partition_dedup` #54279 - let mut duplicates = Vec::new(); - for pair in keys.windows(2) { - if pair[0] == pair[1] { - duplicates.push(pair[0]); - } - } - - if !duplicates.is_empty() { - Err(eco_format!("duplicate bibliography keys: {}", duplicates.join(", "))) - } else { - Ok(result) - } -} - -/// Parse a bibliography file (bib/yml/yaml) -fn parse_bib(path_str: &str, src: &str) -> StrResult> { - let path = Path::new(path_str); - let ext = path.extension().and_then(OsStr::to_str).unwrap_or_default(); - match ext.to_lowercase().as_str() { - "yml" | "yaml" => { - hayagriva::io::from_yaml_str(src).map_err(format_hayagriva_error) - } - "bib" => hayagriva::io::from_biblatex_str(src).map_err(|err| { - err.into_iter() - .next() - .map(|error| format_biblatex_error(path_str, src, error)) - .unwrap_or_else(|| eco_format!("failed to parse {path_str}")) - }), - _ => bail!("unknown bibliography format (must be .yml/.yaml or .bib)"), - } -} - -/// Format a Hayagriva loading error. -fn format_hayagriva_error(error: YamlBibliographyError) -> EcoString { - eco_format!("{error}") -} - -/// Format a BibLaTeX loading error. -fn format_biblatex_error(path: &str, src: &str, error: BibLaTeXError) -> EcoString { - 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!("parsing failed at {path}:{line}: {msg}") -} - -/// Hayagriva only supports strings, but we have a content supplement. To deal -/// with this, we pass this string to hayagriva instead of our content, find it -/// in the output and replace it with the content. -const SUPPLEMENT: &str = "cdc579c45cf3d648905c142c7082683f"; - -/// Format a display string into content. -fn format_display_string( - string: &DisplayString, - mut supplement: Option, - span: Span, -) -> Content { - let mut stops: Vec<_> = string - .formatting - .iter() - .flat_map(|(range, _)| [range.start, range.end]) - .collect(); - - if let Some(i) = string.value.find(SUPPLEMENT) { - stops.push(i); - stops.push(i + SUPPLEMENT.len()); - } - - stops.sort(); - stops.dedup(); - stops.push(string.value.len()); - - let mut start = 0; - let mut seq = vec![]; - for stop in stops { - let segment = string.value.get(start..stop).unwrap_or_default(); - if segment.is_empty() { - continue; - } - - let mut content = if segment == SUPPLEMENT && supplement.is_some() { - supplement.take().unwrap_or_default() - } else { - TextElem::packed(segment).spanned(span) - }; - - for (range, fmt) in &string.formatting { - if !range.contains(&start) { - continue; - } - - content = match fmt { - Formatting::Bold => content.strong(), - Formatting::Italic => content.emph(), - Formatting::Link(link) => { - LinkElem::new(Destination::Url(link.as_str().into()).into(), content) - .pack() - } - }; - } - - seq.push(content); - start = stop; - } - - Content::sequence(seq) -} diff --git a/library/src/meta/context.rs b/library/src/meta/context.rs deleted file mode 100644 index a42c6980..00000000 --- a/library/src/meta/context.rs +++ /dev/null @@ -1,220 +0,0 @@ -use crate::prelude::*; - -/// Provides access to the location of content. -/// -/// This is useful in combination with [queries]($func/query), -/// [counters]($func/counter), [state]($func/state), and [links]($func/link). -/// See their documentation for more details. -/// -/// ```example -/// #locate(loc => [ -/// My location: \ -/// #loc.position()! -/// ]) -/// ``` -/// -/// ## Methods -/// ### page() -/// Returns the page number for this location. -/// -/// Note that this does not return the value of the [page counter]($func/counter) -/// at this location, but the true page number (starting from one). -/// -/// If you want to know the value of the page counter, use -/// `{counter(page).at(loc)}` instead. -/// -/// - returns: integer -/// -/// ### position() -/// Returns a dictionary with the page number and the x, y position for this -/// location. The page number starts at one and the coordinates are measured -/// from the top-left of the page. -/// -/// If you only need the page number, use `page()` instead as it allows Typst -/// to skip unnecessary work. -/// -/// - returns: dictionary -/// -/// ### page-numbering() -/// Returns the page numbering pattern of the page at this location. This can be -/// used when displaying the page counter in order to obtain the local numbering. -/// This is useful if you are building custom indices or outlines. -/// -/// If the page numbering is set to `none` at that location, this function returns `none`. -/// -/// - returns: string or function or none -/// -/// Display: Locate -/// Category: meta -#[func] -pub fn locate( - /// A function that receives a `location`. Its return value is displayed - /// in the document. - /// - /// 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. -/// -/// Display: Locate -/// Category: special -#[element(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 { - Ok(vt.delayed(|vt| { - let location = self.0.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`]($func/measure) function. See its documentation for more details. -/// In the future, the provided styles might also be directly accessed to look -/// up styles defined by [set rules]($styling/#set-rules). -/// -/// ```example -/// #let thing(body) = style(styles => { -/// let size = measure(body, styles) -/// [Width of "#body" is #size.width] -/// }) -/// -/// #thing[Hey] \ -/// #thing[Welcome] -/// ``` -/// -/// Display: Style -/// Category: meta -#[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. -/// -/// Display: Style -/// Category: special -#[element(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 { - 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`]($type/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]($func/measure). -/// -/// You can also use this function to resolve [`ratio`]($type/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. -/// -/// Display: Layout -/// Category: meta -#[func] -pub fn layout( - /// A function to call with the outer container's size. Its return value is - /// displayed in the document. - /// - /// The container's size is given as a [dictionary]($type/dictionary) with - /// the keys `width` and `height`. - /// - /// 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. -/// -/// Display: Layout -/// Category: special -#[element(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 { - // 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/library/src/meta/counter.rs b/library/src/meta/counter.rs deleted file mode 100644 index 9a223b32..00000000 --- a/library/src/meta/counter.rs +++ /dev/null @@ -1,683 +0,0 @@ -use std::fmt::{self, Debug, Formatter, Write}; -use std::str::FromStr; - -use ecow::{eco_vec, EcoVec}; -use smallvec::{smallvec, SmallVec}; -use typst::eval::Tracer; -use typst::model::DelayedErrors; - -use super::{FigureElem, HeadingElem, Numbering, NumberingPattern}; -use crate::layout::PageElem; -use crate::math::EquationElem; -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]($func/heading.numbering). -/// -/// The `display` method optionally takes an argument telling it how to format -/// the counter. This can be a [numbering pattern or a -/// function]($func/numbering). -/// -/// ```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(). -/// ``` -/// -/// ## Custom counters { #custom-counters } -/// To define your own counter, call the `counter` function with a string as a -/// key. This key identifies the counter globally. -/// -/// ```example -/// #let mine = counter("mycounter") -/// #mine.display() \ -/// #mine.step() -/// #mine.display() \ -/// #mine.update(c => c * 3) -/// #mine.display() \ -/// ``` -/// -/// ## How to step { #how-to-step } -/// When you define and use a custom counter, in general, you should first step -/// the counter and then display it. This way, the stepping behaviour of a -/// counter can depend on the element it is stepped for. If you were writing a -/// 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 { #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 { #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(, 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 -/// #lorem(10) -/// -/// #mine.step() -/// #mine.step() -/// ``` -/// -/// Let's dissect what happens in the example above: -/// -/// - We call [`locate`]($func/locate) to get access to the current location in -/// the document. We then pass this location to our counter's `at` method to -/// get its value at the current location. The `at` method always returns an -/// array because counters can have multiple levels. As the counter starts at -/// one, the first value is thus `{(1,)}`. -/// -/// - We now [`query`]($func/query) the document for all elements with the -/// `{}` label. The result is an array from which we extract the first -/// (and only) element's [location]($type/content.location). We then look up -/// the value of the counter at that location. The first update to the counter -/// sets it to `{1 + 3 = 4}`. At the introduction heading, the value is thus -/// `{(4,)}`. -/// -/// - 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 `{(6,)}`. -/// -/// ## Other kinds of state { #other-state } -/// The `counter` function is closely related to [state]($func/state) function. -/// Read its documentation for more details on state management in Typst and -/// why it doesn't just use normal variables for counters. -/// -/// ## Methods -/// ### display() -/// Displays the value of the counter. -/// -/// - numbering: string or function (positional) -/// A [numbering pattern or a function]($func/numbering), which specifies how -/// to display the counter. If given a function, that function receives each -/// number of the counter as a separate argument. If the amount of numbers -/// varies, e.g. for the heading argument, you can use an -/// [argument sink]($type/arguments). -/// -/// If this is omitted, displays the counter with the numbering style for the -/// counted element or with the pattern `{"1.1"}` if no such style exists. -/// -/// - both: boolean (named) -/// If enabled, displays the current and final top-level count together. Both -/// can be styled through a single numbering pattern. This is used by the page -/// numbering property to display the current and total number of pages when a -/// pattern like `{"1 / 1"}` is given. -/// -/// - returns: content -/// -/// ### step() -/// Increases the value of the counter by one. -/// -/// The update will be in effect at the position where the returned content is -/// inserted into the document. If you don't put the output into the document, -/// nothing happens! This would be the case, for example, if you write -/// `{let _ = counter(page).step()}`. Counter updates are always applied in -/// layout order and in that case, Typst wouldn't know when to step the counter. -/// -/// - level: integer (named) -/// The depth at which to step the counter. Defaults to `{1}`. -/// -/// - returns: content -/// -/// ### update() -/// Updates the value of the counter. -/// -/// Just like with `step`, the update only occurs if you put the resulting -/// content into the document. -/// -/// - value: integer or array or function (positional, required) -/// If given an integer or array of integers, sets the counter to that value. -/// If given a function, that function receives the previous counter value -/// (with each number as a separate argument) and has to return the new -/// value (integer or array). -/// -/// - returns: content -/// -/// ### at() -/// Gets the value of the counter at the given location. Always returns an -/// array of integers, even if the counter has just one number. -/// -/// - location: location (positional, required) -/// The location at which the counter value should be retrieved. A suitable -/// location can be retrieved from [`locate`]($func/locate) or -/// [`query`]($func/query). -/// -/// - returns: array -/// -/// ### final() -/// Gets the value of the counter at the end of the document. Always returns an -/// array of integers, even if the counter has just one number. -/// -/// - location: location (positional, required) -/// Can be any location. Why is it required then? Typst has to evaluate parts -/// of your code multiple times to determine all counter values. By only -/// allowing this method within [`locate`]($func/locate) calls, the amount of -/// code that can depend on the method's result is reduced. If you could call -/// `final` directly at the top level of a module, the evaluation of the whole -/// module and its exports could depend on the counter's value. -/// -/// - returns: array -/// -/// Display: Counter -/// Category: meta -#[func] -pub fn counter( - /// The key that identifies this counter. - /// - /// - If it is a string, creates a custom counter that is only affected by - /// manual updates, - /// - If this is a `{