diff options
Diffstat (limited to 'crates/typst-library')
88 files changed, 0 insertions, 29178 deletions
diff --git a/crates/typst-library/Cargo.toml b/crates/typst-library/Cargo.toml deleted file mode 100644 index 426c56f3..00000000 --- a/crates/typst-library/Cargo.toml +++ /dev/null @@ -1,53 +0,0 @@ -[package] -name = "typst-library" -description = "The standard library for Typst." -version.workspace = true -rust-version.workspace = true -authors.workspace = true -edition.workspace = true -homepage.workspace = true -repository.workspace = true -license.workspace = true -categories.workspace = true -keywords.workspace = true - -[lib] -test = false -doctest = false -bench = false - -[dependencies] -typst = { workspace = true } -az = { workspace = true } -chinese-number = { workspace = true } -ciborium = { workspace = true } -comemo = { workspace = true } -csv = { workspace = true } -ecow = { workspace = true } -hayagriva = { workspace = true } -hypher = { workspace = true } -icu_properties = { workspace = true } -icu_provider = { workspace = true } -icu_provider_adapters = { workspace = true } -icu_provider_blob = { workspace = true } -icu_segmenter = { workspace = true } -indexmap = { workspace = true } -kurbo = { workspace = true } -lipsum = { workspace = true } -log = { workspace = true } -once_cell = { workspace = true } -roxmltree = { workspace = true } -rustybuzz = { workspace = true } -serde_json = { workspace = true } -serde_yaml = { workspace = true } -smallvec = { workspace = true } -syntect = { workspace = true } -time = { workspace = true } -toml = { workspace = true, features = ["display"] } -tracing = { workspace = true } -ttf-parser = { workspace = true } -typed-arena = { workspace = true } -unicode-bidi = { workspace = true } -unicode-math-class = { workspace = true } -unicode-script = { workspace = true } -unicode-segmentation = { workspace = true } diff --git a/crates/typst-library/assets/cj_linebreak_data.postcard b/crates/typst-library/assets/cj_linebreak_data.postcard Binary files differdeleted file mode 100644 index 910dd167..00000000 --- a/crates/typst-library/assets/cj_linebreak_data.postcard +++ /dev/null diff --git a/crates/typst-library/assets/icudata.postcard b/crates/typst-library/assets/icudata.postcard Binary files differdeleted file mode 100644 index a1fdbd48..00000000 --- a/crates/typst-library/assets/icudata.postcard +++ /dev/null diff --git a/crates/typst-library/assets/syntect.bin b/crates/typst-library/assets/syntect.bin Binary files differdeleted file mode 100644 index 043602a4..00000000 --- a/crates/typst-library/assets/syntect.bin +++ /dev/null diff --git a/crates/typst-library/src/compute/calc.rs b/crates/typst-library/src/compute/calc.rs deleted file mode 100644 index 6276905c..00000000 --- a/crates/typst-library/src/compute/calc.rs +++ /dev/null @@ -1,949 +0,0 @@ -//! Calculations and processing of numeric values. - -use std::cmp; -use std::cmp::Ordering; -use std::ops::{Div, Rem}; - -use typst::eval::{Module, Scope}; - -use crate::prelude::*; - -/// Hook up all calculation definitions. -pub(super) fn define(global: &mut Scope) { - global.category("calculate"); - global.define_module(module()); -} - -/// A module with calculation definitions. -fn module() -> Module { - let mut scope = Scope::new(); - scope.category("calculate"); - scope.define_func::<abs>(); - scope.define_func::<pow>(); - scope.define_func::<exp>(); - scope.define_func::<sqrt>(); - scope.define_func::<sin>(); - scope.define_func::<cos>(); - scope.define_func::<tan>(); - scope.define_func::<asin>(); - scope.define_func::<acos>(); - scope.define_func::<atan>(); - scope.define_func::<atan2>(); - scope.define_func::<sinh>(); - scope.define_func::<cosh>(); - scope.define_func::<tanh>(); - scope.define_func::<log>(); - scope.define_func::<ln>(); - scope.define_func::<fact>(); - scope.define_func::<perm>(); - scope.define_func::<binom>(); - scope.define_func::<gcd>(); - scope.define_func::<lcm>(); - scope.define_func::<floor>(); - scope.define_func::<ceil>(); - scope.define_func::<trunc>(); - scope.define_func::<fract>(); - scope.define_func::<round>(); - scope.define_func::<clamp>(); - scope.define_func::<min>(); - scope.define_func::<max>(); - scope.define_func::<even>(); - scope.define_func::<odd>(); - scope.define_func::<rem>(); - scope.define_func::<div_euclid>(); - scope.define_func::<rem_euclid>(); - scope.define_func::<quo>(); - scope.define("inf", f64::INFINITY); - scope.define("nan", f64::NAN); - scope.define("pi", std::f64::consts::PI); - scope.define("tau", std::f64::consts::TAU); - scope.define("e", std::f64::consts::E); - Module::new("calc", scope) -} - -/// Calculates the absolute value of a numeric value. -/// -/// ```example -/// #calc.abs(-5) \ -/// #calc.abs(5pt - 2cm) \ -/// #calc.abs(2fr) -/// ``` -#[func(title = "Absolute")] -pub fn abs( - /// The value whose absolute value to calculate. - value: ToAbs, -) -> Value { - value.0 -} - -/// A value of which the absolute value can be taken. -pub struct ToAbs(Value); - -cast! { - ToAbs, - v: i64 => Self(v.abs().into_value()), - v: f64 => Self(v.abs().into_value()), - v: Length => Self(Value::Length(v.try_abs() - .ok_or("cannot take absolute value of this length")?)), - v: Angle => Self(Value::Angle(v.abs())), - v: Ratio => Self(Value::Ratio(v.abs())), - v: Fr => Self(Value::Fraction(v.abs())), -} - -/// Raises a value to some exponent. -/// -/// ```example -/// #calc.pow(2, 3) -/// ``` -#[func(title = "Power")] -pub fn pow( - /// The callsite span. - span: Span, - /// The base of the power. - base: Num, - /// The exponent of the power. - exponent: Spanned<Num>, -) -> SourceResult<Num> { - match exponent.v { - _ if exponent.v.float() == 0.0 && base.float() == 0.0 => { - bail!(span, "zero to the power of zero is undefined") - } - Num::Int(i) if i32::try_from(i).is_err() => { - bail!(exponent.span, "exponent is too large") - } - Num::Float(f) if !f.is_normal() && f != 0.0 => { - bail!(exponent.span, "exponent may not be infinite, subnormal, or NaN") - } - _ => {} - }; - - let result = match (base, exponent.v) { - (Num::Int(a), Num::Int(b)) if b >= 0 => a - .checked_pow(b as u32) - .map(Num::Int) - .ok_or("the result is too large") - .at(span)?, - (a, b) => Num::Float(if a.float() == std::f64::consts::E { - b.float().exp() - } else if a.float() == 2.0 { - b.float().exp2() - } else if let Num::Int(b) = b { - a.float().powi(b as i32) - } else { - a.float().powf(b.float()) - }), - }; - - if result.float().is_nan() { - bail!(span, "the result is not a real number") - } - - Ok(result) -} - -/// Raises a value to some exponent of e. -/// -/// ```example -/// #calc.exp(1) -/// ``` -#[func(title = "Exponential")] -pub fn exp( - /// The callsite span. - span: Span, - /// The exponent of the power. - exponent: Spanned<Num>, -) -> SourceResult<f64> { - match exponent.v { - Num::Int(i) if i32::try_from(i).is_err() => { - bail!(exponent.span, "exponent is too large") - } - Num::Float(f) if !f.is_normal() && f != 0.0 => { - bail!(exponent.span, "exponent may not be infinite, subnormal, or NaN") - } - _ => {} - }; - - let result = exponent.v.float().exp(); - if result.is_nan() { - bail!(span, "the result is not a real number") - } - - Ok(result) -} - -/// Calculates the square root of a number. -/// -/// ```example -/// #calc.sqrt(16) \ -/// #calc.sqrt(2.5) -/// ``` -#[func(title = "Square Root")] -pub fn sqrt( - /// The number whose square root to calculate. Must be non-negative. - value: Spanned<Num>, -) -> SourceResult<f64> { - if value.v.float() < 0.0 { - bail!(value.span, "cannot take square root of negative number"); - } - Ok(value.v.float().sqrt()) -} - -/// Calculates the sine of an angle. -/// -/// When called with an integer or a float, they will be interpreted as -/// radians. -/// -/// ```example -/// #assert(calc.sin(90deg) == calc.sin(-270deg)) -/// #calc.sin(1.5) \ -/// #calc.sin(90deg) -/// ``` -#[func(title = "Sine")] -pub fn sin( - /// The angle whose sine to calculate. - angle: AngleLike, -) -> f64 { - match angle { - AngleLike::Angle(a) => a.sin(), - AngleLike::Int(n) => (n as f64).sin(), - AngleLike::Float(n) => n.sin(), - } -} - -/// Calculates the cosine of an angle. -/// -/// When called with an integer or a float, they will be interpreted as -/// radians. -/// -/// ```example -/// #calc.cos(90deg) \ -/// #calc.cos(1.5) \ -/// #calc.cos(90deg) -/// ``` -#[func(title = "Cosine")] -pub fn cos( - /// The angle whose cosine to calculate. - angle: AngleLike, -) -> f64 { - match angle { - AngleLike::Angle(a) => a.cos(), - AngleLike::Int(n) => (n as f64).cos(), - AngleLike::Float(n) => n.cos(), - } -} - -/// Calculates the tangent of an angle. -/// -/// When called with an integer or a float, they will be interpreted as -/// radians. -/// -/// ```example -/// #calc.tan(1.5) \ -/// #calc.tan(90deg) -/// ``` -#[func(title = "Tangent")] -pub fn tan( - /// The angle whose tangent to calculate. - angle: AngleLike, -) -> f64 { - match angle { - AngleLike::Angle(a) => a.tan(), - AngleLike::Int(n) => (n as f64).tan(), - AngleLike::Float(n) => n.tan(), - } -} - -/// Calculates the arcsine of a number. -/// -/// ```example -/// #calc.asin(0) \ -/// #calc.asin(1) -/// ``` -#[func(title = "Arcsine")] -pub fn asin( - /// The number whose arcsine to calculate. Must be between -1 and 1. - value: Spanned<Num>, -) -> SourceResult<Angle> { - let val = value.v.float(); - if val < -1.0 || val > 1.0 { - bail!(value.span, "value must be between -1 and 1"); - } - Ok(Angle::rad(val.asin())) -} - -/// Calculates the arccosine of a number. -/// -/// ```example -/// #calc.acos(0) \ -/// #calc.acos(1) -/// ``` -#[func(title = "Arccosine")] -pub fn acos( - /// The number whose arcsine to calculate. Must be between -1 and 1. - value: Spanned<Num>, -) -> SourceResult<Angle> { - let val = value.v.float(); - if val < -1.0 || val > 1.0 { - bail!(value.span, "value must be between -1 and 1"); - } - Ok(Angle::rad(val.acos())) -} - -/// Calculates the arctangent of a number. -/// -/// ```example -/// #calc.atan(0) \ -/// #calc.atan(1) -/// ``` -#[func(title = "Arctangent")] -pub fn atan( - /// The number whose arctangent to calculate. - value: Num, -) -> Angle { - Angle::rad(value.float().atan()) -} - -/// Calculates the four-quadrant arctangent of a coordinate. -/// -/// The arguments are `(x, y)`, not `(y, x)`. -/// -/// ```example -/// #calc.atan2(1, 1) \ -/// #calc.atan2(-2, -3) -/// ``` -#[func(title = "Four-quadrant Arctangent")] -pub fn atan2( - /// The X coordinate. - x: Num, - /// The Y coordinate. - y: Num, -) -> Angle { - Angle::rad(f64::atan2(y.float(), x.float())) -} - -/// Calculates the hyperbolic sine of a hyperbolic angle. -/// -/// ```example -/// #calc.sinh(0) \ -/// #calc.sinh(1.5) -/// ``` -#[func(title = "Hyperbolic Sine")] -pub fn sinh( - /// The hyperbolic angle whose hyperbolic sine to calculate. - value: f64, -) -> f64 { - value.sinh() -} - -/// Calculates the hyperbolic cosine of a hyperbolic angle. -/// -/// ```example -/// #calc.cosh(0) \ -/// #calc.cosh(1.5) -/// ``` -#[func(title = "Hyperbolic Cosine")] -pub fn cosh( - /// The hyperbolic angle whose hyperbolic cosine to calculate. - value: f64, -) -> f64 { - value.cosh() -} - -/// Calculates the hyperbolic tangent of an hyperbolic angle. -/// -/// ```example -/// #calc.tanh(0) \ -/// #calc.tanh(1.5) -/// ``` -#[func(title = "Hyperbolic Tangent")] -pub fn tanh( - /// The hyperbolic angle whose hyperbolic tangent to calculate. - value: f64, -) -> f64 { - value.tanh() -} - -/// Calculates the logarithm of a number. -/// -/// If the base is not specified, the logarithm is calculated in base 10. -/// -/// ```example -/// #calc.log(100) -/// ``` -#[func(title = "Logarithm")] -pub fn log( - /// The callsite span. - span: Span, - /// The number whose logarithm to calculate. Must be strictly positive. - value: Spanned<Num>, - /// The base of the logarithm. May not be zero. - #[named] - #[default(Spanned::new(10.0, Span::detached()))] - base: Spanned<f64>, -) -> SourceResult<f64> { - let number = value.v.float(); - if number <= 0.0 { - bail!(value.span, "value must be strictly positive") - } - - if !base.v.is_normal() { - bail!(base.span, "base may not be zero, NaN, infinite, or subnormal") - } - - let result = if base.v == std::f64::consts::E { - number.ln() - } else if base.v == 2.0 { - number.log2() - } else if base.v == 10.0 { - number.log10() - } else { - number.log(base.v) - }; - - if result.is_infinite() || result.is_nan() { - bail!(span, "the result is not a real number") - } - - Ok(result) -} - -/// Calculates the natural logarithm of a number. -/// -/// ```example -/// #calc.ln(calc.e) -/// ``` -#[func(title = "Natural Logarithm")] -pub fn ln( - /// The callsite span. - span: Span, - /// The number whose logarithm to calculate. Must be strictly positive. - value: Spanned<Num>, -) -> SourceResult<f64> { - let number = value.v.float(); - if number <= 0.0 { - bail!(value.span, "value must be strictly positive") - } - - let result = number.ln(); - if result.is_infinite() { - bail!(span, "result close to -inf") - } - - Ok(result) -} - -/// Calculates the factorial of a number. -/// -/// ```example -/// #calc.fact(5) -/// ``` -#[func(title = "Factorial")] -pub fn fact( - /// The number whose factorial to calculate. Must be non-negative. - number: u64, -) -> StrResult<i64> { - Ok(fact_impl(1, number).ok_or("the result is too large")?) -} - -/// Calculates a permutation. -/// -/// Returns the `k`-permutation of `n`, or the number of ways to choose `k` -/// items from a set of `n` with regard to order. -/// -/// ```example -/// $ "perm"(n, k) &= n!/((n - k)!) \ -/// "perm"(5, 3) &= #calc.perm(5, 3) $ -/// ``` -#[func(title = "Permutation")] -pub fn perm( - /// The base number. Must be non-negative. - base: u64, - /// The number of permutations. Must be non-negative. - numbers: u64, -) -> StrResult<i64> { - // By convention. - if base < numbers { - return Ok(0); - } - - Ok(fact_impl(base - numbers + 1, base).ok_or("the result is too large")?) -} - -/// Calculates the product of a range of numbers. Used to calculate -/// permutations. Returns None if the result is larger than `i64::MAX` -fn fact_impl(start: u64, end: u64) -> Option<i64> { - // By convention - if end + 1 < start { - return Some(0); - } - - let real_start: u64 = cmp::max(1, start); - let mut count: u64 = 1; - for i in real_start..=end { - count = count.checked_mul(i)?; - } - - count.try_into().ok() -} - -/// Calculates a binomial coefficient. -/// -/// Returns the `k`-combination of `n`, or the number of ways to choose `k` -/// items from a set of `n` without regard to order. -/// -/// ```example -/// #calc.binom(10, 5) -/// ``` -#[func(title = "Binomial")] -pub fn binom( - /// The upper coefficient. Must be non-negative. - n: u64, - /// The lower coefficient. Must be non-negative. - k: u64, -) -> StrResult<i64> { - Ok(binom_impl(n, k).ok_or("the result is too large")?) -} - -/// Calculates a binomial coefficient, with `n` the upper coefficient and `k` -/// the lower coefficient. Returns `None` if the result is larger than -/// `i64::MAX` -fn binom_impl(n: u64, k: u64) -> Option<i64> { - if k > n { - return Some(0); - } - - // By symmetry - let real_k = cmp::min(n - k, k); - if real_k == 0 { - return Some(1); - } - - let mut result: u64 = 1; - for i in 0..real_k { - result = result.checked_mul(n - i)?.checked_div(i + 1)?; - } - - result.try_into().ok() -} - -/// Calculates the greatest common divisor of two integers. -/// -/// ```example -/// #calc.gcd(7, 42) -/// ``` -#[func(title = "Greatest Common Divisor")] -pub fn gcd( - /// The first integer. - a: i64, - /// The second integer. - b: i64, -) -> i64 { - let (mut a, mut b) = (a, b); - while b != 0 { - let temp = b; - b = a % b; - a = temp; - } - - a.abs() -} - -/// Calculates the least common multiple of two integers. -/// -/// ```example -/// #calc.lcm(96, 13) -/// ``` -#[func(title = "Least Common Multiple")] -pub fn lcm( - /// The first integer. - a: i64, - /// The second integer. - b: i64, -) -> StrResult<i64> { - if a == b { - return Ok(a.abs()); - } - - Ok(a.checked_div(gcd(a, b)) - .and_then(|gcd| gcd.checked_mul(b)) - .map(|v| v.abs()) - .ok_or("the return value is too large")?) -} - -/// Rounds a number down to the nearest integer. -/// -/// If the number is already an integer, it is returned unchanged. -/// -/// ```example -/// #assert(calc.floor(3.14) == 3) -/// #assert(calc.floor(3) == 3) -/// #calc.floor(500.1) -/// ``` -#[func] -pub fn floor( - /// The number to round down. - value: Num, -) -> i64 { - match value { - Num::Int(n) => n, - Num::Float(n) => n.floor() as i64, - } -} - -/// Rounds a number up to the nearest integer. -/// -/// If the number is already an integer, it is returned unchanged. -/// -/// ```example -/// #assert(calc.ceil(3.14) == 4) -/// #assert(calc.ceil(3) == 3) -/// #calc.ceil(500.1) -/// ``` -#[func] -pub fn ceil( - /// The number to round up. - value: Num, -) -> i64 { - match value { - Num::Int(n) => n, - Num::Float(n) => n.ceil() as i64, - } -} - -/// Returns the integer part of a number. -/// -/// If the number is already an integer, it is returned unchanged. -/// -/// ```example -/// #assert(calc.trunc(3) == 3) -/// #assert(calc.trunc(-3.7) == -3) -/// #calc.trunc(15.9) -/// ``` -#[func(title = "Truncate")] -pub fn trunc( - /// The number to truncate. - value: Num, -) -> i64 { - match value { - Num::Int(n) => n, - Num::Float(n) => n.trunc() as i64, - } -} - -/// Returns the fractional part of a number. -/// -/// If the number is an integer, returns `0`. -/// -/// ```example -/// #assert(calc.fract(3) == 0) -/// #calc.fract(-3.1) -/// ``` -#[func(title = "Fractional")] -pub fn fract( - /// The number to truncate. - value: Num, -) -> Num { - match value { - Num::Int(_) => Num::Int(0), - Num::Float(n) => Num::Float(n.fract()), - } -} - -/// Rounds a number to the nearest integer. -/// -/// Optionally, a number of decimal places can be specified. -/// -/// ```example -/// #assert(calc.round(3.14) == 3) -/// #assert(calc.round(3.5) == 4) -/// #calc.round(3.1415, digits: 2) -/// ``` -#[func] -pub fn round( - /// The number to round. - value: Num, - /// The number of decimal places. - #[named] - #[default(0)] - digits: i64, -) -> Num { - match value { - Num::Int(n) if digits == 0 => Num::Int(n), - _ => { - let n = value.float(); - let factor = 10.0_f64.powi(digits as i32); - Num::Float((n * factor).round() / factor) - } - } -} - -/// Clamps a number between a minimum and maximum value. -/// -/// ```example -/// #assert(calc.clamp(5, 0, 10) == 5) -/// #assert(calc.clamp(5, 6, 10) == 6) -/// #calc.clamp(5, 0, 4) -/// ``` -#[func] -pub fn clamp( - /// The number to clamp. - value: Num, - /// The inclusive minimum value. - min: Num, - /// The inclusive maximum value. - max: Spanned<Num>, -) -> SourceResult<Num> { - if max.v.float() < min.float() { - bail!(max.span, "max must be greater than or equal to min") - } - Ok(value.apply3(min, max.v, i64::clamp, f64::clamp)) -} - -/// Determines the minimum of a sequence of values. -/// -/// ```example -/// #calc.min(1, -3, -5, 20, 3, 6) \ -/// #calc.min("typst", "in", "beta") -/// ``` -#[func(title = "Minimum")] -pub fn min( - /// The callsite span. - span: Span, - /// The sequence of values from which to extract the minimum. - /// Must not be empty. - #[variadic] - values: Vec<Spanned<Value>>, -) -> SourceResult<Value> { - minmax(span, values, Ordering::Less) -} - -/// Determines the maximum of a sequence of values. -/// -/// ```example -/// #calc.max(1, -3, -5, 20, 3, 6) \ -/// #calc.max("typst", "in", "beta") -/// ``` -#[func(title = "Maximum")] -pub fn max( - /// The callsite span. - span: Span, - /// The sequence of values from which to extract the maximum. - /// Must not be empty. - #[variadic] - values: Vec<Spanned<Value>>, -) -> SourceResult<Value> { - minmax(span, values, Ordering::Greater) -} - -/// Find the minimum or maximum of a sequence of values. -fn minmax( - span: Span, - values: Vec<Spanned<Value>>, - goal: Ordering, -) -> SourceResult<Value> { - let mut iter = values.into_iter(); - let Some(Spanned { v: mut extremum, .. }) = iter.next() else { - bail!(span, "expected at least one value"); - }; - - for Spanned { v, span } in iter { - let ordering = typst::eval::ops::compare(&v, &extremum).at(span)?; - if ordering == goal { - extremum = v; - } - } - - Ok(extremum) -} - -/// Determines whether an integer is even. -/// -/// ```example -/// #calc.even(4) \ -/// #calc.even(5) \ -/// #range(10).filter(calc.even) -/// ``` -#[func] -pub fn even( - /// The number to check for evenness. - value: i64, -) -> bool { - value % 2 == 0 -} - -/// Determines whether an integer is odd. -/// -/// ```example -/// #calc.odd(4) \ -/// #calc.odd(5) \ -/// #range(10).filter(calc.odd) -/// ``` -#[func] -pub fn odd( - /// The number to check for oddness. - value: i64, -) -> bool { - value % 2 != 0 -} - -/// Calculates the remainder of two numbers. -/// -/// The value `calc.rem(x, y)` always has the same sign as `x`, and is smaller -/// in magnitude than `y`. -/// -/// ```example -/// #calc.rem(7, 3) \ -/// #calc.rem(7, -3) \ -/// #calc.rem(-7, 3) \ -/// #calc.rem(-7, -3) \ -/// #calc.rem(1.75, 0.5) -/// ``` -#[func(title = "Remainder")] -pub fn rem( - /// The dividend of the remainder. - dividend: Num, - /// The divisor of the remainder. - divisor: Spanned<Num>, -) -> SourceResult<Num> { - if divisor.v.float() == 0.0 { - bail!(divisor.span, "divisor must not be zero"); - } - Ok(dividend.apply2(divisor.v, Rem::rem, Rem::rem)) -} - -/// Performs euclidean division of two numbers. -/// -/// The result of this computation is that of a division rounded to the integer -/// `{n}` such that the dividend is greater than or equal to `{n}` times the divisor. -/// -/// ```example -/// #calc.div-euclid(7, 3) \ -/// #calc.div-euclid(7, -3) \ -/// #calc.div-euclid(-7, 3) \ -/// #calc.div-euclid(-7, -3) \ -/// #calc.div-euclid(1.75, 0.5) -/// ``` -#[func(title = "Euclidean Division")] -pub fn div_euclid( - /// The dividend of the division. - dividend: Num, - /// The divisor of the division. - divisor: Spanned<Num>, -) -> SourceResult<Num> { - if divisor.v.float() == 0.0 { - bail!(divisor.span, "divisor must not be zero"); - } - Ok(dividend.apply2(divisor.v, i64::div_euclid, f64::div_euclid)) -} - -/// This calculates the least nonnegative remainder of a division. -/// -/// Warning: Due to a floating point round-off error, the remainder may equal the absolute -/// value of the divisor if the dividend is much smaller in magnitude than the divisor -/// and the dividend is negative. This only applies for floating point inputs. -/// -/// ```example -/// #calc.rem-euclid(7, 3) \ -/// #calc.rem-euclid(7, -3) \ -/// #calc.rem-euclid(-7, 3) \ -/// #calc.rem-euclid(-7, -3) \ -/// #calc.rem(1.75, 0.5) -/// ``` -#[func(title = "Euclidean Remainder")] -pub fn rem_euclid( - /// The dividend of the remainder. - dividend: Num, - /// The divisor of the remainder. - divisor: Spanned<Num>, -) -> SourceResult<Num> { - if divisor.v.float() == 0.0 { - bail!(divisor.span, "divisor must not be zero"); - } - Ok(dividend.apply2(divisor.v, i64::rem_euclid, f64::rem_euclid)) -} - -/// Calculates the quotient (floored division) of two numbers. -/// -/// ```example -/// $ "quo"(a, b) &= floor(a/b) \ -/// "quo"(14, 5) &= #calc.quo(14, 5) \ -/// "quo"(3.46, 0.5) &= #calc.quo(3.46, 0.5) $ -/// ``` -#[func(title = "Quotient")] -pub fn quo( - /// The dividend of the quotient. - dividend: Num, - /// The divisor of the quotient. - divisor: Spanned<Num>, -) -> SourceResult<i64> { - if divisor.v.float() == 0.0 { - bail!(divisor.span, "divisor must not be zero"); - } - - Ok(floor(dividend.apply2(divisor.v, Div::div, Div::div))) -} - -/// A value which can be passed to functions that work with integers and floats. -#[derive(Debug, Copy, Clone)] -pub enum Num { - Int(i64), - Float(f64), -} - -impl Num { - fn apply2( - self, - other: Self, - int: impl FnOnce(i64, i64) -> i64, - float: impl FnOnce(f64, f64) -> f64, - ) -> Num { - match (self, other) { - (Self::Int(a), Self::Int(b)) => Num::Int(int(a, b)), - (a, b) => Num::Float(float(a.float(), b.float())), - } - } - - fn apply3( - self, - other: Self, - third: Self, - int: impl FnOnce(i64, i64, i64) -> i64, - float: impl FnOnce(f64, f64, f64) -> f64, - ) -> Num { - match (self, other, third) { - (Self::Int(a), Self::Int(b), Self::Int(c)) => Num::Int(int(a, b, c)), - (a, b, c) => Num::Float(float(a.float(), b.float(), c.float())), - } - } - - fn float(self) -> f64 { - match self { - Self::Int(v) => v as f64, - Self::Float(v) => v, - } - } -} - -cast! { - Num, - self => match self { - Self::Int(v) => v.into_value(), - Self::Float(v) => v.into_value(), - }, - v: i64 => Self::Int(v), - v: f64 => Self::Float(v), -} - -/// A value that can be passed to a trigonometric function. -pub enum AngleLike { - Int(i64), - Float(f64), - Angle(Angle), -} - -cast! { - AngleLike, - v: i64 => Self::Int(v), - v: f64 => Self::Float(v), - v: Angle => Self::Angle(v), -} diff --git a/crates/typst-library/src/compute/data.rs b/crates/typst-library/src/compute/data.rs deleted file mode 100644 index e4767ebf..00000000 --- a/crates/typst-library/src/compute/data.rs +++ /dev/null @@ -1,609 +0,0 @@ -use typst::diag::{format_xml_like_error, FileError}; -use typst::eval::Bytes; -use typst::syntax::is_newline; - -use crate::prelude::*; - -/// Hook up all data loading definitions. -pub(super) fn define(global: &mut Scope) { - global.category("data-loading"); - global.define_func::<read>(); - global.define_func::<csv>(); - global.define_func::<json>(); - global.define_func::<toml>(); - global.define_func::<yaml>(); - global.define_func::<cbor>(); - global.define_func::<xml>(); -} - -/// Reads plain text or data from a file. -/// -/// By default, the file will be read as UTF-8 and returned as a [string]($str). -/// -/// If you specify `{encoding: none}`, this returns raw [bytes]($bytes) instead. -/// -/// # Example -/// ```example -/// An example for a HTML file: \ -/// #let text = read("data.html") -/// #raw(text, lang: "html") -/// -/// Raw bytes: -/// #read("tiger.jpg", encoding: none) -/// ``` -#[func] -pub fn read( - /// The virtual machine. - vm: &mut Vm, - /// Path to a file. - path: Spanned<EcoString>, - /// The encoding to read the file with. - /// - /// If set to `{none}`, this function returns raw bytes. - #[named] - #[default(Some(Encoding::Utf8))] - encoding: Option<Encoding>, -) -> SourceResult<Readable> { - let Spanned { v: path, span } = path; - let id = vm.resolve_path(&path).at(span)?; - let data = vm.world().file(id).at(span)?; - Ok(match encoding { - None => Readable::Bytes(data), - Some(Encoding::Utf8) => Readable::Str( - std::str::from_utf8(&data) - .map_err(|_| "file is not valid utf-8") - .at(span)? - .into(), - ), - }) -} - -/// An encoding of a file. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)] -pub enum Encoding { - /// The Unicode UTF-8 encoding. - Utf8, -} - -/// A value that can be read from a file. -#[derive(Debug, Clone, PartialEq, Hash)] -pub enum Readable { - /// A decoded string. - Str(Str), - /// Raw bytes. - Bytes(Bytes), -} - -impl Readable { - fn as_slice(&self) -> &[u8] { - match self { - Readable::Bytes(v) => v, - Readable::Str(v) => v.as_bytes(), - } - } -} - -cast! { - Readable, - self => match self { - Self::Str(v) => v.into_value(), - Self::Bytes(v) => v.into_value(), - }, - v: Str => Self::Str(v), - v: Bytes => Self::Bytes(v), -} - -impl From<Readable> for Bytes { - fn from(value: Readable) -> Self { - match value { - Readable::Bytes(v) => v, - Readable::Str(v) => v.as_bytes().into(), - } - } -} - -/// Reads structured data from a CSV file. -/// -/// The CSV file will be read and parsed into a 2-dimensional array of strings: -/// Each row in the CSV file will be represented as an array of strings, and all -/// rows will be collected into a single array. Header rows will not be -/// stripped. -/// -/// # Example -/// ```example -/// #let results = csv("data.csv") -/// -/// #table( -/// columns: 2, -/// [*Condition*], [*Result*], -/// ..results.flatten(), -/// ) -/// ``` -#[func(scope, title = "CSV")] -pub fn csv( - /// The virtual machine. - vm: &mut Vm, - /// Path to a CSV file. - path: Spanned<EcoString>, - /// The delimiter that separates columns in the CSV file. - /// Must be a single ASCII character. - #[named] - #[default] - delimiter: Delimiter, -) -> SourceResult<Array> { - let Spanned { v: path, span } = path; - let id = vm.resolve_path(&path).at(span)?; - let data = vm.world().file(id).at(span)?; - self::csv::decode(Spanned::new(Readable::Bytes(data), span), delimiter) -} - -#[scope] -impl csv { - /// Reads structured data from a CSV string/bytes. - #[func(title = "Decode CSV")] - pub fn decode( - /// CSV data. - data: Spanned<Readable>, - /// The delimiter that separates columns in the CSV file. - /// Must be a single ASCII character. - #[named] - #[default] - delimiter: Delimiter, - ) -> SourceResult<Array> { - let Spanned { v: data, span } = data; - let mut builder = ::csv::ReaderBuilder::new(); - builder.has_headers(false); - builder.delimiter(delimiter.0 as u8); - let mut reader = builder.from_reader(data.as_slice()); - let mut array = Array::new(); - - for (line, result) in reader.records().enumerate() { - // Original solution use line from error, but that is incorrect with - // `has_headers` set to `false`. See issue: - // https://github.com/BurntSushi/rust-csv/issues/184 - let line = line + 1; // Counting lines from 1 - let row = result.map_err(|err| format_csv_error(err, line)).at(span)?; - let sub = row.into_iter().map(|field| field.into_value()).collect(); - array.push(Value::Array(sub)) - } - - Ok(array) - } -} - -/// The delimiter to use when parsing CSV files. -pub struct Delimiter(char); - -impl Default for Delimiter { - fn default() -> Self { - Self(',') - } -} - -cast! { - Delimiter, - self => self.0.into_value(), - v: EcoString => { - let mut chars = v.chars(); - let first = chars.next().ok_or("delimiter must not be empty")?; - if chars.next().is_some() { - bail!("delimiter must be a single character"); - } - - if !first.is_ascii() { - bail!("delimiter must be an ASCII character"); - } - - Self(first) - }, -} - -/// Format the user-facing CSV error message. -fn format_csv_error(err: ::csv::Error, line: usize) -> EcoString { - match err.kind() { - ::csv::ErrorKind::Utf8 { .. } => "file is not valid utf-8".into(), - ::csv::ErrorKind::UnequalLengths { expected_len, len, .. } => { - eco_format!( - "failed to parse CSV (found {len} instead of \ - {expected_len} fields in line {line})" - ) - } - _ => eco_format!("failed to parse CSV ({err})"), - } -} - -/// Reads structured data from a JSON file. -/// -/// The file must contain a valid JSON object or array. JSON objects will be -/// converted into Typst dictionaries, and JSON arrays will be converted into -/// Typst arrays. Strings and booleans will be converted into the Typst -/// equivalents, `null` will be converted into `{none}`, and numbers will be -/// converted to floats or integers depending on whether they are whole numbers. -/// -/// The function returns a dictionary or an array, depending on the JSON file. -/// -/// The JSON files in the example contain objects with the keys `temperature`, -/// `unit`, and `weather`. -/// -/// # Example -/// ```example -/// #let forecast(day) = block[ -/// #box(square( -/// width: 2cm, -/// inset: 8pt, -/// fill: if day.weather == "sunny" { -/// yellow -/// } else { -/// aqua -/// }, -/// align( -/// bottom + right, -/// strong(day.weather), -/// ), -/// )) -/// #h(6pt) -/// #set text(22pt, baseline: -8pt) -/// #day.temperature °#day.unit -/// ] -/// -/// #forecast(json("monday.json")) -/// #forecast(json("tuesday.json")) -/// ``` -#[func(scope, title = "JSON")] -pub fn json( - /// The virtual machine. - vm: &mut Vm, - /// Path to a JSON file. - path: Spanned<EcoString>, -) -> SourceResult<Value> { - let Spanned { v: path, span } = path; - let id = vm.resolve_path(&path).at(span)?; - let data = vm.world().file(id).at(span)?; - json::decode(Spanned::new(Readable::Bytes(data), span)) -} - -#[scope] -impl json { - /// Reads structured data from a JSON string/bytes. - #[func(title = "Decode JSON")] - pub fn decode( - /// JSON data. - data: Spanned<Readable>, - ) -> SourceResult<Value> { - let Spanned { v: data, span } = data; - serde_json::from_slice(data.as_slice()) - .map_err(|err| eco_format!("failed to parse JSON ({err})")) - .at(span) - } - - /// Encodes structured data into a JSON string. - #[func(title = "Encode JSON")] - pub fn encode( - /// Value to be encoded. - value: Spanned<Value>, - /// Whether to pretty print the JSON with newlines and indentation. - #[named] - #[default(true)] - pretty: bool, - ) -> SourceResult<Str> { - let Spanned { v: value, span } = value; - if pretty { - serde_json::to_string_pretty(&value) - } else { - serde_json::to_string(&value) - } - .map(|v| v.into()) - .map_err(|err| eco_format!("failed to encode value as JSON ({err})")) - .at(span) - } -} - -/// Reads structured data from a TOML file. -/// -/// The file must contain a valid TOML table. TOML tables will be converted into -/// Typst dictionaries, and TOML arrays will be converted into Typst arrays. -/// Strings, booleans and datetimes will be converted into the Typst equivalents -/// and numbers will be converted to floats or integers depending on whether -/// they are whole numbers. -/// -/// The TOML file in the example consists of a table with the keys `title`, -/// `version`, and `authors`. -/// -/// # Example -/// ```example -/// #let details = toml("details.toml") -/// -/// Title: #details.title \ -/// Version: #details.version \ -/// Authors: #(details.authors -/// .join(", ", last: " and ")) -/// ``` -#[func(scope, title = "TOML")] -pub fn toml( - /// The virtual machine. - vm: &mut Vm, - /// Path to a TOML file. - path: Spanned<EcoString>, -) -> SourceResult<Value> { - let Spanned { v: path, span } = path; - let id = vm.resolve_path(&path).at(span)?; - let data = vm.world().file(id).at(span)?; - toml::decode(Spanned::new(Readable::Bytes(data), span)) -} - -#[scope] -impl toml { - /// Reads structured data from a TOML string/bytes. - #[func(title = "Decode TOML")] - pub fn decode( - /// TOML data. - data: Spanned<Readable>, - ) -> SourceResult<Value> { - let Spanned { v: data, span } = data; - let raw = std::str::from_utf8(data.as_slice()) - .map_err(|_| "file is not valid utf-8") - .at(span)?; - ::toml::from_str(raw) - .map_err(|err| format_toml_error(err, raw)) - .at(span) - } - - /// Encodes structured data into a TOML string. - #[func(title = "Encode TOML")] - pub fn encode( - /// Value to be encoded. - value: Spanned<Value>, - /// Whether to pretty-print the resulting TOML. - #[named] - #[default(true)] - pretty: bool, - ) -> SourceResult<Str> { - let Spanned { v: value, span } = value; - if pretty { ::toml::to_string_pretty(&value) } else { ::toml::to_string(&value) } - .map(|v| v.into()) - .map_err(|err| eco_format!("failed to encode value as TOML ({err})")) - .at(span) - } -} - -/// Format the user-facing TOML error message. -fn format_toml_error(error: ::toml::de::Error, raw: &str) -> EcoString { - if let Some(head) = error.span().and_then(|range| raw.get(..range.start)) { - let line = head.lines().count(); - let column = 1 + head.chars().rev().take_while(|&c| !is_newline(c)).count(); - eco_format!( - "failed to parse TOML ({} at line {line} column {column})", - error.message(), - ) - } else { - eco_format!("failed to parse TOML ({})", error.message()) - } -} - -/// Reads structured data from a YAML file. -/// -/// The file must contain a valid YAML object or array. YAML mappings will be -/// converted into Typst dictionaries, and YAML sequences will be converted into -/// Typst arrays. Strings and booleans will be converted into the Typst -/// equivalents, null-values (`null`, `~` or empty ``) will be converted into -/// `{none}`, and numbers will be converted to floats or integers depending on -/// whether they are whole numbers. Custom YAML tags are ignored, though the -/// loaded value will still be present. -/// -/// The YAML files in the example contain objects with authors as keys, -/// each with a sequence of their own submapping with the keys -/// "title" and "published" -/// -/// # Example -/// ```example -/// #let bookshelf(contents) = { -/// for (author, works) in contents { -/// author -/// for work in works [ -/// - #work.title (#work.published) -/// ] -/// } -/// } -/// -/// #bookshelf( -/// yaml("scifi-authors.yaml") -/// ) -/// ``` -#[func(scope, title = "YAML")] -pub fn yaml( - /// The virtual machine. - vm: &mut Vm, - /// Path to a YAML file. - path: Spanned<EcoString>, -) -> SourceResult<Value> { - let Spanned { v: path, span } = path; - let id = vm.resolve_path(&path).at(span)?; - let data = vm.world().file(id).at(span)?; - yaml::decode(Spanned::new(Readable::Bytes(data), span)) -} - -#[scope] -impl yaml { - /// Reads structured data from a YAML string/bytes. - #[func(title = "Decode YAML")] - pub fn decode( - /// YAML data. - data: Spanned<Readable>, - ) -> SourceResult<Value> { - let Spanned { v: data, span } = data; - serde_yaml::from_slice(data.as_slice()) - .map_err(|err| eco_format!("failed to parse YAML ({err})")) - .at(span) - } - - /// Encode structured data into a YAML string. - #[func(title = "Encode YAML")] - pub fn encode( - /// Value to be encoded. - value: Spanned<Value>, - ) -> SourceResult<Str> { - let Spanned { v: value, span } = value; - serde_yaml::to_string(&value) - .map(|v| v.into()) - .map_err(|err| eco_format!("failed to encode value as YAML ({err})")) - .at(span) - } -} - -/// Reads structured data from a CBOR file. -/// -/// The file must contain a valid cbor serialization. Mappings will be -/// converted into Typst dictionaries, and sequences will be converted into -/// Typst arrays. Strings and booleans will be converted into the Typst -/// equivalents, null-values (`null`, `~` or empty ``) will be converted into -/// `{none}`, and numbers will be converted to floats or integers depending on -/// whether they are whole numbers. -#[func(scope, title = "CBOR")] -pub fn cbor( - /// The virtual machine. - vm: &mut Vm, - /// Path to a CBOR file. - path: Spanned<EcoString>, -) -> SourceResult<Value> { - let Spanned { v: path, span } = path; - let id = vm.resolve_path(&path).at(span)?; - let data = vm.world().file(id).at(span)?; - cbor::decode(Spanned::new(data, span)) -} - -#[scope] -impl cbor { - /// Reads structured data from CBOR bytes. - #[func(title = "Decode CBOR")] - pub fn decode( - /// cbor data. - data: Spanned<Bytes>, - ) -> SourceResult<Value> { - let Spanned { v: data, span } = data; - ciborium::from_reader(data.as_slice()) - .map_err(|err| eco_format!("failed to parse CBOR ({err})")) - .at(span) - } - - /// Encode structured data into CBOR bytes. - #[func(title = "Encode CBOR")] - pub fn encode( - /// Value to be encoded. - value: Spanned<Value>, - ) -> SourceResult<Bytes> { - let Spanned { v: value, span } = value; - let mut res = Vec::new(); - ciborium::into_writer(&value, &mut res) - .map(|_| res.into()) - .map_err(|err| eco_format!("failed to encode value as CBOR ({err})")) - .at(span) - } -} - -/// Reads structured data from an XML file. -/// -/// The XML file is parsed into an array of dictionaries and strings. XML nodes -/// can be elements or strings. Elements are represented as dictionaries with -/// the the following keys: -/// -/// - `tag`: The name of the element as a string. -/// - `attrs`: A dictionary of the element's attributes as strings. -/// - `children`: An array of the element's child nodes. -/// -/// The XML file in the example contains a root `news` tag with multiple -/// `article` tags. Each article has a `title`, `author`, and `content` tag. The -/// `content` tag contains one or more paragraphs, which are represented as `p` -/// tags. -/// -/// # Example -/// ```example -/// #let find-child(elem, tag) = { -/// elem.children -/// .find(e => "tag" in e and e.tag == tag) -/// } -/// -/// #let article(elem) = { -/// let title = find-child(elem, "title") -/// let author = find-child(elem, "author") -/// let pars = find-child(elem, "content") -/// -/// heading(title.children.first()) -/// text(10pt, weight: "medium")[ -/// Published by -/// #author.children.first() -/// ] -/// -/// for p in pars.children { -/// if (type(p) == "dictionary") { -/// parbreak() -/// p.children.first() -/// } -/// } -/// } -/// -/// #let data = xml("example.xml") -/// #for elem in data.first().children { -/// if (type(elem) == "dictionary") { -/// article(elem) -/// } -/// } -/// ``` -#[func(scope, title = "XML")] -pub fn xml( - /// The virtual machine. - vm: &mut Vm, - /// Path to an XML file. - path: Spanned<EcoString>, -) -> SourceResult<Value> { - let Spanned { v: path, span } = path; - let id = vm.resolve_path(&path).at(span)?; - let data = vm.world().file(id).at(span)?; - xml::decode(Spanned::new(Readable::Bytes(data), span)) -} - -#[scope] -impl xml { - /// Reads structured data from an XML string/bytes. - #[func(title = "Decode XML")] - pub fn decode( - /// XML data. - data: Spanned<Readable>, - ) -> SourceResult<Value> { - let Spanned { v: data, span } = data; - let text = std::str::from_utf8(data.as_slice()) - .map_err(FileError::from) - .at(span)?; - let document = - roxmltree::Document::parse(text).map_err(format_xml_error).at(span)?; - Ok(convert_xml(document.root())) - } -} - -/// Convert an XML node to a Typst value. -fn convert_xml(node: roxmltree::Node) -> Value { - if node.is_text() { - return node.text().unwrap_or_default().into_value(); - } - - let children: Array = node.children().map(convert_xml).collect(); - if node.is_root() { - return Value::Array(children); - } - - let tag: Str = node.tag_name().name().into(); - let attrs: Dict = node - .attributes() - .map(|attr| (attr.name().into(), attr.value().into_value())) - .collect(); - - Value::Dict(dict! { - "tag" => tag, - "attrs" => attrs, - "children" => children, - }) -} - -/// Format the user-facing XML error message. -fn format_xml_error(error: roxmltree::Error) -> EcoString { - format_xml_like_error("XML", error) -} diff --git a/crates/typst-library/src/compute/foundations.rs b/crates/typst-library/src/compute/foundations.rs deleted file mode 100644 index fef00269..00000000 --- a/crates/typst-library/src/compute/foundations.rs +++ /dev/null @@ -1,236 +0,0 @@ -use typst::eval::{ - Datetime, Duration, EvalMode, Module, Never, NoneValue, Plugin, Regex, Repr, Version, -}; - -use crate::prelude::*; - -/// Hook up all foundational definitions. -pub(super) fn define(global: &mut Scope) { - global.category("foundations"); - global.define_type::<bool>(); - global.define_type::<i64>(); - global.define_type::<f64>(); - global.define_type::<Str>(); - global.define_type::<Bytes>(); - global.define_type::<Content>(); - global.define_type::<Array>(); - global.define_type::<Dict>(); - global.define_type::<Func>(); - global.define_type::<Args>(); - global.define_type::<Type>(); - global.define_type::<Module>(); - global.define_type::<Regex>(); - global.define_type::<Datetime>(); - global.define_type::<Duration>(); - global.define_type::<Version>(); - global.define_type::<Plugin>(); - global.define_func::<repr>(); - global.define_func::<panic>(); - global.define_func::<assert>(); - global.define_func::<eval>(); -} - -/// Returns the string representation of a value. -/// -/// When inserted into content, most values are displayed as this representation -/// in monospace with syntax-highlighting. The exceptions are `{none}`, -/// integers, floats, strings, content, and functions. -/// -/// **Note:** This function is for debugging purposes. Its output should not be -/// considered stable and may change at any time! -/// -/// # Example -/// ```example -/// #none vs #repr(none) \ -/// #"hello" vs #repr("hello") \ -/// #(1, 2) vs #repr((1, 2)) \ -/// #[*Hi*] vs #repr([*Hi*]) -/// ``` -#[func(title = "Representation")] -pub fn repr( - /// The value whose string representation to produce. - value: Value, -) -> Str { - value.repr().into() -} - -/// Fails with an error. -/// -/// Arguments are displayed to the user (not rendered in the document) as -/// strings, converting with `repr` if necessary. -/// -/// # Example -/// The code below produces the error `panicked with: "this is wrong"`. -/// ```typ -/// #panic("this is wrong") -/// ``` -#[func(keywords = ["error"])] -pub fn panic( - /// The values to panic with and display to the user. - #[variadic] - values: Vec<Value>, -) -> StrResult<Never> { - let mut msg = EcoString::from("panicked"); - if !values.is_empty() { - msg.push_str(" with: "); - for (i, value) in values.iter().enumerate() { - if i > 0 { - msg.push_str(", "); - } - msg.push_str(&value.repr()); - } - } - Err(msg) -} - -/// Ensures that a condition is fulfilled. -/// -/// Fails with an error if the condition is not fulfilled. Does not -/// produce any output in the document. -/// -/// If you wish to test equality between two values, see -/// [`assert.eq`]($assert.eq) and [`assert.ne`]($assert.ne). -/// -/// # Example -/// ```typ -/// #assert(1 < 2, message: "math broke") -/// ``` -#[func(scope)] -pub fn assert( - /// The condition that must be true for the assertion to pass. - condition: bool, - /// The error message when the assertion fails. - #[named] - message: Option<EcoString>, -) -> StrResult<NoneValue> { - if !condition { - if let Some(message) = message { - bail!("assertion failed: {message}"); - } else { - bail!("assertion failed"); - } - } - Ok(NoneValue) -} - -#[scope] -impl assert { - /// Ensures that two values are equal. - /// - /// Fails with an error if the first value is not equal to the second. Does not - /// produce any output in the document. - /// - /// ```typ - /// #assert.eq(10, 10) - /// ``` - #[func(title = "Assert Equal")] - pub fn eq( - /// The first value to compare. - left: Value, - /// The second value to compare. - right: Value, - /// An optional message to display on error instead of the representations - /// of the compared values. - #[named] - message: Option<EcoString>, - ) -> StrResult<NoneValue> { - if left != right { - if let Some(message) = message { - bail!("equality assertion failed: {message}"); - } else { - bail!( - "equality assertion failed: value {} was not equal to {}", - left.repr(), - right.repr() - ); - } - } - Ok(NoneValue) - } - - /// Ensures that two values are not equal. - /// - /// Fails with an error if the first value is equal to the second. Does not - /// produce any output in the document. - /// - /// ```typ - /// #assert.ne(3, 4) - /// ``` - #[func(title = "Assert Not Equal")] - pub fn ne( - /// The first value to compare. - left: Value, - /// The second value to compare. - right: Value, - /// An optional message to display on error instead of the representations - /// of the compared values. - #[named] - message: Option<EcoString>, - ) -> StrResult<NoneValue> { - if left == right { - if let Some(message) = message { - bail!("inequality assertion failed: {message}"); - } else { - bail!( - "inequality assertion failed: value {} was equal to {}", - left.repr(), - right.repr() - ); - } - } - Ok(NoneValue) - } -} - -/// Evaluates a string as Typst code. -/// -/// This function should only be used as a last resort. -/// -/// # Example -/// ```example -/// #eval("1 + 1") \ -/// #eval("(1, 2, 3, 4)").len() \ -/// #eval("*Markup!*", mode: "markup") \ -/// ``` -#[func(title = "Evaluate")] -pub fn eval( - /// The virtual machine. - vm: &mut Vm, - /// A string of Typst code to evaluate. - /// - /// The code in the string cannot interact with the file system. - source: Spanned<String>, - /// The syntactical mode in which the string is parsed. - /// - /// ```example - /// #eval("= Heading", mode: "markup") - /// #eval("1_2^3", mode: "math") - /// ``` - #[named] - #[default(EvalMode::Code)] - mode: EvalMode, - /// A scope of definitions that are made available. - /// - /// ```example - /// #eval("x + 1", scope: (x: 2)) \ - /// #eval( - /// "abc/xyz", - /// mode: "math", - /// scope: ( - /// abc: $a + b + c$, - /// xyz: $x + y + z$, - /// ), - /// ) - /// ``` - #[named] - #[default] - scope: Dict, -) -> SourceResult<Value> { - let Spanned { v: text, span } = source; - let dict = scope; - let mut scope = Scope::new(); - for (key, value) in dict { - scope.define(key, value); - } - typst::eval::eval_string(vm.world(), &text, span, mode, scope) -} diff --git a/crates/typst-library/src/compute/mod.rs b/crates/typst-library/src/compute/mod.rs deleted file mode 100644 index f1af24c5..00000000 --- a/crates/typst-library/src/compute/mod.rs +++ /dev/null @@ -1,20 +0,0 @@ -//! Computational functions. - -pub mod calc; -pub mod sys; - -mod data; -mod foundations; - -pub use self::data::*; -pub use self::foundations::*; - -use crate::prelude::*; - -/// Hook up all compute definitions. -pub(super) fn define(global: &mut Scope) { - self::foundations::define(global); - self::data::define(global); - self::calc::define(global); - self::sys::define(global); -} diff --git a/crates/typst-library/src/compute/sys.rs b/crates/typst-library/src/compute/sys.rs deleted file mode 100644 index 6404e625..00000000 --- a/crates/typst-library/src/compute/sys.rs +++ /dev/null @@ -1,24 +0,0 @@ -//! System-related things. - -use typst::eval::{Module, Scope, Version}; - -/// Hook up all calculation definitions. -pub(super) fn define(global: &mut Scope) { - global.category("sys"); - global.define_module(module()); -} - -/// A module with system-related things. -fn module() -> Module { - let mut scope = Scope::deduplicating(); - scope.category("sys"); - scope.define( - "version", - Version::from_iter([ - env!("CARGO_PKG_VERSION_MAJOR").parse::<u32>().unwrap(), - env!("CARGO_PKG_VERSION_MINOR").parse::<u32>().unwrap(), - env!("CARGO_PKG_VERSION_PATCH").parse::<u32>().unwrap(), - ]), - ); - Module::new("sys", scope) -} diff --git a/crates/typst-library/src/layout/align.rs b/crates/typst-library/src/layout/align.rs deleted file mode 100644 index 9c18266d..00000000 --- a/crates/typst-library/src/layout/align.rs +++ /dev/null @@ -1,46 +0,0 @@ -use crate::prelude::*; - -/// Aligns content horizontally and vertically. -/// -/// # Example -/// ```example -/// #set align(center) -/// -/// Centered text, a sight to see \ -/// In perfect balance, visually \ -/// Not left nor right, it stands alone \ -/// A work of art, a visual throne -/// ``` -#[elem(Show)] -pub struct AlignElem { - /// The [alignment]($alignment) along both axes. - /// - /// ```example - /// #set page(height: 6cm) - /// #set text(lang: "ar") - /// - /// مثال - /// #align( - /// end + horizon, - /// rect(inset: 12pt)[ركن] - /// ) - /// ``` - #[positional] - #[fold] - #[default] - pub alignment: Align, - - /// The content to align. - #[required] - pub body: Content, -} - -impl Show for AlignElem { - #[tracing::instrument(name = "AlignElem::show", skip_all)] - fn show(&self, _: &mut Vt, styles: StyleChain) -> SourceResult<Content> { - Ok(self - .body() - .clone() - .styled(Self::set_alignment(self.alignment(styles)))) - } -} diff --git a/crates/typst-library/src/layout/columns.rs b/crates/typst-library/src/layout/columns.rs deleted file mode 100644 index bf111506..00000000 --- a/crates/typst-library/src/layout/columns.rs +++ /dev/null @@ -1,169 +0,0 @@ -use crate::prelude::*; -use crate::text::TextElem; - -/// Separates a region into multiple equally sized columns. -/// -/// The `column` function allows to separate the interior of any container into -/// multiple columns. It will not equalize the height of the columns, instead, -/// the columns will take up the height of their container or the remaining -/// height on the page. The columns function can break across pages if -/// necessary. -/// -/// If you need to insert columns across your whole document, you can use the -/// [`{page}` function's `columns` parameter]($page.columns) instead. -/// -/// # Example -/// ```example -/// = Towards Advanced Deep Learning -/// -/// #box(height: 68pt, -/// columns(2, gutter: 11pt)[ -/// #set par(justify: true) -/// This research was funded by the -/// National Academy of Sciences. -/// NAoS provided support for field -/// tests and interviews with a -/// grant of up to USD 40.000 for a -/// period of 6 months. -/// ] -/// ) -/// -/// In recent years, deep learning has -/// increasingly been used to solve a -/// variety of problems. -/// ``` -#[elem(Layout)] -pub struct ColumnsElem { - /// The number of columns. - #[positional] - #[default(NonZeroUsize::new(2).unwrap())] - pub count: NonZeroUsize, - - /// The size of the gutter space between each column. - #[resolve] - #[default(Ratio::new(0.04).into())] - pub gutter: Rel<Length>, - - /// The content that should be layouted into the columns. - #[required] - pub body: Content, -} - -impl Layout for ColumnsElem { - #[tracing::instrument(name = "ColumnsElem::layout", skip_all)] - fn layout( - &self, - vt: &mut Vt, - styles: StyleChain, - regions: Regions, - ) -> SourceResult<Fragment> { - let body = self.body(); - - // Separating the infinite space into infinite columns does not make - // much sense. - if !regions.size.x.is_finite() { - return body.layout(vt, styles, regions); - } - - // Determine the width of the gutter and each column. - let columns = self.count(styles).get(); - let gutter = self.gutter(styles).relative_to(regions.base().x); - let width = (regions.size.x - gutter * (columns - 1) as f64) / columns as f64; - - let backlog: Vec<_> = std::iter::once(®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::hard(Size::new(regions.size.x, height)); - let mut cursor = Abs::zero(); - - for _ in 0..columns { - let Some(frame) = frames.next() else { break }; - if !regions.expand.y { - output.size_mut().y.set_max(frame.height()); - } - - let width = frame.width(); - let x = if dir == Dir::LTR { - cursor - } else { - regions.size.x - cursor - width - }; - - output.push_frame(Point::with_x(x), frame); - cursor += width + gutter; - } - - finished.push(output); - } - - Ok(Fragment::frames(finished)) - } -} - -/// Forces a column break. -/// -/// The function will behave like a [page break]($pagebreak) when used in a -/// single column layout or the last column on a page. Otherwise, content after -/// the column break will be placed in the next column. -/// -/// # Example -/// ```example -/// #set page(columns: 2) -/// Preliminary findings from our -/// ongoing research project have -/// revealed a hitherto unknown -/// phenomenon of extraordinary -/// significance. -/// -/// #colbreak() -/// Through rigorous experimentation -/// and analysis, we have discovered -/// a hitherto uncharacterized process -/// that defies our current -/// understanding of the fundamental -/// laws of nature. -/// ``` -#[elem(title = "Column Break", Behave)] -pub struct ColbreakElem { - /// If `{true}`, the column break is skipped if the current column is - /// already empty. - #[default(false)] - pub weak: bool, -} - -impl Behave for ColbreakElem { - fn behaviour(&self) -> Behaviour { - if self.weak(StyleChain::default()) { - Behaviour::Weak(1) - } else { - Behaviour::Destructive - } - } -} diff --git a/crates/typst-library/src/layout/container.rs b/crates/typst-library/src/layout/container.rs deleted file mode 100644 index 9268f8df..00000000 --- a/crates/typst-library/src/layout/container.rs +++ /dev/null @@ -1,502 +0,0 @@ -use typst::eval::AutoValue; - -use crate::layout::{Spacing, VElem}; -use crate::prelude::*; - -/// An inline-level container that sizes content. -/// -/// All elements except inline math, text, and boxes are block-level and cannot -/// occur inside of a paragraph. The box function can be used to integrate such -/// elements into a paragraph. Boxes take the size of their contents by default -/// but can also be sized explicitly. -/// -/// # Example -/// ```example -/// Refer to the docs -/// #box( -/// height: 9pt, -/// image("docs.svg") -/// ) -/// for more information. -/// ``` -#[elem(Layout)] -pub struct BoxElem { - /// The width of the box. - /// - /// Boxes can have [fractional]($fraction) widths, as the example below - /// demonstrates. - /// - /// _Note:_ Currently, only boxes and only their widths might be fractionally - /// sized within paragraphs. Support for fractionally sized images, shapes, - /// and more might be added in the future. - /// - /// ```example - /// Line in #box(width: 1fr, line(length: 100%)) between. - /// ``` - pub width: Sizing, - - /// The height of the box. - pub height: Smart<Rel<Length>>, - - /// An amount to shift the box's baseline by. - /// - /// ```example - /// Image: #box(baseline: 40%, image("tiger.jpg", width: 2cm)). - /// ``` - #[resolve] - pub baseline: Rel<Length>, - - /// The box's background color. See the - /// [rectangle's documentation]($rect.fill) for more details. - pub fill: Option<Paint>, - - /// The box's border color. See the - /// [rectangle's documentation]($rect.stroke) for more details. - #[resolve] - #[fold] - pub stroke: Sides<Option<Option<Stroke>>>, - - /// How much to round the box's corners. See the - /// [rectangle's documentation]($rect.radius) for more details. - #[resolve] - #[fold] - pub radius: Corners<Option<Rel<Length>>>, - - /// How much to pad the box's content. - /// - /// _Note:_ When the box contains text, its exact size depends on the - /// current [text edges]($text.top-edge). - /// - /// ```example - /// #rect(inset: 0pt)[Tight] - /// ``` - #[resolve] - #[fold] - pub inset: Sides<Option<Rel<Length>>>, - - /// How much to expand the box's size without affecting the layout. - /// - /// This is useful to prevent padding from affecting line layout. For a - /// generalized version of the example below, see the documentation for the - /// [raw text's block parameter]($raw.block). - /// - /// ```example - /// An inline - /// #box( - /// fill: luma(235), - /// inset: (x: 3pt, y: 0pt), - /// outset: (y: 3pt), - /// radius: 2pt, - /// )[rectangle]. - /// ``` - #[resolve] - #[fold] - pub outset: Sides<Option<Rel<Length>>>, - - /// Whether to clip the content inside the box. - #[default(false)] - pub clip: bool, - - /// The contents of the box. - #[positional] - pub body: Option<Content>, -} - -impl Layout for BoxElem { - #[tracing::instrument(name = "BoxElem::layout", skip_all)] - fn layout( - &self, - vt: &mut Vt, - styles: StyleChain, - regions: Regions, - ) -> SourceResult<Fragment> { - let width = match self.width(styles) { - Sizing::Auto => Smart::Auto, - Sizing::Rel(rel) => Smart::Custom(rel), - Sizing::Fr(_) => Smart::Custom(Ratio::one().into()), - }; - - // Resolve the sizing to a concrete size. - let sizing = Axes::new(width, self.height(styles)); - let expand = sizing.as_ref().map(Smart::is_custom); - let size = sizing - .resolve(styles) - .zip_map(regions.base(), |s, b| s.map(|v| v.relative_to(b))) - .unwrap_or(regions.base()); - - // Apply inset. - let mut body = self.body(styles).unwrap_or_default(); - let inset = self.inset(styles); - if inset.iter().any(|v| !v.is_zero()) { - body = body.padded(inset.map(|side| side.map(Length::from))); - } - - // Select the appropriate base and expansion for the child depending - // on whether it is automatically or relatively sized. - let pod = Regions::one(size, expand); - let mut frame = body.layout(vt, styles, pod)?.into_frame(); - - // Enforce correct size. - *frame.size_mut() = expand.select(size, frame.size()); - - // Apply baseline shift. - let shift = self.baseline(styles).relative_to(frame.height()); - if !shift.is_zero() { - frame.set_baseline(frame.baseline() - shift); - } - - // Prepare fill and stroke. - let fill = self.fill(styles); - let stroke = self.stroke(styles).map(|s| s.map(Stroke::unwrap_or_default)); - - // Clip the contents - if self.clip(styles) { - let outset = self.outset(styles).relative_to(frame.size()); - let size = frame.size() + outset.sum_by_axis(); - let radius = self.radius(styles); - frame.clip(clip_rect(size, radius, &stroke)); - } - - // Add fill and/or stroke. - if fill.is_some() || stroke.iter().any(Option::is_some) { - let outset = self.outset(styles); - let radius = self.radius(styles); - frame.fill_and_stroke(fill, stroke, outset, radius, self.span()); - } - - // Apply metadata. - frame.meta(styles, false); - frame.set_kind(FrameKind::Hard); - - Ok(Fragment::frame(frame)) - } -} - -/// A block-level container. -/// -/// Such a container can be used to separate content, size it, and give it a -/// background or border. -/// -/// # Examples -/// With a block, you can give a background to content while still allowing it -/// to break across multiple pages. -/// ```example -/// #set page(height: 100pt) -/// #block( -/// fill: luma(230), -/// inset: 8pt, -/// radius: 4pt, -/// lorem(30), -/// ) -/// ``` -/// -/// Blocks are also useful to force elements that would otherwise be inline to -/// become block-level, especially when writing show rules. -/// ```example -/// #show heading: it => it.body -/// = Blockless -/// More text. -/// -/// #show heading: it => block(it.body) -/// = Blocky -/// More text. -/// ``` -#[elem(Layout)] -pub struct BlockElem { - /// The block's width. - /// - /// ```example - /// #set align(center) - /// #block( - /// width: 60%, - /// inset: 8pt, - /// fill: silver, - /// lorem(10), - /// ) - /// ``` - pub width: Smart<Rel<Length>>, - - /// The block's height. When the height is larger than the remaining space - /// on a page and [`breakable`]($block.breakable) is `{true}`, the - /// block will continue on the next page with the remaining height. - /// - /// ```example - /// #set page(height: 80pt) - /// #set align(center) - /// #block( - /// width: 80%, - /// height: 150%, - /// fill: aqua, - /// ) - /// ``` - pub height: Smart<Rel<Length>>, - - /// Whether the block can be broken and continue on the next page. - /// - /// ```example - /// #set page(height: 80pt) - /// The following block will - /// jump to its own page. - /// #block( - /// breakable: false, - /// lorem(15), - /// ) - /// ``` - #[default(true)] - pub breakable: bool, - - /// The block's background color. See the - /// [rectangle's documentation]($rect.fill) for more details. - pub fill: Option<Paint>, - - /// The block's border color. See the - /// [rectangle's documentation]($rect.stroke) for more details. - #[resolve] - #[fold] - pub stroke: Sides<Option<Option<Stroke>>>, - - /// How much to round the block's corners. See the - /// [rectangle's documentation]($rect.radius) for more details. - #[resolve] - #[fold] - pub radius: Corners<Option<Rel<Length>>>, - - /// How much to pad the block's content. See the - /// [box's documentation]($box.inset) for more details. - #[resolve] - #[fold] - pub inset: Sides<Option<Rel<Length>>>, - - /// How much to expand the block's size without affecting the layout. See - /// the [box's documentation]($box.outset) for more details. - #[resolve] - #[fold] - pub outset: Sides<Option<Rel<Length>>>, - - /// The spacing around this block. This is shorthand to set `above` and - /// `below` to the same value. - /// - /// ```example - /// #set align(center) - /// #show math.equation: set block(above: 8pt, below: 16pt) - /// - /// This sum of $x$ and $y$: - /// $ x + y = z $ - /// A second paragraph. - /// ``` - #[external] - #[default(Em::new(1.2).into())] - pub spacing: Spacing, - - /// The spacing between this block and its predecessor. Takes precedence - /// over `spacing`. Can be used in combination with a show rule to adjust - /// the spacing around arbitrary block-level elements. - #[external] - #[default(Em::new(1.2).into())] - pub above: Spacing, - #[internal] - #[parse( - let spacing = args.named("spacing")?; - args.named("above")? - .map(VElem::block_around) - .or_else(|| spacing.map(VElem::block_spacing)) - )] - #[default(VElem::block_spacing(Em::new(1.2).into()))] - pub above: VElem, - - /// The spacing between this block and its successor. Takes precedence - /// over `spacing`. - #[external] - #[default(Em::new(1.2).into())] - pub below: Spacing, - #[internal] - #[parse( - args.named("below")? - .map(VElem::block_around) - .or_else(|| spacing.map(VElem::block_spacing)) - )] - #[default(VElem::block_spacing(Em::new(1.2).into()))] - pub below: VElem, - - /// Whether to clip the content inside the block. - #[default(false)] - pub clip: bool, - - /// The contents of the block. - #[positional] - pub body: Option<Content>, - - /// Whether this block must stick to the following one. - /// - /// Use this to prevent page breaks between e.g. a heading and its body. - #[internal] - #[default(false)] - pub sticky: bool, -} - -impl Layout for BlockElem { - #[tracing::instrument(name = "BlockElem::layout", skip_all)] - fn layout( - &self, - vt: &mut Vt, - styles: StyleChain, - regions: Regions, - ) -> SourceResult<Fragment> { - // Apply inset. - let mut body = self.body(styles).unwrap_or_default(); - let inset = self.inset(styles); - if inset.iter().any(|v| !v.is_zero()) { - body = body.clone().padded(inset.map(|side| side.map(Length::from))); - } - - // Resolve the sizing to a concrete size. - let sizing = Axes::new(self.width(styles), self.height(styles)); - let mut expand = sizing.as_ref().map(Smart::is_custom); - let mut size = sizing - .resolve(styles) - .zip_map(regions.base(), |s, b| s.map(|v| v.relative_to(b))) - .unwrap_or(regions.base()); - - // Layout the child. - let mut frames = if self.breakable(styles) { - // Measure to ensure frames for all regions have the same width. - if sizing.x == Smart::Auto { - let pod = Regions::one(size, Axes::splat(false)); - let frame = body.measure(vt, styles, pod)?.into_frame(); - size.x = frame.width(); - expand.x = true; - } - - let mut pod = regions; - pod.size.x = size.x; - pod.expand = expand; - - if expand.y { - pod.full = size.y; - } - - // Generate backlog for fixed height. - let mut heights = vec![]; - if sizing.y.is_custom() { - let mut remaining = size.y; - for region in regions.iter() { - let limited = region.y.min(remaining); - heights.push(limited); - remaining -= limited; - if Abs::zero().fits(remaining) { - break; - } - } - - if let Some(last) = heights.last_mut() { - *last += remaining; - } - - pod.size.y = heights[0]; - pod.backlog = &heights[1..]; - pod.last = None; - } - - let mut frames = body.layout(vt, styles, pod)?.into_frames(); - for (frame, &height) in frames.iter_mut().zip(&heights) { - *frame.size_mut() = - expand.select(Size::new(size.x, height), frame.size()); - } - frames - } else { - let pod = Regions::one(size, expand); - let mut frames = body.layout(vt, styles, pod)?.into_frames(); - *frames[0].size_mut() = expand.select(size, frames[0].size()); - frames - }; - - // Prepare fill and stroke. - let fill = self.fill(styles); - let stroke = self.stroke(styles).map(|s| s.map(Stroke::unwrap_or_default)); - - // Clip the contents - if self.clip(styles) { - for frame in frames.iter_mut() { - let outset = self.outset(styles).relative_to(frame.size()); - let size = frame.size() + outset.sum_by_axis(); - let radius = self.radius(styles); - frame.clip(clip_rect(size, radius, &stroke)); - } - } - - // Add fill and/or stroke. - if fill.is_some() || stroke.iter().any(Option::is_some) { - let mut skip = false; - if let [first, rest @ ..] = frames.as_slice() { - skip = first.is_empty() && rest.iter().any(|frame| !frame.is_empty()); - } - - let outset = self.outset(styles); - let radius = self.radius(styles); - for frame in frames.iter_mut().skip(skip as usize) { - frame.fill_and_stroke( - fill.clone(), - stroke.clone(), - outset, - radius, - self.span(), - ); - } - } - - // Apply metadata. - for frame in &mut frames { - frame.set_kind(FrameKind::Hard); - frame.meta(styles, false); - } - - Ok(Fragment::frames(frames)) - } -} - -/// Defines how to size a grid cell along an axis. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub enum Sizing { - /// A track that fits its cell's contents. - Auto, - /// A track size specified in absolute terms and relative to the parent's - /// size. - Rel(Rel<Length>), - /// A track size specified as a fraction of the remaining free space in the - /// parent. - Fr(Fr), -} - -impl Sizing { - /// Whether this is fractional sizing. - pub fn is_fractional(self) -> bool { - matches!(self, Self::Fr(_)) - } -} - -impl Default for Sizing { - fn default() -> Self { - Self::Auto - } -} - -impl<T: Into<Spacing>> From<T> for Sizing { - fn from(spacing: T) -> Self { - match spacing.into() { - Spacing::Rel(rel) => Self::Rel(rel), - Spacing::Fr(fr) => Self::Fr(fr), - } - } -} - -cast! { - Sizing, - self => match self { - Self::Auto => Value::Auto, - Self::Rel(rel) => rel.into_value(), - Self::Fr(fr) => fr.into_value(), - }, - _: AutoValue => Self::Auto, - v: Rel<Length> => Self::Rel(v), - v: Fr => Self::Fr(v), -} diff --git a/crates/typst-library/src/layout/enum.rs b/crates/typst-library/src/layout/enum.rs deleted file mode 100644 index 0c98c18a..00000000 --- a/crates/typst-library/src/layout/enum.rs +++ /dev/null @@ -1,319 +0,0 @@ -use std::str::FromStr; - -use crate::layout::{BlockElem, GridLayouter, ParElem, Sizing, Spacing}; -use crate::meta::{Numbering, NumberingPattern}; -use crate::prelude::*; -use crate::text::TextElem; - -/// A numbered list. -/// -/// Displays a sequence of items vertically and numbers them consecutively. -/// -/// # Example -/// ```example -/// Automatically numbered: -/// + Preparations -/// + Analysis -/// + Conclusions -/// -/// Manually numbered: -/// 2. What is the first step? -/// 5. I am confused. -/// + Moving on ... -/// -/// Multiple lines: -/// + This enum item has multiple -/// lines because the next line -/// is indented. -/// -/// Function call. -/// #enum[First][Second] -/// ``` -/// -/// You can easily switch all your enumerations to a different numbering style -/// with a set rule. -/// ```example -/// #set enum(numbering: "a)") -/// -/// + Starting off ... -/// + Don't forget step two -/// ``` -/// -/// You can also use [`enum.item`]($enum.item) to programmatically customize the -/// number of each item in the enumeration: -/// -/// ```example -/// #enum( -/// enum.item(1)[First step], -/// enum.item(5)[Fifth step], -/// enum.item(10)[Tenth step] -/// ) -/// ``` -/// -/// # Syntax -/// This functions also has dedicated syntax: -/// -/// - Starting a line with a plus sign creates an automatically numbered -/// enumeration item. -/// - Starting a line with a number followed by a dot creates an explicitly -/// numbered enumeration item. -/// -/// Enumeration items can contain multiple paragraphs and other block-level -/// content. All content that is indented more than an item's marker becomes -/// part of that item. -#[elem(scope, title = "Numbered List", Layout)] -pub struct EnumElem { - /// If this is `{false}`, the items are spaced apart with - /// [enum spacing]($enum.spacing). If it is `{true}`, they use normal - /// [leading]($par.leading) instead. This makes the enumeration more - /// compact, which can look better if the items are short. - /// - /// In markup mode, the value of this parameter is determined based on - /// whether items are separated with a blank line. If items directly follow - /// each other, this is set to `{true}`; if items are separated by a blank - /// line, this is set to `{false}`. - /// - /// ```example - /// + If an enum has a lot of text, and - /// maybe other inline content, it - /// should not be tight anymore. - /// - /// + To make an enum wide, simply - /// insert a blank line between the - /// items. - /// ``` - #[default(true)] - pub tight: bool, - - /// How to number the enumeration. Accepts a - /// [numbering pattern or function]($numbering). - /// - /// If the numbering pattern contains multiple counting symbols, they apply - /// to nested enums. If given a function, the function receives one argument - /// if `full` is `{false}` and multiple arguments if `full` is `{true}`. - /// - /// ```example - /// #set enum(numbering: "1.a)") - /// + Different - /// + Numbering - /// + Nested - /// + Items - /// + Style - /// - /// #set enum(numbering: n => super[#n]) - /// + Superscript - /// + Numbering! - /// ``` - #[default(Numbering::Pattern(NumberingPattern::from_str("1.").unwrap()))] - #[borrowed] - pub numbering: Numbering, - - /// Which number to start the enumeration with. - /// - /// ```example - /// #enum( - /// start: 3, - /// [Skipping], - /// [Ahead], - /// ) - /// ``` - #[default(1)] - pub start: usize, - - /// Whether to display the full numbering, including the numbers of - /// all parent enumerations. - /// - /// - /// ```example - /// #set enum(numbering: "1.a)", full: true) - /// + Cook - /// + Heat water - /// + Add integredients - /// + Eat - /// ``` - #[default(false)] - pub full: bool, - - /// The indentation of each item. - #[resolve] - pub indent: Length, - - /// The space between the numbering and the body of each item. - #[resolve] - #[default(Em::new(0.5).into())] - pub body_indent: Length, - - /// The spacing between the items of a wide (non-tight) enumeration. - /// - /// If set to `{auto}`, uses the spacing [below blocks]($block.below). - pub spacing: Smart<Spacing>, - - /// The horizontal alignment that enum numbers should have. - /// - /// By default, this is set to `{end}`, which aligns enum numbers - /// towards end of the current text direction (in left-to-right script, - /// for example, this is the same as `{right}`). The choice of `{end}` - /// for horizontal alignment of enum numbers is usually preferred over - /// `{start}`, as numbers then grow away from the text instead of towards - /// it, avoiding certain visual issues. This option lets you override this - /// behavior, however. - /// - /// ````example - /// #set enum(number-align: start) - /// - /// Here are some powers of two: - /// 1. One - /// 2. Two - /// 4. Four - /// 8. Eight - /// 16. Sixteen - /// 32. Thirty two - /// ```` - #[default(HAlign::End)] - pub number_align: HAlign, - - /// The numbered list's items. - /// - /// When using the enum syntax, adjacent items are automatically collected - /// into enumerations, even through constructs like for loops. - /// - /// ```example - /// #for phase in ( - /// "Launch", - /// "Orbit", - /// "Descent", - /// ) [+ #phase] - /// ``` - #[variadic] - pub children: Vec<EnumItem>, - - /// The numbers of parent items. - #[internal] - #[fold] - parents: Parent, -} - -#[scope] -impl EnumElem { - #[elem] - type EnumItem; -} - -impl Layout for EnumElem { - #[tracing::instrument(name = "EnumElem::layout", skip_all)] - fn layout( - &self, - vt: &mut Vt, - styles: StyleChain, - regions: Regions, - ) -> SourceResult<Fragment> { - let numbering = self.numbering(styles); - let indent = self.indent(styles); - let body_indent = self.body_indent(styles); - let gutter = if self.tight(styles) { - ParElem::leading_in(styles).into() - } else { - self.spacing(styles) - .unwrap_or_else(|| *BlockElem::below_in(styles).amount()) - }; - - let mut cells = vec![]; - let mut number = self.start(styles); - let mut parents = self.parents(styles); - let full = self.full(styles); - - // Horizontally align based on the given respective parameter. - // Vertically align to the top to avoid inheriting `horizon` or `bottom` - // alignment from the context and having the number be displaced in - // relation to the item it refers to. - let number_align = self.number_align(styles) + VAlign::Top; - - for item in self.children() { - number = item.number(styles).unwrap_or(number); - - let resolved = if full { - parents.push(number); - let content = numbering.apply_vt(vt, &parents)?.display(); - parents.pop(); - content - } else { - match numbering { - Numbering::Pattern(pattern) => { - TextElem::packed(pattern.apply_kth(parents.len(), number)) - } - other => other.apply_vt(vt, &[number])?.display(), - } - }; - - // Disable overhang as a workaround to end-aligned dots glitching - // and decreasing spacing between numbers and items. - let resolved = - resolved.aligned(number_align).styled(TextElem::set_overhang(false)); - - cells.push(Content::empty()); - cells.push(resolved); - cells.push(Content::empty()); - cells.push(item.body().clone().styled(Self::set_parents(Parent(number)))); - number = number.saturating_add(1); - } - - let layouter = GridLayouter::new( - Axes::with_x(&[ - Sizing::Rel(indent.into()), - Sizing::Auto, - Sizing::Rel(body_indent.into()), - Sizing::Auto, - ]), - Axes::with_y(&[gutter.into()]), - &cells, - regions, - styles, - self.span(), - ); - - Ok(layouter.layout(vt)?.fragment) - } -} - -/// An enumeration item. -#[elem(name = "item", title = "Numbered List Item")] -pub struct EnumItem { - /// The item's number. - #[positional] - pub number: Option<usize>, - - /// The item's body. - #[required] - pub body: Content, -} - -cast! { - EnumItem, - array: Array => { - let mut iter = array.into_iter(); - let (number, body) = match (iter.next(), iter.next(), iter.next()) { - (Some(a), Some(b), None) => (a.cast()?, b.cast()?), - _ => bail!("array must contain exactly two entries"), - }; - Self::new(body).with_number(number) - }, - v: Content => v.to::<Self>().cloned().unwrap_or_else(|| Self::new(v.clone())), -} - -#[derive(Debug, Clone, Copy, PartialEq, Hash)] -struct Parent(usize); - -cast! { - Parent, - self => self.0.into_value(), - v: usize => Self(v), -} - -impl Fold for Parent { - type Output = Vec<usize>; - - fn fold(self, mut outer: Self::Output) -> Self::Output { - outer.push(self.0); - outer - } -} diff --git a/crates/typst-library/src/layout/flow.rs b/crates/typst-library/src/layout/flow.rs deleted file mode 100644 index 17a39254..00000000 --- a/crates/typst-library/src/layout/flow.rs +++ /dev/null @@ -1,714 +0,0 @@ -use std::mem; - -use comemo::Prehashed; - -use crate::layout::{ - AlignElem, BlockElem, ColbreakElem, ColumnsElem, ParElem, PlaceElem, Spacing, VElem, -}; -use crate::meta::{FootnoteElem, FootnoteEntry}; -use crate::prelude::*; -use crate::visualize::{ - CircleElem, EllipseElem, ImageElem, LineElem, PathElem, PolygonElem, RectElem, - SquareElem, -}; - -/// Arranges spacing, paragraphs and block-level elements into a flow. -/// -/// This element is responsible for layouting both the top-level content flow -/// and the contents of boxes. -#[elem(Layout)] -pub struct FlowElem { - /// The children that will be arranges into a flow. - #[variadic] - pub children: Vec<Prehashed<Content>>, -} - -impl Layout for FlowElem { - #[tracing::instrument(name = "FlowElem::layout", skip_all)] - fn layout( - &self, - vt: &mut Vt, - styles: StyleChain, - regions: Regions, - ) -> SourceResult<Fragment> { - if !regions.size.x.is_finite() && regions.expand.x { - bail!(error!(self.span(), "cannot expand into infinite width")); - } - if !regions.size.y.is_finite() && regions.expand.y { - bail!(error!(self.span(), "cannot expand into infinite height")); - } - let mut layouter = FlowLayouter::new(regions, styles); - - for mut child in self.children().iter().map(|c| &**c) { - let outer = styles; - let mut styles = styles; - if let Some((elem, map)) = child.to_styled() { - child = elem; - styles = outer.chain(map); - } - - if let Some(elem) = child.to::<VElem>() { - layouter.layout_spacing(vt, elem, styles)?; - } else if let Some(elem) = child.to::<ParElem>() { - layouter.layout_par(vt, elem, styles)?; - } else if child.is::<LineElem>() - || child.is::<RectElem>() - || child.is::<SquareElem>() - || child.is::<EllipseElem>() - || child.is::<CircleElem>() - || child.is::<ImageElem>() - || child.is::<PolygonElem>() - || child.is::<PathElem>() - { - let layoutable = child.with::<dyn Layout>().unwrap(); - layouter.layout_single(vt, layoutable, styles)?; - } else if child.is::<MetaElem>() { - let mut frame = Frame::soft(Size::zero()); - frame.meta(styles, true); - layouter.items.push(FlowItem::Frame { - frame, - align: Axes::splat(FixedAlign::Start), - sticky: true, - movable: false, - }); - } else if let Some(placed) = child.to::<PlaceElem>() { - layouter.layout_placed(vt, placed, styles)?; - } else if child.can::<dyn Layout>() { - layouter.layout_multiple(vt, child, styles)?; - } else if child.is::<ColbreakElem>() { - if !layouter.regions.backlog.is_empty() || layouter.regions.last.is_some() - { - layouter.finish_region(vt)?; - } - } else { - bail!(child.span(), "unexpected flow child"); - } - } - - layouter.finish(vt) - } -} - -/// Performs flow layout. -struct FlowLayouter<'a> { - /// Whether this is the root flow. - root: bool, - /// The regions to layout children into. - regions: Regions<'a>, - /// The shared styles. - styles: StyleChain<'a>, - /// Whether the flow should expand to fill the region. - expand: Axes<bool>, - /// The initial size of `regions.size` that was available before we started - /// subtracting. - initial: Size, - /// Whether the last block was a paragraph. - last_was_par: bool, - /// Spacing and layouted blocks for the current region. - items: Vec<FlowItem>, - /// A queue of floats. - pending_floats: Vec<FlowItem>, - /// Whether we have any footnotes in the current region. - has_footnotes: bool, - /// Footnote configuration. - footnote_config: FootnoteConfig, - /// Finished frames for previous regions. - finished: Vec<Frame>, -} - -/// Cached footnote configuration. -struct FootnoteConfig { - separator: Content, - clearance: Abs, - gap: Abs, -} - -/// A prepared item in a flow layout. -#[derive(Debug)] -enum FlowItem { - /// Spacing between other items and whether it is weak. - Absolute(Abs, bool), - /// Fractional spacing between other items. - Fractional(Fr), - /// A frame for a layouted block, how to align it, whether it sticks to the - /// item after it (for orphan prevention), and whether it is movable - /// (to keep it together with its footnotes). - Frame { frame: Frame, align: Axes<FixedAlign>, sticky: bool, movable: bool }, - /// An absolutely placed frame. - Placed { - frame: Frame, - x_align: FixedAlign, - y_align: Smart<Option<FixedAlign>>, - delta: Axes<Rel<Abs>>, - float: bool, - clearance: Abs, - }, - /// A footnote frame (can also be the separator). - Footnote(Frame), -} - -impl FlowItem { - /// The inherent height of the item. - fn height(&self) -> Abs { - match self { - Self::Absolute(v, _) => *v, - Self::Fractional(_) | Self::Placed { .. } => Abs::zero(), - Self::Frame { frame, .. } | Self::Footnote(frame) => frame.height(), - } - } -} - -impl<'a> FlowLayouter<'a> { - /// Create a new flow layouter. - fn new(mut regions: Regions<'a>, styles: StyleChain<'a>) -> Self { - let expand = regions.expand; - - // Disable vertical expansion & root for children. - regions.expand.y = false; - let root = mem::replace(&mut regions.root, false); - - Self { - root, - regions, - styles, - expand, - initial: regions.size, - last_was_par: false, - items: vec![], - pending_floats: vec![], - has_footnotes: false, - footnote_config: FootnoteConfig { - separator: FootnoteEntry::separator_in(styles), - clearance: FootnoteEntry::clearance_in(styles), - gap: FootnoteEntry::gap_in(styles), - }, - finished: vec![], - } - } - - /// Layout vertical spacing. - #[tracing::instrument(name = "FlowLayouter::layout_spacing", skip_all)] - fn layout_spacing( - &mut self, - vt: &mut Vt, - v: &VElem, - styles: StyleChain, - ) -> SourceResult<()> { - self.layout_item( - vt, - match v.amount() { - Spacing::Rel(rel) => FlowItem::Absolute( - rel.resolve(styles).relative_to(self.initial.y), - v.weakness(styles) > 0, - ), - Spacing::Fr(fr) => FlowItem::Fractional(*fr), - }, - ) - } - - /// Layout a paragraph. - #[tracing::instrument(name = "FlowLayouter::layout_par", skip_all)] - fn layout_par( - &mut self, - vt: &mut Vt, - par: &ParElem, - styles: StyleChain, - ) -> SourceResult<()> { - let align = AlignElem::alignment_in(styles).resolve(styles); - let leading = ParElem::leading_in(styles); - let consecutive = self.last_was_par; - let lines = par - .layout(vt, styles, consecutive, self.regions.base(), self.regions.expand.x)? - .into_frames(); - - let mut sticky = self.items.len(); - for (i, item) in self.items.iter().enumerate().rev() { - match *item { - FlowItem::Absolute(_, _) => {} - FlowItem::Frame { sticky: true, .. } => sticky = i, - _ => break, - } - } - - if let Some(first) = lines.first() { - if !self.regions.size.y.fits(first.height()) && !self.regions.in_last() { - let carry: Vec<_> = self.items.drain(sticky..).collect(); - self.finish_region(vt)?; - for item in carry { - self.layout_item(vt, item)?; - } - } - } - - for (i, frame) in lines.into_iter().enumerate() { - if i > 0 { - self.layout_item(vt, FlowItem::Absolute(leading, true))?; - } - - self.layout_item( - vt, - FlowItem::Frame { frame, align, sticky: false, movable: true }, - )?; - } - - self.last_was_par = true; - Ok(()) - } - - /// Layout into a single region. - #[tracing::instrument(name = "FlowLayouter::layout_single", skip_all)] - fn layout_single( - &mut self, - vt: &mut Vt, - content: &dyn Layout, - styles: StyleChain, - ) -> SourceResult<()> { - let align = AlignElem::alignment_in(styles).resolve(styles); - let sticky = BlockElem::sticky_in(styles); - let pod = Regions::one(self.regions.base(), Axes::splat(false)); - let frame = content.layout(vt, styles, pod)?.into_frame(); - self.layout_item(vt, FlowItem::Frame { frame, align, sticky, movable: true })?; - self.last_was_par = false; - Ok(()) - } - - /// Layout a placed element. - fn layout_placed( - &mut self, - vt: &mut Vt, - placed: &PlaceElem, - styles: StyleChain, - ) -> SourceResult<()> { - let float = placed.float(styles); - let clearance = placed.clearance(styles); - let alignment = placed.alignment(styles); - let delta = Axes::new(placed.dx(styles), placed.dy(styles)).resolve(styles); - let x_align = alignment.map_or(FixedAlign::Center, |align| { - align.x().unwrap_or_default().resolve(styles) - }); - let y_align = alignment.map(|align| align.y().map(VAlign::fix)); - let frame = placed.layout(vt, styles, self.regions)?.into_frame(); - let item = FlowItem::Placed { frame, x_align, y_align, delta, float, clearance }; - self.layout_item(vt, item) - } - - /// Layout into multiple regions. - fn layout_multiple( - &mut self, - vt: &mut Vt, - block: &Content, - styles: StyleChain, - ) -> SourceResult<()> { - // Temporarily delegerate rootness to the columns. - let is_root = self.root; - if is_root && block.is::<ColumnsElem>() { - self.root = false; - self.regions.root = true; - } - - let mut notes = Vec::new(); - - if self.regions.is_full() { - // Skip directly if region is already full. - self.finish_region(vt)?; - } - - // How to align the block. - let align = if let Some(align) = block.to::<AlignElem>() { - align.alignment(styles) - } else if let Some((_, local)) = block.to_styled() { - AlignElem::alignment_in(styles.chain(local)) - } else { - AlignElem::alignment_in(styles) - } - .resolve(styles); - - // Layout the block itself. - let sticky = BlockElem::sticky_in(styles); - let fragment = block.layout(vt, styles, self.regions)?; - - for (i, frame) in fragment.into_iter().enumerate() { - // Find footnotes in the frame. - if self.root { - find_footnotes(&mut notes, &frame); - } - - if i > 0 { - self.finish_region(vt)?; - } - - let item = FlowItem::Frame { frame, align, sticky, movable: false }; - self.layout_item(vt, item)?; - } - - self.try_handle_footnotes(vt, notes)?; - - self.root = is_root; - self.regions.root = false; - self.last_was_par = false; - - Ok(()) - } - - /// Layout a finished frame. - #[tracing::instrument(name = "FlowLayouter::layout_item", skip_all)] - fn layout_item(&mut self, vt: &mut Vt, mut item: FlowItem) -> SourceResult<()> { - match item { - FlowItem::Absolute(v, weak) => { - if weak - && !self - .items - .iter() - .any(|item| matches!(item, FlowItem::Frame { .. })) - { - return Ok(()); - } - self.regions.size.y -= v - } - FlowItem::Fractional(_) => {} - FlowItem::Frame { ref frame, movable, .. } => { - let height = frame.height(); - if !self.regions.size.y.fits(height) && !self.regions.in_last() { - self.finish_region(vt)?; - } - - self.regions.size.y -= height; - if self.root && movable { - let mut notes = Vec::new(); - find_footnotes(&mut notes, frame); - self.items.push(item); - if !self.handle_footnotes(vt, &mut notes, true, false)? { - let item = self.items.pop(); - self.finish_region(vt)?; - self.items.extend(item); - self.regions.size.y -= height; - self.handle_footnotes(vt, &mut notes, true, true)?; - } - return Ok(()); - } - } - FlowItem::Placed { float: false, .. } => {} - FlowItem::Placed { - ref mut frame, - ref mut y_align, - float: true, - clearance, - .. - } => { - // If the float doesn't fit, queue it for the next region. - if !self.regions.size.y.fits(frame.height() + clearance) - && !self.regions.in_last() - { - self.pending_floats.push(item); - return Ok(()); - } - - // Select the closer placement, top or bottom. - if y_align.is_auto() { - let ratio = (self.regions.size.y - - (frame.height() + clearance) / 2.0) - / self.regions.full; - let better_align = - if ratio <= 0.5 { FixedAlign::End } else { FixedAlign::Start }; - *y_align = Smart::Custom(Some(better_align)); - } - - // Add some clearance so that the float doesn't touch the main - // content. - frame.size_mut().y += clearance; - if *y_align == Smart::Custom(Some(FixedAlign::End)) { - frame.translate(Point::with_y(clearance)); - } - - self.regions.size.y -= frame.height(); - - // Find footnotes in the frame. - if self.root { - let mut notes = vec![]; - find_footnotes(&mut notes, frame); - self.try_handle_footnotes(vt, notes)?; - } - } - FlowItem::Footnote(_) => {} - } - - self.items.push(item); - Ok(()) - } - - /// Finish the frame for one region. - fn finish_region(&mut self, vt: &mut Vt) -> SourceResult<()> { - // Trim weak spacing. - while self - .items - .last() - .map_or(false, |item| matches!(item, FlowItem::Absolute(_, true))) - { - self.items.pop(); - } - - // Determine the used size. - let mut fr = Fr::zero(); - let mut used = Size::zero(); - let mut footnote_height = Abs::zero(); - let mut float_top_height = Abs::zero(); - let mut float_bottom_height = Abs::zero(); - let mut first_footnote = true; - for item in &self.items { - match item { - FlowItem::Absolute(v, _) => used.y += *v, - FlowItem::Fractional(v) => fr += *v, - FlowItem::Frame { frame, .. } => { - used.y += frame.height(); - used.x.set_max(frame.width()); - } - FlowItem::Placed { float: false, .. } => {} - FlowItem::Placed { frame, float: true, y_align, .. } => match y_align { - Smart::Custom(Some(FixedAlign::Start)) => { - float_top_height += frame.height() - } - Smart::Custom(Some(FixedAlign::End)) => { - float_bottom_height += frame.height() - } - _ => {} - }, - FlowItem::Footnote(frame) => { - footnote_height += frame.height(); - if !first_footnote { - footnote_height += self.footnote_config.gap; - } - first_footnote = false; - used.x.set_max(frame.width()); - } - } - } - used.y += footnote_height + float_top_height + float_bottom_height; - - // Determine the size of the flow in this region depending on whether - // the region expands. Also account for fractional spacing and - // footnotes. - let mut size = self.expand.select(self.initial, used).min(self.initial); - if (fr.get() > 0.0 || self.has_footnotes) && self.initial.y.is_finite() { - size.y = self.initial.y; - } - - let mut output = Frame::soft(size); - let mut ruler = FixedAlign::Start; - let mut float_top_offset = Abs::zero(); - let mut offset = float_top_height; - let mut float_bottom_offset = Abs::zero(); - let mut footnote_offset = Abs::zero(); - - // Place all frames. - for item in self.items.drain(..) { - match item { - FlowItem::Absolute(v, _) => { - offset += v; - } - FlowItem::Fractional(v) => { - let remaining = self.initial.y - used.y; - offset += v.share(fr, remaining); - } - FlowItem::Frame { frame, align, .. } => { - ruler = ruler.max(align.y); - let x = align.x.position(size.x - frame.width()); - let y = offset + ruler.position(size.y - used.y); - let pos = Point::new(x, y); - offset += frame.height(); - output.push_frame(pos, frame); - } - FlowItem::Placed { frame, x_align, y_align, delta, float, .. } => { - let x = x_align.position(size.x - frame.width()); - let y = if float { - match y_align { - Smart::Custom(Some(FixedAlign::Start)) => { - let y = float_top_offset; - float_top_offset += frame.height(); - y - } - Smart::Custom(Some(FixedAlign::End)) => { - let y = size.y - footnote_height - float_bottom_height - + float_bottom_offset; - float_bottom_offset += frame.height(); - y - } - _ => unreachable!("float must be y aligned"), - } - } else { - match y_align { - Smart::Custom(Some(align)) => { - align.position(size.y - frame.height()) - } - _ => offset + ruler.position(size.y - used.y), - } - }; - - let pos = Point::new(x, y) - + delta.zip_map(size, Rel::relative_to).to_point(); - - output.push_frame(pos, frame); - } - FlowItem::Footnote(frame) => { - let y = size.y - footnote_height + footnote_offset; - footnote_offset += frame.height() + self.footnote_config.gap; - output.push_frame(Point::with_y(y), frame); - } - } - } - - // Advance to the next region. - self.finished.push(output); - self.regions.next(); - self.initial = self.regions.size; - self.has_footnotes = false; - - // Try to place floats. - for item in mem::take(&mut self.pending_floats) { - self.layout_item(vt, item)?; - } - - Ok(()) - } - - /// Finish layouting and return the resulting fragment. - fn finish(mut self, vt: &mut Vt) -> SourceResult<Fragment> { - if self.expand.y { - while !self.regions.backlog.is_empty() { - self.finish_region(vt)?; - } - } - - self.finish_region(vt)?; - while !self.items.is_empty() { - self.finish_region(vt)?; - } - - Ok(Fragment::frames(self.finished)) - } -} - -impl FlowLayouter<'_> { - fn try_handle_footnotes( - &mut self, - vt: &mut Vt, - mut notes: Vec<FootnoteElem>, - ) -> SourceResult<()> { - if self.root && !self.handle_footnotes(vt, &mut notes, false, false)? { - self.finish_region(vt)?; - self.handle_footnotes(vt, &mut notes, false, true)?; - } - Ok(()) - } - - /// Processes all footnotes in the frame. - #[tracing::instrument(skip_all)] - fn handle_footnotes( - &mut self, - vt: &mut Vt, - notes: &mut Vec<FootnoteElem>, - movable: bool, - force: bool, - ) -> SourceResult<bool> { - let items_len = self.items.len(); - let notes_len = notes.len(); - - // Process footnotes one at a time. - let mut k = 0; - while k < notes.len() { - if notes[k].is_ref() { - k += 1; - continue; - } - - if !self.has_footnotes { - self.layout_footnote_separator(vt)?; - } - - self.regions.size.y -= self.footnote_config.gap; - let checkpoint = vt.locator.clone(); - let frames = FootnoteEntry::new(notes[k].clone()) - .pack() - .layout(vt, self.styles, self.regions.with_root(false))? - .into_frames(); - - // If the entries didn't fit, abort (to keep footnote and entry - // together). - if !force - && (k == 0 || movable) - && frames.first().map_or(false, Frame::is_empty) - { - // Remove existing footnotes attempts because we need to - // move the item to the next page. - notes.truncate(notes_len); - - // Undo region modifications. - for item in self.items.drain(items_len..) { - self.regions.size.y -= item.height(); - } - - // Undo Vt modifications. - *vt.locator = checkpoint; - - return Ok(false); - } - - let prev = notes.len(); - for (i, frame) in frames.into_iter().enumerate() { - find_footnotes(notes, &frame); - if i > 0 { - self.finish_region(vt)?; - self.layout_footnote_separator(vt)?; - self.regions.size.y -= self.footnote_config.gap; - } - self.regions.size.y -= frame.height(); - self.items.push(FlowItem::Footnote(frame)); - } - - k += 1; - - // Process the nested notes before dealing with further top-level - // notes. - let nested = notes.len() - prev; - if nested > 0 { - notes[k..].rotate_right(nested); - } - } - - Ok(true) - } - - /// Layout and save the footnote separator, typically a line. - #[tracing::instrument(skip_all)] - fn layout_footnote_separator(&mut self, vt: &mut Vt) -> SourceResult<()> { - let expand = Axes::new(self.regions.expand.x, false); - let pod = Regions::one(self.regions.base(), expand); - let separator = &self.footnote_config.separator; - - let mut frame = separator.layout(vt, self.styles, pod)?.into_frame(); - frame.size_mut().y += self.footnote_config.clearance; - frame.translate(Point::with_y(self.footnote_config.clearance)); - - self.has_footnotes = true; - self.regions.size.y -= frame.height(); - self.items.push(FlowItem::Footnote(frame)); - - Ok(()) - } -} - -/// Finds all footnotes in the frame. -#[tracing::instrument(skip_all)] -fn find_footnotes(notes: &mut Vec<FootnoteElem>, frame: &Frame) { - for (_, item) in frame.items() { - match item { - FrameItem::Group(group) => find_footnotes(notes, &group.frame), - FrameItem::Meta(Meta::Elem(content), _) - if !notes.iter().any(|note| note.location() == content.location()) => - { - let Some(footnote) = content.to::<FootnoteElem>() else { continue }; - notes.push(footnote.clone()); - } - _ => {} - } - } -} diff --git a/crates/typst-library/src/layout/fragment.rs b/crates/typst-library/src/layout/fragment.rs deleted file mode 100644 index 3550df2a..00000000 --- a/crates/typst-library/src/layout/fragment.rs +++ /dev/null @@ -1,87 +0,0 @@ -use crate::prelude::*; - -/// A partial layout result. -#[derive(Clone)] -pub struct Fragment(Vec<Frame>); - -impl Fragment { - /// Create a fragment from a single frame. - pub fn frame(frame: Frame) -> Self { - Self(vec![frame]) - } - - /// Create a fragment from multiple frames. - pub fn frames(frames: Vec<Frame>) -> Self { - Self(frames) - } - - /// Return `true` if the length is 0. - pub fn is_empty(&self) -> bool { - self.0.is_empty() - } - - /// The number of frames in the fragment. - pub fn len(&self) -> usize { - self.0.len() - } - - /// Extract the first and only frame. - /// - /// Panics if there are multiple frames. - #[track_caller] - pub fn into_frame(self) -> Frame { - assert_eq!(self.0.len(), 1, "expected exactly one frame"); - self.0.into_iter().next().unwrap() - } - - /// Extract the frames. - pub fn into_frames(self) -> Vec<Frame> { - self.0 - } - - /// Iterate over the contained frames. - pub fn iter(&self) -> std::slice::Iter<Frame> { - self.0.iter() - } - - /// Iterate over the contained frames. - pub fn iter_mut(&mut self) -> std::slice::IterMut<Frame> { - self.0.iter_mut() - } -} - -impl Debug for Fragment { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - match self.0.as_slice() { - [frame] => frame.fmt(f), - frames => frames.fmt(f), - } - } -} - -impl IntoIterator for Fragment { - type Item = Frame; - type IntoIter = std::vec::IntoIter<Frame>; - - fn into_iter(self) -> Self::IntoIter { - self.0.into_iter() - } -} - -impl<'a> IntoIterator for &'a Fragment { - type Item = &'a Frame; - type IntoIter = std::slice::Iter<'a, Frame>; - - fn into_iter(self) -> Self::IntoIter { - self.0.iter() - } -} - -impl<'a> IntoIterator for &'a mut Fragment { - type Item = &'a mut Frame; - type IntoIter = std::slice::IterMut<'a, Frame>; - - fn into_iter(self) -> Self::IntoIter { - self.0.iter_mut() - } -} diff --git a/crates/typst-library/src/layout/grid.rs b/crates/typst-library/src/layout/grid.rs deleted file mode 100644 index 1cde6564..00000000 --- a/crates/typst-library/src/layout/grid.rs +++ /dev/null @@ -1,734 +0,0 @@ -use smallvec::{smallvec, SmallVec}; - -use crate::layout::Sizing; -use crate::prelude::*; -use crate::text::TextElem; - -/// Arranges content in a grid. -/// -/// The grid element allows you to arrange content in a grid. You can define the -/// number of rows and columns, as well as the size of the gutters between them. -/// There are multiple sizing modes for columns and rows that can be used to -/// create complex layouts. -/// -/// The sizing of the grid is determined by the track sizes specified in the -/// arguments. Because each of the sizing parameters accepts the same values, we -/// will explain them just once, here. Each sizing argument accepts an array of -/// individual track sizes. A track size is either: -/// -/// - `{auto}`: The track will be sized to fit its contents. It will be at most -/// as large as the remaining space. If there is more than one `{auto}` track -/// which, and together they claim more than the available space, the `{auto}` -/// tracks will fairly distribute the available space among themselves. -/// -/// - A fixed or relative length (e.g. `{10pt}` or `{20% - 1cm}`): The track -/// will be exactly of this size. -/// -/// - A fractional length (e.g. `{1fr}`): Once all other tracks have been sized, -/// the remaining space will be divided among the fractional tracks according -/// to their fractions. For example, if there are two fractional tracks, each -/// with a fraction of `{1fr}`, they will each take up half of the remaining -/// space. -/// -/// To specify a single track, the array can be omitted in favor of a single -/// value. To specify multiple `{auto}` tracks, enter the number of tracks -/// instead of an array. For example, `columns:` `{3}` is equivalent to -/// `columns:` `{(auto, auto, auto)}`. -/// -/// # Examples -/// The example below demonstrates the different track sizing options. -/// -/// ```example -/// // We use `rect` to emphasize the -/// // area of cells. -/// #set rect( -/// inset: 8pt, -/// fill: rgb("e4e5ea"), -/// width: 100%, -/// ) -/// -/// #grid( -/// columns: (60pt, 1fr, 2fr), -/// rows: (auto, 60pt), -/// gutter: 3pt, -/// rect[Fixed width, auto height], -/// rect[1/3 of the remains], -/// rect[2/3 of the remains], -/// rect(height: 100%)[Fixed height], -/// image("tiger.jpg", height: 100%), -/// image("tiger.jpg", height: 100%), -/// ) -/// ``` -/// -/// You can also [spread]($arguments/#spreading) an array of strings or content -/// into a grid to populate its cells. -/// -/// ```example -/// #grid( -/// columns: 5, -/// gutter: 5pt, -/// ..range(25).map(str) -/// ) -/// ``` -#[elem(Layout)] -pub struct GridElem { - /// The column sizes. - /// - /// Either specify a track size array or provide an integer to create a grid - /// with that many `{auto}`-sized columns. Note that opposed to rows and - /// gutters, providing a single track size will only ever create a single - /// column. - #[borrowed] - pub columns: TrackSizings, - - /// The row sizes. - /// - /// If there are more cells than fit the defined rows, the last row is - /// repeated until there are no more cells. - #[borrowed] - pub rows: TrackSizings, - - /// The gaps between rows & columns. - /// - /// If there are more gutters than defined sizes, the last gutter is repeated. - #[external] - pub gutter: TrackSizings, - - /// The gaps between columns. Takes precedence over `gutter`. - #[parse( - let gutter = args.named("gutter")?; - args.named("column-gutter")?.or_else(|| gutter.clone()) - )] - #[borrowed] - pub column_gutter: TrackSizings, - - /// The gaps between rows. Takes precedence over `gutter`. - #[parse(args.named("row-gutter")?.or_else(|| gutter.clone()))] - #[borrowed] - pub row_gutter: TrackSizings, - - /// The contents of the grid cells. - /// - /// The cells are populated in row-major order. - #[variadic] - pub children: Vec<Content>, -} - -impl Layout for GridElem { - #[tracing::instrument(name = "GridElem::layout", skip_all)] - fn layout( - &self, - vt: &mut Vt, - styles: StyleChain, - regions: Regions, - ) -> SourceResult<Fragment> { - let columns = self.columns(styles); - let rows = self.rows(styles); - let column_gutter = self.column_gutter(styles); - let row_gutter = self.row_gutter(styles); - - // Prepare grid layout by unifying content and gutter tracks. - let layouter = GridLayouter::new( - Axes::new(&columns.0, &rows.0), - Axes::new(&column_gutter.0, &row_gutter.0), - &self.children, - regions, - styles, - self.span(), - ); - - // Measure the columns and layout the grid row-by-row. - Ok(layouter.layout(vt)?.fragment) - } -} - -/// Track sizing definitions. -#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)] -pub struct TrackSizings(pub SmallVec<[Sizing; 4]>); - -cast! { - TrackSizings, - self => self.0.into_value(), - sizing: Sizing => Self(smallvec![sizing]), - count: NonZeroUsize => Self(smallvec![Sizing::Auto; count.get()]), - values: Array => Self(values.into_iter().map(Value::cast).collect::<StrResult<_>>()?), -} - -/// Performs grid layout. -pub struct GridLayouter<'a> { - /// The grid cells. - cells: &'a [Content], - /// Whether this is an RTL grid. - is_rtl: bool, - /// Whether this grid has gutters. - has_gutter: bool, - /// The column tracks including gutter tracks. - cols: Vec<Sizing>, - /// The row tracks including gutter tracks. - rows: Vec<Sizing>, - /// The regions to layout children into. - regions: Regions<'a>, - /// The inherited styles. - styles: StyleChain<'a>, - /// Resolved column sizes. - rcols: Vec<Abs>, - /// The sum of `rcols`. - width: Abs, - /// Resolve row sizes, by region. - rrows: Vec<Vec<RowPiece>>, - /// Rows in the current region. - lrows: Vec<Row>, - /// The initial size of the current region before we started subtracting. - initial: Size, - /// Frames for finished regions. - finished: Vec<Frame>, - /// The span of the grid element. - span: Span, -} - -/// The resulting sizes of columns and rows in a grid. -#[derive(Debug)] -pub struct GridLayout { - /// The fragment. - pub fragment: Fragment, - /// The column widths. - pub cols: Vec<Abs>, - /// The heights of the resulting rows segments, by region. - pub rows: Vec<Vec<RowPiece>>, -} - -/// Details about a resulting row piece. -#[derive(Debug)] -pub struct RowPiece { - /// The height of the segment. - pub height: Abs, - /// The index of the row. - pub y: usize, -} - -/// Produced by initial row layout, auto and relative rows are already finished, -/// fractional rows not yet. -enum Row { - /// Finished row frame of auto or relative row with y index. - Frame(Frame, usize), - /// Fractional row with y index. - Fr(Fr, usize), -} - -impl<'a> GridLayouter<'a> { - /// Create a new grid layouter. - /// - /// This prepares grid layout by unifying content and gutter tracks. - pub fn new( - tracks: Axes<&[Sizing]>, - gutter: Axes<&[Sizing]>, - cells: &'a [Content], - regions: Regions<'a>, - styles: StyleChain<'a>, - span: Span, - ) -> Self { - let mut cols = vec![]; - let mut rows = vec![]; - - // Number of content columns: Always at least one. - let c = tracks.x.len().max(1); - - // Number of content rows: At least as many as given, but also at least - // as many as needed to place each item. - let r = { - let len = cells.len(); - let given = tracks.y.len(); - let needed = len / c + (len % c).clamp(0, 1); - given.max(needed) - }; - - let has_gutter = gutter.any(|tracks| !tracks.is_empty()); - let auto = Sizing::Auto; - let zero = Sizing::Rel(Rel::zero()); - let get_or = |tracks: &[_], idx, default| { - tracks.get(idx).or(tracks.last()).copied().unwrap_or(default) - }; - - // Collect content and gutter columns. - for x in 0..c { - cols.push(get_or(tracks.x, x, auto)); - if has_gutter { - cols.push(get_or(gutter.x, x, zero)); - } - } - - // Collect content and gutter rows. - for y in 0..r { - rows.push(get_or(tracks.y, y, auto)); - if has_gutter { - rows.push(get_or(gutter.y, y, zero)); - } - } - - // Remove superfluous gutter tracks. - if has_gutter { - cols.pop(); - rows.pop(); - } - - // Reverse for RTL. - let is_rtl = TextElem::dir_in(styles) == Dir::RTL; - if is_rtl { - cols.reverse(); - } - - // We use these regions for auto row measurement. Since at that moment, - // columns are already sized, we can enable horizontal expansion. - let mut regions = regions; - regions.expand = Axes::new(true, false); - - Self { - cells, - is_rtl, - has_gutter, - rows, - regions, - styles, - rcols: vec![Abs::zero(); cols.len()], - cols, - width: Abs::zero(), - rrows: vec![], - lrows: vec![], - initial: regions.size, - finished: vec![], - span, - } - } - - /// Determines the columns sizes and then layouts the grid row-by-row. - pub fn layout(mut self, vt: &mut Vt) -> SourceResult<GridLayout> { - self.measure_columns(vt)?; - - for y in 0..self.rows.len() { - // Skip to next region if current one is full, but only for content - // rows, not for gutter rows. - if self.regions.is_full() && (!self.has_gutter || y % 2 == 0) { - self.finish_region(vt)?; - } - - match self.rows[y] { - Sizing::Auto => self.layout_auto_row(vt, y)?, - Sizing::Rel(v) => self.layout_relative_row(vt, v, y)?, - Sizing::Fr(v) => self.lrows.push(Row::Fr(v, y)), - } - } - - self.finish_region(vt)?; - - Ok(GridLayout { - fragment: Fragment::frames(self.finished), - cols: self.rcols, - rows: self.rrows, - }) - } - - /// Determine all column sizes. - #[tracing::instrument(name = "GridLayouter::measure_columns", skip_all)] - fn measure_columns(&mut self, vt: &mut Vt) -> SourceResult<()> { - // Sum of sizes of resolved relative tracks. - let mut rel = Abs::zero(); - - // Sum of fractions of all fractional tracks. - let mut fr = Fr::zero(); - - // Resolve the size of all relative columns and compute the sum of all - // fractional tracks. - for (&col, rcol) in self.cols.iter().zip(&mut self.rcols) { - match col { - Sizing::Auto => {} - Sizing::Rel(v) => { - let resolved = - v.resolve(self.styles).relative_to(self.regions.base().x); - *rcol = resolved; - rel += resolved; - } - Sizing::Fr(v) => fr += v, - } - } - - // Size that is not used by fixed-size columns. - let available = self.regions.size.x - rel; - if available >= Abs::zero() { - // Determine size of auto columns. - let (auto, count) = self.measure_auto_columns(vt, available)?; - - // If there is remaining space, distribute it to fractional columns, - // otherwise shrink auto columns. - let remaining = available - auto; - if remaining >= Abs::zero() { - self.grow_fractional_columns(remaining, fr); - } else { - self.shrink_auto_columns(available, count); - } - } - - // Sum up the resolved column sizes once here. - self.width = self.rcols.iter().sum(); - - Ok(()) - } - - /// Measure the size that is available to auto columns. - fn measure_auto_columns( - &mut self, - vt: &mut Vt, - available: Abs, - ) -> SourceResult<(Abs, usize)> { - let mut auto = Abs::zero(); - let mut count = 0; - - // Determine size of auto columns by laying out all cells in those - // columns, measuring them and finding the largest one. - for (x, &col) in self.cols.iter().enumerate() { - if col != Sizing::Auto { - continue; - } - - let mut resolved = Abs::zero(); - for y in 0..self.rows.len() { - if let Some(cell) = self.cell(x, y) { - // For relative rows, we can already resolve the correct - // base and for auto and fr we could only guess anyway. - let height = match self.rows[y] { - Sizing::Rel(v) => { - v.resolve(self.styles).relative_to(self.regions.base().y) - } - _ => self.regions.base().y, - }; - - let size = Size::new(available, height); - let pod = Regions::one(size, Axes::splat(false)); - let frame = cell.measure(vt, self.styles, pod)?.into_frame(); - resolved.set_max(frame.width()); - } - } - - self.rcols[x] = resolved; - auto += resolved; - count += 1; - } - - Ok((auto, count)) - } - - /// Distribute remaining space to fractional columns. - fn grow_fractional_columns(&mut self, remaining: Abs, fr: Fr) { - if fr.is_zero() { - return; - } - - for (&col, rcol) in self.cols.iter().zip(&mut self.rcols) { - if let Sizing::Fr(v) = col { - *rcol = v.share(fr, remaining); - } - } - } - - /// Redistribute space to auto columns so that each gets a fair share. - fn shrink_auto_columns(&mut self, available: Abs, count: usize) { - let mut last; - let mut fair = -Abs::inf(); - let mut redistribute = available; - let mut overlarge = count; - let mut changed = true; - - // Iteratively remove columns that don't need to be shrunk. - while changed && overlarge > 0 { - changed = false; - last = fair; - fair = redistribute / (overlarge as f64); - - for (&col, &rcol) in self.cols.iter().zip(&self.rcols) { - // Remove an auto column if it is not overlarge (rcol <= fair), - // but also hasn't already been removed (rcol > last). - if col == Sizing::Auto && rcol <= fair && rcol > last { - redistribute -= rcol; - overlarge -= 1; - changed = true; - } - } - } - - // Redistribute space fairly among overlarge columns. - for (&col, rcol) in self.cols.iter().zip(&mut self.rcols) { - if col == Sizing::Auto && *rcol > fair { - *rcol = fair; - } - } - } - - /// Layout a row with automatic height. Such a row may break across multiple - /// regions. - fn layout_auto_row(&mut self, vt: &mut Vt, y: usize) -> SourceResult<()> { - // Determine the size for each region of the row. If the first region - // ends up empty for some column, skip the region and remeasure. - let mut resolved = match self.measure_auto_row(vt, y, true)? { - Some(resolved) => resolved, - None => { - self.finish_region(vt)?; - self.measure_auto_row(vt, y, false)?.unwrap() - } - }; - - // Nothing to layout. - if resolved.is_empty() { - return Ok(()); - } - - // Layout into a single region. - if let &[first] = resolved.as_slice() { - let frame = self.layout_single_row(vt, first, y)?; - self.push_row(frame, y); - return Ok(()); - } - - // Expand all but the last region. - // Skip the first region if the space is eaten up by an fr row. - let len = resolved.len(); - for (region, target) in self - .regions - .iter() - .zip(&mut resolved[..len - 1]) - .skip(self.lrows.iter().any(|row| matches!(row, Row::Fr(..))) as usize) - { - target.set_max(region.y); - } - - // Layout into multiple regions. - let fragment = self.layout_multi_row(vt, &resolved, y)?; - let len = fragment.len(); - for (i, frame) in fragment.into_iter().enumerate() { - self.push_row(frame, y); - if i + 1 < len { - self.finish_region(vt)?; - } - } - - Ok(()) - } - - /// Measure the regions sizes of an auto row. The option is always `Some(_)` - /// if `can_skip` is false. - fn measure_auto_row( - &mut self, - vt: &mut Vt, - y: usize, - can_skip: bool, - ) -> SourceResult<Option<Vec<Abs>>> { - let mut resolved: Vec<Abs> = vec![]; - - for (x, &rcol) in self.rcols.iter().enumerate() { - if let Some(cell) = self.cell(x, y) { - let mut pod = self.regions; - pod.size.x = rcol; - - let frames = cell.measure(vt, self.styles, pod)?.into_frames(); - - // Skip the first region if one cell in it is empty. Then, - // remeasure. - if let [first, rest @ ..] = frames.as_slice() { - if can_skip - && first.is_empty() - && rest.iter().any(|frame| !frame.is_empty()) - { - return Ok(None); - } - } - - let mut sizes = frames.iter().map(|frame| frame.height()); - for (target, size) in resolved.iter_mut().zip(&mut sizes) { - target.set_max(size); - } - - // New heights are maximal by virtue of being new. Note that - // this extend only uses the rest of the sizes iterator. - resolved.extend(sizes); - } - } - - Ok(Some(resolved)) - } - - /// Layout a row with relative height. Such a row cannot break across - /// multiple regions, but it may force a region break. - fn layout_relative_row( - &mut self, - vt: &mut Vt, - v: Rel<Length>, - y: usize, - ) -> SourceResult<()> { - let resolved = v.resolve(self.styles).relative_to(self.regions.base().y); - let frame = self.layout_single_row(vt, resolved, y)?; - - // Skip to fitting region. - let height = frame.height(); - while !self.regions.size.y.fits(height) && !self.regions.in_last() { - self.finish_region(vt)?; - - // Don't skip multiple regions for gutter and don't push a row. - if self.has_gutter && y % 2 == 1 { - return Ok(()); - } - } - - self.push_row(frame, y); - - Ok(()) - } - - /// Layout a row with fixed height and return its frame. - fn layout_single_row( - &mut self, - vt: &mut Vt, - height: Abs, - y: usize, - ) -> SourceResult<Frame> { - if !height.is_finite() { - bail!(error!(self.span, "cannot create grid with infinite height")); - } - - let mut output = Frame::soft(Size::new(self.width, height)); - let mut pos = Point::zero(); - - for (x, &rcol) in self.rcols.iter().enumerate() { - if let Some(cell) = self.cell(x, y) { - let size = Size::new(rcol, height); - let mut pod = Regions::one(size, Axes::splat(true)); - if self.rows[y] == Sizing::Auto { - pod.full = self.regions.full; - } - let frame = cell.layout(vt, self.styles, pod)?.into_frame(); - output.push_frame(pos, frame); - } - - pos.x += rcol; - } - - Ok(output) - } - - /// Layout a row spanning multiple regions. - fn layout_multi_row( - &mut self, - vt: &mut Vt, - heights: &[Abs], - y: usize, - ) -> SourceResult<Fragment> { - // Prepare frames. - let mut outputs: Vec<_> = heights - .iter() - .map(|&h| Frame::soft(Size::new(self.width, h))) - .collect(); - - // Prepare regions. - let size = Size::new(self.width, heights[0]); - let mut pod = Regions::one(size, Axes::splat(true)); - pod.full = self.regions.full; - pod.backlog = &heights[1..]; - - // Layout the row. - let mut pos = Point::zero(); - for (x, &rcol) in self.rcols.iter().enumerate() { - if let Some(cell) = self.cell(x, y) { - pod.size.x = rcol; - - // Push the layouted frames into the individual output frames. - let fragment = cell.layout(vt, self.styles, pod)?; - for (output, frame) in outputs.iter_mut().zip(fragment) { - output.push_frame(pos, frame); - } - } - - pos.x += rcol; - } - - Ok(Fragment::frames(outputs)) - } - - /// Push a row frame into the current region. - fn push_row(&mut self, frame: Frame, y: usize) { - self.regions.size.y -= frame.height(); - self.lrows.push(Row::Frame(frame, y)); - } - - /// Finish rows for one region. - fn finish_region(&mut self, vt: &mut Vt) -> SourceResult<()> { - // Determine the height of existing rows in the region. - let mut used = Abs::zero(); - let mut fr = Fr::zero(); - for row in &self.lrows { - match row { - Row::Frame(frame, _) => used += frame.height(), - Row::Fr(v, _) => fr += *v, - } - } - - // Determine the size of the grid in this region, expanding fully if - // there are fr rows. - let mut size = Size::new(self.width, used).min(self.initial); - if fr.get() > 0.0 && self.initial.y.is_finite() { - size.y = self.initial.y; - } - - // The frame for the region. - let mut output = Frame::soft(size); - let mut pos = Point::zero(); - let mut rrows = vec![]; - - // Place finished rows and layout fractional rows. - for row in std::mem::take(&mut self.lrows) { - let (frame, y) = match row { - Row::Frame(frame, y) => (frame, y), - Row::Fr(v, y) => { - let remaining = self.regions.full - used; - let height = v.share(fr, remaining); - (self.layout_single_row(vt, height, y)?, y) - } - }; - - let height = frame.height(); - output.push_frame(pos, frame); - rrows.push(RowPiece { height, y }); - pos.y += height; - } - - self.finished.push(output); - self.rrows.push(rrows); - self.regions.next(); - self.initial = self.regions.size; - - Ok(()) - } - - /// Get the content of the cell in column `x` and row `y`. - /// - /// Returns `None` if it's a gutter cell. - #[track_caller] - fn cell(&self, mut x: usize, y: usize) -> Option<&'a Content> { - assert!(x < self.cols.len()); - assert!(y < self.rows.len()); - - // Columns are reorder, but the cell slice is not. - if self.is_rtl { - x = self.cols.len() - 1 - x; - } - - if self.has_gutter { - // Even columns and rows are children, odd ones are gutter. - if x % 2 == 0 && y % 2 == 0 { - let c = 1 + self.cols.len() / 2; - self.cells.get((y / 2) * c + x / 2) - } else { - None - } - } else { - let c = self.cols.len(); - self.cells.get(y * c + x) - } - } -} diff --git a/crates/typst-library/src/layout/hide.rs b/crates/typst-library/src/layout/hide.rs deleted file mode 100644 index af3d0631..00000000 --- a/crates/typst-library/src/layout/hide.rs +++ /dev/null @@ -1,29 +0,0 @@ -use smallvec::smallvec; - -use crate::prelude::*; - -/// Hides content without affecting layout. -/// -/// The `hide` function allows you to hide content while the layout still 'sees' -/// it. This is useful to create whitespace that is exactly as large as some -/// content. It may also be useful to redact content because its arguments are -/// not included in the output. -/// -/// # Example -/// ```example -/// Hello Jane \ -/// #hide[Hello] Joe -/// ``` -#[elem(Show)] -pub struct HideElem { - /// The content to hide. - #[required] - pub body: Content, -} - -impl Show for HideElem { - #[tracing::instrument(name = "HideElem::show", skip(self))] - fn show(&self, _: &mut Vt, _: StyleChain) -> SourceResult<Content> { - Ok(self.body().clone().styled(MetaElem::set_data(smallvec![Meta::Hide]))) - } -} diff --git a/crates/typst-library/src/layout/list.rs b/crates/typst-library/src/layout/list.rs deleted file mode 100644 index 7c089760..00000000 --- a/crates/typst-library/src/layout/list.rs +++ /dev/null @@ -1,238 +0,0 @@ -use crate::layout::{BlockElem, GridLayouter, ParElem, Sizing, Spacing}; -use crate::prelude::*; -use crate::text::TextElem; - -/// A bullet list. -/// -/// Displays a sequence of items vertically, with each item introduced by a -/// marker. -/// -/// # Example -/// ```example -/// Normal list. -/// - Text -/// - Math -/// - Layout -/// - ... -/// -/// Multiple lines. -/// - This list item spans multiple -/// lines because it is indented. -/// -/// Function call. -/// #list( -/// [Foundations], -/// [Calculate], -/// [Construct], -/// [Data Loading], -/// ) -/// ``` -/// -/// # Syntax -/// This functions also has dedicated syntax: Start a line with a hyphen, -/// followed by a space to create a list item. A list item can contain multiple -/// paragraphs and other block-level content. All content that is indented -/// more than an item's marker becomes part of that item. -#[elem(scope, title = "Bullet List", Layout)] -pub struct ListElem { - /// If this is `{false}`, the items are spaced apart with - /// [list spacing]($list.spacing). If it is `{true}`, they use normal - /// [leading]($par.leading) instead. This makes the list more compact, which - /// can look better if the items are short. - /// - /// In markup mode, the value of this parameter is determined based on - /// whether items are separated with a blank line. If items directly follow - /// each other, this is set to `{true}`; if items are separated by a blank - /// line, this is set to `{false}`. - /// - /// ```example - /// - If a list has a lot of text, and - /// maybe other inline content, it - /// should not be tight anymore. - /// - /// - To make a list wide, simply insert - /// a blank line between the items. - /// ``` - #[default(true)] - pub tight: bool, - - /// The marker which introduces each item. - /// - /// Instead of plain content, you can also pass an array with multiple - /// markers that should be used for nested lists. If the list nesting depth - /// exceeds the number of markers, the last one is repeated. For total - /// control, you may pass a function that maps the list's nesting depth - /// (starting from `{0}`) to a desired marker. - /// - /// ```example - /// #set list(marker: [--]) - /// - A more classic list - /// - With en-dashes - /// - /// #set list(marker: ([•], [--])) - /// - Top-level - /// - Nested - /// - Items - /// - Items - /// ``` - #[borrowed] - #[default(ListMarker::Content(vec![TextElem::packed('•')]))] - pub marker: ListMarker, - - /// The indent of each item. - #[resolve] - pub indent: Length, - - /// The spacing between the marker and the body of each item. - #[resolve] - #[default(Em::new(0.5).into())] - pub body_indent: Length, - - /// The spacing between the items of a wide (non-tight) list. - /// - /// If set to `{auto}`, uses the spacing [below blocks]($block.below). - pub spacing: Smart<Spacing>, - - /// The bullet list's children. - /// - /// When using the list syntax, adjacent items are automatically collected - /// into lists, even through constructs like for loops. - /// - /// ```example - /// #for letter in "ABC" [ - /// - Letter #letter - /// ] - /// ``` - #[variadic] - pub children: Vec<ListItem>, - - /// The nesting depth. - #[internal] - #[fold] - depth: Depth, -} - -#[scope] -impl ListElem { - #[elem] - type ListItem; -} - -impl Layout for ListElem { - #[tracing::instrument(name = "ListElem::layout", skip_all)] - fn layout( - &self, - vt: &mut Vt, - styles: StyleChain, - regions: Regions, - ) -> SourceResult<Fragment> { - let indent = self.indent(styles); - let body_indent = self.body_indent(styles); - let gutter = if self.tight(styles) { - ParElem::leading_in(styles).into() - } else { - self.spacing(styles) - .unwrap_or_else(|| *BlockElem::below_in(styles).amount()) - }; - - let depth = self.depth(styles); - let marker = self - .marker(styles) - .resolve(vt, depth)? - // avoid '#set align' interference with the list - .aligned(HAlign::Start + VAlign::Top); - - let mut cells = vec![]; - for item in self.children() { - cells.push(Content::empty()); - cells.push(marker.clone()); - cells.push(Content::empty()); - cells.push(item.body().clone().styled(Self::set_depth(Depth))); - } - - let layouter = GridLayouter::new( - Axes::with_x(&[ - Sizing::Rel(indent.into()), - Sizing::Auto, - Sizing::Rel(body_indent.into()), - Sizing::Auto, - ]), - Axes::with_y(&[gutter.into()]), - &cells, - regions, - styles, - self.span(), - ); - - Ok(layouter.layout(vt)?.fragment) - } -} - -/// A bullet list item. -#[elem(name = "item", title = "Bullet List Item")] -pub struct ListItem { - /// The item's body. - #[required] - pub body: Content, -} - -cast! { - ListItem, - v: Content => v.to::<Self>().cloned().unwrap_or_else(|| Self::new(v.clone())), -} - -/// A list's marker. -#[derive(Debug, Clone, PartialEq, Hash)] -pub enum ListMarker { - Content(Vec<Content>), - Func(Func), -} - -impl ListMarker { - /// Resolve the marker for the given depth. - fn resolve(&self, vt: &mut Vt, depth: usize) -> SourceResult<Content> { - Ok(match self { - Self::Content(list) => { - list.get(depth).or(list.last()).cloned().unwrap_or_default() - } - Self::Func(func) => func.call_vt(vt, [depth])?.display(), - }) - } -} - -cast! { - ListMarker, - self => match self { - Self::Content(vec) => if vec.len() == 1 { - vec.into_iter().next().unwrap().into_value() - } else { - vec.into_value() - }, - Self::Func(func) => func.into_value(), - }, - v: Content => Self::Content(vec![v]), - array: Array => { - if array.is_empty() { - bail!("array must contain at least one marker"); - } - Self::Content(array.into_iter().map(Value::display).collect()) - }, - v: Func => Self::Func(v), -} - -#[derive(Debug, Clone, Copy, PartialEq, Hash)] -struct Depth; - -cast! { - Depth, - self => Value::None, - _: Value => Self, -} - -impl Fold for Depth { - type Output = usize; - - fn fold(self, outer: Self::Output) -> Self::Output { - outer + 1 - } -} diff --git a/crates/typst-library/src/layout/measure.rs b/crates/typst-library/src/layout/measure.rs deleted file mode 100644 index d41b7f95..00000000 --- a/crates/typst-library/src/layout/measure.rs +++ /dev/null @@ -1,53 +0,0 @@ -use crate::prelude::*; - -/// Measures the layouted size of content. -/// -/// The `measure` function lets you determine the layouted size of content. Note -/// that an infinite space is assumed, therefore the measured height/width may -/// not necessarily match the final height/width of the measured content. If you -/// want to measure in the current layout dimensions, you can combine `measure` -/// and [`layout`]($layout). -/// -/// # Example -/// The same content can have a different size depending on the styles that -/// are active when it is layouted. For example, in the example below -/// `[#content]` is of course bigger when we increase the font size. -/// -/// ```example -/// #let content = [Hello!] -/// #content -/// #set text(14pt) -/// #content -/// ``` -/// -/// To do a meaningful measurement, you therefore first need to retrieve the -/// active styles with the [`style`]($style) function. You can then pass them to -/// the `measure` function. -/// -/// ```example -/// #let thing(body) = style(styles => { -/// let size = measure(body, styles) -/// [Width of "#body" is #size.width] -/// }) -/// -/// #thing[Hey] \ -/// #thing[Welcome] -/// ``` -/// -/// The measure function returns a dictionary with the entries `width` and -/// `height`, both of type [`length`]($length). -#[func] -pub fn measure( - /// The virtual machine. - vm: &mut Vm, - /// The content whose size to measure. - content: Content, - /// The styles with which to layout the content. - styles: Styles, -) -> SourceResult<Dict> { - let pod = Regions::one(Axes::splat(Abs::inf()), Axes::splat(false)); - let styles = StyleChain::new(&styles); - let frame = content.measure(&mut vm.vt, styles, pod)?.into_frame(); - let Size { x, y } = frame.size(); - Ok(dict! { "width" => x, "height" => y }) -} diff --git a/crates/typst-library/src/layout/mod.rs b/crates/typst-library/src/layout/mod.rs deleted file mode 100644 index bcbe3b2b..00000000 --- a/crates/typst-library/src/layout/mod.rs +++ /dev/null @@ -1,786 +0,0 @@ -//! Composable layouts. - -mod align; -mod columns; -mod container; -#[path = "enum.rs"] -mod enum_; -mod flow; -mod fragment; -mod grid; -mod hide; -mod list; -#[path = "measure.rs"] -mod measure_; -mod pad; -mod page; -mod par; -mod place; -mod regions; -mod repeat; -mod spacing; -mod stack; -mod table; -mod terms; -mod transform; - -pub use self::align::*; -pub use self::columns::*; -pub use self::container::*; -pub use self::enum_::*; -pub use self::flow::*; -pub use self::fragment::*; -pub use self::grid::*; -pub use self::hide::*; -pub use self::list::*; -pub use self::measure_::*; -pub use self::pad::*; -pub use self::page::*; -pub use self::par::*; -pub use self::place::*; -pub use self::regions::*; -pub use self::repeat::*; -pub use self::spacing::*; -pub use self::stack::*; -pub use self::table::*; -pub use self::terms::*; -pub use self::transform::*; - -use std::borrow::Cow; -use std::mem; - -use typed_arena::Arena; -use typst::diag::SourceResult; -use typst::eval::Tracer; -use typst::model::{applicable, realize, DelayedErrors, StyleVecBuilder}; - -use crate::math::{EquationElem, LayoutMath}; -use crate::meta::{CiteElem, CiteGroup, DocumentElem}; -use crate::prelude::*; -use crate::shared::BehavedBuilder; -use crate::text::{LinebreakElem, SmartquoteElem, SpaceElem, TextElem}; -use crate::visualize::{ - CircleElem, EllipseElem, ImageElem, LineElem, PathElem, PolygonElem, RectElem, - SquareElem, -}; - -/// Hook up all layout definitions. -pub(super) fn define(global: &mut Scope) { - global.category("layout"); - global.define_type::<Length>(); - global.define_type::<Angle>(); - global.define_type::<Ratio>(); - global.define_type::<Rel<Length>>(); - global.define_type::<Fr>(); - global.define_type::<Dir>(); - global.define_type::<Align>(); - global.define_elem::<PageElem>(); - global.define_elem::<PagebreakElem>(); - global.define_elem::<VElem>(); - global.define_elem::<ParElem>(); - global.define_elem::<ParbreakElem>(); - global.define_elem::<HElem>(); - global.define_elem::<BoxElem>(); - global.define_elem::<BlockElem>(); - global.define_elem::<ListElem>(); - global.define_elem::<EnumElem>(); - global.define_elem::<TermsElem>(); - global.define_elem::<TableElem>(); - global.define_elem::<StackElem>(); - global.define_elem::<GridElem>(); - global.define_elem::<ColumnsElem>(); - global.define_elem::<ColbreakElem>(); - global.define_elem::<PlaceElem>(); - global.define_elem::<AlignElem>(); - global.define_elem::<PadElem>(); - global.define_elem::<RepeatElem>(); - global.define_elem::<MoveElem>(); - global.define_elem::<ScaleElem>(); - global.define_elem::<RotateElem>(); - global.define_elem::<HideElem>(); - global.define_func::<measure>(); -} - -/// Root-level layout. -pub trait LayoutRoot { - /// Layout into one frame per page. - fn layout_root(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Document>; -} - -impl LayoutRoot for Content { - #[tracing::instrument(name = "Content::layout_root", skip_all)] - fn layout_root(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Document> { - #[comemo::memoize] - fn cached( - content: &Content, - world: Tracked<dyn World + '_>, - introspector: Tracked<Introspector>, - locator: Tracked<Locator>, - delayed: TrackedMut<DelayedErrors>, - tracer: TrackedMut<Tracer>, - styles: StyleChain, - ) -> SourceResult<Document> { - let mut locator = Locator::chained(locator); - let mut vt = Vt { - world, - introspector, - locator: &mut locator, - delayed, - tracer, - }; - let scratch = Scratch::default(); - let (realized, styles) = realize_root(&mut vt, &scratch, content, styles)?; - realized - .with::<dyn LayoutRoot>() - .unwrap() - .layout_root(&mut vt, styles) - } - - tracing::info!("Starting layout"); - cached( - self, - vt.world, - vt.introspector, - vt.locator.track(), - TrackedMut::reborrow_mut(&mut vt.delayed), - TrackedMut::reborrow_mut(&mut vt.tracer), - styles, - ) - } -} - -/// Layout into regions. -pub trait Layout { - /// Layout into one frame per region. - fn layout( - &self, - vt: &mut Vt, - styles: StyleChain, - regions: Regions, - ) -> SourceResult<Fragment>; - - /// Layout without side effects. - /// - /// This element must be layouted again in the same order for the results to - /// be valid. - #[tracing::instrument(name = "Layout::measure", skip_all)] - fn measure( - &self, - vt: &mut Vt, - styles: StyleChain, - regions: Regions, - ) -> SourceResult<Fragment> { - let mut locator = Locator::chained(vt.locator.track()); - let mut vt = Vt { - world: vt.world, - introspector: vt.introspector, - locator: &mut locator, - tracer: TrackedMut::reborrow_mut(&mut vt.tracer), - delayed: TrackedMut::reborrow_mut(&mut vt.delayed), - }; - self.layout(&mut vt, styles, regions) - } -} - -impl Layout for Content { - #[tracing::instrument(name = "Content::layout", skip_all)] - fn layout( - &self, - vt: &mut Vt, - styles: StyleChain, - regions: Regions, - ) -> SourceResult<Fragment> { - #[allow(clippy::too_many_arguments)] - #[comemo::memoize] - fn cached( - content: &Content, - world: Tracked<dyn World + '_>, - introspector: Tracked<Introspector>, - locator: Tracked<Locator>, - delayed: TrackedMut<DelayedErrors>, - tracer: TrackedMut<Tracer>, - styles: StyleChain, - regions: Regions, - ) -> SourceResult<Fragment> { - let mut locator = Locator::chained(locator); - let mut vt = Vt { - world, - introspector, - locator: &mut locator, - delayed, - tracer, - }; - let scratch = Scratch::default(); - let (realized, styles) = realize_block(&mut vt, &scratch, content, styles)?; - realized - .with::<dyn Layout>() - .unwrap() - .layout(&mut vt, styles, regions) - } - - tracing::info!("Layouting `Content`"); - - let fragment = cached( - self, - vt.world, - vt.introspector, - vt.locator.track(), - TrackedMut::reborrow_mut(&mut vt.delayed), - TrackedMut::reborrow_mut(&mut vt.tracer), - styles, - regions, - )?; - - vt.locator.visit_frames(&fragment); - Ok(fragment) - } -} - -/// Realize into an element that is capable of root-level layout. -#[tracing::instrument(skip_all)] -fn realize_root<'a>( - vt: &mut Vt, - scratch: &'a Scratch<'a>, - content: &'a Content, - styles: StyleChain<'a>, -) -> SourceResult<(Cow<'a, Content>, StyleChain<'a>)> { - if content.can::<dyn LayoutRoot>() && !applicable(content, styles) { - return Ok((Cow::Borrowed(content), styles)); - } - - let mut builder = Builder::new(vt, scratch, true); - builder.accept(content, styles)?; - builder.interrupt_page(Some(styles), true)?; - let (pages, shared) = builder.doc.unwrap().pages.finish(); - Ok((Cow::Owned(DocumentElem::new(pages.to_vec()).pack()), shared)) -} - -/// Realize into an element that is capable of block-level layout. -#[tracing::instrument(skip_all)] -fn realize_block<'a>( - vt: &mut Vt, - scratch: &'a Scratch<'a>, - content: &'a Content, - styles: StyleChain<'a>, -) -> SourceResult<(Cow<'a, Content>, StyleChain<'a>)> { - // These elements implement `Layout` but still require a flow for - // proper layout. - if content.can::<dyn Layout>() - && !content.is::<BoxElem>() - && !content.is::<LineElem>() - && !content.is::<RectElem>() - && !content.is::<SquareElem>() - && !content.is::<EllipseElem>() - && !content.is::<CircleElem>() - && !content.is::<ImageElem>() - && !content.is::<PolygonElem>() - && !content.is::<PathElem>() - && !content.is::<PlaceElem>() - && !applicable(content, styles) - { - return Ok((Cow::Borrowed(content), styles)); - } - - let mut builder = Builder::new(vt, scratch, false); - builder.accept(content, styles)?; - builder.interrupt_par()?; - let (children, shared) = builder.flow.0.finish(); - Ok((Cow::Owned(FlowElem::new(children.to_vec()).pack()), shared)) -} - -/// Builds a document or a flow element from content. -struct Builder<'a, 'v, 't> { - /// The virtual typesetter. - vt: &'v mut Vt<'t>, - /// Scratch arenas for building. - scratch: &'a Scratch<'a>, - /// The current document building state. - doc: Option<DocBuilder<'a>>, - /// The current flow building state. - flow: FlowBuilder<'a>, - /// The current paragraph building state. - par: ParBuilder<'a>, - /// The current list building state. - list: ListBuilder<'a>, - /// The current citation grouping state. - cites: CiteGroupBuilder<'a>, -} - -/// Temporary storage arenas for building. -#[derive(Default)] -struct Scratch<'a> { - /// An arena where intermediate style chains are stored. - styles: Arena<StyleChain<'a>>, - /// An arena where intermediate content resulting from show rules is stored. - content: Arena<Content>, -} - -impl<'a, 'v, 't> Builder<'a, 'v, 't> { - fn new(vt: &'v mut Vt<'t>, scratch: &'a Scratch<'a>, top: bool) -> Self { - Self { - vt, - scratch, - doc: top.then(DocBuilder::default), - flow: FlowBuilder::default(), - par: ParBuilder::default(), - list: ListBuilder::default(), - cites: CiteGroupBuilder::default(), - } - } - - fn accept( - &mut self, - mut content: &'a Content, - styles: StyleChain<'a>, - ) -> SourceResult<()> { - if content.can::<dyn LayoutMath>() && !content.is::<EquationElem>() { - content = - self.scratch.content.alloc(EquationElem::new(content.clone()).pack()); - } - - if let Some(realized) = realize(self.vt, content, styles)? { - let stored = self.scratch.content.alloc(realized); - return self.accept(stored, styles); - } - - if let Some((elem, local)) = content.to_styled() { - return self.styled(elem, local, styles); - } - - if let Some(children) = content.to_sequence() { - for elem in children { - self.accept(elem, styles)?; - } - return Ok(()); - } - - if self.cites.accept(content, styles) { - return Ok(()); - } - - self.interrupt_cites()?; - - if self.list.accept(content, styles) { - return Ok(()); - } - - self.interrupt_list()?; - - if self.list.accept(content, styles) { - return Ok(()); - } - - if self.par.accept(content, styles) { - return Ok(()); - } - - self.interrupt_par()?; - - if self.flow.accept(content, styles) { - return Ok(()); - } - - let keep = content - .to::<PagebreakElem>() - .map_or(false, |pagebreak| !pagebreak.weak(styles)); - - self.interrupt_page(keep.then_some(styles), false)?; - - if let Some(doc) = &mut self.doc { - if doc.accept(content, styles) { - return Ok(()); - } - } - - if content.is::<PagebreakElem>() { - bail!(content.span(), "pagebreaks are not allowed inside of containers"); - } else { - bail!(content.span(), "{} is not allowed here", content.func().name()); - } - } - - fn styled( - &mut self, - elem: &'a Content, - map: &'a Styles, - styles: StyleChain<'a>, - ) -> SourceResult<()> { - let stored = self.scratch.styles.alloc(styles); - let styles = stored.chain(map); - self.interrupt_style(map, None)?; - self.accept(elem, styles)?; - self.interrupt_style(map, Some(styles))?; - Ok(()) - } - - fn interrupt_style( - &mut self, - local: &Styles, - outer: Option<StyleChain<'a>>, - ) -> SourceResult<()> { - if let Some(Some(span)) = local.interruption::<DocumentElem>() { - if self.doc.is_none() { - bail!(span, "document set rules are not allowed inside of containers"); - } - if outer.is_none() - && (!self.flow.0.is_empty() - || !self.par.0.is_empty() - || !self.list.items.is_empty()) - { - bail!(span, "document set rules must appear before any content"); - } - } else if let Some(Some(span)) = local.interruption::<PageElem>() { - if self.doc.is_none() { - bail!(span, "page configuration is not allowed inside of containers"); - } - self.interrupt_page(outer, false)?; - } else if local.interruption::<ParElem>().is_some() - || local.interruption::<AlignElem>().is_some() - { - self.interrupt_par()?; - } else if local.interruption::<ListElem>().is_some() - || local.interruption::<EnumElem>().is_some() - || local.interruption::<TermsElem>().is_some() - { - self.interrupt_list()?; - } - Ok(()) - } - - fn interrupt_cites(&mut self) -> SourceResult<()> { - if !self.cites.items.is_empty() { - let staged = mem::take(&mut self.cites.staged); - let (group, styles) = mem::take(&mut self.cites).finish(); - let stored = self.scratch.content.alloc(group); - self.accept(stored, styles)?; - for (content, styles) in staged { - self.accept(content, styles)?; - } - } - Ok(()) - } - - fn interrupt_list(&mut self) -> SourceResult<()> { - self.interrupt_cites()?; - if !self.list.items.is_empty() { - let staged = mem::take(&mut self.list.staged); - let (list, styles) = mem::take(&mut self.list).finish(); - let stored = self.scratch.content.alloc(list); - self.accept(stored, styles)?; - for (content, styles) in staged { - self.accept(content, styles)?; - } - } - Ok(()) - } - - fn interrupt_par(&mut self) -> SourceResult<()> { - self.interrupt_list()?; - if !self.par.0.is_empty() { - let (par, styles) = mem::take(&mut self.par).finish(); - let stored = self.scratch.content.alloc(par); - self.accept(stored, styles)?; - } - - Ok(()) - } - - fn interrupt_page( - &mut self, - styles: Option<StyleChain<'a>>, - last: bool, - ) -> SourceResult<()> { - self.interrupt_par()?; - let Some(doc) = &mut self.doc else { return Ok(()) }; - if (doc.keep_next && styles.is_some()) || self.flow.0.has_strong_elements(last) { - let (flow, shared) = mem::take(&mut self.flow).0.finish(); - let styles = if shared == StyleChain::default() { - styles.unwrap_or_default() - } else { - shared - }; - let page = PageElem::new(FlowElem::new(flow.to_vec()).pack()); - let stored = self.scratch.content.alloc(page.pack()); - self.accept(stored, styles)?; - } - Ok(()) - } -} - -/// Accepts pagebreaks and pages. -struct DocBuilder<'a> { - /// The page runs built so far. - pages: StyleVecBuilder<'a, Cow<'a, Content>>, - /// Whether to keep a following page even if it is empty. - keep_next: bool, - /// Whether the next page should be cleared to an even or odd number. - clear_next: Option<Parity>, -} - -impl<'a> DocBuilder<'a> { - fn accept(&mut self, content: &'a Content, styles: StyleChain<'a>) -> bool { - if let Some(pagebreak) = content.to::<PagebreakElem>() { - self.keep_next = !pagebreak.weak(styles); - self.clear_next = pagebreak.to(styles); - return true; - } - - if let Some(page) = content.to::<PageElem>() { - let elem = if let Some(clear_to) = self.clear_next.take() { - let mut page = page.clone(); - page.push_clear_to(Some(clear_to)); - Cow::Owned(page.pack()) - } else { - Cow::Borrowed(content) - }; - - self.pages.push(elem, styles); - self.keep_next = false; - return true; - } - - false - } -} - -impl Default for DocBuilder<'_> { - fn default() -> Self { - Self { - pages: StyleVecBuilder::new(), - keep_next: true, - clear_next: None, - } - } -} - -/// Accepts flow content. -#[derive(Default)] -struct FlowBuilder<'a>(BehavedBuilder<'a>, bool); - -impl<'a> FlowBuilder<'a> { - fn accept(&mut self, content: &'a Content, styles: StyleChain<'a>) -> bool { - if content.is::<ParbreakElem>() { - self.1 = true; - return true; - } - - let last_was_parbreak = self.1; - self.1 = false; - - if content.is::<VElem>() - || content.is::<ColbreakElem>() - || content.is::<MetaElem>() - || content.is::<PlaceElem>() - { - self.0.push(Cow::Borrowed(content), styles); - return true; - } - - if content.can::<dyn Layout>() || content.is::<ParElem>() { - let is_tight_list = if let Some(elem) = content.to::<ListElem>() { - elem.tight(styles) - } else if let Some(elem) = content.to::<EnumElem>() { - elem.tight(styles) - } else if let Some(elem) = content.to::<TermsElem>() { - elem.tight(styles) - } else { - false - }; - - if !last_was_parbreak && is_tight_list { - let leading = ParElem::leading_in(styles); - let spacing = VElem::list_attach(leading.into()); - self.0.push(Cow::Owned(spacing.pack()), styles); - } - - let (above, below) = if let Some(block) = content.to::<BlockElem>() { - (block.above(styles), block.below(styles)) - } else { - (BlockElem::above_in(styles), BlockElem::below_in(styles)) - }; - - self.0.push(Cow::Owned(above.pack()), styles); - self.0.push(Cow::Borrowed(content), styles); - self.0.push(Cow::Owned(below.pack()), styles); - return true; - } - - false - } -} - -/// Accepts paragraph content. -#[derive(Default)] -struct ParBuilder<'a>(BehavedBuilder<'a>); - -impl<'a> ParBuilder<'a> { - fn accept(&mut self, content: &'a Content, styles: StyleChain<'a>) -> bool { - if content.is::<MetaElem>() { - if self.0.has_strong_elements(false) { - self.0.push(Cow::Borrowed(content), styles); - return true; - } - } else if content.is::<SpaceElem>() - || content.is::<TextElem>() - || content.is::<HElem>() - || content.is::<LinebreakElem>() - || content.is::<SmartquoteElem>() - || content.to::<EquationElem>().map_or(false, |elem| !elem.block(styles)) - || content.is::<BoxElem>() - { - self.0.push(Cow::Borrowed(content), styles); - return true; - } - - false - } - - fn finish(self) -> (Content, StyleChain<'a>) { - let (children, shared) = self.0.finish(); - - // Find the first span that isn't detached. - let span = children - .iter() - .map(|(cnt, _)| cnt.span()) - .find(|span| !span.is_detached()) - .unwrap_or_else(Span::detached); - - (ParElem::new(children.to_vec()).spanned(span).pack(), shared) - } -} - -/// Accepts list / enum items, spaces, paragraph breaks. -struct ListBuilder<'a> { - /// The list items collected so far. - items: StyleVecBuilder<'a, Cow<'a, Content>>, - /// Whether the list contains no paragraph breaks. - tight: bool, - /// Trailing content for which it is unclear whether it is part of the list. - staged: Vec<(&'a Content, StyleChain<'a>)>, -} - -impl<'a> ListBuilder<'a> { - fn accept(&mut self, content: &'a Content, styles: StyleChain<'a>) -> bool { - if !self.items.is_empty() - && (content.is::<SpaceElem>() || content.is::<ParbreakElem>()) - { - self.staged.push((content, styles)); - return true; - } - - if (content.is::<ListItem>() - || content.is::<EnumItem>() - || content.is::<TermItem>()) - && self - .items - .elems() - .next() - .map_or(true, |first| first.func() == content.func()) - { - self.items.push(Cow::Borrowed(content), styles); - self.tight &= self.staged.drain(..).all(|(t, _)| !t.is::<ParbreakElem>()); - return true; - } - - false - } - - fn finish(self) -> (Content, StyleChain<'a>) { - let (items, shared) = self.items.finish(); - let item = items.items().next().unwrap(); - let output = if item.is::<ListItem>() { - ListElem::new( - items - .iter() - .map(|(item, local)| { - let item = item.to::<ListItem>().unwrap(); - item.clone() - .with_body(item.body().clone().styled_with_map(local.clone())) - }) - .collect::<Vec<_>>(), - ) - .with_tight(self.tight) - .pack() - } else if item.is::<EnumItem>() { - EnumElem::new( - items - .iter() - .map(|(item, local)| { - let item = item.to::<EnumItem>().unwrap(); - item.clone() - .with_body(item.body().clone().styled_with_map(local.clone())) - }) - .collect::<Vec<_>>(), - ) - .with_tight(self.tight) - .pack() - } else if item.is::<TermItem>() { - TermsElem::new( - items - .iter() - .map(|(item, local)| { - let item = item.to::<TermItem>().unwrap(); - item.clone() - .with_term(item.term().clone().styled_with_map(local.clone())) - .with_description( - item.description().clone().styled_with_map(local.clone()), - ) - }) - .collect::<Vec<_>>(), - ) - .with_tight(self.tight) - .pack() - } else { - unreachable!() - }; - (output, shared) - } -} - -impl Default for ListBuilder<'_> { - fn default() -> Self { - Self { - items: StyleVecBuilder::default(), - tight: true, - staged: vec![], - } - } -} - -/// Accepts citations. -#[derive(Default)] -struct CiteGroupBuilder<'a> { - /// The styles. - styles: StyleChain<'a>, - /// The citations. - items: Vec<CiteElem>, - /// Trailing content for which it is unclear whether it is part of the list. - staged: Vec<(&'a Content, StyleChain<'a>)>, -} - -impl<'a> CiteGroupBuilder<'a> { - fn accept(&mut self, content: &'a Content, styles: StyleChain<'a>) -> bool { - if !self.items.is_empty() - && (content.is::<SpaceElem>() || content.is::<MetaElem>()) - { - self.staged.push((content, styles)); - return true; - } - - if let Some(citation) = content.to::<CiteElem>() { - if self.items.is_empty() { - self.styles = styles; - } - self.staged.retain(|(elem, _)| !elem.is::<SpaceElem>()); - self.items.push(citation.clone()); - return true; - } - - false - } - - fn finish(self) -> (Content, StyleChain<'a>) { - let span = self.items.first().map(|cite| cite.span()).unwrap_or(Span::detached()); - (CiteGroup::new(self.items).pack().spanned(span), self.styles) - } -} diff --git a/crates/typst-library/src/layout/pad.rs b/crates/typst-library/src/layout/pad.rs deleted file mode 100644 index d1b0cb1f..00000000 --- a/crates/typst-library/src/layout/pad.rs +++ /dev/null @@ -1,121 +0,0 @@ -use crate::prelude::*; - -/// Adds spacing around content. -/// -/// The spacing can be specified for each side individually, or for all sides at -/// once by specifying a positional argument. -/// -/// # Example -/// ```example -/// #set align(center) -/// -/// #pad(x: 16pt, image("typing.jpg")) -/// _Typing speeds can be -/// measured in words per minute._ -/// ``` -#[elem(title = "Padding", Layout)] -pub struct PadElem { - /// The padding at the left side. - #[parse( - let all = args.named("rest")?.or(args.find()?); - let x = args.named("x")?.or(all); - let y = args.named("y")?.or(all); - args.named("left")?.or(x) - )] - pub left: Rel<Length>, - - /// The padding at the top side. - #[parse(args.named("top")?.or(y))] - pub top: Rel<Length>, - - /// The padding at the right side. - #[parse(args.named("right")?.or(x))] - pub right: Rel<Length>, - - /// The padding at the bottom side. - #[parse(args.named("bottom")?.or(y))] - pub bottom: Rel<Length>, - - /// The horizontal padding. Both `left` and `right` take precedence over - /// this. - #[external] - pub x: Rel<Length>, - - /// The vertical padding. Both `top` and `bottom` take precedence over this. - #[external] - pub y: Rel<Length>, - - /// The padding for all sides. All other parameters take precedence over - /// this. - #[external] - pub rest: Rel<Length>, - - /// The content to pad at the sides. - #[required] - pub body: Content, -} - -impl Layout for PadElem { - #[tracing::instrument(name = "PadElem::layout", skip_all)] - fn layout( - &self, - vt: &mut Vt, - styles: StyleChain, - regions: Regions, - ) -> SourceResult<Fragment> { - let sides = Sides::new( - self.left(styles), - self.top(styles), - self.right(styles), - self.bottom(styles), - ); - - // Layout child into padded regions. - let mut backlog = vec![]; - let padding = sides.resolve(styles); - let pod = regions.map(&mut backlog, |size| shrink(size, padding)); - let mut fragment = self.body().layout(vt, styles, pod)?; - - for frame in &mut fragment { - // Apply the padding inversely such that the grown size padded - // yields the frame's size. - let padded = grow(frame.size(), padding); - let padding = padding.relative_to(padded); - let offset = Point::new(padding.left, padding.top); - - // Grow the frame and translate everything in the frame inwards. - frame.set_size(padded); - frame.translate(offset); - } - - Ok(fragment) - } -} - -/// Shrink a size by padding relative to the size itself. -fn shrink(size: Size, padding: Sides<Rel<Abs>>) -> Size { - size - padding.relative_to(size).sum_by_axis() -} - -/// Grow a size by padding relative to the grown size. -/// This is the inverse operation to `shrink()`. -/// -/// For the horizontal axis the derivation looks as follows. -/// (Vertical axis is analogous.) -/// -/// Let w be the grown target width, -/// s be the given width, -/// l be the left padding, -/// r be the right padding, -/// p = l + r. -/// -/// We want that: w - l.resolve(w) - r.resolve(w) = s -/// -/// Thus: w - l.resolve(w) - r.resolve(w) = s -/// <=> w - p.resolve(w) = s -/// <=> w - p.rel * w - p.abs = s -/// <=> (1 - p.rel) * w = s + p.abs -/// <=> w = (s + p.abs) / (1 - p.rel) -fn grow(size: Size, padding: Sides<Rel<Abs>>) -> Size { - size.zip_map(padding.sum_by_axis(), |s, p| (s + p.abs).safe_div(1.0 - p.rel.get())) -} diff --git a/crates/typst-library/src/layout/page.rs b/crates/typst-library/src/layout/page.rs deleted file mode 100644 index 53646c7d..00000000 --- a/crates/typst-library/src/layout/page.rs +++ /dev/null @@ -1,949 +0,0 @@ -use std::borrow::Cow; -use std::ptr; -use std::str::FromStr; - -use typst::eval::AutoValue; - -use crate::layout::{AlignElem, ColumnsElem}; -use crate::meta::{Counter, CounterKey, ManualPageCounter, Numbering}; -use crate::prelude::*; -use crate::text::TextElem; - -/// Layouts its child onto one or multiple pages. -/// -/// Although this function is primarily used in set rules to affect page -/// properties, it can also be used to explicitly render its argument onto -/// a set of pages of its own. -/// -/// Pages can be set to use `{auto}` as their width or height. In this case, the -/// pages will grow to fit their content on the respective axis. -/// -/// The [Guide for Page Setup]($guides/page-setup-guide) explains how to use -/// this and related functions to set up a document with many examples. -/// -/// # Example -/// ```example -/// >>> #set page(margin: auto) -/// #set page("us-letter") -/// -/// There you go, US friends! -/// ``` -#[elem] -pub struct PageElem { - /// A standard paper size to set width and height. - #[external] - #[default(Paper::A4)] - pub paper: Paper, - - /// The width of the page. - /// - /// ```example - /// #set page( - /// width: 3cm, - /// margin: (x: 0cm), - /// ) - /// - /// #for i in range(3) { - /// box(square(width: 1cm)) - /// } - /// ``` - #[resolve] - #[parse( - let paper = args.named_or_find::<Paper>("paper")?; - args.named("width")? - .or_else(|| paper.map(|paper| Smart::Custom(paper.width().into()))) - )] - #[default(Smart::Custom(Paper::A4.width().into()))] - pub width: Smart<Length>, - - /// The height of the page. - /// - /// If this is set to `{auto}`, page breaks can only be triggered manually - /// by inserting a [page break]($pagebreak). Most examples throughout this - /// documentation use `{auto}` for the height of the page to dynamically - /// grow and shrink to fit their content. - #[resolve] - #[parse( - args.named("height")? - .or_else(|| paper.map(|paper| Smart::Custom(paper.height().into()))) - )] - #[default(Smart::Custom(Paper::A4.height().into()))] - pub height: Smart<Length>, - - /// Whether the page is flipped into landscape orientation. - /// - /// ```example - /// #set page( - /// "us-business-card", - /// flipped: true, - /// fill: rgb("f2e5dd"), - /// ) - /// - /// #set align(bottom + end) - /// #text(14pt)[*Sam H. Richards*] \ - /// _Procurement Manager_ - /// - /// #set text(10pt) - /// 17 Main Street \ - /// New York, NY 10001 \ - /// +1 555 555 5555 - /// ``` - #[default(false)] - pub flipped: bool, - - /// The page's margins. - /// - /// - `{auto}`: The margins are set automatically to 2.5/21 times the smaller - /// dimension of the page. This results in 2.5cm margins for an A4 page. - /// - A single length: The same margin on all sides. - /// - A dictionary: With a dictionary, the margins can be set individually. - /// The dictionary can contain the following keys in order of precedence: - /// - `top`: The top margin. - /// - `right`: The right margin. - /// - `bottom`: The bottom margin. - /// - `left`: The left margin. - /// - `inside`: The margin at the inner side of the page (where the - /// [binding]($page.binding) is). - /// - `outside`: The margin at the outer side of the page (opposite to the - /// [binding]($page.binding)). - /// - `x`: The horizontal margins. - /// - `y`: The vertical margins. - /// - `rest`: The margins on all sides except those for which the - /// dictionary explicitly sets a size. - /// - /// The values for `left` and `right` are mutually exclusive with - /// the values for `inside` and `outside`. - /// - /// ```example - /// #set page( - /// width: 3cm, - /// height: 4cm, - /// margin: (x: 8pt, y: 4pt), - /// ) - /// - /// #rect( - /// width: 100%, - /// height: 100%, - /// fill: aqua, - /// ) - /// ``` - #[fold] - pub margin: Margin, - - /// On which side the pages will be bound. - /// - /// - `{auto}`: Equivalent to `left` if the [text direction]($text.dir) - /// is left-to-right and `right` if it is right-to-left. - /// - `left`: Bound on the left side. - /// - `right`: Bound on the right side. - /// - /// This affects the meaning of the `inside` and `outside` options for - /// margins. - pub binding: Smart<Binding>, - - /// How many columns the page has. - /// - /// If you need to insert columns into a page or other container, you can - /// also use the [`columns` function]($columns). - /// - /// ```example:single - /// #set page(columns: 2, height: 4.8cm) - /// Climate change is one of the most - /// pressing issues of our time, with - /// the potential to devastate - /// communities, ecosystems, and - /// economies around the world. It's - /// clear that we need to take urgent - /// action to reduce our carbon - /// emissions and mitigate the impacts - /// of a rapidly changing climate. - /// ``` - #[default(NonZeroUsize::ONE)] - pub columns: NonZeroUsize, - - /// The page's background color. - /// - /// This instructs the printer to color the complete page with the given - /// color. If you are considering larger production runs, it may be more - /// environmentally friendly and cost-effective to source pre-dyed pages and - /// not set this property. - /// - /// ```example - /// #set page(fill: rgb("444352")) - /// #set text(fill: rgb("fdfdfd")) - /// *Dark mode enabled.* - /// ``` - #[borrowed] - pub fill: Option<Paint>, - - /// How to [number]($numbering) the pages. - /// - /// If an explicit `footer` (or `header` for top-aligned numbering) is - /// given, the numbering is ignored. - /// - /// ```example - /// #set page( - /// height: 100pt, - /// margin: (top: 16pt, bottom: 24pt), - /// numbering: "1 / 1", - /// ) - /// - /// #lorem(48) - /// ``` - #[borrowed] - pub numbering: Option<Numbering>, - - /// The alignment of the page numbering. - /// - /// If the vertical component is `top`, the numbering is placed into the - /// header and if it is `bottom`, it is placed in the footer. Horizon - /// alignment is forbidden. If an explicit matching `header` or `footer` is - /// given, the numbering is ignored. - /// - /// ```example - /// #set page( - /// margin: (top: 16pt, bottom: 24pt), - /// numbering: "1", - /// number-align: right, - /// ) - /// - /// #lorem(30) - /// ``` - #[default(HAlign::Center + VAlign::Bottom)] - #[parse({ - let option: Option<Spanned<Align>> = args.named("number-align")?; - if let Some(Spanned { v: align, span }) = option { - if align.y() == Some(VAlign::Horizon) { - bail!(span, "page number cannot be `horizon`-aligned"); - } - } - option.map(|spanned| spanned.v) - })] - pub number_align: Align, - - /// The page's header. Fills the top margin of each page. - /// - /// ```example - /// #set par(justify: true) - /// #set page( - /// margin: (top: 32pt, bottom: 20pt), - /// header: [ - /// #set text(8pt) - /// #smallcaps[Typst Academcy] - /// #h(1fr) _Exercise Sheet 3_ - /// ], - /// ) - /// - /// #lorem(19) - /// ``` - #[borrowed] - pub header: Option<Content>, - - /// The amount the header is raised into the top margin. - #[resolve] - #[default(Ratio::new(0.3).into())] - pub header_ascent: Rel<Length>, - - /// The page's footer. Fills the bottom margin of each page. - /// - /// For just a page number, the `numbering` property, typically suffices. If - /// you want to create a custom footer, but still display the page number, - /// you can directly access the [page counter]($counter). - /// - /// ```example - /// #set par(justify: true) - /// #set page( - /// height: 100pt, - /// margin: 20pt, - /// footer: [ - /// #set align(right) - /// #set text(8pt) - /// #counter(page).display( - /// "1 of I", - /// both: true, - /// ) - /// ] - /// ) - /// - /// #lorem(48) - /// ``` - #[borrowed] - pub footer: Option<Content>, - - /// The amount the footer is lowered into the bottom margin. - #[resolve] - #[default(Ratio::new(0.3).into())] - pub footer_descent: Rel<Length>, - - /// Content in the page's background. - /// - /// This content will be placed behind the page's body. It can be - /// used to place a background image or a watermark. - /// - /// ```example - /// #set page(background: rotate(24deg, - /// text(18pt, fill: rgb("FFCBC4"))[ - /// *CONFIDENTIAL* - /// ] - /// )) - /// - /// = Typst's secret plans - /// In the year 2023, we plan to take - /// over the world (of typesetting). - /// ``` - #[borrowed] - pub background: Option<Content>, - - /// Content in the page's foreground. - /// - /// This content will overlay the page's body. - /// - /// ```example - /// #set page(foreground: text(24pt)[🥸]) - /// - /// Reviewer 2 has marked our paper - /// "Weak Reject" because they did - /// not understand our approach... - /// ``` - #[borrowed] - pub foreground: Option<Content>, - - /// The contents of the page(s). - /// - /// Multiple pages will be created if the content does not fit on a single - /// page. A new page with the page properties prior to the function invocation - /// will be created after the body has been typeset. - #[required] - pub body: Content, - - /// Whether the page should be aligned to an even or odd page. - #[internal] - pub clear_to: Option<Parity>, -} - -impl PageElem { - /// A document can consist of multiple `PageElem`s, one per run of pages - /// with equal properties (not one per actual output page!). The `number` is - /// the physical page number of the first page of this run. It is mutated - /// while we post-process the pages in this function. This function returns - /// a fragment consisting of multiple frames, one per output page of this - /// page run. - #[tracing::instrument(skip_all)] - pub fn layout( - &self, - vt: &mut Vt, - styles: StyleChain, - page_counter: &mut ManualPageCounter, - extend_to: Option<Parity>, - ) -> SourceResult<Fragment> { - tracing::info!("Page layout"); - - // When one of the lengths is infinite the page fits its content along - // that axis. - let width = self.width(styles).unwrap_or(Abs::inf()); - let height = self.height(styles).unwrap_or(Abs::inf()); - let mut size = Size::new(width, height); - if self.flipped(styles) { - std::mem::swap(&mut size.x, &mut size.y); - } - - let mut min = width.min(height); - if !min.is_finite() { - min = Paper::A4.width(); - } - - // Determine the margins. - let default = Rel::<Length>::from((2.5 / 21.0) * min); - let margin = self.margin(styles); - let two_sided = margin.two_sided.unwrap_or(false); - let margin = margin - .sides - .map(|side| side.and_then(Smart::as_custom).unwrap_or(default)) - .resolve(styles) - .relative_to(size); - - // Determine the binding. - let binding = - self.binding(styles) - .unwrap_or_else(|| match TextElem::dir_in(styles) { - Dir::LTR => Binding::Left, - _ => Binding::Right, - }); - - // Realize columns. - let mut child = self.body().clone(); - let columns = self.columns(styles); - if columns.get() > 1 { - child = ColumnsElem::new(child).with_count(columns).pack(); - } - - let area = size - margin.sum_by_axis(); - let mut regions = Regions::repeat(area, area.map(Abs::is_finite)); - regions.root = true; - - // Layout the child. - let mut frames = child.layout(vt, styles, regions)?.into_frames(); - - // Align the child to the pagebreak's parity. - // Check for page count after adding the pending frames - if extend_to - .is_some_and(|p| !p.matches(page_counter.physical().get() + frames.len())) - { - // Insert empty page after the current pages. - let size = area.map(Abs::is_finite).select(area, Size::zero()); - frames.push(Frame::hard(size)); - } - - let fill = self.fill(styles); - let foreground = Cow::Borrowed(self.foreground(styles)); - let background = Cow::Borrowed(self.background(styles)); - let header_ascent = self.header_ascent(styles); - let footer_descent = self.footer_descent(styles); - let numbering = self.numbering(styles); - let numbering_meta = Meta::PageNumbering(numbering.clone().into_value()); - let number_align = self.number_align(styles); - let mut header = Cow::Borrowed(self.header(styles)); - let mut footer = Cow::Borrowed(self.footer(styles)); - - // Construct the numbering (for header or footer). - let numbering_marginal = Cow::Owned(numbering.as_ref().map(|numbering| { - let both = match numbering { - Numbering::Pattern(pattern) => pattern.pieces() >= 2, - Numbering::Func(_) => true, - }; - - let mut counter = - Counter::new(CounterKey::Page).display(Some(numbering.clone()), both); - - // We interpret the Y alignment as selecting header or footer - // and then ignore it for aligning the actual number. - if let Some(x) = number_align.x() { - counter = counter.aligned(x.into()); - } - - counter - })); - - if matches!(number_align.y(), Some(VAlign::Top)) { - header = if header.is_some() { header } else { numbering_marginal }; - } else { - footer = if footer.is_some() { footer } else { numbering_marginal }; - } - - // Post-process pages. - for frame in frames.iter_mut() { - tracing::info!("Layouting page #{}", page_counter.physical()); - - // The padded width of the page's content without margins. - let pw = frame.width(); - - // If two sided, left becomes inside and right becomes outside. - // Thus, for left-bound pages, we want to swap on even pages and - // for right-bound pages, we want to swap on odd pages. - let mut margin = margin; - if two_sided && binding.swap(page_counter.physical()) { - std::mem::swap(&mut margin.left, &mut margin.right); - } - - // Realize margins. - frame.set_size(frame.size() + margin.sum_by_axis()); - frame.translate(Point::new(margin.left, margin.top)); - frame.push_positionless_meta(numbering_meta.clone()); - - // The page size with margins. - let size = frame.size(); - - // Realize overlays. - for (name, marginal) in [ - ("header", &header), - ("footer", &footer), - ("background", &background), - ("foreground", &foreground), - ] { - tracing::info!("Layouting {name}"); - - let Some(content) = &**marginal else { continue }; - - let (pos, area, align); - if ptr::eq(marginal, &header) { - let ascent = header_ascent.relative_to(margin.top); - pos = Point::with_x(margin.left); - area = Size::new(pw, margin.top - ascent); - align = Align::BOTTOM; - } else if ptr::eq(marginal, &footer) { - let descent = footer_descent.relative_to(margin.bottom); - pos = Point::new(margin.left, size.y - margin.bottom + descent); - area = Size::new(pw, margin.bottom - descent); - align = Align::TOP; - } else { - pos = Point::zero(); - area = size; - align = HAlign::Center + VAlign::Horizon; - }; - - let pod = Regions::one(area, Axes::splat(true)); - let sub = content - .clone() - .styled(AlignElem::set_alignment(align)) - .layout(vt, styles, pod)? - .into_frame(); - - if ptr::eq(marginal, &header) || ptr::eq(marginal, &background) { - frame.prepend_frame(pos, sub); - } else { - frame.push_frame(pos, sub); - } - } - - if let Some(fill) = fill { - frame.fill(fill.clone()); - } - - page_counter.visit(vt, frame)?; - - // Add a PDF page label if there is a numbering. - if let Some(num) = numbering { - if let Some(page_label) = num.apply_pdf(page_counter.logical()) { - frame.push_positionless_meta(Meta::PdfPageLabel(page_label)); - } - } - - page_counter.step(); - } - - Ok(Fragment::frames(frames)) - } -} - -/// Specification of the page's margins. -#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash)] -pub struct Margin { - /// The margins for each side. - pub sides: Sides<Option<Smart<Rel<Length>>>>, - /// Whether to swap `left` and `right` to make them `inside` and `outside` - /// (when to swap depends on the binding). - pub two_sided: Option<bool>, -} - -impl Margin { - /// Create an instance with four equal components. - pub fn splat(value: Option<Smart<Rel<Length>>>) -> Self { - Self { sides: Sides::splat(value), two_sided: None } - } -} - -impl Fold for Margin { - type Output = Margin; - - fn fold(self, outer: Self::Output) -> Self::Output { - let sides = - self.sides - .zip(outer.sides) - .map(|(inner, outer)| match (inner, outer) { - (Some(value), Some(outer)) => Some(value.fold(outer)), - _ => inner.or(outer), - }); - let two_sided = self.two_sided.or(outer.two_sided); - Margin { sides, two_sided } - } -} - -cast! { - Margin, - self => { - let mut dict = Dict::new(); - let mut handle = |key: &str, component: Value| { - let value = component.into_value(); - if value != Value::None { - dict.insert(key.into(), value); - } - }; - - handle("top", self.sides.top.into_value()); - handle("bottom", self.sides.bottom.into_value()); - if self.two_sided.unwrap_or(false) { - handle("inside", self.sides.left.into_value()); - handle("outside", self.sides.right.into_value()); - } else { - handle("left", self.sides.left.into_value()); - handle("right", self.sides.right.into_value()); - } - - Value::Dict(dict) - }, - _: AutoValue => Self::splat(Some(Smart::Auto)), - v: Rel<Length> => Self::splat(Some(Smart::Custom(v))), - mut dict: Dict => { - let mut take = |key| dict.take(key).ok().map(Value::cast).transpose(); - - let rest = take("rest")?; - let x = take("x")?.or(rest); - let y = take("y")?.or(rest); - let top = take("top")?.or(y); - let bottom = take("bottom")?.or(y); - let outside = take("outside")?; - let inside = take("inside")?; - let left = take("left")?; - let right = take("right")?; - - let implicitly_two_sided = outside.is_some() || inside.is_some(); - let implicitly_not_two_sided = left.is_some() || right.is_some(); - if implicitly_two_sided && implicitly_not_two_sided { - bail!("`inside` and `outside` are mutually exclusive with `left` and `right`"); - } - - // - If 'implicitly_two_sided' is false here, then - // 'implicitly_not_two_sided' will be guaranteed to be true - // due to the previous two 'if' conditions. - // - If both are false, this means that this margin change does not - // affect lateral margins, and thus shouldn't make a difference on - // the 'two_sided' attribute of this margin. - let two_sided = (implicitly_two_sided || implicitly_not_two_sided) - .then_some(implicitly_two_sided); - - dict.finish(&[ - "left", "top", "right", "bottom", "outside", "inside", "x", "y", "rest", - ])?; - - Margin { - sides: Sides { - left: inside.or(left).or(x), - top, - right: outside.or(right).or(x), - bottom, - }, - two_sided, - } - } -} - -/// Specification of the page's binding. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub enum Binding { - /// Bound on the left, as customary in LTR languages. - Left, - /// Bound on the right, as customary in RTL languages. - Right, -} - -impl Binding { - /// Whether to swap left and right margin for the page with this number. - fn swap(self, number: NonZeroUsize) -> bool { - match self { - // Left-bound must swap on even pages - // (because it is correct on the first page). - Self::Left => number.get() % 2 == 0, - // Right-bound must swap on odd pages - // (because it is wrong on the first page). - Self::Right => number.get() % 2 == 1, - } - } -} - -cast! { - Binding, - self => match self { - Self::Left => Align::LEFT.into_value(), - Self::Right => Align::RIGHT.into_value(), - }, - v: Align => match v { - Align::LEFT => Self::Left, - Align::RIGHT => Self::Right, - _ => bail!("must be `left` or `right`"), - }, -} - -/// A header, footer, foreground or background definition. -#[derive(Debug, Clone, Hash)] -pub enum Marginal { - /// Bare content. - Content(Content), - /// A closure mapping from a page number to content. - Func(Func), -} - -impl Marginal { - /// Resolve the marginal based on the page number. - pub fn resolve(&self, vt: &mut Vt, page: usize) -> SourceResult<Cow<'_, Content>> { - Ok(match self { - Self::Content(content) => Cow::Borrowed(content), - Self::Func(func) => Cow::Owned(func.call_vt(vt, [page])?.display()), - }) - } -} - -cast! { - Marginal, - self => match self { - Self::Content(v) => v.into_value(), - Self::Func(v) => v.into_value(), - }, - v: Content => Self::Content(v), - v: Func => Self::Func(v), -} - -/// A manual page break. -/// -/// Must not be used inside any containers. -/// -/// # Example -/// ```example -/// The next page contains -/// more details on compound theory. -/// #pagebreak() -/// -/// == Compound Theory -/// In 1984, the first ... -/// ``` -#[elem(title = "Page Break")] -pub struct PagebreakElem { - /// If `{true}`, the page break is skipped if the current page is already - /// empty. - #[default(false)] - pub weak: bool, - - /// If given, ensures that the next page will be an even/odd page, with an - /// empty page in between if necessary. - /// - /// ```example - /// #set page(height: 30pt) - /// - /// First. - /// #pagebreak(to: "odd") - /// Third. - /// ``` - pub to: Option<Parity>, -} - -/// Whether something should be even or odd. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)] -pub enum Parity { - /// Next page will be an even page. - Even, - /// Next page will be an odd page. - Odd, -} - -impl Parity { - /// Whether the given number matches the parity. - fn matches(self, number: usize) -> bool { - match self { - Self::Even => number % 2 == 0, - Self::Odd => number % 2 == 1, - } - } -} - -/// Specification of a paper. -#[derive(Debug, Copy, Clone, Hash)] -pub struct Paper { - /// The name of the paper. - name: &'static str, - /// The width of the paper in millimeters. - width: Scalar, - /// The height of the paper in millimeters. - height: Scalar, -} - -impl Paper { - /// The width of the paper. - pub fn width(self) -> Abs { - Abs::mm(self.width.get()) - } - - /// The height of the paper. - pub fn height(self) -> Abs { - Abs::mm(self.height.get()) - } -} - -/// Defines paper constants and a paper parsing implementation. -macro_rules! papers { - ($(($var:ident: $width:expr, $height: expr, $name:literal))*) => { - /// Predefined papers. - /// - /// Each paper is parsable from its name in kebab-case. - impl Paper { - $(pub const $var: Self = Self { - name: $name, - width: Scalar::new($width), - height: Scalar::new($height), - };)* - } - - impl FromStr for Paper { - type Err = &'static str; - - fn from_str(name: &str) -> Result<Self, Self::Err> { - match name.to_lowercase().as_str() { - $($name => Ok(Self::$var),)* - _ => Err("unknown paper size"), - } - } - } - - cast! { - Paper, - self => self.name.into_value(), - $( - /// Produces a paper of the respective size. - $name => Self::$var, - )* - } - }; -} - -// All paper sizes in mm. -// -// Resources: -// - https://papersizes.io/ -// - https://en.wikipedia.org/wiki/Paper_size -// - https://www.theedkins.co.uk/jo/units/oldunits/print.htm -// - https://vintagepaper.co/blogs/news/traditional-paper-sizes -papers! { - // ---------------------------------------------------------------------- // - // ISO 216 A Series - (A0: 841.0, 1189.0, "a0") - (A1: 594.0, 841.0, "a1") - (A2: 420.0, 594.0, "a2") - (A3: 297.0, 420.0, "a3") - (A4: 210.0, 297.0, "a4") - (A5: 148.0, 210.0, "a5") - (A6: 105.0, 148.0, "a6") - (A7: 74.0, 105.0, "a7") - (A8: 52.0, 74.0, "a8") - (A9: 37.0, 52.0, "a9") - (A10: 26.0, 37.0, "a10") - (A11: 18.0, 26.0, "a11") - - // ISO 216 B Series - (ISO_B1: 707.0, 1000.0, "iso-b1") - (ISO_B2: 500.0, 707.0, "iso-b2") - (ISO_B3: 353.0, 500.0, "iso-b3") - (ISO_B4: 250.0, 353.0, "iso-b4") - (ISO_B5: 176.0, 250.0, "iso-b5") - (ISO_B6: 125.0, 176.0, "iso-b6") - (ISO_B7: 88.0, 125.0, "iso-b7") - (ISO_B8: 62.0, 88.0, "iso-b8") - - // ISO 216 C Series - (ISO_C3: 324.0, 458.0, "iso-c3") - (ISO_C4: 229.0, 324.0, "iso-c4") - (ISO_C5: 162.0, 229.0, "iso-c5") - (ISO_C6: 114.0, 162.0, "iso-c6") - (ISO_C7: 81.0, 114.0, "iso-c7") - (ISO_C8: 57.0, 81.0, "iso-c8") - - // DIN D Series (extension to ISO) - (DIN_D3: 272.0, 385.0, "din-d3") - (DIN_D4: 192.0, 272.0, "din-d4") - (DIN_D5: 136.0, 192.0, "din-d5") - (DIN_D6: 96.0, 136.0, "din-d6") - (DIN_D7: 68.0, 96.0, "din-d7") - (DIN_D8: 48.0, 68.0, "din-d8") - - // SIS (used in academia) - (SIS_G5: 169.0, 239.0, "sis-g5") - (SIS_E5: 115.0, 220.0, "sis-e5") - - // ANSI Extensions - (ANSI_A: 216.0, 279.0, "ansi-a") - (ANSI_B: 279.0, 432.0, "ansi-b") - (ANSI_C: 432.0, 559.0, "ansi-c") - (ANSI_D: 559.0, 864.0, "ansi-d") - (ANSI_E: 864.0, 1118.0, "ansi-e") - - // ANSI Architectural Paper - (ARCH_A: 229.0, 305.0, "arch-a") - (ARCH_B: 305.0, 457.0, "arch-b") - (ARCH_C: 457.0, 610.0, "arch-c") - (ARCH_D: 610.0, 914.0, "arch-d") - (ARCH_E1: 762.0, 1067.0, "arch-e1") - (ARCH_E: 914.0, 1219.0, "arch-e") - - // JIS B Series - (JIS_B0: 1030.0, 1456.0, "jis-b0") - (JIS_B1: 728.0, 1030.0, "jis-b1") - (JIS_B2: 515.0, 728.0, "jis-b2") - (JIS_B3: 364.0, 515.0, "jis-b3") - (JIS_B4: 257.0, 364.0, "jis-b4") - (JIS_B5: 182.0, 257.0, "jis-b5") - (JIS_B6: 128.0, 182.0, "jis-b6") - (JIS_B7: 91.0, 128.0, "jis-b7") - (JIS_B8: 64.0, 91.0, "jis-b8") - (JIS_B9: 45.0, 64.0, "jis-b9") - (JIS_B10: 32.0, 45.0, "jis-b10") - (JIS_B11: 22.0, 32.0, "jis-b11") - - // SAC D Series - (SAC_D0: 764.0, 1064.0, "sac-d0") - (SAC_D1: 532.0, 760.0, "sac-d1") - (SAC_D2: 380.0, 528.0, "sac-d2") - (SAC_D3: 264.0, 376.0, "sac-d3") - (SAC_D4: 188.0, 260.0, "sac-d4") - (SAC_D5: 130.0, 184.0, "sac-d5") - (SAC_D6: 92.0, 126.0, "sac-d6") - - // ISO 7810 ID - (ISO_ID_1: 85.6, 53.98, "iso-id-1") - (ISO_ID_2: 74.0, 105.0, "iso-id-2") - (ISO_ID_3: 88.0, 125.0, "iso-id-3") - - // ---------------------------------------------------------------------- // - // Asia - (ASIA_F4: 210.0, 330.0, "asia-f4") - - // Japan - (JP_SHIROKU_BAN_4: 264.0, 379.0, "jp-shiroku-ban-4") - (JP_SHIROKU_BAN_5: 189.0, 262.0, "jp-shiroku-ban-5") - (JP_SHIROKU_BAN_6: 127.0, 188.0, "jp-shiroku-ban-6") - (JP_KIKU_4: 227.0, 306.0, "jp-kiku-4") - (JP_KIKU_5: 151.0, 227.0, "jp-kiku-5") - (JP_BUSINESS_CARD: 91.0, 55.0, "jp-business-card") - - // China - (CN_BUSINESS_CARD: 90.0, 54.0, "cn-business-card") - - // Europe - (EU_BUSINESS_CARD: 85.0, 55.0, "eu-business-card") - - // French Traditional (AFNOR) - (FR_TELLIERE: 340.0, 440.0, "fr-tellière") - (FR_COURONNE_ECRITURE: 360.0, 460.0, "fr-couronne-écriture") - (FR_COURONNE_EDITION: 370.0, 470.0, "fr-couronne-édition") - (FR_RAISIN: 500.0, 650.0, "fr-raisin") - (FR_CARRE: 450.0, 560.0, "fr-carré") - (FR_JESUS: 560.0, 760.0, "fr-jésus") - - // United Kingdom Imperial - (UK_BRIEF: 406.4, 342.9, "uk-brief") - (UK_DRAFT: 254.0, 406.4, "uk-draft") - (UK_FOOLSCAP: 203.2, 330.2, "uk-foolscap") - (UK_QUARTO: 203.2, 254.0, "uk-quarto") - (UK_CROWN: 508.0, 381.0, "uk-crown") - (UK_BOOK_A: 111.0, 178.0, "uk-book-a") - (UK_BOOK_B: 129.0, 198.0, "uk-book-b") - - // Unites States - (US_LETTER: 215.9, 279.4, "us-letter") - (US_LEGAL: 215.9, 355.6, "us-legal") - (US_TABLOID: 279.4, 431.8, "us-tabloid") - (US_EXECUTIVE: 84.15, 266.7, "us-executive") - (US_FOOLSCAP_FOLIO: 215.9, 342.9, "us-foolscap-folio") - (US_STATEMENT: 139.7, 215.9, "us-statement") - (US_LEDGER: 431.8, 279.4, "us-ledger") - (US_OFICIO: 215.9, 340.36, "us-oficio") - (US_GOV_LETTER: 203.2, 266.7, "us-gov-letter") - (US_GOV_LEGAL: 215.9, 330.2, "us-gov-legal") - (US_BUSINESS_CARD: 88.9, 50.8, "us-business-card") - (US_DIGEST: 139.7, 215.9, "us-digest") - (US_TRADE: 152.4, 228.6, "us-trade") - - // ---------------------------------------------------------------------- // - // Other - (NEWSPAPER_COMPACT: 280.0, 430.0, "newspaper-compact") - (NEWSPAPER_BERLINER: 315.0, 470.0, "newspaper-berliner") - (NEWSPAPER_BROADSHEET: 381.0, 578.0, "newspaper-broadsheet") - (PRESENTATION_16_9: 297.0, 167.0625, "presentation-16-9") - (PRESENTATION_4_3: 280.0, 210.0, "presentation-4-3") -} diff --git a/crates/typst-library/src/layout/par.rs b/crates/typst-library/src/layout/par.rs deleted file mode 100644 index 2d7c9080..00000000 --- a/crates/typst-library/src/layout/par.rs +++ /dev/null @@ -1,1520 +0,0 @@ -use comemo::Prehashed; -use typst::eval::Tracer; -use typst::model::DelayedErrors; -use unicode_bidi::{BidiInfo, Level as BidiLevel}; -use unicode_script::{Script, UnicodeScript}; - -use crate::layout::{AlignElem, BoxElem, HElem, Sizing, Spacing}; -use crate::math::EquationElem; -use crate::prelude::*; -use crate::text::{ - breakpoints, char_is_cjk_script, is_gb_style, shape, Breakpoint, LinebreakElem, - Quoter, Quotes, ShapedGlyph, ShapedText, SmartquoteElem, SpaceElem, TextElem, - BEGIN_PUNCT_PAT, END_PUNCT_PAT, -}; - -/// Arranges text, spacing and inline-level elements into a paragraph. -/// -/// Although this function is primarily used in set rules to affect paragraph -/// properties, it can also be used to explicitly render its argument onto a -/// paragraph of its own. -/// -/// # Example -/// ```example -/// #show par: set block(spacing: 0.65em) -/// #set par( -/// first-line-indent: 1em, -/// justify: true, -/// ) -/// -/// We proceed by contradiction. -/// Suppose that there exists a set -/// of positive integers $a$, $b$, and -/// $c$ that satisfies the equation -/// $a^n + b^n = c^n$ for some -/// integer value of $n > 2$. -/// -/// Without loss of generality, -/// let $a$ be the smallest of the -/// three integers. Then, we ... -/// ``` -#[elem(title = "Paragraph", Construct)] -pub struct ParElem { - /// The spacing between lines. - #[resolve] - #[ghost] - #[default(Em::new(0.65).into())] - pub leading: Length, - - /// Whether to justify text in its line. - /// - /// Hyphenation will be enabled for justified paragraphs if the - /// [text function's `hyphenate` property]($text.hyphenate) is set to - /// `{auto}` and the current language is known. - /// - /// Note that the current [alignment]($align) still has an effect on the - /// placement of the last line except if it ends with a - /// [justified line break]($linebreak.justify). - #[ghost] - #[default(false)] - pub justify: bool, - - /// How to determine line breaks. - /// - /// When this property is set to `{auto}`, its default value, optimized line - /// breaks will be used for justified paragraphs. Enabling optimized line - /// breaks for ragged paragraphs may also be worthwhile to improve the - /// appearance of the text. - /// - /// ```example - /// #set page(width: 207pt) - /// #set par(linebreaks: "simple") - /// Some texts feature many longer - /// words. Those are often exceedingly - /// challenging to break in a visually - /// pleasing way. - /// - /// #set par(linebreaks: "optimized") - /// Some texts feature many longer - /// words. Those are often exceedingly - /// challenging to break in a visually - /// pleasing way. - /// ``` - #[ghost] - pub linebreaks: Smart<Linebreaks>, - - /// The indent the first line of a paragraph should have. - /// - /// Only the first line of a consecutive paragraph will be indented (not - /// the first one in a block or on the page). - /// - /// By typographic convention, paragraph breaks are indicated either by some - /// space between paragraphs or by indented first lines. Consider reducing - /// the [paragraph spacing]($block.spacing) to the [`leading`] when - /// using this property (e.g. using - /// `[#show par: set block(spacing: 0.65em)]`). - #[ghost] - pub first_line_indent: Length, - - /// The indent all but the first line of a paragraph should have. - #[ghost] - #[resolve] - pub hanging_indent: Length, - - /// The contents of the paragraph. - #[external] - #[required] - pub body: Content, - - /// The paragraph's children. - #[internal] - #[variadic] - pub children: Vec<Prehashed<Content>>, -} - -impl Construct for ParElem { - fn construct(vm: &mut Vm, args: &mut Args) -> SourceResult<Content> { - // The paragraph constructor is special: It doesn't create a paragraph - // element. Instead, it just ensures that the passed content lives in a - // separate paragraph and styles it. - let styles = Self::set(vm, args)?; - let body = args.expect::<Content>("body")?; - Ok(Content::sequence([ - ParbreakElem::new().pack(), - body.styled_with_map(styles), - ParbreakElem::new().pack(), - ])) - } -} - -impl ParElem { - /// Layout the paragraph into a collection of lines. - #[tracing::instrument(name = "ParElement::layout", skip_all)] - pub fn layout( - &self, - vt: &mut Vt, - styles: StyleChain, - consecutive: bool, - region: Size, - expand: bool, - ) -> SourceResult<Fragment> { - #[comemo::memoize] - #[allow(clippy::too_many_arguments)] - fn cached( - par: &ParElem, - world: Tracked<dyn World + '_>, - introspector: Tracked<Introspector>, - locator: Tracked<Locator>, - delayed: TrackedMut<DelayedErrors>, - tracer: TrackedMut<Tracer>, - styles: StyleChain, - consecutive: bool, - region: Size, - expand: bool, - ) -> SourceResult<Fragment> { - let mut locator = Locator::chained(locator); - let mut vt = Vt { - world, - introspector, - locator: &mut locator, - delayed, - tracer, - }; - let children = par.children(); - - // Collect all text into one string for BiDi analysis. - let (text, segments, spans) = collect(children, &styles, consecutive)?; - - // Perform BiDi analysis and then prepare paragraph layout by building a - // representation on which we can do line breaking without layouting - // each and every line from scratch. - let p = prepare(&mut vt, children, &text, segments, spans, styles, region)?; - - // Break the paragraph into lines. - let lines = linebreak(&vt, &p, region.x - p.hang); - - // Stack the lines into one frame per region. - finalize(&mut vt, &p, &lines, region, expand) - } - - let fragment = cached( - self, - vt.world, - vt.introspector, - vt.locator.track(), - TrackedMut::reborrow_mut(&mut vt.delayed), - TrackedMut::reborrow_mut(&mut vt.tracer), - styles, - consecutive, - region, - expand, - )?; - - vt.locator.visit_frames(&fragment); - Ok(fragment) - } -} - -/// How to determine line breaks in a paragraph. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)] -pub enum Linebreaks { - /// Determine the line breaks in a simple first-fit style. - Simple, - /// Optimize the line breaks for the whole paragraph. - /// - /// Typst will try to produce more evenly filled lines of text by - /// considering the whole paragraph when calculating line breaks. - Optimized, -} - -/// A paragraph break. -/// -/// This starts a new paragraph. Especially useful when used within code like -/// [for loops]($scripting/#loops). Multiple consecutive -/// paragraph breaks collapse into a single one. -/// -/// # Example -/// ```example -/// #for i in range(3) { -/// [Blind text #i: ] -/// lorem(5) -/// parbreak() -/// } -/// ``` -/// -/// # Syntax -/// Instead of calling this function, you can insert a blank line into your -/// markup to create a paragraph break. -#[elem(title = "Paragraph Break", Unlabellable)] -pub struct ParbreakElem {} - -impl Unlabellable for ParbreakElem {} - -/// Range of a substring of text. -type Range = std::ops::Range<usize>; - -// The characters by which spacing, inline content and pins are replaced in the -// paragraph's full text. -const SPACING_REPLACE: char = ' '; // Space -const OBJ_REPLACE: char = '\u{FFFC}'; // Object Replacement Character - -/// A paragraph representation in which children are already layouted and text -/// is already preshaped. -/// -/// In many cases, we can directly reuse these results when constructing a line. -/// Only when a line break falls onto a text index that is not safe-to-break per -/// rustybuzz, we have to reshape that portion. -pub(crate) struct Preparation<'a> { - /// Bidirectional text embedding levels for the paragraph. - pub bidi: BidiInfo<'a>, - /// Text runs, spacing and layouted elements. - pub items: Vec<Item<'a>>, - /// The span mapper. - pub spans: SpanMapper, - /// Whether to hyphenate if it's the same for all children. - pub hyphenate: Option<bool>, - /// The text language if it's the same for all children. - pub lang: Option<Lang>, - /// The paragraph's resolved horizontal alignment. - pub align: FixedAlign, - /// Whether to justify the paragraph. - pub justify: bool, - /// The paragraph's hanging indent. - pub hang: Abs, - /// Whether to add spacing between CJK and Latin characters. - pub cjk_latin_spacing: bool, - /// Whether font fallback is enabled for this paragraph. - pub fallback: bool, - /// The leading of the paragraph. - pub leading: Abs, - /// How to determine line breaks. - pub linebreaks: Smart<Linebreaks>, - /// The text size. - pub size: Abs, -} - -impl<'a> Preparation<'a> { - /// Find the item that contains the given `text_offset`. - pub fn find(&self, text_offset: usize) -> Option<&Item<'a>> { - let mut cursor = 0; - for item in &self.items { - let end = cursor + item.len(); - if (cursor..end).contains(&text_offset) { - return Some(item); - } - cursor = end; - } - None - } - - /// Return the items that intersect the given `text_range`. - /// - /// Returns the expanded range around the items and the items. - pub fn slice(&self, text_range: Range) -> (Range, &[Item<'a>]) { - let mut cursor = 0; - let mut start = 0; - let mut end = 0; - let mut expanded = text_range.clone(); - - for (i, item) in self.items.iter().enumerate() { - if cursor <= text_range.start { - start = i; - expanded.start = cursor; - } - - let len = item.len(); - if cursor < text_range.end || cursor + len <= text_range.end { - end = i + 1; - expanded.end = cursor + len; - } else { - break; - } - - cursor += len; - } - - (expanded, &self.items[start..end]) - } -} - -/// A segment of one or multiple collapsed children. -#[derive(Debug, Copy, Clone)] -enum Segment<'a> { - /// One or multiple collapsed text or text-equivalent children. Stores how - /// long the segment is (in bytes of the full text string). - Text(usize), - /// Horizontal spacing between other segments. - Spacing(Spacing), - /// A mathematical equation. - Equation(&'a EquationElem), - /// A box with arbitrary content. - Box(&'a BoxElem, bool), - /// Metadata. - Meta, -} - -impl Segment<'_> { - /// The text length of the item. - fn len(&self) -> usize { - match *self { - Self::Text(len) => len, - Self::Spacing(_) => SPACING_REPLACE.len_utf8(), - Self::Box(_, true) => SPACING_REPLACE.len_utf8(), - Self::Equation(_) | Self::Box(_, _) => OBJ_REPLACE.len_utf8(), - Self::Meta => 0, - } - } -} - -/// A prepared item in a paragraph layout. -#[derive(Debug)] -pub(crate) enum Item<'a> { - /// A shaped text run with consistent style and direction. - Text(ShapedText<'a>), - /// Absolute spacing between other items. - Absolute(Abs), - /// Fractional spacing between other items. - Fractional(Fr, Option<(&'a BoxElem, StyleChain<'a>)>), - /// Layouted inline-level content. - Frame(Frame), - /// Metadata. - Meta(Frame), -} - -impl<'a> Item<'a> { - /// If this a text item, return it. - pub fn text(&self) -> Option<&ShapedText<'a>> { - match self { - Self::Text(shaped) => Some(shaped), - _ => None, - } - } - - pub fn text_mut(&mut self) -> Option<&mut ShapedText<'a>> { - match self { - Self::Text(shaped) => Some(shaped), - _ => None, - } - } - - /// The text length of the item. - pub fn len(&self) -> usize { - match self { - Self::Text(shaped) => shaped.text.len(), - Self::Absolute(_) | Self::Fractional(_, _) => SPACING_REPLACE.len_utf8(), - Self::Frame(_) => OBJ_REPLACE.len_utf8(), - Self::Meta(_) => 0, - } - } - - /// The natural layouted width of the item. - pub fn width(&self) -> Abs { - match self { - Self::Text(shaped) => shaped.width, - Self::Absolute(v) => *v, - Self::Frame(frame) => frame.width(), - Self::Fractional(_, _) | Self::Meta(_) => Abs::zero(), - } - } -} - -/// Maps byte offsets back to spans. -#[derive(Default)] -pub struct SpanMapper(Vec<(usize, Span)>); - -impl SpanMapper { - /// Create a new span mapper. - pub fn new() -> Self { - Self::default() - } - - /// Push a span for a segment with the given length. - pub fn push(&mut self, len: usize, span: Span) { - self.0.push((len, span)); - } - - /// Determine the span at the given byte offset. - /// - /// May return a detached span. - pub fn span_at(&self, offset: usize) -> (Span, u16) { - let mut cursor = 0; - for &(len, span) in &self.0 { - if (cursor..=cursor + len).contains(&offset) { - return (span, u16::try_from(offset - cursor).unwrap_or(0)); - } - cursor += len; - } - (Span::detached(), 0) - } -} - -/// A layouted line, consisting of a sequence of layouted paragraph items that -/// are mostly borrowed from the preparation phase. This type enables you to -/// measure the size of a line in a range before committing to building the -/// line's frame. -/// -/// At most two paragraph items must be created individually for this line: The -/// first and last one since they may be broken apart by the start or end of the -/// line, respectively. But even those can partially reuse previous results when -/// the break index is safe-to-break per rustybuzz. -struct Line<'a> { - /// Bidi information about the paragraph. - bidi: &'a BidiInfo<'a>, - /// The trimmed range the line spans in the paragraph. - trimmed: Range, - /// The untrimmed end where the line ends. - end: usize, - /// A reshaped text item if the line sliced up a text item at the start. - first: Option<Item<'a>>, - /// Inner items which don't need to be reprocessed. - inner: &'a [Item<'a>], - /// A reshaped text item if the line sliced up a text item at the end. If - /// there is only one text item, this takes precedence over `first`. - last: Option<Item<'a>>, - /// The width of the line. - width: Abs, - /// Whether the line should be justified. - justify: bool, - /// Whether the line ends with a hyphen or dash, either naturally or through - /// hyphenation. - dash: bool, -} - -impl<'a> Line<'a> { - /// Iterate over the line's items. - fn items(&self) -> impl Iterator<Item = &Item<'a>> { - self.first.iter().chain(self.inner).chain(&self.last) - } - - /// Return items that intersect the given `text_range`. - fn slice(&self, text_range: Range) -> impl Iterator<Item = &Item<'a>> { - let mut cursor = self.trimmed.start; - let mut start = 0; - let mut end = 0; - - for (i, item) in self.items().enumerate() { - if cursor <= text_range.start { - start = i; - } - - let len = item.len(); - if cursor < text_range.end || cursor + len <= text_range.end { - end = i + 1; - } else { - break; - } - - cursor += len; - } - - self.items().skip(start).take(end - start) - } - - /// How many glyphs are in the text where we can insert additional - /// space when encountering underfull lines. - fn justifiables(&self) -> usize { - let mut count = 0; - for shaped in self.items().filter_map(Item::text) { - count += shaped.justifiables(); - } - // CJK character at line end should not be adjusted. - if self - .items() - .last() - .and_then(Item::text) - .map(|s| s.cjk_justifiable_at_last()) - .unwrap_or(false) - { - count -= 1; - } - - count - } - - /// How much can the line stretch - fn stretchability(&self) -> Abs { - self.items().filter_map(Item::text).map(|s| s.stretchability()).sum() - } - - /// How much can the line shrink - fn shrinkability(&self) -> Abs { - self.items().filter_map(Item::text).map(|s| s.shrinkability()).sum() - } - - /// The sum of fractions in the line. - fn fr(&self) -> Fr { - self.items() - .filter_map(|item| match item { - Item::Fractional(fr, _) => Some(*fr), - _ => None, - }) - .sum() - } -} - -/// Collect all text of the paragraph into one string. This also performs -/// string-level preprocessing like case transformations. -#[allow(clippy::type_complexity)] -fn collect<'a>( - children: &'a [Prehashed<Content>], - styles: &'a StyleChain<'a>, - consecutive: bool, -) -> SourceResult<(String, Vec<(Segment<'a>, StyleChain<'a>)>, SpanMapper)> { - let mut full = String::new(); - let mut quoter = Quoter::new(); - let mut segments = Vec::with_capacity(2 + children.len()); - let mut spans = SpanMapper::new(); - let mut iter = children.iter().map(|c| &**c).peekable(); - - let first_line_indent = ParElem::first_line_indent_in(*styles); - if !first_line_indent.is_zero() - && consecutive - && AlignElem::alignment_in(*styles).resolve(*styles).x - == TextElem::dir_in(*styles).start().into() - { - full.push(SPACING_REPLACE); - segments.push((Segment::Spacing(first_line_indent.into()), *styles)); - } - - let hang = ParElem::hanging_indent_in(*styles); - if !hang.is_zero() { - full.push(SPACING_REPLACE); - segments.push((Segment::Spacing((-hang).into()), *styles)); - } - - while let Some(mut child) = iter.next() { - let outer = styles; - let mut styles = *styles; - if let Some((elem, local)) = child.to_styled() { - child = elem; - styles = outer.chain(local); - } - - let segment = if child.is::<SpaceElem>() { - full.push(' '); - Segment::Text(1) - } else if let Some(elem) = child.to::<TextElem>() { - let prev = full.len(); - if let Some(case) = TextElem::case_in(styles) { - full.push_str(&case.apply(elem.text())); - } else { - full.push_str(elem.text()); - } - Segment::Text(full.len() - prev) - } else if let Some(elem) = child.to::<HElem>() { - if elem.amount().is_zero() { - continue; - } - - full.push(SPACING_REPLACE); - Segment::Spacing(*elem.amount()) - } else if let Some(elem) = child.to::<LinebreakElem>() { - let c = if elem.justify(styles) { '\u{2028}' } else { '\n' }; - full.push(c); - Segment::Text(c.len_utf8()) - } else if let Some(elem) = child.to::<SmartquoteElem>() { - let prev = full.len(); - if SmartquoteElem::enabled_in(styles) { - let quotes = SmartquoteElem::quotes_in(styles); - let lang = TextElem::lang_in(styles); - let region = TextElem::region_in(styles); - let quotes = Quotes::new( - quotes, - lang, - region, - SmartquoteElem::alternative_in(styles), - ); - let peeked = iter.peek().and_then(|child| { - let child = if let Some((child, _)) = child.to_styled() { - child - } else { - child - }; - if let Some(elem) = child.to::<TextElem>() { - elem.text().chars().next() - } else if child.is::<SmartquoteElem>() { - Some('"') - } else if child.is::<SpaceElem>() - || child.is::<HElem>() - || child.is::<LinebreakElem>() - { - Some(SPACING_REPLACE) - } else { - Some(OBJ_REPLACE) - } - }); - - full.push_str(quoter.quote("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::<EquationElem>() { - full.push(OBJ_REPLACE); - Segment::Equation(elem) - } else if let Some(elem) = child.to::<BoxElem>() { - let frac = elem.width(styles).is_fractional(); - full.push(if frac { SPACING_REPLACE } else { OBJ_REPLACE }); - Segment::Box(elem, frac) - } else if child.is::<MetaElem>() { - Segment::Meta - } else { - bail!(child.span(), "unexpected paragraph child"); - }; - - if let Some(last) = full.chars().last() { - quoter.last(last, child.is::<SmartquoteElem>()); - } - - spans.push(segment.len(), child.span()); - - if let (Some((Segment::Text(last_len), last_styles)), Segment::Text(len)) = - (segments.last_mut(), segment) - { - if *last_styles == styles { - *last_len += len; - continue; - } - } - - segments.push((segment, styles)); - } - - Ok((full, segments, spans)) -} - -/// Prepare paragraph layout by shaping the whole paragraph and layouting all -/// contained inline-level content. -fn prepare<'a>( - vt: &mut Vt, - children: &'a [Prehashed<Content>], - text: &'a str, - segments: Vec<(Segment<'a>, StyleChain<'a>)>, - spans: SpanMapper, - styles: StyleChain<'a>, - region: Size, -) -> SourceResult<Preparation<'a>> { - let dir = TextElem::dir_in(styles); - let bidi = BidiInfo::new( - text, - match dir { - Dir::LTR => Some(BidiLevel::ltr()), - Dir::RTL => Some(BidiLevel::rtl()), - _ => None, - }, - ); - - let mut cursor = 0; - let mut items = Vec::with_capacity(segments.len()); - - // Shape / layout the children and collect them into items. - for (segment, styles) in segments { - let end = cursor + segment.len(); - match segment { - Segment::Text(_) => { - shape_range(&mut items, vt, &bidi, cursor..end, &spans, styles); - } - Segment::Spacing(spacing) => match spacing { - Spacing::Rel(v) => { - let resolved = v.resolve(styles).relative_to(region.x); - items.push(Item::Absolute(resolved)); - } - Spacing::Fr(v) => { - items.push(Item::Fractional(v, None)); - } - }, - Segment::Equation(equation) => { - let pod = Regions::one(region, Axes::splat(false)); - let mut frame = equation.layout(vt, styles, pod)?.into_frame(); - frame.translate(Point::with_y(TextElem::baseline_in(styles))); - items.push(Item::Frame(frame)); - } - Segment::Box(elem, _) => { - if let Sizing::Fr(v) = elem.width(styles) { - items.push(Item::Fractional(v, Some((elem, styles)))); - } else { - let pod = Regions::one(region, Axes::splat(false)); - let mut frame = elem.layout(vt, styles, pod)?.into_frame(); - frame.translate(Point::with_y(TextElem::baseline_in(styles))); - items.push(Item::Frame(frame)); - } - } - Segment::Meta => { - let mut frame = Frame::soft(Size::zero()); - frame.meta(styles, true); - items.push(Item::Meta(frame)); - } - } - - cursor = end; - } - - let cjk_latin_spacing = TextElem::cjk_latin_spacing_in(styles).is_auto(); - if cjk_latin_spacing { - add_cjk_latin_spacing(&mut items); - } - - Ok(Preparation { - bidi, - items, - spans, - hyphenate: shared_get(styles, children, TextElem::hyphenate_in), - lang: shared_get(styles, children, TextElem::lang_in), - align: AlignElem::alignment_in(styles).resolve(styles).x, - justify: ParElem::justify_in(styles), - hang: ParElem::hanging_indent_in(styles), - cjk_latin_spacing, - fallback: TextElem::fallback_in(styles), - leading: ParElem::leading_in(styles), - linebreaks: ParElem::linebreaks_in(styles), - size: TextElem::size_in(styles), - }) -} - -/// Add some spacing between Han characters and western characters. -/// See Requirements for Chinese Text Layout, Section 3.2.2 Mixed Text Composition in Horizontal -/// Written Mode -fn add_cjk_latin_spacing(items: &mut [Item]) { - let mut items = items.iter_mut().filter(|x| !matches!(x, Item::Meta(_))).peekable(); - let mut prev: Option<&ShapedGlyph> = None; - while let Some(item) = items.next() { - let Some(text) = item.text_mut() else { - prev = None; - continue; - }; - - // Since we only call this function in [`prepare`], we can assume - // that the Cow is owned, and `to_mut` can be called without overhead. - debug_assert!(matches!(text.glyphs, std::borrow::Cow::Owned(_))); - let mut glyphs = text.glyphs.to_mut().iter_mut().peekable(); - - while let Some(glyph) = glyphs.next() { - let next = glyphs.peek().map(|n| n as _).or_else(|| { - items - .peek() - .and_then(|i| i.text()) - .and_then(|shaped| shaped.glyphs.first()) - }); - - // Case 1: CJK followed by a Latin character - if glyph.is_cjk_script() && next.map_or(false, |g| g.is_letter_or_number()) { - // The spacing is default to 1/4 em, and can be shrunk to 1/8 em. - glyph.x_advance += Em::new(0.25); - glyph.adjustability.shrinkability.1 += Em::new(0.125); - text.width += Em::new(0.25).at(text.size); - } - - // Case 2: Latin followed by a CJK character - if glyph.is_cjk_script() && prev.map_or(false, |g| g.is_letter_or_number()) { - glyph.x_advance += Em::new(0.25); - glyph.x_offset += Em::new(0.25); - glyph.adjustability.shrinkability.0 += Em::new(0.125); - text.width += Em::new(0.25).at(text.size); - } - - prev = Some(glyph); - } - } -} - -/// Group a range of text by BiDi level and script, shape the runs and generate -/// items for them. -fn shape_range<'a>( - items: &mut Vec<Item<'a>>, - vt: &Vt, - bidi: &BidiInfo<'a>, - range: Range, - spans: &SpanMapper, - styles: StyleChain<'a>, -) { - let script = TextElem::script_in(styles); - let lang = TextElem::lang_in(styles); - let region = TextElem::region_in(styles); - let mut process = |range: Range, level: BidiLevel| { - let dir = if level.is_ltr() { Dir::LTR } else { Dir::RTL }; - let shaped = - shape(vt, range.start, &bidi.text[range], spans, styles, dir, lang, region); - items.push(Item::Text(shaped)); - }; - - let mut prev_level = BidiLevel::ltr(); - let mut prev_script = Script::Unknown; - let mut cursor = range.start; - - // Group by embedding level and script. If the text's script is explicitly - // set (rather than inferred from the glyphs), we keep the script at an - // unchanging `Script::Unknown` so that only level changes cause breaks. - for i in range.clone() { - if !bidi.text.is_char_boundary(i) { - continue; - } - - let level = bidi.levels[i]; - let curr_script = match script { - Smart::Auto => { - bidi.text[i..].chars().next().map_or(Script::Unknown, |c| c.script()) - } - Smart::Custom(_) => Script::Unknown, - }; - - if level != prev_level || !is_compatible(curr_script, prev_script) { - if cursor < i { - process(cursor..i, prev_level); - } - cursor = i; - prev_level = level; - prev_script = curr_script; - } else if is_generic_script(prev_script) { - prev_script = curr_script; - } - } - - process(cursor..range.end, prev_level); -} - -/// Whether this is not a specific script. -fn is_generic_script(script: Script) -> bool { - matches!(script, Script::Unknown | Script::Common | Script::Inherited) -} - -/// Whether these script can be part of the same shape run. -fn is_compatible(a: Script, b: Script) -> bool { - is_generic_script(a) || is_generic_script(b) || a == b -} - -/// Get a style property, but only if it is the same for all children of the -/// paragraph. -fn shared_get<T: PartialEq>( - styles: StyleChain<'_>, - children: &[Prehashed<Content>], - getter: fn(StyleChain) -> T, -) -> Option<T> { - let value = getter(styles); - children - .iter() - .filter_map(|child| child.to_styled()) - .all(|(_, local)| getter(styles.chain(local)) == value) - .then_some(value) -} - -/// Find suitable linebreaks. -fn linebreak<'a>(vt: &Vt, p: &'a Preparation<'a>, width: Abs) -> Vec<Line<'a>> { - let linebreaks = p.linebreaks.unwrap_or_else(|| { - if p.justify { - Linebreaks::Optimized - } else { - Linebreaks::Simple - } - }); - - match linebreaks { - Linebreaks::Simple => linebreak_simple(vt, p, width), - Linebreaks::Optimized => linebreak_optimized(vt, p, width), - } -} - -/// Perform line breaking in simple first-fit style. This means that we build -/// lines greedily, always taking the longest possible line. This may lead to -/// very unbalanced line, but is fast and simple. -fn linebreak_simple<'a>(vt: &Vt, p: &'a Preparation<'a>, width: Abs) -> Vec<Line<'a>> { - let mut lines = Vec::with_capacity(16); - let mut start = 0; - let mut last = None; - - breakpoints(p, |end, breakpoint| { - // Compute the line and its size. - let mut attempt = line(vt, p, start..end, breakpoint); - - // If the line doesn't fit anymore, we push the last fitting attempt - // into the stack and rebuild the line from the attempt's end. The - // resulting line cannot be broken up further. - if !width.fits(attempt.width) { - if let Some((last_attempt, last_end)) = last.take() { - lines.push(last_attempt); - start = last_end; - attempt = line(vt, p, start..end, breakpoint); - } - } - - // Finish the current line if there is a mandatory line break (i.e. - // due to "\n") or if the line doesn't fit horizontally already - // since then no shorter line will be possible. - if breakpoint == Breakpoint::Mandatory || !width.fits(attempt.width) { - lines.push(attempt); - start = end; - last = None; - } else { - last = Some((attempt, end)); - } - }); - - if let Some((line, _)) = last { - lines.push(line); - } - - lines -} - -/// Perform line breaking in optimized Knuth-Plass style. Here, we use more -/// context to determine the line breaks than in the simple first-fit style. For -/// example, we might choose to cut a line short even though there is still a -/// bit of space to improve the fit of one of the following lines. The -/// Knuth-Plass algorithm is based on the idea of "cost". A line which has a -/// very tight or very loose fit has a higher cost than one that is just right. -/// Ending a line with a hyphen incurs extra cost and endings two successive -/// lines with hyphens even more. -/// -/// To find the layout with the minimal total cost the algorithm uses dynamic -/// programming: For each possible breakpoint it determines the optimal -/// paragraph layout _up to that point_. It walks over all possible start points -/// for a line ending at that point and finds the one for which the cost of the -/// line plus the cost of the optimal paragraph up to the start point (already -/// computed and stored in dynamic programming table) is minimal. The final -/// result is simply the layout determined for the last breakpoint at the end of -/// text. -fn linebreak_optimized<'a>(vt: &Vt, p: &'a Preparation<'a>, width: Abs) -> Vec<Line<'a>> { - /// The cost of a line or paragraph layout. - type Cost = f64; - - /// An entry in the dynamic programming table. - struct Entry<'a> { - pred: usize, - total: Cost, - line: Line<'a>, - } - - // Cost parameters. - const HYPH_COST: Cost = 0.5; - const RUNT_COST: Cost = 0.5; - const CONSECUTIVE_DASH_COST: Cost = 0.3; - const MAX_COST: Cost = 1_000_000.0; - const MIN_RATIO: f64 = -1.0; - - // Dynamic programming table. - let mut active = 0; - let mut table = vec![Entry { - pred: 0, - total: 0.0, - line: line(vt, p, 0..0, Breakpoint::Mandatory), - }]; - - let em = p.size; - let mut lines = Vec::with_capacity(16); - breakpoints(p, |end, breakpoint| { - let k = table.len(); - let eof = end == p.bidi.text.len(); - let mut best: Option<Entry> = None; - - // Find the optimal predecessor. - for (i, pred) in table.iter().enumerate().skip(active) { - // Layout the line. - let start = pred.line.end; - - let attempt = line(vt, p, start..end, breakpoint); - - // Determine how much the line's spaces would need to be stretched - // to make it the desired width. - let delta = width - attempt.width; - // Determine how much stretch are permitted. - let adjust = if delta >= Abs::zero() { - attempt.stretchability() - } else { - attempt.shrinkability() - }; - // Ideally, the ratio should between -1.0 and 1.0, but sometimes a value above 1.0 - // is possible, in which case the line is underfull. - let mut ratio = delta / adjust; - if ratio.is_nan() { - // The line is not stretchable, but it just fits. - // This often happens with monospace fonts and CJK texts. - ratio = 0.0; - } - if ratio > 1.0 { - // We should stretch the line above its stretchability. Now - // calculate the extra amount. Also, don't divide by zero. - let extra_stretch = - (delta - adjust) / attempt.justifiables().max(1) as f64; - // Normalize the amount by half Em size. - ratio = 1.0 + extra_stretch / (em / 2.0); - } - - // Determine the cost of the line. - let min_ratio = if p.justify { MIN_RATIO } else { 0.0 }; - let mut cost = if ratio < min_ratio { - // The line is overfull. This is the case if - // - justification is on, but we'd need to shrink too much - // - justification is off and the line just doesn't fit - // - // If this is the earliest breakpoint in the active set - // (active == i), remove it from the active set. If there is an - // earlier one (active < i), then the logically shorter line was - // in fact longer (can happen with negative spacing) and we - // can't trim the active set just yet. - if active == i { - active += 1; - } - MAX_COST - } else if breakpoint == Breakpoint::Mandatory || eof { - // This is a mandatory break and the line is not overfull, so - // all breakpoints before this one become inactive since no line - // can span above the mandatory break. - active = k; - // If ratio > 0, we need to stretch the line only when justify is needed. - // If ratio < 0, we always need to shrink the line. - if (ratio > 0.0 && attempt.justify) || ratio < 0.0 { - ratio.powi(3).abs() - } else { - 0.0 - } - } else { - // Normal line with cost of |ratio^3|. - ratio.powi(3).abs() - }; - - // Penalize runts. - if k == i + 1 && eof { - cost += RUNT_COST; - } - - // Penalize hyphens. - if breakpoint == Breakpoint::Hyphen { - cost += HYPH_COST; - } - - // In Knuth paper, cost = (1 + 100|r|^3 + p)^2 + a, - // where r is the ratio, p=50 is the penalty, and a=3000 is consecutive the penalty. - // We divide the whole formula by 10, resulting (0.01 + |r|^3 + p)^2 + a, - // where p=0.5 and a=0.3 - cost = (0.01 + cost).powi(2); - - // Penalize two consecutive dashes (not necessarily hyphens) extra. - if attempt.dash && pred.line.dash { - cost += CONSECUTIVE_DASH_COST; - } - - // The total cost of this line and its chain of predecessors. - let total = pred.total + cost; - - // If this attempt is better than what we had before, take it! - if best.as_ref().map_or(true, |best| best.total >= total) { - best = Some(Entry { pred: i, total, line: attempt }); - } - } - - table.push(best.unwrap()); - }); - - // Retrace the best path. - let mut idx = table.len() - 1; - while idx != 0 { - table.truncate(idx + 1); - let entry = table.pop().unwrap(); - lines.push(entry.line); - idx = entry.pred; - } - - lines.reverse(); - lines -} - -/// Create a line which spans the given range. -fn line<'a>( - vt: &Vt, - p: &'a Preparation, - mut range: Range, - breakpoint: Breakpoint, -) -> Line<'a> { - let end = range.end; - let mut justify = - p.justify && end < p.bidi.text.len() && breakpoint != Breakpoint::Mandatory; - - if range.is_empty() { - return Line { - bidi: &p.bidi, - end, - trimmed: range, - first: None, - inner: &[], - last: None, - width: Abs::zero(), - justify, - dash: false, - }; - } - - // Slice out the relevant items. - let (expanded, mut inner) = p.slice(range.clone()); - let mut width = Abs::zero(); - - // Reshape the last item if it's split in half or hyphenated. - let mut last = None; - let mut dash = false; - if let Some((Item::Text(shaped), before)) = inner.split_last() { - // Compute the range we want to shape, trimming whitespace at the - // end of the line. - let base = expanded.end - shaped.text.len(); - let start = range.start.max(base); - let text = &p.bidi.text[start..range.end]; - // U+200B ZERO WIDTH SPACE is used to provide a line break opportunity, - // we want to trim it too. - let trimmed = text.trim_end().trim_end_matches('\u{200B}'); - range.end = start + trimmed.len(); - - // Deal with hyphens, dashes and justification. - let shy = trimmed.ends_with('\u{ad}'); - let hyphen = breakpoint == Breakpoint::Hyphen; - dash = hyphen || shy || trimmed.ends_with(['-', '–', '—']); - justify |= text.ends_with('\u{2028}'); - - // Deal with CJK punctuation at line ends. - let gb_style = is_gb_style(shaped.lang, shaped.region); - let maybe_adjust_last_glyph = trimmed.ends_with(END_PUNCT_PAT) - || (p.cjk_latin_spacing && trimmed.ends_with(char_is_cjk_script)); - - // Usually, we don't want to shape an empty string because: - // - We don't want the height of trimmed whitespace in a different - // font to be considered for the line height. - // - Even if it's in the same font, its unnecessary. - // - // There is one exception though. When the whole line is empty, we - // need the shaped empty string to make the line the appropriate - // height. That is the case exactly if the string is empty and there - // are no other items in the line. - if hyphen || start + shaped.text.len() > range.end || maybe_adjust_last_glyph { - if hyphen || start < range.end || before.is_empty() { - let mut reshaped = shaped.reshape(vt, &p.spans, start..range.end); - if hyphen || shy { - reshaped.push_hyphen(vt, p.fallback); - } - - if let Some(last_glyph) = reshaped.glyphs.last() { - if last_glyph.is_cjk_left_aligned_punctuation(gb_style) { - // If the last glyph is a CJK punctuation, we want to shrink it. - // See Requirements for Chinese Text Layout, Section 3.1.6.3 - // Compression of punctuation marks at line start or line end - let shrink_amount = last_glyph.shrinkability().1; - let punct = reshaped.glyphs.to_mut().last_mut().unwrap(); - punct.shrink_right(shrink_amount); - reshaped.width -= shrink_amount.at(reshaped.size); - } else if p.cjk_latin_spacing - && last_glyph.is_cjk_script() - && (last_glyph.x_advance - last_glyph.x_offset) > Em::one() - { - // If the last glyph is a CJK character adjusted by [`add_cjk_latin_spacing`], - // restore the original width. - let shrink_amount = - last_glyph.x_advance - last_glyph.x_offset - Em::one(); - let glyph = reshaped.glyphs.to_mut().last_mut().unwrap(); - glyph.x_advance -= shrink_amount; - glyph.adjustability.shrinkability.1 = Em::zero(); - reshaped.width -= shrink_amount.at(reshaped.size); - } - } - - width += reshaped.width; - last = Some(Item::Text(reshaped)); - } - - inner = before; - } - } - - // Deal with CJK characters at line starts. - let text = &p.bidi.text[range.start..end]; - let maybe_adjust_first_glyph = text.starts_with(BEGIN_PUNCT_PAT) - || (p.cjk_latin_spacing && text.starts_with(char_is_cjk_script)); - - // Reshape the start item if it's split in half. - let mut first = None; - if let Some((Item::Text(shaped), after)) = inner.split_first() { - // Compute the range we want to shape. - let base = expanded.start; - let end = range.end.min(base + shaped.text.len()); - - // Reshape if necessary. - if range.start + shaped.text.len() > end || maybe_adjust_first_glyph { - // If the range is empty, we don't want to push an empty text item. - if range.start < end { - let reshaped = shaped.reshape(vt, &p.spans, range.start..end); - width += reshaped.width; - first = Some(Item::Text(reshaped)); - } - - inner = after; - } - } - - if maybe_adjust_first_glyph { - let reshaped = first.as_mut().or(last.as_mut()).and_then(Item::text_mut); - if let Some(reshaped) = reshaped { - if let Some(first_glyph) = reshaped.glyphs.first() { - if first_glyph.is_cjk_right_aligned_punctuation() { - // If the first glyph is a CJK punctuation, we want to shrink it. - let shrink_amount = first_glyph.shrinkability().0; - let glyph = reshaped.glyphs.to_mut().first_mut().unwrap(); - glyph.shrink_left(shrink_amount); - let amount_abs = shrink_amount.at(reshaped.size); - reshaped.width -= amount_abs; - width -= amount_abs; - } else if p.cjk_latin_spacing - && first_glyph.is_cjk_script() - && first_glyph.x_offset > Em::zero() - { - // If the first glyph is a CJK character adjusted by [`add_cjk_latin_spacing`], - // restore the original width. - let shrink_amount = first_glyph.x_offset; - let glyph = reshaped.glyphs.to_mut().first_mut().unwrap(); - glyph.x_advance -= shrink_amount; - glyph.x_offset = Em::zero(); - glyph.adjustability.shrinkability.0 = Em::zero(); - let amount_abs = shrink_amount.at(reshaped.size); - reshaped.width -= amount_abs; - width -= amount_abs; - } - } - } - } - - // Measure the inner items. - for item in inner { - width += item.width(); - } - - Line { - bidi: &p.bidi, - trimmed: range, - end, - first, - inner, - last, - width, - justify, - dash, - } -} - -/// Combine layouted lines into one frame per region. -fn finalize( - vt: &mut Vt, - p: &Preparation, - lines: &[Line], - region: Size, - expand: bool, -) -> SourceResult<Fragment> { - // Determine the paragraph's width: Full width of the region if we - // should expand or there's fractional spacing, fit-to-width otherwise. - let width = if !region.x.is_finite() - || (!expand && lines.iter().all(|line| line.fr().is_zero())) - { - region - .x - .min(p.hang + lines.iter().map(|line| line.width).max().unwrap_or_default()) - } else { - region.x - }; - - // Stack the lines into one frame per region. - let mut frames: Vec<Frame> = lines - .iter() - .map(|line| commit(vt, p, line, width, region.y)) - .collect::<SourceResult<_>>()?; - - // Prevent orphans. - if frames.len() >= 2 && !frames[1].is_empty() { - let second = frames.remove(1); - let first = &mut frames[0]; - merge(first, second, p.leading); - } - - // Prevent widows. - let len = frames.len(); - if len >= 2 && !frames[len - 2].is_empty() { - let second = frames.pop().unwrap(); - let first = frames.last_mut().unwrap(); - merge(first, second, p.leading); - } - - Ok(Fragment::frames(frames)) -} - -/// Merge two line frames -fn merge(first: &mut Frame, second: Frame, leading: Abs) { - let offset = first.height() + leading; - let total = offset + second.height(); - first.push_frame(Point::with_y(offset), second); - first.size_mut().y = total; -} - -/// Commit to a line and build its frame. -fn commit( - vt: &mut Vt, - p: &Preparation, - line: &Line, - width: Abs, - full: Abs, -) -> SourceResult<Frame> { - let mut remaining = width - line.width - p.hang; - let mut offset = Abs::zero(); - - // Reorder the line from logical to visual order. - let (reordered, starts_rtl) = reorder(line); - if !starts_rtl { - offset += p.hang; - } - - // Handle hanging punctuation to the left. - if let Some(Item::Text(text)) = reordered.first() { - if let Some(glyph) = text.glyphs.first() { - if !text.dir.is_positive() - && TextElem::overhang_in(text.styles) - && (reordered.len() > 1 || text.glyphs.len() > 1) - { - let amount = overhang(glyph.c) * glyph.x_advance.at(text.size); - offset -= amount; - remaining += amount; - } - } - } - - // Handle hanging punctuation to the right. - if let Some(Item::Text(text)) = reordered.last() { - if let Some(glyph) = text.glyphs.last() { - if text.dir.is_positive() - && TextElem::overhang_in(text.styles) - && (reordered.len() > 1 || text.glyphs.len() > 1) - { - let amount = overhang(glyph.c) * glyph.x_advance.at(text.size); - remaining += amount; - } - } - } - - // Determine how much additional space is needed. - // The justicication_ratio is for the first step justification, - // extra_justification is for the last step. - // For more info on multi-step justification, see Procedures for Inter- - // Character Space Expansion in W3C document Chinese Layout Requirements. - let fr = line.fr(); - let mut justification_ratio = 0.0; - let mut extra_justification = Abs::zero(); - - let shrink = line.shrinkability(); - let stretch = line.stretchability(); - if remaining < Abs::zero() && shrink > Abs::zero() { - // Attempt to reduce the length of the line, using shrinkability. - justification_ratio = (remaining / shrink).max(-1.0); - remaining = (remaining + shrink).min(Abs::zero()); - } else if line.justify && fr.is_zero() { - // Attempt to increase the length of the line, using stretchability. - if stretch > Abs::zero() { - justification_ratio = (remaining / stretch).min(1.0); - remaining = (remaining - stretch).max(Abs::zero()); - } - - let justifiables = line.justifiables(); - if justifiables > 0 && remaining > Abs::zero() { - // Underfull line, distribute the extra space. - extra_justification = remaining / justifiables as f64; - remaining = Abs::zero(); - } - } - - let mut top = Abs::zero(); - let mut bottom = Abs::zero(); - - // Build the frames and determine the height and baseline. - let mut frames = vec![]; - for item in reordered { - let mut push = |offset: &mut Abs, frame: Frame| { - let width = frame.width(); - top.set_max(frame.baseline()); - bottom.set_max(frame.size().y - frame.baseline()); - frames.push((*offset, frame)); - *offset += width; - }; - - match item { - Item::Absolute(v) => { - offset += *v; - } - Item::Fractional(v, elem) => { - let amount = v.share(fr, remaining); - if let Some((elem, styles)) = elem { - let region = Size::new(amount, full); - let pod = Regions::one(region, Axes::new(true, false)); - let mut frame = elem.layout(vt, *styles, pod)?.into_frame(); - frame.translate(Point::with_y(TextElem::baseline_in(*styles))); - push(&mut offset, frame); - } else { - offset += amount; - } - } - Item::Text(shaped) => { - let frame = shaped.build(vt, justification_ratio, extra_justification); - push(&mut offset, frame); - } - Item::Frame(frame) | Item::Meta(frame) => { - push(&mut offset, frame.clone()); - } - } - } - - // Remaining space is distributed now. - if !fr.is_zero() { - remaining = Abs::zero(); - } - - let size = Size::new(width, top + bottom); - let mut output = Frame::soft(size); - output.set_baseline(top); - - // Construct the line's frame. - for (offset, frame) in frames { - let x = offset + p.align.position(remaining); - let y = top - frame.baseline(); - output.push_frame(Point::new(x, y), frame); - } - - Ok(output) -} - -/// Return a line's items in visual order. -fn reorder<'a>(line: &'a Line<'a>) -> (Vec<&Item<'a>>, bool) { - let mut reordered = vec![]; - - // The bidi crate doesn't like empty lines. - if line.trimmed.is_empty() { - return (line.slice(line.trimmed.clone()).collect(), false); - } - - // Find the paragraph that contains the line. - let para = line - .bidi - .paragraphs - .iter() - .find(|para| para.range.contains(&line.trimmed.start)) - .unwrap(); - - // Compute the reordered ranges in visual order (left to right). - let (levels, runs) = line.bidi.visual_runs(para, line.trimmed.clone()); - let starts_rtl = levels.first().map_or(false, |level| level.is_rtl()); - - // Collect the reordered items. - for run in runs { - // Skip reset L1 runs because handling them would require reshaping - // again in some cases. - if line.bidi.levels[run.start] != levels[run.start] { - continue; - } - - let prev = reordered.len(); - reordered.extend(line.slice(run.clone())); - - if levels[run.start].is_rtl() { - reordered[prev..].reverse(); - } - } - - (reordered, starts_rtl) -} - -/// How much a character should hang into the end margin. -/// -/// For more discussion, see: -/// https://recoveringphysicist.com/21/ -fn overhang(c: char) -> f64 { - match c { - // Dashes. - '–' | '—' => 0.2, - '-' => 0.55, - - // Punctuation. - '.' | ',' => 0.8, - ':' | ';' => 0.3, - - // Arabic - '\u{60C}' | '\u{6D4}' => 0.4, - - _ => 0.0, - } -} diff --git a/crates/typst-library/src/layout/place.rs b/crates/typst-library/src/layout/place.rs deleted file mode 100644 index c8e83383..00000000 --- a/crates/typst-library/src/layout/place.rs +++ /dev/null @@ -1,124 +0,0 @@ -use crate::prelude::*; - -/// Places content at an absolute position. -/// -/// Placed content will not affect the position of other content. Place is -/// always relative to its parent container and will be in the foreground of all -/// other content in the container. Page margins will be respected. -/// -/// -/// # Example -/// ```example -/// #set page(height: 60pt) -/// Hello, world! -/// -/// #place( -/// top + right, -/// square( -/// width: 20pt, -/// stroke: 2pt + blue -/// ), -/// ) -/// ``` -#[elem(Layout, Behave)] -pub struct PlaceElem { - /// Relative to which position in the parent container to place the content. - /// - /// Cannot be `{auto}` if `float` is `{false}` and must be either - /// `{auto}`, `{top}`, or `{bottom}` if `float` is `{true}`. - /// - /// When an axis of the page is `{auto}` sized, all alignments relative to - /// that axis will be ignored, instead, the item will be placed in the - /// origin of the axis. - #[positional] - #[default(Smart::Custom(Align::START))] - pub alignment: Smart<Align>, - - /// Whether the placed element has floating layout. - /// - /// Floating elements are positioned at the top or bottom of the page, - /// displacing in-flow content. - /// - /// ```example - /// #set page(height: 150pt) - /// #let note(where, body) = place( - /// center + where, - /// float: true, - /// clearance: 6pt, - /// rect(body), - /// ) - /// - /// #lorem(10) - /// #note(bottom)[Bottom 1] - /// #note(bottom)[Bottom 2] - /// #lorem(40) - /// #note(top)[Top] - /// #lorem(10) - /// ``` - pub float: bool, - - /// The amount of clearance the placed element has in a floating layout. - #[default(Em::new(1.5).into())] - #[resolve] - pub clearance: Length, - - /// The horizontal displacement of the placed content. - /// - /// ```example - /// #set page(height: 100pt) - /// #for i in range(16) { - /// let amount = i * 4pt - /// place(center, dx: amount - 32pt, dy: amount)[A] - /// } - /// ``` - pub dx: Rel<Length>, - - /// The vertical displacement of the placed content. - pub dy: Rel<Length>, - - /// The content to place. - #[required] - pub body: Content, -} - -impl Layout for PlaceElem { - #[tracing::instrument(name = "PlaceElem::layout", skip_all)] - fn layout( - &self, - vt: &mut Vt, - styles: StyleChain, - regions: Regions, - ) -> SourceResult<Fragment> { - // The pod is the base area of the region because for absolute - // placement we don't really care about the already used area. - let base = regions.base(); - let float = self.float(styles); - let alignment = self.alignment(styles); - - if float - && alignment - .map_or(false, |align| matches!(align.y(), None | Some(VAlign::Horizon))) - { - bail!(self.span(), "floating placement must be `auto`, `top`, or `bottom`"); - } else if !float && alignment.is_auto() { - return Err("automatic positioning is only available for floating placement") - .hint("you can enable floating placement with `place(float: true, ..)`") - .at(self.span()); - } - - let child = self - .body() - .clone() - .aligned(alignment.unwrap_or_else(|| Align::CENTER)); - - let pod = Regions::one(base, Axes::splat(false)); - let frame = child.layout(vt, styles, pod)?.into_frame(); - Ok(Fragment::frame(frame)) - } -} - -impl Behave for PlaceElem { - fn behaviour(&self) -> Behaviour { - Behaviour::Ignorant - } -} diff --git a/crates/typst-library/src/layout/regions.rs b/crates/typst-library/src/layout/regions.rs deleted file mode 100644 index 6dd549b0..00000000 --- a/crates/typst-library/src/layout/regions.rs +++ /dev/null @@ -1,144 +0,0 @@ -use std::fmt::{self, Debug, Formatter}; - -use typst::geom::{Abs, Axes, Size}; - -/// A sequence of regions to layout into. -#[derive(Copy, Clone, Hash)] -pub struct Regions<'a> { - /// The remaining size of the first region. - pub size: Size, - /// The full height of the region for relative sizing. - pub full: Abs, - /// The height of followup regions. The width is the same for all regions. - pub backlog: &'a [Abs], - /// The height of the final region that is repeated once the backlog is - /// drained. The width is the same for all regions. - pub last: Option<Abs>, - /// Whether elements should expand to fill the regions instead of shrinking - /// to fit the content. - pub expand: Axes<bool>, - /// Whether these are the root regions or direct descendants. - /// - /// True for the padded page regions and columns directly in the page, - /// false otherwise. - pub root: bool, -} - -impl Regions<'_> { - /// Create a new region sequence with exactly one region. - pub fn one(size: Size, expand: Axes<bool>) -> Self { - Self { - size, - full: size.y, - backlog: &[], - last: None, - expand, - root: false, - } - } - - /// Create a new sequence of same-size regions that repeats indefinitely. - pub fn repeat(size: Size, expand: Axes<bool>) -> Self { - Self { - size, - full: size.y, - backlog: &[], - last: Some(size.y), - expand, - root: false, - } - } - - /// The base size, which doesn't take into account that the regions is - /// already partially used up. - /// - /// This is also used for relative sizing. - pub fn base(&self) -> Size { - Size::new(self.size.x, self.full) - } - - /// Create new regions where all sizes are mapped with `f`. - /// - /// Note that since all regions must have the same width, the width returned - /// by `f` is ignored for the backlog and the final region. - pub fn map<'v, F>(&self, backlog: &'v mut Vec<Abs>, mut f: F) -> Regions<'v> - where - F: FnMut(Size) -> Size, - { - let x = self.size.x; - backlog.clear(); - backlog.extend(self.backlog.iter().map(|&y| f(Size::new(x, y)).y)); - Regions { - size: f(self.size), - full: f(Size::new(x, self.full)).y, - backlog, - last: self.last.map(|y| f(Size::new(x, y)).y), - expand: self.expand, - root: false, - } - } - - /// Whether the first region is full and a region break is called for. - pub fn is_full(&self) -> bool { - Abs::zero().fits(self.size.y) && !self.in_last() - } - - /// Whether the first region is the last usable region. - /// - /// If this is true, calling `next()` will have no effect. - pub fn in_last(&self) -> bool { - self.backlog.is_empty() && self.last.map_or(true, |height| self.size.y == height) - } - - /// The same regions, but with different `root` configuration. - pub fn with_root(self, root: bool) -> Self { - Self { root, ..self } - } - - /// Advance to the next region if there is any. - pub fn next(&mut self) { - if let Some(height) = self - .backlog - .split_first() - .map(|(first, tail)| { - self.backlog = tail; - *first - }) - .or(self.last) - { - self.size.y = height; - self.full = height; - } - } - - /// An iterator that returns the sizes of the first and all following - /// regions, equivalently to what would be produced by calling - /// [`next()`](Self::next) repeatedly until all regions are exhausted. - /// This iterator may be infinite. - pub fn iter(&self) -> impl Iterator<Item = Size> + '_ { - let first = std::iter::once(self.size); - let backlog = self.backlog.iter(); - let last = self.last.iter().cycle(); - first.chain(backlog.chain(last).map(|&h| Size::new(self.size.x, h))) - } -} - -impl Debug for Regions<'_> { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - f.write_str("Regions ")?; - let mut list = f.debug_list(); - let mut prev = self.size.y; - list.entry(&self.size); - for &height in self.backlog { - list.entry(&Size::new(self.size.x, height)); - prev = height; - } - if let Some(last) = self.last { - if last != prev { - list.entry(&Size::new(self.size.x, last)); - } - list.entry(&(..)); - } - list.finish() - } -} diff --git a/crates/typst-library/src/layout/repeat.rs b/crates/typst-library/src/layout/repeat.rs deleted file mode 100644 index ce31164e..00000000 --- a/crates/typst-library/src/layout/repeat.rs +++ /dev/null @@ -1,75 +0,0 @@ -use crate::layout::AlignElem; -use crate::prelude::*; - -/// Repeats content to the available space. -/// -/// This can be useful when implementing a custom index, reference, or outline. -/// -/// Space may be inserted between the instances of the body parameter, so be -/// sure to include negative space if you need the instances to overlap. -/// -/// Errors if there no bounds on the available space, as it would create -/// infinite content. -/// -/// # Example -/// ```example -/// Sign on the dotted line: -/// #box(width: 1fr, repeat[.]) -/// -/// #set text(10pt) -/// #v(8pt, weak: true) -/// #align(right)[ -/// Berlin, the 22nd of December, 2022 -/// ] -/// ``` -#[elem(Layout)] -pub struct RepeatElem { - /// The content to repeat. - #[required] - pub body: Content, -} - -impl Layout for RepeatElem { - #[tracing::instrument(name = "RepeatElem::layout", skip_all)] - fn layout( - &self, - vt: &mut Vt, - styles: StyleChain, - regions: Regions, - ) -> SourceResult<Fragment> { - let pod = Regions::one(regions.size, Axes::new(false, false)); - let piece = self.body().layout(vt, styles, pod)?.into_frame(); - let align = AlignElem::alignment_in(styles).resolve(styles); - - let fill = regions.size.x; - let width = piece.width(); - let count = (fill / width).floor(); - let remaining = fill % width; - let apart = remaining / (count - 1.0); - - let size = Size::new(regions.size.x, piece.height()); - - if !size.is_finite() { - bail!(self.span(), "repeat with no size restrictions"); - } - - let mut frame = Frame::soft(size); - if piece.has_baseline() { - frame.set_baseline(piece.baseline()); - } - - let mut offset = Abs::zero(); - if count == 1.0 { - offset += align.x.position(remaining); - } - - if width > Abs::zero() { - for _ in 0..(count as usize).min(1000) { - frame.push_frame(Point::with_x(offset), piece.clone()); - offset += piece.width() + apart; - } - } - - Ok(Fragment::frame(frame)) - } -} diff --git a/crates/typst-library/src/layout/spacing.rs b/crates/typst-library/src/layout/spacing.rs deleted file mode 100644 index 88b6e2cd..00000000 --- a/crates/typst-library/src/layout/spacing.rs +++ /dev/null @@ -1,262 +0,0 @@ -use std::borrow::Cow; - -use crate::prelude::*; - -/// Inserts horizontal spacing into a paragraph. -/// -/// The spacing can be absolute, relative, or fractional. In the last case, the -/// remaining space on the line is distributed among all fractional spacings -/// according to their relative fractions. -/// -/// # Example -/// ```example -/// First #h(1cm) Second \ -/// First #h(30%) Second \ -/// First #h(2fr) Second #h(1fr) Third -/// ``` -/// -/// # Mathematical Spacing { #math-spacing } -/// In [mathematical formulas]($category/math), you can additionally use these -/// constants to add spacing between elements: `thin`, `med`, `thick`, `quad`, `wide`. -#[elem(title = "Spacing (H)", Behave)] -pub struct HElem { - /// How much spacing to insert. - #[required] - pub amount: Spacing, - - /// If `{true}`, the spacing collapses at the start or end of a paragraph. - /// Moreover, from multiple adjacent weak spacings all but the largest one - /// collapse. - /// - /// Weak spacing in markup also causes all adjacent markup spaces to be - /// removed, regardless of the amount of spacing inserted. To force a space - /// next to weak spacing, you can explicitly write `[#" "]` (for a normal - /// space) or `[~]` (for a non-breaking space). The latter can be useful to - /// create a construct that always attaches to the preceding word with one - /// non-breaking space, independently of whether a markup space existed in - /// front or not. - /// - /// ```example - /// #h(1cm, weak: true) - /// We identified a group of _weak_ - /// specimens that fail to manifest - /// in most cases. However, when - /// #h(8pt, weak: true) supported - /// #h(8pt, weak: true) on both sides, - /// they do show up. - /// - /// Further #h(0pt, weak: true) more, - /// even the smallest of them swallow - /// adjacent markup spaces. - /// ``` - #[default(false)] - pub weak: bool, -} - -impl HElem { - /// Zero-width horizontal weak spacing that eats surrounding spaces. - pub fn hole() -> Self { - Self::new(Abs::zero().into()).with_weak(true) - } -} - -impl Behave for HElem { - fn behaviour(&self) -> Behaviour { - if self.amount().is_fractional() { - Behaviour::Destructive - } else if self.weak(StyleChain::default()) { - Behaviour::Weak(1) - } else { - Behaviour::Invisible - } - } - - fn larger( - &self, - prev: &(Cow<Content>, Behaviour, StyleChain), - styles: StyleChain, - ) -> bool { - let Some(other) = prev.0.to::<Self>() else { return false }; - match (self.amount(), other.amount()) { - (Spacing::Fr(this), Spacing::Fr(other)) => this > other, - (Spacing::Rel(this), Spacing::Rel(other)) => { - this.resolve(styles) > other.resolve(prev.2) - } - _ => false, - } - } -} - -/// Inserts vertical spacing into a flow of blocks. -/// -/// The spacing can be absolute, relative, or fractional. In the last case, -/// the remaining space on the page is distributed among all fractional spacings -/// according to their relative fractions. -/// -/// # Example -/// ```example -/// #grid( -/// rows: 3cm, -/// columns: 6, -/// gutter: 1fr, -/// [A #parbreak() B], -/// [A #v(0pt) B], -/// [A #v(10pt) B], -/// [A #v(0pt, weak: true) B], -/// [A #v(40%, weak: true) B], -/// [A #v(1fr) B], -/// ) -/// ``` -#[elem(title = "Spacing (V)", Behave)] -pub struct VElem { - /// How much spacing to insert. - #[required] - pub amount: Spacing, - - /// If `{true}`, the spacing collapses at the start or end of a flow. - /// Moreover, from multiple adjacent weak spacings all but the largest one - /// collapse. Weak spacings will always collapse adjacent paragraph spacing, - /// even if the paragraph spacing is larger. - /// - /// ```example - /// The following theorem is - /// foundational to the field: - /// #v(4pt, weak: true) - /// $ x^2 + y^2 = r^2 $ - /// #v(4pt, weak: true) - /// The proof is simple: - /// ``` - #[external] - pub weak: bool, - - /// The element's weakness level, see also [`Behaviour`]. - #[internal] - #[parse(args.named("weak")?.map(|v: bool| v as usize))] - pub weakness: usize, -} - -impl VElem { - /// Normal strong spacing. - pub fn strong(amount: Spacing) -> Self { - Self::new(amount).with_weakness(0) - } - - /// User-created weak spacing. - pub fn weak(amount: Spacing) -> Self { - Self::new(amount).with_weakness(1) - } - - /// Weak spacing with list attach weakness. - pub fn list_attach(amount: Spacing) -> Self { - Self::new(amount).with_weakness(2) - } - - /// Weak spacing with BlockElem::ABOVE/BELOW weakness. - pub fn block_around(amount: Spacing) -> Self { - Self::new(amount).with_weakness(3) - } - - /// Weak spacing with BlockElem::SPACING weakness. - pub fn block_spacing(amount: Spacing) -> Self { - Self::new(amount).with_weakness(4) - } -} - -impl Behave for VElem { - fn behaviour(&self) -> Behaviour { - if self.amount().is_fractional() { - Behaviour::Destructive - } else if self.weakness(StyleChain::default()) > 0 { - Behaviour::Weak(self.weakness(StyleChain::default())) - } else { - Behaviour::Invisible - } - } - - fn larger( - &self, - prev: &(Cow<Content>, Behaviour, StyleChain), - styles: StyleChain, - ) -> bool { - let Some(other) = prev.0.to::<Self>() else { return false }; - match (self.amount(), other.amount()) { - (Spacing::Fr(this), Spacing::Fr(other)) => this > other, - (Spacing::Rel(this), Spacing::Rel(other)) => { - this.resolve(styles) > other.resolve(prev.2) - } - _ => false, - } - } -} - -cast! { - VElem, - v: Content => v.to::<Self>().cloned().ok_or("expected `v` element")?, -} - -/// Kinds of spacing. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub enum Spacing { - /// Spacing specified in absolute terms and relative to the parent's size. - Rel(Rel<Length>), - /// Spacing specified as a fraction of the remaining free space in the - /// parent. - Fr(Fr), -} - -impl Spacing { - /// Whether this is fractional spacing. - pub fn is_fractional(self) -> bool { - matches!(self, Self::Fr(_)) - } - - /// Whether the spacing is actually no spacing. - pub fn is_zero(&self) -> bool { - match self { - Self::Rel(rel) => rel.is_zero(), - Self::Fr(fr) => fr.is_zero(), - } - } -} - -impl From<Abs> for Spacing { - fn from(abs: Abs) -> Self { - Self::Rel(abs.into()) - } -} - -impl From<Em> for Spacing { - fn from(em: Em) -> Self { - Self::Rel(Rel::new(Ratio::zero(), em.into())) - } -} - -impl From<Length> for Spacing { - fn from(length: Length) -> Self { - Self::Rel(length.into()) - } -} - -impl From<Fr> for Spacing { - fn from(fr: Fr) -> Self { - Self::Fr(fr) - } -} - -cast! { - Spacing, - self => match self { - Self::Rel(rel) => { - if rel.rel.is_zero() { - rel.abs.into_value() - } else if rel.abs.is_zero() { - rel.rel.into_value() - } else { - rel.into_value() - } - } - Self::Fr(fr) => fr.into_value(), - }, - v: Rel<Length> => Self::Rel(v), - v: Fr => Self::Fr(v), -} diff --git a/crates/typst-library/src/layout/stack.rs b/crates/typst-library/src/layout/stack.rs deleted file mode 100644 index c12d2048..00000000 --- a/crates/typst-library/src/layout/stack.rs +++ /dev/null @@ -1,345 +0,0 @@ -use crate::layout::{AlignElem, Spacing}; -use crate::prelude::*; - -/// Arranges content and spacing horizontally or vertically. -/// -/// The stack places a list of items along an axis, with optional spacing -/// between each item. -/// -/// # Example -/// ```example -/// #stack( -/// dir: ttb, -/// rect(width: 40pt), -/// rect(width: 120pt), -/// rect(width: 90pt), -/// ) -/// ``` -#[elem(Layout)] -pub struct StackElem { - /// The direction along which the items are stacked. Possible values are: - /// - /// - `{ltr}`: Left to right. - /// - `{rtl}`: Right to left. - /// - `{ttb}`: Top to bottom. - /// - `{btt}`: Bottom to top. - /// - /// You can use the `start` and `end` methods to obtain the initial and - /// final points (respectively) of a direction, as `alignment`. You can also - /// use the `axis` method to determine whether a direction is - /// `{"horizontal"}` or `{"vertical"}`. The `inv` method returns a - /// direction's inverse direction. - /// - /// For example, `{ttb.start()}` is `top`, `{ttb.end()}` is `bottom`, - /// `{ttb.axis()}` is `{"vertical"}` and `{ttb.inv()}` is equal to `btt`. - #[default(Dir::TTB)] - pub dir: Dir, - - /// Spacing to insert between items where no explicit spacing was provided. - pub spacing: Option<Spacing>, - - /// The children to stack along the axis. - #[variadic] - pub children: Vec<StackChild>, -} - -impl Layout for StackElem { - #[tracing::instrument(name = "StackElem::layout", skip_all)] - fn layout( - &self, - vt: &mut Vt, - styles: StyleChain, - regions: Regions, - ) -> SourceResult<Fragment> { - let mut layouter = StackLayouter::new(self.dir(styles), regions, styles); - - // Spacing to insert before the next block. - let spacing = self.spacing(styles); - let mut deferred = None; - - for child in self.children() { - match child { - StackChild::Spacing(kind) => { - layouter.layout_spacing(*kind); - deferred = None; - } - StackChild::Block(block) => { - if let Some(kind) = deferred { - layouter.layout_spacing(kind); - } - - layouter.layout_block(vt, block, styles)?; - deferred = spacing; - } - } - } - - Ok(layouter.finish()) - } -} - -/// A child of a stack element. -#[derive(Clone, PartialEq, Hash)] -pub enum StackChild { - /// Spacing between other children. - Spacing(Spacing), - /// Arbitrary block-level content. - Block(Content), -} - -impl Debug for StackChild { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - match self { - Self::Spacing(kind) => kind.fmt(f), - Self::Block(block) => block.fmt(f), - } - } -} - -cast! { - StackChild, - self => match self { - Self::Spacing(spacing) => spacing.into_value(), - Self::Block(content) => content.into_value(), - }, - v: Spacing => Self::Spacing(v), - v: Content => Self::Block(v), -} - -/// Performs stack layout. -struct StackLayouter<'a> { - /// The stacking direction. - dir: Dir, - /// The axis of the stacking direction. - axis: Axis, - /// The regions to layout children into. - regions: Regions<'a>, - /// The inherited styles. - styles: StyleChain<'a>, - /// Whether the stack itself should expand to fill the region. - expand: Axes<bool>, - /// The initial size of the current region before we started subtracting. - initial: Size, - /// The generic size used by the frames for the current region. - used: Gen<Abs>, - /// The sum of fractions in the current region. - fr: Fr, - /// Already layouted items whose exact positions are not yet known due to - /// fractional spacing. - items: Vec<StackItem>, - /// Finished frames for previous regions. - finished: Vec<Frame>, -} - -/// A prepared item in a stack layout. -enum StackItem { - /// Absolute spacing between other items. - Absolute(Abs), - /// Fractional spacing between other items. - Fractional(Fr), - /// A frame for a layouted block. - Frame(Frame, Axes<FixedAlign>), -} - -impl<'a> StackLayouter<'a> { - /// Create a new stack layouter. - fn new(dir: Dir, mut regions: Regions<'a>, styles: StyleChain<'a>) -> Self { - let axis = dir.axis(); - let expand = regions.expand; - - // Disable expansion along the block axis for children. - regions.expand.set(axis, false); - - Self { - dir, - axis, - regions, - styles, - expand, - initial: regions.size, - used: Gen::zero(), - fr: Fr::zero(), - items: vec![], - finished: vec![], - } - } - - /// Add spacing along the spacing direction. - #[tracing::instrument(name = "StackLayouter::layout_spacing", skip_all)] - fn layout_spacing(&mut self, spacing: Spacing) { - match spacing { - Spacing::Rel(v) => { - // Resolve the spacing and limit it to the remaining space. - let resolved = v - .resolve(self.styles) - .relative_to(self.regions.base().get(self.axis)); - let remaining = self.regions.size.get_mut(self.axis); - let limited = resolved.min(*remaining); - if self.dir.axis() == Axis::Y { - *remaining -= limited; - } - self.used.main += limited; - self.items.push(StackItem::Absolute(resolved)); - } - Spacing::Fr(v) => { - self.fr += v; - self.items.push(StackItem::Fractional(v)); - } - } - } - - /// Layout an arbitrary block. - #[tracing::instrument(name = "StackLayouter::layout_block", skip_all)] - fn layout_block( - &mut self, - vt: &mut Vt, - block: &Content, - styles: StyleChain, - ) -> SourceResult<()> { - if self.regions.is_full() { - self.finish_region(); - } - - // Block-axis alignment of the `AlignElement` is respected by stacks. - let align = if let Some(align) = block.to::<AlignElem>() { - align.alignment(styles) - } else if let Some((_, local)) = block.to_styled() { - AlignElem::alignment_in(styles.chain(local)) - } else { - AlignElem::alignment_in(styles) - } - .resolve(styles); - - let fragment = block.layout(vt, styles, self.regions)?; - let len = fragment.len(); - for (i, frame) in fragment.into_iter().enumerate() { - // Grow our size, shrink the region and save the frame for later. - let size = frame.size(); - if self.dir.axis() == Axis::Y { - self.regions.size.y -= size.y; - } - - let gen = match self.axis { - Axis::X => Gen::new(size.y, size.x), - Axis::Y => Gen::new(size.x, size.y), - }; - - self.used.main += gen.main; - self.used.cross.set_max(gen.cross); - - self.items.push(StackItem::Frame(frame, align)); - - if i + 1 < len { - self.finish_region(); - } - } - - Ok(()) - } - - /// Advance to the next region. - fn finish_region(&mut self) { - // Determine the size of the stack in this region depending on whether - // the region expands. - let mut size = self - .expand - .select(self.initial, self.used.into_axes(self.axis)) - .min(self.initial); - - // Expand fully if there are fr spacings. - let full = self.initial.get(self.axis); - let remaining = full - self.used.main; - if self.fr.get() > 0.0 && full.is_finite() { - self.used.main = full; - size.set(self.axis, full); - } - - let mut output = Frame::hard(size); - let mut cursor = Abs::zero(); - let mut ruler: FixedAlign = self.dir.start().into(); - - // Place all frames. - for item in self.items.drain(..) { - match item { - StackItem::Absolute(v) => cursor += v, - StackItem::Fractional(v) => cursor += v.share(self.fr, remaining), - StackItem::Frame(frame, align) => { - if self.dir.is_positive() { - ruler = ruler.max(align.get(self.axis)); - } else { - ruler = ruler.min(align.get(self.axis)); - } - - // Align along the main axis. - let parent = size.get(self.axis); - let child = frame.size().get(self.axis); - let main = ruler.position(parent - self.used.main) - + if self.dir.is_positive() { - cursor - } else { - self.used.main - child - cursor - }; - - // Align along the cross axis. - let other = self.axis.other(); - let cross = align - .get(other) - .position(size.get(other) - frame.size().get(other)); - - let pos = Gen::new(cross, main).to_point(self.axis); - cursor += child; - output.push_frame(pos, frame); - } - } - } - - // Advance to the next region. - self.regions.next(); - self.initial = self.regions.size; - self.used = Gen::zero(); - self.fr = Fr::zero(); - self.finished.push(output); - } - - /// Finish layouting and return the resulting frames. - fn finish(mut self) -> Fragment { - self.finish_region(); - Fragment::frames(self.finished) - } -} - -/// A container with a main and cross component. -#[derive(Default, Copy, Clone, Eq, PartialEq, Hash)] -struct Gen<T> { - /// The main component. - pub cross: T, - /// The cross component. - pub main: T, -} - -impl<T> Gen<T> { - /// Create a new instance from the two components. - const fn new(cross: T, main: T) -> Self { - Self { cross, main } - } - - /// Convert to the specific representation, given the current main axis. - fn into_axes(self, main: Axis) -> Axes<T> { - match main { - Axis::X => Axes::new(self.main, self.cross), - Axis::Y => Axes::new(self.cross, self.main), - } - } -} - -impl Gen<Abs> { - /// The zero value. - fn zero() -> Self { - Self { cross: Abs::zero(), main: Abs::zero() } - } - - /// Convert to a point. - fn to_point(self, main: Axis) -> Point { - self.into_axes(main).to_point() - } -} diff --git a/crates/typst-library/src/layout/table.rs b/crates/typst-library/src/layout/table.rs deleted file mode 100644 index 0eff7a44..00000000 --- a/crates/typst-library/src/layout/table.rs +++ /dev/null @@ -1,361 +0,0 @@ -use typst::eval::{CastInfo, Reflect}; - -use crate::layout::{AlignElem, GridLayouter, TrackSizings}; -use crate::meta::Figurable; -use crate::prelude::*; - -/// A table of items. -/// -/// Tables are used to arrange content in cells. Cells can contain arbitrary -/// content, including multiple paragraphs and are specified in row-major order. -/// Because tables are just grids with configurable cell properties, refer to -/// the [grid documentation]($grid) for more information on how to size the -/// table tracks. -/// -/// To give a table a caption and make it [referenceable]($ref), put it into a -/// [figure]($figure). -/// -/// # Example -/// ```example -/// #table( -/// columns: (1fr, auto, auto), -/// inset: 10pt, -/// align: horizon, -/// [], [*Area*], [*Parameters*], -/// image("cylinder.svg"), -/// $ pi h (D^2 - d^2) / 4 $, -/// [ -/// $h$: height \ -/// $D$: outer radius \ -/// $d$: inner radius -/// ], -/// image("tetrahedron.svg"), -/// $ sqrt(2) / 12 a^3 $, -/// [$a$: edge length] -/// ) -/// ``` -#[elem(Layout, LocalName, Figurable)] -pub struct TableElem { - /// The column sizes. See the [grid documentation]($grid) for more - /// information on track sizing. - #[borrowed] - pub columns: TrackSizings, - - /// The row sizes. See the [grid documentation]($grid) for more information - /// on track sizing. - #[borrowed] - pub rows: TrackSizings, - - /// The gaps between rows & columns. See the [grid documentation]($grid) for - /// more information on gutters. - #[external] - pub gutter: TrackSizings, - - /// The gaps between columns. Takes precedence over `gutter`. See the - /// [grid documentation]($grid) for more information on gutters. - #[borrowed] - #[parse( - let gutter = args.named("gutter")?; - args.named("column-gutter")?.or_else(|| gutter.clone()) - )] - pub column_gutter: TrackSizings, - - /// The gaps between rows. Takes precedence over `gutter`. See the - /// [grid documentation]($grid) for more information on gutters. - #[parse(args.named("row-gutter")?.or_else(|| gutter.clone()))] - #[borrowed] - pub row_gutter: TrackSizings, - - /// How to fill the cells. - /// - /// This can be a color or a function that returns a color. The function is - /// passed the cells' column and row index, starting at zero. This can be - /// used to implement striped tables. - /// - /// ```example - /// #table( - /// fill: (col, _) => if calc.odd(col) { luma(240) } else { white }, - /// align: (col, row) => - /// if row == 0 { center } - /// else if col == 0 { left } - /// else { right }, - /// columns: 4, - /// [], [*Q1*], [*Q2*], [*Q3*], - /// [Revenue:], [1000 €], [2000 €], [3000 €], - /// [Expenses:], [500 €], [1000 €], [1500 €], - /// [Profit:], [500 €], [1000 €], [1500 €], - /// ) - /// ``` - #[borrowed] - pub fill: Celled<Option<Paint>>, - - /// How to align the cells' content. - /// - /// This can either be a single alignment, an array of alignments - /// (corresponding to each column) or a function that returns an alignment. - /// The function is passed the cells' column and row index, starting at zero. - /// If set to `{auto}`, the outer alignment is used. - /// - /// ```example - /// #table( - /// columns: 3, - /// align: (x, y) => (left, center, right).at(x), - /// [Hello], [Hello], [Hello], - /// [A], [B], [C], - /// ) - /// ``` - #[borrowed] - pub align: Celled<Smart<Align>>, - - /// How to [stroke]($stroke) the cells. - /// - /// Strokes can be disabled by setting this to `{none}`. - /// - /// _Note:_ Richer stroke customization for individual cells is not yet - /// implemented, but will be in the future. In the meantime, you can use the - /// third-party [tablex library](https://github.com/PgBiel/typst-tablex/). - #[resolve] - #[fold] - #[default(Some(Stroke::default()))] - pub stroke: Option<Stroke>, - - /// How much to pad the cells' content. - /// - /// ```example - /// #table( - /// inset: 10pt, - /// [Hello], - /// [World], - /// ) - /// - /// #table( - /// columns: 2, - /// inset: ( - /// x: 20pt, - /// y: 10pt, - /// ), - /// [Hello], - /// [World], - /// ) - /// ``` - #[fold] - #[default(Sides::splat(Abs::pt(5.0).into()))] - pub inset: Sides<Option<Rel<Length>>>, - - /// The contents of the table cells. - #[variadic] - pub children: Vec<Content>, -} - -impl Layout for TableElem { - #[tracing::instrument(name = "TableElem::layout", skip_all)] - fn layout( - &self, - vt: &mut Vt, - styles: StyleChain, - regions: Regions, - ) -> SourceResult<Fragment> { - let inset = self.inset(styles); - let align = self.align(styles); - let columns = self.columns(styles); - let rows = self.rows(styles); - let column_gutter = self.column_gutter(styles); - let row_gutter = self.row_gutter(styles); - - let tracks = Axes::new(columns.0.as_slice(), rows.0.as_slice()); - let gutter = Axes::new(column_gutter.0.as_slice(), row_gutter.0.as_slice()); - let cols = tracks.x.len().max(1); - let cells: Vec<_> = self - .children() - .iter() - .enumerate() - .map(|(i, child)| { - let mut child = child.clone().padded(inset); - - let x = i % cols; - let y = i / cols; - if let Smart::Custom(alignment) = align.resolve(vt, x, y)? { - child = child.styled(AlignElem::set_alignment(alignment)); - } - - Ok(child) - }) - .collect::<SourceResult<_>>()?; - - let fill = self.fill(styles); - let stroke = self.stroke(styles).map(Stroke::unwrap_or_default); - - // Prepare grid layout by unifying content and gutter tracks. - let layouter = - GridLayouter::new(tracks, gutter, &cells, regions, styles, self.span()); - - // Measure the columns and layout the grid row-by-row. - let mut layout = layouter.layout(vt)?; - - // Add lines and backgrounds. - for (frame, rows) in layout.fragment.iter_mut().zip(&layout.rows) { - if layout.cols.is_empty() || rows.is_empty() { - continue; - } - - // Render table lines. - if let Some(stroke) = &stroke { - let thickness = stroke.thickness; - let half = thickness / 2.0; - - // Render horizontal lines. - for offset in points(rows.iter().map(|piece| piece.height)) { - let target = Point::with_x(frame.width() + thickness); - let hline = Geometry::Line(target).stroked(stroke.clone()); - frame.prepend( - Point::new(-half, offset), - FrameItem::Shape(hline, self.span()), - ); - } - - // Render vertical lines. - for offset in points(layout.cols.iter().copied()) { - let target = Point::with_y(frame.height() + thickness); - let vline = Geometry::Line(target).stroked(stroke.clone()); - frame.prepend( - Point::new(offset, -half), - FrameItem::Shape(vline, self.span()), - ); - } - } - - // Render cell backgrounds. - let mut dx = Abs::zero(); - for (x, &col) in layout.cols.iter().enumerate() { - let mut dy = Abs::zero(); - for row in rows { - if let Some(fill) = fill.resolve(vt, x, row.y)? { - let pos = Point::new(dx, dy); - let size = Size::new(col, row.height); - let rect = Geometry::Rect(size).filled(fill); - frame.prepend(pos, FrameItem::Shape(rect, self.span())); - } - dy += row.height; - } - dx += col; - } - } - - Ok(layout.fragment) - } -} - -/// Turn an iterator of extents into an iterator of offsets before, in between, -/// and after the extents, e.g. [10mm, 5mm] -> [0mm, 10mm, 15mm]. -fn points(extents: impl IntoIterator<Item = Abs>) -> impl Iterator<Item = Abs> { - let mut offset = Abs::zero(); - std::iter::once(Abs::zero()).chain(extents).map(move |extent| { - offset += extent; - offset - }) -} - -/// A value that can be configured per cell. -#[derive(Debug, Clone, PartialEq, Hash)] -pub enum Celled<T> { - /// A bare value, the same for all cells. - Value(T), - /// A closure mapping from cell coordinates to a value. - Func(Func), - /// An array of alignment values corresponding to each column. - Array(Vec<T>), -} - -impl<T: Default + Clone + FromValue> Celled<T> { - /// Resolve the value based on the cell position. - pub fn resolve(&self, vt: &mut Vt, x: usize, y: usize) -> SourceResult<T> { - Ok(match self { - Self::Value(value) => value.clone(), - Self::Func(func) => func.call_vt(vt, [x, y])?.cast().at(func.span())?, - Self::Array(array) => x - .checked_rem(array.len()) - .and_then(|i| array.get(i)) - .cloned() - .unwrap_or_default(), - }) - } -} - -impl<T: Default> Default for Celled<T> { - fn default() -> Self { - Self::Value(T::default()) - } -} - -impl<T: Reflect> Reflect for Celled<T> { - fn input() -> CastInfo { - T::input() + Array::input() + Func::input() - } - - fn output() -> CastInfo { - T::output() + Array::output() + Func::output() - } - - fn castable(value: &Value) -> bool { - Array::castable(value) || Func::castable(value) || T::castable(value) - } -} - -impl<T: IntoValue> IntoValue for Celled<T> { - fn into_value(self) -> Value { - match self { - Self::Value(value) => value.into_value(), - Self::Func(func) => func.into_value(), - Self::Array(arr) => arr.into_value(), - } - } -} - -impl<T: FromValue> FromValue for Celled<T> { - fn from_value(value: Value) -> StrResult<Self> { - match value { - Value::Func(v) => Ok(Self::Func(v)), - Value::Array(array) => Ok(Self::Array( - array.into_iter().map(T::from_value).collect::<StrResult<_>>()?, - )), - v if T::castable(&v) => Ok(Self::Value(T::from_value(v)?)), - v => Err(Self::error(&v)), - } - } -} - -impl LocalName for TableElem { - fn local_name(lang: Lang, _: Option<Region>) -> &'static str { - match lang { - Lang::ALBANIAN => "Tabel", - Lang::ARABIC => "جدول", - Lang::BOKMÅL => "Tabell", - Lang::CHINESE => "表", - Lang::CZECH => "Tabulka", - Lang::DANISH => "Tabel", - Lang::DUTCH => "Tabel", - Lang::FILIPINO => "Talaan", - Lang::FINNISH => "Taulukko", - Lang::FRENCH => "Tableau", - Lang::GERMAN => "Tabelle", - Lang::GREEK => "Πίνακας", - Lang::HUNGARIAN => "Táblázat", - Lang::ITALIAN => "Tabella", - Lang::NYNORSK => "Tabell", - Lang::POLISH => "Tabela", - Lang::PORTUGUESE => "Tabela", - Lang::ROMANIAN => "Tabelul", - Lang::RUSSIAN => "Таблица", - Lang::SLOVENIAN => "Tabela", - Lang::SPANISH => "Tabla", - Lang::SWEDISH => "Tabell", - Lang::TURKISH => "Tablo", - Lang::UKRAINIAN => "Таблиця", - Lang::VIETNAMESE => "Bảng", - Lang::JAPANESE => "表", - Lang::ENGLISH | _ => "Table", - } - } -} - -impl Figurable for TableElem {} diff --git a/crates/typst-library/src/layout/terms.rs b/crates/typst-library/src/layout/terms.rs deleted file mode 100644 index d373768d..00000000 --- a/crates/typst-library/src/layout/terms.rs +++ /dev/null @@ -1,162 +0,0 @@ -use crate::layout::{BlockElem, HElem, ParElem, Spacing, VElem}; -use crate::prelude::*; - -/// A list of terms and their descriptions. -/// -/// Displays a sequence of terms and their descriptions vertically. When the -/// descriptions span over multiple lines, they use hanging indent to -/// communicate the visual hierarchy. -/// -/// # Example -/// ```example -/// / Ligature: A merged glyph. -/// / Kerning: A spacing adjustment -/// between two adjacent letters. -/// ``` -/// -/// # Syntax -/// This function also has dedicated syntax: Starting a line with a slash, -/// followed by a term, a colon and a description creates a term list item. -#[elem(scope, title = "Term List", Layout)] -pub struct TermsElem { - /// If this is `{false}`, the items are spaced apart with - /// [term list spacing]($terms.spacing). If it is `{true}`, they use normal - /// [leading]($par.leading) instead. This makes the term list more compact, - /// which can look better if the items are short. - /// - /// In markup mode, the value of this parameter is determined based on - /// whether items are separated with a blank line. If items directly follow - /// each other, this is set to `{true}`; if items are separated by a blank - /// line, this is set to `{false}`. - /// - /// ```example - /// / Fact: If a term list has a lot - /// of text, and maybe other inline - /// content, it should not be tight - /// anymore. - /// - /// / Tip: To make it wide, simply - /// insert a blank line between the - /// items. - /// ``` - #[default(true)] - pub tight: bool, - - /// The separator between the item and the description. - /// - /// If you want to just separate them with a certain amount of space, use - /// `{h(2cm, weak: true)}` as the separator and replace `{2cm}` with your - /// desired amount of space. - /// - /// ```example - /// #set terms(separator: [: ]) - /// - /// / Colon: A nice separator symbol. - /// ``` - #[default(HElem::new(Em::new(0.6).into()).with_weak(true).pack())] - #[borrowed] - pub separator: Content, - - /// The indentation of each item. - pub indent: Length, - - /// The hanging indent of the description. - /// - /// This is in addition to the whole item's `indent`. - /// - /// ```example - /// #set terms(hanging-indent: 0pt) - /// / Term: This term list does not - /// make use of hanging indents. - /// ``` - #[default(Em::new(2.0).into())] - pub hanging_indent: Length, - - /// The spacing between the items of a wide (non-tight) term list. - /// - /// If set to `{auto}`, uses the spacing [below blocks]($block.below). - pub spacing: Smart<Spacing>, - - /// The term list's children. - /// - /// When using the term list syntax, adjacent items are automatically - /// collected into term lists, even through constructs like for loops. - /// - /// ```example - /// #for (year, product) in ( - /// "1978": "TeX", - /// "1984": "LaTeX", - /// "2019": "Typst", - /// ) [/ #product: Born in #year.] - /// ``` - #[variadic] - pub children: Vec<TermItem>, -} - -#[scope] -impl TermsElem { - #[elem] - type TermItem; -} - -impl Layout for TermsElem { - #[tracing::instrument(name = "TermsElem::layout", skip_all)] - fn layout( - &self, - vt: &mut Vt, - styles: StyleChain, - regions: Regions, - ) -> SourceResult<Fragment> { - let separator = self.separator(styles); - let indent = self.indent(styles); - let hanging_indent = self.hanging_indent(styles); - let gutter = if self.tight(styles) { - ParElem::leading_in(styles).into() - } else { - self.spacing(styles) - .unwrap_or_else(|| *BlockElem::below_in(styles).amount()) - }; - - let mut seq = vec![]; - for (i, child) in self.children().iter().enumerate() { - if i > 0 { - seq.push(VElem::new(gutter).with_weakness(1).pack()); - } - if !indent.is_zero() { - seq.push(HElem::new(indent.into()).pack()); - } - seq.push(child.term().clone().strong()); - seq.push((*separator).clone()); - seq.push(child.description().clone()); - } - - Content::sequence(seq) - .styled(ParElem::set_hanging_indent(hanging_indent + indent)) - .layout(vt, styles, regions) - } -} - -/// A term list item. -#[elem(name = "item", title = "Term List Item")] -pub struct TermItem { - /// The term described by the list item. - #[required] - pub term: Content, - - /// The description of the term. - #[required] - pub description: Content, -} - -cast! { - TermItem, - array: Array => { - let mut iter = array.into_iter(); - let (term, description) = match (iter.next(), iter.next(), iter.next()) { - (Some(a), Some(b), None) => (a.cast()?, b.cast()?), - _ => bail!("array must contain exactly two entries"), - }; - Self::new(term, description) - }, - v: Content => v.to::<Self>().cloned().ok_or("expected term item or array")?, -} diff --git a/crates/typst-library/src/layout/transform.rs b/crates/typst-library/src/layout/transform.rs deleted file mode 100644 index 012a146d..00000000 --- a/crates/typst-library/src/layout/transform.rs +++ /dev/null @@ -1,187 +0,0 @@ -use typst::geom::Transform; - -use crate::prelude::*; - -/// Moves content without affecting layout. -/// -/// The `move` function allows you to move content while the layout still 'sees' -/// it at the original positions. Containers will still be sized as if the -/// content was not moved. -/// -/// # Example -/// ```example -/// #rect(inset: 0pt, move( -/// dx: 6pt, dy: 6pt, -/// rect( -/// inset: 8pt, -/// fill: white, -/// stroke: black, -/// [Abra cadabra] -/// ) -/// )) -/// ``` -#[elem(Layout)] -pub struct MoveElem { - /// The horizontal displacement of the content. - pub dx: Rel<Length>, - - /// The vertical displacement of the content. - pub dy: Rel<Length>, - - /// The content to move. - #[required] - pub body: Content, -} - -impl Layout for MoveElem { - #[tracing::instrument(name = "MoveElem::layout", skip_all)] - fn layout( - &self, - vt: &mut Vt, - styles: StyleChain, - regions: Regions, - ) -> SourceResult<Fragment> { - let pod = Regions::one(regions.base(), Axes::splat(false)); - let mut frame = self.body().layout(vt, styles, pod)?.into_frame(); - let delta = Axes::new(self.dx(styles), self.dy(styles)).resolve(styles); - let delta = delta.zip_map(regions.base(), Rel::relative_to); - frame.translate(delta.to_point()); - Ok(Fragment::frame(frame)) - } -} - -/// Rotates content without affecting layout. -/// -/// Rotates an element by a given angle. The layout will act as if the element -/// was not rotated. -/// -/// # Example -/// ```example -/// #stack( -/// dir: ltr, -/// spacing: 1fr, -/// ..range(16) -/// .map(i => rotate(24deg * i)[X]), -/// ) -/// ``` -#[elem(Layout)] -pub struct RotateElem { - /// The amount of rotation. - /// - /// ```example - /// #rotate(-1.571rad)[Space!] - /// ``` - /// - #[positional] - pub angle: Angle, - - /// The origin of the rotation. - /// - /// If, for instance, you wanted the bottom left corner of the rotated - /// element to stay aligned with the baseline, you would set it to `bottom + - /// left` instead. - /// - /// ```example - /// #set text(spacing: 8pt) - /// #let square = square.with(width: 8pt) - /// - /// #box(square()) - /// #box(rotate(30deg, origin: center, square())) - /// #box(rotate(30deg, origin: top + left, square())) - /// #box(rotate(30deg, origin: bottom + right, square())) - /// ``` - #[fold] - #[default(HAlign::Center + VAlign::Horizon)] - pub origin: Align, - - /// The content to rotate. - #[required] - pub body: Content, -} - -impl Layout for RotateElem { - #[tracing::instrument(name = "RotateElem::layout", skip_all)] - fn layout( - &self, - vt: &mut Vt, - styles: StyleChain, - regions: Regions, - ) -> SourceResult<Fragment> { - let pod = Regions::one(regions.base(), Axes::splat(false)); - let mut frame = self.body().layout(vt, styles, pod)?.into_frame(); - let Axes { x, y } = self - .origin(styles) - .resolve(styles) - .zip_map(frame.size(), FixedAlign::position); - let ts = Transform::translate(x, y) - .pre_concat(Transform::rotate(self.angle(styles))) - .pre_concat(Transform::translate(-x, -y)); - frame.transform(ts); - Ok(Fragment::frame(frame)) - } -} - -/// Scales content without affecting layout. -/// -/// Lets you mirror content by specifying a negative scale on a single axis. -/// -/// # Example -/// ```example -/// #set align(center) -/// #scale(x: -100%)[This is mirrored.] -/// ``` -#[elem(Layout)] -pub struct ScaleElem { - /// The horizontal scaling factor. - /// - /// The body will be mirrored horizontally if the parameter is negative. - #[parse( - let all = args.find()?; - args.named("x")?.or(all) - )] - #[default(Ratio::one())] - pub x: Ratio, - - /// The vertical scaling factor. - /// - /// The body will be mirrored vertically if the parameter is negative. - #[parse(args.named("y")?.or(all))] - #[default(Ratio::one())] - pub y: Ratio, - - /// The origin of the transformation. - /// - /// ```example - /// A#box(scale(75%)[A])A \ - /// B#box(scale(75%, origin: bottom + left)[B])B - /// ``` - #[fold] - #[default(HAlign::Center + VAlign::Horizon)] - pub origin: Align, - - /// The content to scale. - #[required] - pub body: Content, -} - -impl Layout for ScaleElem { - #[tracing::instrument(name = "ScaleElem::layout", skip_all)] - fn layout( - &self, - vt: &mut Vt, - styles: StyleChain, - regions: Regions, - ) -> SourceResult<Fragment> { - let pod = Regions::one(regions.base(), Axes::splat(false)); - let mut frame = self.body().layout(vt, styles, pod)?.into_frame(); - let Axes { x, y } = self - .origin(styles) - .resolve(styles) - .zip_map(frame.size(), FixedAlign::position); - let transform = Transform::translate(x, y) - .pre_concat(Transform::scale(self.x(styles), self.y(styles))) - .pre_concat(Transform::translate(-x, -y)); - frame.transform(transform); - Ok(Fragment::frame(frame)) - } -} diff --git a/crates/typst-library/src/lib.rs b/crates/typst-library/src/lib.rs deleted file mode 100644 index 212debb0..00000000 --- a/crates/typst-library/src/lib.rs +++ /dev/null @@ -1,170 +0,0 @@ -//! Typst's standard library. - -#![allow(clippy::wildcard_in_or_patterns)] -#![allow(clippy::manual_range_contains)] -#![allow(clippy::comparison_chain)] - -pub mod compute; -pub mod layout; -pub mod math; -pub mod meta; -pub mod prelude; -pub mod shared; -pub mod symbols; -pub mod text; -pub mod visualize; - -use typst::eval::{Array, LangItems, Library, Module, Scope, Smart}; -use typst::geom::{Align, Color, Dir}; -use typst::model::{NativeElement, Styles}; - -use self::layout::LayoutRoot; - -/// Construct the standard library. -pub fn build() -> Library { - let math = math::module(); - let global = global(math.clone()); - Library { global, math, styles: styles(), items: items() } -} - -/// Construct the module with global definitions. -#[tracing::instrument(skip_all)] -fn global(math: Module) -> Module { - let mut global = Scope::deduplicating(); - text::define(&mut global); - global.define_module(math); - layout::define(&mut global); - visualize::define(&mut global); - meta::define(&mut global); - symbols::define(&mut global); - compute::define(&mut global); - prelude(&mut global); - Module::new("global", global) -} - -/// Defines scoped values that are globally available, too. -fn prelude(global: &mut Scope) { - global.reset_category(); - global.define("black", Color::BLACK); - global.define("gray", Color::GRAY); - global.define("silver", Color::SILVER); - global.define("white", Color::WHITE); - global.define("navy", Color::NAVY); - global.define("blue", Color::BLUE); - global.define("aqua", Color::AQUA); - global.define("teal", Color::TEAL); - global.define("eastern", Color::EASTERN); - global.define("purple", Color::PURPLE); - global.define("fuchsia", Color::FUCHSIA); - global.define("maroon", Color::MAROON); - global.define("red", Color::RED); - global.define("orange", Color::ORANGE); - global.define("yellow", Color::YELLOW); - global.define("olive", Color::OLIVE); - global.define("green", Color::GREEN); - global.define("lime", Color::LIME); - global.define("luma", Color::luma_data()); - global.define("oklab", Color::oklab_data()); - global.define("oklch", Color::oklch_data()); - global.define("rgb", Color::rgb_data()); - global.define("cmyk", Color::cmyk_data()); - global.define("range", Array::range_data()); - global.define("ltr", Dir::LTR); - global.define("rtl", Dir::RTL); - global.define("ttb", Dir::TTB); - global.define("btt", Dir::BTT); - global.define("start", Align::START); - global.define("left", Align::LEFT); - global.define("center", Align::CENTER); - global.define("right", Align::RIGHT); - global.define("end", Align::END); - global.define("top", Align::TOP); - global.define("horizon", Align::HORIZON); - global.define("bottom", Align::BOTTOM); -} - -/// Construct the standard style map. -fn styles() -> Styles { - Styles::new() -} - -/// Construct the standard lang item mapping. -fn items() -> LangItems { - LangItems { - layout: |world, content, styles| content.layout_root(world, styles), - em: text::TextElem::size_in, - dir: text::TextElem::dir_in, - space: || text::SpaceElem::new().pack(), - linebreak: || text::LinebreakElem::new().pack(), - text: |text| text::TextElem::new(text).pack(), - text_elem: text::TextElem::elem(), - text_str: |content| Some(content.to::<text::TextElem>()?.text()), - smart_quote: |double| text::SmartquoteElem::new().with_double(double).pack(), - parbreak: || layout::ParbreakElem::new().pack(), - strong: |body| text::StrongElem::new(body).pack(), - emph: |body| text::EmphElem::new(body).pack(), - raw: |text, lang, block| { - let mut elem = text::RawElem::new(text).with_block(block); - if let Some(lang) = lang { - elem.push_lang(Some(lang)); - } - elem.pack() - }, - raw_languages: text::RawElem::languages, - link: |url| meta::LinkElem::from_url(url).pack(), - reference: |target, supplement| { - let mut elem = meta::RefElem::new(target); - if let Some(supplement) = supplement { - elem.push_supplement(Smart::Custom(Some(meta::Supplement::Content( - supplement, - )))); - } - elem.pack() - }, - bibliography_keys: meta::BibliographyElem::keys, - heading: |level, title| meta::HeadingElem::new(title).with_level(level).pack(), - heading_elem: meta::HeadingElem::elem(), - list_item: |body| layout::ListItem::new(body).pack(), - enum_item: |number, body| { - let mut elem = layout::EnumItem::new(body); - if let Some(number) = number { - elem.push_number(Some(number)); - } - elem.pack() - }, - term_item: |term, description| layout::TermItem::new(term, description).pack(), - equation: |body, block| math::EquationElem::new(body).with_block(block).pack(), - math_align_point: || math::AlignPointElem::new().pack(), - math_delimited: |open, body, close| math::LrElem::new(open + body + close).pack(), - math_attach: |base, t, b, tl, bl, tr, br| { - let mut elem = math::AttachElem::new(base); - if let Some(t) = t { - elem.push_t(Some(t)); - } - if let Some(b) = b { - elem.push_b(Some(b)); - } - if let Some(tl) = tl { - elem.push_tl(Some(tl)); - } - if let Some(bl) = bl { - elem.push_bl(Some(bl)); - } - if let Some(tr) = tr { - elem.push_tr(Some(tr)); - } - if let Some(br) = br { - elem.push_br(Some(br)); - } - elem.pack() - }, - math_primes: |count| math::PrimesElem::new(count).pack(), - math_accent: |base, accent| { - math::AccentElem::new(base, math::Accent::new(accent)).pack() - }, - math_frac: |num, denom| math::FracElem::new(num, denom).pack(), - math_root: |index, radicand| { - math::RootElem::new(radicand).with_index(index).pack() - }, - } -} diff --git a/crates/typst-library/src/math/accent.rs b/crates/typst-library/src/math/accent.rs deleted file mode 100644 index 1b2d4793..00000000 --- a/crates/typst-library/src/math/accent.rs +++ /dev/null @@ -1,137 +0,0 @@ -use crate::math::*; - -/// How much the accent can be shorter than the base. -const ACCENT_SHORT_FALL: Em = Em::new(0.5); - -/// Attaches an accent to a base. -/// -/// # Example -/// ```example -/// $grave(a) = accent(a, `)$ \ -/// $arrow(a) = accent(a, arrow)$ \ -/// $tilde(a) = accent(a, \u{0303})$ -/// ``` -#[elem(LayoutMath)] -pub struct AccentElem { - /// The base to which the accent is applied. - /// May consist of multiple letters. - /// - /// ```example - /// $arrow(A B C)$ - /// ``` - #[required] - pub base: Content, - - /// The accent to apply to the base. - /// - /// Supported accents include: - /// - /// | Accent | Name | Codepoint | - /// | ------------- | --------------- | --------- | - /// | Grave | `grave` | <code>`</code> | - /// | Acute | `acute` | `´` | - /// | Circumflex | `hat` | `^` | - /// | Tilde | `tilde` | `~` | - /// | Macron | `macron` | `¯` | - /// | Breve | `breve` | `˘` | - /// | Dot | `dot` | `.` | - /// | Double dot | `dot.double` | `¨` | - /// | Triple dot | `dot.triple` | <code>⃛</code> | - /// | Quadruple dot | `dot.quad` | <code>⃜</code> | - /// | Diaeresis | `diaer` | `¨` | - /// | Circle | `circle` | `∘` | - /// | Double acute | `acute.double` | `˝` | - /// | Caron | `caron` | `ˇ` | - /// | Right arrow | `arrow`, `->` | `→` | - /// | Left arrow | `arrow.l`, `<-` | `←` | - #[required] - pub accent: Accent, -} - -impl LayoutMath for AccentElem { - #[tracing::instrument(skip(ctx))] - fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { - ctx.style(ctx.style.with_cramped(true)); - let base = ctx.layout_fragment(self.base())?; - ctx.unstyle(); - - // Preserve class to preserve automatic spacing. - let base_class = base.class().unwrap_or(MathClass::Normal); - let base_attach = match &base { - MathFragment::Glyph(base) => { - attachment(ctx, base.id, base.italics_correction) - } - _ => (base.width() + base.italics_correction()) / 2.0, - }; - - // Forcing the accent to be at least as large as the base makes it too - // wide in many case. - let Accent(c) = self.accent(); - let glyph = GlyphFragment::new(ctx, *c, self.span()); - let short_fall = ACCENT_SHORT_FALL.scaled(ctx); - let variant = glyph.stretch_horizontal(ctx, base.width(), short_fall); - let accent = variant.frame; - let accent_attach = match variant.id { - Some(id) => attachment(ctx, id, variant.italics_correction), - None => accent.width() / 2.0, - }; - - // Descent is negative because the accent's ink bottom is above the - // baseline. Therefore, the default gap is the accent's negated descent - // minus the accent base height. Only if the base is very small, we need - // a larger gap so that the accent doesn't move too low. - let accent_base_height = scaled!(ctx, accent_base_height); - let gap = -accent.descent() - base.height().min(accent_base_height); - let size = Size::new(base.width(), accent.height() + gap + base.height()); - let accent_pos = Point::with_x(base_attach - accent_attach); - let base_pos = Point::with_y(accent.height() + gap); - let base_ascent = base.ascent(); - let baseline = base_pos.y + base.ascent(); - - let mut frame = Frame::soft(size); - frame.set_baseline(baseline); - frame.push_frame(accent_pos, accent); - frame.push_frame(base_pos, base.into_frame()); - ctx.push( - FrameFragment::new(ctx, frame) - .with_class(base_class) - .with_base_ascent(base_ascent), - ); - - Ok(()) - } -} - -/// The horizontal attachment position for the given glyph. -fn attachment(ctx: &MathContext, id: GlyphId, italics_correction: Abs) -> Abs { - ctx.table - .glyph_info - .and_then(|info| info.top_accent_attachments) - .and_then(|attachments| attachments.get(id)) - .map(|record| record.value.scaled(ctx)) - .unwrap_or_else(|| { - let advance = ctx.ttf.glyph_hor_advance(id).unwrap_or_default(); - (advance.scaled(ctx) + italics_correction) / 2.0 - }) -} - -/// An accent character. -#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash)] -pub struct Accent(char); - -impl Accent { - /// Normalize a character into an accent. - pub fn new(c: char) -> Self { - Self(Symbol::combining_accent(c).unwrap_or(c)) - } -} - -cast! { - Accent, - self => self.0.into_value(), - v: char => Self::new(v), - v: Content => match v.to::<TextElem>() { - Some(elem) => Value::Str(elem.text().clone().into()).cast()?, - None => bail!("expected text"), - }, -} diff --git a/crates/typst-library/src/math/align.rs b/crates/typst-library/src/math/align.rs deleted file mode 100644 index 4192e97b..00000000 --- a/crates/typst-library/src/math/align.rs +++ /dev/null @@ -1,60 +0,0 @@ -use crate::math::*; - -/// A math alignment point: `&`, `&&`. -#[elem(title = "Alignment Point", LayoutMath)] -pub struct AlignPointElem {} - -impl LayoutMath for AlignPointElem { - #[tracing::instrument(skip(ctx))] - fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { - ctx.push(MathFragment::Align); - Ok(()) - } -} - -pub(super) struct AlignmentResult { - pub points: Vec<Abs>, - pub width: Abs, -} - -/// Determine the position of the alignment points. -pub(super) fn alignments(rows: &[MathRow]) -> AlignmentResult { - let mut widths = Vec::<Abs>::new(); - - let mut pending_width = Abs::zero(); - for row in rows { - let mut width = Abs::zero(); - let mut alignment_index = 0; - - for fragment in row.iter() { - if matches!(fragment, MathFragment::Align) { - if alignment_index < widths.len() { - widths[alignment_index].set_max(width); - } else { - widths.push(width.max(pending_width)); - } - width = Abs::zero(); - alignment_index += 1; - } else { - width += fragment.width(); - } - } - if widths.is_empty() { - pending_width.set_max(width); - } else if alignment_index < widths.len() { - widths[alignment_index].set_max(width); - } else { - widths.push(width.max(pending_width)); - } - } - - let mut points = widths; - for i in 1..points.len() { - let prev = points[i - 1]; - points[i] += prev; - } - AlignmentResult { - width: points.last().copied().unwrap_or(pending_width), - points, - } -} diff --git a/crates/typst-library/src/math/attach.rs b/crates/typst-library/src/math/attach.rs deleted file mode 100644 index 3e6b69f2..00000000 --- a/crates/typst-library/src/math/attach.rs +++ /dev/null @@ -1,440 +0,0 @@ -use super::*; - -/// A base with optional attachments. -/// -/// ```example -/// $ attach( -/// Pi, t: alpha, b: beta, -/// tl: 1, tr: 2+3, bl: 4+5, br: 6, -/// ) $ -/// ``` -#[elem(LayoutMath)] -pub struct AttachElem { - /// The base to which things are attached. - #[required] - pub base: Content, - - /// The top attachment, smartly positioned at top-right or above the base. - /// - /// You can wrap the base in `{limits()}` or `{scripts()}` to override the - /// smart positioning. - pub t: Option<Content>, - - /// The bottom attachment, smartly positioned at the bottom-right or below - /// the base. - /// - /// You can wrap the base in `{limits()}` or `{scripts()}` to override the - /// smart positioning. - pub b: Option<Content>, - - /// The top-left attachment (before the base). - pub tl: Option<Content>, - - /// The bottom-left attachment (before base). - pub bl: Option<Content>, - - /// The top-right attachment (after the base). - pub tr: Option<Content>, - - /// The bottom-right attachment (after the base). - pub br: Option<Content>, -} - -impl LayoutMath for AttachElem { - #[tracing::instrument(skip(ctx))] - fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { - type GetAttachment = fn(&AttachElem, styles: StyleChain) -> Option<Content>; - let layout_attachment = |ctx: &mut MathContext, getter: GetAttachment| { - getter(self, ctx.styles()) - .map(|elem| ctx.layout_fragment(&elem)) - .transpose() - }; - - let base = ctx.layout_fragment(self.base())?; - - ctx.style(ctx.style.for_superscript()); - let tl = layout_attachment(ctx, Self::tl)?; - let tr = layout_attachment(ctx, Self::tr)?; - let t = layout_attachment(ctx, Self::t)?; - ctx.unstyle(); - - ctx.style(ctx.style.for_subscript()); - let bl = layout_attachment(ctx, Self::bl)?; - let br = layout_attachment(ctx, Self::br)?; - let b = layout_attachment(ctx, Self::b)?; - ctx.unstyle(); - - let limits = base.limits().active(ctx); - let (t, tr) = if limits || tr.is_some() { (t, tr) } else { (None, t) }; - let (b, br) = if limits || br.is_some() { (b, br) } else { (None, b) }; - layout_attachments(ctx, base, [tl, t, tr, bl, b, br]) - } -} - -/// Grouped primes. -/// -/// ```example -/// $ a'''_b = a^'''_b $ -/// ``` -/// -/// # Syntax -/// This function has dedicated syntax: use apostrophes instead of primes. They -/// will automatically attach to the previous element, moving superscripts to -/// the next level. -#[elem(LayoutMath)] -pub struct PrimesElem { - /// The number of grouped primes. - #[required] - pub count: usize, -} - -impl LayoutMath for PrimesElem { - #[tracing::instrument(skip(ctx))] - fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { - match *self.count() { - count @ 1..=4 => { - let f = ctx.layout_fragment(&TextElem::packed(match count { - 1 => '′', - 2 => '″', - 3 => '‴', - 4 => '⁗', - _ => unreachable!(), - }))?; - ctx.push(f); - } - count => { - // Custom amount of primes - let prime = ctx.layout_fragment(&TextElem::packed('′'))?.into_frame(); - let width = prime.width() * (count + 1) as f64 / 2.0; - let mut frame = Frame::soft(Size::new(width, prime.height())); - frame.set_baseline(prime.ascent()); - - for i in 0..count { - frame.push_frame( - Point::new(prime.width() * (i as f64 / 2.0), Abs::zero()), - prime.clone(), - ) - } - ctx.push(FrameFragment::new(ctx, frame)); - } - } - Ok(()) - } -} - -/// Forces a base to display attachments as scripts. -/// -/// ```example -/// $ scripts(sum)_1^2 != sum_1^2 $ -/// ``` -#[elem(LayoutMath)] -pub struct ScriptsElem { - /// The base to attach the scripts to. - #[required] - pub body: Content, -} - -impl LayoutMath for ScriptsElem { - #[tracing::instrument(skip(ctx))] - fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { - let mut fragment = ctx.layout_fragment(self.body())?; - fragment.set_limits(Limits::Never); - ctx.push(fragment); - Ok(()) - } -} - -/// Forces a base to display attachments as limits. -/// -/// ```example -/// $ limits(A)_1^2 != A_1^2 $ -/// ``` -#[elem(LayoutMath)] -pub struct LimitsElem { - /// The base to attach the limits to. - #[required] - pub body: Content, - - /// Whether to also force limits in inline equations. - /// - /// When applying limits globally (e.g., through a show rule), it is - /// typically a good idea to disable this. - #[default(true)] - pub inline: bool, -} - -impl LayoutMath for LimitsElem { - #[tracing::instrument(skip(ctx))] - fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { - let mut fragment = ctx.layout_fragment(self.body())?; - fragment.set_limits(if self.inline(ctx.styles()) { - Limits::Always - } else { - Limits::Display - }); - ctx.push(fragment); - Ok(()) - } -} - -/// Describes in which situation a frame should use limits for attachments. -#[derive(Debug, Copy, Clone)] -pub enum Limits { - /// Always scripts. - Never, - /// Display limits only in `display` math. - Display, - /// Always limits. - Always, -} - -impl Limits { - /// The default limit configuration if the given character is the base. - pub fn for_char(c: char) -> Self { - match unicode_math_class::class(c) { - Some(MathClass::Large) => { - if is_integral_char(c) { - Limits::Never - } else { - Limits::Display - } - } - Some(MathClass::Relation) => Limits::Always, - _ => Limits::Never, - } - } - - /// Whether limits should be displayed in this context - pub fn active(&self, ctx: &MathContext) -> bool { - match self { - Self::Always => true, - Self::Display => ctx.style.size == MathSize::Display, - Self::Never => false, - } - } -} - -macro_rules! measure { - ($e: ident, $attr: ident) => { - $e.as_ref().map(|e| e.$attr()).unwrap_or_default() - }; -} - -/// Layout the attachments. -fn layout_attachments( - ctx: &mut MathContext, - base: MathFragment, - [tl, t, tr, bl, b, br]: [Option<MathFragment>; 6], -) -> SourceResult<()> { - let (shift_up, shift_down) = - compute_shifts_up_and_down(ctx, &base, [&tl, &tr, &bl, &br]); - - let sup_delta = Abs::zero(); - let sub_delta = -base.italics_correction(); - let (base_width, base_ascent, base_descent) = - (base.width(), base.ascent(), base.descent()); - let base_class = base.class().unwrap_or(MathClass::Normal); - - let ascent = base_ascent - .max(shift_up + measure!(tr, ascent)) - .max(shift_up + measure!(tl, ascent)) - .max(shift_up + measure!(t, height)); - - let descent = base_descent - .max(shift_down + measure!(br, descent)) - .max(shift_down + measure!(bl, descent)) - .max(shift_down + measure!(b, height)); - - let pre_sup_width = measure!(tl, width); - let pre_sub_width = measure!(bl, width); - let pre_width_dif = pre_sup_width - pre_sub_width; // Could be negative. - let pre_width_max = pre_sup_width.max(pre_sub_width); - let post_max_width = - (sup_delta + measure!(tr, width)).max(sub_delta + measure!(br, width)); - - let (center_frame, base_offset) = attach_top_and_bottom(ctx, base, t, b); - let base_pos = - Point::new(sup_delta + pre_width_max, ascent - base_ascent - base_offset); - if [&tl, &bl, &tr, &br].iter().all(|&e| e.is_none()) { - ctx.push(FrameFragment::new(ctx, center_frame).with_class(base_class)); - return Ok(()); - } - - let mut frame = Frame::soft(Size::new( - pre_width_max + base_width + post_max_width + scaled!(ctx, space_after_script), - ascent + descent, - )); - frame.set_baseline(ascent); - frame.push_frame(base_pos, center_frame); - - if let Some(tl) = tl { - let pos = - Point::new(-pre_width_dif.min(Abs::zero()), ascent - shift_up - tl.ascent()); - frame.push_frame(pos, tl.into_frame()); - } - - if let Some(bl) = bl { - let pos = - Point::new(pre_width_dif.max(Abs::zero()), ascent + shift_down - bl.ascent()); - frame.push_frame(pos, bl.into_frame()); - } - - if let Some(tr) = tr { - let pos = Point::new( - sup_delta + pre_width_max + base_width, - ascent - shift_up - tr.ascent(), - ); - frame.push_frame(pos, tr.into_frame()); - } - - if let Some(br) = br { - let pos = Point::new( - sub_delta + pre_width_max + base_width, - ascent + shift_down - br.ascent(), - ); - frame.push_frame(pos, br.into_frame()); - } - - ctx.push(FrameFragment::new(ctx, frame).with_class(base_class)); - - Ok(()) -} - -fn attach_top_and_bottom( - ctx: &mut MathContext, - base: MathFragment, - t: Option<MathFragment>, - b: Option<MathFragment>, -) -> (Frame, Abs) { - let upper_gap_min = scaled!(ctx, upper_limit_gap_min); - let upper_rise_min = scaled!(ctx, upper_limit_baseline_rise_min); - let lower_gap_min = scaled!(ctx, lower_limit_gap_min); - let lower_drop_min = scaled!(ctx, lower_limit_baseline_drop_min); - - let mut base_offset = Abs::zero(); - let mut width = base.width(); - let mut height = base.height(); - - if let Some(t) = &t { - let top_gap = upper_gap_min.max(upper_rise_min - t.descent()); - width.set_max(t.width()); - height += t.height() + top_gap; - base_offset = top_gap + t.height(); - } - - if let Some(b) = &b { - let bottom_gap = lower_gap_min.max(lower_drop_min - b.ascent()); - width.set_max(b.width()); - height += b.height() + bottom_gap; - } - - let base_pos = Point::new((width - base.width()) / 2.0, base_offset); - let delta = base.italics_correction() / 2.0; - - let mut frame = Frame::soft(Size::new(width, height)); - frame.set_baseline(base_pos.y + base.ascent()); - frame.push_frame(base_pos, base.into_frame()); - - if let Some(t) = t { - let top_pos = Point::with_x((width - t.width()) / 2.0 + delta); - frame.push_frame(top_pos, t.into_frame()); - } - - if let Some(b) = b { - let bottom_pos = - Point::new((width - b.width()) / 2.0 - delta, height - b.height()); - frame.push_frame(bottom_pos, b.into_frame()); - } - - (frame, base_offset) -} - -fn compute_shifts_up_and_down( - ctx: &MathContext, - base: &MathFragment, - [tl, tr, bl, br]: [&Option<MathFragment>; 4], -) -> (Abs, Abs) { - let sup_shift_up = if ctx.style.cramped { - scaled!(ctx, superscript_shift_up_cramped) - } else { - scaled!(ctx, superscript_shift_up) - }; - - let sup_bottom_min = scaled!(ctx, superscript_bottom_min); - let sup_bottom_max_with_sub = scaled!(ctx, superscript_bottom_max_with_subscript); - let sup_drop_max = scaled!(ctx, superscript_baseline_drop_max); - let gap_min = scaled!(ctx, sub_superscript_gap_min); - let sub_shift_down = scaled!(ctx, subscript_shift_down); - let sub_top_max = scaled!(ctx, subscript_top_max); - let sub_drop_min = scaled!(ctx, subscript_baseline_drop_min); - - let mut shift_up = Abs::zero(); - let mut shift_down = Abs::zero(); - let is_char_box = is_character_box(base); - - if tl.is_some() || tr.is_some() { - let ascent = match &base { - MathFragment::Frame(frame) => frame.base_ascent, - _ => base.ascent(), - }; - shift_up = shift_up - .max(sup_shift_up) - .max(if is_char_box { Abs::zero() } else { ascent - sup_drop_max }) - .max(sup_bottom_min + measure!(tl, descent)) - .max(sup_bottom_min + measure!(tr, descent)); - } - - if bl.is_some() || br.is_some() { - shift_down = shift_down - .max(sub_shift_down) - .max(if is_char_box { Abs::zero() } else { base.descent() + sub_drop_min }) - .max(measure!(bl, ascent) - sub_top_max) - .max(measure!(br, ascent) - sub_top_max); - } - - for (sup, sub) in [(tl, bl), (tr, br)] { - if let (Some(sup), Some(sub)) = (&sup, &sub) { - let sup_bottom = shift_up - sup.descent(); - let sub_top = sub.ascent() - shift_down; - let gap = sup_bottom - sub_top; - if gap >= gap_min { - continue; - } - - let increase = gap_min - gap; - let sup_only = - (sup_bottom_max_with_sub - sup_bottom).clamp(Abs::zero(), increase); - let rest = (increase - sup_only) / 2.0; - shift_up += sup_only + rest; - shift_down += rest; - } - } - - (shift_up, shift_down) -} - -/// Determines if the character is one of a variety of integral signs -fn is_integral_char(c: char) -> bool { - ('∫'..='∳').contains(&c) || ('⨋'..='⨜').contains(&c) -} - -/// Whether the fragment consists of a single character or atomic piece of text. -fn is_character_box(fragment: &MathFragment) -> bool { - match fragment { - MathFragment::Glyph(_) | MathFragment::Variant(_) => { - fragment.class() != Some(MathClass::Large) - } - MathFragment::Frame(fragment) => is_atomic_text_frame(&fragment.frame), - _ => false, - } -} - -/// Handles e.g. "sin", "log", "exp", "CustomOperator". -fn is_atomic_text_frame(frame: &Frame) -> bool { - // Meta information isn't visible or renderable, so we exclude it. - let mut iter = frame - .items() - .map(|(_, item)| item) - .filter(|item| !matches!(item, FrameItem::Meta(_, _))); - matches!(iter.next(), Some(FrameItem::Text(_))) && iter.next().is_none() -} diff --git a/crates/typst-library/src/math/cancel.rs b/crates/typst-library/src/math/cancel.rs deleted file mode 100644 index 455750f7..00000000 --- a/crates/typst-library/src/math/cancel.rs +++ /dev/null @@ -1,230 +0,0 @@ -use super::*; - -/// Displays a diagonal line over a part of an equation. -/// -/// This is commonly used to show the elimination of a term. -/// -/// # Example -/// ```example -/// >>> #set page(width: 140pt) -/// Here, we can simplify: -/// $ (a dot b dot cancel(x)) / -/// cancel(x) $ -/// ``` -#[elem(LayoutMath)] -pub struct CancelElem { - /// The content over which the line should be placed. - #[required] - pub body: Content, - - /// The length of the line, relative to the length of the diagonal spanning - /// the whole element being "cancelled". A value of `{100%}` would then have - /// the line span precisely the element's diagonal. - /// - /// ```example - /// >>> #set page(width: 140pt) - /// $ a + cancel(x, length: #200%) - /// - cancel(x, length: #200%) $ - /// ``` - #[default(Rel::new(Ratio::one(), Abs::pt(3.0).into()))] - pub length: Rel<Length>, - - /// Whether the cancel line should be inverted (flipped along the y-axis). - /// For the default angle setting, inverted means the cancel line - /// points to the top left instead of top right. - /// - /// ```example - /// >>> #set page(width: 140pt) - /// $ (a cancel((b + c), inverted: #true)) / - /// cancel(b + c, inverted: #true) $ - /// ``` - #[default(false)] - pub inverted: bool, - - /// Whether two opposing cancel lines should be drawn, forming a cross over - /// the element. Overrides `inverted`. - /// - /// ```example - /// >>> #set page(width: 140pt) - /// $ cancel(Pi, cross: #true) $ - /// ``` - #[default(false)] - pub cross: bool, - - /// How much to rotate the cancel line. - /// - /// - If `{auto}`, the line assumes the default angle; that is, along the - /// diagonal line of the content box. - /// - If given an angle, the line is rotated by that angle clockwise w.r.t - /// the y-axis. - /// - If given a function `angle => angle`, the line is rotated by the angle - /// returned by that function. The function receives the default angle as - /// its input. - /// - /// ```example - /// >>> #set page(width: 140pt) - /// $ cancel(Pi) - /// cancel(Pi, angle: #0deg) - /// cancel(Pi, angle: #45deg) - /// cancel(Pi, angle: #90deg) - /// cancel(1/(1+x), angle: #(a => a + 45deg)) - /// cancel(1/(1+x), angle: #(a => a + 90deg)) $ - /// ``` - pub angle: Smart<CancelAngle>, - - /// How to [stroke]($stroke) the cancel line. - /// - /// ```example - /// >>> #set page(width: 140pt) - /// $ cancel( - /// sum x, - /// stroke: #( - /// paint: red, - /// thickness: 1.5pt, - /// dash: "dashed", - /// ), - /// ) $ - /// ``` - #[resolve] - #[fold] - #[default(Stroke { - // Default stroke has 0.5pt for better visuals. - thickness: Smart::Custom(Abs::pt(0.5)), - ..Default::default() - })] - pub stroke: Stroke, -} - -impl LayoutMath for CancelElem { - fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { - let body = ctx.layout_fragment(self.body())?; - // Use the same math class as the body, in order to preserve automatic spacing around it. - let body_class = body.class().unwrap_or(MathClass::Special); - let mut body = body.into_frame(); - - let styles = ctx.styles(); - let body_size = body.size(); - let span = self.span(); - let length = self.length(styles).resolve(styles); - - let stroke = self.stroke(styles).unwrap_or(FixedStroke { - paint: TextElem::fill_in(styles).as_decoration(), - ..Default::default() - }); - - let invert = self.inverted(styles); - let cross = self.cross(styles); - let angle = self.angle(styles); - - let invert_first_line = !cross && invert; - let first_line = draw_cancel_line( - ctx, - length, - stroke.clone(), - invert_first_line, - &angle, - body_size, - span, - )?; - - // The origin of our line is the very middle of the element. - let center = body_size.to_point() / 2.0; - body.push_frame(center, first_line); - - if cross { - // Draw the second line. - let second_line = - draw_cancel_line(ctx, length, stroke, true, &angle, body_size, span)?; - - body.push_frame(center, second_line); - } - - ctx.push(FrameFragment::new(ctx, body).with_class(body_class)); - - Ok(()) - } -} - -/// Defines the cancel line. -#[derive(Debug, Clone, PartialEq, Hash)] -pub enum CancelAngle { - Angle(Angle), - Func(Func), -} - -cast! { - CancelAngle, - self => match self { - Self::Angle(v) => v.into_value(), - Self::Func(v) => v.into_value() - }, - v: Angle => CancelAngle::Angle(v), - v: Func => CancelAngle::Func(v), -} - -/// Draws a cancel line. -fn draw_cancel_line( - ctx: &mut MathContext, - length_scale: Rel<Abs>, - stroke: FixedStroke, - invert: bool, - angle: &Smart<CancelAngle>, - body_size: Size, - span: Span, -) -> SourceResult<Frame> { - let default = default_angle(body_size); - let mut angle = match angle { - // Non specified angle defaults to the diagonal - Smart::Auto => default, - Smart::Custom(angle) => match angle { - // This specifies the absolute angle w.r.t y-axis clockwise. - CancelAngle::Angle(v) => *v, - // This specifies a function that takes the default angle as input. - CancelAngle::Func(func) => { - func.call_vt(ctx.vt, [default])?.cast().at(span)? - } - }, - }; - - // invert means flipping along the y-axis - if invert { - angle *= -1.0; - } - - // same as above, the default length is the diagonal of the body box. - let default_length = body_size.to_point().hypot(); - let length = length_scale.relative_to(default_length); - - // Draw a vertical line of length and rotate it by angle - let start = Point::new(Abs::zero(), length / 2.0); - let delta = Point::new(Abs::zero(), -length); - - let mut frame = Frame::soft(body_size); - frame.push(start, FrameItem::Shape(Geometry::Line(delta).stroked(stroke), span)); - - // Having the middle of the line at the origin is convenient here. - frame.transform(Transform::rotate(angle)); - Ok(frame) -} - -/// The default line angle for a body of the given size. -fn default_angle(body: Size) -> Angle { - // The default cancel line is the diagonal. - // We infer the default angle from - // the diagonal w.r.t to the body box. - // - // The returned angle is in the range of [0, Pi/2] - // - // Note that the angle is computed w.r.t to the y-axis - // - // B - // /| - // diagonal / | height - // / | - // / | - // O ---- - // width - let (width, height) = (body.x, body.y); - let default_angle = (width / height).atan(); // arctangent (in the range [0, Pi/2]) - Angle::rad(default_angle) -} diff --git a/crates/typst-library/src/math/class.rs b/crates/typst-library/src/math/class.rs deleted file mode 100644 index d2c5192d..00000000 --- a/crates/typst-library/src/math/class.rs +++ /dev/null @@ -1,38 +0,0 @@ -use super::*; - -/// Forced use of a certain math class. -/// -/// This is useful to treat certain symbols as if they were of a different -/// class, e.g. to make a symbol behave like a relation. -/// -/// # Example -/// ```example -/// #let loves = math.class( -/// "relation", -/// sym.suit.heart, -/// ) -/// -/// $x loves y and y loves 5$ -/// ``` -#[elem(LayoutMath)] -pub struct ClassElem { - /// The class to apply to the content. - #[required] - pub class: MathClass, - - /// The content to which the class is applied. - #[required] - pub body: Content, -} - -impl LayoutMath for ClassElem { - fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { - ctx.style(ctx.style.with_class(*self.class())); - let mut fragment = ctx.layout_fragment(self.body())?; - ctx.unstyle(); - - fragment.set_class(*self.class()); - ctx.push(fragment); - Ok(()) - } -} diff --git a/crates/typst-library/src/math/ctx.rs b/crates/typst-library/src/math/ctx.rs deleted file mode 100644 index 789bd332..00000000 --- a/crates/typst-library/src/math/ctx.rs +++ /dev/null @@ -1,335 +0,0 @@ -use comemo::Prehashed; -use ttf_parser::gsub::SubstitutionSubtable; -use ttf_parser::math::MathValue; -use typst::font::{FontStyle, FontWeight}; -use typst::model::realize; -use typst::syntax::is_newline; -use unicode_segmentation::UnicodeSegmentation; - -use super::*; -use crate::text::{tags, BottomEdge, BottomEdgeMetric, TopEdge, TopEdgeMetric}; - -macro_rules! scaled { - ($ctx:expr, text: $text:ident, display: $display:ident $(,)?) => { - match $ctx.style.size { - MathSize::Display => scaled!($ctx, $display), - _ => scaled!($ctx, $text), - } - }; - ($ctx:expr, $name:ident) => { - $ctx.constants.$name().scaled($ctx) - }; -} - -macro_rules! percent { - ($ctx:expr, $name:ident) => { - $ctx.constants.$name() as f64 / 100.0 - }; -} - -/// The context for math layout. -pub struct MathContext<'a, 'b, 'v> { - pub vt: &'v mut Vt<'b>, - pub regions: Regions<'static>, - pub font: &'a Font, - pub ttf: &'a ttf_parser::Face<'a>, - pub table: ttf_parser::math::Table<'a>, - pub constants: ttf_parser::math::Constants<'a>, - pub ssty_table: Option<ttf_parser::gsub::AlternateSubstitution<'a>>, - pub glyphwise_tables: Option<Vec<GlyphwiseSubsts<'a>>>, - pub space_width: Em, - pub fragments: Vec<MathFragment>, - pub local: Styles, - pub style: MathStyle, - pub size: Abs, - outer: StyleChain<'a>, - style_stack: Vec<(MathStyle, Abs)>, -} - -impl<'a, 'b, 'v> MathContext<'a, 'b, 'v> { - pub fn new( - vt: &'v mut Vt<'b>, - styles: StyleChain<'a>, - regions: Regions, - font: &'a Font, - block: bool, - ) -> Self { - let math_table = font.ttf().tables().math.unwrap(); - let gsub_table = font.ttf().tables().gsub; - let constants = math_table.constants.unwrap(); - - let ssty_table = gsub_table - .and_then(|gsub| { - gsub.features - .find(ttf_parser::Tag::from_bytes(b"ssty")) - .and_then(|feature| feature.lookup_indices.get(0)) - .and_then(|index| gsub.lookups.get(index)) - }) - .and_then(|ssty| ssty.subtables.get::<SubstitutionSubtable>(0)) - .and_then(|ssty| match ssty { - SubstitutionSubtable::Alternate(alt_glyphs) => Some(alt_glyphs), - _ => None, - }); - - let features = tags(styles); - let glyphwise_tables = gsub_table.map(|gsub| { - features - .into_iter() - .filter_map(|feature| GlyphwiseSubsts::new(gsub, feature)) - .collect() - }); - - let size = TextElem::size_in(styles); - let ttf = font.ttf(); - let space_width = ttf - .glyph_index(' ') - .and_then(|id| ttf.glyph_hor_advance(id)) - .map(|advance| font.to_em(advance)) - .unwrap_or(THICK); - - let variant = variant(styles); - Self { - vt, - regions: Regions::one(regions.base(), Axes::splat(false)), - font, - ttf: font.ttf(), - table: math_table, - constants, - ssty_table, - glyphwise_tables, - space_width, - fragments: vec![], - local: Styles::new(), - style: MathStyle { - variant: MathVariant::Serif, - size: if block { MathSize::Display } else { MathSize::Text }, - class: Smart::Auto, - cramped: false, - bold: variant.weight >= FontWeight::BOLD, - italic: match variant.style { - FontStyle::Normal => Smart::Auto, - FontStyle::Italic | FontStyle::Oblique => Smart::Custom(true), - }, - }, - size, - outer: styles, - style_stack: vec![], - } - } - - pub fn push(&mut self, fragment: impl Into<MathFragment>) { - self.fragments.push(fragment.into()); - } - - pub fn extend(&mut self, fragments: Vec<MathFragment>) { - self.fragments.extend(fragments); - } - - pub fn layout_fragment( - &mut self, - elem: &dyn LayoutMath, - ) -> SourceResult<MathFragment> { - let row = self.layout_fragments(elem)?; - Ok(MathRow::new(row).into_fragment(self)) - } - - pub fn layout_fragments( - &mut self, - elem: &dyn LayoutMath, - ) -> SourceResult<Vec<MathFragment>> { - let prev = std::mem::take(&mut self.fragments); - elem.layout_math(self)?; - Ok(std::mem::replace(&mut self.fragments, prev)) - } - - pub fn layout_row(&mut self, elem: &dyn LayoutMath) -> SourceResult<MathRow> { - let fragments = self.layout_fragments(elem)?; - Ok(MathRow::new(fragments)) - } - - pub fn layout_frame(&mut self, elem: &dyn LayoutMath) -> SourceResult<Frame> { - Ok(self.layout_fragment(elem)?.into_frame()) - } - - pub fn layout_box(&mut self, boxed: &BoxElem) -> SourceResult<Frame> { - Ok(boxed - .layout(self.vt, self.outer.chain(&self.local), self.regions)? - .into_frame()) - } - - pub fn layout_content(&mut self, content: &Content) -> SourceResult<Frame> { - Ok(content - .layout(self.vt, self.outer.chain(&self.local), self.regions)? - .into_frame()) - } - - pub fn layout_text(&mut self, elem: &TextElem) -> SourceResult<MathFragment> { - let text = elem.text(); - let span = elem.span(); - let mut chars = text.chars(); - let fragment = if let Some(mut glyph) = chars - .next() - .filter(|_| chars.next().is_none()) - .map(|c| self.style.styled_char(c)) - .and_then(|c| GlyphFragment::try_new(self, c, span)) - { - // A single letter that is available in the math font. - match self.style.size { - MathSize::Script => { - glyph.make_scriptsize(self); - } - MathSize::ScriptScript => { - glyph.make_scriptscriptsize(self); - } - _ => (), - } - - let class = self.style.class.as_custom().or(glyph.class); - if class == Some(MathClass::Large) { - let mut variant = if self.style.size == MathSize::Display { - let height = scaled!(self, display_operator_min_height); - glyph.stretch_vertical(self, height, Abs::zero()) - } else { - glyph.into_variant() - }; - // TeXbook p 155. Large operators are always vertically centered on the axis. - variant.center_on_axis(self); - variant.into() - } else { - glyph.into() - } - } else if text.chars().all(|c| c.is_ascii_digit() || c == '.') { - // Numbers aren't that difficult. - let mut fragments = vec![]; - for c in text.chars() { - let c = self.style.styled_char(c); - fragments.push(GlyphFragment::new(self, c, span).into()); - } - let frame = MathRow::new(fragments).into_frame(self); - FrameFragment::new(self, frame).into() - } else { - // Anything else is handled by Typst's standard text layout. - let mut style = self.style; - if self.style.italic == Smart::Auto { - style = style.with_italic(false); - } - let text: EcoString = text.chars().map(|c| style.styled_char(c)).collect(); - if text.contains(is_newline) { - let mut fragments = vec![]; - for (i, piece) in text.split(is_newline).enumerate() { - if i != 0 { - fragments.push(MathFragment::Linebreak); - } - if !piece.is_empty() { - fragments.push(self.layout_complex_text(piece, span)?.into()); - } - } - let mut frame = MathRow::new(fragments).into_frame(self); - let axis = scaled!(self, axis_height); - frame.set_baseline(frame.height() / 2.0 + axis); - FrameFragment::new(self, frame).into() - } else { - self.layout_complex_text(&text, span)?.into() - } - }; - Ok(fragment) - } - - pub fn layout_complex_text( - &mut self, - text: &str, - span: Span, - ) -> SourceResult<FrameFragment> { - let spaced = text.graphemes(true).nth(1).is_some(); - let elem = TextElem::packed(text) - .styled(TextElem::set_top_edge(TopEdge::Metric(TopEdgeMetric::Bounds))) - .styled(TextElem::set_bottom_edge(BottomEdge::Metric( - BottomEdgeMetric::Bounds, - ))) - .spanned(span); - - // There isn't a natural width for a paragraph in a math environment; - // because it will be placed somewhere probably not at the left margin - // it will overflow. So emulate an `hbox` instead and allow the paragraph - // to extend as far as needed. - let span = elem.span(); - let frame = ParElem::new(vec![Prehashed::new(elem)]) - .spanned(span) - .layout( - self.vt, - self.outer.chain(&self.local), - false, - Size::splat(Abs::inf()), - false, - )? - .into_frame(); - - Ok(FrameFragment::new(self, frame) - .with_class(MathClass::Alphabetic) - .with_spaced(spaced)) - } - - pub fn styles(&self) -> StyleChain { - self.outer.chain(&self.local) - } - - pub fn realize(&mut self, content: &Content) -> SourceResult<Option<Content>> { - realize(self.vt, content, self.outer.chain(&self.local)) - } - - pub fn style(&mut self, style: MathStyle) { - self.style_stack.push((self.style, self.size)); - let base_size = TextElem::size_in(self.styles()) / self.style.size.factor(self); - self.size = base_size * style.size.factor(self); - self.local.set(TextElem::set_size(TextSize(self.size.into()))); - self.local - .set(TextElem::set_style(if style.italic == Smart::Custom(true) { - FontStyle::Italic - } else { - FontStyle::Normal - })); - self.local.set(TextElem::set_weight(if style.bold { - FontWeight::BOLD - } else { - // The normal weight is what we started with. - // It's 400 for CM Regular, 450 for CM Book. - self.font.info().variant.weight - })); - self.style = style; - } - - pub fn unstyle(&mut self) { - (self.style, self.size) = self.style_stack.pop().unwrap(); - self.local.unset(); - self.local.unset(); - self.local.unset(); - } -} - -pub(super) trait Scaled { - fn scaled(self, ctx: &MathContext) -> Abs; -} - -impl Scaled for i16 { - fn scaled(self, ctx: &MathContext) -> Abs { - ctx.font.to_em(self).scaled(ctx) - } -} - -impl Scaled for u16 { - fn scaled(self, ctx: &MathContext) -> Abs { - ctx.font.to_em(self).scaled(ctx) - } -} - -impl Scaled for Em { - fn scaled(self, ctx: &MathContext) -> Abs { - self.at(ctx.size) - } -} - -impl Scaled for MathValue<'_> { - fn scaled(self, ctx: &MathContext) -> Abs { - self.value.scaled(ctx) - } -} diff --git a/crates/typst-library/src/math/frac.rs b/crates/typst-library/src/math/frac.rs deleted file mode 100644 index c3014178..00000000 --- a/crates/typst-library/src/math/frac.rs +++ /dev/null @@ -1,157 +0,0 @@ -use super::*; - -const FRAC_AROUND: Em = Em::new(0.1); - -/// A mathematical fraction. -/// -/// # Example -/// ```example -/// $ 1/2 < (x+1)/2 $ -/// $ ((x+1)) / 2 = frac(a, b) $ -/// ``` -/// -/// # Syntax -/// This function also has dedicated syntax: Use a slash to turn neighbouring -/// expressions into a fraction. Multiple atoms can be grouped into a single -/// expression using round grouping parenthesis. Such parentheses are removed -/// from the output, but you can nest multiple to force them. -#[elem(title = "Fraction", LayoutMath)] -pub struct FracElem { - /// The fraction's numerator. - #[required] - pub num: Content, - - /// The fraction's denominator. - #[required] - pub denom: Content, -} - -impl LayoutMath for FracElem { - #[tracing::instrument(skip(ctx))] - fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { - layout(ctx, self.num(), std::slice::from_ref(self.denom()), false, self.span()) - } -} - -/// A binomial expression. -/// -/// # Example -/// ```example -/// $ binom(n, k) $ -/// $ binom(n, k_1, k_2, k_3, ..., k_m) $ -/// ``` -#[elem(title = "Binomial", LayoutMath)] -pub struct BinomElem { - /// The binomial's upper index. - #[required] - pub upper: Content, - - /// The binomial's lower index. - #[required] - #[variadic] - #[parse( - let values = args.all::<Spanned<Value>>()?; - if values.is_empty() { - // Prevents one element binomials - bail!(args.span, "missing argument: lower"); - } - values.into_iter().map(|spanned| spanned.v.display()).collect() - )] - pub lower: Vec<Content>, -} - -impl LayoutMath for BinomElem { - fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { - layout(ctx, self.upper(), self.lower(), true, self.span()) - } -} - -/// Layout a fraction or binomial. -fn layout( - ctx: &mut MathContext, - num: &Content, - denom: &[Content], - binom: bool, - span: Span, -) -> SourceResult<()> { - let short_fall = DELIM_SHORT_FALL.scaled(ctx); - let axis = scaled!(ctx, axis_height); - let thickness = scaled!(ctx, fraction_rule_thickness); - let shift_up = scaled!( - ctx, - text: fraction_numerator_shift_up, - display: fraction_numerator_display_style_shift_up, - ); - let shift_down = scaled!( - ctx, - text: fraction_denominator_shift_down, - display: fraction_denominator_display_style_shift_down, - ); - let num_min = scaled!( - ctx, - text: fraction_numerator_gap_min, - display: fraction_num_display_style_gap_min, - ); - let denom_min = scaled!( - ctx, - text: fraction_denominator_gap_min, - display: fraction_denom_display_style_gap_min, - ); - - ctx.style(ctx.style.for_numerator()); - let num = ctx.layout_frame(num)?; - ctx.unstyle(); - - ctx.style(ctx.style.for_denominator()); - let denom = ctx.layout_frame(&Content::sequence( - // Add a comma between each element. - denom.iter().flat_map(|a| [TextElem::packed(','), a.clone()]).skip(1), - ))?; - ctx.unstyle(); - - let around = FRAC_AROUND.scaled(ctx); - let num_gap = (shift_up - axis - num.descent()).max(num_min + thickness / 2.0); - let denom_gap = (shift_down + axis - denom.ascent()).max(denom_min + thickness / 2.0); - - let line_width = num.width().max(denom.width()); - let width = line_width + 2.0 * around; - let height = num.height() + num_gap + thickness + denom_gap + denom.height(); - let size = Size::new(width, height); - let num_pos = Point::with_x((width - num.width()) / 2.0); - let line_pos = - Point::new((width - line_width) / 2.0, num.height() + num_gap + thickness / 2.0); - let denom_pos = Point::new((width - denom.width()) / 2.0, height - denom.height()); - let baseline = line_pos.y + axis; - - let mut frame = Frame::soft(size); - frame.set_baseline(baseline); - frame.push_frame(num_pos, num); - frame.push_frame(denom_pos, denom); - - if binom { - let mut left = - GlyphFragment::new(ctx, '(', span).stretch_vertical(ctx, height, short_fall); - left.center_on_axis(ctx); - ctx.push(left); - ctx.push(FrameFragment::new(ctx, frame)); - let mut right = - GlyphFragment::new(ctx, ')', span).stretch_vertical(ctx, height, short_fall); - right.center_on_axis(ctx); - ctx.push(right); - } else { - frame.push( - line_pos, - FrameItem::Shape( - Geometry::Line(Point::with_x(line_width)).stroked(FixedStroke { - paint: TextElem::fill_in(ctx.styles()).as_decoration(), - thickness, - ..FixedStroke::default() - }), - span, - ), - ); - ctx.push(FrameFragment::new(ctx, frame)); - } - - Ok(()) -} diff --git a/crates/typst-library/src/math/fragment.rs b/crates/typst-library/src/math/fragment.rs deleted file mode 100644 index 76ee2512..00000000 --- a/crates/typst-library/src/math/fragment.rs +++ /dev/null @@ -1,513 +0,0 @@ -use rustybuzz::Feature; -use ttf_parser::gsub::{ - AlternateSet, AlternateSubstitution, SingleSubstitution, SubstitutionSubtable, -}; -use ttf_parser::opentype_layout::LayoutTable; - -use super::*; - -#[derive(Debug, Clone)] -pub enum MathFragment { - Glyph(GlyphFragment), - Variant(VariantFragment), - Frame(FrameFragment), - Spacing(Abs), - Space(Abs), - Linebreak, - Align, -} - -impl MathFragment { - pub fn size(&self) -> Size { - Size::new(self.width(), self.height()) - } - - pub fn width(&self) -> Abs { - match self { - Self::Glyph(glyph) => glyph.width, - Self::Variant(variant) => variant.frame.width(), - Self::Frame(fragment) => fragment.frame.width(), - Self::Spacing(amount) => *amount, - Self::Space(amount) => *amount, - _ => Abs::zero(), - } - } - - pub fn height(&self) -> Abs { - match self { - Self::Glyph(glyph) => glyph.height(), - Self::Variant(variant) => variant.frame.height(), - Self::Frame(fragment) => fragment.frame.height(), - _ => Abs::zero(), - } - } - - pub fn ascent(&self) -> Abs { - match self { - Self::Glyph(glyph) => glyph.ascent, - Self::Variant(variant) => variant.frame.ascent(), - Self::Frame(fragment) => fragment.frame.baseline(), - _ => Abs::zero(), - } - } - - pub fn descent(&self) -> Abs { - match self { - Self::Glyph(glyph) => glyph.descent, - Self::Variant(variant) => variant.frame.descent(), - Self::Frame(fragment) => fragment.frame.descent(), - _ => Abs::zero(), - } - } - - pub fn class(&self) -> Option<MathClass> { - self.style().and_then(|style| style.class.as_custom()).or(match self { - Self::Glyph(glyph) => glyph.class, - Self::Variant(variant) => variant.class, - Self::Frame(fragment) => Some(fragment.class), - _ => None, - }) - } - - pub fn style(&self) -> Option<MathStyle> { - match self { - Self::Glyph(glyph) => Some(glyph.style), - Self::Variant(variant) => Some(variant.style), - Self::Frame(fragment) => Some(fragment.style), - _ => None, - } - } - - pub fn font_size(&self) -> Option<Abs> { - match self { - Self::Glyph(glyph) => Some(glyph.font_size), - Self::Variant(variant) => Some(variant.font_size), - Self::Frame(fragment) => Some(fragment.font_size), - _ => None, - } - } - - pub fn set_class(&mut self, class: MathClass) { - macro_rules! set_style_class { - ($fragment:ident) => { - if $fragment.style.class.is_custom() { - $fragment.style.class = Smart::Custom(class); - } - }; - } - - match self { - Self::Glyph(glyph) => { - glyph.class = Some(class); - set_style_class!(glyph); - } - Self::Variant(variant) => { - variant.class = Some(class); - set_style_class!(variant); - } - Self::Frame(fragment) => { - fragment.class = class; - set_style_class!(fragment); - } - _ => {} - } - } - - pub fn set_limits(&mut self, limits: Limits) { - match self { - Self::Glyph(glyph) => glyph.limits = limits, - Self::Variant(variant) => variant.limits = limits, - Self::Frame(fragment) => fragment.limits = limits, - _ => {} - } - } - - pub fn is_spaced(&self) -> bool { - match self { - MathFragment::Frame(frame) => { - match self.style().and_then(|style| style.class.as_custom()) { - Some(MathClass::Fence) => true, - Some(_) => false, - None => frame.spaced, - } - } - _ => self.class() == Some(MathClass::Fence), - } - } - - pub fn italics_correction(&self) -> Abs { - match self { - Self::Glyph(glyph) => glyph.italics_correction, - Self::Variant(variant) => variant.italics_correction, - _ => Abs::zero(), - } - } - - pub fn into_frame(self) -> Frame { - match self { - Self::Glyph(glyph) => glyph.into_frame(), - Self::Variant(variant) => variant.frame, - Self::Frame(fragment) => fragment.frame, - _ => Frame::soft(self.size()), - } - } - - pub fn limits(&self) -> Limits { - match self { - MathFragment::Glyph(glyph) => glyph.limits, - MathFragment::Variant(variant) => variant.limits, - MathFragment::Frame(fragment) => fragment.limits, - _ => Limits::Never, - } - } -} - -impl From<GlyphFragment> for MathFragment { - fn from(glyph: GlyphFragment) -> Self { - Self::Glyph(glyph) - } -} - -impl From<VariantFragment> for MathFragment { - fn from(variant: VariantFragment) -> Self { - Self::Variant(variant) - } -} - -impl From<FrameFragment> for MathFragment { - fn from(fragment: FrameFragment) -> Self { - Self::Frame(fragment) - } -} - -#[derive(Clone)] -pub struct GlyphFragment { - pub id: GlyphId, - pub c: char, - pub font: Font, - pub lang: Lang, - pub fill: Paint, - pub shift: Abs, - pub width: Abs, - pub ascent: Abs, - pub descent: Abs, - pub italics_correction: Abs, - pub style: MathStyle, - pub font_size: Abs, - pub class: Option<MathClass>, - pub span: Span, - pub meta: SmallVec<[Meta; 1]>, - pub limits: Limits, -} - -impl GlyphFragment { - pub fn new(ctx: &MathContext, c: char, span: Span) -> Self { - let id = ctx.ttf.glyph_index(c).unwrap_or_default(); - let id = Self::adjust_glyph_index(ctx, id); - Self::with_id(ctx, c, id, span) - } - - pub fn try_new(ctx: &MathContext, c: char, span: Span) -> Option<Self> { - let c = ctx.style.styled_char(c); - let id = ctx.ttf.glyph_index(c)?; - let id = Self::adjust_glyph_index(ctx, id); - Some(Self::with_id(ctx, c, id, span)) - } - - pub fn with_id(ctx: &MathContext, c: char, id: GlyphId, span: Span) -> Self { - let class = match c { - ':' => Some(MathClass::Relation), - '.' | '/' | '⋯' | '⋱' | '⋰' | '⋮' => Some(MathClass::Normal), - _ => unicode_math_class::class(c), - }; - let mut fragment = Self { - id, - c, - font: ctx.font.clone(), - lang: TextElem::lang_in(ctx.styles()), - fill: TextElem::fill_in(ctx.styles()).as_decoration(), - shift: TextElem::baseline_in(ctx.styles()), - style: ctx.style, - font_size: ctx.size, - width: Abs::zero(), - ascent: Abs::zero(), - descent: Abs::zero(), - limits: Limits::for_char(c), - italics_correction: Abs::zero(), - class, - span, - meta: MetaElem::data_in(ctx.styles()), - }; - fragment.set_id(ctx, id); - fragment - } - - /// Apply GSUB substitutions. - fn adjust_glyph_index(ctx: &MathContext, id: GlyphId) -> GlyphId { - if let Some(glyphwise_tables) = &ctx.glyphwise_tables { - glyphwise_tables.iter().fold(id, |id, table| table.apply(id)) - } else { - id - } - } - - /// Sets element id and boxes in appropriate way without changing other - /// styles. This is used to replace the glyph with a stretch variant. - pub fn set_id(&mut self, ctx: &MathContext, id: GlyphId) { - let advance = ctx.ttf.glyph_hor_advance(id).unwrap_or_default(); - let italics = italics_correction(ctx, id).unwrap_or_default(); - let bbox = ctx.ttf.glyph_bounding_box(id).unwrap_or(Rect { - x_min: 0, - y_min: 0, - x_max: 0, - y_max: 0, - }); - - let mut width = advance.scaled(ctx); - if !is_extended_shape(ctx, id) { - width += italics; - } - - self.id = id; - self.width = width; - self.ascent = bbox.y_max.scaled(ctx); - self.descent = -bbox.y_min.scaled(ctx); - self.italics_correction = italics; - } - - pub fn height(&self) -> Abs { - self.ascent + self.descent - } - - pub fn into_variant(self) -> VariantFragment { - VariantFragment { - c: self.c, - id: Some(self.id), - style: self.style, - font_size: self.font_size, - italics_correction: self.italics_correction, - class: self.class, - span: self.span, - limits: self.limits, - frame: self.into_frame(), - } - } - - pub fn into_frame(self) -> Frame { - let item = TextItem { - font: self.font.clone(), - size: self.font_size, - fill: self.fill, - lang: self.lang, - text: self.c.into(), - glyphs: vec![Glyph { - id: self.id.0, - x_advance: Em::from_length(self.width, self.font_size), - x_offset: Em::zero(), - range: 0..self.c.len_utf8() as u16, - span: (self.span, 0), - }], - }; - let size = Size::new(self.width, self.ascent + self.descent); - let mut frame = Frame::soft(size); - frame.set_baseline(self.ascent); - frame.push(Point::with_y(self.ascent + self.shift), FrameItem::Text(item)); - frame.meta_iter(self.meta); - frame - } - - pub fn make_scriptsize(&mut self, ctx: &MathContext) { - let alt_id = - script_alternatives(ctx, self.id).and_then(|alts| alts.alternates.get(0)); - - if let Some(alt_id) = alt_id { - self.set_id(ctx, alt_id); - } - } - - pub fn make_scriptscriptsize(&mut self, ctx: &MathContext) { - let alts = script_alternatives(ctx, self.id); - let alt_id = alts - .and_then(|alts| alts.alternates.get(1).or_else(|| alts.alternates.get(0))); - - if let Some(alt_id) = alt_id { - self.set_id(ctx, alt_id); - } - } -} - -impl Debug for GlyphFragment { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - write!(f, "GlyphFragment({:?})", self.c) - } -} - -#[derive(Clone)] -pub struct VariantFragment { - pub c: char, - pub id: Option<GlyphId>, - pub italics_correction: Abs, - pub frame: Frame, - pub style: MathStyle, - pub font_size: Abs, - pub class: Option<MathClass>, - pub span: Span, - pub limits: Limits, -} - -impl VariantFragment { - /// Vertically adjust the fragment's frame so that it is centered - /// on the axis. - pub fn center_on_axis(&mut self, ctx: &MathContext) { - let h = self.frame.height(); - self.frame.set_baseline(h / 2.0 + scaled!(ctx, axis_height)); - } -} - -impl Debug for VariantFragment { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - write!(f, "VariantFragment({:?})", self.c) - } -} - -#[derive(Debug, Clone)] -pub struct FrameFragment { - pub frame: Frame, - pub style: MathStyle, - pub font_size: Abs, - pub class: MathClass, - pub limits: Limits, - pub spaced: bool, - pub base_ascent: Abs, -} - -impl FrameFragment { - pub fn new(ctx: &MathContext, mut frame: Frame) -> Self { - let base_ascent = frame.ascent(); - frame.meta(ctx.styles(), false); - Self { - frame, - font_size: ctx.size, - style: ctx.style, - class: MathClass::Normal, - limits: Limits::Never, - spaced: false, - base_ascent, - } - } - - pub fn with_class(self, class: MathClass) -> Self { - Self { class, ..self } - } - - pub fn with_limits(self, limits: Limits) -> Self { - Self { limits, ..self } - } - - pub fn with_spaced(self, spaced: bool) -> Self { - Self { spaced, ..self } - } - - pub fn with_base_ascent(self, base_ascent: Abs) -> Self { - Self { base_ascent, ..self } - } -} - -/// Look up the italics correction for a glyph. -fn italics_correction(ctx: &MathContext, id: GlyphId) -> Option<Abs> { - Some(ctx.table.glyph_info?.italic_corrections?.get(id)?.scaled(ctx)) -} - -/// Look up the script/scriptscript alternates for a glyph -fn script_alternatives<'a>( - ctx: &MathContext<'a, '_, '_>, - id: GlyphId, -) -> Option<AlternateSet<'a>> { - ctx.ssty_table.and_then(|ssty| { - ssty.coverage.get(id).and_then(|index| ssty.alternate_sets.get(index)) - }) -} - -/// Look up the italics correction for a glyph. -fn is_extended_shape(ctx: &MathContext, id: GlyphId) -> bool { - ctx.table - .glyph_info - .and_then(|info| info.extended_shapes) - .and_then(|info| info.get(id)) - .is_some() -} - -/// Look up a kerning value at a specific corner and height. -/// -/// This can be integrated once we've found a font that actually provides this -/// data. -#[allow(unused)] -fn kern_at_height( - ctx: &MathContext, - id: GlyphId, - corner: Corner, - height: Abs, -) -> Option<Abs> { - let kerns = ctx.table.glyph_info?.kern_infos?.get(id)?; - let kern = match corner { - Corner::TopLeft => kerns.top_left, - Corner::TopRight => kerns.top_right, - Corner::BottomRight => kerns.bottom_right, - Corner::BottomLeft => kerns.bottom_left, - }?; - - let mut i = 0; - while i < kern.count() && height > kern.height(i)?.scaled(ctx) { - i += 1; - } - - Some(kern.kern(i)?.scaled(ctx)) -} - -/// An OpenType substitution table that is applicable to glyph-wise substitutions. -pub enum GlyphwiseSubsts<'a> { - Single(SingleSubstitution<'a>), - Alternate(AlternateSubstitution<'a>, u32), -} - -impl<'a> GlyphwiseSubsts<'a> { - pub fn new(gsub: LayoutTable<'a>, feature: Feature) -> Option<Self> { - let table = gsub - .features - .find(ttf_parser::Tag(feature.tag.0)) - .and_then(|feature| feature.lookup_indices.get(0)) - .and_then(|index| gsub.lookups.get(index))?; - let table = table.subtables.get::<SubstitutionSubtable>(0)?; - match table { - SubstitutionSubtable::Single(single_glyphs) => { - Some(Self::Single(single_glyphs)) - } - SubstitutionSubtable::Alternate(alt_glyphs) => { - Some(Self::Alternate(alt_glyphs, feature.value)) - } - _ => None, - } - } - - pub fn try_apply(&self, glyph_id: GlyphId) -> Option<GlyphId> { - match self { - Self::Single(single) => match single { - SingleSubstitution::Format1 { coverage, delta } => coverage - .get(glyph_id) - .map(|_| GlyphId(glyph_id.0.wrapping_add(*delta as u16))), - SingleSubstitution::Format2 { coverage, substitutes } => { - coverage.get(glyph_id).and_then(|idx| substitutes.get(idx)) - } - }, - Self::Alternate(alternate, value) => alternate - .coverage - .get(glyph_id) - .and_then(|idx| alternate.alternate_sets.get(idx)) - .and_then(|set| set.alternates.get(*value as u16)), - } - } - - pub fn apply(&self, glyph_id: GlyphId) -> GlyphId { - self.try_apply(glyph_id).unwrap_or(glyph_id) - } -} diff --git a/crates/typst-library/src/math/lr.rs b/crates/typst-library/src/math/lr.rs deleted file mode 100644 index 39143620..00000000 --- a/crates/typst-library/src/math/lr.rs +++ /dev/null @@ -1,195 +0,0 @@ -use super::*; - -/// How much less high scaled delimiters can be than what they wrap. -pub(super) const DELIM_SHORT_FALL: Em = Em::new(0.1); - -/// Scales delimiters. -/// -/// While matched delimiters scale by default, this can be used to scale -/// unmatched delimiters and to control the delimiter scaling more precisely. -#[elem(title = "Left/Right", LayoutMath)] -pub struct LrElem { - /// The size of the brackets, relative to the height of the wrapped content. - pub size: Smart<Rel<Length>>, - - /// The delimited content, including the delimiters. - #[required] - #[parse( - let mut body = Content::empty(); - for (i, arg) in args.all::<Content>()?.into_iter().enumerate() { - if i > 0 { - body += TextElem::packed(','); - } - body += arg; - } - body - )] - pub body: Content, -} - -impl LayoutMath for LrElem { - #[tracing::instrument(skip(ctx))] - fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { - let mut body = self.body(); - if let Some(elem) = body.to::<LrElem>() { - if elem.size(ctx.styles()).is_auto() { - body = elem.body(); - } - } - - let mut fragments = ctx.layout_fragments(body)?; - let axis = scaled!(ctx, axis_height); - let max_extent = fragments - .iter() - .map(|fragment| (fragment.ascent() - axis).max(fragment.descent() + axis)) - .max() - .unwrap_or_default(); - - let height = self - .size(ctx.styles()) - .unwrap_or(Rel::one()) - .resolve(ctx.styles()) - .relative_to(2.0 * max_extent); - - match fragments.as_mut_slice() { - [one] => scale(ctx, one, height, None), - [first, .., last] => { - scale(ctx, first, height, Some(MathClass::Opening)); - scale(ctx, last, height, Some(MathClass::Closing)); - } - _ => {} - } - - ctx.extend(fragments); - - Ok(()) - } -} - -/// Scale a math fragment to a height. -fn scale( - ctx: &mut MathContext, - fragment: &mut MathFragment, - height: Abs, - apply: Option<MathClass>, -) { - if matches!( - fragment.class(), - Some(MathClass::Opening | MathClass::Closing | MathClass::Fence) - ) { - let glyph = match fragment { - MathFragment::Glyph(glyph) => glyph.clone(), - MathFragment::Variant(variant) => { - GlyphFragment::new(ctx, variant.c, variant.span) - } - _ => return, - }; - - let short_fall = DELIM_SHORT_FALL.scaled(ctx); - let mut stretched = glyph.stretch_vertical(ctx, height, short_fall); - stretched.center_on_axis(ctx); - - *fragment = MathFragment::Variant(stretched); - if let Some(class) = apply { - fragment.set_class(class); - } - } -} - -/// Floors an expression. -/// -/// ```example -/// $ floor(x/2) $ -/// ``` -#[func] -pub fn floor( - /// The size of the brackets, relative to the height of the wrapped content. - #[named] - size: Option<Smart<Rel<Length>>>, - /// The expression to floor. - body: Content, -) -> Content { - delimited(body, '⌊', '⌋', size) -} - -/// Ceils an expression. -/// -/// ```example -/// $ ceil(x/2) $ -/// ``` -#[func] -pub fn ceil( - /// The size of the brackets, relative to the height of the wrapped content. - #[named] - size: Option<Smart<Rel<Length>>>, - /// The expression to ceil. - body: Content, -) -> Content { - delimited(body, '⌈', '⌉', size) -} - -/// Rounds an expression. -/// -/// ```example -/// $ round(x/2) $ -/// ``` -#[func] -pub fn round( - /// The size of the brackets, relative to the height of the wrapped content. - #[named] - size: Option<Smart<Rel<Length>>>, - /// The expression to round. - body: Content, -) -> Content { - delimited(body, '⌊', '⌉', size) -} - -/// Takes the absolute value of an expression. -/// -/// ```example -/// $ abs(x/2) $ -/// ``` -#[func] -pub fn abs( - /// The size of the brackets, relative to the height of the wrapped content. - #[named] - size: Option<Smart<Rel<Length>>>, - /// The expression to take the absolute value of. - body: Content, -) -> Content { - delimited(body, '|', '|', size) -} - -/// Takes the norm of an expression. -/// -/// ```example -/// $ norm(x/2) $ -/// ``` -#[func] -pub fn norm( - /// The size of the brackets, relative to the height of the wrapped content. - #[named] - size: Option<Smart<Rel<Length>>>, - /// The expression to take the norm of. - body: Content, -) -> Content { - delimited(body, '‖', '‖', size) -} - -fn delimited( - body: Content, - left: char, - right: char, - size: Option<Smart<Rel<Length>>>, -) -> Content { - let mut elem = LrElem::new(Content::sequence([ - TextElem::packed(left), - body, - TextElem::packed(right), - ])); - // Push size only if size is provided - if let Some(size) = size { - elem.push_size(size); - } - elem.pack() -} diff --git a/crates/typst-library/src/math/matrix.rs b/crates/typst-library/src/math/matrix.rs deleted file mode 100644 index b5d21ed6..00000000 --- a/crates/typst-library/src/math/matrix.rs +++ /dev/null @@ -1,655 +0,0 @@ -use super::*; - -const DEFAULT_ROW_GAP: Em = Em::new(0.5); -const DEFAULT_COL_GAP: Em = Em::new(0.5); -const VERTICAL_PADDING: Ratio = Ratio::new(0.1); - -const DEFAULT_STROKE_THICKNESS: Em = Em::new(0.05); - -/// A column vector. -/// -/// Content in the vector's elements can be aligned with the `&` symbol. -/// -/// # Example -/// ```example -/// $ vec(a, b, c) dot vec(1, 2, 3) -/// = a + 2b + 3c $ -/// ``` -#[elem(title = "Vector", LayoutMath)] -pub struct VecElem { - /// The delimiter to use. - /// - /// ```example - /// #set math.vec(delim: "[") - /// $ vec(1, 2) $ - /// ``` - #[default(Some(Delimiter::Paren))] - pub delim: Option<Delimiter>, - - /// The gap between elements. - /// - /// ```example - /// #set math.vec(gap: 1em) - /// $ vec(1, 2) $ - /// ``` - #[resolve] - #[default(DEFAULT_ROW_GAP.into())] - pub gap: Rel<Length>, - - /// The elements of the vector. - #[variadic] - pub children: Vec<Content>, -} - -impl LayoutMath for VecElem { - #[tracing::instrument(skip(ctx))] - fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { - let delim = self.delim(ctx.styles()); - let frame = layout_vec_body( - ctx, - self.children(), - FixedAlign::Center, - self.gap(ctx.styles()), - )?; - layout_delimiters( - ctx, - frame, - delim.map(Delimiter::open), - delim.map(Delimiter::close), - self.span(), - ) - } -} - -/// A matrix. -/// -/// The elements of a row should be separated by commas, while the rows -/// themselves should be separated by semicolons. The semicolon syntax merges -/// preceding arguments separated by commas into an array. You can also use this -/// special syntax of math function calls to define custom functions that take -/// 2D data. -/// -/// Content in cells that are in the same row can be aligned with the `&` symbol. -/// -/// # Example -/// ```example -/// $ mat( -/// 1, 2, ..., 10; -/// 2, 2, ..., 10; -/// dots.v, dots.v, dots.down, dots.v; -/// 10, 10, ..., 10; -/// ) $ -/// ``` -#[elem(title = "Matrix", LayoutMath)] -pub struct MatElem { - /// The delimiter to use. - /// - /// ```example - /// #set math.mat(delim: "[") - /// $ mat(1, 2; 3, 4) $ - /// ``` - #[default(Some(Delimiter::Paren))] - pub delim: Option<Delimiter>, - - /// Draws augmentation lines in a matrix. - /// - /// - `{none}`: No lines are drawn. - /// - A single number: A vertical augmentation line is drawn - /// after the specified column number. Negative numbers start from the end. - /// - A dictionary: With a dictionary, multiple augmentation lines can be - /// drawn both horizontally and vertically. Additionally, the style of the - /// lines can be set. The dictionary can contain the following keys: - /// - `hline`: The offsets at which horizontal lines should be drawn. - /// For example, an offset of `2` would result in a horizontal line - /// being drawn after the second row of the matrix. Accepts either an - /// integer for a single line, or an array of integers - /// for multiple lines. Like for a single number, negative numbers start from the end. - /// - `vline`: The offsets at which vertical lines should be drawn. - /// For example, an offset of `2` would result in a vertical line being - /// drawn after the second column of the matrix. Accepts either an - /// integer for a single line, or an array of integers - /// for multiple lines. Like for a single number, negative numbers start from the end. - /// - `stroke`: How to [stroke]($stroke) the line. If set to `{auto}`, - /// takes on a thickness of 0.05em and square line caps. - /// - /// ```example - /// $ mat(1, 0, 1; 0, 1, 2; augment: #2) $ - /// // Equivalent to: - /// $ mat(1, 0, 1; 0, 1, 2; augment: #(-1)) $ - /// ``` - /// - /// ```example - /// $ mat(0, 0, 0; 1, 1, 1; augment: #(hline: 1, stroke: 2pt + green)) $ - /// ``` - #[resolve] - #[fold] - pub augment: Option<Augment>, - - /// The gap between rows and columns. - /// - /// ```example - /// #set math.mat(gap: 1em) - /// $ mat(1, 2; 3, 4) $ - /// ``` - #[external] - pub gap: Rel<Length>, - - /// The gap between rows. Takes precedence over `gap`. - /// - /// ```example - /// #set math.mat(row-gap: 1em) - /// $ mat(1, 2; 3, 4) $ - /// ``` - #[resolve] - #[parse( - let gap = args.named("gap")?; - args.named("row-gap")?.or(gap) - )] - #[default(DEFAULT_ROW_GAP.into())] - pub row_gap: Rel<Length>, - - /// The gap between columns. Takes precedence over `gap`. - /// - /// ```example - /// #set math.mat(column-gap: 1em) - /// $ mat(1, 2; 3, 4) $ - /// ``` - #[resolve] - #[parse(args.named("column-gap")?.or(gap))] - #[default(DEFAULT_COL_GAP.into())] - pub column_gap: Rel<Length>, - - /// An array of arrays with the rows of the matrix. - /// - /// ```example - /// #let data = ((1, 2, 3), (4, 5, 6)) - /// #let matrix = math.mat(..data) - /// $ v := matrix $ - /// ``` - #[variadic] - #[parse( - let mut rows = vec![]; - let mut width = 0; - - let values = args.all::<Spanned<Value>>()?; - if values.iter().any(|spanned| matches!(spanned.v, Value::Array(_))) { - for Spanned { v, span } in values { - let array = v.cast::<Array>().at(span)?; - let row: Vec<_> = array.into_iter().map(Value::display).collect(); - width = width.max(row.len()); - rows.push(row); - } - } else { - rows = vec![values.into_iter().map(|spanned| spanned.v.display()).collect()]; - } - - for row in &mut rows { - if row.len() < width { - row.resize(width, Content::empty()); - } - } - - rows - )] - pub rows: Vec<Vec<Content>>, -} - -impl LayoutMath for MatElem { - #[tracing::instrument(skip(ctx))] - fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { - // validate inputs - - let augment = self.augment(ctx.styles()); - let rows = self.rows(); - - if let Some(aug) = &augment { - for &offset in &aug.hline.0 { - if offset == 0 || offset.unsigned_abs() >= rows.len() { - bail!( - self.span(), - "cannot draw a horizontal line after row {} of a matrix with {} rows", - if offset < 0 { rows.len() as isize + offset } else { offset }, - rows.len() - ); - } - } - - let ncols = self.rows().first().map_or(0, |row| row.len()); - - for &offset in &aug.vline.0 { - if offset == 0 || offset.unsigned_abs() >= ncols { - bail!( - self.span(), - "cannot draw a vertical line after column {} of a matrix with {} columns", - if offset < 0 { ncols as isize + offset } else { offset }, - ncols - ); - } - } - } - - let delim = self.delim(ctx.styles()); - let frame = layout_mat_body( - ctx, - rows, - augment, - Axes::new(self.column_gap(ctx.styles()), self.row_gap(ctx.styles())), - self.span(), - )?; - - layout_delimiters( - ctx, - frame, - delim.map(Delimiter::open), - delim.map(Delimiter::close), - self.span(), - ) - } -} - -/// A case distinction. -/// -/// Content across different branches can be aligned with the `&` symbol. -/// -/// # Example -/// ```example -/// $ f(x, y) := cases( -/// 1 "if" (x dot y)/2 <= 0, -/// 2 "if" x "is even", -/// 3 "if" x in NN, -/// 4 "else", -/// ) $ -/// ``` -#[elem(LayoutMath)] -pub struct CasesElem { - /// The delimiter to use. - /// - /// ```example - /// #set math.cases(delim: "[") - /// $ x = cases(1, 2) $ - /// ``` - #[default(Delimiter::Brace)] - pub delim: Delimiter, - - /// Whether the direction of cases should be reversed. - /// - /// ```example - /// #set math.cases(reverse: true) - /// $ cases(1, 2) = x $ - /// ``` - #[default(false)] - pub reverse: bool, - - /// The gap between branches. - /// - /// ```example - /// #set math.cases(gap: 1em) - /// $ x = cases(1, 2) $ - /// ``` - #[resolve] - #[default(DEFAULT_ROW_GAP.into())] - pub gap: Rel<Length>, - - /// The branches of the case distinction. - #[variadic] - pub children: Vec<Content>, -} - -impl LayoutMath for CasesElem { - #[tracing::instrument(skip(ctx))] - fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { - let delim = self.delim(ctx.styles()); - let frame = layout_vec_body( - ctx, - self.children(), - FixedAlign::Start, - self.gap(ctx.styles()), - )?; - - let (open, close) = if self.reverse(ctx.styles()) { - (None, Some(delim.close())) - } else { - (Some(delim.open()), None) - }; - - layout_delimiters(ctx, frame, open, close, self.span()) - } -} - -/// A vector / matrix delimiter. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)] -pub enum Delimiter { - /// Delimit with parentheses. - #[string("(")] - Paren, - /// Delimit with brackets. - #[string("[")] - Bracket, - /// Delimit with curly braces. - #[string("{")] - Brace, - /// Delimit with vertical bars. - #[string("|")] - Bar, - /// Delimit with double vertical bars. - #[string("||")] - DoubleBar, -} - -impl Delimiter { - /// The delimiter's opening character. - fn open(self) -> char { - match self { - Self::Paren => '(', - Self::Bracket => '[', - Self::Brace => '{', - Self::Bar => '|', - Self::DoubleBar => '‖', - } - } - - /// The delimiter's closing character. - fn close(self) -> char { - match self { - Self::Paren => ')', - Self::Bracket => ']', - Self::Brace => '}', - Self::Bar => '|', - Self::DoubleBar => '‖', - } - } -} - -/// Layout the inner contents of a vector. -fn layout_vec_body( - ctx: &mut MathContext, - column: &[Content], - align: FixedAlign, - row_gap: Rel<Abs>, -) -> SourceResult<Frame> { - let gap = row_gap.relative_to(ctx.regions.base().y); - ctx.style(ctx.style.for_denominator()); - let mut flat = vec![]; - for child in column { - flat.push(ctx.layout_row(child)?); - } - ctx.unstyle(); - Ok(stack(ctx, flat, align, gap, 0)) -} - -/// Layout the inner contents of a matrix. -fn layout_mat_body( - ctx: &mut MathContext, - rows: &[Vec<Content>], - augment: Option<Augment<Abs>>, - gap: Axes<Rel<Abs>>, - span: Span, -) -> SourceResult<Frame> { - let gap = gap.zip_map(ctx.regions.base(), Rel::relative_to); - let half_gap = gap * 0.5; - - // We provide a default stroke thickness that scales - // with font size to ensure that augmentation lines - // look correct by default at all matrix sizes. - // The line cap is also set to square because it looks more "correct". - let default_stroke_thickness = DEFAULT_STROKE_THICKNESS.scaled(ctx); - let default_stroke = FixedStroke { - thickness: default_stroke_thickness, - paint: TextElem::fill_in(ctx.styles()).as_decoration(), - line_cap: LineCap::Square, - ..Default::default() - }; - - let (hline, vline, stroke) = match augment { - Some(v) => { - // need to get stroke here for ownership - let stroke = v.stroke_or(default_stroke); - - (v.hline, v.vline, stroke) - } - _ => (Offsets::default(), Offsets::default(), default_stroke), - }; - - let ncols = rows.first().map_or(0, |row| row.len()); - let nrows = rows.len(); - if ncols == 0 || nrows == 0 { - return Ok(Frame::soft(Size::zero())); - } - - // Before the full matrix body can be laid out, the - // individual cells must first be independently laid out - // so we can ensure alignment across rows and columns. - - // This variable stores the maximum ascent and descent for each row. - let mut heights = vec![(Abs::zero(), Abs::zero()); nrows]; - - // We want to transpose our data layout to columns - // before final layout. For efficiency, the columns - // variable is set up here and newly generated - // individual cells are then added to it. - let mut cols = vec![vec![]; ncols]; - - ctx.style(ctx.style.for_denominator()); - for (row, (ascent, descent)) in rows.iter().zip(&mut heights) { - for (cell, col) in row.iter().zip(&mut cols) { - let cell = ctx.layout_row(cell)?; - - ascent.set_max(cell.ascent()); - descent.set_max(cell.descent()); - - col.push(cell); - } - } - ctx.unstyle(); - - // For each row, combine maximum ascent and descent into a row height. - // Sum the row heights, then add the total height of the gaps between rows. - let total_height = - heights.iter().map(|&(a, b)| a + b).sum::<Abs>() + gap.y * (nrows - 1) as f64; - - // Width starts at zero because it can't be calculated until later - let mut frame = Frame::soft(Size::new(Abs::zero(), total_height)); - - let mut x = Abs::zero(); - - for (index, col) in cols.into_iter().enumerate() { - let AlignmentResult { points, width: rcol } = alignments(&col); - - let mut y = Abs::zero(); - - for (cell, &(ascent, descent)) in col.into_iter().zip(&heights) { - let cell = cell.into_aligned_frame(ctx, &points, FixedAlign::Center); - let pos = Point::new( - if points.is_empty() { x + (rcol - cell.width()) / 2.0 } else { x }, - y + ascent - cell.ascent(), - ); - - frame.push_frame(pos, cell); - - y += ascent + descent + gap.y; - } - - // Advance to the end of the column - x += rcol; - - // If a vertical line should be inserted after this column - if vline.0.contains(&(index as isize + 1)) - || vline.0.contains(&(1 - ((ncols - index) as isize))) - { - frame.push( - Point::with_x(x + half_gap.x), - line_item(total_height, true, stroke.clone(), span), - ); - } - - // Advance to the start of the next column - x += gap.x; - } - - // Once all the columns are laid out, the total width can be calculated - let total_width = x - gap.x; - - // This allows the horizontal lines to be laid out - for line in hline.0 { - let real_line = - if line < 0 { nrows - line.unsigned_abs() } else { line as usize }; - let offset = (heights[0..real_line].iter().map(|&(a, b)| a + b).sum::<Abs>() - + gap.y * (real_line - 1) as f64) - + half_gap.y; - - frame.push( - Point::with_y(offset), - line_item(total_width, false, stroke.clone(), span), - ); - } - - frame.size_mut().x = total_width; - - Ok(frame) -} - -fn line_item(length: Abs, vertical: bool, stroke: FixedStroke, span: Span) -> FrameItem { - let line_geom = if vertical { - Geometry::Line(Point::with_y(length)) - } else { - Geometry::Line(Point::with_x(length)) - }; - - FrameItem::Shape( - Shape { - geometry: line_geom, - fill: None, - stroke: Some(stroke), - }, - span, - ) -} - -/// Layout the outer wrapper around the body of a vector or matrix. -fn layout_delimiters( - ctx: &mut MathContext, - mut frame: Frame, - left: Option<char>, - right: Option<char>, - span: Span, -) -> SourceResult<()> { - let axis = scaled!(ctx, axis_height); - let short_fall = DELIM_SHORT_FALL.scaled(ctx); - let height = frame.height(); - let target = height + VERTICAL_PADDING.of(height); - frame.set_baseline(height / 2.0 + axis); - - if let Some(left) = left { - let mut left = - GlyphFragment::new(ctx, left, span).stretch_vertical(ctx, target, short_fall); - left.center_on_axis(ctx); - ctx.push(left); - } - - ctx.push(FrameFragment::new(ctx, frame)); - - if let Some(right) = right { - let mut right = GlyphFragment::new(ctx, right, span) - .stretch_vertical(ctx, target, short_fall); - right.center_on_axis(ctx); - ctx.push(right); - } - - Ok(()) -} - -/// Parameters specifying how augmentation lines -/// should be drawn on a matrix. -#[derive(Debug, Default, Clone, PartialEq, Hash)] -pub struct Augment<T: Numeric = Length> { - pub hline: Offsets, - pub vline: Offsets, - pub stroke: Smart<Stroke<T>>, -} - -impl Augment<Abs> { - fn stroke_or(&self, fallback: FixedStroke) -> FixedStroke { - match &self.stroke { - Smart::Custom(v) => v.clone().unwrap_or(fallback), - Smart::Auto => fallback, - } - } -} - -impl Resolve for Augment { - type Output = Augment<Abs>; - - fn resolve(self, styles: StyleChain) -> Self::Output { - Augment { - hline: self.hline, - vline: self.vline, - stroke: self.stroke.resolve(styles), - } - } -} - -impl Fold for Augment<Abs> { - type Output = Augment<Abs>; - - fn fold(mut self, outer: Self::Output) -> Self::Output { - // Special case for handling `auto` strokes in subsequent `Augment`. - if self.stroke.is_auto() && outer.stroke.is_custom() { - self.stroke = outer.stroke; - } else { - self.stroke = self.stroke.fold(outer.stroke); - } - - self - } -} - -cast! { - Augment, - self => { - // if the stroke is auto and there is only one vertical line, - if self.stroke.is_auto() && self.hline.0.is_empty() && self.vline.0.len() == 1 { - return self.vline.0[0].into_value(); - } - - let d = dict! { - "hline" => self.hline.into_value(), - "vline" => self.vline.into_value(), - "stroke" => self.stroke.into_value() - }; - - d.into_value() - }, - v: isize => Augment { - hline: Offsets::default(), - vline: Offsets(smallvec![v]), - stroke: Smart::Auto, - }, - mut dict: Dict => { - // need the transpose for the defaults to work - let hline = dict.take("hline").ok().map(Offsets::from_value) - .transpose().unwrap_or_default().unwrap_or_default(); - let vline = dict.take("vline").ok().map(Offsets::from_value) - .transpose().unwrap_or_default().unwrap_or_default(); - - let stroke = dict.take("stroke").ok().map(Stroke::from_value) - .transpose()?.map(Smart::Custom).unwrap_or(Smart::Auto); - - Augment { hline, vline, stroke } - }, -} - -cast! { - Augment<Abs>, - self => self.into_value(), -} - -/// The offsets at which augmentation lines should be drawn on a matrix. -#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)] -pub struct Offsets(SmallVec<[isize; 1]>); - -cast! { - Offsets, - self => self.0.into_value(), - v: isize => Self(smallvec![v]), - v: Array => Self(v.into_iter().map(Value::cast).collect::<StrResult<_>>()?), -} diff --git a/crates/typst-library/src/math/mod.rs b/crates/typst-library/src/math/mod.rs deleted file mode 100644 index 7ced638b..00000000 --- a/crates/typst-library/src/math/mod.rs +++ /dev/null @@ -1,500 +0,0 @@ -//! Mathematical formulas. - -#[macro_use] -mod ctx; -mod accent; -mod align; -mod attach; -mod cancel; -mod class; -mod frac; -mod fragment; -mod lr; -mod matrix; -mod op; -mod root; -mod row; -mod spacing; -mod stretch; -mod style; -mod underover; - -pub use self::accent::*; -pub use self::align::*; -pub use self::attach::*; -pub use self::cancel::*; -pub use self::class::*; -pub use self::frac::*; -pub use self::lr::*; -pub use self::matrix::*; -pub use self::op::*; -pub use self::root::*; -pub use self::style::*; -pub use self::underover::*; - -use std::borrow::Cow; - -use ttf_parser::{GlyphId, Rect}; -use typst::eval::{Module, Scope}; -use typst::font::{Font, FontWeight}; -use typst::model::Guard; -use typst::util::option_eq; -use unicode_math_class::MathClass; - -use self::ctx::*; -use self::fragment::*; -use self::row::*; -use self::spacing::*; -use crate::layout::{AlignElem, BoxElem, HElem, ParElem, Spacing}; -use crate::meta::{ - Count, Counter, CounterUpdate, LocalNameIn, Numbering, Outlinable, Refable, - Supplement, -}; -use crate::prelude::*; -use crate::shared::BehavedBuilder; -use crate::text::{ - families, variant, FontFamily, FontList, LinebreakElem, SpaceElem, TextElem, TextSize, -}; - -/// Create a module with all math definitions. -pub fn module() -> Module { - let mut math = Scope::deduplicating(); - math.category("math"); - math.define_elem::<EquationElem>(); - math.define_elem::<TextElem>(); - math.define_elem::<LrElem>(); - math.define_elem::<AttachElem>(); - math.define_elem::<ScriptsElem>(); - math.define_elem::<LimitsElem>(); - math.define_elem::<AccentElem>(); - math.define_elem::<UnderlineElem>(); - math.define_elem::<OverlineElem>(); - math.define_elem::<UnderbraceElem>(); - math.define_elem::<OverbraceElem>(); - math.define_elem::<UnderbracketElem>(); - math.define_elem::<OverbracketElem>(); - math.define_elem::<CancelElem>(); - math.define_elem::<FracElem>(); - math.define_elem::<BinomElem>(); - math.define_elem::<VecElem>(); - math.define_elem::<MatElem>(); - math.define_elem::<CasesElem>(); - math.define_elem::<RootElem>(); - math.define_elem::<ClassElem>(); - math.define_elem::<OpElem>(); - math.define_func::<abs>(); - math.define_func::<norm>(); - math.define_func::<floor>(); - math.define_func::<ceil>(); - math.define_func::<round>(); - math.define_func::<sqrt>(); - math.define_func::<upright>(); - math.define_func::<bold>(); - math.define_func::<italic>(); - math.define_func::<serif>(); - math.define_func::<sans>(); - math.define_func::<cal>(); - math.define_func::<frak>(); - math.define_func::<mono>(); - math.define_func::<bb>(); - math.define_func::<display>(); - math.define_func::<inline>(); - math.define_func::<script>(); - math.define_func::<sscript>(); - - // Text operators, spacings, and symbols. - op::define(&mut math); - spacing::define(&mut math); - for (name, symbol) in crate::symbols::SYM { - math.define(*name, symbol.clone()); - } - - Module::new("math", math) -} - -/// A mathematical equation. -/// -/// Can be displayed inline with text or as a separate block. -/// -/// # Example -/// ```example -/// #set text(font: "New Computer Modern") -/// -/// Let $a$, $b$, and $c$ be the side -/// lengths of right-angled triangle. -/// Then, we know that: -/// $ a^2 + b^2 = c^2 $ -/// -/// Prove by induction: -/// $ sum_(k=1)^n k = (n(n+1)) / 2 $ -/// ``` -/// -/// # Syntax -/// This function also has dedicated syntax: Write mathematical markup within -/// dollar signs to create an equation. Starting and ending the equation with at -/// least one space lifts it into a separate block that is centered -/// horizontally. For more details about math syntax, see the -/// [main math page]($category/math). -#[elem( - Locatable, Synthesize, Show, Finalize, Layout, LayoutMath, Count, LocalName, Refable, - Outlinable -)] -pub struct EquationElem { - /// Whether the equation is displayed as a separate block. - #[default(false)] - pub block: bool, - - /// How to [number]($numbering) block-level equations. - /// - /// ```example - /// #set math.equation(numbering: "(1)") - /// - /// We define: - /// $ phi.alt := (1 + sqrt(5)) / 2 $ <ratio> - /// - /// With @ratio, we get: - /// $ F_n = floor(1 / sqrt(5) phi.alt^n) $ - /// ``` - pub numbering: Option<Numbering>, - - /// A supplement for the equation. - /// - /// For references to equations, this is added before the referenced number. - /// - /// If a function is specified, it is passed the referenced equation and - /// should return content. - /// - /// ```example - /// #set math.equation(numbering: "(1)", supplement: [Eq.]) - /// - /// We define: - /// $ phi.alt := (1 + sqrt(5)) / 2 $ <ratio> - /// - /// With @ratio, we get: - /// $ F_n = floor(1 / sqrt(5) phi.alt^n) $ - /// ``` - pub supplement: Smart<Option<Supplement>>, - - /// The contents of the equation. - #[required] - pub body: Content, -} - -impl Synthesize for EquationElem { - fn synthesize(&mut self, vt: &mut Vt, styles: StyleChain) -> SourceResult<()> { - // Resolve the supplement. - let supplement = match self.supplement(styles) { - Smart::Auto => TextElem::packed(Self::local_name_in(styles)), - Smart::Custom(None) => Content::empty(), - Smart::Custom(Some(supplement)) => supplement.resolve(vt, [self.clone()])?, - }; - - self.push_block(self.block(styles)); - self.push_numbering(self.numbering(styles)); - self.push_supplement(Smart::Custom(Some(Supplement::Content(supplement)))); - - Ok(()) - } -} - -impl Show for EquationElem { - #[tracing::instrument(name = "EquationElem::show", skip_all)] - fn show(&self, _: &mut Vt, styles: StyleChain) -> SourceResult<Content> { - let mut realized = self.clone().pack().guarded(Guard::Base(Self::elem())); - if self.block(styles) { - realized = AlignElem::new(realized).pack(); - } - Ok(realized) - } -} - -impl Finalize for EquationElem { - fn finalize(&self, realized: Content, style: StyleChain) -> Content { - let mut realized = realized; - if self.block(style) { - realized = realized.styled(AlignElem::set_alignment(Align::CENTER)); - } - realized - .styled(TextElem::set_weight(FontWeight::from_number(450))) - .styled(TextElem::set_font(FontList(vec![FontFamily::new( - "New Computer Modern Math", - )]))) - } -} - -impl Layout for EquationElem { - #[tracing::instrument(name = "EquationElem::layout", skip_all)] - fn layout( - &self, - vt: &mut Vt, - styles: StyleChain, - regions: Regions, - ) -> SourceResult<Fragment> { - const NUMBER_GUTTER: Em = Em::new(0.5); - - let block = self.block(styles); - - // Find a math font. - let variant = variant(styles); - let world = vt.world; - let Some(font) = families(styles).find_map(|family| { - let id = world.book().select(family, variant)?; - let font = world.font(id)?; - let _ = font.ttf().tables().math?.constants?; - Some(font) - }) else { - bail!(self.span(), "current font does not support math"); - }; - - let mut ctx = MathContext::new(vt, styles, regions, &font, block); - let mut frame = ctx.layout_frame(self)?; - - if block { - if let Some(numbering) = self.numbering(styles) { - let pod = Regions::one(regions.base(), Axes::splat(false)); - let counter = Counter::of(Self::elem()) - .display(Some(numbering), false) - .layout(vt, styles, pod)? - .into_frame(); - - let full_counter_width = counter.width() + NUMBER_GUTTER.resolve(styles); - let width = if regions.size.x.is_finite() { - regions.size.x - } else { - frame.width() + 2.0 * full_counter_width - }; - - let height = frame.height().max(counter.height()); - let align = AlignElem::alignment_in(styles).resolve(styles).x; - frame.resize(Size::new(width, height), Axes::splat(align)); - - let dir = TextElem::dir_in(styles); - let offset = match (align, dir) { - (FixedAlign::Start, Dir::RTL) => full_counter_width, - (FixedAlign::End, Dir::LTR) => -full_counter_width, - _ => Abs::zero(), - }; - frame.translate(Point::with_x(offset)); - - let x = if dir.is_positive() { - frame.width() - counter.width() - } else { - Abs::zero() - }; - let y = (frame.height() - counter.height()) / 2.0; - - frame.push_frame(Point::new(x, y), counter) - } - } else { - let font_size = TextElem::size_in(styles); - let slack = ParElem::leading_in(styles) * 0.7; - let top_edge = TextElem::top_edge_in(styles).resolve(font_size, &font, None); - let bottom_edge = - -TextElem::bottom_edge_in(styles).resolve(font_size, &font, None); - - let ascent = top_edge.max(frame.ascent() - slack); - let descent = bottom_edge.max(frame.descent() - slack); - frame.translate(Point::with_y(ascent - frame.baseline())); - frame.size_mut().y = ascent + descent; - } - - // Apply metadata. - frame.meta(styles, false); - - Ok(Fragment::frame(frame)) - } -} - -impl Count for EquationElem { - fn update(&self) -> Option<CounterUpdate> { - (self.block(StyleChain::default()) - && self.numbering(StyleChain::default()).is_some()) - .then(|| CounterUpdate::Step(NonZeroUsize::ONE)) - } -} - -impl LocalName for EquationElem { - fn local_name(lang: Lang, region: Option<Region>) -> &'static str { - match lang { - Lang::ALBANIAN => "Ekuacion", - Lang::ARABIC => "معادلة", - Lang::BOKMÅL => "Ligning", - Lang::CHINESE if option_eq(region, "TW") => "方程式", - Lang::CHINESE => "公式", - Lang::CZECH => "Rovnice", - Lang::DANISH => "Ligning", - Lang::DUTCH => "Vergelijking", - Lang::FILIPINO => "Ekwasyon", - Lang::FINNISH => "Yhtälö", - Lang::FRENCH => "Équation", - Lang::GERMAN => "Gleichung", - Lang::GREEK => "Εξίσωση", - Lang::HUNGARIAN => "Egyenlet", - Lang::ITALIAN => "Equazione", - Lang::NYNORSK => "Likning", - Lang::POLISH => "Równanie", - Lang::PORTUGUESE => "Equação", - Lang::ROMANIAN => "Ecuația", - Lang::RUSSIAN => "Уравнение", - Lang::SLOVENIAN => "Enačba", - Lang::SPANISH => "Ecuación", - Lang::SWEDISH => "Ekvation", - Lang::TURKISH => "Denklem", - Lang::UKRAINIAN => "Рівняння", - Lang::VIETNAMESE => "Phương trình", - Lang::JAPANESE => "式", - Lang::ENGLISH | _ => "Equation", - } - } -} - -impl Refable for EquationElem { - fn supplement(&self) -> Content { - // After synthesis, this should always be custom content. - match self.supplement(StyleChain::default()) { - Smart::Custom(Some(Supplement::Content(content))) => content, - _ => Content::empty(), - } - } - - fn counter(&self) -> Counter { - Counter::of(Self::elem()) - } - - fn numbering(&self) -> Option<Numbering> { - self.numbering(StyleChain::default()) - } -} - -impl Outlinable for EquationElem { - fn outline(&self, vt: &mut Vt) -> SourceResult<Option<Content>> { - if !self.block(StyleChain::default()) { - return Ok(None); - } - let Some(numbering) = self.numbering(StyleChain::default()) else { - return Ok(None); - }; - - // After synthesis, this should always be custom content. - let mut supplement = match self.supplement(StyleChain::default()) { - Smart::Custom(Some(Supplement::Content(content))) => content, - _ => Content::empty(), - }; - - if !supplement.is_empty() { - supplement += TextElem::packed("\u{a0}"); - } - - let numbers = self - .counter() - .at(vt, self.location().unwrap())? - .display(vt, &numbering)?; - - Ok(Some(supplement + numbers)) - } -} - -pub trait LayoutMath { - fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()>; -} - -impl LayoutMath for EquationElem { - #[tracing::instrument(skip(ctx))] - fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { - self.body().layout_math(ctx) - } -} - -impl LayoutMath for Content { - #[tracing::instrument(skip(ctx))] - fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { - // Directly layout the body of nested equations instead of handling it - // like a normal equation so that things like this work: - // ``` - // #let my = $pi$ - // $ my r^2 $ - // ``` - if let Some(elem) = self.to::<EquationElem>() { - return elem.layout_math(ctx); - } - - if let Some(realized) = ctx.realize(self)? { - return realized.layout_math(ctx); - } - - if self.is_sequence() { - let mut bb = BehavedBuilder::new(); - self.sequence_recursive_for_each(&mut |child: &Content| { - bb.push(Cow::Owned(child.clone()), StyleChain::default()) - }); - - for (child, _) in bb.finish().0.iter() { - child.layout_math(ctx)?; - } - return Ok(()); - } - - if let Some((elem, styles)) = self.to_styled() { - if TextElem::font_in(ctx.styles().chain(styles)) - != TextElem::font_in(ctx.styles()) - { - let frame = ctx.layout_content(self)?; - ctx.push(FrameFragment::new(ctx, frame).with_spaced(true)); - return Ok(()); - } - - let prev_map = std::mem::replace(&mut ctx.local, styles.clone()); - let prev_size = ctx.size; - ctx.local.apply(prev_map.clone()); - ctx.size = TextElem::size_in(ctx.styles()); - elem.layout_math(ctx)?; - ctx.size = prev_size; - ctx.local = prev_map; - return Ok(()); - } - - if self.is::<SpaceElem>() { - ctx.push(MathFragment::Space(ctx.space_width.scaled(ctx))); - return Ok(()); - } - - if self.is::<LinebreakElem>() { - ctx.push(MathFragment::Linebreak); - return Ok(()); - } - - if let Some(elem) = self.to::<HElem>() { - if let Spacing::Rel(rel) = elem.amount() { - if rel.rel.is_zero() { - ctx.push(MathFragment::Spacing(rel.abs.resolve(ctx.styles()))); - } - } - return Ok(()); - } - - if let Some(elem) = self.to::<TextElem>() { - let fragment = ctx.layout_text(elem)?; - ctx.push(fragment); - return Ok(()); - } - - if let Some(boxed) = self.to::<BoxElem>() { - let frame = ctx.layout_box(boxed)?; - ctx.push(FrameFragment::new(ctx, frame).with_spaced(true)); - return Ok(()); - } - - if let Some(elem) = self.with::<dyn LayoutMath>() { - return elem.layout_math(ctx); - } - - let mut frame = ctx.layout_content(self)?; - if !frame.has_baseline() { - let axis = scaled!(ctx, axis_height); - frame.set_baseline(frame.height() / 2.0 + axis); - } - ctx.push(FrameFragment::new(ctx, frame).with_spaced(true)); - - Ok(()) - } -} diff --git a/crates/typst-library/src/math/op.rs b/crates/typst-library/src/math/op.rs deleted file mode 100644 index 9e35d207..00000000 --- a/crates/typst-library/src/math/op.rs +++ /dev/null @@ -1,115 +0,0 @@ -use super::*; - -/// A text operator in an equation. -/// -/// # Example -/// ```example -/// $ tan x = (sin x)/(cos x) $ -/// $ op("custom", -/// limits: #true)_(n->oo) n $ -/// ``` -/// -/// # Predefined Operators { #predefined } -/// Typst predefines the operators `arccos`, `arcsin`, `arctan`, `arg`, `cos`, -/// `cosh`, `cot`, `coth`, `csc`, `csch`, `ctg`, `deg`, `det`, `dim`, `exp`, -/// `gcd`, `hom`, `id`, `im`, `inf`, `ker`, `lg`, `lim`, `liminf`, `limsup`, -/// `ln`, `log`, `max`, `min`, `mod`, `Pr`, `sec`, `sech`, `sin`, `sinc`, -/// `sinh`, `sup`, `tan`, `tanh`, `tg` and `tr`. -#[elem(title = "Text Operator", LayoutMath)] -pub struct OpElem { - /// The operator's text. - #[required] - pub text: Content, - - /// Whether the operator should show attachments as limits in display mode. - #[default(false)] - pub limits: bool, -} - -impl LayoutMath for OpElem { - #[tracing::instrument(skip(ctx))] - fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { - let fragment = ctx.layout_fragment(self.text())?; - ctx.push( - FrameFragment::new(ctx, fragment.into_frame()) - .with_class(MathClass::Large) - .with_limits(if self.limits(ctx.styles()) { - Limits::Display - } else { - Limits::Never - }), - ); - Ok(()) - } -} - -macro_rules! ops { - ($($name:ident $(: $value:literal)? $(($tts:tt))?),* $(,)?) => { - pub(super) fn define(math: &mut Scope) { - $({ - let operator = EcoString::from(ops!(@name $name $(: $value)?)); - math.define( - stringify!($name), - OpElem::new(TextElem::new(operator).into()) - .with_limits(ops!(@limit $($tts)*)) - .pack() - ); - })* - - let dif = |d| { - HElem::new(THIN.into()).with_weak(true).pack() - + MathStyleElem::new(TextElem::packed(d)).with_italic(Some(false)).pack() - }; - math.define("dif", dif('d')); - math.define("Dif", dif('D')); - } - }; - (@name $name:ident) => { stringify!($name) }; - (@name $name:ident: $value:literal) => { $value }; - (@limit limits) => { true }; - (@limit) => { false }; -} - -ops! { - arccos, - arcsin, - arctan, - arg, - cos, - cosh, - cot, - coth, - csc, - csch, - ctg, - deg, - det (limits), - dim, - exp, - gcd (limits), - hom, - id, - im, - inf (limits), - ker, - lg, - lim (limits), - liminf: "lim inf" (limits), - limsup: "lim sup" (limits), - ln, - log, - max (limits), - min (limits), - mod, - Pr (limits), - sec, - sech, - sin, - sinc, - sinh, - sup (limits), - tan, - tanh, - tg, - tr, -} diff --git a/crates/typst-library/src/math/root.rs b/crates/typst-library/src/math/root.rs deleted file mode 100644 index ba918ea9..00000000 --- a/crates/typst-library/src/math/root.rs +++ /dev/null @@ -1,137 +0,0 @@ -use super::*; - -/// A square root. -/// -/// ```example -/// $ sqrt(3 - 2 sqrt(2)) = sqrt(2) - 1 $ -/// ``` -#[func(title = "Square Root")] -pub fn sqrt( - /// The expression to take the square root of. - radicand: Content, -) -> Content { - RootElem::new(radicand).pack() -} - -/// A general root. -/// -/// ```example -/// $ root(3, x) $ -/// ``` -#[elem(LayoutMath)] -pub struct RootElem { - /// Which root of the radicand to take. - #[positional] - pub index: Option<Content>, - - /// The expression to take the root of. - #[required] - pub radicand: Content, -} - -impl LayoutMath for RootElem { - #[tracing::instrument(skip(ctx))] - fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { - layout(ctx, self.index(ctx.styles()).as_ref(), self.radicand(), self.span()) - } -} - -/// Layout a root. -/// -/// TeXbook page 443, page 360 -/// See also: https://www.w3.org/TR/mathml-core/#radicals-msqrt-mroot -fn layout( - ctx: &mut MathContext, - index: Option<&Content>, - radicand: &Content, - span: Span, -) -> SourceResult<()> { - let gap = scaled!( - ctx, - text: radical_vertical_gap, - display: radical_display_style_vertical_gap, - ); - let thickness = scaled!(ctx, radical_rule_thickness); - let extra_ascender = scaled!(ctx, radical_extra_ascender); - let kern_before = scaled!(ctx, radical_kern_before_degree); - let kern_after = scaled!(ctx, radical_kern_after_degree); - let raise_factor = percent!(ctx, radical_degree_bottom_raise_percent); - - // Layout radicand. - ctx.style(ctx.style.with_cramped(true)); - let radicand = ctx.layout_frame(radicand)?; - ctx.unstyle(); - - // Layout root symbol. - let target = radicand.height() + thickness + gap; - let sqrt = GlyphFragment::new(ctx, '√', span) - .stretch_vertical(ctx, target, Abs::zero()) - .frame; - - // Layout the index. - ctx.style(ctx.style.with_size(MathSize::ScriptScript)); - let index = index.map(|elem| ctx.layout_frame(elem)).transpose()?; - ctx.unstyle(); - - // TeXbook, page 443, item 11 - // Keep original gap, and then distribute any remaining free space - // equally above and below. - let gap = gap.max((sqrt.height() - thickness - radicand.height() + gap) / 2.0); - - let sqrt_ascent = radicand.ascent() + gap + thickness; - let descent = sqrt.height() - sqrt_ascent; - let inner_ascent = sqrt_ascent + extra_ascender; - - let mut sqrt_offset = Abs::zero(); - let mut shift_up = Abs::zero(); - let mut ascent = inner_ascent; - - if let Some(index) = &index { - sqrt_offset = kern_before + index.width() + kern_after; - // The formula below for how much raise the index by comes from - // the TeXbook, page 360, in the definition of `\root`. - // However, the `+ index.descent()` part is different from TeX. - // Without it, descenders can collide with the surd, a rarity - // in practice, but possible. MS Word also adjusts index positions - // for descenders. - shift_up = raise_factor * (inner_ascent - descent) + index.descent(); - ascent.set_max(shift_up + index.ascent()); - } - - let radicand_x = sqrt_offset + sqrt.width(); - let radicand_y = ascent - radicand.ascent(); - let width = radicand_x + radicand.width(); - let size = Size::new(width, ascent + descent); - - // The extra "- thickness" comes from the fact that the sqrt is placed - // in `push_frame` with respect to its top, not its baseline. - let sqrt_pos = Point::new(sqrt_offset, radicand_y - gap - thickness); - let line_pos = Point::new(radicand_x, radicand_y - gap - (thickness / 2.0)); - let radicand_pos = Point::new(radicand_x, radicand_y); - - let mut frame = Frame::soft(size); - frame.set_baseline(ascent); - - if let Some(index) = index { - let index_pos = Point::new(kern_before, ascent - index.ascent() - shift_up); - frame.push_frame(index_pos, index); - } - - frame.push_frame(sqrt_pos, sqrt); - frame.push( - line_pos, - FrameItem::Shape( - Geometry::Line(Point::with_x(radicand.width())).stroked(FixedStroke { - paint: TextElem::fill_in(ctx.styles()).as_decoration(), - thickness, - ..FixedStroke::default() - }), - span, - ), - ); - - frame.push_frame(radicand_pos, radicand); - ctx.push(FrameFragment::new(ctx, frame)); - - Ok(()) -} diff --git a/crates/typst-library/src/math/row.rs b/crates/typst-library/src/math/row.rs deleted file mode 100644 index 70813598..00000000 --- a/crates/typst-library/src/math/row.rs +++ /dev/null @@ -1,261 +0,0 @@ -use std::iter::once; - -use crate::layout::AlignElem; - -use super::*; - -pub const TIGHT_LEADING: Em = Em::new(0.25); - -#[derive(Debug, Default, Clone)] -pub struct MathRow(Vec<MathFragment>); - -impl MathRow { - pub fn new(fragments: Vec<MathFragment>) -> Self { - let iter = fragments.into_iter().peekable(); - let mut last: Option<usize> = None; - let mut space: Option<MathFragment> = None; - let mut resolved: Vec<MathFragment> = vec![]; - - for mut fragment in iter { - match fragment { - // Keep space only if supported by spaced fragments. - MathFragment::Space(_) => { - if last.is_some() { - space = Some(fragment); - } - continue; - } - - // Explicit spacing disables automatic spacing. - MathFragment::Spacing(_) => { - last = None; - space = None; - resolved.push(fragment); - continue; - } - - // Alignment points are resolved later. - MathFragment::Align => { - resolved.push(fragment); - continue; - } - - // New line, new things. - MathFragment::Linebreak => { - resolved.push(fragment); - space = None; - last = None; - continue; - } - - _ => {} - } - - // Convert variable operators into binary operators if something - // precedes them and they are not preceded by a operator or comparator. - if fragment.class() == Some(MathClass::Vary) - && matches!( - last.and_then(|i| resolved[i].class()), - Some( - MathClass::Normal - | MathClass::Alphabetic - | MathClass::Closing - | MathClass::Fence - ) - ) - { - fragment.set_class(MathClass::Binary); - } - - // Insert spacing between the last and this item. - if let Some(i) = last { - if let Some(s) = spacing(&resolved[i], space.take(), &fragment) { - resolved.insert(i + 1, s); - } - } - - last = Some(resolved.len()); - resolved.push(fragment); - } - - Self(resolved) - } - - pub fn iter(&self) -> std::slice::Iter<'_, MathFragment> { - self.0.iter() - } - - /// Extract the sublines of the row. - /// - /// It is very unintuitive, but in current state of things, a `MathRow` can - /// contain several actual rows. That function deconstructs it to "single" - /// rows. Hopefully this is only a temporary hack. - pub fn rows(&self) -> Vec<Self> { - self.0 - .split(|frag| matches!(frag, MathFragment::Linebreak)) - .map(|slice| Self(slice.to_vec())) - .collect() - } - - pub fn ascent(&self) -> Abs { - self.iter().map(MathFragment::ascent).max().unwrap_or_default() - } - - pub fn descent(&self) -> Abs { - self.iter().map(MathFragment::descent).max().unwrap_or_default() - } - - pub fn class(&self) -> MathClass { - // Predict the class of the output of 'into_fragment' - if self.0.len() == 1 { - self.0 - .first() - .and_then(|fragment| fragment.class()) - .unwrap_or(MathClass::Special) - } else { - // FrameFragment::new() (inside 'into_fragment' in this branch) defaults - // to MathClass::Normal for its class. - MathClass::Normal - } - } - - pub fn into_frame(self, ctx: &MathContext) -> Frame { - let styles = ctx.styles(); - let align = AlignElem::alignment_in(styles).resolve(styles).x; - self.into_aligned_frame(ctx, &[], align) - } - - pub fn into_fragment(self, ctx: &MathContext) -> MathFragment { - if self.0.len() == 1 { - self.0.into_iter().next().unwrap() - } else { - FrameFragment::new(ctx, self.into_frame(ctx)).into() - } - } - - pub fn into_aligned_frame( - self, - ctx: &MathContext, - points: &[Abs], - align: FixedAlign, - ) -> Frame { - if !self.iter().any(|frag| matches!(frag, MathFragment::Linebreak)) { - return self.into_line_frame(points, align); - } - - let leading = if ctx.style.size >= MathSize::Text { - ParElem::leading_in(ctx.styles()) - } else { - TIGHT_LEADING.scaled(ctx) - }; - - let mut rows: Vec<_> = self.rows(); - - if matches!(rows.last(), Some(row) if row.0.is_empty()) { - rows.pop(); - } - - let AlignmentResult { points, width } = alignments(&rows); - let mut frame = Frame::soft(Size::zero()); - - for (i, row) in rows.into_iter().enumerate() { - let sub = row.into_line_frame(&points, align); - let size = frame.size_mut(); - if i > 0 { - size.y += leading; - } - - let mut pos = Point::with_y(size.y); - if points.is_empty() { - pos.x = align.position(width - sub.width()); - } - size.y += sub.height(); - size.x.set_max(sub.width()); - frame.push_frame(pos, sub); - } - - frame - } - - fn into_line_frame(self, points: &[Abs], align: FixedAlign) -> Frame { - let ascent = self.ascent(); - let mut frame = Frame::soft(Size::new(Abs::zero(), ascent + self.descent())); - frame.set_baseline(ascent); - - let mut next_x = { - let mut widths = Vec::new(); - if !points.is_empty() && align != FixedAlign::Start { - let mut width = Abs::zero(); - for fragment in self.iter() { - if matches!(fragment, MathFragment::Align) { - widths.push(width); - width = Abs::zero(); - } else { - width += fragment.width(); - } - } - widths.push(width); - } - let widths = widths; - - let mut prev_points = once(Abs::zero()).chain(points.iter().copied()); - let mut point_widths = points.iter().copied().zip(widths); - let mut alternator = LeftRightAlternator::Right; - move || match align { - FixedAlign::Start => prev_points.next(), - FixedAlign::End => { - point_widths.next().map(|(point, width)| point - width) - } - _ => point_widths - .next() - .zip(prev_points.next()) - .zip(alternator.next()) - .map(|(((point, width), prev_point), alternator)| match alternator { - LeftRightAlternator::Left => prev_point, - LeftRightAlternator::Right => point - width, - }), - } - }; - let mut x = next_x().unwrap_or_default(); - - for fragment in self.0.into_iter() { - if matches!(fragment, MathFragment::Align) { - x = next_x().unwrap_or(x); - continue; - } - - let y = ascent - fragment.ascent(); - let pos = Point::new(x, y); - x += fragment.width(); - frame.push_frame(pos, fragment.into_frame()); - } - - frame.size_mut().x = x; - frame - } -} - -impl<T: Into<MathFragment>> From<T> for MathRow { - fn from(fragment: T) -> Self { - Self(vec![fragment.into()]) - } -} - -#[derive(Debug, Copy, Clone, Eq, PartialEq)] -enum LeftRightAlternator { - Left, - Right, -} - -impl Iterator for LeftRightAlternator { - type Item = LeftRightAlternator; - - fn next(&mut self) -> Option<Self::Item> { - let r = Some(*self); - match self { - Self::Left => *self = Self::Right, - Self::Right => *self = Self::Left, - } - r - } -} diff --git a/crates/typst-library/src/math/spacing.rs b/crates/typst-library/src/math/spacing.rs deleted file mode 100644 index 3dfce024..00000000 --- a/crates/typst-library/src/math/spacing.rs +++ /dev/null @@ -1,63 +0,0 @@ -use super::*; - -pub(super) const THIN: Em = Em::new(1.0 / 6.0); -pub(super) const MEDIUM: Em = Em::new(2.0 / 9.0); -pub(super) const THICK: Em = Em::new(5.0 / 18.0); -pub(super) const QUAD: Em = Em::new(1.0); -pub(super) const WIDE: Em = Em::new(2.0); - -/// Hook up all spacings. -pub(super) fn define(math: &mut Scope) { - math.define("thin", HElem::new(THIN.into()).pack()); - math.define("med", HElem::new(MEDIUM.into()).pack()); - math.define("thick", HElem::new(THICK.into()).pack()); - math.define("quad", HElem::new(QUAD.into()).pack()); - math.define("wide", HElem::new(WIDE.into()).pack()); -} - -/// Create the spacing between two fragments in a given style. -pub(super) fn spacing( - l: &MathFragment, - space: Option<MathFragment>, - r: &MathFragment, -) -> Option<MathFragment> { - use MathClass::*; - - let class = |f: &MathFragment| f.class().unwrap_or(Special); - let resolve = |v: Em, f: &MathFragment| { - Some(MathFragment::Spacing(f.font_size().map_or(Abs::zero(), |size| v.at(size)))) - }; - let script = - |f: &MathFragment| f.style().map_or(false, |s| s.size <= MathSize::Script); - - match (class(l), class(r)) { - // No spacing before punctuation; thin spacing after punctuation, unless - // in script size. - (_, Punctuation) => None, - (Punctuation, _) if !script(l) => resolve(THIN, l), - - // No spacing after opening delimiters and before closing delimiters. - (Opening, _) | (_, Closing) => None, - - // Thick spacing around relations, unless followed by a another relation - // or in script size. - (Relation, Relation) => None, - (Relation, _) if !script(l) => resolve(THICK, l), - (_, Relation) if !script(r) => resolve(THICK, r), - - // Medium spacing around binary operators, unless in script size. - (Binary, _) if !script(l) => resolve(MEDIUM, l), - (_, Binary) if !script(r) => resolve(MEDIUM, r), - - // Thin spacing around large operators, unless to the left of - // an opening delimiter. TeXBook, p170 - (Large, Opening | Fence) => None, - (Large, _) => resolve(THIN, l), - (_, Large) => resolve(THIN, r), - - // Spacing around spaced frames. - _ if (l.is_spaced() || r.is_spaced()) => space, - - _ => None, - } -} diff --git a/crates/typst-library/src/math/stretch.rs b/crates/typst-library/src/math/stretch.rs deleted file mode 100644 index e9bf6890..00000000 --- a/crates/typst-library/src/math/stretch.rs +++ /dev/null @@ -1,199 +0,0 @@ -use ttf_parser::math::{GlyphAssembly, GlyphConstruction, GlyphPart}; -use ttf_parser::LazyArray16; - -use super::*; - -/// Maximum number of times extenders can be repeated. -const MAX_REPEATS: usize = 1024; - -impl GlyphFragment { - /// Try to stretch a glyph to a desired height. - pub fn stretch_vertical( - self, - ctx: &MathContext, - height: Abs, - short_fall: Abs, - ) -> VariantFragment { - stretch_glyph(ctx, self, height, short_fall, false) - } - - /// Try to stretch a glyph to a desired width. - pub fn stretch_horizontal( - self, - ctx: &MathContext, - width: Abs, - short_fall: Abs, - ) -> VariantFragment { - stretch_glyph(ctx, self, width, short_fall, true) - } -} - -/// Try to stretch a glyph to a desired width or height. -/// -/// The resulting frame may not have the exact desired width. -fn stretch_glyph( - ctx: &MathContext, - mut base: GlyphFragment, - target: Abs, - short_fall: Abs, - horizontal: bool, -) -> VariantFragment { - let short_target = target - short_fall; - let mut min_overlap = Abs::zero(); - let construction = ctx - .table - .variants - .and_then(|variants| { - min_overlap = variants.min_connector_overlap.scaled(ctx); - if horizontal { - variants.horizontal_constructions - } else { - variants.vertical_constructions - } - .get(base.id) - }) - .unwrap_or(GlyphConstruction { assembly: None, variants: LazyArray16::new(&[]) }); - - // If the base glyph is good enough, use it. - let advance = if horizontal { base.width } else { base.height() }; - if short_target <= advance { - return base.into_variant(); - } - - // Search for a pre-made variant with a good advance. - let mut best_id = base.id; - let mut best_advance = base.width; - for variant in construction.variants { - best_id = variant.variant_glyph; - best_advance = base.font.to_em(variant.advance_measurement).at(base.font_size); - if short_target <= best_advance { - break; - } - } - - // This is either good or the best we've got. - if short_target <= best_advance || construction.assembly.is_none() { - base.set_id(ctx, best_id); - return base.into_variant(); - } - - // Assemble from parts. - let assembly = construction.assembly.unwrap(); - assemble(ctx, base, assembly, min_overlap, target, horizontal) -} - -/// Assemble a glyph from parts. -fn assemble( - ctx: &MathContext, - base: GlyphFragment, - assembly: GlyphAssembly, - min_overlap: Abs, - target: Abs, - horizontal: bool, -) -> VariantFragment { - // Determine the number of times the extenders need to be repeated as well - // as a ratio specifying how much to spread the parts apart - // (0 = maximal overlap, 1 = minimal overlap). - let mut full; - let mut ratio; - let mut repeat = 0; - loop { - full = Abs::zero(); - ratio = 0.0; - - let mut parts = parts(assembly, repeat).peekable(); - let mut growable = Abs::zero(); - - while let Some(part) = parts.next() { - let mut advance = part.full_advance.scaled(ctx); - if let Some(next) = parts.peek() { - let max_overlap = part - .end_connector_length - .min(next.start_connector_length) - .scaled(ctx); - - advance -= max_overlap; - growable += max_overlap - min_overlap; - } - - full += advance; - } - - if full < target { - let delta = target - full; - ratio = (delta / growable).min(1.0); - full += ratio * growable; - } - - if target <= full || repeat >= MAX_REPEATS { - break; - } - - repeat += 1; - } - - let mut selected = vec![]; - let mut parts = parts(assembly, repeat).peekable(); - while let Some(part) = parts.next() { - let mut advance = part.full_advance.scaled(ctx); - if let Some(next) = parts.peek() { - let max_overlap = - part.end_connector_length.min(next.start_connector_length).scaled(ctx); - advance -= max_overlap; - advance += ratio * (max_overlap - min_overlap); - } - - let mut fragment = base.clone(); - fragment.set_id(ctx, part.glyph_id); - selected.push((fragment, advance)); - } - - let size; - let baseline; - if horizontal { - let height = base.ascent + base.descent; - size = Size::new(full, height); - baseline = base.ascent; - } else { - let axis = scaled!(ctx, axis_height); - let width = selected.iter().map(|(f, _)| f.width).max().unwrap_or_default(); - size = Size::new(width, full); - baseline = full / 2.0 + axis; - } - - let mut frame = Frame::soft(size); - let mut offset = Abs::zero(); - frame.set_baseline(baseline); - frame.meta_iter(base.meta); - - for (fragment, advance) in selected { - let pos = if horizontal { - Point::new(offset, frame.baseline() - fragment.ascent) - } else { - Point::with_y(full - offset - fragment.height()) - }; - frame.push_frame(pos, fragment.into_frame()); - offset += advance; - } - - VariantFragment { - c: base.c, - id: None, - frame, - style: base.style, - font_size: base.font_size, - italics_correction: Abs::zero(), - class: base.class, - span: base.span, - limits: base.limits, - } -} - -/// Return an iterator over the assembly's parts with extenders repeated the -/// specified number of times. -fn parts(assembly: GlyphAssembly, repeat: usize) -> impl Iterator<Item = GlyphPart> + '_ { - assembly.parts.into_iter().flat_map(move |part| { - let count = if part.part_flags.extender() { repeat } else { 1 }; - std::iter::repeat(part).take(count) - }) -} diff --git a/crates/typst-library/src/math/style.rs b/crates/typst-library/src/math/style.rs deleted file mode 100644 index 774fadac..00000000 --- a/crates/typst-library/src/math/style.rs +++ /dev/null @@ -1,574 +0,0 @@ -use super::*; - -/// Bold font style in math. -/// -/// ```example -/// $ bold(A) := B^+ $ -/// ``` -#[func] -pub fn bold( - /// The content to style. - body: Content, -) -> Content { - MathStyleElem::new(body).with_bold(Some(true)).pack() -} - -/// Upright (non-italic) font style in math. -/// -/// ```example -/// $ upright(A) != A $ -/// ``` -#[func] -pub fn upright( - /// The content to style. - body: Content, -) -> Content { - MathStyleElem::new(body).with_italic(Some(false)).pack() -} - -/// Italic font style in math. -/// -/// For roman letters and greek lowercase letters, this is already the default. -#[func] -pub fn italic( - /// The content to style. - body: Content, -) -> Content { - MathStyleElem::new(body).with_italic(Some(true)).pack() -} -/// Serif (roman) font style in math. -/// -/// This is already the default. -#[func] -pub fn serif( - /// The content to style. - body: Content, -) -> Content { - MathStyleElem::new(body).with_variant(Some(MathVariant::Serif)).pack() -} - -/// Sans-serif font style in math. -/// -/// ```example -/// $ sans(A B C) $ -/// ``` -#[func(title = "Sans Serif")] -pub fn sans( - /// The content to style. - body: Content, -) -> Content { - MathStyleElem::new(body).with_variant(Some(MathVariant::Sans)).pack() -} - -/// Calligraphic font style in math. -/// -/// ```example -/// Let $cal(P)$ be the set of ... -/// ``` -#[func(title = "Calligraphic")] -pub fn cal( - /// The content to style. - body: Content, -) -> Content { - MathStyleElem::new(body).with_variant(Some(MathVariant::Cal)).pack() -} - -/// Fraktur font style in math. -/// -/// ```example -/// $ frak(P) $ -/// ``` -#[func(title = "Fraktur")] -pub fn frak( - /// The content to style. - body: Content, -) -> Content { - MathStyleElem::new(body).with_variant(Some(MathVariant::Frak)).pack() -} - -/// Monospace font style in math. -/// -/// ```example -/// $ mono(x + y = z) $ -/// ``` -#[func(title = "Monospace")] -pub fn mono( - /// The content to style. - body: Content, -) -> Content { - MathStyleElem::new(body).with_variant(Some(MathVariant::Mono)).pack() -} - -/// Blackboard bold (double-struck) font style in math. -/// -/// For uppercase latin letters, blackboard bold is additionally available -/// through [symbols]($category/symbols/sym) of the form `NN` and `RR`. -/// -/// ```example -/// $ bb(b) $ -/// $ bb(N) = NN $ -/// $ f: NN -> RR $ -/// ``` -#[func(title = "Blackboard Bold")] -pub fn bb( - /// The content to style. - body: Content, -) -> Content { - MathStyleElem::new(body).with_variant(Some(MathVariant::Bb)).pack() -} - -/// Forced display style in math. -/// -/// This is the normal size for block equations. -/// -/// ```example -/// $sum_i x_i/2 = display(sum_i x_i/2)$ -/// ``` -#[func(title = "Display Size")] -pub fn display( - /// The content to size. - body: Content, - /// Whether to impose a height restriction for exponents, like regular sub- - /// and superscripts do. - #[named] - #[default(false)] - cramped: bool, -) -> Content { - MathStyleElem::new(body) - .with_size(Some(MathSize::Display)) - .with_cramped(Some(cramped)) - .pack() -} - -/// Forced inline (text) style in math. -/// -/// This is the normal size for inline equations. -/// -/// ```example -/// $ sum_i x_i/2 -/// = inline(sum_i x_i/2) $ -/// ``` -#[func(title = "Inline Size")] -pub fn inline( - /// The content to size. - body: Content, - /// Whether to impose a height restriction for exponents, like regular sub- - /// and superscripts do. - #[named] - #[default(false)] - cramped: bool, -) -> Content { - MathStyleElem::new(body) - .with_size(Some(MathSize::Text)) - .with_cramped(Some(cramped)) - .pack() -} - -/// Forced script style in math. -/// -/// This is the smaller size used in powers or sub- or superscripts. -/// -/// ```example -/// $sum_i x_i/2 = script(sum_i x_i/2)$ -/// ``` -#[func(title = "Script Size")] -pub fn script( - /// The content to size. - body: Content, - /// Whether to impose a height restriction for exponents, like regular sub- - /// and superscripts do. - #[named] - #[default(true)] - cramped: bool, -) -> Content { - MathStyleElem::new(body) - .with_size(Some(MathSize::Script)) - .with_cramped(Some(cramped)) - .pack() -} - -/// Forced second script style in math. -/// -/// This is the smallest size, used in second-level sub- and superscripts -/// (script of the script). -/// -/// ```example -/// $sum_i x_i/2 = sscript(sum_i x_i/2)$ -/// ``` -#[func(title = "Script-Script Size")] -pub fn sscript( - /// The content to size. - body: Content, - /// Whether to impose a height restriction for exponents, like regular sub- - /// and superscripts do. - #[named] - #[default(true)] - cramped: bool, -) -> Content { - MathStyleElem::new(body) - .with_size(Some(MathSize::ScriptScript)) - .with_cramped(Some(cramped)) - .pack() -} - -/// A font variant in math. -#[elem(LayoutMath)] -pub struct MathStyleElem { - /// The content to style. - #[required] - pub body: Content, - - /// The variant to select. - pub variant: Option<MathVariant>, - - /// Whether to use bold glyphs. - pub bold: Option<bool>, - - /// Whether to use italic glyphs. - pub italic: Option<bool>, - - /// Whether to use forced size - pub size: Option<MathSize>, - - /// Whether to limit height of exponents - pub cramped: Option<bool>, -} - -impl LayoutMath for MathStyleElem { - #[tracing::instrument(skip(ctx))] - fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { - let mut style = ctx.style; - if let Some(variant) = self.variant(StyleChain::default()) { - style = style.with_variant(variant); - } - if let Some(bold) = self.bold(StyleChain::default()) { - style = style.with_bold(bold); - } - if let Some(italic) = self.italic(StyleChain::default()) { - style = style.with_italic(italic); - } - if let Some(size) = self.size(StyleChain::default()) { - style = style.with_size(size); - } - if let Some(cramped) = self.cramped(StyleChain::default()) { - style = style.with_cramped(cramped); - } - ctx.style(style); - self.body().layout_math(ctx)?; - ctx.unstyle(); - Ok(()) - } -} - -/// Text properties in math. -#[derive(Debug, Copy, Clone, Eq, PartialEq)] -pub struct MathStyle { - /// The style variant to select. - pub variant: MathVariant, - /// The size of the glyphs. - pub size: MathSize, - /// The class of the element. - pub class: Smart<MathClass>, - /// Affects the height of exponents. - pub cramped: bool, - /// Whether to use bold glyphs. - pub bold: bool, - /// Whether to use italic glyphs. - pub italic: Smart<bool>, -} - -impl MathStyle { - /// This style, with the given `variant`. - pub fn with_variant(self, variant: MathVariant) -> Self { - Self { variant, ..self } - } - - /// This style, with the given `size`. - pub fn with_size(self, size: MathSize) -> Self { - Self { size, ..self } - } - - // This style, with the given `class`. - pub fn with_class(self, class: MathClass) -> Self { - Self { class: Smart::Custom(class), ..self } - } - - /// This style, with `cramped` set to the given value. - pub fn with_cramped(self, cramped: bool) -> Self { - Self { cramped, ..self } - } - - /// This style, with `bold` set to the given value. - pub fn with_bold(self, bold: bool) -> Self { - Self { bold, ..self } - } - - /// This style, with `italic` set to the given value. - pub fn with_italic(self, italic: bool) -> Self { - Self { italic: Smart::Custom(italic), ..self } - } - - /// The style for subscripts in the current style. - pub fn for_subscript(self) -> Self { - self.for_superscript().with_cramped(true) - } - - /// The style for superscripts in the current style. - pub fn for_superscript(self) -> Self { - self.with_size(match self.size { - MathSize::Display | MathSize::Text => MathSize::Script, - MathSize::Script | MathSize::ScriptScript => MathSize::ScriptScript, - }) - } - - /// The style for numerators in the current style. - pub fn for_numerator(self) -> Self { - self.with_size(match self.size { - MathSize::Display => MathSize::Text, - MathSize::Text => MathSize::Script, - MathSize::Script | MathSize::ScriptScript => MathSize::ScriptScript, - }) - } - - /// The style for denominators in the current style. - pub fn for_denominator(self) -> Self { - self.for_numerator().with_cramped(true) - } - - /// Apply the style to a character. - pub fn styled_char(self, c: char) -> char { - styled_char(self, c) - } -} - -/// The size of elements in an equation. -/// -/// See the TeXbook p. 141. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Cast, Hash)] -pub enum MathSize { - /// Second-level sub- and superscripts. - ScriptScript, - /// Sub- and superscripts. - Script, - /// Math in text. - Text, - /// Math on its own line. - Display, -} - -impl MathSize { - pub(super) fn factor(self, ctx: &MathContext) -> f64 { - match self { - Self::Display | Self::Text => 1.0, - Self::Script => percent!(ctx, script_percent_scale_down), - Self::ScriptScript => percent!(ctx, script_script_percent_scale_down), - } - } -} - -/// A mathematical style variant, as defined by Unicode. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Cast, Hash)] -pub enum MathVariant { - Serif, - Sans, - Cal, - Frak, - Mono, - Bb, -} - -impl Default for MathVariant { - fn default() -> Self { - Self::Serif - } -} - -/// Select the correct styled math letter. -/// -/// https://www.w3.org/TR/mathml-core/#new-text-transform-mappings -/// https://en.wikipedia.org/wiki/Mathematical_Alphanumeric_Symbols -pub(super) fn styled_char(style: MathStyle, c: char) -> char { - use MathVariant::*; - - let MathStyle { variant, bold, .. } = style; - let italic = style.italic.unwrap_or(matches!( - c, - 'a'..='z' | 'ı' | 'ȷ' | 'A'..='Z' | 'α'..='ω' | - '∂' | 'ϵ' | 'ϑ' | 'ϰ' | 'ϕ' | 'ϱ' | 'ϖ' - )); - - if let Some(c) = basic_exception(c) { - return c; - } - - if let Some(c) = latin_exception(c, variant, bold, italic) { - return c; - } - - if let Some(c) = greek_exception(c, variant, bold, italic) { - return c; - } - - let base = match c { - 'A'..='Z' => 'A', - 'a'..='z' => 'a', - 'Α'..='Ω' => 'Α', - 'α'..='ω' => 'α', - '0'..='9' => '0', - _ => return c, - }; - - let tuple = (variant, bold, italic); - let start = match c { - // Latin upper. - 'A'..='Z' => match tuple { - (Serif, false, false) => 0x0041, - (Serif, true, false) => 0x1D400, - (Serif, false, true) => 0x1D434, - (Serif, true, true) => 0x1D468, - (Sans, false, false) => 0x1D5A0, - (Sans, true, false) => 0x1D5D4, - (Sans, false, true) => 0x1D608, - (Sans, true, true) => 0x1D63C, - (Cal, false, _) => 0x1D49C, - (Cal, true, _) => 0x1D4D0, - (Frak, false, _) => 0x1D504, - (Frak, true, _) => 0x1D56C, - (Mono, _, _) => 0x1D670, - (Bb, _, _) => 0x1D538, - }, - - // Latin lower. - 'a'..='z' => match tuple { - (Serif, false, false) => 0x0061, - (Serif, true, false) => 0x1D41A, - (Serif, false, true) => 0x1D44E, - (Serif, true, true) => 0x1D482, - (Sans, false, false) => 0x1D5BA, - (Sans, true, false) => 0x1D5EE, - (Sans, false, true) => 0x1D622, - (Sans, true, true) => 0x1D656, - (Cal, false, _) => 0x1D4B6, - (Cal, true, _) => 0x1D4EA, - (Frak, false, _) => 0x1D51E, - (Frak, true, _) => 0x1D586, - (Mono, _, _) => 0x1D68A, - (Bb, _, _) => 0x1D552, - }, - - // Greek upper. - 'Α'..='Ω' => match tuple { - (Serif, false, false) => 0x0391, - (Serif, true, false) => 0x1D6A8, - (Serif, false, true) => 0x1D6E2, - (Serif, true, true) => 0x1D71C, - (Sans, _, false) => 0x1D756, - (Sans, _, true) => 0x1D790, - (Cal | Frak | Mono | Bb, _, _) => return c, - }, - - // Greek lower. - 'α'..='ω' => match tuple { - (Serif, false, false) => 0x03B1, - (Serif, true, false) => 0x1D6C2, - (Serif, false, true) => 0x1D6FC, - (Serif, true, true) => 0x1D736, - (Sans, _, false) => 0x1D770, - (Sans, _, true) => 0x1D7AA, - (Cal | Frak | Mono | Bb, _, _) => return c, - }, - - // Numbers. - '0'..='9' => match tuple { - (Serif, false, _) => 0x0030, - (Serif, true, _) => 0x1D7CE, - (Bb, _, _) => 0x1D7D8, - (Sans, false, _) => 0x1D7E2, - (Sans, true, _) => 0x1D7EC, - (Mono, _, _) => 0x1D7F6, - (Cal | Frak, _, _) => return c, - }, - - _ => unreachable!(), - }; - - std::char::from_u32(start + (c as u32 - base as u32)).unwrap() -} - -fn basic_exception(c: char) -> Option<char> { - Some(match c { - '〈' => '⟨', - '〉' => '⟩', - '《' => '⟪', - '》' => '⟫', - _ => return None, - }) -} - -fn latin_exception( - c: char, - variant: MathVariant, - bold: bool, - italic: bool, -) -> Option<char> { - use MathVariant::*; - Some(match (c, variant, bold, italic) { - ('B', Cal, false, _) => 'ℬ', - ('E', Cal, false, _) => 'ℰ', - ('F', Cal, false, _) => 'ℱ', - ('H', Cal, false, _) => 'ℋ', - ('I', Cal, false, _) => 'ℐ', - ('L', Cal, false, _) => 'ℒ', - ('M', Cal, false, _) => 'ℳ', - ('R', Cal, false, _) => 'ℛ', - ('C', Frak, false, _) => 'ℭ', - ('H', Frak, false, _) => 'ℌ', - ('I', Frak, false, _) => 'ℑ', - ('R', Frak, false, _) => 'ℜ', - ('Z', Frak, false, _) => 'ℨ', - ('C', Bb, ..) => 'ℂ', - ('H', Bb, ..) => 'ℍ', - ('N', Bb, ..) => 'ℕ', - ('P', Bb, ..) => 'ℙ', - ('Q', Bb, ..) => 'ℚ', - ('R', Bb, ..) => 'ℝ', - ('Z', Bb, ..) => 'ℤ', - ('h', Serif, false, true) => 'ℎ', - ('e', Cal, false, _) => 'ℯ', - ('g', Cal, false, _) => 'ℊ', - ('o', Cal, false, _) => 'ℴ', - ('ı', Serif, .., true) => '𝚤', - ('ȷ', Serif, .., true) => '𝚥', - _ => return None, - }) -} - -fn greek_exception( - c: char, - variant: MathVariant, - bold: bool, - italic: bool, -) -> Option<char> { - use MathVariant::*; - let list = match c { - 'ϴ' => ['𝚹', '𝛳', '𝜭', '𝝧', '𝞡'], - '∇' => ['𝛁', '𝛻', '𝜵', '𝝯', '𝞩'], - '∂' => ['𝛛', '𝜕', '𝝏', '𝞉', '𝟃'], - 'ϵ' => ['𝛜', '𝜖', '𝝐', '𝞊', '𝟄'], - 'ϑ' => ['𝛝', '𝜗', '𝝑', '𝞋', '𝟅'], - 'ϰ' => ['𝛞', '𝜘', '𝝒', '𝞌', '𝟆'], - 'ϕ' => ['𝛟', '𝜙', '𝝓', '𝞍', '𝟇'], - 'ϱ' => ['𝛠', '𝜚', '𝝔', '𝞎', '𝟈'], - 'ϖ' => ['𝛡', '𝜛', '𝝕', '𝞏', '𝟉'], - _ => return None, - }; - - Some(match (variant, bold, italic) { - (Serif, true, false) => list[0], - (Serif, false, true) => list[1], - (Serif, true, true) => list[2], - (Sans, _, false) => list[3], - (Sans, _, true) => list[4], - _ => return None, - }) -} diff --git a/crates/typst-library/src/math/underover.rs b/crates/typst-library/src/math/underover.rs deleted file mode 100644 index 6fc76830..00000000 --- a/crates/typst-library/src/math/underover.rs +++ /dev/null @@ -1,315 +0,0 @@ -use super::*; - -const BRACE_GAP: Em = Em::new(0.25); -const BRACKET_GAP: Em = Em::new(0.25); - -/// A marker to distinguish under- vs. overlines. -enum LineKind { - Over, - Under, -} - -/// A horizontal line under content. -/// -/// ```example -/// $ underline(1 + 2 + ... + 5) $ -/// ``` -#[elem(LayoutMath)] -pub struct UnderlineElem { - /// The content above the line. - #[required] - pub body: Content, -} - -impl LayoutMath for UnderlineElem { - #[tracing::instrument(skip(ctx))] - fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { - layout_underoverline(ctx, self.body(), self.span(), LineKind::Under) - } -} - -/// A horizontal line over content. -/// -/// ```example -/// $ overline(1 + 2 + ... + 5) $ -/// ``` -#[elem(LayoutMath)] -pub struct OverlineElem { - /// The content below the line. - #[required] - pub body: Content, -} - -impl LayoutMath for OverlineElem { - #[tracing::instrument(skip(ctx))] - fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { - layout_underoverline(ctx, self.body(), self.span(), LineKind::Over) - } -} - -/// layout under- or overlined content -fn layout_underoverline( - ctx: &mut MathContext, - body: &Content, - span: Span, - line: LineKind, -) -> SourceResult<()> { - let (extra_height, content, line_pos, content_pos, baseline, bar_height); - match line { - LineKind::Under => { - let sep = scaled!(ctx, underbar_extra_descender); - bar_height = scaled!(ctx, underbar_rule_thickness); - let gap = scaled!(ctx, underbar_vertical_gap); - extra_height = sep + bar_height + gap; - - content = ctx.layout_fragment(body)?; - - line_pos = Point::with_y(content.height() + gap + bar_height / 2.0); - content_pos = Point::zero(); - baseline = content.ascent() - } - LineKind::Over => { - let sep = scaled!(ctx, overbar_extra_ascender); - bar_height = scaled!(ctx, overbar_rule_thickness); - let gap = scaled!(ctx, overbar_vertical_gap); - extra_height = sep + bar_height + gap; - - ctx.style(ctx.style.with_cramped(true)); - content = ctx.layout_fragment(body)?; - ctx.unstyle(); - - line_pos = Point::with_y(sep + bar_height / 2.0); - content_pos = Point::with_y(extra_height); - baseline = content.ascent() + extra_height; - } - } - - let width = content.width(); - let height = content.height() + extra_height; - let size = Size::new(width, height); - - let content_class = content.class().unwrap_or(MathClass::Normal); - let mut frame = Frame::soft(size); - frame.set_baseline(baseline); - frame.push_frame(content_pos, content.into_frame()); - frame.push( - line_pos, - FrameItem::Shape( - Geometry::Line(Point::with_x(width)).stroked(FixedStroke { - paint: TextElem::fill_in(ctx.styles()).as_decoration(), - thickness: bar_height, - ..FixedStroke::default() - }), - span, - ), - ); - - ctx.push(FrameFragment::new(ctx, frame).with_class(content_class)); - - Ok(()) -} - -/// A horizontal brace under content, with an optional annotation below. -/// -/// ```example -/// $ underbrace(1 + 2 + ... + 5, "numbers") $ -/// ``` -#[elem(LayoutMath)] -pub struct UnderbraceElem { - /// The content above the brace. - #[required] - pub body: Content, - - /// The optional content below the brace. - #[positional] - pub annotation: Option<Content>, -} - -impl LayoutMath for UnderbraceElem { - #[tracing::instrument(skip(ctx))] - fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { - layout_underoverspreader( - ctx, - self.body(), - &self.annotation(ctx.styles()), - '⏟', - BRACE_GAP, - false, - self.span(), - ) - } -} - -/// A horizontal brace over content, with an optional annotation above. -/// -/// ```example -/// $ overbrace(1 + 2 + ... + 5, "numbers") $ -/// ``` -#[elem(LayoutMath)] -pub struct OverbraceElem { - /// The content below the brace. - #[required] - pub body: Content, - - /// The optional content above the brace. - #[positional] - pub annotation: Option<Content>, -} - -impl LayoutMath for OverbraceElem { - #[tracing::instrument(skip(ctx))] - fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { - layout_underoverspreader( - ctx, - self.body(), - &self.annotation(ctx.styles()), - '⏞', - BRACE_GAP, - true, - self.span(), - ) - } -} - -/// A horizontal bracket under content, with an optional annotation below. -/// -/// ```example -/// $ underbracket(1 + 2 + ... + 5, "numbers") $ -/// ``` -#[elem(LayoutMath)] -pub struct UnderbracketElem { - /// The content above the bracket. - #[required] - pub body: Content, - - /// The optional content below the bracket. - #[positional] - pub annotation: Option<Content>, -} - -impl LayoutMath for UnderbracketElem { - #[tracing::instrument(skip(ctx))] - fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { - layout_underoverspreader( - ctx, - self.body(), - &self.annotation(ctx.styles()), - '⎵', - BRACKET_GAP, - false, - self.span(), - ) - } -} - -/// A horizontal bracket over content, with an optional annotation above. -/// -/// ```example -/// $ overbracket(1 + 2 + ... + 5, "numbers") $ -/// ``` -#[elem(LayoutMath)] -pub struct OverbracketElem { - /// The content below the bracket. - #[required] - pub body: Content, - - /// The optional content above the bracket. - #[positional] - pub annotation: Option<Content>, -} - -impl LayoutMath for OverbracketElem { - #[tracing::instrument(skip(ctx))] - fn layout_math(&self, ctx: &mut MathContext) -> SourceResult<()> { - layout_underoverspreader( - ctx, - self.body(), - &self.annotation(ctx.styles()), - '⎴', - BRACKET_GAP, - true, - self.span(), - ) - } -} - -/// Layout an over- or underbrace-like object. -fn layout_underoverspreader( - ctx: &mut MathContext, - body: &Content, - annotation: &Option<Content>, - c: char, - gap: Em, - reverse: bool, - span: Span, -) -> SourceResult<()> { - let gap = gap.scaled(ctx); - let body = ctx.layout_row(body)?; - let body_class = body.class(); - let body = body.into_fragment(ctx); - let glyph = GlyphFragment::new(ctx, c, span); - let stretched = glyph.stretch_horizontal(ctx, body.width(), Abs::zero()); - - let mut rows = vec![MathRow::new(vec![body]), stretched.into()]; - ctx.style(if reverse { - ctx.style.for_subscript() - } else { - ctx.style.for_superscript() - }); - rows.extend( - annotation - .as_ref() - .map(|annotation| ctx.layout_row(annotation)) - .transpose()?, - ); - ctx.unstyle(); - - let mut baseline = 0; - if reverse { - rows.reverse(); - baseline = rows.len() - 1; - } - - let frame = stack(ctx, rows, FixedAlign::Center, gap, baseline); - ctx.push(FrameFragment::new(ctx, frame).with_class(body_class)); - - Ok(()) -} - -/// Stack rows on top of each other. -/// -/// Add a `gap` between each row and uses the baseline of the `baseline`th -/// row for the whole frame. -pub(super) fn stack( - ctx: &MathContext, - rows: Vec<MathRow>, - align: FixedAlign, - gap: Abs, - baseline: usize, -) -> Frame { - let rows: Vec<_> = rows.into_iter().flat_map(|r| r.rows()).collect(); - let AlignmentResult { points, width } = alignments(&rows); - let rows: Vec<_> = rows - .into_iter() - .map(|row| row.into_aligned_frame(ctx, &points, align)) - .collect(); - - let mut y = Abs::zero(); - let mut frame = Frame::soft(Size::new( - width, - rows.iter().map(|row| row.height()).sum::<Abs>() - + rows.len().saturating_sub(1) as f64 * gap, - )); - - for (i, row) in rows.into_iter().enumerate() { - let x = align.position(width - row.width()); - let pos = Point::new(x, y); - if i == baseline { - frame.set_baseline(y + row.baseline()); - } - y += row.height() + gap; - frame.push_frame(pos, row); - } - - frame -} diff --git a/crates/typst-library/src/meta/bibliography.rs b/crates/typst-library/src/meta/bibliography.rs deleted file mode 100644 index b84c330a..00000000 --- a/crates/typst-library/src/meta/bibliography.rs +++ /dev/null @@ -1,1038 +0,0 @@ -use std::collections::HashMap; -use std::ffi::OsStr; -use std::fmt::{self, Debug, Formatter}; -use std::hash::{Hash, Hasher}; -use std::path::Path; -use std::sync::Arc; - -use comemo::Prehashed; -use ecow::EcoVec; -use hayagriva::citationberg; -use hayagriva::io::BibLaTeXError; -use hayagriva::{ - BibliographyDriver, BibliographyRequest, CitationItem, CitationRequest, - SpecificLocator, -}; -use indexmap::IndexMap; -use once_cell::sync::Lazy; -use smallvec::SmallVec; -use typed_arena::Arena; -use typst::diag::FileError; -use typst::eval::{eval_string, Bytes, CastInfo, EvalMode, Reflect}; -use typst::font::FontStyle; -use typst::util::{option_eq, PicoStr}; - -use crate::layout::{ - BlockElem, GridElem, HElem, PadElem, ParElem, Sizing, TrackSizings, VElem, -}; -use crate::meta::{ - CitationForm, CiteGroup, FootnoteElem, HeadingElem, LinkElem, LocalName, LocalNameIn, -}; -use crate::prelude::*; -use crate::text::{Delta, SubElem, SuperElem, TextElem}; - -/// A bibliography / reference listing. -/// -/// You can create a new bibliography by calling this function with a path -/// to a bibliography file in either one of two formats: -/// -/// - A Hayagriva `.yml` file. Hayagriva is a new bibliography file format -/// designed for use with Typst. Visit its -/// [documentation](https://github.com/typst/hayagriva/blob/main/docs/file-format.md) -/// for more details. -/// - A BibLaTeX `.bib` file. -/// -/// As soon as you add a bibliography somewhere in your document, you can start -/// citing things with reference syntax (`[@key]`) or explicit calls to the -/// [citation]($cite) function (`[#cite(<key>)]`). The bibliography will only -/// show entries for works that were referenced in the document. -/// -/// # Styles -/// Typst offers a wide selection of built-in -/// [citation and bibliography styles]($bibliography.style). Beyond those, you -/// can add and use custom [CSL](https://citationstyles.org/) (Citation Style -/// Language) files. Wondering which style to use? Here are some good defaults -/// based on what discipline you're working in: -/// -/// | Fields | Typical Styles | -/// |-----------------|--------------------------------------------------------| -/// | Engineering, IT | `{"ieee"}` | -/// | Psychology, Life Sciences | `{"apa"}` | -/// | Social sciences | `{"chicago-author-date"}` | -/// | Humanities | `{"mla"}`, `{"chicago-notes"}`, `{"harvard-cite-them-right"}` | -/// | Economics | `{"harvard-cite-them-right"}` | -/// | Physics | `{"american-physics-society"}` | -/// -/// # Example -/// ```example -/// This was already noted by -/// pirates long ago. @arrgh -/// -/// Multiple sources say ... -/// @arrgh @netwok. -/// -/// #bibliography("works.bib") -/// ``` -#[elem(Locatable, Synthesize, Show, Finalize, LocalName)] -pub struct BibliographyElem { - /// Path(s) to Hayagriva `.yml` and/or BibLaTeX `.bib` files. - #[required] - #[parse( - let (paths, bibliography) = Bibliography::parse(vm, args)?; - paths - )] - pub path: BibPaths, - - /// The title of the bibliography. - /// - /// - When set to `{auto}`, an appropriate title for the - /// [text language]($text.lang) will be used. This is the default. - /// - When set to `{none}`, the bibliography will not have a title. - /// - A custom title can be set by passing content. - /// - /// The bibliography's heading will not be numbered by default, but you can - /// force it to be with a show-set rule: - /// `{show bibliography: set heading(numbering: "1.")}` - #[default(Some(Smart::Auto))] - pub title: Option<Smart<Content>>, - - /// Whether to include all works from the given bibliography files, even - /// those that weren't cited in the document. - /// - /// To selectively add individual cited works without showing them, you can - /// also use the `cite` function with [`form`]($cite.form) set to `{none}`. - #[default(false)] - pub full: bool, - - /// The bibliography style. - /// - /// Should be either one of the built-in styles (see below) or a path to - /// a [CSL file](https://citationstyles.org/). Some of the styles listed - /// below appear twice, once with their full name and once with a short - /// alias. - #[parse(CslStyle::parse(vm, args)?)] - #[default(CslStyle::from_name("ieee").unwrap())] - pub style: CslStyle, - - /// The loaded bibliography. - #[internal] - #[required] - #[parse(bibliography)] - pub bibliography: Bibliography, - - /// The language setting where the bibliography is. - #[internal] - #[synthesized] - pub lang: Lang, - - /// The region setting where the bibliography is. - #[internal] - #[synthesized] - pub region: Option<Region>, -} - -/// A list of bibliography file paths. -#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)] -pub struct BibPaths(Vec<EcoString>); - -cast! { - BibPaths, - self => self.0.into_value(), - v: EcoString => Self(vec![v]), - v: Array => Self(v.into_iter().map(Value::cast).collect::<StrResult<_>>()?), -} - -impl BibliographyElem { - /// Find the document's bibliography. - pub fn find(introspector: Tracked<Introspector>) -> StrResult<Self> { - let query = introspector.query(&Self::elem().select()); - let mut iter = query.iter(); - let Some(elem) = iter.next() else { - bail!("the document does not contain a bibliography"); - }; - - if iter.next().is_some() { - bail!("multiple bibliographies are not yet supported"); - } - - Ok(elem.to::<Self>().cloned().unwrap()) - } - - /// Whether the bibliography contains the given key. - pub fn has(vt: &Vt, key: impl Into<PicoStr>) -> bool { - let key = key.into(); - vt.introspector - .query(&Self::elem().select()) - .iter() - .any(|elem| elem.to::<Self>().unwrap().bibliography().has(key)) - } - - /// Find all bibliography keys. - pub fn keys( - introspector: Tracked<Introspector>, - ) -> Vec<(EcoString, Option<EcoString>)> { - let mut vec = vec![]; - for elem in introspector.query(&Self::elem().select()).iter() { - let this = elem.to::<Self>().unwrap(); - for entry in this.bibliography().entries() { - let key = entry.key().into(); - let detail = entry.title().map(|title| title.value.to_str().into()); - vec.push((key, detail)) - } - } - vec - } -} - -impl Synthesize for BibliographyElem { - fn synthesize(&mut self, _vt: &mut Vt, styles: StyleChain) -> SourceResult<()> { - self.push_full(self.full(styles)); - self.push_style(self.style(styles)); - self.push_lang(TextElem::lang_in(styles)); - self.push_region(TextElem::region_in(styles)); - Ok(()) - } -} - -impl Show for BibliographyElem { - #[tracing::instrument(name = "BibliographyElem::show", skip_all)] - fn show(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Content> { - const COLUMN_GUTTER: Em = Em::new(0.65); - const INDENT: Em = Em::new(1.5); - - let mut seq = vec![]; - if let Some(title) = self.title(styles) { - let title = title.unwrap_or_else(|| { - TextElem::packed(Self::local_name_in(styles)).spanned(self.span()) - }); - - seq.push(HeadingElem::new(title).with_level(NonZeroUsize::ONE).pack()); - } - - Ok(vt.delayed(|vt| { - let span = self.span(); - let works = Works::generate(vt.world, vt.introspector).at(span)?; - let references = works - .references - .as_ref() - .ok_or("CSL style is not suitable for bibliographies") - .at(span)?; - - let row_gutter = *BlockElem::below_in(styles).amount(); - if references.iter().any(|(prefix, _)| prefix.is_some()) { - let mut cells = vec![]; - for (prefix, reference) in references { - cells.push(prefix.clone().unwrap_or_default()); - cells.push(reference.clone()); - } - - seq.push(VElem::new(row_gutter).with_weakness(3).pack()); - seq.push( - GridElem::new(cells) - .with_columns(TrackSizings(smallvec![Sizing::Auto; 2])) - .with_column_gutter(TrackSizings(smallvec![COLUMN_GUTTER.into()])) - .with_row_gutter(TrackSizings(smallvec![(row_gutter).into()])) - .pack(), - ); - } else { - for (_, reference) in references { - seq.push(VElem::new(row_gutter).with_weakness(3).pack()); - seq.push(reference.clone()); - } - } - - let mut content = Content::sequence(seq); - if works.hanging_indent { - content = content.styled(ParElem::set_hanging_indent(INDENT.into())); - } - - Ok(content) - })) - } -} - -impl Finalize for BibliographyElem { - fn finalize(&self, realized: Content, _: StyleChain) -> Content { - const INDENT: Em = Em::new(1.0); - realized - .styled(HeadingElem::set_numbering(None)) - .styled(PadElem::set_left(INDENT.into())) - } -} - -impl LocalName for BibliographyElem { - fn local_name(lang: Lang, region: Option<Region>) -> &'static str { - match lang { - Lang::ALBANIAN => "Bibliografi", - Lang::ARABIC => "المراجع", - Lang::BOKMÅL => "Bibliografi", - Lang::CHINESE if option_eq(region, "TW") => "書目", - Lang::CHINESE => "参考文献", - Lang::CZECH => "Bibliografie", - Lang::DANISH => "Bibliografi", - Lang::DUTCH => "Bibliografie", - Lang::FILIPINO => "Bibliograpiya", - Lang::FINNISH => "Viitteet", - Lang::FRENCH => "Bibliographie", - Lang::GERMAN => "Bibliographie", - Lang::GREEK => "Βιβλιογραφία", - Lang::HUNGARIAN => "Irodalomjegyzék", - Lang::ITALIAN => "Bibliografia", - Lang::NYNORSK => "Bibliografi", - Lang::POLISH => "Bibliografia", - Lang::PORTUGUESE => "Bibliografia", - Lang::ROMANIAN => "Bibliografie", - Lang::RUSSIAN => "Библиография", - Lang::SLOVENIAN => "Literatura", - Lang::SPANISH => "Bibliografía", - Lang::SWEDISH => "Bibliografi", - Lang::TURKISH => "Kaynakça", - Lang::UKRAINIAN => "Бібліографія", - Lang::VIETNAMESE => "Tài liệu tham khảo", - Lang::JAPANESE => "参考文献", - Lang::ENGLISH | _ => "Bibliography", - } - } -} - -/// A loaded bibliography. -#[ty] -#[derive(Clone, PartialEq)] -pub struct Bibliography { - map: Arc<IndexMap<PicoStr, hayagriva::Entry>>, - hash: u128, -} - -impl Bibliography { - /// Parse the bibliography argument. - fn parse(vm: &mut Vm, args: &mut Args) -> SourceResult<(BibPaths, Bibliography)> { - let Spanned { v: paths, span } = - args.expect::<Spanned<BibPaths>>("path to bibliography file")?; - - // Load bibliography files. - let data = paths - .0 - .iter() - .map(|path| { - let id = vm.resolve_path(path).at(span)?; - vm.world().file(id).at(span) - }) - .collect::<SourceResult<Vec<Bytes>>>()?; - - // Parse. - let bibliography = Self::load(&paths, &data).at(span)?; - - Ok((paths, bibliography)) - } - - /// Load bibliography entries from paths. - #[comemo::memoize] - fn load(paths: &BibPaths, data: &[Bytes]) -> StrResult<Bibliography> { - let mut map = IndexMap::new(); - let mut duplicates = Vec::<EcoString>::new(); - - // We might have multiple bib/yaml files - for (path, bytes) in paths.0.iter().zip(data) { - let src = std::str::from_utf8(bytes).map_err(FileError::from)?; - - let ext = Path::new(path.as_str()) - .extension() - .and_then(OsStr::to_str) - .unwrap_or_default(); - - let library = match ext.to_lowercase().as_str() { - "yml" | "yaml" => hayagriva::io::from_yaml_str(src) - .map_err(|err| eco_format!("failed to parse YAML ({err})"))?, - "bib" => hayagriva::io::from_biblatex_str(src) - .map_err(|errors| format_biblatex_error(path, src, errors))?, - _ => bail!("unknown bibliography format (must be .yml/.yaml or .bib)"), - }; - - for entry in library { - match map.entry(entry.key().into()) { - indexmap::map::Entry::Vacant(vacant) => { - vacant.insert(entry); - } - indexmap::map::Entry::Occupied(_) => { - duplicates.push(entry.key().into()); - } - } - } - } - - if !duplicates.is_empty() { - bail!("duplicate bibliography keys: {}", duplicates.join(", ")); - } - - Ok(Bibliography { - map: Arc::new(map), - hash: typst::util::hash128(data), - }) - } - - fn has(&self, key: impl Into<PicoStr>) -> bool { - self.map.contains_key(&key.into()) - } - - fn entries(&self) -> impl Iterator<Item = &hayagriva::Entry> { - self.map.values() - } -} - -impl Debug for Bibliography { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - f.debug_set().entries(self.map.keys()).finish() - } -} - -impl Hash for Bibliography { - fn hash<H: Hasher>(&self, state: &mut H) { - self.hash.hash(state); - } -} - -impl Repr for Bibliography { - fn repr(&self) -> EcoString { - "..".into() - } -} - -cast! { - type Bibliography, -} - -/// Format a BibLaTeX loading error. -fn format_biblatex_error(path: &str, src: &str, errors: Vec<BibLaTeXError>) -> EcoString { - let Some(error) = errors.first() else { - return eco_format!("failed to parse BibLaTeX file ({path})"); - }; - - let (span, msg) = match error { - BibLaTeXError::Parse(error) => (&error.span, error.kind.to_string()), - BibLaTeXError::Type(error) => (&error.span, error.kind.to_string()), - }; - let line = src.get(..span.start).unwrap_or_default().lines().count(); - eco_format!("failed to parse BibLaTeX file ({path}:{line}: {msg})") -} - -/// A loaded CSL style. -#[ty] -#[derive(Debug, Clone, PartialEq, Hash)] -pub struct CslStyle { - name: Option<EcoString>, - style: Arc<Prehashed<citationberg::IndependentStyle>>, -} - -impl CslStyle { - /// Parse the style argument. - pub fn parse(vm: &mut Vm, args: &mut Args) -> SourceResult<Option<CslStyle>> { - let Some(Spanned { v: string, span }) = - args.named::<Spanned<EcoString>>("style")? - else { - return Ok(None); - }; - - Ok(Some(Self::parse_impl(vm, &string).at(span)?)) - } - - /// Parse the style argument with `Smart`. - pub fn parse_smart( - vm: &mut Vm, - args: &mut Args, - ) -> SourceResult<Option<Smart<CslStyle>>> { - let Some(Spanned { v: smart, span }) = - args.named::<Spanned<Smart<EcoString>>>("style")? - else { - return Ok(None); - }; - - Ok(Some(match smart { - Smart::Auto => Smart::Auto, - Smart::Custom(string) => { - Smart::Custom(Self::parse_impl(vm, &string).at(span)?) - } - })) - } - - /// Parse internally. - fn parse_impl(vm: &mut Vm, string: &str) -> StrResult<CslStyle> { - let ext = Path::new(string) - .extension() - .and_then(OsStr::to_str) - .unwrap_or_default() - .to_lowercase(); - - if ext == "csl" { - let id = vm.resolve_path(string)?; - let data = vm.world().file(id)?; - CslStyle::from_data(&data) - } else { - CslStyle::from_name(string) - } - } - - /// Load a built-in CSL style. - #[comemo::memoize] - pub fn from_name(name: &str) -> StrResult<Self> { - match hayagriva::archive::style_by_name(name) { - Some(citationberg::Style::Independent(style)) => Ok(Self { - name: Some(name.into()), - style: Arc::new(Prehashed::new(style)), - }), - _ => bail!("unknown style: `{name}`"), - } - } - - /// Load a CSL style from file contents. - #[comemo::memoize] - pub fn from_data(data: &Bytes) -> StrResult<Self> { - let text = std::str::from_utf8(data.as_slice()).map_err(FileError::from)?; - citationberg::IndependentStyle::from_xml(text) - .map(|style| Self { name: None, style: Arc::new(Prehashed::new(style)) }) - .map_err(|err| eco_format!("failed to load CSL style ({err})")) - } - - /// Get the underlying independent style. - pub fn get(&self) -> &citationberg::IndependentStyle { - self.style.as_ref() - } -} - -// This Reflect impl is technically a bit wrong because it doesn't say what -// FromValue and IntoValue really do. Instead, it says what the `style` argument -// on `bibliography` and `cite` expect (through manual parsing). -impl Reflect for CslStyle { - #[comemo::memoize] - fn input() -> CastInfo { - let ty = std::iter::once(CastInfo::Type(Type::of::<Str>())); - let options = hayagriva::archive::styles() - .map(|style| CastInfo::Value(style.name.into_value(), style.full_name)); - CastInfo::Union(ty.chain(options).collect()) - } - - fn output() -> CastInfo { - EcoString::output() - } - - fn castable(value: &Value) -> bool { - if let Value::Dyn(dynamic) = &value { - if dynamic.is::<Self>() { - return true; - } - } - - false - } -} - -impl FromValue for CslStyle { - fn from_value(value: Value) -> StrResult<Self> { - if let Value::Dyn(dynamic) = &value { - if let Some(concrete) = dynamic.downcast::<Self>() { - return Ok(concrete.clone()); - } - } - - Err(<Self as Reflect>::error(&value)) - } -} - -impl IntoValue for CslStyle { - fn into_value(self) -> Value { - Value::dynamic(self) - } -} - -impl Repr for CslStyle { - fn repr(&self) -> EcoString { - self.name - .as_ref() - .map(|name| name.repr()) - .unwrap_or_else(|| "..".into()) - } -} - -/// Fully formatted citations and references, generated once (through -/// memoization) for the whole document. This setup is necessary because -/// citation formatting is inherently stateful and we need access to all -/// citations to do it. -pub(super) struct Works { - /// Maps from the location of a citation group to its rendered content. - pub citations: HashMap<Location, SourceResult<Content>>, - /// Lists all references in the bibliography, with optional prefix, or - /// `None` if the citation style can't be used for bibliographies. - pub references: Option<Vec<(Option<Content>, Content)>>, - /// Whether the bibliography should have hanging indent. - pub hanging_indent: bool, -} - -impl Works { - /// Generate all citations and the whole bibliography. - #[comemo::memoize] - pub fn generate( - world: Tracked<dyn World + '_>, - introspector: Tracked<Introspector>, - ) -> StrResult<Arc<Self>> { - let mut generator = Generator::new(world, introspector)?; - let rendered = generator.drive(); - let works = generator.display(&rendered)?; - Ok(Arc::new(works)) - } -} - -/// Context for generating the bibliography. -struct Generator<'a> { - /// The world that is used to evaluate mathematical material in citations. - world: Tracked<'a, dyn World + 'a>, - /// The document's bibliography. - bibliography: BibliographyElem, - /// The document's citation groups. - groups: EcoVec<Prehashed<Content>>, - /// Details about each group that are accumulated while driving hayagriva's - /// bibliography driver and needed when processing hayagriva's output. - infos: Vec<GroupInfo>, - /// Citations with unresolved keys. - failures: HashMap<Location, SourceResult<Content>>, -} - -/// Details about a group of merged citations. All citations are put into groups -/// of adjacent ones (e.g., `@foo @bar` will merge into a group of length two). -/// Even single citations will be put into groups of length ones. -struct GroupInfo { - /// The group's location. - location: Location, - /// The group's span. - span: Span, - /// Whether the group should be displayed in a footnote. - footnote: bool, - /// Details about the groups citations. - subinfos: SmallVec<[CiteInfo; 1]>, -} - -/// Details about a citation item in a request. -struct CiteInfo { - /// The citation's key. - key: Label, - /// The citation's supplement. - supplement: Option<Content>, - /// Whether this citation was hidden. - hidden: bool, -} - -impl<'a> Generator<'a> { - /// Create a new generator. - fn new( - world: Tracked<'a, dyn World + 'a>, - introspector: Tracked<Introspector>, - ) -> StrResult<Self> { - let bibliography = BibliographyElem::find(introspector)?; - let groups = introspector.query(&CiteGroup::elem().select()); - let infos = Vec::with_capacity(groups.len()); - Ok(Self { - world, - bibliography, - groups, - infos, - failures: HashMap::new(), - }) - } - - /// Drives hayagriva's citation driver. - fn drive(&mut self) -> hayagriva::Rendered { - static LOCALES: Lazy<Vec<citationberg::Locale>> = - Lazy::new(hayagriva::archive::locales); - - let database = self.bibliography.bibliography(); - let bibliography_style = self.bibliography.style(StyleChain::default()); - let styles = Arena::new(); - - // Process all citation groups. - let mut driver = BibliographyDriver::new(); - for elem in &self.groups { - let group = elem.to::<CiteGroup>().unwrap(); - let location = group.location().unwrap(); - let children = group.children(); - - // Groups should never be empty. - let Some(first) = children.first() else { continue }; - - let mut subinfos = SmallVec::with_capacity(children.len()); - let mut items = Vec::with_capacity(children.len()); - let mut errors = EcoVec::new(); - let mut normal = true; - - // Create infos and items for each child in the group. - for child in children { - let key = *child.key(); - let Some(entry) = database.map.get(&key.into_inner()) else { - errors.push(error!( - child.span(), - "key `{}` does not exist in the bibliography", - key.as_str() - )); - continue; - }; - - let supplement = child.supplement(StyleChain::default()); - let locator = supplement.as_ref().map(|_| { - SpecificLocator( - citationberg::taxonomy::Locator::Custom, - hayagriva::LocatorPayload::Transparent, - ) - }); - - let mut hidden = false; - let special_form = match child.form(StyleChain::default()) { - None => { - hidden = true; - None - } - Some(CitationForm::Normal) => None, - Some(CitationForm::Prose) => Some(hayagriva::CitePurpose::Prose), - Some(CitationForm::Full) => Some(hayagriva::CitePurpose::Full), - Some(CitationForm::Author) => Some(hayagriva::CitePurpose::Author), - Some(CitationForm::Year) => Some(hayagriva::CitePurpose::Year), - }; - - normal &= special_form.is_none(); - subinfos.push(CiteInfo { key, supplement, hidden }); - items.push(CitationItem::new(entry, locator, None, hidden, special_form)); - } - - if !errors.is_empty() { - self.failures.insert(location, Err(errors)); - continue; - } - - let style = match first.style(StyleChain::default()) { - Smart::Auto => &bibliography_style.style, - Smart::Custom(style) => styles.alloc(style.style), - }; - - self.infos.push(GroupInfo { - location, - subinfos, - span: first.span(), - footnote: normal - && style.settings.class == citationberg::StyleClass::Note, - }); - - driver.citation(CitationRequest::new( - items, - style, - Some(locale(*first.lang(), *first.region())), - &LOCALES, - None, - )); - } - - let locale = locale(*self.bibliography.lang(), *self.bibliography.region()); - - // Add hidden items for everything if we should print the whole - // bibliography. - if self.bibliography.full(StyleChain::default()) { - for entry in database.map.values() { - driver.citation(CitationRequest::new( - vec![CitationItem::new(entry, None, None, true, None)], - bibliography_style.get(), - Some(locale.clone()), - &LOCALES, - None, - )); - } - } - - driver.finish(BibliographyRequest { - style: bibliography_style.get(), - locale: Some(locale), - locale_files: &LOCALES, - }) - } - - /// Displays hayagriva's output as content for the citations and references. - fn display(&mut self, rendered: &hayagriva::Rendered) -> StrResult<Works> { - let citations = self.display_citations(rendered); - let references = self.display_references(rendered); - let hanging_indent = - rendered.bibliography.as_ref().map_or(false, |b| b.hanging_indent); - Ok(Works { citations, references, hanging_indent }) - } - - /// Display the citation groups. - fn display_citations( - &mut self, - rendered: &hayagriva::Rendered, - ) -> HashMap<Location, SourceResult<Content>> { - // Determine for each citation key where in the bibliography it is, - // so that we can link there. - let mut links = HashMap::new(); - if let Some(bibliography) = &rendered.bibliography { - let location = self.bibliography.location().unwrap(); - for (k, item) in bibliography.items.iter().enumerate() { - links.insert(item.key.as_str(), location.variant(k + 1)); - } - } - - let mut output = std::mem::take(&mut self.failures); - for (info, citation) in self.infos.iter().zip(&rendered.citations) { - let supplement = |i: usize| info.subinfos.get(i)?.supplement.clone(); - let link = |i: usize| links.get(info.subinfos.get(i)?.key.as_str()).copied(); - - let renderer = ElemRenderer { - world: self.world, - span: info.span, - supplement: &supplement, - link: &link, - }; - - let content = if info.subinfos.iter().all(|sub| sub.hidden) { - Content::empty() - } else { - let mut content = - renderer.display_elem_children(&citation.citation, &mut None); - - if info.footnote { - content = FootnoteElem::with_content(content).pack(); - } - - content - }; - - output.insert(info.location, Ok(content)); - } - - output - } - - /// Display the bibliography references. - fn display_references( - &self, - rendered: &hayagriva::Rendered, - ) -> Option<Vec<(Option<Content>, Content)>> { - let rendered = rendered.bibliography.as_ref()?; - - // Determine for each citation key where it first occured, so that we - // can link there. - let mut first_occurances = HashMap::new(); - for info in &self.infos { - for subinfo in &info.subinfos { - let key = subinfo.key.as_str(); - first_occurances.entry(key).or_insert(info.location); - } - } - - // The location of the bibliography. - let location = self.bibliography.location().unwrap(); - - let mut output = vec![]; - for (k, item) in rendered.items.iter().enumerate() { - let renderer = ElemRenderer { - world: self.world, - span: self.bibliography.span(), - supplement: &|_| None, - link: &|_| None, - }; - - // Each reference is assigned a manually created well-known location - // that is derived from the bibliography's location. This way, - // citations can link to them. - let backlink = location.variant(k + 1); - - // Render the first field. - let mut prefix = item.first_field.as_ref().map(|elem| { - let mut content = renderer.display_elem_child(elem, &mut None); - if let Some(location) = first_occurances.get(item.key.as_str()) { - let dest = Destination::Location(*location); - content = content.linked(dest); - } - content.backlinked(backlink) - }); - - // Render the main reference content. - let reference = renderer - .display_elem_children(&item.content, &mut prefix) - .backlinked(backlink); - - output.push((prefix, reference)); - } - - Some(output) - } -} - -/// Renders hayagriva elements into content. -struct ElemRenderer<'a> { - /// The world that is used to evaluate mathematical material. - world: Tracked<'a, dyn World + 'a>, - /// The span that is attached to all of the resulting content. - span: Span, - /// Resolves the supplement of i-th citation in the request. - supplement: &'a dyn Fn(usize) -> Option<Content>, - /// Resolves where the i-th citation in the request should link to. - link: &'a dyn Fn(usize) -> Option<Location>, -} - -impl ElemRenderer<'_> { - /// Display rendered hayagriva elements. - /// - /// The `prefix` can be a separate content storage where `left-margin` - /// elements will be accumulated into. - fn display_elem_children( - &self, - elems: &hayagriva::ElemChildren, - prefix: &mut Option<Content>, - ) -> Content { - Content::sequence( - elems.0.iter().map(|elem| self.display_elem_child(elem, prefix)), - ) - } - - /// Display a rendered hayagriva element. - fn display_elem_child( - &self, - elem: &hayagriva::ElemChild, - prefix: &mut Option<Content>, - ) -> Content { - match elem { - hayagriva::ElemChild::Text(formatted) => self.display_formatted(formatted), - hayagriva::ElemChild::Elem(elem) => self.display_elem(elem, prefix), - hayagriva::ElemChild::Markup(markup) => self.display_math(markup), - hayagriva::ElemChild::Link { text, url } => self.display_link(text, url), - hayagriva::ElemChild::Transparent { cite_idx, format } => { - self.display_transparent(*cite_idx, format) - } - } - } - - /// Display a block-level element. - fn display_elem( - &self, - elem: &hayagriva::Elem, - prefix: &mut Option<Content>, - ) -> Content { - use citationberg::Display; - - let block_level = matches!(elem.display, Some(Display::Block | Display::Indent)); - - let mut suf_prefix = None; - let mut content = self.display_elem_children( - &elem.children, - if block_level { &mut suf_prefix } else { prefix }, - ); - - if let Some(prefix) = suf_prefix { - const COLUMN_GUTTER: Em = Em::new(0.65); - content = GridElem::new(vec![prefix, content]) - .with_columns(TrackSizings(smallvec![Sizing::Auto; 2])) - .with_column_gutter(TrackSizings(smallvec![COLUMN_GUTTER.into()])) - .pack(); - } - - match elem.display { - Some(Display::Block) => { - content = BlockElem::new().with_body(Some(content)).pack(); - } - Some(Display::Indent) => { - content = PadElem::new(content).pack(); - } - Some(Display::LeftMargin) => { - *prefix.get_or_insert_with(Default::default) += content; - return Content::empty(); - } - _ => {} - } - - if let Some(hayagriva::ElemMeta::Entry(i)) = elem.meta { - if let Some(location) = (self.link)(i) { - let dest = Destination::Location(location); - content = content.linked(dest); - } - } - - content - } - - /// Display math. - fn display_math(&self, math: &str) -> Content { - eval_string(self.world, math, self.span, EvalMode::Math, Scope::new()) - .map(Value::display) - .unwrap_or_else(|_| TextElem::packed(math).spanned(self.span)) - } - - /// Display a link. - fn display_link(&self, text: &hayagriva::Formatted, url: &str) -> Content { - let dest = Destination::Url(url.into()); - LinkElem::new(dest.into(), self.display_formatted(text)).pack() - } - - /// Display transparent pass-through content. - fn display_transparent(&self, i: usize, format: &hayagriva::Formatting) -> Content { - let content = (self.supplement)(i).unwrap_or_default(); - apply_formatting(content, format) - } - - /// Display formatted hayagriva text as content. - fn display_formatted(&self, formatted: &hayagriva::Formatted) -> Content { - let content = TextElem::packed(formatted.text.as_str()).spanned(self.span); - apply_formatting(content, &formatted.formatting) - } -} - -/// Applies formatting to content. -fn apply_formatting(mut content: Content, format: &hayagriva::Formatting) -> Content { - match format.font_style { - citationberg::FontStyle::Normal => {} - citationberg::FontStyle::Italic => { - content = content.styled(TextElem::set_style(FontStyle::Italic)); - } - } - - match format.font_variant { - citationberg::FontVariant::Normal => {} - citationberg::FontVariant::SmallCaps => { - content = content.styled(TextElem::set_smallcaps(true)); - } - } - - match format.font_weight { - citationberg::FontWeight::Normal => {} - citationberg::FontWeight::Bold => { - content = content.styled(TextElem::set_delta(Delta(300))); - } - citationberg::FontWeight::Light => { - content = content.styled(TextElem::set_delta(Delta(-100))); - } - } - - match format.text_decoration { - citationberg::TextDecoration::None => {} - citationberg::TextDecoration::Underline => { - content = content.underlined(); - } - } - - match format.vertical_align { - citationberg::VerticalAlign::None => {} - citationberg::VerticalAlign::Baseline => {} - citationberg::VerticalAlign::Sup => { - // Add zero-width weak spacing to make the superscript "sticky". - content = HElem::hole().pack() + SuperElem::new(content).pack(); - } - citationberg::VerticalAlign::Sub => { - content = HElem::hole().pack() + SubElem::new(content).pack(); - } - } - - content -} - -/// Create a locale code from language and optionally region. -fn locale(lang: Lang, region: Option<Region>) -> citationberg::LocaleCode { - let mut value = String::with_capacity(5); - value.push_str(lang.as_str()); - if let Some(region) = region { - value.push('-'); - value.push_str(region.as_str()) - } - citationberg::LocaleCode(value) -} diff --git a/crates/typst-library/src/meta/cite.rs b/crates/typst-library/src/meta/cite.rs deleted file mode 100644 index c0bd71ae..00000000 --- a/crates/typst-library/src/meta/cite.rs +++ /dev/null @@ -1,156 +0,0 @@ -use crate::meta::bibliography::Works; -use crate::meta::CslStyle; -use crate::prelude::*; -use crate::text::TextElem; - -/// Cite a work from the bibliography. -/// -/// Before you starting citing, you need to add a [bibliography]($bibliography) -/// somewhere in your document. -/// -/// # Example -/// ```example -/// This was already noted by -/// pirates long ago. @arrgh -/// -/// Multiple sources say ... -/// @arrgh @netwok. -/// -/// You can also call `cite` -/// explicitly. #cite(<arrgh>) -/// -/// #bibliography("works.bib") -/// ``` -/// -/// # Syntax -/// This function indirectly has dedicated syntax. [References]($ref) can be -/// used to cite works from the bibliography. The label then corresponds to the -/// citation key. -#[elem(Synthesize)] -pub struct CiteElem { - /// The citation key that identifies the entry in the bibliography that - /// shall be cited, as a label. - /// - /// ```example - /// // All the same - /// @netwok \ - /// #cite(<netwok>) \ - /// #cite(label("netwok")) - /// >>> #set text(0pt) - /// >>> #bibliography("works.bib", style: "apa") - /// ``` - #[required] - pub key: Label, - - /// A supplement for the citation such as page or chapter number. - /// - /// In reference syntax, the supplement can be added in square brackets: - /// - /// ```example - /// This has been proven. @distress[p.~7] - /// - /// #bibliography("works.bib") - /// ``` - pub supplement: Option<Content>, - - /// The kind of citation to produce. Different forms are useful in different - /// scenarios: A normal citation is useful as a source at the end of a - /// sentence, while a "prose" citation is more suitable for inclusion in the - /// flow of text. - /// - /// If set to `{none}`, the cited work is included in the bibliography, but - /// nothing will be displayed. - /// - /// ```example - /// #cite(<netwok>, form: "prose") - /// show the outsized effects of - /// pirate life on the human psyche. - /// >>> #set text(0pt) - /// >>> #bibliography("works.bib", style: "apa") - /// ``` - #[default(Some(CitationForm::Normal))] - pub form: Option<CitationForm>, - - /// The citation style. - /// - /// Should be either `{auto}`, one of the built-in styles (see below) or a - /// path to a [CSL file](https://citationstyles.org/). Some of the styles - /// listed below appear twice, once with their full name and once with a - /// short alias. - /// - /// When set to `{auto}`, automatically use the - /// [bibliography's style]($bibliography.style) for the citations. - #[parse(CslStyle::parse_smart(vm, args)?)] - pub style: Smart<CslStyle>, - - /// The text language setting where the citation is. - #[internal] - #[synthesized] - pub lang: Lang, - - /// The text region setting where the citation is. - #[internal] - #[synthesized] - pub region: Option<Region>, -} - -impl Synthesize for CiteElem { - fn synthesize(&mut self, _vt: &mut Vt, styles: StyleChain) -> SourceResult<()> { - self.push_supplement(self.supplement(styles)); - self.push_form(self.form(styles)); - self.push_style(self.style(styles)); - self.push_lang(TextElem::lang_in(styles)); - self.push_region(TextElem::region_in(styles)); - Ok(()) - } -} - -cast! { - CiteElem, - v: Content => v.to::<Self>().cloned().ok_or("expected citation")?, -} - -/// The form of the citation. -#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash, Cast)] -pub enum CitationForm { - /// Display in the standard way for the active style. - #[default] - Normal, - /// Produces a citation that is suitable for inclusion in a sentence. - Prose, - /// Mimics a bibliography entry, with full information about the cited work. - Full, - /// Shows only the cited work's author(s). - Author, - /// Shows only the cited work's year. - Year, -} - -/// A group of citations. -/// -/// This is automatically created from adjacent citations during show rule -/// application. -#[elem(Locatable, Show)] -pub struct CiteGroup { - /// The citations. - #[required] - pub children: Vec<CiteElem>, -} - -impl Show for CiteGroup { - #[tracing::instrument(name = "CiteGroup::show", skip(self, vt))] - fn show(&self, vt: &mut Vt, _: StyleChain) -> SourceResult<Content> { - Ok(vt.delayed(|vt| { - let location = self.location().unwrap(); - let span = self.span(); - Works::generate(vt.world, vt.introspector) - .at(span)? - .citations - .get(&location) - .cloned() - .unwrap_or_else(|| { - bail!(span, "failed to format citation (this is a bug)") - }) - })) - } -} diff --git a/crates/typst-library/src/meta/context.rs b/crates/typst-library/src/meta/context.rs deleted file mode 100644 index 59b35577..00000000 --- a/crates/typst-library/src/meta/context.rs +++ /dev/null @@ -1,169 +0,0 @@ -use crate::prelude::*; - -/// Provides access to the location of content. -/// -/// This is useful in combination with [queries]($query), [counters]($counter), -/// [state]($state), and [links]($link). See their documentation for more -/// details. -/// -/// ```example -/// #locate(loc => [ -/// My location: \ -/// #loc.position()! -/// ]) -/// ``` -#[func] -pub fn locate( - /// A function that receives a [`location`]($location). Its return value is - /// displayed in the document. - /// - /// This function is called once for each time the content returned by - /// `locate` appears in the document. That makes it possible to generate - /// content that depends on its own location in the document. - func: Func, -) -> Content { - LocateElem::new(func).pack() -} - -/// Executes a `locate` call. -#[elem(Locatable, Show)] -struct LocateElem { - /// The function to call with the location. - #[required] - func: Func, -} - -impl Show for LocateElem { - #[tracing::instrument(name = "LocateElem::show", skip(self, vt))] - fn show(&self, vt: &mut Vt, _: StyleChain) -> SourceResult<Content> { - Ok(vt.delayed(|vt| { - let location = self.location().unwrap(); - Ok(self.func().call_vt(vt, [location])?.display()) - })) - } -} - -/// Provides access to active styles. -/// -/// The styles are currently opaque and only useful in combination with the -/// [`measure`]($measure) function. See its documentation for more details. In -/// the future, the provided styles might also be directly accessed to look up -/// styles defined by [set rules]($styling/#set-rules). -/// -/// ```example -/// #let thing(body) = style(styles => { -/// let size = measure(body, styles) -/// [Width of "#body" is #size.width] -/// }) -/// -/// #thing[Hey] \ -/// #thing[Welcome] -/// ``` -#[func] -pub fn style( - /// A function to call with the styles. Its return value is displayed - /// in the document. - /// - /// This function is called once for each time the content returned by - /// `style` appears in the document. That makes it possible to generate - /// content that depends on the style context it appears in. - func: Func, -) -> Content { - StyleElem::new(func).pack() -} - -/// Executes a style access. -#[elem(Show)] -struct StyleElem { - /// The function to call with the styles. - #[required] - func: Func, -} - -impl Show for StyleElem { - #[tracing::instrument(name = "StyleElem::show", skip_all)] - fn show(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Content> { - Ok(self.func().call_vt(vt, [styles.to_map()])?.display()) - } -} - -/// Provides access to the current outer container's (or page's, if none) size -/// (width and height). -/// -/// The given function must accept a single parameter, `size`, which is a -/// dictionary with keys `width` and `height`, both of type [`length`]($length). -/// -/// ```example -/// #let text = lorem(30) -/// #layout(size => style(styles => [ -/// #let (height,) = measure( -/// block(width: size.width, text), -/// styles, -/// ) -/// This text is #height high with -/// the current page width: \ -/// #text -/// ])) -/// ``` -/// -/// If the `layout` call is placed inside of a box width a width of `{800pt}` -/// and a height of `{400pt}`, then the specified function will be given the -/// parameter `{(width: 800pt, height: 400pt)}`. If it placed directly into the -/// page it receives the page's dimensions minus its margins. This is mostly -/// useful in combination with [measurement]($measure). -/// -/// You can also use this function to resolve [`ratio`]($ratio) to fixed -/// lengths. This might come in handy if you're building your own layout -/// abstractions. -/// -/// ```example -/// #layout(size => { -/// let half = 50% * size.width -/// [Half a page is #half wide.] -/// }) -/// ``` -/// -/// Note that this function will provide an infinite width or height if one of -/// the page width or height is `auto`, respectively. -#[func] -pub fn layout( - /// A function to call with the outer container's size. Its return value is - /// displayed in the document. - /// - /// The container's size is given as a [dictionary]($dictionary) with the - /// keys `width` and `height`. - /// - /// This function is called once for each time the content returned by - /// `layout` appears in the document. That makes it possible to generate - /// content that depends on the size of the container it is inside of. - func: Func, -) -> Content { - LayoutElem::new(func).pack() -} - -/// Executes a `layout` call. -#[elem(Layout)] -struct LayoutElem { - /// The function to call with the outer container's (or page's) size. - #[required] - func: Func, -} - -impl Layout for LayoutElem { - #[tracing::instrument(name = "LayoutElem::layout", skip_all)] - fn layout( - &self, - vt: &mut Vt, - styles: StyleChain, - regions: Regions, - ) -> SourceResult<Fragment> { - // Gets the current region's base size, which will be the size of the - // outer container, or of the page if there is no such container. - let Size { x, y } = regions.base(); - let result = self - .func() - .call_vt(vt, [dict! { "width" => x, "height" => y }])? - .display(); - result.layout(vt, styles, regions) - } -} diff --git a/crates/typst-library/src/meta/counter.rs b/crates/typst-library/src/meta/counter.rs deleted file mode 100644 index 0e733efb..00000000 --- a/crates/typst-library/src/meta/counter.rs +++ /dev/null @@ -1,722 +0,0 @@ -use std::str::FromStr; - -use ecow::{eco_vec, EcoVec}; -use smallvec::{smallvec, SmallVec}; -use typst::eval::{Repr, Tracer}; -use typst::model::DelayedErrors; - -use crate::layout::PageElem; -use crate::math::EquationElem; -use crate::meta::{FigureElem, HeadingElem, Numbering, NumberingPattern}; -use crate::prelude::*; - -/// Counts through pages, elements, and more. -/// -/// With the counter function, you can access and modify counters for pages, -/// headings, figures, and more. Moreover, you can define custom counters for -/// other things you want to count. -/// -/// # Displaying a counter { #displaying } -/// To display the current value of the heading counter, you call the `counter` -/// function with the `key` set to `heading` and then call the `display` method -/// on the counter. To see any output, you also have to enable heading -/// [numbering]($heading.numbering). -/// -/// The `display` method optionally takes an argument telling it how to format -/// the counter. This can be a [numbering pattern or a function]($numbering). -/// -/// ```example -/// #set heading(numbering: "1.") -/// -/// = Introduction -/// Some text here. -/// -/// = Background -/// The current value is: -/// #counter(heading).display() -/// -/// Or in roman numerals: -/// #counter(heading).display("I") -/// ``` -/// -/// # Modifying a counter { #modifying } -/// To modify a counter, you can use the `step` and `update` methods: -/// -/// - The `step` method increases the value of the counter by one. Because -/// counters can have multiple levels (in the case of headings for sections, -/// subsections, and so on), the `step` method optionally takes a `level` -/// argument. If given, the counter steps at the given depth. -/// -/// - The `update` method allows you to arbitrarily modify the counter. In its -/// basic form, you give it an integer (or multiple for multiple levels). For -/// more flexibility, you can instead also give it a function that gets the -/// current value and returns a new value. -/// -/// The heading counter is stepped before the heading is displayed, so -/// `Analysis` gets the number seven even though the counter is at six after the -/// second update. -/// -/// ```example -/// #set heading(numbering: "1.") -/// -/// = Introduction -/// #counter(heading).step() -/// -/// = Background -/// #counter(heading).update(3) -/// #counter(heading).update(n => n * 2) -/// -/// = Analysis -/// Let's skip 7.1. -/// #counter(heading).step(level: 2) -/// -/// == Analysis -/// Still at #counter(heading).display(). -/// ``` -/// -/// To define your own counter, call the `counter` function with a string as a -/// key. This key identifies the counter globally. -/// -/// ```example -/// #let mine = counter("mycounter") -/// #mine.display() \ -/// #mine.step() -/// #mine.display() \ -/// #mine.update(c => c * 3) -/// #mine.display() \ -/// ``` -/// -/// # How to step -/// When you define and use a custom counter, in general, you should first step -/// the counter and then display it. This way, the stepping behaviour of a -/// counter can depend on the element it is stepped for. If you were writing a -/// counter for, let's say, theorems, your theorem's definition would thus first -/// include the counter step and only then display the counter and the theorem's -/// contents. -/// -/// ```example -/// #let c = counter("theorem") -/// #let theorem(it) = block[ -/// #c.step() -/// *Theorem #c.display():* #it -/// ] -/// -/// #theorem[$1 = 1$] -/// #theorem[$2 < 3$] -/// ``` -/// -/// The rationale behind this is best explained on the example of the heading -/// counter: An update to the heading counter depends on the heading's level. -/// By stepping directly before the heading, we can correctly step from `1` to -/// `1.1` when encountering a level 2 heading. If we were to step after the -/// heading, we wouldn't know what to step to. -/// -/// Because counters should always be stepped before the elements they count, -/// they always start at zero. This way, they are at one for the first display -/// (which happens after the first step). -/// -/// # Page counter -/// The page counter is special. It is automatically stepped at each pagebreak. -/// But like other counters, you can also step it manually. For example, you -/// could have Roman page numbers for your preface, then switch to Arabic page -/// numbers for your main content and reset the page counter to one. -/// -/// ```example -/// >>> #set page( -/// >>> height: 100pt, -/// >>> margin: (bottom: 24pt, rest: 16pt), -/// >>> ) -/// #set page(numbering: "(i)") -/// -/// = Preface -/// The preface is numbered with -/// roman numerals. -/// -/// #set page(numbering: "1 / 1") -/// #counter(page).update(1) -/// -/// = Main text -/// Here, the counter is reset to one. -/// We also display both the current -/// page and total number of pages in -/// Arabic numbers. -/// ``` -/// -/// # Time travel -/// Counters can travel through time! You can find out the final value of the -/// counter before it is reached and even determine what the value was at any -/// particular location in the document. -/// -/// ```example -/// #let mine = counter("mycounter") -/// -/// = Values -/// #locate(loc => { -/// let start-val = mine.at(loc) -/// let elements = query(<intro>, loc) -/// let intro-val = mine.at( -/// elements.first().location() -/// ) -/// let final-val = mine.final(loc) -/// [Starts as: #start-val \ -/// Value at intro is: #intro-val \ -/// Final value is: #final-val \ ] -/// }) -/// -/// #mine.update(n => n + 3) -/// -/// = Introduction <intro> -/// #lorem(10) -/// -/// #mine.step() -/// #mine.step() -/// ``` -/// -/// Let's dissect what happens in the example above: -/// -/// - We call [`locate`]($locate) to get access to the current location in the -/// document. We then pass this location to our counter's `at` method to get -/// its value at the current location. The `at` method always returns an array -/// because counters can have multiple levels. As the counter starts at zero, -/// the first value is thus `{(0,)}`. -/// -/// - We now [`query`]($query) the document for all elements with the -/// `{<intro>}` label. The result is an array from which we extract the first -/// (and only) element's [location]($content.location). We then look up the -/// value of the counter at that location. The first update to the counter -/// sets it to `{0 + 3 = 3}`. At the introduction heading, the value is thus -/// `{(3,)}`. -/// -/// - Last but not least, we call the `final` method on the counter. It tells us -/// what the counter's value will be at the end of the document. We also need -/// to give it a location to prove that we are inside of a `locate` call, but -/// which one doesn't matter. After the heading follow two calls to `step()`, -/// so the final value is `{(5,)}`. -/// -/// # Other kinds of state { #other-state } -/// The `counter` type is closely related to [state]($state) type. Read its -/// documentation for more details on state management in Typst and why it -/// doesn't just use normal variables for counters. -#[ty(scope)] -#[derive(Debug, Clone, PartialEq, Hash)] -pub struct Counter(CounterKey); - -impl Counter { - /// Create a new counter identified by a key. - pub fn new(key: CounterKey) -> Counter { - Self(key) - } - - /// The counter for the given element. - pub fn of(func: Element) -> Self { - Self::construct(CounterKey::Selector(Selector::Elem(func, None))) - } - - /// Gets the current and final value of the state combined in one state. - pub fn both(&self, vt: &mut Vt, location: Location) -> SourceResult<CounterState> { - let sequence = self.sequence(vt)?; - let offset = vt - .introspector - .query(&self.selector().before(location.into(), true)) - .len(); - let (mut at_state, at_page) = sequence[offset].clone(); - let (mut final_state, final_page) = sequence.last().unwrap().clone(); - if self.is_page() { - let at_delta = - vt.introspector.page(location).get().saturating_sub(at_page.get()); - at_state.step(NonZeroUsize::ONE, at_delta); - let final_delta = - vt.introspector.pages().get().saturating_sub(final_page.get()); - final_state.step(NonZeroUsize::ONE, final_delta); - } - Ok(CounterState(smallvec![at_state.first(), final_state.first()])) - } - - /// Produce the whole sequence of counter states. - /// - /// This has to happen just once for all counters, cutting down the number - /// of counter updates from quadratic to linear. - fn sequence( - &self, - vt: &mut Vt, - ) -> SourceResult<EcoVec<(CounterState, NonZeroUsize)>> { - self.sequence_impl( - vt.world, - vt.introspector, - vt.locator.track(), - TrackedMut::reborrow_mut(&mut vt.delayed), - TrackedMut::reborrow_mut(&mut vt.tracer), - ) - } - - /// Memoized implementation of `sequence`. - #[comemo::memoize] - fn sequence_impl( - &self, - world: Tracked<dyn World + '_>, - introspector: Tracked<Introspector>, - locator: Tracked<Locator>, - delayed: TrackedMut<DelayedErrors>, - tracer: TrackedMut<Tracer>, - ) -> SourceResult<EcoVec<(CounterState, NonZeroUsize)>> { - let mut locator = Locator::chained(locator); - let mut vt = Vt { - world, - introspector, - locator: &mut locator, - delayed, - tracer, - }; - - let mut state = CounterState::init(&self.0); - let mut page = NonZeroUsize::ONE; - let mut stops = eco_vec![(state.clone(), page)]; - - for elem in introspector.query(&self.selector()) { - if self.is_page() { - let prev = page; - page = introspector.page(elem.location().unwrap()); - - let delta = page.get() - prev.get(); - if delta > 0 { - state.step(NonZeroUsize::ONE, delta); - } - } - - if let Some(update) = match elem.to::<UpdateElem>() { - Some(elem) => Some(elem.update().clone()), - None => match elem.with::<dyn Count>() { - Some(countable) => countable.update().clone(), - None => Some(CounterUpdate::Step(NonZeroUsize::ONE)), - }, - } { - state.update(&mut vt, update)?; - } - - stops.push((state.clone(), page)); - } - - Ok(stops) - } - - /// The selector relevant for this counter's updates. - fn selector(&self) -> Selector { - let mut selector = select_where!(UpdateElem, Key => self.0.clone()); - - if let CounterKey::Selector(key) = &self.0 { - selector = Selector::Or(eco_vec![selector, key.clone()]); - } - - selector - } - - /// Whether this is the page counter. - fn is_page(&self) -> bool { - self.0 == CounterKey::Page - } -} - -#[scope] -impl Counter { - /// Create a new counter identified by a key. - #[func(constructor)] - pub fn construct( - /// The key that identifies this counter. - /// - /// - If it is a string, creates a custom counter that is only affected - /// by manual updates, - /// - If this is a `{<label>}`, counts through all elements with that - /// label, - /// - If this is an element function or selector, counts through its - /// elements, - /// - If this is the [`page`]($page) function, counts through pages. - key: CounterKey, - ) -> Counter { - Self(key) - } - - /// Displays the current value of the counter. - #[func] - pub fn display( - self, - /// A [numbering pattern or a function]($numbering), which specifies how - /// to display the counter. If given a function, that function receives - /// each number of the counter as a separate argument. If the amount of - /// numbers varies, e.g. for the heading argument, you can use an - /// [argument sink]($arguments). - /// - /// If this is omitted, displays the counter with the numbering style - /// for the counted element or with the pattern `{"1.1"}` if no such - /// style exists. - #[default] - numbering: Option<Numbering>, - /// If enabled, displays the current and final top-level count together. - /// Both can be styled through a single numbering pattern. This is used - /// by the page numbering property to display the current and total - /// number of pages when a pattern like `{"1 / 1"}` is given. - #[named] - #[default(false)] - both: bool, - ) -> Content { - DisplayElem::new(self, numbering, both).pack() - } - - /// Increases the value of the counter by one. - /// - /// The update will be in effect at the position where the returned content - /// is inserted into the document. If you don't put the output into the - /// document, nothing happens! This would be the case, for example, if you - /// write `{let _ = counter(page).step()}`. Counter updates are always - /// applied in layout order and in that case, Typst wouldn't know when to - /// step the counter. - #[func] - pub fn step( - self, - /// The depth at which to step the counter. Defaults to `{1}`. - #[named] - #[default(NonZeroUsize::ONE)] - level: NonZeroUsize, - ) -> Content { - self.update(CounterUpdate::Step(level)) - } - - /// Updates the value of the counter. - /// - /// Just like with `step`, the update only occurs if you put the resulting - /// content into the document. - #[func] - pub fn update( - self, - /// If given an integer or array of integers, sets the counter to that - /// value. If given a function, that function receives the previous - /// counter value (with each number as a separate argument) and has to - /// return the new value (integer or array). - update: CounterUpdate, - ) -> Content { - UpdateElem::new(self.0, update).pack() - } - - /// Gets the value of the counter at the given location. Always returns an - /// array of integers, even if the counter has just one number. - #[func] - pub fn at( - &self, - /// The virtual typesetter. - vt: &mut Vt, - /// The location at which the counter value should be retrieved. A - /// suitable location can be retrieved from [`locate`]($locate) or - /// [`query`]($query). - location: Location, - ) -> SourceResult<CounterState> { - let sequence = self.sequence(vt)?; - let offset = vt - .introspector - .query(&self.selector().before(location.into(), true)) - .len(); - let (mut state, page) = sequence[offset].clone(); - if self.is_page() { - let delta = vt.introspector.page(location).get().saturating_sub(page.get()); - state.step(NonZeroUsize::ONE, delta); - } - - Ok(state) - } - - /// Gets the value of the counter at the end of the document. Always returns - /// an array of integers, even if the counter has just one number. - #[func] - pub fn final_( - &self, - /// The virtual typesetter. - vt: &mut Vt, - /// Can be an arbitrary location, as its value is irrelevant for the - /// method's return value. Why is it required then? Typst has to - /// evaluate parts of your code multiple times to determine all counter - /// values. By only allowing this method within [`locate`]($locate) - /// calls, the amount of code that can depend on the method's result is - /// reduced. If you could call `final` directly at the top level of a - /// module, the evaluation of the whole module and its exports could - /// depend on the counter's value. - location: Location, - ) -> SourceResult<CounterState> { - let _ = location; - let sequence = self.sequence(vt)?; - let (mut state, page) = sequence.last().unwrap().clone(); - if self.is_page() { - let delta = vt.introspector.pages().get().saturating_sub(page.get()); - state.step(NonZeroUsize::ONE, delta); - } - Ok(state) - } -} - -impl Repr for Counter { - fn repr(&self) -> EcoString { - eco_format!("counter({})", self.0.repr()) - } -} - -cast! { - type Counter, -} - -/// Identifies a counter. -#[derive(Debug, Clone, PartialEq, Hash)] -pub enum CounterKey { - /// The page counter. - Page, - /// Counts elements matching the given selectors. Only works for locatable - /// elements or labels. - Selector(Selector), - /// Counts through manual counters with the same key. - Str(Str), -} - -cast! { - CounterKey, - self => match self { - Self::Page => PageElem::elem().into_value(), - Self::Selector(v) => v.into_value(), - Self::Str(v) => v.into_value(), - }, - v: Str => Self::Str(v), - v: Label => Self::Selector(Selector::Label(v)), - v: Element => { - if v == PageElem::elem() { - Self::Page - } else { - Self::Selector(LocatableSelector::from_value(v.into_value())?.0) - } - }, - v: LocatableSelector => Self::Selector(v.0), -} - -impl Repr for CounterKey { - fn repr(&self) -> EcoString { - match self { - Self::Page => "page".into(), - Self::Selector(selector) => selector.repr(), - Self::Str(str) => str.repr(), - } - } -} - -/// An update to perform on a counter. -#[ty] -#[derive(Debug, Clone, PartialEq, Hash)] -pub enum CounterUpdate { - /// Set the counter to the specified state. - Set(CounterState), - /// Increase the number for the given level by one. - Step(NonZeroUsize), - /// Apply the given function to the counter's state. - Func(Func), -} - -impl Repr for CounterUpdate { - fn repr(&self) -> EcoString { - "..".into() - } -} - -cast! { - type CounterUpdate, - v: CounterState => Self::Set(v), - v: Func => Self::Func(v), -} - -/// Elements that have special counting behaviour. -pub trait Count { - /// Get the counter update for this element. - fn update(&self) -> Option<CounterUpdate>; -} - -/// Counts through elements with different levels. -#[derive(Debug, Clone, PartialEq, Hash)] -pub struct CounterState(pub SmallVec<[usize; 3]>); - -impl CounterState { - /// Get the initial counter state for the key. - pub fn init(key: &CounterKey) -> Self { - Self(match key { - // special case, because pages always start at one. - CounterKey::Page => smallvec![1], - _ => smallvec![0], - }) - } - - /// Advance the counter and return the numbers for the given heading. - pub fn update(&mut self, vt: &mut Vt, update: CounterUpdate) -> SourceResult<()> { - match update { - CounterUpdate::Set(state) => *self = state, - CounterUpdate::Step(level) => self.step(level, 1), - CounterUpdate::Func(func) => { - *self = func.call_vt(vt, self.0.iter().copied())?.cast().at(func.span())? - } - } - Ok(()) - } - - /// Advance the number of the given level by the specified amount. - pub fn step(&mut self, level: NonZeroUsize, by: usize) { - let level = level.get(); - - if self.0.len() >= level { - self.0[level - 1] = self.0[level - 1].saturating_add(by); - self.0.truncate(level); - } - - while self.0.len() < level { - self.0.push(1); - } - } - - /// Get the first number of the state. - pub fn first(&self) -> usize { - self.0.first().copied().unwrap_or(1) - } - - /// Display the counter state with a numbering. - pub fn display(&self, vt: &mut Vt, numbering: &Numbering) -> SourceResult<Content> { - Ok(numbering.apply_vt(vt, &self.0)?.display()) - } -} - -cast! { - CounterState, - self => Value::Array(self.0.into_iter().map(IntoValue::into_value).collect()), - num: usize => Self(smallvec![num]), - array: Array => Self(array - .into_iter() - .map(Value::cast) - .collect::<StrResult<_>>()?), -} - -/// Executes a display of a state. -#[elem(Locatable, Show)] -struct DisplayElem { - /// The counter. - #[required] - counter: Counter, - - /// The numbering to display the counter with. - #[required] - numbering: Option<Numbering>, - - /// Whether to display both the current and final value. - #[required] - both: bool, -} - -impl Show for DisplayElem { - #[tracing::instrument(name = "DisplayElem::show", skip_all)] - fn show(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Content> { - Ok(vt.delayed(|vt| { - let location = self.location().unwrap(); - let counter = self.counter(); - let numbering = self - .numbering() - .clone() - .or_else(|| { - let CounterKey::Selector(Selector::Elem(func, _)) = counter.0 else { - return None; - }; - - if func == HeadingElem::elem() { - HeadingElem::numbering_in(styles).clone() - } else if func == FigureElem::elem() { - FigureElem::numbering_in(styles) - } else if func == EquationElem::elem() { - EquationElem::numbering_in(styles) - } else { - None - } - }) - .unwrap_or_else(|| NumberingPattern::from_str("1.1").unwrap().into()); - - let state = if *self.both() { - counter.both(vt, location)? - } else { - counter.at(vt, location)? - }; - - state.display(vt, &numbering) - })) - } -} - -/// Executes an update of a counter. -#[elem(Locatable, Show)] -struct UpdateElem { - /// The key that identifies the counter. - #[required] - key: CounterKey, - - /// The update to perform on the counter. - #[required] - update: CounterUpdate, -} - -impl Show for UpdateElem { - #[tracing::instrument(name = "UpdateElem::show", skip(self))] - fn show(&self, _: &mut Vt, _: StyleChain) -> SourceResult<Content> { - Ok(Content::empty()) - } -} - -/// An specialized handler of the page counter that tracks both the physical -/// and the logical page counter. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub struct ManualPageCounter { - physical: NonZeroUsize, - logical: usize, -} - -impl ManualPageCounter { - /// Create a new fast page counter, starting at 1. - pub fn new() -> Self { - Self { physical: NonZeroUsize::ONE, logical: 1 } - } - - /// Get the current physical page counter state. - pub fn physical(&self) -> NonZeroUsize { - self.physical - } - - /// Get the current logical page counter state. - pub fn logical(&self) -> usize { - self.logical - } - - /// Advance past a page. - pub fn visit(&mut self, vt: &mut Vt, page: &Frame) -> SourceResult<()> { - for (_, item) in page.items() { - match item { - FrameItem::Group(group) => self.visit(vt, &group.frame)?, - FrameItem::Meta(Meta::Elem(elem), _) => { - let Some(elem) = elem.to::<UpdateElem>() else { continue }; - if *elem.key() == CounterKey::Page { - let mut state = CounterState(smallvec![self.logical]); - state.update(vt, elem.update().clone())?; - self.logical = state.first(); - } - } - _ => {} - } - } - - Ok(()) - } - - /// Step past a page _boundary._ - pub fn step(&mut self) { - self.physical = self.physical.saturating_add(1); - self.logical += 1; - } -} - -impl Default for ManualPageCounter { - fn default() -> Self { - Self::new() - } -} diff --git a/crates/typst-library/src/meta/document.rs b/crates/typst-library/src/meta/document.rs deleted file mode 100644 index 581be60c..00000000 --- a/crates/typst-library/src/meta/document.rs +++ /dev/null @@ -1,124 +0,0 @@ -use comemo::Prehashed; -use typst::eval::Datetime; - -use crate::layout::{LayoutRoot, PageElem}; -use crate::meta::ManualPageCounter; -use crate::prelude::*; - -/// The root element of a document and its metadata. -/// -/// All documents are automatically wrapped in a `document` element. You cannot -/// create a document element yourself. This function is only used with -/// [set rules]($styling/#set-rules) to specify document metadata. Such a set -/// rule must appear before any of the document's contents. -/// -/// ```example -/// #set document(title: [Hello]) -/// -/// This has no visible output, but -/// embeds metadata into the PDF! -/// ``` -/// -/// Note that metadata set with this function is not rendered within the -/// document. Instead, it is embedded in the compiled PDF file. -#[elem(Construct, LayoutRoot)] -pub struct DocumentElem { - /// The document's title. This is often rendered as the title of the - /// PDF viewer window. - /// - /// While this can be arbitrary content, PDF viewers only support plain text - /// titles, so the conversion might be lossy. - pub title: Option<Content>, - - /// The document's authors. - pub author: Author, - - /// The document's keywords. - pub keywords: Keywords, - - /// The document's creation date. - /// - /// If this is `{auto}` (default), Typst uses the current date and time. - /// Setting it to `{none}` prevents Typst from embedding any creation date - /// into the PDF metadata. - /// - /// The year component must be at least zero in order to be embedded into a - /// PDF. - pub date: Smart<Option<Datetime>>, - - /// The page runs. - #[variadic] - pub children: Vec<Prehashed<Content>>, -} - -impl Construct for DocumentElem { - fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> { - bail!(args.span, "can only be used in set rules") - } -} - -impl LayoutRoot for DocumentElem { - /// Layout the document into a sequence of frames, one per page. - #[tracing::instrument(name = "DocumentElem::layout_root", skip_all)] - fn layout_root(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Document> { - tracing::info!("Document layout"); - - let mut pages = vec![]; - let mut page_counter = ManualPageCounter::new(); - - let children = self.children(); - let mut iter = children.iter().map(|c| &**c).peekable(); - - while let Some(mut child) = iter.next() { - let outer = styles; - let mut styles = styles; - if let Some((elem, local)) = child.to_styled() { - styles = outer.chain(local); - child = elem; - } - - if let Some(page) = child.to::<PageElem>() { - let extend_to = iter.peek().and_then(|&next| { - next.to_styled() - .map_or(next, |(elem, _)| elem) - .to::<PageElem>()? - .clear_to(styles) - }); - let fragment = page.layout(vt, styles, &mut page_counter, extend_to)?; - pages.extend(fragment); - } else { - bail!(child.span(), "unexpected document child"); - } - } - - Ok(Document { - pages, - title: self.title(styles).map(|content| content.plain_text()), - author: self.author(styles).0, - keywords: self.keywords(styles).0, - date: self.date(styles), - }) - } -} - -/// A list of authors. -#[derive(Debug, Default, Clone, PartialEq, Hash)] -pub struct Author(Vec<EcoString>); - -cast! { - Author, - self => self.0.into_value(), - v: EcoString => Self(vec![v]), - v: Array => Self(v.into_iter().map(Value::cast).collect::<StrResult<_>>()?), -} - -/// A list of keywords. -#[derive(Debug, Default, Clone, PartialEq, Hash)] -pub struct Keywords(Vec<EcoString>); - -cast! { - Keywords, - self => self.0.into_value(), - v: EcoString => Self(vec![v]), - v: Array => Self(v.into_iter().map(Value::cast).collect::<StrResult<_>>()?), -} diff --git a/crates/typst-library/src/meta/figure.rs b/crates/typst-library/src/meta/figure.rs deleted file mode 100644 index e96b076c..00000000 --- a/crates/typst-library/src/meta/figure.rs +++ /dev/null @@ -1,588 +0,0 @@ -use std::borrow::Cow; -use std::str::FromStr; - -use crate::layout::{BlockElem, PlaceElem, VElem}; -use crate::meta::{ - Count, Counter, CounterKey, CounterUpdate, Numbering, NumberingPattern, Outlinable, - Refable, Supplement, -}; -use crate::prelude::*; -use crate::text::TextElem; -use crate::visualize::ImageElem; - -/// A figure with an optional caption. -/// -/// Automatically detects its kind to select the correct counting track. For -/// example, figures containing images will be numbered separately from figures -/// containing tables. -/// -/// # Examples -/// The example below shows a basic figure with an image: -/// ```example -/// @glacier shows a glacier. Glaciers -/// are complex systems. -/// -/// #figure( -/// image("glacier.jpg", width: 80%), -/// caption: [A curious figure.], -/// ) <glacier> -/// ``` -/// -/// You can also insert [tables]($table) into figures to give them a caption. -/// The figure will detect this and automatically use a separate counter. -/// -/// ```example -/// #figure( -/// table( -/// columns: 4, -/// [t], [1], [2], [3], -/// [y], [0.3s], [0.4s], [0.8s], -/// ), -/// caption: [Timing results], -/// ) -/// ``` -/// -/// This behaviour can be overridden by explicitly specifying the figure's -/// `kind`. All figures of the same kind share a common counter. -/// -/// # Figure behaviour -/// By default, figures are placed within the flow of content. To make them -/// float to the top or bottom of the page, you can use the -/// [`placement`]($figure.placement) argument. -/// -/// If your figure is too large and its contents are breakable across pages -/// (e.g. if it contains a large table), then you can make the figure itself -/// breakable across pages as well with this show rule: -/// ```typ -/// #show figure: set block(breakable: true) -/// ``` -/// -/// See the [block]($block.breakable) documentation for more information about -/// breakable and non-breakable blocks. -/// -/// # Caption customization -/// You can modify the appearance of the figure's caption with its associated -/// [`caption`]($figure.caption) function. In the example below, we emphasize -/// all captions: -/// -/// ```example -/// #show figure.caption: emph -/// -/// #figure( -/// rect[Hello], -/// caption: [I am emphasized!], -/// ) -/// ``` -/// -/// By using a [`where`]($function.where) selector, we can scope such rules to -/// specific kinds of figures. For example, to position the caption above -/// tables, but keep it below for all other kinds of figures, we could write the -/// following show-set rule: -/// -/// ```example -/// #show figure.where( -/// kind: table -/// ): set figure.caption(position: top) -/// -/// #figure( -/// table(columns: 2)[A][B][C][D], -/// caption: [I'm up here], -/// ) -/// ``` -#[elem(scope, Locatable, Synthesize, Count, Show, Finalize, Refable, Outlinable)] -pub struct FigureElem { - /// The content of the figure. Often, an [image]($image). - #[required] - pub body: Content, - - /// The figure's placement on the page. - /// - /// - `{none}`: The figure stays in-flow exactly where it was specified - /// like other content. - /// - `{auto}`: The figure picks `{top}` or `{bottom}` depending on which - /// is closer. - /// - `{top}`: The figure floats to the top of the page. - /// - `{bottom}`: The figure floats to the bottom of the page. - /// - /// The gap between the main flow content and the floating figure is - /// controlled by the [`clearance`]($place.clearance) argument on the - /// `place` function. - /// - /// ```example - /// #set page(height: 200pt) - /// - /// = Introduction - /// #figure( - /// placement: bottom, - /// caption: [A glacier], - /// image("glacier.jpg", width: 60%), - /// ) - /// #lorem(60) - /// ``` - pub placement: Option<Smart<VAlign>>, - - /// The figure's caption. - pub caption: Option<FigureCaption>, - - /// The kind of figure this is. - /// - /// All figures of the same kind share a common counter. - /// - /// If set to `{auto}`, the figure will try to automatically determine its - /// kind based on the type of its body. Automatically detected kinds are - /// [tables]($table) and [code]($raw). In other cases, the inferred kind is - /// that of an [image]($image). - /// - /// Setting this to something other than `{auto}` will override the - /// automatic detection. This can be useful if - /// - you wish to create a custom figure type that is not an - /// [image]($image), a [table]($table) or [code]($raw), - /// - you want to force the figure to use a specific counter regardless of - /// its content. - /// - /// You can set the kind to be an element function or a string. If you set - /// it to an element function other than [`{table}`]($table), [`{raw}`](raw) - /// or [`{image}`](image), you will need to manually specify the figure's - /// supplement. - /// - /// ```example - /// #figure( - /// circle(radius: 10pt), - /// caption: [A curious atom.], - /// kind: "atom", - /// supplement: [Atom], - /// ) - /// ``` - #[default(Smart::Auto)] - pub kind: Smart<FigureKind>, - - /// The figure's supplement. - /// - /// If set to `{auto}`, the figure will try to automatically determine the - /// correct supplement based on the `kind` and the active - /// [text language]($text.lang). If you are using a custom figure type, you - /// will need to manually specify the supplement. - /// - /// If a function is specified, it is passed the first descendant of the - /// specified `kind` (typically, the figure's body) and should return - /// content. - /// - /// ```example - /// #figure( - /// [The contents of my figure!], - /// caption: [My custom figure], - /// supplement: [Bar], - /// kind: "foo", - /// ) - /// ``` - #[borrowed] - pub supplement: Smart<Option<Supplement>>, - - /// How to number the figure. Accepts a - /// [numbering pattern or function]($numbering). - #[default(Some(NumberingPattern::from_str("1").unwrap().into()))] - pub numbering: Option<Numbering>, - - /// The vertical gap between the body and caption. - #[default(Em::new(0.65).into())] - pub gap: Length, - - /// Whether the figure should appear in an [`outline`]($outline) of figures. - #[default(true)] - pub outlined: bool, - - /// Convenience field to get access to the counter for this figure. - /// - /// The counter only depends on the `kind`: - /// - For (tables)[@table]: `{counter(figure.where(kind: table))}` - /// - For (images)[@image]: `{counter(figure.where(kind: image))}` - /// - For a custom kind: `{counter(figure.where(kind: kind))}` - /// - /// These are the counters you'll need to modify if you want to skip a - /// number or reset the counter. - #[synthesized] - pub counter: Option<Counter>, -} - -#[scope] -impl FigureElem { - #[elem] - type FigureCaption; -} - -impl Synthesize for FigureElem { - fn synthesize(&mut self, vt: &mut Vt, styles: StyleChain) -> SourceResult<()> { - let numbering = self.numbering(styles); - - // Determine the figure's kind. - let kind = self.kind(styles).unwrap_or_else(|| { - self.body() - .query_first(Selector::can::<dyn Figurable>()) - .map(|elem| FigureKind::Elem(elem.func())) - .unwrap_or_else(|| FigureKind::Elem(ImageElem::elem())) - }); - - // Resolve the supplement. - let supplement = match self.supplement(styles).as_ref() { - Smart::Auto => { - // Default to the local name for the kind, if available. - let name = match &kind { - FigureKind::Elem(func) => func - .local_name( - TextElem::lang_in(styles), - TextElem::region_in(styles), - ) - .map(TextElem::packed), - FigureKind::Name(_) => None, - }; - - if numbering.is_some() && name.is_none() { - bail!(self.span(), "please specify the figure's supplement") - } - - Some(name.unwrap_or_default()) - } - Smart::Custom(None) => None, - Smart::Custom(Some(supplement)) => { - // Resolve the supplement with the first descendant of the kind or - // just the body, if none was found. - let descendant = match kind { - FigureKind::Elem(func) => self - .body() - .query_first(Selector::Elem(func, None)) - .map(Cow::Owned), - FigureKind::Name(_) => None, - }; - - let target = descendant.unwrap_or_else(|| Cow::Borrowed(self.body())); - Some(supplement.resolve(vt, [target])?) - } - }; - - // Construct the figure's counter. - let counter = - Counter::new(CounterKey::Selector(select_where!(Self, Kind => kind.clone()))); - - // Fill the figure's caption. - let mut caption = self.caption(styles); - if let Some(caption) = &mut caption { - caption.push_kind(kind.clone()); - caption.push_supplement(supplement.clone()); - caption.push_numbering(numbering.clone()); - caption.push_counter(Some(counter.clone())); - caption.push_figure_location(self.location()); - } - - self.push_placement(self.placement(styles)); - self.push_caption(caption); - self.push_kind(Smart::Custom(kind)); - self.push_supplement(Smart::Custom(supplement.map(Supplement::Content))); - self.push_numbering(numbering); - self.push_outlined(self.outlined(styles)); - self.push_counter(Some(counter)); - - Ok(()) - } -} - -impl Show for FigureElem { - #[tracing::instrument(name = "FigureElem::show", skip_all)] - fn show(&self, _: &mut Vt, styles: StyleChain) -> SourceResult<Content> { - let mut realized = self.body().clone(); - - // Build the caption, if any. - if let Some(caption) = self.caption(styles) { - let v = VElem::weak(self.gap(styles).into()).pack(); - realized = if caption.position(styles) == VAlign::Bottom { - realized + v + caption.pack() - } else { - caption.pack() + v + realized - }; - } - - // Wrap the contents in a block. - realized = BlockElem::new() - .with_body(Some(realized)) - .pack() - .aligned(Align::CENTER); - - // Wrap in a float. - if let Some(align) = self.placement(styles) { - realized = PlaceElem::new(realized) - .with_float(true) - .with_alignment(align.map(|align| HAlign::Center + align)) - .pack(); - } - - Ok(realized) - } -} - -impl Finalize for FigureElem { - fn finalize(&self, realized: Content, _: StyleChain) -> Content { - // Allow breakable figures with `show figure: set block(breakable: true)`. - realized.styled(BlockElem::set_breakable(false)) - } -} - -impl Count for FigureElem { - fn update(&self) -> Option<CounterUpdate> { - // If the figure is numbered, step the counter by one. - // This steps the `counter(figure)` which is global to all numbered figures. - self.numbering(StyleChain::default()) - .is_some() - .then(|| CounterUpdate::Step(NonZeroUsize::ONE)) - } -} - -impl Refable for FigureElem { - fn supplement(&self) -> Content { - // After synthesis, this should always be custom content. - let default = StyleChain::default(); - match self.supplement(default).as_ref() { - Smart::Custom(Some(Supplement::Content(content))) => content.clone(), - _ => Content::empty(), - } - } - - fn counter(&self) -> Counter { - self.counter().clone().unwrap_or_else(|| Counter::of(Self::elem())) - } - - fn numbering(&self) -> Option<Numbering> { - self.numbering(StyleChain::default()) - } -} - -impl Outlinable for FigureElem { - fn outline(&self, vt: &mut Vt) -> SourceResult<Option<Content>> { - if !self.outlined(StyleChain::default()) { - return Ok(None); - } - - let Some(caption) = self.caption(StyleChain::default()) else { - return Ok(None); - }; - - let mut realized = caption.body().clone(); - if let ( - Smart::Custom(Some(Supplement::Content(mut supplement))), - Some(counter), - Some(numbering), - ) = ( - self.supplement(StyleChain::default()).clone(), - self.counter(), - self.numbering(StyleChain::default()), - ) { - let location = self.location().unwrap(); - let numbers = counter.at(vt, location)?.display(vt, &numbering)?; - - if !supplement.is_empty() { - supplement += TextElem::packed('\u{a0}'); - } - - let separator = caption.get_separator(StyleChain::default()); - - realized = supplement + numbers + separator + caption.body(); - } - - Ok(Some(realized)) - } -} - -/// The caption of a figure. This element can be used in set and show rules to -/// customize the appearance of captions for all figures or figures of a -/// specific kind. -/// -/// In addition to its `pos` and `body`, the `caption` also provides the -/// figure's `kind`, `supplement`, `counter`, `numbering`, and `location` as -/// fields. These parts can be used in [`where`]($function.where) selectors and -/// show rules to build a completely custom caption. -/// -/// ```example -/// #show figure.caption: emph -/// -/// #figure( -/// rect[Hello], -/// caption: [A rectangle], -/// ) -/// ``` -#[elem(name = "caption", Synthesize, Show)] -pub struct FigureCaption { - /// The caption's position in the figure. Either `{top}` or `{bottom}`. - /// - /// ```example - /// #show figure.where( - /// kind: table - /// ): set figure.caption(position: top) - /// - /// #figure( - /// table(columns: 2)[A][B], - /// caption: [I'm up here], - /// ) - /// - /// #figure( - /// rect[Hi], - /// caption: [I'm down here], - /// ) - /// - /// #figure( - /// table(columns: 2)[A][B], - /// caption: figure.caption( - /// position: bottom, - /// [I'm down here too!] - /// ) - /// ) - /// ``` - #[default(VAlign::Bottom)] - #[parse({ - let option: Option<Spanned<VAlign>> = args.named("position")?; - if let Some(Spanned { v: align, span }) = option { - if align == VAlign::Horizon { - bail!(span, "expected `top` or `bottom`"); - } - } - option.map(|spanned| spanned.v) - })] - pub position: VAlign, - - /// The separator which will appear between the number and body. - /// - /// ```example - /// #set figure.caption(separator: [ --- ]) - /// - /// #figure( - /// rect[Hello], - /// caption: [A rectangle], - /// ) - /// ``` - /// - /// If set to `{auto}`, the separator will be adapted to the current - /// [language]($text.lang) and [region]($text.region). - pub separator: Smart<Content>, - - /// The caption's body. - /// - /// Can be used alongside `kind`, `supplement`, `counter`, `numbering`, and - /// `location` to completely customize the caption. - /// - /// ```example - /// #show figure.caption: it => [ - /// #underline(it.body) | - /// #it.supplement #it.counter.display(it.numbering) - /// ] - /// - /// #figure( - /// rect[Hello], - /// caption: [A rectangle], - /// ) - /// ``` - #[required] - pub body: Content, - - /// The figure's supplement. - #[synthesized] - pub kind: FigureKind, - - /// The figure's supplement. - #[synthesized] - #[default(None)] - pub supplement: Option<Content>, - - /// How to number the figure. - #[synthesized] - #[default(None)] - pub numbering: Option<Numbering>, - - /// The counter for the figure. - #[synthesized] - #[default(None)] - pub counter: Option<Counter>, - - /// The figure's location. - #[internal] - #[synthesized] - #[default(None)] - pub figure_location: Option<Location>, -} - -impl FigureCaption { - /// Gets the default separator in the given language and (optionally) - /// region. - fn local_separator(lang: Lang, _: Option<Region>) -> &'static str { - match lang { - Lang::CHINESE => "\u{2003}", - Lang::FRENCH => ".\u{a0}– ", - Lang::RUSSIAN => ". ", - Lang::ENGLISH | _ => ": ", - } - } - - fn get_separator(&self, styles: StyleChain) -> Content { - self.separator(styles).unwrap_or_else(|| { - TextElem::packed(Self::local_separator( - TextElem::lang_in(styles), - TextElem::region_in(styles), - )) - }) - } -} - -impl Synthesize for FigureCaption { - fn synthesize(&mut self, _: &mut Vt, styles: StyleChain) -> SourceResult<()> { - self.push_position(self.position(styles)); - self.push_separator(Smart::Custom(self.get_separator(styles))); - Ok(()) - } -} - -impl Show for FigureCaption { - #[tracing::instrument(name = "FigureCaption::show", skip_all)] - fn show(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Content> { - let mut realized = self.body().clone(); - - if let (Some(mut supplement), Some(numbering), Some(counter), Some(location)) = ( - self.supplement().clone(), - self.numbering(), - self.counter(), - self.figure_location(), - ) { - let numbers = counter.at(vt, *location)?.display(vt, numbering)?; - if !supplement.is_empty() { - supplement += TextElem::packed('\u{a0}'); - } - realized = supplement + numbers + self.get_separator(styles) + realized; - } - - Ok(realized) - } -} - -cast! { - FigureCaption, - v: Content => v.to::<Self>().cloned().unwrap_or_else(|| Self::new(v.clone())), -} - -/// The `kind` parameter of a [`FigureElem`]. -#[derive(Debug, Clone, PartialEq, Hash)] -pub enum FigureKind { - /// The kind is an element function. - Elem(Element), - /// The kind is a name. - Name(EcoString), -} - -cast! { - FigureKind, - self => match self { - Self::Elem(v) => v.into_value(), - Self::Name(v) => v.into_value(), - }, - v: Element => Self::Elem(v), - v: EcoString => Self::Name(v), -} - -/// An element that can be auto-detected in a figure. -/// -/// This trait is used to determine the type of a figure. -pub trait Figurable {} diff --git a/crates/typst-library/src/meta/footnote.rs b/crates/typst-library/src/meta/footnote.rs deleted file mode 100644 index 189dbcb8..00000000 --- a/crates/typst-library/src/meta/footnote.rs +++ /dev/null @@ -1,305 +0,0 @@ -use comemo::Prehashed; -use std::str::FromStr; - -use crate::layout::{HElem, ParElem}; -use crate::meta::{Count, Counter, CounterUpdate, Numbering, NumberingPattern}; -use crate::prelude::*; -use crate::text::{SuperElem, TextElem, TextSize}; -use crate::visualize::LineElem; - -/// A footnote. -/// -/// Includes additional remarks and references on the same page with footnotes. -/// A footnote will insert a superscript number that links to the note at the -/// bottom of the page. Notes are numbered sequentially throughout your document -/// and can break across multiple pages. -/// -/// To customize the appearance of the entry in the footnote listing, see -/// [`footnote.entry`]($footnote.entry). The footnote itself is realized as a -/// normal superscript, so you can use a set rule on the [`super`]($super) -/// function to customize it. -/// -/// # Example -/// ```example -/// Check the docs for more details. -/// #footnote[https://typst.app/docs] -/// ``` -/// -/// The footnote automatically attaches itself to the preceding word, even if -/// there is a space before it in the markup. To force space, you can use the -/// string `[#" "]` or explicit [horizontal spacing]($h). -/// -/// By giving a label to a footnote, you can have multiple references to it. -/// -/// ```example -/// You can edit Typst documents online. -/// #footnote[https://typst.app/app] <fn> -/// Checkout Typst's website. @fn -/// And the online app. #footnote(<fn>) -/// ``` -/// -/// _Note:_ Set and show rules in the scope where `footnote` is called may not -/// apply to the footnote's content. See [here][issue] for more information. -/// -/// [issue]: https://github.com/typst/typst/issues/1467#issuecomment-1588799440 -#[elem(scope, Locatable, Synthesize, Show, Count)] -pub struct FootnoteElem { - /// How to number footnotes. - /// - /// By default, the footnote numbering continues throughout your document. - /// If you prefer per-page footnote numbering, you can reset the footnote - /// [counter]($counter) in the page [header]($page.header). In the future, - /// there might be a simpler way to achieve this. - /// - /// ```example - /// #set footnote(numbering: "*") - /// - /// Footnotes: - /// #footnote[Star], - /// #footnote[Dagger] - /// ``` - #[borrowed] - #[default(Numbering::Pattern(NumberingPattern::from_str("1").unwrap()))] - pub numbering: Numbering, - - /// The content to put into the footnote. Can also be the label of another - /// footnote this one should point to. - #[required] - pub body: FootnoteBody, -} - -#[scope] -impl FootnoteElem { - #[elem] - type FootnoteEntry; -} - -impl FootnoteElem { - /// Creates a new footnote that the passed content as its body. - pub fn with_content(content: Content) -> Self { - Self::new(FootnoteBody::Content(content)) - } - - /// Creates a new footnote referencing the footnote with the specified label. - pub fn with_label(label: Label) -> Self { - Self::new(FootnoteBody::Reference(label)) - } - - /// Tests if this footnote is a reference to another footnote. - pub fn is_ref(&self) -> bool { - matches!(self.body(), FootnoteBody::Reference(_)) - } - - /// Returns the content of the body of this footnote if it is not a ref. - pub fn body_content(&self) -> Option<&Content> { - match self.body() { - FootnoteBody::Content(content) => Some(content), - _ => None, - } - } - - /// Returns the location of the definition of this footnote. - pub fn declaration_location(&self, vt: &Vt) -> StrResult<Location> { - match self.body() { - FootnoteBody::Reference(label) => { - let element: Prehashed<Content> = vt.introspector.query_label(*label)?; - let footnote = element - .to::<FootnoteElem>() - .ok_or("referenced element should be a footnote")?; - footnote.declaration_location(vt) - } - _ => Ok(self.location().unwrap()), - } - } -} - -impl Synthesize for FootnoteElem { - fn synthesize(&mut self, _vt: &mut Vt, styles: StyleChain) -> SourceResult<()> { - self.push_numbering(self.numbering(styles).clone()); - Ok(()) - } -} - -impl Show for FootnoteElem { - #[tracing::instrument(name = "FootnoteElem::show", skip_all)] - fn show(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Content> { - Ok(vt.delayed(|vt| { - let loc = self.declaration_location(vt).at(self.span())?; - let numbering = self.numbering(styles); - let counter = Counter::of(Self::elem()); - let num = counter.at(vt, loc)?.display(vt, numbering)?; - let sup = SuperElem::new(num).pack(); - let loc = loc.variant(1); - // Add zero-width weak spacing to make the footnote "sticky". - Ok(HElem::hole().pack() + sup.linked(Destination::Location(loc))) - })) - } -} - -impl Count for FootnoteElem { - fn update(&self) -> Option<CounterUpdate> { - (!self.is_ref()).then(|| CounterUpdate::Step(NonZeroUsize::ONE)) - } -} - -/// The body of a footnote can be either some content or a label referencing -/// another footnote. -#[derive(Debug, Clone, PartialEq, Hash)] -pub enum FootnoteBody { - Content(Content), - Reference(Label), -} - -cast! { - FootnoteBody, - self => match self { - Self::Content(v) => v.into_value(), - Self::Reference(v) => v.into_value(), - }, - v: Content => Self::Content(v), - v: Label => Self::Reference(v), -} - -/// An entry in a footnote list. -/// -/// This function is not intended to be called directly. Instead, it is used -/// in set and show rules to customize footnote listings. -/// -/// _Note:_ Set and show rules for `footnote.entry` must be defined at the -/// beginning of the document in order to work correctly. -/// See [here](https://github.com/typst/typst/issues/1348#issuecomment-1566316463) -/// for more information. -/// -/// ```example -/// #show footnote.entry: set text(red) -/// -/// My footnote listing -/// #footnote[It's down here] -/// has red text! -/// ``` -#[elem(name = "entry", title = "Footnote Entry", Show, Finalize)] -pub struct FootnoteEntry { - /// The footnote for this entry. It's location can be used to determine - /// the footnote counter state. - /// - /// ```example - /// #show footnote.entry: it => { - /// let loc = it.note.location() - /// numbering( - /// "1: ", - /// ..counter(footnote).at(loc), - /// ) - /// it.note.body - /// } - /// - /// Customized #footnote[Hello] - /// listing #footnote[World! 🌏] - /// ``` - #[required] - pub note: FootnoteElem, - - /// The separator between the document body and the footnote listing. - /// - /// ```example - /// #set footnote.entry( - /// separator: repeat[.] - /// ) - /// - /// Testing a different separator. - /// #footnote[ - /// Unconventional, but maybe - /// not that bad? - /// ] - /// ``` - #[default( - LineElem::new() - .with_length(Ratio::new(0.3).into()) - .with_stroke(Stroke { - thickness: Smart::Custom(Abs::pt(0.5).into()), - ..Default::default() - }) - .pack() - )] - pub separator: Content, - - /// The amount of clearance between the document body and the separator. - /// - /// ```example - /// #set footnote.entry(clearance: 3em) - /// - /// Footnotes also need ... - /// #footnote[ - /// ... some space to breathe. - /// ] - /// ``` - #[default(Em::new(1.0).into())] - #[resolve] - pub clearance: Length, - - /// The gap between footnote entries. - /// - /// ```example - /// #set footnote.entry(gap: 0.8em) - /// - /// Footnotes: - /// #footnote[Spaced], - /// #footnote[Apart] - /// ``` - #[default(Em::new(0.5).into())] - #[resolve] - pub gap: Length, - - /// The indent of each footnote entry. - /// - /// ```example - /// #set footnote.entry(indent: 0em) - /// - /// Footnotes: - /// #footnote[No], - /// #footnote[Indent] - /// ``` - #[default(Em::new(1.0).into())] - pub indent: Length, -} - -impl Show for FootnoteEntry { - fn show(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Content> { - let note = self.note(); - let number_gap = Em::new(0.05); - let default = StyleChain::default(); - let numbering = note.numbering(default); - let counter = Counter::of(FootnoteElem::elem()); - let Some(loc) = note.location() else { - bail!(error!(self.span(), "footnote entry must have a location").with_hint( - "try using a query or a show rule to customize the footnote instead" - )) - }; - - let num = counter.at(vt, loc)?.display(vt, numbering)?; - let sup = SuperElem::new(num) - .pack() - .linked(Destination::Location(loc)) - .backlinked(loc.variant(1)); - Ok(Content::sequence([ - HElem::new(self.indent(styles).into()).pack(), - sup, - HElem::new(number_gap.into()).with_weak(true).pack(), - note.body_content().unwrap().clone(), - ])) - } -} - -impl Finalize for FootnoteEntry { - fn finalize(&self, realized: Content, _: StyleChain) -> Content { - let text_size = Em::new(0.85); - let leading = Em::new(0.5); - realized - .styled(ParElem::set_leading(leading.into())) - .styled(TextElem::set_size(TextSize(text_size.into()))) - } -} - -cast! { - FootnoteElem, - v: Content => v.to::<Self>().cloned().unwrap_or_else(|| Self::with_content(v.clone())), -} diff --git a/crates/typst-library/src/meta/heading.rs b/crates/typst-library/src/meta/heading.rs deleted file mode 100644 index 0f814dc9..00000000 --- a/crates/typst-library/src/meta/heading.rs +++ /dev/null @@ -1,269 +0,0 @@ -use typst::font::FontWeight; -use typst::util::option_eq; - -use super::{Counter, CounterUpdate, LocalName, Numbering, Outlinable, Refable}; -use crate::layout::{BlockElem, HElem, VElem}; -use crate::meta::{Count, LocalNameIn, Supplement}; -use crate::prelude::*; -use crate::text::{SpaceElem, TextElem, TextSize}; - -/// A section heading. -/// -/// With headings, you can structure your document into sections. Each heading -/// has a _level,_ which starts at one and is unbounded upwards. This level -/// indicates the logical role of the following content (section, subsection, -/// etc.) A top-level heading indicates a top-level section of the document -/// (not the document's title). -/// -/// Typst can automatically number your headings for you. To enable numbering, -/// specify how you want your headings to be numbered with a -/// [numbering pattern or function]($numbering). -/// -/// Independently from the numbering, Typst can also automatically generate an -/// [outline]($outline) of all headings for you. To exclude one or more headings -/// from this outline, you can set the `outlined` parameter to `{false}`. -/// -/// # Example -/// ```example -/// #set heading(numbering: "1.a)") -/// -/// = Introduction -/// In recent years, ... -/// -/// == Preliminaries -/// To start, ... -/// ``` -/// -/// # Syntax -/// Headings have dedicated syntax: They can be created by starting a line with -/// one or multiple equals signs, followed by a space. The number of equals -/// signs determines the heading's logical nesting depth. -#[elem(Locatable, Synthesize, Count, Show, Finalize, LocalName, Refable, Outlinable)] -pub struct HeadingElem { - /// The logical nesting depth of the heading, starting from one. - #[default(NonZeroUsize::ONE)] - pub level: NonZeroUsize, - - /// How to number the heading. Accepts a - /// [numbering pattern or function]($numbering). - /// - /// ```example - /// #set heading(numbering: "1.a.") - /// - /// = A section - /// == A subsection - /// === A sub-subsection - /// ``` - #[borrowed] - pub numbering: Option<Numbering>, - - /// A supplement for the heading. - /// - /// For references to headings, this is added before the referenced number. - /// - /// If a function is specified, it is passed the referenced heading and - /// should return content. - /// - /// ```example - /// #set heading(numbering: "1.", supplement: [Chapter]) - /// - /// = Introduction <intro> - /// In @intro, we see how to turn - /// Sections into Chapters. And - /// in @intro[Part], it is done - /// manually. - /// ``` - pub supplement: Smart<Option<Supplement>>, - - /// Whether the heading should appear in the [outline]($outline). - /// - /// Note that this property, if set to `{true}`, ensures the heading is also - /// shown as a bookmark in the exported PDF's outline (when exporting to - /// PDF). To change that behavior, use the `bookmarked` property. - /// - /// ```example - /// #outline() - /// - /// #heading[Normal] - /// This is a normal heading. - /// - /// #heading(outlined: false)[Hidden] - /// This heading does not appear - /// in the outline. - /// ``` - #[default(true)] - pub outlined: bool, - - /// Whether the heading should appear as a bookmark in the exported PDF's - /// outline. Doesn't affect other export formats, such as PNG. - /// - /// The default value of `{auto}` indicates that the heading will only - /// appear in the exported PDF's outline if its `outlined` property is set - /// to `{true}`, that is, if it would also be listed in Typst's - /// [outline]($outline). Setting this property to either `{true}` (bookmark) - /// or `{false}` (don't bookmark) bypasses that behavior. - /// - /// ```example - /// #heading[Normal heading] - /// This heading will be shown in - /// the PDF's bookmark outline. - /// - /// #heading(bookmarked: false)[Not bookmarked] - /// This heading won't be - /// bookmarked in the resulting - /// PDF. - /// ``` - #[default(Smart::Auto)] - pub bookmarked: Smart<bool>, - - /// The heading's title. - #[required] - pub body: Content, -} - -impl Synthesize for HeadingElem { - fn synthesize(&mut self, vt: &mut Vt, styles: StyleChain) -> SourceResult<()> { - // Resolve the supplement. - let supplement = match self.supplement(styles) { - Smart::Auto => TextElem::packed(Self::local_name_in(styles)), - Smart::Custom(None) => Content::empty(), - Smart::Custom(Some(supplement)) => supplement.resolve(vt, [self.clone()])?, - }; - - self.push_level(self.level(styles)); - self.push_numbering(self.numbering(styles).clone()); - self.push_supplement(Smart::Custom(Some(Supplement::Content(supplement)))); - self.push_outlined(self.outlined(styles)); - self.push_bookmarked(self.bookmarked(styles)); - - Ok(()) - } -} - -impl Show for HeadingElem { - #[tracing::instrument(name = "HeadingElem::show", skip_all)] - fn show(&self, _: &mut Vt, styles: StyleChain) -> SourceResult<Content> { - let mut realized = self.body().clone(); - if let Some(numbering) = self.numbering(styles).as_ref() { - realized = Counter::of(Self::elem()) - .display(Some(numbering.clone()), false) - .spanned(self.span()) - + HElem::new(Em::new(0.3).into()).with_weak(true).pack() - + realized; - } - Ok(BlockElem::new().with_body(Some(realized)).pack()) - } -} - -impl Finalize for HeadingElem { - fn finalize(&self, realized: Content, styles: StyleChain) -> Content { - let level = self.level(styles).get(); - let scale = match level { - 1 => 1.4, - 2 => 1.2, - _ => 1.0, - }; - - let size = Em::new(scale); - let above = Em::new(if level == 1 { 1.8 } else { 1.44 }) / scale; - let below = Em::new(0.75) / scale; - - let mut styles = Styles::new(); - styles.set(TextElem::set_size(TextSize(size.into()))); - styles.set(TextElem::set_weight(FontWeight::BOLD)); - styles.set(BlockElem::set_above(VElem::block_around(above.into()))); - styles.set(BlockElem::set_below(VElem::block_around(below.into()))); - styles.set(BlockElem::set_sticky(true)); - realized.styled_with_map(styles) - } -} - -impl Count for HeadingElem { - fn update(&self) -> Option<CounterUpdate> { - self.numbering(StyleChain::default()) - .is_some() - .then(|| CounterUpdate::Step(self.level(StyleChain::default()))) - } -} - -cast! { - HeadingElem, - v: Content => v.to::<Self>().ok_or("expected heading")?.clone(), -} - -impl Refable for HeadingElem { - fn supplement(&self) -> Content { - // After synthesis, this should always be custom content. - match self.supplement(StyleChain::default()) { - Smart::Custom(Some(Supplement::Content(content))) => content, - _ => Content::empty(), - } - } - - fn counter(&self) -> Counter { - Counter::of(Self::elem()) - } - - fn numbering(&self) -> Option<Numbering> { - self.numbering(StyleChain::default()).clone() - } -} - -impl Outlinable for HeadingElem { - fn outline(&self, vt: &mut Vt) -> SourceResult<Option<Content>> { - if !self.outlined(StyleChain::default()) { - return Ok(None); - } - - let mut content = self.body().clone(); - let default = StyleChain::default(); - if let Some(numbering) = self.numbering(default).as_ref() { - let numbers = Counter::of(Self::elem()) - .at(vt, self.location().unwrap())? - .display(vt, numbering)?; - content = numbers + SpaceElem::new().pack() + content; - }; - - Ok(Some(content)) - } - - fn level(&self) -> NonZeroUsize { - self.level(StyleChain::default()) - } -} - -impl LocalName for HeadingElem { - fn local_name(lang: Lang, region: Option<Region>) -> &'static str { - match lang { - Lang::ALBANIAN => "Kapitull", - Lang::ARABIC => "الفصل", - Lang::BOKMÅL => "Kapittel", - Lang::CHINESE if option_eq(region, "TW") => "小節", - Lang::CHINESE => "小节", - Lang::CZECH => "Kapitola", - Lang::DANISH => "Afsnit", - Lang::DUTCH => "Hoofdstuk", - Lang::FILIPINO => "Seksyon", - Lang::FINNISH => "Osio", - Lang::FRENCH => "Chapitre", - Lang::GERMAN => "Abschnitt", - Lang::GREEK => "Κεφάλαιο", - Lang::HUNGARIAN => "Fejezet", - Lang::ITALIAN => "Sezione", - Lang::NYNORSK => "Kapittel", - Lang::POLISH => "Sekcja", - Lang::PORTUGUESE if option_eq(region, "PT") => "Secção", - Lang::PORTUGUESE => "Seção", - Lang::ROMANIAN => "Secțiunea", - Lang::RUSSIAN => "Раздел", - Lang::SLOVENIAN => "Poglavje", - Lang::SPANISH => "Sección", - Lang::SWEDISH => "Kapitel", - Lang::TURKISH => "Bölüm", - Lang::UKRAINIAN => "Розділ", - Lang::VIETNAMESE => "Phần", // TODO: This may be wrong. - Lang::JAPANESE => "節", - Lang::ENGLISH | _ => "Section", - } - } -} diff --git a/crates/typst-library/src/meta/link.rs b/crates/typst-library/src/meta/link.rs deleted file mode 100644 index 74857f1b..00000000 --- a/crates/typst-library/src/meta/link.rs +++ /dev/null @@ -1,133 +0,0 @@ -use crate::prelude::*; -use crate::text::{Hyphenate, TextElem}; - -/// Links to a URL or a location in the document. -/// -/// By default, links are not styled any different from normal text. However, -/// you can easily apply a style of your choice with a show rule. -/// -/// # Example -/// ```example -/// #show link: underline -/// -/// https://example.com \ -/// -/// #link("https://example.com") \ -/// #link("https://example.com")[ -/// See example.com -/// ] -/// ``` -/// -/// # Syntax -/// This function also has dedicated syntax: Text that starts with `http://` or -/// `https://` is automatically turned into a link. -#[elem(Show)] -pub struct LinkElem { - /// The destination the link points to. - /// - /// - To link to web pages, `dest` should be a valid URL string. If the URL - /// is in the `mailto:` or `tel:` scheme and the `body` parameter is - /// omitted, the email address or phone number will be the link's body, - /// without the scheme. - /// - /// - To link to another part of the document, `dest` can take one of three - /// forms: - /// - A [label]($label) attached to an element. If you also want automatic - /// text for the link based on the element, consider using a - /// [reference]($ref) instead. - /// - /// - A [location]($locate) resulting from a [`locate`]($locate) call or - /// [`query`]($query). - /// - /// - A dictionary with a `page` key of type [integer]($int) and `x` and - /// `y` coordinates of type [length]($length). Pages are counted from - /// one, and the coordinates are relative to the page's top left corner. - /// - /// ```example - /// = Introduction <intro> - /// #link("mailto:hello@typst.app") \ - /// #link(<intro>)[Go to intro] \ - /// #link((page: 1, x: 0pt, y: 0pt))[ - /// Go to top - /// ] - /// ``` - #[required] - #[parse( - let dest = args.expect::<LinkTarget>("destination")?; - dest.clone() - )] - pub dest: LinkTarget, - - /// The content that should become a link. - /// - /// If `dest` is an URL string, the parameter can be omitted. In this case, - /// the URL will be shown as the link. - #[required] - #[parse(match &dest { - LinkTarget::Dest(Destination::Url(url)) => match args.eat()? { - Some(body) => body, - None => body_from_url(url), - }, - _ => args.expect("body")?, - })] - pub body: Content, -} - -impl LinkElem { - /// Create a link element from a URL with its bare text. - pub fn from_url(url: EcoString) -> Self { - let body = body_from_url(&url); - Self::new(LinkTarget::Dest(Destination::Url(url)), body) - } -} - -impl Show for LinkElem { - #[tracing::instrument(name = "LinkElem::show", skip(self, vt))] - fn show(&self, vt: &mut Vt, _: StyleChain) -> SourceResult<Content> { - let body = self.body().clone(); - let linked = match self.dest() { - LinkTarget::Dest(dest) => body.linked(dest.clone()), - LinkTarget::Label(label) => vt - .delayed(|vt| { - let elem = vt.introspector.query_label(*label).at(self.span())?; - let dest = Destination::Location(elem.location().unwrap()); - Ok(Some(body.clone().linked(dest))) - }) - .unwrap_or(body), - }; - - Ok(linked.styled(TextElem::set_hyphenate(Hyphenate(Smart::Custom(false))))) - } -} - -fn body_from_url(url: &EcoString) -> Content { - let mut text = url.as_str(); - for prefix in ["mailto:", "tel:"] { - text = text.trim_start_matches(prefix); - } - let shorter = text.len() < url.len(); - TextElem::packed(if shorter { text.into() } else { url.clone() }) -} - -/// A target where a link can go. -#[derive(Debug, Clone, PartialEq, Hash)] -pub enum LinkTarget { - Dest(Destination), - Label(Label), -} - -cast! { - LinkTarget, - self => match self { - Self::Dest(v) => v.into_value(), - Self::Label(v) => v.into_value(), - }, - v: Destination => Self::Dest(v), - v: Label => Self::Label(v), -} - -impl From<Destination> for LinkTarget { - fn from(dest: Destination) -> Self { - Self::Dest(dest) - } -} diff --git a/crates/typst-library/src/meta/metadata.rs b/crates/typst-library/src/meta/metadata.rs deleted file mode 100644 index b4ae64cb..00000000 --- a/crates/typst-library/src/meta/metadata.rs +++ /dev/null @@ -1,40 +0,0 @@ -use crate::prelude::*; - -/// Exposes a value to the query system without producing visible content. -/// -/// This element can be retrieved with the [`query`]($query) function and from -/// the command with [`typst query`]($reference/meta/query/#cli-queries). Its -/// purpose is to expose an arbitrary value to the introspection system. To -/// identify a metadata value among others, you can attach a [`label`]($label) -/// to it and query for that label. -/// -/// The `metadata` element is especially useful for command line queries because -/// it allows you to expose arbitrary values to the outside world. -/// -/// ```example -/// // Put metadata somewhere. -/// #metadata("This is a note") <note> -/// -/// // And find it from anywhere else. -/// #locate(loc => { -/// query(<note>, loc).first().value -/// }) -/// ``` -#[elem(Behave, Show, Locatable)] -pub struct MetadataElem { - /// The value to embed into the document. - #[required] - pub value: Value, -} - -impl Show for MetadataElem { - fn show(&self, _vt: &mut Vt, _styles: StyleChain) -> SourceResult<Content> { - Ok(Content::empty()) - } -} - -impl Behave for MetadataElem { - fn behaviour(&self) -> Behaviour { - Behaviour::Invisible - } -} diff --git a/crates/typst-library/src/meta/mod.rs b/crates/typst-library/src/meta/mod.rs deleted file mode 100644 index ffe861ab..00000000 --- a/crates/typst-library/src/meta/mod.rs +++ /dev/null @@ -1,76 +0,0 @@ -//! Interaction between document parts. - -mod bibliography; -mod cite; -mod context; -mod counter; -mod document; -mod figure; -mod footnote; -mod heading; -mod link; -mod metadata; -#[path = "numbering.rs"] -mod numbering_; -mod outline; -#[path = "query.rs"] -mod query_; -mod reference; -mod state; - -pub use self::bibliography::*; -pub use self::cite::*; -pub use self::context::*; -pub use self::counter::*; -pub use self::document::*; -pub use self::figure::*; -pub use self::footnote::*; -pub use self::heading::*; -pub use self::link::*; -pub use self::metadata::*; -pub use self::numbering_::*; -pub use self::outline::*; -pub use self::query_::*; -pub use self::reference::*; -pub use self::state::*; - -use crate::prelude::*; -use crate::text::TextElem; - -/// Hook up all meta definitions. -pub(super) fn define(global: &mut Scope) { - global.category("meta"); - global.define_type::<Label>(); - global.define_type::<Selector>(); - global.define_type::<Location>(); - global.define_type::<Counter>(); - global.define_type::<State>(); - global.define_elem::<DocumentElem>(); - global.define_elem::<RefElem>(); - global.define_elem::<LinkElem>(); - global.define_elem::<OutlineElem>(); - global.define_elem::<HeadingElem>(); - global.define_elem::<FigureElem>(); - global.define_elem::<FootnoteElem>(); - global.define_elem::<CiteElem>(); - global.define_elem::<BibliographyElem>(); - global.define_elem::<MetadataElem>(); - global.define_func::<locate>(); - global.define_func::<style>(); - global.define_func::<layout>(); - global.define_func::<numbering>(); - global.define_func::<query>(); -} - -/// An element that has a local name. -pub trait LocalNameIn: LocalName { - /// Gets the local name from the style chain. - fn local_name_in(styles: StyleChain) -> &'static str - where - Self: Sized, - { - Self::local_name(TextElem::lang_in(styles), TextElem::region_in(styles)) - } -} - -impl<T: LocalName> LocalNameIn for T {} diff --git a/crates/typst-library/src/meta/numbering.rs b/crates/typst-library/src/meta/numbering.rs deleted file mode 100644 index 72906ff6..00000000 --- a/crates/typst-library/src/meta/numbering.rs +++ /dev/null @@ -1,567 +0,0 @@ -use std::str::FromStr; - -use chinese_number::{ChineseCase, ChineseCountMethod, ChineseVariant, NumberToChinese}; -use ecow::EcoVec; -use typst::doc::{PdfPageLabel, PdfPageLabelStyle}; - -use crate::prelude::*; -use crate::text::Case; - -/// Applies a numbering to a sequence of numbers. -/// -/// A numbering defines how a sequence of numbers should be displayed as -/// content. It is defined either through a pattern string or an arbitrary -/// function. -/// -/// A numbering pattern consists of counting symbols, for which the actual -/// number is substituted, their prefixes, and one suffix. The prefixes and the -/// suffix are repeated as-is. -/// -/// # Example -/// ```example -/// #numbering("1.1)", 1, 2, 3) \ -/// #numbering("1.a.i", 1, 2) \ -/// #numbering("I – 1", 12, 2) \ -/// #numbering( -/// (..nums) => nums -/// .pos() -/// .map(str) -/// .join(".") + ")", -/// 1, 2, 3, -/// ) -/// ``` -#[func] -pub fn numbering( - /// The virtual machine. - vm: &mut Vm, - /// Defines how the numbering works. - /// - /// **Counting symbols** are `1`, `a`, `A`, `i`, `I`, `い`, `イ`, `א`, `가`, - /// `ㄱ`, and `*`. They are replaced by the number in the sequence, in the - /// given case. - /// - /// The `*` character means that symbols should be used to count, in the - /// order of `*`, `†`, `‡`, `§`, `¶`, and `‖`. If there are more than six - /// items, the number is represented using multiple symbols. - /// - /// **Suffixes** are all characters after the last counting symbol. They are - /// repeated as-is at the end of any rendered number. - /// - /// **Prefixes** are all characters that are neither counting symbols nor - /// suffixes. They are repeated as-is at in front of their rendered - /// equivalent of their counting symbol. - /// - /// This parameter can also be an arbitrary function that gets each number - /// as an individual argument. When given a function, the `numbering` - /// function just forwards the arguments to that function. While this is not - /// particularly useful in itself, it means that you can just give arbitrary - /// numberings to the `numbering` function without caring whether they are - /// defined as a pattern or function. - numbering: Numbering, - /// The numbers to apply the numbering to. Must be positive. - /// - /// If `numbering` is a pattern and more numbers than counting symbols are - /// given, the last counting symbol with its prefix is repeated. - #[variadic] - numbers: Vec<usize>, -) -> SourceResult<Value> { - numbering.apply_vm(vm, &numbers) -} - -/// How to number a sequence of things. -#[derive(Debug, Clone, PartialEq, Hash)] -pub enum Numbering { - /// A pattern with prefix, numbering, lower / upper case and suffix. - Pattern(NumberingPattern), - /// A closure mapping from an item's number to content. - Func(Func), -} - -impl Numbering { - /// Apply the pattern to the given numbers. - pub fn apply_vm(&self, vm: &mut Vm, numbers: &[usize]) -> SourceResult<Value> { - Ok(match self { - Self::Pattern(pattern) => Value::Str(pattern.apply(numbers).into()), - Self::Func(func) => { - let args = Args::new(func.span(), numbers.iter().copied()); - func.call_vm(vm, args)? - } - }) - } - - /// Apply the pattern to the given numbers. - pub fn apply_vt(&self, vt: &mut Vt, numbers: &[usize]) -> SourceResult<Value> { - Ok(match self { - Self::Pattern(pattern) => Value::Str(pattern.apply(numbers).into()), - Self::Func(func) => func.call_vt(vt, numbers.iter().copied())?, - }) - } - - /// Create a new `PdfNumbering` from a `Numbering` applied to a page - /// number. - pub fn apply_pdf(&self, number: usize) -> Option<PdfPageLabel> { - let Numbering::Pattern(pat) = self else { - return None; - }; - - let Some((prefix, kind, case)) = pat.pieces.first() else { - return None; - }; - - // If there is a suffix, we cannot use the common style optimisation, - // since PDF does not provide a suffix field. - let mut style = None; - if pat.suffix.is_empty() { - use NumberingKind as Kind; - use PdfPageLabelStyle as Style; - match (kind, case) { - (Kind::Arabic, _) => style = Some(Style::Arabic), - (Kind::Roman, Case::Lower) => style = Some(Style::LowerRoman), - (Kind::Roman, Case::Upper) => style = Some(Style::UpperRoman), - (Kind::Letter, Case::Lower) if number <= 26 => { - style = Some(Style::LowerAlpha) - } - (Kind::Letter, Case::Upper) if number <= 26 => { - style = Some(Style::UpperAlpha) - } - _ => {} - } - } - - // Prefix and offset depend on the style: If it is supported by the PDF - // spec, we use the given prefix and an offset. Otherwise, everything - // goes into prefix. - let prefix = if style.is_none() { - Some(pat.apply(&[number])) - } else { - (!prefix.is_empty()).then(|| prefix.clone()) - }; - - let offset = style.and(NonZeroUsize::new(number)); - Some(PdfPageLabel { prefix, style, offset }) - } - - /// Trim the prefix suffix if this is a pattern. - pub fn trimmed(mut self) -> Self { - if let Self::Pattern(pattern) = &mut self { - pattern.trimmed = true; - } - self - } -} - -impl From<NumberingPattern> for Numbering { - fn from(pattern: NumberingPattern) -> Self { - Self::Pattern(pattern) - } -} - -cast! { - Numbering, - self => match self { - Self::Pattern(pattern) => pattern.into_value(), - Self::Func(func) => func.into_value(), - }, - v: NumberingPattern => Self::Pattern(v), - v: Func => Self::Func(v), -} - -/// How to turn a number into text. -/// -/// A pattern consists of a prefix, followed by one of `1`, `a`, `A`, `i`, -/// `I`, `い`, `イ`, `א`, `가`, `ㄱ`, or `*`, and then a suffix. -/// -/// Examples of valid patterns: -/// - `1)` -/// - `a.` -/// - `(I)` -#[derive(Debug, Clone, Eq, PartialEq, Hash)] -pub struct NumberingPattern { - pub pieces: EcoVec<(EcoString, NumberingKind, Case)>, - pub suffix: EcoString, - trimmed: bool, -} - -impl NumberingPattern { - /// Apply the pattern to the given number. - pub fn apply(&self, numbers: &[usize]) -> EcoString { - let mut fmt = EcoString::new(); - let mut numbers = numbers.iter(); - - for (i, ((prefix, kind, case), &n)) in - self.pieces.iter().zip(&mut numbers).enumerate() - { - if i > 0 || !self.trimmed { - fmt.push_str(prefix); - } - fmt.push_str(&kind.apply(n, *case)); - } - - for ((prefix, kind, case), &n) in - self.pieces.last().into_iter().cycle().zip(numbers) - { - if prefix.is_empty() { - fmt.push_str(&self.suffix); - } else { - fmt.push_str(prefix); - } - fmt.push_str(&kind.apply(n, *case)); - } - - if !self.trimmed { - fmt.push_str(&self.suffix); - } - - fmt - } - - /// Apply only the k-th segment of the pattern to a number. - pub fn apply_kth(&self, k: usize, number: usize) -> EcoString { - let mut fmt = EcoString::new(); - if let Some((prefix, _, _)) = self.pieces.first() { - fmt.push_str(prefix); - } - if let Some((_, kind, case)) = self - .pieces - .iter() - .chain(self.pieces.last().into_iter().cycle()) - .nth(k) - { - fmt.push_str(&kind.apply(number, *case)); - } - fmt.push_str(&self.suffix); - fmt - } - - /// How many counting symbols this pattern has. - pub fn pieces(&self) -> usize { - self.pieces.len() - } -} - -impl FromStr for NumberingPattern { - type Err = &'static str; - - fn from_str(pattern: &str) -> Result<Self, Self::Err> { - let mut pieces = EcoVec::new(); - let mut handled = 0; - - for (i, c) in pattern.char_indices() { - let Some(kind) = NumberingKind::from_char(c.to_ascii_lowercase()) else { - continue; - }; - - let prefix = pattern[handled..i].into(); - let case = - if c.is_uppercase() || c == '壹' { Case::Upper } else { Case::Lower }; - pieces.push((prefix, kind, case)); - handled = c.len_utf8() + i; - } - - let suffix = pattern[handled..].into(); - if pieces.is_empty() { - return Err("invalid numbering pattern"); - } - - Ok(Self { pieces, suffix, trimmed: false }) - } -} - -cast! { - NumberingPattern, - self => { - let mut pat = EcoString::new(); - for (prefix, kind, case) in &self.pieces { - pat.push_str(prefix); - let mut c = kind.to_char(); - if *case == Case::Upper { - c = c.to_ascii_uppercase(); - } - pat.push(c); - } - pat.push_str(&self.suffix); - pat.into_value() - }, - v: Str => v.parse()?, -} - -/// Different kinds of numberings. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub enum NumberingKind { - Arabic, - Letter, - Roman, - Symbol, - Hebrew, - SimplifiedChinese, - // TODO: Pick the numbering pattern based on languages choice. - // As the `1st` numbering character of Chinese (Simplified) and - // Chinese (Traditional) is same, we are unable to determine - // if the context is Simplified or Traditional by only this - // character. - #[allow(unused)] - TraditionalChinese, - HiraganaIroha, - KatakanaIroha, - KoreanJamo, - KoreanSyllable, -} - -impl NumberingKind { - /// Create a numbering kind from a lowercase character. - pub fn from_char(c: char) -> Option<Self> { - Some(match c { - '1' => NumberingKind::Arabic, - 'a' => NumberingKind::Letter, - 'i' => NumberingKind::Roman, - '*' => NumberingKind::Symbol, - 'א' => NumberingKind::Hebrew, - '一' | '壹' => NumberingKind::SimplifiedChinese, - 'い' => NumberingKind::HiraganaIroha, - 'イ' => NumberingKind::KatakanaIroha, - 'ㄱ' => NumberingKind::KoreanJamo, - '가' => NumberingKind::KoreanSyllable, - _ => return None, - }) - } - - /// The lowercase character for this numbering kind. - pub fn to_char(self) -> char { - match self { - Self::Arabic => '1', - Self::Letter => 'a', - Self::Roman => 'i', - Self::Symbol => '*', - Self::Hebrew => 'א', - Self::SimplifiedChinese => '一', - Self::TraditionalChinese => '一', - Self::HiraganaIroha => 'い', - Self::KatakanaIroha => 'イ', - Self::KoreanJamo => 'ㄱ', - Self::KoreanSyllable => '가', - } - } - - /// Apply the numbering to the given number. - pub fn apply(self, mut n: usize, case: Case) -> EcoString { - match self { - Self::Arabic => { - eco_format!("{n}") - } - Self::Letter => zeroless::<26>( - |x| match case { - Case::Lower => char::from(b'a' + x as u8), - Case::Upper => char::from(b'A' + x as u8), - }, - n, - ), - Self::HiraganaIroha => zeroless::<47>( - |x| { - [ - 'い', 'ろ', 'は', 'に', 'ほ', 'へ', 'と', 'ち', 'り', 'ぬ', 'る', - 'を', 'わ', 'か', 'よ', 'た', 'れ', 'そ', 'つ', 'ね', 'な', 'ら', - 'む', 'う', 'ゐ', 'の', 'お', 'く', 'や', 'ま', 'け', 'ふ', 'こ', - 'え', 'て', 'あ', 'さ', 'き', 'ゆ', 'め', 'み', 'し', 'ゑ', 'ひ', - 'も', 'せ', 'す', - ][x] - }, - n, - ), - Self::KatakanaIroha => zeroless::<47>( - |x| { - [ - 'イ', 'ロ', 'ハ', 'ニ', 'ホ', 'ヘ', 'ト', 'チ', 'リ', 'ヌ', 'ル', - 'ヲ', 'ワ', 'カ', 'ヨ', 'タ', 'レ', 'ソ', 'ツ', 'ネ', 'ナ', 'ラ', - 'ム', 'ウ', 'ヰ', 'ノ', 'オ', 'ク', 'ヤ', 'マ', 'ケ', 'フ', 'コ', - 'エ', 'テ', 'ア', 'サ', 'キ', 'ユ', 'メ', 'ミ', 'シ', 'ヱ', 'ヒ', - 'モ', 'セ', 'ス', - ][x] - }, - n, - ), - Self::Roman => { - if n == 0 { - return 'N'.into(); - } - - // Adapted from Yann Villessuzanne's roman.rs under the - // Unlicense, at https://github.com/linfir/roman.rs/ - let mut fmt = EcoString::new(); - for &(name, value) in &[ - ("M̅", 1000000), - ("D̅", 500000), - ("C̅", 100000), - ("L̅", 50000), - ("X̅", 10000), - ("V̅", 5000), - ("I̅V̅", 4000), - ("M", 1000), - ("CM", 900), - ("D", 500), - ("CD", 400), - ("C", 100), - ("XC", 90), - ("L", 50), - ("XL", 40), - ("X", 10), - ("IX", 9), - ("V", 5), - ("IV", 4), - ("I", 1), - ] { - while n >= value { - n -= value; - for c in name.chars() { - match case { - Case::Lower => fmt.extend(c.to_lowercase()), - Case::Upper => fmt.push(c), - } - } - } - } - - fmt - } - Self::Symbol => { - if n == 0 { - return '-'.into(); - } - - const SYMBOLS: &[char] = &['*', '†', '‡', '§', '¶', '‖']; - let symbol = SYMBOLS[(n - 1) % SYMBOLS.len()]; - let amount = ((n - 1) / SYMBOLS.len()) + 1; - std::iter::repeat(symbol).take(amount).collect() - } - Self::Hebrew => { - if n == 0 { - return '-'.into(); - } - - let mut fmt = EcoString::new(); - 'outer: for &(name, value) in &[ - ('ת', 400), - ('ש', 300), - ('ר', 200), - ('ק', 100), - ('צ', 90), - ('פ', 80), - ('ע', 70), - ('ס', 60), - ('נ', 50), - ('מ', 40), - ('ל', 30), - ('כ', 20), - ('י', 10), - ('ט', 9), - ('ח', 8), - ('ז', 7), - ('ו', 6), - ('ה', 5), - ('ד', 4), - ('ג', 3), - ('ב', 2), - ('א', 1), - ] { - while n >= value { - match n { - 15 => fmt.push_str("ט״ו"), - 16 => fmt.push_str("ט״ז"), - _ => { - let append_geresh = n == value && fmt.is_empty(); - if n == value && !fmt.is_empty() { - fmt.push('״'); - } - fmt.push(name); - if append_geresh { - fmt.push('׳'); - } - - n -= value; - continue; - } - } - break 'outer; - } - } - fmt - } - l @ (Self::SimplifiedChinese | Self::TraditionalChinese) => { - let chinese_case = match case { - Case::Lower => ChineseCase::Lower, - Case::Upper => ChineseCase::Upper, - }; - - match (n as u8).to_chinese( - match l { - Self::SimplifiedChinese => ChineseVariant::Simple, - Self::TraditionalChinese => ChineseVariant::Traditional, - _ => unreachable!(), - }, - chinese_case, - ChineseCountMethod::TenThousand, - ) { - Ok(num_str) => EcoString::from(num_str), - Err(_) => '-'.into(), - } - } - Self::KoreanJamo => zeroless::<14>( - |x| { - [ - 'ㄱ', 'ㄴ', 'ㄷ', 'ㄹ', 'ㅁ', 'ㅂ', 'ㅅ', 'ㅇ', 'ㅈ', 'ㅊ', 'ㅋ', - 'ㅌ', 'ㅍ', 'ㅎ', - ][x] - }, - n, - ), - Self::KoreanSyllable => zeroless::<14>( - |x| { - [ - '가', '나', '다', '라', '마', '바', '사', '아', '자', '차', '카', - '타', '파', '하', - ][x] - }, - n, - ), - } - } -} - -/// Stringify a number using a base-N counting system with no zero digit. -/// -/// This is best explained by example. Suppose our digits are 'A', 'B', and 'C'. -/// we would get the following: -/// -/// ```text -/// 1 => "A" -/// 2 => "B" -/// 3 => "C" -/// 4 => "AA" -/// 5 => "AB" -/// 6 => "AC" -/// 7 => "BA" -/// 8 => "BB" -/// 9 => "BC" -/// 10 => "CA" -/// 11 => "CB" -/// 12 => "CC" -/// 13 => "AAA" -/// etc. -/// ``` -/// -/// You might be familiar with this scheme from the way spreadsheet software -/// tends to label its columns. -fn zeroless<const N_DIGITS: usize>( - mk_digit: impl Fn(usize) -> char, - mut n: usize, -) -> EcoString { - if n == 0 { - return '-'.into(); - } - let mut cs = vec![]; - while n > 0 { - n -= 1; - cs.push(mk_digit(n % N_DIGITS)); - n /= N_DIGITS; - } - cs.into_iter().rev().collect() -} diff --git a/crates/typst-library/src/meta/outline.rs b/crates/typst-library/src/meta/outline.rs deleted file mode 100644 index ebd6c4b0..00000000 --- a/crates/typst-library/src/meta/outline.rs +++ /dev/null @@ -1,525 +0,0 @@ -use std::str::FromStr; - -use typst::util::option_eq; - -use crate::layout::{BoxElem, HElem, HideElem, ParbreakElem, RepeatElem, Spacing}; -use crate::meta::{ - Counter, CounterKey, HeadingElem, LocalName, LocalNameIn, Numbering, - NumberingPattern, Refable, -}; -use crate::prelude::*; -use crate::text::{LinebreakElem, SpaceElem, TextElem}; - -/// A table of contents, figures, or other elements. -/// -/// This function generates a list of all occurrences of an element in the -/// document, up to a given depth. The element's numbering and page number will -/// be displayed in the outline alongside its title or caption. By default this -/// generates a table of contents. -/// -/// # Example -/// ```example -/// #outline() -/// -/// = Introduction -/// #lorem(5) -/// -/// = Prior work -/// #lorem(10) -/// ``` -/// -/// # Alternative outlines -/// By setting the `target` parameter, the outline can be used to generate a -/// list of other kinds of elements than headings. In the example below, we list -/// all figures containing images by setting `target` to `{figure.where(kind: -/// image)}`. We could have also set it to just `figure`, but then the list -/// would also include figures containing tables or other material. For more -/// details on the `where` selector, [see here]($function.where). -/// -/// ```example -/// #outline( -/// title: [List of Figures], -/// target: figure.where(kind: image), -/// ) -/// -/// #figure( -/// image("tiger.jpg"), -/// caption: [A nice figure!], -/// ) -/// ``` -/// -/// # Styling the outline -/// The outline element has several options for customization, such as its -/// `title` and `indent` parameters. If desired, however, it is possible to have -/// more control over the outline's look and style through the -/// [`outline.entry`]($outline.entry) element. -#[elem(scope, keywords = ["Table of Contents"], Show, Finalize, LocalName)] -pub struct OutlineElem { - /// The title of the outline. - /// - /// - When set to `{auto}`, an appropriate title for the - /// [text language]($text.lang) will be used. This is the default. - /// - When set to `{none}`, the outline will not have a title. - /// - A custom title can be set by passing content. - /// - /// The outline's heading will not be numbered by default, but you can - /// force it to be with a show-set rule: - /// `{show outline: set heading(numbering: "1.")}` - /// ``` - #[default(Some(Smart::Auto))] - pub title: Option<Smart<Content>>, - - /// The type of element to include in the outline. - /// - /// To list figures containing a specific kind of element, like a table, you - /// can write `{figure.where(kind: table)}`. - /// - /// ```example - /// #outline( - /// title: [List of Tables], - /// target: figure.where(kind: table), - /// ) - /// - /// #figure( - /// table( - /// columns: 4, - /// [t], [1], [2], [3], - /// [y], [0.3], [0.7], [0.5], - /// ), - /// caption: [Experiment results], - /// ) - /// ``` - #[default(LocatableSelector(select_where!(HeadingElem, Outlined => true)))] - #[borrowed] - pub target: LocatableSelector, - - /// The maximum level up to which elements are included in the outline. When - /// this argument is `{none}`, all elements are included. - /// - /// ```example - /// #set heading(numbering: "1.") - /// #outline(depth: 2) - /// - /// = Yes - /// Top-level section. - /// - /// == Still - /// Subsection. - /// - /// === Nope - /// Not included. - /// ``` - pub depth: Option<NonZeroUsize>, - - /// How to indent the outline's entries. - /// - /// - `{none}`: No indent - /// - `{auto}`: Indents the numbering of the nested entry with the title of - /// its parent entry. This only has an effect if the entries are numbered - /// (e.g., via [heading numbering]($heading.numbering)). - /// - [Relative length]($relative): Indents the item by this length - /// multiplied by its nesting level. Specifying `{2em}`, for instance, - /// would indent top-level headings (not nested) by `{0em}`, second level - /// headings by `{2em}` (nested once), third-level headings by `{4em}` - /// (nested twice) and so on. - /// - [Function]($function): You can completely customize this setting with - /// a function. That function receives the nesting level as a parameter - /// (starting at 0 for top-level headings/elements) and can return a - /// relative length or content making up the indent. For example, - /// `{n => n * 2em}` would be equivalent to just specifying `{2em}`, while - /// `{n => [→ ] * n}` would indent with one arrow per nesting level. - /// - /// *Migration hints:* Specifying `{true}` (equivalent to `{auto}`) or - /// `{false}` (equivalent to `{none}`) for this option is deprecated and - /// will be removed in a future release. - /// - /// ```example - /// #set heading(numbering: "1.a.") - /// - /// #outline( - /// title: [Contents (Automatic)], - /// indent: auto, - /// ) - /// - /// #outline( - /// title: [Contents (Length)], - /// indent: 2em, - /// ) - /// - /// #outline( - /// title: [Contents (Function)], - /// indent: n => [→ ] * n, - /// ) - /// - /// = About ACME Corp. - /// == History - /// === Origins - /// #lorem(10) - /// - /// == Products - /// #lorem(10) - /// ``` - #[default(None)] - #[borrowed] - pub indent: Option<Smart<OutlineIndent>>, - - /// Content to fill the space between the title and the page number. Can be - /// set to `none` to disable filling. - /// - /// ```example - /// #outline(fill: line(length: 100%)) - /// - /// = A New Beginning - /// ``` - #[default(Some(RepeatElem::new(TextElem::packed(".")).pack()))] - pub fill: Option<Content>, -} - -#[scope] -impl OutlineElem { - #[elem] - type OutlineEntry; -} - -impl Show for OutlineElem { - #[tracing::instrument(name = "OutlineElem::show", skip_all)] - fn show(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Content> { - let mut seq = vec![ParbreakElem::new().pack()]; - // Build the outline title. - if let Some(title) = self.title(styles) { - let title = title.unwrap_or_else(|| { - TextElem::packed(Self::local_name_in(styles)).spanned(self.span()) - }); - - seq.push(HeadingElem::new(title).with_level(NonZeroUsize::ONE).pack()); - } - - let indent = self.indent(styles); - let depth = self.depth(styles).unwrap_or(NonZeroUsize::new(usize::MAX).unwrap()); - - let mut ancestors: Vec<&Content> = vec![]; - let elems = vt.introspector.query(&self.target(styles).0); - - for elem in &elems { - let Some(entry) = OutlineEntry::from_outlinable( - vt, - self.span(), - elem.clone().into_inner(), - self.fill(styles), - )? - else { - continue; - }; - - let level = entry.level(); - if depth < *level { - continue; - } - - // Deals with the ancestors of the current element. - // This is only applicable for elements with a hierarchy/level. - while ancestors - .last() - .and_then(|ancestor| ancestor.with::<dyn Outlinable>()) - .map_or(false, |last| last.level() >= *level) - { - ancestors.pop(); - } - - OutlineIndent::apply(indent, vt, &ancestors, &mut seq, self.span())?; - - // Add the overridable outline entry, followed by a line break. - seq.push(entry.pack()); - seq.push(LinebreakElem::new().pack()); - - ancestors.push(elem); - } - - seq.push(ParbreakElem::new().pack()); - - Ok(Content::sequence(seq)) - } -} - -impl Finalize for OutlineElem { - fn finalize(&self, realized: Content, _: StyleChain) -> Content { - realized - .styled(HeadingElem::set_outlined(false)) - .styled(HeadingElem::set_numbering(None)) - } -} - -impl LocalName for OutlineElem { - fn local_name(lang: Lang, region: Option<Region>) -> &'static str { - match lang { - Lang::ALBANIAN => "Përmbajtja", - Lang::ARABIC => "المحتويات", - Lang::BOKMÅL => "Innhold", - Lang::CHINESE if option_eq(region, "TW") => "目錄", - Lang::CHINESE => "目录", - Lang::CZECH => "Obsah", - Lang::DANISH => "Indhold", - Lang::DUTCH => "Inhoudsopgave", - Lang::FILIPINO => "Talaan ng mga Nilalaman", - Lang::FINNISH => "Sisällys", - Lang::FRENCH => "Table des matières", - Lang::GERMAN => "Inhaltsverzeichnis", - Lang::GREEK => "Περιεχόμενα", - Lang::HUNGARIAN => "Tartalomjegyzék", - Lang::ITALIAN => "Indice", - Lang::NYNORSK => "Innhald", - Lang::POLISH => "Spis treści", - Lang::PORTUGUESE if option_eq(region, "PT") => "Índice", - Lang::PORTUGUESE => "Sumário", - Lang::ROMANIAN => "Cuprins", - Lang::RUSSIAN => "Содержание", - Lang::SLOVENIAN => "Kazalo", - Lang::SPANISH => "Índice", - Lang::SWEDISH => "Innehåll", - Lang::TURKISH => "İçindekiler", - Lang::UKRAINIAN => "Зміст", - Lang::VIETNAMESE => "Mục lục", - Lang::JAPANESE => "目次", - Lang::ENGLISH | _ => "Contents", - } - } -} - -/// Marks an element as being able to be outlined. This is used to implement the -/// `#outline()` element. -pub trait Outlinable: Refable { - /// Produce an outline item for this element. - fn outline(&self, vt: &mut Vt) -> SourceResult<Option<Content>>; - - /// Returns the nesting level of this element. - fn level(&self) -> NonZeroUsize { - NonZeroUsize::ONE - } -} - -#[derive(Debug, Clone, PartialEq, Hash)] -pub enum OutlineIndent { - Bool(bool), - Rel(Rel<Length>), - Func(Func), -} - -impl OutlineIndent { - fn apply( - indent: &Option<Smart<Self>>, - vt: &mut Vt, - ancestors: &Vec<&Content>, - seq: &mut Vec<Content>, - span: Span, - ) -> SourceResult<()> { - match indent { - // 'none' | 'false' => no indenting - None | Some(Smart::Custom(OutlineIndent::Bool(false))) => {} - - // 'auto' | 'true' => use numbering alignment for indenting - Some(Smart::Auto | Smart::Custom(OutlineIndent::Bool(true))) => { - // Add hidden ancestors numberings to realize the indent. - let mut hidden = Content::empty(); - for ancestor in ancestors { - let ancestor_outlinable = ancestor.with::<dyn Outlinable>().unwrap(); - - if let Some(numbering) = ancestor_outlinable.numbering() { - let numbers = ancestor_outlinable - .counter() - .at(vt, ancestor.location().unwrap())? - .display(vt, &numbering)?; - - hidden += numbers + SpaceElem::new().pack(); - }; - } - - if !ancestors.is_empty() { - seq.push(HideElem::new(hidden).pack()); - seq.push(SpaceElem::new().pack()); - } - } - - // Length => indent with some fixed spacing per level - Some(Smart::Custom(OutlineIndent::Rel(length))) => { - seq.push( - HElem::new(Spacing::Rel(*length)).pack().repeat(ancestors.len()), - ); - } - - // Function => call function with the current depth and take - // the returned content - Some(Smart::Custom(OutlineIndent::Func(func))) => { - let depth = ancestors.len(); - let LengthOrContent(content) = - func.call_vt(vt, [depth])?.cast().at(span)?; - if !content.is_empty() { - seq.push(content); - } - } - }; - - Ok(()) - } -} - -cast! { - OutlineIndent, - self => match self { - Self::Bool(v) => v.into_value(), - Self::Rel(v) => v.into_value(), - Self::Func(v) => v.into_value() - }, - v: bool => OutlineIndent::Bool(v), - v: Rel<Length> => OutlineIndent::Rel(v), - v: Func => OutlineIndent::Func(v), -} - -struct LengthOrContent(Content); - -cast! { - LengthOrContent, - v: Rel<Length> => Self(HElem::new(Spacing::Rel(v)).pack()), - v: Content => Self(v), -} - -/// Represents each entry line in an outline, including the reference to the -/// outlined element, its page number, and the filler content between both. -/// -/// This element is intended for use with show rules to control the appearance -/// of outlines. To customize an entry's line, you can build it from scratch by -/// accessing the `level`, `element`, `body`, `fill` and `page` fields on the -/// entry. -/// -/// ```example -/// #set heading(numbering: "1.") -/// -/// #show outline.entry.where( -/// level: 1 -/// ): it => { -/// v(12pt, weak: true) -/// strong(it) -/// } -/// -/// #outline(indent: auto) -/// -/// = Introduction -/// = Background -/// == History -/// == State of the Art -/// = Analysis -/// == Setup -/// ``` -#[elem(name = "entry", title = "Outline Entry", Show)] -pub struct OutlineEntry { - /// The nesting level of this outline entry. Starts at `{1}` for top-level - /// entries. - #[required] - pub level: NonZeroUsize, - - /// The element this entry refers to. Its location will be available - /// through the [`location`]($content.location) method on content - /// and can be [linked]($link) to. - #[required] - pub element: Content, - - /// The content which is displayed in place of the referred element at its - /// entry in the outline. For a heading, this would be its number followed - /// by the heading's title, for example. - #[required] - pub body: Content, - - /// The content used to fill the space between the element's outline and - /// its page number, as defined by the outline element this entry is - /// located in. When `{none}`, empty space is inserted in that gap instead. - /// - /// Note that, when using show rules to override outline entries, it is - /// recommended to wrap the filling content in a [`box`]($box) with - /// fractional width. For example, `{box(width: 1fr, repeat[-])}` would show - /// precisely as many `-` characters as necessary to fill a particular gap. - #[required] - pub fill: Option<Content>, - - /// The page number of the element this entry links to, formatted with the - /// numbering set for the referenced page. - #[required] - pub page: Content, -} - -impl OutlineEntry { - /// Generates an OutlineEntry from the given element, if possible (errors if - /// the element does not implement `Outlinable`). If the element should not - /// be outlined (e.g. heading with 'outlined: false'), does not generate an - /// entry instance (returns `Ok(None)`). - fn from_outlinable( - vt: &mut Vt, - span: Span, - elem: Content, - fill: Option<Content>, - ) -> SourceResult<Option<Self>> { - let Some(outlinable) = elem.with::<dyn Outlinable>() else { - bail!(span, "cannot outline {}", elem.func().name()); - }; - - let Some(body) = outlinable.outline(vt)? else { - return Ok(None); - }; - - let location = elem.location().unwrap(); - let page_numbering = vt - .introspector - .page_numbering(location) - .cast::<Option<Numbering>>() - .unwrap() - .unwrap_or_else(|| { - Numbering::Pattern(NumberingPattern::from_str("1").unwrap()) - }); - - let page = Counter::new(CounterKey::Page) - .at(vt, location)? - .display(vt, &page_numbering)?; - - Ok(Some(Self::new(outlinable.level(), elem, body, fill, page))) - } -} - -impl Show for OutlineEntry { - fn show(&self, _vt: &mut Vt, _: StyleChain) -> SourceResult<Content> { - let mut seq = vec![]; - let elem = self.element(); - - // In case a user constructs an outline entry with an arbitrary element. - let Some(location) = elem.location() else { - if elem.can::<dyn Locatable>() && elem.can::<dyn Outlinable>() { - bail!(error!(self.span(), "{} must have a location", elem.func().name()) - .with_hint( - "try using a query or a show rule to customize the outline.entry instead", - )) - } else { - bail!(self.span(), "cannot outline {}", elem.func().name()) - } - }; - - // The body text remains overridable. - seq.push(self.body().clone().linked(Destination::Location(location))); - - // Add filler symbols between the section name and page number. - if let Some(filler) = self.fill() { - seq.push(SpaceElem::new().pack()); - seq.push( - BoxElem::new() - .with_body(Some(filler.clone())) - .with_width(Fr::one().into()) - .pack(), - ); - seq.push(SpaceElem::new().pack()); - } else { - seq.push(HElem::new(Fr::one().into()).pack()); - } - - // Add the page number. - let page = self.page().clone().linked(Destination::Location(location)); - seq.push(page); - - Ok(Content::sequence(seq)) - } -} diff --git a/crates/typst-library/src/meta/query.rs b/crates/typst-library/src/meta/query.rs deleted file mode 100644 index d6c600d7..00000000 --- a/crates/typst-library/src/meta/query.rs +++ /dev/null @@ -1,157 +0,0 @@ -use crate::prelude::*; - -/// Finds elements in the document. -/// -/// The `query` functions lets you search your document for elements of a -/// particular type or with a particular label. To use it, you first need to -/// retrieve the current document location with the [`locate`]($locate) -/// function. -/// -/// # Finding elements -/// In the example below, we create a custom page header that displays the text -/// "Typst Academy" in small capitals and the current section title. On the -/// first page, the section title is omitted because the header is before the -/// first section heading. -/// -/// To realize this layout, we call `locate` and then query for all headings -/// after the current location. The function we pass to locate is called twice -/// in this case: Once per page. -/// -/// - On the first page the query for all headings before the current location -/// yields an empty array: There are no previous headings. We check for this -/// case and and just display "Typst Academy". -/// -/// - For the second page, we retrieve the last element from the query's result. -/// This is the latest heading before the current position and as such, it is -/// the heading of the section we are currently in. We access its content -/// through the `body` field and display it alongside "Typst Academy". -/// -/// ```example -/// >>> #set page( -/// >>> width: 240pt, -/// >>> height: 180pt, -/// >>> margin: (top: 35pt, rest: 15pt), -/// >>> header-ascent: 12pt, -/// >>> ) -/// #set page(header: locate(loc => { -/// let elems = query( -/// selector(heading).before(loc), -/// loc, -/// ) -/// let academy = smallcaps[ -/// Typst Academy -/// ] -/// if elems == () { -/// align(right, academy) -/// } else { -/// let body = elems.last().body -/// academy + h(1fr) + emph(body) -/// } -/// })) -/// -/// = Introduction -/// #lorem(23) -/// -/// = Background -/// #lorem(30) -/// -/// = Analysis -/// #lorem(15) -/// ``` -/// -/// # A word of caution { #caution } -/// To resolve all your queries, Typst evaluates and layouts parts of the -/// document multiple times. However, there is no guarantee that your queries -/// can actually be completely resolved. If you aren't careful a query can -/// affect itself—leading to a result that never stabilizes. -/// -/// In the example below, we query for all headings in the document. We then -/// generate as many headings. In the beginning, there's just one heading, -/// titled `Real`. Thus, `count` is `1` and one `Fake` heading is generated. -/// Typst sees that the query's result has changed and processes it again. This -/// time, `count` is `2` and two `Fake` headings are generated. This goes on and -/// on. As we can see, the output has five headings. This is because Typst -/// simply gives up after five attempts. -/// -/// In general, you should try not to write queries that affect themselves. The -/// same words of caution also apply to other introspection features like -/// [counters]($counter) and [state]($state). -/// -/// ```example -/// = Real -/// #locate(loc => { -/// let elems = query(heading, loc) -/// let count = elems.len() -/// count * [= Fake] -/// }) -/// ``` -/// -/// # Command line queries -/// You can also perform queries from the command line with the `typst query` -/// command. This command executes an arbitrary query on the document and -/// returns the resulting elements in serialized form. Consider the following -/// `example.typ` file which contains some invisible [metadata]($metadata): -/// -/// ```typ -/// #metadata("This is a note") <note> -/// ``` -/// -/// You can execute a query on it as follows using Typst's CLI: -/// ```sh -/// $ typst query example.typ "<note>" -/// [ -/// { -/// "func": "metadata", -/// "value": "This is a note", -/// "label": "<note>" -/// } -/// ] -/// ``` -/// -/// Frequently, you're interested in only one specific field of the resulting -/// elements. In the case of the `metadata` element, the `value` field is the -/// interesting one. You can extract just this field with the `--field` -/// argument. -/// -/// ```sh -/// $ typst query example.typ "<note>" --field value -/// ["This is a note"] -/// ``` -/// -/// If you are interested in just a single element, you can use the `--one` -/// flag to extract just it. -/// -/// ```sh -/// $ typst query example.typ "<note>" --field value --one -/// "This is a note" -/// ``` -#[func] -pub fn query( - /// The virtual machine. - vm: &mut Vm, - /// Can be an element function like a `heading` or `figure`, a `{<label>}` - /// or a more complex selector like `{heading.where(level: 1)}`. - /// - /// Currently, only a subset of element functions is supported. Aside from - /// headings and figures, this includes equations, references and all - /// elements with an explicit label. As a result, you _can_ query for e.g. - /// [`strong`]($strong) elements, but you will find only those that have an - /// explicit label attached to them. This limitation will be resolved in the - /// future. - target: LocatableSelector, - /// Can be an arbitrary location, as its value is irrelevant for the - /// function's return value. Why is it required then? As noted before, Typst - /// has to evaluate parts of your code multiple times to determine the - /// values of all state. By only allowing this function within - /// [`locate`]($locate) calls, the amount of code that can depend on the - /// query's result is reduced. If you could call it directly at the top - /// level of a module, the evaluation of the whole module and its exports - /// could depend on the query's result. - location: Location, -) -> Array { - let _ = location; - let vec = vm.vt.introspector.query(&target.0); - vec.into_iter() - .map(|elem| Value::Content(elem.into_inner())) - .collect() -} diff --git a/crates/typst-library/src/meta/reference.rs b/crates/typst-library/src/meta/reference.rs deleted file mode 100644 index 3a463c80..00000000 --- a/crates/typst-library/src/meta/reference.rs +++ /dev/null @@ -1,283 +0,0 @@ -use super::{BibliographyElem, CiteElem, Counter, Figurable, Numbering}; -use crate::math::EquationElem; -use crate::meta::FootnoteElem; -use crate::prelude::*; -use crate::text::TextElem; - -/// A reference to a label or bibliography. -/// -/// Produces a textual reference to a label. For example, a reference to a -/// heading will yield an appropriate string such as "Section 1" for a reference -/// to the first heading. The references are also links to the respective -/// element. Reference syntax can also be used to [cite]($cite) from a -/// bibliography. -/// -/// Referenceable elements include [headings]($heading), [figures]($figure), -/// [equations]($math.equation), and [footnotes]($footnote). To create a custom -/// referenceable element like a theorem, you can create a figure of a custom -/// [`kind`]($figure.kind) and write a show rule for it. In the future, there -/// might be a more direct way to define a custom referenceable element. -/// -/// If you just want to link to a labelled element and not get an automatic -/// textual reference, consider using the [`link`]($link) function instead. -/// -/// # Example -/// ```example -/// #set heading(numbering: "1.") -/// #set math.equation(numbering: "(1)") -/// -/// = Introduction <intro> -/// Recent developments in -/// typesetting software have -/// rekindled hope in previously -/// frustrated researchers. @distress -/// As shown in @results, we ... -/// -/// = Results <results> -/// We discuss our approach in -/// comparison with others. -/// -/// == Performance <perf> -/// @slow demonstrates what slow -/// software looks like. -/// $ O(n) = 2^n $ <slow> -/// -/// #bibliography("works.bib") -/// ``` -/// -/// # Syntax -/// This function also has dedicated syntax: A reference to a label can be -/// created by typing an `@` followed by the name of the label (e.g. -/// `[= Introduction <intro>]` can be referenced by typing `[@intro]`). -/// -/// To customize the supplement, add content in square brackets after the -/// reference: `[@intro[Chapter]]`. -/// -/// # Customization -/// If you write a show rule for references, you can access the referenced -/// element through the `element` field of the reference. The `element` may -/// be `{none}` even if it exists if Typst hasn't discovered it yet, so you -/// always need to handle that case in your code. -/// -/// ```example -/// #set heading(numbering: "1.") -/// #set math.equation(numbering: "(1)") -/// -/// #show ref: it => { -/// let eq = math.equation -/// let el = it.element -/// if el != none and el.func() == eq { -/// // Override equation references. -/// numbering( -/// el.numbering, -/// ..counter(eq).at(el.location()) -/// ) -/// } else { -/// // Other references as usual. -/// it -/// } -/// } -/// -/// = Beginnings <beginning> -/// In @beginning we prove @pythagoras. -/// $ a^2 + b^2 = c^2 $ <pythagoras> -/// ``` -#[elem(title = "Reference", Synthesize, Locatable, Show)] -pub struct RefElem { - /// The target label that should be referenced. - /// - /// Can be a label that is defined in the document or an entry from the - /// [`bibliography`]($bibliography). - #[required] - pub target: Label, - - /// A supplement for the reference. - /// - /// For references to headings or figures, this is added before the - /// referenced number. For citations, this can be used to add a page number. - /// - /// If a function is specified, it is passed the referenced element and - /// should return content. - /// - /// ```example - /// #set heading(numbering: "1.") - /// #set ref(supplement: it => { - /// if it.func() == heading { - /// "Chapter" - /// } else { - /// "Thing" - /// } - /// }) - /// - /// = Introduction <intro> - /// In @intro, we see how to turn - /// Sections into Chapters. And - /// in @intro[Part], it is done - /// manually. - /// ``` - #[borrowed] - pub supplement: Smart<Option<Supplement>>, - - /// A synthesized citation. - #[synthesized] - pub citation: Option<CiteElem>, - - /// The referenced element. - #[synthesized] - pub element: Option<Content>, -} - -impl Synthesize for RefElem { - fn synthesize(&mut self, vt: &mut Vt, styles: StyleChain) -> SourceResult<()> { - let citation = self.to_citation(vt, styles)?; - self.push_citation(Some(citation)); - self.push_element(None); - - let target = *self.target(); - if !BibliographyElem::has(vt, target) { - if let Ok(elem) = vt.introspector.query_label(target) { - self.push_element(Some(elem.into_inner())); - return Ok(()); - } - } - - Ok(()) - } -} - -impl Show for RefElem { - #[tracing::instrument(name = "RefElem::show", skip_all)] - fn show(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Content> { - Ok(vt.delayed(|vt| { - let target = *self.target(); - let elem = vt.introspector.query_label(target); - let span = self.span(); - - if BibliographyElem::has(vt, target) { - if elem.is_ok() { - bail!(span, "label occurs in the document and its bibliography"); - } - - return Ok(self.to_citation(vt, styles)?.spanned(span).pack()); - } - - let elem = elem.at(span)?; - - if elem.func() == FootnoteElem::elem() { - return Ok(FootnoteElem::with_label(target).spanned(span).pack()); - } - - let refable = elem - .with::<dyn Refable>() - .ok_or_else(|| { - if elem.can::<dyn Figurable>() { - eco_format!( - "cannot reference {} directly, try putting it into a figure", - elem.func().name() - ) - } else { - eco_format!("cannot reference {}", elem.func().name()) - } - }) - .at(span)?; - - let numbering = refable - .numbering() - .ok_or_else(|| { - eco_format!( - "cannot reference {} without numbering", - elem.func().name() - ) - }) - .hint(eco_format!( - "you can enable {} numbering with `#set {}(numbering: \"1.\")`", - elem.func().name(), - if elem.func() == EquationElem::elem() { - "math.equation" - } else { - elem.func().name() - } - )) - .at(span)?; - - let numbers = refable - .counter() - .at(vt, elem.location().unwrap())? - .display(vt, &numbering.trimmed())?; - - let supplement = match self.supplement(styles).as_ref() { - Smart::Auto => refable.supplement(), - Smart::Custom(None) => Content::empty(), - Smart::Custom(Some(supplement)) => { - supplement.resolve(vt, [(*elem).clone()])? - } - }; - - let mut content = numbers; - if !supplement.is_empty() { - content = supplement + TextElem::packed("\u{a0}") + content; - } - - Ok(content.linked(Destination::Location(elem.location().unwrap()))) - })) - } -} - -impl RefElem { - /// Turn the reference into a citation. - pub fn to_citation(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<CiteElem> { - let mut elem = CiteElem::new(*self.target()); - elem.set_location(self.location().unwrap()); - elem.synthesize(vt, styles)?; - elem.push_supplement(match self.supplement(styles).clone() { - Smart::Custom(Some(Supplement::Content(content))) => Some(content), - _ => None, - }); - - Ok(elem) - } -} - -/// Additional content for a reference. -#[derive(Debug, Clone, PartialEq, Hash)] -pub enum Supplement { - Content(Content), - Func(Func), -} - -impl Supplement { - /// Tries to resolve the supplement into its content. - pub fn resolve<T: IntoValue>( - &self, - vt: &mut Vt, - args: impl IntoIterator<Item = T>, - ) -> SourceResult<Content> { - Ok(match self { - Supplement::Content(content) => content.clone(), - Supplement::Func(func) => func.call_vt(vt, args)?.display(), - }) - } -} - -cast! { - Supplement, - self => match self { - Self::Content(v) => v.into_value(), - Self::Func(v) => v.into_value(), - }, - v: Content => Self::Content(v), - v: Func => Self::Func(v), -} - -/// Marks an element as being able to be referenced. This is used to implement -/// the `@ref` element. -pub trait Refable { - /// The supplement, if not overridden by the reference. - fn supplement(&self) -> Content; - - /// Returns the counter of this element. - fn counter(&self) -> Counter; - - /// Returns the numbering of this element. - fn numbering(&self) -> Option<Numbering>; -} diff --git a/crates/typst-library/src/meta/state.rs b/crates/typst-library/src/meta/state.rs deleted file mode 100644 index 4f04628f..00000000 --- a/crates/typst-library/src/meta/state.rs +++ /dev/null @@ -1,413 +0,0 @@ -use std::fmt::Debug; - -use ecow::{eco_vec, EcoVec}; -use typst::eval::{Repr, Tracer}; -use typst::model::DelayedErrors; - -use crate::prelude::*; - -/// Manages stateful parts of your document. -/// -/// Let's say you have some computations in your document and want to remember -/// the result of your last computation to use it in the next one. You might try -/// something similar to the code below and expect it to output 10, 13, 26, and -/// 21. However this **does not work** in Typst. If you test this code, you will -/// see that Typst complains with the following error message: _Variables from -/// outside the function are read-only and cannot be modified._ -/// -/// ```typ -/// #let x = 0 -/// #let compute(expr) = { -/// x = eval( -/// expr.replace("x", str(x)) -/// ) -/// [New value is #x. ] -/// } -/// -/// #compute("10") \ -/// #compute("x + 3") \ -/// #compute("x * 2") \ -/// #compute("x - 5") -/// ``` -/// -/// # State and document markup { #state-and-markup } -/// Why does it do that? Because, in general, this kind of computation with side -/// effects is problematic in document markup and Typst is upfront about that. -/// For the results to make sense, the computation must proceed in the same -/// order in which the results will be laid out in the document. In our simple -/// example, that's the case, but in general it might not be. -/// -/// Let's look at a slightly different, but similar kind of state: The heading -/// numbering. We want to increase the heading counter at each heading. Easy -/// enough, right? Just add one. Well, it's not that simple. Consider the -/// following example: -/// -/// ```example -/// #set heading(numbering: "1.") -/// #let template(body) = [ -/// = Outline -/// ... -/// #body -/// ] -/// -/// #show: template -/// -/// = Introduction -/// ... -/// ``` -/// -/// Here, Typst first processes the body of the document after the show rule, -/// sees the `Introduction` heading, then passes the resulting content to the -/// `template` function and only then sees the `Outline`. Just counting up would -/// number the `Introduction` with `1` and the `Outline` with `2`. -/// -/// # Managing state in Typst { #state-in-typst } -/// So what do we do instead? We use Typst's state management system. Calling -/// the `state` function with an identifying string key and an optional initial -/// value gives you a state value which exposes a few methods. The two most -/// important ones are `display` and `update`: -/// -/// - The `display` method shows the current value of the state. You can -/// optionally give it a function that receives the value and formats it in -/// some way. -/// -/// - The `update` method modifies the state. You can give it any value. If -/// given a non-function value, it sets the state to that value. If given a -/// function, that function receives the previous state and has to return the -/// new state. -/// -/// Our initial example would now look like this: -/// -/// ```example -/// #let s = state("x", 0) -/// #let compute(expr) = [ -/// #s.update(x => -/// eval(expr.replace("x", str(x))) -/// ) -/// New value is #s.display(). -/// ] -/// -/// #compute("10") \ -/// #compute("x + 3") \ -/// #compute("x * 2") \ -/// #compute("x - 5") -/// ``` -/// -/// State managed by Typst is always updated in layout order, not in evaluation -/// order. The `update` method returns content and its effect occurs at the -/// position where the returned content is inserted into the document. -/// -/// As a result, we can now also store some of the computations in -/// variables, but they still show the correct results: -/// -/// ```example -/// >>> #let s = state("x", 0) -/// >>> #let compute(expr) = [ -/// >>> #s.update(x => -/// >>> eval(expr.replace("x", str(x))) -/// >>> ) -/// >>> New value is #s.display(). -/// >>> ] -/// <<< ... -/// -/// #let more = [ -/// #compute("x * 2") \ -/// #compute("x - 5") -/// ] -/// -/// #compute("10") \ -/// #compute("x + 3") \ -/// #more -/// ``` -/// -/// This example is of course a bit silly, but in practice this is often exactly -/// what you want! A good example are heading counters, which is why Typst's -/// [counting system]($counter) is very similar to its state system. -/// -/// # Time Travel -/// By using Typst's state management system you also get time travel -/// capabilities! By combining the state system with [`locate`]($locate) and -/// [`query`]($query), we can find out what the value of the state will be at -/// any position in the document from anywhere else. In particular, the `at` -/// method gives us the value of the state at any location and the `final` -/// methods gives us the value of the state at the end of the document. -/// -/// ```example -/// >>> #let s = state("x", 0) -/// >>> #let compute(expr) = [ -/// >>> #s.update(x => { -/// >>> eval(expr.replace("x", str(x))) -/// >>> }) -/// >>> New value is #s.display(). -/// >>> ] -/// <<< ... -/// -/// Value at `<here>` is -/// #locate(loc => s.at( -/// query(<here>, loc) -/// .first() -/// .location() -/// )) -/// -/// #compute("10") \ -/// #compute("x + 3") \ -/// *Here.* <here> \ -/// #compute("x * 2") \ -/// #compute("x - 5") -/// ``` -/// -/// # A word of caution { #caution } -/// To resolve the values of all states, Typst evaluates parts of your code -/// multiple times. However, there is no guarantee that your state manipulation -/// can actually be completely resolved. -/// -/// For instance, if you generate state updates depending on the final value of -/// a state, the results might never converge. The example below illustrates -/// this. We initialize our state with `1` and then update it to its own final -/// value plus 1. So it should be `2`, but then its final value is `2`, so it -/// should be `3`, and so on. This example display `4` because Typst simply -/// gives up after a few attempts. -/// -/// ```example -/// #let s = state("x", 1) -/// #locate(loc => { -/// s.update(s.final(loc) + 1) -/// }) -/// #s.display() -/// ``` -/// -/// In general, you should _typically_ not generate state updates from within -/// `locate` calls or `display` calls of state or counters. Instead, pass a -/// function to `update` that determines the value of the state based on its -/// previous value. -#[ty(scope)] -#[derive(Debug, Clone, PartialEq, Hash)] -pub struct State { - /// The key that identifies the state. - key: Str, - /// The initial value of the state. - init: Value, -} - -impl State { - /// Create a new state identified by a key. - pub fn new(key: Str, init: Value) -> State { - Self { key, init } - } - - /// Produce the whole sequence of states. - /// - /// This has to happen just once for all states, cutting down the number - /// of state updates from quadratic to linear. - fn sequence(&self, vt: &mut Vt) -> SourceResult<EcoVec<Value>> { - self.sequence_impl( - vt.world, - vt.introspector, - vt.locator.track(), - TrackedMut::reborrow_mut(&mut vt.delayed), - TrackedMut::reborrow_mut(&mut vt.tracer), - ) - } - - /// Memoized implementation of `sequence`. - #[comemo::memoize] - fn sequence_impl( - &self, - world: Tracked<dyn World + '_>, - introspector: Tracked<Introspector>, - locator: Tracked<Locator>, - delayed: TrackedMut<DelayedErrors>, - tracer: TrackedMut<Tracer>, - ) -> SourceResult<EcoVec<Value>> { - let mut locator = Locator::chained(locator); - let mut vt = Vt { - world, - introspector, - locator: &mut locator, - delayed, - tracer, - }; - let mut state = self.init.clone(); - let mut stops = eco_vec![state.clone()]; - - for elem in introspector.query(&self.selector()) { - let elem = elem.to::<UpdateElem>().unwrap(); - match elem.update() { - StateUpdate::Set(value) => state = value.clone(), - StateUpdate::Func(func) => state = func.call_vt(&mut vt, [state])?, - } - stops.push(state.clone()); - } - - Ok(stops) - } - - /// The selector for this state's updates. - fn selector(&self) -> Selector { - select_where!(UpdateElem, Key => self.key.clone()) - } -} - -#[scope] -impl State { - /// Create a new state identified by a key. - #[func(constructor)] - pub fn construct( - /// The key that identifies this state. - key: Str, - /// The initial value of the state. - #[default] - init: Value, - ) -> State { - Self::new(key, init) - } - - /// Displays the current value of the state. - #[func] - pub fn display( - self, - /// A function which receives the value of the state and can return - /// arbitrary content which is then displayed. If this is omitted, the - /// value is directly displayed. - #[default] - func: Option<Func>, - ) -> Content { - DisplayElem::new(self, func).pack() - } - - /// Update the value of the state. - /// - /// The update will be in effect at the position where the returned content - /// is inserted into the document. If you don't put the output into the - /// document, nothing happens! This would be the case, for example, if you - /// write `{let _ = state("key").update(7)}`. State updates are always - /// applied in layout order and in that case, Typst wouldn't know when to - /// update the state. - #[func] - pub fn update( - self, - /// If given a non function-value, sets the state to that value. If - /// given a function, that function receives the previous state and has - /// to return the new state. - update: StateUpdate, - ) -> Content { - UpdateElem::new(self.key, update).pack() - } - - /// Get the value of the state at the given location. - #[func] - pub fn at( - &self, - /// The virtual typesetter. - vt: &mut Vt, - /// The location at which the state's value should be retrieved. A - /// suitable location can be retrieved from [`locate`]($locate) or - /// [`query`]($query). - location: Location, - ) -> SourceResult<Value> { - let sequence = self.sequence(vt)?; - let offset = vt - .introspector - .query(&self.selector().before(location.into(), true)) - .len(); - Ok(sequence[offset].clone()) - } - - /// Get the value of the state at the end of the document. - #[func] - pub fn final_( - &self, - /// The virtual typesetter. - vt: &mut Vt, - /// Can be an arbitrary location, as its value is irrelevant for the - /// method's return value. Why is it required then? As noted before, - /// Typst has to evaluate parts of your code multiple times to determine - /// the values of all state. By only allowing this method within - /// [`locate`]($locate) calls, the amount of code that can depend on the - /// method's result is reduced. If you could call `final` directly at - /// the top level of a module, the evaluation of the whole module and - /// its exports could depend on the state's value. - location: Location, - ) -> SourceResult<Value> { - let _ = location; - let sequence = self.sequence(vt)?; - Ok(sequence.last().unwrap().clone()) - } -} - -impl Repr for State { - fn repr(&self) -> EcoString { - eco_format!("state({}, {})", self.key.repr(), self.init.repr()) - } -} - -cast! { - type State, -} - -/// An update to perform on a state. -#[ty] -#[derive(Debug, Clone, PartialEq, Hash)] -pub enum StateUpdate { - /// Set the state to the specified value. - Set(Value), - /// Apply the given function to the state. - Func(Func), -} - -impl Repr for StateUpdate { - fn repr(&self) -> EcoString { - "..".into() - } -} - -cast! { - type StateUpdate, - v: Func => Self::Func(v), - v: Value => Self::Set(v), -} - -/// Executes a display of a state. -#[elem(Locatable, Show)] -struct DisplayElem { - /// The state. - #[required] - state: State, - - /// The function to display the state with. - #[required] - func: Option<Func>, -} - -impl Show for DisplayElem { - #[tracing::instrument(name = "DisplayElem::show", skip(self, vt))] - fn show(&self, vt: &mut Vt, _: StyleChain) -> SourceResult<Content> { - Ok(vt.delayed(|vt| { - let location = self.location().unwrap(); - let value = self.state().at(vt, location)?; - Ok(match self.func() { - Some(func) => func.call_vt(vt, [value])?.display(), - None => value.display(), - }) - })) - } -} - -/// Executes a display of a state. -#[elem(Locatable, Show)] -struct UpdateElem { - /// The key that identifies the state. - #[required] - key: Str, - - /// The update to perform on the state. - #[required] - update: StateUpdate, -} - -impl Show for UpdateElem { - #[tracing::instrument(name = "UpdateElem::show")] - fn show(&self, _: &mut Vt, _: StyleChain) -> SourceResult<Content> { - Ok(Content::empty()) - } -} diff --git a/crates/typst-library/src/prelude.rs b/crates/typst-library/src/prelude.rs deleted file mode 100644 index 00700540..00000000 --- a/crates/typst-library/src/prelude.rs +++ /dev/null @@ -1,42 +0,0 @@ -//! Helpful imports for creating library functionality. - -#[doc(no_inline)] -pub use std::fmt::{self, Debug, Formatter}; -#[doc(no_inline)] -pub use std::num::NonZeroUsize; - -#[doc(no_inline)] -pub use comemo::{Track, Tracked, TrackedMut}; -#[doc(no_inline)] -pub use ecow::{eco_format, EcoString}; -#[doc(no_inline)] -pub use smallvec::{smallvec, SmallVec}; -#[doc(no_inline)] -pub use typst::diag::{bail, error, At, Hint, SourceResult, StrResult}; -#[doc(no_inline)] -pub use typst::doc::*; -#[doc(no_inline)] -pub use typst::eval::{ - array, cast, dict, format_str, func, scope, ty, Args, Array, Bytes, Cast, Dict, - FromValue, Func, IntoValue, Repr, Scope, Smart, Str, Symbol, Type, Value, Vm, -}; -#[doc(no_inline)] -pub use typst::geom::*; -#[doc(no_inline)] -pub use typst::model::{ - elem, select_where, Behave, Behaviour, Construct, Content, Element, ElementFields, - Finalize, Fold, Introspector, Label, LocalName, Locatable, LocatableSelector, - Location, Locator, MetaElem, NativeElement, PlainText, Resolve, Selector, Set, Show, - StyleChain, StyleVec, Styles, Synthesize, Unlabellable, Vt, -}; -#[doc(no_inline)] -pub use typst::syntax::{FileId, Span, Spanned}; -#[doc(no_inline)] -pub use typst::util::NonZeroExt; -#[doc(no_inline)] -pub use typst::World; - -#[doc(no_inline)] -pub use crate::layout::{Fragment, Layout, Regions}; -#[doc(no_inline)] -pub use crate::shared::{ContentExt, StylesExt}; diff --git a/crates/typst-library/src/shared/behave.rs b/crates/typst-library/src/shared/behave.rs deleted file mode 100644 index e152fbb5..00000000 --- a/crates/typst-library/src/shared/behave.rs +++ /dev/null @@ -1,114 +0,0 @@ -//! Element interaction. - -use std::borrow::Cow; - -use typst::model::{Behave, Behaviour, Content, StyleChain, StyleVec, StyleVecBuilder}; - -/// A wrapper around a [`StyleVecBuilder`] that allows elements to interact. -#[derive(Debug)] -pub struct BehavedBuilder<'a> { - /// The internal builder. - builder: StyleVecBuilder<'a, Cow<'a, Content>>, - /// Staged weak and ignorant elements that we can't yet commit to the - /// builder. The option is `Some(_)` for weak elements and `None` for - /// ignorant elements. - staged: Vec<(Cow<'a, Content>, Behaviour, StyleChain<'a>)>, - /// What the last non-ignorant item was. - last: Behaviour, -} - -impl<'a> BehavedBuilder<'a> { - /// Create a new style-vec builder. - pub fn new() -> Self { - Self { - builder: StyleVecBuilder::new(), - staged: vec![], - last: Behaviour::Destructive, - } - } - - /// Whether the builder is totally empty. - pub fn is_empty(&self) -> bool { - self.builder.is_empty() && self.staged.is_empty() - } - - /// Whether the builder is empty except for some weak elements that will - /// probably collapse. - pub fn has_strong_elements(&self, last: bool) -> bool { - !self.builder.is_empty() - || self.staged.iter().any(|(_, behaviour, _)| { - !matches!(behaviour, Behaviour::Weak(_) | Behaviour::Invisible) - || (last && *behaviour == Behaviour::Invisible) - }) - } - - /// Push an item into the sequence. - pub fn push(&mut self, elem: Cow<'a, Content>, styles: StyleChain<'a>) { - let interaction = elem - .with::<dyn Behave>() - .map_or(Behaviour::Supportive, Behave::behaviour); - - match interaction { - Behaviour::Weak(level) => { - if matches!(self.last, Behaviour::Weak(_)) { - let item = elem.with::<dyn Behave>().unwrap(); - let i = self.staged.iter().position(|prev| { - let Behaviour::Weak(prev_level) = prev.1 else { return false }; - level < prev_level - || (level == prev_level && item.larger(prev, styles)) - }); - let Some(i) = i else { return }; - self.staged.remove(i); - } - - if self.last != Behaviour::Destructive { - self.staged.push((elem, interaction, styles)); - self.last = interaction; - } - } - Behaviour::Supportive => { - self.flush(true); - self.builder.push(elem, styles); - self.last = interaction; - } - Behaviour::Destructive => { - self.flush(false); - self.builder.push(elem, styles); - self.last = interaction; - } - Behaviour::Ignorant | Behaviour::Invisible => { - self.staged.push((elem, interaction, styles)); - } - } - } - - /// Iterate over the contained elements. - pub fn elems(&self) -> impl DoubleEndedIterator<Item = &Cow<'a, Content>> { - self.builder.elems().chain(self.staged.iter().map(|(item, ..)| item)) - } - - /// Return the finish style vec and the common prefix chain. - pub fn finish(mut self) -> (StyleVec<Cow<'a, Content>>, StyleChain<'a>) { - self.flush(false); - self.builder.finish() - } - - /// Push the staged elements, filtering out weak elements if `supportive` is - /// false. - fn flush(&mut self, supportive: bool) { - for (item, interaction, styles) in self.staged.drain(..) { - if supportive - || interaction == Behaviour::Ignorant - || interaction == Behaviour::Invisible - { - self.builder.push(item, styles); - } - } - } -} - -impl<'a> Default for BehavedBuilder<'a> { - fn default() -> Self { - Self::new() - } -} diff --git a/crates/typst-library/src/shared/ext.rs b/crates/typst-library/src/shared/ext.rs deleted file mode 100644 index 60614820..00000000 --- a/crates/typst-library/src/shared/ext.rs +++ /dev/null @@ -1,92 +0,0 @@ -//! Extension traits. - -use crate::layout::{AlignElem, MoveElem, PadElem}; -use crate::prelude::*; -use crate::text::{EmphElem, FontFamily, FontList, StrongElem, TextElem, UnderlineElem}; - -/// Additional methods on content. -pub trait ContentExt { - /// Make this content strong. - fn strong(self) -> Self; - - /// Make this content emphasized. - fn emph(self) -> Self; - - /// Underline this content. - fn underlined(self) -> Self; - - /// Link the content somewhere. - fn linked(self, dest: Destination) -> Self; - - /// Make the content linkable by `.linked(Destination::Location(loc))`. - /// - /// Should be used in combination with [`Location::variant`]. - fn backlinked(self, loc: Location) -> Self; - - /// Set alignments for this content. - fn aligned(self, align: Align) -> Self; - - /// Pad this content at the sides. - fn padded(self, padding: Sides<Rel<Length>>) -> Self; - - /// Transform this content's contents without affecting layout. - fn moved(self, delta: Axes<Rel<Length>>) -> Self; -} - -impl ContentExt for Content { - fn strong(self) -> Self { - StrongElem::new(self).pack() - } - - fn emph(self) -> Self { - EmphElem::new(self).pack() - } - - fn underlined(self) -> Self { - UnderlineElem::new(self).pack() - } - - fn linked(self, dest: Destination) -> Self { - self.styled(MetaElem::set_data(smallvec![Meta::Link(dest)])) - } - - fn backlinked(self, loc: Location) -> Self { - let mut backlink = Content::empty(); - backlink.set_location(loc); - self.styled(MetaElem::set_data(smallvec![Meta::Elem(backlink)])) - } - - fn aligned(self, align: Align) -> Self { - self.styled(AlignElem::set_alignment(align)) - } - - fn padded(self, padding: Sides<Rel<Length>>) -> Self { - PadElem::new(self) - .with_left(padding.left) - .with_top(padding.top) - .with_right(padding.right) - .with_bottom(padding.bottom) - .pack() - } - - fn moved(self, delta: Axes<Rel<Length>>) -> Self { - MoveElem::new(self).with_dx(delta.x).with_dy(delta.y).pack() - } -} - -/// Additional methods for style lists. -pub trait StylesExt { - /// Set a font family composed of a preferred family and existing families - /// from a style chain. - fn set_family(&mut self, preferred: FontFamily, existing: StyleChain); -} - -impl StylesExt for Styles { - fn set_family(&mut self, preferred: FontFamily, existing: StyleChain) { - self.set(TextElem::set_font(FontList( - std::iter::once(preferred) - .chain(TextElem::font_in(existing).into_iter().cloned()) - .collect(), - ))); - } -} diff --git a/crates/typst-library/src/shared/mod.rs b/crates/typst-library/src/shared/mod.rs deleted file mode 100644 index f54241cf..00000000 --- a/crates/typst-library/src/shared/mod.rs +++ /dev/null @@ -1,7 +0,0 @@ -//! Shared definitions for the standard library. - -mod behave; -mod ext; - -pub use behave::*; -pub use ext::*; diff --git a/crates/typst-library/src/symbols/emoji.rs b/crates/typst-library/src/symbols/emoji.rs deleted file mode 100644 index b35dfcaa..00000000 --- a/crates/typst-library/src/symbols/emoji.rs +++ /dev/null @@ -1,1356 +0,0 @@ -use typst::eval::{symbols, Module, Scope, Symbol}; - -/// A module with all emoji. -pub fn emoji() -> Module { - let mut scope = Scope::new(); - for (name, symbol) in EMOJI { - scope.define(*name, symbol.clone()); - } - Module::new("emoji", scope) -} - -/// A list of named emoji. -const EMOJI: &[(&str, Symbol)] = symbols! { - abacus: '🧮', - abc: '🔤', - abcd: '🔡', - ABCD: '🔠', - accordion: '🪗', - aesculapius: '⚕', - airplane: [ - '✈', - landing: '🛬', - small: '🛩', - takeoff: '🛫', - ], - alembic: '⚗', - alien: ['👽', monster: '👾'], - ambulance: '🚑', - amphora: '🏺', - anchor: '⚓', - anger: '💢', - ant: '🐜', - apple: [green: '🍏', red: '🍎'], - arm: [mech: '🦾', muscle: '💪', selfie: '🤳'], - arrow: [ - r.filled: '➡', - r.hook: '↪', - r.soon: '🔜', - l.filled: '⬅', - l.hook: '↩', - l.back: '🔙', - l.end: '🔚', - t.filled: '⬆', - t.curve: '⤴', - t.top: '🔝', - b.filled: '⬇', - b.curve: '⤵', - l.r: '↔', - l.r.on: '🔛', - t.b: '↕', - bl: '↙', - br: '↘', - tl: '↖', - tr: '↗', - ], - arrows: [cycle: '🔄'], - ast: ['*', box: '✳'], - atm: '🏧', - atom: '⚛', - aubergine: '🍆', - avocado: '🥑', - axe: '🪓', - baby: ['👶', angel: '👼', box: '🚼'], - babybottle: '🍼', - backpack: '🎒', - bacon: '🥓', - badger: '🦡', - badminton: '🏸', - bagel: '🥯', - baggageclaim: '🛄', - baguette: '🥖', - balloon: '🎈', - ballot: [check: '☑'], - ballotbox: '🗳', - banana: '🍌', - banjo: '🪕', - bank: '🏦', - barberpole: '💈', - baseball: '⚾', - basecap: '🧢', - basket: '🧺', - basketball: ['⛹', ball: '🏀'], - bat: '🦇', - bathtub: ['🛀', foam: '🛁'], - battery: ['🔋', low: '🪫'], - beach: [palm: '🏝', umbrella: '🏖'], - beads: '📿', - beans: '🫘', - bear: '🐻', - beaver: '🦫', - bed: ['🛏', person: '🛌'], - bee: '🐝', - beer: ['🍺', clink: '🍻'], - beetle: ['🪲', lady: '🐞'], - bell: ['🔔', ding: '🛎', not: '🔕'], - bento: '🍱', - bicyclist: ['🚴', mountain: '🚵'], - bike: ['🚲', not: '🚳'], - bikini: '👙', - billiards: '🎱', - bin: '🗑', - biohazard: '☣', - bird: '🐦', - bison: '🦬', - blood: '🩸', - blouse: '👚', - blowfish: '🐡', - blueberries: '🫐', - boar: '🐗', - boat: [ - sail: '⛵', - row: '🚣', - motor: '🛥', - speed: '🚤', - canoe: '🛶', - ], - bolt: '🔩', - bomb: '💣', - bone: '🦴', - book: [ - red: '📕', - blue: '📘', - green: '📗', - orange: '📙', - spiral: '📒', - open: '📖', - ], - bookmark: '🔖', - books: '📚', - boomerang: '🪃', - bordercontrol: '🛂', - bouquet: '💐', - bow: '🏹', - bowl: [spoon: '🥣', steam: '🍜'], - bowling: '🎳', - boxing: '🥊', - boy: '👦', - brain: '🧠', - bread: '🍞', - brick: '🧱', - bride: '👰', - bridge: [fog: '🌁', night: '🌉'], - briefcase: '💼', - briefs: '🩲', - brightness: [high: '🔆', low: '🔅'], - broccoli: '🥦', - broom: '🧹', - brush: '🖌', - bubble: [ - speech.r: '💬', - speech.l: '🗨', - thought: '💭', - anger.r: '🗯', - ], - bubbles: '🫧', - bubbletea: '🧋', - bucket: '🪣', - buffalo: [water: '🐃'], - bug: '🐛', - builder: '👷', - burger: '🍔', - burrito: '🌯', - bus: [ - '🚌', - front: '🚍', - small: '🚐', - stop: '🚏', - trolley: '🚎', - ], - butter: '🧈', - butterfly: '🦋', - button: ['🔲', alt: '🔳', radio: '🔘'], - cabinet: [file: '🗄'], - cablecar: ['🚠', small: '🚡'], - cactus: '🌵', - cake: [ - '🎂', - fish: '🍥', - moon: '🥮', - slice: '🍰', - ], - calendar: ['📅', spiral: '🗓', tearoff: '📆'], - camel: ['🐫', dromedar: '🐪'], - camera: [ - '📷', - flash: '📸', - movie: '🎥', - movie.box: '🎦', - video: '📹', - ], - camping: '🏕', - can: '🥫', - candle: '🕯', - candy: '🍬', - cane: '🦯', - car: [ - '🚗', - front: '🚘', - pickup: '🛻', - police: '🚓', - police.front: '🚔', - racing: '🏎', - rickshaw: '🛺', - suv: '🚙', - ], - card: [credit: '💳', id: '🪪'], - cardindex: '📇', - carrot: '🥕', - cart: '🛒', - cassette: '📼', - castle: [eu: '🏰', jp: '🏯'], - cat: [ - '🐈', - face: '🐱', - face.angry: '😾', - face.cry: '😿', - face.heart: '😻', - face.joy: '😹', - face.kiss: '😽', - face.laugh: '😸', - face.shock: '🙀', - face.smile: '😺', - face.smirk: '😼', - ], - chain: '🔗', - chains: '⛓', - chair: '🪑', - champagne: '🍾', - chart: [ - bar: '📊', - up: '📈', - down: '📉', - yen.up: '💹', - ], - checkmark: [heavy: '✔', box: '✅'], - cheese: '🧀', - cherries: '🍒', - chess: '♟', - chestnut: '🌰', - chicken: [ - '🐔', - baby: '🐥', - baby.egg: '🐣', - baby.head: '🐤', - leg: '🍗', - male: '🐓', - ], - child: '🧒', - chipmunk: '🐿', - chocolate: '🍫', - chopsticks: '🥢', - church: ['⛪', love: '💒'], - cigarette: ['🚬', not: '🚭'], - circle: [ - black: '⚫', - blue: '🔵', - brown: '🟤', - green: '🟢', - orange: '🟠', - purple: '🟣', - white: '⚪', - red: '🔴', - yellow: '🟡', - stroked: '⭕', - ], - circus: '🎪', - city: [ - '🏙', - dusk: '🌆', - night: '🌃', - sunset: '🌇', - ], - clamp: '🗜', - clapperboard: '🎬', - climbing: '🧗', - clip: '📎', - clipboard: '📋', - clips: '🖇', - clock: [ - one: '🕐', - one.thirty: '🕜', - two: '🕑', - two.thirty: '🕝', - three: '🕒', - three.thirty: '🕞', - four: '🕓', - four.thirty: '🕟', - five: '🕔', - five.thirty: '🕠', - six: '🕕', - six.thirty: '🕡', - seven: '🕖', - seven.thirty: '🕢', - eight: '🕗', - eight.thirty: '🕣', - nine: '🕘', - nine.thirty: '🕤', - ten: '🕙', - ten.thirty: '🕥', - eleven: '🕚', - eleven.thirty: '🕦', - twelve: '🕛', - twelve.thirty: '🕧', - alarm: '⏰', - old: '🕰', - timer: '⏲', - ], - cloud: [ - '☁', - dust: '💨', - rain: '🌧', - snow: '🌨', - storm: '⛈', - sun: '⛅', - sun.hidden: '🌥', - sun.rain: '🌦', - thunder: '🌩', - ], - coat: ['🧥', lab: '🥼'], - cockroach: '🪳', - cocktail: [martini: '🍸', tropical: '🍹'], - coconut: '🥥', - coffee: '☕', - coffin: '⚰', - coin: '🪙', - comet: '☄', - compass: '🧭', - computer: '🖥', - computermouse: '🖱', - confetti: '🎊', - construction: '🚧', - controller: '🎮', - cookie: ['🍪', fortune: '🥠'], - cooking: '🍳', - cool: '🆒', - copyright: '©', - coral: '🪸', - corn: '🌽', - couch: '🛋', - couple: '💑', - cow: ['🐄', face: '🐮'], - crab: '🦀', - crane: '🏗', - crayon: '🖍', - cricket: '🦗', - cricketbat: '🏏', - crocodile: '🐊', - croissant: '🥐', - crossmark: ['❌', box: '❎'], - crown: '👑', - crutch: '🩼', - crystal: '🔮', - cucumber: '🥒', - cup: [straw: '🥤'], - cupcake: '🧁', - curling: '🥌', - curry: '🍛', - custard: '🍮', - customs: '🛃', - cutlery: '🍴', - cyclone: '🌀', - dancing: [man: '🕺', woman: '💃', women.bunny: '👯'], - darts: '🎯', - dash: [wave.double: '〰'], - deer: '🦌', - desert: '🏜', - detective: '🕵', - diamond: [ - blue: '🔷', - blue.small: '🔹', - orange: '🔶', - orange.small: '🔸', - dot: '💠', - ], - die: '🎲', - dino: [pod: '🦕', rex: '🦖'], - disc: [cd: '💿', dvd: '📀', mini: '💽'], - discoball: '🪩', - diving: '🤿', - dodo: '🦤', - dog: [ - '🐕', - face: '🐶', - guide: '🦮', - poodle: '🐩', - ], - dollar: '💲', - dolphin: '🐬', - donut: '🍩', - door: '🚪', - dove: [peace: '🕊'], - dragon: ['🐉', face: '🐲'], - dress: ['👗', kimono: '👘', sari: '🥻'], - drop: '💧', - drops: '💦', - drum: ['🥁', big: '🪘'], - duck: '🦆', - dumpling: '🥟', - eagle: '🦅', - ear: ['👂', aid: '🦻'], - egg: '🥚', - eighteen: [not: '🔞'], - elephant: '🐘', - elevator: '🛗', - elf: '🧝', - email: '📧', - excl: [ - '❗', - white: '❕', - double: '‼', - quest: '⁉', - ], - explosion: '💥', - extinguisher: '🧯', - eye: '👁', - eyes: '👀', - face: [ - grin: '😀', - angry: '😠', - angry.red: '😡', - anguish: '😧', - astonish: '😲', - bandage: '🤕', - beam: '😁', - blank: '😶', - clown: '🤡', - cold: '🥶', - concern: '😦', - cool: '😎', - cover: '🤭', - cowboy: '🤠', - cry: '😭', - devil.smile: '😈', - devil.frown: '👿', - diagonal: '🫤', - disguise: '🥸', - dizzy: '😵', - dotted: '🫥', - down: '😞', - down.sweat: '😓', - drool: '🤤', - explode: '🤯', - eyeroll: '🙄', - friendly: '☺', - fear: '😨', - fear.sweat: '😰', - fever: '🤒', - flush: '😳', - frown: '☹', - frown.slight: '🙁', - frust: '😣', - goofy: '🤪', - halo: '😇', - happy: '😊', - heart: '😍', - hearts: '🥰', - heat: '🥵', - hug: '🤗', - inv: '🙃', - joy: '😂', - kiss: '😗', - kiss.smile: '😙', - kiss.heart: '😘', - kiss.blush: '😚', - lick: '😋', - lie: '🤥', - mask: '😷', - meh: '😒', - melt: '🫠', - money: '🤑', - monocle: '🧐', - nausea: '🤢', - nerd: '🤓', - neutral: '😐', - open: '😃', - party: '🥳', - peek: '🫣', - plead: '🥺', - relief: '😌', - rofl: '🤣', - sad: '😔', - salute: '🫡', - shock: '😱', - shush: '🤫', - skeptic: '🤨', - sleep: '😴', - sleepy: '😪', - smile: '😄', - smile.slight: '🙂', - smile.sweat: '😅', - smile.tear: '🥲', - smirk: '😏', - sneeze: '🤧', - speak.not: '🫢', - squint: '😆', - stars: '🤩', - straight: '😑', - suffer: '😖', - surprise: '😯', - symbols: '🤬', - tear: '😢', - tear.relief: '😥', - tear.withheld: '🥹', - teeth: '😬', - think: '🤔', - tired: '😫', - tongue: '😛', - tongue.squint: '😝', - tongue.wink: '😜', - triumph: '😤', - unhappy: '😕', - vomit: '🤮', - weary: '😩', - wink: '😉', - woozy: '🥴', - worry: '😟', - wow: '😮', - yawn: '🥱', - zip: '🤐', - ], - factory: '🏭', - fairy: '🧚', - faith: [ - christ: '✝', - dharma: '☸', - islam: '☪', - judaism: '✡', - menorah: '🕎', - om: '🕉', - orthodox: '☦', - peace: '☮', - star.dot: '🔯', - worship: '🛐', - yinyang: '☯', - ], - falafel: '🧆', - family: '👪', - fax: '📠', - feather: '🪶', - feeding: [breast: '🤱'], - fencing: '🤺', - ferriswheel: '🎡', - filebox: '🗃', - filedividers: '🗂', - film: '🎞', - finger: [ - r: '👉', - l: '👈', - t: '👆', - t.alt: '☝', - b: '👇', - front: '🫵', - m: '🖕', - ], - fingers: [cross: '🤞', pinch: '🤌', snap: '🫰'], - fire: '🔥', - firecracker: '🧨', - fireengine: '🚒', - fireworks: '🎆', - fish: ['🐟', tropical: '🐠'], - fishing: '🎣', - fist: [ - front: '👊', - r: '🤜', - l: '🤛', - raised: '✊', - ], - flag: [ - black: '🏴', - white: '🏳', - goal: '🏁', - golf: '⛳', - red: '🚩', - ], - flags: [jp.crossed: '🎌'], - flamingo: '🦩', - flashlight: '🔦', - flatbread: '🫓', - fleur: '⚜', - floppy: '💾', - flower: [ - hibiscus: '🌺', - lotus: '🪷', - pink: '🌸', - rose: '🌹', - sun: '🌻', - tulip: '🌷', - white: '💮', - wilted: '🥀', - yellow: '🌼', - ], - fly: '🪰', - fog: '🌫', - folder: ['📁', open: '📂'], - fondue: '🫕', - foot: '🦶', - football: ['⚽', am: '🏈'], - forex: '💱', - fountain: '⛲', - fox: '🦊', - free: '🆓', - fries: '🍟', - frisbee: '🥏', - frog: [face: '🐸'], - fuelpump: '⛽', - garlic: '🧄', - gear: '⚙', - gem: '💎', - genie: '🧞', - ghost: '👻', - giraffe: '🦒', - girl: '👧', - glass: [ - clink: '🥂', - milk: '🥛', - pour: '🫗', - tumbler: '🥃', - ], - glasses: ['👓', sun: '🕶'], - globe: [ - am: '🌎', - as.au: '🌏', - eu.af: '🌍', - meridian: '🌐', - ], - gloves: '🧤', - goal: '🥅', - goat: '🐐', - goggles: '🥽', - golfing: '🏌', - gorilla: '🦍', - grapes: '🍇', - guard: [man: '💂'], - guitar: '🎸', - gymnastics: '🤸', - haircut: '💇', - hammer: ['🔨', pick: '⚒', wrench: '🛠'], - hamsa: '🪬', - hamster: [face: '🐹'], - hand: [ - raised: '✋', - raised.alt: '🤚', - r: '🫱', - l: '🫲', - t: '🫴', - b: '🫳', - ok: '👌', - call: '🤙', - love: '🤟', - part: '🖖', - peace: '✌', - pinch: '🤏', - rock: '🤘', - splay: '🖐', - wave: '👋', - write: '✍', - ], - handbag: '👜', - handball: '🤾', - handholding: [man.man: '👬', woman.man: '👫', woman.woman: '👭'], - hands: [ - folded: '🙏', - palms: '🤲', - clap: '👏', - heart: '🫶', - open: '👐', - raised: '🙌', - shake: '🤝', - ], - hash: '#', - hat: [ribbon: '👒', top: '🎩'], - headphone: '🎧', - heart: [ - '❤', - arrow: '💘', - beat: '💓', - black: '🖤', - blue: '💙', - box: '💟', - broken: '💔', - brown: '🤎', - double: '💕', - excl: '❣', - green: '💚', - grow: '💗', - orange: '🧡', - purple: '💜', - real: '🫀', - revolve: '💞', - ribbon: '💝', - spark: '💖', - white: '🤍', - yellow: '💛', - ], - hedgehog: '🦔', - helicopter: '🚁', - helix: '🧬', - helmet: [cross: '⛑', military: '🪖'], - hippo: '🦛', - hockey: '🏑', - hole: '🕳', - honey: '🍯', - hongbao: '🧧', - hook: '🪝', - horn: [postal: '📯'], - horse: [ - '🐎', - carousel: '🎠', - face: '🐴', - race: '🏇', - ], - hospital: '🏥', - hotdog: '🌭', - hotel: ['🏨', love: '🏩'], - hotspring: '♨', - hourglass: ['⌛', flow: '⏳'], - house: [ - '🏠', - derelict: '🏚', - garden: '🏡', - multiple: '🏘', - ], - hundred: '💯', - hut: '🛖', - ice: '🧊', - icecream: ['🍨', shaved: '🍧', soft: '🍦'], - icehockey: '🏒', - id: '🆔', - info: 'ℹ', - izakaya: '🏮', - jar: '🫙', - jeans: '👖', - jigsaw: '🧩', - joystick: '🕹', - juggling: '🤹', - juice: '🧃', - kaaba: '🕋', - kadomatsu: '🎍', - kangaroo: '🦘', - gachi: '🈷', - go: '🈴', - hi: '㊙', - ka: '🉑', - kachi: '🈹', - kara: '🈳', - kon: '🈲', - man: '🈵', - muryo: '🈚', - shin: '🈸', - shuku: '㊗', - toku: '🉐', - yo: '🈺', - yubi: '🈯', - yuryo: '🈶', - koko: '🈁', - sa: '🈂', - kebab: '🥙', - key: ['🔑', old: '🗝'], - keyboard: '⌨', - kiss: '💏', - kissmark: '💋', - kite: '🪁', - kiwi: '🥝', - knife: ['🔪', dagger: '🗡'], - knot: '🪢', - koala: '🐨', - koinobori: '🎏', - label: '🏷', - lacrosse: '🥍', - ladder: '🪜', - lamp: [diya: '🪔'], - laptop: '💻', - a: '🅰', - ab: '🆎', - b: '🅱', - cl: '🆑', - o: '🅾', - leaf: [ - clover.three: '☘', - clover.four: '🍀', - fall: '🍂', - herb: '🌿', - maple: '🍁', - wind: '🍃', - ], - leftluggage: '🛅', - leg: ['🦵', mech: '🦿'], - lemon: '🍋', - leopard: '🐆', - letter: [love: '💌'], - liberty: '🗽', - lightbulb: '💡', - lightning: '⚡', - lion: '🦁', - lipstick: '💄', - litter: ['🚮', not: '🚯'], - lizard: '🦎', - llama: '🦙', - lobster: '🦞', - lock: [ - '🔒', - key: '🔐', - open: '🔓', - pen: '🔏', - ], - lollipop: '🍭', - lotion: '🧴', - luggage: '🧳', - lungs: '🫁', - mage: '🧙', - magnet: '🧲', - magnify: [r: '🔎', l: '🔍'], - mahjong: [dragon.red: '🀄'], - mail: ['✉', arrow: '📩'], - mailbox: [ - closed.empty: '📪', - closed.full: '📫', - open.empty: '📭', - open.full: '📬', - ], - mammoth: '🦣', - man: [ - '👨', - box: '🚹', - crown: '🤴', - guapimao: '👲', - levitate: '🕴', - old: '👴', - pregnant: '🫃', - turban: '👳', - tuxedo: '🤵', - ], - mango: '🥭', - map: [world: '🗺', jp: '🗾'], - martialarts: '🥋', - masks: '🎭', - mate: '🧉', - matryoshka: '🪆', - meat: ['🥩', bone: '🍖'], - medal: [ - first: '🥇', - second: '🥈', - third: '🥉', - sports: '🏅', - military: '🎖', - ], - megaphone: ['📢', simple: '📣'], - melon: '🍈', - merperson: '🧜', - metro: 'Ⓜ', - microbe: '🦠', - microphone: ['🎤', studio: '🎙'], - microscope: '🔬', - milkyway: '🌌', - mirror: '🪞', - mixer: '🎛', - money: [ - bag: '💰', - dollar: '💵', - euro: '💶', - pound: '💷', - yen: '💴', - wings: '💸', - ], - monkey: [ - '🐒', - face: '🐵', - hear.not: '🙉', - see.not: '🙈', - speak.not: '🙊', - ], - moon: [ - crescent: '🌙', - full: '🌕', - full.face: '🌝', - new: '🌑', - new.face: '🌚', - wane.one: '🌖', - wane.two: '🌗', - wane.three.face: '🌜', - wane.three: '🌘', - wax.one: '🌒', - wax.two: '🌓', - wax.two.face: '🌛', - wax.three: '🌔', - ], - mortarboard: '🎓', - mosque: '🕌', - mosquito: '🦟', - motorcycle: '🏍', - motorway: '🛣', - mountain: [ - '⛰', - fuji: '🗻', - snow: '🏔', - sunrise: '🌄', - ], - mouse: ['🐁', face: '🐭'], - mousetrap: '🪤', - mouth: ['👄', bite: '🫦'], - moyai: '🗿', - museum: '🏛', - mushroom: '🍄', - musicalscore: '🎼', - nails: [polish: '💅'], - namebadge: '📛', - nazar: '🧿', - necktie: '👔', - needle: '🪡', - nest: [empty: '🪹', eggs: '🪺'], - new: '🆕', - newspaper: ['📰', rolled: '🗞'], - ng: '🆖', - ningyo: '🎎', - ninja: '🥷', - noentry: '⛔', - nose: '👃', - notebook: ['📓', deco: '📔'], - notepad: '🗒', - notes: ['🎵', triple: '🎶'], - numbers: '🔢', - octopus: '🐙', - office: '🏢', - oil: '🛢', - ok: '🆗', - olive: '🫒', - oni: '👹', - onion: '🧅', - orangutan: '🦧', - otter: '🦦', - owl: '🦉', - ox: '🐂', - oyster: '🦪', - package: '📦', - paella: '🥘', - page: ['📄', curl: '📃', pencil: '📝'], - pager: '📟', - pages: [tabs: '📑'], - painting: '🖼', - palette: '🎨', - pancakes: '🥞', - panda: '🐼', - parachute: '🪂', - park: '🏞', - parking: '🅿', - parrot: '🦜', - partalteration: '〽', - party: '🎉', - peach: '🍑', - peacock: '🦚', - peanuts: '🥜', - pear: '🍐', - pedestrian: ['🚶', not: '🚷'], - pen: [ball: '🖊', fountain: '🖋'], - pencil: '✏', - penguin: '🐧', - pepper: ['🫑', hot: '🌶'], - person: [ - '🧑', - angry: '🙎', - beard: '🧔', - blonde: '👱', - bow: '🙇', - crown: '🫅', - deaf: '🧏', - facepalm: '🤦', - frown: '🙍', - hijab: '🧕', - kneel: '🧎', - lotus: '🧘', - massage: '💆', - no: '🙅', - ok: '🙆', - old: '🧓', - pregnant: '🫄', - raise: '🙋', - sassy: '💁', - shrug: '🤷', - stand: '🧍', - steam: '🧖', - ], - petri: '🧫', - phone: [ - '📱', - arrow: '📲', - classic: '☎', - not: '📵', - off: '📴', - receiver: '📞', - signal: '📶', - vibrate: '📳', - ], - piano: '🎹', - pick: '⛏', - pie: '🥧', - pig: ['🐖', face: '🐷', nose: '🐽'], - pill: '💊', - pin: ['📌', round: '📍'], - pinata: '🪅', - pineapple: '🍍', - pingpong: '🏓', - pistol: '🔫', - pizza: '🍕', - placard: '🪧', - planet: '🪐', - plant: '🪴', - plaster: '🩹', - plate: [cutlery: '🍽'], - playback: [ - down: '⏬', - eject: '⏏', - forward: '⏩', - pause: '⏸', - record: '⏺', - repeat: '🔁', - repeat.once: '🔂', - repeat.v: '🔃', - restart: '⏮', - rewind: '⏪', - shuffle: '🔀', - skip: '⏭', - stop: '⏹', - toggle: '⏯', - up: '⏫', - ], - playingcard: [flower: '🎴', joker: '🃏'], - plunger: '🪠', - policeofficer: '👮', - poo: '💩', - popcorn: '🍿', - post: [eu: '🏤', jp: '🏣'], - postbox: '📮', - potato: ['🥔', sweet: '🍠'], - pouch: '👝', - powerplug: '🔌', - present: '🎁', - pretzel: '🥨', - printer: '🖨', - prints: [foot: '👣', paw: '🐾'], - prohibited: '🚫', - projector: '📽', - pumpkin: [lantern: '🎃'], - purse: '👛', - quest: ['❓', white: '❔'], - rabbit: ['🐇', face: '🐰'], - raccoon: '🦝', - radio: '📻', - radioactive: '☢', - railway: '🛤', - rainbow: '🌈', - ram: '🐏', - rat: '🐀', - razor: '🪒', - receipt: '🧾', - recycling: '♻', - reg: '®', - restroom: '🚻', - rhino: '🦏', - ribbon: ['🎀', remind: '🎗'], - rice: [ - '🍚', - cracker: '🍘', - ear: '🌾', - onigiri: '🍙', - ], - ring: '💍', - ringbuoy: '🛟', - robot: '🤖', - rock: '🪨', - rocket: '🚀', - rollercoaster: '🎢', - rosette: '🏵', - rugby: '🏉', - ruler: ['📏', triangle: '📐'], - running: '🏃', - safetypin: '🧷', - safetyvest: '🦺', - sake: '🍶', - salad: '🥗', - salt: '🧂', - sandwich: '🥪', - santa: [man: '🎅', woman: '🤶'], - satdish: '📡', - satellite: '🛰', - saw: '🪚', - saxophone: '🎷', - scales: '⚖', - scarf: '🧣', - school: '🏫', - scissors: '✂', - scooter: ['🛴', motor: '🛵'], - scorpion: '🦂', - screwdriver: '🪛', - scroll: '📜', - seal: '🦭', - seat: '💺', - seedling: '🌱', - shark: '🦈', - sheep: '🐑', - shell: [spiral: '🐚'], - shield: '🛡', - ship: ['🚢', cruise: '🛳', ferry: '⛴'], - shirt: [sports: '🎽', t: '👕'], - shoe: [ - '👞', - ballet: '🩰', - flat: '🥿', - heel: '👠', - hike: '🥾', - ice: '⛸', - roller: '🛼', - sandal.heel: '👡', - ski: '🎿', - sneaker: '👟', - tall: '👢', - thong: '🩴', - ], - shopping: '🛍', - shorts: '🩳', - shoshinsha: '🔰', - shower: '🚿', - shrimp: ['🦐', fried: '🍤'], - shrine: '⛩', - sign: [crossing: '🚸', stop: '🛑'], - silhouette: [ - '👤', - double: '👥', - hug: '🫂', - speak: '🗣', - ], - siren: '🚨', - skateboard: '🛹', - skewer: [dango: '🍡', oden: '🍢'], - skiing: '⛷', - skull: ['💀', bones: '☠'], - skunk: '🦨', - sled: '🛷', - slide: '🛝', - slider: '🎚', - sloth: '🦥', - slots: '🎰', - snail: '🐌', - snake: '🐍', - snowboarding: '🏂', - snowflake: '❄', - snowman: ['⛄', snow: '☃'], - soap: '🧼', - socks: '🧦', - softball: '🥎', - sos: '🆘', - soup: '🍲', - spaghetti: '🍝', - sparkle: [box: '❇'], - sparkler: '🎇', - sparkles: '✨', - speaker: [ - '🔈', - not: '🔇', - wave: '🔉', - waves: '🔊', - ], - spider: '🕷', - spiderweb: '🕸', - spinach: '🥬', - sponge: '🧽', - spoon: '🥄', - square: [ - black: '⬛', - black.tiny: '▪', - black.small: '◾', - black.medium: '◼', - white: '⬜', - white.tiny: '▫', - white.small: '◽', - white.medium: '◻', - blue: '🟦', - brown: '🟫', - green: '🟩', - orange: '🟧', - purple: '🟪', - red: '🟥', - yellow: '🟨', - ], - squid: '🦑', - stadium: '🏟', - star: [ - '⭐', - arc: '💫', - box: '✴', - glow: '🌟', - shoot: '🌠', - ], - stethoscope: '🩺', - store: [big: '🏬', small: '🏪'], - strawberry: '🍓', - suit: [ - club: '♣', - diamond: '♦', - heart: '♥', - spade: '♠', - ], - sun: ['☀', cloud: '🌤', face: '🌞'], - sunrise: '🌅', - superhero: '🦸', - supervillain: '🦹', - surfing: '🏄', - sushi: '🍣', - swan: '🦢', - swimming: '🏊', - swimsuit: '🩱', - swords: '⚔', - symbols: '🔣', - synagogue: '🕍', - syringe: '💉', - taco: '🌮', - takeout: '🥡', - tamale: '🫔', - tanabata: '🎋', - tangerine: '🍊', - tap: ['🚰', not: '🚱'], - taxi: ['🚕', front: '🚖'], - teacup: '🍵', - teapot: '🫖', - teddy: '🧸', - telescope: '🔭', - temple: '🛕', - ten: '🔟', - tengu: '👺', - tennis: '🎾', - tent: '⛺', - testtube: '🧪', - thermometer: '🌡', - thread: '🧵', - thumb: [up: '👍', down: '👎'], - ticket: [event: '🎟', travel: '🎫'], - tiger: ['🐅', face: '🐯'], - tm: '™', - toilet: '🚽', - toiletpaper: '🧻', - tomato: '🍅', - tombstone: '🪦', - tongue: '👅', - toolbox: '🧰', - tooth: '🦷', - toothbrush: '🪥', - tornado: '🌪', - tower: [tokyo: '🗼'], - trackball: '🖲', - tractor: '🚜', - trafficlight: [v: '🚦', h: '🚥'], - train: [ - '🚆', - car: '🚃', - light: '🚈', - metro: '🚇', - mono: '🚝', - mountain: '🚞', - speed: '🚄', - speed.bullet: '🚅', - steam: '🚂', - stop: '🚉', - suspend: '🚟', - tram: '🚊', - tram.car: '🚋', - ], - transgender: '⚧', - tray: [inbox: '📥', mail: '📨', outbox: '📤'], - tree: [ - deciduous: '🌳', - evergreen: '🌲', - palm: '🌴', - xmas: '🎄', - ], - triangle: [ - r: '▶', - l: '◀', - t: '🔼', - b: '🔽', - t.red: '🔺', - b.red: '🔻', - ], - trident: '🔱', - troll: '🧌', - trophy: '🏆', - truck: ['🚚', trailer: '🚛'], - trumpet: '🎺', - tsukimi: '🎑', - turkey: '🦃', - turtle: '🐢', - tv: '📺', - ufo: '🛸', - umbrella: [ - open: '☂', - closed: '🌂', - rain: '☔', - sun: '⛱', - ], - unicorn: '🦄', - unknown: '🦳', - up: '🆙', - urn: '⚱', - vampire: '🧛', - violin: '🎻', - volcano: '🌋', - volleyball: '🏐', - vs: '🆚', - waffle: '🧇', - wand: '🪄', - warning: '⚠', - watch: ['⌚', stop: '⏱'], - watermelon: '🍉', - waterpolo: '🤽', - wave: '🌊', - wc: '🚾', - weightlifting: '🏋', - whale: ['🐋', spout: '🐳'], - wheel: '🛞', - wheelchair: ['🦽', box: '♿', motor: '🦼'], - wind: '🌬', - windchime: '🎐', - window: '🪟', - wine: '🍷', - wolf: '🐺', - woman: [ - '👩', - box: '🚺', - crown: '👸', - old: '👵', - pregnant: '🤰', - ], - wood: '🪵', - worm: '🪱', - wrench: '🔧', - wrestling: '🤼', - xray: '🩻', - yarn: '🧶', - yoyo: '🪀', - zebra: '🦓', - zodiac: [ - aquarius: '♒', - aries: '♈', - cancer: '♋', - capri: '♑', - gemini: '♊', - leo: '♌', - libra: '♎', - ophi: '⛎', - pisces: '♓', - sagit: '♐', - scorpio: '♏', - taurus: '♉', - virgo: '♍', - ], - zombie: '🧟', - zzz: '💤', -}; diff --git a/crates/typst-library/src/symbols/mod.rs b/crates/typst-library/src/symbols/mod.rs deleted file mode 100644 index 0d288c3b..00000000 --- a/crates/typst-library/src/symbols/mod.rs +++ /dev/null @@ -1,17 +0,0 @@ -//! Modifiable symbols. - -mod emoji; -mod sym; - -pub use emoji::*; -pub use sym::*; - -use crate::prelude::*; - -/// Hook up all symbol definitions. -pub(super) fn define(global: &mut Scope) { - global.category("symbols"); - global.define_type::<Symbol>(); - global.define_module(sym()); - global.define_module(emoji()); -} diff --git a/crates/typst-library/src/symbols/sym.rs b/crates/typst-library/src/symbols/sym.rs deleted file mode 100644 index 79696a38..00000000 --- a/crates/typst-library/src/symbols/sym.rs +++ /dev/null @@ -1,873 +0,0 @@ -use typst::eval::{symbols, Module, Scope, Symbol}; - -/// A module with all general symbols. -pub fn sym() -> Module { - let mut scope = Scope::new(); - for (name, symbol) in SYM { - scope.define(*name, symbol.clone()); - } - Module::new("sym", scope) -} - -/// The list of general symbols. -pub(crate) const SYM: &[(&str, Symbol)] = symbols! { - // Control. - wj: '\u{2060}', - zwj: '\u{200D}', - zwnj: '\u{200C}', - zws: '\u{200B}', - - // Spaces. - space: [ - ' ', - nobreak: '\u{A0}', - en: '\u{2002}', - quad: '\u{2003}', - third: '\u{2004}', - quarter: '\u{2005}', - sixth: '\u{2006}', - med: '\u{205F}', - fig: '\u{2007}', - punct: '\u{2008}', - thin: '\u{2009}', - hair: '\u{200A}', - ], - - // Delimiters. - paren: [l: '(', r: ')', t: '⏜', b: '⏝'], - brace: [l: '{', r: '}', t: '⏞', b: '⏟'], - bracket: [l: '[', l.double: '⟦', r: ']', r.double: '⟧', t: '⎴', b: '⎵'], - turtle: [l: '〔', r: '〕', t: '⏠', b: '⏡'], - bar: [v: '|', v.double: '‖', v.triple: '⦀', v.broken: '¦', v.circle: '⦶', h: '―'], - fence: [l: '⧘', l.double: '⧚', r: '⧙', r.double: '⧛', dotted: '⦙'], - angle: [ - '∠', - l: '⟨', - r: '⟩', - l.double: '《', - r.double: '》', - acute: '⦟', - arc: '∡', - arc.rev: '⦛', - rev: '⦣', - right: '∟', - right.rev: '⯾', - right.arc: '⊾', - right.dot: '⦝', - right.sq: '⦜', - spatial: '⟀', - spheric: '∢', - spheric.rev: '⦠', - spheric.top: '⦡', - ], - - // Punctuation. - amp: ['&', inv: '⅋'], - ast: [ - op: '∗', - basic: '*', - low: '⁎', - double: '⁑', - triple: '⁂', - small: '﹡', - circle: '⊛', - square: '⧆', - ], - at: '@', - backslash: ['\\', circle: '⦸', not: '⧷'], - co: '℅', - colon: [':', eq: '≔', double.eq: '⩴'], - comma: ',', - dagger: ['†', double: '‡'], - dash: [ - en: '–', - em: '—', - fig: '‒', - wave: '〜', - colon: '∹', - circle: '⊝', - wave.double: '〰', - ], - dot: [ - op: '⋅', - basic: '.', - c: '·', - circle: '⊙', - circle.big: '⨀', - square: '⊡', - double: '¨', - triple: '\u{20db}', - quad: '\u{20dc}', - ], - excl: ['!', double: '‼', inv: '¡', quest: '⁉'], - quest: ['?', double: '⁇', excl: '⁈', inv: '¿'], - interrobang: '‽', - hash: '#', - hyph: ['‐', minus: '\u{2D}', nobreak: '\u{2011}', point: '‧', soft: '\u{ad}'], - percent: '%', - copyright: ['©', sound: '℗'], - permille: '‰', - pilcrow: ['¶', rev: '⁋'], - section: '§', - semi: [';', rev: '⁏'], - slash: ['/', double: '⫽', triple: '⫻', big: '\u{29f8}'], - dots: [h.c: '⋯', h: '…', v: '⋮', down: '⋱', up: '⋰'], - tilde: [ - op: '∼', - basic: '~', - eq: '≃', - eq.not: '≄', - eq.rev: '⋍', - equiv: '≅', - equiv.not: '≇', - nequiv: '≆', - not: '≁', - rev: '∽', - rev.equiv: '≌', - triple: '≋', - ], - - // Accents, quotes, and primes. - acute: ['´', double: '˝'], - breve: '˘', - caret: '‸', - caron: 'ˇ', - hat: '^', - diaer: '¨', - grave: '`', - macron: '¯', - quote: [ - double: '"', - single: '\'', - l.double: '“', - l.single: '‘', - r.double: '”', - r.single: '’', - angle.l.double: '«', - angle.l.single: '‹', - angle.r.double: '»', - angle.r.single: '›', - high.double: '‟', - high.single: '‛', - low.double: '„', - low.single: '‚', - ], - prime: [ - '′', - rev: '‵', - double: '″', - double.rev: '‶', - triple: '‴', - triple.rev: '‷', - quad: '⁗', - ], - - // https://en.wikipedia.org/wiki/List_of_mathematical_symbols_by_subject - // Arithmetic. - plus: [ - '+', - circle: '⊕', - circle.arrow: '⟴', - circle.big: '⨁', - dot: '∔', - minus: '±', - small: '﹢', - square: '⊞', - triangle: '⨹', - ], - minus: [ - '−', - circle: '⊖', - dot: '∸', - plus: '∓', - square: '⊟', - tilde: '≂', - triangle: '⨺', - ], - div: ['÷', circle: '⨸'], - times: [ - '×', - big: '⨉', - circle: '⊗', - circle.big: '⨂', - div: '⋇', - three.l: '⋋', - three.r: '⋌', - l: '⋉', - r: '⋊', - square: '⊠', - triangle: '⨻', - ], - ratio: '∶', - - // Relations. - eq: [ - '=', - star: '≛', - circle: '⊜', - colon: '≕', - def: '≝', - delta: '≜', - equi: '≚', - est: '≙', - gt: '⋝', - lt: '⋜', - m: '≞', - not: '≠', - prec: '⋞', - quest: '≟', - small: '﹦', - succ: '⋟', - triple: '≡', - quad: '≣', - ], - gt: [ - '>', - circle: '⧁', - curly: '≻', - curly.approx: '⪸', - curly.double: '⪼', - curly.eq: '≽', - curly.eq.not: '⋡', - curly.equiv: '⪴', - curly.napprox: '⪺', - curly.nequiv: '⪶', - curly.not: '⊁', - curly.ntilde: '⋩', - curly.tilde: '≿', - dot: '⋗', - double: '≫', - eq: '≥', - eq.slant: '⩾', - eq.lt: '⋛', - eq.not: '≱', - equiv: '≧', - lt: '≷', - lt.not: '≹', - nequiv: '≩', - not: '≯', - ntilde: '⋧', - small: '﹥', - tilde: '≳', - tilde.not: '≵', - tri: '⊳', - tri.eq: '⊵', - tri.eq.not: '⋭', - tri.not: '⋫', - triple: '⋙', - triple.nested: '⫸', - ], - lt: [ - '<', - circle: '⧀', - curly: '≺', - curly.approx: '⪷', - curly.double: '⪻', - curly.eq: '≼', - curly.eq.not: '⋠', - curly.equiv: '⪳', - curly.napprox: '⪹', - curly.nequiv: '⪵', - curly.not: '⊀', - curly.ntilde: '⋨', - curly.tilde: '≾', - dot: '⋖', - double: '≪', - eq: '≤', - eq.slant: '⩽' , - eq.gt: '⋚', - eq.not: '≰', - equiv: '≦', - gt: '≶', - gt.not: '≸', - nequiv: '≨', - not: '≮', - ntilde: '⋦', - small: '﹤', - tilde: '≲', - tilde.not: '≴', - tri: '⊲', - tri.eq: '⊴', - tri.eq.not: '⋬', - tri.not: '⋪', - triple: '⋘', - triple.nested: '⫷', - ], - approx: ['≈', eq: '≊', not: '≉'], - prec: [ - '≺', - approx: '⪷', - double: '⪻', - eq: '≼', - eq.not: '⋠', - equiv: '⪳', - napprox: '⪹', - nequiv: '⪵', - not: '⊀', - ntilde: '⋨', - tilde: '≾', - ], - succ: [ - '≻', - approx: '⪸', - double: '⪼', - eq: '≽', - eq.not: '⋡', - equiv: '⪴', - napprox: '⪺', - nequiv: '⪶', - not: '⊁', - ntilde: '⋩', - tilde: '≿', - ], - equiv: ['≡', not: '≢'], - prop: '∝', - - // Set theory. - emptyset: ['∅', rev: '⦰'], - nothing: ['∅', rev: '⦰'], - without: '∖', - complement: '∁', - in: [ - '∈', - not: '∉', - rev: '∋', - rev.not: '∌', - rev.small: '∍', - small: '∊', - ], - subset: [ - '⊂', - dot: '⪽', - double: '⋐', - eq: '⊆', - eq.not: '⊈', - eq.sq: '⊑', - eq.sq.not: '⋢', - neq: '⊊', - not: '⊄', - sq: '⊏', - sq.neq: '⋤', - ], - supset: [ - '⊃', - dot: '⪾', - double: '⋑', - eq: '⊇', - eq.not: '⊉', - eq.sq: '⊒', - eq.sq.not: '⋣', - neq: '⊋', - not: '⊅', - sq: '⊐', - sq.neq: '⋥', - ], - union: [ - '∪', - arrow: '⊌', - big: '⋃', - dot: '⊍', - dot.big: '⨃', - double: '⋓', - minus: '⩁', - or: '⩅', - plus: '⊎', - plus.big: '⨄', - sq: '⊔', - sq.big: '⨆', - sq.double: '⩏', - ], - sect: [ - '∩', - and: '⩄', - big: '⋂', - dot: '⩀', - double: '⋒', - sq: '⊓', - sq.big: '⨅', - sq.double: '⩎', - ], - - // Calculus. - infinity: '∞', - oo: '∞', - diff: '∂', - nabla: '∇', - sum: ['∑', integral: '⨋'], - product: ['∏', co: '∐'], - integral: [ - '∫', - arrow.hook: '⨗', - ccw: '⨑', - cont: '∮', - cont.ccw: '∳', - cont.cw: '∲', - cw: '∱', - dash: '⨍', - dash.double: '⨎', - double: '∬', - quad: '⨌', - sect: '⨙', - slash: '⨏', - square: '⨖', - surf: '∯', - times: '⨘', - triple: '∭', - union: '⨚', - vol: '∰', - ], - laplace: '∆', - - // Logic. - forall: '∀', - exists: ['∃', not: '∄'], - top: '⊤', - bot: '⊥', - not: '¬', - and: ['∧', big: '⋀', curly: '⋏', dot: '⟑', double: '⩓'], - or: ['∨', big: '⋁', curly: '⋎', dot: '⟇', double: '⩔'], - xor: ['⊕', big: '⨁'], - models: '⊧', - therefore: '∴', - because: '∵', - qed: '∎', - - // Function and category theory. - compose: '∘', - convolve: '∗', - multimap: '⊸', - - // Number theory. - divides: ['∣', not: '∤'], - - // Algebra. - wreath: '≀', - - // Geometry. - parallel: ['∥', circle: '⦷', not: '∦'], - perp: ['⟂', circle: '⦹'], - - // Miscellaneous Technical. - diameter: '⌀', - join: ['⨝', r: '⟖', l: '⟕', l.r: '⟗'], - degree: ['°', c: '℃', f: '℉'], - smash: '⨳', - - // Currency. - bitcoin: '₿', - dollar: '$', - euro: '€', - franc: '₣', - lira: '₺', - peso: '₱', - pound: '£', - ruble: '₽', - rupee: '₹', - won: '₩', - yen: '¥', - - // Miscellaneous. - ballot: ['☐', x: '☒'], - checkmark: ['✓', light: '🗸'], - floral: ['❦', l: '☙', r: '❧'], - notes: [up: '🎜', down: '🎝'], - refmark: '※', - servicemark: '℠', - maltese: '✠', - suit: [club: '♣', diamond: '♦', heart: '♥', spade: '♠'], - - // Shapes. - bullet: '•', - circle: [ - stroked: '○', - stroked.tiny: '∘', - stroked.small: '⚬', - stroked.big: '◯', - filled: '●', - filled.tiny: '⦁', - filled.small: '∙', - filled.big: '⬤', - dotted: '◌', - nested: '⊚', - ], - ellipse: [ - stroked.h: '⬭', - stroked.v: '⬯', - filled.h: '⬬', - filled.v: '⬮', - ], - triangle: [ - stroked.r: '▷', - stroked.l: '◁', - stroked.t: '△', - stroked.b: '▽', - stroked.bl: '◺', - stroked.br: '◿', - stroked.tl: '◸', - stroked.tr: '◹', - stroked.small.r: '▹', - stroked.small.b: '▿', - stroked.small.l: '◃', - stroked.small.t: '▵', - stroked.rounded: '🛆', - stroked.nested: '⟁', - stroked.dot: '◬', - filled.r: '▶', - filled.l: '◀', - filled.t: '▲', - filled.b: '▼', - filled.bl: '◣', - filled.br: '◢', - filled.tl: '◤', - filled.tr: '◥', - filled.small.r: '▸', - filled.small.b: '▾', - filled.small.l: '◂', - filled.small.t: '▴', - ], - square: [ - stroked: '□', - stroked.tiny: '▫', - stroked.small: '◽', - stroked.medium: '◻', - stroked.big: '⬜', - stroked.dotted: '⬚', - stroked.rounded: '▢', - filled: '■', - filled.tiny: '▪', - filled.small: '◾', - filled.medium: '◼', - filled.big: '⬛', - ], - rect: [ - stroked.h: '▭', - stroked.v: '▯', - filled.h: '▬', - filled.v: '▮', - ], - penta: [stroked: '⬠', filled: '⬟'], - hexa: [stroked: '⬡', filled: '⬢'], - diamond: [ - stroked: '◇', - stroked.small: '⋄', - stroked.medium: '⬦', - stroked.dot: '⟐', - filled: '◆', - filled.medium: '⬥', - filled.small: '⬩', - ], - lozenge: [ - stroked: '◊', - stroked.small: '⬫', - stroked.medium: '⬨', - filled: '⧫', - filled.small: '⬪', - filled.medium: '⬧', - ], - star: [op: '⋆', stroked: '★', filled: '★'], - - // Arrows, harpoons, and tacks. - arrow: [ - r: '→', - r.long.bar: '⟼', - r.bar: '↦', - r.curve: '⤷', - r.dashed: '⇢', - r.dotted: '⤑', - r.double: '⇒', - r.double.bar: '⤇', - r.double.long: '⟹', - r.double.long.bar: '⟾', - r.double.not: '⇏', - r.filled: '➡', - r.hook: '↪', - r.long: '⟶', - r.long.squiggly: '⟿', - r.loop: '↬', - r.not: '↛', - r.quad: '⭆', - r.squiggly: '⇝', - r.stop: '⇥', - r.stroked: '⇨', - r.tail: '↣', - r.triple: '⇛', - r.twohead.bar: '⤅', - r.twohead: '↠', - r.wave: '↝', - l: '←', - l.bar: '↤', - l.curve: '⤶', - l.dashed: '⇠', - l.dotted: '⬸', - l.double: '⇐', - l.double.bar: '⤆', - l.double.long: '⟸', - l.double.long.bar: '⟽', - l.double.not: '⇍', - l.filled: '⬅', - l.hook: '↩', - l.long: '⟵', - l.long.bar: '⟻', - l.long.squiggly: '⬳', - l.loop: '↫', - l.not: '↚', - l.quad: '⭅', - l.squiggly: '⇜', - l.stop: '⇤', - l.stroked: '⇦', - l.tail: '↢', - l.triple: '⇚', - l.twohead.bar: '⬶', - l.twohead: '↞', - l.wave: '↜', - t: '↑', - t.bar: '↥', - t.curve: '⤴', - t.dashed: '⇡', - t.double: '⇑', - t.filled: '⬆', - t.quad: '⟰', - t.stop: '⤒', - t.stroked: '⇧', - t.triple: '⤊', - t.twohead: '↟', - b: '↓', - b.bar: '↧', - b.curve: '⤵', - b.dashed: '⇣', - b.double: '⇓', - b.filled: '⬇', - b.quad: '⟱', - b.stop: '⤓', - b.stroked: '⇩', - b.triple: '⤋', - b.twohead: '↡', - l.r: '↔', - l.r.double: '⇔', - l.r.double.long: '⟺', - l.r.double.not: '⇎', - l.r.filled: '⬌', - l.r.long: '⟷', - l.r.not: '↮', - l.r.stroked: '⬄', - l.r.wave: '↭', - t.b: '↕', - t.b.double: '⇕', - t.b.filled: '⬍', - t.b.stroked: '⇳', - tr: '↗', - tr.double: '⇗', - tr.filled: '⬈', - tr.hook: '⤤', - tr.stroked: '⬀', - br: '↘', - br.double: '⇘', - br.filled: '⬊', - br.hook: '⤥', - br.stroked: '⬂', - tl: '↖', - tl.double: '⇖', - tl.filled: '⬉', - tl.hook: '⤣', - tl.stroked: '⬁', - bl: '↙', - bl.double: '⇙', - bl.filled: '⬋', - bl.hook: '⤦', - bl.stroked: '⬃', - tl.br: '⤡', - tr.bl: '⤢', - ccw: '↺', - ccw.half: '↶', - cw: '↻', - cw.half: '↷', - zigzag: '↯', - ], - arrows: [ - rr: '⇉', - ll: '⇇', - tt: '⇈', - bb: '⇊', - lr: '⇆', - lr.stop: '↹', - rl: '⇄', - tb: '⇅', - bt: '⇵', - rrr: '⇶', - lll: '⬱', - ], - arrowhead: [ - t: '⌃', - b: '⌄', - ], - harpoon: [ - rt: '⇀', - rt.bar: '⥛', - rt.stop: '⥓', - rb: '⇁', - rb.bar: '⥟', - rb.stop: '⥗', - lt: '↼', - lt.bar: '⥚', - lt.stop: '⥒', - lb: '↽', - lb.bar: '⥞', - lb.stop: '⥖', - tl: '↿', - tl.bar: '⥠', - tl.stop: '⥘', - tr: '↾', - tr.bar: '⥜', - tr.stop: '⥔', - bl: '⇃', - bl.bar: '⥡', - bl.stop: '⥙', - br: '⇂', - br.bar: '⥝', - br.stop: '⥕', - lt.rt: '⥎', - lb.rb: '⥐', - lb.rt: '⥋', - lt.rb: '⥊', - tl.bl: '⥑', - tr.br: '⥏', - tl.br: '⥍', - tr.bl: '⥌', - ], - harpoons: [ - rtrb: '⥤', - blbr: '⥥', - bltr: '⥯', - lbrb: '⥧', - ltlb: '⥢', - ltrb: '⇋', - ltrt: '⥦', - rblb: '⥩', - rtlb: '⇌', - rtlt: '⥨', - tlbr: '⥮', - tltr: '⥣', - ], - tack: [ - r: '⊢', - r.not: '⊬', - r.long: '⟝', - r.short: '⊦', - r.double: '⊨', - r.double.not: '⊭', - l: '⊣', - l.long: '⟞', - l.short: '⫞', - l.double: '⫤', - t: '⊥', - t.big: '⟘', - t.double: '⫫', - t.short: '⫠', - b: '⊤', - b.big: '⟙', - b.double: '⫪', - b.short: '⫟', - l.r: '⟛', - ], - - // Lowercase Greek. - alpha: 'α', - beta: ['β', alt: 'ϐ'], - chi: 'χ', - delta: 'δ', - epsilon: ['ε', alt: 'ϵ'], - eta: 'η', - gamma: 'γ', - iota: 'ι', - kai: 'ϗ', - kappa: ['κ', alt: 'ϰ'], - lambda: 'λ', - mu: 'μ', - nu: 'ν', - ohm: ['Ω', inv: '℧'], - omega: 'ω', - omicron: 'ο', - phi: ['φ', alt: 'ϕ'], - pi: ['π', alt: 'ϖ'], - psi: 'ψ', - rho: ['ρ', alt: 'ϱ'], - sigma: ['σ', alt: 'ς'], - tau: 'τ', - theta: ['θ', alt: 'ϑ'], - upsilon: 'υ', - xi: 'ξ', - zeta: 'ζ', - - // Uppercase Greek. - Alpha: 'Α', - Beta: 'Β', - Chi: 'Χ', - Delta: 'Δ', - Epsilon: 'Ε', - Eta: 'Η', - Gamma: 'Γ', - Iota: 'Ι', - Kai: 'Ϗ', - Kappa: 'Κ', - Lambda: 'Λ', - Mu: 'Μ', - Nu: 'Ν', - Omega: 'Ω', - Omicron: 'Ο', - Phi: 'Φ', - Pi: 'Π', - Psi: 'Ψ', - Rho: 'Ρ', - Sigma: 'Σ', - Tau: 'Τ', - Theta: 'Θ', - Upsilon: 'Υ', - Xi: 'Ξ', - Zeta: 'Ζ', - - // Hebrew. - aleph: 'א', - alef: 'א', - beth: 'ב', - bet: 'ב', - gimmel: 'ג', - gimel: 'ג', - shin: 'ש', - - // Double-struck. - AA: '𝔸', - BB: '𝔹', - CC: 'ℂ', - DD: '𝔻', - EE: '𝔼', - FF: '𝔽', - GG: '𝔾', - HH: 'ℍ', - II: '𝕀', - JJ: '𝕁', - KK: '𝕂', - LL: '𝕃', - MM: '𝕄', - NN: 'ℕ', - OO: '𝕆', - PP: 'ℙ', - QQ: 'ℚ', - RR: 'ℝ', - SS: '𝕊', - TT: '𝕋', - UU: '𝕌', - VV: '𝕍', - WW: '𝕎', - XX: '𝕏', - YY: '𝕐', - ZZ: 'ℤ', - - // Miscellaneous letter-likes. - ell: 'ℓ', - planck: ['ℎ', reduce: 'ℏ'], - angstrom: 'Å', - kelvin: 'K', - Re: 'ℜ', - Im: 'ℑ', - dotless: [i: '𝚤', j: '𝚥'], -}; diff --git a/crates/typst-library/src/text/deco.rs b/crates/typst-library/src/text/deco.rs deleted file mode 100644 index aecff401..00000000 --- a/crates/typst-library/src/text/deco.rs +++ /dev/null @@ -1,566 +0,0 @@ -use kurbo::{BezPath, Line, ParamCurve}; -use ttf_parser::{GlyphId, OutlineBuilder}; - -use crate::prelude::*; -use crate::text::{BottomEdge, BottomEdgeMetric, TextElem, TopEdge, TopEdgeMetric}; - -/// Underlines text. -/// -/// # Example -/// ```example -/// This is #underline[important]. -/// ``` -#[elem(Show)] -pub struct UnderlineElem { - /// How to [stroke]($stroke) the line. - /// - /// If set to `{auto}`, takes on the text's color and a thickness defined in - /// the current font. - /// - /// ```example - /// Take #underline( - /// stroke: 1.5pt + red, - /// offset: 2pt, - /// [care], - /// ) - /// ``` - #[resolve] - #[fold] - pub stroke: Smart<Stroke>, - - /// The position of the line relative to the baseline, read from the font - /// tables if `{auto}`. - /// - /// ```example - /// #underline(offset: 5pt)[ - /// The Tale Of A Faraway Line I - /// ] - /// ``` - #[resolve] - pub offset: Smart<Length>, - - /// The amount by which to extend the line beyond (or within if negative) - /// the content. - /// - /// ```example - /// #align(center, - /// underline(extent: 2pt)[Chapter 1] - /// ) - /// ``` - #[resolve] - pub extent: Length, - - /// Whether the line skips sections in which it would collide with the - /// glyphs. - /// - /// ```example - /// This #underline(evade: true)[is great]. - /// This #underline(evade: false)[is less great]. - /// ``` - #[default(true)] - pub evade: bool, - - /// Whether the line is placed behind the content it underlines. - /// - /// ```example - /// #set underline(stroke: (thickness: 1em, paint: maroon, cap: "round")) - /// #underline(background: true)[This is stylized.] \ - /// #underline(background: false)[This is partially hidden.] - /// ``` - #[default(false)] - pub background: bool, - - /// The content to underline. - #[required] - pub body: Content, -} - -impl Show for UnderlineElem { - #[tracing::instrument(name = "UnderlineElem::show", skip_all)] - fn show(&self, _: &mut Vt, styles: StyleChain) -> SourceResult<Content> { - Ok(self.body().clone().styled(TextElem::set_deco(Decoration { - line: DecoLine::Underline { - stroke: self.stroke(styles).unwrap_or_default(), - offset: self.offset(styles), - evade: self.evade(styles), - background: self.background(styles), - }, - extent: self.extent(styles), - }))) - } -} - -/// Adds a line over text. -/// -/// # Example -/// ```example -/// #overline[A line over text.] -/// ``` -#[elem(Show)] -pub struct OverlineElem { - /// How to [stroke]($stroke) the line. - /// - /// If set to `{auto}`, takes on the text's color and a thickness defined in - /// the current font. - /// - /// ```example - /// #set text(fill: olive) - /// #overline( - /// stroke: green.darken(20%), - /// offset: -12pt, - /// [The Forest Theme], - /// ) - /// ``` - #[resolve] - #[fold] - pub stroke: Smart<Stroke>, - - /// The position of the line relative to the baseline. Read from the font - /// tables if `{auto}`. - /// - /// ```example - /// #overline(offset: -1.2em)[ - /// The Tale Of A Faraway Line II - /// ] - /// ``` - #[resolve] - pub offset: Smart<Length>, - - /// The amount by which to extend the line beyond (or within if negative) - /// the content. - /// - /// ```example - /// #set overline(extent: 4pt) - /// #set underline(extent: 4pt) - /// #overline(underline[Typography Today]) - /// ``` - #[resolve] - pub extent: Length, - - /// Whether the line skips sections in which it would collide with the - /// glyphs. - /// - /// ```example - /// #overline( - /// evade: false, - /// offset: -7.5pt, - /// stroke: 1pt, - /// extent: 3pt, - /// [Temple], - /// ) - /// ``` - #[default(true)] - pub evade: bool, - - /// Whether the line is placed behind the content it overlines. - /// - /// ```example - /// #set overline(stroke: (thickness: 1em, paint: maroon, cap: "round")) - /// #overline(background: true)[This is stylized.] \ - /// #overline(background: false)[This is partially hidden.] - /// ``` - #[default(false)] - pub background: bool, - - /// The content to add a line over. - #[required] - pub body: Content, -} - -impl Show for OverlineElem { - #[tracing::instrument(name = "OverlineElem::show", skip_all)] - fn show(&self, _: &mut Vt, styles: StyleChain) -> SourceResult<Content> { - Ok(self.body().clone().styled(TextElem::set_deco(Decoration { - line: DecoLine::Overline { - stroke: self.stroke(styles).unwrap_or_default(), - offset: self.offset(styles), - evade: self.evade(styles), - background: self.background(styles), - }, - extent: self.extent(styles), - }))) - } -} - -/// Strikes through text. -/// -/// # Example -/// ```example -/// This is #strike[not] relevant. -/// ``` -#[elem(title = "Strikethrough", Show)] -pub struct StrikeElem { - /// How to [stroke]($stroke) the line. - /// - /// If set to `{auto}`, takes on the text's color and a thickness defined in - /// the current font. - /// - /// _Note:_ Please don't use this for real redaction as you can still copy - /// paste the text. - /// - /// ```example - /// This is #strike(stroke: 1.5pt + red)[very stricken through]. \ - /// This is #strike(stroke: 10pt)[redacted]. - /// ``` - #[resolve] - #[fold] - pub stroke: Smart<Stroke>, - - /// The position of the line relative to the baseline. Read from the font - /// tables if `{auto}`. - /// - /// This is useful if you are unhappy with the offset your font provides. - /// - /// ```example - /// #set text(font: "Inria Serif") - /// This is #strike(offset: auto)[low-ish]. \ - /// This is #strike(offset: -3.5pt)[on-top]. - /// ``` - #[resolve] - pub offset: Smart<Length>, - - /// The amount by which to extend the line beyond (or within if negative) - /// the content. - /// - /// ```example - /// This #strike(extent: -2pt)[skips] parts of the word. - /// This #strike(extent: 2pt)[extends] beyond the word. - /// ``` - #[resolve] - pub extent: Length, - - /// Whether the line is placed behind the content. - /// - /// ```example - /// #set strike(stroke: red) - /// #strike(background: true)[This is behind.] \ - /// #strike(background: false)[This is in front.] - /// ``` - #[default(false)] - pub background: bool, - - /// The content to strike through. - #[required] - pub body: Content, -} - -impl Show for StrikeElem { - #[tracing::instrument(name = "StrikeElem::show", skip_all)] - fn show(&self, _: &mut Vt, styles: StyleChain) -> SourceResult<Content> { - Ok(self.body().clone().styled(TextElem::set_deco(Decoration { - // Note that we do not support evade option for strikethrough. - line: DecoLine::Strikethrough { - stroke: self.stroke(styles).unwrap_or_default(), - offset: self.offset(styles), - background: self.background(styles), - }, - extent: self.extent(styles), - }))) - } -} - -/// Highlights text with a background color. -/// -/// # Example -/// ```example -/// This is #highlight[important]. -/// ``` -#[elem(Show)] -pub struct HighlightElem { - /// The color to highlight the text with. - /// (Default: 0xffff5f) - /// - /// ```example - /// This is #highlight(fill: blue)[with blue]. - /// ``` - #[default(Color::from_u8(0xFF, 0xFF, 0x5F, 0xFF).into())] - pub fill: Paint, - - /// The top end of the background rectangle. - /// - /// ```example - /// #set highlight(top-edge: "ascender") - /// #highlight[a] #highlight[aib] - /// - /// #set highlight(top-edge: "x-height") - /// #highlight[a] #highlight[aib] - /// ``` - #[default(TopEdge::Metric(TopEdgeMetric::Ascender))] - pub top_edge: TopEdge, - - /// The bottom end of the background rectangle. - /// - /// ```example - /// #set highlight(bottom-edge: "descender") - /// #highlight[a] #highlight[ap] - /// - /// #set highlight(bottom-edge: "baseline") - /// #highlight[a] #highlight[ap] - /// ``` - #[default(BottomEdge::Metric(BottomEdgeMetric::Descender))] - pub bottom_edge: BottomEdge, - - /// The amount by which to extend the background to the sides beyond - /// (or within if negative) the content. - /// - /// ```example - /// A long #highlight(extent: 4pt)[background]. - /// ``` - #[resolve] - pub extent: Length, - - /// The content that should be highlighted. - #[required] - pub body: Content, -} - -impl Show for HighlightElem { - #[tracing::instrument(name = "HighlightElem::show", skip_all)] - fn show(&self, _: &mut Vt, styles: StyleChain) -> SourceResult<Content> { - Ok(self.body().clone().styled(TextElem::set_deco(Decoration { - line: DecoLine::Highlight { - fill: self.fill(styles), - top_edge: self.top_edge(styles), - bottom_edge: self.bottom_edge(styles), - }, - extent: self.extent(styles), - }))) - } -} - -/// Defines a line-based decoration that is positioned over, under or on top of text, -/// or highlights the text with a background. -#[ty] -#[derive(Debug, Clone, Eq, PartialEq, Hash)] -pub struct Decoration { - line: DecoLine, - extent: Abs, -} - -impl Fold for Decoration { - type Output = Vec<Self>; - - fn fold(self, mut outer: Self::Output) -> Self::Output { - outer.insert(0, self); - outer - } -} - -impl Repr for Decoration { - fn repr(&self) -> EcoString { - eco_format!("{self:?}") - } -} - -cast! { - type Decoration, -} - -/// A kind of decorative line. -#[derive(Debug, Clone, Eq, PartialEq, Hash)] -enum DecoLine { - Underline { stroke: Stroke<Abs>, offset: Smart<Abs>, evade: bool, background: bool }, - Strikethrough { stroke: Stroke<Abs>, offset: Smart<Abs>, background: bool }, - Overline { stroke: Stroke<Abs>, offset: Smart<Abs>, evade: bool, background: bool }, - Highlight { fill: Paint, top_edge: TopEdge, bottom_edge: BottomEdge }, -} - -/// Add line decorations to a single run of shaped text. -pub(super) fn decorate( - frame: &mut Frame, - deco: &Decoration, - text: &TextItem, - width: Abs, - shift: Abs, - pos: Point, -) { - let font_metrics = text.font.metrics(); - - if let DecoLine::Highlight { fill, top_edge, bottom_edge } = &deco.line { - let (top, bottom) = determine_edges(text, *top_edge, *bottom_edge); - let rect = Geometry::Rect(Size::new(width + 2.0 * deco.extent, top - bottom)) - .filled(fill.clone()); - let origin = Point::new(pos.x - deco.extent, pos.y - top - shift); - frame.prepend(origin, FrameItem::Shape(rect, Span::detached())); - return; - } - - let (stroke, metrics, offset, evade, background) = match &deco.line { - DecoLine::Strikethrough { stroke, offset, background } => { - (stroke, font_metrics.strikethrough, offset, false, *background) - } - DecoLine::Overline { stroke, offset, evade, background } => { - (stroke, font_metrics.overline, offset, *evade, *background) - } - DecoLine::Underline { stroke, offset, evade, background } => { - (stroke, font_metrics.underline, offset, *evade, *background) - } - _ => return, - }; - - let offset = offset.unwrap_or(-metrics.position.at(text.size)) - shift; - let stroke = stroke.clone().unwrap_or(FixedStroke { - paint: text.fill.as_decoration(), - thickness: metrics.thickness.at(text.size), - ..FixedStroke::default() - }); - - let gap_padding = 0.08 * text.size; - let min_width = 0.162 * text.size; - - let start = pos.x - deco.extent; - let end = pos.x + width + deco.extent; - - let mut push_segment = |from: Abs, to: Abs, prepend: bool| { - let origin = Point::new(from, pos.y + offset); - let target = Point::new(to - from, Abs::zero()); - - if target.x >= min_width || !evade { - let shape = Geometry::Line(target).stroked(stroke.clone()); - - if prepend { - frame.prepend(origin, FrameItem::Shape(shape, Span::detached())); - } else { - frame.push(origin, FrameItem::Shape(shape, Span::detached())); - } - } - }; - - if !evade { - push_segment(start, end, background); - return; - } - - let line = Line::new( - kurbo::Point::new(pos.x.to_raw(), offset.to_raw()), - kurbo::Point::new((pos.x + width).to_raw(), offset.to_raw()), - ); - - let mut x = pos.x; - let mut intersections = vec![]; - - for glyph in text.glyphs.iter() { - let dx = glyph.x_offset.at(text.size) + x; - let mut builder = - BezPathBuilder::new(font_metrics.units_per_em, text.size, dx.to_raw()); - - let bbox = text.font.ttf().outline_glyph(GlyphId(glyph.id), &mut builder); - let path = builder.finish(); - - x += glyph.x_advance.at(text.size); - - // Only do the costly segments intersection test if the line - // intersects the bounding box. - let intersect = bbox.map_or(false, |bbox| { - let y_min = -text.font.to_em(bbox.y_max).at(text.size); - let y_max = -text.font.to_em(bbox.y_min).at(text.size); - offset >= y_min && offset <= y_max - }); - - if intersect { - // Find all intersections of segments with the line. - intersections.extend( - path.segments() - .flat_map(|seg| seg.intersect_line(line)) - .map(|is| Abs::raw(line.eval(is.line_t).x)), - ); - } - } - - // Add start and end points, taking padding into account. - intersections.push(start - gap_padding); - intersections.push(end + gap_padding); - // When emitting the decorative line segments, we move from left to - // right. The intersections are not necessarily in this order, yet. - intersections.sort(); - - for edge in intersections.windows(2) { - let l = edge[0]; - let r = edge[1]; - - // If we are too close, don't draw the segment - if r - l < gap_padding { - continue; - } else { - push_segment(l + gap_padding, r - gap_padding, background); - } - } -} - -// Return the top/bottom edge of the text given the metric of the font. -fn determine_edges( - text: &TextItem, - top_edge: TopEdge, - bottom_edge: BottomEdge, -) -> (Abs, Abs) { - let mut bbox = None; - if top_edge.is_bounds() || bottom_edge.is_bounds() { - let ttf = text.font.ttf(); - bbox = text - .glyphs - .iter() - .filter_map(|g| ttf.glyph_bounding_box(ttf_parser::GlyphId(g.id))) - .reduce(|a, b| ttf_parser::Rect { - y_max: a.y_max.max(b.y_max), - y_min: a.y_min.min(b.y_min), - ..a - }); - } - - let top = top_edge.resolve(text.size, &text.font, bbox); - let bottom = bottom_edge.resolve(text.size, &text.font, bbox); - (top, bottom) -} - -/// Builds a kurbo [`BezPath`] for a glyph. -struct BezPathBuilder { - path: BezPath, - units_per_em: f64, - font_size: Abs, - x_offset: f64, -} - -impl BezPathBuilder { - fn new(units_per_em: f64, font_size: Abs, x_offset: f64) -> Self { - Self { - path: BezPath::new(), - units_per_em, - font_size, - x_offset, - } - } - - fn finish(self) -> BezPath { - self.path - } - - fn p(&self, x: f32, y: f32) -> kurbo::Point { - kurbo::Point::new(self.s(x) + self.x_offset, -self.s(y)) - } - - fn s(&self, v: f32) -> f64 { - Em::from_units(v, self.units_per_em).at(self.font_size).to_raw() - } -} - -impl OutlineBuilder for BezPathBuilder { - fn move_to(&mut self, x: f32, y: f32) { - self.path.move_to(self.p(x, y)); - } - - fn line_to(&mut self, x: f32, y: f32) { - self.path.line_to(self.p(x, y)); - } - - fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) { - self.path.quad_to(self.p(x1, y1), self.p(x, y)); - } - - fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) { - self.path.curve_to(self.p(x1, y1), self.p(x2, y2), self.p(x, y)); - } - - fn close(&mut self) { - self.path.close_path(); - } -} diff --git a/crates/typst-library/src/text/linebreak.rs b/crates/typst-library/src/text/linebreak.rs deleted file mode 100644 index 8a69e0a8..00000000 --- a/crates/typst-library/src/text/linebreak.rs +++ /dev/null @@ -1,262 +0,0 @@ -use icu_properties::maps::CodePointMapData; -use icu_properties::LineBreak; -use icu_provider::AsDeserializingBufferProvider; -use icu_provider_adapters::fork::ForkByKeyProvider; -use icu_provider_blob::BlobDataProvider; -use icu_segmenter::LineSegmenter; -use once_cell::sync::Lazy; -use typst::doc::Lang; -use typst::syntax::link_prefix; - -use crate::layout::Preparation; -use crate::text::TextElem; - -/// Generated by the following command: -/// -/// ```sh -/// icu4x-datagen --locales full \ -/// --format blob \ -/// --keys-for-bin target/debug/typst \ -/// --out crates/typst-library/assets/icudata.postcard \ -/// --overwrite -/// ``` -/// -/// Install icu_datagen with `cargo install icu_datagen`. -static ICU_DATA: &[u8] = include_bytes!("../../assets/icudata.postcard"); - -/// Generated by the following command: -/// -/// ```sh -/// icu4x-datagen --locales zh ja \ -/// --format blob \ -/// --keys segmenter/line@1 \ -/// --out crates/typst-library/assets/cj_linebreak_data.postcard \ -/// --overwrite -/// ``` -/// -/// The used icu_datagen should be patched by -/// https://github.com/peng1999/icu4x/commit/b9beb6cbf633d61fc3d7983e5baf7f4449fbfae5 -static CJ_LINEBREAK_DATA: &[u8] = - include_bytes!("../../assets/cj_linebreak_data.postcard"); - -/// The general line break segmenter. -static SEGMENTER: Lazy<LineSegmenter> = Lazy::new(|| { - let provider = BlobDataProvider::try_new_from_static_blob(ICU_DATA).unwrap(); - LineSegmenter::try_new_lstm_with_buffer_provider(&provider).unwrap() -}); - -/// The line break segmenter for Chinese/Japanese text. -static CJ_SEGMENTER: Lazy<LineSegmenter> = Lazy::new(|| { - let provider = BlobDataProvider::try_new_from_static_blob(ICU_DATA).unwrap(); - let cj_blob = BlobDataProvider::try_new_from_static_blob(CJ_LINEBREAK_DATA).unwrap(); - let cj_provider = ForkByKeyProvider::new(cj_blob, provider); - LineSegmenter::try_new_lstm_with_buffer_provider(&cj_provider).unwrap() -}); - -/// The Unicode line break properties for each code point. -static LINEBREAK_DATA: Lazy<CodePointMapData<LineBreak>> = Lazy::new(|| { - let provider = BlobDataProvider::try_new_from_static_blob(ICU_DATA).unwrap(); - let deser_provider = provider.as_deserializing(); - icu_properties::maps::load_line_break(&deser_provider).unwrap() -}); - -/// A line break opportunity. -#[derive(Debug, Copy, Clone, Eq, PartialEq)] -pub(crate) enum Breakpoint { - /// Just a normal opportunity (e.g. after a space). - Normal, - /// A mandatory breakpoint (after '\n' or at the end of the text). - Mandatory, - /// An opportunity for hyphenating. - Hyphen, -} - -/// Calls `f` for all possible points in the text where lines can broken. -/// -/// Yields for each breakpoint the text index, whether the break is mandatory -/// (after `\n`) and whether a hyphen is required (when breaking inside of a -/// word). -/// -/// This is an internal instead of an external iterator because it makes the -/// code much simpler and the consumers of this function don't need the -/// composability and flexibility of external iteration anyway. -pub(crate) fn breakpoints<'a>( - p: &'a Preparation<'a>, - mut f: impl FnMut(usize, Breakpoint), -) { - let text = p.bidi.text; - let hyphenate = p.hyphenate != Some(false); - let lb = LINEBREAK_DATA.as_borrowed(); - let segmenter = match p.lang { - Some(Lang::CHINESE | Lang::JAPANESE) => &CJ_SEGMENTER, - _ => &SEGMENTER, - }; - - let mut last = 0; - let mut iter = segmenter.segment_str(text).peekable(); - - loop { - // Special case for links. UAX #14 doesn't handle them well. - let (head, tail) = text.split_at(last); - if head.ends_with("://") || tail.starts_with("www.") { - let (link, _) = link_prefix(tail); - let end = last + link.len(); - linebreak_link(link, |i| f(last + i, Breakpoint::Normal)); - while iter.peek().map_or(false, |&p| p < end) { - iter.next(); - } - } - - // Get the UAX #14 linebreak opportunities. - let Some(point) = iter.next() else { break }; - - // Skip breakpoint if there is no char before it. icu4x generates one - // at offset 0, but we don't want it. - let Some(c) = text[..point].chars().next_back() else { continue }; - - // Find out whether the last break was mandatory by checking against - // rules LB4 and LB5, special-casing the end of text according to LB3. - // See also: https://docs.rs/icu_segmenter/latest/icu_segmenter/struct.LineSegmenter.html - let breakpoint = if point == text.len() { - Breakpoint::Mandatory - } else { - match lb.get(c) { - // Fix for: https://github.com/unicode-org/icu4x/issues/4146 - LineBreak::Glue | LineBreak::WordJoiner | LineBreak::ZWJ => continue, - LineBreak::MandatoryBreak - | LineBreak::CarriageReturn - | LineBreak::LineFeed - | LineBreak::NextLine => Breakpoint::Mandatory, - _ => Breakpoint::Normal, - } - }; - - // Hyphenate between the last and current breakpoint. - 'hyphenate: { - if !hyphenate { - break 'hyphenate; - } - - // Extract a hyphenatable "word". - let word = &text[last..point].trim_end_matches(|c: char| !c.is_alphabetic()); - if word.is_empty() { - break 'hyphenate; - } - - let end = last + word.len(); - let mut offset = last; - - // Determine the language to hyphenate this word in. - let Some(lang) = lang_at(p, last) else { break 'hyphenate }; - - for syllable in hypher::hyphenate(word, lang) { - // Don't hyphenate after the final syllable. - offset += syllable.len(); - if offset == end { - continue; - } - - // Filter out hyphenation opportunities where hyphenation was - // actually disabled. - if !hyphenate_at(p, offset) { - continue; - } - - // Filter out forbidden hyphenation opportunities. - if matches!( - syllable.chars().next_back().map(|c| lb.get(c)), - Some(LineBreak::Glue | LineBreak::WordJoiner | LineBreak::ZWJ) - ) { - continue; - } - - // Call `f` for the word-internal hyphenation opportunity. - f(offset, Breakpoint::Hyphen); - } - } - - // Call `f` for the UAX #14 break opportunity. - f(point, breakpoint); - - last = point; - } -} - -/// Produce linebreak opportunities for a link. -fn linebreak_link(link: &str, mut f: impl FnMut(usize)) { - #[derive(PartialEq)] - enum Class { - Alphabetic, - Digit, - Open, - Other, - } - - impl Class { - fn of(c: char) -> Self { - if c.is_alphabetic() { - Class::Alphabetic - } else if c.is_numeric() { - Class::Digit - } else if matches!(c, '(' | '[') { - Class::Open - } else { - Class::Other - } - } - } - - let mut offset = 0; - let mut prev = Class::Other; - - for (end, c) in link.char_indices() { - let class = Class::of(c); - - // Emit opportunities when going from - // - other -> other - // - alphabetic -> numeric - // - numeric -> alphabetic - // Never before after opening delimiters. - if end > 0 - && prev != Class::Open - && if class == Class::Other { prev == Class::Other } else { class != prev } - { - let piece = &link[offset..end]; - if piece.len() < 16 { - // For bearably long segments, emit them as one. - offset = end; - f(offset); - } else { - // If it gets very long (e.g. a hash in the URL), just allow a - // break at every char. - for c in piece.chars() { - offset += c.len_utf8(); - f(offset); - } - } - } - - prev = class; - } -} - -/// Whether hyphenation is enabled at the given offset. -fn hyphenate_at(p: &Preparation, offset: usize) -> bool { - p.hyphenate - .or_else(|| { - let shaped = p.find(offset)?.text()?; - Some(TextElem::hyphenate_in(shaped.styles)) - }) - .unwrap_or(false) -} - -/// The text language at the given offset. -fn lang_at(p: &Preparation, offset: usize) -> Option<hypher::Lang> { - let lang = p.lang.or_else(|| { - let shaped = p.find(offset)?.text()?; - Some(TextElem::lang_in(shaped.styles)) - })?; - - let bytes = lang.as_str().as_bytes().try_into().ok()?; - hypher::Lang::from_iso(bytes) -} diff --git a/crates/typst-library/src/text/misc.rs b/crates/typst-library/src/text/misc.rs deleted file mode 100644 index 1bf28a34..00000000 --- a/crates/typst-library/src/text/misc.rs +++ /dev/null @@ -1,315 +0,0 @@ -use crate::prelude::*; -use crate::text::TextElem; - -/// A text space. -#[elem(Behave, Unlabellable, PlainText, Repr)] -pub struct SpaceElem {} - -impl Repr for SpaceElem { - fn repr(&self) -> EcoString { - EcoString::inline("[ ]") - } -} - -impl Behave for SpaceElem { - fn behaviour(&self) -> Behaviour { - Behaviour::Weak(2) - } -} - -impl Unlabellable for SpaceElem {} - -impl PlainText for SpaceElem { - fn plain_text(&self, text: &mut EcoString) { - text.push(' '); - } -} - -/// Inserts a line break. -/// -/// Advances the paragraph to the next line. A single trailing line break at the -/// end of a paragraph is ignored, but more than one creates additional empty -/// lines. -/// -/// # Example -/// ```example -/// *Date:* 26.12.2022 \ -/// *Topic:* Infrastructure Test \ -/// *Severity:* High \ -/// ``` -/// -/// # Syntax -/// This function also has dedicated syntax: To insert a line break, simply write -/// a backslash followed by whitespace. This always creates an unjustified -/// break. -#[elem(title = "Line Break", Behave)] -pub struct LinebreakElem { - /// Whether to justify the line before the break. - /// - /// This is useful if you found a better line break opportunity in your - /// justified text than Typst did. - /// - /// ```example - /// #set par(justify: true) - /// #let jb = linebreak(justify: true) - /// - /// I have manually tuned the #jb - /// line breaks in this paragraph #jb - /// for an _interesting_ result. #jb - /// ``` - #[default(false)] - pub justify: bool, -} - -impl Behave for LinebreakElem { - fn behaviour(&self) -> Behaviour { - Behaviour::Destructive - } -} - -/// Strongly emphasizes content by increasing the font weight. -/// -/// Increases the current font weight by a given `delta`. -/// -/// # Example -/// ```example -/// This is *strong.* \ -/// This is #strong[too.] \ -/// -/// #show strong: set text(red) -/// And this is *evermore.* -/// ``` -/// -/// # Syntax -/// This function also has dedicated syntax: To strongly emphasize content, -/// simply enclose it in stars/asterisks (`*`). Note that this only works at -/// word boundaries. To strongly emphasize part of a word, you have to use the -/// function. -#[elem(title = "Strong Emphasis", Show)] -pub struct StrongElem { - /// The delta to apply on the font weight. - /// - /// ```example - /// #set strong(delta: 0) - /// No *effect!* - /// ``` - #[default(300)] - pub delta: i64, - - /// The content to strongly emphasize. - #[required] - pub body: Content, -} - -impl Show for StrongElem { - #[tracing::instrument(name = "StrongElem::show", skip_all)] - fn show(&self, _: &mut Vt, styles: StyleChain) -> SourceResult<Content> { - Ok(self - .body() - .clone() - .styled(TextElem::set_delta(Delta(self.delta(styles))))) - } -} - -/// A delta that is summed up when folded. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub struct Delta(pub i64); - -cast! { - Delta, - self => self.0.into_value(), - v: i64 => Self(v), -} - -impl Fold for Delta { - type Output = i64; - - fn fold(self, outer: Self::Output) -> Self::Output { - outer + self.0 - } -} - -/// Emphasizes content by setting it in italics. -/// -/// - If the current [text style]($text.style) is `{"normal"}`, this turns it -/// into `{"italic"}`. -/// - If it is already `{"italic"}` or `{"oblique"}`, it turns it back to -/// `{"normal"}`. -/// -/// # Example -/// ```example -/// This is _emphasized._ \ -/// This is #emph[too.] -/// -/// #show emph: it => { -/// text(blue, it.body) -/// } -/// -/// This is _emphasized_ differently. -/// ``` -/// -/// # Syntax -/// This function also has dedicated syntax: To emphasize content, simply -/// enclose it in underscores (`_`). Note that this only works at word -/// boundaries. To emphasize part of a word, you have to use the function. -#[elem(title = "Emphasis", Show)] -pub struct EmphElem { - /// The content to emphasize. - #[required] - pub body: Content, -} - -impl Show for EmphElem { - #[tracing::instrument(name = "EmphElem::show", skip(self))] - fn show(&self, _: &mut Vt, _: StyleChain) -> SourceResult<Content> { - Ok(self.body().clone().styled(TextElem::set_emph(Toggle))) - } -} - -/// A toggle that turns on and off alternatingly if folded. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub struct Toggle; - -cast! { - Toggle, - self => Value::None, - _: Value => Self, -} - -impl Fold for Toggle { - type Output = bool; - - fn fold(self, outer: Self::Output) -> Self::Output { - !outer - } -} - -/// Converts text or content to lowercase. -/// -/// # Example -/// ```example -/// #lower("ABC") \ -/// #lower[*My Text*] \ -/// #lower[already low] -/// ``` -#[func(title = "Lowercase")] -pub fn lower( - /// The text to convert to lowercase. - text: Caseable, -) -> Caseable { - case(text, Case::Lower) -} - -/// Converts text or content to uppercase. -/// -/// # Example -/// ```example -/// #upper("abc") \ -/// #upper[*my text*] \ -/// #upper[ALREADY HIGH] -/// ``` -#[func(title = "Uppercase")] -pub fn upper( - /// The text to convert to uppercase. - text: Caseable, -) -> Caseable { - case(text, Case::Upper) -} - -/// Change the case of text. -fn case(text: Caseable, case: Case) -> Caseable { - match text { - Caseable::Str(v) => Caseable::Str(case.apply(&v).into()), - Caseable::Content(v) => { - Caseable::Content(v.styled(TextElem::set_case(Some(case)))) - } - } -} - -/// A value whose case can be changed. -pub enum Caseable { - Str(Str), - Content(Content), -} - -cast! { - Caseable, - self => match self { - Self::Str(v) => v.into_value(), - Self::Content(v) => v.into_value(), - }, - v: Str => Self::Str(v), - v: Content => Self::Content(v), -} - -/// A case transformation on text. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)] -pub enum Case { - /// Everything is lowercased. - Lower, - /// Everything is uppercased. - Upper, -} - -impl Case { - /// Apply the case to a string. - pub fn apply(self, text: &str) -> String { - match self { - Self::Lower => text.to_lowercase(), - Self::Upper => text.to_uppercase(), - } - } -} - -/// Displays text in small capitals. -/// -/// _Note:_ This enables the OpenType `smcp` feature for the font. Not all fonts -/// support this feature. Sometimes smallcaps are part of a dedicated font and -/// sometimes they are not available at all. In the future, this function will -/// support selecting a dedicated smallcaps font as well as synthesizing -/// smallcaps from normal letters, but this is not yet implemented. -/// -/// # Example -/// ```example -/// #set par(justify: true) -/// #set heading(numbering: "I.") -/// -/// #show heading: it => { -/// set block(below: 10pt) -/// set text(weight: "regular") -/// align(center, smallcaps(it)) -/// } -/// -/// = Introduction -/// #lorem(40) -/// ``` -#[func(title = "Small Capitals")] -pub fn smallcaps( - /// The text to display to small capitals. - body: Content, -) -> Content { - body.styled(TextElem::set_smallcaps(true)) -} - -/// Creates blind text. -/// -/// This function yields a Latin-like _Lorem Ipsum_ blind text with the given -/// number of words. The sequence of words generated by the function is always -/// the same but randomly chosen. As usual for blind texts, it does not make any -/// sense. Use it as a placeholder to try layouts. -/// -/// # Example -/// ```example -/// = Blind Text -/// #lorem(30) -/// -/// = More Blind Text -/// #lorem(15) -/// ``` -#[func(keywords = ["Blind Text"])] -pub fn lorem( - /// The length of the blind text in words. - words: usize, -) -> Str { - lipsum::lipsum(words).replace("--", "–").into() -} diff --git a/crates/typst-library/src/text/mod.rs b/crates/typst-library/src/text/mod.rs deleted file mode 100644 index 202ab2c5..00000000 --- a/crates/typst-library/src/text/mod.rs +++ /dev/null @@ -1,992 +0,0 @@ -//! Text handling. - -mod deco; -mod linebreak; -mod misc; -mod quote; -mod quotes; -mod raw; -mod shaping; -mod shift; - -pub use self::deco::*; -pub(crate) use self::linebreak::*; -pub use self::misc::*; -pub use self::quote::*; -pub use self::quotes::*; -pub use self::raw::*; -pub use self::shaping::*; -pub use self::shift::*; - -use rustybuzz::Tag; -use ttf_parser::Rect; -use typst::diag::{bail, error, SourceResult}; -use typst::eval::Never; -use typst::font::{Font, FontStretch, FontStyle, FontWeight, VerticalFontMetric}; - -use crate::layout::ParElem; -use crate::prelude::*; - -/// Hook up all text definitions. -pub(super) fn define(global: &mut Scope) { - global.category("text"); - global.define_elem::<TextElem>(); - global.define_elem::<LinebreakElem>(); - global.define_elem::<SmartquoteElem>(); - global.define_elem::<StrongElem>(); - global.define_elem::<EmphElem>(); - global.define_elem::<SubElem>(); - global.define_elem::<SuperElem>(); - global.define_elem::<UnderlineElem>(); - global.define_elem::<OverlineElem>(); - global.define_elem::<StrikeElem>(); - global.define_elem::<HighlightElem>(); - global.define_elem::<QuoteElem>(); - global.define_elem::<RawElem>(); - global.define_func::<lower>(); - global.define_func::<upper>(); - global.define_func::<smallcaps>(); - global.define_func::<lorem>(); -} - -/// Customizes the look and layout of text in a variety of ways. -/// -/// This function is used frequently, both with set rules and directly. While -/// the set rule is often the simpler choice, calling the `text` function -/// directly can be useful when passing text as an argument to another function. -/// -/// # Example -/// ```example -/// #set text(18pt) -/// With a set rule. -/// -/// #emph(text(blue)[ -/// With a function call. -/// ]) -/// ``` -#[elem(Construct, PlainText, Repr)] -pub struct TextElem { - /// A font family name or priority list of font family names. - /// - /// When processing text, Typst tries all specified font families in order - /// until it finds a font that has the necessary glyphs. In the example - /// below, the font `Inria Serif` is preferred, but since it does not - /// contain Arabic glyphs, the arabic text uses `Noto Sans Arabic` instead. - /// - /// The collection of available fonts differs by platform: - /// - /// - In the web app, you can see the list of available fonts by clicking on - /// the "Ag" button. You can provide additional fonts by uploading `.ttf` - /// or `.otf` files into your project. They will be discovered - /// automatically. - /// - /// - Locally, Typst uses your installed system fonts. In addition, you can - /// use the `--font-path` argument or `TYPST_FONT_PATHS` environment - /// variable to add directories that should be scanned for fonts. - /// - /// ```example - /// #set text(font: "PT Sans") - /// This is sans-serif. - /// - /// #set text(font: ( - /// "Inria Serif", - /// "Noto Sans Arabic", - /// )) - /// - /// This is Latin. \ - /// هذا عربي. - /// ``` - #[default(FontList(vec![FontFamily::new("Linux Libertine")]))] - #[borrowed] - #[ghost] - pub font: FontList, - - /// Whether to allow last resort font fallback when the primary font list - /// contains no match. This lets Typst search through all available fonts - /// for the most similar one that has the necessary glyphs. - /// - /// _Note:_ Currently, there are no warnings when fallback is disabled and - /// no glyphs are found. Instead, your text shows up in the form of "tofus": - /// Small boxes that indicate the lack of an appropriate glyph. In the - /// future, you will be able to instruct Typst to issue warnings so you know - /// something is up. - /// - /// ```example - /// #set text(font: "Inria Serif") - /// هذا عربي - /// - /// #set text(fallback: false) - /// هذا عربي - /// ``` - #[default(true)] - #[ghost] - pub fallback: bool, - - /// The desired font style. - /// - /// When an italic style is requested and only an oblique one is available, - /// it is used. Similarly, the other way around, an italic style can stand - /// in for an oblique one. When neither an italic nor an oblique style is - /// available, Typst selects the normal style. Since most fonts are only - /// available either in an italic or oblique style, the difference between - /// italic and oblique style is rarely observable. - /// - /// If you want to emphasize your text, you should do so using the - /// [emph]($emph) function instead. This makes it easy to adapt the style - /// later if you change your mind about how to signify the emphasis. - /// - /// ```example - /// #text(font: "Linux Libertine", style: "italic")[Italic] - /// #text(font: "DejaVu Sans", style: "oblique")[Oblique] - /// ``` - #[ghost] - pub style: FontStyle, - - /// The desired thickness of the font's glyphs. Accepts an integer between - /// `{100}` and `{900}` or one of the predefined weight names. When the - /// desired weight is not available, Typst selects the font from the family - /// that is closest in weight. - /// - /// If you want to strongly emphasize your text, you should do so using the - /// [strong]($strong) function instead. This makes it easy to adapt the - /// style later if you change your mind about how to signify the strong - /// emphasis. - /// - /// ```example - /// #set text(font: "IBM Plex Sans") - /// - /// #text(weight: "light")[Light] \ - /// #text(weight: "regular")[Regular] \ - /// #text(weight: "medium")[Medium] \ - /// #text(weight: 500)[Medium] \ - /// #text(weight: "bold")[Bold] - /// ``` - #[ghost] - pub weight: FontWeight, - - /// The desired width of the glyphs. Accepts a ratio between `{50%}` and - /// `{200%}`. When the desired width is not available, Typst selects the - /// font from the family that is closest in stretch. This will only stretch - /// the text if a condensed or expanded version of the font is available. - /// - /// If you want to adjust the amount of space between characters instead of - /// stretching the glyphs itself, use the [`tracking`]($text.tracking) - /// property instead. - /// - /// ```example - /// #text(stretch: 75%)[Condensed] \ - /// #text(stretch: 100%)[Normal] - /// ``` - #[ghost] - pub stretch: FontStretch, - - /// The size of the glyphs. This value forms the basis of the `em` unit: - /// `{1em}` is equivalent to the font size. - /// - /// You can also give the font size itself in `em` units. Then, it is - /// relative to the previous font size. - /// - /// ```example - /// #set text(size: 20pt) - /// very #text(1.5em)[big] text - /// ``` - #[parse(args.named_or_find("size")?)] - #[fold] - #[default(Abs::pt(11.0))] - #[ghost] - pub size: TextSize, - - /// The glyph fill paint. - /// - /// ```example - /// #set text(fill: red) - /// This text is red. - /// ``` - #[parse({ - let paint: Option<Spanned<Paint>> = args.named_or_find("fill")?; - if let Some(paint) = &paint { - if let Paint::Gradient(gradient) = &paint.v { - if gradient.relative() == Smart::Custom(Relative::Self_) { - bail!( - error!( - paint.span, - "gradients on text must be relative to the parent" - ) - .with_hint("make sure to set `relative: auto` on your text fill") - ); - } - } - } - paint.map(|paint| paint.v) - })] - #[default(Color::BLACK.into())] - #[ghost] - pub fill: Paint, - - /// The amount of space that should be added between characters. - /// - /// ```example - /// #set text(tracking: 1.5pt) - /// Distant text. - /// ``` - #[resolve] - #[ghost] - pub tracking: Length, - - /// The amount of space between words. - /// - /// Can be given as an absolute length, but also relative to the width of - /// the space character in the font. - /// - /// If you want to adjust the amount of space between characters rather than - /// words, use the [`tracking`]($text.tracking) property instead. - /// - /// ```example - /// #set text(spacing: 200%) - /// Text with distant words. - /// ``` - #[resolve] - #[default(Rel::one())] - #[ghost] - pub spacing: Rel<Length>, - - /// Whether to automatically insert spacing between CJK and Latin characters. - /// - /// ```example - /// #set text(cjk-latin-spacing: auto) - /// 第4章介绍了基本的API。 - /// - /// #set text(cjk-latin-spacing: none) - /// 第4章介绍了基本的API。 - /// ``` - #[ghost] - pub cjk_latin_spacing: Smart<Option<Never>>, - - /// An amount to shift the text baseline by. - /// - /// ```example - /// A #text(baseline: 3pt)[lowered] - /// word. - /// ``` - #[resolve] - #[ghost] - pub baseline: Length, - - /// Whether certain glyphs can hang over into the margin in justified text. - /// This can make justification visually more pleasing. - /// - /// ```example - /// #set par(justify: true) - /// This justified text has a hyphen in - /// the paragraph's first line. Hanging - /// the hyphen slightly into the margin - /// results in a clearer paragraph edge. - /// - /// #set text(overhang: false) - /// This justified text has a hyphen in - /// the paragraph's first line. Hanging - /// the hyphen slightly into the margin - /// results in a clearer paragraph edge. - /// ``` - #[default(true)] - #[ghost] - pub overhang: bool, - - /// The top end of the conceptual frame around the text used for layout and - /// positioning. This affects the size of containers that hold text. - /// - /// ```example - /// #set rect(inset: 0pt) - /// #set text(size: 20pt) - /// - /// #set text(top-edge: "ascender") - /// #rect(fill: aqua)[Typst] - /// - /// #set text(top-edge: "cap-height") - /// #rect(fill: aqua)[Typst] - /// ``` - #[default(TopEdge::Metric(TopEdgeMetric::CapHeight))] - #[ghost] - pub top_edge: TopEdge, - - /// The bottom end of the conceptual frame around the text used for layout - /// and positioning. This affects the size of containers that hold text. - /// - /// ```example - /// #set rect(inset: 0pt) - /// #set text(size: 20pt) - /// - /// #set text(bottom-edge: "baseline") - /// #rect(fill: aqua)[Typst] - /// - /// #set text(bottom-edge: "descender") - /// #rect(fill: aqua)[Typst] - /// ``` - #[default(BottomEdge::Metric(BottomEdgeMetric::Baseline))] - #[ghost] - pub bottom_edge: BottomEdge, - - /// An [ISO 639-1/2/3 language code.](https://en.wikipedia.org/wiki/ISO_639) - /// - /// Setting the correct language affects various parts of Typst: - /// - /// - The text processing pipeline can make more informed choices. - /// - Hyphenation will use the correct patterns for the language. - /// - [Smart quotes]($smartquote) turns into the correct quotes for the - /// language. - /// - And all other things which are language-aware. - /// - /// ```example - /// #set text(lang: "de") - /// #outline() - /// - /// = Einleitung - /// In diesem Dokument, ... - /// ``` - #[default(Lang::ENGLISH)] - #[ghost] - pub lang: Lang, - - /// An [ISO 3166-1 alpha-2 region code.](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) - /// - /// This lets the text processing pipeline make more informed choices. - #[ghost] - pub region: Option<Region>, - - /// The OpenType writing script. - /// - /// The combination of `{lang}` and `{script}` determine how font features, - /// such as glyph substitution, are implemented. Frequently the value is a - /// modified (all-lowercase) ISO 15924 script identifier, and the `math` - /// writing script is used for features appropriate for mathematical - /// symbols. - /// - /// When set to `{auto}`, the default and recommended setting, an - /// appropriate script is chosen for each block of characters sharing a - /// common Unicode script property. - /// - /// ```example - /// #set text( - /// font: "Linux Libertine", - /// size: 20pt, - /// ) - /// - /// #let scedilla = [Ş] - /// #scedilla // S with a cedilla - /// - /// #set text(lang: "ro", script: "latn") - /// #scedilla // S with a subscript comma - /// - /// #set text(lang: "ro", script: "grek") - /// #scedilla // S with a cedilla - /// ``` - #[ghost] - pub script: Smart<WritingScript>, - - /// The dominant direction for text and inline objects. Possible values are: - /// - /// - `{auto}`: Automatically infer the direction from the `lang` property. - /// - `{ltr}`: Layout text from left to right. - /// - `{rtl}`: Layout text from right to left. - /// - /// When writing in right-to-left scripts like Arabic or Hebrew, you should - /// set the [text language]($text.lang) or direction. While individual runs - /// of text are automatically layouted in the correct direction, setting the - /// dominant direction gives the bidirectional reordering algorithm the - /// necessary information to correctly place punctuation and inline objects. - /// Furthermore, setting the direction affects the alignment values `start` - /// and `end`, which are equivalent to `left` and `right` in `ltr` text and - /// the other way around in `rtl` text. - /// - /// If you set this to `rtl` and experience bugs or in some way bad looking - /// output, please do get in touch with us through the - /// [contact form](https://typst.app/contact) or our - /// [Discord server]($community/#discord)! - /// - /// ```example - /// #set text(dir: rtl) - /// هذا عربي. - /// ``` - #[resolve] - #[ghost] - pub dir: TextDir, - - /// Whether to hyphenate text to improve line breaking. When `{auto}`, text - /// will be hyphenated if and only if justification is enabled. - /// - /// Setting the [text language]($text.lang) ensures that the correct - /// hyphenation patterns are used. - /// - /// ```example - /// #set page(width: 200pt) - /// - /// #set par(justify: true) - /// This text illustrates how - /// enabling hyphenation can - /// improve justification. - /// - /// #set text(hyphenate: false) - /// This text illustrates how - /// enabling hyphenation can - /// improve justification. - /// ``` - #[resolve] - #[ghost] - pub hyphenate: Hyphenate, - - /// Whether to apply kerning. - /// - /// When enabled, specific letter pairings move closer together or further - /// apart for a more visually pleasing result. The example below - /// demonstrates how decreasing the gap between the "T" and "o" results in a - /// more natural look. Setting this to `{false}` disables kerning by turning - /// off the OpenType `kern` font feature. - /// - /// ```example - /// #set text(size: 25pt) - /// Totally - /// - /// #set text(kerning: false) - /// Totally - /// ``` - #[default(true)] - #[ghost] - pub kerning: bool, - - /// Whether to apply stylistic alternates. - /// - /// Sometimes fonts contain alternative glyphs for the same codepoint. - /// Setting this to `{true}` switches to these by enabling the OpenType - /// `salt` font feature. - /// - /// ```example - /// #set text( - /// font: "IBM Plex Sans", - /// size: 20pt, - /// ) - /// - /// 0, a, g, ß - /// - /// #set text(alternates: true) - /// 0, a, g, ß - /// ``` - #[default(false)] - #[ghost] - pub alternates: bool, - - /// Which stylistic set to apply. Font designers can categorize alternative - /// glyphs forms into stylistic sets. As this value is highly font-specific, - /// you need to consult your font to know which sets are available. When set - /// to an integer between `{1}` and `{20}`, enables the corresponding - /// OpenType font feature from `ss01`, ..., `ss20`. - #[ghost] - pub stylistic_set: Option<StylisticSet>, - - /// Whether standard ligatures are active. - /// - /// Certain letter combinations like "fi" are often displayed as a single - /// merged glyph called a _ligature._ Setting this to `{false}` disables - /// these ligatures by turning off the OpenType `liga` and `clig` font - /// features. - /// - /// ```example - /// #set text(size: 20pt) - /// A fine ligature. - /// - /// #set text(ligatures: false) - /// A fine ligature. - /// ``` - #[default(true)] - #[ghost] - pub ligatures: bool, - - /// Whether ligatures that should be used sparingly are active. Setting this - /// to `{true}` enables the OpenType `dlig` font feature. - #[default(false)] - #[ghost] - pub discretionary_ligatures: bool, - - /// Whether historical ligatures are active. Setting this to `{true}` - /// enables the OpenType `hlig` font feature. - #[default(false)] - #[ghost] - pub historical_ligatures: bool, - - /// Which kind of numbers / figures to select. When set to `{auto}`, the - /// default numbers for the font are used. - /// - /// ```example - /// #set text(font: "Noto Sans", 20pt) - /// #set text(number-type: "lining") - /// Number 9. - /// - /// #set text(number-type: "old-style") - /// Number 9. - /// ``` - #[ghost] - pub number_type: Smart<NumberType>, - - /// The width of numbers / figures. When set to `{auto}`, the default - /// numbers for the font are used. - /// - /// ```example - /// #set text(font: "Noto Sans", 20pt) - /// #set text(number-width: "proportional") - /// A 12 B 34. \ - /// A 56 B 78. - /// - /// #set text(number-width: "tabular") - /// A 12 B 34. \ - /// A 56 B 78. - /// ``` - #[ghost] - pub number_width: Smart<NumberWidth>, - - /// Whether to have a slash through the zero glyph. Setting this to `{true}` - /// enables the OpenType `zero` font feature. - /// - /// ```example - /// 0, #text(slashed-zero: true)[0] - /// ``` - #[default(false)] - #[ghost] - pub slashed_zero: bool, - - /// Whether to turn numbers into fractions. Setting this to `{true}` - /// enables the OpenType `frac` font feature. - /// - /// It is not advisable to enable this property globally as it will mess - /// with all appearances of numbers after a slash (e.g., in URLs). Instead, - /// enable it locally when you want a fraction. - /// - /// ```example - /// 1/2 \ - /// #text(fractions: true)[1/2] - /// ``` - #[default(false)] - #[ghost] - pub fractions: bool, - - /// Raw OpenType features to apply. - /// - /// - If given an array of strings, sets the features identified by the - /// strings to `{1}`. - /// - If given a dictionary mapping to numbers, sets the features - /// identified by the keys to the values. - /// - /// ```example - /// // Enable the `frac` feature manually. - /// #set text(features: ("frac",)) - /// 1/2 - /// ``` - #[fold] - #[ghost] - pub features: FontFeatures, - - /// Content in which all text is styled according to the other arguments. - #[external] - #[required] - pub body: Content, - - /// The text. - #[required] - #[variant(0)] - pub text: EcoString, - - /// A delta to apply on the font weight. - #[internal] - #[fold] - #[ghost] - pub delta: Delta, - - /// Whether the font style should be inverted. - #[internal] - #[fold] - #[default(false)] - #[ghost] - pub emph: Toggle, - - /// Decorative lines. - #[internal] - #[fold] - #[ghost] - pub deco: Decoration, - - /// A case transformation that should be applied to the text. - #[internal] - #[ghost] - pub case: Option<Case>, - - /// Whether small capital glyphs should be used. ("smcp") - #[internal] - #[default(false)] - #[ghost] - pub smallcaps: bool, -} - -impl TextElem { - /// Create a new packed text element. - pub fn packed(text: impl Into<EcoString>) -> Content { - Self::new(text.into()).pack() - } -} - -impl Repr for TextElem { - fn repr(&self) -> EcoString { - eco_format!("[{}]", self.text) - } -} - -impl Construct for TextElem { - fn construct(vm: &mut Vm, args: &mut Args) -> SourceResult<Content> { - // The text constructor is special: It doesn't create a text element. - // Instead, it leaves the passed argument structurally unchanged, but - // styles all text in it. - let styles = Self::set(vm, args)?; - let body = args.expect::<Content>("body")?; - Ok(body.styled_with_map(styles)) - } -} - -impl PlainText for TextElem { - fn plain_text(&self, text: &mut EcoString) { - text.push_str(self.text()); - } -} - -/// A lowercased font family like "arial". -#[derive(Clone, Eq, PartialEq, Hash)] -pub struct FontFamily(EcoString); - -impl FontFamily { - /// Create a named font family variant. - pub fn new(string: &str) -> Self { - Self(string.to_lowercase().into()) - } - - /// The lowercased family name. - pub fn as_str(&self) -> &str { - &self.0 - } -} - -impl Debug for FontFamily { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - self.0.fmt(f) - } -} - -cast! { - FontFamily, - self => self.0.into_value(), - string: EcoString => Self::new(&string), -} - -/// Font family fallback list. -#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)] -pub struct FontList(pub Vec<FontFamily>); - -impl<'a> IntoIterator for &'a FontList { - type IntoIter = std::slice::Iter<'a, FontFamily>; - type Item = &'a FontFamily; - - fn into_iter(self) -> Self::IntoIter { - self.0.iter() - } -} - -cast! { - FontList, - self => if self.0.len() == 1 { - self.0.into_iter().next().unwrap().0.into_value() - } else { - self.0.into_value() - }, - family: FontFamily => Self(vec![family]), - values: Array => Self(values.into_iter().map(|v| v.cast()).collect::<StrResult<_>>()?), -} - -/// The size of text. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub struct TextSize(pub Length); - -impl Fold for TextSize { - type Output = Abs; - - fn fold(self, outer: Self::Output) -> Self::Output { - self.0.em.at(outer) + self.0.abs - } -} - -cast! { - TextSize, - self => self.0.into_value(), - v: Length => Self(v), -} - -/// Specifies the top edge of text. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub enum TopEdge { - /// An edge specified via font metrics or bounding box. - Metric(TopEdgeMetric), - /// An edge specified as a length. - Length(Length), -} - -impl TopEdge { - /// Determine if the edge is specified from bounding box info. - pub fn is_bounds(&self) -> bool { - matches!(self, Self::Metric(TopEdgeMetric::Bounds)) - } - - /// Resolve the value of the text edge given a font's metrics. - pub fn resolve(self, font_size: Abs, font: &Font, bbox: Option<Rect>) -> Abs { - match self { - TopEdge::Metric(metric) => { - if let Ok(metric) = metric.try_into() { - font.metrics().vertical(metric).at(font_size) - } else { - bbox.map(|bbox| (font.to_em(bbox.y_max)).at(font_size)) - .unwrap_or_default() - } - } - TopEdge::Length(length) => length.at(font_size), - } - } -} - -cast! { - TopEdge, - self => match self { - Self::Metric(metric) => metric.into_value(), - Self::Length(length) => length.into_value(), - }, - v: TopEdgeMetric => Self::Metric(v), - v: Length => Self::Length(v), -} - -/// Metrics that describe the top edge of text. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)] -pub enum TopEdgeMetric { - /// The font's ascender, which typically exceeds the height of all glyphs. - Ascender, - /// The approximate height of uppercase letters. - CapHeight, - /// The approximate height of non-ascending lowercase letters. - XHeight, - /// The baseline on which the letters rest. - Baseline, - /// The top edge of the glyph's bounding box. - Bounds, -} - -impl TryInto<VerticalFontMetric> for TopEdgeMetric { - type Error = (); - - fn try_into(self) -> Result<VerticalFontMetric, Self::Error> { - match self { - Self::Ascender => Ok(VerticalFontMetric::Ascender), - Self::CapHeight => Ok(VerticalFontMetric::CapHeight), - Self::XHeight => Ok(VerticalFontMetric::XHeight), - Self::Baseline => Ok(VerticalFontMetric::Baseline), - _ => Err(()), - } - } -} - -/// Specifies the top edge of text. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub enum BottomEdge { - /// An edge specified via font metrics or bounding box. - Metric(BottomEdgeMetric), - /// An edge specified as a length. - Length(Length), -} - -impl BottomEdge { - /// Determine if the edge is specified from bounding box info. - pub fn is_bounds(&self) -> bool { - matches!(self, Self::Metric(BottomEdgeMetric::Bounds)) - } - - /// Resolve the value of the text edge given a font's metrics. - pub fn resolve(self, font_size: Abs, font: &Font, bbox: Option<Rect>) -> Abs { - match self { - BottomEdge::Metric(metric) => { - if let Ok(metric) = metric.try_into() { - font.metrics().vertical(metric).at(font_size) - } else { - bbox.map(|bbox| (font.to_em(bbox.y_min)).at(font_size)) - .unwrap_or_default() - } - } - BottomEdge::Length(length) => length.at(font_size), - } - } -} - -cast! { - BottomEdge, - self => match self { - Self::Metric(metric) => metric.into_value(), - Self::Length(length) => length.into_value(), - }, - v: BottomEdgeMetric => Self::Metric(v), - v: Length => Self::Length(v), -} - -/// Metrics that describe the bottom edge of text. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)] -pub enum BottomEdgeMetric { - /// The baseline on which the letters rest. - Baseline, - /// The font's descender, which typically exceeds the depth of all glyphs. - Descender, - /// The bottom edge of the glyph's bounding box. - Bounds, -} - -impl TryInto<VerticalFontMetric> for BottomEdgeMetric { - type Error = (); - - fn try_into(self) -> Result<VerticalFontMetric, Self::Error> { - match self { - Self::Baseline => Ok(VerticalFontMetric::Baseline), - Self::Descender => Ok(VerticalFontMetric::Descender), - _ => Err(()), - } - } -} - -/// The direction of text and inline objects in their line. -#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash)] -pub struct TextDir(pub Smart<Dir>); - -cast! { - TextDir, - self => self.0.into_value(), - v: Smart<Dir> => { - if v.map_or(false, |dir| dir.axis() == Axis::Y) { - bail!("text direction must be horizontal"); - } - Self(v) - }, -} - -impl Resolve for TextDir { - type Output = Dir; - - fn resolve(self, styles: StyleChain) -> Self::Output { - match self.0 { - Smart::Auto => TextElem::lang_in(styles).dir(), - Smart::Custom(dir) => dir, - } - } -} - -/// Whether to hyphenate text. -#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash)] -pub struct Hyphenate(pub Smart<bool>); - -cast! { - Hyphenate, - self => self.0.into_value(), - v: Smart<bool> => Self(v), -} - -impl Resolve for Hyphenate { - type Output = bool; - - fn resolve(self, styles: StyleChain) -> Self::Output { - match self.0 { - Smart::Auto => ParElem::justify_in(styles), - Smart::Custom(v) => v, - } - } -} - -/// A stylistic set in a font. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub struct StylisticSet(u8); - -impl StylisticSet { - /// Create a new set, clamping to 1-20. - pub fn new(index: u8) -> Self { - Self(index.clamp(1, 20)) - } - - /// Get the value, guaranteed to be 1-20. - pub fn get(self) -> u8 { - self.0 - } -} - -cast! { - StylisticSet, - self => self.0.into_value(), - v: i64 => match v { - 1 ..= 20 => Self::new(v as u8), - _ => bail!("stylistic set must be between 1 and 20"), - }, -} - -/// Which kind of numbers / figures to select. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)] -pub enum NumberType { - /// Numbers that fit well with capital text (the OpenType `lnum` - /// font feature). - Lining, - /// Numbers that fit well into a flow of upper- and lowercase text (the - /// OpenType `onum` font feature). - OldStyle, -} - -/// The width of numbers / figures. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)] -pub enum NumberWidth { - /// Numbers with glyph-specific widths (the OpenType `pnum` font feature). - Proportional, - /// Numbers of equal width (the OpenType `tnum` font feature). - Tabular, -} - -/// OpenType font features settings. -#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)] -pub struct FontFeatures(pub Vec<(Tag, u32)>); - -cast! { - FontFeatures, - self => self.0 - .into_iter() - .map(|(tag, num)| { - let bytes = tag.to_bytes(); - let key = std::str::from_utf8(&bytes).unwrap_or_default(); - (key.into(), num.into_value()) - }) - .collect::<Dict>() - .into_value(), - values: Array => Self(values - .into_iter() - .map(|v| { - let tag = v.cast::<EcoString>()?; - Ok((Tag::from_bytes_lossy(tag.as_bytes()), 1)) - }) - .collect::<StrResult<_>>()?), - values: Dict => Self(values - .into_iter() - .map(|(k, v)| { - let num = v.cast::<u32>()?; - let tag = Tag::from_bytes_lossy(k.as_bytes()); - Ok((tag, num)) - }) - .collect::<StrResult<_>>()?), -} - -impl Fold for FontFeatures { - type Output = Self; - - fn fold(mut self, outer: Self::Output) -> Self::Output { - self.0.extend(outer.0); - self - } -} diff --git a/crates/typst-library/src/text/quote.rs b/crates/typst-library/src/text/quote.rs deleted file mode 100644 index 8d7bd15b..00000000 --- a/crates/typst-library/src/text/quote.rs +++ /dev/null @@ -1,208 +0,0 @@ -use crate::layout::{BlockElem, HElem, PadElem, Spacing, VElem}; -use crate::meta::{CitationForm, CiteElem}; -use crate::prelude::*; -use crate::text::{SmartquoteElem, SpaceElem, TextElem}; - -/// Displays a quote alongside an optional attribution. -/// -/// # Example -/// ```example -/// Plato is often misquoted as the author of #quote[I know that I know -/// nothing], however, this is a derivation form his original quote: -/// -/// #set quote(block: true) -/// -/// #quote(attribution: [Plato])[ -/// ... ἔοικα γοῦν τούτου γε σμικρῷ τινι αὐτῷ τούτῳ σοφώτερος εἶναι, ὅτι -/// ἃ μὴ οἶδα οὐδὲ οἴομαι εἰδέναι. -/// ] -/// #quote(attribution: [from the Henry Cary literal translation of 1897])[ -/// ... I seem, then, in just this little thing to be wiser than this man at -/// any rate, that what I do not know I do not think I know either. -/// ] -/// ``` -/// -/// By default block quotes are padded left and right by `{1em}`, alignment and -/// padding can be controlled with show rules: -/// ```example -/// #set quote(block: true) -/// #show quote: set align(center) -/// #show quote: set pad(x: 5em) -/// -/// #quote[ -/// You cannot pass... I am a servant of the Secret Fire, wielder of the -/// flame of Anor. You cannot pass. The dark fire will not avail you, -/// flame of Udûn. Go back to the Shadow! You cannot pass. -/// ] -/// ``` -#[elem(Finalize, Show, Synthesize)] -pub struct QuoteElem { - /// Whether this is a block quote. - /// - /// ```example - /// An inline citation would look like - /// this: #quote( - /// attribution: [René Descartes] - /// )[ - /// cogito, ergo sum - /// ], and a block equation like this: - /// #quote( - /// block: true, - /// attribution: [JFK] - /// )[ - /// Ich bin ein Berliner. - /// ] - /// ``` - block: bool, - - /// Whether double quotes should be added around this quote. - /// - /// The double quotes used are inferred from the `quotes` property on - /// [smartquote]($smartquote), which is affected by the `lang` property on - /// [text]($text). - /// - /// - `{true}`: Wrap this quote in double quotes. - /// - `{false}`: Do not wrap this quote in double quotes. - /// - `{auto}`: Infer whether to wrap this quote in double quotes based on - /// the `block` property. If `block` is `{false}`, double quotes are - /// automatically added. - /// - /// ```example - /// #set text(lang: "de") - /// - /// Ein deutsch-sprechender Author - /// zitiert unter umständen JFK: - /// #quote[Ich bin ein Berliner.] - /// - /// #set text(lang: "en") - /// - /// And an english speaking one may - /// translate the quote: - /// #quote[I am a Berliner.] - /// ``` - quotes: Smart<bool>, - - /// The attribution of this quote, usually the author or source. Can be a - /// label pointing to a bibliography entry or any content. By default only - /// displayed for block quotes, but can be changed using a `{show}` rule. - /// - /// ```example - /// #quote(attribution: [René Descartes])[ - /// cogito, ergo sum - /// ] - /// - /// #show quote.where(block: false): it => { - /// ["] + h(0pt, weak: true) + it.body + h(0pt, weak: true) + ["] - /// if it.attribution != none [ (#it.attribution)] - /// } - /// - /// #quote( - /// attribution: link("https://typst.app/home")[typst.com] - /// )[ - /// Compose papers faster - /// ] - /// - /// #set quote(block: true) - /// - /// #quote(attribution: <tolkien54>)[ - /// You cannot pass... I am a servant - /// of the Secret Fire, wielder of the - /// flame of Anor. You cannot pass. The - /// dark fire will not avail you, flame - /// of Udûn. Go back to the Shadow! You - /// cannot pass. - /// ] - /// - /// #bibliography("works.bib", style: "apa") - /// ``` - #[borrowed] - attribution: Option<Attribution>, - - /// The quote. - #[required] - body: Content, -} - -#[derive(Debug, Clone, PartialEq, Hash)] -pub enum Attribution { - Content(Content), - Label(Label), -} - -cast! { - Attribution, - self => match self { - Self::Content(content) => content.into_value(), - Self::Label(label) => label.into_value(), - }, - content: Content => Self::Content(content), - label: Label => Self::Label(label), -} - -impl Synthesize for QuoteElem { - fn synthesize(&mut self, _: &mut Vt, styles: StyleChain) -> SourceResult<()> { - self.push_block(self.block(styles)); - self.push_quotes(self.quotes(styles)); - Ok(()) - } -} - -impl Show for QuoteElem { - fn show(&self, _: &mut Vt, styles: StyleChain) -> SourceResult<Content> { - let mut realized = self.body().clone(); - let block = self.block(styles); - - if self.quotes(styles) == Smart::Custom(true) || !block { - // Add zero-width weak spacing to make the quotes "sticky". - let hole = HElem::hole().pack(); - let quote = SmartquoteElem::new().with_double(true).pack(); - realized = - Content::sequence([quote.clone(), hole.clone(), realized, hole, quote]); - } - - if block { - realized = BlockElem::new().with_body(Some(realized)).pack(); - - if let Some(attribution) = self.attribution(styles).as_ref() { - let mut seq = vec![TextElem::packed('—'), SpaceElem::new().pack()]; - - match attribution { - Attribution::Content(content) => { - seq.push(content.clone()); - } - Attribution::Label(label) => { - seq.push( - CiteElem::new(*label) - .with_form(Some(CitationForm::Prose)) - .pack(), - ); - } - } - - // Use v(0.9em, weak: true) bring the attribution closer to the - // quote. - let weak_v = VElem::weak(Spacing::Rel(Em::new(0.9).into())).pack(); - realized += weak_v + Content::sequence(seq).aligned(Align::END); - } - - realized = PadElem::new(realized).pack(); - } else if let Some(Attribution::Label(label)) = self.attribution(styles) { - realized += SpaceElem::new().pack() + CiteElem::new(*label).pack(); - } - - Ok(realized) - } -} - -impl Finalize for QuoteElem { - fn finalize(&self, realized: Content, _: StyleChain) -> Content { - let x = Em::new(1.0).into(); - let above = Em::new(2.4).into(); - let below = Em::new(1.8).into(); - realized - .styled(PadElem::set_left(x)) - .styled(PadElem::set_right(x)) - .styled(BlockElem::set_above(VElem::block_around(above))) - .styled(BlockElem::set_below(VElem::block_around(below))) - } -} diff --git a/crates/typst-library/src/text/quotes.rs b/crates/typst-library/src/text/quotes.rs deleted file mode 100644 index 7a19b9dc..00000000 --- a/crates/typst-library/src/text/quotes.rs +++ /dev/null @@ -1,369 +0,0 @@ -use typst::syntax::is_newline; -use unicode_segmentation::UnicodeSegmentation; - -use crate::prelude::*; - -/// A language-aware quote that reacts to its context. -/// -/// Automatically turns into an appropriate opening or closing quote based on -/// the active [text language]($text.lang). -/// -/// # Example -/// ```example -/// "This is in quotes." -/// -/// #set text(lang: "de") -/// "Das ist in Anführungszeichen." -/// -/// #set text(lang: "fr") -/// "C'est entre guillemets." -/// ``` -/// -/// # Syntax -/// This function also has dedicated syntax: The normal quote characters -/// (`'` and `"`). Typst automatically makes your quotes smart. -#[elem] -pub struct SmartquoteElem { - /// Whether this should be a double quote. - #[default(true)] - pub double: bool, - - /// Whether smart quotes are enabled. - /// - /// To disable smartness for a single quote, you can also escape it with a - /// backslash. - /// - /// ```example - /// #set smartquote(enabled: false) - /// - /// These are "dumb" quotes. - /// ``` - #[default(true)] - pub enabled: bool, - - /// Whether to use alternative quotes. - /// - /// Does nothing for languages that don't have alternative quotes, or if - /// explicit quotes were set. - /// - /// ```example - /// #set text(lang: "de") - /// #set smartquote(alternative: true) - /// - /// "Das ist in anderen Anführungszeichen." - /// ``` - #[default(false)] - pub alternative: bool, - - /// The quotes to use. - /// - /// - When set to `{auto}`, the appropriate single quotes for the - /// [text language]($text.lang) will be used. This is the default. - /// - Custom quotes can be passed as a string, array, or dictionary of either - /// - [string]($str): a string consisting of two characters containing the - /// opening and closing double quotes (characters here refer to Unicode - /// grapheme clusters) - /// - [array]($array): an array containing the opening and closing double - /// quotes - /// - [dictionary]($dictionary): an array containing the double and single - /// quotes, each specified as either `{auto}`, string, or array - /// - /// ```example - /// #set text(lang: "de") - /// 'Das sind normale Anführungszeichen.' - /// - /// #set smartquote(quotes: "()") - /// "Das sind eigene Anführungszeichen." - /// - /// #set smartquote(quotes: (single: ("[[", "]]"), double: auto)) - /// 'Das sind eigene Anführungszeichen.' - /// ``` - #[borrowed] - pub quotes: Smart<QuoteDict>, -} - -/// State machine for smart quote substitution. -#[derive(Debug, Clone)] -pub struct Quoter { - /// How many quotes have been opened. - quote_depth: usize, - /// Whether an opening quote might follow. - expect_opening: bool, - /// Whether the last character was numeric. - last_num: bool, - /// The previous type of quote character, if it was an opening quote. - prev_quote_type: Option<bool>, -} - -impl Quoter { - /// Start quoting. - pub fn new() -> Self { - Self { - quote_depth: 0, - expect_opening: true, - last_num: false, - prev_quote_type: None, - } - } - - /// Process the last seen character. - pub fn last(&mut self, c: char, is_quote: bool) { - self.expect_opening = is_ignorable(c) || is_opening_bracket(c); - self.last_num = c.is_numeric(); - if !is_quote { - self.prev_quote_type = None; - } - } - - /// Process and substitute a quote. - pub fn quote<'a>( - &mut self, - quotes: &Quotes<'a>, - double: bool, - peeked: Option<char>, - ) -> &'a str { - let peeked = peeked.unwrap_or(' '); - let mut expect_opening = self.expect_opening; - if let Some(prev_double) = self.prev_quote_type.take() { - if double != prev_double { - expect_opening = true; - } - } - - if expect_opening { - self.quote_depth += 1; - self.prev_quote_type = Some(double); - quotes.open(double) - } else if self.quote_depth > 0 - && (peeked.is_ascii_punctuation() || is_ignorable(peeked)) - { - self.quote_depth -= 1; - quotes.close(double) - } else if self.last_num { - quotes.prime(double) - } else { - quotes.fallback(double) - } - } -} - -impl Default for Quoter { - fn default() -> Self { - Self::new() - } -} - -fn is_ignorable(c: char) -> bool { - c.is_whitespace() || is_newline(c) -} - -fn is_opening_bracket(c: char) -> bool { - matches!(c, '(' | '{' | '[') -} - -/// Decides which quotes to substitute smart quotes with. -pub struct Quotes<'s> { - /// The opening single quote. - pub single_open: &'s str, - /// The closing single quote. - pub single_close: &'s str, - /// The opening double quote. - pub double_open: &'s str, - /// The closing double quote. - pub double_close: &'s str, -} - -impl<'s> Quotes<'s> { - /// Create a new `Quotes` struct with the given quotes, optionally falling - /// back to the defaults for a language and region. - /// - /// The language should be specified as an all-lowercase ISO 639-1 code, the - /// region as an all-uppercase ISO 3166-alpha2 code. - /// - /// Currently, the supported languages are: English, Czech, Danish, German, - /// Swiss / Liechtensteinian German, Estonian, Icelandic, Lithuanian, - /// Latvian, Slovak, Slovenian, Spanish, Bosnian, Finnish, Swedish, French, - /// Hungarian, Polish, Romanian, Japanese, Traditional Chinese, Russian, and - /// Norwegian. - /// - /// For unknown languages, the English quotes are used as fallback. - pub fn new( - quotes: &'s Smart<QuoteDict>, - lang: Lang, - region: Option<Region>, - alternative: bool, - ) -> Self { - let region = region.as_ref().map(Region::as_str); - - let default = ("‘", "’", "“", "”"); - let low_high = ("‚", "‘", "„", "“"); - - let (single_open, single_close, double_open, double_close) = match lang.as_str() { - "de" if matches!(region, Some("CH" | "LI")) => match alternative { - false => ("‹", "›", "«", "»"), - true => low_high, - }, - "cs" | "da" | "de" | "sk" | "sl" if alternative => ("›", "‹", "»", "«"), - "cs" | "da" | "de" | "et" | "is" | "lt" | "lv" | "sk" | "sl" => low_high, - "fr" | "ru" if alternative => default, - "fr" => ("‹\u{00A0}", "\u{00A0}›", "«\u{00A0}", "\u{00A0}»"), - "fi" | "sv" if alternative => ("’", "’", "»", "»"), - "bs" | "fi" | "sv" => ("’", "’", "”", "”"), - "es" if matches!(region, Some("ES") | None) => ("“", "”", "«", "»"), - "hu" | "pl" | "ro" => ("’", "’", "„", "”"), - "no" | "nb" | "nn" if alternative => low_high, - "ru" | "no" | "nb" | "nn" | "ua" => ("’", "’", "«", "»"), - "gr" => ("‘", "’", "«", "»"), - _ if lang.dir() == Dir::RTL => ("’", "‘", "”", "“"), - _ => default, - }; - - fn inner_or_default<'s>( - quotes: Smart<&'s QuoteDict>, - f: impl FnOnce(&'s QuoteDict) -> Smart<&'s QuoteSet>, - default: [&'s str; 2], - ) -> [&'s str; 2] { - match quotes.and_then(f) { - Smart::Auto => default, - Smart::Custom(QuoteSet { open, close }) => { - [open, close].map(|s| s.as_str()) - } - } - } - - let quotes = quotes.as_ref(); - let [single_open, single_close] = - inner_or_default(quotes, |q| q.single.as_ref(), [single_open, single_close]); - let [double_open, double_close] = - inner_or_default(quotes, |q| q.double.as_ref(), [double_open, double_close]); - - Self { - single_open, - single_close, - double_open, - double_close, - } - } - - /// The opening quote. - fn open(&self, double: bool) -> &'s str { - if double { - self.double_open - } else { - self.single_open - } - } - - /// The closing quote. - fn close(&self, double: bool) -> &'s str { - if double { - self.double_close - } else { - self.single_close - } - } - - /// Which character should be used as a prime. - fn prime(&self, double: bool) -> &'static str { - if double { - "″" - } else { - "′" - } - } - - /// Which character should be used as a fallback quote. - fn fallback(&self, double: bool) -> &'static str { - if double { - "\"" - } else { - "’" - } - } -} - -/// An opening and closing quote. -#[derive(Debug, Clone, Eq, PartialEq, Hash)] -pub struct QuoteSet { - open: EcoString, - close: EcoString, -} - -cast! { - QuoteSet, - self => array![self.open, self.close].into_value(), - value: Array => { - let [open, close] = array_to_set(value)?; - Self { open, close } - }, - value: Str => { - let [open, close] = str_to_set(value.as_str())?; - Self { open, close } - }, -} - -fn str_to_set(value: &str) -> StrResult<[EcoString; 2]> { - let mut iter = value.graphemes(true); - match (iter.next(), iter.next(), iter.next()) { - (Some(open), Some(close), None) => Ok([open.into(), close.into()]), - _ => { - let count = value.graphemes(true).count(); - bail!( - "expected 2 characters, found {count} character{}", - if count > 1 { "s" } else { "" } - ); - } - } -} - -fn array_to_set(value: Array) -> StrResult<[EcoString; 2]> { - let value = value.as_slice(); - if value.len() != 2 { - bail!( - "expected 2 quotes, found {} quote{}", - value.len(), - if value.len() > 1 { "s" } else { "" } - ); - } - - let open: EcoString = value[0].clone().cast()?; - let close: EcoString = value[1].clone().cast()?; - - Ok([open, close]) -} - -/// A dict of single and double quotes. -#[derive(Debug, Clone, Eq, PartialEq, Hash)] -pub struct QuoteDict { - double: Smart<QuoteSet>, - single: Smart<QuoteSet>, -} - -cast! { - QuoteDict, - self => dict! { "double" => self.double, "single" => self.single }.into_value(), - mut value: Dict => { - let keys = ["double", "single"]; - - let double = value - .take("double") - .ok() - .map(FromValue::from_value) - .transpose()? - .unwrap_or(Smart::Auto); - let single = value - .take("single") - .ok() - .map(FromValue::from_value) - .transpose()? - .unwrap_or(Smart::Auto); - - value.finish(&keys)?; - - Self { single, double } - }, - value: QuoteSet => Self { - double: Smart::Custom(value), - single: Smart::Auto, - }, -} diff --git a/crates/typst-library/src/text/raw.rs b/crates/typst-library/src/text/raw.rs deleted file mode 100644 index c5e152cf..00000000 --- a/crates/typst-library/src/text/raw.rs +++ /dev/null @@ -1,835 +0,0 @@ -use std::hash::Hash; -use std::ops::Range; -use std::sync::Arc; - -use ecow::EcoVec; -use once_cell::sync::Lazy; -use once_cell::unsync::Lazy as UnsyncLazy; -use syntect::highlighting as synt; -use syntect::parsing::{SyntaxDefinition, SyntaxSet, SyntaxSetBuilder}; -use typst::diag::FileError; -use typst::eval::Bytes; -use typst::syntax::{self, split_newlines, LinkedNode}; -use typst::util::option_eq; -use unicode_segmentation::UnicodeSegmentation; - -use crate::layout::BlockElem; -use crate::meta::Figurable; -use crate::prelude::*; -use crate::text::{ - FontFamily, FontList, Hyphenate, LinebreakElem, SmartquoteElem, TextElem, TextSize, -}; - -// Shorthand for highlighter closures. -type StyleFn<'a> = &'a mut dyn FnMut(&LinkedNode, Range<usize>, synt::Style) -> Content; -type LineFn<'a> = &'a mut dyn FnMut(i64, Range<usize>, &mut Vec<Content>); - -/// Raw text with optional syntax highlighting. -/// -/// Displays the text verbatim and in a monospace font. This is typically used -/// to embed computer code into your document. -/// -/// # Example -/// ````example -/// Adding `rbx` to `rcx` gives -/// the desired result. -/// -/// What is ```rust fn main()``` in Rust -/// would be ```c int main()``` in C. -/// -/// ```rust -/// fn main() { -/// println!("Hello World!"); -/// } -/// ``` -/// -/// This has ``` `backticks` ``` in it -/// (but the spaces are trimmed). And -/// ``` here``` the leading space is -/// also trimmed. -/// ```` -/// -/// # Syntax -/// This function also has dedicated syntax. You can enclose text in 1 or 3+ -/// backticks (`` ` ``) to make it raw. Two backticks produce empty raw text. -/// When you use three or more backticks, you can additionally specify a -/// language tag for syntax highlighting directly after the opening backticks. -/// Within raw blocks, everything (except for the language tag, if applicable) -/// is rendered as is, in particular, there are no escape sequences. -/// -/// The language tag is an identifier that directly follows the opening -/// backticks only if there are three or more backticks. If your text starts -/// with something that looks like an identifier, but no syntax highlighting is -/// needed, start the text with a single space (which will be trimmed) or use -/// the single backtick syntax. If your text should start or end with a -/// backtick, put a space before or after it (it will be trimmed). -#[elem( - scope, - title = "Raw Text / Code", - Synthesize, - Show, - Finalize, - LocalName, - Figurable, - PlainText -)] -pub struct RawElem { - /// The raw text. - /// - /// You can also use raw blocks creatively to create custom syntaxes for - /// your automations. - /// - /// ````example - /// // Parse numbers in raw blocks with the - /// // `mydsl` tag and sum them up. - /// #show raw.where(lang: "mydsl"): it => { - /// let sum = 0 - /// for part in it.text.split("+") { - /// sum += int(part.trim()) - /// } - /// sum - /// } - /// - /// ```mydsl - /// 1 + 2 + 3 + 4 + 5 - /// ``` - /// ```` - #[required] - pub text: EcoString, - - /// Whether the raw text is displayed as a separate block. - /// - /// In markup mode, using one-backtick notation makes this `{false}`. - /// Using three-backtick notation makes it `{true}` if the enclosed content - /// contains at least one line break. - /// - /// ````example - /// // Display inline code in a small box - /// // that retains the correct baseline. - /// #show raw.where(block: false): box.with( - /// fill: luma(240), - /// inset: (x: 3pt, y: 0pt), - /// outset: (y: 3pt), - /// radius: 2pt, - /// ) - /// - /// // Display block code in a larger block - /// // with more padding. - /// #show raw.where(block: true): block.with( - /// fill: luma(240), - /// inset: 10pt, - /// radius: 4pt, - /// ) - /// - /// With `rg`, you can search through your files quickly. - /// This example searches the current directory recursively - /// for the text `Hello World`: - /// - /// ```bash - /// rg "Hello World" - /// ``` - /// ```` - #[default(false)] - pub block: bool, - - /// The language to syntax-highlight in. - /// - /// Apart from typical language tags known from Markdown, this supports the - /// `{"typ"}` and `{"typc"}` tags for Typst markup and Typst code, - /// respectively. - /// - /// ````example - /// ```typ - /// This is *Typst!* - /// ``` - /// - /// This is ```typ also *Typst*```, but inline! - /// ```` - #[borrowed] - pub lang: Option<EcoString>, - - /// The horizontal alignment that each line in a raw block should have. - /// This option is ignored if this is not a raw block (if specified - /// `block: false` or single backticks were used in markup mode). - /// - /// By default, this is set to `{start}`, meaning that raw text is - /// aligned towards the start of the text direction inside the block - /// by default, regardless of the current context's alignment (allowing - /// you to center the raw block itself without centering the text inside - /// it, for example). - /// - /// ````example - /// #set raw(align: center) - /// - /// ```typc - /// let f(x) = x - /// code = "centered" - /// ``` - /// ```` - #[default(HAlign::Start)] - pub align: HAlign, - - /// One or multiple additional syntax definitions to load. The syntax - /// definitions should be in the - /// [`sublime-syntax` file format](https://www.sublimetext.com/docs/syntax.html). - /// - /// ````example - /// #set raw(syntaxes: "SExpressions.sublime-syntax") - /// - /// ```sexp - /// (defun factorial (x) - /// (if (zerop x) - /// ; with a comment - /// 1 - /// (* x (factorial (- x 1))))) - /// ``` - /// ```` - #[parse( - let (syntaxes, syntaxes_data) = parse_syntaxes(vm, args)?; - syntaxes - )] - #[fold] - pub syntaxes: SyntaxPaths, - - /// The raw file buffers of syntax definition files. - #[internal] - #[parse(syntaxes_data)] - #[fold] - pub syntaxes_data: Vec<Bytes>, - - /// The theme to use for syntax highlighting. Theme files should be in the - /// in the [`tmTheme` file format](https://www.sublimetext.com/docs/color_schemes_tmtheme.html). - /// - /// Applying a theme only affects the color of specifically highlighted - /// text. It does not consider the theme's foreground and background - /// properties, so that you retain control over the color of raw text. You - /// can apply the foreground color yourself with the [`text`]($text) - /// function and the background with a [filled block]($block.fill). You - /// could also use the [`xml`]($xml) function to extract these properties - /// from the theme. - /// - /// ````example - /// #set raw(theme: "halcyon.tmTheme") - /// #show raw: it => block( - /// fill: rgb("#1d2433"), - /// inset: 8pt, - /// radius: 5pt, - /// text(fill: rgb("#a2aabc"), it) - /// ) - /// - /// ```typ - /// = Chapter 1 - /// #let hi = "Hello World" - /// ``` - /// ```` - #[parse( - let (theme_path, theme_data) = parse_theme(vm, args)?; - theme_path.map(Some) - )] - #[borrowed] - pub theme: Option<EcoString>, - - /// The raw file buffer of syntax theme file. - #[internal] - #[parse(theme_data.map(Some))] - #[borrowed] - pub theme_data: Option<Bytes>, - - /// The size for a tab stop in spaces. A tab is replaced with enough spaces to - /// align with the next multiple of the size. - /// - /// ````example - /// #set raw(tab-size: 8) - /// ```tsv - /// Year Month Day - /// 2000 2 3 - /// 2001 2 1 - /// 2002 3 10 - /// ``` - /// ```` - #[default(2)] - pub tab_size: usize, - - /// The stylized lines of raw text. - /// - /// Made accessible for the [`raw.line` element]($raw.line). - /// Allows more styling control in `show` rules. - #[synthesized] - pub lines: Vec<RawLine>, -} - -#[scope] -impl RawElem { - #[elem] - type RawLine; -} - -impl RawElem { - /// The supported language names and tags. - pub fn languages() -> Vec<(&'static str, Vec<&'static str>)> { - SYNTAXES - .syntaxes() - .iter() - .map(|syntax| { - ( - syntax.name.as_str(), - syntax.file_extensions.iter().map(|s| s.as_str()).collect(), - ) - }) - .chain([("Typst", vec!["typ"]), ("Typst (code)", vec!["typc"])]) - .collect() - } -} - -impl Synthesize for RawElem { - fn synthesize(&mut self, _vt: &mut Vt, styles: StyleChain) -> SourceResult<()> { - self.push_lang(self.lang(styles).clone()); - - let mut text = self.text().clone(); - if text.contains('\t') { - let tab_size = RawElem::tab_size_in(styles); - text = align_tabs(&text, tab_size); - } - - let lines = split_newlines(&text); - let count = lines.len() as i64; - - let lang = self - .lang(styles) - .as_ref() - .as_ref() - .map(|s| s.to_lowercase()) - .or(Some("txt".into())); - - let extra_syntaxes = UnsyncLazy::new(|| { - load_syntaxes(&self.syntaxes(styles), &self.syntaxes_data(styles)).unwrap() - }); - - let theme = self.theme(styles).as_ref().as_ref().map(|theme_path| { - load_theme(theme_path, self.theme_data(styles).as_ref().as_ref().unwrap()) - .unwrap() - }); - - let theme = theme.as_deref().unwrap_or(&THEME); - let foreground = theme.settings.foreground.unwrap_or(synt::Color::BLACK); - - let mut seq = vec![]; - if matches!(lang.as_deref(), Some("typ" | "typst" | "typc")) { - let root = match lang.as_deref() { - Some("typc") => syntax::parse_code(&text), - _ => syntax::parse(&text), - }; - - ThemedHighlighter::new( - &text, - LinkedNode::new(&root), - synt::Highlighter::new(theme), - &mut |_, range, style| styled(&text[range], foreground, style), - &mut |i, range, line| { - seq.push( - RawLine::new( - i + 1, - count, - EcoString::from(&text[range]), - Content::sequence(line.drain(..)), - ) - .spanned(self.span()), - ); - }, - ) - .highlight(); - } else if let Some((syntax_set, syntax)) = lang.and_then(|token| { - SYNTAXES - .find_syntax_by_token(&token) - .map(|syntax| (&*SYNTAXES, syntax)) - .or_else(|| { - extra_syntaxes - .find_syntax_by_token(&token) - .map(|syntax| (&**extra_syntaxes, syntax)) - }) - }) { - let mut highlighter = syntect::easy::HighlightLines::new(syntax, theme); - for (i, line) in lines.into_iter().enumerate() { - let mut line_content = vec![]; - for (style, piece) in - highlighter.highlight_line(line, syntax_set).into_iter().flatten() - { - line_content.push(styled(piece, foreground, style)); - } - - seq.push( - RawLine::new( - i as i64 + 1, - count, - EcoString::from(line), - Content::sequence(line_content), - ) - .spanned(self.span()), - ); - } - } else { - seq.extend(lines.into_iter().enumerate().map(|(i, line)| { - RawLine::new( - i as i64 + 1, - count, - EcoString::from(line), - TextElem::packed(line), - ) - .spanned(self.span()) - })); - }; - - self.push_lines(seq); - - Ok(()) - } -} - -impl Show for RawElem { - #[tracing::instrument(name = "RawElem::show", skip_all)] - fn show(&self, _: &mut Vt, styles: StyleChain) -> SourceResult<Content> { - let mut lines = EcoVec::with_capacity((2 * self.lines().len()).saturating_sub(1)); - for (i, line) in self.lines().iter().enumerate() { - if i != 0 { - lines.push(LinebreakElem::new().pack()); - } - - lines.push(line.clone().pack()); - } - - let mut realized = Content::sequence(lines); - if self.block(styles) { - // Align the text before inserting it into the block. - realized = realized.aligned(self.align(styles).into()); - realized = BlockElem::new().with_body(Some(realized)).pack(); - } - - Ok(realized) - } -} - -impl Finalize for RawElem { - fn finalize(&self, realized: Content, _: StyleChain) -> Content { - let mut styles = Styles::new(); - styles.set(TextElem::set_overhang(false)); - styles.set(TextElem::set_hyphenate(Hyphenate(Smart::Custom(false)))); - styles.set(TextElem::set_size(TextSize(Em::new(0.8).into()))); - styles - .set(TextElem::set_font(FontList(vec![FontFamily::new("DejaVu Sans Mono")]))); - styles.set(SmartquoteElem::set_enabled(false)); - realized.styled_with_map(styles) - } -} - -impl LocalName for RawElem { - fn local_name(lang: Lang, region: Option<Region>) -> &'static str { - match lang { - Lang::ALBANIAN => "List", - Lang::ARABIC => "قائمة", - Lang::BOKMÅL => "Utskrift", - Lang::CHINESE if option_eq(region, "TW") => "程式", - Lang::CHINESE => "代码", - Lang::CZECH => "Seznam", - Lang::DANISH => "Liste", - Lang::DUTCH => "Listing", - Lang::FILIPINO => "Listahan", - Lang::FINNISH => "Esimerkki", - Lang::FRENCH => "Liste", - Lang::GERMAN => "Listing", - Lang::GREEK => "Παράθεση", - Lang::ITALIAN => "Codice", - Lang::NYNORSK => "Utskrift", - Lang::POLISH => "Program", - Lang::ROMANIAN => "Listă", // TODO: I dunno - Lang::RUSSIAN => "Листинг", - Lang::SLOVENIAN => "Program", - Lang::SPANISH => "Listado", - Lang::SWEDISH => "Listing", - Lang::TURKISH => "Liste", - Lang::UKRAINIAN => "Лістинг", - Lang::VIETNAMESE => "Chương trình", // TODO: This may be wrong. - Lang::JAPANESE => "リスト", - Lang::ENGLISH | _ => "Listing", - } - } -} - -impl Figurable for RawElem {} - -impl PlainText for RawElem { - fn plain_text(&self, text: &mut EcoString) { - text.push_str(self.text()); - } -} - -/// A highlighted line of raw text. -/// -/// This is a helper element that is synthesized by [`raw`]($raw) elements. -/// -/// It allows you to access various properties of the line, such as the line -/// number, the raw non-highlighted text, the highlighted text, and whether it -/// is the first or last line of the raw block. -#[elem(name = "line", title = "Raw Text / Code Line", Show, PlainText)] -pub struct RawLine { - /// The line number of the raw line inside of the raw block, starts at 1. - #[required] - pub number: i64, - - /// The total number of lines in the raw block. - #[required] - pub count: i64, - - /// The line of raw text. - #[required] - pub text: EcoString, - - /// The highlighted raw text. - #[required] - pub body: Content, -} - -impl Show for RawLine { - fn show(&self, _vt: &mut Vt, _styles: StyleChain) -> SourceResult<Content> { - Ok(self.body().clone()) - } -} - -impl PlainText for RawLine { - fn plain_text(&self, text: &mut EcoString) { - text.push_str(self.text()); - } -} - -/// Wrapper struct for the state required to highlight typst code. -struct ThemedHighlighter<'a> { - /// The code being highlighted. - code: &'a str, - /// The current node being highlighted. - node: LinkedNode<'a>, - /// The highlighter. - highlighter: synt::Highlighter<'a>, - /// The current scopes. - scopes: Vec<syntect::parsing::Scope>, - /// The current highlighted line. - current_line: Vec<Content>, - /// The range of the current line. - range: Range<usize>, - /// The current line number. - line: i64, - /// The function to style a piece of text. - style_fn: StyleFn<'a>, - /// The function to append a line. - line_fn: LineFn<'a>, -} - -impl<'a> ThemedHighlighter<'a> { - pub fn new( - code: &'a str, - top: LinkedNode<'a>, - highlighter: synt::Highlighter<'a>, - style_fn: StyleFn<'a>, - line_fn: LineFn<'a>, - ) -> Self { - Self { - code, - node: top, - highlighter, - range: 0..0, - scopes: Vec::new(), - current_line: Vec::new(), - line: 0, - style_fn, - line_fn, - } - } - - pub fn highlight(&mut self) { - self.highlight_inner(); - - if !self.current_line.is_empty() { - (self.line_fn)( - self.line, - self.range.start..self.code.len(), - &mut self.current_line, - ); - - self.current_line.clear(); - } - } - - fn highlight_inner(&mut self) { - if self.node.children().len() == 0 { - let style = self.highlighter.style_for_stack(&self.scopes); - let segment = &self.code[self.node.range()]; - - let mut len = 0; - for (i, line) in split_newlines(segment).into_iter().enumerate() { - if i != 0 { - (self.line_fn)( - self.line, - self.range.start..self.range.end + len - 1, - &mut self.current_line, - ); - self.range.start = self.range.end + len; - self.line += 1; - } - - let offset = self.node.range().start + len; - let token_range = offset..(offset + line.len()); - self.current_line - .push((self.style_fn)(&self.node, token_range, style)); - - len += line.len() + 1; - } - - self.range.end += segment.len(); - } - - for child in self.node.children() { - let mut scopes = self.scopes.clone(); - if let Some(tag) = typst::syntax::highlight(&child) { - scopes.push(syntect::parsing::Scope::new(tag.tm_scope()).unwrap()) - } - - std::mem::swap(&mut scopes, &mut self.scopes); - self.node = child; - self.highlight_inner(); - std::mem::swap(&mut scopes, &mut self.scopes); - } - } -} - -/// Style a piece of text with a syntect style. -fn styled(piece: &str, foreground: synt::Color, style: synt::Style) -> Content { - let mut body = TextElem::packed(piece); - - if style.foreground != foreground { - body = body.styled(TextElem::set_fill(to_typst(style.foreground).into())); - } - - if style.font_style.contains(synt::FontStyle::BOLD) { - body = body.strong(); - } - - if style.font_style.contains(synt::FontStyle::ITALIC) { - body = body.emph(); - } - - if style.font_style.contains(synt::FontStyle::UNDERLINE) { - body = body.underlined(); - } - - body -} - -fn to_typst(synt::Color { r, g, b, a }: synt::Color) -> Color { - Color::from_u8(r, g, b, a) -} - -fn to_syn(color: Color) -> synt::Color { - let [r, g, b, a] = color.to_vec4_u8(); - synt::Color { r, g, b, a } -} - -/// A list of bibliography file paths. -#[derive(Debug, Default, Clone, PartialEq, Hash)] -pub struct SyntaxPaths(Vec<EcoString>); - -cast! { - SyntaxPaths, - self => self.0.into_value(), - v: EcoString => Self(vec![v]), - v: Array => Self(v.into_iter().map(Value::cast).collect::<StrResult<_>>()?), -} - -impl Fold for SyntaxPaths { - type Output = Self; - - fn fold(mut self, outer: Self::Output) -> Self::Output { - self.0.extend(outer.0); - self - } -} - -/// Load a syntax set from a list of syntax file paths. -#[comemo::memoize] -fn load_syntaxes(paths: &SyntaxPaths, bytes: &[Bytes]) -> StrResult<Arc<SyntaxSet>> { - let mut out = SyntaxSetBuilder::new(); - - // We might have multiple sublime-syntax/yaml files - for (path, bytes) in paths.0.iter().zip(bytes.iter()) { - let src = std::str::from_utf8(bytes).map_err(FileError::from)?; - out.add(SyntaxDefinition::load_from_str(src, false, None).map_err(|err| { - eco_format!("failed to parse syntax file `{path}` ({err})") - })?); - } - - Ok(Arc::new(out.build())) -} - -/// Function to parse the syntaxes argument. -/// Much nicer than having it be part of the `element` macro. -fn parse_syntaxes( - vm: &mut Vm, - args: &mut Args, -) -> SourceResult<(Option<SyntaxPaths>, Option<Vec<Bytes>>)> { - let Some(Spanned { v: paths, span }) = - args.named::<Spanned<SyntaxPaths>>("syntaxes")? - else { - return Ok((None, None)); - }; - - // Load syntax files. - let data = paths - .0 - .iter() - .map(|path| { - let id = vm.resolve_path(path).at(span)?; - vm.world().file(id).at(span) - }) - .collect::<SourceResult<Vec<Bytes>>>()?; - - // Check that parsing works. - let _ = load_syntaxes(&paths, &data).at(span)?; - - Ok((Some(paths), Some(data))) -} - -#[comemo::memoize] -fn load_theme(path: &str, bytes: &Bytes) -> StrResult<Arc<synt::Theme>> { - let mut cursor = std::io::Cursor::new(bytes.as_slice()); - - synt::ThemeSet::load_from_reader(&mut cursor) - .map(Arc::new) - .map_err(|err| eco_format!("failed to parse theme file `{path}` ({err})")) -} - -/// Function to parse the theme argument. -/// Much nicer than having it be part of the `element` macro. -fn parse_theme( - vm: &mut Vm, - args: &mut Args, -) -> SourceResult<(Option<EcoString>, Option<Bytes>)> { - let Some(Spanned { v: path, span }) = args.named::<Spanned<EcoString>>("theme")? - else { - return Ok((None, None)); - }; - - // Load theme file. - let id = vm.resolve_path(&path).at(span)?; - let data = vm.world().file(id).at(span)?; - - // Check that parsing works. - let _ = load_theme(&path, &data).at(span)?; - - Ok((Some(path), Some(data))) -} - -/// The syntect syntax definitions. -/// -/// Code for syntax set generation is below. The `syntaxes` directory is from -/// <https://github.com/sharkdp/bat/tree/master/assets/syntaxes> -/// -/// ```ignore -/// fn main() { -/// let mut builder = syntect::parsing::SyntaxSet::load_defaults_nonewlines().into_builder(); -/// builder.add_from_folder("syntaxes/02_Extra", false).unwrap(); -/// syntect::dumps::dump_to_file(&builder.build(), "syntect.bin").unwrap(); -/// } -/// ``` -/// -/// The following syntaxes are disabled due to compatibility issues: -/// ```text -/// syntaxes/02_Extra/Assembly (ARM).sublime-syntax -/// syntaxes/02_Extra/Elixir/Regular Expressions (Elixir).sublime-syntax -/// syntaxes/02_Extra/JavaScript (Babel).sublime-syntax -/// syntaxes/02_Extra/LiveScript.sublime-syntax -/// syntaxes/02_Extra/PowerShell.sublime-syntax -/// syntaxes/02_Extra/SCSS_Sass/Syntaxes/Sass.sublime-syntax -/// syntaxes/02_Extra/SLS/SLS.sublime-syntax -/// syntaxes/02_Extra/VimHelp.sublime-syntax -/// syntaxes/02_Extra/cmd-help/syntaxes/cmd-help.sublime-syntax -/// ``` -pub static SYNTAXES: Lazy<syntect::parsing::SyntaxSet> = - Lazy::new(|| syntect::dumps::from_binary(include_bytes!("../../assets/syntect.bin"))); - -/// The default theme used for syntax highlighting. -pub static THEME: Lazy<synt::Theme> = Lazy::new(|| synt::Theme { - name: Some("Typst Light".into()), - author: Some("The Typst Project Developers".into()), - settings: synt::ThemeSettings::default(), - scopes: vec![ - item("comment", Some("#8a8a8a"), None), - item("constant.character.escape", Some("#1d6c76"), None), - item("markup.bold", None, Some(synt::FontStyle::BOLD)), - item("markup.italic", None, Some(synt::FontStyle::ITALIC)), - item("markup.underline", None, Some(synt::FontStyle::UNDERLINE)), - item("markup.raw", Some("#818181"), None), - item("string.other.math.typst", None, None), - item("punctuation.definition.math", Some("#298e0d"), None), - item("keyword.operator.math", Some("#1d6c76"), None), - item("markup.heading, entity.name.section", None, Some(synt::FontStyle::BOLD)), - item( - "markup.heading.typst", - None, - Some(synt::FontStyle::BOLD | synt::FontStyle::UNDERLINE), - ), - item("punctuation.definition.list", Some("#8b41b1"), None), - item("markup.list.term", None, Some(synt::FontStyle::BOLD)), - item("entity.name.label, markup.other.reference", Some("#1d6c76"), None), - item("keyword, constant.language, variable.language", Some("#d73a49"), None), - item("storage.type, storage.modifier", Some("#d73a49"), None), - item("constant", Some("#b60157"), None), - item("string", Some("#298e0d"), None), - item("entity.name, variable.function, support", Some("#4b69c6"), None), - item("support.macro", Some("#16718d"), None), - item("meta.annotation", Some("#301414"), None), - item("entity.other, meta.interpolation", Some("#8b41b1"), None), - ], -}); - -/// Create a syntect theme item. -fn item( - scope: &str, - color: Option<&str>, - font_style: Option<synt::FontStyle>, -) -> synt::ThemeItem { - synt::ThemeItem { - scope: scope.parse().unwrap(), - style: synt::StyleModifier { - foreground: color.map(|s| to_syn(s.parse::<Color>().unwrap())), - background: None, - font_style, - }, - } -} - -/// Replace tabs with spaces to align with multiples of `tab_size`. -fn align_tabs(text: &str, tab_size: usize) -> EcoString { - let replacement = " ".repeat(tab_size); - let divisor = tab_size.max(1); - let amount = text.chars().filter(|&c| c == '\t').count(); - - let mut res = EcoString::with_capacity(text.len() - amount + amount * tab_size); - let mut column = 0; - - for grapheme in text.graphemes(true) { - match grapheme { - "\t" => { - let required = tab_size - column % divisor; - res.push_str(&replacement[..required]); - column += required; - } - "\n" => { - res.push_str(grapheme); - column = 0; - } - _ => { - res.push_str(grapheme); - column += 1; - } - } - } - - res -} diff --git a/crates/typst-library/src/text/shaping.rs b/crates/typst-library/src/text/shaping.rs deleted file mode 100644 index 2d820c64..00000000 --- a/crates/typst-library/src/text/shaping.rs +++ /dev/null @@ -1,1123 +0,0 @@ -use std::borrow::Cow; -use std::ops::Range; -use std::str::FromStr; - -use az::SaturatingAs; -use rustybuzz::{Feature, Tag, UnicodeBuffer}; -use typst::font::{Font, FontStyle, FontVariant}; -use typst::util::SliceExt; -use unicode_script::{Script, UnicodeScript}; - -use crate::layout::SpanMapper; -use crate::prelude::*; -use crate::text::{decorate, NumberType, NumberWidth, TextElem}; - -/// The result of shaping text. -/// -/// This type contains owned or borrowed shaped text runs, which can be -/// measured, used to reshape substrings more quickly and converted into a -/// frame. -pub struct ShapedText<'a> { - /// The start of the text in the full paragraph. - pub base: usize, - /// The text that was shaped. - pub text: &'a str, - /// The text direction. - pub dir: Dir, - /// The text language. - pub lang: Lang, - /// The text region. - pub region: Option<Region>, - /// The text's style properties. - pub styles: StyleChain<'a>, - /// The font variant. - pub variant: FontVariant, - /// The font size. - pub size: Abs, - /// The width of the text's bounding box. - pub width: Abs, - /// The shaped glyphs. - pub glyphs: Cow<'a, [ShapedGlyph]>, -} - -/// A single glyph resulting from shaping. -#[derive(Debug, Clone)] -pub struct ShapedGlyph { - /// The font the glyph is contained in. - pub font: Font, - /// The glyph's index in the font. - pub glyph_id: u16, - /// The advance width of the glyph. - pub x_advance: Em, - /// The horizontal offset of the glyph. - pub x_offset: Em, - /// The vertical offset of the glyph. - pub y_offset: Em, - /// The adjustability of the glyph. - pub adjustability: Adjustability, - /// The byte range of this glyph's cluster in the full paragraph. A cluster - /// is a sequence of one or multiple glyphs that cannot be separated and - /// must always be treated as a union. - /// - /// The range values of the glyphs in a [`ShapedText`] should not - /// overlap with each other, and they should be monotonically - /// increasing (for left-to-right or top-to-bottom text) or - /// monotonically decreasing (for right-to-left or bottom-to-top - /// text). - pub range: Range<usize>, - /// Whether splitting the shaping result before this glyph would yield the - /// same results as shaping the parts to both sides of `text_index` - /// separately. - pub safe_to_break: bool, - /// The first char in this glyph's cluster. - pub c: char, - /// The source code location of the glyph and its byte offset within it. - pub span: (Span, u16), - /// Whether this glyph is justifiable for CJK scripts. - pub is_justifiable: bool, - /// The script of the glyph. - pub script: Script, -} - -#[derive(Debug, Clone, Default)] -pub struct Adjustability { - /// The left and right strechability - pub stretchability: (Em, Em), - /// The left and right shrinkability - pub shrinkability: (Em, Em), -} - -impl ShapedGlyph { - /// Whether the glyph is a space. - pub fn is_space(&self) -> bool { - is_space(self.c) - } - - /// Whether the glyph is justifiable. - pub fn is_justifiable(&self) -> bool { - // GB style is not relevant here. - self.is_justifiable - } - - /// Whether the glyph is part of a CJK script. - pub fn is_cjk_script(&self) -> bool { - is_cjk_script(self.c, self.script) - } - - pub fn is_cjk_punctuation(&self) -> bool { - self.is_cjk_left_aligned_punctuation(true) - || self.is_cjk_right_aligned_punctuation() - || self.is_cjk_center_aligned_punctuation(true) - } - - /// See <https://www.w3.org/TR/clreq/#punctuation_width_adjustment> - pub fn is_cjk_left_aligned_punctuation(&self, gb_style: bool) -> bool { - is_cjk_left_aligned_punctuation( - self.c, - self.x_advance, - self.stretchability(), - gb_style, - ) - } - - /// See <https://www.w3.org/TR/clreq/#punctuation_width_adjustment> - pub fn is_cjk_right_aligned_punctuation(&self) -> bool { - is_cjk_right_aligned_punctuation(self.c, self.x_advance, self.stretchability()) - } - - /// See <https://www.w3.org/TR/clreq/#punctuation_width_adjustment> - pub fn is_cjk_center_aligned_punctuation(&self, gb_style: bool) -> bool { - is_cjk_center_aligned_punctuation(self.c, gb_style) - } - - /// Whether the glyph is a western letter or number. - pub fn is_letter_or_number(&self) -> bool { - matches!(self.c.script(), Script::Latin | Script::Greek | Script::Cyrillic) - || matches!(self.c, '#' | '$' | '%' | '&') - || self.c.is_ascii_digit() - } - - pub fn base_adjustability(&self, gb_style: bool) -> Adjustability { - let width = self.x_advance; - if self.is_space() { - Adjustability { - // The number for spaces is from Knuth-Plass' paper - stretchability: (Em::zero(), width / 2.0), - shrinkability: (Em::zero(), width / 3.0), - } - } else if self.is_cjk_left_aligned_punctuation(gb_style) { - Adjustability { - stretchability: (Em::zero(), Em::zero()), - shrinkability: (Em::zero(), width / 2.0), - } - } else if self.is_cjk_right_aligned_punctuation() { - Adjustability { - stretchability: (Em::zero(), Em::zero()), - shrinkability: (width / 2.0, Em::zero()), - } - } else if self.is_cjk_center_aligned_punctuation(gb_style) { - Adjustability { - stretchability: (Em::zero(), Em::zero()), - shrinkability: (width / 4.0, width / 4.0), - } - } else { - Adjustability::default() - } - } - - /// The stretchability of the character. - pub fn stretchability(&self) -> (Em, Em) { - self.adjustability.stretchability - } - - /// The shrinkability of the character. - pub fn shrinkability(&self) -> (Em, Em) { - self.adjustability.shrinkability - } - - /// Shrink the width of glyph on the left side. - pub fn shrink_left(&mut self, amount: Em) { - self.x_offset -= amount; - self.x_advance -= amount; - self.adjustability.shrinkability.0 -= amount; - self.adjustability.stretchability.0 += amount; - } - - /// Shrink the width of glyph on the right side. - pub fn shrink_right(&mut self, amount: Em) { - self.x_advance -= amount; - self.adjustability.shrinkability.1 -= amount; - self.adjustability.stretchability.1 += amount; - } -} - -/// A side you can go toward. -enum Side { - /// To the left-hand side. - Left, - /// To the right-hand side. - Right, -} - -impl<'a> ShapedText<'a> { - /// Build the shaped text's frame. - /// - /// The `justification` defines how much extra advance width each - /// [justifiable glyph](ShapedGlyph::is_justifiable) will get. - pub fn build( - &self, - vt: &Vt, - justification_ratio: f64, - extra_justification: Abs, - ) -> Frame { - let (top, bottom) = self.measure(vt); - let size = Size::new(self.width, top + bottom); - - let mut offset = Abs::zero(); - let mut frame = Frame::soft(size); - frame.set_baseline(top); - - let shift = TextElem::baseline_in(self.styles); - let lang = TextElem::lang_in(self.styles); - let decos = TextElem::deco_in(self.styles); - let fill = TextElem::fill_in(self.styles); - - for ((font, y_offset), group) in - self.glyphs.as_ref().group_by_key(|g| (g.font.clone(), g.y_offset)) - { - let mut range = group[0].range.clone(); - for glyph in group { - range.start = range.start.min(glyph.range.start); - range.end = range.end.max(glyph.range.end); - } - - let pos = Point::new(offset, top + shift - y_offset.at(self.size)); - let glyphs = group - .iter() - .map(|glyph| { - let adjustability_left = if justification_ratio < 0.0 { - glyph.shrinkability().0 - } else { - glyph.stretchability().0 - }; - let adjustability_right = if justification_ratio < 0.0 { - glyph.shrinkability().1 - } else { - glyph.stretchability().1 - }; - - let justification_left = adjustability_left * justification_ratio; - let mut justification_right = - adjustability_right * justification_ratio; - if glyph.is_justifiable() { - justification_right += - Em::from_length(extra_justification, self.size) - } - - frame.size_mut().x += justification_left.at(self.size) - + justification_right.at(self.size); - - Glyph { - id: glyph.glyph_id, - x_advance: glyph.x_advance - + justification_left - + justification_right, - x_offset: glyph.x_offset + justification_left, - range: (glyph.range.start - range.start).saturating_as() - ..(glyph.range.end - range.start).saturating_as(), - span: glyph.span, - } - }) - .collect(); - - let item = TextItem { - font, - size: self.size, - lang, - fill: fill.clone(), - text: self.text[range.start - self.base..range.end - self.base].into(), - glyphs, - }; - - let width = item.width(); - if decos.is_empty() { - frame.push(pos, FrameItem::Text(item)); - } else { - // Apply line decorations. - frame.push(pos, FrameItem::Text(item.clone())); - for deco in &decos { - decorate(&mut frame, deco, &item, width, shift, pos); - } - } - - offset += width; - } - - // Apply metadata. - frame.meta(self.styles, false); - - frame - } - - /// Measure the top and bottom extent of this text. - fn measure(&self, vt: &Vt) -> (Abs, Abs) { - let mut top = Abs::zero(); - let mut bottom = Abs::zero(); - - let top_edge = TextElem::top_edge_in(self.styles); - let bottom_edge = TextElem::bottom_edge_in(self.styles); - - // Expand top and bottom by reading the font's vertical metrics. - let mut expand = |font: &Font, bbox: Option<ttf_parser::Rect>| { - top.set_max(top_edge.resolve(self.size, font, bbox)); - bottom.set_max(-bottom_edge.resolve(self.size, font, bbox)); - }; - - if self.glyphs.is_empty() { - // When there are no glyphs, we just use the vertical metrics of the - // first available font. - let world = vt.world; - for family in families(self.styles) { - if let Some(font) = world - .book() - .select(family, self.variant) - .and_then(|id| world.font(id)) - { - expand(&font, None); - break; - } - } - } else { - for g in self.glyphs.iter() { - let bbox = if top_edge.is_bounds() || bottom_edge.is_bounds() { - g.font.ttf().glyph_bounding_box(ttf_parser::GlyphId(g.glyph_id)) - } else { - None - }; - expand(&g.font, bbox); - } - } - - (top, bottom) - } - - /// How many glyphs are in the text where we can insert additional - /// space when encountering underfull lines. - pub fn justifiables(&self) -> usize { - self.glyphs.iter().filter(|g| g.is_justifiable()).count() - } - - /// Whether the last glyph is a CJK character which should not be justified - /// on line end. - pub fn cjk_justifiable_at_last(&self) -> bool { - self.glyphs - .last() - .map(|g| g.is_cjk_script() || g.is_cjk_punctuation()) - .unwrap_or(false) - } - - /// The stretchability of the text. - pub fn stretchability(&self) -> Abs { - self.glyphs - .iter() - .map(|g| g.stretchability().0 + g.stretchability().1) - .sum::<Em>() - .at(self.size) - } - - /// The shrinkability of the text - pub fn shrinkability(&self) -> Abs { - self.glyphs - .iter() - .map(|g| g.shrinkability().0 + g.shrinkability().1) - .sum::<Em>() - .at(self.size) - } - - /// Reshape a range of the shaped text, reusing information from this - /// shaping process if possible. - /// - /// The text `range` is relative to the whole paragraph. - pub fn reshape( - &'a self, - vt: &Vt, - spans: &SpanMapper, - text_range: Range<usize>, - ) -> ShapedText<'a> { - let text = &self.text[text_range.start - self.base..text_range.end - self.base]; - if let Some(glyphs) = self.slice_safe_to_break(text_range.clone()) { - #[cfg(debug_assertions)] - assert_all_glyphs_in_range(glyphs, text, text_range.clone()); - Self { - base: text_range.start, - text, - dir: self.dir, - lang: self.lang, - region: self.region, - styles: self.styles, - size: self.size, - variant: self.variant, - width: glyphs.iter().map(|g| g.x_advance).sum::<Em>().at(self.size), - glyphs: Cow::Borrowed(glyphs), - } - } else { - shape( - vt, - text_range.start, - text, - spans, - self.styles, - self.dir, - self.lang, - self.region, - ) - } - } - - /// Push a hyphen to end of the text. - pub fn push_hyphen(&mut self, vt: &Vt, fallback: bool) { - let world = vt.world; - let book = world.book(); - let fallback_func = if fallback { - Some(|| book.select_fallback(None, self.variant, "-")) - } else { - None - }; - let mut chain = families(self.styles) - .map(|family| book.select(family, self.variant)) - .chain(fallback_func.iter().map(|f| f())) - .flatten(); - - chain.find_map(|id| { - let font = world.font(id)?; - let ttf = font.ttf(); - let glyph_id = ttf.glyph_index('-')?; - let x_advance = font.to_em(ttf.glyph_hor_advance(glyph_id)?); - let range = self - .glyphs - .last() - .map(|g| g.range.end..g.range.end) - // In the unlikely chance that we hyphenate after an empty line, - // ensure that the glyph range still falls after self.base so - // that subtracting either of the endpoints by self.base doesn't - // underflow. See <https://github.com/typst/typst/issues/2283>. - .unwrap_or_else(|| self.base..self.base); - self.width += x_advance.at(self.size); - self.glyphs.to_mut().push(ShapedGlyph { - font, - glyph_id: glyph_id.0, - x_advance, - x_offset: Em::zero(), - y_offset: Em::zero(), - adjustability: Adjustability::default(), - range, - safe_to_break: true, - c: '-', - span: (Span::detached(), 0), - is_justifiable: false, - script: Script::Common, - }); - Some(()) - }); - } - - /// Find the subslice of glyphs that represent the given text range if both - /// sides are safe to break. - fn slice_safe_to_break(&self, text_range: Range<usize>) -> Option<&[ShapedGlyph]> { - let Range { mut start, mut end } = text_range; - if !self.dir.is_positive() { - std::mem::swap(&mut start, &mut end); - } - - let left = self.find_safe_to_break(start, Side::Left)?; - let right = self.find_safe_to_break(end, Side::Right)?; - Some(&self.glyphs[left..right]) - } - - /// Find the glyph offset matching the text index that is most towards the - /// given side and safe-to-break. - fn find_safe_to_break(&self, text_index: usize, towards: Side) -> Option<usize> { - let ltr = self.dir.is_positive(); - - // Handle edge cases. - let len = self.glyphs.len(); - if text_index == self.base { - return Some(if ltr { 0 } else { len }); - } else if text_index == self.base + self.text.len() { - return Some(if ltr { len } else { 0 }); - } - - // Find any glyph with the text index. - let found = self.glyphs.binary_search_by(|g: &ShapedGlyph| { - let ordering = g.range.start.cmp(&text_index); - if ltr { - ordering - } else { - ordering.reverse() - } - }); - let mut idx = match found { - Ok(idx) => idx, - Err(idx) => { - // Handle the special case where we break before a '\n' - // - // For example: (assume `a` is a CJK character with three bytes) - // text: " a \n b " - // index: 0 1 2 3 4 5 - // text_index: ^ - // glyphs: 0 . 1 - // - // We will get found = Err(1), because '\n' does not have a glyph. - // But it's safe to break here. Thus the following condition: - // - glyphs[0].end == text_index == 3 - // - text[3] == '\n' - return (idx > 0 - && self.glyphs[idx - 1].range.end == text_index - && self.text[text_index - self.base..].starts_with('\n')) - .then_some(idx); - } - }; - - let next = match towards { - Side::Left => usize::checked_sub, - Side::Right => usize::checked_add, - }; - - // Search for the outermost glyph with the text index. - while let Some(next) = next(idx, 1) { - if self.glyphs.get(next).map_or(true, |g| g.range.start != text_index) { - break; - } - idx = next; - } - - // RTL needs offset one because the left side of the range should be - // exclusive and the right side inclusive, contrary to the normal - // behaviour of ranges. - self.glyphs[idx].safe_to_break.then_some(idx + usize::from(!ltr)) - } -} - -impl Debug for ShapedText<'_> { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - self.text.fmt(f) - } -} - -/// Holds shaping results and metadata common to all shaped segments. -struct ShapingContext<'a, 'v> { - vt: &'a Vt<'v>, - spans: &'a SpanMapper, - glyphs: Vec<ShapedGlyph>, - used: Vec<Font>, - styles: StyleChain<'a>, - size: Abs, - variant: FontVariant, - tags: Vec<rustybuzz::Feature>, - fallback: bool, - dir: Dir, -} - -/// Shape text into [`ShapedText`]. -#[allow(clippy::too_many_arguments)] -pub fn shape<'a>( - vt: &Vt, - base: usize, - text: &'a str, - spans: &SpanMapper, - styles: StyleChain<'a>, - dir: Dir, - lang: Lang, - region: Option<Region>, -) -> ShapedText<'a> { - let size = TextElem::size_in(styles); - let mut ctx = ShapingContext { - vt, - spans, - size, - glyphs: vec![], - used: vec![], - styles, - variant: variant(styles), - tags: tags(styles), - fallback: TextElem::fallback_in(styles), - dir, - }; - - if !text.is_empty() { - shape_segment(&mut ctx, base, text, families(styles)); - } - - track_and_space(&mut ctx); - calculate_adjustability(&mut ctx, lang, region); - - #[cfg(debug_assertions)] - assert_all_glyphs_in_range(&ctx.glyphs, text, base..(base + text.len())); - #[cfg(debug_assertions)] - assert_glyph_ranges_in_order(&ctx.glyphs, dir); - - ShapedText { - base, - text, - dir, - lang, - region, - styles, - variant: ctx.variant, - size, - width: ctx.glyphs.iter().map(|g| g.x_advance).sum::<Em>().at(size), - glyphs: Cow::Owned(ctx.glyphs), - } -} - -/// Shape text with font fallback using the `families` iterator. -fn shape_segment<'a>( - ctx: &mut ShapingContext, - base: usize, - text: &str, - mut families: impl Iterator<Item = &'a str> + Clone, -) { - // Fonts dont have newlines and tabs. - if text.chars().all(|c| c == '\n' || c == '\t') { - return; - } - - // Find the next available family. - let world = ctx.vt.world; - let book = world.book(); - let mut selection = families.find_map(|family| { - book.select(family, ctx.variant) - .and_then(|id| world.font(id)) - .filter(|font| !ctx.used.contains(font)) - }); - - // Do font fallback if the families are exhausted and fallback is enabled. - if selection.is_none() && ctx.fallback { - let first = ctx.used.first().map(Font::info); - selection = book - .select_fallback(first, ctx.variant, text) - .and_then(|id| world.font(id)) - .filter(|font| !ctx.used.contains(font)); - } - - // Extract the font id or shape notdef glyphs if we couldn't find any font. - let Some(font) = selection else { - if let Some(font) = ctx.used.first().cloned() { - shape_tofus(ctx, base, text, font); - } - return; - }; - - ctx.used.push(font.clone()); - - // Fill the buffer with our text. - let mut buffer = UnicodeBuffer::new(); - buffer.push_str(text); - buffer.set_language(language(ctx.styles)); - if let Some(script) = TextElem::script_in(ctx.styles).as_custom().and_then(|script| { - rustybuzz::Script::from_iso15924_tag(Tag::from_bytes(script.as_bytes())) - }) { - buffer.set_script(script) - } - buffer.set_direction(match ctx.dir { - Dir::LTR => rustybuzz::Direction::LeftToRight, - Dir::RTL => rustybuzz::Direction::RightToLeft, - _ => unimplemented!("vertical text layout"), - }); - - // Shape! - let buffer = rustybuzz::shape(font.rusty(), &ctx.tags, buffer); - let infos = buffer.glyph_infos(); - let pos = buffer.glyph_positions(); - let ltr = ctx.dir.is_positive(); - - // Collect the shaped glyphs, doing fallback and shaping parts again with - // the next font if necessary. - let mut i = 0; - while i < infos.len() { - let info = &infos[i]; - let cluster = info.cluster as usize; - - // Add the glyph to the shaped output. - if info.glyph_id != 0 { - // Determine the text range of the glyph. - let start = base + cluster; - let end = base - + if ltr { i.checked_add(1) } else { i.checked_sub(1) } - .and_then(|last| infos.get(last)) - .map_or(text.len(), |info| info.cluster as usize); - - let c = text[cluster..].chars().next().unwrap(); - let script = c.script(); - let x_advance = font.to_em(pos[i].x_advance); - ctx.glyphs.push(ShapedGlyph { - font: font.clone(), - glyph_id: info.glyph_id as u16, - // TODO: Don't ignore y_advance. - x_advance, - x_offset: font.to_em(pos[i].x_offset), - y_offset: font.to_em(pos[i].y_offset), - adjustability: Adjustability::default(), - range: start..end, - safe_to_break: !info.unsafe_to_break(), - c, - span: ctx.spans.span_at(start), - is_justifiable: is_justifiable( - c, - script, - x_advance, - Adjustability::default().stretchability, - ), - script, - }); - } else { - // First, search for the end of the tofu sequence. - let k = i; - while infos.get(i + 1).map_or(false, |info| info.glyph_id == 0) { - i += 1; - } - - // Then, determine the start and end text index for the tofu - // sequence. - // - // Examples: - // Everything is shown in visual order. Tofus are written as "_". - // We want to find out that the tofus span the text `2..6`. - // Note that the clusters are longer than 1 char. - // - // Left-to-right: - // Text: h a l i h a l l o - // Glyphs: A _ _ C E - // Clusters: 0 2 4 6 8 - // k=1 i=2 - // - // Right-to-left: - // Text: O L L A H I L A H - // Glyphs: E C _ _ A - // Clusters: 8 6 4 2 0 - // k=2 i=3 - let start = infos[if ltr { k } else { i }].cluster as usize; - let end = if ltr { i.checked_add(1) } else { k.checked_sub(1) } - .and_then(|last| infos.get(last)) - .map_or(text.len(), |info| info.cluster as usize); - - // Trim half-baked cluster. - let remove = base + start..base + end; - while ctx.glyphs.last().map_or(false, |g| remove.contains(&g.range.start)) { - ctx.glyphs.pop(); - } - - // Recursively shape the tofu sequence with the next family. - shape_segment(ctx, base + start, &text[start..end], families.clone()); - } - - i += 1; - } - - ctx.used.pop(); -} - -/// Shape the text with tofus from the given font. -fn shape_tofus(ctx: &mut ShapingContext, base: usize, text: &str, font: Font) { - let x_advance = font.advance(0).unwrap_or_default(); - let add_glyph = |(cluster, c): (usize, char)| { - let start = base + cluster; - let end = start + c.len_utf8(); - let script = c.script(); - ctx.glyphs.push(ShapedGlyph { - font: font.clone(), - glyph_id: 0, - x_advance, - x_offset: Em::zero(), - y_offset: Em::zero(), - adjustability: Adjustability::default(), - range: start..end, - safe_to_break: true, - c, - span: ctx.spans.span_at(start), - is_justifiable: is_justifiable( - c, - script, - x_advance, - Adjustability::default().stretchability, - ), - script, - }); - }; - if ctx.dir.is_positive() { - text.char_indices().for_each(add_glyph); - } else { - text.char_indices().rev().for_each(add_glyph); - } -} - -/// Apply tracking and spacing to the shaped glyphs. -fn track_and_space(ctx: &mut ShapingContext) { - let tracking = Em::from_length(TextElem::tracking_in(ctx.styles), ctx.size); - let spacing = - TextElem::spacing_in(ctx.styles).map(|abs| Em::from_length(abs, ctx.size)); - - let mut glyphs = ctx.glyphs.iter_mut().peekable(); - while let Some(glyph) = glyphs.next() { - // Make non-breaking space same width as normal space. - if glyph.c == '\u{00A0}' { - glyph.x_advance -= nbsp_delta(&glyph.font).unwrap_or_default(); - } - - if glyph.is_space() { - glyph.x_advance = spacing.relative_to(glyph.x_advance); - } - - if glyphs - .peek() - .map_or(false, |next| glyph.range.start != next.range.start) - { - glyph.x_advance += tracking; - } - } -} - -pub fn is_gb_style(lang: Lang, region: Option<Region>) -> bool { - // Most CJK variants, including zh-CN, ja-JP, zh-SG, zh-MY use GB-style punctuation, - // while zh-HK and zh-TW use alternative style. We default to use GB-style. - !(lang == Lang::CHINESE - && matches!(region.as_ref().map(Region::as_str), Some("TW" | "HK"))) -} - -/// Calculate stretchability and shrinkability of each glyph, -/// and CJK punctuation adjustments according to Chinese Layout Requirements. -fn calculate_adjustability(ctx: &mut ShapingContext, lang: Lang, region: Option<Region>) { - let gb_style = is_gb_style(lang, region); - - for glyph in &mut ctx.glyphs { - glyph.adjustability = glyph.base_adjustability(gb_style); - } - - let mut glyphs = ctx.glyphs.iter_mut().peekable(); - while let Some(glyph) = glyphs.next() { - // Only GB style needs further adjustment. - if glyph.is_cjk_punctuation() && !gb_style { - continue; - } - - // Now we apply consecutive punctuation adjustment, specified in Chinese Layout - // Requirements, section 3.1.6.1 Punctuation Adjustment Space, and Japanese Layout - // Requirements, section 3.1 Line Composition Rules for Punctuation Marks - let Some(next) = glyphs.peek_mut() else { continue }; - let width = glyph.x_advance; - let delta = width / 2.0; - if glyph.is_cjk_punctuation() - && next.is_cjk_punctuation() - && (glyph.shrinkability().1 + next.shrinkability().0) >= delta - { - let left_delta = glyph.shrinkability().1.min(delta); - glyph.shrink_right(left_delta); - next.shrink_left(delta - left_delta); - } - } -} - -/// Difference between non-breaking and normal space. -fn nbsp_delta(font: &Font) -> Option<Em> { - let space = font.ttf().glyph_index(' ')?.0; - let nbsp = font.ttf().glyph_index('\u{00A0}')?.0; - Some(font.advance(nbsp)? - font.advance(space)?) -} - -/// Resolve the font variant. -pub fn variant(styles: StyleChain) -> FontVariant { - let mut variant = FontVariant::new( - TextElem::style_in(styles), - TextElem::weight_in(styles), - TextElem::stretch_in(styles), - ); - - let delta = TextElem::delta_in(styles); - variant.weight = variant - .weight - .thicken(delta.clamp(i16::MIN as i64, i16::MAX as i64) as i16); - - if TextElem::emph_in(styles) { - variant.style = match variant.style { - FontStyle::Normal => FontStyle::Italic, - FontStyle::Italic => FontStyle::Normal, - FontStyle::Oblique => FontStyle::Normal, - } - } - - variant -} - -/// Resolve a prioritized iterator over the font families. -pub fn families(styles: StyleChain) -> impl Iterator<Item = &str> + Clone { - const FALLBACKS: &[&str] = &[ - "linux libertine", - "twitter color emoji", - "noto color emoji", - "apple color emoji", - "segoe ui emoji", - ]; - - let tail = if TextElem::fallback_in(styles) { FALLBACKS } else { &[] }; - TextElem::font_in(styles) - .into_iter() - .map(|family| family.as_str()) - .chain(tail.iter().copied()) -} - -/// Collect the tags of the OpenType features to apply. -pub fn tags(styles: StyleChain) -> Vec<Feature> { - let mut tags = vec![]; - let mut feat = |tag, value| { - tags.push(Feature::new(Tag::from_bytes(tag), value, ..)); - }; - - // Features that are on by default in Harfbuzz are only added if disabled. - if !TextElem::kerning_in(styles) { - feat(b"kern", 0); - } - - // Features that are off by default in Harfbuzz are only added if enabled. - if TextElem::smallcaps_in(styles) { - feat(b"smcp", 1); - } - - if TextElem::alternates_in(styles) { - feat(b"salt", 1); - } - - let storage; - if let Some(set) = TextElem::stylistic_set_in(styles) { - storage = [b's', b's', b'0' + set.get() / 10, b'0' + set.get() % 10]; - feat(&storage, 1); - } - - if !TextElem::ligatures_in(styles) { - feat(b"liga", 0); - feat(b"clig", 0); - } - - if TextElem::discretionary_ligatures_in(styles) { - feat(b"dlig", 1); - } - - if TextElem::historical_ligatures_in(styles) { - feat(b"hilg", 1); - } - - match TextElem::number_type_in(styles) { - Smart::Auto => {} - Smart::Custom(NumberType::Lining) => feat(b"lnum", 1), - Smart::Custom(NumberType::OldStyle) => feat(b"onum", 1), - } - - match TextElem::number_width_in(styles) { - Smart::Auto => {} - Smart::Custom(NumberWidth::Proportional) => feat(b"pnum", 1), - Smart::Custom(NumberWidth::Tabular) => feat(b"tnum", 1), - } - - if TextElem::slashed_zero_in(styles) { - feat(b"zero", 1); - } - - if TextElem::fractions_in(styles) { - feat(b"frac", 1); - } - - for (tag, value) in TextElem::features_in(styles).0 { - tags.push(Feature::new(tag, value, ..)) - } - - tags -} - -/// Process the language and and region of a style chain into a -/// rustybuzz-compatible BCP 47 language. -fn language(styles: StyleChain) -> rustybuzz::Language { - let mut bcp: EcoString = TextElem::lang_in(styles).as_str().into(); - if let Some(region) = TextElem::region_in(styles) { - bcp.push('-'); - bcp.push_str(region.as_str()); - } - rustybuzz::Language::from_str(&bcp).unwrap() -} - -/// Returns true if all glyphs in `glyphs` have ranges within the range `range`. -#[cfg(debug_assertions)] -fn assert_all_glyphs_in_range(glyphs: &[ShapedGlyph], text: &str, range: Range<usize>) { - if glyphs - .iter() - .any(|g| g.range.start < range.start || g.range.end > range.end) - { - panic!("one or more glyphs in {text:?} fell out of range"); - } -} - -/// Asserts that the ranges of `glyphs` is in the proper order according to `dir`. -/// -/// This asserts instead of returning a bool in order to provide a more informative message when the invariant is violated. -#[cfg(debug_assertions)] -fn assert_glyph_ranges_in_order(glyphs: &[ShapedGlyph], dir: Dir) { - if glyphs.is_empty() { - return; - } - - // Iterator::is_sorted and friends are unstable as of Rust 1.70.0 - for i in 0..(glyphs.len() - 1) { - let a = &glyphs[i]; - let b = &glyphs[i + 1]; - let ord = a.range.start.cmp(&b.range.start); - let ord = if dir.is_positive() { ord } else { ord.reverse() }; - if ord == std::cmp::Ordering::Greater { - panic!( - "glyph ranges should be monotonically {}, \ - but found glyphs out of order:\n\n\ - first: {a:#?}\nsecond: {b:#?}", - if dir.is_positive() { "increasing" } else { "decreasing" }, - ); - } - } -} - -// The CJK punctuation that can appear at the beginning or end of a line. -pub(crate) const BEGIN_PUNCT_PAT: &[char] = - &['“', '‘', '《', '〈', '(', '『', '「', '【', '〖', '〔', '[', '{']; -pub(crate) const END_PUNCT_PAT: &[char] = &[ - '”', '’', ',', '.', '。', '、', ':', ';', '》', '〉', ')', '』', '」', '】', - '〗', '〕', ']', '}', '?', '!', -]; - -/// Whether the glyph is a space. -#[inline] -fn is_space(c: char) -> bool { - matches!(c, ' ' | '\u{00A0}' | ' ') -} - -/// Whether the glyph is part of a CJK script. -#[inline] -pub fn char_is_cjk_script(c: char) -> bool { - is_cjk_script(c, c.script()) -} - -/// Whether the glyph is part of a CJK script. -#[inline] -fn is_cjk_script(c: char, script: Script) -> bool { - use Script::*; - // U+30FC: Katakana-Hiragana Prolonged Sound Mark - matches!(script, Hiragana | Katakana | Han) || c == '\u{30FC}' -} - -/// See <https://www.w3.org/TR/clreq/#punctuation_width_adjustment> -#[inline] -fn is_cjk_left_aligned_punctuation( - c: char, - x_advance: Em, - stretchability: (Em, Em), - gb_style: bool, -) -> bool { - // CJK quotation marks shares codepoints with latin quotation marks. - // But only the CJK ones have full width. - if matches!(c, '”' | '’') && x_advance + stretchability.1 == Em::one() { - return true; - } - - if gb_style && matches!(c, ',' | '。' | '.' | '、' | ':' | ';' | '!' | '?') - { - // In GB style, exclamations and question marks are also left aligned and can be adjusted. - // Note that they are not adjustable in other styles. - return true; - } - - // See appendix A.3 https://www.w3.org/TR/clreq/#tables_of_chinese_punctuation_marks - matches!(c, '》' | ')' | '』' | '」' | '】' | '〗' | '〕' | '〉' | ']' | '}') -} - -/// See <https://www.w3.org/TR/clreq/#punctuation_width_adjustment> -#[inline] -fn is_cjk_right_aligned_punctuation( - c: char, - x_advance: Em, - stretchability: (Em, Em), -) -> bool { - // CJK quotation marks shares codepoints with latin quotation marks. - // But only the CJK ones have full width. - if matches!(c, '“' | '‘') && x_advance + stretchability.0 == Em::one() { - return true; - } - // See appendix A.3 https://www.w3.org/TR/clreq/#tables_of_chinese_punctuation_marks - matches!(c, '《' | '(' | '『' | '「' | '【' | '〖' | '〔' | '〈' | '[' | '{') -} - -/// See <https://www.w3.org/TR/clreq/#punctuation_width_adjustment> -#[inline] -fn is_cjk_center_aligned_punctuation(c: char, gb_style: bool) -> bool { - if !gb_style && matches!(c, ',' | '。' | '.' | '、' | ':' | ';') { - return true; - } - - // U+30FB: Katakana Middle Dot - matches!(c, '\u{30FB}') -} - -/// Whether the glyph is justifiable. -/// -/// Quotations in latin script and CJK are unfortunately the same codepoint -/// (U+2018, U+2019, U+201C, U+201D), but quotations in Chinese must be -/// fullwidth. This heuristics can therefore fail for monospace latin fonts. -/// However, since monospace fonts are usually not justified this edge case -/// should be rare enough. -#[inline] -fn is_justifiable( - c: char, - script: Script, - x_advance: Em, - stretchability: (Em, Em), -) -> bool { - // GB style is not relevant here. - is_space(c) - || is_cjk_script(c, script) - || is_cjk_left_aligned_punctuation(c, x_advance, stretchability, true) - || is_cjk_right_aligned_punctuation(c, x_advance, stretchability) - || is_cjk_center_aligned_punctuation(c, true) -} diff --git a/crates/typst-library/src/text/shift.rs b/crates/typst-library/src/text/shift.rs deleted file mode 100644 index a1862098..00000000 --- a/crates/typst-library/src/text/shift.rs +++ /dev/null @@ -1,223 +0,0 @@ -use crate::prelude::*; -use crate::text::{variant, SpaceElem, TextElem, TextSize}; - -/// Renders text in subscript. -/// -/// The text is rendered smaller and its baseline is lowered. -/// -/// # Example -/// ```example -/// Revenue#sub[yearly] -/// ``` -#[elem(title = "Subscript", Show)] -pub struct SubElem { - /// Whether to prefer the dedicated subscript characters of the font. - /// - /// If this is enabled, Typst first tries to transform the text to subscript - /// codepoints. If that fails, it falls back to rendering lowered and shrunk - /// normal letters. - /// - /// ```example - /// N#sub(typographic: true)[1] - /// N#sub(typographic: false)[1] - /// ``` - #[default(true)] - pub typographic: bool, - - /// The baseline shift for synthetic subscripts. Does not apply if - /// `typographic` is true and the font has subscript codepoints for the - /// given `body`. - #[default(Em::new(0.2).into())] - pub baseline: Length, - - /// The font size for synthetic subscripts. Does not apply if - /// `typographic` is true and the font has subscript codepoints for the - /// given `body`. - #[default(TextSize(Em::new(0.6).into()))] - pub size: TextSize, - - /// The text to display in subscript. - #[required] - pub body: Content, -} - -impl Show for SubElem { - #[tracing::instrument(name = "SubElem::show", skip_all)] - fn show(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Content> { - let body = self.body().clone(); - let mut transformed = None; - if self.typographic(styles) { - if let Some(text) = search_text(&body, true) { - if is_shapable(vt, &text, styles) { - transformed = Some(TextElem::packed(text)); - } - } - }; - - Ok(transformed.unwrap_or_else(|| { - body.styled(TextElem::set_baseline(self.baseline(styles))) - .styled(TextElem::set_size(self.size(styles))) - })) - } -} - -/// Renders text in superscript. -/// -/// The text is rendered smaller and its baseline is raised. -/// -/// # Example -/// ```example -/// 1#super[st] try! -/// ``` -#[elem(title = "Superscript", Show)] -pub struct SuperElem { - /// Whether to prefer the dedicated superscript characters of the font. - /// - /// If this is enabled, Typst first tries to transform the text to - /// superscript codepoints. If that fails, it falls back to rendering - /// raised and shrunk normal letters. - /// - /// ```example - /// N#super(typographic: true)[1] - /// N#super(typographic: false)[1] - /// ``` - #[default(true)] - pub typographic: bool, - - /// The baseline shift for synthetic superscripts. Does not apply if - /// `typographic` is true and the font has superscript codepoints for the - /// given `body`. - #[default(Em::new(-0.5).into())] - pub baseline: Length, - - /// The font size for synthetic superscripts. Does not apply if - /// `typographic` is true and the font has superscript codepoints for the - /// given `body`. - #[default(TextSize(Em::new(0.6).into()))] - pub size: TextSize, - - /// The text to display in superscript. - #[required] - pub body: Content, -} - -impl Show for SuperElem { - #[tracing::instrument(name = "SuperElem::show", skip_all)] - fn show(&self, vt: &mut Vt, styles: StyleChain) -> SourceResult<Content> { - let body = self.body().clone(); - let mut transformed = None; - if self.typographic(styles) { - if let Some(text) = search_text(&body, false) { - if is_shapable(vt, &text, styles) { - transformed = Some(TextElem::packed(text)); - } - } - }; - - Ok(transformed.unwrap_or_else(|| { - body.styled(TextElem::set_baseline(self.baseline(styles))) - .styled(TextElem::set_size(self.size(styles))) - })) - } -} - -/// Find and transform the text contained in `content` to the given script kind -/// if and only if it only consists of `Text`, `Space`, and `Empty` leafs. -fn search_text(content: &Content, sub: bool) -> Option<EcoString> { - if content.is::<SpaceElem>() { - Some(' '.into()) - } else if let Some(elem) = content.to::<TextElem>() { - convert_script(elem.text(), sub) - } else if let Some(children) = content.to_sequence() { - let mut full = EcoString::new(); - for item in children { - match search_text(item, sub) { - Some(text) => full.push_str(&text), - None => return None, - } - } - Some(full) - } else { - None - } -} - -/// Checks whether the first retrievable family contains all code points of the -/// given string. -fn is_shapable(vt: &Vt, text: &str, styles: StyleChain) -> bool { - let world = vt.world; - for family in TextElem::font_in(styles) { - if let Some(font) = world - .book() - .select(family.as_str(), variant(styles)) - .and_then(|id| world.font(id)) - { - return text.chars().all(|c| font.ttf().glyph_index(c).is_some()); - } - } - - false -} - -/// Convert a string to sub- or superscript codepoints if all characters -/// can be mapped to such a codepoint. -fn convert_script(text: &str, sub: bool) -> Option<EcoString> { - let mut result = EcoString::with_capacity(text.len()); - let converter = if sub { to_subscript_codepoint } else { to_superscript_codepoint }; - - for c in text.chars() { - match converter(c) { - Some(c) => result.push(c), - None => return None, - } - } - - Some(result) -} - -/// Convert a character to its corresponding Unicode superscript. -fn to_superscript_codepoint(c: char) -> Option<char> { - char::from_u32(match c { - '0' => 0x2070, - '1' => 0x00B9, - '2' => 0x00B2, - '3' => 0x00B3, - '4'..='9' => 0x2070 + (c as u32 + 4 - '4' as u32), - '+' => 0x207A, - '-' => 0x207B, - '=' => 0x207C, - '(' => 0x207D, - ')' => 0x207E, - 'n' => 0x207F, - 'i' => 0x2071, - ' ' => 0x0020, - _ => return None, - }) -} - -/// Convert a character to its corresponding Unicode subscript. -fn to_subscript_codepoint(c: char) -> Option<char> { - char::from_u32(match c { - '0' => 0x2080, - '1'..='9' => 0x2080 + (c as u32 - '0' as u32), - '+' => 0x208A, - '-' => 0x208B, - '=' => 0x208C, - '(' => 0x208D, - ')' => 0x208E, - 'a' => 0x2090, - 'e' => 0x2091, - 'o' => 0x2092, - 'x' => 0x2093, - 'h' => 0x2095, - 'k' => 0x2096, - 'l' => 0x2097, - 'm' => 0x2098, - 'n' => 0x2099, - 'p' => 0x209A, - 's' => 0x209B, - 't' => 0x209C, - ' ' => 0x0020, - _ => return None, - }) -} diff --git a/crates/typst-library/src/visualize/image.rs b/crates/typst-library/src/visualize/image.rs deleted file mode 100644 index 0996ae7c..00000000 --- a/crates/typst-library/src/visualize/image.rs +++ /dev/null @@ -1,271 +0,0 @@ -use std::ffi::OsStr; - -use typst::image::{Image, ImageFormat, RasterFormat, VectorFormat}; -use typst::util::option_eq; - -use crate::compute::Readable; -use crate::meta::Figurable; -use crate::prelude::*; -use crate::text::families; - -/// A raster or vector graphic. -/// -/// Supported formats are PNG, JPEG, GIF and SVG. -/// -/// _Note:_ Work on SVG export is ongoing and there might be visual inaccuracies -/// in the resulting PDF. Make sure to double-check embedded SVG images. If you -/// have an issue, also feel free to report it on [GitHub][gh-svg]. -/// -/// # Example -/// ```example -/// #figure( -/// image("molecular.jpg", width: 80%), -/// caption: [ -/// A step in the molecular testing -/// pipeline of our lab. -/// ], -/// ) -/// ``` -/// -/// [gh-svg]: https://github.com/typst/typst/issues?q=is%3Aopen+is%3Aissue+label%3Asvg -#[elem(scope, Layout, LocalName, Figurable)] -pub struct ImageElem { - /// Path to an image file. - #[required] - #[parse( - let Spanned { v: path, span } = - args.expect::<Spanned<EcoString>>("path to image file")?; - let id = vm.resolve_path(&path).at(span)?; - let data = vm.world().file(id).at(span)?; - path - )] - #[borrowed] - pub path: EcoString, - - /// The raw file data. - #[internal] - #[required] - #[parse(Readable::Bytes(data))] - pub data: Readable, - - /// The image's format. Detected automatically by default. - pub format: Smart<ImageFormat>, - - /// The width of the image. - pub width: Smart<Rel<Length>>, - - /// The height of the image. - pub height: Smart<Rel<Length>>, - - /// A text describing the image. - pub alt: Option<EcoString>, - - /// How the image should adjust itself to a given area. - #[default(ImageFit::Cover)] - pub fit: ImageFit, -} - -#[scope] -impl ImageElem { - /// Decode a raster or vector graphic from bytes or a string. - /// - /// ```example - /// #let original = read("diagram.svg") - /// #let changed = original.replace( - /// "#2B80FF", // blue - /// green.to-hex(), - /// ) - /// - /// #image.decode(original) - /// #image.decode(changed) - /// ``` - #[func(title = "Decode Image")] - pub fn decode( - /// The data to decode as an image. Can be a string for SVGs. - data: Readable, - /// The image's format. Detected automatically by default. - #[named] - format: Option<Smart<ImageFormat>>, - /// The width of the image. - #[named] - width: Option<Smart<Rel<Length>>>, - /// The height of the image. - #[named] - height: Option<Smart<Rel<Length>>>, - /// A text describing the image. - #[named] - alt: Option<Option<EcoString>>, - /// How the image should adjust itself to a given area. - #[named] - fit: Option<ImageFit>, - ) -> StrResult<Content> { - let mut elem = ImageElem::new(EcoString::new(), data); - if let Some(format) = format { - elem.push_format(format); - } - if let Some(width) = width { - elem.push_width(width); - } - if let Some(height) = height { - elem.push_height(height); - } - if let Some(alt) = alt { - elem.push_alt(alt); - } - if let Some(fit) = fit { - elem.push_fit(fit); - } - Ok(elem.pack()) - } -} - -impl Layout for ImageElem { - #[tracing::instrument(name = "ImageElem::layout", skip_all)] - fn layout( - &self, - vt: &mut Vt, - styles: StyleChain, - regions: Regions, - ) -> SourceResult<Fragment> { - // Take the format that was explicitly defined, or parse the extension, - // or try to detect the format. - let data = self.data(); - let format = match self.format(styles) { - Smart::Custom(v) => v, - Smart::Auto => { - let ext = std::path::Path::new(self.path().as_str()) - .extension() - .and_then(OsStr::to_str) - .unwrap_or_default() - .to_lowercase(); - - match ext.as_str() { - "png" => ImageFormat::Raster(RasterFormat::Png), - "jpg" | "jpeg" => ImageFormat::Raster(RasterFormat::Jpg), - "gif" => ImageFormat::Raster(RasterFormat::Gif), - "svg" | "svgz" => ImageFormat::Vector(VectorFormat::Svg), - _ => match &data { - Readable::Str(_) => ImageFormat::Vector(VectorFormat::Svg), - Readable::Bytes(bytes) => match RasterFormat::detect(bytes) { - Some(f) => ImageFormat::Raster(f), - None => bail!(self.span(), "unknown image format"), - }, - }, - } - } - }; - - let image = Image::with_fonts( - data.clone().into(), - format, - self.alt(styles), - vt.world, - &families(styles).map(|s| s.into()).collect::<Vec<_>>(), - ) - .at(self.span())?; - - let sizing = Axes::new(self.width(styles), self.height(styles)); - let region = sizing - .zip_map(regions.base(), |s, r| s.map(|v| v.resolve(styles).relative_to(r))) - .unwrap_or(regions.base()); - - let expand = sizing.as_ref().map(Smart::is_custom) | regions.expand; - let region_ratio = region.x / region.y; - - // Find out whether the image is wider or taller than the target size. - let pxw = image.width() as f64; - let pxh = image.height() as f64; - let px_ratio = pxw / pxh; - let wide = px_ratio > region_ratio; - - // The space into which the image will be placed according to its fit. - let target = if expand.x && expand.y { - region - } else if expand.x || (!expand.y && wide && region.x.is_finite()) { - Size::new(region.x, region.y.min(region.x.safe_div(px_ratio))) - } else if region.y.is_finite() { - Size::new(region.x.min(region.y * px_ratio), region.y) - } else { - Size::new(Abs::pt(pxw), Abs::pt(pxh)) - }; - - // Compute the actual size of the fitted image. - let fit = self.fit(styles); - let fitted = match fit { - ImageFit::Cover | ImageFit::Contain => { - if wide == (fit == ImageFit::Contain) { - Size::new(target.x, target.x / px_ratio) - } else { - Size::new(target.y * px_ratio, target.y) - } - } - ImageFit::Stretch => target, - }; - - // First, place the image in a frame of exactly its size and then resize - // the frame to the target size, center aligning the image in the - // process. - let mut frame = Frame::soft(fitted); - frame.push(Point::zero(), FrameItem::Image(image, fitted, self.span())); - frame.resize(target, Axes::splat(FixedAlign::Center)); - - // Create a clipping group if only part of the image should be visible. - if fit == ImageFit::Cover && !target.fits(fitted) { - frame.clip(Path::rect(frame.size())); - } - - // Apply metadata. - frame.meta(styles, false); - - Ok(Fragment::frame(frame)) - } -} - -impl LocalName for ImageElem { - fn local_name(lang: Lang, region: Option<Region>) -> &'static str { - match lang { - Lang::ALBANIAN => "Figurë", - Lang::ARABIC => "شكل", - Lang::BOKMÅL => "Figur", - Lang::CHINESE if option_eq(region, "TW") => "圖", - Lang::CHINESE => "图", - Lang::CZECH => "Obrázek", - Lang::DANISH => "Figur", - Lang::DUTCH => "Figuur", - Lang::FILIPINO => "Pigura", - Lang::FINNISH => "Kuva", - Lang::FRENCH => "Fig.", - Lang::GERMAN => "Abbildung", - Lang::GREEK => "Σχήμα", - Lang::HUNGARIAN => "Ábra", - Lang::ITALIAN => "Figura", - Lang::NYNORSK => "Figur", - Lang::POLISH => "Rysunek", - Lang::PORTUGUESE => "Figura", - Lang::ROMANIAN => "Figura", - Lang::RUSSIAN => "Рис.", - Lang::SLOVENIAN => "Slika", - Lang::SPANISH => "Figura", - Lang::SWEDISH => "Figur", - Lang::TURKISH => "Şekil", - Lang::UKRAINIAN => "Рисунок", - Lang::VIETNAMESE => "Hình", - Lang::JAPANESE => "図", - Lang::ENGLISH | _ => "Figure", - } - } -} - -impl Figurable for ImageElem {} - -/// How an image should adjust itself to a given area. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)] -pub enum ImageFit { - /// The image should completely cover the area. This is the default. - Cover, - /// The image should be fully contained in the area. - Contain, - /// The image should be stretched so that it exactly fills the area, even if - /// this means that the image will be distorted. - Stretch, -} diff --git a/crates/typst-library/src/visualize/line.rs b/crates/typst-library/src/visualize/line.rs deleted file mode 100644 index da497cf4..00000000 --- a/crates/typst-library/src/visualize/line.rs +++ /dev/null @@ -1,86 +0,0 @@ -use crate::prelude::*; - -/// A line from one point to another. -/// -/// # Example -/// ```example -/// #set page(height: 100pt) -/// -/// #line(length: 100%) -/// #line(end: (50%, 50%)) -/// #line( -/// length: 4cm, -/// stroke: 2pt + maroon, -/// ) -/// ``` -#[elem(Layout)] -pub struct LineElem { - /// The start point of the line. - /// - /// Must be an array of exactly two relative lengths. - #[resolve] - pub start: Axes<Rel<Length>>, - - /// The offset from `start` where the line ends. - #[resolve] - pub end: Option<Axes<Rel<Length>>>, - - /// The line's length. This is only respected if `end` is `none`. - #[resolve] - #[default(Abs::pt(30.0).into())] - pub length: Rel<Length>, - - /// The angle at which the line points away from the origin. This is only - /// respected if `end` is `none`. - pub angle: Angle, - - /// How to [stroke]($stroke) the line. - /// - /// ```example - /// #set line(length: 100%) - /// #stack( - /// spacing: 1em, - /// line(stroke: 2pt + red), - /// line(stroke: (paint: blue, thickness: 4pt, cap: "round")), - /// line(stroke: (paint: blue, thickness: 1pt, dash: "dashed")), - /// line(stroke: (paint: blue, thickness: 1pt, dash: ("dot", 2pt, 4pt, 2pt))), - /// ) - /// ``` - #[resolve] - #[fold] - pub stroke: Stroke, -} - -impl Layout for LineElem { - #[tracing::instrument(name = "LineElem::layout", skip_all)] - fn layout( - &self, - _: &mut Vt, - styles: StyleChain, - regions: Regions, - ) -> SourceResult<Fragment> { - let resolve = - |axes: Axes<Rel<Abs>>| axes.zip_map(regions.base(), Rel::relative_to); - let start = resolve(self.start(styles)); - let delta = - self.end(styles).map(|end| resolve(end) - start).unwrap_or_else(|| { - let length = self.length(styles); - let angle = self.angle(styles); - let x = angle.cos() * length; - let y = angle.sin() * length; - resolve(Axes::new(x, y)) - }); - - let stroke = self.stroke(styles).unwrap_or_default(); - let size = start.max(start + delta).max(Size::zero()); - let target = regions.expand.select(regions.size, size); - - if !target.is_finite() { - bail!(error!(self.span(), "cannot create line with infinite length")); - } - let mut frame = Frame::soft(target); - let shape = Geometry::Line(delta.to_point()).stroked(stroke); - frame.push(start.to_point(), FrameItem::Shape(shape, self.span())); - Ok(Fragment::frame(frame)) - } -} diff --git a/crates/typst-library/src/visualize/mod.rs b/crates/typst-library/src/visualize/mod.rs deleted file mode 100644 index e39d50bc..00000000 --- a/crates/typst-library/src/visualize/mod.rs +++ /dev/null @@ -1,31 +0,0 @@ -//! Drawing and visualization. - -mod image; -mod line; -mod path; -mod polygon; -mod shape; - -pub use self::image::*; -pub use self::line::*; -pub use self::path::*; -pub use self::polygon::*; -pub use self::shape::*; - -use crate::prelude::*; - -/// Hook up all visualize definitions. -pub(super) fn define(global: &mut Scope) { - global.category("visualize"); - global.define_type::<Color>(); - global.define_type::<Gradient>(); - global.define_type::<Stroke>(); - global.define_elem::<ImageElem>(); - global.define_elem::<LineElem>(); - global.define_elem::<RectElem>(); - global.define_elem::<SquareElem>(); - global.define_elem::<EllipseElem>(); - global.define_elem::<CircleElem>(); - global.define_elem::<PolygonElem>(); - global.define_elem::<PathElem>(); -} diff --git a/crates/typst-library/src/visualize/path.rs b/crates/typst-library/src/visualize/path.rs deleted file mode 100644 index 43b11526..00000000 --- a/crates/typst-library/src/visualize/path.rs +++ /dev/null @@ -1,208 +0,0 @@ -use kurbo::{CubicBez, ParamCurveExtrema}; -use typst::eval::Reflect; - -use crate::prelude::*; - -use PathVertex::{AllControlPoints, MirroredControlPoint, Vertex}; - -/// A path through a list of points, connected by Bezier curves. -/// -/// # Example -/// ```example -/// #path( -/// fill: blue.lighten(80%), -/// stroke: blue, -/// closed: true, -/// (0pt, 50pt), -/// (100%, 50pt), -/// ((50%, 0pt), (40pt, 0pt)), -/// ) -/// ``` -#[elem(Layout)] -pub struct PathElem { - /// How to fill the path. - /// - /// When setting a fill, the default stroke disappears. To create a - /// rectangle with both fill and stroke, you have to configure both. - /// - /// Currently all paths are filled according to the [non-zero winding - /// rule](https://en.wikipedia.org/wiki/Nonzero-rule). - pub fill: Option<Paint>, - - /// How to [stroke]($stroke) the path. This can be: - /// - /// Can be set to `{none}` to disable the stroke or to `{auto}` for a - /// stroke of `{1pt}` black if and if only if no fill is given. - #[resolve] - #[fold] - pub stroke: Smart<Option<Stroke>>, - - /// Whether to close this path with one last bezier curve. This curve will - /// takes into account the adjacent control points. If you want to close - /// with a straight line, simply add one last point that's the same as the - /// start point. - #[default(false)] - pub closed: bool, - - /// The vertices of the path. - /// - /// Each vertex can be defined in 3 ways: - /// - /// - A regular point, as given to the [`line`]($line) or - /// [`polygon`]($polygon) function. - /// - An array of two points, the first being the vertex and the second - /// being the control point. The control point is expressed relative to - /// the vertex and is mirrored to get the second control point. The given - /// control point is the one that affects the curve coming _into_ this - /// vertex (even for the first point). The mirrored control point affects - /// the curve going out of this vertex. - /// - An array of three points, the first being the vertex and the next - /// being the control points (control point for curves coming in and out, - /// respectively). - #[variadic] - pub vertices: Vec<PathVertex>, -} - -impl Layout for PathElem { - #[tracing::instrument(name = "PathElem::layout", skip_all)] - fn layout( - &self, - _: &mut Vt, - styles: StyleChain, - regions: Regions, - ) -> SourceResult<Fragment> { - let resolve = |axes: Axes<Rel<Length>>| { - axes.resolve(styles) - .zip_map(regions.base(), Rel::relative_to) - .to_point() - }; - - let vertices = self.vertices(); - let points: Vec<Point> = vertices.iter().map(|c| resolve(c.vertex())).collect(); - - let mut size = Size::zero(); - if points.is_empty() { - return Ok(Fragment::frame(Frame::soft(size))); - } - - // Only create a path if there are more than zero points. - // Construct a closed path given all points. - let mut path = Path::new(); - path.move_to(points[0]); - - let mut add_cubic = - |from_point: Point, to_point: Point, from: PathVertex, to: PathVertex| { - let from_control_point = resolve(from.control_point_from()) + from_point; - let to_control_point = resolve(to.control_point_to()) + to_point; - path.cubic_to(from_control_point, to_control_point, to_point); - - let p0 = kurbo::Point::new(from_point.x.to_raw(), from_point.y.to_raw()); - let p1 = kurbo::Point::new( - from_control_point.x.to_raw(), - from_control_point.y.to_raw(), - ); - let p2 = kurbo::Point::new( - to_control_point.x.to_raw(), - to_control_point.y.to_raw(), - ); - let p3 = kurbo::Point::new(to_point.x.to_raw(), to_point.y.to_raw()); - let extrema = CubicBez::new(p0, p1, p2, p3).bounding_box(); - size.x.set_max(Abs::raw(extrema.x1)); - size.y.set_max(Abs::raw(extrema.y1)); - }; - - for (vertex_window, point_window) in vertices.windows(2).zip(points.windows(2)) { - let from = vertex_window[0]; - let to = vertex_window[1]; - let from_point = point_window[0]; - let to_point = point_window[1]; - - add_cubic(from_point, to_point, from, to); - } - - if self.closed(styles) { - let from = *vertices.last().unwrap(); // We checked that we have at least one element. - let to = vertices[0]; - let from_point = *points.last().unwrap(); - let to_point = points[0]; - - add_cubic(from_point, to_point, from, to); - path.close_path(); - } - - // Prepare fill and stroke. - let fill = self.fill(styles); - let stroke = match self.stroke(styles) { - Smart::Auto if fill.is_none() => Some(FixedStroke::default()), - Smart::Auto => None, - Smart::Custom(stroke) => stroke.map(Stroke::unwrap_or_default), - }; - - let mut frame = Frame::soft(size); - let shape = Shape { geometry: Geometry::Path(path), stroke, fill }; - frame.push(Point::zero(), FrameItem::Shape(shape, self.span())); - - Ok(Fragment::frame(frame)) - } -} - -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub enum PathVertex { - Vertex(Axes<Rel<Length>>), - MirroredControlPoint(Axes<Rel<Length>>, Axes<Rel<Length>>), - AllControlPoints(Axes<Rel<Length>>, Axes<Rel<Length>>, Axes<Rel<Length>>), -} - -impl PathVertex { - pub fn vertex(&self) -> Axes<Rel<Length>> { - match self { - Vertex(x) => *x, - MirroredControlPoint(x, _) => *x, - AllControlPoints(x, _, _) => *x, - } - } - - pub fn control_point_from(&self) -> Axes<Rel<Length>> { - match self { - Vertex(_) => Axes::new(Rel::zero(), Rel::zero()), - MirroredControlPoint(_, a) => a.map(|x| -x), - AllControlPoints(_, _, b) => *b, - } - } - - pub fn control_point_to(&self) -> Axes<Rel<Length>> { - match self { - Vertex(_) => Axes::new(Rel::zero(), Rel::zero()), - MirroredControlPoint(_, a) => *a, - AllControlPoints(_, a, _) => *a, - } - } -} - -cast! { - PathVertex, - self => match self { - Vertex(x) => x.into_value(), - MirroredControlPoint(x, c) => array![x, c].into_value(), - AllControlPoints(x, c1, c2) => array![x, c1, c2].into_value(), - }, - array: Array => { - let mut iter = array.into_iter(); - match (iter.next(), iter.next(), iter.next(), iter.next()) { - (Some(a), None, None, None) => { - Vertex(a.cast()?) - }, - (Some(a), Some(b), None, None) => { - if Axes::<Rel<Length>>::castable(&a) { - MirroredControlPoint(a.cast()?, b.cast()?) - } else { - Vertex(Axes::new(a.cast()?, b.cast()?)) - } - }, - (Some(a), Some(b), Some(c), None) => { - AllControlPoints(a.cast()?, b.cast()?, c.cast()?) - }, - _ => bail!("path vertex must have 1, 2, or 3 points"), - } - }, -} diff --git a/crates/typst-library/src/visualize/polygon.rs b/crates/typst-library/src/visualize/polygon.rs deleted file mode 100644 index b1ed9eaa..00000000 --- a/crates/typst-library/src/visualize/polygon.rs +++ /dev/null @@ -1,164 +0,0 @@ -use std::f64::consts::PI; - -use crate::prelude::*; - -/// A closed polygon. -/// -/// The polygon is defined by its corner points and is closed automatically. -/// -/// # Example -/// ```example -/// #polygon( -/// fill: blue.lighten(80%), -/// stroke: blue, -/// (20%, 0pt), -/// (60%, 0pt), -/// (80%, 2cm), -/// (0%, 2cm), -/// ) -/// ``` -#[elem(scope, Layout)] -pub struct PolygonElem { - /// How to fill the polygon. - /// - /// When setting a fill, the default stroke disappears. To create a - /// rectangle with both fill and stroke, you have to configure both. - /// - /// Currently all polygons are filled according to the - /// [non-zero winding rule](https://en.wikipedia.org/wiki/Nonzero-rule). - pub fill: Option<Paint>, - - /// How to [stroke]($stroke) the polygon. This can be: - /// - /// Can be set to `{none}` to disable the stroke or to `{auto}` for a - /// stroke of `{1pt}` black if and if only if no fill is given. - #[resolve] - #[fold] - pub stroke: Smart<Option<Stroke>>, - - /// The vertices of the polygon. Each point is specified as an array of two - /// [relative lengths]($relative). - #[variadic] - pub vertices: Vec<Axes<Rel<Length>>>, -} - -#[scope] -impl PolygonElem { - /// A regular polygon, defined by its size and number of vertices. - /// - /// ```example - /// #polygon.regular( - /// fill: blue.lighten(80%), - /// stroke: blue, - /// size: 30pt, - /// vertices: 3, - /// ) - /// ``` - #[func(title = "Regular Polygon")] - pub fn regular( - /// How to fill the polygon. See the general - /// [polygon's documentation]($polygon.fill) for more details. - #[named] - fill: Option<Option<Paint>>, - - /// How to stroke the polygon. See the general - /// [polygon's documentation]($polygon.stroke) for more details. - #[named] - stroke: Option<Smart<Option<Stroke>>>, - - /// The diameter of the [circumcircle](https://en.wikipedia.org/wiki/Circumcircle) - /// of the regular polygon. - #[named] - #[default(Em::one().into())] - size: Length, - - /// The number of vertices in the polygon. - #[named] - #[default(3)] - vertices: u64, - ) -> Content { - let radius = size / 2.0; - let angle = |i: f64| { - 2.0 * PI * i / (vertices as f64) + PI * (1.0 / 2.0 - 1.0 / vertices as f64) - }; - let (horizontal_offset, vertical_offset) = (0..=vertices) - .map(|v| { - ( - (radius * angle(v as f64).cos()) + radius, - (radius * angle(v as f64).sin()) + radius, - ) - }) - .fold((radius, radius), |(min_x, min_y), (v_x, v_y)| { - ( - if min_x < v_x { min_x } else { v_x }, - if min_y < v_y { min_y } else { v_y }, - ) - }); - let vertices = (0..=vertices) - .map(|v| { - let x = (radius * angle(v as f64).cos()) + radius - horizontal_offset; - let y = (radius * angle(v as f64).sin()) + radius - vertical_offset; - Axes::new(x, y).map(Rel::from) - }) - .collect(); - - let mut elem = PolygonElem::new(vertices); - if let Some(fill) = fill { - elem.push_fill(fill); - } - if let Some(stroke) = stroke { - elem.push_stroke(stroke); - } - elem.pack() - } -} - -impl Layout for PolygonElem { - #[tracing::instrument(name = "PolygonElem::layout", skip_all)] - fn layout( - &self, - _: &mut Vt, - styles: StyleChain, - regions: Regions, - ) -> SourceResult<Fragment> { - let points: Vec<Point> = self - .vertices() - .iter() - .map(|c| { - c.resolve(styles).zip_map(regions.base(), Rel::relative_to).to_point() - }) - .collect(); - - let size = points.iter().fold(Point::zero(), |max, c| c.max(max)).to_size(); - if !size.is_finite() { - bail!(error!(self.span(), "cannot create polygon with infinite size")); - } - let mut frame = Frame::hard(size); - - // Only create a path if there are more than zero points. - if points.is_empty() { - return Ok(Fragment::frame(frame)); - } - - // Prepare fill and stroke. - let fill = self.fill(styles); - let stroke = match self.stroke(styles) { - Smart::Auto if fill.is_none() => Some(FixedStroke::default()), - Smart::Auto => None, - Smart::Custom(stroke) => stroke.map(Stroke::unwrap_or_default), - }; - - // Construct a closed path given all points. - let mut path = Path::new(); - path.move_to(points[0]); - for &point in &points[1..] { - path.line_to(point); - } - path.close_path(); - - let shape = Shape { geometry: Geometry::Path(path), stroke, fill }; - frame.push(Point::zero(), FrameItem::Shape(shape, self.span())); - - Ok(Fragment::frame(frame)) - } -} diff --git a/crates/typst-library/src/visualize/shape.rs b/crates/typst-library/src/visualize/shape.rs deleted file mode 100644 index 779b07ff..00000000 --- a/crates/typst-library/src/visualize/shape.rs +++ /dev/null @@ -1,547 +0,0 @@ -use std::f64::consts::SQRT_2; - -use crate::prelude::*; - -/// A rectangle with optional content. -/// -/// # Example -/// ```example -/// // Without content. -/// #rect(width: 35%, height: 30pt) -/// -/// // With content. -/// #rect[ -/// Automatically sized \ -/// to fit the content. -/// ] -/// ``` -#[elem(title = "Rectangle", Layout)] -pub struct RectElem { - /// The rectangle's width, relative to its parent container. - pub width: Smart<Rel<Length>>, - - /// The rectangle's height, relative to its parent container. - pub height: Smart<Rel<Length>>, - - /// How to fill the rectangle. - /// - /// When setting a fill, the default stroke disappears. To create a - /// rectangle with both fill and stroke, you have to configure both. - /// - /// ```example - /// #rect(fill: blue) - /// ``` - pub fill: Option<Paint>, - - /// How to stroke the rectangle. This can be: - /// - /// - `{none}` to disable stroking - /// - `{auto}` for a stroke of `{1pt + black}` if and if only if no fill is - /// given. - /// - Any kind of [stroke]($stroke) - /// - A dictionary describing the stroke for each side inidvidually. The - /// dictionary can contain the following keys in order of precedence: - /// - `top`: The top stroke. - /// - `right`: The right stroke. - /// - `bottom`: The bottom stroke. - /// - `left`: The left stroke. - /// - `x`: The horizontal stroke. - /// - `y`: The vertical stroke. - /// - `rest`: The stroke on all sides except those for which the - /// dictionary explicitly sets a size. - /// - /// ```example - /// #stack( - /// dir: ltr, - /// spacing: 1fr, - /// rect(stroke: red), - /// rect(stroke: 2pt), - /// rect(stroke: 2pt + red), - /// ) - /// ``` - #[resolve] - #[fold] - pub stroke: Smart<Sides<Option<Option<Stroke>>>>, - - /// How much to round the rectangle's corners, relative to the minimum of - /// the width and height divided by two. This can be: - /// - /// - A relative length for a uniform corner radius. - /// - A dictionary: With a dictionary, the stroke for each side can be set - /// individually. The dictionary can contain the following keys in order - /// of precedence: - /// - `top-left`: The top-left corner radius. - /// - `top-right`: The top-right corner radius. - /// - `bottom-right`: The bottom-right corner radius. - /// - `bottom-left`: The bottom-left corner radius. - /// - `left`: The top-left and bottom-left corner radii. - /// - `top`: The top-left and top-right corner radii. - /// - `right`: The top-right and bottom-right corner radii. - /// - `bottom`: The bottom-left and bottom-right corner radii. - /// - `rest`: The radii for all corners except those for which the - /// dictionary explicitly sets a size. - /// - /// ```example - /// #set rect(stroke: 4pt) - /// #rect( - /// radius: ( - /// left: 5pt, - /// top-right: 20pt, - /// bottom-right: 10pt, - /// ), - /// stroke: ( - /// left: red, - /// top: yellow, - /// right: green, - /// bottom: blue, - /// ), - /// ) - /// ``` - #[resolve] - #[fold] - pub radius: Corners<Option<Rel<Length>>>, - - /// How much to pad the rectangle's content. - /// See the [box's documentation]($box.outset) for more details. - #[resolve] - #[fold] - #[default(Sides::splat(Abs::pt(5.0).into()))] - pub inset: Sides<Option<Rel<Length>>>, - - /// How much to expand the rectangle's size without affecting the layout. - /// See the [box's documentation]($box.outset) for more details. - #[resolve] - #[fold] - pub outset: Sides<Option<Rel<Length>>>, - - /// The content to place into the rectangle. - /// - /// When this is omitted, the rectangle takes on a default size of at most - /// `{45pt}` by `{30pt}`. - #[positional] - pub body: Option<Content>, -} - -impl Layout for RectElem { - #[tracing::instrument(name = "RectElem::layout", skip_all)] - fn layout( - &self, - vt: &mut Vt, - styles: StyleChain, - regions: Regions, - ) -> SourceResult<Fragment> { - layout( - vt, - styles, - regions, - ShapeKind::Rect, - &self.body(styles), - Axes::new(self.width(styles), self.height(styles)), - self.fill(styles), - self.stroke(styles), - self.inset(styles), - self.outset(styles), - self.radius(styles), - self.span(), - ) - } -} - -/// A square with optional content. -/// -/// # Example -/// ```example -/// // Without content. -/// #square(size: 40pt) -/// -/// // With content. -/// #square[ -/// Automatically \ -/// sized to fit. -/// ] -/// ``` -#[elem(Layout)] -pub struct SquareElem { - /// The square's side length. This is mutually exclusive with `width` and - /// `height`. - #[external] - pub size: Smart<Length>, - - /// The square's width. This is mutually exclusive with `size` and `height`. - /// - /// In contrast to `size`, this can be relative to the parent container's - /// width. - #[parse( - let size = args.named::<Smart<Length>>("size")?.map(|s| s.map(Rel::from)); - match size { - None => args.named("width")?, - size => size, - } - )] - pub width: Smart<Rel<Length>>, - - /// The square's height. This is mutually exclusive with `size` and `width`. - /// - /// In contrast to `size`, this can be relative to the parent container's - /// height. - #[parse(match size { - None => args.named("height")?, - size => size, - })] - pub height: Smart<Rel<Length>>, - - /// How to fill the square. See the [rectangle's documentation]($rect.fill) - /// for more details. - pub fill: Option<Paint>, - - /// How to stroke the square. See the - /// [rectangle's documentation]($rect.stroke) for more details. - #[resolve] - #[fold] - pub stroke: Smart<Sides<Option<Option<Stroke>>>>, - - /// How much to round the square's corners. See the - /// [rectangle's documentation]($rect.radius) for more details. - #[resolve] - #[fold] - pub radius: Corners<Option<Rel<Length>>>, - - /// How much to pad the square's content. See the - /// [box's documentation]($box.inset) for more details. - #[resolve] - #[fold] - #[default(Sides::splat(Abs::pt(5.0).into()))] - pub inset: Sides<Option<Rel<Length>>>, - - /// How much to expand the square's size without affecting the layout. See - /// the [box's documentation]($box.outset) for more details. - #[resolve] - #[fold] - pub outset: Sides<Option<Rel<Length>>>, - - /// The content to place into the square. The square expands to fit this - /// content, keeping the 1-1 aspect ratio. - /// - /// When this is omitted, the square takes on a default size of at most - /// `{30pt}`. - #[positional] - pub body: Option<Content>, -} - -impl Layout for SquareElem { - #[tracing::instrument(name = "SquareElem::layout", skip_all)] - fn layout( - &self, - vt: &mut Vt, - styles: StyleChain, - regions: Regions, - ) -> SourceResult<Fragment> { - layout( - vt, - styles, - regions, - ShapeKind::Square, - &self.body(styles), - Axes::new(self.width(styles), self.height(styles)), - self.fill(styles), - self.stroke(styles), - self.inset(styles), - self.outset(styles), - self.radius(styles), - self.span(), - ) - } -} - -/// An ellipse with optional content. -/// -/// # Example -/// ```example -/// // Without content. -/// #ellipse(width: 35%, height: 30pt) -/// -/// // With content. -/// #ellipse[ -/// #set align(center) -/// Automatically sized \ -/// to fit the content. -/// ] -/// ``` -#[elem(Layout)] -pub struct EllipseElem { - /// The ellipse's width, relative to its parent container. - pub width: Smart<Rel<Length>>, - - /// The ellipse's height, relative to its parent container. - pub height: Smart<Rel<Length>>, - - /// How to fill the ellipse. See the [rectangle's documentation]($rect.fill) - /// for more details. - pub fill: Option<Paint>, - - /// How to stroke the ellipse. See the - /// [rectangle's documentation]($rect.stroke) for more details. - #[resolve] - #[fold] - pub stroke: Smart<Option<Stroke>>, - - /// How much to pad the ellipse's content. See the - /// [box's documentation]($box.inset) for more details. - #[resolve] - #[fold] - #[default(Sides::splat(Abs::pt(5.0).into()))] - pub inset: Sides<Option<Rel<Length>>>, - - /// How much to expand the ellipse's size without affecting the layout. See - /// the [box's documentation]($box.outset) for more details. - #[resolve] - #[fold] - pub outset: Sides<Option<Rel<Length>>>, - - /// The content to place into the ellipse. - /// - /// When this is omitted, the ellipse takes on a default size of at most - /// `{45pt}` by `{30pt}`. - #[positional] - pub body: Option<Content>, -} - -impl Layout for EllipseElem { - #[tracing::instrument(name = "EllipseElem::layout", skip_all)] - fn layout( - &self, - vt: &mut Vt, - styles: StyleChain, - regions: Regions, - ) -> SourceResult<Fragment> { - layout( - vt, - styles, - regions, - ShapeKind::Ellipse, - &self.body(styles), - Axes::new(self.width(styles), self.height(styles)), - self.fill(styles), - self.stroke(styles).map(Sides::splat), - self.inset(styles), - self.outset(styles), - Corners::splat(Rel::zero()), - self.span(), - ) - } -} - -/// A circle with optional content. -/// -/// # Example -/// ```example -/// // Without content. -/// #circle(radius: 25pt) -/// -/// // With content. -/// #circle[ -/// #set align(center + horizon) -/// Automatically \ -/// sized to fit. -/// ] -/// ``` -#[elem(Layout)] -pub struct CircleElem { - /// The circle's radius. This is mutually exclusive with `width` and - /// `height`. - #[external] - pub radius: Length, - - /// The circle's width. This is mutually exclusive with `radius` and - /// `height`. - /// - /// In contrast to `radius`, this can be relative to the parent container's - /// width. - #[parse( - let size = args - .named::<Smart<Length>>("radius")? - .map(|s| s.map(|r| 2.0 * Rel::from(r))); - match size { - None => args.named("width")?, - size => size, - } - )] - pub width: Smart<Rel<Length>>, - - /// The circle's height. This is mutually exclusive with `radius` and - /// `width`. - /// - /// In contrast to `radius`, this can be relative to the parent container's - /// height. - #[parse(match size { - None => args.named("height")?, - size => size, - })] - pub height: Smart<Rel<Length>>, - - /// How to fill the circle. See the [rectangle's documentation]($rect.fill) - /// for more details. - pub fill: Option<Paint>, - - /// How to stroke the circle. See the - /// [rectangle's documentation]($rect.stroke) for more details. - #[resolve] - #[fold] - #[default(Smart::Auto)] - pub stroke: Smart<Option<Stroke>>, - - /// How much to pad the circle's content. See the - /// [box's documentation]($box.inset) for more details. - #[resolve] - #[fold] - #[default(Sides::splat(Abs::pt(5.0).into()))] - pub inset: Sides<Option<Rel<Length>>>, - - /// How much to expand the circle's size without affecting the layout. See - /// the [box's documentation]($box.outset) for more details. - #[resolve] - #[fold] - pub outset: Sides<Option<Rel<Length>>>, - - /// The content to place into the circle. The circle expands to fit this - /// content, keeping the 1-1 aspect ratio. - #[positional] - pub body: Option<Content>, -} - -impl Layout for CircleElem { - #[tracing::instrument(name = "CircleElem::layout", skip_all)] - fn layout( - &self, - vt: &mut Vt, - styles: StyleChain, - regions: Regions, - ) -> SourceResult<Fragment> { - layout( - vt, - styles, - regions, - ShapeKind::Circle, - &self.body(styles), - Axes::new(self.width(styles), self.height(styles)), - self.fill(styles), - self.stroke(styles).map(Sides::splat), - self.inset(styles), - self.outset(styles), - Corners::splat(Rel::zero()), - self.span(), - ) - } -} - -/// Layout a shape. -#[tracing::instrument(name = "shape::layout", skip_all)] -#[allow(clippy::too_many_arguments)] -fn layout( - vt: &mut Vt, - styles: StyleChain, - regions: Regions, - kind: ShapeKind, - body: &Option<Content>, - sizing: Axes<Smart<Rel<Length>>>, - fill: Option<Paint>, - stroke: Smart<Sides<Option<Stroke<Abs>>>>, - mut inset: Sides<Rel<Abs>>, - outset: Sides<Rel<Abs>>, - radius: Corners<Rel<Abs>>, - span: Span, -) -> SourceResult<Fragment> { - let resolved = sizing - .zip_map(regions.base(), |s, r| s.map(|v| v.resolve(styles).relative_to(r))); - - let mut frame; - if let Some(child) = body { - let region = resolved.unwrap_or(regions.base()); - if kind.is_round() { - inset = inset.map(|side| side + Ratio::new(0.5 - SQRT_2 / 4.0)); - } - - // Pad the child. - let child = child.clone().padded(inset.map(|side| side.map(Length::from))); - let expand = sizing.as_ref().map(Smart::is_custom); - let pod = Regions::one(region, expand); - frame = child.layout(vt, styles, pod)?.into_frame(); - - // Enforce correct size. - *frame.size_mut() = expand.select(region, frame.size()); - - // Relayout with full expansion into square region to make sure - // the result is really a square or circle. - if kind.is_quadratic() { - frame.set_size(Size::splat(frame.size().max_by_side())); - let length = frame.size().max_by_side().min(region.min_by_side()); - let pod = Regions::one(Size::splat(length), Axes::splat(true)); - frame = child.layout(vt, styles, pod)?.into_frame(); - } - - // Enforce correct size again. - *frame.size_mut() = expand.select(region, frame.size()); - if kind.is_quadratic() { - frame.set_size(Size::splat(frame.size().max_by_side())); - } - } else { - // The default size that a shape takes on if it has no child and - // enough space. - let default = Size::new(Abs::pt(45.0), Abs::pt(30.0)); - let mut size = resolved.unwrap_or(default.min(regions.base())); - if kind.is_quadratic() { - size = Size::splat(size.min_by_side()); - } - frame = Frame::soft(size); - } - - // Prepare stroke. - let stroke = match stroke { - Smart::Auto if fill.is_none() => Sides::splat(Some(FixedStroke::default())), - Smart::Auto => Sides::splat(None), - Smart::Custom(strokes) => strokes.map(|s| s.map(Stroke::unwrap_or_default)), - }; - - // Add fill and/or stroke. - if fill.is_some() || stroke.iter().any(Option::is_some) { - if kind.is_round() { - let outset = outset.relative_to(frame.size()); - let size = frame.size() + outset.sum_by_axis(); - let pos = Point::new(-outset.left, -outset.top); - let shape = ellipse(size, fill, stroke.left); - frame.prepend(pos, FrameItem::Shape(shape, span)); - } else { - frame.fill_and_stroke(fill, stroke, outset, radius, span); - } - } - - // Apply metadata. - frame.meta(styles, false); - - Ok(Fragment::frame(frame)) -} - -/// A category of shape. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub enum ShapeKind { - /// A rectangle with equal side lengths. - Square, - /// A quadrilateral with four right angles. - Rect, - /// An ellipse with coinciding foci. - Circle, - /// A curve around two focal points. - Ellipse, -} - -impl ShapeKind { - /// Whether this shape kind is curvy. - fn is_round(self) -> bool { - matches!(self, Self::Circle | Self::Ellipse) - } - - /// Whether this shape kind has equal side length. - fn is_quadratic(self) -> bool { - matches!(self, Self::Square | Self::Circle) - } -} |
